diff --git a/src/main/java/com/budwk/app/base/sms/impl/ypi/UnifiedMessageV4ServiceImpl.java b/src/main/java/com/budwk/app/base/sms/impl/ypi/UnifiedMessageV4ServiceImpl.java new file mode 100644 index 0000000..f9732a6 --- /dev/null +++ b/src/main/java/com/budwk/app/base/sms/impl/ypi/UnifiedMessageV4ServiceImpl.java @@ -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 loginNames, String title, String content, String link) { + if (loginNames == null || loginNames.isEmpty()) { + return; + } + List 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 users, String title, String content, String pcUrl, String mobileUrl) { + if (users == null || users.isEmpty()) { + return; + } + List 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 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) { + } +}