Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -1,39 +1,56 @@
|
||||
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 java.util.List;
|
||||
|
||||
public interface SmsService {
|
||||
|
||||
/**
|
||||
* 单发
|
||||
* @param loginName 工号
|
||||
* @param content 内容
|
||||
* 发送短信消息。
|
||||
*
|
||||
* @param request 短信请求参数,需传接收人、发送人、模板ID或消息内容等字段
|
||||
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
|
||||
*/
|
||||
void send(String loginName, String content);
|
||||
MsgPlatformResponse sendSms(MsgPlatformSmsRequest request);
|
||||
|
||||
/**
|
||||
* 单发
|
||||
* @param loginName 工号
|
||||
* @param title 标题
|
||||
* @param content 内容
|
||||
* 通过工号发送短信文本消息的便捷方法。
|
||||
* controller 只需要传接收人工号,以及短信内容即可。
|
||||
*
|
||||
* @param account 接收人工号
|
||||
* @param content 短信内容
|
||||
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
|
||||
*/
|
||||
void send(String loginName, String title, String content);
|
||||
MsgPlatformResponse sendSmsByAccount(String account, String content);
|
||||
|
||||
/**
|
||||
* 单发
|
||||
* @param loginName 工号
|
||||
* @param title 标题
|
||||
* @param content 内容
|
||||
* @param link 链接
|
||||
* 通过手机号发送短信文本消息的便捷方法。
|
||||
* controller 只需要传接收人手机号,以及短信内容即可。
|
||||
*
|
||||
* @param mobile 接收人手机号
|
||||
* @param content 短信内容
|
||||
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
|
||||
*/
|
||||
void send(String loginName, String title, String content, String link);
|
||||
MsgPlatformResponse sendSmsByMobile(String mobile, String content);
|
||||
|
||||
/**
|
||||
* 群发 多人接收内容相同时使用该方法
|
||||
* @param loginNames 工号
|
||||
* @param title 标题
|
||||
* @param content 内容
|
||||
* @param link 链接
|
||||
* 发送微信消息。
|
||||
*
|
||||
* @param request 微信请求参数,需传接收人、发送人、消息类型、模板ID或消息内容等字段
|
||||
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
|
||||
*/
|
||||
void massSend(List<String> loginNames, String title, String content, String link);
|
||||
MsgPlatformResponse sendWechat(MsgPlatformWechatRequest request);
|
||||
|
||||
/**
|
||||
* 发送微信文本消息的便捷方法。
|
||||
* controller 只需要传接收人工号或手机号,以及微信文本内容即可。
|
||||
*
|
||||
* @param accountOrMobile 接收人工号或手机号
|
||||
* @param content 微信文本内容
|
||||
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
|
||||
*/
|
||||
MsgPlatformResponse sendWechatTextByAccountOrMobile(String accountOrMobile, String content);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
package com.budwk.app.base.sms.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.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.base.Globals;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.integration.jedis.RedisService;
|
||||
import org.nutz.ioc.impl.PropertiesProxy;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import 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.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 发送短信消息。
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过工号发送短信文本消息的便捷方法。
|
||||
* 该方法适合 controller 里只传“工号 + 内容”的简单场景,发送人信息从配置中读取。
|
||||
*
|
||||
* @param account 接收人工号
|
||||
* @param content 短信内容
|
||||
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
|
||||
*/
|
||||
@Override
|
||||
public MsgPlatformResponse sendSmsByAccount(String account, String content) {
|
||||
MsgPlatformSmsRequest request = new MsgPlatformSmsRequest();
|
||||
request.setInfo(content);
|
||||
request.setRecipients(buildAccountRecipients(account));
|
||||
fillDefaultSenderInfo(request);
|
||||
return sendSms(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过手机号发送短信文本消息的便捷方法。
|
||||
* 该方法适合 controller 里只传“手机号 + 内容”的简单场景,发送人信息从配置中读取。
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送微信消息。
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送微信文本消息的便捷方法。
|
||||
* 该方法适合 controller 里只传“工号/手机号 + 内容”的简单场景,发送人信息从配置中读取。
|
||||
*
|
||||
* @param accountOrMobile 接收人工号或手机号
|
||||
* @param content 微信文本内容
|
||||
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
|
||||
*/
|
||||
@Override
|
||||
public MsgPlatformResponse sendWechatTextByAccountOrMobile(String accountOrMobile, String content) {
|
||||
MsgPlatformWechatRequest request = new MsgPlatformWechatRequest();
|
||||
request.setInfo(content);
|
||||
request.setWechatType("text");
|
||||
request.setRecipients(buildWechatRecipients(accountOrMobile));
|
||||
fillDefaultSenderInfo(request);
|
||||
return sendWechat(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装平台公共请求体。
|
||||
*
|
||||
* @param request 平台请求参数
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验公共请求参数,避免 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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 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 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析短信接收人手机号。
|
||||
* 如果 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验接收人数量,避免超过第三方平台单次上限。
|
||||
*
|
||||
* @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 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建手机号发送专用接收人列表。
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建微信便捷发送专用接收人列表。
|
||||
*
|
||||
* @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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package com.budwk.app.base.sms.impl.jshvc;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import com.budwk.app.base.sms.SmsService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.integration.jedis.RedisService;
|
||||
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.util.NutMap;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @ClassName SmsJshvcServiceImpl
|
||||
* @Author JyuHsin
|
||||
* @Date 2025/11/26 19:32
|
||||
* @Version 1.0
|
||||
* @Description TODO
|
||||
*/
|
||||
@Slf4j
|
||||
@IocBean
|
||||
public class SmsJshvcServiceImpl implements SmsService {
|
||||
|
||||
private static final String APPID = "";
|
||||
private static final String APP_SECRET = "";
|
||||
private static final String TOKEN_URL = "";
|
||||
private static final String MSG_URL = "";
|
||||
private static final String REDIS_KEY_MSG_ACCESS_TOKEN = "msg:token:";
|
||||
|
||||
@Inject
|
||||
private RedisService redisService;
|
||||
|
||||
@Override
|
||||
public void send(String loginName, String content) {
|
||||
// send(loginName, "智慧工会", content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(String loginName, String title, String content) {
|
||||
send(loginName, "智慧工会", content, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(String loginName, String title, String content, String link) {
|
||||
// Map<String, String> paramMap = Map.of("userId", loginName);
|
||||
// doSend(title, content, List.of(paramMap), link);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void massSend(List<String> loginNames, String title, String content, String link) {
|
||||
// List<Map<String, String>> receivers = loginNames.stream()
|
||||
// .map(name -> Map.of("userId", name))
|
||||
// .toList();
|
||||
// doSend(title, content, receivers, link);
|
||||
}
|
||||
|
||||
/**
|
||||
* sign: 请求签名:(accessToken + 第一个receivers 的userID )的32位小写的MD5加密值,其中如果相应部分没有则忽略
|
||||
* msgType: 0: 普通消息(默认) 1: 必读消息 2: 验证码(为验证码时消息一定不入收件箱)
|
||||
* expiredTime: 当msgType 为1必读消息 ,该字段为必填字段 格式:yyyy-MM-dd HH:mm:ss
|
||||
* sendType: 1.只发送PC门户 2.只发送移动校园 3.邮件 4.短信 5.微信企业号 6.钉钉企业内部应用工作通知 7.微信服务号 8.welink
|
||||
* wxSendType: 当发送类型为5时此字段才会生效:text(文本消息)、textcard(文本卡片)、nes(图文卡片,如果图文,则qyWeChatImgUrl必填)、button(按钮卡片详见示例),不传默认为text
|
||||
* receiverType: 1:用户 2:用户组 3:部门,默认为1
|
||||
*
|
||||
* @param title
|
||||
* @param content
|
||||
* @param receivers
|
||||
*/
|
||||
private void doSend(String title, String content, List<Map<String, String>> receivers, String link) {
|
||||
|
||||
if (Lang.isEmpty(receivers)) {
|
||||
log.error("send fail by receivers is null");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取token
|
||||
String accessToken = buildAccessToken();
|
||||
// 获取第一个接收人的userID
|
||||
String firstUserId = receivers.get(0).get("userId");
|
||||
// md5小写加密生成签名
|
||||
String sign = DigestUtil.md5Hex(accessToken + firstUserId);
|
||||
|
||||
NutMap paramsMap = new NutMap();
|
||||
paramsMap.put("sign", sign);
|
||||
paramsMap.put("msgType", "0");
|
||||
paramsMap.put("subject", title);
|
||||
paramsMap.put("content", content);
|
||||
paramsMap.put("sendType", "5");
|
||||
paramsMap.put("receivers", receivers);
|
||||
if (StrUtil.isBlank(link)) {
|
||||
paramsMap.put("wxSendType", "text");
|
||||
} else {
|
||||
paramsMap.put("wxSendType", "textcard");
|
||||
paramsMap.put("mobileUrl", link);
|
||||
// 这是图文卡片,后面用到再对接吧
|
||||
// paramsMap.put("qyWeChatImgUrl", "");
|
||||
}
|
||||
|
||||
log.debug("send params: {}", Json.toJson(paramsMap));
|
||||
|
||||
HttpRequest request = HttpRequest.post(MSG_URL)
|
||||
.header("appId", APPID)
|
||||
.header("accessToken", accessToken)
|
||||
.body(Json.toJson(paramsMap));
|
||||
|
||||
try {
|
||||
HttpResponse response = request.execute();
|
||||
log.info("send response body: {}", response.body());
|
||||
|
||||
NutMap bodyMap = Json.fromJson(NutMap.class, response.body());
|
||||
if (bodyMap.getInt("status") == 200) {
|
||||
log.info("send success code: {}, msg: {}", bodyMap.getInt("status"), bodyMap.getString("msg"));
|
||||
} else {
|
||||
log.error("send error code: {}, msg: {}", bodyMap.getInt("status"), bodyMap.getString("msg"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildAccessToken() {
|
||||
String token = redisService.get(REDIS_KEY_MSG_ACCESS_TOKEN);
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
return token;
|
||||
}
|
||||
HttpRequest request = HttpRequest.get(TOKEN_URL);
|
||||
request.header("appId", APPID);
|
||||
request.header("appSecret", APP_SECRET);
|
||||
|
||||
HttpResponse response = request.execute();
|
||||
|
||||
log.info("accessToken response: {}", Json.toJson(response.body()));
|
||||
|
||||
NutMap bodyMap = Json.fromJson(NutMap.class, response.body());
|
||||
if (bodyMap.getInt("errcode") == 0) {
|
||||
log.info("accessToken status: {}", bodyMap.getInt("errorcode"));
|
||||
redisService.setex(REDIS_KEY_MSG_ACCESS_TOKEN, 60 * 120, bodyMap.getString("data"));
|
||||
return bodyMap.getString("data");
|
||||
} else {
|
||||
log.error("accessToken status: {}", bodyMap.getInt("errorcode"));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
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";
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.budwk.app.base.sms.model;
|
||||
|
||||
/**
|
||||
* 短信发送请求参数。
|
||||
*/
|
||||
public class MsgPlatformSmsRequest extends MsgPlatformBaseRequest {
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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;
|
||||
}
|
||||
@@ -25,12 +25,15 @@ import com.budwk.app.sys.services.SysUserService;
|
||||
import com.budwk.app.sys.views.View_user;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Lang;
|
||||
@@ -146,10 +149,20 @@ public class SysUnitController {
|
||||
@At
|
||||
@ApiOperation("按关键字查询单位下的人员")
|
||||
@Ok("json:{ignoreNull:true}")
|
||||
public Result listUserSelect(@Valid String unitId, @Valid String keyWord) {
|
||||
public Result listUserSelect(@Valid String unitId, @Valid String keyWord, @Valid String roleCode) {
|
||||
Sql sql = Sqls.create("select id,loginname,username,unitname from vw_user $condition");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.and(View_user::getUnitId, "=", unitId);
|
||||
// 选择校领导角色时,人员范围来自提案配置中的校领导单位;其他角色仍限定当前单位。
|
||||
if ("UNIT_SCHOOL_LEADER".equals(roleCode)) {
|
||||
ProposalConfig proposalConfig = sysUnitService.dao().fetch(ProposalConfig.class, Cnd.NEW());
|
||||
if (proposalConfig == null || Lang.isEmpty(proposalConfig.getSchoolLeaderUnitIds())) {
|
||||
return Result.error("请在提案基础设置里配置校领导单位!");
|
||||
}
|
||||
cnd.and(View_user::getUnitId, "in", proposalConfig.getSchoolLeaderUnitIds());
|
||||
} else {
|
||||
cnd.and(View_user::getUnitId, "=", unitId);
|
||||
}
|
||||
cnd.and(View_user::getMember, "=", 1);
|
||||
SqlExpressionGroup seg = new SqlExpressionGroup();
|
||||
seg.orLike(View_user::getLoginname, keyWord, true);
|
||||
seg.orLike(View_user::getUsername, keyWord, true);
|
||||
@@ -165,13 +178,14 @@ public class SysUnitController {
|
||||
|
||||
@At
|
||||
@Ok("json")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result insertUnitUserRole(String userId, String roleCode, String unitId) {
|
||||
Sys_role role = sysRoleService.fetch(Cnd.where("code", "=", roleCode));
|
||||
if (Lang.isEmpty(role)) {
|
||||
throw new BaseException("无法找到{}对应编码的角色", roleCode);
|
||||
}
|
||||
sysRoleService.clear("sys_user_role", Cnd.where("roleId", "=", role.getId()).and("userId", "=", userId).and("unitId", "=", unitId));
|
||||
sysRoleService.insert("sys_user_role", Chain.make("roleId", role.getId()).add("userId", userId).add("unitId", unitId));
|
||||
sysRoleService.clear("sys_user_role", Cnd.where("roleId", "=", role.getId()).and("userId", "=", userId).and("unitId", "=", unitId).and("underTakeId", "=", unitId));
|
||||
sysRoleService.insert("sys_user_role", Chain.make("roleId", role.getId()).add("userId", userId).add("unitId", unitId).add("underTakeId", unitId));
|
||||
sysRoleService.clearCache();
|
||||
sysUserService.clearCache();
|
||||
return Result.success();
|
||||
@@ -179,12 +193,13 @@ public class SysUnitController {
|
||||
|
||||
@At
|
||||
@Ok("json")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result deleteUnitUserRole(String userId, String roleCode, String unitId) {
|
||||
Sys_role role = sysRoleService.fetch(Cnd.where("code", "=", roleCode));
|
||||
if (Lang.isEmpty(role)) {
|
||||
throw new BaseException("无法找到{}对应编码的角色", roleCode);
|
||||
}
|
||||
sysRoleService.clear("sys_user_role", Cnd.where("roleId", "=", role.getId()).and("userId", "=", userId).and("unitId", "=", unitId));
|
||||
sysRoleService.clear("sys_user_role", Cnd.where("roleId", "=", role.getId()).and("userId", "=", userId).and("unitId", "=", unitId).and("underTakeId", "=", unitId));
|
||||
sysRoleService.clearCache();
|
||||
sysUserService.clearCache();
|
||||
return Result.success();
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -102,11 +103,12 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
|
||||
});
|
||||
|
||||
//发送学校平台消息
|
||||
// for (String loginName : loginNames) {
|
||||
// smsService.send(loginName, sysMsg.getTitle(), sysMsg.getNote());
|
||||
// }
|
||||
ThreadUtil.execute(() -> {
|
||||
//smsService.massSend(loginNames, sysMsg.getTitle(), HtmlUtil.cleanHtmlTag(StrUtil.blankToDefault(sysMsg.getNote(),"")), sysMsg.getUrl());
|
||||
for (String loginName : loginNames) {
|
||||
smsService.sendSmsByAccount(loginName,sysMsg.getNote());
|
||||
smsService.sendWechatTextByAccountOrMobile(loginName,sysMsg.getNote());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return sysMsg;
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ public class ActivityCultureAuditActivityController {
|
||||
cnd.andEX("tissue.unionId", "=", unionId);
|
||||
cnd.andEX("tissue.activity_type", "=", activity_type);
|
||||
if (approval) {
|
||||
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
|
||||
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
|
||||
} else {
|
||||
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
|
||||
}
|
||||
|
||||
+21
@@ -9,6 +9,7 @@ import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.sys.models.Sys_home_activity;
|
||||
import com.budwk.app.sys.models.Sys_user;
|
||||
import com.budwk.app.sys.services.SysMsgService;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
@@ -81,17 +82,20 @@ public class ActivityWorksCollectionManageController {
|
||||
|
||||
@At
|
||||
@SaCheckPermission("activity.workscollection.manage")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result insert(@Param("data") @Valid Activity_works_collection worksCollection) {
|
||||
worksCollection.setIsSubmit(true);
|
||||
dao.insertWith(worksCollection, "subjectTypes");
|
||||
worksCollection.getSubjectTypes().forEach(item -> {
|
||||
dao.insertLinks(item, "worksTypes");
|
||||
});
|
||||
syncHomeActivity(worksCollection);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("activity.workscollection.manage")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result save(@Param("data") @Valid Activity_works_collection worksCollection) {
|
||||
worksCollection.setIsSubmit(false);
|
||||
dao.insertWith(worksCollection, "subjectTypes");
|
||||
@@ -122,6 +126,7 @@ public class ActivityWorksCollectionManageController {
|
||||
Cnd.where(Activity_works_worksType::getId, "not in ", workIds)
|
||||
.and(Activity_works_worksType::getSubjectId,"=",item.getId()));
|
||||
});
|
||||
syncHomeActivity(worksCollection);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@@ -132,6 +137,7 @@ public class ActivityWorksCollectionManageController {
|
||||
dao.delete(Activity_works_collection.class, id);
|
||||
dao.clear(Activity_works_collection_upload.class, Cnd.where(Activity_works_collection_upload::getActivityId, "=", id));
|
||||
dao.clear(Activity_works_subjectType.class, Cnd.where(Activity_works_subjectType::getActivityId, "=", id));
|
||||
dao.delete(Sys_home_activity.class, id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@@ -148,11 +154,26 @@ public class ActivityWorksCollectionManageController {
|
||||
|
||||
@At
|
||||
@SaCheckPermission("activity.workscollection.manage")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result enableChange(@Valid String id, @Valid Boolean enable) {
|
||||
dao.update(Activity_works_collection.class, Chain.make("enable", enable), Cnd.where("id", "=", id));
|
||||
Activity_works_collection worksCollection = dao.fetch(Activity_works_collection.class, id);
|
||||
worksCollection.setEnable(enable);
|
||||
syncHomeActivity(worksCollection);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步作品征集到首页活动表:开启的活动写入首页,关闭的活动移除首页入口。
|
||||
*/
|
||||
private void syncHomeActivity(Activity_works_collection worksCollection) {
|
||||
if (Boolean.TRUE.equals(worksCollection.getEnable())) {
|
||||
dao.insertOrUpdate(worksCollection.covertToSysHomeActivity());
|
||||
} else {
|
||||
dao.delete(Sys_home_activity.class, worksCollection.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("activity.workscollection.manage")
|
||||
public Result sendNotice(@Valid String id) {
|
||||
|
||||
+24
-3
@@ -11,6 +11,7 @@ import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
|
||||
import com.budwk.app.zhgh.activity.workscollection.models.Activity_works_collection;
|
||||
import com.budwk.app.zhgh.activity.workscollection.models.Activity_works_collection_upload;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.FieldFilter;
|
||||
@@ -18,6 +19,7 @@ import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.dao.util.Daos;
|
||||
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
||||
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;
|
||||
@@ -94,11 +96,17 @@ public class ActivityWorksCollectionUploadController {
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("activity.workscollection.upload")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result insert(@Param("data") @Valid Activity_works_collection_upload upload) {
|
||||
|
||||
String activityId = upload.getActivityId();
|
||||
Activity_works_collection activity = dao.fetch(Activity_works_collection.class, activityId);
|
||||
if (activity.getEndDateTime().getTime() < System.currentTimeMillis()){
|
||||
long now = System.currentTimeMillis();
|
||||
// 作品上传只允许在活动开始时间和结束时间之间提交,防止活动未开始或已结束后继续上传。
|
||||
if (activity.getStartDateTime().getTime() > now) {
|
||||
return Result.error("活动未开始,不能上传!");
|
||||
}
|
||||
if (activity.getEndDateTime().getTime() < now){
|
||||
return Result.error("活动已结束!");
|
||||
}
|
||||
if (activity.getActivityGroupId() != null) {
|
||||
@@ -124,9 +132,15 @@ public class ActivityWorksCollectionUploadController {
|
||||
|
||||
@At
|
||||
@SaCheckPermission("activity.workscollection.upload")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result update(@Param("data") @Valid Activity_works_collection_upload upload) {
|
||||
Activity_works_collection activity = dao.fetch(Activity_works_collection.class, upload.getActivityId());
|
||||
if (activity.getEndDateTime().getTime() < System.currentTimeMillis()){
|
||||
long now = System.currentTimeMillis();
|
||||
// 编辑作品同样按活动时间窗口控制,避免过期或未开始活动被继续提交作品信息。
|
||||
if (activity.getStartDateTime().getTime() > now) {
|
||||
return Result.error("活动未开始,不能上传!");
|
||||
}
|
||||
if (activity.getEndDateTime().getTime() < now){
|
||||
return Result.error("活动已结束!");
|
||||
}
|
||||
dao.updateIgnoreNull(upload);
|
||||
@@ -155,12 +169,19 @@ public class ActivityWorksCollectionUploadController {
|
||||
|
||||
@At
|
||||
@SaCheckPermission("activity.workscollection.upload")
|
||||
/**
|
||||
* 查询当前登录人有权限上传作品的活动主题。
|
||||
*
|
||||
* @param year 活动年度,前端传年度选择框的 yyyy 值,按活动开始时间所在年份过滤。
|
||||
* @return Result,data 为活动列表,仅返回 id、name,且保留已开启和面向对象权限过滤。
|
||||
*/
|
||||
@ApiOperation("获取有权限的活动列表")
|
||||
@Ok("json:{ignoreNull:true}")
|
||||
public Result listPerMissionActivity() {
|
||||
public Result listPerMissionActivity(Long year) {
|
||||
Dao extDao = Daos.ext(dao, FieldFilter.create(Activity_works_collection.class, "id|name|"));
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.and(Activity_works_collection::getEnable,"=",1);
|
||||
cnd.andEX("YEAR(startDateTime)", "=", year);
|
||||
cnd.desc(Activity_works_collection::getStartDateTime);
|
||||
List<Activity_works_collection> list = extDao.query(Activity_works_collection.class, cnd);
|
||||
list = list.stream().filter(item -> {
|
||||
|
||||
+20
-1
@@ -1,6 +1,8 @@
|
||||
package com.budwk.app.zhgh.activity.workscollection.models;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import com.budwk.app.sys.models.Sys_home_activity;
|
||||
import com.budwk.app.sys.services.SysHomeConvert;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.*;
|
||||
@@ -17,7 +19,7 @@ import java.util.List;
|
||||
@Table("activity_works_collection")
|
||||
@TableMeta("{'mysql-charset':'utf8mb4'}")
|
||||
@Comment("作品征集活动")
|
||||
public class Activity_works_collection extends BaseModel {
|
||||
public class Activity_works_collection extends BaseModel implements SysHomeConvert {
|
||||
|
||||
@Name
|
||||
@Comment("ID")
|
||||
@@ -123,4 +125,21 @@ public class Activity_works_collection extends BaseModel {
|
||||
@Many(field = "activityId")
|
||||
private List<Activity_works_collection_upload> uploads;
|
||||
|
||||
@Override
|
||||
public Sys_home_activity covertToSysHomeActivity() {
|
||||
Sys_home_activity sysHomeActivity = new Sys_home_activity();
|
||||
sysHomeActivity.setId(this.getId());
|
||||
sysHomeActivity.setName(this.getName());
|
||||
sysHomeActivity.setCover(this.getCover());
|
||||
sysHomeActivity.setContent(this.getContent());
|
||||
sysHomeActivity.setUrl("/platform/activity/worksCollection/upload");
|
||||
sysHomeActivity.setH5Url("/platform/activity/worksCollection/upload/h5");
|
||||
sysHomeActivity.setStartDate(this.getStartDateTime());
|
||||
sysHomeActivity.setEndDate(this.getEndDateTime());
|
||||
sysHomeActivity.setAllowUserGroupId(this.getActivityGroupId());
|
||||
sysHomeActivity.setEnable(Boolean.TRUE.equals(this.getEnable()));
|
||||
sysHomeActivity.setClassPath(this.getClass().getPackageName() + this.getClass().getName());
|
||||
return sysHomeActivity;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-1
@@ -68,7 +68,7 @@ public class ClubUserJoinApplyController {
|
||||
@Inject
|
||||
private ClubUserJoinService clubUserJoinService;
|
||||
|
||||
@At("/")
|
||||
@At("")
|
||||
@SaCheckPermission("club.join.apply")
|
||||
@Ok("beetl:/platform/zhgh/club/join/apply/index.html")
|
||||
public void index() {
|
||||
@@ -128,6 +128,12 @@ public class ClubUserJoinApplyController {
|
||||
@SLog(tag = "社团管理系统-申请入/退会", msg = "申请社团入/退会")
|
||||
public Result submit(@Param("data") ClubUserApply clubUserApply,
|
||||
@Param("mode") Boolean mode) {
|
||||
if (Boolean.TRUE.equals(mode)) {
|
||||
String qualificationMsg = clubUserJoinService.checkJoinQualification(SecurityUtil.getUserId());
|
||||
if (StrUtil.isNotBlank(qualificationMsg)) {
|
||||
return Result.error(99, qualificationMsg);
|
||||
}
|
||||
}
|
||||
if(mode == false) {
|
||||
clubUserApply = clubUserJoinService.buildExitApply(clubUserApply.getClubId(), SecurityUtil.getUserId());
|
||||
if (ObjectUtil.isEmpty(clubUserApply)) {
|
||||
@@ -192,6 +198,10 @@ public class ClubUserJoinApplyController {
|
||||
@ApiOperation("校验是否申请过社团")
|
||||
@SaCheckPermission(value = {"club.join.apply", "h5.club.join.apply"}, mode = SaMode.OR)
|
||||
public Result checkApplyClub(@Valid String clubId) {
|
||||
String qualificationMsg = clubUserJoinService.checkJoinQualification(SecurityUtil.getUserId());
|
||||
if (StrUtil.isNotBlank(qualificationMsg)) {
|
||||
return Result.error(99, qualificationMsg);
|
||||
}
|
||||
View_user user = dao.fetch(View_user.class, Cnd.where("id", "=", SecurityUtil.getUserId()));
|
||||
if (user.getMember() != 1) {
|
||||
return Result.error(99, "抱歉,您不满足入会的条件!");
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ public class ClubUserJoinApprovalController {
|
||||
@Inject
|
||||
private ClubUserJoinService clubUserJoinService;
|
||||
|
||||
@At("/")
|
||||
@At("")
|
||||
@SaCheckPermission("club.join.clubApproval")
|
||||
@Ok("beetl:/platform/zhgh/club/join/clubApproval/index.html")
|
||||
public void index() {}
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ public class ClubUserJoinMineController {
|
||||
@Inject
|
||||
private ClubUserJoinService clubUserJoinService;
|
||||
|
||||
@At("/")
|
||||
@At("")
|
||||
@SaCheckPermission("club.join.mine")
|
||||
@Ok("beetl:/platform/zhgh/club/join/mine/index.html")
|
||||
public void index() {}
|
||||
|
||||
@@ -25,4 +25,12 @@ public interface ClubUserJoinService extends BaseService<ClubUserApply> {
|
||||
* @return 入会时间
|
||||
*/
|
||||
java.util.Date resolveJoinTime(String clubId, String userId);
|
||||
|
||||
/**
|
||||
* 校验当前用户是否满足申请加入社团的资格。
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 校验不通过时返回提示语,通过时返回null
|
||||
*/
|
||||
String checkJoinQualification(String userId);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.budwk.app.zhgh.club.service.impl;
|
||||
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.sys.views.View_user;
|
||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
||||
@@ -15,10 +18,37 @@ import java.util.List;
|
||||
|
||||
@IocBean(args = {"refer:dao"})
|
||||
public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> implements ClubUserJoinService {
|
||||
private static final List<String> ALLOW_JOIN_PERSON_TYPES = List.of(
|
||||
"教职工",
|
||||
"未起薪人员",
|
||||
"教职工新入会",
|
||||
"教职工其他",
|
||||
"劳动服务公司",
|
||||
"后勤聘用人员",
|
||||
"退休人员[一]",
|
||||
"退休人员[二]",
|
||||
"校聘劳务派遣全额"
|
||||
);
|
||||
|
||||
public ClubUserJoinServiceImpl(Dao dao) {
|
||||
super(dao);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String checkJoinQualification(String userId) {
|
||||
View_user user = dao().fetch(View_user.class, Cnd.where("id", "=", userId));
|
||||
if (user == null) {
|
||||
return "抱歉,您不满足入会的条件!";
|
||||
}
|
||||
if (user.getMember() == null || user.getMember() != 1) {
|
||||
return "抱歉,您不满足入会的条件!";
|
||||
}
|
||||
if (StrUtil.isBlank(user.getPersonType()) || !ALLOW_JOIN_PERSON_TYPES.contains(user.getPersonType())) {
|
||||
return "抱歉,您不满足入会的条件!";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClubUserApply buildExitApply(String clubId, String userId) {
|
||||
// 优先复用最近一次申请记录中的补充信息,避免用户重复填写基础资料。
|
||||
|
||||
@@ -275,7 +275,14 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
||||
}
|
||||
cnd.andEX("year(info.createTime)", "=", pageForm.getYear());
|
||||
cnd.and("info.userId", "=", SecurityUtil.getUserId());
|
||||
cnd.desc("createTime");
|
||||
// 我的注册列表默认按社团编码升序展示,点击表头时按用户选择的方向排序。
|
||||
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
|
||||
cnd.asc("info.clubCode");
|
||||
} else if ("clubCode".equals(pageForm.getPageOrderName())) {
|
||||
cnd.orderBy("info.clubCode", PageUtil.getOrder(pageForm.getPageOrderBy()));
|
||||
} else {
|
||||
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
|
||||
}
|
||||
cnd.groupBy("info.id");
|
||||
sql.setCondition(cnd);
|
||||
return listPageVO(pageForm, sql, ClubRegisterPageVo.class);
|
||||
|
||||
+11
-13
@@ -1,6 +1,7 @@
|
||||
package com.budwk.app.zhgh.dayofficework.message.strategy.impl;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.budwk.app.base.sms.SmsService;
|
||||
import com.budwk.app.sys.models.Sys_user;
|
||||
import com.budwk.app.zhgh.dayofficework.message.enums.GlobalMessageChannel;
|
||||
import com.budwk.app.zhgh.dayofficework.message.strategy.GlobalMessageSendStrategy;
|
||||
@@ -21,6 +22,8 @@ public class SmsMessageSendStrategy implements GlobalMessageSendStrategy {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private SmsService smsService;
|
||||
|
||||
@Override
|
||||
public GlobalMessageChannel getChannel() {
|
||||
@@ -70,29 +73,24 @@ public class SmsMessageSendStrategy implements GlobalMessageSendStrategy {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信消息
|
||||
* 发送短信消息。
|
||||
* 接收人由全局消息服务传入的用户ID或登录名查询得到,发送时使用用户工号对接通讯平台;
|
||||
* 返回结果由 SmsService 内部解析并记录,当前策略只负责逐个用户触发发送并记录异常。
|
||||
*/
|
||||
private void sendSmsMessage(String title, String content, Integer type, List<Sys_user> users, JSONObject config) {
|
||||
for (Sys_user user : users) {
|
||||
try {
|
||||
String mobile = user.getMobile();
|
||||
if (mobile == null || mobile.trim().isEmpty()) {
|
||||
log.warn("用户{}没有手机号,跳过短信发送", user.getUsername());
|
||||
String loginName = user.getLoginname();
|
||||
if (loginName == null || loginName.trim().isEmpty()) {
|
||||
log.warn("用户{}没有工号,跳过短信发送", user.getUsername());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 构建短信内容
|
||||
String smsContent = buildSmsContent(title, content, type);
|
||||
|
||||
// 执行发送
|
||||
|
||||
boolean success = true;
|
||||
|
||||
if (success) {
|
||||
log.info("短信发送成功:用户{},手机号:{}", user.getUsername(), mobile);
|
||||
} else {
|
||||
log.warn("短信发送失败:用户{},手机号:{}", user.getUsername(), mobile);
|
||||
}
|
||||
smsService.sendSmsByAccount(loginName, smsContent);
|
||||
log.info("短信发送完成:用户{},工号:{}", user.getUsername(), loginName);
|
||||
} catch (Exception e) {
|
||||
log.error("发送短信给用户{}时出错:{}", user.getUsername(), e.getMessage(), e);
|
||||
}
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package com.budwk.app.zhgh.dayofficework.message.strategy.impl;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.budwk.app.base.sms.SmsService;
|
||||
import com.budwk.app.sys.models.Sys_user;
|
||||
import com.budwk.app.zhgh.dayofficework.message.enums.GlobalMessageChannel;
|
||||
import com.budwk.app.zhgh.dayofficework.message.strategy.GlobalMessageSendStrategy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 微信发送策略
|
||||
*/
|
||||
@IocBean
|
||||
@Slf4j
|
||||
public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private SmsService smsService;
|
||||
|
||||
@Override
|
||||
public GlobalMessageChannel getChannel() {
|
||||
return GlobalMessageChannel.WECHAT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportAsync() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(String title, String content, Integer type, List<String> receiverIds, JSONObject config) {
|
||||
try {
|
||||
log.info("开始发送微信消息,标题:{},接收人数量:{}", title, receiverIds.size());
|
||||
|
||||
// receiverIds 为 sys_user.id,按用户ID查询后使用工号对接通讯平台微信文本消息接口。
|
||||
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where(Sys_user::getId, "in", receiverIds));
|
||||
sendWechatMessage(title, content, type, users, config);
|
||||
|
||||
log.info("微信消息发送完成,标题:{},接收人数量:{}", title, users.size());
|
||||
} catch (Exception e) {
|
||||
log.error("微信消息发送失败,标题:{},错误:{}", title, e.getMessage(), e);
|
||||
throw new RuntimeException("微信消息发送失败:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendByLoginName(String title, String content, Integer type, List<String> receiverLoginNames, JSONObject config) {
|
||||
try {
|
||||
log.info("开始发送微信消息(按登录名),标题:{},接收人数量:{}", title, receiverLoginNames.size());
|
||||
|
||||
// receiverLoginNames 为 sys_user.loginname,查询用户后仍使用工号作为通讯平台接收人标识。
|
||||
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where(Sys_user::getLoginname, "in", receiverLoginNames));
|
||||
sendWechatMessage(title, content, type, users, config);
|
||||
|
||||
log.info("微信消息发送完成(按登录名),标题:{},接收人数量:{}", title, users.size());
|
||||
} catch (Exception e) {
|
||||
log.error("微信消息发送失败(按登录名),标题:{},错误:{}", title, e.getMessage(), e);
|
||||
throw new RuntimeException("微信消息发送失败:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送微信文本消息。
|
||||
* 接收人来自全局消息服务传入的用户ID或登录名,方法内部转换为 Sys_user 后取工号发送;
|
||||
* SmsService 返回 MsgPlatformResponse,当前策略按单人发送记录日志,单个用户失败不影响其他接收人。
|
||||
*/
|
||||
private void sendWechatMessage(String title, String content, Integer type, List<Sys_user> users, JSONObject config) {
|
||||
for (Sys_user user : users) {
|
||||
try {
|
||||
String loginName = user.getLoginname();
|
||||
if (loginName == null || loginName.trim().isEmpty()) {
|
||||
log.warn("用户{}没有工号,跳过微信发送", user.getUsername());
|
||||
continue;
|
||||
}
|
||||
|
||||
String wechatContent = buildWechatContent(title, content, type);
|
||||
smsService.sendWechatTextByAccountOrMobile(loginName, wechatContent);
|
||||
log.info("微信发送完成:用户{},工号:{}", user.getUsername(), loginName);
|
||||
} catch (Exception e) {
|
||||
log.error("发送微信给用户{}时出错:{}", user.getUsername(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建微信文本内容。
|
||||
* type=2 表示待办消息,其余类型按系统通知处理,内容格式保持与短信策略一致。
|
||||
*/
|
||||
private String buildWechatContent(String title, String content, Integer type) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if (type != null && type == 2) {
|
||||
sb.append("【待办通知】");
|
||||
} else {
|
||||
sb.append("【系统通知】");
|
||||
}
|
||||
|
||||
sb.append(title);
|
||||
if (content != null && !content.trim().isEmpty()) {
|
||||
sb.append(":").append(content);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+17
-16
@@ -113,22 +113,23 @@ public class UnionReimburseApplyController {
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
// @At
|
||||
// @ApiOperation("重新提交申请")
|
||||
// @Aop(TransAop.READ_COMMITTED)
|
||||
// @SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
|
||||
// public Result submitAgain(@Param("data") UnionReimburse unionReimburse, @Param("taskId") Long taskId) {
|
||||
// if (StrUtil.isBlank(unionReimburse.getId())) unionReimburse.setCreateTime(new Date());
|
||||
//
|
||||
// dao.insertOrUpdate(unionReimburse);
|
||||
//
|
||||
// Dict dict = Dict.create();
|
||||
// dict.set(FlowConst.PROCESS_TASK_ID_KEY, taskId);
|
||||
// dict.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.RE_APPLY.getCode());
|
||||
// dict.set("userId", unionReimburse.getCondolenceUserId());
|
||||
// flowCommonService.executeTask(dict);
|
||||
// return Result.success();
|
||||
// }
|
||||
@At
|
||||
@ApiOperation("重新提交申请")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
|
||||
public Result submitAgain(@Param("data") UnionReimburse unionReimburse, @Param("taskId") Long taskId) {
|
||||
// 退回重提也必须复用当前提交保存逻辑,避免绕过发票明细落库和提交校验。
|
||||
Result result = unionReimburseService.saveApply(unionReimburse, 2);
|
||||
if (result.getCode() != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Dict dict = Dict.create();
|
||||
dict.set(FlowConst.PROCESS_TASK_ID_KEY, taskId);
|
||||
dict.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.RE_APPLY.getCode());
|
||||
flowCommonService.executeTask(dict);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckLogin
|
||||
|
||||
+1
@@ -120,6 +120,7 @@ public class ProposalSecondedController {
|
||||
Cnd cnd = Cnd.NEW();
|
||||
|
||||
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
|
||||
cnd.and("info.createUserId", "!=", SecurityUtil.getUserId());
|
||||
|
||||
if (approval) {
|
||||
// 已附议页签只展示当前登录附议人已经处理过的记录。
|
||||
|
||||
+1
-1
@@ -179,7 +179,7 @@ public class TeacherCongressDelegatePushController {
|
||||
""");
|
||||
sql.setParam("sessionId", sessionId);
|
||||
Cnd cnd = Cnd.NEW();
|
||||
// cnd.and("u.member", "=", 1);
|
||||
cnd.and("u.member", "=", 1);
|
||||
cnd.and("u.username", "is not", null);
|
||||
cnd.andEX("u.id", "not in", userIds);
|
||||
cnd.and("tcd.userId", "is", null);
|
||||
|
||||
Reference in New Issue
Block a user