add websocket push

This commit is contained in:
Donghuang 2022-06-18 16:05:31 +08:00
parent 5eb73ca7a1
commit b3a3cf5ec8
38 changed files with 532 additions and 171 deletions

View File

@ -29,6 +29,10 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>me.chyxion.tigon</groupId>
<artifactId>tigon-web-controller</artifactId>
</dependency>
<!-- Log --> <!-- Log -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@ -2,7 +2,7 @@ package com.pudonghot.ambition.crm.auth;
import lombok.val; import lombok.val;
import java.util.*; import java.util.*;
import me.chyxion.tigon.mybatis.Search;
import me.chyxion.tigon.model.ViewModel; import me.chyxion.tigon.model.ViewModel;
import me.chyxion.tigon.shiro.AuthRealm; import me.chyxion.tigon.shiro.AuthRealm;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
@ -62,13 +62,13 @@ public class AuthRealmSupport implements AuthRealm<Principal, String> {
*/ */
@Override @Override
public Collection<String> roles(final Principal principal) { public Collection<String> roles(final Principal principal) {
val user = userService.find(new Search(User.EMPLOYEE_ID, principal)); val user = userService.find(principal.getUserId());
val roles = new ArrayList<String>(4); val roles = new ArrayList<String>(4);
if (user.isAdmin()) { if (user.isAdmin()) {
roles.add(User.ROLE_ADMIN); roles.add(User.ROLE_ADMIN);
} }
val account = user.getAccount(); val account = user.getAccount();
if (ArrayUtils.contains(new String[] {User.ROLE_LELI, User.ROLE_CHYXION}, account)) { if (ArrayUtils.contains(new String[] {User.ROLE_LELI, User.ROLE_DONGHUANG}, account)) {
roles.add(account); roles.add(account);
} }
return roles; return roles;

View File

@ -1,5 +1,6 @@
package com.pudonghot.ambition.crm.controller; package com.pudonghot.ambition.crm.common.controller;
import com.pudonghot.ambition.crm.controller.AbstractBaseController;
import me.chyxion.tigon.model.ViewModel; import me.chyxion.tigon.model.ViewModel;
import com.pudonghot.ambition.crm.model.User; import com.pudonghot.ambition.crm.model.User;
import me.chyxion.tigon.shiro.service.AuthService; import me.chyxion.tigon.shiro.service.AuthService;

View File

@ -1,4 +1,4 @@
package com.pudonghot.ambition.crm.controller; package com.pudonghot.ambition.crm.common.controller;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;

View File

@ -1,6 +1,7 @@
package com.pudonghot.ambition.crm.controller; package com.pudonghot.ambition.crm.common.controller;
import javax.validation.Valid; import javax.validation.Valid;
import me.chyxion.tigon.form.FC2; import me.chyxion.tigon.form.FC2;
import me.chyxion.tigon.model.M0; import me.chyxion.tigon.model.M0;
import me.chyxion.tigon.model.ViewModel; import me.chyxion.tigon.model.ViewModel;

View File

@ -1,4 +1,4 @@
package com.pudonghot.ambition.crm.controller; package com.pudonghot.ambition.crm.common.controller;
import me.chyxion.tigon.form.FC2; import me.chyxion.tigon.form.FC2;
import me.chyxion.tigon.model.M0; import me.chyxion.tigon.model.M0;

View File

@ -1,7 +1,7 @@
package com.pudonghot.ambition.crm.controller; package com.pudonghot.ambition.crm.common.controller;
import com.pudonghot.ambition.crm.common.controller.BaseController;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.chyxion.tigon.model.M0;
import me.chyxion.tigon.mybatis.Search; import me.chyxion.tigon.mybatis.Search;
import me.chyxion.tigon.model.ViewModel; import me.chyxion.tigon.model.ViewModel;
import me.chyxion.tigon.model.ListResult; import me.chyxion.tigon.model.ListResult;
@ -19,8 +19,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
* May 10, 2016 4:50:58 PM * May 10, 2016 4:50:58 PM
*/ */
@Slf4j @Slf4j
public class BaseQueryController<Model extends M0<String>> public class BaseQueryController<Model> extends BaseController {
extends BaseController {
@Autowired @Autowired
protected BaseQueryService<String, Model> queryService; protected BaseQueryService<String, Model> queryService;

View File

@ -0,0 +1,21 @@
package com.pudonghot.ambition.crm.common.request;
import lombok.Getter;
import lombok.Setter;
import me.chyxion.tigon.web.controller2.request.BaseListCtrlrReqApi;
/**
* @author Donghuang
* @date Jun 18, 2022 13:27:22
*/
@Getter
@Setter
public class BaseListCtrlrReq implements BaseListCtrlrReqApi {
private static final long serialVersionUID = 1L;
private Integer start;
private Integer limit;
private String search;
private String filters;
private String orders;
}

View File

@ -177,7 +177,7 @@ public abstract class AbstractBaseController {
} }
if (StringUtils.isNotBlank(strSearch)) { if (StringUtils.isNotBlank(strSearch)) {
val orSearch = new Search(); val orSearch = new Search();
for (final String col : searchCols()) { for (val col : searchCols()) {
orSearch.or(new Search().like(col, decodeLike(strSearch))); orSearch.or(new Search().like(col, decodeLike(strSearch)));
} }
search.and(orSearch); search.and(orSearch);

View File

@ -1,5 +1,6 @@
package com.pudonghot.ambition.crm.controller; package com.pudonghot.ambition.crm.controller;
import com.pudonghot.ambition.crm.common.controller.BaseQueryController;
import lombok.val; import lombok.val;
import java.util.List; import java.util.List;
import java.util.Arrays; import java.util.Arrays;

View File

@ -1,20 +1,19 @@
package com.pudonghot.ambition.crm.controller; package com.pudonghot.ambition.crm.controller;
import lombok.val;
import org.apache.shiro.SecurityUtils;
import me.chyxion.tigon.mybatis.Search;
import org.springframework.util.Assert;
import me.chyxion.tigon.model.ViewModel;
import com.pudonghot.ambition.crm.model.User;
import javax.validation.constraints.NotBlank;
import me.chyxion.tigon.kit.bean.BeanService;
import org.springframework.stereotype.Controller;
import me.chyxion.tigon.shiro.service.AuthService;
import com.pudonghot.ambition.crm.service.UserService;
import com.pudonghot.ambition.crm.auth.SessionAbility; import com.pudonghot.ambition.crm.auth.SessionAbility;
import com.pudonghot.ambition.crm.auth.model.AuthToken; import com.pudonghot.ambition.crm.auth.model.AuthToken;
import com.pudonghot.ambition.crm.auth.model.Principal; import com.pudonghot.ambition.crm.auth.model.Principal;
import lombok.val;
import me.chyxion.tigon.kit.bean.BeanService;
import me.chyxion.tigon.mybatis.Search;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.util.Assert;
import me.chyxion.tigon.model.ViewModel;
import com.pudonghot.ambition.crm.model.User;
import org.springframework.stereotype.Controller;
import me.chyxion.tigon.shiro.service.AuthService;
import javax.validation.constraints.NotBlank;
import com.pudonghot.ambition.crm.service.UserService;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@ -45,8 +44,10 @@ public class AuthController implements SessionAbility {
@RequestParam("password") @RequestParam("password")
String password) { String password) {
val user = userService.find(new Search(User.ACCOUNT, loginId)); val user = findUser(loginId);
Assert.state(user != null, "Unknown account"); Assert.state(user != null, () -> "Incorrect account or password");
Assert.state(user.getEnabled(), "Account is disabled, please contact admin");
val principal = new Principal(); val principal = new Principal();
principal.setAccount(loginId); principal.setAccount(loginId);
principal.setUserId(user.getId()); principal.setUserId(user.getId());
@ -69,4 +70,12 @@ public class AuthController implements SessionAbility {
public ViewModel<User> info() { public ViewModel<User> info() {
return userService.findViewModel(getUserId()); return userService.findViewModel(getUserId());
} }
User findUser(final String loginId) {
val user = userService.find(new Search(User.ACCOUNT, loginId));
if (user != null) {
return user;
}
return userService.find(new Search(User.EMPLOYEE_ID, loginId));
}
} }

View File

@ -1,5 +1,7 @@
package com.pudonghot.ambition.crm.controller; package com.pudonghot.ambition.crm.controller;
import com.pudonghot.ambition.crm.common.controller.BaseQueryController;
import com.pudonghot.ambition.crm.service.ExportTaskService;
import lombok.val; import lombok.val;
import java.util.*; import java.util.*;
import javax.validation.Valid; import javax.validation.Valid;
@ -14,6 +16,7 @@ import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import com.pudonghot.ambition.crm.model.User; import com.pudonghot.ambition.crm.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import com.pudonghot.ambition.crm.model.Customer; import com.pudonghot.ambition.crm.model.Customer;
@ -107,6 +110,9 @@ public class CustomerController
Pair.of(CRITERION_COLS.get("issue"), Boolean.class)); Pair.of(CRITERION_COLS.get("issue"), Boolean.class));
} }
@Autowired
private ExportTaskService exportTaskService;
@RequestMapping("/list") @RequestMapping("/list")
public ListResult<ViewModel<Customer>> list( public ListResult<ViewModel<Customer>> list(
@Min(0) @Min(0)
@ -164,6 +170,12 @@ public class CustomerController
return FileDownloadUtils.toResponseEntity(((CustomerService) queryService).exportCSV(getUserId()), null); return FileDownloadUtils.toResponseEntity(((CustomerService) queryService).exportCSV(getUserId()), null);
} }
@RequiresRoles(User.ROLE_ADMIN)
@RequestMapping("/async-export")
public void asyncExportCSV() {
exportTaskService.doExport(getUserId());
}
@RequiresRoles(User.ROLE_ADMIN) @RequiresRoles(User.ROLE_ADMIN)
@PostMapping("/delete") @PostMapping("/delete")
public void delete(@NotBlank @RequestParam("id") String id) { public void delete(@NotBlank @RequestParam("id") String id) {

View File

@ -4,6 +4,8 @@ import java.util.List;
import java.util.Arrays; import java.util.Arrays;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.Max; import javax.validation.constraints.Max;
import com.pudonghot.ambition.crm.common.controller.BaseQueryController;
import me.chyxion.tigon.model.ViewModel; import me.chyxion.tigon.model.ViewModel;
import javax.validation.constraints.Min; import javax.validation.constraints.Min;
import me.chyxion.tigon.model.ListResult; import me.chyxion.tigon.model.ListResult;

View File

@ -3,6 +3,8 @@ package com.pudonghot.ambition.crm.controller;
import java.util.List; import java.util.List;
import java.util.Arrays; import java.util.Arrays;
import javax.validation.Valid; import javax.validation.Valid;
import com.pudonghot.ambition.crm.common.controller.BaseQueryController;
import me.chyxion.tigon.mybatis.Search; import me.chyxion.tigon.mybatis.Search;
import javax.validation.constraints.Max; import javax.validation.constraints.Max;
import javax.validation.constraints.Min; import javax.validation.constraints.Min;

View File

@ -1,6 +1,8 @@
package com.pudonghot.ambition.crm.controller; package com.pudonghot.ambition.crm.controller;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import com.pudonghot.ambition.crm.common.controller.BaseController;
import com.pudonghot.ambition.crm.model.User; import com.pudonghot.ambition.crm.model.User;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;

View File

@ -0,0 +1,69 @@
package com.pudonghot.ambition.crm.controller;
import lombok.val;
import javax.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import me.chyxion.tigon.mybatis.Search;
import com.pudonghot.ambition.crm.model.User;
import me.chyxion.tigon.web.controller2.ArgQuery;
import org.springframework.stereotype.Controller;
import com.pudonghot.ambition.crm.model.ExportTask;
import com.pudonghot.ambition.crm.auth.SessionAbility;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import me.chyxion.tigon.web.controller2.BaseQueryController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.beans.factory.annotation.Autowired;
import com.pudonghot.ambition.crm.ws.service.WebSocketService;
import me.chyxion.tigon.web.controller2.response.ListCtrlrResp;
import com.pudonghot.ambition.crm.common.request.BaseListCtrlrReq;
import com.pudonghot.ambition.crm.ws.model.ExportTaskUpdatedWsResp;
/**
* @author Donghuang
* @date Jun 18, 2022 09:47:30
*/
@Slf4j
@Controller
@RequestMapping("/export-task")
public class ExportTaskController
extends BaseQueryController<Long,
BaseListCtrlrReq,
ExportTask,
ExportTask>
implements SessionAbility {
/**
* {@inheritDoc}
*/
@Override
@RequiresRoles(User.ROLE_ADMIN)
public ListCtrlrResp<ExportTask> list(@Valid final BaseListCtrlrReq req) {
return list(req, new Search());
}
/**
* {@inheritDoc}
*/
@Override
protected void before(final ArgQuery<?> arg) {
super.before(arg);
val search = arg.getSearch();
search.eq(ExportTask.CREATED_BY, getUserId());
if (arg.getType() == ArgQuery.Type.LIST) {
search.desc(ExportTask.DATE_CREATED);
}
}
@Autowired
private WebSocketService webSocketService;
@PostMapping("/test-push")
@RequiresRoles(User.ROLE_ADMIN)
public void testPush(@RequestBody ExportTaskUpdatedWsResp resp) {
resp.setEmployeeKey(getUserId());
webSocketService.publish(resp);
}
}

View File

@ -1,6 +1,8 @@
package com.pudonghot.ambition.crm.controller; package com.pudonghot.ambition.crm.controller;
import javax.validation.Valid; import javax.validation.Valid;
import com.pudonghot.ambition.crm.common.controller.BaseQueryController;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import javax.validation.constraints.Max; import javax.validation.constraints.Max;
import javax.validation.constraints.Min; import javax.validation.constraints.Min;

View File

@ -2,6 +2,8 @@ package com.pudonghot.ambition.crm.controller;
import java.util.List; import java.util.List;
import java.util.Arrays; import java.util.Arrays;
import com.pudonghot.ambition.crm.common.controller.BaseQueryController;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.chyxion.tigon.model.ViewModel; import me.chyxion.tigon.model.ViewModel;
import javax.validation.constraints.Max; import javax.validation.constraints.Max;
@ -30,7 +32,7 @@ public class ImportRecordController
"u.name", "u.name",
"u.en_name", "u.en_name",
"u.account", "u.account",
"u.employee_id" ); "u.employee_id");
@RequestMapping("/list") @RequestMapping("/list")
@RequiresRoles(User.ROLE_ADMIN) @RequiresRoles(User.ROLE_ADMIN)

View File

@ -4,6 +4,7 @@ import java.util.List;
import java.util.Arrays; import java.util.Arrays;
import javax.validation.Valid; import javax.validation.Valid;
import com.pudonghot.ambition.crm.common.controller.BaseQueryController;
import lombok.val; import lombok.val;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import me.chyxion.tigon.mybatis.Search; import me.chyxion.tigon.mybatis.Search;
@ -71,14 +72,14 @@ public class LocalProductController
return result; return result;
} }
@RequiresRoles(value = {User.ROLE_LELI, User.ROLE_CHYXION, User.ROLE_ADMIN}, logical = Logical.OR) @RequiresRoles(value = {User.ROLE_LELI, User.ROLE_DONGHUANG, User.ROLE_ADMIN}, logical = Logical.OR)
@RequestMapping(value = "/create", method = RequestMethod.POST) @RequestMapping(value = "/create", method = RequestMethod.POST)
public void create( public void create(
@Valid LocalProductFormForCreate form) { @Valid LocalProductFormForCreate form) {
((LocalProductService) queryService).create(form); ((LocalProductService) queryService).create(form);
} }
@RequiresRoles(value = {User.ROLE_LELI, User.ROLE_CHYXION, User.ROLE_ADMIN}, logical = Logical.OR) @RequiresRoles(value = {User.ROLE_LELI, User.ROLE_DONGHUANG, User.ROLE_ADMIN}, logical = Logical.OR)
@RequestMapping(value = "/update", method = RequestMethod.POST) @RequestMapping(value = "/update", method = RequestMethod.POST)
public void update( public void update(
@Valid LocalProductFormForUpdate form) { @Valid LocalProductFormForUpdate form) {
@ -98,7 +99,7 @@ public class LocalProductController
return viewModel; return viewModel;
} }
@RequiresRoles(value = {User.ROLE_LELI, User.ROLE_CHYXION, User.ROLE_ADMIN}, logical = Logical.OR) @RequiresRoles(value = {User.ROLE_LELI, User.ROLE_DONGHUANG, User.ROLE_ADMIN}, logical = Logical.OR)
@RequestMapping(value = "/add-image", method = RequestMethod.POST) @RequestMapping(value = "/add-image", method = RequestMethod.POST)
public ViewModel<AttachedImage> addImage( public ViewModel<AttachedImage> addImage(
@Valid LocalProductImageFormForCreate form) { @Valid LocalProductImageFormForCreate form) {
@ -106,19 +107,19 @@ public class LocalProductController
return new ViewModel<>(((LocalProductService) queryService).addImage(form)); return new ViewModel<>(((LocalProductService) queryService).addImage(form));
} }
@RequiresRoles(value = {User.ROLE_LELI, User.ROLE_CHYXION, User.ROLE_ADMIN}, logical = Logical.OR) @RequiresRoles(value = {User.ROLE_LELI, User.ROLE_DONGHUANG, User.ROLE_ADMIN}, logical = Logical.OR)
@RequestMapping(value = "/remove-image", method = RequestMethod.POST) @RequestMapping(value = "/remove-image", method = RequestMethod.POST)
public void removeImage(@NotBlank @RequestParam("id") String id) { public void removeImage(@NotBlank @RequestParam("id") String id) {
((LocalProductService) queryService).removeImage(id); ((LocalProductService) queryService).removeImage(id);
} }
@RequiresRoles(value = {User.ROLE_LELI, User.ROLE_CHYXION, User.ROLE_ADMIN}, logical = Logical.OR) @RequiresRoles(value = {User.ROLE_LELI, User.ROLE_DONGHUANG, User.ROLE_ADMIN}, logical = Logical.OR)
@RequestMapping(value = "/update-image", method = RequestMethod.POST) @RequestMapping(value = "/update-image", method = RequestMethod.POST)
public void updateImage(@Valid LocalProductImageFormForUpdate form) { public void updateImage(@Valid LocalProductImageFormForUpdate form) {
((LocalProductService) queryService).updateImage(form); ((LocalProductService) queryService).updateImage(form);
} }
@RequiresRoles(value = {User.ROLE_LELI, User.ROLE_CHYXION, User.ROLE_ADMIN}, logical = Logical.OR) @RequiresRoles(value = {User.ROLE_LELI, User.ROLE_DONGHUANG, User.ROLE_ADMIN}, logical = Logical.OR)
@RequestMapping(value = "/add-attachment", method = RequestMethod.POST) @RequestMapping(value = "/add-attachment", method = RequestMethod.POST)
public ViewModel<AttachedFile> addAttachment( public ViewModel<AttachedFile> addAttachment(
@Valid LocalProductAttachedFileFormForCreate form) { @Valid LocalProductAttachedFileFormForCreate form) {
@ -131,13 +132,13 @@ public class LocalProductController
((LocalProductService) queryService).removeAttachment(id); ((LocalProductService) queryService).removeAttachment(id);
} }
@RequiresRoles(value = {User.ROLE_LELI, User.ROLE_CHYXION, User.ROLE_ADMIN}, logical = Logical.OR) @RequiresRoles(value = {User.ROLE_LELI, User.ROLE_DONGHUANG, User.ROLE_ADMIN}, logical = Logical.OR)
@RequestMapping(value = "/update-attachment", method = RequestMethod.POST) @RequestMapping(value = "/update-attachment", method = RequestMethod.POST)
public void updateImage(@Valid LocalProductAttachedFileFormForUpdate form) { public void updateImage(@Valid LocalProductAttachedFileFormForUpdate form) {
((LocalProductService) queryService).updateAttachment(form); ((LocalProductService) queryService).updateAttachment(form);
} }
@RequiresRoles(value = {User.ROLE_LELI, User.ROLE_CHYXION, User.ROLE_ADMIN}, logical = Logical.OR) @RequiresRoles(value = {User.ROLE_LELI, User.ROLE_DONGHUANG, User.ROLE_ADMIN}, logical = Logical.OR)
@RequestMapping(value = "/delete", method = RequestMethod.POST) @RequestMapping(value = "/delete", method = RequestMethod.POST)
public void delete(@NotBlank @RequestParam("id") String id) { public void delete(@NotBlank @RequestParam("id") String id) {
((LocalProductService) queryService).delete(id); ((LocalProductService) queryService).delete(id);

View File

@ -5,6 +5,8 @@ import java.util.List;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import javax.validation.Valid; import javax.validation.Valid;
import com.pudonghot.ambition.crm.common.controller.BaseQueryController;
import me.chyxion.tigon.mybatis.Search; import me.chyxion.tigon.mybatis.Search;
import javax.validation.constraints.Max; import javax.validation.constraints.Max;
import javax.validation.constraints.Min; import javax.validation.constraints.Min;

View File

@ -1,6 +1,8 @@
package com.pudonghot.ambition.crm.controller; package com.pudonghot.ambition.crm.controller;
import java.util.*; import java.util.*;
import com.pudonghot.ambition.crm.common.controller.BaseQueryController;
import lombok.val; import lombok.val;
import javax.validation.Valid; import javax.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;

View File

@ -1,6 +1,9 @@
package com.pudonghot.ambition.crm.service; package com.pudonghot.ambition.crm.service;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import com.pudonghot.ambition.crm.model.ExportTask;
import me.chyxion.tigon.service2.BaseQueryService;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
/** /**
@ -8,13 +11,20 @@ import org.springframework.validation.annotation.Validated;
* @date May 29, 2022 21:24:42 * @date May 29, 2022 21:24:42
*/ */
@Validated @Validated
public interface ExportTaskService { public interface ExportTaskService extends BaseQueryService<Long, ExportTask> {
/** /**
* do export * do export
* *
* @param operator * @param employeeKey employee key
*/ */
void doExport(@NotBlank String operator); void doExport(@NotBlank String employeeKey);
/**
* notify task updated
*
* @param employeeKey employee key
*/
void notifyTaskUpdated(@NotBlank String employeeKey);
} }

View File

@ -1,32 +1,102 @@
package com.pudonghot.ambition.crm.service.support; package com.pudonghot.ambition.crm.service.support;
import lombok.val; import lombok.val;
import java.io.File;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
import me.chyxion.tigon.mybatis.Search;
import org.apache.commons.io.FileUtils;
import org.springframework.util.Assert;
import java.util.concurrent.ConcurrentMap;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
import com.pudonghot.ambition.crm.model.ExportTask;
import org.springframework.core.task.TaskExecutor; import org.springframework.core.task.TaskExecutor;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Value;
import com.pudonghot.ambition.crm.mapper.ExportTaskMapper;
import com.pudonghot.ambition.crm.service.CustomerService; import com.pudonghot.ambition.crm.service.CustomerService;
import com.pudonghot.ambition.crm.service.ExportTaskService; import com.pudonghot.ambition.crm.service.ExportTaskService;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import com.pudonghot.ambition.crm.ws.service.WebSocketService;
import me.chyxion.tigon.service.support2.BaseQueryServiceSupport;
import com.pudonghot.ambition.crm.enumeration.EnumExportTaskStatus;
import com.pudonghot.ambition.crm.ws.model.ExportTaskUpdatedWsResp;
/** /**
* @author Donghuang * @author Donghuang
* @date May 29, 2022 21:29:06 * @date May 29, 2022 21:29:06
*/ */
@Slf4j
@Service @Service
public class ExportTaskServiceSupport implements ExportTaskService { public class ExportTaskServiceSupport
extends BaseQueryServiceSupport<Long, ExportTask, ExportTaskMapper, ExportTask>
implements ExportTaskService {
private final ConcurrentMap<String, Long> LOCK_MAP = new ConcurrentHashMap<>();
@Autowired @Autowired
@Qualifier("exportTaskExecutor") @Qualifier("exportTaskExecutor")
private TaskExecutor threadPoolTaskExecutor; private TaskExecutor threadPoolTaskExecutor;
@Autowired @Autowired
private CustomerService customerService; private CustomerService customerService;
@Value("${export.location:/data/export}")
private String exportLocation;
@Autowired
private WebSocketService webSocketService;
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public void doExport(final String operator) { public void doExport(final String employeeKey) {
val now = Long.valueOf(System.currentTimeMillis());
Assert.state(now.equals(LOCK_MAP.putIfAbsent(employeeKey, now)),
"There is a running export task, please try again later!");
threadPoolTaskExecutor.execute(() ->{ threadPoolTaskExecutor.execute(() ->{
val exportFile = customerService.exportCSV(operator); try {
val exportTask = new ExportTask();
exportTask.setCreatedBy(employeeKey);
exportTask.setDateCreated(new Date());
exportTask.setStatus(EnumExportTaskStatus.RUNNING);
mapper.insert(exportTask);
try {
val exportFile = customerService.exportCSV(employeeKey);
val distDir = new File(new File(exportLocation),
DateFormatUtils.format(now, "yyyyMMdd"));
FileUtils.moveFileToDirectory(exportFile, distDir, true);
exportTask.setStatus(EnumExportTaskStatus.DONE);
exportTask.setLocation(new File(distDir, exportFile.getName()).getPath());
exportTask.setDateUpdated(new Date());
mapper.update(exportTask);
} catch (final Throwable e) {
log.error("Export file error caused.", e);
exportTask.setStatus(EnumExportTaskStatus.FAILED);
exportTask.setNote(ExceptionUtils.getStackTrace(e));
exportTask.setDateUpdated(new Date());
mapper.update(exportTask);
}
notifyTaskUpdated(employeeKey);
}
finally {
LOCK_MAP.remove(employeeKey);
}
}); });
} }
/**
* {@inheritDoc}
*/
@Override
public void notifyTaskUpdated(final String employeeKey) {
webSocketService.publish(
new ExportTaskUpdatedWsResp(employeeKey,
mapper.count(new Search(ExportTask.CREATED_BY, employeeKey)
.isFalse(ExportTask.DOWNLOADED))));
}
} }

View File

@ -1,25 +1,16 @@
package com.pudonghot.ambition.crm.util; package com.pudonghot.ambition.crm.util;
import com.pudonghot.ambition.crm.exception.CVSFileImportingException;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.io.*; import java.io.*;
import lombok.val;
import lombok.SneakyThrows;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.Charset; import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Iterator; import org.springframework.http.MediaType;
import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.core.io.InputStreamResource;
/** /**
* @author Donghuang * @author Donghuang
@ -27,7 +18,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/ */
@Slf4j @Slf4j
public class FileDownloadUtils { public class FileDownloadUtils {
private static final AtomicBoolean lock = new AtomicBoolean(false);
/** /**
* file to response entity * file to response entity

View File

@ -0,0 +1,23 @@
package com.pudonghot.ambition.crm.ws.model;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import javax.validation.constraints.NotBlank;
/**
* @author Donghuang
* @date Jun 17, 2022 15:01:49
*/
@Getter
@Setter
@ToString
public class BaseWsResp implements Serializable {
private static final long serialVersionUID = 1L;
@NotBlank
private String employeeKey;
@NotBlank
private String type;
}

View File

@ -0,0 +1,29 @@
package com.pudonghot.ambition.crm.ws.model;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* @author Donghuang
* @date Jun 17, 2022 15:01:49
*/
@Getter
@Setter
@ToString
public class ExportTaskUpdatedWsResp extends BaseWsResp {
private static final long serialVersionUID = 1L;
private Integer countUndownloaded;
private Integer countFailed;
public ExportTaskUpdatedWsResp() {
setType("EXPORT_TASK_UPDATED");
}
public ExportTaskUpdatedWsResp(final String employeeKey, final Integer countUndownloaded) {
this();
this.countUndownloaded = countUndownloaded;
setEmployeeKey(employeeKey);
}
}

View File

@ -1,7 +1,10 @@
package com.pudonghot.ambition.crm.ws.service; package com.pudonghot.ambition.crm.ws.service;
import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import com.pudonghot.ambition.crm.ws.model.BaseWsResp;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
/** /**
@ -14,8 +17,16 @@ public interface WebSocketService {
/** /**
* publish websocket message * publish websocket message
* *
* @param employeeKey * @param resp resp
* @param payload */
void publish(@NotNull @Valid BaseWsResp resp);
/**
* publish websocket message
*
* @param employeeKey employee key
* @param payload payload
*/ */
void publish(@NotBlank String employeeKey, @NotNull Object payload); void publish(@NotBlank String employeeKey, @NotNull Object payload);
} }

View File

@ -2,6 +2,7 @@ package com.pudonghot.ambition.crm.ws.service.impl;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.pudonghot.ambition.crm.ws.model.BaseWsResp;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import com.pudonghot.ambition.crm.ws.service.WebSocketService; import com.pudonghot.ambition.crm.ws.service.WebSocketService;
import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.SimpMessagingTemplate;
@ -17,6 +18,16 @@ public class WebSocketServiceImpl implements WebSocketService {
@Autowired @Autowired
private SimpMessagingTemplate message; private SimpMessagingTemplate message;
/**
* {@inheritDoc}
*/
@Override
public void publish(final BaseWsResp resp) {
publish(resp.getEmployeeKey(), resp);
}
@Override
public void publish(final String employeeKey, public void publish(final String employeeKey,
final Object payload) { final Object payload) {
log.debug("Publish websocket message [{}] -> [{}].", employeeKey, payload); log.debug("Publish websocket message [{}] -> [{}].", employeeKey, payload);

@ -1 +1 @@
Subproject commit a69a6519311d6a5ec932f2f7e1e71bb381a04a20 Subproject commit 56a42f79cac7f8369bacf51e323242c0ae202e55

View File

@ -0,0 +1,4 @@
package com.pudonghot.ambition.crm;
public class Test {
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* @author Donghuang
* @date Jun 18, 2022 10:07:26
*/
-->
<!DOCTYPE mapper PUBLIC
"-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pudonghot.ambition.crm.mapper.ExportTaskMapper">
</mapper>

View File

@ -4,18 +4,20 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.util.Date; import java.util.Date;
import java.io.Serializable; import java.io.Serializable;
import me.chyxion.tigon.mybatis.BasicEntity;
import me.chyxion.tigon.mybatis.NotUpdate; import me.chyxion.tigon.mybatis.NotUpdate;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
/** /**
* @author Shaun Chyxion <br> * Base Database model
* chyxion@163.com <br> *
* Aug 09, 2017 22:24:40 * @author Donghuang
* @date Jun 18, 2022 09:39:26
*/ */
@Getter @Getter
@Setter @Setter
@FieldNameConstants(prefix = "") @FieldNameConstants(prefix = "")
public class BaseDbModel implements Serializable { public class BaseDbModel implements Serializable, BasicEntity {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private Long id; private Long id;
@ -25,6 +27,37 @@ public class BaseDbModel implements Serializable {
private Date dateCreated; private Date dateCreated;
private String updatedBy; private String updatedBy;
private Date dateUpdated; private Date dateUpdated;
private boolean enabled; private Boolean enabled;
private String note; private String note;
/**
* convenient method
*
* @return true if is enabled
*/
public boolean isEnabled() {
return enabled != null && enabled;
}
/**
* init date created
*/
@Override
public void beforeInsert() {
if (dateCreated == null) {
dateCreated = new Date();
}
if (enabled == null) {
enabled = Boolean.TRUE;
}
}
/**
* update date updated
*/
@Override
public void beforeUpdate() {
dateUpdated = new Date();
}
} }

View File

@ -3,8 +3,9 @@ package com.pudonghot.ambition.crm.model;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import me.chyxion.tigon.mybatis.Table; import me.chyxion.tigon.mybatis.Table;
import me.chyxion.tigon.mybatis.NotUpdate;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import me.chyxion.tigon.mybatis.UseGeneratedKeys;
import me.chyxion.tigon.mybatis.NotUpdateWhenNull;
import com.pudonghot.ambition.crm.enumeration.EnumExportTaskStatus; import com.pudonghot.ambition.crm.enumeration.EnumExportTaskStatus;
/** /**
@ -13,6 +14,7 @@ import com.pudonghot.ambition.crm.enumeration.EnumExportTaskStatus;
*/ */
@Getter @Getter
@Setter @Setter
@UseGeneratedKeys
@Table("crm_export_task") @Table("crm_export_task")
@FieldNameConstants(prefix = "") @FieldNameConstants(prefix = "")
public class ExportTask extends BaseDbModel { public class ExportTask extends BaseDbModel {
@ -20,6 +22,7 @@ public class ExportTask extends BaseDbModel {
// Properties // Properties
private EnumExportTaskStatus status; private EnumExportTaskStatus status;
@NotUpdate @NotUpdateWhenNull
private String location; private String location;
private boolean downloaded;
} }

View File

@ -6,6 +6,7 @@ import me.chyxion.tigon.model.M3;
import me.chyxion.tigon.mybatis.Table; import me.chyxion.tigon.mybatis.Table;
import me.chyxion.tigon.mybatis.Transient; import me.chyxion.tigon.mybatis.Transient;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import me.chyxion.tigon.mybatis.UseGeneratedKeys;
/** /**
* @version 0.0.1 * @version 0.0.1
@ -16,6 +17,7 @@ import lombok.experimental.FieldNameConstants;
*/ */
@Getter @Getter
@Setter @Setter
@UseGeneratedKeys
@Table("crm_file_info") @Table("crm_file_info")
@FieldNameConstants(prefix = "") @FieldNameConstants(prefix = "")
public class FileInfo extends M3<String, String> { public class FileInfo extends M3<String, String> {

View File

@ -25,9 +25,9 @@ public class User extends M3<String, String> {
// roles // roles
public static final String ROLE_ADMIN = "ADMIN"; public static final String ROLE_ADMIN = "ADMIN";
public static final String ROLE_LELI = "leli"; public static final String ROLE_LELI = "leli";
public static final String ROLE_CHYXION = "donghuang"; public static final String ROLE_DONGHUANG = "donghuang";
// Properties // Properties
private String account; private String account;
@NotUpdate @NotUpdate
private String employeeId; private String employeeId;

View File

@ -6,16 +6,6 @@ export default Service.extend({
ajax: service('ajax'), ajax: service('ajax'),
connect() { connect() {
const me = this; const me = this;
if (me.get('connected')) {
console.info('Websocket is connected, reconnect.');
me.disconnect();
}
// connect
if (!me.get('stompClient')) {
me.set('stompClient', Stomp.over(new SockJS('/stomp')));
}
const user = me.get('ajax.user'); const user = me.get('ajax.user');
if (!user) { if (!user) {
@ -23,15 +13,57 @@ export default Service.extend({
return; return;
} }
me.get('stompClient').connect({}, function() { if (me.get('connected')) {
console.info('Connect websocket.', user); console.info('Websocket is connected, reconnect.');
me.stompClient.subscribe('/topic/' + user.id, function(msg) { me.disconnect();
}
const client = new StompJs.Client({
// brokerURL: 'ws://localhost:15674/ws',
webSocketFactory: function () {
// Note that the URL is different from the WebSocket URL
return new SockJS('/stomp');
},
connectHeaders: {
user: user.id,
// passcode: 'password',
},
debug: function (str) {
console.log(str);
},
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});
client.onConnect = function(frame) {
// Do something, all subscribes must be done is this callback
// This is needed because this will be executed after a (re)connect
console.info('Websocket connected: ', frame);
me.set('subscription', client.subscribe('/topic/' + user.id, function(msg) {
console.info('On websocket message: ', msg); console.info('On websocket message: ', msg);
$.trigger('WEBSOCKET', JSON.parse(msg.body)); $.trigger('WEBSOCKET', JSON.parse(msg.body));
}); }));
me.set('connected', true); me.set('connected', true);
console.info('Websocket connected.'); };
});
client.onDisconnect = function(frame) {
console.log('On websocket disconnect: ', frame);
me.set('connected', false);
};
client.onStompError = function (frame) {
// Will be invoked in case of error encountered at Broker
// Bad login/passcode typically will cause an error
// Complaint brokers will set `message` header with a brief message. Body may contain details.
// Compliant brokers will terminate the connection after any error
console.log('Broker reported error: ', frame.headers['message']);
console.log('Additional details: ', frame.body);
me.set('connected', false);
};
client.activate();
me.set('client', client);
}, },
willDestroy() { willDestroy() {
const me = this; const me = this;
@ -40,10 +72,8 @@ export default Service.extend({
}, },
disconnect() { disconnect() {
const me = this; const me = this;
if (me.get('connected') && me.get('stompClient')) { if (me.get('connected') && me.get('client')) {
me.get('stompClient').disconnect(); me.get('client').deactivate();
me.set('connected', false);
console.info('Websocket disconnected.');
return; return;
} }

View File

@ -110,8 +110,9 @@ module.exports = function(defaults) {
importVendor(app, 'node_modules/bootstrap-wysiwyg/src/bootstrap-wysiwyg.js'); importVendor(app, 'node_modules/bootstrap-wysiwyg/src/bootstrap-wysiwyg.js');
// WebSocket // WebSocket
importVendor(app, 'node_modules/@stomp/stompjs/bundles/stomp.umd.js');
importVendor(app, 'node_modules/sockjs-client/dist/sockjs.js'); importVendor(app, 'node_modules/sockjs-client/dist/sockjs.js');
importVendor(app, 'node_modules/stomp-websocket/lib/stomp.js'); // importVendor(app, 'node_modules/stomp-websocket/lib/stomp.js');
// Bootstrap Toolkit // Bootstrap Toolkit
importVendor(app, 'node_modules/responsive-toolkit/dist/bootstrap-toolkit.js'); importVendor(app, 'node_modules/responsive-toolkit/dist/bootstrap-toolkit.js');

View File

@ -1,79 +1,79 @@
{ {
"name": "ambition-crm", "name": "ambition-crm",
"version": "0.0.2", "version": "0.0.2",
"description": "Ambition CRM", "description": "Ambition CRM",
"private": true, "private": true,
"directories": { "directories": {
"doc": "doc", "doc": "doc",
"test": "tests" "test": "tests"
}, },
"scripts": { "scripts": {
"build": "ember build", "build": "ember build",
"start": "ember server", "start": "ember server",
"test": "ember test" "test": "ember test"
}, },
"repository": "", "repository": "",
"author": "Shaun Chyxion", "author": "Shaun Chyxion",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@ember/jquery": "1.1.0", "@ember/jquery": "1.1.0",
"@ember/optional-features": "^1.3.0", "@ember/optional-features": "^1.3.0",
"@ember/test-helpers": "^2.8.1", "@ember/test-helpers": "^2.8.1",
"bootbox": "5.1.3", "bootbox": "5.1.3",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"bootstrap-colorpicker": "^2.5.2", "bootstrap-colorpicker": "^2.5.2",
"bootstrap-tagsinput": "^0.7.1", "bootstrap-tagsinput": "^0.7.1",
"bootstrap-wysiwyg": "^2.0.1", "bootstrap-wysiwyg": "^2.0.1",
"broccoli-asset-rev": "^3.0.0", "broccoli-asset-rev": "^3.0.0",
"ember-ajax": "^5.0.0", "ember-ajax": "^5.0.0",
"ember-auto-import": "^2.4.2", "ember-auto-import": "^2.4.2",
"ember-cli": "3.28.5", "ember-cli": "3.28.5",
"ember-cli-app-version": "^5.0.0", "ember-cli-app-version": "^5.0.0",
"ember-cli-babel": "^7.19.0", "ember-cli-babel": "^7.19.0",
"ember-cli-clipboard": "^0.16.0", "ember-cli-clipboard": "^0.16.0",
"ember-cli-dependency-checker": "^3.1.0", "ember-cli-dependency-checker": "^3.1.0",
"ember-cli-eslint": "^5.1.0", "ember-cli-eslint": "^5.1.0",
"ember-cli-htmlbars": "^6.0.1", "ember-cli-htmlbars": "^6.0.1",
"ember-cli-inject-live-reload": "^2.0.2", "ember-cli-inject-live-reload": "^2.0.2",
"ember-cli-less": "^3.0.1", "ember-cli-less": "^3.0.1",
"ember-cli-moment-shim": "^3.7.1", "ember-cli-moment-shim": "^3.7.1",
"ember-cli-shims": "^1.2.0", "ember-cli-shims": "^1.2.0",
"ember-cli-sri": "^2.1.1", "ember-cli-sri": "^2.1.1",
"ember-cli-template-lint": "^2.0.2", "ember-cli-template-lint": "^2.0.2",
"ember-cli-uglify": "^3.0.0", "ember-cli-uglify": "^3.0.0",
"ember-composable-helpers": "^3.1.1", "ember-composable-helpers": "^3.1.1",
"ember-export-application-global": "^2.0.0", "ember-export-application-global": "^2.0.0",
"ember-load-initializers": "^2.1.1", "ember-load-initializers": "^2.1.1",
"ember-maybe-import-regenerator": "^1.0.0", "ember-maybe-import-regenerator": "^1.0.0",
"ember-moment": "^10.0.0", "ember-moment": "^10.0.0",
"ember-qunit": "^5.1.5", "ember-qunit": "^5.1.5",
"ember-radio-button": "^2.0.1", "ember-radio-button": "^2.0.1",
"ember-resolver": "^8.0.0", "ember-resolver": "^8.0.0",
"ember-route-action-helper": "^2.0.8", "ember-route-action-helper": "^2.0.8",
"ember-source": "3.28.9", "ember-source": "3.28.9",
"ember-truth-helpers": "^3.0.0", "ember-truth-helpers": "^3.0.0",
"emberx-select": "3.1.1", "emberx-select": "3.1.1",
"eslint-plugin-ember": "^10.6.1", "eslint-plugin-ember": "^10.6.1",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"fuelux": "^3.17.0", "fuelux": "^3.17.0",
"glob": "^8.0.3", "glob": "^8.0.3",
"jquery-colorbox": "^1.6.4", "jquery-colorbox": "^1.6.4",
"json-format-highlight": "^1.0.4", "json-format-highlight": "^1.0.4",
"loader.js": "^4.7.0", "loader.js": "^4.7.0",
"moment": "^2.24.0", "moment": "^2.24.0",
"qunit": "^2.19.1", "qunit": "^2.19.1",
"qunit-dom": "^2.0.0", "qunit-dom": "^2.0.0",
"responsive-toolkit": "^2.6.3", "responsive-toolkit": "^2.6.3",
"select2": "^4.0.13", "select2": "^4.0.13",
"toastr": "^2.1.4", "toastr": "^2.1.4",
"validate.js": "^0.13.1", "validate.js": "^0.13.1",
"webpack": "^5.73.0" "webpack": "^5.73.0"
}, },
"dependencies": { "dependencies": {
"sockjs-client": "^1.6.1", "@stomp/stompjs": "^6.1.2",
"stomp-websocket": "^2.3.4-next" "sockjs-client": "^1.6.1"
}, },
"engines": { "engines": {
"node": "6.* || 8.* || >= 10.*" "node": "6.* || 8.* || >= 10.*"
} }
} }