build / build-push (push) Successful in 49s
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,三断言全过
134 lines
5.8 KiB
Java
134 lines
5.8 KiB
Java
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;
|
||
}
|
||
}
|