mirror of
http://git.pig4cloud.com/pig/pigx.git
synced 2026-11-08 04:39:00 +08:00
fix(flow): unify workflow notices and draft reliability
(cherry picked from commit 1632fbb83d24be1523e6fa809ed2ad0ca3e6cb0b)
This commit is contained in:
1 parent
4f6e796c55
commit
d0aa3a7284
17 files changed
+289
-108
No files matched your search
+8
-4
@@ -1533,7 +1533,11 @@ CREATE TABLE `sys_message` (
|
||||
`update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '删除时间',
|
||||
`tenant_id` bigint DEFAULT NULL COMMENT '租户',
|
||||
PRIMARY KEY (`id`)
|
||||
`business_type` varchar(32) DEFAULT NULL COMMENT '业务类型',
|
||||
`business_id` varchar(64) DEFAULT NULL COMMENT '业务记录ID',
|
||||
`request_key` varchar(100) DEFAULT NULL COMMENT '调用方事件标识',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_message_request` (`tenant_id`,`request_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='站内信息';
|
||||
|
||||
-- ----------------------------
|
||||
@@ -3243,7 +3247,7 @@ CREATE TABLE IF NOT EXISTS flow_draft (
|
||||
kind varchar(16) NOT NULL, flow_id varchar(64) NOT NULL DEFAULT '', title varchar(200) NOT NULL,
|
||||
payload longtext NOT NULL, revision bigint NOT NULL, updated_at timestamp(3) NOT NULL,
|
||||
PRIMARY KEY(id), KEY idx_draft_owner(tenant_id,user_id,kind,updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
CREATE TABLE IF NOT EXISTS flow_notice (
|
||||
id varchar(36) NOT NULL, tenant_id bigint NOT NULL, user_id varchar(64) NOT NULL,
|
||||
event_key varchar(180) NOT NULL, kind varchar(16) NOT NULL, title varchar(200) NOT NULL,
|
||||
@@ -3253,7 +3257,7 @@ CREATE TABLE IF NOT EXISTS flow_notice (
|
||||
reminder_key varchar(64) NULL,
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_notice_event(tenant_id,user_id,event_key), KEY idx_notice_owner(tenant_id,user_id,created_at),
|
||||
UNIQUE KEY uk_notice_reminder(tenant_id,reminder_key), KEY idx_notice_due(status,scheduled_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
CREATE TABLE IF NOT EXISTS flow_callback (
|
||||
id varchar(36) NOT NULL, tenant_id bigint NOT NULL, task_id varchar(64) NOT NULL,
|
||||
instance_id varchar(64) NOT NULL, target_url varchar(2000) NOT NULL, payload longtext NOT NULL,
|
||||
@@ -3262,4 +3266,4 @@ CREATE TABLE IF NOT EXISTS flow_callback (
|
||||
lease_token varchar(36) NULL, lease_until timestamp(3) NULL, last_error varchar(500) NULL,
|
||||
created_at timestamp(3) NOT NULL, updated_at timestamp(3) NOT NULL,
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_callback_task(tenant_id,task_id), KEY idx_callback_due(status,next_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
+5
-1
@@ -1484,7 +1484,11 @@ CREATE TABLE `sys_message` (
|
||||
`update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '删除时间',
|
||||
`tenant_id` bigint DEFAULT NULL COMMENT '租户',
|
||||
PRIMARY KEY (`id`)
|
||||
`business_type` varchar(32) DEFAULT NULL COMMENT '业务类型',
|
||||
`business_id` varchar(64) DEFAULT NULL COMMENT '业务记录ID',
|
||||
`request_key` varchar(100) DEFAULT NULL COMMENT '调用方事件标识',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_message_request` (`tenant_id`,`request_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='站内信息';
|
||||
|
||||
-- ----------------------------
|
||||
|
||||
+3
-3
@@ -296,7 +296,7 @@ CREATE TABLE IF NOT EXISTS flow_draft (
|
||||
kind varchar(16) NOT NULL, flow_id varchar(64) NOT NULL DEFAULT '', title varchar(200) NOT NULL,
|
||||
payload longtext NOT NULL, revision bigint NOT NULL, updated_at timestamp(3) NOT NULL,
|
||||
PRIMARY KEY(id), KEY idx_draft_owner(tenant_id,user_id,kind,updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
CREATE TABLE IF NOT EXISTS flow_notice (
|
||||
id varchar(36) NOT NULL, tenant_id bigint NOT NULL, user_id varchar(64) NOT NULL,
|
||||
event_key varchar(180) NOT NULL, kind varchar(16) NOT NULL, title varchar(200) NOT NULL,
|
||||
@@ -306,7 +306,7 @@ CREATE TABLE IF NOT EXISTS flow_notice (
|
||||
reminder_key varchar(64) NULL,
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_notice_event(tenant_id,user_id,event_key), KEY idx_notice_owner(tenant_id,user_id,created_at),
|
||||
UNIQUE KEY uk_notice_reminder(tenant_id,reminder_key), KEY idx_notice_due(status,scheduled_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
CREATE TABLE IF NOT EXISTS flow_callback (
|
||||
id varchar(36) NOT NULL, tenant_id bigint NOT NULL, task_id varchar(64) NOT NULL,
|
||||
instance_id varchar(64) NOT NULL, target_url varchar(2000) NOT NULL, payload longtext NOT NULL,
|
||||
@@ -315,4 +315,4 @@ CREATE TABLE IF NOT EXISTS flow_callback (
|
||||
lease_token varchar(36) NULL, lease_until timestamp(3) NULL, last_error varchar(500) NULL,
|
||||
created_at timestamp(3) NOT NULL, updated_at timestamp(3) NOT NULL,
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_callback_task(tenant_id,task_id), KEY idx_callback_due(status,next_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
+5
-3
@@ -4,8 +4,7 @@ import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 流程通知的持久化状态;编码与 flow_notice.status 及升级 SQL 保持一致。 待触发提醒发送后转为 SENT,任务结束时转为 CANCELLED;列表仅展示
|
||||
* SENT。
|
||||
* 流程通知事件状态;READY 经统一信息推送发送后转为 SENT,待触发提醒随任务结束取消。
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@@ -14,7 +13,10 @@ public enum FlowNoticeStatusEnum {
|
||||
/** 等待截止时间,到期后确定当前审批人。 */
|
||||
PENDING("PENDING"),
|
||||
|
||||
/** 通知已生成,允许接收人查看及标记已读。 */
|
||||
/** 等待投递到统一站内信。 */
|
||||
READY("READY"),
|
||||
|
||||
/** 已投递到统一站内信,已读状态由 sys_message_relation 维护。 */
|
||||
SENT("SENT"),
|
||||
|
||||
/** 任务已经结束,取消尚未发送的提醒。 */
|
||||
|
||||
+18
@@ -97,4 +97,22 @@ public class FlowDurableConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
/** 复用流程事件表投递站内信;失败延迟一分钟再试,不阻塞引擎事务。 */
|
||||
@Scheduled(fixedDelayString = "${flow.delivery.scan-delay:10000}", scheduler = "flowDeliveryScheduler")
|
||||
public void scanNotices() {
|
||||
var notices = jdbc.queryForList(
|
||||
"SELECT id FROM flow_notice WHERE status=? AND scheduled_at<=CURRENT_TIMESTAMP(3) ORDER BY scheduled_at LIMIT 100",
|
||||
FlowNoticeStatusEnum.READY.getCode());
|
||||
for (var row : notices) {
|
||||
try {
|
||||
reliability.getObject().deliverNotice(String.valueOf(row.get("id")));
|
||||
}
|
||||
catch (Exception exception) {
|
||||
jdbc.update("UPDATE flow_notice SET scheduled_at=TIMESTAMPADD(SECOND,60,CURRENT_TIMESTAMP(3)) WHERE id=? AND status=?",
|
||||
row.get("id"), FlowNoticeStatusEnum.READY.getCode());
|
||||
log.warn("流程站内信暂未发送,事件 {}", row.get("id"), exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+3
-26
@@ -55,8 +55,9 @@ public class FlowWorkspaceController {
|
||||
*/
|
||||
@Operation(summary = "分页查询个人草稿")
|
||||
@GetMapping("/drafts")
|
||||
public R<List<Map<String, Object>>> list(@RequestParam String kind, @RequestParam(defaultValue = "1") int page) {
|
||||
return R.ok(drafts.list(tenant(), user(), kind, page));
|
||||
public R<List<Map<String, Object>>> list(@RequestParam String kind, @RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(required = false) String flowId) {
|
||||
return R.ok(drafts.list(tenant(), user(), kind, page, flowId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,30 +85,6 @@ public class FlowWorkspaceController {
|
||||
return R.ok(drafts.delete(tenant(), user(), id, revision));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询个人站内通知。
|
||||
* @param page 页码
|
||||
* @return 通知列表
|
||||
*/
|
||||
@Operation(summary = "分页查询个人通知")
|
||||
@GetMapping("/notices")
|
||||
public R<List<Map<String, Object>>> notices(@RequestParam(defaultValue = "1") int page) {
|
||||
return R.ok(reliability.notices(tenant(), user(), page));
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记个人通知已读。
|
||||
* @param id 通知 ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
@Operation(summary = "标记个人通知已读")
|
||||
@SysLog("标记个人通知已读")
|
||||
@PutMapping("/notices/{id}/read")
|
||||
public R<Void> read(@PathVariable String id) {
|
||||
reliability.read(tenant(), user(), id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对自己发起的实例催办。
|
||||
* @param id 实例 ID
|
||||
|
||||
+4
-3
@@ -77,12 +77,13 @@ public class FlowDraftService {
|
||||
* @param user 当前用户
|
||||
* @param kind DESIGN 或 APPLICATION
|
||||
* @param page 从 1 开始的页码
|
||||
* @param flowId 可选的流程 ID,为空时查询全部流程
|
||||
* @return 每页最多 50 个草稿
|
||||
*/
|
||||
public List<Map<String, Object>> list(long tenant, long user, String kind, int page) {
|
||||
public List<Map<String, Object>> list(long tenant, long user, String kind, int page, String flowId) {
|
||||
return jdbc.queryForList(
|
||||
"SELECT id,kind,flow_id,title,revision,updated_at FROM flow_draft WHERE tenant_id=? AND user_id=? AND kind=? ORDER BY updated_at DESC LIMIT 50 OFFSET ?",
|
||||
tenant, user, kind, Math.max(0L, (long) page - 1) * 50);
|
||||
"SELECT id,kind,flow_id,title,revision,updated_at FROM flow_draft WHERE tenant_id=? AND user_id=? AND kind=? AND (? IS NULL OR flow_id=?) ORDER BY updated_at DESC, id DESC LIMIT 50 OFFSET ?",
|
||||
tenant, user, kind, flowId, flowId, Math.max(0L, (long) page - 1) * 50);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+48
-32
@@ -3,6 +3,8 @@ package com.pig4cloud.pigx.flow.service;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.pig4cloud.pigx.admin.api.dto.MessageNoticeDTO;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteMessageService;
|
||||
import com.pig4cloud.pigx.common.data.tenant.TenantBroker;
|
||||
import com.pig4cloud.pigx.flow.dto.Node;
|
||||
import com.pig4cloud.pigx.flow.constant.NodeStatusEnum;
|
||||
@@ -36,6 +38,8 @@ public class FlowReliabilityService {
|
||||
|
||||
private final TaskService taskService;
|
||||
|
||||
private final RemoteMessageService messageService;
|
||||
|
||||
/**
|
||||
* 在引擎事务内记录任务事件;持久化失败必须回滚引擎操作。
|
||||
* @param task 引擎当前任务
|
||||
@@ -108,10 +112,10 @@ public class FlowReliabilityService {
|
||||
private void notice(long tenant, String user, String event, String kind, String title, String task,
|
||||
String instance) {
|
||||
jdbc.update(
|
||||
"INSERT INTO flow_notice(id,tenant_id,user_id,event_key,kind,title,task_id,instance_id,created_at,sent_at,status) VALUES(?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),?) ON DUPLICATE KEY UPDATE event_key=event_key",
|
||||
"INSERT INTO flow_notice(id,tenant_id,user_id,event_key,kind,title,task_id,instance_id,created_at,scheduled_at,status) VALUES(?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),?) ON DUPLICATE KEY UPDATE event_key=event_key",
|
||||
UUID.randomUUID().toString(), tenant, user, event, kind,
|
||||
StrUtil.maxLength(StrUtil.blankToDefault(title, "流程任务"), 200), task, instance,
|
||||
FlowNoticeStatusEnum.SENT.getCode());
|
||||
FlowNoticeStatusEnum.READY.getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,30 +128,33 @@ public class FlowReliabilityService {
|
||||
*/
|
||||
@Transactional
|
||||
public int urge(long tenant, long user, String instance) {
|
||||
requireStarter(tenant, user, instance, true);
|
||||
String flowName = requireStarter(tenant, user, instance, true);
|
||||
var tasks = taskService.createTaskQuery()
|
||||
.processInstanceId(instance)
|
||||
.taskTenantId(String.valueOf(tenant))
|
||||
.active()
|
||||
.list();
|
||||
if (tasks.isEmpty())
|
||||
throw new IllegalStateException("流程当前没有可催办任务");
|
||||
throw new IllegalStateException("当前没有待处理的审批,暂时无法催办");
|
||||
var recent = jdbc.queryForList(
|
||||
"SELECT id FROM flow_notice WHERE tenant_id=? AND instance_id=? AND kind='URGE' AND created_at>?",
|
||||
tenant, instance, Timestamp.from(Instant.now().minusSeconds(600)));
|
||||
if (!recent.isEmpty())
|
||||
throw new IllegalStateException("已发送催办,请十分钟后再试");
|
||||
throw new IllegalStateException("已提交过催办,两次催办需间隔十分钟");
|
||||
int count = 0;
|
||||
String event = UUID.randomUUID().toString();
|
||||
for (var task : tasks) {
|
||||
if (StrUtil.isBlank(task.getAssignee()))
|
||||
continue;
|
||||
notice(tenant, task.getAssignee(), "URGE:" + event + StrUtil.COLON + task.getId(), "URGE", task.getName(),
|
||||
// 建模时会在节点名称末尾追加五位随机数,通知展示时移除内部后缀。
|
||||
String nodeName = StrUtil.blankToDefault(task.getName(), "审批").replaceFirst("\\[\\d{5}\\]$", StrUtil.EMPTY);
|
||||
notice(tenant, task.getAssignee(), "URGE:" + event + StrUtil.COLON + task.getId(), "URGE",
|
||||
flowName + " · " + nodeName,
|
||||
task.getId(), instance);
|
||||
count++;
|
||||
}
|
||||
if (count == 0)
|
||||
throw new IllegalStateException("流程当前没有已分配的待办任务");
|
||||
throw new IllegalStateException("当前待办尚未分配处理人,暂时无法催办");
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -175,35 +182,43 @@ public class FlowReliabilityService {
|
||||
return;
|
||||
}
|
||||
jdbc.update(
|
||||
"UPDATE flow_notice SET user_id=?,title=?,status=?,sent_at=CURRENT_TIMESTAMP(3),created_at=CURRENT_TIMESTAMP(3) WHERE id=? AND tenant_id=? AND status=?",
|
||||
"UPDATE flow_notice SET user_id=?,title=?,status=?,scheduled_at=CURRENT_TIMESTAMP(3),created_at=CURRENT_TIMESTAMP(3) WHERE id=? AND tenant_id=? AND status=?",
|
||||
task.getAssignee(), StrUtil.maxLength(StrUtil.blankToDefault(task.getName(), "流程任务"), 200),
|
||||
FlowNoticeStatusEnum.SENT.getCode(), watches.get(0).get("id"), tenant,
|
||||
FlowNoticeStatusEnum.READY.getCode(), watches.get(0).get("id"), tenant,
|
||||
FlowNoticeStatusEnum.PENDING.getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页读取个人通知。
|
||||
* @param tenant 当前租户
|
||||
* @param user 当前用户
|
||||
* @param page 从 1 开始的页码
|
||||
* @return 最近通知,每页最多 50 条
|
||||
* 投递已提交的流程事件到统一站内信;行锁防止扫描器并发发送同一事件。
|
||||
* @param id 流程通知事件 ID
|
||||
*/
|
||||
public List<Map<String, Object>> notices(long tenant, long user, int page) {
|
||||
return jdbc.queryForList(
|
||||
"SELECT * FROM flow_notice WHERE tenant_id=? AND user_id=? AND status=? ORDER BY created_at DESC LIMIT 50 OFFSET ?",
|
||||
tenant, String.valueOf(user), FlowNoticeStatusEnum.SENT.getCode(), Math.max(0L, (long) page - 1) * 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记个人通知已读,其他用户记录不受影响。
|
||||
* @param tenant 当前租户
|
||||
* @param user 当前用户
|
||||
* @param id 通知 ID
|
||||
*/
|
||||
public void read(long tenant, long user, String id) {
|
||||
jdbc.update(
|
||||
"UPDATE flow_notice SET read_at=COALESCE(read_at,CURRENT_TIMESTAMP(3)) WHERE id=? AND tenant_id=? AND user_id=? AND status=?",
|
||||
id, tenant, String.valueOf(user), FlowNoticeStatusEnum.SENT.getCode());
|
||||
@Transactional
|
||||
public void deliverNotice(String id) {
|
||||
var rows = jdbc.queryForList("SELECT * FROM flow_notice WHERE id=? AND status=? FOR UPDATE", id,
|
||||
FlowNoticeStatusEnum.READY.getCode());
|
||||
if (rows.isEmpty())
|
||||
return;
|
||||
var row = rows.get(0);
|
||||
long tenant = ((Number) row.get("tenant_id")).longValue();
|
||||
MessageNoticeDTO notice = new MessageNoticeDTO();
|
||||
notice.setCategory("1");
|
||||
notice.setAllFlag("0");
|
||||
notice.setUserIds(List.of(Long.parseLong(String.valueOf(row.get("user_id")))));
|
||||
String label = switch (String.valueOf(row.get("kind"))) {
|
||||
case "URGE" -> "催办";
|
||||
case "OVERDUE" -> "超时提醒";
|
||||
default -> "待办";
|
||||
};
|
||||
notice.setTitle(label + StrUtil.COLON + row.get("title"));
|
||||
notice.setContent("您有一条" + label + "消息,请打开对应审批任务处理。");
|
||||
notice.setBusinessType("FLOW_TASK");
|
||||
notice.setBusinessId(String.valueOf(row.get("task_id")));
|
||||
notice.setRequestKey("flow:" + id);
|
||||
var result = TenantBroker.applyAs(tenant, t -> messageService.sendNotice(notice));
|
||||
if (result == null || result.getCode() != 0)
|
||||
throw new IllegalStateException("流程站内信发送失败");
|
||||
jdbc.update("UPDATE flow_notice SET status=?,sent_at=CURRENT_TIMESTAMP(3) WHERE id=? AND tenant_id=?",
|
||||
FlowNoticeStatusEnum.SENT.getCode(), id, tenant);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,15 +261,16 @@ public class FlowReliabilityService {
|
||||
FlowNoticeStatusEnum.CANCELLED.getCode(), tenant, task, FlowNoticeStatusEnum.PENDING.getCode());
|
||||
}
|
||||
|
||||
private void requireStarter(long tenant, long user, String instance, boolean running) {
|
||||
private String requireStarter(long tenant, long user, String instance, boolean running) {
|
||||
var rows = jdbc.queryForList(
|
||||
"SELECT status FROM process_instance_record WHERE process_instance_id=? AND user_id=? AND tenant_id=? AND del_flag='0' LIMIT 1"
|
||||
"SELECT status,name FROM process_instance_record WHERE process_instance_id=? AND user_id=? AND tenant_id=? AND del_flag='0' LIMIT 1"
|
||||
+ (running ? " FOR UPDATE" : StrUtil.EMPTY),
|
||||
instance, user, tenant);
|
||||
if (rows.isEmpty())
|
||||
throw new IllegalStateException("流程不存在或无权访问");
|
||||
if (running && !Objects.equals(Convert.toInt(rows.get(0).get("status")), NodeStatusEnum.JXZ.getCode()))
|
||||
throw new IllegalStateException("流程已结束,不能催办");
|
||||
return StrUtil.blankToDefault(Convert.toStr(rows.get(0).get("name")), "流程申请");
|
||||
}
|
||||
|
||||
}
|
||||
+24
-21
@@ -171,6 +171,9 @@ public class TaskServiceImpl implements ITaskService {
|
||||
.taskTenantId(TenantContextHolder.getTenantId().toString())
|
||||
.taskAssignee(String.valueOf(userId))
|
||||
.singleResult());
|
||||
if (!view && task.isEmpty()) {
|
||||
return R.failed("任务已结束或已转交,无法办理");
|
||||
}
|
||||
TaskResultDto taskResultDto = new TaskResultDto();
|
||||
|
||||
if (task.isPresent()) {
|
||||
@@ -347,7 +350,7 @@ public class TaskServiceImpl implements ITaskService {
|
||||
ProcessNodeRecordAssignUser currentAssignment = findActiveAssignment(task.getId(), userId);
|
||||
ProcessNodeRecord nodeRecord = lockActiveNodeRecord(task, currentAssignment, TenantContextHolder.getTenantId());
|
||||
if (nodeRecord == null) {
|
||||
return R.failed("当前审批节点已结束,无法操作");
|
||||
return R.failed("当前审批环节已结束,无法继续操作");
|
||||
}
|
||||
|
||||
Map<String, Object> taskLocalParamMap = taskParamDto.getTaskLocalParamMap();
|
||||
@@ -449,7 +452,7 @@ public class TaskServiceImpl implements ITaskService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R addSign(AddSignTaskDto addSignTaskDto) {
|
||||
if (addSignTaskDto == null || StrUtil.isBlank(addSignTaskDto.getTaskId())) {
|
||||
return R.failed("任务ID不能为空");
|
||||
return R.failed("审批信息不完整,请重新打开");
|
||||
}
|
||||
List<Long> requestedUserIds = addSignTaskDto.getTargetUserIdList();
|
||||
if (CollUtil.isEmpty(requestedUserIds)) {
|
||||
@@ -481,7 +484,7 @@ public class TaskServiceImpl implements ITaskService {
|
||||
ProcessNodeRecordAssignUser sourceAssignment = findActiveAssignment(sourceTask.getId(), userId);
|
||||
ProcessNodeRecord nodeRecord = lockActiveNodeRecord(sourceTask, sourceAssignment, tenantId);
|
||||
if (nodeRecord == null) {
|
||||
return R.failed("当前审批节点已结束,无法加签");
|
||||
return R.failed("当前审批环节已结束,无法加签");
|
||||
}
|
||||
|
||||
String flowId = NodeUtil.getFlowId(sourceTask.getProcessDefinitionId());
|
||||
@@ -489,17 +492,17 @@ public class TaskServiceImpl implements ITaskService {
|
||||
.getPublishedNode(sourceTask.getProcessDefinitionId(), sourceTask.getTaskDefinitionKey())
|
||||
.getData();
|
||||
if (!isParallelCountersign(node)) {
|
||||
return R.failed("仅并行会签节点支持加签");
|
||||
return R.failed("只有多人同时审批的会签环节支持加签");
|
||||
}
|
||||
if (!Boolean.TRUE.equals(node.getAllowAddSign())) {
|
||||
return R.failed("当前节点未开启加签");
|
||||
return R.failed("当前审批环节未开启加签功能");
|
||||
}
|
||||
if (isProcessStarter(sourceTask.getProcessInstanceId(), userId)) {
|
||||
return R.failed("流程发起人不能发起加签");
|
||||
}
|
||||
|
||||
if (!isOriginalApprover(sourceTask, sourceAssignment, userId)) {
|
||||
return R.failed("只有未完成审批的原始审批人可以加签");
|
||||
return R.failed("只有最初指定的审批人,且尚未完成审批时,才能加签");
|
||||
}
|
||||
|
||||
List<Long> participantUserIds = listParticipantUserIds(nodeRecord.getId());
|
||||
@@ -577,7 +580,7 @@ public class TaskServiceImpl implements ITaskService {
|
||||
}
|
||||
catch (FlowableException exception) {
|
||||
log.warn("动态加签时多实例节点已不可用: taskId={}", sourceTask.getId(), exception);
|
||||
return rollbackFailed("当前审批节点已结束,无法加签");
|
||||
return rollbackFailed("当前审批环节已结束,无法加签");
|
||||
}
|
||||
|
||||
LinkedHashSet<Long> participantUserIdsAfter = new LinkedHashSet<>(participantUserIds);
|
||||
@@ -600,11 +603,11 @@ public class TaskServiceImpl implements ITaskService {
|
||||
@Override
|
||||
public R delegateTask(TaskParamDto taskParamDto) {
|
||||
if (taskParamDto == null || StrUtil.isBlank(taskParamDto.getTaskId())) {
|
||||
return R.failed("任务ID不能为空");
|
||||
return R.failed("审批信息不完整,请重新打开");
|
||||
}
|
||||
if (taskParamDto.getTargetUserIdList() == null || taskParamDto.getTargetUserIdList().size() != 1
|
||||
|| taskParamDto.getTargetUserIdList().get(0) == null) {
|
||||
return R.failed("委托仅支持选择一名用户");
|
||||
return R.failed("一次只能委托给一人");
|
||||
}
|
||||
long userId = SecurityUtils.getUser().getId();
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
@@ -620,10 +623,10 @@ public class TaskServiceImpl implements ITaskService {
|
||||
ProcessNodeRecordAssignUser sourceAssignment = findActiveAssignment(task.getId(), userId);
|
||||
ProcessNodeRecord nodeRecord = lockActiveNodeRecord(task, sourceAssignment, tenantId);
|
||||
if (nodeRecord == null) {
|
||||
return R.failed("当前审批节点已结束,无法委托");
|
||||
return R.failed("当前审批环节已结束,无法委托");
|
||||
}
|
||||
if (listParticipantUserIds(nodeRecord.getId()).contains(targetUserId)) {
|
||||
return R.failed("委托目标已参与当前会签");
|
||||
return R.failed("所选人员已参与本轮会签,请选择其他人");
|
||||
}
|
||||
if (queryValidTenantUsers(taskParamDto.getTargetUserIdList(), tenantId).size() != 1) {
|
||||
return R.failed("委托用户不存在、不属于当前租户或已被锁定");
|
||||
@@ -678,11 +681,11 @@ public class TaskServiceImpl implements ITaskService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R setAssignee(TaskParamDto taskParamDto) {
|
||||
if (taskParamDto == null || StrUtil.isBlank(taskParamDto.getTaskId())) {
|
||||
return R.failed("任务ID不能为空");
|
||||
return R.failed("审批信息不完整,请重新打开");
|
||||
}
|
||||
if (taskParamDto.getTargetUserIdList() == null || taskParamDto.getTargetUserIdList().size() != 1
|
||||
|| taskParamDto.getTargetUserIdList().get(0) == null) {
|
||||
return R.failed("转办仅支持选择一名用户");
|
||||
return R.failed("一次只能转办给一人");
|
||||
}
|
||||
|
||||
long userId = SecurityUtils.getUser().getId();
|
||||
@@ -701,17 +704,17 @@ public class TaskServiceImpl implements ITaskService {
|
||||
ProcessNodeRecordAssignUser sourceAssignment = findActiveAssignment(task.getId(), userId);
|
||||
ProcessNodeRecord nodeRecord = lockActiveNodeRecord(task, sourceAssignment, tenantId);
|
||||
if (nodeRecord == null) {
|
||||
return R.failed("当前审批节点已结束,无法转办");
|
||||
return R.failed("当前审批环节已结束,无法转办");
|
||||
}
|
||||
if (listParticipantUserIds(nodeRecord.getId()).contains(targetUserId)) {
|
||||
return R.failed("转办目标已参与当前会签");
|
||||
return R.failed("所选人员已参与本轮会签,请选择其他人");
|
||||
}
|
||||
if (queryValidTenantUsers(List.of(targetUserId), tenantId).size() != 1) {
|
||||
return R.failed("转办用户不存在或不属于当前租户");
|
||||
}
|
||||
|
||||
if (sourceAssignment == null) {
|
||||
return R.failed("未找到当前任务的执行人记录");
|
||||
return R.failed("未找到当前审批的处理人记录,请联系管理员");
|
||||
}
|
||||
|
||||
Map<String, Object> taskLocalParamMap = taskParamDto.getTaskLocalParamMap();
|
||||
@@ -778,7 +781,7 @@ public class TaskServiceImpl implements ITaskService {
|
||||
public R withdrawProcessInstance(TaskParamDto taskParamDto) {
|
||||
String processInstanceId = taskParamDto.getProcessInstanceId();
|
||||
if (StrUtil.isBlank(processInstanceId)) {
|
||||
return R.failed("流程实例ID不能为空");
|
||||
return R.failed("申请信息不完整,请重新打开");
|
||||
}
|
||||
|
||||
long userId = SecurityUtils.getUser().getId();
|
||||
@@ -798,7 +801,7 @@ public class TaskServiceImpl implements ITaskService {
|
||||
Object rejectToStarter = runtimeService.getVariable(processInstanceId,
|
||||
ProcessInstanceConstant.REJECT_TO_STARTER_VAR);
|
||||
if (Boolean.TRUE.equals(rejectToStarter)) {
|
||||
return R.failed("驳回待提交状态不可撤回");
|
||||
return R.failed("申请已被驳回,正在等待重新提交,不能撤回");
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
@@ -813,7 +816,7 @@ public class TaskServiceImpl implements ITaskService {
|
||||
.ne(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.JXZ.getCode()))
|
||||
.exists();
|
||||
if (hasProcessedApproval) {
|
||||
return R.failed("首个审批节点已处理,无法撤回");
|
||||
return R.failed("第一步审批已处理,无法撤回");
|
||||
}
|
||||
|
||||
return terminateProcessInstance(processInstanceId, ProcessInstanceConstant.FINISH_REASON_WITHDRAW);
|
||||
@@ -861,7 +864,7 @@ public class TaskServiceImpl implements ITaskService {
|
||||
|
||||
// 2. 验证是 root 任务(发起人节点)
|
||||
if (!StrUtil.equals(task.getTaskDefinitionKey(), "root")) {
|
||||
return R.failed("非重新提交任务,无法操作");
|
||||
return R.failed("当前审批不支持重新提交");
|
||||
}
|
||||
|
||||
String processInstanceId = task.getProcessInstanceId();
|
||||
@@ -1046,7 +1049,7 @@ public class TaskServiceImpl implements ITaskService {
|
||||
|
||||
private R terminateProcessInstance(String reqProcessInstanceId, String finishReason) {
|
||||
if (StrUtil.isBlank(reqProcessInstanceId)) {
|
||||
return R.failed("流程实例ID不能为空");
|
||||
return R.failed("申请信息不完整,请重新打开");
|
||||
}
|
||||
|
||||
List<String> allStopProcessInstanceIdList = getAllStopProcessInstanceIdList(reqProcessInstanceId);
|
||||
|
||||
+58
-14
@@ -1,6 +1,9 @@
|
||||
package com.pig4cloud.pigx.flow.service;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.admin.api.dto.MessageNoticeDTO;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteMessageService;
|
||||
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.pig4cloud.pigx.flow.dto.Node;
|
||||
import com.pig4cloud.pigx.flow.constant.FlowNoticeStatusEnum;
|
||||
@@ -43,6 +46,10 @@ class FlowWorkspaceTests {
|
||||
|
||||
private FlowDraftService drafts;
|
||||
|
||||
private RemoteMessageService messages;
|
||||
|
||||
private final List<MessageNoticeDTO> sentNotices = new ArrayList<>();
|
||||
|
||||
private HttpServer server;
|
||||
|
||||
private final List<String> eventIds = new ArrayList<>();
|
||||
@@ -61,7 +68,7 @@ class FlowWorkspaceTests {
|
||||
if (!sql.isBlank())
|
||||
jdbc.execute(sql);
|
||||
jdbc.execute(
|
||||
"CREATE TABLE process_instance_record(id bigint primary key,process_instance_id varchar(64),tenant_id bigint,user_id bigint,status int,del_flag char(1))");
|
||||
"CREATE TABLE process_instance_record(id bigint primary key,process_instance_id varchar(64),tenant_id bigint,user_id bigint,status int,del_flag char(1),name varchar(200))");
|
||||
var manager = new DataSourceTransactionManager(source);
|
||||
transaction = new TransactionTemplate(manager);
|
||||
var configuration = new SpringProcessEngineConfiguration();
|
||||
@@ -92,7 +99,13 @@ class FlowWorkspaceTests {
|
||||
node.setEventConfig("http://127.0.0.1:" + server.getAddress().getPort() + "/callback");
|
||||
var nodes = mock(IProcessNodeDataService.class);
|
||||
when(nodes.getPublishedNode(anyString(), anyString())).thenReturn(R.ok(node));
|
||||
var target = new FlowReliabilityService(jdbc, nodes, engine.getTaskService());
|
||||
messages = mock(RemoteMessageService.class);
|
||||
when(messages.sendNotice(any())).thenAnswer(invocation -> {
|
||||
assertThat(TenantContextHolder.getTenantId()).isEqualTo(1L);
|
||||
sentNotices.add(invocation.getArgument(0));
|
||||
return R.ok(123L);
|
||||
});
|
||||
var target = new FlowReliabilityService(jdbc, nodes, engine.getTaskService(), messages);
|
||||
var proxy = new ProxyFactory(target);
|
||||
proxy.addAdvice(new TransactionInterceptor(manager, new AnnotationTransactionAttributeSource()));
|
||||
reliability = (FlowReliabilityService) proxy.getProxy();
|
||||
@@ -106,7 +119,7 @@ class FlowWorkspaceTests {
|
||||
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:flowable="http://flowable.org/bpmn" targetNamespace="test">
|
||||
<process id="approval" isExecutable="true">
|
||||
<startEvent id="start"/><sequenceFlow id="s1" sourceRef="start" targetRef="approve"/>
|
||||
<userTask id="approve" name="审批" flowable:assignee="1001"/><sequenceFlow id="s2" sourceRef="approve" targetRef="end"/><endEvent id="end"/>
|
||||
<userTask id="approve" name="审批[51421]" flowable:assignee="1001"/><sequenceFlow id="s2" sourceRef="approve" targetRef="end"/><endEvent id="end"/>
|
||||
</process>
|
||||
</definitions>
|
||||
""")
|
||||
@@ -137,25 +150,34 @@ class FlowWorkspaceTests {
|
||||
assertThat(logMapper.writeValueAsString(new Object[] { input })).doesNotContain("private-title", "secret",
|
||||
"payload", "title");
|
||||
assertThat(FlowNoticeStatusEnum.values()).extracting(FlowNoticeStatusEnum::getCode)
|
||||
.containsExactly("PENDING", "SENT", "CANCELLED", "DISABLED");
|
||||
.containsExactly("PENDING", "READY", "SENT", "CANCELLED", "DISABLED");
|
||||
var design = drafts.save(1, 7,
|
||||
new FlowDraftService.Draft(null, "DESIGN", "approval", "设计", "{\"step1\":{}}", 0));
|
||||
var application = drafts.save(1, 7,
|
||||
new FlowDraftService.Draft(null, "APPLICATION", "approval", "申请", "{\"formData\":{\"amount\":8}}", 0));
|
||||
assertThat(new FlowDraftService(jdbc).get(1, 7, design.get("id").toString()).get("payload"))
|
||||
.isEqualTo("{\"step1\":{}}");
|
||||
assertThat(drafts.list(1, 7, "APPLICATION", 1)).hasSize(1);
|
||||
assertThat(drafts.list(1, 7, "APPLICATION", 1, null)).hasSize(1);
|
||||
assertThat(drafts.list(1, 7, "APPLICATION", 1, "approval")).hasSize(1);
|
||||
assertThat(drafts.list(1, 7, "APPLICATION", 1, "other-flow")).isEmpty();
|
||||
assertThat(drafts.list(1, 8, "APPLICATION", 1, "approval")).isEmpty();
|
||||
assertThat(drafts.get(1, 7, application.get("id").toString()).get("payload"))
|
||||
.isEqualTo("{\"formData\":{\"amount\":8}}");
|
||||
var instance = engine.getRuntimeService()
|
||||
.startProcessInstanceByKeyAndTenantId("approval", java.util.Map.of(), "1");
|
||||
jdbc.update("INSERT INTO process_instance_record VALUES(1,?,1,7,1,'0')", instance.getId());
|
||||
jdbc.update("INSERT INTO process_instance_record VALUES(1,?,1,7,1,'0','差旅申请')", instance.getId());
|
||||
var task = engine.getTaskService().createTaskQuery().singleResult();
|
||||
assertThat(reliability.notices(1, 1001, 1)).hasSize(1);
|
||||
String notice = reliability.notices(1, 1001, 1).get(0).get("id").toString();
|
||||
reliability.read(1, 1001, notice);
|
||||
assertThat(reliability.notices(1, 1001, 1).get(0).get("read_at")).isNotNull();
|
||||
assertThat(reliability.notices(1, 0, 1)).isEmpty();
|
||||
verifyNoInteractions(messages);
|
||||
String notice = jdbc.queryForObject("SELECT id FROM flow_notice WHERE status='READY'", String.class);
|
||||
reliability.deliverNotice(notice);
|
||||
reliability.deliverNotice(notice);
|
||||
assertThat(sentNotices).hasSize(1);
|
||||
assertThat(sentNotices.get(0).getUserIds()).containsExactly(1001L);
|
||||
assertThat(sentNotices.get(0).getBusinessType()).isEqualTo("FLOW_TASK");
|
||||
assertThat(sentNotices.get(0).getBusinessId()).isEqualTo(task.getId());
|
||||
assertThat(sentNotices.get(0).getRequestKey()).isEqualTo("flow:" + notice);
|
||||
assertThat(sentNotices.get(0).getCategory()).isEqualTo("1");
|
||||
assertThat(sentNotices.get(0).getAllFlag()).isEqualTo("0");
|
||||
assertThat(jdbc.queryForObject("SELECT count(*) FROM flow_notice WHERE status='PENDING'", Integer.class))
|
||||
.isEqualTo(1);
|
||||
engine.getRuntimeService().suspendProcessInstanceById(instance.getId());
|
||||
@@ -177,12 +199,19 @@ class FlowWorkspaceTests {
|
||||
var scheduler = new FlowDurableConfiguration(jdbc, provider, mock(ObjectProvider.class));
|
||||
scheduler.scanReminders();
|
||||
scheduler.scanReminders();
|
||||
assertThat(reliability.notices(1, 2002, 1)).hasSize(2);
|
||||
assertThat(jdbc.queryForObject("SELECT count(*) FROM flow_notice WHERE tenant_id=1 AND user_id='2002'", Integer.class)).isEqualTo(2);
|
||||
engine.getTaskService().setAssignee(task.getId(), "1001");
|
||||
assertThat(reliability.notices(1, 1001, 1)).hasSize(2);
|
||||
assertThat(jdbc.queryForObject("SELECT count(*) FROM flow_notice WHERE tenant_id=1 AND user_id='1001'", Integer.class)).isEqualTo(2);
|
||||
engine.getTaskService().setAssignee(task.getId(), "2002");
|
||||
assertThat(reliability.urge(1, 7, instance.getId())).isEqualTo(1);
|
||||
assertThatThrownBy(() -> reliability.urge(1, 7, instance.getId())).hasMessageContaining("十分钟");
|
||||
scheduler.scanNotices();
|
||||
assertThat(sentNotices).extracting(MessageNoticeDTO::getTitle)
|
||||
.anyMatch(title -> title.startsWith("待办:"))
|
||||
.anyMatch(title -> title.equals("催办:差旅申请 · 审批"))
|
||||
.anyMatch(title -> title.startsWith("超时提醒:"));
|
||||
assertThat(sentNotices.stream().filter(message -> message.getTitle().startsWith("超时提醒:"))
|
||||
.map(MessageNoticeDTO::getUserIds).toList()).containsExactly(List.of(2002L));
|
||||
engine.getTaskService().complete(task.getId());
|
||||
assertThat(jdbc.queryForObject("SELECT count(*) FROM flow_notice WHERE status='PENDING'", Integer.class))
|
||||
.isZero();
|
||||
@@ -237,11 +266,26 @@ class FlowWorkspaceTests {
|
||||
})).hasMessage("rollback");
|
||||
assertThat(jdbc.queryForObject("SELECT count(*) FROM flow_notice", Integer.class)).isZero();
|
||||
assertThat(engine.getTaskService().createTaskQuery().count()).isZero();
|
||||
verifyNoInteractions(messages);
|
||||
var instance = engine.getRuntimeService()
|
||||
.startProcessInstanceByKeyAndTenantId("approval", java.util.Map.of(), "1");
|
||||
jdbc.update("INSERT INTO process_instance_record VALUES(1,?,1,7,1,'0')", instance.getId());
|
||||
jdbc.update("INSERT INTO process_instance_record VALUES(1,?,1,7,1,'0','差旅申请')", instance.getId());
|
||||
assertThatThrownBy(() -> reliability.urge(2, 7, instance.getId())).hasMessageContaining("无权");
|
||||
assertThatThrownBy(() -> reliability.callbacks(1, 8, instance.getId())).hasMessageContaining("无权");
|
||||
String notice = jdbc.queryForObject("SELECT id FROM flow_notice WHERE status='READY'", String.class);
|
||||
doReturn(R.failed("发送暂不可用"), R.ok(123L)).when(messages).sendNotice(any());
|
||||
ObjectProvider<FlowReliabilityService> provider = mock(ObjectProvider.class);
|
||||
when(provider.getObject()).thenReturn(reliability);
|
||||
var scheduler = new FlowDurableConfiguration(jdbc, provider, mock(ObjectProvider.class));
|
||||
scheduler.scanNotices();
|
||||
assertThat(jdbc.queryForObject("SELECT status FROM flow_notice WHERE id=?", String.class, notice)).isEqualTo("READY");
|
||||
assertThat(jdbc.queryForObject("SELECT scheduled_at>CURRENT_TIMESTAMP(3) FROM flow_notice WHERE id=?", Boolean.class, notice)).isTrue();
|
||||
jdbc.update("UPDATE flow_notice SET scheduled_at=CURRENT_TIMESTAMP(3) WHERE id=?", notice);
|
||||
scheduler.scanNotices();
|
||||
assertThat(jdbc.queryForObject("SELECT status FROM flow_notice WHERE id=?", String.class, notice)).isEqualTo("SENT");
|
||||
var captor = org.mockito.ArgumentCaptor.forClass(MessageNoticeDTO.class);
|
||||
verify(messages, times(2)).sendNotice(captor.capture());
|
||||
assertThat(captor.getAllValues()).extracting(MessageNoticeDTO::getRequestKey).containsExactly("flow:" + notice, "flow:" + notice);
|
||||
String task = engine.getTaskService().createTaskQuery().singleResult().getId();
|
||||
jdbc.execute("DROP TABLE flow_callback");
|
||||
assertThatThrownBy(() -> engine.getTaskService().complete(task)).isInstanceOf(RuntimeException.class);
|
||||
|
||||
+9
@@ -189,6 +189,15 @@ class TaskServiceImplTests {
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryTaskRejectsUnavailableTaskInHandlingMode() {
|
||||
TaskQuery query = mock(TaskQuery.class, org.mockito.Answers.RETURNS_SELF);
|
||||
when(taskService.createTaskQuery()).thenReturn(query);
|
||||
R result = taskServiceImpl.queryTask("finished-or-transferred", false);
|
||||
assertThat(result.getCode()).isEqualTo(1);
|
||||
assertThat(result.getMsg()).isEqualTo("任务已结束或已转交,无法办理");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stopProcessInstancePropagatesTerminateReasonBeforeMovingToEnd() {
|
||||
ChangeActivityStateBuilder stateChangeBuilder = prepareTerminationFlow(null);
|
||||
|
||||
+9
@@ -50,4 +50,13 @@ public class MessageNoticeDTO {
|
||||
*/
|
||||
private Integer sort = 0;
|
||||
|
||||
/** 业务类型;FLOW_TASK 表示流程审批任务,空值按普通消息打开。 */
|
||||
private String businessType;
|
||||
|
||||
/** 业务记录 ID;FLOW_TASK 时为任务 ID。 */
|
||||
private String businessId;
|
||||
|
||||
/** 调用方事件标识,同一租户内用于重试去重;普通消息可不传。 */
|
||||
private String requestKey;
|
||||
|
||||
}
|
||||
+9
@@ -107,4 +107,13 @@ public class SysMessageEntity extends Model<SysMessageEntity> {
|
||||
@Schema(description = "租户")
|
||||
private Long tenantId;
|
||||
|
||||
/** 业务类型;FLOW_TASK 表示流程审批任务,空值按普通消息打开。 */
|
||||
private String businessType;
|
||||
|
||||
/** 业务记录 ID;FLOW_TASK 时为任务 ID。 */
|
||||
private String businessId;
|
||||
|
||||
/** 调用方事件标识,同一租户内用于重试去重;普通消息可不传。 */
|
||||
private String requestKey;
|
||||
|
||||
}
|
||||
+3
-1
@@ -25,6 +25,7 @@ import com.pig4cloud.pigx.admin.api.dto.MessageNoticeDTO;
|
||||
import com.pig4cloud.pigx.admin.api.dto.MessageSmsDTO;
|
||||
import com.pig4cloud.pigx.common.core.constant.ServiceNameConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.common.feign.annotation.NoToken;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -71,7 +72,8 @@ public interface RemoteMessageService {
|
||||
* @param noticeDTO 站内消息 DTO
|
||||
* @return {@link R }
|
||||
*/
|
||||
@PostMapping("/sysMessage/send/notice")
|
||||
@NoToken
|
||||
@PostMapping("/sysMessage/internal/notice")
|
||||
R sendNotice(@RequestBody MessageNoticeDTO noticeDTO);
|
||||
|
||||
}
|
||||
+12
@@ -267,4 +267,16 @@ public class SysMessageController {
|
||||
return sysMessageService.sendNotice(noticeDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务服务内部发送站内信,复用统一消息存储和接收人关系。
|
||||
* @param noticeDTO 消息及业务跳转信息
|
||||
* @return 消息 ID
|
||||
*/
|
||||
@Inner
|
||||
@PostMapping("/internal/notice")
|
||||
@Operation(summary = "内部发送站内信")
|
||||
public R sendInternalNotice(@Valid @RequestBody MessageNoticeDTO noticeDTO) {
|
||||
return sysMessageService.sendNotice(noticeDTO);
|
||||
}
|
||||
|
||||
}
|
||||
+13
@@ -608,6 +608,16 @@ public class SysMessageServiceImpl extends ServiceImpl<SysMessageMapper, SysMess
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R sendNotice(MessageNoticeDTO noticeDTO) {
|
||||
if (StrUtil.isNotBlank(noticeDTO.getRequestKey())) {
|
||||
SysMessageEntity existing = baseMapper.selectOne(Wrappers.<SysMessageEntity>lambdaQuery()
|
||||
.eq(SysMessageEntity::getRequestKey, noticeDTO.getRequestKey()));
|
||||
if (existing != null) {
|
||||
return R.ok(existing.getId());
|
||||
}
|
||||
}
|
||||
if (!YesNoEnum.YES.getCode().equals(noticeDTO.getAllFlag()) && CollUtil.isEmpty(noticeDTO.getUserIds())) {
|
||||
return R.failed("指定用户站内信必须提供接收人");
|
||||
}
|
||||
// 构建消息VO
|
||||
SysMessageVO messageVO = new SysMessageVO();
|
||||
messageVO.setCategory(noticeDTO.getCategory());
|
||||
@@ -615,6 +625,9 @@ public class SysMessageServiceImpl extends ServiceImpl<SysMessageMapper, SysMess
|
||||
messageVO.setContent(noticeDTO.getContent());
|
||||
messageVO.setAllFlag(noticeDTO.getAllFlag());
|
||||
messageVO.setSort(noticeDTO.getSort());
|
||||
messageVO.setBusinessType(noticeDTO.getBusinessType());
|
||||
messageVO.setBusinessId(noticeDTO.getBusinessId());
|
||||
messageVO.setRequestKey(StrUtil.blankToDefault(noticeDTO.getRequestKey(), null));
|
||||
|
||||
// 设置接收用户列表
|
||||
if (CollUtil.isNotEmpty(noticeDTO.getUserIds())) {
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.pig4cloud.pigx.admin.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.pig4cloud.pigx.admin.api.dto.MessageNoticeDTO;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysMessageEntity;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysMessageRelationEntity;
|
||||
import com.pig4cloud.pigx.admin.mapper.SysMessageMapper;
|
||||
import com.pig4cloud.pigx.admin.mapper.SysMessageRelationMapper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class SysMessageNoticeTests {
|
||||
|
||||
@Test
|
||||
void flowNoticeUsesSharedMessageAndReceiverAndDeduplicatesRetries() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "notice"), SysMessageEntity.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "relation"), SysMessageRelationEntity.class);
|
||||
var mapper = mock(SysMessageMapper.class);
|
||||
var relations = mock(SysMessageRelationMapper.class);
|
||||
var service = new SysMessageServiceImpl(null, null, relations, null, null, null, null);
|
||||
ReflectionTestUtils.setField(service, "baseMapper", mapper);
|
||||
var saved = new AtomicReference<SysMessageEntity>();
|
||||
when(mapper.selectOne(any(LambdaQueryWrapper.class))).thenAnswer(invocation -> saved.get());
|
||||
when(mapper.insert(any(SysMessageEntity.class))).thenAnswer(invocation -> {
|
||||
SysMessageEntity entity = invocation.getArgument(0);
|
||||
entity.setId(123L);
|
||||
saved.set(entity);
|
||||
return 1;
|
||||
});
|
||||
var notice = new MessageNoticeDTO();
|
||||
notice.setCategory("1");
|
||||
notice.setTitle("待办:财务审批");
|
||||
notice.setContent("请打开审批任务处理");
|
||||
notice.setUserIds(List.of(1001L));
|
||||
notice.setBusinessType("FLOW_TASK");
|
||||
notice.setBusinessId("task-A");
|
||||
notice.setRequestKey("flow:event-A");
|
||||
assertThat(service.sendNotice(notice).getData()).isEqualTo(123L);
|
||||
assertThat(service.sendNotice(notice).getData()).isEqualTo(123L);
|
||||
verify(mapper, times(1)).insert(any(SysMessageEntity.class));
|
||||
assertThat(saved.get().getBusinessType()).isEqualTo("FLOW_TASK");
|
||||
assertThat(saved.get().getBusinessId()).isEqualTo("task-A");
|
||||
assertThat(saved.get().getRequestKey()).isEqualTo("flow:event-A");
|
||||
var recipient = ArgumentCaptor.forClass(SysMessageRelationEntity.class);
|
||||
verify(relations, times(1)).insert(recipient.capture());
|
||||
assertThat(recipient.getValue().getMsgId()).isEqualTo(123L);
|
||||
assertThat(recipient.getValue().getUserId()).isEqualTo(1001L);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user