This commit is contained in:
2026-06-17 17:18:32 +08:00
parent c1a22401d5
commit 13fc53e849
12 changed files with 493 additions and 804 deletions
@@ -2,17 +2,14 @@ package com.budwk.app.base.sms;
import com.budwk.app.base.sms.model.MsgPlatformResponse; import com.budwk.app.base.sms.model.MsgPlatformResponse;
import com.budwk.app.base.sms.model.MsgPlatformSmsRequest; import com.budwk.app.base.sms.model.MsgPlatformSmsRequest;
import com.budwk.app.base.sms.model.MsgPlatformWechatRequest;
import java.util.List;
public interface SmsService { public interface SmsService {
/** /**
* 发送短信消息。 * 发送阿里云短信消息。
* *
* @param request 短信请求参数,需传接收人、发送人、模板ID或消息内容等字段 * @param request 短信请求参数,需传接收人、模板 Code、模板变量或消息内容等字段
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse} * @return 阿里云短信发送结果,返回类型为 {@link MsgPlatformResponse}
*/ */
MsgPlatformResponse sendSms(MsgPlatformSmsRequest request); MsgPlatformResponse sendSms(MsgPlatformSmsRequest request);
@@ -22,7 +19,7 @@ public interface SmsService {
* *
* @param account 接收人工号 * @param account 接收人工号
* @param content 短信内容 * @param content 短信内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse} * @return 阿里云短信发送结果,返回类型为 {@link MsgPlatformResponse}
*/ */
MsgPlatformResponse sendSmsByAccount(String account, String content); MsgPlatformResponse sendSmsByAccount(String account, String content);
@@ -32,25 +29,7 @@ public interface SmsService {
* *
* @param mobile 接收人手机号 * @param mobile 接收人手机号
* @param content 短信内容 * @param content 短信内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse} * @return 阿里云短信发送结果,返回类型为 {@link MsgPlatformResponse}
*/ */
MsgPlatformResponse sendSmsByMobile(String mobile, String content); MsgPlatformResponse sendSmsByMobile(String mobile, String content);
/**
* 发送微信消息。
*
* @param request 微信请求参数,需传接收人、发送人、消息类型、模板ID或消息内容等字段
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
MsgPlatformResponse sendWechat(MsgPlatformWechatRequest request);
/**
* 发送微信文本消息的便捷方法。
* controller 只需要传接收人工号或手机号,以及微信文本内容即可。
*
* @param accountOrMobile 接收人工号或手机号
* @param content 微信文本内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
MsgPlatformResponse sendWechatTextByAccountOrMobile(String accountOrMobile, String content);
} }
@@ -1,544 +1,477 @@
package com.budwk.app.base.sms.impl; package com.budwk.app.base.sms.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.budwk.app.base.exception.BaseException; import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.sms.SmsService; import com.budwk.app.base.sms.SmsService;
import com.budwk.app.base.sms.model.*; import com.budwk.app.base.sms.model.MsgPlatformRecipient;
import com.budwk.app.base.sms.model.MsgPlatformResponse;
import com.budwk.app.base.sms.model.MsgPlatformSmsRequest;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.web.commons.base.Globals; import com.budwk.app.web.commons.base.Globals;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.integration.jedis.RedisService; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.impl.PropertiesProxy; import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings; import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import java.io.*; import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* @ClassName SmsJshvcServiceImpl * 阿里云短信发送服务。
* @Author JyuHsin * 统一封装阿里云 SendSms 接口,业务代码只需要传接收人工号/手机号和短信内容;
* @Date 2025/11/26 19:32 * 签名、模板、模板变量名等固定参数从 sms.aliyun.* 配置中读取。
* @Version 1.0
* @Description TODO
*/ */
@Slf4j @Slf4j
@IocBean @IocBean
public class SmsServiceImpl implements SmsService { public class SmsServiceImpl implements SmsService {
private static final Log log = Logs.get();
private static final String SMS_API_PATH = "/tp_mp/api/SmsService/saveSmsInfo";
private static final String WECHAT_API_PATH = "/tp_mp/api/WechatService/saveWechatInfo";
private static final int MAX_RECEIVER_COUNT = 500;
@Inject private static final String SMS_ACTION = "SendSms";
private PropertiesProxy conf; private static final String SMS_VERSION = "2017-05-25";
private static final String SIGN_ALGORITHM = "ACS3-HMAC-SHA256";
private static final String EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
/** @Inject
* 发送短信消息。 private PropertiesProxy conf;
*
* @param request 短信请求参数,需传接收人、发送人、模板ID或消息内容等字段
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendSms(MsgPlatformSmsRequest request) {
validateCommonRequest(request);
JSONObject payload = buildCommonPayload(request, true);
return doPost(SMS_API_PATH, payload);
}
/** @Inject
* 通过工号发送短信文本消息的便捷方法。 private Dao dao;
* 该方法适合 controller 里只传“工号 + 内容”的简单场景,发送人信息从配置中读取。
*
* @param account 接收人工号
* @param content 短信内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendSmsByAccount(String account, String content) {
MsgPlatformSmsRequest request = new MsgPlatformSmsRequest();
request.setInfo(content);
request.setRecipients(buildAccountRecipients(account));
fillDefaultSenderInfo(request);
return sendSms(request);
}
/** /**
* 通过手机号发送短信文本消息的便捷方法。 * 发送短信消息。
* 该方法适合 controller 里只传“手机号 + 内容”的简单场景,发送人信息从配置中读取。 * 参数说明:
* * 1. recipients:接收人列表,mobile 为手机号;如果 mobile 为空但 account 不为空,会按工号查询 sys_user.mobile。
* @param mobile 接收人手机号 * 2. templateId:可选,阿里云短信模板 Code;为空时读取 sms.aliyun.template-code。
* @param content 短信内容 * 3. sendUserSign:可选,阿里云短信签名;为空时读取 sms.aliyun.sign-name。
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse} * 4. customVar:可选,阿里云模板变量 Map;为空时使用 sms.aliyun.template-param-name 作为变量名,info 作为变量值。
*/ * 返回值为 MsgPlatformResponse,code=0/result=true 表示阿里云返回 Code=OK,msgId 保存阿里云 BizId。
@Override *
public MsgPlatformResponse sendSmsByMobile(String mobile, String content) { * @param request 短信发送请求参数
MsgPlatformSmsRequest request = new MsgPlatformSmsRequest(); * @return 阿里云短信发送结果
request.setInfo(content); */
request.setRecipients(buildMobileRecipients(mobile)); @Override
request.setCustomVar(Map.of("1","","2","","3",content)); public MsgPlatformResponse sendSms(MsgPlatformSmsRequest request) {
fillDefaultSenderInfo(request); MsgPlatformResponse skipResponse = checkSendEnabled();
return sendSms(request); if (skipResponse != null) {
} return skipResponse;
}
validateSmsRequest(request);
/** String phoneNumbers = resolvePhoneNumbers(request);
* 发送微信消息。 String signName = Strings.isBlank(request.getSendUserSign()) ? getRequiredConfig("sms.aliyun.sign-name", "请先配置 sms.aliyun.sign-name") : request.getSendUserSign();
* String templateCode = Strings.isBlank(request.getTemplateId()) ? getRequiredConfig("sms.aliyun.template-code", "请先配置 sms.aliyun.template-code") : request.getTemplateId();
* @param request 微信请求参数,需传接收人、发送人、消息类型、模板ID或消息内容等字段 String templateParam = buildTemplateParam(request);
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendWechat(MsgPlatformWechatRequest request) {
validateCommonRequest(request);
if (Strings.isBlank(request.getWechatType())) {
request.setWechatType("text");
}
if ("news".equalsIgnoreCase(request.getWechatType()) && Strings.isBlank(request.getInfo())) {
if (request.getNewsItems() == null || request.getNewsItems().isEmpty()) {
throw new BaseException("微信图文消息必须传 info 或 newsItems");
}
request.setInfo(JSONUtil.toJsonStr(request.getNewsItems()));
}
JSONObject payload = buildCommonPayload(request, false);
payload.set("wechat_type", request.getWechatType());
return doPost(WECHAT_API_PATH, payload);
}
/** return sendToAliyun(phoneNumbers, signName, templateCode, templateParam);
* 发送微信文本消息的便捷方法。 }
* 该方法适合 controller 里只传“工号/手机号 + 内容”的简单场景,发送人信息从配置中读取。
*
* @param accountOrMobile 接收人工号或手机号
* @param content 微信文本内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendWechatTextByAccountOrMobile(String accountOrMobile, String content) {
MsgPlatformWechatRequest request = new MsgPlatformWechatRequest();
request.setInfo(content);
request.setWechatType("text");
request.setRecipients(buildWechatRecipients(accountOrMobile));
fillDefaultSenderInfo(request);
return sendWechat(request);
}
/** /**
* 组装平台公共请求体。 * 通过工号发送阿里云短信。
* * 参数说明:
* @param request 平台请求参数 * account 传系统用户工号,对应 sys_user.loginname;方法内部查询 sys_user.mobile。
* @param includeMobile 是否在 person_info 第 5 段写入手机号 * content 传模板变量内容,默认写入 sms.aliyun.template-param-name 对应变量。
* @return 返回发送给第三方平台的 JSON 对象 * 返回值为 MsgPlatformResponse,code=0/result=true 表示发送请求提交成功。
*/ *
private JSONObject buildCommonPayload(MsgPlatformBaseRequest request, boolean includeMobile) { * @param account 接收人工号
JSONObject payload = new JSONObject(); * @param content 短信模板变量内容
payload.set("tp_name", getRequiredConfig("msg.platform.tp-name", "请先配置 msg.platform.tp-name")); * @return 阿里云短信发送结果
payload.set("secret_key", getSecretKey()); */
payload.set("person_info", resolvePersonInfo(request, includeMobile)); @Override
payload.set("template_id", Strings.sNull(request.getTemplateId())); public MsgPlatformResponse sendSmsByAccount(String account, String content) {
payload.set("info", Strings.sNull(request.getInfo())); if (Strings.isBlank(account)) {
payload.set("send_priority", Strings.isBlank(request.getSendPriority()) ? "3" : request.getSendPriority()); throw new BaseException("接收人工号不能为空");
payload.set("send_user_id", Strings.sNull(request.getSendUserId())); }
payload.set("send_user_name", Strings.sNull(request.getSendUserName())); Sys_user user = dao.fetch(Sys_user.class, Cnd.where(Sys_user::getLoginname, "=", account));
payload.set("send_time", Strings.sNull(request.getSendTime())); if (user == null) {
payload.set("send_unit_id", Strings.sNull(request.getSendUnitId())); throw new BaseException("未找到工号对应用户:" + account);
payload.set("send_unit_name", Strings.sNull(request.getSendUnitName())); }
payload.set("send_user_sign", Strings.sNull(request.getSendUserSign())); return sendSmsByMobile(user.getMobile(), content);
payload.set("receipt_id", Strings.isBlank(request.getReceiptId()) ? "0" : request.getReceiptId()); }
if (request.getCustomVar() != null && !request.getCustomVar().isEmpty()) {
payload.set("custom_var", JSONUtil.toJsonStr(request.getCustomVar()));
}
return payload;
}
/** /**
* 校验公共请求参数,避免 controller 传错参数后才到第三方接口报错。 * 通过手机号发送阿里云短信。
* * 参数说明:
* @param request 平台请求参数 * mobile 传 11 位手机号。
*/ * content 传模板变量内容,默认写入 sms.aliyun.template-param-name 对应变量。
private void validateCommonRequest(MsgPlatformBaseRequest request) { * 返回值为 MsgPlatformResponse,code=0/result=true 表示发送请求提交成功。
if (request == null) { *
throw new BaseException("请求参数不能为空"); * @param mobile 接收人手机号
} * @param content 短信模板变量内容
if (Strings.isBlank(request.getPersonInfo()) && (request.getRecipients() == null || request.getRecipients().isEmpty())) { * @return 阿里云短信发送结果
throw new BaseException("personInfo 和 recipients 不能同时为空"); */
} @Override
if (Strings.isBlank(request.getTemplateId()) && Strings.isBlank(request.getInfo())) { public MsgPlatformResponse sendSmsByMobile(String mobile, String content) {
throw new BaseException("templateId 和 info 至少需要传一个"); MsgPlatformRecipient recipient = new MsgPlatformRecipient();
} recipient.setMobile(mobile);
if (Strings.isBlank(request.getSendUserId())) {
throw new BaseException("sendUserId 不能为空");
}
if (Strings.isBlank(request.getSendUserName())) {
throw new BaseException("sendUserName 不能为空");
}
if (Strings.isBlank(request.getSendPriority())) {
request.setSendPriority("3");
}
if ("4".equals(request.getSendPriority()) && Strings.isBlank(request.getSendTime())) {
throw new BaseException("sendPriority=4 时必须传 sendTime");
}
validateReceiverCount(request.getPersonInfo(), request.getRecipients());
}
/** MsgPlatformSmsRequest request = new MsgPlatformSmsRequest();
* 解析 person_info。 request.setInfo(content);
* 已传原始 personInfo 时直接使用;否则根据 recipients 自动拼接平台要求的格式。 request.setRecipients(List.of(recipient));
* return sendSms(request);
* @param request 平台请求参数 }
* @param includeMobile 是否在第 5 段写入手机号
* @return 平台要求的 person_info 字符串
*/
private String resolvePersonInfo(MsgPlatformBaseRequest request, boolean includeMobile) {
if (Strings.isNotBlank(request.getPersonInfo())) {
return request.getPersonInfo();
}
List<MsgPlatformRecipient> recipients = request.getRecipients();
if (recipients == null || recipients.isEmpty()) {
throw new BaseException("接收人列表不能为空");
}
return recipients.stream().map(recipient -> buildReceiverLine(recipient, includeMobile)).collect(Collectors.joining("^@^"));
}
/** /**
* 将单个接收人转成平台要求的分隔符格式。 * 检查当前环境是否允许真实发送短信。
* * 线下项目以 Globals.sso=false 识别,直接跳过阿里云调用,避免本地或离线环境误发短信;
* @param recipient 接收人对象 * 阿里云短信开关未启用时同样返回跳过结果,不影响待办主流程继续执行。
* @param includeMobile 是否在最后一段补手机号 *
* @return 单个接收人的 person_info 片段 * @return 不允许发送时返回跳过响应;允许发送时返回 null
*/ */
private String buildReceiverLine(MsgPlatformRecipient recipient, boolean includeMobile) { private MsgPlatformResponse checkSendEnabled() {
if (recipient == null) { if (!Globals.sso) {
throw new BaseException("接收人信息中存在空对象"); log.warn("线下项目不允许发送短信,已跳过阿里云短信调用");
} return buildSkippedResponse("线下项目不允许发送短信,已跳过实际发送");
List<String> segments = new ArrayList<>(5); }
segments.add(Strings.sNull(recipient.getName())); if (!Globals.MyConfig.getBoolean("AppSms", false)) {
segments.add(Strings.sNull(recipient.getAccount())); log.warn("系统参数 AppSms 未开启,已跳过阿里云短信调用");
segments.add(Strings.sNull(recipient.getUnitId())); return buildSkippedResponse("系统参数 AppSms 未开启,已跳过实际发送");
segments.add(Strings.sNull(recipient.getUnitName())); }
if (includeMobile) { if (!conf.getBoolean("sms.aliyun.enabled", false)) {
segments.add(resolveSmsMobile(recipient)); log.warn("阿里云短信未启用,已跳过阿里云短信调用");
} else { return buildSkippedResponse("阿里云短信未启用,已跳过实际发送");
segments.add(""); }
} return null;
return String.join("|", segments); }
}
/** /**
* 解析短信接收人手机号。 * 构建跳过发送的统一响应。
* 如果 controller 传的是手机号,则校验手机号格式; * 该响应表示业务流程可继续,但没有发生真实外部短信调用。
* 如果传的是工号,则允许手机号为空,按“姓名|工号|部门ID|部门名称|”格式发送。 *
* * @param message 跳过原因
* @param recipient 接收人信息 * @return 统一短信响应对象
* @return 短信 person_info 第 5 段内容 */
*/ private MsgPlatformResponse buildSkippedResponse(String message) {
private String resolveSmsMobile(MsgPlatformRecipient recipient) { MsgPlatformResponse response = new MsgPlatformResponse();
String mobile = recipient.getMobile(); response.setHttpStatus(200);
if (Strings.isBlank(mobile)) { response.setCode(0);
if (Strings.isBlank(recipient.getAccount())) { response.setResult(true);
throw new BaseException("短信接收人手机号和工号不能同时为空"); response.setMsg(message);
} return response;
return ""; }
}
if (!mobile.matches("^\\d{11}$")) {
throw new BaseException("短信接收人手机号必须为11位数字");
}
return mobile;
}
/** /**
* 校验接收人数量,避免超过第三方平台单次上限。 * 校验阿里云短信请求参数。
* * 必须至少提供接收人和模板变量内容;模板变量可通过 customVar 显式传入。
* @param personInfo 原始 person_info *
* @param recipients 接收人列表 * @param request 短信请求参数
*/ */
private void validateReceiverCount(String personInfo, List<MsgPlatformRecipient> recipients) { private void validateSmsRequest(MsgPlatformSmsRequest request) {
int receiverCount; if (request == null) {
if (Strings.isNotBlank(personInfo)) { throw new BaseException("短信请求参数不能为空");
receiverCount = personInfo.split("\\^@\\^", -1).length; }
} else { if (request.getRecipients() == null || request.getRecipients().isEmpty()) {
receiverCount = recipients == null ? 0 : recipients.size(); throw new BaseException("短信接收人不能为空");
} }
if (receiverCount > MAX_RECEIVER_COUNT) { if ((request.getCustomVar() == null || request.getCustomVar().isEmpty()) && Strings.isBlank(request.getInfo())) {
throw new BaseException("单次发送人数不能超过" + MAX_RECEIVER_COUNT + "人"); throw new BaseException("短信模板变量内容不能为空");
} }
} }
/** /**
* 构建工号发送专用接收人列表。 * 解析接收人手机号。
* * recipients.mobile 优先;mobile 为空时,使用 recipients.account 按工号查询 sys_user.mobile。
* @param account controller 传入的工号 *
* @return 仅包含一个接收人的列表 * @param request 短信请求参数
*/ * @return 逗号分隔的手机号列表
private List<MsgPlatformRecipient> buildAccountRecipients(String account) { */
if (Strings.isBlank(account)) { private String resolvePhoneNumbers(MsgPlatformSmsRequest request) {
throw new BaseException("接收人工号不能为空"); List<String> mobiles = new ArrayList<>();
} for (MsgPlatformRecipient recipient : request.getRecipients()) {
MsgPlatformRecipient recipient = new MsgPlatformRecipient(); if (recipient == null) {
recipient.setAccount(account); continue;
List<MsgPlatformRecipient> recipients = new ArrayList<>(1); }
recipients.add(recipient); String mobile = recipient.getMobile();
return recipients; if (Strings.isBlank(mobile) && Strings.isNotBlank(recipient.getAccount())) {
} Sys_user user = dao.fetch(Sys_user.class, Cnd.where(Sys_user::getLoginname, "=", recipient.getAccount()));
mobile = user == null ? "" : user.getMobile();
}
if (Strings.isBlank(mobile)) {
log.warn("短信接收人缺少手机号,account={}", recipient.getAccount());
continue;
}
if (!mobile.matches("^1\\d{10}$")) {
log.warn("短信接收人手机号格式不正确,mobile={}", mobile);
continue;
}
mobiles.add(mobile);
}
if (mobiles.isEmpty()) {
throw new BaseException("没有可发送短信的有效手机号");
}
return mobiles.stream().distinct().collect(Collectors.joining(","));
}
/** /**
* 构建手机号发送专用接收人列表。 * 构建阿里云模板变量。
* * 如果业务传了 customVar,则直接作为模板变量;否则用 sms.aliyun.template-param-name 配置项包裹 info。
* @param mobile controller 传入的手机号 *
* @return 仅包含一个接收人的列表 * @param request 短信请求参数
*/ * @return JSON 字符串格式的模板变量
private List<MsgPlatformRecipient> buildMobileRecipients(String mobile) { */
if (Strings.isBlank(mobile)) { private String buildTemplateParam(MsgPlatformSmsRequest request) {
throw new BaseException("接收人手机号不能为空"); Map<String, String> customVar = request.getCustomVar();
} if (customVar != null && !customVar.isEmpty()) {
if (!mobile.matches("^\\d{11}$")) { return JSONUtil.toJsonStr(customVar);
throw new BaseException("接收人手机号必须为11位数字"); }
} String paramName = conf.get("sms.aliyun.template-param-name", "content");
MsgPlatformRecipient recipient = new MsgPlatformRecipient(); JSONObject params = new JSONObject();
recipient.setMobile(mobile); params.set(paramName, request.getInfo());
List<MsgPlatformRecipient> recipients = new ArrayList<>(1); return JSONUtil.toJsonStr(params);
recipients.add(recipient); }
return recipients;
}
/** /**
* 构建微信便捷发送专用接收人列表。 * 调用阿里云 SendSms 接口。
* * 使用阿里云 V3 签名机制,HTTP 请求参数放在 query 中,body 为空。
* @param accountOrMobile controller 传入的企业微信成员ID、工号或手机号 *
* @return 仅包含一个接收人的列表 * @param phoneNumbers 手机号,多个手机号用英文逗号分隔
*/ * @param signName 阿里云短信签名
private List<MsgPlatformRecipient> buildWechatRecipients(String accountOrMobile) { * @param templateCode 阿里云短信模板 Code
if (Strings.isBlank(accountOrMobile)) { * @param templateParam 模板变量 JSON 字符串
throw new BaseException("微信接收人工号或手机号不能为空"); * @return 阿里云短信发送结果
} */
MsgPlatformRecipient recipient = new MsgPlatformRecipient(); private MsgPlatformResponse sendToAliyun(String phoneNumbers, String signName, String templateCode, String templateParam) {
recipient.setAccount(accountOrMobile); String endpoint = conf.get("sms.aliyun.endpoint", "dysmsapi.aliyuncs.com");
List<MsgPlatformRecipient> recipients = new ArrayList<>(1); String accessKeyId = getRequiredConfig("sms.aliyun.access-key-id", "请先配置 sms.aliyun.access-key-id");
recipients.add(recipient); String accessKeySecret = getRequiredConfig("sms.aliyun.access-key-secret", "请先配置 sms.aliyun.access-key-secret");
return recipients;
}
/** Map<String, String> queryParams = Map.of(
* 为便捷发送方法补默认发送人信息。 "PhoneNumbers", phoneNumbers,
* 这些字段从配置中读取,避免 controller 每次重复传固定参数。 "SignName", signName,
* "TemplateCode", templateCode,
* @param request 平台请求参数 "TemplateParam", templateParam
*/ );
private void fillDefaultSenderInfo(MsgPlatformBaseRequest request) { Map<String, String> headers = buildSignedHeaders(endpoint, accessKeyId, accessKeySecret, queryParams);
request.setSendUserId(getRequiredConfig("msg.platform.default-send-user-id", "请先配置 msg.platform.default-send-user-id")); String requestUrl = "https://" + endpoint + "/?" + canonicalQueryString(queryParams);
request.setSendUserName(getRequiredConfig("msg.platform.default-send-user-name", "请先配置 msg.platform.default-send-user-name"));
request.setSendUnitId(conf.get("msg.platform.default-send-unit-id", ""));
request.setSendUnitName(conf.get("msg.platform.default-send-unit-name", ""));
request.setSendUserSign(conf.get("msg.platform.default-send-user-sign", ""));
request.setTemplateId(conf.get("msg.platform.default-template-id", ""));
}
/** HttpURLConnection connection = null;
* 调用地大通讯平台 REST 接口。 try {
* connection = (HttpURLConnection) new URL(requestUrl).openConnection();
* @param apiPath 具体接口路径 connection.setRequestMethod("POST");
* @param payload 发送报文 connection.setDoOutput(false);
* @return 平台响应结果 connection.setDoInput(true);
*/ connection.setUseCaches(false);
private MsgPlatformResponse doPost(String apiPath, JSONObject payload) { connection.setConnectTimeout(conf.getInt("sms.aliyun.connect-timeout", 5000));
connection.setReadTimeout(conf.getInt("sms.aliyun.read-timeout", 10000));
for (Map.Entry<String, String> header : headers.entrySet()) {
connection.setRequestProperty(header.getKey(), header.getValue());
}
if (!Globals.sso){ int httpStatus = connection.getResponseCode();
log.error("【log】开发模式不允许发送消息通知"); String responseBody = readResponseBody(connection, httpStatus);
MsgPlatformResponse response = new MsgPlatformResponse(); return parseAliyunResponse(httpStatus, responseBody);
response.setHttpStatus(200); } catch (IOException e) {
response.setCode(0); log.error("调用阿里云短信接口异常", e);
response.setResult(true); throw new BaseException("调用阿里云短信接口失败:" + e.getMessage());
response.setMsg("开发模式不允许发送消息通知"); } finally {
response.setRawBody(JSONUtil.toJsonStr(payload)); if (connection != null) {
return response; connection.disconnect();
} }
if (!Globals.MyConfig.getBoolean("AppSms", false)) { }
log.error("【log】短信配置未开启"); }
MsgPlatformResponse response = new MsgPlatformResponse();
response.setHttpStatus(200);
response.setCode(0);
response.setResult(true);
response.setMsg("短信配置未开启");
response.setRawBody(JSONUtil.toJsonStr(payload));
return response;
}
if (!conf.getBoolean("msg.platform.enabled", false)) {
MsgPlatformResponse response = new MsgPlatformResponse();
response.setHttpStatus(200);
response.setCode(0);
response.setResult(true);
response.setMsg("通讯平台未启用,已跳过实际发送");
response.setRawBody(JSONUtil.toJsonStr(payload));
return response;
}
HttpURLConnection connection = null;
try {
String requestBody = JSONUtil.toJsonStr(payload);
log.error("发送内容:=================================================="+requestBody);
String requestUrl = buildRequestUrl(apiPath);
log.warnf("发送地址:=================================================="+requestUrl);
connection = (HttpURLConnection) new URL(requestUrl).openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setUseCaches(false);
connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
connection.setConnectTimeout(conf.getInt("msg.platform.connect-timeout", 5000));
connection.setReadTimeout(conf.getInt("msg.platform.read-timeout", 10000));
try (OutputStream outputStream = connection.getOutputStream()) {
outputStream.write(requestBody.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
int httpStatus = connection.getResponseCode();
String responseBody = readResponseBody(connection, httpStatus);
MsgPlatformResponse response = parseResponse(httpStatus, responseBody);
log.warnf("接口调用返回结果=============================="+responseBody);
if (response.getCode() != null && response.getCode() != 0) {
log.warnf("通讯平台调用失败, httpStatus=%s, body=%s", httpStatus, responseBody);
}
return response;
} catch (IOException e) {
log.error("调用通讯平台接口异常", e);
throw new BaseException("调用通讯平台接口失败: " + e.getMessage());
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
/** /**
* 构建完整请求地址,支持配置带或不带结尾斜杠的 base-url。 * 构建阿里云 V3 签名请求头。
* * 参与签名的头包括 host、x-acs-action、x-acs-content-sha256、x-acs-date、x-acs-signature-nonce、x-acs-version。
* @param apiPath 接口路径 *
* @return 完整请求地址 * @param endpoint 阿里云短信服务域名
*/ * @param accessKeyId AccessKey ID
private String buildRequestUrl(String apiPath) { * @param accessKeySecret AccessKey Secret
String baseUrl = getRequiredConfig("msg.platform.base-url", "请先配置 msg.platform.base-url"); * @param queryParams SendSms 请求参数
if (baseUrl.endsWith("/")) { * @return 已包含 Authorization 的请求头
return baseUrl.substring(0, baseUrl.length() - 1) + apiPath; */
} private Map<String, String> buildSignedHeaders(String endpoint, String accessKeyId, String accessKeySecret, Map<String, String> queryParams) {
return baseUrl + apiPath; String date = Instant.now().truncatedTo(ChronoUnit.SECONDS).toString();
} String nonce = UUID.randomUUID().toString();
String signedHeaders = "host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version";
String canonicalHeaders = "host:" + endpoint + "\n"
+ "x-acs-action:" + SMS_ACTION + "\n"
+ "x-acs-content-sha256:" + EMPTY_BODY_SHA256 + "\n"
+ "x-acs-date:" + date + "\n"
+ "x-acs-signature-nonce:" + nonce + "\n"
+ "x-acs-version:" + SMS_VERSION + "\n";
String canonicalRequest = "POST\n"
+ "/\n"
+ canonicalQueryString(queryParams) + "\n"
+ canonicalHeaders + "\n"
+ signedHeaders + "\n"
+ EMPTY_BODY_SHA256;
String stringToSign = SIGN_ALGORITHM + "\n" + sha256Hex(canonicalRequest);
String signature = hmacSha256Hex(stringToSign, accessKeySecret);
String authorization = SIGN_ALGORITHM
+ " Credential=" + accessKeyId
+ ",SignedHeaders=" + signedHeaders
+ ",Signature=" + signature;
/** return Map.of(
* 读取第三方接口响应报文。 "host", endpoint,
* "x-acs-action", SMS_ACTION,
* @param connection HttpURLConnection "x-acs-version", SMS_VERSION,
* @param httpStatus HTTP 状态码 "x-acs-date", date,
* @return 原始响应字符串 "x-acs-signature-nonce", nonce,
* @throws IOException 读取流失败时抛出 "x-acs-content-sha256", EMPTY_BODY_SHA256,
*/ "Authorization", authorization
private String readResponseBody(HttpURLConnection connection, int httpStatus) throws IOException { );
InputStream inputStream = httpStatus >= 200 && httpStatus < 400 ? connection.getInputStream() : connection.getErrorStream(); }
if (inputStream == null) {
return "";
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
StringBuilder builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
return builder.toString();
}
}
/** /**
* 解析平台响应体。 * 生成规范化 QueryString,按参数名排序后进行 RFC3986 编码。
* *
* @param httpStatus HTTP 状态码 * @param params 请求参数
* @param responseBody 原始响应字符串 * @return 规范化 QueryString
* @return 解析后的平台响应对象 */
*/ private String canonicalQueryString(Map<String, String> params) {
private MsgPlatformResponse parseResponse(int httpStatus, String responseBody) { return params.entrySet().stream()
MsgPlatformResponse response = new MsgPlatformResponse(); .sorted(Map.Entry.comparingByKey())
response.setHttpStatus(httpStatus); .map(entry -> percentEncode(entry.getKey()) + "=" + percentEncode(entry.getValue()))
response.setRawBody(responseBody); .collect(Collectors.joining("&"));
if (Strings.isBlank(responseBody)) { }
response.setCode(httpStatus);
response.setResult(false);
response.setMsg("通讯平台未返回响应内容");
return response;
}
if (!JSONUtil.isTypeJSON(responseBody)) {
response.setCode(httpStatus);
response.setResult(false);
response.setMsg("通讯平台返回内容不是合法JSON");
return response;
}
JSONObject jsonObject = JSONUtil.parseObj(responseBody);
response.setCode(jsonObject.getInt("code"));
response.setMsg(jsonObject.getStr("msg"));
response.setResult(jsonObject.getBool("result"));
response.setMsgId(jsonObject.getStr("msg_id"));
response.setRemainingQuota(jsonObject.getInt("remaining_quota"));
response.setDescription(jsonObject.getStr("description"));
return response;
}
/** /**
* 获取平台密钥。 * RFC3986 URL 编码,匹配阿里云 OpenAPI 签名要求。
* 优先读取已加密密钥;未配置时再根据原始密钥按“Base64 后 SHA”规则自动计算。 *
* * @param value 原始字符串
* @return 平台要求的 secret_key * @return 编码后的字符串
*/ */
private String getSecretKey() { private String percentEncode(String value) {
String encryptedSecretKey = conf.get("msg.platform.encrypted-secret-key", ""); return URLEncoder.encode(Strings.sNull(value), StandardCharsets.UTF_8)
if (Strings.isNotBlank(encryptedSecretKey)) { .replace("+", "%20")
return encryptedSecretKey; .replace("*", "%2A")
} .replace("%7E", "~");
String rawSecret = getRequiredConfig("msg.platform.raw-secret-key", "请先配置 msg.platform.raw-secret-key 或 msg.platform.encrypted-secret-key"); }
return shaHex(rawSecret);
}
/** /**
* 读取必填配置项。 * 解析阿里云短信响应。
* *
* @param key 配置 key * @param httpStatus HTTP 状态码
* @param errorMessage 配置缺失时抛出的异常信息 * @param responseBody 原始响应体
* @return 配置值 * @return 统一响应对象
*/ */
private String getRequiredConfig(String key, String errorMessage) { private MsgPlatformResponse parseAliyunResponse(int httpStatus, String responseBody) {
String value = conf.get(key, ""); MsgPlatformResponse response = new MsgPlatformResponse();
if (Strings.isBlank(value)) { response.setHttpStatus(httpStatus);
throw new BaseException(errorMessage); response.setRawBody(responseBody);
} if (Strings.isBlank(responseBody) || !JSONUtil.isTypeJSON(responseBody)) {
return value; response.setCode(httpStatus);
} response.setResult(false);
response.setMsg("阿里云短信返回内容为空或不是合法JSON");
return response;
}
/** JSONObject jsonObject = JSONUtil.parseObj(responseBody);
* 生成 SHA 十六进制摘要。 String code = jsonObject.getStr("Code");
* response.setCode("OK".equalsIgnoreCase(code) ? 0 : httpStatus);
* @param content 待加密内容 response.setResult("OK".equalsIgnoreCase(code));
* @return SHA 十六进制字符串 response.setMsg(jsonObject.getStr("Message"));
*/ response.setMsgId(jsonObject.getStr("BizId"));
private String shaHex(String content) { response.setDescription(code);
try { return response;
MessageDigest messageDigest = MessageDigest.getInstance("SHA"); }
messageDigest.update(content.getBytes());
byte[] hash = messageDigest.digest(); /**
return Base64.getEncoder().encodeToString(hash); * 读取 HTTP 响应内容。
} catch (Exception e) { *
throw new BaseException("生成通讯平台 secret_key 失败: " + e.getMessage()); * @param connection HTTP 连接
} * @param httpStatus HTTP 状态码
} * @return 响应体字符串
* @throws IOException 读取失败时抛出
*/
private String readResponseBody(HttpURLConnection connection, int httpStatus) throws IOException {
InputStream inputStream = httpStatus >= 200 && httpStatus < 400 ? connection.getInputStream() : connection.getErrorStream();
if (inputStream == null) {
return "";
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
StringBuilder builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
return builder.toString();
}
}
/**
* 读取必填配置。
*
* @param key 配置 key
* @param errorMessage 缺失时提示
* @return 配置值
*/
private String getRequiredConfig(String key, String errorMessage) {
String value = conf.get(key, "");
if (Strings.isBlank(value)) {
throw new BaseException(errorMessage);
}
return value;
}
/**
* 生成 SHA256 十六进制摘要。
*
* @param content 原始内容
* @return 十六进制摘要
*/
private String sha256Hex(String content) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return bytesToHex(digest.digest(content.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new BaseException("生成阿里云签名摘要失败:" + e.getMessage());
}
}
/**
* 生成 HMAC-SHA256 十六进制签名。
*
* @param content 签名原文
* @param secret AccessKey Secret
* @return 十六进制签名
*/
private String hmacSha256Hex(String content, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return bytesToHex(mac.doFinal(content.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new BaseException("生成阿里云短信签名失败:" + e.getMessage());
}
}
/**
* 字节数组转十六进制字符串。
*
* @param bytes 字节数组
* @return 十六进制字符串
*/
private String bytesToHex(byte[] bytes) {
StringBuilder builder = new StringBuilder(bytes.length * 2);
for (byte item : bytes) {
String hex = Integer.toHexString(item & 0xff);
if (hex.length() == 1) {
builder.append('0');
}
builder.append(hex);
}
return builder.toString();
}
} }
@@ -1,87 +0,0 @@
package com.budwk.app.base.sms.model;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 通讯平台公共请求参数。
*/
@Data
public class MsgPlatformBaseRequest {
/**
* 接收人信息原始字符串。
* 如果 controller 已经按平台要求拼好了 person_info,可以直接传该字段;
* 否则可只传 recipients,由 service 自动组装。
*/
private String personInfo;
/**
* 接收人列表。
* 当 personInfo 为空时,service 会根据该列表自动拼接平台要求的 person_info 字符串。
*/
private List<MsgPlatformRecipient> recipients;
/**
* 模板ID。
* templateId 和 info 至少需要传一个,使用模板发送时优先传该字段。
*/
private String templateId;
/**
* 消息内容。
* templateId 和 info 至少需要传一个;不使用模板时,需要传完整消息内容。
*/
private String info;
/**
* 发送优先级。
* 3 表示立即发送,4 表示定时发送。
*/
private String sendPriority = "3";
/**
* 发送人账号。
*/
private String sendUserId;
/**
* 发送人姓名。
*/
private String sendUserName;
/**
* 模板变量。
* 使用模板发送时可传该字段,service 会自动转成平台要求的 JSON 字符串。
*/
private Map<String, String> customVar;
/**
* 定时发送时间,格式必须为 yyyy-MM-dd HH:mm:ss。
* 当 sendPriority=4 时该字段必填。
*/
private String sendTime;
/**
* 发送机构ID。
*/
private String sendUnitId;
/**
* 发送机构名称。
*/
private String sendUnitName;
/**
* 发送签名。
* 旧通用模板或特定签名场景可传该字段。
*/
private String sendUserSign;
/**
* 回执ID。
* 不需要回执时可保持默认值 0。
*/
private String receiptId = "0";
}
@@ -1,29 +0,0 @@
package com.budwk.app.base.sms.model;
import lombok.Data;
/**
* 微信图文消息单条数据。
*/
@Data
public class MsgPlatformNewsItem {
/**
* 图文标题。
*/
private String title;
/**
* 图文描述。
*/
private String description;
/**
* 点击跳转链接。
*/
private String url;
/**
* 图文封面图片链接。
*/
private String picurl;
}
@@ -3,34 +3,34 @@ package com.budwk.app.base.sms.model;
import lombok.Data; import lombok.Data;
/** /**
* 通讯平台接收人信息。 * 短信接收人信息。
*/ */
@Data @Data
public class MsgPlatformRecipient { public class MsgPlatformRecipient {
/** /**
* 接收人姓名,对应 person_info 中的第 1 段。 * 接收人姓名,便于日志排查。
*/ */
private String name; private String name;
/** /**
* 接收人账号,对应 person_info 中的第 2 段。 * 接收人工号。
* 短信场景一般传学工号;微信场景可传企业微信成员ID、学工号或手机号。 * mobile 为空时,可用该字段按 sys_user.loginname 查询手机号。
*/ */
private String account; private String account;
/** /**
* 接收人部门ID,对应 person_info 中的第 3 段。 * 接收人部门ID,当前阿里云短信发送不使用,预留给调用方传递上下文。
*/ */
private String unitId; private String unitId;
/** /**
* 接收人部门名称,对应 person_info 中的第 4 段。 * 接收人部门名称,当前阿里云短信发送不使用,预留给调用方传递上下文。
*/ */
private String unitName; private String unitName;
/** /**
* 接收人手机号,对应短信 person_info 中的第 5 段。 * 接收人手机号。
* 微信接口不会使用该字段,微信接口会自动补空串以保留分隔符格式。 * 阿里云短信发送时优先使用该字段,必须为 11 位大陆手机号。
*/ */
private String mobile; private String mobile;
} }
@@ -3,7 +3,7 @@ package com.budwk.app.base.sms.model;
import lombok.Data; import lombok.Data;
/** /**
* 通讯平台响应结果。 * 短信发送响应结果。
*/ */
@Data @Data
public class MsgPlatformResponse { public class MsgPlatformResponse {
@@ -13,37 +13,37 @@ public class MsgPlatformResponse {
private Integer httpStatus; private Integer httpStatus;
/** /**
* 平台返回业务状态码,0 表示成功。 * 业务状态码,0 表示阿里云返回 OK。
*/ */
private Integer code; private Integer code;
/** /**
* 平台返回消息提示。 * 阿里云返回消息提示。
*/ */
private String msg; private String msg;
/** /**
* 平台返回业务是否成功。 * 是否发送成功。
*/ */
private Boolean result; private Boolean result;
/** /**
* 发送成功后的消息ID。 * 发送成功后的阿里云 BizId。
*/ */
private String msgId; private String msgId;
/** /**
* 短信剩余额度,仅短信接口返回。 * 短信剩余额度,当前阿里云接口不返回,预留兼容字段。
*/ */
private Integer remainingQuota; private Integer remainingQuota;
/** /**
* 平台失败描述。 * 阿里云返回 Code,失败时可用于定位错误类型。
*/ */
private String description; private String description;
/** /**
* 平台原始响应报文,排查问题时可直接查看。 * 阿里云原始响应报文,排查问题时可直接查看。
*/ */
private String rawBody; private String rawBody;
} }
@@ -1,7 +1,42 @@
package com.budwk.app.base.sms.model; package com.budwk.app.base.sms.model;
import lombok.Data;
import java.util.List;
import java.util.Map;
/** /**
* 短信发送请求参数。 * 阿里云短信发送请求参数。
*/ */
public class MsgPlatformSmsRequest extends MsgPlatformBaseRequest { @Data
public class MsgPlatformSmsRequest {
/**
* 接收人列表。
* mobile 为接收手机号;mobile 为空但 account 不为空时,会按工号查询 sys_user.mobile。
*/
private List<MsgPlatformRecipient> recipients;
/**
* 阿里云短信模板 Code。
* 为空时使用 sms.aliyun.template-code 配置。
*/
private String templateId;
/**
* 短信模板变量内容。
* customVar 为空时,会使用 sms.aliyun.template-param-name 作为变量名包装该内容。
*/
private String info;
/**
* 阿里云模板变量。
* key 为模板变量名,value 为变量内容,例如模板中有 ${content} 时传 content -> 具体通知内容。
*/
private Map<String, String> customVar;
/**
* 阿里云短信签名。
* 为空时使用 sms.aliyun.sign-name 配置。
*/
private String sendUserSign;
} }
@@ -1,25 +0,0 @@
package com.budwk.app.base.sms.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 微信发送请求参数。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MsgPlatformWechatRequest extends MsgPlatformBaseRequest {
/**
* 微信消息类型。
* 可传 text、news、image、file、video、voice,默认 text。
*/
private String wechatType = "text";
/**
* 图文消息列表。
* 当 wechatType=news 且未直接传 info 时,service 会将该列表转成平台要求的 JSON 数组字符串。
*/
private List<MsgPlatformNewsItem> newsItems;
}
@@ -58,8 +58,8 @@ public class ProcessTaskStartEventListener implements ProcessEventListener {
List<ProcessTaskActor> taskActors = dao.query(ProcessTaskActor.class, Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId())); List<ProcessTaskActor> taskActors = dao.query(ProcessTaskActor.class, Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId()));
for (ProcessTaskActor taskActor : taskActors) { for (ProcessTaskActor taskActor : taskActors) {
// 模拟发送消息 // 待办通知内容会同步进入本地消息和短信模板变量,需明确包含接收人、实例和处理环节。
String message = StrUtil.format("您有一条待办任务,实例:{},处理环节:{}", taskActor.getActorName(), instanceName, taskDisplayName); String message = StrUtil.format("{},您有一条待办任务,实例:{},处理环节:{}", taskActor.getActorName(), instanceName, taskDisplayName);
log.info(message); log.info(message);
globalMessageSendService.sendMessage(taskDisplayName, message, 2, List.of(taskActor.getActorId()), null); globalMessageSendService.sendMessage(taskDisplayName, message, 2, List.of(taskActor.getActorId()), null);
} }
@@ -6,7 +6,6 @@ import cn.hutool.http.HtmlUtil;
import com.budwk.app.base.constant.RedisConstant; import com.budwk.app.base.constant.RedisConstant;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.sms.SmsService; import com.budwk.app.base.sms.SmsService;
import com.budwk.app.base.sms.model.MsgPlatformWechatRequest;
import com.budwk.app.base.utils.ManyAddOrRenewUtil; import com.budwk.app.base.utils.ManyAddOrRenewUtil;
import com.budwk.app.sys.models.Sys_msg; import com.budwk.app.sys.models.Sys_msg;
import com.budwk.app.sys.models.Sys_msg_user; import com.budwk.app.sys.models.Sys_msg_user;
@@ -106,7 +105,6 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
ThreadUtil.execute(() -> { ThreadUtil.execute(() -> {
for (String loginName : loginNames) { for (String loginName : loginNames) {
smsService.sendSmsByAccount(loginName,sysMsg.getNote()); smsService.sendSmsByAccount(loginName,sysMsg.getNote());
smsService.sendWechatTextByAccountOrMobile(loginName,sysMsg.getNote());
} }
}); });
@@ -74,23 +74,23 @@ public class SmsMessageSendStrategy implements GlobalMessageSendStrategy {
/** /**
* 发送短信消息。 * 发送短信消息。
* 接收人由全局消息服务传入的用户ID或登录名查询得到,发送时使用用户工号对接通讯平台; * 接收人由全局消息服务传入的用户ID或登录名查询得到,发送时使用用户手机号对接阿里云短信;
* 返回结果由 SmsService 内部解析并记录,当前策略只负责逐个用户触发发送并记录异常。 * 单个用户手机号缺失或发送失败只记录日志,不影响其他待审核人接收短信。
*/ */
private void sendSmsMessage(String title, String content, Integer type, List<Sys_user> users, JSONObject config) { private void sendSmsMessage(String title, String content, Integer type, List<Sys_user> users, JSONObject config) {
for (Sys_user user : users) { for (Sys_user user : users) {
try { try {
String loginName = user.getLoginname(); String mobile = user.getMobile();
if (loginName == null || loginName.trim().isEmpty()) { if (mobile == null || mobile.trim().isEmpty()) {
log.warn("用户{}没有工号,跳过短信发送", user.getUsername()); log.warn("用户{}没有手机号,跳过短信发送", user.getUsername());
continue; continue;
} }
// 构建短信内容 // 构建短信内容
String smsContent = buildSmsContent(title, content, type); String smsContent = buildSmsContent(title, content, type);
smsService.sendSmsByAccount(loginName, smsContent); smsService.sendSmsByMobile(mobile, smsContent);
log.info("短信发送完成:用户{},工号:{}", user.getUsername(), loginName); log.info("短信发送完成:用户{},手机号:{}", user.getUsername(), mobile);
} catch (Exception e) { } catch (Exception e) {
log.error("发送短信给用户{}时出错:{}", user.getUsername(), e.getMessage(), e); log.error("发送短信给用户{}时出错:{}", user.getUsername(), e.getMessage(), e);
} }
@@ -103,10 +103,8 @@ public class SmsMessageSendStrategy implements GlobalMessageSendStrategy {
private String buildSmsContent(String title, String content, Integer type) { private String buildSmsContent(String title, String content, Integer type) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
// 根据消息类型添加前缀 // 待办短信由阿里云模板统一展示“待办提醒”,这里不再重复拼接【待办通知】前缀。
if (type != null && type == 2) { if (type == null || type != 2) {
sb.append("【待办通知】");
} else {
sb.append("【系统通知】"); sb.append("【系统通知】");
} }
@@ -1,113 +0,0 @@
package com.budwk.app.zhgh.dayofficework.message.strategy.impl;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.sms.SmsService;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.dayofficework.message.enums.GlobalMessageChannel;
import com.budwk.app.zhgh.dayofficework.message.strategy.GlobalMessageSendStrategy;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.List;
/**
* 微信发送策略
*/
@IocBean
@Slf4j
public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
@Inject
private Dao dao;
@Inject
private SmsService smsService;
@Override
public GlobalMessageChannel getChannel() {
return GlobalMessageChannel.WECHAT;
}
@Override
public boolean supportAsync() {
return true;
}
@Override
public void send(String title, String content, Integer type, List<String> receiverIds, JSONObject config) {
try {
log.info("开始发送微信消息,标题:{},接收人数量:{}", title, receiverIds.size());
// receiverIds 为 sys_user.id,按用户ID查询后使用工号对接通讯平台微信文本消息接口。
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where(Sys_user::getId, "in", receiverIds));
sendWechatMessage(title, content, type, users, config);
log.info("微信消息发送完成,标题:{},接收人数量:{}", title, users.size());
} catch (Exception e) {
log.error("微信消息发送失败,标题:{},错误:{}", title, e.getMessage(), e);
throw new RuntimeException("微信消息发送失败:" + e.getMessage(), e);
}
}
@Override
public void sendByLoginName(String title, String content, Integer type, List<String> receiverLoginNames, JSONObject config) {
try {
log.info("开始发送微信消息(按登录名),标题:{},接收人数量:{}", title, receiverLoginNames.size());
// receiverLoginNames 为 sys_user.loginname,查询用户后仍使用工号作为通讯平台接收人标识。
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where(Sys_user::getLoginname, "in", receiverLoginNames));
sendWechatMessage(title, content, type, users, config);
log.info("微信消息发送完成(按登录名),标题:{},接收人数量:{}", title, users.size());
} catch (Exception e) {
log.error("微信消息发送失败(按登录名),标题:{},错误:{}", title, e.getMessage(), e);
throw new RuntimeException("微信消息发送失败:" + e.getMessage(), e);
}
}
/**
* 发送微信文本消息。
* 接收人来自全局消息服务传入的用户ID或登录名,方法内部转换为 Sys_user 后取工号发送;
* SmsService 返回 MsgPlatformResponse,当前策略按单人发送记录日志,单个用户失败不影响其他接收人。
*/
private void sendWechatMessage(String title, String content, Integer type, List<Sys_user> users, JSONObject config) {
for (Sys_user user : users) {
try {
String loginName = user.getLoginname();
if (loginName == null || loginName.trim().isEmpty()) {
log.warn("用户{}没有工号,跳过微信发送", user.getUsername());
continue;
}
String wechatContent = buildWechatContent(title, content, type);
smsService.sendWechatTextByAccountOrMobile(loginName, wechatContent);
log.info("微信发送完成:用户{},工号:{}", user.getUsername(), loginName);
} catch (Exception e) {
log.error("发送微信给用户{}时出错:{}", user.getUsername(), e.getMessage(), e);
}
}
}
/**
* 构建微信文本内容。
* type=2 表示待办消息,其余类型按系统通知处理,内容格式保持与短信策略一致。
*/
private String buildWechatContent(String title, String content, Integer type) {
StringBuilder sb = new StringBuilder();
if (type != null && type == 2) {
sb.append("【待办通知】");
} else {
sb.append("【系统通知】");
}
sb.append(title);
if (content != null && !content.trim().isEmpty()) {
sb.append(":").append(content);
}
return sb.toString();
}
}