- 多出口:FlwTask 增 edge_name 列(sql/20260916),FlwTaskRepository.claimAndFinish
同步落列;FlowTaskService.handle 出度>1 时 edgeName 必填;FlowEngine.continueAdvance
按 edge name 选路(单出口原路径逐字节兼容,未命中不推进);FlowAdvanceService/
FlowNodeAdvanceListener 透传;BranchScope 从 FlowEngine 提取
- DefValidator:多出口出口命名必填且同节点唯一;跨分支连线发布期明确拒绝
- 退回 POST /task/{id}/returnBack:默认退主管线最近已批节点,精确失效当前节点待办,
split 幂等 upsert 使退回重走并行段不撞 flw_branch uk
- 移交 POST /task/{id}/transfer:TRANSFERRED(5)+新建待办,AssigneeResolver.requireUserName
- 或签 ANY 补齐:首个同意失效同节点兄弟待办
- 已办口径/撤回守卫:status in (1,2,4,5);新事件 flow.returned/flow.transferred
- FlowDiagramService:EdgeVo.name/NodeVo.state/DiagramVo.layout,办件图状态计算
- 新 IT:多出口×3、ANY 或签、退回重走并行段、退回/移交/撤回
mvn test 84/84 绿
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
package com.yunhai.flowcenter.notify;
|
||||
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.openapi.core.PlatformService;
|
||||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||||
import com.yunhai.platform.openapi.msg.YschMsgOpenApi;
|
||||
import com.yunhai.platform.openapi.msg.model.SendMsgRequest;
|
||||
import com.yunhai.platform.openapi.msg.model.TodoCountRequest;
|
||||
import com.yunhai.platform.utils.http.IDHttp;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -11,30 +14,56 @@ import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* UserMsg 站内信客户端(概览 §七 契约,M1 手工三件套路线的<b>直达版</b>):
|
||||
* 经共享 IDHttp(yh-openapi 装配的出站自动签名 RestTemplate,X-App-Key/X-Timestamp/X-Signature
|
||||
* = SignUtil.signRequest 请求级 canonical,与 svc-hall FlowCenterClient 同一套通道)直调
|
||||
* UserMsg /oapi/msg/**。前提:UserMsg 侧 trusted-apps 白名单收本应用 appKey(本地占位 100019);
|
||||
* SDK(yh-usermsg-spring-boot-starter)落地后整体切换为 SDK 调用,契约不变。
|
||||
* 发送方不阻断主业务:失败只记日志(对齐 PartyLearn 容错语义),由调用方在 AFTER_COMMIT 监听器里容错。
|
||||
* UserMsg 站内信客户端(概览 §七 契约):主路径走 SDK 类型化门面 {@link YschMsgOpenApi}
|
||||
* (服务目录 key={@code usermsg}:yh.call.targets > yh.open.services > 枚举兜底
|
||||
* base-host + /usermsg)——签名/信封/错误映射由 SDK 统一承载,本应用零专属地址配置
|
||||
* (《子应用配置优化方案》§4.2)。目录未显式配置时回落 legacy
|
||||
* {@code flow-center.usermsg.base-url}(ad-hoc,迁移期兼容)。<br><br>
|
||||
* 发送方不阻断主业务:失败只记日志(对齐 PartyLearn 容错语义),由调用方在 AFTER_COMMIT
|
||||
* 监听器里容错。{@code flow-center.usermsg.enabled=false} 时全部方法静默返回(UserMsg 未部署
|
||||
* 的部署形态;生产当前即此态,避免打 localhost 的空转告警)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserMsgClient {
|
||||
|
||||
/** 服务目录命名目标(UserMsg 已入 PlatformService 枚举:key=usermsg、ctx=/usermsg) */
|
||||
private static final String DIRECTORY_KEY = PlatformService.USERMSG.key();
|
||||
|
||||
private final YschOpenCall openCall;
|
||||
private final IDHttp http;
|
||||
private final boolean enabled;
|
||||
private final String legacyBaseUrl;
|
||||
private final boolean directoryResolved;
|
||||
|
||||
@Value("${flow-center.usermsg.enabled:false}")
|
||||
private boolean enabled;
|
||||
|
||||
@Value("${flow-center.usermsg.base-url:http://localhost:8080}")
|
||||
private String baseUrl;
|
||||
public UserMsgClient(YschOpenCall openCall,
|
||||
IDHttp http,
|
||||
@Value("${flow-center.usermsg.enabled:false}") boolean enabled,
|
||||
@Value("${flow-center.usermsg.base-url:}") String baseUrl) {
|
||||
this.openCall = openCall;
|
||||
this.http = http;
|
||||
this.enabled = enabled;
|
||||
this.legacyBaseUrl = baseUrl == null ? "" : trimTrailing(baseUrl.trim());
|
||||
this.directoryResolved = openCall != null && openCall.hasTarget(DIRECTORY_KEY);
|
||||
if (directoryResolved) {
|
||||
YschMsgOpenApi.setCall(openCall);
|
||||
}
|
||||
if (enabled && !directoryResolved && legacyBaseUrl.isEmpty()) {
|
||||
// enabled=true 表示确实要发;目录与 legacy 键都缺 → 构造期 fail-fast
|
||||
throw new IllegalStateException("UserMsg 调用地址未配置:请配置服务目录 yh.open.services."
|
||||
+ DIRECTORY_KEY + "(推荐,容器直连完整 URL)或回落键 flow-center.usermsg.base-url");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean enabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/** 是否走服务目录命名目标(配置自检用) */
|
||||
public boolean directoryResolved() {
|
||||
return directoryResolved;
|
||||
}
|
||||
|
||||
/** 发送站内信(content 必填;msgNo 幂等,重发安全) */
|
||||
public void sendUserMsg(Integer schId, List<Integer> userIds, Integer msgType, String bizType,
|
||||
String bizId, String title, String content, String jumpPath, String msgNo,
|
||||
@@ -57,7 +86,11 @@ public class UserMsgClient {
|
||||
body.setSchId(schId);
|
||||
body.setExpireDays(expireDays);
|
||||
try {
|
||||
http.post(trimTrailing(baseUrl) + "/oapi/msg/sendUserMsg", body, Object.class);
|
||||
if (directoryResolved) {
|
||||
YschMsgOpenApi.sendUserMsg(body);
|
||||
} else {
|
||||
http.post(legacyBaseUrl + "/oapi/msg/sendUserMsg", body, Object.class);
|
||||
}
|
||||
} catch (BizException e) {
|
||||
log.warn("[flow-center] UserMsg 发送被拒:msgNo={} msg={}", msgNo, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
@@ -75,7 +108,11 @@ public class UserMsgClient {
|
||||
body.setCount(count);
|
||||
body.setSchId(schId);
|
||||
try {
|
||||
http.post(trimTrailing(baseUrl) + "/oapi/msg/todo/inc", body, Object.class);
|
||||
if (directoryResolved) {
|
||||
YschMsgOpenApi.todoInc(body);
|
||||
} else {
|
||||
http.post(legacyBaseUrl + "/oapi/msg/todo/inc", body, Object.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[flow-center] UserMsg 角标增量失败(不阻断主业务):userIds={} count={} {}",
|
||||
userIds, count, e.toString());
|
||||
@@ -85,31 +122,4 @@ public class UserMsgClient {
|
||||
private static String trimTrailing(String url) {
|
||||
return url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
|
||||
}
|
||||
|
||||
/** /oapi/msg/sendUserMsg 请求体(UserMsg SendMsgRequest 同构,字段逐字对齐) */
|
||||
@Data
|
||||
public static class SendMsgRequest {
|
||||
private List<Integer> userIds;
|
||||
private String title;
|
||||
/** 必填(UserMsg @NotBlank) */
|
||||
private String content;
|
||||
private String msgNo;
|
||||
private String bizType;
|
||||
private String bizId;
|
||||
/** 0系统通知/1业务提醒/2审批待办 */
|
||||
private Integer msgType;
|
||||
/** 0无跳转/1应用内路径/2外链 */
|
||||
private Integer jumpType;
|
||||
private String jumpPath;
|
||||
private Integer schId;
|
||||
private Integer expireDays;
|
||||
}
|
||||
|
||||
/** /oapi/msg/todo/inc 请求体(UserMsg TodoCountRequest 同构) */
|
||||
@Data
|
||||
public static class TodoCountRequest {
|
||||
private List<Integer> userIds;
|
||||
private Integer count;
|
||||
private Integer schId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,12 @@ public class UserMsgNotifier {
|
||||
case TASK_HANDLED:
|
||||
notifyTaskHandled(event);
|
||||
break;
|
||||
case RETURNED:
|
||||
notifyReturned(event);
|
||||
break;
|
||||
case TRANSFERRED:
|
||||
notifyTaskCreated(event);
|
||||
break;
|
||||
case COMPLETED:
|
||||
case REJECTED:
|
||||
case WITHDRAWN:
|
||||
@@ -80,6 +86,36 @@ public class UserMsgNotifier {
|
||||
Collections.singletonList(task.getAssigneeId()), -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退回:通知被退回节点的全部新待办人(RETURNED 事件 data.targetNodeIds 由服务层带上——
|
||||
* 退回重建任务在事件发布前已提交,这里按节点查待办即可);todo 角标由 notifyTaskCreated
|
||||
* 通用链路在引擎重建任务时已发过,这里只补一条带退回意见的消息。
|
||||
*/
|
||||
private void notifyReturned(FlowEvent event) {
|
||||
FlwInstance instance = instanceOf(event.getInstanceId());
|
||||
if (instance == null || event.getData() == null) {
|
||||
return;
|
||||
}
|
||||
Object fromNode = event.getData().get("fromNodeName");
|
||||
Object opinion = event.getData().get("opinion");
|
||||
StringBuilder content = new StringBuilder("「").append(safe(instance.getBizTitle())).append("」")
|
||||
.append("被退回(").append(fromNode == null ? "" : fromNode).append(")");
|
||||
if (opinion != null && !String.valueOf(opinion).isEmpty()) {
|
||||
content.append(",意见:").append(opinion);
|
||||
}
|
||||
java.util.List<Integer> targets = event.getData().get("targetUserIds") == null
|
||||
? Collections.emptyList()
|
||||
: (java.util.List<Integer>) event.getData().get("targetUserIds");
|
||||
if (targets.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
userMsgClient.sendUserMsg(instance.getSchId().intValue(), targets, 1,
|
||||
"flow.returned", String.valueOf(instance.getId()),
|
||||
"办件被退回:" + safe(instance.getBizTitle()), content.toString(),
|
||||
"/instance/" + instance.getId(),
|
||||
"returned:" + instance.getId() + ":" + instance.getVersion(), 30);
|
||||
}
|
||||
|
||||
private void notifyResult(FlowEvent event) {
|
||||
FlwInstance instance = instanceOf(event.getInstanceId());
|
||||
if (instance == null) {
|
||||
|
||||
Reference in New Issue
Block a user