整改生日祝福
This commit is contained in:
@@ -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<String> loginNameList, List<NutMap> receivers,
|
||||
String title, String content, String pcUrl, String mobileUrl);
|
||||
}
|
||||
|
||||
@@ -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<String> 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<String> loginNameList, List<NutMap> receivers,
|
||||
String title, String content, String pcUrl, String mobileUrl) {
|
||||
List<NutMap> resultReceivers = buildAndValidateReceivers(sendType, loginNameList, receivers, title, content);
|
||||
List<NutMap> 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<List<NutMap>> 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<NutMap> buildAndValidateReceivers(String sendType, List<String> loginNameList,
|
||||
List<NutMap> 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<NutMap> 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<MsgPlatformRecipient> 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<String> 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<NutMap> 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<MsgPlatformRecipient> 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<MsgPlatformRecipient> buildAccountRecipients(String account) {
|
||||
if (Strings.isBlank(account)) {
|
||||
throw new BaseException("接收人工号不能为空");
|
||||
}
|
||||
MsgPlatformRecipient recipient = new MsgPlatformRecipient();
|
||||
recipient.setAccount(account);
|
||||
List<MsgPlatformRecipient> 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<NutMap> 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<MsgPlatformRecipient> 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<MsgPlatformRecipient> 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<MsgPlatformRecipient> buildWechatRecipients(String accountOrMobile) {
|
||||
if (Strings.isBlank(accountOrMobile)) {
|
||||
throw new BaseException("微信接收人工号或手机号不能为空");
|
||||
}
|
||||
MsgPlatformRecipient recipient = new MsgPlatformRecipient();
|
||||
recipient.setAccount(accountOrMobile);
|
||||
List<MsgPlatformRecipient> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.budwk.app.base.sms.model;
|
||||
|
||||
/**
|
||||
* 短信发送请求参数。
|
||||
*/
|
||||
public class MsgPlatformSmsRequest extends MsgPlatformBaseRequest {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<Sys_msg> 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<Sys_msg> implements SysMs
|
||||
|
||||
//发送学校平台消息
|
||||
ThreadUtil.execute(() -> {
|
||||
for (String loginName : loginNames) {
|
||||
smsService.sendSmsByAccount(loginName,sysMsg.getNote());
|
||||
smsService.sendWechatTextByAccountOrMobile(loginName,sysMsg.getNote());
|
||||
// 46表示短信和钉钉组合发送;按照消息中心要求,组合中包含短信时手机号必填。
|
||||
List<NutMap> 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;
|
||||
|
||||
+2
-13
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+23
-16
@@ -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<Sys_user> users, JSONObject config) {
|
||||
List<NutMap> 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("学校消息中心短信发送失败");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-14
@@ -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<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;
|
||||
}
|
||||
List<String> 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("学校消息中心微信发送失败");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+71
@@ -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));
|
||||
}
|
||||
}
|
||||
-13
@@ -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("请输入发送标题");
|
||||
|
||||
+22
-2
@@ -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;
|
||||
}
|
||||
|
||||
+5
-3
@@ -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)) {
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ public interface UserBirthdayMsgLogService extends BaseService<UserBirthdayMsgLo
|
||||
* @param loginNames 接收人工号集合
|
||||
* @param title 消息标题
|
||||
* @param content 消息正文
|
||||
* @param link 移动端生日祝福完整链接
|
||||
* @param link 移动端生日祝福链接,无跳转时传空字符串
|
||||
* @param pushBy 推送人用户ID
|
||||
* @param pushByName 推送人姓名
|
||||
* @param pushType 手动推送或系统推送
|
||||
|
||||
+2
-4
@@ -53,11 +53,9 @@ public interface UserBirthdayService extends BaseService<Sys_user> {
|
||||
/**
|
||||
* 向当天生日的会员发送系统生日通知,供 Quartz 任务调用。
|
||||
*
|
||||
* @param title 消息标题
|
||||
* @param content 消息正文
|
||||
* @return 实际进入发送队列的接收人数
|
||||
*/
|
||||
int sendTodayBirthdayMessages(String title, String content);
|
||||
int sendTodayBirthdayMessages();
|
||||
|
||||
/**
|
||||
* 获取最新生日页面配置;未配置时返回空配置对象。
|
||||
@@ -67,7 +65,7 @@ public interface UserBirthdayService extends BaseService<Sys_user> {
|
||||
/**
|
||||
* 新增或更新生日页面配置。
|
||||
*
|
||||
* @param config 背景图片和福利贺卡文件ID
|
||||
* @param config 图片、查询天数、自动发送开关及默认消息内容
|
||||
* @return 保存后的配置
|
||||
*/
|
||||
UserBirthdayConfig saveConfig(UserBirthdayConfig config);
|
||||
|
||||
+38
-16
@@ -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<Sys_user> 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<Sys_user> 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<Sys_user> implement
|
||||
sql.setCallback(Sqls.callback.strList());
|
||||
dao().execute(sql);
|
||||
List<String> 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<Sys_user> 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<String> loginNames, String title, String content,
|
||||
String pushBy, String pushByName, String pushType) {
|
||||
@@ -187,24 +217,16 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> 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();
|
||||
}
|
||||
|
||||
@@ -1,248 +1,291 @@
|
||||
<template>
|
||||
<div class="h5-signature">
|
||||
<slot v-if="$slots.default"></slot>
|
||||
<div v-else class="signature-entry">
|
||||
<!-- 未签名时展示统一空状态和入口,组件放入 van-field 时仍保持完整宽度。 -->
|
||||
<div v-if="!signatureContent" class="signature-entry-panel">
|
||||
<van-icon class="signature-placeholder-icon" name="edit"></van-icon>
|
||||
<div class="signature-placeholder-text">请完成本人手写签名</div>
|
||||
<van-button @click="openSignature" class="signature-open-button" block plain
|
||||
native-type="button">进入签字板</van-button>
|
||||
</div>
|
||||
<div class="h5-signature">
|
||||
<slot v-if="$slots.default"></slot>
|
||||
<div v-else class="signature-entry">
|
||||
<!-- 未签名时展示统一空状态和入口,组件放入 van-field 时仍保持完整宽度。 -->
|
||||
<div v-if="!signatureContent" class="signature-entry-panel">
|
||||
<van-icon class="signature-placeholder-icon" name="edit"></van-icon>
|
||||
<div class="signature-placeholder-text">请完成本人手写签名</div>
|
||||
<van-button @click="openSignature" class="signature-open-button" block plain
|
||||
native-type="button">进入签字板</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 已签名时直接预览结果,并在下方提供统一的重新签名入口。 -->
|
||||
<div v-else class="signature-entry-panel signature-entry-panel--completed">
|
||||
<van-image :src="signatureContent" class="signature-preview" fit="contain"></van-image>
|
||||
<div class="signature-completed-text">
|
||||
<van-icon name="passed"></van-icon>
|
||||
<span>已完成本人手写签名</span>
|
||||
</div>
|
||||
</div>
|
||||
<button v-if="signatureContent" @click="openSignature" class="signature-reset-button"
|
||||
type="button">重新签名</button>
|
||||
<!-- 已签名时直接预览结果,并在下方提供统一的重新签名入口。 -->
|
||||
<div v-else class="signature-entry-panel signature-entry-panel--completed">
|
||||
<van-image :src="signatureContent" class="signature-preview" fit="contain"></van-image>
|
||||
<div class="signature-completed-text">
|
||||
<van-icon name="passed"></van-icon>
|
||||
<span>已完成本人手写签名</span>
|
||||
</div>
|
||||
<van-popup v-model="showSignaturePanel" :close-on-click-overlay="false"
|
||||
:style="{ height: '100vh', width: '100vw' }" @close="handleSignaturePopupClose">
|
||||
<signature @save="save"></signature>
|
||||
</van-popup>
|
||||
</div>
|
||||
<button v-if="signatureContent" @click="openSignature" class="signature-reset-button"
|
||||
type="button">重新签名</button>
|
||||
</div>
|
||||
<van-popup v-model="showSignaturePanel" :close-on-click-overlay="false"
|
||||
:style="{ height: '100vh', width: '100vw' }" @close="handleSignaturePopupClose">
|
||||
<signature @save="save"></signature>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
name: "h5-signature",
|
||||
components: {
|
||||
signature: httpVueLoader("/components/plugins/sysSignature/index.vue?v=" + new Date().getTime())
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler(val) {
|
||||
this.signatureContent = val
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
signatureContent: null,
|
||||
showSignaturePanel: false,
|
||||
historyLayerName: "",
|
||||
historyLayerUnsubscribe: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 打开签字板。
|
||||
* 无请求参数;优先向全局历史栈增加当前签字弹层,返回值为空。
|
||||
*/
|
||||
openSignature() {
|
||||
const manager = window.h5HistoryLayerManager
|
||||
if (manager && manager.open(this.historyLayerName)) return
|
||||
this.showSignaturePanel = true
|
||||
},
|
||||
|
||||
// 保存成功或业务主动关闭时同步回退签字弹层历史,管理器不可用时直接关闭 Popup。
|
||||
closeSignaturePanel() {
|
||||
const manager = window.h5HistoryLayerManager
|
||||
if (manager && manager.close(this.historyLayerName)) return
|
||||
this.showSignaturePanel = false
|
||||
},
|
||||
|
||||
// Vant 组件自行关闭时仅处理仍位于栈顶的签字层,避免返回同步触发重复 history.back。
|
||||
handleSignaturePopupClose() {
|
||||
const manager = window.h5HistoryLayerManager
|
||||
if (!manager) return
|
||||
const stack = manager.stack
|
||||
if (stack[stack.length - 1] === this.historyLayerName) {
|
||||
manager.close(this.historyLayerName)
|
||||
}
|
||||
},
|
||||
|
||||
save(signature) {
|
||||
const file = this.base64ToFile(signature, "signature.png")
|
||||
const formData = new FormData()
|
||||
formData.append("file", file)
|
||||
axios
|
||||
.post("/platform/signature/saveH5Signature", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
return res.data
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
// 保存成功后使用项目统一的 Vant 反馈,避免浏览器原生弹框阻塞签字流程。
|
||||
this.$toast.success({
|
||||
message: "签名保存成功",
|
||||
duration: 1500,
|
||||
forbidClick: true
|
||||
})
|
||||
this.$emit("input", res.data)
|
||||
this.closeSignaturePanel()
|
||||
} else {
|
||||
// 接口返回失败时保留签字板,方便用户重试。
|
||||
this.$toast.fail({
|
||||
message: res.msg || "签名保存失败,请重试",
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
base64ToFile(base64Data, filename) {
|
||||
// 将base64的数据部分提取出来
|
||||
const parts = base64Data.split(";base64,")
|
||||
const contentType = parts[0].split(":")[1]
|
||||
const raw = window.atob(parts[1])
|
||||
const rawLength = raw.length
|
||||
const uInt8Array = new Uint8Array(rawLength)
|
||||
|
||||
for (let i = 0; i < rawLength; ++i) {
|
||||
uInt8Array[i] = raw.charCodeAt(i)
|
||||
}
|
||||
|
||||
// 使用Blob对象创建File对象
|
||||
const blob = new Blob([uInt8Array], { type: contentType })
|
||||
blob.lastModifiedDate = new Date()
|
||||
blob.name = filename
|
||||
return new File([blob], filename, { type: contentType })
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const manager = window.h5HistoryLayerManager
|
||||
if (!manager || typeof manager.ensureRegistered !== "function" || typeof manager.subscribe !== "function") return
|
||||
|
||||
// 每个组件实例使用唯一弹层名称,同一页面存在多个签字组件时互不影响。
|
||||
this.historyLayerName = "h5-signature-" + this._uid
|
||||
manager.ensureRegistered("h5-signature-page-" + window.location.pathname + window.location.search)
|
||||
this.historyLayerUnsubscribe = manager.subscribe((stack) => {
|
||||
this.showSignaturePanel = stack.includes(this.historyLayerName)
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (typeof this.historyLayerUnsubscribe === "function") {
|
||||
this.historyLayerUnsubscribe()
|
||||
}
|
||||
name: "h5-signature",
|
||||
components: {
|
||||
signature: httpVueLoader("/components/plugins/sysSignature/index.vue?v=" + new Date().getTime())
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler(val) {
|
||||
this.signatureContent = val
|
||||
// 父页面异步初始化后仍未提供签名时,统一回显当前用户在“我的签名”中保存的签名。
|
||||
if (!val) {
|
||||
this.loadUserSignature()
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
signatureContent: null,
|
||||
userSignatureContent: "",
|
||||
userSignatureLoaded: false,
|
||||
userSignatureLoading: false,
|
||||
showSignaturePanel: false,
|
||||
historyLayerName: "",
|
||||
historyLayerUnsubscribe: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 读取当前登录用户在“我的签名”中保存的签名。
|
||||
* 无请求参数;接口返回用户签名记录,组件仅在当前 v-model 为空时回填签名 URL。
|
||||
*/
|
||||
loadUserSignature() {
|
||||
if (this.value) return
|
||||
if (this.userSignatureLoaded) {
|
||||
this.applyUserSignature()
|
||||
return
|
||||
}
|
||||
if (this.userSignatureLoading) return
|
||||
|
||||
this.userSignatureLoading = true
|
||||
this.$axios.post("/platform/signature/get")
|
||||
.then((res) => {
|
||||
const signatureData = res.code === 0 ? (res.data || {}) : {}
|
||||
this.userSignatureContent = signatureData.signature || ""
|
||||
this.userSignatureLoaded = true
|
||||
this.applyUserSignature()
|
||||
})
|
||||
.catch(() => {
|
||||
// 默认签名读取失败时保留空状态,用户仍可进入签字板完成本次签名。
|
||||
this.userSignatureLoaded = true
|
||||
})
|
||||
.finally(() => {
|
||||
this.userSignatureLoading = false
|
||||
})
|
||||
},
|
||||
|
||||
// 仅为空值补充默认签名,避免覆盖历史业务签名或用户刚完成的现场签名。
|
||||
applyUserSignature() {
|
||||
if (this.value || !this.userSignatureContent) return
|
||||
this.signatureContent = this.userSignatureContent
|
||||
this.$emit("input", this.userSignatureContent)
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开签字板。
|
||||
* 无请求参数;优先向全局历史栈增加当前签字弹层,返回值为空。
|
||||
*/
|
||||
openSignature() {
|
||||
const manager = window.h5HistoryLayerManager
|
||||
if (manager && manager.open(this.historyLayerName)) return
|
||||
this.showSignaturePanel = true
|
||||
},
|
||||
|
||||
// 保存成功或业务主动关闭时同步回退签字弹层历史,管理器不可用时直接关闭 Popup。
|
||||
closeSignaturePanel() {
|
||||
const manager = window.h5HistoryLayerManager
|
||||
if (manager && manager.close(this.historyLayerName)) return
|
||||
this.showSignaturePanel = false
|
||||
},
|
||||
|
||||
// Vant 组件自行关闭时仅处理仍位于栈顶的签字层,避免返回同步触发重复 history.back。
|
||||
handleSignaturePopupClose() {
|
||||
const manager = window.h5HistoryLayerManager
|
||||
if (!manager) return
|
||||
const stack = manager.stack
|
||||
if (stack[stack.length - 1] === this.historyLayerName) {
|
||||
manager.close(this.historyLayerName)
|
||||
}
|
||||
},
|
||||
|
||||
save(signature) {
|
||||
const file = this.base64ToFile(signature, "signature.png")
|
||||
const formData = new FormData()
|
||||
formData.append("file", file)
|
||||
axios
|
||||
.post("/platform/signature/saveH5Signature", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
return res.data
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
// 保存成功后使用项目统一的 Vant 反馈,避免浏览器原生弹框阻塞签字流程。
|
||||
this.$toast.success({
|
||||
message: "签名保存成功",
|
||||
duration: 1500,
|
||||
forbidClick: true
|
||||
})
|
||||
this.$emit("input", res.data)
|
||||
this.closeSignaturePanel()
|
||||
} else {
|
||||
// 接口返回失败时保留签字板,方便用户重试。
|
||||
this.$toast.fail({
|
||||
message: res.msg || "签名保存失败,请重试",
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
base64ToFile(base64Data, filename) {
|
||||
// 将base64的数据部分提取出来
|
||||
const parts = base64Data.split(";base64,")
|
||||
const contentType = parts[0].split(":")[1]
|
||||
const raw = window.atob(parts[1])
|
||||
const rawLength = raw.length
|
||||
const uInt8Array = new Uint8Array(rawLength)
|
||||
|
||||
for (let i = 0; i < rawLength; ++i) {
|
||||
uInt8Array[i] = raw.charCodeAt(i)
|
||||
}
|
||||
|
||||
// 使用Blob对象创建File对象
|
||||
const blob = new Blob([uInt8Array], { type: contentType })
|
||||
blob.lastModifiedDate = new Date()
|
||||
blob.name = filename
|
||||
return new File([blob], filename, { type: contentType })
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const manager = window.h5HistoryLayerManager
|
||||
if (!manager || typeof manager.ensureRegistered !== "function" || typeof manager.subscribe !== "function") return
|
||||
|
||||
// 每个组件实例使用唯一弹层名称,同一页面存在多个签字组件时互不影响。
|
||||
this.historyLayerName = "h5-signature-" + this._uid
|
||||
manager.ensureRegistered("h5-signature-page-" + window.location.pathname + window.location.search)
|
||||
this.historyLayerUnsubscribe = manager.subscribe((stack) => {
|
||||
this.showSignaturePanel = stack.includes(this.historyLayerName)
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (typeof this.historyLayerUnsubscribe === "function") {
|
||||
this.historyLayerUnsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.h5-signature {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.signature-entry {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.signature-entry-panel {
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border: 1px dashed #dfe6ef;
|
||||
border-radius: 9px;
|
||||
background: #fbfcfe;
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border: 1px dashed #dfe6ef;
|
||||
border-radius: 9px;
|
||||
background: #fbfcfe;
|
||||
}
|
||||
|
||||
.signature-placeholder-icon {
|
||||
margin-bottom: 6px;
|
||||
color: #c7cfda;
|
||||
font-size: 38px;
|
||||
margin-bottom: 6px;
|
||||
color: #c7cfda;
|
||||
font-size: 38px;
|
||||
}
|
||||
|
||||
.signature-placeholder-text {
|
||||
margin-bottom: 12px;
|
||||
color: #9aa4b2;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 12px;
|
||||
color: #9aa4b2;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.signature-open-button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
color: var(--color-primary, #1989fa);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
background: linear-gradient(90deg, #f0f7ff, #f5f9ff);
|
||||
border-color: #d8e9ff;
|
||||
border-radius: 7px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
color: var(--color-primary, #1989fa);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
background: linear-gradient(90deg, #f0f7ff, #f5f9ff);
|
||||
border-color: #d8e9ff;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.signature-entry-panel--completed {
|
||||
min-height: 150px;
|
||||
justify-content: space-between;
|
||||
min-height: 150px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.signature-preview {
|
||||
width: 100%;
|
||||
height: 96px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 96px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.signature-completed-text {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #5f6b7a;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #5f6b7a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.signature-completed-text .van-icon {
|
||||
margin-right: 5px;
|
||||
color: #28a745;
|
||||
font-size: 16px;
|
||||
margin-right: 5px;
|
||||
color: #28a745;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.signature-reset-button {
|
||||
align-self: center;
|
||||
margin-top: 10px;
|
||||
padding: 3px 12px;
|
||||
color: var(--color-primary, #1989fa);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
align-self: center;
|
||||
margin-top: 10px;
|
||||
padding: 3px 12px;
|
||||
color: var(--color-primary, #1989fa);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<div slot="header">
|
||||
<span>生日祝福配置</span>
|
||||
</div>
|
||||
<el-form ref="configFormRef" :model="configForm" :rules="configRules"
|
||||
label-width="150px" style="max-width:900px">
|
||||
<el-form-item label="页面背景图片" prop="picUrl">
|
||||
<file-upload :value.sync="configForm.picUrl" :upload_number="1" upload_mode="image"
|
||||
accept=".jpg,.jpeg,.png" upload_result_category="interval"></file-upload>
|
||||
<div class="text-muted">建议使用竖版生日主题图片。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="福利贺卡图片" prop="birthdayUrl">
|
||||
<file-upload :value.sync="configForm.birthdayUrl" :upload_number="1" upload_mode="image"
|
||||
accept=".jpg,.jpeg,.png" upload_result_category="interval"></file-upload>
|
||||
<div class="text-muted">移动端点击“领取生日福利”后展示该图片。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="默认查询天数" prop="birthdayDays">
|
||||
<el-input-number v-model="configForm.birthdayDays" :min="1"
|
||||
controls-position="right"></el-input-number>
|
||||
<span class="text-muted ml10">生日人员页未选择日期时,默认查询今天起未来N天。</span>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="自动发送" prop="autoSendEnabled">
|
||||
<el-switch v-model="configForm.autoSendEnabled"
|
||||
active-text="开启" inactive-text="关闭"></el-switch>
|
||||
</el-form-item>-->
|
||||
<el-form-item label="发送标题" prop="sendTitle">
|
||||
<el-input v-model="configForm.sendTitle" maxlength="255" show-word-limit
|
||||
placeholder="请输入发送标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="发送内容" prop="sendContent">
|
||||
<el-input v-model="configForm.sendContent" type="textarea" :rows="6"
|
||||
placeholder="请输入发送内容"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="formLoading" @click="saveConfig">保存配置</el-button>
|
||||
<el-button @click="previewBirthday">预览生日页面</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</guava>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
formLoading: false,
|
||||
configForm: {
|
||||
id: "",
|
||||
picUrl: "",
|
||||
birthdayUrl: "",
|
||||
birthdayDays: 15,
|
||||
autoSendEnabled: true,
|
||||
sendTitle: "请点击查收您的生日福利",
|
||||
sendContent: "校工会祝您:生日快乐!幸福安康"
|
||||
},
|
||||
configRules: {
|
||||
birthdayDays: [
|
||||
{required:true,message:"请输入默认查询天数",trigger:["blur","change"]}
|
||||
],
|
||||
sendTitle: [
|
||||
{required:true,message:"请输入发送标题",trigger:["blur","change"]}
|
||||
],
|
||||
sendContent: [
|
||||
{required:true,message:"请输入发送内容",trigger:["blur","change"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadConfig() {
|
||||
this.$set(this, "formLoading", true)
|
||||
this.$axios.post(loc() + "/getConfig").then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "configForm", Object.assign({}, this.configForm, res.data || {}))
|
||||
} else {
|
||||
this.$message.warning(res.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
})
|
||||
},
|
||||
saveConfig() {
|
||||
this.$refs.configFormRef.validate((valid) => {
|
||||
if (!valid) return
|
||||
this.$confirm("确定保存生日祝福配置吗?", "提示", {type:"warning"}).then(() => {
|
||||
this.$set(this, "formLoading", true)
|
||||
this.$axios.post(loc() + "/saveConfig", this.configForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
this.$set(this, "configForm", res.data)
|
||||
} else {
|
||||
this.$message.warning(res.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
})
|
||||
}).catch(() => {})
|
||||
})
|
||||
},
|
||||
previewBirthday() {
|
||||
let url = "/platform/staffManage/birthday/manage/h5"
|
||||
if (this.configForm.birthdayUrl) {
|
||||
url = url + "?id=" + encodeURIComponent(this.configForm.birthdayUrl)
|
||||
}
|
||||
window.open(url, "_blank")
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadConfig()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -38,11 +38,10 @@ layout("/layouts/platform.html"){
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mt10">
|
||||
<table-tool label="生日人员(默认展示未来15天,日期查询忽略年份)"
|
||||
<table-tool :label="'生日人员(默认展示未来' + birthdayDays + '天,日期查询忽略年份)'"
|
||||
:columns.sync="tableColumns">
|
||||
<el-button size="small" type="primary" @click="doExport">导出Excel</el-button>
|
||||
<el-button size="small" type="primary" @click="openSendByQuery">发送通知</el-button>
|
||||
<el-button size="small" type="primary" @click="openConfig">配置图片</el-button>
|
||||
</table-tool>
|
||||
<el-table ref="table" v-loading="tableLoading" :data="tableData" :size="tableSize"
|
||||
row-key="id" style="width:100%" @sort-change="pageOrder">
|
||||
@@ -107,25 +106,6 @@ layout("/layouts/platform.html"){
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="生日祝福页面图片配置" :visible.sync="configDialogVisible"
|
||||
:close-on-click-modal="false" width="50%" @close="closeConfig">
|
||||
<el-form ref="configFormRef" :model="configForm" label-width="110px">
|
||||
<el-form-item label="页面背景图片" prop="picUrl">
|
||||
<file-upload :value.sync="configForm.picUrl" :upload_number="1" upload_mode="image"
|
||||
accept=".jpg,.jpeg,.png" upload_result_category="interval"></file-upload>
|
||||
<div class="text-muted">建议使用竖版生日主题图片。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="福利贺卡图片" prop="birthdayUrl">
|
||||
<file-upload :value.sync="configForm.birthdayUrl" :upload_number="1" upload_mode="image"
|
||||
accept=".jpg,.jpeg,.png" upload_result_category="interval"></file-upload>
|
||||
<div class="text-muted">移动端点击“领取生日福利”后展示该图片。</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button @click="closeConfig">取消</el-button>
|
||||
<el-button type="primary" :loading="formLoading" @click="saveConfig">保存</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
@@ -172,8 +152,8 @@ layout("/layouts/platform.html"){
|
||||
sendQueryForm: {title:"",content:""},
|
||||
sendUserDialogVisible: false,
|
||||
sendUserForm: {userId:"",userInfo:"",title:"",content:""},
|
||||
configDialogVisible: false,
|
||||
configForm: {id:"",picUrl:"",birthdayUrl:""}
|
||||
birthdayDays: 15,
|
||||
defaultMsgConfig: {title:"",content:""}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@@ -231,7 +211,10 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
},
|
||||
openSendByQuery() {
|
||||
this.$set(this, "sendQueryForm", Object.assign(this.requestParams(), {title:"",content:""}))
|
||||
this.$set(this, "sendQueryForm", Object.assign(this.requestParams(), {
|
||||
title:this.defaultMsgConfig.title,
|
||||
content:this.defaultMsgConfig.content
|
||||
}))
|
||||
this.$set(this, "sendQueryDialogVisible", true)
|
||||
},
|
||||
closeSendByQuery() {
|
||||
@@ -259,7 +242,12 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
},
|
||||
openSendByUser(row) {
|
||||
this.$set(this, "sendUserForm", {userId:row.id,userInfo:row.username + "(" + row.loginname + ")",title:"",content:""})
|
||||
this.$set(this, "sendUserForm", {
|
||||
userId:row.id,
|
||||
userInfo:row.username + "(" + row.loginname + ")",
|
||||
title:this.defaultMsgConfig.title,
|
||||
content:this.defaultMsgConfig.content
|
||||
})
|
||||
this.$set(this, "sendUserDialogVisible", true)
|
||||
},
|
||||
closeSendByUser() {
|
||||
@@ -286,41 +274,20 @@ layout("/layouts/platform.html"){
|
||||
}).catch(() => {})
|
||||
})
|
||||
},
|
||||
openConfig() {
|
||||
this.$set(this, "formLoading", true)
|
||||
loadBirthdayConfig() {
|
||||
this.$axios.post(loc() + "/getConfig").then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "configForm", Object.assign({id:"",picUrl:"",birthdayUrl:""}, res.data || {}))
|
||||
this.$set(this, "configDialogVisible", true)
|
||||
} else {
|
||||
this.$message.warning(res.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
})
|
||||
},
|
||||
closeConfig() {
|
||||
this.$set(this, "configDialogVisible", false)
|
||||
},
|
||||
saveConfig() {
|
||||
this.$confirm("确定保存生日祝福页面配置吗?", "提示", {type:"warning"}).then(() => {
|
||||
this.$set(this, "formLoading", true)
|
||||
this.$axios.post(loc() + "/saveOrModifyConfig", this.configForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
this.$set(this, "configForm", res.data)
|
||||
this.closeConfig()
|
||||
} else {
|
||||
this.$message.warning(res.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
if (res.code !== 0 || !res.data) return
|
||||
this.$set(this, "birthdayDays", res.data.birthdayDays || 15)
|
||||
this.$set(this, "defaultMsgConfig", {
|
||||
title:res.data.sendTitle || "",
|
||||
content:res.data.sendContent || ""
|
||||
})
|
||||
}).catch(() => {})
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initOrganizationOptions()
|
||||
this.loadBirthdayConfig()
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user