diff --git a/nanopart-server/pom.xml b/nanopart-server/pom.xml index 2b2f9b27..dc5ecf22 100644 --- a/nanopart-server/pom.xml +++ b/nanopart-server/pom.xml @@ -96,6 +96,33 @@ black-list-service 2.0.0-SNAPSHOT + + + cn.axzo.nanopart + op-api + 2.0.0-SNAPSHOT + + + + cn.axzo.nanopart + op-server + 2.0.0-SNAPSHOT + + + + cn.axzo.im.center + im-center-api + + + + cn.axzo.basics + basics-profiles-api + + + + cn.axzo.framework.rocketmq + axzo-common-rocketmq + diff --git a/nanopart-server/src/main/java/cn/axzo/nanopart/NanopartApplication.java b/nanopart-server/src/main/java/cn/axzo/nanopart/NanopartApplication.java index 8cf18f79..521bb1b8 100644 --- a/nanopart-server/src/main/java/cn/axzo/nanopart/NanopartApplication.java +++ b/nanopart-server/src/main/java/cn/axzo/nanopart/NanopartApplication.java @@ -1,17 +1,20 @@ package cn.axzo.nanopart; +import cn.axzo.nanopart.config.RocketMQEventConfiguration; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Import; @MapperScan(value = {"cn.axzo.**.mapper"}) @SpringBootApplication @EnableFeignClients(basePackages = { - "cn.axzo.nanopart.api" + "cn.axzo" }) @EnableAspectJAutoProxy() +@Import(RocketMQEventConfiguration.class) public class NanopartApplication { public static void main(String[] args) { SpringApplication.run(NanopartApplication.class, args); diff --git a/nanopart-server/src/main/java/cn/axzo/nanopart/config/FeignConfig.java b/nanopart-server/src/main/java/cn/axzo/nanopart/config/FeignConfig.java new file mode 100644 index 00000000..dbb23e00 --- /dev/null +++ b/nanopart-server/src/main/java/cn/axzo/nanopart/config/FeignConfig.java @@ -0,0 +1,138 @@ +package cn.axzo.nanopart.config; + +import cn.azxo.framework.common.constatns.Constants; +import cn.hutool.core.util.ReflectUtil; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import feign.Target.HardCodedTarget; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.Objects; + +/** + * @author liuyang + * @date 2020/10/24 + **/ +@Component +@Slf4j +@Profile({"dev", "test", "local", "pre"}) +public class FeignConfig implements RequestInterceptor, EnvironmentAware { + private Environment environment; + @Value("${maokaiEnvUrl:http://dev-app.axzo.cn/maokai}") + private String maokaiEnvUrl; + @Value("${apolloEnvUrl:http://dev-app.axzo.cn/apollo}") + private String apolloEnvUrl; + @Value("${mnsEnvUrl:http://dev-app.axzo.cn/mns}") + private String mnsEnvUrl; + @Value("${pudgeEnvUrl:http://dev-app.axzo.cn/pudge}") + private String pudgeEnvUrl; + @Value("${outmanEnvUrl:http://dev-app.axzo.cn/outman}") + private String outmanEnvUrl; + @Value("${thronesEnvUrl:http://dev-app.axzo.cn/thrones}") + private String thronesEnvUrl; + @Value("${workspaceEnvUrl:http://dev-app.axzo.cn/workspace}") + private String workspaceEnvUrl; + @Value("${thirdPartUrl:http://dev-app.axzo.cn/thirdParty}") + private String thirdPartUrl; + @Value("${nanopartUrl:http://dev-app.axzo.cn/nanopart}") + private String nanopartUrl; + @Value("${tyrEnvUrl:http://dev-app.axzo.cn/tyr}") + private String tyrEnvUrl; + @Value("${plutoEnvUrl:http://dev-app.axzo.cn/pluto}") + private String plutoUrl; + @Value("${sennaEnvUrl:http://dev-app.axzo.cn/senna}") + private String sennaEnvUrl; + @Value("${workflowEngineEnvUrl:http://dev-app.axzo.cn/workflow-engine}") + private String workflowEngineEnvUrl; + @Value("${msgCenterEngineEnvUrl:http://dev-app.axzo.cn/msg-center}") + private String msgCenterEngineEnvUrl; + @Value("${imCenterEnvUrl:http://dev-app.axzo.cn/im-center}") + private String imCenterEnvUrl; + @Value("${msgCenterEngineEnvUrl:http://dev-app.axzo.cn/braum}") + private String braumEnvUrl; + @Value("${labourEnvUrl:http://dev-app.axzo.cn/labour}") + private String labourEvnUrl; + @Value("${epicEnvUrl:http://dev-app.axzo.cn/epic}") + private String epicEnvUrl; + @Value("${karmaEnvUrl:http://dev-app.axzo.cn/karma}") + private String karmaEnvUrl; + @Value("${dataCollectionUrl:http://dev-app.axzo.cn/dataCollection}") + private String dataCollectionUrl; + @Value("${attendanceUrl:http://dev-app.axzo.cn/attendance}") + private String attendanceApi; + private static String POD_NAMESPACE; + + static { + Map env = System.getenv(); + if (env != null) { + POD_NAMESPACE = env.get("MY_POD_NAMESPACE"); + } + log.info("init FeignConfig, POD_NAMESPACE value is {}", POD_NAMESPACE); + } + + @SneakyThrows + @Override + public void apply(RequestTemplate requestTemplate) { + if (POD_NAMESPACE == null) { + HardCodedTarget target = (HardCodedTarget) requestTemplate.feignTarget(); + String url = requestTemplate.feignTarget().url(); + // 如需修改微服务地址,建议通过外部化参数来调整 + url = url.replace("http://maokai:8080", maokaiEnvUrl); + url = url.replace("http://apollo:11000", apolloEnvUrl); + url = url.replace("http://mns:8989", mnsEnvUrl); + url = url.replace("http://pudge:10099", pudgeEnvUrl); + url = url.replace("http://outman:8989", outmanEnvUrl); + url = url.replace("http://thrones", thronesEnvUrl); + url = url.replace("http://workspace:8080", workspaceEnvUrl); + url = url.replace("http://third-party:11000", thirdPartUrl); + url = url.replace("http://nanopart:8080", nanopartUrl); + url = url.replace("http://tyr:8080", tyrEnvUrl); + url = url.replace("http://pluto:8080", plutoUrl); + url = url.replace("http://senna:8080", sennaEnvUrl); + url = url.replace("http://workflow-engine:8080", workflowEngineEnvUrl); + url = url.replace("http://msg-center:8080", msgCenterEngineEnvUrl); + url = url.replace("http://im-center:8080", imCenterEnvUrl); + url = url.replace("http://braum:8080", braumEnvUrl); + url = url.replace("http://labour:8080", labourEvnUrl); + url = url.replace("http://epic:8080", epicEnvUrl); + url = url.replace("http://karma:8080", karmaEnvUrl); + url = url.replace("http://data-collection:21200", dataCollectionUrl); + url = url.replace("http://attendance:8080", attendanceApi); + String profile = environment.getProperty("spring.profiles.active"); + if(Objects.equals(profile, "test") && url.contains("dev-app.axzo.cn")) { + url = url.replace("dev-app", "test-api"); + } + if(Objects.equals(profile, "pre") && url.contains("dev-app.axzo.cn")) { + url = url.replace("dev-app", "pre-api"); + } + if(Objects.equals(profile, "live") && url.contains("dev-app.axzo.cn")) { + url = url.replace("dev-app", "live-api"); + } + requestTemplate.target(url); + Field field = ReflectUtil.getField(target.getClass(), "url"); + field.setAccessible(true); + field.set(target, url); + } + requestTemplate.header(Constants.CTX_LOG_ID_MDC, MDC.get(Constants.CTX_LOG_ID_MDC)); + requestTemplate.header("X-SERVER-NAME", environment.getProperty("spring.application.name")); + } + + /** + * Set the {@code Environment} that this component runs in. + * + * @param environment + */ + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } +} diff --git a/nanopart-server/src/main/java/cn/axzo/nanopart/config/RocketMQEventConfiguration.java b/nanopart-server/src/main/java/cn/axzo/nanopart/config/RocketMQEventConfiguration.java new file mode 100644 index 00000000..31ff6a9e --- /dev/null +++ b/nanopart-server/src/main/java/cn/axzo/nanopart/config/RocketMQEventConfiguration.java @@ -0,0 +1,90 @@ +package cn.axzo.nanopart.config; + +import cn.axzo.framework.rocketmq.BaseListener; +import cn.axzo.framework.rocketmq.DefaultEventConsumer; +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.framework.rocketmq.EventHandlerRepository; +import cn.axzo.framework.rocketmq.EventProducer; +import cn.axzo.framework.rocketmq.RocketMQEventProducer; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.spring.annotation.ConsumeMode; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +import java.util.function.Consumer; + +/** + * @Author: liyong.tian + * @Date: 2023/7/25 14:43 + * @Description: + */ +@Slf4j +public class RocketMQEventConfiguration { + + @Value("${spring.application.name}") + private String appName; + + @Value("${topic}") + private String topic; + + @Bean + public RocketMQTemplate ser(){ + return new RocketMQTemplate(); + } + @Bean + EventProducer eventProducer(RocketMQTemplate rocketMQTemplate) { + return new RocketMQEventProducer(rocketMQTemplate, + "nanopart", + appName, + EventProducer.Context.builder() + .meta(RocketMQEventProducer.RocketMQMessageMeta.builder() + .topic(topic) + .build()) + .build(), + null + ); + } + + @Bean + EventConsumer eventConsumer(EventHandlerRepository eventHandlerRepository) { + Consumer callback = (eventWrapper) -> { + if (eventWrapper.isHandled()) { + // 只收集被App真正消费的消息. + //String topic = (String) eventWrapper.getExt().get(EVENT_TOPIC_KEY); + + } + }; + return new DefaultEventConsumer(appName, eventHandlerRepository, callback); + } + + @Slf4j + @Component + @RocketMQMessageListener(topic = "topic_im_center_${spring.profiles.active}", + consumerGroup = "GID_topic_im_center_${spring.application.name}_${spring.profiles.active}", + consumeMode = ConsumeMode.ORDERLY, + nameServer = "${rocketmq.name-server}" + ) + public static class DefaultListener extends BaseListener implements RocketMQListener { + + @Autowired + private EventConsumer eventConsumer; + + @Override + public void onMessage(MessageExt message) { + super.onEvent(message, eventConsumer); + } + } + + @Bean + EventHandlerRepository eventHandlerRepository() { + return new EventHandlerRepository((ex, logText) -> { + log.warn("MQ, handle warning {}", logText, ex); + }); + } +} diff --git a/nanopart-server/src/main/java/cn/axzo/nanopart/config/XxlJobConfig.java b/nanopart-server/src/main/java/cn/axzo/nanopart/config/XxlJobConfig.java new file mode 100644 index 00000000..21788baa --- /dev/null +++ b/nanopart-server/src/main/java/cn/axzo/nanopart/config/XxlJobConfig.java @@ -0,0 +1,69 @@ +package cn.axzo.nanopart.config; + +import cn.azxo.framework.common.logger.JobLoggerTemplate; +import cn.azxo.framework.common.service.JobParamResolver; +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * xxl-job config + * + * @author xuxueli 2017-04-28 + */ +@Configuration +public class XxlJobConfig { + + private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class); + + // @Value("http://dev-xxl-job.axzo.cn/xxl-job-admin") + @Value("${xxl.job.admin.addresses}") + private String adminAddresses; + + @Value("${xxl.job.executor.appname}") + private String appName; + + @Value("") + private String ip; + + @Value("${xxl.job.executor.port}") + private int port; + + // @Value("${xxl.job.accessToken}") + @Value("") + private String accessToken; + + @Value("") + private String logPath; + + @Value("-1") + private int logRetentionDays; + + @Bean + public XxlJobSpringExecutor xxlJobExecutor() { + logger.info(">>>>>>>>>>> xxl-job config init."); + XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); + xxlJobSpringExecutor.setAdminAddresses(adminAddresses); + xxlJobSpringExecutor.setAppname(appName); + xxlJobSpringExecutor.setIp(ip); + xxlJobSpringExecutor.setPort(port); + xxlJobSpringExecutor.setAccessToken(accessToken); + xxlJobSpringExecutor.setLogPath(logPath); + xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); + + return xxlJobSpringExecutor; + } + + @Bean("jobParamResolver") + public JobParamResolver jobParamResolver() { + return new JobParamResolver(); + } + + @Bean("jobLoggerTemplate") + public JobLoggerTemplate jobLoggerTemplate() { + return new JobLoggerTemplate(); + } +} diff --git a/nanopart-server/src/main/java/cn/axzo/nanopart/config/exception/ExceptionAdviceHandler.java b/nanopart-server/src/main/java/cn/axzo/nanopart/config/exception/ExceptionAdviceHandler.java index 5392e0a5..13db216b 100644 --- a/nanopart-server/src/main/java/cn/axzo/nanopart/config/exception/ExceptionAdviceHandler.java +++ b/nanopart-server/src/main/java/cn/axzo/nanopart/config/exception/ExceptionAdviceHandler.java @@ -1,6 +1,7 @@ package cn.axzo.nanopart.config.exception; import cn.axzo.framework.domain.web.result.ApiResult; +import cn.axzo.pokonyan.exception.BusinessException; import lombok.extern.slf4j.Slf4j; import org.springframework.core.annotation.Order; import org.springframework.util.CollectionUtils; @@ -41,6 +42,12 @@ public class ExceptionAdviceHandler { return ApiResult.err(e.getMessage()); } + @ExceptionHandler(BusinessException.class) + public ApiResult businessExceptionHandler(BusinessException e){ + log.warn("业务异常", e); + return ApiResult.err(e.getErrorMsg()); + } + @ExceptionHandler(BindException.class) public ApiResult bindExceptionHandler(BindException e) { log.warn("业务异常", e); diff --git a/op/README.md b/op/README.md new file mode 100644 index 00000000..5fe9c670 --- /dev/null +++ b/op/README.md @@ -0,0 +1 @@ +# 项目介绍 \ No newline at end of file diff --git a/op/RELEASE.md b/op/RELEASE.md new file mode 100644 index 00000000..a033ae0c --- /dev/null +++ b/op/RELEASE.md @@ -0,0 +1,2 @@ +# 发布记录 + diff --git a/op/op-api/pom.xml b/op/op-api/pom.xml new file mode 100644 index 00000000..de4b0e10 --- /dev/null +++ b/op/op-api/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + op + cn.axzo.nanopart + ${revision} + ../pom.xml + + + op-api + jar + op-api + + + + org.springframework.cloud + spring-cloud-openfeign-core + + + cn.axzo.framework + axzo-common-domain + + + + cn.axzo.basics + basics-common + + + + cn.axzo.basics + basics-profiles-api + + + + diff --git a/op/op-api/src/main/java/cn/axzo/op/api/OpMessageConfigApi.java b/op/op-api/src/main/java/cn/axzo/op/api/OpMessageConfigApi.java new file mode 100644 index 00000000..8f1cfb8c --- /dev/null +++ b/op/op-api/src/main/java/cn/axzo/op/api/OpMessageConfigApi.java @@ -0,0 +1,342 @@ +package cn.axzo.op.api; + +import cn.axzo.framework.domain.web.result.ApiPageResult; +import cn.axzo.framework.domain.web.result.ApiResult; +import com.alibaba.fastjson.JSONObject; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.Date; +import java.util.List; + +@FeignClient(name = "nanopart", url = "${axzo.service.nanopart:http://nanopart:8080}") +public interface OpMessageConfigApi { + + @PostMapping("/api/op-message-config/page") + ApiPageResult page(@RequestBody PageOpMessageConfigReq req); + + @PostMapping("/api/op-message-config/create") + ApiResult create(@Valid @RequestBody CreateOpMessageConfigParam req); + + @PostMapping("/api/op-message-config/delete") + ApiResult delete(@Valid @RequestBody DeleteOpMessageConfigParam param); + + @PostMapping("/api/op-message-config/update") + ApiResult update(@Valid @RequestBody UpdateOpMessageConfigParam param); + + @PostMapping("/api/op-message-config/draft/save") + ApiResult saveDraft(@Valid @RequestBody SaveDraftOpMessageConfigParam req); + + @PostMapping("/api/op-message-config/draft/submit") + ApiResult submitDraft(@Valid @RequestBody SaveDraftOpMessageConfigParam req); + + @Builder + @Data + @NoArgsConstructor + @AllArgsConstructor + class UpdateOpMessageConfigParam { + + @NotNull(message = "id不能为空") + private Long id; + + private String name; + + private String sendImAccount; + + private JSONObject receiveData; + + private ActionEnum action; + + private String title; + + private String content; + + private String contentType; + + private String coverImg; + + private JSONObject jumpData; + + private JSONObject pushData; + + private JSONObject ext; + + private Date planStartTime; + + @NotNull(message = "operatePersonId不能为空") + private Long operatePersonId; + } + + @Getter + @AllArgsConstructor + enum ActionEnum { + SUBMIT_DRAFT + } + @Builder + @Data + @NoArgsConstructor + @AllArgsConstructor + class DeleteOpMessageConfigParam { + + @NotNull(message = "id不能为空") + private Long id; + + @NotNull(message = "operatePersonId不能为空") + private Long operatePersonId; + } + + @Builder + @Data + @NoArgsConstructor + @AllArgsConstructor + class CreateOpMessageConfigParam { + private String name; + + private String sendImAccount; + + private JSONObject receiveData; + + private Status status; + + private String title; + + private String content; + + private String contentType; + + private String coverImg; + + private JSONObject jumpData; + + private JSONObject pushData; + + private JSONObject ext; + + private Date planStartTime; + + @NotNull(message = "operatePersonId不能为空") + private Long operatePersonId; + } + + @SuperBuilder + @Data + @NoArgsConstructor + @AllArgsConstructor + class ListOpMessageConfigReq { + + private List ids; + + private Date planStartTimeLE; + + private List statuses; + + private String nameLike; + + private String sendImAccount; + + private String sendImAccountLike; + + private String contentType; + + private List receivePlatforms; + + /** + * 返回发送的信息: + * IM机器人账号:返回头像、名字等信息 + * 账号有机器人和系统人员信息,查询的接口不同 + */ + private boolean needSendUserInfo; + + /** + * 返回创建人的信息 + */ + private boolean needCreatePersonInfo; + + /** + * 返回提交审核人的信息 + */ + private boolean needSubmitPersonInfo; + } + + @SuperBuilder + @Data + @NoArgsConstructor + @AllArgsConstructor + class PageOpMessageConfigReq extends ListOpMessageConfigReq { + Integer page; + + Integer pageSize; + + /** + * 排序:使用示例,createTime__DESC + */ + List sort; + } + + @Data + @SuperBuilder + @AllArgsConstructor + @NoArgsConstructor + class OpMessageConfigResp extends OpMessageConfigBaseResp { + private SendUserInfo sendUserInfo; + + private PersonProfileDto createPersonProfile; + + private PersonProfileDto submitPersonProfile; + } + + @Data + @SuperBuilder + @AllArgsConstructor + @NoArgsConstructor + class PersonProfileDto { + + private String phone; + private String realName; + } + + @Data + @SuperBuilder + @AllArgsConstructor + @NoArgsConstructor + class OpMessageConfigBaseResp { + private Long id; + + /** + * im-center服务im_message_task的id + */ + private Long imMessageTaskId; + + /** + * 消息名字 + */ + private String name; + + /** + * 发送者的三方平台账号id + */ + private String sendImAccount; + + /** + * 消息接收配置 + */ + private JSONObject receiveData; + + private String status; + + private String title; + + private String content; + + private String contentType; + + private String coverImg; + + private JSONObject jumpData; + + private JSONObject pushData; + + private JSONObject ext; + + private Date planStartTime; + + private Date startedTime; + + private Date finishedTime; + + private Integer isDelete; + + private Date createAt; + + private Date updateAt; + + private Long createPersonId; + + /** + * 删除人id + */ + private Long deletePersonId; + + /** + * 更新人id + */ + private Long updatePersonId; + + private Date submitTime; + } + + + @Builder + @Data + @NoArgsConstructor + @AllArgsConstructor + class SendUserInfo { + + /** + * im账号 + */ + private String imAccount; + + /** + * 昵称 + */ + private String nickName; + + + /** + * 头像 + */ + private String headImageUrl; + } + + enum Status { + DRAFT, + PENDING, + RUNNING, + COMPLETED, + ; + } + + @Builder + @Data + @NoArgsConstructor + @AllArgsConstructor + class SaveDraftOpMessageConfigParam { + + private Long id; + + private String name; + + private String sendImAccount; + + private JSONObject receiveData; + + private String title; + + private String content; + + private String contentType; + + private String coverImg; + + private JSONObject jumpData; + + private JSONObject pushData; + + private JSONObject ext; + + private Date planStartTime; + + @NotNull(message = "operatePersonId不能为空") + private Long operatePersonId; + } +} diff --git a/op/op-api/src/main/java/cn/axzo/op/api/config/NanopartApiAutoConfiguration.java b/op/op-api/src/main/java/cn/axzo/op/api/config/NanopartApiAutoConfiguration.java new file mode 100644 index 00000000..180299f5 --- /dev/null +++ b/op/op-api/src/main/java/cn/axzo/op/api/config/NanopartApiAutoConfiguration.java @@ -0,0 +1,9 @@ +package cn.axzo.op.api.config; + +import cn.axzo.op.api.constant.NanopartConstant; +import org.springframework.cloud.openfeign.EnableFeignClients; + +@EnableFeignClients(NanopartConstant.BASIC_FEIGN_PACKAGE) +public class NanopartApiAutoConfiguration { + +} diff --git a/op/op-api/src/main/java/cn/axzo/op/api/constant/NanopartConstant.java b/op/op-api/src/main/java/cn/axzo/op/api/constant/NanopartConstant.java new file mode 100644 index 00000000..9abc2e18 --- /dev/null +++ b/op/op-api/src/main/java/cn/axzo/op/api/constant/NanopartConstant.java @@ -0,0 +1,13 @@ +package cn.axzo.op.api.constant; + +/** + * @author: chenwenjian + * @date: 2023/8/14 9:38 + * @description: + * @modifiedBy: + * @version: 1.0 + */ +public class NanopartConstant { + + public static final String BASIC_FEIGN_PACKAGE = "cn.axzo"; +} \ No newline at end of file diff --git a/op/op-api/src/main/resources/META-INF/spring.factories b/op/op-api/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..bf796ec6 --- /dev/null +++ b/op/op-api/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.axzo.nanopart.api.config.NanopartApiAutoConfiguration \ No newline at end of file diff --git a/op/op-server/pom.xml b/op/op-server/pom.xml new file mode 100644 index 00000000..040cbe95 --- /dev/null +++ b/op/op-server/pom.xml @@ -0,0 +1,99 @@ + + + + cn.axzo.nanopart + op + ${revision} + ../pom.xml + + 4.0.0 + + op-server + jar + + op-server + + + + cn.axzo.framework + axzo-web-spring-boot-starter + + + cn.axzo.framework + axzo-spring-cloud-starter + + + cn.axzo.framework + axzo-consumer-spring-cloud-starter + + + cn.axzo.framework + axzo-processor-spring-boot-starter + + + + cn.axzo.framework + axzo-mybatisplus-spring-boot-starter + + + + cn.axzo.framework + axzo-swagger-yapi-spring-boot-starter + + + mysql + mysql-connector-java + + + cn.hutool + hutool-all + + + + cn.axzo.basics + basics-common + + + + cn.axzo.framework + axzo-logger-spring-boot-starter + + + + cn.axzo.nanopart + op-api + 2.0.0-SNAPSHOT + + + + cn.axzo.pokonyan + pokonyan + + + + cn.axzo.im.center + im-center-api + + + + cn.axzo.basics + basics-profiles-api + + + + com.xuxueli + xxl-job-core + + + + cn.axzo.framework.rocketmq + axzo-common-rocketmq + + + + cn.axzo.msgcenter + msg-center-api + 1.0.1-SNAPSHOT + + + diff --git a/op/op-server/src/main/java/cn/axzo/nanopart/server/config/BizResultCode.java b/op/op-server/src/main/java/cn/axzo/nanopart/server/config/BizResultCode.java new file mode 100644 index 00000000..542ccbbc --- /dev/null +++ b/op/op-server/src/main/java/cn/axzo/nanopart/server/config/BizResultCode.java @@ -0,0 +1,20 @@ +package cn.axzo.nanopart.server.config; + +import cn.axzo.pokonyan.exception.ResultCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum BizResultCode implements ResultCode { + + OP_MESSAGE_CONFIG_NOT_FOUND("100", "运营消息配置不存在"), + DELETE_OP_MESSAGE_CONFIG_STATUS_ERROR("101", "删除运营消息配置失败,状态异常"), + UPDATE_OP_MESSAGE_CONFIG_STATUS_ERROR("102", "更新运营消息配置失败,状态异常"), + OP_MESSAGE_CONFIG_ID_NOT_NULL("103", "id不能为空"), + OP_MESSAGE_CONFIG_STATUS_ERROR("104", "操作运营消息配置失败,状态异常"),; + + + private String errorCode; + private String errorMessage; +} diff --git a/op/op-server/src/main/java/cn/axzo/nanopart/server/controller/OpMessageConfigController.java b/op/op-server/src/main/java/cn/axzo/nanopart/server/controller/OpMessageConfigController.java new file mode 100644 index 00000000..8a55ea30 --- /dev/null +++ b/op/op-server/src/main/java/cn/axzo/nanopart/server/controller/OpMessageConfigController.java @@ -0,0 +1,152 @@ +package cn.axzo.nanopart.server.controller; + +import cn.axzo.framework.domain.web.result.ApiPageResult; +import cn.axzo.framework.domain.web.result.ApiResult; +import cn.axzo.nanopart.server.domain.OpMessageConfig; +import cn.axzo.nanopart.server.service.OpMessageConfigService; +import cn.axzo.op.api.OpMessageConfigApi; +import cn.axzo.pokonyan.exception.Aassert; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; + +import static cn.axzo.nanopart.server.config.BizResultCode.OP_MESSAGE_CONFIG_ID_NOT_NULL; + +@RestController +public class OpMessageConfigController implements OpMessageConfigApi { + + + @Autowired + private OpMessageConfigService opMessageConfigService; + + @Override + public ApiPageResult page(PageOpMessageConfigReq param) { + OpMessageConfigService.PageOpMessageConfigParam pageOpMessageConfigParam = OpMessageConfigService.PageOpMessageConfigParam.builder().build(); + BeanUtils.copyProperties(param, pageOpMessageConfigParam); + + Page page = opMessageConfigService.page(pageOpMessageConfigParam); + + return ApiPageResult.ok(page.convert(record -> { + OpMessageConfigResp opMessageConfigResp = OpMessageConfigResp.builder().build(); + BeanUtils.copyProperties(record, opMessageConfigResp); + opMessageConfigResp.setStatus(record.getStatus().name()); + + if (record.getSendUserInfo() != null) { + SendUserInfo sendUserInfo = SendUserInfo.builder().build(); + BeanUtils.copyProperties(record.getSendUserInfo(), sendUserInfo); + opMessageConfigResp.setSendUserInfo(sendUserInfo); + } + + if (record.getCreatePersonProfile() != null) { + PersonProfileDto createPersonProfile = PersonProfileDto.builder().build(); + BeanUtils.copyProperties(record.getCreatePersonProfile(), createPersonProfile); + opMessageConfigResp.setCreatePersonProfile(createPersonProfile); + } + + if (record.getSubmitPersonProfile() != null) { + PersonProfileDto submitPersonProfile = PersonProfileDto.builder().build(); + BeanUtils.copyProperties(record.getSubmitPersonProfile(), submitPersonProfile); + opMessageConfigResp.setSubmitPersonProfile(submitPersonProfile); + } + + return opMessageConfigResp; + })); + } + + @Override + public ApiResult create(CreateOpMessageConfigParam param) { + OpMessageConfigService.CreateOpMessageConfigParam createOpMessageConfigParam = OpMessageConfigService.CreateOpMessageConfigParam.builder().build(); + BeanUtils.copyProperties(param, createOpMessageConfigParam); + // 默认状态为待执行,创建人就是提交审核人 + createOpMessageConfigParam.setSubmitPersonId(param.getOperatePersonId()); + createOpMessageConfigParam.setCreatePersonId(param.getOperatePersonId()); + createOpMessageConfigParam.setStatus(OpMessageConfig.Status.PENDING); + createOpMessageConfigParam.setPlanStartTime(resolvePlanStartTime(param.getReceiveData(), param.getPlanStartTime())); + + OpMessageConfig opMessageConfig = opMessageConfigService.create(createOpMessageConfigParam); + return ApiResult.ok(to(opMessageConfig)); + } + + private Date resolvePlanStartTime(JSONObject receiveData, Date planStartTime) { + if (planStartTime != null) { + return planStartTime; + } + if (receiveData == null) { + return null; + } + if (receiveData.toJavaObject(OpMessageConfig.ReceiveData.class).isNowStrategy()) { + return new Date(); + } + return null; + } + + @Override + public ApiResult delete(DeleteOpMessageConfigParam param) { + OpMessageConfigService.DeleteOpMessageConfigParam deleteOpMessageConfigParam = OpMessageConfigService.DeleteOpMessageConfigParam.builder() + .id(param.getId()) + .deletePersonId(param.getOperatePersonId()) + .build(); + opMessageConfigService.delete(deleteOpMessageConfigParam); + return ApiResult.ok(); + } + + @Override + public ApiResult update(UpdateOpMessageConfigParam param) { + OpMessageConfigService.UpdateOpMessageConfigParam updateOpMessageConfigParam = OpMessageConfigService.UpdateOpMessageConfigParam.builder().build(); + BeanUtils.copyProperties(param, updateOpMessageConfigParam); + updateOpMessageConfigParam.setUpdatePersonId(param.getOperatePersonId()); + updateOpMessageConfigParam.setPlanStartTime(resolvePlanStartTime(param.getReceiveData(), param.getPlanStartTime())); + + OpMessageConfig opMessageConfig = opMessageConfigService.update(updateOpMessageConfigParam); + return ApiResult.ok(to(opMessageConfig)); + } + + @Override + public ApiResult saveDraft(SaveDraftOpMessageConfigParam req) { + + if (req.getId() == null) { + OpMessageConfigService.CreateOpMessageConfigParam createOpMessageConfigParam = OpMessageConfigService.CreateOpMessageConfigParam.builder().build(); + BeanUtils.copyProperties(req, createOpMessageConfigParam); + createOpMessageConfigParam.setStatus(OpMessageConfig.Status.DRAFT); + createOpMessageConfigParam.setCreatePersonId(req.getOperatePersonId()); + + OpMessageConfig opMessageConfig = opMessageConfigService.create(createOpMessageConfigParam); + return ApiResult.ok(to(opMessageConfig)); + } + + OpMessageConfigService.UpdateOpMessageConfigParam updateOpMessageConfigParam = OpMessageConfigService.UpdateOpMessageConfigParam.builder().build(); + BeanUtils.copyProperties(req, updateOpMessageConfigParam); + updateOpMessageConfigParam.setUpdatePersonId(req.getOperatePersonId()); + updateOpMessageConfigParam.setPlanStartTime(resolvePlanStartTime(req.getReceiveData(), req.getPlanStartTime())); + + OpMessageConfig opMessageConfig = opMessageConfigService.update(updateOpMessageConfigParam); + return ApiResult.ok(to(opMessageConfig)); + } + + @Override + public ApiResult submitDraft(SaveDraftOpMessageConfigParam req) { + Aassert.notNull(req.getId(), OP_MESSAGE_CONFIG_ID_NOT_NULL); + + OpMessageConfigService.UpdateOpMessageConfigParam updateOpMessageConfigParam = OpMessageConfigService.UpdateOpMessageConfigParam.builder().build(); + BeanUtils.copyProperties(req, updateOpMessageConfigParam); + updateOpMessageConfigParam.setAction(OpMessageConfig.ActionEnum.SUBMIT_DRAFT); + updateOpMessageConfigParam.setUpdatePersonId(req.getOperatePersonId()); + updateOpMessageConfigParam.setSubmitPersonId(req.getOperatePersonId()); + updateOpMessageConfigParam.setSubmitTime(new Date()); + updateOpMessageConfigParam.setPlanStartTime(resolvePlanStartTime(req.getReceiveData(), req.getPlanStartTime())); + + OpMessageConfig opMessageConfig = opMessageConfigService.update(updateOpMessageConfigParam); + return ApiResult.ok(to(opMessageConfig)); + } + + public OpMessageConfigBaseResp to(OpMessageConfig opMessageConfig) { + OpMessageConfigBaseResp opMessageConfigResp = OpMessageConfigBaseResp.builder().build();; + BeanUtils.copyProperties(opMessageConfig, opMessageConfigResp); + opMessageConfigResp.setStatus(opMessageConfig.getStatus().name()); + return opMessageConfigResp; + } +} diff --git a/op/op-server/src/main/java/cn/axzo/nanopart/server/controller/PrivateController.java b/op/op-server/src/main/java/cn/axzo/nanopart/server/controller/PrivateController.java new file mode 100644 index 00000000..a2eaeb99 --- /dev/null +++ b/op/op-server/src/main/java/cn/axzo/nanopart/server/controller/PrivateController.java @@ -0,0 +1,32 @@ +package cn.axzo.nanopart.server.controller; + +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.nanopart.server.event.outer.PushYouMengMessageHandler; +import cn.axzo.nanopart.server.xxljob.SendMessageJob; +import com.alibaba.fastjson.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/private") +public class PrivateController { + + @Autowired + private SendMessageJob sendMessageJob; + @Autowired + private PushYouMengMessageHandler pushYouMengMessageHandler; + + @PostMapping("/send-message/job/run") + public Object runSendMessage(@RequestBody SendMessageJob.SendMessageParam param) throws Exception { + return sendMessageJob.execute(JSONObject.toJSONString(param)); + } + + @PostMapping("/you-meng/push") + public Object pushYouMeng(@RequestBody Event param) throws Exception { + pushYouMengMessageHandler.onEvent(param, null); + return null; + } +} diff --git a/op/op-server/src/main/java/cn/axzo/nanopart/server/domain/OpMessageConfig.java b/op/op-server/src/main/java/cn/axzo/nanopart/server/domain/OpMessageConfig.java new file mode 100644 index 00000000..eb452cda --- /dev/null +++ b/op/op-server/src/main/java/cn/axzo/nanopart/server/domain/OpMessageConfig.java @@ -0,0 +1,312 @@ +package cn.axzo.nanopart.server.domain; + +import cn.axzo.nanopart.server.config.BizResultCode; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Sets; +import com.google.common.collect.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +@Data +@SuperBuilder +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@TableName(value = "`op_message_config`", autoResultMap = true) +public class OpMessageConfig { + + private static final Set CAN_DELETE_STATUSES = Sets.newHashSet( + Status.DRAFT, + Status.PENDING + ); + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * im-center服务im_message_task的id + */ + @TableField(value = "im_message_task_id") + private Long imMessageTaskId; + + /** + * 消息名字 + */ + @TableField(value = "name") + private String name; + + /** + * 发送者的三方平台账号id + */ + @TableField(value = "send_im_account") + private String sendImAccount; + + /** + * 消息接收配置 + */ + @TableField(typeHandler = FastjsonTypeHandler.class) + private JSONObject receiveData; + + private Status status; + + @TableField(value = "title") + private String title; + + @TableField(value = "content") + private String content; + + @TableField(value = "content_type") + private String contentType; + + @TableField(value = "cover_img") + private String coverImg; + + @TableField(typeHandler = FastjsonTypeHandler.class) + private JSONObject jumpData; + + @TableField(typeHandler = FastjsonTypeHandler.class) + private JSONObject pushData; + + @TableField(typeHandler = FastjsonTypeHandler.class) + private JSONObject ext; + + @TableField + private Date planStartTime; + + @TableField + private Date startedTime; + + @TableField + private Date finishedTime; + + @TableField + private Integer isDelete; + + @TableField + private Date createAt; + + @TableField + private Date submitTime; + + @TableField + private Date updateAt; + + /** + * 创建人id + */ + @TableField + private Long createPersonId; + + /** + * 删除人id + */ + @TableField + private Long deletePersonId; + + /** + * 更新人id + */ + @TableField + private Long updatePersonId; + + /** + * 提交审核人 + */ + @TableField + private Long submitPersonId; + + public enum Status { + DRAFT, + PENDING, + RUNNING, + COMPLETED, + ; + } + + public boolean canDelete() { + return CAN_DELETE_STATUSES.contains(this.getStatus()); + } + + @Getter + @AllArgsConstructor + public enum ActionEnum { + SUBMIT_DRAFT, + RUN, + COMPLETE + ; + + private static final Table STATUS_FLOWS = HashBasedTable.create(); + + static { + STATUS_FLOWS.put(Status.DRAFT, SUBMIT_DRAFT, Status.PENDING); + STATUS_FLOWS.put(Status.PENDING, RUN, Status.RUNNING); + STATUS_FLOWS.put(Status.RUNNING, COMPLETE, Status.COMPLETED); + } + + public Status getNextStatus(Status oldStatus) { + return Optional.ofNullable(STATUS_FLOWS.get(oldStatus, this)).orElseThrow(BizResultCode.OP_MESSAGE_CONFIG_STATUS_ERROR::toException); + } + } + + public ReceiveData resolveReceiveData() { + JSONObject receiveData = Optional.ofNullable(this.getReceiveData()).orElseGet(JSONObject::new); + return JSONObject.toJavaObject(receiveData, ReceiveData.class); + } + + public JumpData resolveJumpData() { + JSONObject jumpData = Optional.ofNullable(this.getJumpData()).orElseGet(JSONObject::new); + return JSONObject.toJavaObject(jumpData, JumpData.class); + } + + public PushData resolvePushData() { + JSONObject pushData = Optional.ofNullable(this.getPushData()).orElseGet(JSONObject::new); + return JSONObject.toJavaObject(pushData, PushData.class); + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ReceiveData { + + private List persons; + + /** + * 推送时间策略:now(立即发送)、specifyTime(指定时间) + */ + private String strategy; + + private String fileName; + + private String fileKey; + + /** + * 接收的类型:all(全部员工)、import(导入名单) + */ + private String type; + + /** + * 接收渠道平台:CMP(B端: APP(管理端)+ CMS)、CM(C端:APP(工人端)) + */ + private List platforms; + + public boolean isNowStrategy() { + return Objects.equals("now", this.getStrategy()); + } + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Person { + + private Long ouId; + + private Long personId; + } + + @Getter + @AllArgsConstructor + public enum ReceiveType { + ALL("all", "全部员工"), + IMPORT("import", "导入名单"); + + private String code; + + private String desc; + + public static ReceiveType of(String typeCode) { + if (StringUtils.isBlank(typeCode)) { + return null; + } + + return Arrays.stream(values()) + .filter(e -> Objects.equals(e.getCode(), typeCode)) + .findFirst() + .orElse(null); + } + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class JumpData { + private boolean switchOn; + + private List platforms; + } + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class Platform { + + private JumpPlatform platform; + + private String url; + } + + @Getter + @AllArgsConstructor + public enum JumpPlatform { + PC, + CM_IOS, + CM_ANDROID, + CMP_IOS, + CMP_ANDROID + ; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class PushData { + + private boolean switchOn; + + /** + * 声音文件 + */ + private String voiceFile; + + private String voiceFileName; + + /** + * 提醒方式:voice(声音)、vibrate(震动) + */ + private String ability; + + /** + * push类型:system(系统消息)、op(运营消息) + */ + private String type; + + /** + * 声音类型:custom(自定义)、system(系统 + */ + private String voiceType; + } +} diff --git a/op/op-server/src/main/java/cn/axzo/nanopart/server/event/outer/EventTypeEnum.java b/op/op-server/src/main/java/cn/axzo/nanopart/server/event/outer/EventTypeEnum.java new file mode 100644 index 00000000..a8eae015 --- /dev/null +++ b/op/op-server/src/main/java/cn/axzo/nanopart/server/event/outer/EventTypeEnum.java @@ -0,0 +1,31 @@ +package cn.axzo.nanopart.server.event.outer; + +import cn.axzo.framework.rocketmq.Event; +import lombok.Getter; + +/** + * @Classname EventTypeEnum + * @Date 2021/2/7 6:05 下午 + * @Created by lilong + */ +@Getter +public enum EventTypeEnum { + + MESSAGE_HISTORY_UPDATED("message-history", "message-history-updated", "发送记录修改") + ; + + EventTypeEnum(String model, String name, String desc) { + this.eventCode = Event.EventCode.builder() + .module(model) + .name(name) + .build(); + this.model = model; + this.name = name; + this.desc = desc; + } + + private String model; + private String name; + private String desc; + private Event.EventCode eventCode; +} diff --git a/op/op-server/src/main/java/cn/axzo/nanopart/server/event/outer/PushYouMengMessageHandler.java b/op/op-server/src/main/java/cn/axzo/nanopart/server/event/outer/PushYouMengMessageHandler.java new file mode 100644 index 00000000..ddcfca67 --- /dev/null +++ b/op/op-server/src/main/java/cn/axzo/nanopart/server/event/outer/PushYouMengMessageHandler.java @@ -0,0 +1,184 @@ +package cn.axzo.nanopart.server.event.outer; + +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.framework.rocketmq.EventHandler; +import cn.axzo.msg.center.api.MessagePushApi; +import cn.axzo.msg.center.api.request.MsgBody4Guest; +import cn.axzo.nanopart.server.domain.OpMessageConfig; +import cn.axzo.nanopart.server.event.payload.MessageHistoryUpdatedPayload; +import cn.axzo.nanopart.server.service.OpMessageConfigService; +import cn.axzo.nanopart.server.xxljob.SendMessageJob; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static cn.axzo.nanopart.server.event.payload.MessageHistoryUpdatedPayload.PLATFORM_ROUTER_TYPE; + +@Slf4j +@Component +public class PushYouMengMessageHandler implements EventHandler, InitializingBean { + + @Autowired + private EventConsumer eventConsumer; + @Autowired + private OpMessageConfigService opMessageConfigService; + @Autowired + private MessagePushApi messagePushApi; + + /** + * 按照这个顺序解析routerType + */ + private static final List ROUTER_TYPE_SORTED = Lists.newArrayList( + MessageHistoryUpdatedPayload.IOS_PLATFORM, + MessageHistoryUpdatedPayload.ANDROID_PLATFORM, + MessageHistoryUpdatedPayload.WEBVIEW_PLATFORM, + MessageHistoryUpdatedPayload.MINI_PROGRAM_PLATFORM, + MessageHistoryUpdatedPayload.WECHAT_MINI_PROGRAM_PLATFORM); + + /** + * 按照这个顺序解析IOS的url + */ + private static final List IOS_URL_SORTED = Lists.newArrayList( + MessageHistoryUpdatedPayload.IOS_PLATFORM, + MessageHistoryUpdatedPayload.WEBVIEW_PLATFORM, + MessageHistoryUpdatedPayload.MINI_PROGRAM_PLATFORM, + MessageHistoryUpdatedPayload.WECHAT_MINI_PROGRAM_PLATFORM); + + /** + * 按照这个顺序解析ANDROID的url + */ + private static final List ANDROID_URL_SORTED = Lists.newArrayList( + MessageHistoryUpdatedPayload.ANDROID_PLATFORM, + MessageHistoryUpdatedPayload.WEBVIEW_PLATFORM, + MessageHistoryUpdatedPayload.MINI_PROGRAM_PLATFORM, + MessageHistoryUpdatedPayload.WECHAT_MINI_PROGRAM_PLATFORM); + + + @Override + public void onEvent(Event event, EventConsumer.Context context) { + log.info("begin push-handler rocketmq event: {}", event); + MessageHistoryUpdatedPayload payload = event.normalizedData(MessageHistoryUpdatedPayload.class); + if (Objects.isNull(payload) || StringUtils.isBlank(payload.getNewMessageHistory().getBizId())) { + return; + } + + if (!payload.getNewMessageHistory().getBizId().startsWith(SendMessageJob.BIZ_ID_PREFIX)) { + log.info("push-handler 非op-message的消息"); + return; + } + + if (!payload.isSuccess()) { + log.info("push-handler is not success"); + return; + } + + if (payload.getNewMessageHistory().getImMessageTaskId() == null) { + log.info("push-handler imMessageTaskId is null"); + return; + } + + Optional opMessageConfigDTO = opMessageConfigService.page(OpMessageConfigService.PageOpMessageConfigParam.builder() + .imMessageTaskId(payload.getNewMessageHistory().getImMessageTaskId()) + .build()) + .getRecords() + .stream() + .findFirst(); + if (!opMessageConfigDTO.isPresent()) { + log.info("push-handler imMessageTaskId is not found,{}", payload.getNewMessageHistory().getImMessageTaskId()); + return; + } + + if (opMessageConfigDTO.get().getPushData() == null) { + log.info("push-handler, opMessageConfig:{},未配置push信息", opMessageConfigDTO.get().getId()); + return; + } + OpMessageConfig.PushData pushData = opMessageConfigDTO.get().getPushData().toJavaObject(OpMessageConfig.PushData.class); + + if (BooleanUtils.isNotTrue(pushData.isSwitchOn())) { + log.info("push-handler, opMessageConfig:{},push开关未打开", opMessageConfigDTO.get().getId()); + return; + } + + MessageHistoryUpdatedPayload.MessageHistory newMessageHistory = payload.getNewMessageHistory(); + MessageHistoryUpdatedPayload.MessageBody messageBody = newMessageHistory.resolveMessageBody(); + messagePushApi.sendPushMessage(MsgBody4Guest.builder() + .ty(0) + .f("0") + .appClient(newMessageHistory.getAppType()) + .m(messageBody.getMsgContent()) + .m3(newMessageHistory.getReceivePersonId()) + .m2(resolveM2(messageBody, pushData)) + // 为了兼容老版本,保证老版本的工人端能收到push,所以新的push都alias都是person + .t("not_identity" + newMessageHistory.getReceivePersonId()) + .ouId(newMessageHistory.getReceiveOuId()) + .pushType(pushData.getType()) + .build()); + + log.info("end push-handler rocketmq event: {}", event); + } + + private String resolveM2(MessageHistoryUpdatedPayload.MessageBody messageBody, + OpMessageConfig.PushData pushData) { + JSONObject jsonObject = new JSONObject() + .fluentPut("t", messageBody.getMsgHeader()) + .fluentPut("type", 0); + if (StringUtils.isNotBlank(pushData.getVoiceFile())) { + jsonObject.fluentPut("audio", pushData.getVoiceFile()); + } + MessageHistoryUpdatedPayload.CardDetailButton cardDetailButton = messageBody.resolveMsgBody().getCardDetailButton(); + if (cardDetailButton != null && CollectionUtils.isNotEmpty(cardDetailButton.getActionPaths())) { + resolveRouter(jsonObject, cardDetailButton.getActionPaths()); + } + return jsonObject.toJSONString(); + } + + /** + * + * @param jsonObject + * @param actionPaths + */ + private void resolveRouter(JSONObject jsonObject, List actionPaths) { + + Map actionPathMap = actionPaths.stream() + .collect(Collectors.toMap(MessageHistoryUpdatedPayload.ActionPath::getPlatform, Function.identity())); + Optional routerType = ROUTER_TYPE_SORTED.stream() + .map(actionPathMap::get) + .filter(Objects::nonNull) + .findFirst(); + routerType.ifPresent(actionPath -> jsonObject.fluentPut("rt", PLATFORM_ROUTER_TYPE.get(actionPath.getPlatform()))); + + Optional ios = IOS_URL_SORTED.stream() + .map(actionPathMap::get) + .filter(Objects::nonNull) + .findFirst(); + // IOS的跳转URL + ios.ifPresent(actionPath -> jsonObject.fluentPut("ir", actionPath.getUrl())); + + Optional android = ANDROID_URL_SORTED.stream() + .map(actionPathMap::get) + .filter(Objects::nonNull) + .findFirst(); + // ANDROID的跳转URL + android.ifPresent(actionPath -> jsonObject.fluentPut("ar", actionPath.getUrl())); + + } + + @Override + public void afterPropertiesSet() throws Exception { + eventConsumer.registerHandler(EventTypeEnum.MESSAGE_HISTORY_UPDATED.getEventCode(), this); + } +} diff --git a/op/op-server/src/main/java/cn/axzo/nanopart/server/event/payload/MessageHistoryUpdatedPayload.java b/op/op-server/src/main/java/cn/axzo/nanopart/server/event/payload/MessageHistoryUpdatedPayload.java new file mode 100644 index 00000000..3b4c24b7 --- /dev/null +++ b/op/op-server/src/main/java/cn/axzo/nanopart/server/event/payload/MessageHistoryUpdatedPayload.java @@ -0,0 +1,176 @@ +package cn.axzo.nanopart.server.event.payload; + +import cn.axzo.im.center.common.enums.AppTypeEnum; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Maps; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MessageHistoryUpdatedPayload implements Serializable { + + public static final String IOS_PLATFORM = "IOS"; + public static final String ANDROID_PLATFORM = "ANDROID"; + public static final String WEBVIEW_PLATFORM = "WEBVIEW"; + public static final String MINI_PROGRAM_PLATFORM = "MINI_PROGRAM"; + public static final String WECHAT_MINI_PROGRAM_PLATFORM = "WECHAT_MINI_PROGRAM"; + + public static final Map PLATFORM_ROUTER_TYPE = Maps.newHashMap(); + static { + PLATFORM_ROUTER_TYPE.put(IOS_PLATFORM, 2); + PLATFORM_ROUTER_TYPE.put(ANDROID_PLATFORM, 2); + PLATFORM_ROUTER_TYPE.put(WEBVIEW_PLATFORM, 3); + PLATFORM_ROUTER_TYPE.put(MINI_PROGRAM_PLATFORM, 1); + PLATFORM_ROUTER_TYPE.put(WECHAT_MINI_PROGRAM_PLATFORM, 5); + } + + private MessageHistory newMessageHistory; + private MessageHistory oldMessageHistory; + + public boolean isSuccess() { + return !Objects.equals(newMessageHistory.getStatus(), oldMessageHistory.getStatus()) + && Objects.equals(newMessageHistory.getStatus(), MessageHistory.Status.SUCCEED.name()); + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class MessageHistory implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + /** + * 上游业务请求ID + */ + private String bizId; + + /** + * 普通用户,通过appType包装 + * 包装以后进行账户注册 + */ + private String messageId; + + /** + * 发送者IM账户 + */ + private String fromAccount; + + /** + * 发送者IM账户 + */ + private String toAccount; + + /** + * 终端类型 + * + * @see AppTypeEnum + */ + private String appType; + + /** + * channel 网易云信 + */ + private String channel; + + private String messageBody; + + private String result; + + private Long imMessageTaskId; + + private String receivePersonId; + + private Long receiveOuId; + + private String status; + + private Integer isDelete; + + private Date createAt; + + private Date updateAt; + + public enum Status { + PENDING, + SUCCEED, + FAILED, + ; + } + + public MessageBody resolveMessageBody() { + return JSONObject.parseObject(messageBody, MessageBody.class); + } + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class MessageBody { + + /** + * 模板消息:Template、聊天消息:Chat、通知消息:Notify + */ + private String msgType; + + /** + * 消息标题 + */ + private String msgHeader; + + /** + * 消息内容 + */ + private String msgContent; + + /** + * 消息通知结构体 + */ + private String msgBody; + + public MsgBody resolveMsgBody() { + return JSONObject.parseObject(msgBody, MsgBody.class); + } + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class MsgBody { + private CardDetailButton cardDetailButton; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class CardDetailButton { + private List actionPaths; + + + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ActionPath { + private String platform; + + private String url; + } +} diff --git a/op/op-server/src/main/java/cn/axzo/nanopart/server/mapper/OpMessageConfigMapper.java b/op/op-server/src/main/java/cn/axzo/nanopart/server/mapper/OpMessageConfigMapper.java new file mode 100644 index 00000000..5f65d564 --- /dev/null +++ b/op/op-server/src/main/java/cn/axzo/nanopart/server/mapper/OpMessageConfigMapper.java @@ -0,0 +1,9 @@ +package cn.axzo.nanopart.server.mapper; + +import cn.axzo.nanopart.server.domain.OpMessageConfig; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface OpMessageConfigMapper extends BaseMapper { +} diff --git a/op/op-server/src/main/java/cn/axzo/nanopart/server/service/OpMessageConfigService.java b/op/op-server/src/main/java/cn/axzo/nanopart/server/service/OpMessageConfigService.java new file mode 100644 index 00000000..3a83c88a --- /dev/null +++ b/op/op-server/src/main/java/cn/axzo/nanopart/server/service/OpMessageConfigService.java @@ -0,0 +1,281 @@ +package cn.axzo.nanopart.server.service; + +import cn.axzo.basics.profiles.dto.basic.PersonProfileDto; +import cn.axzo.im.center.api.feign.RobotInfoApi; +import cn.axzo.nanopart.server.domain.OpMessageConfig; +import cn.axzo.pokonyan.dao.mysql.QueryWrapperHelper; +import cn.axzo.pokonyan.dao.page.IPageParam; +import cn.axzo.pokonyan.dao.wrapper.CriteriaField; +import cn.axzo.pokonyan.dao.wrapper.Operator; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.springframework.beans.BeanUtils; +import org.springframework.util.CollectionUtils; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public interface OpMessageConfigService extends IService { + + Page page(PageOpMessageConfigParam param); + + OpMessageConfig create(CreateOpMessageConfigParam param); + + OpMessageConfig update(UpdateOpMessageConfigParam param); + + void delete(DeleteOpMessageConfigParam param); + + @Builder + @Data + @NoArgsConstructor + @AllArgsConstructor + class DeleteOpMessageConfigParam { + private Long id; + + private Long deletePersonId; + } + + @Builder + @Data + @NoArgsConstructor + @AllArgsConstructor + class UpdateOpMessageConfigParam { + + private Long id; + + private String name; + + private String sendImAccount; + + private JSONObject receiveData; + + private String title; + + private String content; + + private String contentType; + + private String coverImg; + + private JSONObject jumpData; + + private JSONObject pushData; + + private JSONObject ext; + + private Date planStartTime; + + private Long updatePersonId; + + private Long submitPersonId; + + private OpMessageConfig.ActionEnum action; + + private Date submitTime; + + private Long imMessageTaskId; + + private Date startedTime; + + private Date finishedTime; + + public OpMessageConfig to() { + OpMessageConfig opMessageConfig = OpMessageConfig.builder().build(); + BeanUtils.copyProperties(this, opMessageConfig); + return opMessageConfig; + } + } + + @Builder + @Data + @NoArgsConstructor + @AllArgsConstructor + class CreateOpMessageConfigParam { + private String name; + + private String sendImAccount; + + private JSONObject receiveData; + + private OpMessageConfig.Status status; + + private String title; + + private String content; + + private String contentType; + + private String coverImg; + + private JSONObject jumpData; + + private JSONObject pushData; + + private JSONObject ext; + + private Date planStartTime; + + private Long createPersonId; + + private Long submitPersonId; + + public OpMessageConfig to() { + OpMessageConfig opMessageConfig = OpMessageConfig.builder().build(); + BeanUtils.copyProperties(this, opMessageConfig); + opMessageConfig.setSubmitTime(new Date()); + return opMessageConfig; + } + } + + @SuperBuilder + @Data + @NoArgsConstructor + @AllArgsConstructor + class ListOpMessageConfigParam { + + @CriteriaField(field = "id", operator = Operator.IN) + private List ids; + + @CriteriaField(field = "name", operator = Operator.LIKE) + private String nameLike; + + @CriteriaField(field = "sendImAccount", operator = Operator.EQ) + private String sendImAccount; + + @CriteriaField(field = "sendImAccount", operator = Operator.LIKE) + private String sendImAccountLike; + + @CriteriaField(field = "planStartTime", operator = Operator.LE) + private Date planStartTimeLE; + + @CriteriaField(field = "status", operator = Operator.IN) + private List statuses; + + @CriteriaField(field = "contentType", operator = Operator.EQ) + private String contentType; + + @CriteriaField(field = "imMessageTaskId", operator = Operator.EQ) + private Long imMessageTaskId; + + @CriteriaField(ignore = true) + private List receivePlatforms;; + + /** + * 返回发送的信息: + * IM机器人账号:返回头像、名字等信息 + * 账号有机器人和系统人员信息,查询的接口不同 + */ + @CriteriaField(ignore = true) + private boolean needSendUserInfo; + + /** + * 返回创建人的信息 + */ + @CriteriaField(ignore = true) + private boolean needCreatePersonInfo; + + /** + * 返回提交审核人的信息 + */ + @CriteriaField(ignore = true) + private boolean needSubmitPersonInfo; + } + + @Builder + @Data + @NoArgsConstructor + @AllArgsConstructor + class SendUserInfo { + + /** + * im账号 + */ + private String imAccount; + + /** + * 昵称 + */ + private String nickName; + + + /** + * 头像 + */ + private String headImageUrl; + + public static SendUserInfo from(RobotInfoApi.RobotInfoDTO robotInfoDTO) { + return SendUserInfo.builder() + .imAccount(robotInfoDTO.getImAccount()) + .nickName(robotInfoDTO.getNickName()) + .headImageUrl(robotInfoDTO.getHeadImageUrl()) + .build(); + } + } + + @SuperBuilder + @Data + @NoArgsConstructor + @AllArgsConstructor + class PageOpMessageConfigParam extends ListOpMessageConfigParam implements IPageParam { + @CriteriaField(ignore = true) + Integer page; + + @CriteriaField(ignore = true) + Integer pageSize; + + /** + * 排序:使用示例,createTime__DESC + */ + @CriteriaField(ignore = true) + List sort; + + public QueryWrapper toQueryWrapper() { + QueryWrapper wrapper = QueryWrapperHelper.fromBean(this, OpMessageConfig.class); + wrapper.eq("is_delete", 0); + + if (!CollectionUtils.isEmpty(this.getReceivePlatforms())) { + String sql = this.getReceivePlatforms().stream() + .map(e -> "json_contains(receive_data->'$.platforms', '\"" + e + "\"')") + .collect(Collectors.joining(" or ")); + wrapper.and(e -> e.apply(sql)); + } + return wrapper; + } + } + + @SuperBuilder + @Data + @NoArgsConstructor + @AllArgsConstructor + class OpMessageConfigDTO extends OpMessageConfig { + + private SendUserInfo sendUserInfo; + + private PersonProfileDto createPersonProfile; + + private PersonProfileDto submitPersonProfile; + + public static OpMessageConfigDTO from(OpMessageConfig opMessageConfig, + Map sendUserInfos, + Map createPersonProfiles, + Map submitPersonProfiles) { + OpMessageConfigDTO opMessageConfigDTO = OpMessageConfigDTO.builder().build(); + BeanUtils.copyProperties(opMessageConfig, opMessageConfigDTO); + + opMessageConfigDTO.setSendUserInfo(sendUserInfos.get(opMessageConfigDTO.getSendImAccount())); + + opMessageConfigDTO.setCreatePersonProfile(createPersonProfiles.get(opMessageConfigDTO.getCreatePersonId())); + opMessageConfigDTO.setSubmitPersonProfile(submitPersonProfiles.get(opMessageConfigDTO.getSubmitPersonId())); + return opMessageConfigDTO; + } + } +} diff --git a/op/op-server/src/main/java/cn/axzo/nanopart/server/service/impl/OpMessageConfigServiceImpl.java b/op/op-server/src/main/java/cn/axzo/nanopart/server/service/impl/OpMessageConfigServiceImpl.java new file mode 100644 index 00000000..16fa111d --- /dev/null +++ b/op/op-server/src/main/java/cn/axzo/nanopart/server/service/impl/OpMessageConfigServiceImpl.java @@ -0,0 +1,169 @@ +package cn.axzo.nanopart.server.service.impl; + +import cn.axzo.basics.profiles.api.UserProfileServiceApi; +import cn.axzo.basics.profiles.dto.basic.PersonProfileDto; +import cn.axzo.im.center.api.feign.RobotInfoApi; +import cn.axzo.nanopart.server.domain.OpMessageConfig; +import cn.axzo.nanopart.server.mapper.OpMessageConfigMapper; +import cn.axzo.nanopart.server.service.OpMessageConfigService; +import cn.axzo.pokonyan.dao.converter.PageConverter; +import cn.axzo.pokonyan.exception.Aassert; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static cn.axzo.nanopart.server.config.BizResultCode.DELETE_OP_MESSAGE_CONFIG_STATUS_ERROR; +import static cn.axzo.nanopart.server.config.BizResultCode.OP_MESSAGE_CONFIG_NOT_FOUND; + +@Slf4j +@Service +public class OpMessageConfigServiceImpl extends ServiceImpl + implements OpMessageConfigService { + + private static final Long DEFAULT_ZERO_PERSON_ID = 0L; + + @Autowired + private RobotInfoApi robotInfoApi; + @Autowired + private UserProfileServiceApi userProfileServiceApi; + + @Override + public Page page(PageOpMessageConfigParam param) { + QueryWrapper wrapper = param.toQueryWrapper(); + + Page page = this.page(PageConverter.convertToMybatis(param, OpMessageConfig.class), wrapper); + + Map sendUserInfos = listSendUserInfo(param, page.getRecords()); + + Map createPersonProfiles = listCreatePersonProfile(param, page.getRecords()); + Map submitPersonProfiles = listSubmitPersonProfile(param, page.getRecords()); + + return PageConverter.convert(page, (record) -> OpMessageConfigDTO.from(record, + sendUserInfos, + createPersonProfiles, + submitPersonProfiles)); + } + + @Override + public OpMessageConfig create(CreateOpMessageConfigParam param) { + OpMessageConfig opMessageConfig = param.to(); + this.save(opMessageConfig); + return this.getById(opMessageConfig.getId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public OpMessageConfig update(UpdateOpMessageConfigParam param) { + OpMessageConfig oldOpMessageConfig = getAndLock(param.getId()); + + OpMessageConfig opMessageConfig = param.to(); + if (param.getAction() != null) { + opMessageConfig.setStatus(param.getAction().getNextStatus(oldOpMessageConfig.getStatus())); + } + + this.updateById(opMessageConfig); + return this.getById(param.getId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(DeleteOpMessageConfigParam param) { + OpMessageConfig oldOpMessageConfig = getAndLock(param.getId()); + Aassert.check(oldOpMessageConfig.canDelete(), DELETE_OP_MESSAGE_CONFIG_STATUS_ERROR); + + // 因为updateById处理的时候,有拦截器过滤掉了isDelete的更新 + this.lambdaUpdate() + .set(OpMessageConfig::getIsDelete, 1) + .set(OpMessageConfig::getDeletePersonId, param.getDeletePersonId()) + .eq(OpMessageConfig::getId, param.getId()) + .update(); + } + + private OpMessageConfig getAndLock(Long id) { + OpMessageConfig oldOpMessageConfig = this.lambdaQuery() + .eq(OpMessageConfig::getId, id) + .last("for update") + .one(); + Aassert.notNull(oldOpMessageConfig, OP_MESSAGE_CONFIG_NOT_FOUND); + return oldOpMessageConfig; + } + + private Map listSendUserInfo(PageOpMessageConfigParam param, + List opMessageConfigs) { + if (CollectionUtils.isEmpty(opMessageConfigs) || BooleanUtils.isNotTrue(param.isNeedSendUserInfo())) { + return Collections.emptyMap(); + } + + List sendImAccounts = opMessageConfigs.stream() + .map(OpMessageConfig::getSendImAccount) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(sendImAccounts)) { + return Collections.emptyMap(); + } + + // 现在发送者只有机器人,所以发送者的头像信息从robotInfoApi中获取 + return robotInfoApi.page(RobotInfoApi.PageRobotInfoParam.builder() + .imAccounts(sendImAccounts) + .build()) + .getData() + .getList() + .stream() + .map(SendUserInfo::from) + .collect(Collectors.toMap(SendUserInfo::getImAccount, Function.identity())); + } + + private Map listCreatePersonProfile(PageOpMessageConfigParam param, + List opMessageConfigs) { + if (CollectionUtils.isEmpty(opMessageConfigs) || BooleanUtils.isNotTrue(param.isNeedCreatePersonInfo())) { + return Collections.emptyMap(); + } + + List createPersonIds = opMessageConfigs.stream() + .map(OpMessageConfig::getCreatePersonId) + .distinct() + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(createPersonIds)) { + return Collections.emptyMap(); + } + + return userProfileServiceApi.postPersonProfiles(createPersonIds).getData() + .stream() + .collect(Collectors.toMap(PersonProfileDto::getId, Function.identity())); + } + + private Map listSubmitPersonProfile(PageOpMessageConfigParam param, + List opMessageConfigs) { + if (CollectionUtils.isEmpty(opMessageConfigs) || BooleanUtils.isNotTrue(param.isNeedSubmitPersonInfo())) { + return Collections.emptyMap(); + } + + List submitPersonIds = opMessageConfigs.stream() + .map(OpMessageConfig::getSubmitPersonId) + .filter(id -> !Objects.equals(DEFAULT_ZERO_PERSON_ID, id)) + .distinct() + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(submitPersonIds)) { + return Collections.emptyMap(); + } + + return userProfileServiceApi.postPersonProfiles(submitPersonIds).getData() + .stream() + .collect(Collectors.toMap(PersonProfileDto::getId, Function.identity())); + } +} diff --git a/op/op-server/src/main/java/cn/axzo/nanopart/server/xxljob/SendMessageJob.java b/op/op-server/src/main/java/cn/axzo/nanopart/server/xxljob/SendMessageJob.java new file mode 100644 index 00000000..751df7b9 --- /dev/null +++ b/op/op-server/src/main/java/cn/axzo/nanopart/server/xxljob/SendMessageJob.java @@ -0,0 +1,234 @@ +package cn.axzo.nanopart.server.xxljob; + +import cn.axzo.framework.domain.web.result.ApiListResult; +import cn.axzo.im.center.api.feign.MessageApi; +import cn.axzo.im.center.api.vo.req.AsyncSendMessageParam; +import cn.axzo.im.center.api.vo.resp.MessageTaskResp; +import cn.axzo.im.center.common.enums.AppTypeEnum; +import cn.axzo.maokai.api.client.CooperateShipQueryApi; +import cn.axzo.maokai.api.vo.request.CooperateShipQueryReq; +import cn.axzo.maokai.api.vo.response.CooperateShipResp; +import cn.axzo.nanopart.server.domain.OpMessageConfig; +import cn.axzo.nanopart.server.service.OpMessageConfigService; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.google.common.collect.Lists; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SendMessageJob extends IJobHandler { + + public static final String BIZ_ID_PREFIX = "op-message"; + @Autowired + private OpMessageConfigService opMessageConfigService; + @Autowired + private MessageApi messageApi; + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CooperateShipQueryApi cooperateShipQueryApi; + + private static final Integer DEFAULT_PAGE_SIZE = 10; + + @Override + @XxlJob("sendMessageJob") + public ReturnT execute(String s) throws Exception { + log.info("start sendMessageJob,s:{}", s); + SendMessageParam sendMessageParam = Optional.ofNullable(s) + .map(e -> JSONObject.parseObject(e, SendMessageParam.class)) + .orElseGet(() -> SendMessageParam.builder().build()); + Integer pageNumber = 1; + Date now = new Date(); + + SendMessageJob proxySendMessageJob = applicationContext.getBean(this.getClass()); + + while (true) { + OpMessageConfigService.PageOpMessageConfigParam req = OpMessageConfigService.PageOpMessageConfigParam.builder() + .ids(sendMessageParam.getIds()) + .planStartTimeLE(now) + .statuses(Lists.newArrayList(OpMessageConfig.Status.PENDING.name())) + .page(pageNumber) + .pageSize(DEFAULT_PAGE_SIZE) + .build(); + + Page page = opMessageConfigService.page(req); + if (CollectionUtils.isNotEmpty(page.getRecords())) { + // 一个一个更新,数据量不大,一个失败不影响其他的运营消息 + page.getRecords().forEach(e -> { + try { + proxySendMessageJob.sendMessage(e); + } catch (Exception ex) { + log.error("job 执行失败, job{}, ex", e.getId(), ex); + } + }); + } + + if (!page.hasNext()) { + break; + } + } + log.info("end sendMessageJob"); + return ReturnT.SUCCESS; + } + + @Transactional(rollbackFor = Exception.class) + public void sendMessage(OpMessageConfigService.OpMessageConfigDTO opMessageConfig) { + opMessageConfigService.update(OpMessageConfigService.UpdateOpMessageConfigParam.builder() + .id(opMessageConfig.getId()) + .action(OpMessageConfig.ActionEnum.RUN) + .startedTime(new Date()) + .build()); + + OpMessageConfig.ReceiveData receiveData = opMessageConfig.resolveReceiveData(); + OpMessageConfig.ReceiveType receiveType = OpMessageConfig.ReceiveType.of(receiveData.getType()); + boolean isAllPerson = Objects.equals(OpMessageConfig.ReceiveType.ALL, receiveType); + + List appTypes = BooleanUtils.isTrue(isAllPerson) ? receiveData.getPlatforms().stream().map(AppTypeEnum::valueOf).collect(Collectors.toList()) : null; + + List receivePersons = resolveReceivePerson(receiveData); + // 接收方式是import,且解析后没有接收人,则直接完成任务 + // 存在只配置了管理端,但是import的人员没导入ouId,导致接收人为空 + if (CollectionUtils.isEmpty(receivePersons) && Objects.equals(OpMessageConfig.ReceiveType.IMPORT, receiveType)) { + opMessageConfigService.update(OpMessageConfigService.UpdateOpMessageConfigParam.builder() + .id(opMessageConfig.getId()) + // 消息是异步执行的,准确的完成时间,需要监听messageTask的完成时间才准确,现在mq配置较低,消息服务没有发送mq + .finishedTime(new Date()) + .action(OpMessageConfig.ActionEnum.COMPLETE) + .build()); + } else { + MessageTaskResp messageTask = messageApi.sendMessageAsync(AsyncSendMessageParam.builder() + .bizId(String.format("%s:%s", BIZ_ID_PREFIX, opMessageConfig.getId())) + .sendImAccount(opMessageConfig.getSendImAccount()) + .receivePersons(receivePersons) + .allPerson(isAllPerson) + .appTypes(appTypes) + .msgHeader(opMessageConfig.getTitle()) + .msgContent(opMessageConfig.getContent()) + .jumpData(resolveJumpData(opMessageConfig)) + .cardBannerUrl(opMessageConfig.getCoverImg()) + .build()).getData(); + opMessageConfigService.update(OpMessageConfigService.UpdateOpMessageConfigParam.builder() + .id(opMessageConfig.getId()) + .imMessageTaskId(messageTask.getId()) + // 消息是异步执行的,准确的完成时间,需要监听messageTask的完成时间才准确,现在mq配置较低,消息服务没有发送mq + .finishedTime(new Date()) + .action(OpMessageConfig.ActionEnum.COMPLETE) + .build()); + } + } + + + private List resolveReceivePerson(OpMessageConfig.ReceiveData receiveData) { + OpMessageConfig.ReceiveType receiveType = OpMessageConfig.ReceiveType.of(receiveData.getType()); + boolean isAllPerson = Objects.equals(OpMessageConfig.ReceiveType.ALL, receiveType); + + if (BooleanUtils.isTrue(isAllPerson)) { + return Collections.emptyList(); + } + + List cmReceivePersons = listCmReceivePersons(receiveData); + List cmpReceivePersons = listCmPReceivePersons(receiveData); + cmReceivePersons.addAll(cmpReceivePersons); + return cmReceivePersons; + } + + private List listCmReceivePersons(OpMessageConfig.ReceiveData receiveData) { + boolean isCm = receiveData.getPlatforms().stream() + .anyMatch(platform -> AppTypeEnum.valueOf(platform) == AppTypeEnum.CM); + + if (BooleanUtils.isNotTrue(isCm)) { + return Lists.newArrayList(); + } + + return receiveData.getPersons().stream() + .map(OpMessageConfig.Person::getPersonId) + .distinct() + .map(personId -> AsyncSendMessageParam.ReceivePerson.builder() + .personId(personId.toString()) + .appType(AppTypeEnum.CM) + .build()) + .collect(Collectors.toList()); + } + + private List listCmPReceivePersons(OpMessageConfig.ReceiveData receiveData) { + boolean isCmp = receiveData.getPlatforms().stream() + .anyMatch(platform -> AppTypeEnum.valueOf(platform) == AppTypeEnum.CMP); + + if (BooleanUtils.isNotTrue(isCmp)) { + return Lists.newArrayList(); + } + + Map workspaceIds = listWorkspaceIds(receiveData); + + return receiveData.getPersons().stream() + .filter(person -> person.getOuId() != null) + .map(person -> AsyncSendMessageParam.ReceivePerson.builder() + .personId(person.getPersonId().toString()) + .ouId(person.getOuId()) + .appType(AppTypeEnum.CMP) + .workspaceId(workspaceIds.get(person.getOuId())) + .build()) + .collect(Collectors.toList()); + } + + private Map listWorkspaceIds(OpMessageConfig.ReceiveData receiveData) { + List ouIds = receiveData.getPersons().stream() + .map(OpMessageConfig.Person::getOuId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(ouIds)) { + return Collections.emptyMap(); + } + log.info("list workspaceId,{}", ouIds); + ApiListResult cooperateShipRespApiListResult = cooperateShipQueryApi.genericQuery(CooperateShipQueryReq.builder() + .ouIdList(ouIds) + .workspaceType(1) + .build()); + log.info("list workspaceId result,{}", cooperateShipRespApiListResult); + return cooperateShipRespApiListResult.getData() + .stream() + .collect(Collectors.toMap(CooperateShipResp::getOrganizationalUnitId, CooperateShipResp::getWorkspaceId, (f, s) -> f)); + } + + private List resolveJumpData(OpMessageConfigService.OpMessageConfigDTO opMessageConfig) { + return opMessageConfig.resolveJumpData().getPlatforms().stream() + .map(platform -> cn.axzo.im.center.api.vo.req.SendMessageParam.JumpData.builder() + .platform(cn.axzo.im.center.api.vo.req.SendMessageParam.JumpPlatform.valueOf(platform.getPlatform().name())) + .url(platform.getUrl()) + .build()) + .collect(Collectors.toList()); + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SendMessageParam { + private List ids; + } +} diff --git a/op/op-server/src/test/resources/mysql/schema.sql b/op/op-server/src/test/resources/mysql/schema.sql new file mode 100644 index 00000000..71b13e91 --- /dev/null +++ b/op/op-server/src/test/resources/mysql/schema.sql @@ -0,0 +1,30 @@ + +CREATE TABLE IF NOT EXISTS op_message_config +( + id bigint auto_increment comment '主键', + im_message_task_id bigint not null default 0 comment 'im-center服务im_message_task的id', + `name` varchar(50) not null default '' comment '消息名字', + send_im_account varchar(100) not null default '' comment '发送者的三方平台账号id', + receive_data json null comment '消息接收配置', + status varchar(32) not null default 'DRAFT' comment '消息状态:DRAFT、PENDING、RUNNING、COMPLETED', + title varchar(128) not null default '' comment '消息标题', + content varchar(512) not null default '' comment '消息内容', + content_type varchar(32) not null default '' comment '消息形式:IMAGE_TEXT_SAMPLE', + cover_img varchar(512) not null default '' comment '消息封面大图', + jump_data json comment '跳转配置', + push_data json comment 'push配置', + ext varchar(1024) not null default '{}' COMMENT '其它额外信息', + plan_start_time DATETIME(3) null comment '任务计划开始时间,时间大于改时间会对未完成的任务进行执行操作', + started_time DATETIME(3) null comment '实际开始时间', + finished_time DATETIME(3) null comment '实际完成时间', + submit_time DATETIME(3) null comment '提交审核时间', + is_delete tinyint default 0 not null comment '未删除0,删除1', + create_at datetime default CURRENT_TIMESTAMP not null comment '创建时间', + create_person_id bigint not null comment '创建人id', + submit_person_id bigint not null default 0 comment '提交审核人id,可能跟crate_person_id不一样,比如先保存草稿', + delete_person_id bigint not null default 0 comment '删除人id', + update_person_id bigint not null default 0 comment '更新人id', + update_at datetime default CURRENT_TIMESTAMP not null comment '更新时间', + PRIMARY KEY (`id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8 comment '运营消息配置'; \ No newline at end of file diff --git a/op/pom.xml b/op/pom.xml new file mode 100644 index 00000000..32479682 --- /dev/null +++ b/op/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + + cn.axzo.nanopart + nanopart + ${revision} + ../pom.xml + + + cn.axzo.nanopart + op + pom + op + + + 2.0.0-SNAPSHOT + 2.0.0-SNAPSHOT + 1.18.22 + 1.4.2.Final + + + + + + + cn.axzo.infra + axzo-bom + ${axzo-bom.version} + pom + import + + + cn.axzo.infra + axzo-dependencies + ${axzo-dependencies.version} + pom + import + + + + + + + + + org.projectlombok + lombok + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + junit + junit + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + + + + + + axzo + axzo repository + https://nexus.axzo.cn/repository/axzo/ + + + + op-server + op-api + + diff --git a/pom.xml b/pom.xml index 964c6818..783f4ea1 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,7 @@ nanopart-server config job + op