This commit is contained in:
=
2026-08-04 10:45:10 +08:00
parent f8051e7a02
commit 49052142b2
@@ -0,0 +1,191 @@
package com.budwk.app.base.sms.impl.ypi;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.sms.SmsService;
import com.budwk.app.sys.models.Sys_user;
import lombok.extern.slf4j.Slf4j;
import org.nutz.integration.jedis.RedisService;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.List;
/**
* 学校统一消息V4发送服务实现。
*/
@Slf4j
@IocBean
public class UnifiedMessageV4ServiceImpl implements SmsService {
private static final String TOKEN_CACHE_KEY = "unified_message_v4:token:";
private static final int TOKEN_CACHE_SECONDS = 270;
@Inject
private RedisService redisService;
@Inject
private PropertiesProxy conf;
@Override
public void send(String loginName, String content) {
send(loginName, "智慧工会", content);
}
@Override
public void send(String loginName, String title, String content) {
send(loginName, title, content, null);
}
@Override
public void send(String loginName, String title, String content, String link) {
if (StrUtil.isBlank(loginName)) {
throw new BaseException("学校统一消息发送失败:接收人工号不能为空");
}
sendMessage(title, content, createReceivers(List.of(new Receiver(loginName, loginName))), link, link);
}
@Override
public void massSend(List<String> loginNames, String title, String content, String link) {
if (loginNames == null || loginNames.isEmpty()) {
return;
}
List<Receiver> receivers = loginNames.stream()
.filter(StrUtil::isNotBlank)
.distinct()
.map(loginName -> new Receiver(loginName, loginName))
.toList();
sendMessage(title, content, createReceivers(receivers), link, link);
}
@Override
public void massSendByUsers(List<Sys_user> users, String title, String content, String pcUrl, String mobileUrl) {
if (users == null || users.isEmpty()) {
return;
}
List<Receiver> receivers = users.stream()
.filter(user -> StrUtil.isNotBlank(user.getLoginname()) && StrUtil.isNotBlank(user.getUsername()))
.map(user -> new Receiver(user.getLoginname(), user.getUsername()))
.toList();
if (receivers.isEmpty()) {
throw new BaseException("学校统一消息发送失败:接收人工号或姓名不能为空");
}
sendMessage(title, content, createReceivers(receivers), pcUrl, mobileUrl);
}
/**
* 调用统一消息V4普通消息接口;有链接时按外链模式同时传递PC和手机端地址。
*/
private void sendMessage(String title, String content, JSONArray receivers, String pcUrl, String mobileUrl) {
String token = getAccessToken();
boolean hasLink = StrUtil.isNotBlank(pcUrl) || StrUtil.isNotBlank(mobileUrl);
JSONObject body = JSONUtil.createObj();
body.set("title", title);
body.set("messageMode", hasLink ? "2" : "1");
body.set("content", content);
body.set("sendWay", "2");
body.set("channel", JSONUtil.toJsonStr(List.of(getRequiredConfig("school-message.channel"))));
body.set("receive", JSONUtil.toJsonStr(receivers));
if (hasLink) {
body.set("url", StrUtil.blankToDefault(pcUrl, mobileUrl));
body.set("mobileUrl", StrUtil.blankToDefault(mobileUrl, pcUrl));
}
String sendUrl = getRequiredConfig("school-message.send-url");
String requestBody = JSONUtil.toJsonStr(body);
try {
HttpRequest request = HttpRequest.post(sendUrl)
.header("domain-name", getRequiredConfig("school-message.domain-name"))
.header("token", token)
.body(requestBody);
addInterceptDomainHeader(request);
String responseBody = request.execute().body();
JSONObject response = JSONUtil.parseObj(responseBody);
if (response.getInt("state", 0) != 200) {
throw new BaseException("学校统一消息发送失败:{}", response.getStr("message", responseBody));
}
log.info("学校统一消息发送成功,接收人数量={},response={}", receivers.size(), responseBody);
} catch (BaseException e) {
throw e;
} catch (Exception e) {
log.error("学校统一消息发送异常,url={}body={}", sendUrl, requestBody, e);
throw new BaseException("学校统一消息发送异常:{}", e.getMessage());
}
}
/**
* 获取学校统一消息V4令牌;文档有效期为300秒,缓存270秒避免临界过期。
*/
private String getAccessToken() {
String cacheKey = TOKEN_CACHE_KEY + getRequiredConfig("school-message.app-id");
String cachedToken = redisService.get(cacheKey);
if (StrUtil.isNotBlank(cachedToken)) {
return cachedToken;
}
String tokenUrl = getRequiredConfig("school-message.token-url");
try {
HttpRequest request = HttpRequest.post(tokenUrl)
.header("domain-name", getRequiredConfig("school-message.domain-name"))
.header("appId", getRequiredConfig("school-message.app-id"))
.header("appPassword", getRequiredConfig("school-message.app-password"));
addInterceptDomainHeader(request);
String responseBody = request.execute().body();
JSONObject response = JSONUtil.parseObj(responseBody);
JSONObject data = response.getJSONObject("data");
String token = data == null ? null : data.getStr("token");
if (response.getInt("state", 0) != 200 || StrUtil.isBlank(token)) {
throw new BaseException("学校统一消息获取令牌失败:{}", response.getStr("message", responseBody));
}
redisService.setex(cacheKey, TOKEN_CACHE_SECONDS, token);
return token;
} catch (BaseException e) {
throw e;
} catch (Exception e) {
log.error("学校统一消息获取令牌异常,url={}", tokenUrl, e);
throw new BaseException("学校统一消息获取令牌异常:{}", e.getMessage());
}
}
/**
* 可信域名仅在学校平台配置白名单时传递,未配置则不发送该请求头。
*/
private void addInterceptDomainHeader(HttpRequest request) {
String interceptDomainName = conf.get("school-message.intercept-domain-name", "");
if (StrUtil.isNotBlank(interceptDomainName)) {
request.header("intercept-domain-name", interceptDomainName);
}
}
/**
* 学校统一消息所需配置缺失时阻止发送,避免向错误的网关地址提交数据。
*/
private String getRequiredConfig(String key) {
String value = conf.get(key);
if (StrUtil.isBlank(value)) {
throw new BaseException("学校统一消息配置{}为空", key);
}
return value;
}
/**
* 统一消息V4接收人字段固定为工号和姓名。
*/
private JSONArray createReceivers(List<Receiver> receiverList) {
JSONArray receivers = JSONUtil.createArray();
receiverList.forEach(receiver -> receivers.add(JSONUtil.createObj()
.set("userCode", receiver.userCode())
.set("userName", receiver.userName())));
return receivers;
}
/**
* 接收人传输对象,避免调用方遗漏统一消息要求的姓名字段。
*/
private record Receiver(String userCode, String userName) {
}
}