diff --git a/pom.xml b/pom.xml
index c88e46ed6..b602f6a0e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,8 +16,9 @@
yudao-module-bpm
yudao-module-system
yudao-module-infra
-
+ yudao-module-pay
yudao-module-report
+ yudao-module-mp
${project.artifactId}
diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java
new file mode 100644
index 000000000..ee66fe7de
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java
@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.framework.excel.core.convert;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * 金额转换器
+ *
+ * 金额单位:分
+ *
+ * @author 芋道源码
+ */
+public class MoneyConvert implements Converter {
+
+ @Override
+ public Class> supportJavaTypeKey() {
+ throw new UnsupportedOperationException("暂不支持,也不需要");
+ }
+
+ @Override
+ public CellDataTypeEnum supportExcelTypeKey() {
+ throw new UnsupportedOperationException("暂不支持,也不需要");
+ }
+
+ @Override
+ public WriteCellData convertToExcelData(Integer value, ExcelContentProperty contentProperty,
+ GlobalConfiguration globalConfiguration) {
+ BigDecimal result = BigDecimal.valueOf(value)
+ .divide(new BigDecimal(100), 2, RoundingMode.HALF_UP);
+ return new WriteCellData<>(result.toString());
+ }
+
+}
diff --git a/yudao-gateway/src/main/resources/application.yaml b/yudao-gateway/src/main/resources/application.yaml
index 7e9f626e5..6ea12a77d 100644
--- a/yudao-gateway/src/main/resources/application.yaml
+++ b/yudao-gateway/src/main/resources/application.yaml
@@ -7,6 +7,7 @@ spring:
gateway:
# 路由配置项,对应 RouteDefinition 数组
routes:
+ ## system-server 服务
- id: system-admin-api # 路由的编号
uri: grayLb://system-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
@@ -19,6 +20,7 @@ spring:
- Path=/app-api/system/**
filters:
- RewritePath=/app-api/system/v3/api-docs, /v3/api-docs
+ ## infra-server 服务
- id: infra-admin-api # 路由的编号
uri: grayLb://infra-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
@@ -35,22 +37,34 @@ spring:
uri: grayLb://infra-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin/**
+ ## bpm-server 服务
- id: bpm-admin-api # 路由的编号
uri: grayLb://bpm-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/bpm/**
filters:
- RewritePath=/admin-api/bpm/v3/api-docs, /v3/api-docs
+ ## report-server 服务
- id: report-admin-api # 路由的编号
uri: grayLb://report-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/report/**
filters:
- RewritePath=/admin-api/report/v3/api-docs, /v3/api-docs
- - id: report-jmreport # 路由的编号(积木报表)
- uri: grayLb://report-server
+ ## pay-server 服务
+ - id: pay-admin-api # 路由的编号
+ uri: grayLb://pay-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- - Path=/jmreport/**
+ - Path=/admin-api/pay/**
+ filters:
+ - RewritePath=/admin-api/pay/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
+ - id: pay-app-api # 路由的编号
+ uri: grayLb://pay-server
+ predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
+ - Path=/app-api/pay/**
+ filters:
+ - RewritePath=/app-api/pay/v3/api-docs, /v3/api-docs
+
x-forwarded:
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
@@ -68,3 +82,6 @@ knife4j:
- name: bpm-server
service-name: bpm-server
url: /admin-api/bpm/v3/api-docs
+ - name: pay-server
+ service-name: pay-server
+ url: /admin-api/pay/v3/api-docs
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
index b8cdf7209..e36dd9628 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
@@ -1,307 +1 @@
-package cn.iocoder.yudao.module.bpm.service.task;
-
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.lang.Assert;
-import cn.hutool.core.util.StrUtil;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
-import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
-import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
-import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
-import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO;
-import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO;
-import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceExtMapper;
-import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceDeleteReasonEnum;
-import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
-import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
-import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEventPublisher;
-import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
-import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
-import cn.iocoder.yudao.module.system.api.dept.DeptApi;
-import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
-import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
-import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
-import lombok.extern.slf4j.Slf4j;
-import org.flowable.engine.HistoryService;
-import org.flowable.engine.RuntimeService;
-import org.flowable.engine.delegate.event.FlowableCancelledEvent;
-import org.flowable.engine.history.HistoricProcessInstance;
-import org.flowable.engine.repository.ProcessDefinition;
-import org.flowable.engine.runtime.ProcessInstance;
-import org.flowable.task.api.Task;
-import org.springframework.context.annotation.Lazy;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.validation.annotation.Validated;
-
-import javax.annotation.Resource;
-import javax.validation.Valid;
-import java.time.LocalDateTime;
-import java.util.*;
-
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
-import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
-
-/**
- * 流程实例 Service 实现类
- *
- * ProcessDefinition & ProcessInstance & Execution & Task 的关系:
- * 1.
- *
- * HistoricProcessInstance & ProcessInstance 的关系:
- * 1.
- *
- * 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例
- *
- * @author 芋道源码
- */
-@Service
-@Validated
-@Slf4j
-public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService {
-
- @Resource
- private RuntimeService runtimeService;
- @Resource
- private BpmProcessInstanceExtMapper processInstanceExtMapper;
- @Resource
- @Lazy // 解决循环依赖
- private BpmTaskService taskService;
- @Resource
- private BpmProcessDefinitionService processDefinitionService;
- @Resource
- private HistoryService historyService;
- @Resource
- private AdminUserApi adminUserApi;
- @Resource
- private DeptApi deptApi;
- @Resource
- private BpmProcessInstanceResultEventPublisher processInstanceResultEventPublisher;
- @Resource
- private BpmMessageService messageService;
- @Override
- public ProcessInstance getProcessInstance(String id) {
- return runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult();
- }
-
- @Override
- public List getProcessInstances(Set ids) {
- return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list();
- }
-
- @Override
- public PageResult getMyProcessInstancePage(Long userId,
- BpmProcessInstanceMyPageReqVO pageReqVO) {
- // 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页
- PageResult pageResult = processInstanceExtMapper.selectPage(userId, pageReqVO);
- if (CollUtil.isEmpty(pageResult.getList())) {
- return new PageResult<>(pageResult.getTotal());
- }
-
- // 获得流程 Task Map
- List processInstanceIds = convertList(pageResult.getList(), BpmProcessInstanceExtDO::getProcessInstanceId);
- Map> taskMap = taskService.getTaskMapByProcessInstanceIds(processInstanceIds);
- // 转换返回
- return BpmProcessInstanceConvert.INSTANCE.convertPage(pageResult, taskMap);
- }
-
- @Override
- @Transactional(rollbackFor = Exception.class)
- public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) {
- // 获得流程定义
- ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId());
- // 发起流程
- return createProcessInstance0(userId, definition, createReqVO.getVariables(), null);
- }
-
- @Override
- public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) {
- // 获得流程定义
- ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey());
- // 发起流程
- return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey());
- }
-
- @Override
- public BpmProcessInstanceRespVO getProcessInstanceVO(String id) {
- // 获得流程实例
- HistoricProcessInstance processInstance = getHistoricProcessInstance(id);
- if (processInstance == null) {
- return null;
- }
- BpmProcessInstanceExtDO processInstanceExt = processInstanceExtMapper.selectByProcessInstanceId(id);
- Assert.notNull(processInstanceExt, "流程实例拓展({}) 不存在", id);
-
- // 获得流程定义
- ProcessDefinition processDefinition = processDefinitionService
- .getProcessDefinition(processInstance.getProcessDefinitionId());
- Assert.notNull(processDefinition, "流程定义({}) 不存在", processInstance.getProcessDefinitionId());
- BpmProcessDefinitionExtDO processDefinitionExt = processDefinitionService.getProcessDefinitionExt(
- processInstance.getProcessDefinitionId());
- Assert.notNull(processDefinitionExt, "流程定义拓展({}) 不存在", id);
- String bpmnXml = processDefinitionService.getProcessDefinitionBpmnXML(processInstance.getProcessDefinitionId());
-
- // 获得 User
- AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId())).getCheckedData();
- DeptRespDTO dept = null;
- if (startUser != null) {
- dept = deptApi.getDept(startUser.getDeptId()).getCheckedData();
- }
-
- // 拼接结果
- return BpmProcessInstanceConvert.INSTANCE.convert2(processInstance, processInstanceExt,
- processDefinition, processDefinitionExt, bpmnXml, startUser, dept);
- }
-
- @Override
- public void cancelProcessInstance(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) {
- // 校验流程实例存在
- ProcessInstance instance = getProcessInstance(cancelReqVO.getId());
- if (instance == null) {
- throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
- }
- // 只能取消自己的
- if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) {
- throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF);
- }
-
- // 通过删除流程实例,实现流程实例的取消,
- // 删除流程实例,正则执行任务 ACT_RU_TASK. 任务会被删除。通过历史表查询
- deleteProcessInstance(cancelReqVO.getId(),
- BpmProcessInstanceDeleteReasonEnum.CANCEL_TASK.format(cancelReqVO.getReason()));
- }
-
- /**
- * 获得历史的流程实例
- *
- * @param id 流程实例的编号
- * @return 历史的流程实例
- */
- @Override
- public HistoricProcessInstance getHistoricProcessInstance(String id) {
- return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).singleResult();
- }
-
- @Override
- public List getHistoricProcessInstances(Set ids) {
- return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list();
- }
-
- @Override
- public void createProcessInstanceExt(ProcessInstance instance) {
- // 获得流程定义
- ProcessDefinition definition = processDefinitionService.getProcessDefinition2(instance.getProcessDefinitionId());
- // 插入 BpmProcessInstanceExtDO 对象
- BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
- .setProcessInstanceId(instance.getId())
- .setProcessDefinitionId(definition.getId())
- .setName(instance.getProcessDefinitionName())
- .setStartUserId(Long.valueOf(instance.getStartUserId()))
- .setCategory(definition.getCategory())
- .setStatus(BpmProcessInstanceStatusEnum.RUNNING.getStatus())
- .setResult(BpmProcessInstanceResultEnum.PROCESS.getResult());
-
- processInstanceExtMapper.insert(instanceExtDO);
- }
-
- @Override
- public void updateProcessInstanceExtCancel(FlowableCancelledEvent event) {
- // 判断是否为 Reject 不通过。如果是,则不进行更新.
- // 因为,updateProcessInstanceExtReject 方法,已经进行更新了
- if (BpmProcessInstanceDeleteReasonEnum.isRejectReason((String)event.getCause())) {
- return;
- }
-
- // 需要主动查询,因为 instance 只有 id 属性
- // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance
- HistoricProcessInstance processInstance = getHistoricProcessInstance(event.getProcessInstanceId());
- // 更新拓展表
- BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
- .setProcessInstanceId(event.getProcessInstanceId())
- .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置
- .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
- .setResult(BpmProcessInstanceResultEnum.CANCEL.getResult());
- processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);
-
- // 发送流程实例的状态事件
- processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
- BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
- }
-
- @Override
- public void updateProcessInstanceExtComplete(ProcessInstance instance) {
- // 需要主动查询,因为 instance 只有 id 属性
- // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance
- HistoricProcessInstance processInstance = getHistoricProcessInstance(instance.getId());
- // 更新拓展表
- BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
- .setProcessInstanceId(instance.getProcessInstanceId())
- .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置
- .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
- .setResult(BpmProcessInstanceResultEnum.APPROVE.getResult()); // 如果正常完全,说明审批通过
- processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);
-
- // 发送流程被通过的消息
- messageService.sendMessageWhenProcessInstanceApprove(BpmProcessInstanceConvert.INSTANCE.convert2ApprovedReq(instance));
-
- // 发送流程实例的状态事件
- processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
- BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
- }
-
- @Override
- @Transactional(rollbackFor = Exception.class)
- public void updateProcessInstanceExtReject(String id, String reason) {
- // 需要主动查询,因为 instance 只有 id 属性
- ProcessInstance processInstance = getProcessInstance(id);
- // 删除流程实例,以实现驳回任务时,取消整个审批流程
- deleteProcessInstance(id, StrUtil.format(BpmProcessInstanceDeleteReasonEnum.REJECT_TASK.format(reason)));
-
- // 更新 status + result
- // 注意,不能和上面的逻辑更换位置。因为 deleteProcessInstance 会触发流程的取消,进而调用 updateProcessInstanceExtCancel 方法,
- // 设置 result 为 BpmProcessInstanceStatusEnum.CANCEL,显然和 result 不一定是一致的
- BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO().setProcessInstanceId(id)
- .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
- .setResult(BpmProcessInstanceResultEnum.REJECT.getResult());
- processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);
-
- // 发送流程被不通过的消息
- messageService.sendMessageWhenProcessInstanceReject(BpmProcessInstanceConvert.INSTANCE.convert2RejectReq(processInstance, reason));
-
- // 发送流程实例的状态事件
- processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
- BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
- }
-
- private void deleteProcessInstance(String id, String reason) {
- runtimeService.deleteProcessInstance(id, reason);
- }
-
- private String createProcessInstance0(Long userId, ProcessDefinition definition,
- Map variables, String businessKey) {
- // 校验流程定义
- if (definition == null) {
- throw exception(PROCESS_DEFINITION_NOT_EXISTS);
- }
- if (definition.isSuspended()) {
- throw exception(PROCESS_DEFINITION_IS_SUSPENDED);
- }
-
- // 创建流程实例
- ProcessInstance instance = runtimeService.createProcessInstanceBuilder()
- .processDefinitionId(definition.getId())
- .businessKey(businessKey)
- .name(definition.getName().trim())
- .variables(variables)
- .start();
- // 设置流程名字
- runtimeService.setProcessInstanceName(instance.getId(), definition.getName());
-
- // 补全流程实例的拓展表
- processInstanceExtMapper.updateByProcessInstanceId(new BpmProcessInstanceExtDO().setProcessInstanceId(instance.getId())
- .setFormVariables(variables));
- return instance.getId();
- }
-
-}
+package cn.iocoder.yudao.module.bpm.service.task;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceExtMapper;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceDeleteReasonEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEventPublisher;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.event.FlowableCancelledEvent;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
/**
* 流程实例 Service 实现类
*
* ProcessDefinition & ProcessInstance & Execution & Task 的关系:
* 1.
*
* HistoricProcessInstance & ProcessInstance 的关系:
* 1.
*
* 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService {
@Resource
private RuntimeService runtimeService;
@Resource
private BpmProcessInstanceExtMapper processInstanceExtMapper;
@Resource
@Lazy // 解决循环依赖
private BpmTaskService taskService;
@Resource
private BpmProcessDefinitionService processDefinitionService;
@Resource
private HistoryService historyService;
@Resource
private AdminUserApi adminUserApi;
@Resource
private DeptApi deptApi;
@Resource
private BpmProcessInstanceResultEventPublisher processInstanceResultEventPublisher;
@Resource
private BpmMessageService messageService;
@Override
public ProcessInstance getProcessInstance(String id) {
return runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult();
}
@Override
public List getProcessInstances(Set ids) {
return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list();
}
@Override
public PageResult getMyProcessInstancePage(Long userId,
BpmProcessInstanceMyPageReqVO pageReqVO) {
// 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页
PageResult pageResult = processInstanceExtMapper.selectPage(userId, pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return new PageResult<>(pageResult.getTotal());
}
// 获得流程 Task Map
List processInstanceIds = convertList(pageResult.getList(), BpmProcessInstanceExtDO::getProcessInstanceId);
Map> taskMap = taskService.getTaskMapByProcessInstanceIds(processInstanceIds);
// 转换返回
return BpmProcessInstanceConvert.INSTANCE.convertPage(pageResult, taskMap);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) {
// 获得流程定义
ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId());
// 发起流程
return createProcessInstance0(userId, definition, createReqVO.getVariables(), null);
}
@Override
public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) {
// 获得流程定义
ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey());
// 发起流程
return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey());
}
@Override
public BpmProcessInstanceRespVO getProcessInstanceVO(String id) {
// 获得流程实例
HistoricProcessInstance processInstance = getHistoricProcessInstance(id);
if (processInstance == null) {
return null;
}
BpmProcessInstanceExtDO processInstanceExt = processInstanceExtMapper.selectByProcessInstanceId(id);
Assert.notNull(processInstanceExt, "流程实例拓展({}) 不存在", id);
// 获得流程定义
ProcessDefinition processDefinition = processDefinitionService
.getProcessDefinition(processInstance.getProcessDefinitionId());
Assert.notNull(processDefinition, "流程定义({}) 不存在", processInstance.getProcessDefinitionId());
BpmProcessDefinitionExtDO processDefinitionExt = processDefinitionService.getProcessDefinitionExt(
processInstance.getProcessDefinitionId());
Assert.notNull(processDefinitionExt, "流程定义拓展({}) 不存在", id);
String bpmnXml = processDefinitionService.getProcessDefinitionBpmnXML(processInstance.getProcessDefinitionId());
// 获得 User
AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId())).getCheckedData();
DeptRespDTO dept = null;
if (startUser != null) {
dept = deptApi.getDept(startUser.getDeptId()).getCheckedData();
}
// 拼接结果
return BpmProcessInstanceConvert.INSTANCE.convert2(processInstance, processInstanceExt,
processDefinition, processDefinitionExt, bpmnXml, startUser, dept);
}
@Override
public void cancelProcessInstance(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) {
// 校验流程实例存在
ProcessInstance instance = getProcessInstance(cancelReqVO.getId());
if (instance == null) {
throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
}
// 只能取消自己的
if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) {
throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF);
}
// 通过删除流程实例,实现流程实例的取消,
// 删除流程实例,正则执行任务 ACT_RU_TASK. 任务会被删除。通过历史表查询
deleteProcessInstance(cancelReqVO.getId(),
BpmProcessInstanceDeleteReasonEnum.CANCEL_TASK.format(cancelReqVO.getReason()));
}
/**
* 获得历史的流程实例
*
* @param id 流程实例的编号
* @return 历史的流程实例
*/
@Override
public HistoricProcessInstance getHistoricProcessInstance(String id) {
return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).singleResult();
}
@Override
public List getHistoricProcessInstances(Set ids) {
return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list();
}
@Override
public void createProcessInstanceExt(ProcessInstance instance) {
// 获得流程定义
ProcessDefinition definition = processDefinitionService.getProcessDefinition2(instance.getProcessDefinitionId());
// 插入 BpmProcessInstanceExtDO 对象
BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
.setProcessInstanceId(instance.getId())
.setProcessDefinitionId(definition.getId())
.setName(instance.getProcessDefinitionName())
.setStartUserId(Long.valueOf(instance.getStartUserId()))
.setCategory(definition.getCategory())
.setStatus(BpmProcessInstanceStatusEnum.RUNNING.getStatus())
.setResult(BpmProcessInstanceResultEnum.PROCESS.getResult());
processInstanceExtMapper.insert(instanceExtDO);
}
@Override
public void updateProcessInstanceExtCancel(FlowableCancelledEvent event) {
// 判断是否为 Reject 不通过。如果是,则不进行更新.
// 因为,updateProcessInstanceExtReject 方法,已经进行更新了
if (BpmProcessInstanceDeleteReasonEnum.isRejectReason((String)event.getCause())) {
return;
}
// 需要主动查询,因为 instance 只有 id 属性
// 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance
HistoricProcessInstance processInstance = getHistoricProcessInstance(event.getProcessInstanceId());
// 更新拓展表
BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
.setProcessInstanceId(event.getProcessInstanceId())
.setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置
.setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
.setResult(BpmProcessInstanceResultEnum.CANCEL.getResult());
processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);
// 发送流程实例的状态事件
processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
}
@Override
public void updateProcessInstanceExtComplete(ProcessInstance instance) {
// 需要主动查询,因为 instance 只有 id 属性
// 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance
HistoricProcessInstance processInstance = getHistoricProcessInstance(instance.getId());
// 更新拓展表
BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
.setProcessInstanceId(instance.getProcessInstanceId())
.setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置
.setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
.setResult(BpmProcessInstanceResultEnum.APPROVE.getResult()); // 如果正常完全,说明审批通过
processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);
// 发送流程被通过的消息
messageService.sendMessageWhenProcessInstanceApprove(BpmProcessInstanceConvert.INSTANCE.convert2ApprovedReq(instance));
// 发送流程实例的状态事件
processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateProcessInstanceExtReject(String id, String reason) {
// 需要主动查询,因为 instance 只有 id 属性
ProcessInstance processInstance = getProcessInstance(id);
// 删除流程实例,以实现驳回任务时,取消整个审批流程
deleteProcessInstance(id, StrUtil.format(BpmProcessInstanceDeleteReasonEnum.REJECT_TASK.format(reason)));
// 更新 status + result
// 注意,不能和上面的逻辑更换位置。因为 deleteProcessInstance 会触发流程的取消,进而调用 updateProcessInstanceExtCancel 方法,
// 设置 result 为 BpmProcessInstanceStatusEnum.CANCEL,显然和 result 不一定是一致的
BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO().setProcessInstanceId(id)
.setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
.setResult(BpmProcessInstanceResultEnum.REJECT.getResult());
processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);
// 发送流程被不通过的消息
messageService.sendMessageWhenProcessInstanceReject(BpmProcessInstanceConvert.INSTANCE.convert2RejectReq(processInstance, reason));
// 发送流程实例的状态事件
processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
}
private void deleteProcessInstance(String id, String reason) {
runtimeService.deleteProcessInstance(id, reason);
}
private String createProcessInstance0(Long userId, ProcessDefinition definition,
Map variables, String businessKey) {
// 校验流程定义
if (definition == null) {
throw exception(PROCESS_DEFINITION_NOT_EXISTS);
}
if (definition.isSuspended()) {
throw exception(PROCESS_DEFINITION_IS_SUSPENDED);
}
// 创建流程实例
ProcessInstance instance = runtimeService.createProcessInstanceBuilder()
.processDefinitionId(definition.getId())
.businessKey(businessKey)
.name(definition.getName().trim())
.variables(variables)
.start();
// 设置流程名字
runtimeService.setProcessInstanceName(instance.getId(), definition.getName());
// 补全流程实例的拓展表
processInstanceExtMapper.updateByProcessInstanceId(new BpmProcessInstanceExtDO().setProcessInstanceId(instance.getId())
.setFormVariables(variables));
return instance.getId();
}
}
\ No newline at end of file
diff --git a/yudao-module-mp/pom.xml b/yudao-module-mp/pom.xml
new file mode 100644
index 000000000..88735881d
--- /dev/null
+++ b/yudao-module-mp/pom.xml
@@ -0,0 +1,24 @@
+
+
+
+ yudao
+ cn.iocoder.cloud
+ ${revision}
+
+ 4.0.0
+
+ yudao-module-mp
+ pom
+
+
+ wechat 模块,主要实现微信平台的相关业务。
+ 例如:微信公众号、企业微信 SCRM 等
+
+
+ yudao-module-mp-api
+ yudao-module-mp-biz
+
+
+
diff --git a/yudao-module-mp/yudao-module-mp-api/pom.xml b/yudao-module-mp/yudao-module-mp-api/pom.xml
new file mode 100644
index 000000000..9cad0d93f
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-api/pom.xml
@@ -0,0 +1,26 @@
+
+
+
+ yudao-module-mp
+ cn.iocoder.cloud
+ ${revision}
+
+ 4.0.0
+ yudao-module-mp-api
+ jar
+
+ ${project.artifactId}
+
+ mp 模块 API,暴露给其它模块调用
+
+
+
+
+ cn.iocoder.cloud
+ yudao-common
+
+
+
+
diff --git a/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java
new file mode 100644
index 000000000..262306e34
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java
@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.mp.enums;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+
+/**
+ * Mp 错误码枚举类
+ *
+ * mp 系统,使用 1-006-000-000 段
+ */
+public interface ErrorCodeConstants {
+
+ // ========== 公众号账号 1006000000============
+ ErrorCode ACCOUNT_NOT_EXISTS = new ErrorCode(1006000000, "公众号账号不存在");
+ ErrorCode ACCOUNT_GENERATE_QR_CODE_FAIL = new ErrorCode(1006000001, "生成公众号二维码失败,原因:{}");
+ ErrorCode ACCOUNT_CLEAR_QUOTA_FAIL = new ErrorCode(1006000002, "清空公众号的 API 配额失败,原因:{}");
+
+ // ========== 公众号统计 1006001000============
+ ErrorCode STATISTICS_GET_USER_SUMMARY_FAIL = new ErrorCode(1006001000, "获取粉丝增减数据失败,原因:{}");
+ ErrorCode STATISTICS_GET_USER_CUMULATE_FAIL = new ErrorCode(1006001001, "获得粉丝累计数据失败,原因:{}");
+ ErrorCode STATISTICS_GET_UPSTREAM_MESSAGE_FAIL = new ErrorCode(1006001002, "获得消息发送概况数据失败,原因:{}");
+ ErrorCode STATISTICS_GET_INTERFACE_SUMMARY_FAIL = new ErrorCode(1006001003, "获得接口分析数据失败,原因:{}");
+
+ // ========== 公众号标签 1006002000============
+ ErrorCode TAG_NOT_EXISTS = new ErrorCode(1006002000, "标签不存在");
+ ErrorCode TAG_CREATE_FAIL = new ErrorCode(1006002001, "创建标签失败,原因:{}");
+ ErrorCode TAG_UPDATE_FAIL = new ErrorCode(1006002002, "更新标签失败,原因:{}");
+ ErrorCode TAG_DELETE_FAIL = new ErrorCode(1006002003, "删除标签失败,原因:{}");
+ ErrorCode TAG_GET_FAIL = new ErrorCode(1006002004, "获得标签失败,原因:{}");
+
+ // ========== 公众号粉丝 1006003000============
+ ErrorCode USER_NOT_EXISTS = new ErrorCode(1006003000, "粉丝不存在");
+ ErrorCode USER_UPDATE_TAG_FAIL = new ErrorCode(1006003001, "更新粉丝标签失败,原因:{}");
+
+ // ========== 公众号素材 1006004000============
+ ErrorCode MATERIAL_NOT_EXISTS = new ErrorCode(1006004000, "素材不存在");
+ ErrorCode MATERIAL_UPLOAD_FAIL = new ErrorCode(1006004001, "上传素材失败,原因:{}");
+ ErrorCode MATERIAL_IMAGE_UPLOAD_FAIL = new ErrorCode(1006004002, "上传图片失败,原因:{}");
+ ErrorCode MATERIAL_DELETE_FAIL = new ErrorCode(1006004003, "删除素材失败,原因:{}");
+
+ // ========== 公众号消息 1006005000============
+ ErrorCode MESSAGE_SEND_FAIL = new ErrorCode(1006005000, "发送消息失败,原因:{}");
+
+ // ========== 公众号发布能力 1006006000============
+ ErrorCode FREE_PUBLISH_LIST_FAIL = new ErrorCode(1006006000, "获得已成功发布列表失败,原因:{}");
+ ErrorCode FREE_PUBLISH_SUBMIT_FAIL = new ErrorCode(1006006001, "提交发布失败,原因:{}");
+ ErrorCode FREE_PUBLISH_DELETE_FAIL = new ErrorCode(1006006002, "删除发布失败,原因:{}");
+
+ // ========== 公众号草稿 1006007000============
+ ErrorCode DRAFT_LIST_FAIL = new ErrorCode(1006007000, "获得草稿列表失败,原因:{}");
+ ErrorCode DRAFT_CREATE_FAIL = new ErrorCode(1006007001, "创建草稿失败,原因:{}");
+ ErrorCode DRAFT_UPDATE_FAIL = new ErrorCode(1006007002, "更新草稿失败,原因:{}");
+ ErrorCode DRAFT_DELETE_FAIL = new ErrorCode(1006007003, "删除草稿失败,原因:{}");
+
+ // ========== 公众号菜单 1006008000============
+ ErrorCode MENU_SAVE_FAIL = new ErrorCode(1006008000, "创建菜单失败,原因:{}");
+ ErrorCode MENU_DELETE_FAIL = new ErrorCode(1006008001, "删除菜单失败,原因:{}");
+
+ // ========== 公众号自动回复 1006009000============
+ ErrorCode AUTO_REPLY_NOT_EXISTS = new ErrorCode(1006009000, "自动回复不存在");
+ ErrorCode AUTO_REPLY_ADD_SUBSCRIBE_FAIL_EXISTS = new ErrorCode(1006009001, "操作失败,原因:已存在关注时的回复");
+ ErrorCode AUTO_REPLY_ADD_MESSAGE_FAIL_EXISTS = new ErrorCode(1006009002, "操作失败,原因:已存在该消息类型的回复");
+ ErrorCode AUTO_REPLY_ADD_KEYWORD_FAIL_EXISTS = new ErrorCode(1006009003, "操作失败,原因:已关在该关键字的回复");
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyMatchEnum.java b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyMatchEnum.java
new file mode 100644
index 000000000..a5298dbf3
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyMatchEnum.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.mp.enums.message;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 公众号消息自动回复的匹配模式
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum MpAutoReplyMatchEnum {
+
+ ALL(1, "完全匹配"),
+ LIKE(2, "半匹配"),
+ ;
+
+ /**
+ * 匹配
+ */
+ private final Integer match;
+ /**
+ * 匹配的名字
+ */
+ private final String name;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyTypeEnum.java b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyTypeEnum.java
new file mode 100644
index 000000000..300f1f2ba
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyTypeEnum.java
@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.mp.enums.message;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 公众号消息自动回复的类型
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum MpAutoReplyTypeEnum {
+
+ SUBSCRIBE(1, "关注时回复"),
+ MESSAGE(2, "收到消息回复"),
+ KEYWORD(3, "关键词回复"),
+ ;
+
+ /**
+ * 来源
+ */
+ private final Integer type;
+ /**
+ * 类型的名字
+ */
+ private final String name;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpMessageSendFromEnum.java b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpMessageSendFromEnum.java
new file mode 100644
index 000000000..a2af484a5
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpMessageSendFromEnum.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.mp.enums.message;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 微信公众号消息的发送来源
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum MpMessageSendFromEnum {
+
+ USER_TO_MP(1, "粉丝发送给公众号"),
+ MP_TO_USER(2, "公众号发给粉丝"),
+ ;
+
+ /**
+ * 来源
+ */
+ private final Integer from;
+ /**
+ * 来源的名字
+ */
+ private final String name;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/package-info.java b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/package-info.java
new file mode 100644
index 000000000..598718088
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * mp 模块,我们放微信微信公众号。
+ * 例如说:提供微信公众号的账号、菜单、粉丝、标签、消息、自动回复、素材、模板通知、运营数据等功能
+ *
+ * 1. Controller URL:以 /mp/ 开头,避免和其它 Module 冲突
+ * 2. DataObject 表名:以 mp_ 开头,方便在数据库中区分
+ */
+package cn.iocoder.yudao.module.mp;
diff --git a/yudao-module-mp/yudao-module-mp-biz/pom.xml b/yudao-module-mp/yudao-module-mp-biz/pom.xml
new file mode 100644
index 000000000..ff1d72262
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/pom.xml
@@ -0,0 +1,119 @@
+
+
+
+ yudao-module-mp
+ cn.iocoder.cloud
+ ${revision}
+
+ 4.0.0
+ yudao-module-mp-biz
+ jar
+
+ ${project.artifactId}
+
+ mp 模块,我们放微信微信公众号。
+ 例如说:提供微信公众号的账号、菜单、粉丝、标签、消息、自动回复、素材、模板通知、运营数据等功能
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-bootstrap
+
+
+
+
+ cn.iocoder.cloud
+ yudao-module-mp-api
+ ${revision}
+
+
+ cn.iocoder.cloud
+ yudao-module-system-api
+ ${revision}
+
+
+ cn.iocoder.cloud
+ yudao-module-infra-api
+ ${revision}
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-biz-operatelog
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-biz-weixin
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-biz-tenant
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-security
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-mybatis
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-redis
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-rpc
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-discovery
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-config
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-test
+ test
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-monitor
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-excel
+
+
+
+
+
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/MpAccountController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/MpAccountController.java
new file mode 100644
index 000000000..62be175ac
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/MpAccountController.java
@@ -0,0 +1,98 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mp.controller.admin.account.vo.*;
+import cn.iocoder.yudao.module.mp.convert.account.MpAccountConvert;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 公众号账号")
+@RestController
+@RequestMapping("/mp/account")
+@Validated
+public class MpAccountController {
+
+ @Resource
+ private MpAccountService mpAccountService;
+
+ @PostMapping("/create")
+ @Operation(summary = "创建公众号账号")
+ @PreAuthorize("@ss.hasPermission('mp:account:create')")
+ public CommonResult createAccount(@Valid @RequestBody MpAccountCreateReqVO createReqVO) {
+ return success(mpAccountService.createAccount(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新公众号账号")
+ @PreAuthorize("@ss.hasPermission('mp:account:update')")
+ public CommonResult updateAccount(@Valid @RequestBody MpAccountUpdateReqVO updateReqVO) {
+ mpAccountService.updateAccount(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除公众号账号")
+ @Parameter(name = "id", description = "编号", required = true)
+ @PreAuthorize("@ss.hasPermission('mp:account:delete')")
+ public CommonResult deleteAccount(@RequestParam("id") Long id) {
+ mpAccountService.deleteAccount(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得公众号账号")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('mp:account:query')")
+ public CommonResult getAccount(@RequestParam("id") Long id) {
+ MpAccountDO wxAccount = mpAccountService.getAccount(id);
+ return success(MpAccountConvert.INSTANCE.convert(wxAccount));
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得公众号账号分页")
+ @PreAuthorize("@ss.hasPermission('mp:account:query')")
+ public CommonResult> getAccountPage(@Valid MpAccountPageReqVO pageVO) {
+ PageResult pageResult = mpAccountService.getAccountPage(pageVO);
+ return success(MpAccountConvert.INSTANCE.convertPage(pageResult));
+ }
+
+ @GetMapping("/list-all-simple")
+ @Operation(summary = "获取公众号账号精简信息列表")
+ @PreAuthorize("@ss.hasPermission('mp:account:query')")
+ public CommonResult> getSimpleAccounts() {
+ List list = mpAccountService.getAccountList();
+ return success(MpAccountConvert.INSTANCE.convertList02(list));
+ }
+
+ @PutMapping("/generate-qr-code")
+ @Operation(summary = "生成公众号二维码")
+ @Parameter(name = "id", description = "编号", required = true)
+ @PreAuthorize("@ss.hasPermission('mp:account:qr-code')")
+ public CommonResult generateAccountQrCode(@RequestParam("id") Long id) {
+ mpAccountService.generateAccountQrCode(id);
+ return success(true);
+ }
+
+ @PutMapping("/clear-quota")
+ @Operation(summary = "清空公众号 API 配额")
+ @Parameter(name = "id", description = "编号", required = true)
+ @PreAuthorize("@ss.hasPermission('mp:account:clear-quota')")
+ public CommonResult clearAccountQuota(@RequestParam("id") Long id) {
+ mpAccountService.clearAccountQuota(id);
+ return success(true);
+ }
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountBaseVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountBaseVO.java
new file mode 100644
index 000000000..8f688309d
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountBaseVO.java
@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+
+/**
+ * 公众号账号 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ *
+ * @author fengdan
+ */
+@Data
+public class MpAccountBaseVO {
+
+ @Schema(description = "公众号名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
+ @NotEmpty(message = "公众号名称不能为空")
+ private String name;
+
+ @Schema(description = "公众号微信号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudaoyuanma")
+ @NotEmpty(message = "公众号微信号不能为空")
+ private String account;
+
+ @Schema(description = "公众号 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx5b23ba7a5589ecbb")
+ @NotEmpty(message = "公众号 appId 不能为空")
+ private String appId;
+
+ @Schema(description = "公众号密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "3a7b3b20c537e52e74afd395eb85f61f")
+ @NotEmpty(message = "公众号密钥不能为空")
+ private String appSecret;
+
+ @Schema(description = "公众号 token", requiredMode = Schema.RequiredMode.REQUIRED, example = "kangdayuzhen")
+ @NotEmpty(message = "公众号 token 不能为空")
+ private String token;
+
+ @Schema(description = "加密密钥", example = "gjN+Ksei")
+ private String aesKey;
+
+ @Schema(description = "备注", example = "请关注芋道源码,学习技术")
+ private String remark;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java
new file mode 100644
index 000000000..b864f16c5
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 公众号账号创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpAccountCreateReqVO extends MpAccountBaseVO {
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java
new file mode 100644
index 000000000..93093dc19
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 公众号账号分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpAccountPageReqVO extends PageParam {
+
+ @Schema(name = "公众号名称", description = "模糊匹配")
+ private String name;
+
+ @Schema(name = "公众号账号", description = "模糊匹配")
+ private String account;
+
+ @Schema(name = "公众号 appid", description = "模糊匹配")
+ private String appId;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountRespVO.java
new file mode 100644
index 000000000..cb37bb2de
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountRespVO.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.util.Date;
+
+@Schema(description = "管理后台 - 公众号账号 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpAccountRespVO extends MpAccountBaseVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "二维码图片URL", example = "https://www.iocoder.cn/1024.png")
+ private String qrCodeUrl;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Date createTime;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java
new file mode 100644
index 000000000..db4594f6d
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java
@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 公众号账号精简信息 Response VO")
+@Data
+public class MpAccountSimpleRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "公众号名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
+ private String name;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java
new file mode 100644
index 000000000..e93123e6c
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.mp.controller.admin.account.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号账号更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpAccountUpdateReqVO extends MpAccountBaseVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @NotNull(message = "编号不能为空")
+ private Long id;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.http
new file mode 100644
index 000000000..74b8f40b6
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.http
@@ -0,0 +1,5 @@
+### 请求 /mp/material/page 接口 => 成功
+GET {{baseUrl}}/mp/material/page?permanent=true&pageNo=1&pageSize=10
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.java
new file mode 100644
index 000000000..ef175d2af
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.java
@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mp.controller.admin.material.vo.*;
+import cn.iocoder.yudao.module.mp.convert.material.MpMaterialConvert;
+import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO;
+import cn.iocoder.yudao.module.mp.service.material.MpMaterialService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.io.IOException;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 公众号素材")
+@RestController
+@RequestMapping("/mp/material")
+@Validated
+public class MpMaterialController {
+
+ @Resource
+ private MpMaterialService mpMaterialService;
+
+ @Operation(summary = "上传临时素材")
+ @PostMapping("/upload-temporary")
+ @PreAuthorize("@ss.hasPermission('mp:material:upload-temporary')")
+ public CommonResult uploadTemporaryMaterial(
+ @Valid MpMaterialUploadTemporaryReqVO reqVO) throws IOException {
+ MpMaterialDO material = mpMaterialService.uploadTemporaryMaterial(reqVO);
+ return success(MpMaterialConvert.INSTANCE.convert(material));
+ }
+
+ @Operation(summary = "上传永久素材")
+ @PostMapping("/upload-permanent")
+ @PreAuthorize("@ss.hasPermission('mp:material:upload-permanent')")
+ public CommonResult uploadPermanentMaterial(
+ @Valid MpMaterialUploadPermanentReqVO reqVO) throws IOException {
+ MpMaterialDO material = mpMaterialService.uploadPermanentMaterial(reqVO);
+ return success(MpMaterialConvert.INSTANCE.convert(material));
+ }
+
+ @Operation(summary = "删除素材")
+ @DeleteMapping("/delete-permanent")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('mp:material:delete')")
+ public CommonResult deleteMaterial(@RequestParam("id") Long id) {
+ mpMaterialService.deleteMaterial(id);
+ return success(true);
+ }
+
+ @Operation(summary = "上传图文内容中的图片")
+ @PostMapping("/upload-news-image")
+ @PreAuthorize("@ss.hasPermission('mp:material:upload-news-image')")
+ public CommonResult uploadNewsImage(@Valid MpMaterialUploadNewsImageReqVO reqVO)
+ throws IOException {
+ return success(mpMaterialService.uploadNewsImage(reqVO));
+ }
+
+ @Operation(summary = "获得素材分页")
+ @GetMapping("/page")
+ @PreAuthorize("@ss.hasPermission('mp:material:query')")
+ public CommonResult> getMaterialPage(@Valid MpMaterialPageReqVO pageReqVO) {
+ PageResult pageResult = mpMaterialService.getMaterialPage(pageReqVO);
+ return success(MpMaterialConvert.INSTANCE.convertPage(pageResult));
+ }
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java
new file mode 100644
index 000000000..8a2b60ff6
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号素材的分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpMaterialPageReqVO extends PageParam {
+
+ @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ @NotNull(message = "公众号账号的编号不能为空")
+ private Long accountId;
+
+ @Schema(description = "是否永久", example = "true")
+ private Boolean permanent;
+
+ @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", example = "image")
+ private String type;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialRespVO.java
new file mode 100644
index 000000000..222a4225b
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialRespVO.java
@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.Date;
+
+@Schema(description = "管理后台 - 公众号素材 Response VO")
+@Data
+public class MpMaterialRespVO {
+
+ @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long accountId;
+ @Schema(description = "公众号账号的 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890")
+ private String appId;
+
+ @Schema(description = "素材的 media_id", requiredMode = Schema.RequiredMode.REQUIRED, example = "123")
+ private String mediaId;
+
+ @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "image")
+ private String type;
+
+ @Schema(description = "是否永久 true - 永久;false - 临时", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+ private Boolean permanent;
+
+ @Schema(description = "素材的 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+ private String url;
+
+
+ @Schema(description = "名字", example = "yunai.png")
+ private String name;
+
+ @Schema(description = "公众号文件 URL 只有【永久素材】使用", example = "https://mmbiz.qpic.cn/xxx.mp3")
+ private String mpUrl;
+
+ @Schema(description = "视频素材的标题 只有【永久素材】使用", example = "我是标题")
+ private String title;
+ @Schema(description = "视频素材的描述 只有【永久素材】使用", example = "我是介绍")
+ private String introduction;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Date createTime;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java
new file mode 100644
index 000000000..6ede4c5eb
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java
@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material.vo;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号素材上传图文内容中的图片 Request VO")
+@Data
+public class MpMaterialUploadNewsImageReqVO {
+
+ @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ @NotNull(message = "公众号账号的编号不能为空")
+ private Long accountId;
+
+ @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotNull(message = "文件不能为空")
+ @JsonIgnore // 避免被操作日志,进行序列化,导致报错
+ private MultipartFile file;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java
new file mode 100644
index 000000000..9ca8a4ad8
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java
@@ -0,0 +1,53 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material.vo;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import me.chanjar.weixin.common.api.WxConsts;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.validation.constraints.AssertTrue;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号素材上传永久 Request VO")
+@Data
+public class MpMaterialUploadPermanentReqVO {
+
+ @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ @NotNull(message = "公众号账号的编号不能为空")
+ private Long accountId;
+
+ @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "image")
+ @NotEmpty(message = "文件类型不能为空")
+ private String type;
+
+ @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotNull(message = "文件不能为空")
+ @JsonIgnore // 避免被操作日志,进行序列化,导致报错
+ private MultipartFile file;
+
+ @Schema(description = "名字 如果 name 为空,则使用 file 文件名", example = "wechat.mp")
+ private String name;
+
+ @Schema(description = "视频素材的标题 文件类型为 video 时,必填", example = "视频素材的标题")
+ private String title;
+ @Schema(description = "视频素材的描述 文件类型为 video 时,必填", example = "视频素材的描述")
+ private String introduction;
+
+ @AssertTrue(message = "标题不能为空")
+ public boolean isTitleValid() {
+ // 生成场景为管理后台时,必须设置上级菜单,不然生成的菜单 SQL 是无父级菜单的
+ return ObjectUtil.notEqual(type, WxConsts.MediaFileType.VIDEO)
+ || title != null;
+ }
+
+ @AssertTrue(message = "描述不能为空")
+ public boolean isIntroductionValid() {
+ // 生成场景为管理后台时,必须设置上级菜单,不然生成的菜单 SQL 是无父级菜单的
+ return ObjectUtil.notEqual(type, WxConsts.MediaFileType.VIDEO)
+ || introduction != null;
+ }
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java
new file mode 100644
index 000000000..0174cbcc2
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java
@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 公众号素材上传结果 Response VO")
+@Data
+public class MpMaterialUploadRespVO {
+
+ @Schema(description = "素材的 media_id", requiredMode = Schema.RequiredMode.REQUIRED, example = "123")
+ private String mediaId;
+
+ @Schema(description = "素材的 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+ private String url;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java
new file mode 100644
index 000000000..abd6d06d1
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.mp.controller.admin.material.vo;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 公众号素材上传临时 Request VO")
+@Data
+public class MpMaterialUploadTemporaryReqVO {
+
+ @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ @NotNull(message = "公众号账号的编号不能为空")
+ private Long accountId;
+
+ @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "image")
+ @NotEmpty(message = "文件类型不能为空")
+ private String type;
+
+ @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotNull(message = "文件不能为空")
+ @JsonIgnore // 避免被操作日志,进行序列化,导致报错
+ private MultipartFile file;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.http
new file mode 100644
index 000000000..2276b3b4f
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.http
@@ -0,0 +1,50 @@
+### 请求 /mp/menu/save 接口 => 成功
+POST {{baseUrl}}/mp/menu/save
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+ "accountId": "1",
+ "menus": [
+ {
+ "type":"click",
+ "name":"今日歌曲",
+ "menuKey":"V1001_TODAY_MUSIC"
+ },
+ {
+ "name":"搜索",
+ "type":"view",
+ "url":"https://www.soso.com/"
+ },
+ {
+ "name": "父按钮",
+ "children": [
+ {
+ "type":"click",
+ "name":"归去来兮",
+ "menuKey":"MUSIC"
+ },
+ {
+ "name":"不说",
+ "type":"view",
+ "url":"https://www.soso.com/"
+ }]
+ }]
+}
+
+### 请求 /mp/menu/save 接口 => 成功(清空)
+POST {{baseUrl}}/mp/menu/save
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+ "accountId": "1",
+ "menus": []
+}
+
+### 请求 /mp/menu/list 接口 => 成功
+GET {{baseUrl}}/mp/menu/list?accountId=1
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.java
new file mode 100644
index 000000000..b7c883519
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.java
@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.mp.controller.admin.menu;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.MpMenuRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO;
+import cn.iocoder.yudao.module.mp.convert.menu.MpMenuConvert;
+import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
+import cn.iocoder.yudao.module.mp.service.menu.MpMenuService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 公众号菜单")
+@RestController
+@RequestMapping("/mp/menu")
+@Validated
+public class MpMenuController {
+
+ @Resource
+ private MpMenuService mpMenuService;
+
+ @PostMapping("/save")
+ @Operation(summary = "保存公众号菜单")
+ @PreAuthorize("@ss.hasPermission('mp:menu:save')")
+ public CommonResult saveMenu(@Valid @RequestBody MpMenuSaveReqVO createReqVO) {
+ mpMenuService.saveMenu(createReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除公众号菜单")
+ @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "10")
+ @PreAuthorize("@ss.hasPermission('mp:menu:delete')")
+ public CommonResult deleteMenu(@RequestParam("accountId") Long accountId) {
+ mpMenuService.deleteMenuByAccountId(accountId);
+ return success(true);
+ }
+
+ @GetMapping("/list")
+ @Operation(summary = "获得公众号菜单列表")
+ @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "10")
+ @PreAuthorize("@ss.hasPermission('mp:menu:query')")
+ public CommonResult> getMenuList(@RequestParam("accountId") Long accountId) {
+ List list = mpMenuService.getMenuListByAccountId(accountId);
+ return success(MpMenuConvert.INSTANCE.convertList(list));
+ }
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java
new file mode 100644
index 000000000..cd0e5b2e4
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java
@@ -0,0 +1,115 @@
+package cn.iocoder.yudao.module.mp.controller.admin.menu.vo;
+
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import me.chanjar.weixin.common.api.WxConsts;
+import org.hibernate.validator.constraints.URL;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+import static cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
+
+/**
+ * 公众号菜单 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class MpMenuBaseVO {
+
+ /**
+ * 菜单名称
+ */
+ private String name;
+ /**
+ * 菜单标识
+ *
+ * 支持多 DB 类型时,无法直接使用 key + @TableField("menuKey") 来实现转换,原因是 "menuKey" AS key 而存在报错
+ */
+ private String menuKey;
+ /**
+ * 父菜单编号
+ */
+ private Long parentId;
+
+ // ========== 按钮操作 ==========
+
+ /**
+ * 按钮类型
+ *
+ * 枚举 {@link WxConsts.MenuButtonType}
+ */
+ private String type;
+
+ @Schema(description = "网页链接", example = "https://www.iocoder.cn/")
+ @NotEmpty(message = "网页链接不能为空", groups = {ViewButtonGroup.class, MiniProgramButtonGroup.class})
+ @URL(message = "网页链接必须是 URL 格式")
+ private String url;
+
+ @Schema(description = "小程序的 appId", example = "wx1234567890")
+ @NotEmpty(message = "小程序的 appId 不能为空", groups = MiniProgramButtonGroup.class)
+ private String miniProgramAppId;
+
+ @Schema(description = "小程序的页面路径", example = "pages/index/index")
+ @NotEmpty(message = "小程序的页面路径不能为空", groups = MiniProgramButtonGroup.class)
+ private String miniProgramPagePath;
+
+ @Schema(description ="跳转图文的媒体编号", example = "jCQk93AIIgp8ixClWcW_NXXqBKInNWNmq2XnPeDZl7IMVqWiNeL4FfELtggRXd83")
+ @NotEmpty(message = "跳转图文的媒体编号不能为空", groups = ViewLimitedButtonGroup.class)
+ private String articleId;
+
+ // ========== 消息内容 ==========
+
+ @Schema(description = "回复的消息类型 枚举 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC", example = "text")
+ @NotEmpty(message = "回复的消息类型不能为空", groups = {ClickButtonGroup.class, ScanCodeWaitMsgButtonGroup.class})
+ private String replyMessageType;
+
+ @Schema(description = "回复的消息内容", example = "欢迎关注")
+ @NotEmpty(message = "回复的消息内容不能为空", groups = TextMessageGroup.class)
+ private String replyContent;
+
+ @Schema(description = "回复的媒体 id", example = "123456")
+ @NotEmpty(message = "回复的消息 mediaId 不能为空",
+ groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
+ private String replyMediaId;
+ @Schema(description = "回复的媒体 URL", example = "https://www.iocoder.cn/xxx.jpg")
+ @NotEmpty(message = "回复的消息 mediaId 不能为空",
+ groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
+ private String replyMediaUrl;
+
+ @Schema(description = "缩略图的媒体 id", example = "123456")
+ @NotEmpty(message = "回复的消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class})
+ private String replyThumbMediaId;
+ @Schema(description = "缩略图的媒体 URL",example = "https://www.iocoder.cn/xxx.jpg")
+ @NotEmpty(message = "回复的消息 thumbMedia 地址不能为空", groups = {MusicMessageGroup.class})
+ private String replyThumbMediaUrl;
+
+ @Schema(description = "回复的标题", example = "视频标题")
+ @NotEmpty(message = "回复的消息标题不能为空", groups = VideoMessageGroup.class)
+ private String replyTitle;
+ @Schema(description = "回复的描述", example = "视频描述")
+ @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
+ private String replyDescription;
+
+ /**
+ * 回复的图文消息数组
+ *
+ * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
+ */
+ @NotNull(message = "回复的图文消息不能为空", groups = {NewsMessageGroup.class, ViewLimitedButtonGroup.class})
+ @Valid
+ private List replyArticles;
+
+ @Schema(description = "回复的音乐链接", example = "https://www.iocoder.cn/xxx.mp3")
+ @NotEmpty(message = "回复的音乐链接不能为空", groups = MusicMessageGroup.class)
+ @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
+ private String replyMusicUrl;
+ @Schema(description = "高质量音乐链接", example = "https://www.iocoder.cn/xxx.mp3")
+ @NotEmpty(message = "回复的高质量音乐链接不能为空", groups = MusicMessageGroup.class)
+ @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
+ private String replyHqMusicUrl;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuRespVO.java
new file mode 100644
index 000000000..253e2d117
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuRespVO.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.mp.controller.admin.menu.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.util.Date;
+
+@Schema(description = "管理后台 - 公众号菜单 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MpMenuRespVO extends MpMenuBaseVO {
+
+ @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ private Long accountId;
+
+ @Schema(description = "公众号 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890ox")
+ private String appId;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Date createTime;
+
+}
diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java
new file mode 100644
index 000000000..580e62845
--- /dev/null
+++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.mp.controller.admin.menu.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Schema(description = "管理后台 - 公众号菜单保存 Request VO")
+@Data
+public class MpMenuSaveReqVO {
+
+ @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ @NotNull(message = "公众号账号的编号不能为空")
+ private Long accountId;
+
+ @NotEmpty(message = "菜单不能为空")
+ @Valid
+ private List