Files
flow-center-service/src/main/java/com/yunhai/flowcenter/notify/UserMsgClient.java
T
chenxing b4856a480d
build / build-push (push) Successful in 49s
fix(msg): 消息 jump_app 换源 apply 调用方 app_code 快照,替代 bizType 冒充
P1/P2 联合修复(消息归属 + jump_app 值域污染):
- FlwInstance 新增 apply_app_key/apply_app_code 双列快照(自动 DDL)
- apply 从验签上下文(yh.app.auth)取调用方身份,by-key 一次换得 app_code
  (域间边界只在 apply 转换一次;查无/不可达降级 null,不阻断 apply)
- UserMsgNotifier 两处 jumpApp 改源 applyAppCode——bizType 是消息分类
  不是路由键;降级空时接收方按发送方回落(现状行为)
- FlowDefAdminService.save appKey 必填化(2026-10-12 拍板):新建必须
  显式传;升版本/直改可继承源行,源行为空必须补填(存量 19 条 NULL 随编辑收敛)
- 单测:SaveTest 新增必填拦截 3 用例、BizUrlTest 换源断言+降级用例(19 绿)
- E2E(8085+UserBase):100617->apply_app_code=notice 正向落库、
  100018(app_code NULL)->降级成功、错签名 401,三断言全过
2026-10-12 10:44:07 +08:00

134 lines
5.8 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 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
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;
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 幂等,重发安全)。
*
* @param jumpApp 跳转页面归属应用(可空,值域=app_code 如 notice/svc-hall):页面属业务方
* (bizUrl 落地页)时填 apply 调用方 app_code(=instance.applyAppCode,apply
* 时由验签身份按 by-key 换得),接收方优先按它查基址;页面属流程中心自己
* (/task、/instance)传 null,由接收方按发送方 appKey 回落
*/
public void sendUserMsg(Integer schId, List<Integer> userIds, Integer msgType, String bizType,
String bizId, String title, String content, String jumpPath, String jumpApp,
String msgNo, Integer expireDays) {
if (!enabled || userIds == null || userIds.isEmpty()) {
return;
}
SendMsgRequest body = new SendMsgRequest();
body.setUserIds(userIds);
body.setMsgType(msgType);
body.setBizType(bizType);
body.setBizId(bizId);
body.setTitle(title);
body.setContent(content);
if (jumpPath != null) {
body.setJumpType(1);
body.setJumpPath(jumpPath);
}
body.setJumpApp(jumpApp);
body.setMsgNo(msgNo);
body.setSchId(schId);
body.setExpireDays(expireDays);
try {
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) {
log.warn("[flow-center] UserMsg 发送失败(不阻断主业务):msgNo={} {}", msgNo, e.toString());
}
}
/** 待办角标增量(+1 到达 / -1 办结) */
public void todoInc(Integer schId, List<Integer> userIds, int count) {
if (!enabled || userIds == null || userIds.isEmpty()) {
return;
}
TodoCountRequest body = new TodoCountRequest();
body.setUserIds(userIds);
body.setCount(count);
body.setSchId(schId);
try {
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());
}
}
private static String trimTrailing(String url) {
return url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
}
}