diff --git a/src/main/java/com/budwk/app/base/sms/SmsService.java b/src/main/java/com/budwk/app/base/sms/SmsService.java index e01131e..4a9f134 100644 --- a/src/main/java/com/budwk/app/base/sms/SmsService.java +++ b/src/main/java/com/budwk/app/base/sms/SmsService.java @@ -1,56 +1,26 @@ package com.budwk.app.base.sms; -import com.budwk.app.base.sms.model.MsgPlatformResponse; -import com.budwk.app.base.sms.model.MsgPlatformSmsRequest; -import com.budwk.app.base.sms.model.MsgPlatformWechatRequest; +import org.nutz.lang.util.NutMap; import java.util.List; +/** + * 学校消息中心发送服务。 + */ public interface SmsService { /** - * 发送短信消息。 + * 通过学校消息中心发送短信、微信、钉钉或组合消息。 * - * @param request 短信请求参数,需传接收人、发送人、模板ID或消息内容等字段 - * @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse} + * @param sendType 发送渠道:4短信、5微信、6钉钉,组合发送可传45、46、56或456 + * @param loginNameList 按工号发送的接收人列表,适用于微信、钉钉;使用receivers时可传null + * @param receivers 完整接收人列表,每项必须包含userId;包含短信渠道时还必须包含mobile + * @param title 消息标题;仅发送短信时可为空,包含微信或钉钉时必填 + * @param content 消息正文,必填 + * @param pcUrl PC端点击消息后的跳转地址,无跳转时可为空 + * @param mobileUrl 移动端点击消息后的跳转地址,无跳转时可为空 + * @return true表示所有批次均发送成功,false表示发送被关闭、环境不允许或第三方接口返回失败 */ - MsgPlatformResponse sendSms(MsgPlatformSmsRequest request); - - /** - * 通过工号发送短信文本消息的便捷方法。 - * controller 只需要传接收人工号,以及短信内容即可。 - * - * @param account 接收人工号 - * @param content 短信内容 - * @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse} - */ - MsgPlatformResponse sendSmsByAccount(String account, String content); - - /** - * 通过手机号发送短信文本消息的便捷方法。 - * controller 只需要传接收人手机号,以及短信内容即可。 - * - * @param mobile 接收人手机号 - * @param content 短信内容 - * @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse} - */ - 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); + boolean sendMsg(String sendType, List loginNameList, List receivers, + String title, String content, String pcUrl, String mobileUrl); } diff --git a/src/main/java/com/budwk/app/base/sms/impl/SmsServiceImpl.java b/src/main/java/com/budwk/app/base/sms/impl/SmsServiceImpl.java index 184a58d..a98c539 100644 --- a/src/main/java/com/budwk/app/base/sms/impl/SmsServiceImpl.java +++ b/src/main/java/com/budwk/app/base/sms/impl/SmsServiceImpl.java @@ -1,544 +1,305 @@ package com.budwk.app.base.sms.impl; +import cn.hutool.core.collection.ListUtil; 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.http.HttpUtil; 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.base.sms.model.*; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.base.Globals; +import com.budwk.app.zhgh.dayofficework.message.models.GlobalMessage; +import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageService; import lombok.extern.slf4j.Slf4j; -import org.nutz.integration.jedis.RedisService; -import org.nutz.ioc.impl.PropertiesProxy; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; 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.util.NutMap; -import org.nutz.log.Log; -import org.nutz.log.Logs; -import java.io.*; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; import java.util.ArrayList; -import java.util.Base64; +import java.util.Date; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; +import java.util.Set; /** - * @ClassName SmsJshvcServiceImpl - * @Author JyuHsin - * @Date 2025/11/26 19:32 - * @Version 1.0 - * @Description TODO + * 学校消息中心发送服务实现。 */ @Slf4j @IocBean 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 PropertiesProxy conf; + private static final String APP_ID = "72e16cd178767a77"; + private static final String ACCESS_TOKEN = "4fe0cc5172145a353f3f385a084fd900"; + private static final String TAG_ID = "1012"; + private static final String SCHOOL_CODE = "12036"; + private static final String MSG_API = "https://apis.zjvtit.edu.cn/mp_message_pocket_web-mp-restful-message-send/ProxyService/message_pocket_web-mp-restful-message-sendProxyService"; + private static final int BATCH_RECEIVER_COUNT = 450; + private static final Set SUPPORTED_SEND_TYPES = Set.of("4", "5", "6", "45", "46", "56", "456"); - /** - * 发送短信消息。 - * - * @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 GlobalMessageService globalMessageService; - /** - * 通过工号发送短信文本消息的便捷方法。 - * 该方法适合 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); - } + /** + * 发送学校消息中心消息,并将本次调用结果写入全局消息发送表。 + * + * @param sendType 发送渠道:4短信、5微信、6钉钉,组合发送可传45、46、56或456 + * @param loginNameList 按工号发送的接收人列表,适用于微信、钉钉;使用receivers时可传null + * @param receivers 完整接收人列表,每项必须包含userId;包含短信渠道时还必须包含mobile + * @param title 消息标题;仅发送短信时可为空,包含微信或钉钉时必填 + * @param content 消息正文,必填 + * @param pcUrl PC端跳转地址,可为空 + * @param mobileUrl 移动端跳转地址,可为空 + * @return true表示所有批次发送成功,否则返回false + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public boolean sendMsg(String sendType, List loginNameList, List receivers, + String title, String content, String pcUrl, String mobileUrl) { + List resultReceivers = buildAndValidateReceivers(sendType, loginNameList, receivers, title, content); + List batchResults = new ArrayList<>(); - /** - * 通过手机号发送短信文本消息的便捷方法。 - * 该方法适合 controller 里只传“手机号 + 内容”的简单场景,发送人信息从配置中读取。 - * - * @param mobile 接收人手机号 - * @param content 短信内容 - * @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse} - */ - @Override - public MsgPlatformResponse sendSmsByMobile(String mobile, String content) { - MsgPlatformSmsRequest request = new MsgPlatformSmsRequest(); - request.setInfo(content); - request.setRecipients(buildMobileRecipients(mobile)); - request.setCustomVar(Map.of("1","","2","","3",content)); - fillDefaultSenderInfo(request); - return sendSms(request); - } + if (!Globals.MyConfig.getBoolean("SendMsg", false)) { + String message = "消息发送配置未开启,已跳过实际发送"; + log.warn(message); + saveSendRecord(sendType, title, content, pcUrl, mobileUrl, resultReceivers.size(), false, + batchResults, message); + return false; + } + if (!Globals.sso) { + String message = "开发模式不允许发送学校消息中心通知"; + log.warn(message); + saveSendRecord(sendType, title, content, pcUrl, mobileUrl, resultReceivers.size(), false, + batchResults, message); + return false; + } - /** - * 发送微信消息。 - * - * @param request 微信请求参数,需传接收人、发送人、消息类型、模板ID或消息内容等字段 - * @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); - } + boolean sendSuccess = true; + String sendMessage = "发送成功"; + List> receiverBatches = ListUtil.split(resultReceivers, BATCH_RECEIVER_COUNT); + for (int i = 0; i < receiverBatches.size(); i++) { + NutMap batchResult = sendBatch(sendType, receiverBatches.get(i), title, content, pcUrl, mobileUrl, i + 1); + batchResults.add(batchResult); + if (!batchResult.getBoolean("success", false)) { + sendSuccess = false; + sendMessage = batchResult.getString("msg"); + break; + } + } - /** - * 发送微信文本消息的便捷方法。 - * 该方法适合 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); - } + saveSendRecord(sendType, title, content, pcUrl, mobileUrl, resultReceivers.size(), sendSuccess, + batchResults, sendMessage); + return sendSuccess; + } - /** - * 组装平台公共请求体。 - * - * @param request 平台请求参数 - * @param includeMobile 是否在 person_info 第 5 段写入手机号 - * @return 返回发送给第三方平台的 JSON 对象 - */ - private JSONObject buildCommonPayload(MsgPlatformBaseRequest request, boolean includeMobile) { - JSONObject payload = new JSONObject(); - payload.set("tp_name", getRequiredConfig("msg.platform.tp-name", "请先配置 msg.platform.tp-name")); - payload.set("secret_key", getSecretKey()); - payload.set("person_info", resolvePersonInfo(request, includeMobile)); - payload.set("template_id", Strings.sNull(request.getTemplateId())); - payload.set("info", Strings.sNull(request.getInfo())); - payload.set("send_priority", Strings.isBlank(request.getSendPriority()) ? "3" : request.getSendPriority()); - payload.set("send_user_id", Strings.sNull(request.getSendUserId())); - payload.set("send_user_name", Strings.sNull(request.getSendUserName())); - payload.set("send_time", Strings.sNull(request.getSendTime())); - payload.set("send_unit_id", Strings.sNull(request.getSendUnitId())); - payload.set("send_unit_name", Strings.sNull(request.getSendUnitName())); - payload.set("send_user_sign", Strings.sNull(request.getSendUserSign())); - 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; - } + /** + * 校验渠道相关必填项并构建接收人。 + * 包含短信渠道时手机号必填;包含微信或钉钉时标题必填;所有渠道都要求正文和userId。 + * + * @param sendType 发送渠道组合 + * @param loginNameList 工号列表 + * @param receivers 完整接收人列表 + * @param title 消息标题 + * @param content 消息正文 + * @return 可直接提交给第三方平台的接收人列表 + */ + private List buildAndValidateReceivers(String sendType, List loginNameList, + List receivers, String title, String content) { + if (!SUPPORTED_SEND_TYPES.contains(sendType)) { + throw new BaseException("sendType只允许4、5、6、45、46、56或456"); + } + if (Strings.isBlank(content)) { + throw new BaseException("消息内容不能为空"); + } + if ((sendType.contains("5") || sendType.contains("6")) && Strings.isBlank(title)) { + throw new BaseException("发送微信或钉钉消息时标题不能为空"); + } + if (Lang.isEmpty(receivers) && Lang.isEmpty(loginNameList)) { + throw new BaseException("消息接收人不能为空"); + } - /** - * 校验公共请求参数,避免 controller 传错参数后才到第三方接口报错。 - * - * @param request 平台请求参数 - */ - private void validateCommonRequest(MsgPlatformBaseRequest request) { - if (request == null) { - throw new BaseException("请求参数不能为空"); - } - if (Strings.isBlank(request.getPersonInfo()) && (request.getRecipients() == null || request.getRecipients().isEmpty())) { - throw new BaseException("personInfo 和 recipients 不能同时为空"); - } - if (Strings.isBlank(request.getTemplateId()) && Strings.isBlank(request.getInfo())) { - throw new BaseException("templateId 和 info 至少需要传一个"); - } - 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()); - } + List result = new ArrayList<>(); + if (Lang.isNotEmpty(receivers)) { + for (NutMap receiver : receivers) { + if (receiver == null) { + throw new BaseException("接收人信息中存在空对象"); + } + NutMap item = NutMap.NEW(); + item.putAll(receiver); + item.put("email", Strings.sNull(item.getString("email"))); + item.put("flag", item.get("flag") == null ? 0 : item.get("flag")); + validateReceiver(sendType, item); + result.add(item); + } + } else { + for (String loginName : loginNameList) { + NutMap item = NutMap.NEW() + .addv("userId", loginName) + .addv("mobile", "") + .addv("email", "") + .addv("flag", 0); + validateReceiver(sendType, item); + result.add(item); + } + } + return result; + } - /** - * 解析 person_info。 - * 已传原始 personInfo 时直接使用;否则根据 recipients 自动拼接平台要求的格式。 - * - * @param request 平台请求参数 - * @param includeMobile 是否在第 5 段写入手机号 - * @return 平台要求的 person_info 字符串 - */ - private String resolvePersonInfo(MsgPlatformBaseRequest request, boolean includeMobile) { - if (Strings.isNotBlank(request.getPersonInfo())) { - return request.getPersonInfo(); - } - List recipients = request.getRecipients(); - if (recipients == null || recipients.isEmpty()) { - throw new BaseException("接收人列表不能为空"); - } - return recipients.stream().map(recipient -> buildReceiverLine(recipient, includeMobile)).collect(Collectors.joining("^@^")); - } + /** + * 按发送渠道校验单个接收人的必填字段。 + * + * @param sendType 发送渠道组合 + * @param receiver 接收人,userId始终必填,sendType包含4时mobile必填 + */ + private void validateReceiver(String sendType, NutMap receiver) { + if (StrUtil.isBlank(receiver.getString("userId"))) { + throw new BaseException("接收人的userId不能为空"); + } + if (sendType.contains("4") && StrUtil.isBlank(receiver.getString("mobile"))) { + throw new BaseException("发送短信时接收人的mobile不能为空,userId=" + receiver.getString("userId")); + } + } - /** - * 将单个接收人转成平台要求的分隔符格式。 - * - * @param recipient 接收人对象 - * @param includeMobile 是否在最后一段补手机号 - * @return 单个接收人的 person_info 片段 - */ - private String buildReceiverLine(MsgPlatformRecipient recipient, boolean includeMobile) { - if (recipient == null) { - throw new BaseException("接收人信息中存在空对象"); - } - List segments = new ArrayList<>(5); - segments.add(Strings.sNull(recipient.getName())); - segments.add(Strings.sNull(recipient.getAccount())); - segments.add(Strings.sNull(recipient.getUnitId())); - segments.add(Strings.sNull(recipient.getUnitName())); - if (includeMobile) { - segments.add(resolveSmsMobile(recipient)); - } else { - segments.add(""); - } - return String.join("|", segments); - } + /** + * 发送单个接收人批次。 + * + * @param sendType 发送渠道组合 + * @param receivers 当前批次接收人,最多450人 + * @param title 消息标题 + * @param content 消息正文 + * @param pcUrl PC端跳转地址 + * @param mobileUrl 移动端跳转地址 + * @param batchNo 当前批次序号 + * @return 批次发送结果,包含batchNo、success、status、msg和rawBody + */ + private NutMap sendBatch(String sendType, List receivers, String title, String content, + String pcUrl, String mobileUrl, int batchNo) { + NutMap result = NutMap.NEW().addv("batchNo", batchNo).addv("success", false); + try { + String userId = receivers.get(0).getString("userId"); + NutMap payload = NutMap.NEW() + .addv("appId", APP_ID) + .addv("sign", buildSign(userId)) + .addv("schoolCode", SCHOOL_CODE) + .addv("subject", "4".equals(sendType) ? "" : title) + .addv("content", content) + .addv("pcUrl", pcUrl) + .addv("mobileUrl", mobileUrl) + .addv("sendType", sendType) + .addv("sendNow", 1) + .addv("tagId", TAG_ID) + .addv("receivers", receivers); + if (sendType.contains("5")) { + payload.put("wxSendType", "text"); + } + if (sendType.contains("6")) { + payload.put("dingSendType", "text"); + } - /** - * 解析短信接收人手机号。 - * 如果 controller 传的是手机号,则校验手机号格式; - * 如果传的是工号,则允许手机号为空,按“姓名|工号|部门ID|部门名称|”格式发送。 - * - * @param recipient 接收人信息 - * @return 短信 person_info 第 5 段内容 - */ - private String resolveSmsMobile(MsgPlatformRecipient recipient) { - String mobile = recipient.getMobile(); - if (Strings.isBlank(mobile)) { - if (Strings.isBlank(recipient.getAccount())) { - throw new BaseException("短信接收人手机号和工号不能同时为空"); - } - return ""; - } - if (!mobile.matches("^\\d{11}$")) { - throw new BaseException("短信接收人手机号必须为11位数字"); - } - return mobile; - } + HttpRequest request = HttpUtil.createPost(MSG_API) + .header("appId", APP_ID) + .header("accessToken", ACCESS_TOKEN) + .header("Content-Type", "application/json; charset=UTF-8") + .body(JSONUtil.toJsonStr(payload)); + try (HttpResponse response = request.execute()) { + String responseBody = response.body(); + result.put("httpStatus", response.getStatus()); + result.put("rawBody", responseBody); + if (response.getStatus() < 200 || response.getStatus() >= 300) { + result.put("msg", "消息中心HTTP调用失败,状态码:" + response.getStatus()); + return result; + } + if (!JSONUtil.isTypeJSON(responseBody)) { + result.put("msg", "消息中心返回内容不是合法JSON"); + return result; + } + JSONObject responseJson = JSONUtil.parseObj(responseBody); + Integer status = responseJson.getInt("status"); + String message = responseJson.getStr("msg"); + result.put("status", status); + result.put("msg", message); + result.put("success", Integer.valueOf(200).equals(status)); + return result; + } + } catch (Exception e) { + log.error("学校消息中心第{}批发送异常", batchNo, e); + result.put("msg", "消息中心调用异常:" + e.getMessage()); + return result; + } + } - /** - * 校验接收人数量,避免超过第三方平台单次上限。 - * - * @param personInfo 原始 person_info - * @param recipients 接收人列表 - */ - private void validateReceiverCount(String personInfo, List recipients) { - int receiverCount; - if (Strings.isNotBlank(personInfo)) { - receiverCount = personInfo.split("\\^@\\^", -1).length; - } else { - receiverCount = recipients == null ? 0 : recipients.size(); - } - if (receiverCount > MAX_RECEIVER_COUNT) { - throw new BaseException("单次发送人数不能超过" + MAX_RECEIVER_COUNT + "人"); - } - } + /** + * 生成消息中心请求签名。 + * + * @param userId 当前批次第一个接收人的工号 + * @return accessToken、schoolCode、userId拼接后的32位小写MD5值 + */ + private String buildSign(String userId) { + return DigestUtil.md5Hex(ACCESS_TOKEN + SCHOOL_CODE + userId); + } - /** - * 构建工号发送专用接收人列表。 - * - * @param account controller 传入的工号 - * @return 仅包含一个接收人的列表 - */ - private List buildAccountRecipients(String account) { - if (Strings.isBlank(account)) { - throw new BaseException("接收人工号不能为空"); - } - MsgPlatformRecipient recipient = new MsgPlatformRecipient(); - recipient.setAccount(account); - List recipients = new ArrayList<>(1); - recipients.add(recipient); - return recipients; - } + /** + * 将一次逻辑发送的最终结果写入global_message发送表。 + * + * @param sendType 发送渠道组合 + * @param title 消息标题 + * @param content 消息正文 + * @param pcUrl PC端跳转地址 + * @param mobileUrl 移动端跳转地址 + * @param receiverCount 接收人数量 + * @param success 是否全部发送成功 + * @param batchResults 各批次第三方响应 + * @param message 最终结果说明 + */ + private void saveSendRecord(String sendType, String title, String content, String pcUrl, String mobileUrl, + int receiverCount, boolean success, List batchResults, String message) { + GlobalMessage record = new GlobalMessage(); + record.setTitle(Strings.isBlank(title) ? "短信通知" : title); + record.setContent(content); + record.setType(1); + fillSenderInfo(record); + record.setStatus(success ? 2 : 1); + record.setSendTime(new Date()); + record.setSendSuccess(success); - /** - * 构建手机号发送专用接收人列表。 - * - * @param mobile controller 传入的手机号 - * @return 仅包含一个接收人的列表 - */ - private List buildMobileRecipients(String mobile) { - if (Strings.isBlank(mobile)) { - throw new BaseException("接收人手机号不能为空"); - } - if (!mobile.matches("^\\d{11}$")) { - throw new BaseException("接收人手机号必须为11位数字"); - } - MsgPlatformRecipient recipient = new MsgPlatformRecipient(); - recipient.setMobile(mobile); - List recipients = new ArrayList<>(1); - recipients.add(recipient); - return recipients; - } + JSONObject ext = new JSONObject(); + ext.set("sendType", sendType); + ext.set("pcUrl", Strings.sNull(pcUrl)); + ext.set("mobileUrl", Strings.sNull(mobileUrl)); + ext.set("receiverCount", receiverCount); + record.setExt(ext); - /** - * 构建微信便捷发送专用接收人列表。 - * - * @param accountOrMobile controller 传入的企业微信成员ID、工号或手机号 - * @return 仅包含一个接收人的列表 - */ - private List buildWechatRecipients(String accountOrMobile) { - if (Strings.isBlank(accountOrMobile)) { - throw new BaseException("微信接收人工号或手机号不能为空"); - } - MsgPlatformRecipient recipient = new MsgPlatformRecipient(); - recipient.setAccount(accountOrMobile); - List recipients = new ArrayList<>(1); - recipients.add(recipient); - return recipients; - } - - /** - * 为便捷发送方法补默认发送人信息。 - * 这些字段从配置中读取,避免 controller 每次重复传固定参数。 - * - * @param request 平台请求参数 - */ - private void fillDefaultSenderInfo(MsgPlatformBaseRequest request) { - request.setSendUserId(getRequiredConfig("msg.platform.default-send-user-id", "请先配置 msg.platform.default-send-user-id")); - 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", "")); - } - - /** - * 调用地大通讯平台 REST 接口。 - * - * @param apiPath 具体接口路径 - * @param payload 发送报文 - * @return 平台响应结果 - */ - private MsgPlatformResponse doPost(String apiPath, JSONObject payload) { - - if (!Globals.sso){ - 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 (!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。 - * - * @param apiPath 接口路径 - * @return 完整请求地址 - */ - private String buildRequestUrl(String apiPath) { - String baseUrl = getRequiredConfig("msg.platform.base-url", "请先配置 msg.platform.base-url"); - if (baseUrl.endsWith("/")) { - return baseUrl.substring(0, baseUrl.length() - 1) + apiPath; - } - return baseUrl + apiPath; - } - - /** - * 读取第三方接口响应报文。 - * - * @param connection HttpURLConnection - * @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 httpStatus HTTP 状态码 - * @param responseBody 原始响应字符串 - * @return 解析后的平台响应对象 - */ - private MsgPlatformResponse parseResponse(int httpStatus, String responseBody) { - MsgPlatformResponse response = new MsgPlatformResponse(); - response.setHttpStatus(httpStatus); - response.setRawBody(responseBody); - 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; - } - - /** - * 获取平台密钥。 - * 优先读取已加密密钥;未配置时再根据原始密钥按“Base64 后 SHA”规则自动计算。 - * - * @return 平台要求的 secret_key - */ - private String getSecretKey() { - String encryptedSecretKey = conf.get("msg.platform.encrypted-secret-key", ""); - if (Strings.isNotBlank(encryptedSecretKey)) { - return encryptedSecretKey; - } - 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 errorMessage 配置缺失时抛出的异常信息 - * @return 配置值 - */ - private String getRequiredConfig(String key, String errorMessage) { - String value = conf.get(key, ""); - if (Strings.isBlank(value)) { - throw new BaseException(errorMessage); - } - return value; - } - - /** - * 生成 SHA 十六进制摘要。 - * - * @param content 待加密内容 - * @return SHA 十六进制字符串 - */ - private String shaHex(String content) { - try { - MessageDigest messageDigest = MessageDigest.getInstance("SHA"); - messageDigest.update(content.getBytes()); - byte[] hash = messageDigest.digest(); - return Base64.getEncoder().encodeToString(hash); - } catch (Exception e) { - throw new BaseException("生成通讯平台 secret_key 失败: " + e.getMessage()); - } - } + JSONObject sendResult = new JSONObject(); + sendResult.set("success", success); + sendResult.set("msg", Strings.sNull(message)); + sendResult.set("batches", batchResults); + record.setSendResult(JSONUtil.toJsonStr(sendResult)); + globalMessageService.insert(record); + } + /** + * 补充发送记录的操作人信息;异步线程无法获取登录信息时使用system。 + * + * @param record 待保存的发送记录 + */ + private void fillSenderInfo(GlobalMessage record) { + try { + String senderId = SecurityUtil.getUserId(); + String senderName = SecurityUtil.getUserUsername(); + String senderLoginName = SecurityUtil.getUserLoginname(); + record.setSenderId(Strings.isBlank(senderId) ? "system" : senderId); + record.setSenderName(Strings.isBlank(senderName) ? "系统发送" : senderName); + record.setSenderLoginName(Strings.isBlank(senderLoginName) ? "system" : senderLoginName); + } catch (Exception e) { + record.setSenderId("system"); + record.setSenderName("系统发送"); + record.setSenderLoginName("system"); + } + } } diff --git a/src/main/java/com/budwk/app/base/sms/model/MsgPlatformBaseRequest.java b/src/main/java/com/budwk/app/base/sms/model/MsgPlatformBaseRequest.java deleted file mode 100644 index d7ecb8d..0000000 --- a/src/main/java/com/budwk/app/base/sms/model/MsgPlatformBaseRequest.java +++ /dev/null @@ -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 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 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"; -} diff --git a/src/main/java/com/budwk/app/base/sms/model/MsgPlatformNewsItem.java b/src/main/java/com/budwk/app/base/sms/model/MsgPlatformNewsItem.java deleted file mode 100644 index 29d103a..0000000 --- a/src/main/java/com/budwk/app/base/sms/model/MsgPlatformNewsItem.java +++ /dev/null @@ -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; -} diff --git a/src/main/java/com/budwk/app/base/sms/model/MsgPlatformRecipient.java b/src/main/java/com/budwk/app/base/sms/model/MsgPlatformRecipient.java deleted file mode 100644 index e12214b..0000000 --- a/src/main/java/com/budwk/app/base/sms/model/MsgPlatformRecipient.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.budwk.app.base.sms.model; - -import lombok.Data; - -/** - * 通讯平台接收人信息。 - */ -@Data -public class MsgPlatformRecipient { - /** - * 接收人姓名,对应 person_info 中的第 1 段。 - */ - private String name; - - /** - * 接收人账号,对应 person_info 中的第 2 段。 - * 短信场景一般传学工号;微信场景可传企业微信成员ID、学工号或手机号。 - */ - private String account; - - /** - * 接收人部门ID,对应 person_info 中的第 3 段。 - */ - private String unitId; - - /** - * 接收人部门名称,对应 person_info 中的第 4 段。 - */ - private String unitName; - - /** - * 接收人手机号,对应短信 person_info 中的第 5 段。 - * 微信接口不会使用该字段,微信接口会自动补空串以保留分隔符格式。 - */ - private String mobile; -} diff --git a/src/main/java/com/budwk/app/base/sms/model/MsgPlatformResponse.java b/src/main/java/com/budwk/app/base/sms/model/MsgPlatformResponse.java deleted file mode 100644 index 2a5f8e2..0000000 --- a/src/main/java/com/budwk/app/base/sms/model/MsgPlatformResponse.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.budwk.app.base.sms.model; - -import lombok.Data; - -/** - * 通讯平台响应结果。 - */ -@Data -public class MsgPlatformResponse { - /** - * HTTP 状态码,便于 controller 定位网络层问题。 - */ - private Integer httpStatus; - - /** - * 平台返回业务状态码,0 表示成功。 - */ - private Integer code; - - /** - * 平台返回消息提示。 - */ - private String msg; - - /** - * 平台返回业务是否成功。 - */ - private Boolean result; - - /** - * 发送成功后的消息ID。 - */ - private String msgId; - - /** - * 短信剩余额度,仅短信接口返回。 - */ - private Integer remainingQuota; - - /** - * 平台失败描述。 - */ - private String description; - - /** - * 平台原始响应报文,排查问题时可直接查看。 - */ - private String rawBody; -} diff --git a/src/main/java/com/budwk/app/base/sms/model/MsgPlatformSmsRequest.java b/src/main/java/com/budwk/app/base/sms/model/MsgPlatformSmsRequest.java deleted file mode 100644 index 8f086da..0000000 --- a/src/main/java/com/budwk/app/base/sms/model/MsgPlatformSmsRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.budwk.app.base.sms.model; - -/** - * 短信发送请求参数。 - */ -public class MsgPlatformSmsRequest extends MsgPlatformBaseRequest { -} diff --git a/src/main/java/com/budwk/app/base/sms/model/MsgPlatformWechatRequest.java b/src/main/java/com/budwk/app/base/sms/model/MsgPlatformWechatRequest.java deleted file mode 100644 index a6ccbff..0000000 --- a/src/main/java/com/budwk/app/base/sms/model/MsgPlatformWechatRequest.java +++ /dev/null @@ -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 newsItems; -} diff --git a/src/main/java/com/budwk/app/sys/services/impl/SysMsgServiceImpl.java b/src/main/java/com/budwk/app/sys/services/impl/SysMsgServiceImpl.java index 0b0ac40..7db76ab 100644 --- a/src/main/java/com/budwk/app/sys/services/impl/SysMsgServiceImpl.java +++ b/src/main/java/com/budwk/app/sys/services/impl/SysMsgServiceImpl.java @@ -6,7 +6,6 @@ import cn.hutool.http.HtmlUtil; import com.budwk.app.base.constant.RedisConstant; import com.budwk.app.base.service.impl.BaseServiceImpl; 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.sys.models.Sys_msg; import com.budwk.app.sys.models.Sys_msg_user; @@ -72,7 +71,7 @@ public class SysMsgServiceImpl extends BaseServiceImpl implements SysMs Cnd cnd = Cnd.NEW(); cnd.groupBy("loginname"); - FieldFilter fieldFilter = FieldFilter.create(View_user.class, "loginname|username|unitId|unitName|unionId|unionName"); + FieldFilter fieldFilter = FieldFilter.create(View_user.class, "loginname|username|mobile|unitId|unitName|unionId|unionName"); if ("user".equals(sysMsg.getType())) { cnd.and(View_user::getLoginname, "in", users); } @@ -104,11 +103,24 @@ public class SysMsgServiceImpl extends BaseServiceImpl implements SysMs //发送学校平台消息 ThreadUtil.execute(() -> { - for (String loginName : loginNames) { - smsService.sendSmsByAccount(loginName,sysMsg.getNote()); - smsService.sendWechatTextByAccountOrMobile(loginName,sysMsg.getNote()); + // 46表示短信和钉钉组合发送;按照消息中心要求,组合中包含短信时手机号必填。 + List receivers = viewUsers.stream() + .filter(user -> StrUtil.isAllNotBlank(user.getLoginname(), user.getMobile())) + .map(user -> NutMap.NEW() + .addv("userId", user.getLoginname()) + .addv("mobile", user.getMobile()) + .addv("email", "") + .addv("flag", 0)) + .toList(); + if (receivers.isEmpty()) { + log.warn("学校平台消息没有符合要求的接收人,已跳过发送"); + return; + } + boolean success = smsService.sendMsg("46", null, receivers, sysMsg.getTitle(), sysMsg.getNote(), + sysMsg.getUrl(), sysMsg.getUrl()); + if (!success) { + log.warnf("学校平台短信和钉钉组合消息发送失败,消息ID:%s", sysMsg.getId()); } - }); return sysMsg; diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/birthdayWishes/task/BirthdayWishesTask.java b/src/main/java/com/budwk/app/zhgh/dayofficework/birthdayWishes/task/BirthdayWishesTask.java index d6d6871..4189fb4 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/birthdayWishes/task/BirthdayWishesTask.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/birthdayWishes/task/BirthdayWishesTask.java @@ -1,13 +1,10 @@ package com.budwk.app.zhgh.dayofficework.birthdayWishes.task; -import cn.hutool.core.date.DateUtil; import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayService; import lombok.extern.slf4j.Slf4j; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; -import org.nutz.json.Json; import org.quartz.Job; -import org.quartz.JobDataMap; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; @@ -22,16 +19,8 @@ public class BirthdayWishesTask implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { log.info("=================================执行计划任务:{}", context.getJobDetail().getKey().getName()); - JobDataMap dataMap = context.getJobDetail().getJobDataMap(); - log.info("=================================参数:{}", Json.toJson(dataMap)); - - String planName = dataMap.getString("name"); - String today = DateUtil.today(); - String title = planName + today; - String content = dataMap.getString("template"); - - // 统一由生日服务查询当天生日会员、生成移动端链接并记录系统推送日志。 - int receiverCount = userBirthdayService.sendTodayBirthdayMessages(title, content); + // 统一由生日配置控制发送开关和消息文案,任务本身只负责触发。 + int receiverCount = userBirthdayService.sendTodayBirthdayMessages(); log.info("=================================当天生日通知进入发送队列人数:{}", receiverCount); } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/message/strategy/impl/SmsMessageSendStrategy.java b/src/main/java/com/budwk/app/zhgh/dayofficework/message/strategy/impl/SmsMessageSendStrategy.java index dee3355..3bb29c6 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/message/strategy/impl/SmsMessageSendStrategy.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/message/strategy/impl/SmsMessageSendStrategy.java @@ -10,7 +10,9 @@ 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 org.nutz.lang.util.NutMap; +import java.util.ArrayList; import java.util.List; /** @@ -74,26 +76,31 @@ public class SmsMessageSendStrategy implements GlobalMessageSendStrategy { /** * 发送短信消息。 - * 接收人由全局消息服务传入的用户ID或登录名查询得到,发送时使用用户工号对接通讯平台; - * 返回结果由 SmsService 内部解析并记录,当前策略只负责逐个用户触发发送并记录异常。 + * 接收人由全局消息服务传入的用户ID或登录名查询得到;消息中心短信渠道要求工号和手机号同时存在。 */ private void sendSmsMessage(String title, String content, Integer type, List users, JSONObject config) { + List receivers = new ArrayList<>(); for (Sys_user user : users) { - try { - String loginName = user.getLoginname(); - if (loginName == null || loginName.trim().isEmpty()) { - log.warn("用户{}没有工号,跳过短信发送", user.getUsername()); - continue; - } - - // 构建短信内容 - String smsContent = buildSmsContent(title, content, type); - - smsService.sendSmsByAccount(loginName, smsContent); - log.info("短信发送完成:用户{},工号:{}", user.getUsername(), loginName); - } catch (Exception e) { - log.error("发送短信给用户{}时出错:{}", user.getUsername(), e.getMessage(), e); + if (user.getLoginname() == null || user.getLoginname().trim().isEmpty() + || user.getMobile() == null || user.getMobile().trim().isEmpty()) { + log.warn("用户{}缺少工号或手机号,跳过短信发送", user.getUsername()); + continue; } + receivers.add(NutMap.NEW() + .addv("userId", user.getLoginname()) + .addv("mobile", user.getMobile()) + .addv("email", "") + .addv("flag", 0)); + } + if (receivers.isEmpty()) { + log.warn("短信消息没有符合要求的接收人,已跳过发送"); + return; + } + + String smsContent = buildSmsContent(title, content, type); + boolean success = smsService.sendMsg("4", null, receivers, null, smsContent, null, null); + if (!success) { + throw new RuntimeException("学校消息中心短信发送失败"); } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/message/strategy/impl/WechatMessageSendStrategy.java b/src/main/java/com/budwk/app/zhgh/dayofficework/message/strategy/impl/WechatMessageSendStrategy.java index a4b0e19..b59a349 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/message/strategy/impl/WechatMessageSendStrategy.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/message/strategy/impl/WechatMessageSendStrategy.java @@ -70,23 +70,23 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy { /** * 发送微信文本消息。 * 接收人来自全局消息服务传入的用户ID或登录名,方法内部转换为 Sys_user 后取工号发送; - * SmsService 返回 MsgPlatformResponse,当前策略按单人发送记录日志,单个用户失败不影响其他接收人。 + * 接收人统一交给SmsService分批发送并记录第三方调用结果。 */ private void sendWechatMessage(String title, String content, Integer type, List users, JSONObject config) { - for (Sys_user user : users) { - try { - String loginName = user.getLoginname(); - if (loginName == null || loginName.trim().isEmpty()) { - log.warn("用户{}没有工号,跳过微信发送", user.getUsername()); - continue; - } + List loginNames = users.stream() + .map(Sys_user::getLoginname) + .filter(loginName -> loginName != null && !loginName.trim().isEmpty()) + .distinct() + .toList(); + if (loginNames.isEmpty()) { + log.warn("微信消息没有符合要求的接收人,已跳过发送"); + return; + } - 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); - } + String wechatContent = buildWechatContent(title, content, type); + boolean success = smsService.sendMsg("5", loginNames, null, title, wechatContent, null, null); + if (!success) { + throw new RuntimeException("学校消息中心微信发送失败"); } } diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/controller/UserBirthdayConfigController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/controller/UserBirthdayConfigController.java new file mode 100644 index 0000000..fc4cb62 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/controller/UserBirthdayConfigController.java @@ -0,0 +1,71 @@ +package com.budwk.app.zhgh.staffmanage.birthday.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.result.Result; +import com.budwk.app.zhgh.staffmanage.birthday.models.UserBirthdayConfig; +import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +/** + * 生日祝福配置管理。 + * 配置内容包含页面图片、默认查询天数、自动发送开关和发送文案。 + */ +@IocBean +@At("/platform/staffManage/birthday/config") +@Ok("json:full") +@Api(tags = "生日祝福配置") +public class UserBirthdayConfigController { + + @Inject + private UserBirthdayService userBirthdayService; + + @At("") + @Ok("beetl:/platform/zhgh/staffmanage/birthday/config/index.html") + @SaCheckPermission("staff.birthday.config") + public void index() { + } + + /** + * 获取当前生日祝福配置。 + * + * @return Result.data 为 UserBirthdayConfig,包含图片文件ID、查询天数、自动发送开关、标题和内容 + */ + @At + @ApiOperation("获取生日祝福配置") + @SaCheckPermission("staff.birthday.config") + public Result getConfig() { + return Result.success(userBirthdayService.getConfig()); + } + + /** + * 保存生日祝福配置。 + * config.picUrl、birthdayUrl 为图片文件ID;birthdayDays 为正整数; + * autoSendEnabled 为自动发送开关;sendTitle、sendContent 为默认发送文案。 + * + * @return Result.data 为保存后的 UserBirthdayConfig + */ + @At + @Aop(TransAop.READ_COMMITTED) + @ApiOperation("保存生日祝福配置") + @SaCheckPermission("staff.birthday.config") + public Result saveConfig(UserBirthdayConfig config) { + if (config == null || config.getBirthdayDays() == null || config.getBirthdayDays() <= 0) { + return Result.error("默认查询天数必须为正整数"); + } + if (StrUtil.isBlank(config.getSendTitle())) { + return Result.error("请输入发送标题"); + } + if (StrUtil.isBlank(config.getSendContent())) { + return Result.error("请输入发送内容"); + } + return Result.success("保存成功", userBirthdayService.saveConfig(config)); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/controller/UserBirthdayManageController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/controller/UserBirthdayManageController.java index f745412..6942d2b 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/controller/UserBirthdayManageController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/controller/UserBirthdayManageController.java @@ -5,7 +5,6 @@ import cn.dev33.satoken.annotation.SaCheckPermission; import cn.hutool.core.util.StrUtil; import com.budwk.app.base.result.Result; import com.budwk.app.web.commons.auth.utils.SecurityUtil; -import com.budwk.app.zhgh.staffmanage.birthday.models.UserBirthdayConfig; import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdayPageForm; import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdaySendMsgForm; import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayService; @@ -118,18 +117,6 @@ public class UserBirthdayManageController { return Result.success(userBirthdayService.getConfig()); } - /** - * 保存生日页面图片配置。 - * config.picUrl 为背景图片文件ID,config.birthdayUrl 为福利贺卡文件ID;返回保存后的配置对象。 - */ - @At - @Aop(TransAop.READ_COMMITTED) - @ApiOperation("保存生日页面配置") - @SaCheckPermission("staff.birthday.manage") - public Result saveOrModifyConfig(UserBirthdayConfig config) { - return Result.success("保存成功", userBirthdayService.saveConfig(config)); - } - private Result validateMessage(UserBirthdaySendMsgForm form) { if (form == null || StrUtil.isBlank(form.getTitle())) { return Result.error("请输入发送标题"); diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/models/UserBirthdayConfig.java b/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/models/UserBirthdayConfig.java index 6bbdb6e..2d93c1c 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/models/UserBirthdayConfig.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/models/UserBirthdayConfig.java @@ -13,8 +13,8 @@ import org.nutz.dao.entity.annotation.TableMeta; import org.nutz.dao.interceptor.annotation.PrevInsert; /** - * 生日祝福页面配置。 - * picUrl、birthdayUrl 保存 sys_file 文件 ID,由移动端按文件 ID 获取图片。 + * 生日祝福配置。 + * picUrl、birthdayUrl 保存 sys_file 文件 ID,其余字段用于人员查询和自动发送。 */ @Data @EqualsAndHashCode(callSuper = true) @@ -38,4 +38,24 @@ public class UserBirthdayConfig extends BaseModel { @Comment("生日福利贺卡图片文件ID") @ColDefine(type = ColType.VARCHAR, width = 100) private String birthdayUrl; + + @Column + @Comment("默认查询未来生日天数") + @ColDefine(type = ColType.INT) + private Integer birthdayDays; + + @Column + @Comment("是否自动发送生日祝福") + @ColDefine(type = ColType.BOOLEAN) + private Boolean autoSendEnabled; + + @Column + @Comment("默认发送标题") + @ColDefine(type = ColType.VARCHAR, width = 255) + private String sendTitle; + + @Column + @Comment("默认发送内容") + @ColDefine(type = ColType.TEXT) + private String sendContent; } diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/param/UserBirthdayPageForm.java b/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/param/UserBirthdayPageForm.java index 0fc7779..003ee97 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/param/UserBirthdayPageForm.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/param/UserBirthdayPageForm.java @@ -27,16 +27,18 @@ public class UserBirthdayPageForm extends MemberInfoPageForm { /** * 将生日日期条件追加到已有人员查询条件。 - * 未传日期时默认查询今天起十五天内的生日,跨年时自动使用 OR 条件。 + * 未传日期时按配置天数查询,配置无效时默认十五天,跨年时自动使用 OR 条件。 * * @param cnd 已包含人员范围和会员状态的查询条件 + * @param birthdayDays 未选择日期时的默认查询天数 */ - public void buildBirthdaySearch(Cnd cnd) { + public void buildBirthdaySearch(Cnd cnd, Integer birthdayDays) { String start = normalizeMonthDay(startDate); String end = normalizeMonthDay(endDate); if (StrUtil.isAllBlank(start, end)) { + int days = birthdayDays == null || birthdayDays <= 0 ? 15 : birthdayDays; start = DateUtil.format(DateUtil.date(), "MM-dd"); - end = DateUtil.format(DateUtil.offsetDay(DateUtil.date(), 15), "MM-dd"); + end = DateUtil.format(DateUtil.offsetDay(DateUtil.date(), days), "MM-dd"); } if (StrUtil.isAllNotBlank(start, end)) { diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/service/UserBirthdayMsgLogService.java b/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/service/UserBirthdayMsgLogService.java index 8d58a2f..3d6b26e 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/service/UserBirthdayMsgLogService.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/service/UserBirthdayMsgLogService.java @@ -26,7 +26,7 @@ public interface UserBirthdayMsgLogService extends BaseService { /** * 向当天生日的会员发送系统生日通知,供 Quartz 任务调用。 * - * @param title 消息标题 - * @param content 消息正文 * @return 实际进入发送队列的接收人数 */ - int sendTodayBirthdayMessages(String title, String content); + int sendTodayBirthdayMessages(); /** * 获取最新生日页面配置;未配置时返回空配置对象。 @@ -67,7 +65,7 @@ public interface UserBirthdayService extends BaseService { /** * 新增或更新生日页面配置。 * - * @param config 背景图片和福利贺卡文件ID + * @param config 图片、查询天数、自动发送开关及默认消息内容 * @return 保存后的配置 */ UserBirthdayConfig saveConfig(UserBirthdayConfig config); diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/service/impl/UserBirthdayServiceImpl.java b/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/service/impl/UserBirthdayServiceImpl.java index ac63ed0..595cd4d 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/service/impl/UserBirthdayServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/birthday/service/impl/UserBirthdayServiceImpl.java @@ -11,7 +11,6 @@ import com.budwk.app.base.utils.CommonDownloadUtil; import com.budwk.app.sys.models.Sys_msg; import com.budwk.app.sys.models.Sys_user; import com.budwk.app.sys.services.SysMsgService; -import com.budwk.app.web.commons.base.Globals; import com.budwk.app.zhgh.staffmanage.birthday.models.UserBirthdayConfig; import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdayPageForm; import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdaySendMsgForm; @@ -38,6 +37,10 @@ import java.util.List; @IocBean(args = {"refer:dao"}) public class UserBirthdayServiceImpl extends BaseServiceImpl implements UserBirthdayService { + private static final int DEFAULT_BIRTHDAY_DAYS = 15; + private static final String DEFAULT_SEND_TITLE = "请点击查收您的生日福利"; + private static final String DEFAULT_SEND_CONTENT = "校工会祝您:生日快乐!幸福安康"; + @Inject private SysMsgService sysMsgService; @@ -106,7 +109,12 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl implement @Override @Aop(TransAop.READ_COMMITTED) - public int sendTodayBirthdayMessages(String title, String content) { + public int sendTodayBirthdayMessages() { + UserBirthdayConfig config = getConfig(); + if (!Boolean.TRUE.equals(config.getAutoSendEnabled())) { + log.info("生日祝福自动发送已关闭"); + return 0; + } Sql sql = Sqls.create(""" SELECT loginname FROM vw_user @@ -118,18 +126,25 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl implement sql.setCallback(Sqls.callback.strList()); dao().execute(sql); List loginNames = sql.getList(String.class); - return sendMessages(loginNames, title, content, "system", "系统推送", "系统推送"); + return sendMessages(loginNames, config.getSendTitle(), config.getSendContent(), + "system", "系统推送", "系统推送"); } @Override public UserBirthdayConfig getConfig() { UserBirthdayConfig config = dao().fetch(UserBirthdayConfig.class, Cnd.NEW().desc("updatedAt")); - return config == null ? new UserBirthdayConfig() : config; + return fillConfigDefaults(config == null ? new UserBirthdayConfig() : config); } @Override @Aop(TransAop.READ_COMMITTED) public UserBirthdayConfig saveConfig(UserBirthdayConfig config) { + UserBirthdayConfig current = dao().fetch(UserBirthdayConfig.class, Cnd.NEW().desc("updatedAt")); + if (current != null) { + // 配置始终复用已有记录,避免客户端未传 ID 时产生多条生日配置。 + config.setId(current.getId()); + } + fillConfigDefaults(config); dao().insertOrUpdate(config); return config; } @@ -167,15 +182,30 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl implement cnd.and("u.birthday", "is not", null); cnd.and("u.member", "=", 1); pageForm.buildSearch(cnd, "u."); - pageForm.buildBirthdaySearch(cnd); + pageForm.buildBirthdaySearch(cnd, getConfig().getBirthdayDays()); cnd.asc("daysUntilBirthday"); sql.setCondition(cnd); return sql; } + /** + * 补齐历史配置缺失的默认值,保证配置页、手动发送和定时任务使用同一套参数。 + */ + private UserBirthdayConfig fillConfigDefaults(UserBirthdayConfig config) { + if (config.getBirthdayDays() == null || config.getBirthdayDays() <= 0) { + config.setBirthdayDays(DEFAULT_BIRTHDAY_DAYS); + } + if (config.getAutoSendEnabled() == null) { + config.setAutoSendEnabled(true); + } + config.setSendTitle(StrUtil.blankToDefault(config.getSendTitle(), DEFAULT_SEND_TITLE)); + config.setSendContent(StrUtil.blankToDefault(config.getSendContent(), DEFAULT_SEND_CONTENT)); + return config; + } + /** * 将生日消息写入当前项目消息中心并生成发送记录。 - * Sys_msg.url 用于站内消息点击,正文中的完整链接供外部短信/微信渠道访问。 + * 发送内容只包含标题和正文,不向站内、短信或钉钉渠道传递生日页面链接。 */ private int sendMessages(List loginNames, String title, String content, String pushBy, String pushByName, String pushType) { @@ -187,24 +217,16 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl implement return 0; } - UserBirthdayConfig config = getConfig(); - String pagePath = "/platform/staffManage/birthday/manage/h5"; - if (StrUtil.isNotBlank(config.getBirthdayUrl())) { - pagePath = pagePath + "?id=" + config.getBirthdayUrl(); - } - String fullLink = Globals.AppDomain + pagePath; - Sys_msg message = new Sys_msg(); message.setTitle(title); - message.setNote(content + "\n" + fullLink); - message.setUrl(pagePath); + message.setNote(content); message.setType("user"); message.setSendType("show"); message.setSendAt(Times.getTS()); message.setCreatedBy(pushBy); sysMsgService.saveMsg(message, recipients.toArray(String[]::new), true); - userBirthdayMsgLogService.insertLogs(recipients, title, content, fullLink, + userBirthdayMsgLogService.insertLogs(recipients, title, content, "", pushBy, pushByName, pushType); return recipients.size(); } diff --git a/src/main/resources/static/components/plugins/sysSignature/h5Index.vue b/src/main/resources/static/components/plugins/sysSignature/h5Index.vue index 2354af1..8835020 100644 --- a/src/main/resources/static/components/plugins/sysSignature/h5Index.vue +++ b/src/main/resources/static/components/plugins/sysSignature/h5Index.vue @@ -1,248 +1,291 @@ diff --git a/src/main/resources/views/platform/zhgh/staffmanage/birthday/config/index.html b/src/main/resources/views/platform/zhgh/staffmanage/birthday/config/index.html new file mode 100644 index 0000000..3409354 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/staffmanage/birthday/config/index.html @@ -0,0 +1,122 @@ + +
+ + +
+ 生日祝福配置 +
+ + + +
建议使用竖版生日主题图片。
+
+ + +
移动端点击“领取生日福利”后展示该图片。
+
+ + + 生日人员页未选择日期时,默认查询今天起未来N天。 + + + + + + + + + + 保存配置 + 预览生日页面 + +
+
+
+
+ + + diff --git a/src/main/resources/views/platform/zhgh/staffmanage/birthday/manage/index.html b/src/main/resources/views/platform/zhgh/staffmanage/birthday/manage/index.html index 213fffb..b113d81 100644 --- a/src/main/resources/views/platform/zhgh/staffmanage/birthday/manage/index.html +++ b/src/main/resources/views/platform/zhgh/staffmanage/birthday/manage/index.html @@ -38,11 +38,10 @@ layout("/layouts/platform.html"){ - 导出Excel 发送通知 - 配置图片 @@ -107,25 +106,6 @@ layout("/layouts/platform.html"){ - - - - -
建议使用竖版生日主题图片。
-
- - -
移动端点击“领取生日福利”后展示该图片。
-
-
- - 取消 - 保存 - -