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