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;
}
}