This commit is contained in:
@jyuhsin
2026-03-16 15:18:47 +08:00
parent f6dc4b16f4
commit 53a56e46b5
10 changed files with 149 additions and 258 deletions
@@ -4,36 +4,7 @@ import java.util.List;
public interface SmsService {
/**
* 单发
* @param loginName 工号
* @param content 内容
*/
void send(String loginName, String content);
void send(String mobile, String content);
/**
* 单发
* @param loginName 工号
* @param title 标题
* @param content 内容
*/
void send(String loginName, String title, String content);
/**
* 单发
* @param loginName 工号
* @param title 标题
* @param content 内容
* @param link 链接
*/
void send(String loginName, String title, String content, String link);
/**
* 群发 多人接收内容相同时使用该方法
* @param loginNames 工号
* @param title 标题
* @param content 内容
* @param link 链接
*/
void massSend(List<String> loginNames, String title, String content, String link);
void massSend(List<String> mobile, String content);
}
@@ -0,0 +1,73 @@
package com.budwk.app.base.sms.impl;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import com.budwk.app.base.sms.SmsService;
import com.budwk.app.web.commons.base.Globals;
import com.google.common.net.MediaType;
import lombok.extern.slf4j.Slf4j;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import java.util.List;
import java.util.stream.Collectors;
/**
* @ClassName SmsWhcpServiceImpl
* @Author JyuHsin
* @Date 2026/3/16 14:08
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean
public class SmsWhcpServiceImpl implements SmsService {
@Override
public void send(String mobile, String content) {
this.doSend(mobile, content);
}
@Override
public void massSend(List<String> mobiles, String content) {
if (Lang.isEmpty(mobiles)) {
return;
}
String formatMobile = String.join(",", mobiles);
this.doSend(formatMobile, content);
}
private void doSend(String mobile, String content) {
if (!Globals.sso) {
log.info("【log】本地开发环境,不允许调用短信消息接口");
return;
}
String urls = "https://api-shss.zthysms.com/v2/sendSms";
long tKey = System.currentTimeMillis() / 1000;
JSONObject json = new JSONObject();
json.put("username", "whszyxy");
json.put("password", SecureUtil.md5(SecureUtil.md5("Whszyxy123") + tKey));
json.put("tKey", tKey + "");
json.put("mobile", mobile);
json.put("content", "【智慧工会】" + content);
log.info("【log】调用短信消息接口,请求参数:{}", Json.toJson(json));
String result = HttpRequest.post(urls)
.header("Content-Type", "application/json")
.timeout(60000)
.body(json.toJSONString()).execute().body();
log.info("【log】调用短信消息接口,接口响应数据:{}", result);
JSONObject resultJsonBody = JSONObject.parseObject(result);
if (resultJsonBody.getInteger("code") == 200) {
log.info("【log】调用短信消息接口成功");
} else {
log.info("【log】调用短信消息接口失败:{}", resultJsonBody.getString("msg"));
}
}
}
@@ -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 = "1430576717768122368";
private static final String APP_SECRET = "19A0ACB03FBQHL4R4XQD";
private static final String TOKEN_URL = "https://gateway.jshvc.edu.cn/token/gateway/accessToken";
private static final String MSG_URL = "https://gateway.jshvc.edu.cn/mp/restful/v2/message/send";
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 "";
}
}
@@ -4,6 +4,8 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.vo.LabelValueVO;
import com.budwk.app.flow.constant.FlowConst;
@@ -12,10 +14,7 @@ import com.budwk.app.flow.engine.event.ProcessEvent;
import com.budwk.app.flow.engine.event.ProcessPublisher;
import com.budwk.app.flow.engine.model.ProcessModel;
import com.budwk.app.flow.engine.model.TaskModel;
import com.budwk.app.flow.entity.Candidate;
import com.budwk.app.flow.entity.ProcessDefine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.*;
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.flow.enums.ProcessTaskPerformTypeEnum;
@@ -147,7 +146,7 @@ public class FlowCommonController {
@ApiOperation("获取流程定义的审批记录")
public Result approvalRecord(Long instanceId) {
List<ProcessTask> processTaskList = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", instanceId)
.and(ProcessTask::getTaskState, "not in", List.of(ProcessTaskStateEnum.DOING, ProcessTaskStateEnum.WITHDRAW, ProcessTaskStateEnum.ABANDON))
.and(ProcessTask::getTaskState, "not in", List.of(ProcessTaskStateEnum.WITHDRAW, ProcessTaskStateEnum.ABANDON))
.asc(ProcessTask::getUpdatedAt));
List<ProcessTaskVO> res = new ArrayList<>();
processTaskList.forEach(processTask -> {
@@ -155,6 +154,11 @@ public class FlowCommonController {
if (vo.getTaskState().equals(ProcessTaskStateEnum.FINISHED.getCode()) && StrUtil.isNotBlank(vo.getOperator())) {
}
if(vo.getTaskState().equals(ProcessTaskStateEnum.DOING.getCode())) {
List<ProcessTaskActor> taskActorList = dao.query(ProcessTaskActor.class, Cnd.where(ProcessTaskActor::getProcessTaskId, "=", processTask.getId()));
List<JSONObject> list = JSONUtil.toList(JSONUtil.toJsonStr(taskActorList), JSONObject.class);
vo.setTaskActorList(list);
}
res.add(vo);
});
@@ -59,12 +59,9 @@ public class ProcessTaskStartEventListener implements ProcessEventListener {
for (ProcessTaskActor taskActor : taskActors) {
// 模拟发送消息
String message = StrUtil.format("您有一条待办任务,实例:{},处理环节:{}", taskActor.getActorName(), instanceName, taskDisplayName);
String message = StrUtil.format("您有一条待办任务,实例:{},处理环节:{}", instanceName, taskDisplayName);
log.info(message);
globalMessageSendService.sendMessage(taskDisplayName, message, 2, List.of(taskActor.getActorId()), null);
}
}
}
@@ -5,6 +5,7 @@ import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.event.role.RoleEventMsg;
@@ -22,6 +23,7 @@ import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
@@ -380,6 +382,29 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
RoleEventPublisher.broadcast(new RoleEventMsg(unitId, RoleConstant.PROPOSAL_BRANCH_SCHOOL_LEADER.name(), RoleEventMsg.RENEW_ROLE));
}
Set<String> sourceSet = sources.stream().map(Sys_user_source::getLoginname).filter(StrUtil::isNotBlank).collect(Collectors.toSet());
Set<String> userSet = userMap.keySet();
Set<String> notExist = Sets.difference(userSet, sourceSet).immutableCopy();
if (Lang.isNotEmpty(notExist)) {
executorService.execute(() -> {
try {
log.info("用户表中有数据,但数据源没有数据: {} 个", notExist.size());
// 批量处理,避免IN子句过长
List<List<String>> batches = ListUtil.split(notExist.stream().toList(), 500);
for (List<String> batch : batches) {
try {
dao.update(Sys_user.class, Chain.make("userState", "退休").add("member", false), Cnd.where("id", "in", batch));
dao.clear(Sys_user_role.class, Cnd.where("userId", "in", batch).and("roleId", "=", memberRole.getId()));
} catch (Exception e) {
log.error("设置退休异常(用户表中有数据,但数据源没有数据)", e);
}
}
} catch (Exception e) {
log.error("设置退休异常(用户表中有数据,但数据源没有数据)", e);
}
});
}
long endTime = System.currentTimeMillis();
log.info("全量更新用户数据完成,耗时: {} 毫秒", (endTime - startTime));
@@ -57,6 +57,6 @@ public class SysUserAllRenewJob implements Job {
param.setUpdateMode(SysDataUpdateMode.ALL.name());
param.setConditionGroup(conditionGroup);
// sysDataUserUpdateService.update(param);
sysDataUserUpdateService.update(param);
}
}
@@ -15,7 +15,7 @@ import java.util.Base64;
public class CspProcessor extends AbstractProcessor {
public void process(ActionContext ac) throws Throwable {
// 1. 生成 nonce
/*// 1. 生成 nonce
String nonce = Base64.getEncoder().encodeToString(
java.security.SecureRandom.getInstanceStrong().generateSeed(16)
);
@@ -34,7 +34,7 @@ public class CspProcessor extends AbstractProcessor {
"frame-ancestors 'self'; " +
"object-src 'self';";
ac.getResponse().setHeader("Content-Security-Policy", csp);
ac.getResponse().setHeader("Content-Security-Policy", csp);*/
doNext(ac);
}
@@ -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() {
@@ -32,10 +35,10 @@ public class SmsMessageSendStrategy implements GlobalMessageSendStrategy {
return true; // 短信发送支持异步
}
// @Override
// public boolean isEnabled() {
// return true;
// }
@Override
public boolean isEnabled() {
return true;
}
@Override
public void send(String title, String content, Integer type, List<String> receiverIds, JSONObject config) {
@@ -80,51 +83,10 @@ public class SmsMessageSendStrategy implements GlobalMessageSendStrategy {
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.send(mobile, content);
} catch (Exception e) {
log.error("发送短信给用户{}时出错:{}", user.getUsername(), e.getMessage(), e);
}
}
}
/**
* 构建短信内容
*/
private String buildSmsContent(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);
}
// 限制短信长度(一般短信限制70个字符)
String smsContent = sb.toString();
if (smsContent.length() > 67) {
smsContent = smsContent.substring(0, 67) + "...";
}
return smsContent;
}
}
@@ -141,18 +141,18 @@
<body>
<div id="snaker-flow-preview">
<snaker-flow-designer
ref="designer"
v-model="flowData"
:show-doc="false"
:viewer="true"
node-render-type="html"
:high-light="highLight"
ref="designer"
v-model="flowData"
:show-doc="false"
:viewer="true"
node-render-type="html"
:high-light="highLight"
></snaker-flow-designer>
<div
v-if="showApprovalCard"
class="approval-card"
:style="{
v-if="showApprovalCard"
class="approval-card"
:style="{
left: cardPosition.x+'px',
top: cardPosition.y+'px'
}"
@@ -165,11 +165,11 @@
<div class="tab-container" v-if="approvalInfos.length > 1">
<div class="tab-header">
<div
v-for="(item, index) in approvalInfos"
:key="index"
class="tab-item"
:class="{active: activeTabIndex === index}"
@click="activeTabIndex = index"
v-for="(item, index) in approvalInfos"
:key="index"
class="tab-item"
:class="{active: activeTabIndex === index}"
@click="activeTabIndex = index"
>
记录{{index + 1}}
</div>
@@ -187,6 +187,9 @@
<span class="info-value" v-if="currentNode.id === 'startTask'">
{{approvalInfo.ext?.initiatorName}}({{approvalInfo.ext?.initiatorAccount}})
</span>
<span class="info-value" v-else-if="approvalInfo.taskState === 10">
{{ approvalInfo.taskActorList.map(u => u.actorName + '(' + u.actorAccount + ')').join('、') }}
</span>
<span class="info-value"
v-else>{{approvalInfo.taskFormData.userName}}({{approvalInfo.taskFormData?.loginName}})</span>
</div>
@@ -196,23 +199,29 @@
</div>
<div class="info-row">
<span class="info-label">提交结果:</span>
<span class="info-value">{{getSubmitTypeText(approvalInfo.ext.submitType)}}</span>
<span class="info-value" v-if="approvalInfo.taskState !== 10">
{{getSubmitTypeText(approvalInfo.ext.submitType)}}
</span>
<span class="info-value" v-else>
暂未处理
</span>
</div>
<div class="info-row">
<span class="info-label">提交意见:</span>
<span class="info-value">{{approvalInfo.taskFormData.opinion}}</span>
<span class="info-value" v-if="approvalInfo.taskState !== 10">{{approvalInfo.taskFormData.opinion}}</span>
<span class="info-value" v-else>暂未处理</span>
</div>
<!-- <div class="divider"></div>-->
<div class="info-row">
<span class="info-label">提交时间:</span>
<span class="info-value">{{approvalInfo.finishTime}}</span>
<span class="info-value">{{approvalInfo.finishTime || '暂未处理'}}</span>
</div>
</div>
</div>
</div>
</div>
</body>
<script nonce="${cspNonce!}">
<script>
Vue.use(SnakerflowDesigner.default)
const vue = new Vue({
el: "#snaker-flow-preview",
@@ -290,7 +299,7 @@
if (data.type !== "snaker:task") return
const approvalInfos = this.hisApproval.filter((item) => item.taskName === data.id && item.taskState === 20)
const approvalInfos = this.hisApproval.filter((item) => item.taskName === data.id && [10, 20].includes(item.taskState))
if (approvalInfos && approvalInfos.length > 0) {
this.approvalInfos = approvalInfos