diff --git a/easyink-admin/src/main/java/com/easyink/web/controller/common/CommonController.java b/easyink-admin/src/main/java/com/easyink/web/controller/common/CommonController.java index cd97cffa..d4a53768 100644 --- a/easyink-admin/src/main/java/com/easyink/web/controller/common/CommonController.java +++ b/easyink-admin/src/main/java/com/easyink/web/controller/common/CommonController.java @@ -5,6 +5,10 @@ import com.easyink.common.constant.Constants; import com.easyink.common.core.domain.AjaxResult; import com.easyink.common.core.domain.FileVo; +import com.easyink.common.enums.ResultTip; +import com.easyink.common.exception.CustomException; +import com.easyink.common.exception.file.FileException; +import com.easyink.common.exception.file.NoFileException; import com.easyink.common.utils.StringUtils; import com.easyink.common.utils.file.FileUploadUtils; import com.easyink.common.utils.file.FileUtils; @@ -24,6 +28,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; /** @@ -77,13 +83,21 @@ public void fileDownload(@ApiParam("文件名") String fileName, @ApiParam("是 response.setContentType("multipart/form-data"); response.setHeader("Content-Disposition", "attachment;fileName=" + FileUtils.setFileDownloadHeader(request, realFileName)); + // 检查文件是否存在 + File file = new File(filePath); + if (!file.exists()) { + log.error("要下载的文件不存在: {}", filePath); + throw new NoFileException("no file"); + } FileUtils.writeBytes(filePath, response.getOutputStream()); if (Boolean.TRUE.equals(delete)) { FileUtils.deleteFile(filePath); } - } catch (Exception e) { - log.error("下载文件失败 e:{}", ExceptionUtils.getStackTrace(e)); + } catch (IOException e) { + log.error("下载文件失败, e:{}", ExceptionUtils.getStackTrace(e)); + throw new NoFileException("error"); } + } /** diff --git a/easyink-admin/src/main/java/com/easyink/web/controller/wecom/WeConversationArchiveController.java b/easyink-admin/src/main/java/com/easyink/web/controller/wecom/WeConversationArchiveController.java index bcfdca34..ce131930 100644 --- a/easyink-admin/src/main/java/com/easyink/web/controller/wecom/WeConversationArchiveController.java +++ b/easyink-admin/src/main/java/com/easyink/web/controller/wecom/WeConversationArchiveController.java @@ -3,6 +3,8 @@ import com.easyink.common.core.controller.BaseController; import com.easyink.common.core.domain.AjaxResult; import com.easyink.common.core.domain.ConversationArchiveQuery; +import com.easyink.common.core.domain.ConversationArchiveViewContextDTO; +import com.easyink.common.core.page.TableDataInfo; import com.easyink.common.token.TokenService; import com.easyink.common.utils.ServletUtils; import com.easyink.common.utils.StringUtils; @@ -86,4 +88,10 @@ public AjaxResult> getChatAllList(ConversationAr return AjaxResult.success(weConversationArchiveService.getChatAllList(query, tokenService.getLoginUser(ServletUtils.getRequest()))); } + @ApiOperation(value = "查看消息上下文接口", httpMethod = "GET") + @GetMapping("/view/context") + public TableDataInfo viewContext(ConversationArchiveViewContextDTO dto) { + dto.setCorpId(LoginTokenService.getLoginUser().getCorpId()); + return weConversationArchiveService.viewContext(dto); + } } diff --git a/easyink-admin/src/main/java/com/easyink/web/controller/wecom/WeCustomerController.java b/easyink-admin/src/main/java/com/easyink/web/controller/wecom/WeCustomerController.java index 48da6f96..120dba4e 100644 --- a/easyink-admin/src/main/java/com/easyink/web/controller/wecom/WeCustomerController.java +++ b/easyink-admin/src/main/java/com/easyink/web/controller/wecom/WeCustomerController.java @@ -1,5 +1,6 @@ package com.easyink.web.controller.wecom; +import com.easyink.common.annotation.DataScope; import com.easyink.common.annotation.Log; import com.easyink.common.constant.WeConstans; import com.easyink.common.core.controller.BaseController; @@ -15,22 +16,31 @@ import com.easyink.wecom.domain.dto.tag.RemoveWeCustomerTagDTO; import com.easyink.wecom.domain.entity.WeCustomerExportDTO; import com.easyink.wecom.domain.vo.WeMakeCustomerTagVO; -import com.easyink.wecom.domain.vo.customer.WeCustomerSumVO; -import com.easyink.wecom.domain.vo.customer.WeCustomerUserListVO; -import com.easyink.wecom.domain.vo.customer.WeCustomerVO; +import com.easyink.wecom.domain.vo.customer.*; import com.easyink.wecom.login.util.LoginTokenService; import com.easyink.wecom.service.WeCustomerService; +import com.easyink.wecom.utils.OprIdGenerator; +import com.google.common.collect.Lists; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static com.easyink.common.constant.Constants.EXPORT_MAX_WAIT_TIME; /** * 企业微信客户Controller @@ -48,6 +58,9 @@ public class WeCustomerController extends BaseController { @Lazy private WeCustomerService weCustomerService; + @Resource(name = "threadPoolTaskExecutor") + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + /** * 查询企业微信客户列表 */ @@ -70,6 +83,7 @@ public TableDataInfo listV2(@RequestBody WeCustomerSearchDTO weCus //预设分页参数 PageInfoUtil.setPage(pageNum, pageSize); } + weCustomerSearchDTO.setCorpId(LoginTokenService.getLoginUser().getCorpId()); WeCustomer weCustomer=weCustomerService.changeWecustomer(weCustomerSearchDTO); List list = weCustomerService.selectWeCustomerListV3(weCustomer); return getDataTable(list); @@ -93,6 +107,7 @@ public TableDataInfo listDistinct(WeCustomer weCustomer) { @PostMapping("/sum") @ApiOperation("查询企业客户统计数据") public AjaxResult sum(@RequestBody WeCustomerSearchDTO weCustomerSearchDTO) { + weCustomerSearchDTO.setCorpId(LoginTokenService.getLoginUser().getCorpId()); WeCustomer weCustomer=weCustomerService.changeWecustomer(weCustomerSearchDTO); weCustomer.setCorpId(LoginTokenService.getLoginUser().getCorpId()); return AjaxResult.success(weCustomerService.weCustomerCount(weCustomer)); @@ -110,18 +125,65 @@ public AjaxResult> getCustomersByUserIdV2(@PathVariable Strin */ @PreAuthorize("@ss.hasPermi('customerManage:customer:export') || @ss.hasPermi('customerManage:lossRemind:export')") @Log(title = "企业微信客户", businessType = BusinessType.EXPORT) - @PostMapping("/export") +// @PostMapping("/export") @ApiOperation("导出企业微信客户列表") + @Deprecated public AjaxResult export(@RequestBody WeCustomerExportDTO dto) { dto.setCorpId(LoginTokenService.getLoginUser().getCorpId()); log.info("[导出客户]开始导出,corpId:{}", dto.getCorpId()); long startTime = System.currentTimeMillis(); AjaxResult export = weCustomerService.export(dto); long endTime = System.currentTimeMillis(); - log.info("[导出客户]导出完成,corpId:{} , time:{} ", dto.getCorpId(), ( endTime - startTime) / 1000.00D); + log.info("[导出客户]导出完成,corpId:{} , time:{} ", dto.getCorpId(), (endTime - startTime) / 1000.00D); return export; } + /** + * 导出企业微信客户列表V2 + */ + @PreAuthorize("@ss.hasPermi('customerManage:customer:export') || @ss.hasPermi('customerManage:lossRemind:export')") + @Log(title = "企业微信客户", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @ApiOperation("导出企业微信客户列表") + public AjaxResult exportV2(@RequestBody WeCustomerExportDTO dto) { + LoginUser loginUser = LoginTokenService.getLoginUser(); + dto.setCorpId(loginUser.getCorpId()); + dto.setAdmin(loginUser.isSuperAdmin()); + String oprId = OprIdGenerator.EXPORT.get(); + String fileName = UUID.randomUUID() + "_" + "customer" + ".xlsx"; + ExportOprVO result = ExportOprVO.builder().oprId(oprId).fileName(fileName).hasFinished(false).build(); + if(dto.getSelectedProperties() == null || dto.getSelectedProperties().size() == 0) { + dto.setSelectedProperties( Lists.newArrayList("客户","添加时间","所属员工","标签")); + } + WeCustomerExportDTO customer = weCustomerService.transferData(dto); + CompletableFuture future = CompletableFuture.runAsync(() -> { + // 执行异步处理任务 + weCustomerService.genExportData(customer, oprId, fileName); + }, threadPoolTaskExecutor); + try { + // 在3秒内等待异步处理任务完成 + future.get(EXPORT_MAX_WAIT_TIME, TimeUnit.SECONDS); + result.setHasFinished(true); + return AjaxResult.success(result); + } catch (TimeoutException e) { + // 处理未完成,只返回OprId + return AjaxResult.success(result); + } catch (InterruptedException | ExecutionException e) { + // 处理出现异常 + return AjaxResult.error(); + } + } + + + @GetMapping("/export/result") + @ApiOperation("获取导出客户的结果") + public AjaxResult getExportResult(String oprId) { + return AjaxResult.success(new CustomerExportResultVO( + weCustomerService.getExportResult(oprId) + ) + ); + } + /** * 获取企业微信客户详细信息-> 未被使用 */ diff --git a/easyink-admin/src/main/java/com/easyink/web/controller/wecom/transfer/WeComTransferController.java b/easyink-admin/src/main/java/com/easyink/web/controller/wecom/transfer/WeComTransferController.java index 2c127ceb..abb0d183 100644 --- a/easyink-admin/src/main/java/com/easyink/web/controller/wecom/transfer/WeComTransferController.java +++ b/easyink-admin/src/main/java/com/easyink/web/controller/wecom/transfer/WeComTransferController.java @@ -1,6 +1,7 @@ package com.easyink.web.controller.wecom.transfer; import com.easyink.common.core.domain.AjaxResult; +import com.easyink.wecom.service.WeFlowerCustomerRelService; import com.easyink.wecom.service.WeSensitiveActHitService; import com.easyink.wecom.service.WeUserCustomerMessageStatisticsService; import io.swagger.annotations.Api; @@ -24,6 +25,7 @@ public class WeComTransferController { private final WeUserCustomerMessageStatisticsService weUserCustomerMessageStatisticsService; private final WeSensitiveActHitService weSensitiveActHitService; + private final WeFlowerCustomerRelService weFlowerCustomerRelService; @PostMapping("/updateUserActiveChatCnt") @ApiOperation("更新历史数据中员工主动发起的会话数") @@ -38,4 +40,11 @@ public AjaxResult updateSensitive() { weSensitiveActHitService.updateHistorySensitive(); return AjaxResult.success(); } + + @PostMapping("/update/totalAllCustomerCnt") + @ApiOperation("数据统计-联系客户-客户总数,旧数据统计") + public AjaxResult updateTotalAllCustomerCnt() { + weFlowerCustomerRelService.updateTotalAllCustomerCnt(); + return AjaxResult.success(); + } } diff --git a/easyink-admin/src/main/resources/application.yml b/easyink-admin/src/main/resources/application.yml index 42414e4e..4ce12e31 100644 --- a/easyink-admin/src/main/resources/application.yml +++ b/easyink-admin/src/main/resources/application.yml @@ -491,6 +491,9 @@ wecome: needErrcodeUrl: # 如果有错误码需要单独业务处理,不抛出异常的接口 - /externalcontact/get_group_msg_result - /externalcontact/del_corp_tag + # 需要重试判断的code + needRetryCode: + - 45033 # JS SDK 身份校验url authorizeUrl: ${WE_COM_AUTHORIZE_URL:https://open.weixin.qq.com/connect/oauth2/authorize} @@ -558,4 +561,8 @@ thread-pool-prop: corePoolSize: ${THREAD_POOL_SEND_CALLBACK_CORE_SIZE:50} maxPoolSize: ${THREAD_POOL_SEND_CALLBACK_MAX_SIZE:200} queueCapacity: ${THREAD_POOL_SEND_CALLBACK_QUEUE_SIZE:100} +# 获取员工执行群发结果线程池参数(因为企微官方接口频率限制,所以这个线程池的最大线程数限制为5) + messageResultTask: + corePoolSize: ${THREAD_POOL_SEND_CALLBACK_CORE_SIZE:5} + maxPoolSize: ${THREAD_POOL_SEND_CALLBACK_MAX_SIZE:5} diff --git a/easyink-common/src/main/java/com/easyink/common/config/WeComeConfig.java b/easyink-common/src/main/java/com/easyink/common/config/WeComeConfig.java index 9b7bdebc..231c43fc 100644 --- a/easyink-common/src/main/java/com/easyink/common/config/WeComeConfig.java +++ b/easyink-common/src/main/java/com/easyink/common/config/WeComeConfig.java @@ -58,4 +58,9 @@ public class WeComeConfig { */ private String[] needErrcodeUrl; + /** + * 需要重试判断的Code + */ + private Integer[] needRetryCode; + } diff --git a/easyink-common/src/main/java/com/easyink/common/constant/Constants.java b/easyink-common/src/main/java/com/easyink/common/constant/Constants.java index a72fff32..9375f7d0 100644 --- a/easyink-common/src/main/java/com/easyink/common/constant/Constants.java +++ b/easyink-common/src/main/java/com/easyink/common/constant/Constants.java @@ -278,6 +278,12 @@ public static Long[] getInitMenuList(){ */ protected static final String[] CSV_INJECT_CHAR_LIST = {"+", "-", "@", "="}; + /** + * 导出最大等待时间 + */ + public final static int EXPORT_MAX_WAIT_TIME = 3 ; + + /** * 获得可能会引起CSV注入 的特殊字符序列 * @@ -295,4 +301,14 @@ public static Long[] getInitMenuList(){ * 默认的新客留存率值 */ public static final String EMPTY_RETAIN_RATE_VALUE = "-"; + + /** + * 数据统计-联系客户最大查询时间范围 + */ + public static final Integer STATISTIC_MAX_COUNT_DATES = 180; + + /** + * 群发发送人/群名称的分隔符 + */ + public static final String CUSTOMER_PUSH_MESSAGE_SEPARATOR = "、"; } diff --git a/easyink-common/src/main/java/com/easyink/common/constant/conversation/ConversationArchiveConstants.java b/easyink-common/src/main/java/com/easyink/common/constant/conversation/ConversationArchiveConstants.java new file mode 100644 index 00000000..01f4280b --- /dev/null +++ b/easyink-common/src/main/java/com/easyink/common/constant/conversation/ConversationArchiveConstants.java @@ -0,0 +1,55 @@ +package com.easyink.common.constant.conversation; + +/** + * 会话存档常量类 + * + * @author lichaoyu + * @date 2023/9/18 10:11 + */ +public class ConversationArchiveConstants { + + /** + * 发送状态为已发送 + */ + public static final String ACTION_SEND = "send"; + + /** + * 发送状态为已撤回 + */ + public static final String ACTION_RECALL = "recall"; + + /** + * 查询的结果数量开始位置 + */ + public static final int SEARCH_FROM = 0; + + /** + * 查询的结果数量最大值(pageSize设置为10000表示全部查询) + */ + public static final int SEARCH_SIZE = 10000; + + /** + * 返回的msgId标识符 + */ + public static final String MSG_ID = "msgId"; + + /** + * 查询下文信息标识 + */ + public static final String PRIOR_CONTEXT = "after"; + + /** + * 查询上文信息标识 + */ + public static final String NEXT_CONTEXT = "before"; + + /** + * 默认的查询上下文信息的聊天条数 + */ + public static final int DEFAULT_CONTEXT_NUM = 10; + + /** + * 分页查询上下文信息的聊天条数 + */ + public static final int PAGE_CONTEXT_NUM = 20; +} diff --git a/easyink-common/src/main/java/com/easyink/common/core/domain/BaseEntity.java b/easyink-common/src/main/java/com/easyink/common/core/domain/BaseEntity.java index 27f2e8ec..9334e5f1 100644 --- a/easyink-common/src/main/java/com/easyink/common/core/domain/BaseEntity.java +++ b/easyink-common/src/main/java/com/easyink/common/core/domain/BaseEntity.java @@ -88,6 +88,14 @@ public class BaseEntity extends RootEntity implements Serializable { @ApiModelProperty(value = "做游标分页时使用,查找userId > lastId的数据") private String lastId; + /** + * 是否是管理员 ,如果是管理员则部分查询可以简化 + */ + @JsonIgnore + @TableField(exist = false) + private Boolean isAdmin; + + public String getLastId() { return lastId; } @@ -205,5 +213,11 @@ public void setEndTime(String endTime) { this.endTime = DateUtils.parseEndDay(endTime); } + public Boolean getAdmin() { + return isAdmin; + } + public void setAdmin(Boolean admin) { + isAdmin = admin; + } } diff --git a/easyink-common/src/main/java/com/easyink/common/core/domain/ConversationArchiveViewContextDTO.java b/easyink-common/src/main/java/com/easyink/common/core/domain/ConversationArchiveViewContextDTO.java new file mode 100644 index 00000000..50ef6365 --- /dev/null +++ b/easyink-common/src/main/java/com/easyink/common/core/domain/ConversationArchiveViewContextDTO.java @@ -0,0 +1,34 @@ +package com.easyink.common.core.domain; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 会话存档获取上下文DTO + * + * @author lichaoyu + * @date 2023/9/18 16:40 + */ +@Data +@ApiModel("会话存档获取上下文DTO") +public class ConversationArchiveViewContextDTO { + + @ApiModelProperty("企业id") + private String corpId; + + @ApiModelProperty("群聊id") + private String roomId; + + @ApiModelProperty("发送者id") + private String fromId; + + @ApiModelProperty("接收者id") + private String receiveId; + + @ApiModelProperty("查看上下文的消息id") + private String msgId; + + @ApiModelProperty("查询上下文类型: null:前后十条, after:后20条, before:前20条") + private String type; +} diff --git a/easyink-common/src/main/java/com/easyink/common/enums/ResultTip.java b/easyink-common/src/main/java/com/easyink/common/enums/ResultTip.java index 76ea86bd..6cd1c467 100644 --- a/easyink-common/src/main/java/com/easyink/common/enums/ResultTip.java +++ b/easyink-common/src/main/java/com/easyink/common/enums/ResultTip.java @@ -219,6 +219,7 @@ public enum ResultTip { TIP_FAIL_TO_GET_CUSTOMER_INFO(4026,"无法获取客户详情"), TIP_FAIL_ADD_LOSS_TAG(4027, "数据不存在,客户打标签失败"), TIP_FAIL_INSERT_LOSS_TAG(4028, "请设置流失标签"), + TIP_OPTION_REPEAT(4029, "选项值不能重复"), // 动态表单 5000开始 TIP_GROUP_FORM_SOURCE_TYPE_ERROR(50001,"表单分组类别错误"), diff --git a/easyink-common/src/main/java/com/easyink/common/exception/RetryException.java b/easyink-common/src/main/java/com/easyink/common/exception/RetryException.java new file mode 100644 index 00000000..4a3e33f5 --- /dev/null +++ b/easyink-common/src/main/java/com/easyink/common/exception/RetryException.java @@ -0,0 +1,27 @@ +package com.easyink.common.exception; + +import com.dtflys.forest.exceptions.ForestRuntimeException; + +/** + * 重试异常类 + * + * @author lichaoyu + * @date 2023/9/27 11:39 + */ +public class RetryException extends ForestRuntimeException { + + private static final long serialVersionUID = 1L; + + public RetryException(String message) { + super(message); + } + + public RetryException(String message, Throwable cause) { + super(message, cause); + } + + public RetryException(Throwable cause) { + super(cause); + } + +} diff --git a/easyink-common/src/main/java/com/easyink/common/exception/file/NoFileException.java b/easyink-common/src/main/java/com/easyink/common/exception/file/NoFileException.java new file mode 100644 index 00000000..01dddc7d --- /dev/null +++ b/easyink-common/src/main/java/com/easyink/common/exception/file/NoFileException.java @@ -0,0 +1,15 @@ +package com.easyink.common.exception.file; + +/** + * 类名: 文件找不到异常 + * + * @author : silver_chariot + * @date : 2023/9/22 15:34 + **/ +public class NoFileException extends RuntimeException{ + private static final long serialVersionUID = 1L; + + public NoFileException(String message) { + super(message); + } +} diff --git a/easyink-common/src/main/java/com/easyink/common/utils/DateUtils.java b/easyink-common/src/main/java/com/easyink/common/utils/DateUtils.java index 17d8919c..59fc5793 100644 --- a/easyink-common/src/main/java/com/easyink/common/utils/DateUtils.java +++ b/easyink-common/src/main/java/com/easyink/common/utils/DateUtils.java @@ -1,5 +1,6 @@ package com.easyink.common.utils; +import com.easyink.common.constant.Constants; import com.easyink.common.enums.ResultTip; import com.easyink.common.enums.WeOperationsCenterSop; import com.easyink.common.exception.CustomException; @@ -768,4 +769,22 @@ public static Boolean after(String date1, String date2) { return false; } + /** + * 获取当前日期前一天日期的前180天日期范围列表 + * + * @return 日期范围列表,格式为YYYY-MM-DD + */ + public static List get180DateAgoList() { + // 获取当前日期 + LocalDate today = LocalDate.now(); + // 获取前一天日期 + LocalDate yesterday = today.minusDays(1); + // 计算前180天的日期范围 + List dateRange = new ArrayList<>(); + for (int i = 0; i < Constants.STATISTIC_MAX_COUNT_DATES; i++) { + LocalDate date = yesterday.minusDays(i); + dateRange.add(date.format(DateTimeFormatter.ofPattern(YYYY_MM_DD))); + } + return dateRange; + } } diff --git a/easyink-common/src/main/java/com/easyink/common/utils/poi/ExcelUtil.java b/easyink-common/src/main/java/com/easyink/common/utils/poi/ExcelUtil.java index 062af69d..f2e83504 100644 --- a/easyink-common/src/main/java/com/easyink/common/utils/poi/ExcelUtil.java +++ b/easyink-common/src/main/java/com/easyink/common/utils/poi/ExcelUtil.java @@ -89,17 +89,23 @@ public class ExcelUtil { /** * 是否是自定义选择导出字段 */ - private Boolean isCustom; + public Boolean isCustom; /** * 自定义选中导出的字段名集合 */ - private List selectedProperties = new ArrayList<>(); + public List selectedProperties = new ArrayList<>(); public ExcelUtil(Class clazz) { this.clazz = clazz; } + public ExcelUtil(Class clazz, List selectProperties ) { + this.clazz = clazz; + this.isCustom = CollectionUtils.isNotEmpty(selectProperties); + this.selectedProperties = selectProperties; + } + public void init(List list, String sheetName, Type type) { if (list == null) { list = new ArrayList<>(); @@ -143,6 +149,7 @@ public List importExcel(InputStream is) throws Exception { return importExcel(StringUtils.EMPTY, is); } + /** * 对excel表单指定表格索引名转换成list * @@ -411,12 +418,69 @@ public AjaxResult exportExcelV2() { } } + + /** + * 对list数据源将其里面的数据导入到excel + * + * @return 结果 + */ + public AjaxResult exportExcelByPage() { + OutputStream out = null; + try { + // 取出一共有多少个sheet. + double sheetNo = Math.ceil((double) list.size() / SHEET_SIZE); + for (int index = 0; index < sheetNo; index++) { + createSheet(sheetNo, index); + + // 产生一行 + Row row = sheet.createRow(0); + int column = 0; + // 写入各个字段的列头名称 + if (isCustom) { + for (String colName : selectedProperties) { + this.createCell(colName, row, column++); + } + } else { + for (Object[] os : fields) { + Excel excel = (Excel) os[1]; + this.createCell(excel, row, column++); + } + } + if (Type.EXPORT.equals(type)) { + fillExcelDataV2(index); + } + } + String filename = encodingFilename(sheetName); + out = new FileOutputStream(getAbsoluteFile(filename)); + wb.write(out); + return AjaxResult.success(filename); + } catch (Exception e) { + log.error("导出Excel异常{}", ExceptionUtils.getStackTrace(e)); + throw new CustomException("导出Excel失败,请联系网站管理员!"); + } finally { + if (wb != null) { + try { + wb.close(); + } catch (IOException e1) { + log.error("异常信息:{}", e1.getMessage()); + } + } + if (out != null) { + try { + out.close(); + } catch (IOException e1) { + log.error("异常信息:{}", e1.getMessage()); + } + } + } + } + /** * 写入各个字段的列头名称(包括引入注解的属性和扩展属性) * * @return */ - public AjaxResult exportExcelDefinedAndExtProp(){ + public AjaxResult exportExcelDefinedAndExtProp() { OutputStream out = null; try { // 取出一共有多少个sheet. @@ -1000,7 +1064,7 @@ private Object getValue(Object o, String name) throws InvocationTargetException, /** * 得到所有定义字段 */ - private void createExcelField() { + public void createExcelField() { this.fields = new ArrayList<>(); List tempFields = new ArrayList<>(); tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields())); @@ -1034,6 +1098,8 @@ private void createExcelFieldV2() { for (Field field : tempFields) { // 单注解 if (field.isAnnotationPresent(Excel.class)) { + field.getAnnotation(Excel.class); + // 非自定义导出 或者 包含导出字段时才填充相应的表头 putToField(field, field.getAnnotation(Excel.class)); } // 多注解 @@ -1048,7 +1114,33 @@ private void createExcelFieldV2() { } } } - this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList()); + this.fields = this.fields.stream() + .sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())) + .collect(Collectors.toList()); + } + + /*** + * 获取表头 + * @return 表头列表 + */ + + public List> getFields() { + createExcelFieldV2(); + List> finalHeads = new ArrayList<>(); + if(isCustom) { + for(String colName: selectedProperties) { + List head = new ArrayList<>(); + head.add(colName); + finalHeads.add(head); + } + }else { + for(Object[] os : fields) { + List head = new ArrayList<>(); + head.add(((Excel)os[1]).name()); + finalHeads.add(head); + } + } + return finalHeads; } diff --git a/easyink-framework/src/main/java/com/easyink/framework/config/ThreadPoolConfig.java b/easyink-framework/src/main/java/com/easyink/framework/config/ThreadPoolConfig.java index e8cd6506..028cb3f5 100644 --- a/easyink-framework/src/main/java/com/easyink/framework/config/ThreadPoolConfig.java +++ b/easyink-framework/src/main/java/com/easyink/framework/config/ThreadPoolConfig.java @@ -12,10 +12,7 @@ import org.springframework.context.annotation.Primary; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.*; /** * 线程池配置 @@ -116,14 +113,25 @@ public ThreadPoolTaskExecutor sendCallbackExecutor() { } + /** + * 获取员工执行群发结果线程池(因为企微官方接口频率限制,所以这个线程池的最大线程数限制为5) + * + * @return + */ + @Bean("messageResultTaskExecutor") + public ThreadPoolTaskExecutor messageResultTaskExecutor() { + ThreadPoolProperties.BaseThreadProperty prop = threadPoolProperties.getMessageResultTask(); + return init(prop.getCorePoolSize(), prop.getMaxPoolSize(), null, prop.getKeepAliveSeconds(), "messageResultTask"); + } + /** * 构建线程池 * - * @param corePoolSize 核心线程数 - * @param maxPoolSize 最大线程数 - * @param keepAliveSeconds 活跃时间 - * @param queueCapacity 队列容量 - * @param poolNamePrefix 线程池名前缀 + * @param corePoolSize 核心线程数 + * @param maxPoolSize 最大线程数 + * @param keepAliveSeconds 活跃时间 + * @param queueCapacity 队列容量 + * @param poolNamePrefix 线程池名前缀 * @return {@link ThreadPoolTaskExecutor} */ public ThreadPoolTaskExecutor init(Integer corePoolSize, Integer maxPoolSize, Integer queueCapacity, Integer keepAliveSeconds, String poolNamePrefix) { @@ -136,8 +144,10 @@ public ThreadPoolTaskExecutor init(Integer corePoolSize, Integer maxPoolSize, In executor.setCorePoolSize(corePoolSize); //最大线程数 executor.setMaxPoolSize(maxPoolSize); - //队列容量 - executor.setQueueCapacity(queueCapacity); + if (queueCapacity != null) { + //队列容量 + executor.setQueueCapacity(queueCapacity); + } //活跃时间 executor.setKeepAliveSeconds(keepAliveSeconds); construct(executor, poolNamePrefix); diff --git a/easyink-framework/src/main/java/com/easyink/framework/config/properties/ThreadPoolProperties.java b/easyink-framework/src/main/java/com/easyink/framework/config/properties/ThreadPoolProperties.java index 6d6fedc1..10ee5c43 100644 --- a/easyink-framework/src/main/java/com/easyink/framework/config/properties/ThreadPoolProperties.java +++ b/easyink-framework/src/main/java/com/easyink/framework/config/properties/ThreadPoolProperties.java @@ -34,6 +34,10 @@ public class ThreadPoolProperties { * 转发回调 线程池参数 */ private BaseThreadProperty sendCallback; + /** + * 获取员工执行群发结果 线程池参数(因为企微官方接口频率限制,所以这个线程池的最大线程数限制为5) + */ + private BaseThreadProperty messageResultTask; /** * 基础线程池 参数 diff --git a/easyink-framework/src/main/java/com/easyink/framework/web/exception/GlobalExceptionHandler.java b/easyink-framework/src/main/java/com/easyink/framework/web/exception/GlobalExceptionHandler.java index 8cac2268..db663bd7 100644 --- a/easyink-framework/src/main/java/com/easyink/framework/web/exception/GlobalExceptionHandler.java +++ b/easyink-framework/src/main/java/com/easyink/framework/web/exception/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package com.easyink.framework.web.exception; +import cn.hutool.http.HttpResponse; import cn.hutool.json.JSONUtil; import com.dtflys.forest.exceptions.ForestRuntimeException; import com.easyink.common.constant.WeConstans; @@ -8,6 +9,7 @@ import com.easyink.common.enums.WeExceptionTip; import com.easyink.common.exception.BaseException; import com.easyink.common.exception.CustomException; +import com.easyink.common.exception.file.NoFileException; import com.easyink.common.utils.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.poi.ss.formula.functions.T; @@ -23,6 +25,9 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.NoHandlerFoundException; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + /** * 全局异常处理器 * @@ -137,6 +142,15 @@ public AjaxResult weComException(ForestRuntimeException forestExcetion) { return AjaxResult.error("企业微信端未知异常,请联系管理员"); } + @ExceptionHandler(NoFileException.class) + public void noFile (NoFileException e , HttpServletResponse response) { + try { + response.sendError(500); + } catch (IOException ex) { + throw new RuntimeException(ex); + } +// return AjaxResult.error("未获取到文件资源,请重新导出"); + } } diff --git a/easyink-quartz/src/main/java/com/easyink/quartz/task/MessageResultTask.java b/easyink-quartz/src/main/java/com/easyink/quartz/task/MessageResultTask.java index d6ab6af6..09d9e128 100644 --- a/easyink-quartz/src/main/java/com/easyink/quartz/task/MessageResultTask.java +++ b/easyink-quartz/src/main/java/com/easyink/quartz/task/MessageResultTask.java @@ -13,14 +13,13 @@ import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; +import javax.annotation.Resource; import java.util.Arrays; import java.util.Date; import java.util.List; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; /** * 类名: 消息结果定时任务 @@ -34,10 +33,9 @@ public class MessageResultTask { private final WeCorpAccountService weCorpAccountService; private final WeCustomerMessageOriginalService weCustomerMessageOriginalService; private final WeCustomerMessgaeResultMapper weCustomerMessgaeResultMapper; - + @Resource(name = "messageResultTaskExecutor") + private ThreadPoolTaskExecutor messageResultTaskExecutor; private static final int SUB_DAY = 30; - private static ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(10, - new BasicThreadFactory.Builder().namingPattern("MessageResultTaskSchedual-%d").daemon(true).build()); @Autowired public MessageResultTask(WeCorpAccountService weCorpAccountService, WeCustomerMessageOriginalService weCustomerMessageOriginalService, WeCustomerMessgaeResultMapper weCustomerMessgaeResultMapper) { @@ -65,14 +63,14 @@ public void asyncSendResult() { List resultDtoList = weCustomerMessgaeResultMapper.listOfNotSend(corpId, startTime, endTime); for (AsyncResultDTO asyncResultDTO : resultDtoList) { String finalCorpId = corpId; - scheduledExecutorService.schedule(() -> { + messageResultTaskExecutor.execute(() -> { try { asyncResultDTO.setMsgids(Arrays.asList(asyncResultDTO.getMsgArray())); weCustomerMessageOriginalService.asyncResult(asyncResultDTO, finalCorpId); } catch (JsonProcessingException e) { log.error("MessageResultTask定时任务异常 ex:{},messageId:{}", ExceptionUtils.getStackTrace(e), asyncResultDTO.getMessageId()); } - }, 0, TimeUnit.SECONDS); + }); } } } diff --git a/easyink-quartz/src/main/java/com/easyink/quartz/util/AbstractQuartzJob.java b/easyink-quartz/src/main/java/com/easyink/quartz/util/AbstractQuartzJob.java index 458da684..e0c3a267 100644 --- a/easyink-quartz/src/main/java/com/easyink/quartz/util/AbstractQuartzJob.java +++ b/easyink-quartz/src/main/java/com/easyink/quartz/util/AbstractQuartzJob.java @@ -40,7 +40,7 @@ public void execute(JobExecutionContext context) { doExecute(context, sysJob); after(context, sysJob, null); } catch (Exception e) { - log.error("任务执行异常 - :", ExceptionUtils.getStackTrace(e)); + log.error("任务执行异常 - :{}", ExceptionUtils.getStackTrace(e)); after(context, sysJob, e); } } diff --git a/easyink-wecom/pom.xml b/easyink-wecom/pom.xml index 628ef255..9f50c0ae 100644 --- a/easyink-wecom/pom.xml +++ b/easyink-wecom/pom.xml @@ -16,7 +16,30 @@ - + + + com.alibaba + easyexcel + 2.2.6 + + + javax.servlet + servlet-api + + + org.apache.poi + poi + + + org.apache.poi + poi-ooxml + + + org.apache.poi + poi-ooxml-schemas + + + com.easyink diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeAccessTokenClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeAccessTokenClient.java index 36209054..c4345198 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeAccessTokenClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeAccessTokenClient.java @@ -1,6 +1,8 @@ package com.easyink.wecom.client; import com.dtflys.forest.annotation.*; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.WeAccessTokenDTO; import com.easyink.wecom.domain.dto.WeAccessUserInfo3rdDTO; import com.easyink.wecom.domain.dto.WeLoginUserInfoDTO; @@ -18,6 +20,7 @@ * @date: 2021-08-18 17:01 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}") public interface WeAccessTokenClient { /** diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeAgentClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeAgentClient.java index 014c6543..12425f1c 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeAgentClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeAgentClient.java @@ -4,6 +4,8 @@ import com.dtflys.forest.annotation.Get; import com.dtflys.forest.annotation.Header; import com.dtflys.forest.annotation.Query; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.resp.GetAgentResp; import com.easyink.wecom.interceptor.WeAccessTokenInterceptor; import org.springframework.stereotype.Component; @@ -15,6 +17,7 @@ * @date : 2022/7/4 11:29 **/ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeAgentClient { /** diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCropTagClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCropTagClient.java index ebf11346..de80d323 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCropTagClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCropTagClient.java @@ -4,6 +4,8 @@ import com.dtflys.forest.annotation.Body; import com.dtflys.forest.annotation.Header; import com.dtflys.forest.annotation.Post; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.WeResultDTO; import com.easyink.wecom.domain.dto.tag.*; import com.easyink.wecom.interceptor.WeAccessTokenInterceptor; @@ -16,6 +18,7 @@ * @date: 2021-08-18 17:03 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeCropTagClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerAcquisitionClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerAcquisitionClient.java index 33104ad7..6eabe68e 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerAcquisitionClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerAcquisitionClient.java @@ -1,10 +1,11 @@ package com.easyink.wecom.client; import com.dtflys.forest.annotation.*; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.emplecode.CustomerAssistantDTO; import com.easyink.wecom.domain.dto.emplecode.CustomerAssistantResp; import com.easyink.wecom.interceptor.WeAccessTokenInterceptor; -import io.swagger.annotations.ApiOperation; import org.springframework.stereotype.Component; /** @@ -14,6 +15,7 @@ * @date 2023/8/23 10:41 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeCustomerAcquisitionClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerClient.java index fcb0ea41..a3571f34 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerClient.java @@ -1,6 +1,7 @@ package com.easyink.wecom.client; import com.dtflys.forest.annotation.*; +import com.easyink.common.exception.RetryException; import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.*; import com.easyink.wecom.domain.dto.customer.*; @@ -30,6 +31,7 @@ public interface WeCustomerClient { * @return */ @Get(url = "/externalcontact/get_follow_user_list", interceptor = WeAccessTokenInterceptor.class) + @EnableRetry(retryExceptionClass = RetryException.class) FollowUserList getFollowUserList(@Header("corpid") String corpId); @@ -49,6 +51,7 @@ public interface WeCustomerClient { * @param corpId 企业ID * @return */ + @EnableRetry(maxAttempts = 1) @Get(url = "/externalcontact/get", interceptor = WeAccessTokenInterceptor.class) GetExternalDetailResp getV2(@Query("external_userid") String externalUserid, @Header("corpid") String corpId); @@ -60,6 +63,7 @@ public interface WeCustomerClient { * @return {@link GetByUserResp} */ @Post(url = "/externalcontact/batch/get_by_user", interceptor = WeAccessTokenInterceptor.class) + @EnableRetry(retryExceptionClass = RetryException.class) GetByUserResp getByUser(@Body GetByUserReq req, @Header("corpId") String corpId); @@ -70,6 +74,7 @@ public interface WeCustomerClient { * @return */ @Post(url = "/externalcontact/remark", interceptor = WeAccessTokenInterceptor.class) + @EnableRetry(retryExceptionClass = RetryException.class) WeResultDTO remark(@Body WeCustomerDTO.WeCustomerRemark weCustomerRemark, @Header("corpid") String corpId); @@ -87,6 +92,7 @@ public interface WeCustomerClient { * 客户发送欢迎语 */ @Post(url = "/externalcontact/send_welcome_msg", interceptor = WeAccessTokenInterceptor.class) + @EnableRetry(retryExceptionClass = RetryException.class) WeResultDTO sendWelcomeMsg(@Body WeWelcomeMsg wxCpWelcomeMsg, @Header("corpid") String corpId); @@ -96,6 +102,7 @@ public interface WeCustomerClient { * @return */ @Post(url = "/externalcontact/unionid_to_external_userid", interceptor = WeAccessTokenInterceptor.class) + @EnableRetry(retryExceptionClass = RetryException.class) ExternalUserDetail unionidToExternalUserid(@Body ExternalContact unionid, @Header("corpid") String corpId); @@ -105,6 +112,7 @@ public interface WeCustomerClient { * @return */ @Post(url = "/externalcontact/get_user_behavior_data", interceptor = WeAccessTokenInterceptor.class) + @EnableRetry(retryExceptionClass = RetryException.class) UserBehaviorDataDTO getUserBehaviorData(@JSONBody UserBehaviorDataQuery query, @Header("corpid") String corpId); /** @@ -113,6 +121,7 @@ public interface WeCustomerClient { * @return */ @Post(url = "/externalcontact/groupchat/statistic", interceptor = WeAccessTokenInterceptor.class) + @EnableRetry(retryExceptionClass = RetryException.class) GroupChatStatisticDTO getGroupChatStatistic(@JSONBody GroupChatStatisticQuery query, @Header("corpid") String corpId); /** @@ -121,10 +130,12 @@ public interface WeCustomerClient { * @return */ @Post(url = "/externalcontact/groupchat/statistic_group_by_day", interceptor = WeAccessTokenInterceptor.class) + @EnableRetry(retryExceptionClass = RetryException.class) GroupChatStatisticDTO getGroupChatStatisticGroupByDay(@JSONBody GroupChatStatisticQuery query, @Header("corpid") String corpId); @Post(url = "/externalcontact/unionid_to_external_userid" , interceptor = WeAccessTokenInterceptor.class) + @EnableRetry(retryExceptionClass = RetryException.class) UnionId2ExternalUserIdResp unionId2ExternalUserId (@Body("unionid")String unionid ,@Body("openid")String openid) ; @@ -142,6 +153,7 @@ public interface WeCustomerClient { * @return ExternalUserDetail#external_userid */ @Post(url = "/idconvert/unionid_to_external_userid", interceptor = WeAccessTokenInterceptor.class) + @EnableRetry(retryExceptionClass = RetryException.class) ExternalUserDetail getExternalUserIdByUnionIdAndOpenId(@Body("unionid") String unionId, @Body("openid") String openId, @Body("subject_type") Integer subjectType, diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerGroupClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerGroupClient.java index 74c9f3c2..f57dc563 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerGroupClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerGroupClient.java @@ -4,6 +4,8 @@ import com.dtflys.forest.annotation.Body; import com.dtflys.forest.annotation.Header; import com.dtflys.forest.annotation.Post; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.customer.CustomerGroupDetail; import com.easyink.wecom.domain.dto.customer.CustomerGroupList; import com.easyink.wecom.domain.dto.group.GroupChatListReq; @@ -18,6 +20,7 @@ * @date: 2021-08-18 17:05 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeCustomerGroupClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerMessagePushClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerMessagePushClient.java index e12cc30b..d128f3fd 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerMessagePushClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeCustomerMessagePushClient.java @@ -4,6 +4,8 @@ import com.dtflys.forest.annotation.Body; import com.dtflys.forest.annotation.Header; import com.dtflys.forest.annotation.Post; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.message.QueryCustomerMessageStatusResultDTO; import com.easyink.wecom.domain.dto.message.QueryCustomerMessageStatusResultDataObjectDTO; import com.easyink.wecom.domain.dto.message.SendMessageResultDTO; @@ -18,6 +20,7 @@ * @date: 2021-08-18 17:05 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeCustomerMessagePushClient { @@ -35,6 +38,7 @@ public interface WeCustomerMessagePushClient { * * @param queryCustomerMessageStatusResultDataObjectDTO{msgid} 添加企业群发消息任务返回的msgid */ + @EnableRetry(maxAttempts = 1) @Post(url = "/externalcontact/get_group_msg_result") QueryCustomerMessageStatusResultDTO queryCustomerMessageStatus(@Body QueryCustomerMessageStatusResultDataObjectDTO queryCustomerMessageStatusResultDataObjectDTO, @Header("corpid") String corpId); diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeDepartMentClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeDepartMentClient.java index a4e93d8e..390897e6 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeDepartMentClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeDepartMentClient.java @@ -1,6 +1,8 @@ package com.easyink.wecom.client; import com.dtflys.forest.annotation.*; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.WeDepartMentDTO; import com.easyink.wecom.domain.dto.WeResultDTO; import com.easyink.wecom.interceptor.WeAccessTokenInterceptor; @@ -13,6 +15,7 @@ * @date: 2021-08-18 17:05 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeDepartMentClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeExternalContactClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeExternalContactClient.java index 90bec1fe..087033bb 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeExternalContactClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeExternalContactClient.java @@ -1,9 +1,9 @@ package com.easyink.wecom.client; import com.dtflys.forest.annotation.*; -import com.easyink.wecom.domain.dto.emplecode.CustomerAssistantDTO; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.WeExternalContactDTO; -import com.easyink.wecom.domain.dto.emplecode.CustomerAssistantResp; import com.easyink.wecom.domain.dto.transfer.*; import com.easyink.wecom.interceptor.WeAccessTokenInterceptor; import org.springframework.stereotype.Component; @@ -15,6 +15,7 @@ * @date: 2021-08-18 17:08 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeExternalContactClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeGroupChatJoinClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeGroupChatJoinClient.java index 25a61dd3..5b95b0b3 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeGroupChatJoinClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeGroupChatJoinClient.java @@ -4,6 +4,8 @@ import com.dtflys.forest.annotation.Body; import com.dtflys.forest.annotation.Header; import com.dtflys.forest.annotation.Post; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.group.*; import com.easyink.wecom.interceptor.WeAccessTokenInterceptor; import org.springframework.stereotype.Component; @@ -15,6 +17,7 @@ * 2022/2/9 15:02 **/ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeGroupChatJoinClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMediaClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMediaClient.java index 984cfeb9..b3dd9aba 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMediaClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMediaClient.java @@ -1,6 +1,8 @@ package com.easyink.wecom.client; import com.dtflys.forest.annotation.*; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.WeMediaDTO; import com.easyink.wecom.interceptor.WeAccessTokenInterceptor; import org.springframework.stereotype.Component; @@ -15,6 +17,7 @@ * @date: 2021-08-18 17:08 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeMediaClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMessagePushClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMessagePushClient.java index b1c4e13f..97f98630 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMessagePushClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMessagePushClient.java @@ -4,6 +4,8 @@ import com.dtflys.forest.annotation.Body; import com.dtflys.forest.annotation.Header; import com.dtflys.forest.annotation.Post; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.WeMessagePushDTO; import com.easyink.wecom.domain.dto.WeMessagePushGroupDTO; import com.easyink.wecom.domain.dto.WeMessagePushResultDTO; @@ -17,6 +19,7 @@ * @date: 2021-08-18 17:08 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @SuppressWarnings("all") @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeMessagePushClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMomentClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMomentClient.java index 6284ee11..c6afd3bf 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMomentClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMomentClient.java @@ -1,6 +1,8 @@ package com.easyink.wecom.client; import com.dtflys.forest.annotation.*; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.moment.*; import com.easyink.wecom.domain.vo.MomentStrategyGetVO; import com.easyink.wecom.domain.vo.moment.*; @@ -14,6 +16,7 @@ * @date 2022/1/6 14:10 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeMomentClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMsgAuditClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMsgAuditClient.java index 0612acf8..d55579d1 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMsgAuditClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeMsgAuditClient.java @@ -4,6 +4,8 @@ import com.dtflys.forest.annotation.Body; import com.dtflys.forest.annotation.Header; import com.dtflys.forest.annotation.Post; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.msgaudit.WeMsgAuditDTO; import com.easyink.wecom.domain.vo.WeMsgAuditVO; import com.easyink.wecom.interceptor.WeAccessTokenInterceptor; @@ -16,6 +18,7 @@ * @date: 2021-08-18 17:10 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeMsgAuditClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeTicketClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeTicketClient.java index ab4587f9..7eec5919 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeTicketClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeTicketClient.java @@ -4,6 +4,8 @@ import com.dtflys.forest.annotation.Get; import com.dtflys.forest.annotation.Header; import com.dtflys.forest.annotation.Query; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.WeH5TicketDto; import com.easyink.wecom.interceptor.WeAccessTokenInterceptor; import org.springframework.stereotype.Component; @@ -15,6 +17,7 @@ * @date: 2021-08-18 17:12 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeTicketClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeUnionIdClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeUnionIdClient.java index d78d4d1c..f7ee041e 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeUnionIdClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeUnionIdClient.java @@ -4,8 +4,9 @@ import com.dtflys.forest.annotation.Get; import com.dtflys.forest.annotation.Header; import com.dtflys.forest.annotation.Query; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.customer.ExternalUserDetail; -import com.easyink.wecom.interceptor.WeAccessTokenInterceptor; import com.easyink.wecom.interceptor.WeUnionIdInterceptor; import org.springframework.stereotype.Component; @@ -16,6 +17,7 @@ * @date : 2023/1/5 14:03 **/ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeUnionIdInterceptor.class) public interface WeUnionIdClient { /** diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeUpdateIDClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeUpdateIDClient.java index b1ac8a6f..458df7af 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeUpdateIDClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeUpdateIDClient.java @@ -1,6 +1,8 @@ package com.easyink.wecom.client; import com.dtflys.forest.annotation.*; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.CorpIdToOpenCorpIdResp; import com.easyink.wecom.domain.dto.WeResultDTO; import com.easyink.wecom.interceptor.WeAccessTokenInterceptor; @@ -18,6 +20,7 @@ * @date 2022/8/22 17:56 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}") public interface WeUpdateIDClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeUserClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeUserClient.java index 25c5f0d1..b5ec4d08 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeUserClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeUserClient.java @@ -1,6 +1,8 @@ package com.easyink.wecom.client; import com.dtflys.forest.annotation.*; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.*; import com.easyink.wecom.interceptor.WeAccessTokenInterceptor; import org.springframework.stereotype.Component; @@ -14,6 +16,7 @@ * @date: 2021-08-18 17:16 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeUserClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeWelcomeMsgClient.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeWelcomeMsgClient.java index 1e047827..94c48e0a 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/WeWelcomeMsgClient.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/WeWelcomeMsgClient.java @@ -4,6 +4,8 @@ import com.dtflys.forest.annotation.Body; import com.dtflys.forest.annotation.Header; import com.dtflys.forest.annotation.Post; +import com.easyink.common.exception.RetryException; +import com.easyink.wecom.client.retry.EnableRetry; import com.easyink.wecom.domain.dto.welcomemsg.GroupWelcomeMsgAddDTO; import com.easyink.wecom.domain.dto.welcomemsg.GroupWelcomeMsgDeleteDTO; import com.easyink.wecom.domain.dto.welcomemsg.GroupWelcomeMsgResult; @@ -18,6 +20,7 @@ * @date: 2021-08-18 17:08 */ @Component +@EnableRetry(retryExceptionClass = RetryException.class) @BaseRequest(baseURL = "${weComServerUrl}${weComePrefix}", interceptor = WeAccessTokenInterceptor.class) public interface WeWelcomeMsgClient { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/retry/EnableRetry.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/retry/EnableRetry.java index 5e474fe4..e970ca46 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/retry/EnableRetry.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/retry/EnableRetry.java @@ -12,7 +12,7 @@ * @date : 2023/6/25 15:33 **/ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) +@Target({ElementType.METHOD, ElementType.TYPE}) public @interface EnableRetry { /** * 最大请求次数,默认为4次 @@ -28,4 +28,12 @@ */ long retryInterval() default 1000L; + /** + * 重试自定义异常类 + * 在注解上可指定自定义异常类属性,若实际方法抛出的异常是注解指定的异常类的同类或其子类,都会触发重试。 + * + * @return + */ + Class[] retryExceptionClass() default {}; + } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/client/retry/EnableRetryAspect.java b/easyink-wecom/src/main/java/com/easyink/wecom/client/retry/EnableRetryAspect.java index ef9d6704..a9e65099 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/client/retry/EnableRetryAspect.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/client/retry/EnableRetryAspect.java @@ -1,12 +1,17 @@ package com.easyink.wecom.client.retry; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + /** * 类名: 重试切面 (由于forest高版本的重定向存在问题,低版本又不支持重试,所以只能自定义重试接口 ) *

@@ -20,8 +25,13 @@ @Slf4j public class EnableRetryAspect { - @Around("@annotation(retry)") - public Object retryOnCondition(ProceedingJoinPoint joinPoint, EnableRetry retry) throws Throwable { + @Around("@annotation(EnableRetry) || @within(EnableRetry)") + public Object retryOnCondition(ProceedingJoinPoint joinPoint) throws Throwable { + EnableRetry retry = getEnableRetry(joinPoint); + if (retry == null) { + // 未获取到注解,放行原方法 + return joinPoint.proceed(); + } int maxAttempts = retry.maxAttempts(); int attemptCount = 0; long retryInterval = retry.retryInterval(); @@ -34,9 +44,13 @@ public Object retryOnCondition(ProceedingJoinPoint joinPoint, EnableRetry retry) return joinPoint.proceed(); } catch (Throwable exception) { lastException = exception; - log.info("[重试]出现异常,进行重试,请求次数:{},e:{}", attemptCount, ExceptionUtils.getMessage(lastException)); // 自定义重试条件判断 - shouldRetry = attemptCount <= maxAttempts && shouldRetry(exception); + shouldRetry = attemptCount <= maxAttempts && shouldRetry(exception, retry); + if (shouldRetry) { + log.info("[重试]出现异常,进行重试,请求次数:{},e:{}", attemptCount, ExceptionUtils.getMessage(lastException)); + } else { + log.info("[重试]不满足条件,不进行重试,异常原因:{}", ExceptionUtils.getStackTrace(exception)); + } try { Thread.sleep(retryInterval); } catch (InterruptedException e) { @@ -52,11 +66,61 @@ public Object retryOnCondition(ProceedingJoinPoint joinPoint, EnableRetry retry) * 自定义重试条件判断逻辑 * * @param exception - * @return + * @return true 表示需要重试,返回 false 表示不需要重试 */ - private boolean shouldRetry(Throwable exception) { + private boolean shouldRetry(Throwable exception, EnableRetry retry) { // 根据异常类型或其他条件判断是否需要重试 - // 返回 true 表示需要重试,返回 false 表示不需要重试 - return true; + // 获取注解中所有的异常类属性 + Class[] annotationExClassArr = retry.retryExceptionClass(); + // 如果注解中未指定异常类,直接重试 + if (ArrayUtils.isEmpty(annotationExClassArr)) { + return true; + } + // 判断是否匹配注解中的异常,若匹配注解中的异常,则进行重试 + return isMarkException(annotationExClassArr, exception); + } + + + /** + * 从连接点中获取注解信息 + * + * @param joinPoint {@link ProceedingJoinPoint} + * @return {@link EnableRetry} + */ + private EnableRetry getEnableRetry(ProceedingJoinPoint joinPoint) { + // 获取方法签名 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + // 方法上的注解 + EnableRetry methodAnnotation = method.getAnnotation(EnableRetry.class); + EnableRetry classAnnotation = null; + Annotation annotation = signature.getDeclaringType().getAnnotation(EnableRetry.class); + if (annotation != null) { + classAnnotation = (EnableRetry) annotation; + } + // 若方法和类上都有注解,优先使用方法上的注解 + if (methodAnnotation != null && classAnnotation != null) { + return methodAnnotation; + } + return methodAnnotation == null ? classAnnotation : methodAnnotation; + } + + /** + * 判断抛出的异常是否与注解中异常类匹配 + * + * @param annotationExClassArr 注解中的异常数组 + * @param exception {@link Throwable} + * @return true 匹配,false 不匹配 + */ + private boolean isMarkException(Class[] annotationExClassArr, Throwable exception) { + // 获取抛出的异常类属性 + Class exceptionClass = exception.getClass(); + for (Class annotationClass : annotationExClassArr) { + // 只要抛出的异常是注解中异常的同类或是其子类,都算匹配 + if (annotationClass.isAssignableFrom(exceptionClass)) { + return true; + } + } + return false; } } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/domain/WeCustomer.java b/easyink-wecom/src/main/java/com/easyink/wecom/domain/WeCustomer.java index 56e09d01..b6332415 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/domain/WeCustomer.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/domain/WeCustomer.java @@ -1,5 +1,7 @@ package com.easyink.wecom.domain; +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.format.DateTimeFormat; import com.alibaba.fastjson.annotation.JSONField; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; @@ -47,6 +49,7 @@ public class WeCustomer extends BaseEntity { @ApiModelProperty(value = "外部联系人名称") @TableField("name") @Excel(name = "客户",sort = 1) + @ExcelProperty(value = "客户",index = 1) private String name; /** @@ -75,6 +78,8 @@ public class WeCustomer extends BaseEntity { @TableField("birthday") @JsonFormat(pattern = "yyyy-MM-dd") @Excel(name = "出生日期", dateFormat = "yyyy-MM-dd" ,sort = 9) + @ExcelProperty(value = "出生日期",index = 9) + @DateTimeFormat("yyyy-MM-dd") private Date birthday; /** @@ -87,6 +92,7 @@ public class WeCustomer extends BaseEntity { @ApiModelProperty(value = "客户企业简称") @TableField("corp_name") @Excel(name = "公司",sort = 3) + @ExcelProperty(value = "公司",index = 3) private String corpName; @ApiModelProperty(value = "客户企业全称") @@ -134,6 +140,7 @@ public class WeCustomer extends BaseEntity { */ @TableField(exist = false) @Excel(name = "所属员工",sort = 5) + @ExcelProperty(value = "所属员工",index = 5) private String userName; /** @@ -162,12 +169,14 @@ public class WeCustomer extends BaseEntity { */ @TableField(exist = false) @Excel(name = "备注",sort = 2) + @ExcelProperty(value = "备注",index = 2) private String remark; /** * 手机号 */ @TableField(exist = false) @Excel(name = "电话",sort = 10) + @ExcelProperty(value = "电话",index = 10) private String phone; /** * 描述 @@ -183,6 +192,7 @@ public class WeCustomer extends BaseEntity { @ApiModelProperty(value = "部门") @Excel(name = "所属部门", sort = 6) @TableField(exist = false) + @ExcelProperty(value = "所属部门",index = 6) private String departmentName; @ApiModelProperty(value = "跟进人离职时间") diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/domain/WeFlowerCustomerRel.java b/easyink-wecom/src/main/java/com/easyink/wecom/domain/WeFlowerCustomerRel.java index 6a6657eb..525c931e 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/domain/WeFlowerCustomerRel.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/domain/WeFlowerCustomerRel.java @@ -79,11 +79,16 @@ public class WeFlowerCustomerRel { @Excel(name = "添加时间",sort = 7) private Date createTime; - @ApiModelProperty(value = "该成员添加此外部联系人的时间") + @ApiModelProperty(value = "此外部联系人删除成员的时间(流失时间)") @TableField("delete_time") @JsonFormat(pattern = "yyyy-MM-dd") private Date deleteTime; + @ApiModelProperty(value = "此外部联系人被成员删除的时间") + @TableField("del_by_user_time") + @JsonFormat(pattern = "yyyy-mm-dd") + private Date delByUserTime; + @ApiModelProperty(value = "该成员对此客户备注的企业名称 ") @TableField("remark_corp_name") private String remarkCorpName; diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/domain/WeUserBehaviorData.java b/easyink-wecom/src/main/java/com/easyink/wecom/domain/WeUserBehaviorData.java index 25cb9552..cdc289ee 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/domain/WeUserBehaviorData.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/domain/WeUserBehaviorData.java @@ -87,9 +87,14 @@ public class WeUserBehaviorData implements Serializable { @Excel(name = "删除/拉黑成员的客户数,即将成员删除或加入黑名单的客户数") private Integer negativeFeedbackCnt; - @ApiModelProperty(value = "总客户数 ") + @ApiModelProperty(value = "客户总数(由每日定时任务统计,不去重,首页和数据统计共用),【首页】:在职员工在we_flower_customer_rel表中,客户关系status != 2的客户数量 + 系统上记录的已离职的员工在we_flower_customer_rel表中,客户关系status = 3的客户数量。【数据统计】:在职员工在we_flower_customer_rel表中,客户关系status != 2的客户数量。") + @TableField(value = "total_all_contact_cnt") + @Excel(name = "客户总数") + private Integer totalAllContactCnt; + + @ApiModelProperty(value = "留存客户总数,每日定时任务统计,去重") @TableField(value = "total_contact_cnt") - @Excel(name = "总客户数") + @Excel(name = "留存客户总数") private Integer totalContactCnt ; @ApiModelProperty(value = "今日新客流失数") diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/domain/dto/customer/resp/GetByUserResp.java b/easyink-wecom/src/main/java/com/easyink/wecom/domain/dto/customer/resp/GetByUserResp.java index 84aea8f2..d57dbbb1 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/domain/dto/customer/resp/GetByUserResp.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/domain/dto/customer/resp/GetByUserResp.java @@ -121,27 +121,7 @@ public List getCustomerTagRelList(List } - */ - public void activateDelCustomer(List localRelList) { - if (CollectionUtils.isEmpty(localRelList) || CollectionUtils.isEmpty(relList)) { - return; - } - Map statusMap = localRelList.stream().collect(Collectors.toMap(rel -> rel, WeFlowerCustomerRel::getStatus)); - for (WeFlowerCustomerRel rel : relList) { - if (statusMap.containsKey(rel)) { - String status = statusMap.get(rel); - if (status == null) { - continue; - } - // 如果之前就存在该客户关系 ,则把流失/删除状态重置为正常 -// rel.setStatus(CustomerStatusEnum.isDel(Integer.valueOf(status)) ? CustomerStatusEnum.NORMAL.getCode().toString() : status); - } - } - } + } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/domain/dto/statistics/CustomerOverviewDTO.java b/easyink-wecom/src/main/java/com/easyink/wecom/domain/dto/statistics/CustomerOverviewDTO.java index 75b2ef9f..fd83b4f3 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/domain/dto/statistics/CustomerOverviewDTO.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/domain/dto/statistics/CustomerOverviewDTO.java @@ -1,5 +1,7 @@ package com.easyink.wecom.domain.dto.statistics; +import com.easyink.common.constant.GenConstants; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; /** @@ -11,6 +13,22 @@ @Data public class CustomerOverviewDTO extends StatisticsDTO{ + /** + * 获取客户总数排序方式 + * + * @return + */ + public String getTotalAllContactCntSort() { + if(totalAllContactCntSort == null) { + return null; + } + if (GenConstants.ASC.equalsIgnoreCase(totalAllContactCntSort)) { + return GenConstants.ASC; + }else { + return GenConstants.DESC; + } + } + public String getTotalContactCntSort() { if(totalContactCntSort == null) { return null; @@ -77,9 +95,10 @@ public String getServiceResponseRateSort() { } } - /** - * 客户总数排序 正序asc 倒叙desc - */ + @ApiModelProperty("客户总数排序标识符,根据total_all_contact_cnt字段排序,正序asc,倒序desc") + private String totalAllContactCntSort; + + @ApiModelProperty("留存客户总数排序标识符,根据total_contact_cnt字段排序,正序asc,倒序desc") private String totalContactCntSort; /** diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/domain/enums/statistics/StatisticsEnum.java b/easyink-wecom/src/main/java/com/easyink/wecom/domain/enums/statistics/StatisticsEnum.java index 39bed5f6..ac737725 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/domain/enums/statistics/StatisticsEnum.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/domain/enums/statistics/StatisticsEnum.java @@ -31,9 +31,14 @@ public class StatisticsEnum { * @date 2023/4/20 18:31 */ public enum CustomerOverviewSortTypeEnum { + /** * 客户总数排序 */ + TOTAL_ALL_CONTACT_CNT_SORT("totalAllContactCntSort", Comparator.comparing(CustomerOverviewDateVO::getTotalAllContactCnt)), + /** + * 留存客户总数排序 + */ TOTAL_CONTACT_CNT_SORT("totalContactCntSort", Comparator.comparing(CustomerOverviewDateVO::getTotalContactCnt)), /** * 流失客户数排序 diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/WeCustomerExportVO.java b/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/WeCustomerExportVO.java index d20df7cf..82be1222 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/WeCustomerExportVO.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/WeCustomerExportVO.java @@ -1,6 +1,12 @@ package com.easyink.wecom.domain.vo; import cn.hutool.core.collection.CollUtil; +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.format.DateTimeFormat; +import com.alibaba.excel.annotation.write.style.ColumnWidth; +import com.alibaba.excel.annotation.write.style.ContentRowHeight; +import com.alibaba.excel.annotation.write.style.HeadRowHeight; +import com.alibaba.excel.annotation.write.style.HeadStyle; import com.baomidou.mybatisplus.annotation.TableField; import com.easyink.common.annotation.Excel; import com.easyink.common.utils.StringUtils; @@ -34,45 +40,57 @@ @Slf4j @NoArgsConstructor @AllArgsConstructor +@ContentRowHeight(10) +@HeadRowHeight(20) +@ColumnWidth(35) public class WeCustomerExportVO extends WeCustomer { @ApiModelProperty(value = "添加方式,0=未知来源,1=扫描二维码,2=搜索手机号,3=名片分享,4=群聊,5=手机通讯录,6=微信联系人,7=来自微信的添加好友申请,8=安装第三方应用时自动添加的客服人员,9=搜索邮箱,16=通过获客链接添加,201=内部成员共享,202=管理员负责人分配") @TableField(exist = false) - @Excel(name = "来源", sort = 3, readConverterExp = "0=未知来源,1=扫描二维码,2=搜索手机号,3=名片分享,4=群聊,5=手机通讯录,6=微信联系人,7=来自微信的添加好友申请,8=安装第三方应用时自动添加的客服人员,9=搜索邮箱,16=通过获客链接添加,201=内部成员共享,202=管理员负责人分配") + @Excel(name = "来源", sort = 13, readConverterExp = "0=未知来源,1=扫描二维码,2=搜索手机号,3=名片分享,4=群聊,5=手机通讯录,6=微信联系人,7=来自微信的添加好友申请,8=安装第三方应用时自动添加的客服人员,9=搜索邮箱,16=通过获客链接添加,201=内部成员共享,202=管理员负责人分配") + @ExcelProperty(value = "来源", index = 13) private String addWay; @ApiModelProperty(value = "添加时间") @TableField(exist = false) @Excel(name = "添加时间", sort = 4, dateFormat = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd") + @ExcelProperty(value = "添加时间", index = 4) + @DateTimeFormat("yyyy-MM-dd") private Date createTime; @ApiModelProperty(value = "该成员对此外部联系人的描述") @TableField(exist = false) - @Excel(name = "描述", sort = 13) + @Excel(name = "描述", sort = 14) + @ExcelProperty(value = "描述", index = 14) private String description; @ApiModelProperty(value = "标签") @TableField(exist = false) @Excel(name = "标签", sort = 7) + @ExcelProperty(value = "标签", index = 7) private String tags; @TableField(exist = false) @Excel(name = "客户状态" , sort = 8 ,readConverterExp = "0=正常,1=已流失,2=已流失,3=待继承,4=转接中" , defaultValue = "待继承") + @ExcelProperty(value = "客户状态", index = 8) private String customerStatus; @TableField(exist = false) @Excel(name = "邮箱", sort = 11) + @ExcelProperty(value = "邮箱", index = 11) private String email; @Excel(name = "地址", sort = 12) + @ExcelProperty(value = "地址", index = 12) private String address; /** * 扩展属性与值的映射,K:扩展属性名字,V该客户对应的值 */ + @ExcelProperty private Map extendPropMapper; public WeCustomerExportVO(WeCustomerVO weCustomer) { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/customer/CustomerExportResultVO.java b/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/customer/CustomerExportResultVO.java new file mode 100644 index 00000000..6a7199d0 --- /dev/null +++ b/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/customer/CustomerExportResultVO.java @@ -0,0 +1,19 @@ +package com.easyink.wecom.domain.vo.customer; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * 类名: 客户导出结果VO + * + * @author : silver_chariot + * @date : 2023/9/21 15:16 + **/ +@Data +@AllArgsConstructor +public class CustomerExportResultVO { + /** + * 是否已完成 + */ + private Boolean hasFinished ; +} diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/customer/ExportOprVO.java b/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/customer/ExportOprVO.java new file mode 100644 index 00000000..11e6e736 --- /dev/null +++ b/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/customer/ExportOprVO.java @@ -0,0 +1,30 @@ +package com.easyink.wecom.domain.vo.customer; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 类名: 导出操作VO + * + * @author : silver_chariot + * @date : 2023/9/20 9:33 + **/ +@Data +@Builder +@AllArgsConstructor +public class ExportOprVO { + /** + * 操作id + */ + private String oprId; + /** + * 文件名 + */ + private String fileName; + /** + * 是否已完成 + */ + private Boolean hasFinished; +} diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/statistics/CustomerOverviewDateVO.java b/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/statistics/CustomerOverviewDateVO.java index 6768b6ca..23d3dea6 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/statistics/CustomerOverviewDateVO.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/statistics/CustomerOverviewDateVO.java @@ -3,6 +3,7 @@ import com.easyink.common.annotation.Excel; import com.easyink.common.constant.Constants; import com.easyink.common.constant.GenConstants; +import com.easyink.common.constant.WeConstans; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.NoArgsConstructor; @@ -26,22 +27,24 @@ public class CustomerOverviewDateVO { @Excel(name = "日期", sort = 1) private String xTime; - /** - * 客户总数 - */ + @ApiModelProperty("客户总数(由每日定时任务统计,不去重,首页和数据统计共用),因不存在值时要显示'-',故使用String类型,用于前端展示,【首页】:在职员工在we_flower_customer_rel表中,客户关系status != 2的客户数量 + 系统上记录的已离职的员工在we_flower_customer_rel表中,客户关系status = 3的客户数量。【数据统计】:在职员工在we_flower_customer_rel表中,客户关系status != 2的客户数量。") @Excel(name = "客户总数", sort = 2) + private Integer totalAllContactCnt; + + @ApiModelProperty("留存客户总数") + @Excel(name = "留存客户总数", sort = 3) private Integer totalContactCnt; /** * 新增客户数,成员新添加的客户数量 */ - @Excel(name = "新增客户数", sort = 4) + @Excel(name = "新增客户数", sort = 5) private Integer newContactCnt; /** * 当天加入的新客流失数量 , 因为官方没有返回由系统自行统计 */ - @Excel(name = "流失客户数", sort = 3) + @Excel(name = "流失客户数", sort = 4) private Integer contactLossCnt; /** @@ -52,7 +55,7 @@ public class CustomerOverviewDateVO { /** * 新客留存率 */ - @Excel(name = "新客留存率", sort = 5) + @Excel(name = "新客留存率", sort = 6) private String newContactRetentionRate; /** @@ -63,7 +66,7 @@ public class CustomerOverviewDateVO { /** * 新客开口率 */ - @Excel(name = "新客开口率", sort = 6) + @Excel(name = "新客开口率", sort = 7) private String newContactStartTalkRate; /** @@ -74,7 +77,7 @@ public class CustomerOverviewDateVO { /** * 服务响应率 */ - @Excel(name = "服务响应率", sort = 7) + @Excel(name = "服务响应率", sort = 8) private String serviceResponseRate; /** @@ -201,7 +204,8 @@ public CustomerOverviewDateVO(String xTime) { this.newContactRetentionRateBySort = BigDecimal.valueOf(0); this.newContactStartTalkRateBySort = BigDecimal.valueOf(0); this.serviceResponseRateBySort = BigDecimal.valueOf(0); - this.userActiveChatCnt=0; + this.userActiveChatCnt = 0; + this.totalAllContactCnt = 0; } /** @@ -216,7 +220,7 @@ public CustomerOverviewDateVO(String xTime) { * @param repliedWithinThirtyMinCustomerCnt 当天员工首次给客户发消息,客户在30分钟内回复的客户数 * @param userActiveChatCnt 员工主动发起会话数 */ - public void handleAddData(Integer allChatCnt, Integer contactTotalCnt, Integer negativeFeedbackCnt, Integer newCustomerLossCnt, Integer newContactCnt, Integer newContactSpeakCnt, Integer repliedWithinThirtyMinCustomerCnt,Integer userActiveChatCnt){ + public void handleAddData(Integer allChatCnt, Integer contactTotalCnt, Integer negativeFeedbackCnt, Integer newCustomerLossCnt, Integer newContactCnt, Integer newContactSpeakCnt, Integer repliedWithinThirtyMinCustomerCnt, Integer userActiveChatCnt, Integer totalAllContactCnt) { this.allChatCnt += allChatCnt; this.totalContactCnt += contactTotalCnt; this.contactLossCnt += negativeFeedbackCnt; @@ -228,5 +232,6 @@ public void handleAddData(Integer allChatCnt, Integer contactTotalCnt, Integer n this.newContactRetentionRate = getNewContactRetentionRate(); this.newContactStartTalkRate = getNewContactStartTalkRate(); this.serviceResponseRate = getServiceResponseRate(); + this.totalAllContactCnt += totalAllContactCnt; } } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/statistics/CustomerOverviewVO.java b/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/statistics/CustomerOverviewVO.java index 80f64d6b..c1b7edc7 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/statistics/CustomerOverviewVO.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/domain/vo/statistics/CustomerOverviewVO.java @@ -3,6 +3,7 @@ import com.easyink.common.annotation.Excel; import com.easyink.common.constant.Constants; import com.easyink.common.constant.GenConstants; +import com.easyink.common.constant.WeConstans; import com.easyink.wecom.domain.vo.UserBaseVO; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @@ -25,22 +26,26 @@ public class CustomerOverviewVO extends UserBaseVO { */ private String xTime; + @ApiModelProperty("客户总数(由每日定时任务统计,不去重,首页和数据统计共用),【首页】:在职员工在we_flower_customer_rel表中,客户关系status != 2的客户数量 + 系统上记录的已离职的员工在we_flower_customer_rel表中,客户关系status = 3的客户数量。【数据统计】:在职员工在we_flower_customer_rel表中,客户关系status != 2的客户数量。") + @Excel(name = "客户总数", sort = 3) + private Integer totalAllContactCnt; + /** * 客户总数 */ - @Excel(name = "客户总数", sort = 3) + @Excel(name = "留存客户总数", sort = 4) private Integer totalContactCnt; /** * 新增客户数,成员新添加的客户数量 */ - @Excel(name = "新增客户数", sort = 4) + @Excel(name = "新增客户数", sort = 5) private Integer newContactCnt; /** * 当天加入的新客流失数量 , 因为官方没有返回由系统自行统计 */ - @Excel(name = "流失客户数", sort = 5) + @Excel(name = "流失客户数", sort = 6) private Integer contactLossCnt; /** @@ -51,19 +56,19 @@ public class CustomerOverviewVO extends UserBaseVO { /** * 新客留存率 */ - @Excel(name = "新客留存率", sort = 6) + @Excel(name = "新客留存率", sort = 7) private String newContactRetentionRate; /** * 新客开口率 */ - @Excel(name = "新客开口率", sort = 7) + @Excel(name = "新客开口率", sort = 8) private String newContactStartTalkRate; /** * 服务响应率 */ - @Excel(name = "服务响应率", sort = 8) + @Excel(name = "服务响应率", sort = 9) private String serviceResponseRate; /** diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/factory/impl/customer/WeCallBackDelExternalContactImpl.java b/easyink-wecom/src/main/java/com/easyink/wecom/factory/impl/customer/WeCallBackDelExternalContactImpl.java index b2611fbf..c772cefa 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/factory/impl/customer/WeCallBackDelExternalContactImpl.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/factory/impl/customer/WeCallBackDelExternalContactImpl.java @@ -58,7 +58,7 @@ public void eventHandle(WxCpXmlMessageVO message) { weCustomerTransferRecordService.handleTransferSuccess(message.getToUserName(), message.getUserId(), message.getExternalUserId()); } if (message.getExternalUserId() != null && message.getUserId() != null) { - weFlowerCustomerRelService.deleteFollowUser(message.getUserId(), message.getExternalUserId(), Constants.DELETE_CODE, message.getToUserName()); + weFlowerCustomerRelService.deleteCustomer(message.getUserId(), message.getExternalUserId(), Constants.DELETE_CODE, message.getToUserName()); // 如果是在职继承的回调,不记录敏感记录,只更新客户状态 if (DELETE_BY_TRANSFER.equals(message.getSource())) { return; diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/handler/ExtendPropHolder.java b/easyink-wecom/src/main/java/com/easyink/wecom/handler/ExtendPropHolder.java new file mode 100644 index 00000000..a59249ce --- /dev/null +++ b/easyink-wecom/src/main/java/com/easyink/wecom/handler/ExtendPropHolder.java @@ -0,0 +1,82 @@ +package com.easyink.wecom.handler; + +import cn.hutool.core.collection.ListUtil; +import com.easyink.common.constant.UserConstants; +import com.easyink.common.utils.spring.SpringUtils; +import com.easyink.wecom.domain.entity.customer.ExtendPropertyMultipleOption; +import com.easyink.wecom.domain.entity.customer.WeCustomerExtendProperty; +import com.easyink.wecom.service.ExtendPropertyMultipleOptionService; +import com.easyink.wecom.service.WeCustomerExtendPropertyService; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 类名: 扩展字段属性容器 + * + * @author : silver_chariot + * @date : 2023/9/22 11:25 + **/ +public class ExtendPropHolder { + private Map extendPropMap; + private Map optionMap; + /** + * 判断需要导出的字段里是否有扩展字段 + */ + private boolean hasExtendProp; + + public ExtendPropHolder(String corpId, List selectProperties) { + init(corpId , selectProperties); + } + + /** + * 初始化 + * + * @param corpId 企业ID + */ + public void init(String corpId ,List selectedProperties) { + if(CollectionUtils.isEmpty(selectedProperties)) { + return; + } + WeCustomerExtendPropertyService wecustomerExtendPropertyService = SpringUtils.getBean(WeCustomerExtendPropertyService.class); + ExtendPropertyMultipleOptionService extendPropertyMultipleOptionService = SpringUtils.getBean(ExtendPropertyMultipleOptionService.class) ; + // 过滤系统默认字段 + List propList = selectedProperties.stream().filter(a -> !ListUtil.toList(UserConstants.getSysDefaultProperties()).contains(a)).collect(Collectors.toList()); + if(CollectionUtils.isEmpty(propList) ) { + hasExtendProp = false ; + return; + }else { + hasExtendProp =true ; + } + // 查询该企业所有的扩展属性详情 + List extendPropList = wecustomerExtendPropertyService.getList( + WeCustomerExtendProperty.builder() + .corpId(corpId) + .build() + ); + if (CollectionUtils.isEmpty(extendPropList)) { + return; + } + // 扩展属性ID->详情的映射 + extendPropMap = extendPropList.stream() + .collect(Collectors.toMap(WeCustomerExtendProperty::getId, prop -> prop)); + // 多选值ID-> 多选值的映射 + optionMap = extendPropertyMultipleOptionService.getMapByProp(extendPropList); + } + + public Map getExtendPropMap() { + return extendPropMap; + } + + public Map getOptionMap() { + return optionMap; + } + + public boolean isHasExtendProp() { + return hasExtendProp; + } +} diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/interceptor/WeAccessTokenInterceptor.java b/easyink-wecom/src/main/java/com/easyink/wecom/interceptor/WeAccessTokenInterceptor.java index 4ade1abb..22b1e3dd 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/interceptor/WeAccessTokenInterceptor.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/interceptor/WeAccessTokenInterceptor.java @@ -10,6 +10,7 @@ import com.dtflys.forest.utils.ForestDataType; import com.easyink.common.config.WeComeConfig; import com.easyink.common.constant.WeConstans; +import com.easyink.common.exception.RetryException; import com.easyink.common.utils.StringUtils; import com.easyink.common.utils.spring.SpringUtils; import com.easyink.wecom.domain.dto.WeResultDTO; @@ -122,17 +123,40 @@ public void onError(ForestRuntimeException e, ForestRequest forestRequest, Fores */ @Override public void onSuccess(Object o, ForestRequest forestRequest, ForestResponse forestResponse) { - log.info("url:【{}】,result:【{}】", forestRequest.getUrl(), forestResponse.getContent()); + log.info("url:【{}】,result:【{}】", forestRequest.getUrl(), forestResponse.getContent()); WeResultDTO weResultDto = JSONUtil.toBean(forestResponse.getContent(), WeResultDTO.class); + // 匹配需要判断的code,抛出指定异常 + if (needRetry(weResultDto.getErrcode())) { + throw new RetryException(forestResponse.getContent()); + } // 部分uri 错误码需要单独业务处理不抛出异常 String uri = forestRequest.getUrl().replace(urlPrefix, ""); if (PatternMatchUtils.simpleMatch(weComeConfig.getNeedErrcodeUrl(), uri)) { return; } + // 其他情况抛出异常 if (null != weResultDto.getErrcode() && !WeConstans.WE_SUCCESS_CODE.equals(weResultDto.getErrcode()) && !WeConstans.NOT_EXIST_CONTACT.equals(weResultDto.getErrcode())) { throw new ForestRuntimeException(forestResponse.getContent()); } + } + /** + * 是否需要抛出特定异常重试 + * + * @param code 响应的code + * @return true 是,false 否 + */ + private boolean needRetry(Integer code) { + if (code == null) { + return false; + } + Integer[] codes = weComeConfig.getNeedRetryCode(); + for (Integer integer : codes) { + if (integer.equals(code)) { + return true; + } + } + return false; } /** diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeCustomerMapper.java b/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeCustomerMapper.java index f96689ef..f375448d 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeCustomerMapper.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeCustomerMapper.java @@ -6,6 +6,7 @@ import com.easyink.wecom.domain.WeCustomerPortrait; import com.easyink.wecom.domain.WeCustomerSocialConn; import com.easyink.wecom.domain.dto.WeCustomerPushMessageDTO; +import com.easyink.wecom.domain.entity.WeCustomerExportDTO; import com.easyink.wecom.domain.vo.WeCustomerNameAndUserIdVO; import com.easyink.wecom.domain.vo.customer.WeCustomerUserListVO; import com.easyink.wecom.domain.vo.customer.WeCustomerVO; @@ -228,8 +229,17 @@ public interface WeCustomerMapper extends BaseMapper { /** * 查询导出的客户 + * * @param weCustomer 查询条件 * @return 客户 */ List selectExportCustomer(WeCustomer weCustomer); + + /** + * 查询客户总数 + * + * @param dto {@link WeCustomerExportDTO} + * @return 客户总数 + */ + Integer selectWeCustomerCount(WeCustomer dto); } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeCustomerMessgaeResultMapper.java b/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeCustomerMessgaeResultMapper.java index ac189e4f..3910f157 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeCustomerMessgaeResultMapper.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeCustomerMessgaeResultMapper.java @@ -43,6 +43,14 @@ int updateWeCustomerMessgaeResult(@Param("messageId") Long messageId, @Param("ch */ List customerMessagePushs(WeCustomerMessagePushResultDTO weCustomerMessagePushResultDTO); + /** + * 查询微信消息发送的客户情况(未执行) + * + * @param weCustomerMessagePushResultDTO + * @return + */ + List messagePushsByCustomer(WeCustomerMessagePushResultDTO weCustomerMessagePushResultDTO); + /** * 查询微信消息发送情况(已执行) * diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeFlowerCustomerRelMapper.java b/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeFlowerCustomerRelMapper.java index 4643ca24..7613e86a 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeFlowerCustomerRelMapper.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeFlowerCustomerRelMapper.java @@ -199,4 +199,37 @@ WeUserBehaviorData getTotalContactAndLossCnt(@Param("userId") String userId, * @return 各个渠道对应的未流失的客户数(有效客户数) */ List getChannelDateRelEffectCnt(@Param("stateList") List stateList, @Param("userIds") String userIds, @Param("corpId") String corpId, @Param("beginTime") String beginTime, @Param("endTime") String endTime); + + + /** + * 根据结束时间和已激活(is_active = 1)员工ID列表, 获取首页-数据总览-客户总数 + * + * @param corpId 企业ID + * @param beginTime 开始时间,格式为YYYY-MM-DD 00:00:00 + * @param endTime 结束时间,格式为YYYY-MM-DD 23:59:59 + * @param normalUserIdList 过滤员工id列表 + * @return 已激活员工对应的首页-数据总览-客户总数(待继承客户数) + */ + Integer getNormalTotalAllContactCnt(@Param("corpId") String corpId, @Param("beginTime") String beginTime, @Param("endTime") String endTime, @Param("normalUserIdList") List normalUserIdList); + + /** + * 根据结束时间和已离职员工(is_active = 6)ID列表, 获取首页-数据总览-客户总数 + * + * @param corpId 企业ID + * @param beginTime 开始时间,格式为YYYY-MM-DD 00:00:00 + * @param endTime 结束时间,格式为YYYY-MM-DD 23:59:59 + * @param delUserIdList 过滤员工id列表 + * @return 已离职员工对应的首页-数据总览-客户总数(待继承客户数) + */ + Integer getDelTotalAllContactCnt(@Param("corpId") String corpId, @Param("beginTime") String beginTime, @Param("endTime") String endTime, @Param("delUserIdList") List delUserIdList); + + /** + * 根据员工id获取数据统计-客户联系-客户总数 + * + * @param corpId 企业ID + * @param endTime 结束时间,格式为YYYY-MM-DD 23:59:59 + * @param userId 员工ID + * @return 员工对应的客户总数 + */ + List getTotalAllContactCntByUserId(@Param("corpId") String corpId, @Param("endTime") String endTime, @Param("userIdList") List userId); } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeUserBehaviorDataMapper.java b/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeUserBehaviorDataMapper.java index 4eedbd31..4b3e84d5 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeUserBehaviorDataMapper.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/mapper/WeUserBehaviorDataMapper.java @@ -95,4 +95,12 @@ public interface WeUserBehaviorDataMapper extends BaseMapper * @return 原始数据列表 */ List getCustomerOverViewOfDate(CustomerOverviewDTO dto); + + /** + * 批量更新客户总数值 + * + * @param weUserBehaviorDataList {@link WeUserBehaviorData} + * @return 结果 + */ + Integer saveBatchUpdateOrInsert(@Param("list") List weUserBehaviorDataList); } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/WeConversationArchiveService.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/WeConversationArchiveService.java index a2969621..8fc266a2 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/WeConversationArchiveService.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/WeConversationArchiveService.java @@ -2,11 +2,12 @@ import com.alibaba.fastjson.JSONObject; import com.easyink.common.core.domain.ConversationArchiveQuery; +import com.easyink.common.core.domain.ConversationArchiveViewContextDTO; import com.easyink.common.core.domain.model.LoginUser; +import com.easyink.common.core.page.TableDataInfo; import com.easyink.wecom.domain.vo.ConversationArchiveVO; import com.github.pagehelper.PageInfo; -import java.util.List; /** * @author admin @@ -62,5 +63,13 @@ public interface WeConversationArchiveService { * @return ConversationArchiveVO */ PageInfo getChatList(ConversationArchiveQuery query, Integer pageNum, Integer pageSize); + + /** + * 获取指定聊天内容的上下文信息 + * + * @param dto {@link ConversationArchiveViewContextDTO} + * @return {@link PageInfo} + */ + TableDataInfo viewContext(ConversationArchiveViewContextDTO dto); } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/WeCustomerExtendPropertyService.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/WeCustomerExtendPropertyService.java index 86f41ec7..ed353035 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/WeCustomerExtendPropertyService.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/WeCustomerExtendPropertyService.java @@ -8,6 +8,7 @@ import com.easyink.wecom.domain.entity.customer.WeCustomerExtendProperty; import com.easyink.wecom.domain.vo.WeCustomerExportVO; import com.easyink.wecom.domain.vo.customer.WeCustomerVO; +import com.easyink.wecom.handler.ExtendPropHolder; import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -124,8 +125,9 @@ public interface WeCustomerExtendPropertyService extends IService} 需要导出的客户集合 * @param selectedProperties 选择的(需要导出)字段名 + * @param extendPropHolder */ - void setKeyValueMapper(String corpId, List exportCustomerList, List selectedProperties); + void setKeyValueMapper(String corpId, List exportCustomerList, List selectedProperties, ExtendPropHolder extendPropHolder); /** * 根据extendProperties 和 已有的自定义属性和多选值映射,获取客户的 自定义字段名称->所有值用,隔开 的映射 diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/WeCustomerService.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/WeCustomerService.java index 2c86deb1..5a9f669d 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/WeCustomerService.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/WeCustomerService.java @@ -233,6 +233,7 @@ public interface WeCustomerService extends IService { * @param 导出类型 * @return */ + @Deprecated AjaxResult export(WeCustomerExportDTO dto); /** @@ -351,9 +352,29 @@ public interface WeCustomerService extends IService { /** * 通过明文获取密文外部联系人exUserId * - * @param corpId 企业id - * @param externalUserId 外部联系人exUserId + * @param corpId 企业id + * @param externalUserId 外部联系人exUserId * @return */ String getOpenExUserId(String corpId, String externalUserId); + + /** + * 生活需要导出的客户信息 + * + * @param dto 导出客户请求 + * @param oprId 操作id + * @param fileName 导出的文件名 + * @return fileName 导出的文件名 + */ + void genExportData(WeCustomerExportDTO dto, String oprId, String fileName); + + /** + * 获取导出客户的结果 + * + * @param oprId 操作id + * @return true or false + */ + Boolean getExportResult(String oprId); + + WeCustomerExportDTO transferData(WeCustomerExportDTO dto); } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/WeFlowerCustomerRelService.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/WeFlowerCustomerRelService.java index 14dd63c5..a88cf4fd 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/WeFlowerCustomerRelService.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/WeFlowerCustomerRelService.java @@ -21,9 +21,21 @@ public interface WeFlowerCustomerRelService extends IService localRelList); /** * 分批批量插入 @@ -147,4 +160,20 @@ public interface WeFlowerCustomerRelService extends IService userIds); + + /** + * 根据开始和结束时间,获取首页-数据总览-客户总数 + *

说明: 在使用EasyInk系统前,已离职的员工的客户数不会参与统计

+ * + * @param corpId 企业ID + * @param beginTime 开始时间,格式为YYYY-MM-DD 00:00:00 + * @param endTime 结束时间,格式为YYYY-MM-DD 23:59:59 + * @return 首页-数据总览-客户总数 + */ + Integer getTotalAllContactCnt(String corpId, String beginTime, String endTime); + + /** + * 数据统计-联系客户-客户总数-旧数据兼容 + */ + void updateTotalAllCustomerCnt(); } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/WeFlowerCustomerTagRelService.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/WeFlowerCustomerTagRelService.java index 947f14e2..7d24e7a6 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/WeFlowerCustomerTagRelService.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/WeFlowerCustomerTagRelService.java @@ -124,8 +124,17 @@ public interface WeFlowerCustomerTagRelService extends IService tagRelList); + + /** + * 同步远端的标签信息到本地 + * + * @param tagRelList 标签关系列表 + * @param relIds 本地客户-员工关系列表 + */ + void syncLocalTagFromRemote(List tagRelList, List relIds); } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/PageHomeServiceImpl.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/PageHomeServiceImpl.java index 6ff4f156..e62f2a0b 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/PageHomeServiceImpl.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/PageHomeServiceImpl.java @@ -93,14 +93,17 @@ public void getCorpBasicData(String corpId) { int userCount = weUserService.count(new LambdaQueryWrapper() .eq(WeUser::getCorpId, corpId) .eq(WeUser::getIsActivate, WeConstans.WE_USER_IS_ACTIVATE)); - //客户总人数 + // 留存客户总人数 int customerCount = weCustomerService.customerCount(corpId); + // 统计客户总数 + Integer totalAllContactCnt = weFlowerCustomerRelService.getTotalAllContactCnt(corpId, DateUtils.parseBeginDay(DateUtils.dateTime(new Date())), DateUtils.parseEndDay(DateUtils.dateTime(new Date()))); //客户群总数( 1.21.0 改成读取官方统计接口里的数据 ,) int groupCount = weGroupService.count(new LambdaQueryWrapper().eq(WeGroup::getCorpId, corpId) .in(WeGroup :: getStatus , Lists.newArrayList(GroupConstants.NARMAL,GroupConstants.OWNER_LEAVE_EXTEND_SUCCESS))); //群成员总数 int groupMemberCount = weGroupStatisticService.getGroupMemberCnt(corpId, DateUtil.yesterday()); totalMap.put("userCount", userCount); + totalMap.put("totalAllContactCnt", totalAllContactCnt == 0 ? Constants.EMPTY_RETAIN_RATE_VALUE : totalAllContactCnt); totalMap.put("customerCount", customerCount); totalMap.put("groupCount", groupCount); totalMap.put("groupMemberCount", groupMemberCount); @@ -484,8 +487,10 @@ public void doSystemCustomStat(String corpId, boolean isToday, String time) { // 转换为YY:MM:DD HH:MM:SS格式 Date beginTime = DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, time + DateUtils.BEGIN_TIME_SUFFIX); Date endTime = DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, time + DateUtils.END_TIME_SUFFIX); - // 统计客户总数 + // 统计留存客户总数 Tower 任务: 增加客户总数统计数 ( https://tower.im/teams/636204/todos/72041 ) Integer totalContactCnt = weCustomerMapper.countCustomerNumByTime(corpId, DateUtils.parseEndDay(time)); + // 统计客户总数 + Integer totalAllContactCnt = weFlowerCustomerRelService.getTotalAllContactCnt(corpId, DateUtils.getDateTime(beginTime), DateUtils.getDateTime(endTime)); // 今日流失数 Integer todayLossCnt = weFlowerCustomerRelService.count(new LambdaQueryWrapper() .eq(WeFlowerCustomerRel::getCorpId, corpId) @@ -494,6 +499,7 @@ public void doSystemCustomStat(String corpId, boolean isToday, String time) { .eq(WeFlowerCustomerRel::getStatus, CustomerStatusEnum.DRAIN.getCode() .toString())); WeUserBehaviorData userBehaviorData = new WeUserBehaviorData(); + userBehaviorData.setTotalAllContactCnt(totalAllContactCnt); userBehaviorData.setTotalContactCnt(totalContactCnt); userBehaviorData.setNewContactLossCnt(todayLossCnt); userBehaviorData.setCorpId(corpId); diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeConversationArchiveServiceImpl.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeConversationArchiveServiceImpl.java index 83f7424a..87a35f27 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeConversationArchiveServiceImpl.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeConversationArchiveServiceImpl.java @@ -8,7 +8,9 @@ import com.easyink.common.constant.GenConstants; import com.easyink.common.constant.GroupConstants; import com.easyink.common.constant.WeConstans; +import com.easyink.common.constant.conversation.ConversationArchiveConstants; import com.easyink.common.core.domain.ConversationArchiveQuery; +import com.easyink.common.core.domain.ConversationArchiveViewContextDTO; import com.easyink.common.core.domain.conversation.ChatInfoVO; import com.easyink.common.core.domain.conversation.msgtype.MsgTypeEnum; import com.easyink.common.core.domain.conversation.msgtype.RevokeVO; @@ -16,8 +18,10 @@ import com.easyink.common.core.domain.wecom.WeUser; import com.easyink.common.core.elasticsearch.ElasticSearch; import com.easyink.common.core.page.PageDomain; +import com.easyink.common.core.page.TableDataInfo; import com.easyink.common.core.page.TableSupport; import com.easyink.common.utils.DateUtils; +import com.easyink.common.utils.PageInfoUtil; import com.easyink.common.utils.StringUtils; import com.easyink.wecom.domain.WeCustomer; import com.easyink.wecom.domain.WeGroup; @@ -37,10 +41,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; /** @@ -280,8 +281,21 @@ public PageInfo getChatAllList(ConversationArchiveQuery q //关键词查询并高亮显示 if (StringUtils.isNotEmpty(query.getKeyWord())) { - boolQueryBuilder.must(QueryBuilders.boolQuery().must(QueryBuilders.wildcardQuery("text.content.keyword", "*" + query.getKeyWord() + "*"))); - builder.highlighter(new HighlightBuilder().field("text.content")); + // 查询为全部状态查询 + if (StringUtils.isBlank(query.getAction())) { + // 将查询条件改为全部查询 + builder.from(ConversationArchiveConstants.SEARCH_FROM); + builder.size(ConversationArchiveConstants.SEARCH_SIZE); + } + // 撤回类型消息特殊处理 + if (ConversationArchiveConstants.ACTION_RECALL.equals(query.getAction())) { + // 将查询条件改为全部查询 + builder.from(ConversationArchiveConstants.SEARCH_FROM); + builder.size(ConversationArchiveConstants.SEARCH_SIZE); + } else { + boolQueryBuilder.must(QueryBuilders.boolQuery().must(QueryBuilders.wildcardQuery("text.content.keyword", "*" + query.getKeyWord() + "*"))); + builder.highlighter(new HighlightBuilder().field("text.content")); + } } //时间范围查询 @@ -300,7 +314,7 @@ public PageInfo getChatAllList(ConversationArchiveQuery q boolQueryBuilder.filter(queryBuilder.minimumShouldMatch(1)); builder.query(boolQueryBuilder); PageInfo pageInfo = elasticSearch.searchPage(WeConstans.getChatDataIndex(query.getCorpId()), builder, pageNum, pageSize, ConversationArchiveVO.class); - filterData(pageInfo, query.getCorpId()); + filterData(pageInfo, query.getCorpId(), query.getKeyWord(), query.getAction()); List list = pageInfo.getList(); List conversationArchiveVOList = new ArrayList<>(); for (ConversationArchiveVO conversationArchiveVO : list) { @@ -370,6 +384,155 @@ public PageInfo getChatList(ConversationArchiveQuery quer return elasticSearch.searchPage(WeConstans.getChatDataIndex(query.getCorpId()), builder, pageNum, pageSize, ConversationArchiveVO.class); } + /** + * 获取指定聊天内容的上下文信息 + * + * @param dto {@link ConversationArchiveViewContextDTO} + * @return {@link PageInfo} + */ + @Override + public TableDataInfo viewContext(ConversationArchiveViewContextDTO dto) { + if (dto == null || StringUtils.isBlank(dto.getCorpId())) { + return null; + } + // 消息id + String msgId = dto.getMsgId(); + // ES索引 + String index = WeConstans.getChatDataIndex(dto.getCorpId()); + // 发送者id + String fromId = dto.getFromId(); + // 接收者id + String receiveId = dto.getReceiveId(); + // 群聊id + String roomId = dto.getRoomId(); + // 查询条件 + SearchSourceBuilder builder = new SearchSourceBuilder(); + // 返回的信息 + List resultList = new ArrayList<>(); + // 满足匹配消息id条件 + BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.matchQuery(WeConstans.MSG_ID, msgId)); + builder.query(boolQueryBuilder); + // 当前消息的详细内容 + List search = elasticSearch.search(index, builder, ConversationArchiveVO.class); + if (CollectionUtils.isEmpty(search)) { + return null; + } + Long msgTime = search.get(0).getMsgTime(); + if (StringUtils.isBlank(dto.getType())) { + // 将本身这条加进返回列表 + resultList.addAll(search); + // 默认信息上文查询 + SearchSourceBuilder priorBuilder = getPriorContextBuilder(msgTime, ConversationArchiveConstants.DEFAULT_CONTEXT_NUM, fromId, receiveId, roomId); + List defaultPriorList = elasticSearch.search(index, priorBuilder, ConversationArchiveVO.class); + // 默认信息下文查询 + SearchSourceBuilder nextBuilder = getNextContextBuilder(msgTime, ConversationArchiveConstants.DEFAULT_CONTEXT_NUM, fromId, receiveId, roomId); + List defaultNextList = elasticSearch.search(index, nextBuilder, ConversationArchiveVO.class); + resultList.addAll(defaultPriorList); + resultList.addAll(defaultNextList); + } + if (ConversationArchiveConstants.NEXT_CONTEXT.equals(dto.getType())) { + SearchSourceBuilder priorBuilder = getPriorContextBuilder(msgTime, ConversationArchiveConstants.PAGE_CONTEXT_NUM, fromId, receiveId, roomId); + List priorList = elasticSearch.search(index, priorBuilder, ConversationArchiveVO.class); + resultList.addAll(priorList); + } + if (ConversationArchiveConstants.PRIOR_CONTEXT.equals(dto.getType())) { + SearchSourceBuilder nextBuilder = getNextContextBuilder(msgTime, ConversationArchiveConstants.PAGE_CONTEXT_NUM, fromId, receiveId, roomId); + List nextList = elasticSearch.search(index, nextBuilder, ConversationArchiveVO.class); + resultList.addAll(nextList); + } + // 按照发送时间正序排序 + resultList.sort(Comparator.comparing(ConversationArchiveVO::getMsgTime)); + // 补充撤回消息内容 + supplyRecallMsg(resultList, dto.getCorpId()); + return PageInfoUtil.getDataTable(resultList); + } + + /** + * 获取上文信息查询条件 + * + * @param msgTime 消息发送时间 + * @param size 查询消息条数 + * @param fromId 发送者id + * @param receiveId 接收者id + * @param roomId 群聊id + * @return {@link SearchSourceBuilder} + */ + private SearchSourceBuilder getPriorContextBuilder(Long msgTime, int size, String fromId, String receiveId, String roomId) { + if (msgTime == null) { + return null; + } + SearchSourceBuilder builder = new SearchSourceBuilder(); + BoolQueryBuilder priorBuilder; + builder.from(0); + builder.size(size); + // 查询上文数据 + priorBuilder = QueryBuilders.boolQuery().must(QueryBuilders.rangeQuery(WeConstans.MSG_TIME).lt(msgTime)); + // 匹配发送人和接收人信息 + matchFromAndReceiveBuilder(priorBuilder, fromId, receiveId, roomId); + builder.query(priorBuilder); + builder.sort(WeConstans.MSG_TIME, SortOrder.DESC); + return builder; + } + + /** + * 获取下文信息查询条件 + * + * @param msgTime 消息发送时间 + * @param size 查询消息条数 + * @param fromId 发送者id + * @param receiveId 接收者id + * @param roomId 群聊id + * @return {@link SearchSourceBuilder} + */ + private SearchSourceBuilder getNextContextBuilder(Long msgTime, int size, String fromId, String receiveId, String roomId) { + if (msgTime == null) { + return null; + } + SearchSourceBuilder builder = new SearchSourceBuilder(); + BoolQueryBuilder nextBuilder; + builder.from(0); + builder.size(size); + // 查询下文数据 + nextBuilder = QueryBuilders.boolQuery().must(QueryBuilders.rangeQuery(WeConstans.MSG_TIME).gt(msgTime)); + // 匹配发送人和接收人信息 + matchFromAndReceiveBuilder(nextBuilder, fromId, receiveId, roomId); + builder.query(nextBuilder); + builder.sort(WeConstans.MSG_TIME, SortOrder.ASC); + return builder; + } + + /** + * 匹配发送人和接收人信息或群聊信息 + * + * @param queryBuilder {@link BoolQueryBuilder} + * @param fromId 发送人id + * @param receiveId 接收人id + * @param roomId 群聊id + */ + private void matchFromAndReceiveBuilder(BoolQueryBuilder queryBuilder, String fromId, String receiveId, String roomId) { + if (queryBuilder == null) { + return; + } + if (StringUtils.isNotBlank(fromId) && StringUtils.isNotBlank(receiveId)) { + // 发送人匹配 + queryBuilder.should(QueryBuilders.boolQuery().must(QueryBuilders.matchQuery(WeConstans.ROOMID, StringUtils.EMPTY)) + .must(QueryBuilders.matchQuery(WeConstans.FROM, fromId)) + .must(QueryBuilders.matchQuery(WeConstans.TO_LIST_KEYWORD, receiveId))); + // 接收人匹配 + queryBuilder.should(QueryBuilders.boolQuery().must(QueryBuilders.matchQuery(WeConstans.ROOMID, StringUtils.EMPTY)) + .must(QueryBuilders.matchQuery(WeConstans.FROM, receiveId)) + .must(QueryBuilders.matchQuery(WeConstans.TO_LIST_KEYWORD, fromId))); + } + if (StringUtils.isNotBlank(roomId)) { + // 群聊匹配 + queryBuilder.should(QueryBuilders.boolQuery().must(QueryBuilders.termsQuery(WeConstans.ROOMID, roomId)) + .must(QueryBuilders.termsQuery(WeConstans.FROM, fromId))); + queryBuilder.should(QueryBuilders.boolQuery().must(QueryBuilders.termsQuery(WeConstans.ROOMID, roomId)) + .must(QueryBuilders.matchQuery(WeConstans.TO_LIST, fromId))); + } + queryBuilder.minimumShouldMatch(1); + } + private String getExtraChatName(List memberLists) { StringBuilder chatName = new StringBuilder(); int customerNum = 0; @@ -392,12 +555,31 @@ private String getExtraChatName(List memberLists) { return chatName.toString(); } - + /** + * 处理撤回状态消息,补偿消息内容 + * + * @param pageList pageList {@link PageInfo} + * @param corpId 企业ID + */ private void filterData(PageInfo pageList, String corpId) { + filterData(pageList, corpId, null, null); + } + + /** + * 处理撤回状态消息,补偿消息内容 + * + * @param pageList {@link PageInfo} + * @param corpId 企业ID + * @param keyWords 查询匹配内容 + * @param action 消息类型 send:已发送 recall:已撤回 null:全部查询 + */ + private void filterData(PageInfo pageList, String corpId, String keyWords, String action) { if (StringUtils.isEmpty(corpId)) { log.error("corpId不能为空"); return; } + // 全部查询处理 + searchAllMatchKeyWords(keyWords, pageList, corpId, action); for (ConversationArchiveVO conversationArchiveVO : pageList.getList()) { if (conversationArchiveVO.getFromInfo() == null) { String fromId = conversationArchiveVO.getFrom(); @@ -428,13 +610,31 @@ private void filterData(PageInfo pageList, String corpId) conversationArchiveVO.setToListInfo(JSON.parseObject(JSON.toJSONString(weCustomer))); } } + } + // 撤回消息内容补充 + supplyRecallMsg(pageList.getList(), corpId); + // 处理撤回状态关键词查询数据 + recallDataFilter(keyWords, action, pageList); + } + + /** + * 补充撤回消息 + * + * @param pageList {@link ConversationArchiveVO} + * @param corpId 企业ID + */ + private void supplyRecallMsg(List pageList, String corpId) { + if (CollectionUtils.isEmpty(pageList) || StringUtils.isBlank(corpId)) { + return; + } + for (ConversationArchiveVO conversationArchiveVO : pageList) { // 撤回消息内容补充 if (MsgTypeEnum.REVOKE.getType().equals(conversationArchiveVO.getMsgType())) { RevokeVO revoke = conversationArchiveVO.getRevoke(); + SearchSourceBuilder builder = new SearchSourceBuilder(); if (ObjectUtils.isEmpty(revoke) || org.apache.commons.lang3.StringUtils.isBlank(revoke.getPre_msgid())) { continue; } - SearchSourceBuilder builder = new SearchSourceBuilder(); BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.matchQuery(WeConstans.MSG_ID, revoke.getPre_msgid())); builder.query(boolQueryBuilder); List search = elasticSearch.search(WeConstans.getChatDataIndex(corpId), builder, ChatInfoVO.class); @@ -446,4 +646,104 @@ private void filterData(PageInfo pageList, String corpId) } } + /** + * 处理全部状态关键词查询(仅action为null时处理) + * + * @param keyWords 查询匹配内容 + * @param pageList {@link PageInfo} + * @param corpId 企业ID + * @param action action 消息类型 send:已发送 recall:已撤回 null:全部查询 + */ + private void searchAllMatchKeyWords(String keyWords, PageInfo pageList, String corpId, String action) { + if (StringUtils.isAnyBlank(keyWords, corpId) || pageList == null || StringUtils.isNotBlank(action)) { + return; + } + List msgIdList = new ArrayList<>(); + Iterator iterator = pageList.getList().iterator(); + while (iterator.hasNext()) { + ConversationArchiveVO conversationArchiveVO = iterator.next(); + if (conversationArchiveVO.getIsRevoke() != null && conversationArchiveVO.getIsRevoke()) { + msgIdList.add(conversationArchiveVO.getMsgId()); + } + } + SearchSourceBuilder builder = new SearchSourceBuilder(); + // 全部查询 + builder.from(ConversationArchiveConstants.SEARCH_FROM); + builder.size(ConversationArchiveConstants.SEARCH_SIZE); + BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery().should(QueryBuilders.termsQuery(WeConstans.PRE_MSG_ID, msgIdList.toArray(new String[0]))); + builder.query(queryBuilder); + List search = elasticSearch.search(WeConstans.getChatDataIndex(corpId), builder, ConversationArchiveVO.class); + pageList.getList().addAll(search); + // 根据时间倒序 + pageList.getList().sort(Comparator.comparing(ConversationArchiveVO::getMsgTime).reversed()); + // 处理分页数据 + paging(pageList); + } + + /** + * 处理撤回状态关键词查询数据 + * + * @param keyWords 查询匹配内容 + * @param action action 消息类型 send:已发送 recall:已撤回 null:全部查询 + * @param pageList {@link PageInfo} + */ + private void recallDataFilter(String keyWords, String action, PageInfo pageList) { + if (StringUtils.isAnyBlank(keyWords, action) || pageList == null) { + log.info("[会话存档全局检索] 处理撤回关键词查询异常,参数缺失:keyWords:{}, action:{}, pageList:{}", keyWords, action, pageList); + return; + } + // 仅发送状态为recall时才处理 + if (ConversationArchiveConstants.ACTION_RECALL.equals(action)) { + Iterator iterator = pageList.getList().iterator(); + while (iterator.hasNext()) { + ConversationArchiveVO conversationArchiveVO = iterator.next(); + // 撤回信息内容为空,则删除 + if (conversationArchiveVO.getRevoke() == null + || conversationArchiveVO.getRevoke().getContent() == null + || conversationArchiveVO.getRevoke().getContent().getMsgtype() == null) { + iterator.remove(); + continue; + } + // 类型不为撤回,或msgType不是text类型 + if (!MsgTypeEnum.TEXT.getType().equals(conversationArchiveVO.getRevoke().getContent().getMsgtype())) { + iterator.remove(); + // 已删除就跳过当前循环 + continue; + } + if (conversationArchiveVO.getRevoke().getContent().getText() != null + && conversationArchiveVO.getRevoke().getContent().getText().getContent() != null) { + String msgContent = conversationArchiveVO.getRevoke().getContent().getText().getContent(); + // 过滤掉不匹配聊天内容关键词的内容 + if (!msgContent.contains(keyWords)) { + iterator.remove(); + } + } + } + // 处理分页数据 + paging(pageList); + } + } + + /** + * 处理分页参数 + * + * @param pageList {@link PageInfo} + */ + private void paging(PageInfo pageList) { + int total = pageList.getList().size(); + int pages = total; + if (total != 0 && total % pageList.getPageNum() == 0) { + pages = total / pageList.getPageNum(); + } else if (total != 0 && total % pageList.getPageNum() != 0) { + pages = (total / pageList.getPageNum()) + 1; + } + pageList.setTotal(total); + pageList.setPages(pages); + pageList.setHasNextPage(pageList.getPageNum() < pageList.getPages()); + // 手动截取分页 + int start = (pageList.getPageNum() - 1) * pageList.getPageSize(); + int end = Math.min(start + pageList.getPageSize(), total); + pageList.setList(pageList.getList().subList(start, end)); + } + } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerExtendPropertyServiceImpl.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerExtendPropertyServiceImpl.java index c223c30f..b1e7c0cf 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerExtendPropertyServiceImpl.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerExtendPropertyServiceImpl.java @@ -20,6 +20,7 @@ import com.easyink.wecom.domain.entity.customer.WeCustomerExtendPropertyRel; import com.easyink.wecom.domain.vo.WeCustomerExportVO; import com.easyink.wecom.domain.vo.customer.WeCustomerVO; +import com.easyink.wecom.handler.ExtendPropHolder; import com.easyink.wecom.mapper.WeCustomerExtendPropertyMapper; import com.easyink.wecom.service.ExtendPropertyMultipleOptionService; import com.easyink.wecom.service.WeCustomerExtendPropertyRelService; @@ -115,6 +116,29 @@ public void validate(WeCustomerExtendProperty property) { if (!isUnique(property)) { throw new CustomException(ResultTip.TIP_EXTEND_PROP_NAME_EXISTED); } + // 自定义选项属性值是否重复 + if (isOptionRepeat(property.getOptionList())) { + throw new CustomException(ResultTip.TIP_OPTION_REPEAT); + } + } + + /** + * 自定义选项属性值是否重复 + * + * @param optionList {@link ExtendPropertyMultipleOption} + * @return 重复 true; 不重复 false + */ + private Boolean isOptionRepeat(List optionList) { + if (CollectionUtils.isEmpty(optionList)) { + return false; + } + Set names = new HashSet<>(); + for (ExtendPropertyMultipleOption extendPropertyMultipleOption : optionList) { + if (!names.add(extendPropertyMultipleOption.getMultipleValue())) { + return true; + } + } + return false; } @Override @@ -226,6 +250,12 @@ public void editBatch(BatchSaveCustomerExtendPropertyDTO dto) { throw new CustomException(ResultTip.TIP_GENERAL_PARAM_ERROR); } List list = new ArrayList<>(); + for (SaveCustomerExtendPropertyDTO property : dto.getProperties()) { + // 自定义选项属性值是否重复 + if (isOptionRepeat(property.getOptionList())) { + throw new CustomException(ResultTip.TIP_OPTION_REPEAT); + } + } // 转换成 WeCustomerExtendProperty 集合 for (SaveCustomerExtendPropertyDTO source : editList) { WeCustomerExtendProperty property = new WeCustomerExtendProperty(); @@ -261,31 +291,23 @@ public void editBatch(List list, String corpId) { } @Override - public void setKeyValueMapper(String corpId, List customerList, List selectedProperties) { + public void setKeyValueMapper(String corpId, List customerList, List selectedProperties, ExtendPropHolder extendPropHolder) { if (CollectionUtils.isEmpty(customerList) || CollectionUtils.isEmpty(selectedProperties)) { return; } - // 过滤系统默认字段 - selectedProperties = selectedProperties.stream().filter(a -> !ListUtil.toList(UserConstants.getSysDefaultProperties()).contains(a)).collect(Collectors.toList()); - // 查询该企业所有的扩展属性详情 - List extendPropList = this.getList( - WeCustomerExtendProperty.builder() - .corpId(corpId) - .build() - ); - if (CollectionUtils.isEmpty(extendPropList)) { + if(extendPropHolder==null || !extendPropHolder.isHasExtendProp()) { + return; + } + List propList = selectedProperties.stream().filter(a -> !ListUtil.toList(UserConstants.getSysDefaultProperties()).contains(a)).collect(Collectors.toList()); + if(CollectionUtils.isEmpty(propList)) { return; } - // 扩展属性ID->详情的映射 - Map extendPropMap = extendPropList.stream().collect(Collectors.toMap(WeCustomerExtendProperty::getId, prop -> prop)); - // 多选值ID-> 多选值的映射 - Map optionMap = extendPropertyMultipleOptionService.getMapByProp(extendPropList); for (WeCustomerExportVO vo : customerList) { List extendProperties = vo.getExtendProperties(); - Map customerMap = this.mapPropertyName2Value(extendProperties, extendPropMap, optionMap); + Map customerMap = this.mapPropertyName2Value(extendProperties, extendPropHolder.getExtendPropMap(), extendPropHolder.getOptionMap()); LinkedHashMap resultMap = new LinkedHashMap(); // 按选中的字段顺序设置值,如果没有则设为空字符串 - for (String selectedProp : selectedProperties) { + for (String selectedProp : propList) { resultMap.put(selectedProp, customerMap.getOrDefault(selectedProp, StringUtils.EMPTY)); } // 为导出客户实体设置 k:导出字段名 v:导出字段值 的映射 diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerMessagePushServiceImpl.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerMessagePushServiceImpl.java index d31b85dd..f35a5b4e 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerMessagePushServiceImpl.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerMessagePushServiceImpl.java @@ -3,6 +3,7 @@ import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.easyink.common.constant.Constants; import com.easyink.common.constant.GroupConstants; import com.easyink.common.constant.WeConstans; import com.easyink.common.core.domain.entity.WeCorpAccount; @@ -448,7 +449,7 @@ public void delete(Long messageId, String corpId) { */ @Override public void sendToUser(WeCustomerMessageToUserDTO weCustomerMessageToUserDTO) { - String[] customers = weCustomerMessageToUserDTO.getCustomers().split("、"); + String[] customers = weCustomerMessageToUserDTO.getCustomers().split(Constants.CUSTOMER_PUSH_MESSAGE_SEPARATOR); StringBuilder replaceMsg = new StringBuilder(); if (ArrayUtils.isEmpty(customers)) { return; diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerMessgaeResultServiceImpl.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerMessgaeResultServiceImpl.java index 83117e72..9a6f4a6a 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerMessgaeResultServiceImpl.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerMessgaeResultServiceImpl.java @@ -1,7 +1,9 @@ package com.easyink.wecom.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.easyink.common.constant.Constants; import com.easyink.common.constant.WeConstans; +import com.easyink.common.utils.DictUtils; import com.easyink.common.utils.SnowFlakeUtil; import com.easyink.common.utils.StringUtils; import com.easyink.wecom.domain.WeCustomer; @@ -19,7 +21,9 @@ import org.springframework.stereotype.Service; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; /** * 群发消息 微信消息发送结果表 we_customer_messgaeResult @@ -33,13 +37,36 @@ public class WeCustomerMessgaeResultServiceImpl extends ServiceImpl customerMessagePushs(WeCustomerMessagePushResultDTO weCustomerMessagePushResultDTO) { if (StringUtils.isEmpty(weCustomerMessagePushResultDTO.getCorpId())) { return new ArrayList<>(); } if (WeConstans.NOT_SEND.equals(weCustomerMessagePushResultDTO.getSendStatus())) { - return customerMessgaeResultMapper.customerMessagePushs(weCustomerMessagePushResultDTO); + List resultList = customerMessgaeResultMapper.customerMessagePushs(weCustomerMessagePushResultDTO); + if (CollectionUtils.isEmpty(resultList)) { + return Collections.emptyList(); + } + List customerList = customerMessgaeResultMapper.messagePushsByCustomer(weCustomerMessagePushResultDTO); + if (CollectionUtils.isEmpty(customerList)) { + return Collections.emptyList(); + } + resultList.forEach(item -> { + // 筛选出员工名称相同的信息 + List sendCustomerNameList = customerList.stream().filter(i -> i.getUserName().equals(item.getUserName())) + .map(WeCustomerMessageResultVO::getCustomers) + .collect(Collectors.toList()); + // 将发送的客户/客户群名称转换成以逗号分隔的字符串 + String customers = String.join(Constants.CUSTOMER_PUSH_MESSAGE_SEPARATOR, sendCustomerNameList); + item.setCustomers(customers); + }); + return resultList; } else { return customerMessgaeResultMapper.listOfMessageResult(weCustomerMessagePushResultDTO); } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerServiceImpl.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerServiceImpl.java index 25fe3f2b..5b9b8fa9 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerServiceImpl.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeCustomerServiceImpl.java @@ -1,14 +1,23 @@ package com.easyink.wecom.service.impl; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.StrUtil; +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.alibaba.excel.write.metadata.WriteSheet; +import com.alibaba.excel.write.metadata.style.WriteCellStyle; +import com.alibaba.excel.write.metadata.style.WriteFont; +import com.alibaba.excel.write.style.HorizontalCellStyleStrategy; +import com.alibaba.excel.write.style.column.SimpleColumnWidthStyleStrategy; import com.alibaba.fastjson.JSONArray; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.dtflys.forest.exceptions.ForestRuntimeException; import com.easyink.common.annotation.DataScope; +import com.easyink.common.annotation.Excel; +import com.easyink.common.config.RuoYiConfig; import com.easyink.common.constant.Constants; import com.easyink.common.constant.GenConstants; import com.easyink.common.constant.WeConstans; @@ -16,7 +25,7 @@ import com.easyink.common.core.domain.AjaxResult; import com.easyink.common.core.domain.sop.CustomerSopPropertyRel; import com.easyink.common.core.domain.wecom.BaseExtendPropertyRel; -import com.easyink.common.core.domain.wecom.WeUser; +import com.easyink.common.core.text.Convert; import com.easyink.common.enums.CustomerExtendPropertyEnum; import com.easyink.common.enums.CustomerTrajectoryEnums; import com.easyink.common.enums.MethodParamType; @@ -60,26 +69,41 @@ import com.easyink.wecom.domain.vo.customer.WeCustomerVO; import com.easyink.wecom.domain.vo.sop.CustomerSopVO; import com.easyink.wecom.domain.vo.unionid.GetUnionIdVO; +import com.easyink.wecom.handler.ExtendPropHolder; import com.easyink.wecom.login.util.LoginTokenService; import com.easyink.wecom.mapper.WeCustomerMapper; import com.easyink.wecom.mapper.WeExternalUseridMappingMapper; import com.easyink.wecom.service.*; import com.easyink.wecom.service.wechatopen.WechatOpenService; +import com.easyink.wecom.utils.redis.CustomerRedisCache; +import com.github.pagehelper.PageHelper; +import com.google.common.base.Stopwatch; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.poi.ss.usermodel.*; import org.springframework.aop.framework.AopContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import javax.annotation.Resource; import javax.validation.constraints.NotBlank; +import java.io.File; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.*; -import java.util.function.Function; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** @@ -139,6 +163,17 @@ public class WeCustomerServiceImpl extends ServiceImpl selectWeCustomerListV2(WeCustomer weCustomer) { return weCustomerMapper.selectWeCustomerListV2(weCustomer); } - @DataScope - @Override - public List selectWeCustomerListV3(WeCustomer weCustomer) { - if (StringUtils.isBlank(weCustomer.getCorpId())) { - throw new CustomException(ResultTip.TIP_GENERAL_PARAM_ERROR); - } - String corpId = weCustomer.getCorpId(); - if (!hasFilterCustomer(weCustomer, corpId)) { - return Collections.emptyList(); - } - // 查询客户 - PageInfoUtil.startPage(); - List list = weCustomerMapper.selectWeCustomerV3(weCustomer); - // 根据返回的结果获取需要的标签详情 - weFlowerCustomerTagRelService.setTagForCustomers(corpId, list); - // 根据返回的结果获取需要的扩展字段 - weCustomerExtendPropertyService.setExtendPropertyForCustomers(corpId, list); - return list; - } - - /** - * 查询导出的列表 - * - * @param weCustomer 客户条件 - * @return 需要导出的客户 - */ - public List selectExportCustomer(WeCustomer weCustomer) { - if (StringUtils.isBlank(weCustomer.getCorpId())) { - throw new CustomException(ResultTip.TIP_GENERAL_PARAM_ERROR); - } - List list = weCustomerMapper.selectExportCustomer(weCustomer); - return list ; - } /** * 判断是否有过滤条件的客户 @@ -490,7 +492,8 @@ else if (Objects.equals(baseExtendPropertyRel.getPropertyType(), CustomerExtendP } return extendList; } - + @Resource(name = "threadPoolTaskExecutor") + private ThreadPoolTaskExecutor threadPoolTaskExecutor; /** * 获取查询所需集合。如果其中一个为空,返回另一个 @@ -499,93 +502,16 @@ else if (Objects.equals(baseExtendPropertyRel.getPropertyType(), CustomerExtendP * @param list2 集合2 * @return 查询所需的集合 */ - public static List getIntersectionTypeNotFind(List list1,List list2) { - if (list1.size()>0){ - if (list2.size()>0){ + public static List getIntersectionTypeNotFind(List list1, List list2) { + if (list1.size() > 0) { + if (list2.size() > 0) { list1.retainAll(list2); return list1; - }else return list1; + } else return list1; } return list2; } - /** - * 将DTO类转换为实体类 - * - * @param weCustomerSearchDTO - * @return - */ - @Override - public WeCustomer changeWecustomer(WeCustomerSearchDTO weCustomerSearchDTO){ - //初始化添加列表 - WeFlowerCustomerRel weFlowerCustomerRel =new WeFlowerCustomerRel(); - if (weCustomerSearchDTO.getBeginTime()!=null){ - weFlowerCustomerRel.setBeginTime(weCustomerSearchDTO.getBeginTime().toString()); - } - if (weCustomerSearchDTO.getEndTime()!=null){ - weFlowerCustomerRel.setEndTime(weCustomerSearchDTO.getEndTime().toString()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getAddWay())){ - weFlowerCustomerRel.setAddWay(weCustomerSearchDTO.getAddWay()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getEmail())){ - weFlowerCustomerRel.setEmail(weCustomerSearchDTO.getEmail()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getAddress())){ - weFlowerCustomerRel.setAddress(weCustomerSearchDTO.getAddress()); - } - List weFlowerCustomerRels = new ArrayList<>() ; - weFlowerCustomerRels.add(weFlowerCustomerRel); - // 转化实体类 - WeCustomer weCustomer =new WeCustomer(); - weCustomer.setCorpId(LoginTokenService.getLoginUser().getCorpId()); - weCustomer.setExternalUserid(weCustomerSearchDTO.getExternalUserid()); - weCustomer.setStatus(weCustomerSearchDTO.getStatus()); - weCustomer.setWeFlowerCustomerRels(weFlowerCustomerRels); - - if (MapUtils.isNotEmpty(weCustomerSearchDTO.getParams())){ - weCustomer.setParams(weCustomerSearchDTO.getParams()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getName())){ - weCustomer.setName(weCustomerSearchDTO.getName()); - weCustomer.setRemark(weCustomerSearchDTO.getName()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getDesc())){ - weCustomer.setDesc(weCustomerSearchDTO.getDesc()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getUserId())){ - weCustomer.setUserId(weCustomerSearchDTO.getUserId()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getUserIds())){ - weCustomer.setUserIds(weCustomerSearchDTO.getUserIds()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getTagIds())){ - weCustomer.setTagIds(weCustomerSearchDTO.getTagIds()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getGender())){ - weCustomer.setGender(weCustomerSearchDTO.getGender()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getCorpFullName())){ - weCustomer.setCorpFullName(weCustomerSearchDTO.getCorpFullName()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getBirthday())){ - weCustomer.setBirthdayStr(weCustomerSearchDTO.getBirthday()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getPhone())){ - weCustomer.setPhone(weCustomerSearchDTO.getPhone()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getBeginTime())){ - weCustomer.setBeginTime(weCustomerSearchDTO.getBeginTime()); - } - if (!StringUtils.isBlank(weCustomerSearchDTO.getEndTime())){ - weCustomer.setEndTime(weCustomerSearchDTO.getEndTime()); - } - if (weCustomerSearchDTO.getExtendProperties().size()>0){ - weCustomer.setExtendProperties(weCustomerSearchDTO.getExtendProperties()); - } - return weCustomer; - } - /** * 查询客户sop使用客户 * @@ -834,7 +760,6 @@ public WeCustomerSumVO weCustomerCount(WeCustomer weCustomer) { } @Override - @Transactional(rollbackFor = Exception.class) @Async public void syncWeCustomerV2(String corpId) { if (StringUtils.isBlank(corpId)) { @@ -848,7 +773,11 @@ public void syncWeCustomerV2(String corpId) { // 2.每个员工依次调用[批量获取客户详情] 企微API, 同步其客户详情 for (String userId : userIdList) { - batchGetCustomerDetailAndSyncLocal(corpId, userId); + try { + batchGetCustomerDetailAndSyncLocal(corpId, userId); + }catch (Exception e) { + log.error("同步处理单个员工的客户异常,corpId:{}, userId:{}, e:{}", corpId, userId, ExceptionUtils.getStackTrace(e)); + } } long endTime = System.currentTimeMillis(); log.info("同步客户结束[新]:corpId:{}耗时:{}", corpId, Double.valueOf((endTime - startTime) / 1000.00D)); @@ -880,16 +809,13 @@ public void batchGetCustomerDetailAndSyncLocal(String corpId, String userId) { log.info("[批量获取客户详情] 该员工没有添加客户, corpId:{}, userId:{}", corpId, userId); return; } - Map userIdInDbMap = weUserService.list(new LambdaQueryWrapper().select(WeUser::getUserId).eq(WeUser::getCorpId, corpId)) - .stream().collect(Collectors.toMap(WeUser::getUserId, WeUser::getUserId)); // 2. 数据处理:对返回的数据进行处理 - resp.handleData(corpId, userIdInDbMap); + resp.handleData(corpId, null); if (resp.isEmptyResult()) { log.info("[批量获取客户详情] 该员工没有添加客户, corpId:{}, userId:{}", corpId, userId); return; } // 3. 数据对齐:本地成员-客户关系数据与服务端对齐,同步远端修改的数据 - weFlowerCustomerRelService.alignData(resp, userId, corpId); List externalUserIdList = resp.getCustomerList().stream().map(WeCustomer::getExternalUserid).collect(Collectors.toList()); List localRelList = weFlowerCustomerRelService.list( new LambdaQueryWrapper() @@ -897,51 +823,17 @@ public void batchGetCustomerDetailAndSyncLocal(String corpId, String userId) { .eq(WeFlowerCustomerRel::getUserId, userId) .in(WeFlowerCustomerRel::getExternalUserid, externalUserIdList) ); - if (CollectionUtils.isNotEmpty(localRelList)) { - updateCustomerRel(resp, localRelList, corpId); - } - // 对以前删除但是重新加回的客户重新把状态设置为正常 - resp.activateDelCustomer(localRelList); + weFlowerCustomerRelService.alignData(resp, userId, corpId , localRelList); //**** 客户信息更新、插入 , 客户-成员关系更新 , 客户-标签关系更新 // 4. 数据同步:插入、更新 客户数据,员工-客户关系 BatchInsertUtil.doInsert(resp.getCustomerList(), this::batchInsert); BatchInsertUtil.doInsert(resp.getRelList(), list -> weFlowerCustomerRelService.batchInsert(list)); - // 5. 数据同步: 客户-标签关系 ,获取每个客户关系对应的标签,并同步更新数据库 List tagRelList = resp.getCustomerTagRelList(localRelList); List relIds = localRelList.stream().map(WeFlowerCustomerRel::getId).collect(Collectors.toList()); - BatchInsertUtil.doInsert(relIds, list -> weFlowerCustomerTagRelService.remove(new LambdaQueryWrapper() - .in(WeFlowerCustomerTagRel::getFlowerCustomerRelId,list))); - BatchInsertUtil.doInsert(tagRelList, list -> weFlowerCustomerTagRelService.batchInsert(list)); + weFlowerCustomerTagRelService.syncLocalTagFromRemote(tagRelList, relIds); } - /** - * 更新流失后添加的客户数据,客户-员工关系 - * - * @param resp 企微响应 - * @param localRelList 本地数据 - */ - public void updateCustomerRel(GetByUserResp resp, List localRelList, String corpId){ - List updateRelList = new ArrayList<>(); - Map localMap = localRelList.stream().collect(Collectors.toMap(WeFlowerCustomerRel::getExternalUserid, Function.identity())); - // 本地数据与远端数据对比 - for (WeFlowerCustomerRel remoteFlowerCustomerRel : resp.getRelList()) { - WeFlowerCustomerRel localFlowerCustomerRel = localMap.get(remoteFlowerCustomerRel.getExternalUserid()); - // 若该客户与本地记录的添加时间不一样,表示此客户是流失后重新添加员工的客户,将该客户存入列表等待更新状态 - if(localFlowerCustomerRel != null){ - if (remoteFlowerCustomerRel.getCreateTime().compareTo(localFlowerCustomerRel.getCreateTime()) != 0) { - remoteFlowerCustomerRel.setStatus(Constants.NORMAL_CODE); - updateRelList.add(remoteFlowerCustomerRel); - } - } - } - // 存在创建时间不同的客户则更新客户状态 - if (CollectionUtils.isNotEmpty(updateRelList)) { - LambdaUpdateWrapper updateRelWrapper = new LambdaUpdateWrapper<>(); - updateRelWrapper.eq(WeFlowerCustomerRel::getCorpId, corpId); - weFlowerCustomerRelService.batchUpdateStatus(updateRelList); - } - } /** * 调用企微根据corpId获取离职员工列表 @@ -1370,23 +1262,397 @@ public void insert(WeCustomer weCustomer) { this.batchInsert(list); } + /** + * 根据传入的字段获取对应的get方法,如name,对应的getName方法 + * + * @param fieldName 字段名 + * @param person 对象 + * @return + */ + private static Object getFieldValue(String fieldName, Object person) { + try { + String firstLetter = fieldName.substring(0, 1).toUpperCase(); + String getter = "get" + firstLetter + fieldName.substring(1); + Method method = person.getClass().getMethod(getter); + return method.invoke(person); + } catch (Exception e) { + return null; + } + } + + /** + * 将DTO类转换为实体类 + * + * @param weCustomerSearchDTO + * @return + */ + @Override + public WeCustomer changeWecustomer(WeCustomerSearchDTO weCustomerSearchDTO) { + //初始化添加列表 + WeFlowerCustomerRel weFlowerCustomerRel = new WeFlowerCustomerRel(); + if (weCustomerSearchDTO.getBeginTime() != null) { + weFlowerCustomerRel.setBeginTime(weCustomerSearchDTO.getBeginTime().toString()); + } + if (weCustomerSearchDTO.getEndTime() != null) { + weFlowerCustomerRel.setEndTime(weCustomerSearchDTO.getEndTime().toString()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getAddWay())) { + weFlowerCustomerRel.setAddWay(weCustomerSearchDTO.getAddWay()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getEmail())) { + weFlowerCustomerRel.setEmail(weCustomerSearchDTO.getEmail()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getAddress())) { + weFlowerCustomerRel.setAddress(weCustomerSearchDTO.getAddress()); + } + List weFlowerCustomerRels = new ArrayList<>(); + weFlowerCustomerRels.add(weFlowerCustomerRel); + // 转化实体类 + WeCustomer weCustomer = new WeCustomer(); + weCustomer.setCorpId(weCustomerSearchDTO.getCorpId()); + weCustomer.setExternalUserid(weCustomerSearchDTO.getExternalUserid()); + weCustomer.setStatus(weCustomerSearchDTO.getStatus()); + weCustomer.setWeFlowerCustomerRels(weFlowerCustomerRels); + + if (MapUtils.isNotEmpty(weCustomerSearchDTO.getParams())) { + weCustomer.setParams(weCustomerSearchDTO.getParams()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getName())) { + weCustomer.setName(weCustomerSearchDTO.getName()); + weCustomer.setRemark(weCustomerSearchDTO.getName()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getDesc())) { + weCustomer.setDesc(weCustomerSearchDTO.getDesc()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getUserId())) { + weCustomer.setUserId(weCustomerSearchDTO.getUserId()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getUserIds())) { + weCustomer.setUserIds(weCustomerSearchDTO.getUserIds()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getTagIds())) { + weCustomer.setTagIds(weCustomerSearchDTO.getTagIds()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getGender())) { + weCustomer.setGender(weCustomerSearchDTO.getGender()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getCorpFullName())) { + weCustomer.setCorpFullName(weCustomerSearchDTO.getCorpFullName()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getBirthday())) { + weCustomer.setBirthdayStr(weCustomerSearchDTO.getBirthday()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getPhone())) { + weCustomer.setPhone(weCustomerSearchDTO.getPhone()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getBeginTime())) { + weCustomer.setBeginTime(weCustomerSearchDTO.getBeginTime()); + } + if (!StringUtils.isBlank(weCustomerSearchDTO.getEndTime())) { + weCustomer.setEndTime(weCustomerSearchDTO.getEndTime()); + } + if (weCustomerSearchDTO.getExtendProperties().size() > 0) { + weCustomer.setExtendProperties(weCustomerSearchDTO.getExtendProperties()); + } + return weCustomer; + } + + /** + * 查询导出的列表 + * + * @param weCustomer 客户条件 + * @param selectProperties 需要导出的扩展属性 + * @param extendPropHolder 扩展字段属性容器 {@link ExtendPropHolder} + * @return 需要导出的客户 + */ + public List selectExportCustomer(WeCustomer weCustomer, List selectProperties, ExtendPropHolder extendPropHolder) { + if (StringUtils.isBlank(weCustomer.getCorpId())) { + throw new CustomException(ResultTip.TIP_GENERAL_PARAM_ERROR); + } + List list = weCustomerMapper.selectWeCustomerV3(weCustomer); + // 根据返回的结果获取需要的标签详情 + CompletableFuture tagFuture = CompletableFuture.runAsync( + () -> weFlowerCustomerTagRelService.setTagForCustomers(weCustomer.getCorpId(), list), threadPoolTaskExecutor + ); + // 根据返回的结果获取需要的扩展字段 + CompletableFuture extendPropFuture = CompletableFuture.runAsync( + () -> weCustomerExtendPropertyService.setExtendPropertyForCustomers(weCustomer.getCorpId(), list), threadPoolTaskExecutor + ); + CompletableFuture.allOf(tagFuture, extendPropFuture).join(); + +// List list = weCustomerMapper.selectExportCustomer(weCustomer); +// List list = weCustomerMapper.selectWeCustomerListV2(weCustomer) ; + List exportList = list.stream().map(WeCustomerExportVO::new).collect(Collectors.toList()); + weCustomerExtendPropertyService.setKeyValueMapper(weCustomer.getCorpId(), exportList, selectProperties, extendPropHolder); + return exportList; + } + + @DataScope + @Override + public List selectWeCustomerListV3(WeCustomer weCustomer) { + if (StringUtils.isBlank(weCustomer.getCorpId())) { + throw new CustomException(ResultTip.TIP_GENERAL_PARAM_ERROR); + } + String corpId = weCustomer.getCorpId(); + if (!hasFilterCustomer(weCustomer, corpId)) { + return Collections.emptyList(); + } + // 查询客户 + PageInfoUtil.startPage(); + List list = weCustomerMapper.selectWeCustomerV3(weCustomer); + // 根据返回的结果获取需要的标签详情 + CompletableFuture tagFuture = CompletableFuture.runAsync( + () -> weFlowerCustomerTagRelService.setTagForCustomers(corpId, list), threadPoolTaskExecutor + ); + // 根据返回的结果获取需要的扩展字段 + CompletableFuture extendPropFuture = CompletableFuture.runAsync( + () -> weCustomerExtendPropertyService.setExtendPropertyForCustomers(corpId, list), threadPoolTaskExecutor + ); + CompletableFuture.allOf(tagFuture, extendPropFuture).join(); + return list; + } + @Override @DataScope + @Deprecated public AjaxResult export(WeCustomerExportDTO dto) { - List selectProperties=dto.getSelectedProperties(); - WeCustomerSearchDTO weCustomerSearchDTO=new WeCustomerSearchDTO(); + List selectProperties = dto.getSelectedProperties(); + WeCustomerSearchDTO weCustomerSearchDTO = new WeCustomerSearchDTO(); BeanUtils.copyProperties(dto, weCustomerSearchDTO); - WeCustomer weCustomer=changeWecustomer(weCustomerSearchDTO); + WeCustomer weCustomer = changeWecustomer(weCustomerSearchDTO); List list = this.selectWeCustomerListV3(weCustomer); if (CollectionUtils.isEmpty(list)) { throw new CustomException(ResultTip.TIP_NO_DATA_TO_EXPORT); } List exportList = list.stream().map(WeCustomerExportVO::new).collect(Collectors.toList()); - weCustomerExtendPropertyService.setKeyValueMapper(weCustomer.getCorpId(), exportList, selectProperties); + ExtendPropHolder extendPropHolder = new ExtendPropHolder(dto.getCorpId(), dto.getSelectedProperties()); + weCustomerExtendPropertyService.setKeyValueMapper(weCustomer.getCorpId(), exportList, selectProperties, extendPropHolder); ExcelUtil util = new ExcelUtil<>(WeCustomerExportVO.class); return util.exportExcelV2(exportList, "customer", selectProperties); } + @Override + @DataScope + public void genExportData(WeCustomerExportDTO dto, String oprId, String fileName) { + WeCustomerSearchDTO weCustomerSearchDTO = new WeCustomerSearchDTO(); + List selectProperties = dto.getSelectedProperties(); + BeanUtils.copyProperties(dto, weCustomerSearchDTO); + // 获取导出客户的条件 + WeCustomer weCustomer = changeWecustomer(weCustomerSearchDTO); + // 分批导出客户 + OutputStream outputStream = null; + try { + outputStream = Files.newOutputStream(Paths.get(getAbsoluteFile(fileName))); + Stopwatch stopwatch = Stopwatch.createStarted(); + ExcelUtil util = new ExcelUtil<>(WeCustomerExportVO.class, selectProperties); + List> fields = util.getFields(); + Integer totalCount = weCustomerMapper.selectWeCustomerCount(weCustomer); + //每一个Sheet存放数据 + Integer sheetDataRows = EXPORT_SHEET_ROWS; + //每次写入的数据量 + int writeDataRows = EXPORT_QUERY_BATCH_SIZE; + //计算需要的Sheet数量 + int sheetNum = totalCount % sheetDataRows == 0 ? (totalCount / sheetDataRows) : (totalCount / sheetDataRows + 1); + //计算一般情况下每一个Sheet需要写入的次数(一般情况不包含后一个sheet,因为后一个sheet不确定会写入多少条数据) + int oneSheetWriteCount = sheetDataRows / writeDataRows; + //计算后一个sheet需要写入的次数 + int lastSheetWriteCount = totalCount % sheetDataRows == 0 + ? oneSheetWriteCount + : ((totalCount - (totalCount / sheetDataRows) * sheetDataRows) % writeDataRows == 0 ? (totalCount - (totalCount / sheetDataRows) * sheetDataRows) / writeDataRows : ((totalCount - (totalCount / sheetDataRows) * sheetDataRows) / writeDataRows + 1)); //开始分批查询分次写入 + //注意这次的循环就需要进行嵌套循环了,外层循环是Sheet数目,内层循环是写入次数 + List> dataList = new ArrayList<>(); + //todo 待封装 样式 + HorizontalCellStyleStrategy horizontalCellStyleStrategy = getHorizontalCellStyleStrategy(); + ExcelWriter excelWriter = EasyExcel.write(outputStream) + .excludeColumnFiledNames(Arrays.asList("extendPropMapper", "extendProperties", "weFlowerCustomerRels", "extendList", "relIds", "params")) + .registerWriteHandler(horizontalCellStyleStrategy) + .registerWriteHandler(new SimpleColumnWidthStyleStrategy(25)) + .build(); + ExtendPropHolder extendPropHolder = new ExtendPropHolder(dto.getCorpId(), selectProperties); + for (int i = 0; i < sheetNum; i++) { + //创建Sheet + //循环写入次数: j的自增条件是当不是后一个Sheet的时候写入次数为正常的每个Sheet写入的次数,如果是后一个就需要使用计算的次数lastSheetWriteCount + for (int j = 0; j < (i != sheetNum - 1 ? oneSheetWriteCount : lastSheetWriteCount); j++) { + dataList.clear(); + //分页查询 + PageHelper.startPage(j + 1 + oneSheetWriteCount * i, writeDataRows, false); + List resultList = selectExportCustomer(weCustomer, selectProperties, extendPropHolder); + WriteSheet writeSheet = EasyExcel.writerSheet(i, "客户" + (i + 1)).head(fields).build(); + excelWriter.write(getExportDataBySelectedProp(resultList, selectProperties, extendPropHolder), writeSheet); + } + } + // 下载EXCEL + excelWriter.finish(); + outputStream.flush(); + //导出时间结束 + stopwatch.stop(); + // 获取经过的时间并以毫秒为单位打印出来 + long elapsedTimeMillis = stopwatch.elapsed(TimeUnit.SECONDS); + log.info("[导出客户] 耗时:{} 秒 ,总量:{} , oprId:{} ", elapsedTimeMillis,totalCount,oprId); + // 设置导出完成 + customerRedisCache.setExportFinished(oprId); + } catch (Exception e) { + log.error("导出客户异常.e:{}", ExceptionUtils.getStackTrace(e)); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (Exception e) { + log.error("[导出客户]关闭流异常,orpId:{}", oprId); + } + } + } + } + + /** + * 获取样式 + * @return + */ + private HorizontalCellStyleStrategy getHorizontalCellStyleStrategy() { + // 头的策略 + WriteCellStyle headWriteCellStyle = new WriteCellStyle(); + // 背景设置 + headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex()); + headWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER); + headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); + headWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND); + WriteFont headWriteFont = new WriteFont(); + headWriteFont.setFontHeightInPoints((short) 10); + headWriteFont.setBold(true); + headWriteFont.setColor(IndexedColors.WHITE.getIndex()); + headWriteFont.setFontName("Arial"); + headWriteCellStyle.setWriteFont(headWriteFont); + // 内容的策略 + WriteCellStyle contentWriteCellStyle = new WriteCellStyle(); + // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定 +// contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND); + contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); + contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER); + contentWriteCellStyle.setBorderRight(BorderStyle.THIN); + contentWriteCellStyle.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + contentWriteCellStyle.setBorderLeft(BorderStyle.THIN); + contentWriteCellStyle.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + contentWriteCellStyle.setBorderTop(BorderStyle.THIN); + contentWriteCellStyle.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + contentWriteCellStyle.setBorderBottom(BorderStyle.THIN); + contentWriteCellStyle.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + + // 背景绿色 +// contentWriteCellStyle.setFillForegroundColor(IndexedColors.GREEN.getIndex()); + WriteFont contentWriteFont = new WriteFont(); + contentWriteFont.setFontName("Arial"); + // 字体大小 + contentWriteFont.setFontHeightInPoints((short) 10); + contentWriteCellStyle.setWriteFont(contentWriteFont); + // 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现 + HorizontalCellStyleStrategy horizontalCellStyleStrategy = + new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle); + return horizontalCellStyleStrategy; + } + + + /** + * 根据自定义字段和数据获取需要导出的最终数据 + * + * @param resultList 结果数据 + * @param selectProperties 需要导出的表头 + * @param extendPropHolder + * @return + */ + private List getExportDataBySelectedProp(List resultList, List selectProperties, ExtendPropHolder extendPropHolder) { + List list = new ArrayList(); + for (WeCustomerExportVO element : resultList) { + List rowData = new ArrayList<>(); + for (String propName : selectProperties) { + Class currClazz = element.getClass(); + List fields = new ArrayList<>(); + while (currClazz != null) { + Field[] declaredFields = currClazz.getDeclaredFields(); + fields.addAll(Arrays.asList(declaredFields)); + currClazz = currClazz.getSuperclass(); + } + for (Field field : fields) { + Excel excel = field.getDeclaredAnnotation(Excel.class); + if (excel != null) { + // 判断基础字段 + if (propName.equals(excel.name())) { + // 在这里,你可以获取字段的值并将其添加到 rowData 中 + try { + if (!field.isAccessible()) { + field.setAccessible(true); + } + Object fieldValue = formatValue(element, field, excel); + rowData.add(fieldValue); + continue; + } catch (Exception e) { + log.error("设置字段值异常 e;{}", ExceptionUtils.getStackTrace(e)); + } + } + } + } + if (extendPropHolder.isHasExtendProp()) { + for (Map.Entry entry : element.getExtendPropMapper().entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (key.equals(propName)) { + rowData.add(value); + } + } + } + } + list.add(rowData); + } + return list; + } + + /** + * 根据注解格式化设置字段的值 + * + * @param element + * @param field + * @param excel + * @return + * @throws IllegalAccessException + */ + private Object formatValue(WeCustomerExportVO element, Field field, Excel excel) throws IllegalAccessException { + Object fieldValue = field.get(element); + String dateFormat = excel.dateFormat(); + String readConverterExp = excel.readConverterExp(); + String separator = excel.separator(); + String dictType = excel.dictType(); + if (com.easyink.common.utils.StringUtils.isNotEmpty(dateFormat) && com.easyink.common.utils.StringUtils.isNotNull(fieldValue)) { + fieldValue = DateUtils.parseDateToStr(dateFormat, (Date) fieldValue); + } else if (com.easyink.common.utils.StringUtils.isNotEmpty(readConverterExp) && com.easyink.common.utils.StringUtils.isNotNull(fieldValue)) { + fieldValue = ExcelUtil.convertByExp(Convert.toStr(fieldValue), readConverterExp, separator); + } else if (com.easyink.common.utils.StringUtils.isNotEmpty(dictType) && com.easyink.common.utils.StringUtils.isNotNull(fieldValue)) { + fieldValue = ExcelUtil.convertDictByExp(Convert.toStr(fieldValue), dictType, separator); + } else if (fieldValue instanceof BigDecimal && -1 != excel.scale()) { + fieldValue = (((BigDecimal) fieldValue).setScale(excel.scale(), excel.roundingMode())).toString(); + } + return fieldValue; + } + + @Override + public Boolean getExportResult(String oprId) { + return customerRedisCache.hasExportFinished(oprId); + } + + @Override + @DataScope + public WeCustomerExportDTO transferData(WeCustomerExportDTO dto) { + return dto; + } + + public String getAbsoluteFile(String filename) { + String downloadPath = RuoYiConfig.getDownloadPath() + filename; + File desc = new File(downloadPath); + if (!desc.getParentFile().exists()) { + desc.getParentFile().mkdirs(); + } + return downloadPath; + } + + /** * 模糊查询客户 (无需登录可用) * @@ -1488,6 +1754,7 @@ public String getOpenExUserId(String corpId, String externalUserId) { return weExternalUseridMapping.getOpenExternalUserid(); } + /** * 通过接口将外部联系人exUserId转化为密文 * diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeFlowerCustomerRelServiceImpl.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeFlowerCustomerRelServiceImpl.java index 794e6105..1731ee71 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeFlowerCustomerRelServiceImpl.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeFlowerCustomerRelServiceImpl.java @@ -6,6 +6,9 @@ import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; import com.easyink.common.constant.Constants; import com.easyink.common.constant.GenConstants; +import com.easyink.common.constant.WeConstans; +import com.easyink.common.core.domain.entity.WeCorpAccount; +import com.easyink.common.core.domain.wecom.WeUser; import com.easyink.common.enums.AddWayEnum; import com.easyink.common.enums.CustomerStatusEnum; import com.easyink.common.enums.ResultTip; @@ -13,20 +16,23 @@ import com.easyink.common.exception.CustomException; import com.easyink.common.utils.DateUtils; import com.easyink.common.utils.SnowFlakeUtil; +import com.easyink.common.utils.sql.BatchInsertUtil; import com.easyink.wecom.domain.WeCustomer; import com.easyink.wecom.domain.WeFlowerCustomerRel; +import com.easyink.wecom.domain.WeUserBehaviorData; import com.easyink.wecom.domain.dto.customer.resp.GetByUserResp; import com.easyink.wecom.domain.entity.customer.WeCustomerExtendPropertyRel; import com.easyink.wecom.domain.entity.transfer.WeCustomerTransferConfig; import com.easyink.wecom.mapper.WeCustomerExtendPropertyRelMapper; import com.easyink.wecom.mapper.WeFlowerCustomerRelMapper; -import com.easyink.wecom.service.WeCustomerTransferConfigService; -import com.easyink.wecom.service.WeFlowerCustomerRelService; -import com.easyink.wecom.service.WeFlowerCustomerTagRelService; +import com.easyink.wecom.mapper.WeUserBehaviorDataMapper; +import com.easyink.wecom.service.*; import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; @@ -34,6 +40,7 @@ import javax.validation.constraints.NotNull; import java.util.*; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -50,13 +57,19 @@ public class WeFlowerCustomerRelServiceImpl extends ServiceImpl() + .eq(WeFlowerCustomerRel::getUserId, userId) + .eq(WeFlowerCustomerRel::getExternalUserid, externalUserid) + .eq(WeFlowerCustomerRel::getCorpId, corpId) + // 员工删除客户,可能删除的是正常状态的客户(单删),也可能是客户已经将员工删除的客户(双删),二者都需要更新状态 + .in(WeFlowerCustomerRel::getStatus, CustomerStatusEnum.NORMAL.getCode(), CustomerStatusEnum.DRAIN.getCode())); + } + @Override public Map getUserAddCustomerStat(WeFlowerCustomerRel weFlowerCustomerRel) { if (StringUtils.isAnyBlank(weFlowerCustomerRel.getCorpId(), weFlowerCustomerRel.getState())) { @@ -182,33 +213,81 @@ public WeFlowerCustomerRel getLastUser(String externalUserid, String corpId) { } @Override - public void alignData(GetByUserResp resp, String userId, String corpId) { + public void alignData(GetByUserResp resp, String userId, String corpId, List localRelList) { + // 删除远端不存在的本地客户 + delNotExistCustomer(resp, userId, corpId,localRelList); + if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(localRelList)) { + // 更新客户信息 + updateCustomerRel(resp, localRelList, corpId); + } + } + + /** + * 更新流失后添加的客户数据,客户-员工关系 + * + * @param resp 企微响应 + * @param localRelList 本地数据 + */ + public void updateCustomerRel(GetByUserResp resp, List localRelList, String corpId) { + List updateRelList = new ArrayList<>(); + Map localMap = localRelList.stream() + .collect(Collectors.toMap(WeFlowerCustomerRel::getExternalUserid, Function.identity())); + // 本地数据与远端数据对比 + for (WeFlowerCustomerRel remoteFlowerCustomerRel : resp.getRelList()) { + WeFlowerCustomerRel localFlowerCustomerRel = localMap.get(remoteFlowerCustomerRel.getExternalUserid()); + // 若该客户与本地记录的添加时间不一样,表示此客户是流失后重新添加员工的客户,将该客户存入列表等待更新状态 + if (localFlowerCustomerRel != null) { + if (remoteFlowerCustomerRel.getCreateTime().compareTo(localFlowerCustomerRel.getCreateTime()) != 0) { + remoteFlowerCustomerRel.setStatus(Constants.NORMAL_CODE); + updateRelList.add(remoteFlowerCustomerRel); + } + } + } + // 存在创建时间不同的客户则更新客户状态 + if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(updateRelList)) { + LambdaUpdateWrapper updateRelWrapper = new LambdaUpdateWrapper<>(); + updateRelWrapper.eq(WeFlowerCustomerRel::getCorpId, corpId); + BatchInsertUtil.doInsert(updateRelList, this::batchUpdateStatus); + } + } + + /** + * 删除 不存在于远端的客户 + * + * @param resp 获取客户响应 + * @param userId 员工ID + * @param corpId 企业ID + * @param localRelList + */ + public void delNotExistCustomer(GetByUserResp resp, String userId, String corpId, List localRelList) { List customerList = resp.getCustomerList(); - List externalUserIdList = resp.getCustomerList().stream().map(WeCustomer::getExternalUserid).collect(Collectors.toList()); + List externalUserIdList = resp.getCustomerList() + .stream() + .map(WeCustomer::getExternalUserid) + .collect(Collectors.toList()); // 1. 把不存在的客户至为删除状态 if (CollectionUtils.isEmpty(customerList)) { //员工不存在客户,修改员工的正常状态为员工删除客户状态 this.update(WeFlowerCustomerRel.builder() - .userId(userId) - .deleteTime(DateUtils.getNowDate()) - .status(Constants.DELETE_CODE) - .build() + .userId(userId) + .deleteTime(DateUtils.getNowDate()) + .status(Constants.DELETE_CODE) + .build() , new LambdaQueryWrapper() .eq(WeFlowerCustomerRel::getUserId, userId) .eq(WeFlowerCustomerRel::getCorpId, corpId)); } else { - //存在客户,删除本地数据库里所有不存在的客户————————————————问题同上 + //存在客户,删除本地数据库里所有不存在的客户 this.update(WeFlowerCustomerRel.builder() - .userId(userId) - .deleteTime(DateUtils.getNowDate()) - .status(Constants.DELETE_CODE) - .build() + .userId(userId) + .deleteTime(DateUtils.getNowDate()) + .status(Constants.DELETE_CODE) + .build() , new LambdaQueryWrapper() .eq(WeFlowerCustomerRel::getUserId, userId) .notIn(WeFlowerCustomerRel::getExternalUserid, externalUserIdList) .eq(WeFlowerCustomerRel::getCorpId, corpId)); } - } @Override @@ -348,6 +427,81 @@ public Integer getCurrentNewCustomerCnt(String corpId, String beginTime, String return this.count(queryWrapper); } + /** + * 根据开始和结束时间,获取首页-数据总览-客户总数 + * + * @param corpId 企业ID + * @param beginTime 开始时间,格式为YYYY-MM-DD 00:00:00 + * @param endTime 结束时间,格式为YYYY-MM-DD 23:59:59 + * @return 首页-数据总览-客户总数 + */ + @Override + public Integer getTotalAllContactCnt(String corpId, String beginTime, String endTime) { + if(StringUtils.isAnyBlank(corpId, beginTime, endTime)) { + throw new CustomException(ResultTip.TIP_PARAM_MISSING); + } + int totalExternalCnt = 0; + // 获取企业下所有的员工 + List userList = weUserService.list(new LambdaQueryWrapper().eq(WeUser::getCorpId, corpId)); + // 获取状态为已激活的员工 + List nomalUserIdList = userList.stream().filter(item -> WeConstans.WE_USER_IS_ACTIVATE.equals(item.getIsActivate())).map(WeUser::getUserId).collect(Collectors.toList()); + if (org.apache.commons.collections.CollectionUtils.isNotEmpty(nomalUserIdList)) { + // 获取已激活的员工下的客户总数 + totalExternalCnt += weFlowerCustomerRelMapper.getNormalTotalAllContactCnt(corpId, beginTime, endTime, nomalUserIdList); + } + // 获取状态为已删除的员工 + List delUserIdList = userList.stream().filter(item -> WeConstans.WE_USER_IS_LEAVE.equals(item.getIsActivate())).map(WeUser::getUserId).collect(Collectors.toList()); + if (org.apache.commons.collections.CollectionUtils.isNotEmpty(delUserIdList)) { + // 获取已删除的员工下的客户总数 + totalExternalCnt += weFlowerCustomerRelMapper.getDelTotalAllContactCnt(corpId, beginTime, endTime, delUserIdList); + } + return totalExternalCnt; + } + + /** + * 数据统计-联系客户-客户总数-旧数据兼容 + */ + @Override + @Transactional(rollbackFor = Exception.class) + @Async + public void updateTotalAllCustomerCnt() { + List weCorpAccountList = weCorpAccountService.listOfAuthCorpInternalWeCorpAccount(); + if (com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(weCorpAccountList)) { + log.info("[数据统计-联系客户-客户总数-旧数据兼容] 当前无可更新信息的企业,请检查配置或联系管理员"); + return; + } + // 获取当前日期的前180天日期范围列表 + List dateAgoList = DateUtils.get180DateAgoList(); + weCorpAccountList.forEach(corpAccount -> { + String corpId = corpAccount.getCorpId(); + log.info("[数据统计-联系客户-客户总数-旧数据兼容] 开始旧数据兼容,当前企业corpId:{}", corpId); + try { + // 获取企业下所有在职员工 + List userList = weUserService.list(new LambdaQueryWrapper().eq(WeUser::getIsActivate, WeConstans.WE_USER_IS_ACTIVATE).eq(WeUser::getCorpId, corpId)); + // 获取员工id列表 + List userIdList = userList.stream().map(WeUser::getUserId).collect(Collectors.toList()); + // 最后需要更新的信息列表 + List updateList = new ArrayList<>(); + for (String date : dateAgoList) { + // 转换当前时间 + Date statTime = DateUtils.dateTime(DateUtils.YYYY_MM_DD, date); + // 获取每个员工对应日期下的客户总数 + List countList = weFlowerCustomerRelMapper.getTotalAllContactCntByUserId(corpId, DateUtils.parseEndDay(date), userIdList); + // 设置日期时间 + countList.forEach(item -> { + item.setStatTime(statTime); + }); + updateList.addAll(countList); + } + // 批量更新 + BatchInsertUtil.doInsert(updateList, weUserBehaviorDataMapper::saveBatchUpdateOrInsert); + log.info("[数据统计-联系客户-客户总数-旧数据兼容] 旧数据统计结束,本次更新数据条数:{}", updateList.size()); + } catch (Exception e) { + log.info("[数据统计-联系客户-客户总数-旧数据兼容] 出现异常,当前企业corpId:{},异常原因:{}", corpId, ExceptionUtils.getStackTrace(e)); + } + }); + } + /** * 更新已流失重新添加回来的客户状态 * diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeFlowerCustomerTagRelServiceImpl.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeFlowerCustomerTagRelServiceImpl.java index 2f1d0ea9..6652923f 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeFlowerCustomerTagRelServiceImpl.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeFlowerCustomerTagRelServiceImpl.java @@ -162,11 +162,19 @@ public void syncTagFromRemote(WeFlowerCustomerRel localRel, List() + weFlowerCustomerTagRelMapper.delete(new LambdaQueryWrapper() .eq(WeFlowerCustomerTagRel::getFlowerCustomerRelId, localRel.getId()) ); if (CollectionUtils.isNotEmpty(tagRelList)) { - BatchInsertUtil.doInsert(tagRelList, this::batchInsert); + BatchInsertUtil.doInsert(tagRelList, weFlowerCustomerTagRelMapper::batchInsert); } } + + @Override + @Transactional(rollbackFor = Exception.class) + public void syncLocalTagFromRemote(List tagRelList, List relIds) { + BatchInsertUtil.doInsert(relIds, list -> weFlowerCustomerTagRelMapper.delete(new LambdaQueryWrapper() + .in(WeFlowerCustomerTagRel::getFlowerCustomerRelId,list))); + BatchInsertUtil.doInsert(tagRelList, weFlowerCustomerTagRelMapper::batchInsert); + } } diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeSensitiveServiceImpl.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeSensitiveServiceImpl.java index fd215f9d..51f30fd3 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeSensitiveServiceImpl.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeSensitiveServiceImpl.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.easyink.common.constant.GroupConstants; import com.easyink.common.constant.WeConstans; +import com.easyink.common.constant.conversation.ConversationArchiveConstants; import com.easyink.common.core.domain.conversation.ChatInfoVO; import com.easyink.common.core.domain.elastic.ElasticSearchEntity; import com.easyink.common.core.domain.entity.WeCorpAccount; @@ -328,6 +329,7 @@ private PageInfo hitPageInfoHandler(PageInfo pageInfo, S JSONObject json = new JSONObject(); String userId = j.getString(WeConstans.FROM); WeUserVO userVO = weUserService.getUser(corpId, userId); + json.put(ConversationArchiveConstants.MSG_ID, j.get(WeConstans.MSG_ID)); json.put(WeConstans.FROM, userVO.getUserName()); json.put(WeConstans.CONTENT, j.getJSONObject(WeConstans.TEXT).getString(WeConstans.CONTENT)); json.put(WeConstans.MSG_TIME, j.getString(WeConstans.MSG_TIME)); diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeUserCustomerMessageStatisticsServiceImpl.java b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeUserCustomerMessageStatisticsServiceImpl.java index 17198f09..a7d0bc48 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeUserCustomerMessageStatisticsServiceImpl.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/service/impl/WeUserCustomerMessageStatisticsServiceImpl.java @@ -128,6 +128,8 @@ public void getMessageStatistics(String corpId, String time) { } // 获取前一天的数据 String yesterday = time; + String beginTime = DateUtils.parseBeginDay(yesterday); + String endTime = DateUtils.parseEndDay(yesterday); visibleUser.forEach(weUser -> { try { //根据员工id在会话存档中获取全部对话 @@ -171,7 +173,11 @@ public void getMessageStatistics(String corpId, String time) { } } } - WeUserBehaviorData totalContactAndLossCnt = weFlowerCustomerRelMapper.getTotalContactAndLossCnt(userBehaviorData.getUserId(), userBehaviorData.getCorpId(),DateUtils.parseBeginDay(yesterday), DateUtils.parseEndDay(yesterday)); + WeUserBehaviorData totalContactAndLossCnt = weFlowerCustomerRelMapper.getTotalContactAndLossCnt(userBehaviorData.getUserId(), userBehaviorData.getCorpId(), beginTime, endTime); + List userIdList = new ArrayList<>(); + userIdList.add(weUser.getUserId()); + Integer totalAllContactCnt = weFlowerCustomerRelMapper.getNormalTotalAllContactCnt(corpId, beginTime, endTime, userIdList); + userBehaviorData.setTotalAllContactCnt(totalAllContactCnt); userBehaviorData.setContactTotalCnt(totalContactAndLossCnt.getContactTotalCnt()); userBehaviorData.setNewCustomerLossCnt(totalContactAndLossCnt.getNewCustomerLossCnt()); weUserBehaviorDataService.updateById(userBehaviorData); @@ -465,7 +471,7 @@ public List getCustomerOverViewOfDate(CustomerOverviewDT if (DateUtils.dateTime(weUserBehaviorData.getStatTime()).equals(DateUtils.dateTime(date))) { for (UserServiceTimeDTO userServiceTimeDTO : userServiceTimeDTOList) { // 判断是否有对话且时间相等, 且为员工主动发起的会话 - if (Objects.equals(userServiceTimeDTO.getUserId(), weUserBehaviorData.getUserId()) && Objects.equals(judgeTime, userServiceTimeDTO.getSendTime()) && userServiceTimeDTO.getUserActiveDialogue() == ContactSpeakEnum.USER.getCode()) { + if (Objects.equals(userServiceTimeDTO.getUserId(), weUserBehaviorData.getUserId()) && Objects.equals(judgeTime, userServiceTimeDTO.getSendTime()) && ContactSpeakEnum.USER.getCode().equals(userServiceTimeDTO.getUserActiveDialogue())) { chatCnt++; } } @@ -477,7 +483,7 @@ public List getCustomerOverViewOfDate(CustomerOverviewDT weUserBehaviorData.getNewContactCnt(), weUserBehaviorData.getNewContactSpeakCnt(), weUserBehaviorData.getRepliedWithinThirtyMinCustomerCnt() - ,chatCnt); + ,chatCnt, weUserBehaviorData.getTotalAllContactCnt()); } } for (CustomerOverviewVO customerOverviewVO : currNewCustomerCntByTime) { diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/utils/OprIdGenerator.java b/easyink-wecom/src/main/java/com/easyink/wecom/utils/OprIdGenerator.java new file mode 100644 index 00000000..6c1a9a68 --- /dev/null +++ b/easyink-wecom/src/main/java/com/easyink/wecom/utils/OprIdGenerator.java @@ -0,0 +1,51 @@ +package com.easyink.wecom.utils; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.UUID; + +/** + * 类名: oprId 生成器 调用示例:OprIdGenerator.EXPORT.get() + * + * @author : silver_chariot + * @date : 2022/10/13 14:31 + **/ +@Slf4j +@AllArgsConstructor +@Getter +public enum OprIdGenerator { + /** + * 导出操作 + */ + EXPORT("export-"), + + ; + /** + * traceId 前缀 + */ + private final String prefix; + + /** + * 生成随机uuid + * + * @return uuid + */ + private String genUuid() { + return UUID.randomUUID() + .toString() + .replaceAll("-", ""); + } + + /** + * 获取完整的traceId , 使用示例:TraceIdGenerator.RPA.get() + * + * @return TraceId + */ + public String get() { + return getPrefix() + genUuid(); + } + + +} diff --git a/easyink-wecom/src/main/java/com/easyink/wecom/utils/redis/CustomerRedisCache.java b/easyink-wecom/src/main/java/com/easyink/wecom/utils/redis/CustomerRedisCache.java index 487cbb55..f486e99e 100644 --- a/easyink-wecom/src/main/java/com/easyink/wecom/utils/redis/CustomerRedisCache.java +++ b/easyink-wecom/src/main/java/com/easyink/wecom/utils/redis/CustomerRedisCache.java @@ -13,6 +13,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; /** * 类名: 客户redis工具类 @@ -27,6 +28,8 @@ public class CustomerRedisCache extends RedisCache { * 编辑客户回调处理redis key */ private static final String CALL_BACK_EDIT_CUSTOMER_KEY = "callbackEditCustomer:"; + + private static final String EXPORT_CUSTOMER_RESULT = "exportCustomerResult:"; /** * 存入redis的userid和externnaluserid之间的间隔 */ @@ -34,6 +37,46 @@ public class CustomerRedisCache extends RedisCache { private static final int USER_ID_INDEX = 0; private static final int EXTERNAL_USER_ID = 1; + /** + * 获取导出客户结果的Key + * + * @param oprId 操作id + * @return key + */ + private String getExportCustomerResultKey(String oprId) { + return EXPORT_CUSTOMER_RESULT + oprId; + } + + /** + * 设置导出已完成 + * + * @param oprId 操作id + */ + public void setExportFinished(String oprId) { + if (StringUtils.isBlank(oprId)) { + return; + } + setCacheObject(getExportCustomerResultKey(oprId), true,24, TimeUnit.HOURS); + } + + /** + * 判断是导出是否完成 + * + * @param oprId 操作ID + * @return true or false + */ + public Boolean hasExportFinished(String oprId) { + if (StringUtils.isBlank(oprId)) { + return false; + } + Boolean flag = getCacheObject(getExportCustomerResultKey(oprId)); + if (flag == null) { + return false; + } + return flag; + } + + /** * 获取 编辑客户回调处理redis key * diff --git a/easyink-wecom/src/main/resources/mapper/wecom/WeCustomerMapper.xml b/easyink-wecom/src/main/resources/mapper/wecom/WeCustomerMapper.xml index 1bbcaafc..47ba0557 100644 --- a/easyink-wecom/src/main/resources/mapper/wecom/WeCustomerMapper.xml +++ b/easyink-wecom/src/main/resources/mapper/wecom/WeCustomerMapper.xml @@ -974,10 +974,8 @@ wc.avatar, wc.type, wc.gender, - wc.unionid, wc.birthday, wc.corp_name, - wc.corp_full_name, wc.position, wc.is_open_chat, wfcr.id as relId, @@ -986,16 +984,10 @@ wfcr.description, wfcr.remark_mobiles, wfcr.create_time, - wfcr.remark_corp_name, - wfcr.oper_userid, wfcr.add_way, - wfcr.state, wfcr.status, wfcr.address, wfcr.email, - wfcr.wechat_channel, - wu.user_name, - wu.dimission_time, wfcrf.tag_id, wfcrf.flower_customer_rel_id, wcepr.corp_id as wceprCorpId, @@ -1003,17 +995,20 @@ wcepr.property_value, wcepr.extend_property_id, wcepr.user_id as wceprUserId, - wd.name as department + wd.name as department, + wu.user_name FROM we_customer wc LEFT JOIN we_flower_customer_rel wfcr ON wc.external_userid=wfcr.external_userid AND wc.corp_id = wfcr.corp_id - LEFT JOIN we_user wu ON wu.user_id=wfcr.user_id AND wu.corp_id = wc.corp_id LEFT JOIN we_flower_customer_tag_rel wfcrf ON wfcrf.flower_customer_rel_id = wfcr.id + LEFT JOIN we_user wu ON wu.user_id=wfcr.user_id AND wu.corp_id = wc.corp_id LEFT JOIN we_customer_extend_property_rel wcepr ON wcepr.corp_id = wc.corp_id AND wcepr.external_userid = wc.external_userid and wfcr.user_id = wcepr.user_id LEFT JOIN we_department wd ON wc.corp_id = wd.corp_id AND wu.main_department = wd.id - LIMIT 5000 + GROUP BY wfcr.external_userid,wfcr.user_id + ORDER BY + wfcr.create_time DESC + + diff --git a/easyink-wecom/src/main/resources/mapper/wecom/WeCustomerMessgaeResultMapper.xml b/easyink-wecom/src/main/resources/mapper/wecom/WeCustomerMessgaeResultMapper.xml index 547a8c0d..0d500319 100644 --- a/easyink-wecom/src/main/resources/mapper/wecom/WeCustomerMessgaeResultMapper.xml +++ b/easyink-wecom/src/main/resources/mapper/wecom/WeCustomerMessgaeResultMapper.xml @@ -297,4 +297,25 @@ message_id =#{messageId} AND `status`!='0' + + + \ No newline at end of file diff --git a/easyink-wecom/src/main/resources/mapper/wecom/WeFlowerCustomerRelMapper.xml b/easyink-wecom/src/main/resources/mapper/wecom/WeFlowerCustomerRelMapper.xml index 19e730d7..a51e3cc1 100644 --- a/easyink-wecom/src/main/resources/mapper/wecom/WeFlowerCustomerRelMapper.xml +++ b/easyink-wecom/src/main/resources/mapper/wecom/WeFlowerCustomerRelMapper.xml @@ -362,7 +362,7 @@ WHERE corp_id = #{corpId} AND user_id = #{userId} - AND status = '${@com.easyink.common.enums.CustomerStatusEnum@NORMAL.code}' + AND status NOT IN ('${@com.easyink.common.enums.CustomerStatusEnum@DRAIN.code}', '${@com.easyink.common.enums.CustomerStatusEnum@DELETE.code}') AND create_time <= #{endTime} ), 0) AS contactTotalCnt @@ -604,4 +604,51 @@ GROUP BY `date` + + + + + + \ No newline at end of file diff --git a/easyink-wecom/src/main/resources/mapper/wecom/WeUserBehaviorDataMapper.xml b/easyink-wecom/src/main/resources/mapper/wecom/WeUserBehaviorDataMapper.xml index 075a575a..bd6ae1d3 100644 --- a/easyink-wecom/src/main/resources/mapper/wecom/WeUserBehaviorDataMapper.xml +++ b/easyink-wecom/src/main/resources/mapper/wecom/WeUserBehaviorDataMapper.xml @@ -3,6 +3,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> + + + INSERT INTO we_user_behavior_data + (corp_id, user_id, stat_time, total_all_contact_cnt) + VALUES + + ( + #{item.corpId}, #{item.userId}, #{item.statTime}, #{item.totalAllContactCnt} + ) + + ON DUPLICATE KEY UPDATE + total_all_contact_cnt = VALUES(total_all_contact_cnt) + INSERT INTO we_user_behavior_data @@ -22,6 +35,13 @@ resultType="com.easyink.wecom.domain.vo.statistics.CustomerOverviewVO"> select IFNULL( (SELECT + bd.total_all_contact_cnt + FROM we_user_behavior_data bd + WHERE bd.corp_id = #{corpId} + AND bd.stat_time = #{endDate} + AND bd.user_id = ubd.user_id + ) ,0) as totalAllContactCnt, + IFNULL( (SELECT bd.contact_total_cnt FROM we_user_behavior_data bd WHERE bd.corp_id = #{corpId} @@ -68,6 +88,9 @@ GROUP BY ubd.user_id + + ORDER BY totalAllContactCnt ${totalAllContactCntSort} + ORDER BY totalContactCnt ${totalContactCntSort} @@ -115,6 +138,21 @@ resultType="com.easyink.wecom.domain.vo.statistics.CustomerOverviewVO"> select IFNULL( (SELECT + SUM(bd.total_all_contact_cnt) + FROM we_user_behavior_data bd + LEFT JOIN we_user wu ON wu.corp_id = bd.corp_id AND wu.user_id = bd.user_id + WHERE bd.corp_id = #{corpId} + AND bd.stat_time = #{endDate} + + AND bd.user_id IN + + #{userId} + + + ${params.dataScope} + AND wu.is_activate = ${@com.easyink.common.constant.WeConstans@WE_USER_IS_ACTIVATE} + ) , 0) as totalAllContactCnt, + IFNULL( (SELECT SUM(bd.contact_total_cnt) FROM we_user_behavior_data bd LEFT JOIN we_user wu ON wu.corp_id = bd.corp_id AND wu.user_id = bd.user_id @@ -276,6 +314,7 @@ select ubd.user_id, ubd.stat_time as statTime, + IFNULL(ubd.total_all_contact_cnt,0) as totalAllContactCnt, IFNULL(ubd.contact_total_cnt,0) as contactTotalCnt, IFNULL(ubd.new_contact_cnt,0) as newContactCnt, IFNULL(ubd.negative_feedback_cnt,0) as negativeFeedbackCnt, diff --git a/sql/init/easyink.sql b/sql/init/easyink.sql index 2b8e3ad2..7bc14dbf 100644 --- a/sql/init/easyink.sql +++ b/sql/init/easyink.sql @@ -1669,8 +1669,9 @@ CREATE TABLE `we_flower_customer_rel` `email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '邮件', `add_way` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '该成员添加此客户的来源,', `state` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '企业自定义的state参数,用于区分客户具体是通过哪个「联系我」添加,由企业通过创建「联系我」方式指定', - `status` char(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '状态(0正常 1删除流失 2员工删除用户)', - `delete_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '删除时间', + `status` char(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '状态(0正常 1删除流失 2员工删除用户 3待继承 4转接中)', + `delete_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '客户删除员工时间', + `del_by_user_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '员工删除客户时间(如果没有收到删除回调,通过手动同步客户得出的主动删除客户,该字段为默认值)', `wechat_channel` varchar(64) NOT NULL DEFAULT '' COMMENT '该成员添加此客户的来源add_way为10时,对应的视频号信息', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `un_user_external_userid_corpid` (`external_userid`, `user_id`, `corp_id`) USING BTREE, @@ -2166,7 +2167,8 @@ CREATE TABLE `we_user_behavior_data` ( `reply_percentage` double NOT NULL DEFAULT '0' COMMENT '已回复聊天占比,浮点型,客户主动发起聊天后,成员在一个自然日内有回复过消息的聊天数/客户主动发起的聊天数比例,不包括群聊,仅在确有聊天时返回', `avg_reply_time` int(10) NOT NULL DEFAULT '0' COMMENT '平均首次回复时长', `negative_feedback_cnt` int(11) NOT NULL DEFAULT '0' COMMENT '删除/拉黑成员的客户数,即将成员删除或加入黑名单的客户数', - `total_contact_cnt` int(11) NOT NULL DEFAULT '0' COMMENT '总客户数(此总数由于官方统计接口不统计,所以每日定时任务进行统计)', + `total_all_contact_cnt` int(11) NOT NULL DEFAULT '0' COMMENT '客户总数(由每日定时任务统计,不去重,首页和数据统计共用),【首页】:在职员工在we_flower_customer_rel表中,客户关系status != 2的客户数量 + 系统上记录的已离职的员工在we_flower_customer_rel表中,客户关系status = 3的客户数量。【数据统计】:在职员工在we_flower_customer_rel表中,客户关系status != 2的客户数量。', + `total_contact_cnt` int(11) NOT NULL DEFAULT '0' COMMENT '留存客户总数(由每日定时任务统计,去重)', `new_contact_loss_cnt` int(11) NOT NULL DEFAULT '0' COMMENT '当天加入的新客流失数(与negative_feedback_cnt不同,这是只统计当天加的流失的客户,由于当天新增客户的流失数官方统计没有提供,此数据也是由系统自行定时任务计算保存)', `new_contact_speak_cnt` int(11) NOT NULL DEFAULT '0' COMMENT '当天新增客户中与员工对话过的人数(此数据为每日定时任务统计 会话存档ES中查找)', `replied_within_thirty_min_customer_cnt` int(11) NOT NULL DEFAULT '0' COMMENT '当天员工首次给客户发消息,客户在30分钟内回复的客户数(此数据为每日定时任务统计 会话存档ES中查找)', @@ -2175,6 +2177,7 @@ CREATE TABLE `we_user_behavior_data` ( `contact_total_cnt` int(11) NOT NULL DEFAULT '0' COMMENT '当天员工客户总数(we_flower表中查找 每日定时任务获取', `user_active_chat_cnt` int(11) NOT NULL DEFAULT '0' COMMENT '当天员工主动发起的会话数量(DataStatisticsTask定时任务统计)', PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uni_corp_user_id_stat_time_idx` (`corp_id`,`user_id`,`stat_time`) USING BTREE COMMENT '企业ID-员工ID-时间唯一索引', KEY `stat_time_index` (`stat_time`,`user_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='联系客户统计数据 '; diff --git a/sql/update/update-V1.33.0.sql b/sql/update/update-V1.33.0.sql new file mode 100644 index 00000000..a11cfc43 --- /dev/null +++ b/sql/update/update-V1.33.0.sql @@ -0,0 +1,10 @@ +-- 2023-09-20 lcy 添加客户总数字段,修改原客户总数字段描述为留存客户数 Tower 任务: 增加客户总数统计数 ( https://tower.im/teams/636204/todos/72041 ) +ALTER TABLE `we_user_behavior_data` ADD COLUMN `total_all_contact_cnt` INT (11) NOT NULL DEFAULT '0' COMMENT '客户总数(由每日定时任务统计,不去重,首页和数据统计共用),【首页】:在职员工在we_flower_customer_rel表中,客户关系status != 2的客户数量 + 系统上记录的已离职的员工在we_flower_customer_rel表中,客户关系status = 3的客户数量。【数据统计】:在职员工在we_flower_customer_rel表中,客户关系status != 2的客户数量。' AFTER `negative_feedback_cnt`; +ALTER TABLE `we_user_behavior_data` MODIFY COLUMN `total_contact_cnt` int(11) NOT NULL DEFAULT '0' COMMENT '留存客户总数(由每日定时任务统计,去重)'; +-- 2023-09-21 lcy 添加员工删除客户时间字段,修改原流失时间的描述为客户删除员工时间 Tower 任务: 增加客户总数统计数 ( https://tower.im/teams/636204/todos/72041 ) +ALTER TABLE `we_flower_customer_rel` ADD COLUMN `del_by_user_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '员工删除客户时间(如果没有收到删除回调,通过手动同步客户得出的主动删除客户,该字段为默认值)' AFTER `delete_time`; +-- 2023-09-26 lcy 修改客户关系表status字段描述,we_user_behavior_data表创建唯一索引 Tower 任务: 数据统计中客户总数旧数据兼容 ( https://tower.im/teams/636204/todos/75549 ) +ALTER TABLE `we_flower_customer_rel` MODIFY COLUMN `status` char(2) NOT NULL DEFAULT '0' COMMENT '状态(0正常 1删除流失 2员工删除用户 3待继承 4转接中)'; +CREATE UNIQUE INDEX `uni_corp_user_id_stat_time_idx` ON we_user_behavior_data (`corp_id`,`user_id`,`stat_time`) COMMENT '企业ID-员工ID-时间唯一索引'; +-- 2023-09-26 lcy 修改delete_time的默认值 Tower 任务: 增加客户总数统计数 ( https://tower.im/teams/636204/todos/72041 ) +ALTER TABLE `we_flower_customer_rel` MODIFY COLUMN `delete_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '客户删除员工时间'; \ No newline at end of file