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,迁移期兼容)。

* 发送方不阻断主业务:失败只记日志(对齐 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 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 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; } }