This commit is contained in:
2025-08-26 10:01:48 +08:00
40 changed files with 987 additions and 140 deletions

View File

@@ -9,7 +9,10 @@ import com.srs.common.core.controller.BaseController;
// OkHttp 显式导入
import com.srs.common.exception.ServiceException;
import com.srs.common.utils.SecurityUtils;
import com.srs.common.utils.ServletUtils;
import com.srs.framework.web.service.TokenService;
import com.srs.teacher.domain.dto.ConversationDTO;
import com.srs.web.core.config.DifyConfig;
import okhttp3.*;
// Spring 显式导入(不要用 *)
@@ -41,60 +44,8 @@ import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/aitutor/aichat")
public class AiChatController extends BaseController {
/**
* Dify API的访问密钥
* 用于身份验证授权访问Dify服务
*/
private static final String DIFY_API_KEY = "app-BfPtBZDNBuHOS9K1PaZrxQYE";
/**
* Dify API的URL地址
* 用于发送聊天消息请求到Dify服务
*/
private static final String DIFY_API_URL = "http://172.16.129.101/v1:6100/v1/chat-messages";
/**
* Dify反馈API的基础URL
* 用于提交消息反馈(点赞、点踩等)
*/
private static final String DIFY_FEEDBACK_BASE_URL = "http://172.16.129.101/v1:6100/v1/messages";
/**
* Dify获取反馈API的基础URL
* 用于获取消息反馈(点赞、点踩等)
*/
private static final String DIFY_API_FEEDBACK_URL = "http://172.16.129.101/v1:6100/v1/app/feedbacks?page=";
/**
* Dify消息历史记录API的基础URL
* 用于获取消息历史记录
*/
private static final String DIFY_API_HISTORY_URL = "http://172.16.129.101/v1:6100/v1/messages";
/**
* Dify会话API的基础URL
* 用于获取会话列表
*/
private static final String DIFY_CONVERSATIONS_URL = "http://172.16.129.101/v1:6100/v1/conversations";
/**
* Dify文件上传API的URL地址
* 用于上传文件到Dify服务支持图文多模态理解
*/
private static final String DIFY_FILES_URL = "http://172.16.129.101/v1:6100/v1/files/upload";
/**
* Redis中会话ID的key前缀
*/
private static final String CONVERSATION_ID_CACHE_PREFIX = "dify:conversation:id:";
/**
* Redis中会话ID的过期时间小时
*/
private static final int CONVERSATION_ID_CACHE_EXPIRE_HOURS = 1;
@Autowired
private TokenService tokenService;
/**
* HTTP客户端实例
* 配置了5分钟的读取超时时间用于与Dify API进行通信
@@ -112,6 +63,13 @@ public class AiChatController extends BaseController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* Dify配置类
* 用于获取Dify的API密钥和其他配置参数
*/
@Autowired
private DifyConfig difyConfig;
/** 为本次请求设置 “总超时”(含连接/读写),避免无限挂起 */
private Response execWithTimeouts(Request req, int callSecs, int readSecs, int writeSecs) throws IOException {
OkHttpClient shortClient = client.newBuilder()
@@ -162,11 +120,13 @@ public class AiChatController extends BaseController {
// 在主线程中获取当前用户名,避免在异步线程中获取安全上下文
String currentUsername = SecurityUtils.getLoginUser().getUsername();
// 获取JWT token
String currentUserToken = tokenService.getToken(ServletUtils.getRequest());
// String currentUserToken = SecurityUtils.getLoginUser().getToken();
// 异步执行请求处理,避免阻塞主线程
CompletableFuture.runAsync(() -> {
try {
sendToDifyAndStream(requestData, emitter, currentUsername);
sendToDifyAndStream(requestData, emitter, currentUsername, currentUserToken);
} catch (Exception e) {
e.printStackTrace();
try {
@@ -192,7 +152,8 @@ public class AiChatController extends BaseController {
* @param currentUsername
* @throws IOException 当网络请求或IO操作失败时抛出
*/
private void sendToDifyAndStream(Map<String, Object> requestData, SseEmitter emitter, String currentUsername)
private void sendToDifyAndStream(Map<String, Object> requestData, SseEmitter emitter, String currentUsername,
String currentUserToken)
throws IOException {
// 构建请求体参数
@@ -229,9 +190,9 @@ public class AiChatController extends BaseController {
inputs.put("user_name", userName);
}
Object userToken = requestData.get("user_token");
if (userToken != null) {
inputs.put("user_token", userToken);
// Object userToken = requestData.get("user_token");
if (currentUserToken != null) {
inputs.put("user_token", currentUserToken);
}
Object userRole = requestData.get("user_role");
@@ -239,6 +200,11 @@ public class AiChatController extends BaseController {
inputs.put("user_role", userRole);
}
Object userPlatform = requestData.get("user_platform");
if (userPlatform != null) {
inputs.put("user_platform", userPlatform);
}
bodyMap.put("inputs", inputs);
// 自动为对话生成名称
@@ -252,8 +218,8 @@ public class AiChatController extends BaseController {
// 构建HTTP请求
Request httpRequest = new Request.Builder()
.url(DIFY_API_URL) // 设置请求URL
.addHeader("Authorization", "Bearer " + DIFY_API_KEY) // 添加认证头
.url(difyConfig.getApiUrl()) // 设置请求URL
.addHeader("Authorization", "Bearer " + difyConfig.getApiKey()) // 添加认证头
.addHeader("Content-Type", "application/json") // 设置内容类型
.post(body) // 设置为POST请求
.build();
@@ -383,8 +349,8 @@ public class AiChatController extends BaseController {
// 调用 Dify API
Request request = new Request.Builder()
.url(DIFY_FEEDBACK_BASE_URL + "/" + messageId + "/feedbacks")
.addHeader("Authorization", "Bearer " + DIFY_API_KEY)
.url(difyConfig.getFeedbackBaseUrl() + "/" + messageId + "/feedbacks")
.addHeader("Authorization", "Bearer " + difyConfig.getApiKey())
.addHeader("Content-Type", "application/json")
.post(body)
.build();
@@ -430,8 +396,8 @@ public class AiChatController extends BaseController {
// 构建请求
Request request = new Request.Builder()
.url(DIFY_API_FEEDBACK_URL + page + "&limit=" + limitValue)
.addHeader("Authorization", "Bearer " + DIFY_API_KEY)
.url(difyConfig.getApiFeedbackUrl() + page + "&limit=" + limitValue)
.addHeader("Authorization", "Bearer " + difyConfig.getApiKey())
.addHeader("Content-Type", "application/json")
.get()
.build();
@@ -504,9 +470,9 @@ public class AiChatController extends BaseController {
@PreAuthorize("@ss.hasPermi('cph:teacher:list')")
@GetMapping("/getMessagesToAdmin")
public AjaxResult getMessagesToAdmin(@RequestParam String user,
@RequestParam(required = false) String firstId,
@RequestParam(defaultValue = "20") int limit,
@RequestParam(defaultValue = "-updated_at") String sortBy) {
@RequestParam(required = false) String firstId,
@RequestParam(defaultValue = "20") int limit,
@RequestParam(defaultValue = "-updated_at") String sortBy) {
try {
// 首先尝试从Redis缓存中获取会话ID
@@ -532,7 +498,7 @@ public class AiChatController extends BaseController {
return successResult;
} catch (IOException e) {
e.printStackTrace();
return AjaxResult.error("网络请求失败,请稍后重试");
return AjaxResult.error("网络请求失败,请稍后重试getMessagesToAdmin");
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.error("请求处理失败: " + e.getMessage());
@@ -542,8 +508,8 @@ public class AiChatController extends BaseController {
// 权限标识为学生
@GetMapping("/getMessagesToUser")
public AjaxResult getMessagesToUser(@RequestParam(required = false) String firstId,
@RequestParam(defaultValue = "20") int limit,
@RequestParam(defaultValue = "-updated_at") String sortBy) {
@RequestParam(defaultValue = "20") int limit,
@RequestParam(defaultValue = "-updated_at") String sortBy) {
try {
String user = SecurityUtils.getLoginUser().getUsername();
@@ -571,7 +537,7 @@ public class AiChatController extends BaseController {
return successResult;
} catch (IOException e) {
e.printStackTrace();
return AjaxResult.error("网络请求失败,请稍后重试");
return AjaxResult.error("网络请求失败,请稍后重试getMessagesToUser");
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.error("请求处理失败: " + e.getMessage());
@@ -592,7 +558,7 @@ public class AiChatController extends BaseController {
}
user = user.trim();
String cacheKey = CONVERSATION_ID_CACHE_PREFIX + user;
String cacheKey = difyConfig.getConversationCachePrefix() + user;
// 从Redis中获取会话ID
String conversationId = stringRedisTemplate.opsForValue().get(cacheKey);
@@ -606,7 +572,7 @@ public class AiChatController extends BaseController {
/**
* 将会话ID与用户绑定并存储到Redis中
*
* @param user 用户名
* @param user 用户名
* @param conversationId 会话ID
*/
private void cacheConversationId(String user, String conversationId) {
@@ -624,11 +590,11 @@ public class AiChatController extends BaseController {
conversationId = conversationId.trim();
// 将用户与会话ID绑定存储到Redis中
String cacheKey = CONVERSATION_ID_CACHE_PREFIX + user;
String cacheKey = difyConfig.getConversationCachePrefix() + user;
stringRedisTemplate.opsForValue().set(
cacheKey,
conversationId,
Duration.ofHours(CONVERSATION_ID_CACHE_EXPIRE_HOURS));
Duration.ofHours(difyConfig.getConversationCacheExpireHours()));
} catch (Exception e) {
System.out.println("绑定会话ID时发生错误: " + e.getMessage());
}
@@ -640,7 +606,7 @@ public class AiChatController extends BaseController {
public List<ConversationDTO> getConversations(String user, String lastId, int limit, String sortBy)
throws IOException {
// 构建带查询参数的 URL
HttpUrl.Builder urlBuilder = HttpUrl.parse(DIFY_CONVERSATIONS_URL).newBuilder();
HttpUrl.Builder urlBuilder = HttpUrl.parse(difyConfig.getConversationsUrl()).newBuilder();
urlBuilder.addQueryParameter("user", user);
if (lastId != null && !lastId.trim().isEmpty()) {
urlBuilder.addQueryParameter("last_id", lastId);
@@ -651,7 +617,7 @@ public class AiChatController extends BaseController {
// 构建请求
Request request = new Request.Builder()
.url(urlBuilder.build())
.addHeader("Authorization", "Bearer " + DIFY_API_KEY)
.addHeader("Authorization", "Bearer " + difyConfig.getApiKey())
.get()
.build();
@@ -723,7 +689,7 @@ public class AiChatController extends BaseController {
int finalLimit = limit != null && limit > 0 ? Math.min(limit, 100) : 20;
// 构建请求 URL
HttpUrl.Builder urlBuilder = HttpUrl.parse(DIFY_API_HISTORY_URL).newBuilder();
HttpUrl.Builder urlBuilder = HttpUrl.parse(difyConfig.getApiHistoryUrl()).newBuilder();
urlBuilder.addQueryParameter("conversation_id", conversationId);
urlBuilder.addQueryParameter("user", user);
if (firstId != null && !firstId.trim().isEmpty()) {
@@ -733,7 +699,7 @@ public class AiChatController extends BaseController {
Request request = new Request.Builder()
.url(urlBuilder.build())
.addHeader("Authorization", "Bearer " + DIFY_API_KEY)
.addHeader("Authorization", "Bearer " + difyConfig.getApiKey())
.addHeader("Accept", "application/json")
.get()
.build();
@@ -821,8 +787,8 @@ public class AiChatController extends BaseController {
// 构建请求
Request request = new Request.Builder()
.url(DIFY_FILES_URL)
.addHeader("Authorization", "Bearer " + DIFY_API_KEY)
.url(difyConfig.getFilesUrl())
.addHeader("Authorization", "Bearer " + difyConfig.getApiKey())
.post(requestBody)
.build();

View File

@@ -21,6 +21,7 @@ import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDate;
/**
* Dify心理问题发送企业微信 知无涯
@@ -57,18 +58,37 @@ public class WeChatMentalAlertController extends BaseController {
return AjaxResult.error("Token无效或已过期");
}
String studentId = request.getUserId();
// 查询辅导员信息
SysUser teacher = sysUserMapper.selectTeacherByStuNo(request.getUserId());
if (teacher == null || !StringUtils.hasText(teacher.getUserName())) {
log.error("辅导员信息不完整,学号: {}", request.getUserId());
return AjaxResult.error("未分配辅导员或信息不完整");
}
// 获取今天日期
LocalDate today = LocalDate.now();
// 查询该学生今天已触发的心理预警次数
int todayCount = studentMentalRatingMapper.countTodayByStudentId(studentId, today);
/* 保存学生心理问题评级 */
StudentMentalRating record = new StudentMentalRating();
record.setStudentId(request.getUserId());
record.setRating(request.getRating());
studentMentalRatingMapper.insert(record);
// 只有评级为"一级风险"时才发送企业微信通知 陈冠元
if (!"一级风险".equals(request.getRating())) {
return AjaxResult.success("预警已记录,因评级非一级风险未发送通知");
}
// === 判断是否超过当日发送上限3次=== 陈冠元
if (todayCount > 3) {
return AjaxResult.success("预警已记录,因当日已达上限未发送通知");
}
// 构建并发送消息
try {
String content = buildContent(request, teacher);
@@ -88,7 +108,7 @@ public class WeChatMentalAlertController extends BaseController {
? teacher.getNickName()
: teacher.getUserName();
return String.format(
"【心理预警通知】\n" +
"智水AI心理预警通知】\n" +
"辅导员:%s%s\n" +
"学生姓名:%s\n" +
"学号:%s\n" +

View File

@@ -0,0 +1,105 @@
package com.srs.web.core.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component // 让 Spring 扫描到
@ConfigurationProperties(prefix = "dify")
public class DifyConfig {
/**
* Dify 服务的基础 URL例如http://47.112.118.149:8100
*/
private String baseUrl;
/**
* Dify API 密钥
*/
private String apiKey;
/**
* 超时时间配置(可选)
*/
private Duration timeout = Duration.ofSeconds(30);
// ================== API URLs ==================
// 这些 URL 基于 baseUrl 自动生成,不直接暴露在配置文件中
private String apiUrl;
private String feedbackBaseUrl;
private String apiFeedbackUrl;
private String apiHistoryUrl;
private String conversationsUrl;
private String filesUrl;
// ================== Redis 缓存配置 ==================
private String conversationCachePrefix = "dify:conversation:id:";
private int conversationCacheExpireHours = 1;
// -------------------- Getters and Setters --------------------
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public Duration getTimeout() {
return timeout;
}
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
public String getApiUrl() {
return baseUrl + "/v1/chat-messages";
}
public String getFeedbackBaseUrl() {
return baseUrl + "/v1/messages";
}
public String getApiFeedbackUrl() {
return baseUrl + "/v1/app/feedbacks?page=";
}
public String getApiHistoryUrl() {
return baseUrl + "/v1/messages";
}
public String getConversationsUrl() {
return baseUrl + "/v1/conversations";
}
public String getFilesUrl() {
return baseUrl + "/v1/files/upload";
}
public String getConversationCachePrefix() {
return conversationCachePrefix;
}
public void setConversationCachePrefix(String conversationCachePrefix) {
this.conversationCachePrefix = conversationCachePrefix;
}
public int getConversationCacheExpireHours() {
return conversationCacheExpireHours;
}
public void setConversationCacheExpireHours(int conversationCacheExpireHours) {
this.conversationCacheExpireHours = conversationCacheExpireHours;
}
}

View File

@@ -21,7 +21,7 @@ srs:
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080 正式8085 测试8088
port: 8085
port: 8088
servlet:
# 应用的访问路径
context-path: /
@@ -42,6 +42,11 @@ logging:
com.srs: debug
org.springframework: warn
#dify配置
dify:
base-url: http://47.112.118.149:8100 #http://172.16.129.101:6100
api-key: app-2wjqcYI9n6igHTVHdH8qXlnh #app-BfPtBZDNBuHOS9K1PaZrxQYE
# 用户配置
user:
password:

View File

@@ -157,7 +157,7 @@ public class WeChatUtil {
String url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + accessToken;
JSONObject msg = new JSONObject();
msg.put("touser", "2023429229");
msg.put("touser", toUser);
msg.put("msgtype", "text");
msg.put("agentid", weChatConfig.getAgentId());

View File

@@ -126,4 +126,15 @@ public class SrsIdentifytableController extends BaseController {
{
return toAjax(srsIdentifytableService.deleteSrsIdentifytableByIds(ids));
}
/**
* 班级信息
*/
@Log(title = "学生鉴定信息", businessType = BusinessType.DELETE)
@GetMapping("/deptdata")
@ApiOperation("获取对应人的班级信息")
public AjaxResult deptData()
{
return success(srsIdentifytableService.deptDataLsit());
}
}

View File

@@ -55,6 +55,14 @@ public interface CphMsgMapper extends BaseMapper<CphMsg>
*/
public int deleteCphMsgById(Long id);
/**
* 根据接收人和内容中的标识来查找流程消息 (使用LIKE查询) 知无涯
*
* @param cphMsg 包含 receiver 和 content (作为模糊查询的标识) 的查询对象
* @return 匹配的消息列表
*/
public List<CphMsg> selectCphMsgListForFlowable(CphMsg cphMsg);
/**
* 批量删除消息
*

View File

@@ -2,6 +2,8 @@ package com.srs.comprehensive.mapper;
import java.util.List;
import com.srs.common.core.domain.entity.SysDept;
import com.srs.comprehensive.domain.SrsClass;
import com.srs.comprehensive.domain.SrsIdentifytable;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
@@ -107,4 +109,13 @@ public interface SrsIdentifytableMapper extends BaseMapper<SrsIdentifytable> {
* @return 结果
*/
int deleteSrsGraduataByIds(Long[] ids);
/**
* 获取对应人的班级信息
*
* @param userName 用户名称
* @return 结果
*/
List<SrsClass> selectDeptByDeptCodes(String userName);
}

View File

@@ -59,6 +59,14 @@ public interface ICphMsgService
*/
public int deleteCphMsgById(Long id);
/**
* 删除消息信息 精准匹配版
*
* @param cphMsg 消息
* @return 列表
*/
List<CphMsg> selectCphMsgListForFlowable(CphMsg cphMsg);
/**
* 根据学号查询用户ID
*

View File

@@ -3,6 +3,8 @@ package com.srs.comprehensive.service;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import com.srs.comprehensive.domain.CascaderEntity;
import com.srs.comprehensive.domain.SrsClass;
import com.srs.comprehensive.domain.SrsIdentifytable;
/**
@@ -59,4 +61,12 @@ public interface ISrsIdentifytableService extends IService<SrsIdentifytable> {
* @return 结果
*/
int deleteSrsIdentifytableById(Long id);
/**
* 获取对应人的班级信息
*
* @param
* @return 集合结果
*/
List<SrsClass> deptDataLsit();
}

View File

@@ -95,6 +95,16 @@ public class CphMsgServiceImpl extends ServiceImpl<CphMsgMapper,CphMsg> implemen
{
return cphMsgMapper.deleteCphMsgById(id);
}
/**
* 删除消息信息
*
* @param cphMsg 消息
* @return 结果
*/
@Override
public List<CphMsg> selectCphMsgListForFlowable(CphMsg cphMsg) {
return cphMsgMapper.selectCphMsgListForFlowable(cphMsg);
}
/**
* 根据学号查询用户ID

View File

@@ -1,17 +1,20 @@
package com.srs.comprehensive.service.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import com.srs.common.core.domain.entity.SysDept;
import com.srs.common.core.domain.model.LoginUser;
import com.srs.common.utils.DateUtils;
import com.srs.comprehensive.domain.GraduateStudentNews;
import com.srs.comprehensive.domain.SaveStudent;
import com.srs.comprehensive.mapper.GraduateTextMapper;
import com.srs.comprehensive.mapper.SrsGraduateStudentMapper;
import com.srs.common.utils.SecurityUtils;
import com.srs.comprehensive.domain.*;
import com.srs.comprehensive.mapper.*;
import com.srs.comprehensive.service.ICascaderDataStudentService;
import com.srs.comprehensive.util.FileUploadUtil;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import com.srs.comprehensive.mapper.SrsIdentifytableMapper;
import com.srs.comprehensive.domain.SrsIdentifytable;
import com.srs.comprehensive.service.ISrsIdentifytableService;
/**
@@ -156,4 +159,23 @@ public class SrsIdentifytableServiceImpl extends ServiceImpl<SrsIdentifytableMap
public int deleteSrsIdentifytableById(Long id) {
return srsIdentifytableMapper.deleteSrsIdentifytableById(id);
}
/**
* 获取对应人的班级信息
*
* @param
* @return 集合结果
*/
@Override
public List<SrsClass> deptDataLsit() {
//获取登录人的信息
LoginUser loginUser = SecurityUtils.getLoginUser();
List<SrsClass> depts = srsIdentifytableMapper.selectDeptByDeptCodes(loginUser.getUsername());
if(depts != null && depts.size() > 0) {
return depts;
}
return new ArrayList<>();
}
}

View File

@@ -781,6 +781,12 @@
from sys_performance as a
where (a.shstatus IS NOT NULL and a.ksstatus IS NULL) -- 科室复核待办
or (a.ksstatus IS NOT NULL and a.xgstatus IS NULL) -- 学工处长待办
-- 邵政文-(宿舍管理-住宿费用-辅导员确认待办)
union
select concat('zsfy-',count(a.id)) as `all`
from dms_new_record as a
left join view_dms_record as d on a.stu_no = d.stu_no
where d.employee_id = #{tNo} and a.apply_status = 6
</select>

View File

@@ -84,4 +84,16 @@
<select id="getUserIdByStuNo" parameterType="String" resultType="Long">
select user_id from sys_user where user_name = #{stuNo}
</select>
<select id="selectCphMsgListForFlowable" parameterType="CphMsg" resultMap="CphMsgResult">
<include refid="selectCphMsgVo"/>
<where>
<if test="receiver != null "> and receiver = #{receiver}</if>
<if test="content != null and content != ''">
and content LIKE CONCAT('%', #{content}, '%')
</if>
</where>
order by id desc
</select>
</mapper>

View File

@@ -54,6 +54,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where id = #{id}
</select>
<!-- 班级信息 -->
<select id="selectDeptByDeptCodes" parameterType="String" resultType="com.srs.comprehensive.domain.SrsClass">
SELECT DISTINCT v.class_id,v.class_name
FROM srs_dev.view_stu_info v
JOIN srs_class c ON v.teacher_id = c.teacher_id
WHERE v.t_no = #{userName}
</select>
<!-- <insert id="insertSrsIdentifytable" parameterType="SrsIdentifytable" useGeneratedKeys="true" keyProperty="id">-->
<!-- insert into srs_identifytable-->
<!-- <trim prefix="(" suffix=")" suffixOverrides=",">-->

View File

@@ -0,0 +1,55 @@
package com.srs.flowable.listener.disciplinary;
import com.srs.common.utils.WeChatUtil;
import com.srs.common.utils.spring.SpringUtils;
import com.srs.flowable.mapper.DisciplinaryMapper;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 执行监听器:在“学生教育管理科审批归档”节点开始时, 知无涯
* 向负责归档的角色发送企业微信通知。
*/
@Component
@Slf4j
public class ArchivingNotifyListener implements ExecutionListener {
@Override
public void notify(DelegateExecution delegateExecution) {
log.info("流程实例 [{}]: 已进入学生教育管理科审批归档节点,准备发送通知。", delegateExecution.getProcessInstanceId());
try {
DisciplinaryMapper disciplinaryMapper = SpringUtils.getBean(DisciplinaryMapper.class);
// 1. 【核心】定义负责归档的角色的 role_key。
final String ARCHIVING_ROLE_KEY = "xsjyglkspgd";
// 2. 根据角色Key查询用户ID列表
List<Long> userIdList = disciplinaryMapper.getApprovalByRoleKey(ARCHIVING_ROLE_KEY);
if (userIdList == null || userIdList.isEmpty()) {
log.warn("根据角色Key '{}' 未找到任何归档人员,无法发送通知。", ARCHIVING_ROLE_KEY);
return;
}
// 3. 批量查询 userName 并发送通知
List<String> userNameList = disciplinaryMapper.getUserNamesByUserIdList(userIdList);
if (!userNameList.isEmpty()) {
String toUser = String.join("|", userNameList);
WeChatUtil weChatUtil = SpringUtils.getBean(WeChatUtil.class);
String content = "您有一条新的学生违纪归档任务待处理,<a href='http://zhxg.gxsdxy.cn/web/#/pages/Approval/index'>请点击前往处理</a>。";
weChatUtil.sendTextMessage(toUser, content);
log.info("已成功向归档角色 ({}) 发送通知。接收人: {}", ARCHIVING_ROLE_KEY, toUser);
} else {
log.warn("角色 '{}' 中找到了 {} 个用户,但无人配置企业微信账号,未发送任何通知。", ARCHIVING_ROLE_KEY, userIdList.size());
}
} catch (Exception e) {
log.error("向归档角色发送企业微信通知时出现异常。流程将继续。错误详情: {}", e.getMessage(), e);
}
}
}

View File

@@ -9,7 +9,7 @@ import org.flowable.engine.delegate.ExecutionListener;
import org.springframework.stereotype.Component;
/**
* 根据辅导员部门id获取二级学院书记
* 根据辅导员部门id获取二级学院书记 知无涯
*/
@Component
@Slf4j

View File

@@ -0,0 +1,128 @@
package com.srs.flowable.listener.disciplinary; // 您可以根据需要调整包路径
import com.srs.common.utils.SecurityUtils;
import com.srs.comprehensive.domain.CphMsg;
import com.srs.comprehensive.service.ICphMsgService;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* 【通用线上监听器】为即将到来的任务创建内部“我的消息”记录。 知无涯1
* <p>
* 该监听器被设计为在SequenceFlow的'take'事件上触发。
* 它会智能地从流程变量中读取单个接收人(变量名: approval)或多个接收人(变量名: userList)
* 然后为每一位接收人创建一条内部消息。
* </p>
*/
@Component("genericMessageListener")
@Slf4j
public class GenericMessageListener implements ExecutionListener {
@Autowired
private ICphMsgService cphMsgService;
@Override
public void notify(DelegateExecution execution) {
log.info("流程实例 [{}]: 触发通用消息监听器...", execution.getProcessInstanceId());
List<Long> receiverIdList = getReceiverIds(execution);
if (receiverIdList.isEmpty()) {
return;
}
// --- 关键改动获取下一个即将创建的任务ID ---
// 注意在线上监听器中任务尚未创建所以我们无法直接获取任务ID。
// 但我们可以获取到这个“活动”的定义ID它在流程定义中是唯一的。
String activityId = execution.getCurrentActivityId();
String taskName = execution.getCurrentFlowElement() != null ? execution.getCurrentFlowElement().getName() : "";
// --- 关键改动:构建带有隐藏标识的消息内容 ---
String messageText = "您有一条新的【" + taskName + "】待办任务需要处理,";
// 我们嵌入一个隐藏的span它包含了流程实例ID和活动ID作为唯一标识
String hiddenIdentifier = String.format("<span id='flow-identifier' data-proc-inst-id='%s' data-activity-id='%s' style='display:none;'></span>",
execution.getProcessInstanceId(),
activityId);
String finalContent = messageText + hiddenIdentifier;
Long senderId = getSenderId();
for (Long receiverId : receiverIdList) {
try {
CphMsg cphMsg = new CphMsg();
cphMsg.setReceiver(receiverId);
cphMsg.setSender(senderId);
cphMsg.setContent(finalContent); // 使用包含隐藏标识的内容
cphMsgService.insertCphMsg(cphMsg);
log.info("已成功为用户 [{}] 创建内部消息 (关联 Activity ID: {})。", receiverId, activityId);
} catch (Exception e) {
log.error("为用户 [{}] 创建内部消息时发生异常: {}", receiverId, e.getMessage(), e);
}
}
}
/**
* 智能地从流程变量中解析出接收人ID列表。
* 策略是:
* 1. 优先检查 'userList' 变量,这通常用于多实例任务。
* 2. 如果 'userList' 不存在或无效,则检查 'approval' 变量,这通常用于单实例任务。
*
* @param execution 当前的执行实例
* @return 包含一个或多个用户ID的列表如果都找不到则返回空列表。
*/
@SuppressWarnings("unchecked")
private List<Long> getReceiverIds(DelegateExecution execution) {
// 优先检查 'userList' (用于多实例)
Object userListObj = execution.getVariable("userList");
if (userListObj instanceof List && !((List<?>) userListObj).isEmpty()) {
try {
// 确保列表内容是Long类型
return (List<Long>) userListObj;
} catch (ClassCastException e) {
log.error("流程变量 'userList' 不是 Long 类型的列表。", e);
}
}
// 其次检查 'approval' (用于单实例)
Object approvalObj = execution.getVariable("approval");
if (approvalObj instanceof Number) {
return Collections.singletonList(((Number) approvalObj).longValue());
}
// 兼容字符串形式的ID
if (approvalObj instanceof String) {
try {
return Collections.singletonList(Long.parseLong((String) approvalObj));
} catch (NumberFormatException e) {
log.warn("流程变量 'approval' 的值 '{}' 无法转换为Long类型。", approvalObj);
}
}
// 如果都找不到,返回一个不可变的空列表
return Collections.emptyList();
}
/**
* 安全地获取当前操作的发送人ID。
* 在异步执行的监听器中,可能无法获取到安全上下文,因此提供了默认值。
*
* @return 发送人用户ID默认为1L (系统管理员)。
*/
private Long getSenderId() {
try {
Long userId = SecurityUtils.getUserId();
// 如果SecurityUtils返回null也使用默认值
return userId != null ? userId : 1L;
} catch (Exception e) {
log.warn("在监听器中获取发送人ID时发生异常将使用默认系统管理员ID(1L)。异常信息: {}", e.getMessage());
return 1L; // 默认为系统管理员
}
}
}

View File

@@ -0,0 +1,61 @@
package com.srs.flowable.listener.disciplinary;
import com.srs.common.utils.WeChatUtil;
import com.srs.common.utils.spring.SpringUtils;
import com.srs.flowable.mapper.DisciplinaryMapper;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import org.springframework.stereotype.Component;
/**
* 执行监听器:在“申请人(辅导员)接收”节点开始时, 知无涯
* 向流程发起人发送企业微信通知,告知其结果已出。
*/
@Component
@Slf4j
public class InitiatorResultListener implements ExecutionListener {
@Override
public void notify(DelegateExecution delegateExecution) {
log.info("流程实例 [{}]: 已进入辅导员接收结果节点,准备向发起人发送通知。", delegateExecution.getProcessInstanceId());
try {
DisciplinaryMapper disciplinaryMapper = SpringUtils.getBean(DisciplinaryMapper.class);
// 1. 从流程变量中获取发起人的ID。Flowable会自动将发起人ID存入名为 "INITIATOR" 的变量中。
// 这个变量通常是 String 类型。
Object initiatorObj = delegateExecution.getVariable("INITIATOR");
if (initiatorObj == null) {
log.warn("在流程实例 [{}] 中未找到发起人(INITIATOR)变量,无法发送通知。", delegateExecution.getProcessInstanceId());
return;
}
// 2. 将发起人ID转换为 Long 类型,以便查询数据库
Long initiatorUserId;
try {
initiatorUserId = Long.parseLong(initiatorObj.toString());
} catch (NumberFormatException e) {
log.error("发起人(INITIATOR)变量 '{}' 无法转换为Long类型无法发送通知。", initiatorObj);
return;
}
// 3. 使用 initiatorUserId 查询对应的企业微信 userName
String initiatorUserName = disciplinaryMapper.getUserNameByUserId(initiatorUserId);
if (initiatorUserName != null && !initiatorUserName.isEmpty()) {
// 4. 发送通知
WeChatUtil weChatUtil = SpringUtils.getBean(WeChatUtil.class);
String content = "您提交的学生违纪处分申请已有最终处理结果,<a href='http://zhxg.gxsdxy.cn/web/#/pages/Approval/index'>请点击前往处理</a>。\"";
weChatUtil.sendTextMessage(initiatorUserName, content);
log.info("已成功向流程发起人 ({}) 发送结果通知。", initiatorUserName);
} else {
log.warn("找到了发起人(userId:{}),但其对应的企业微信账号(userName)为空,未发送通知。", initiatorUserId);
}
} catch (Exception e) {
log.error("向流程发起人发送结果通知时出现异常。流程将继续。错误详情: {}", e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,99 @@
package com.srs.flowable.listener.disciplinary; // 您可以根据需要调整包路径
import com.srs.comprehensive.domain.CphMsg;
import com.srs.comprehensive.service.ICphMsgService;
import lombok.extern.slf4j.Slf4j;
import org.flowable.task.service.delegate.DelegateTask;
import org.flowable.task.service.delegate.TaskListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* 【任务监听器】在任务完成后,删除内部消息。
* <p>
* 该监听器被设计为在UserTask的'complete'事件上触发。
* 它通过解析消息内容中的隐藏HTML标识来找到与当前已完成任务关联的消息
* 并将其删除。
* </p>
*/
@Component("messageProcessorListener") // 定义一个清晰的Bean名称
@Slf4j
public class MessageProcessorListener implements TaskListener {
@Autowired
private ICphMsgService cphMsgService;
/**
* 依赖检查方法,确保关键服务已注入。
*/
@PostConstruct
public void checkDependencies() {
Assert.notNull(cphMsgService, "ICphMsgService dependency not injected for MessageProcessorListener!");
}
@Override
public void notify(DelegateTask delegateTask) {
// 步骤 1: 获取当前任务的办理人ID
String assigneeIdStr = delegateTask.getAssignee();
if (assigneeIdStr == null || assigneeIdStr.isEmpty()) {
log.warn("任务 [ID: {}] 没有办理人,无法处理消息。", delegateTask.getId());
return;
}
Long receiverId;
try {
receiverId = Long.parseLong(assigneeIdStr);
} catch (NumberFormatException e) {
log.error("任务办理人ID '{}' 无法转换为Long类型。", assigneeIdStr);
return;
}
// 步骤 2: 获取用于查找消息的唯一标识
String processInstanceId = delegateTask.getProcessInstanceId();
// taskDefinitionKey 对应流程图中节点的ID即我之前在 GenericMessageListener 中存入的 activityId
String activityId = delegateTask.getTaskDefinitionKey();
log.info("任务 [ID: {}, Name: {}] (办理人: {}) 已完成。开始查找并处理关联的内部消息...",
delegateTask.getId(), delegateTask.getName(), receiverId);
log.debug("用于查找的关联标识 -> ProcInst ID: [{}], Activity ID: [{}]", processInstanceId, activityId);
try {
// 步骤 3: 构造用于模糊查询的、唯一的标识字符串
// 这个字符串必须与 GenericMessageListener 中生成该标识的逻辑完全一致
String searchIdentifier = String.format("data-proc-inst-id='%s' data-activity-id='%s'",
processInstanceId,
activityId);
// 步骤 4: 创建查询参数并调用服务
CphMsg queryParam = new CphMsg();
queryParam.setReceiver(receiverId);
queryParam.setContent(searchIdentifier); // 将标识作为模糊查询的内容
// 注意:这一步要求 CphMsgMapper.xml 中的 selectCphMsgListForFlowable 方法支持对 content 字段的 LIKE 查询
List<CphMsg> messagesToProcess = cphMsgService.selectCphMsgListForFlowable(queryParam);
if (messagesToProcess.isEmpty()) {
log.info("未找到需要为用户 [{}] 处理的、与当前任务关联的消息。", receiverId);
return;
}
// 步骤 5: 遍历所有找到的消息,并进行处理(删除)
for (CphMsg msg : messagesToProcess) {
try {
cphMsgService.deleteCphMsgById(msg.getId());
log.info("已成功删除与任务关联的消息 [ID: {}]。", msg.getId());
} catch (Exception e) {
// 捕获单个消息删除失败的异常,确保不影响其他消息的处理
log.error("删除消息 [ID: {}] 时发生异常: {}", msg.getId(), e.getMessage(), e);
}
}
} catch (Exception e) {
// 捕获整个处理过程中的异常,确保不影响主流程的流转
log.error("处理用户 [{}] 的内部消息时发生严重异常: {}", receiverId, e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,55 @@
package com.srs.flowable.listener.disciplinary;
import com.srs.common.utils.WeChatUtil;
import com.srs.common.utils.spring.SpringUtils;
import com.srs.flowable.mapper.DisciplinaryMapper;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 执行监听器:在“校领导审批”节点开始时,向校领导发送企业微信通知。 知无涯
* (简化版:因为流程能到达此节点,即表示前序节点已全票通过)
*/
@Component
@Slf4j
public class SchoolLeaderApprovalListener implements ExecutionListener {
@Override
public void notify(DelegateExecution delegateExecution) {
log.info("流程实例 [{}]: 已进入校领导审批节点,准备发送通知。", delegateExecution.getProcessInstanceId());
try {
DisciplinaryMapper disciplinaryMapper = SpringUtils.getBean(DisciplinaryMapper.class);
// 1. 【核心】定义“校领导”这个角色的 role_key
final String LEADER_ROLE_KEY = "xldsp";
// 2. 根据角色Key查询用户ID列表
List<Long> userIdList = disciplinaryMapper.getApprovalByRoleKey(LEADER_ROLE_KEY);
if (userIdList == null || userIdList.isEmpty()) {
log.warn("根据角色Key '{}' 未找到任何校领导用户,无法发送通知。", LEADER_ROLE_KEY);
return;
}
// 3. 批量查询 userName 并发送通知
List<String> userNameList = disciplinaryMapper.getUserNamesByUserIdList(userIdList);
if (!userNameList.isEmpty()) {
String toUser = String.join("|", userNameList);
WeChatUtil weChatUtil = SpringUtils.getBean(WeChatUtil.class);
String content = "校领导您有一条新的学生违纪审批任务待处理,<a href='http://zhxg.gxsdxy.cn/web/#/pages/Approval/index'>请点击前往处理</a>。";
weChatUtil.sendTextMessage(toUser, content);
log.info("已成功向校领导 (角色: {}) 发送通知。接收人: {}", LEADER_ROLE_KEY, toUser);
} else {
log.warn("角色 '{}' 中找到了 {} 个用户,但无人配置企业微信账号,未发送任何通知。", LEADER_ROLE_KEY, userIdList.size());
}
} catch (Exception e) {
log.error("向校领导发送企业微信通知时出现异常。流程将继续。错误详情: {}", e.getMessage(), e);
}
}
}

View File

@@ -1,12 +1,17 @@
package com.srs.flowable.listener.disciplinary;
import com.srs.common.utils.WeChatUtil;
import com.srs.common.utils.spring.SpringUtils;
import com.srs.flowable.mapper.DisciplinaryMapper;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* 设置处分学生审批参数
* 流程结束监听器。
* 在流程的最终节点,向当事学生发送处理结果的通知。 知无涯
*/
@Component
@Slf4j
@@ -14,10 +19,41 @@ public class StuInfoListener implements ExecutionListener {
@Override
public void notify(DelegateExecution delegateExecution) {
Long stuId = (Long)delegateExecution.getVariable("stuId");
// 1. 从流程变量中获取学生ID
Object stuIdObj = delegateExecution.getVariable("stuId");
log.info("学生信息:{}",stuId);
delegateExecution.setVariable("approval",stuId);
// todo 企业微信推送消息
// 安全检查确保stuId存在且类型正确
if (!(stuIdObj instanceof Long)) {
log.error("流程实例 [{}] 在结束时未能获取到有效的 'stuId' 变量,无法发送最终通知。", delegateExecution.getProcessInstanceId());
return;
}
Long stuId = (Long) stuIdObj;
log.info("流程实例 [{}] 即将结束准备向学生ID: {} 发送最终通知。", delegateExecution.getProcessInstanceId(), stuId);
// 2. 向该学生发送企业微信通知
try {
DisciplinaryMapper disciplinaryMapper = SpringUtils.getBean(DisciplinaryMapper.class);
// 根据学生ID查询其对应的企业微信账号(userName)
String userName = disciplinaryMapper.getUserNameByUserId(stuId);
// 检查是否找到了对应的企业微信账号
if (!StringUtils.hasText(userName)) {
log.warn("流程实例 [{}]: 根据学生ID '{}' 未找到对应的企业微信账号,无法发送最终通知。", delegateExecution.getProcessInstanceId(), stuId);
return; // 中止发送逻辑
}
// 3. 构建并发送通知内容
WeChatUtil weChatUtil = SpringUtils.getBean(WeChatUtil.class);
String content = "您好,关于您的违纪处理流程已办结,最终结果已生成。<a href='http://zhxg.gxsdxy.cn/web/#/pages/Disciplinary/MyDisciplinary'>请点击登录系统查看详情</a>。";
weChatUtil.sendTextMessage(userName, content);
log.info("流程实例 [{}]: 已成功向学生ID '{}' (企业微信账号: {}) 发送了流程办结通知。", delegateExecution.getProcessInstanceId(), stuId, userName);
} catch (Exception e) {
// 即使通知失败,也不应影响流程的正常结束
log.error("流程实例 [{}]: 在向学生ID '{}' 发送最终通知时出现异常。错误详情: {}",
delegateExecution.getProcessInstanceId(), stuId, e.getMessage(), e);
}
}
}
}

View File

@@ -0,0 +1,51 @@
package com.srs.flowable.listener.disciplinary;
import com.srs.common.utils.WeChatUtil;
import com.srs.common.utils.spring.SpringUtils;
import com.srs.flowable.mapper.DisciplinaryMapper;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import java.util.List;
public class XGLDSHListener implements ExecutionListener {
@Override
public void notify(DelegateExecution delegateExecution) {
DisciplinaryMapper disciplinaryMapper = SpringUtils.getBean(DisciplinaryMapper.class);
// 固定角色 Key
final String TARGET_ROLE_KEY = "xgldsp";
// 步骤 1: 查询角色下的所有审批人 userId
List<Long> userIdList = disciplinaryMapper.getApprovalByRoleKey(TARGET_ROLE_KEY);
if (userIdList == null || userIdList.isEmpty()) {
throw new RuntimeException("未找到角色 '" + TARGET_ROLE_KEY + "' 的审批人员,无法发送通知。");
}
try {
// 步骤 2: 查询 userName 列表
List<String> userNameList = disciplinaryMapper.getUserNamesByUserIdList(userIdList);
if (userNameList != null && !userNameList.isEmpty()) {
String toUser = String.join("|", userNameList);
WeChatUtil weChatUtil = SpringUtils.getBean(WeChatUtil.class);
String content = "您有一条新的学生违纪审批任务待处理," +
"<a href='http://zhxg.gxsdxy.cn/web/#/pages/Approval/index'>请点击前往处理</a>。";
// 步骤 3: 发送企业微信通知
weChatUtil.sendTextMessage(toUser, content);
// log.info("已成功向角色 '{}' 的成员发送企业微信审批通知。接收人: {}", TARGET_ROLE_KEY, toUser);
} else {
// log.warn("角色 '{}' 下找到 {} 个用户,但无对应的企业微信账号,无法发送通知。", TARGET_ROLE_KEY, userIdList.size());
}
} catch (Exception e) {
// log.error("向角色 '{}' 发送企业微信通知时出现异常,但流程将继续。错误详情: {}", TARGET_ROLE_KEY, e.getMessage(), e);
}
}
}

View File

@@ -10,7 +10,7 @@ import org.springframework.stereotype.Component;
import java.util.List;
/**
* 学生教育管理科审核节点的执行监听器。
* 学生教育管理科审核节点的执行监听器。 知无涯
* 向“学生教育管理科”角色发送企业微信通知。
*/
@Component

View File

@@ -15,7 +15,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestBody;
/**
* 根据辅导员的部门id查询该部门的学无干事人员
* 根据辅导员的部门id查询该部门的学无干事人员 知无涯
*/
@Component
@Slf4j

View File

@@ -1,55 +1,74 @@
package com.srs.flowable.listener.disciplinary;
import com.srs.common.utils.WeChatUtil;
import com.srs.common.utils.spring.SpringUtils;
import com.srs.flowable.mapper.DisciplinaryMapper;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 学院违纪处理委员会审批
* 【线上监听器】为“学院违纪处理委员会”多实例任务准备数据并发送通知 知无涯
*/
@Component
@MapperScan("com.srs.flowable.mapper")
@Component("xywjclwyhListener")
@Slf4j
public class XYWJCLWYHListener implements ExecutionListener {
// 推荐使用依赖注入而不是SpringUtils
@Autowired
private DisciplinaryMapper disciplinaryMapper;
@Autowired
private WeChatUtil weChatUtil;
@Override
public void notify(DelegateExecution delegateExecution) {
DisciplinaryMapper disciplinaryMapper = SpringUtils.getBean("disciplinaryMapper");
// 步骤 1: 根据角色Key获取审批人ID列表 (第1次DB查询)
public void notify(DelegateExecution execution) {
log.info("流程实例 [{}]: 在进入'学院违纪处理委员会'节点前,开始准备审批数据...", execution.getProcessInstanceId());
// 步骤 1: 单一数据库查询,获取所有需要的审批人信息 (ID和企微账号)
// List<DisciplinaryMapper.UserInfo> userInfos = disciplinaryMapper.getUserInfoByRoleKey("xywjclwyh");
// 注意如果您不方便修改Mapper这里可以保持原来的两次查询逻辑依然正确。
// 我们先用原始的两次查询来演示,更具普适性。
List<Long> userIdList = disciplinaryMapper.getApprovalByRoleKey("xywjclwyh");
if (userIdList == null || userIdList.isEmpty()){
log.error("未找到角色相关信息");
if (userIdList == null || userIdList.isEmpty()) {
log.error("角色 'xywjclwyh' 未配置或未找到任何有效用户,流程无法继续。");
throw new RuntimeException("学院违纪处理委员会审批人员未设置");
}else{
delegateExecution.setVariable("userList",userIdList);
// todo 企业微信推送消息
// --- 企业微信推送消息模块 ---
try {
// 步骤 2: 一次性批量查询所有有效的userName (第2次DB查询性能最优)
List<String> userNameList = disciplinaryMapper.getUserNamesByUserIdList(userIdList);
if (!userNameList.isEmpty()) {
// 步骤 3: 将 userName 列表用 "|" 连接成一个字符串
String toUser = String.join("|", userNameList);
WeChatUtil weChatUtil = SpringUtils.getBean(WeChatUtil.class);
String content = "您有一条新的学生违纪审批任务待处理,<a href='http://zhxg.gxsdxy.cn/web/#/pages/Approval/index'>请点击前往处理</a>。";
weChatUtil.sendTextMessage(toUser, content);
log.info("已成功向学院违纪处理委员会发送群组通知。接收人: {}", toUser);
} else {
log.warn("角色 'xywjclwyh' 存在审批人,但无人配置企业微信账号,未发送任何通知。");
}
} catch (Exception e) {
// 保证即使通知失败,流程也能继续
log.error("向学院违纪处理委员会发送企业微信通知时出现异常。流程将继续。错误详情: {}", e.getMessage(), e);
}
}
// 步骤 2: 设置多实例任务需要的流程变量 (这是“线上”为“节点”准备的核心数据)
execution.setVariable("userList", userIdList);
log.info("流程实例 [{}]: 已成功设置'userList'变量,包含 {} 个审批人。", execution.getProcessInstanceId(), userIdList.size());
// 步骤 3: 发送企业微信通知
sendWeChatNotification(userIdList, execution.getProcessInstanceId());
}
}
/**
* 封装并执行企业微信通知逻辑
*/
private void sendWeChatNotification(List<Long> userIdList, String processInstanceId) {
try {
List<String> userNameList = disciplinaryMapper.getUserNamesByUserIdList(userIdList);
if (userNameList == null || userNameList.isEmpty()) {
log.warn("流程实例 [{}]: 角色'xywjclwyh'存在审批人ID但无人配置企微账号未发送通知。", processInstanceId);
return;
}
String toUser = String.join("|", userNameList);
String content = "您有一条新的学生违纪审批任务待处理,<a href='http://zhxg.gxsdxy.cn/web/#/pages/Approval/index'>请点击前往处理</a>。";
weChatUtil.sendTextMessage(toUser, content);
log.info("流程实例 [{}]: 已成功向学院违纪处理委员会发送通知。接收人: {}", processInstanceId, toUser);
} catch (Exception e) {
log.error("流程实例 [{}]: 发送企微通知时出现异常,但不影响主流程。错误: {}", processInstanceId, e.getMessage(), e);
}
}
}

View File

@@ -1,11 +1,13 @@
package com.srs.flowable.listener.relieve;
import com.srs.common.utils.WeChatUtil;
import com.srs.common.utils.spring.SpringUtils;
import com.srs.flowable.mapper.LeaveMapper;
import com.srs.flowable.mapper.RelieveMapper;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
@@ -17,6 +19,9 @@ import java.util.Map;
@Slf4j
public class StuCounselorListener implements ExecutionListener {
@Autowired
private WeChatUtil weChatUtil;
@Override
public void notify(DelegateExecution delegateExecution) {
RelieveMapper relieveMapper = (RelieveMapper) SpringUtils.getBean("relieveMapper");
@@ -31,6 +36,36 @@ public class StuCounselorListener implements ExecutionListener {
delegateExecution.setVariable("deptId",map.get("deptId"));
// todo 企业微信推送消息
/**
* 庞世斌
*/
try {
Long userId = map.get("userId");
// 使用 userId 查询对应的企业微信账号
String userName = relieveMapper.getUserNameByUserId(userId);
if (userName != null && !userName.isEmpty()) {
WeChatUtil weChatUtil = SpringUtils.getBean(WeChatUtil.class);
// 构造包含超链接的消息内容
String content = "您有一条新的学生违纪审批任务待处理," +
"<a href='http://zhxg.gxsdxy.cn/web/#/pages/Approval/index'>点此查看</a>";
// 发送企业微信消息
weChatUtil.sendTextMessage(userName, content);
log.info("✅ 已成功向学务干事(userName:{})发送企业微信审批通知。", userName);
} else {
// 如果找不到 userName记录警告日志但流程继续
log.warn("⚠ 找到了审批人(userId:{}), 但其对应的企业微信账号(userName)为空,无法发送通知。", userId);
}
} catch (Exception e) {
Long userId = map.get("userId"); // 确保日志里带上 userId
log.error("❌ 向学务干事(userId:{})发送企业微信通知时出现异常,但流程将继续。错误详情:", userId, e);
}
}else {
throw new RuntimeException("获取辅导员信息失败");

View File

@@ -23,7 +23,7 @@ public interface DisciplinaryMapper {
*/
String getUserNameByUserId(@Param("userId") Long userId);
/**
* 知无涯新增:根据用户ID列表批量查询用户名列表
* 知无涯 根据用户ID列表批量查询用户名列表
* @param userIdList 用户ID列表
* @return 用户的username列表
*/

View File

@@ -11,6 +11,10 @@ public interface RelieveMapper {
Map<String,Long> getCounselorInfo(Long stuUserId);
String getUserNameByUserId ( long userId);
/**
* 查询学生解除处分申请
*

View File

@@ -113,6 +113,12 @@
<include refid="selectRtStuDisciplinaryRelieveVo"/>
where relieve_id = #{relieveId}
</select>
<!-- 根据userid获取到userName-->
<select id="getUserNameByUserId" parameterType="long" resultType="string">
SELECT user_name
FROM sys_user
WHERE user_id = #{userId}
</select>
</mapper>

View File

@@ -209,7 +209,7 @@ public class TokenService
* @param request
* @return token
*/
private String getToken(HttpServletRequest request)
public String getToken(HttpServletRequest request)
{
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))

View File

@@ -222,6 +222,14 @@ public class RtStuDisciplinaryApplication extends BaseEntity {
@Excel(name = "籍贯")
private String jg;
/**
* 政治面貌
*/
@ApiModelProperty("政治面貌")
@TableField("political_status")
@Excel(name = "政治面貌")
private String politicalStatus;
/**
* 市/县
*/

View File

@@ -214,6 +214,14 @@ public class RtStuDisciplinaryRelieve extends BaseEntity {
@Excel(name = "籍贯")
private String jg;
/**
* 政治面貌
*/
@ApiModelProperty("政治面貌")
@TableField("political_status")
@Excel(name = "政治面貌")
private String politicalStatus;
/**
* 市/县
*/

View File

@@ -265,7 +265,7 @@ public class RtStuDisciplinaryApplicationServiceImpl extends ServiceImpl<RtStuDi
// todo 企业微信推送消息
AjaxResult ajaxResult = flowDefinitionService.startProcessInstanceById("flow_n27gxm4k:8:610039", variables);
AjaxResult ajaxResult = flowDefinitionService.startProcessInstanceById("flow_n27gxm4k:22:720004", variables);
String code = ajaxResult.get("code").toString();
if (code.equals("200")) {

View File

@@ -3,16 +3,20 @@ package com.srs.routine.service.impl;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.srs.common.core.domain.AjaxResult;
import com.srs.common.core.domain.entity.SysUser;
import com.srs.common.doman.dto.ProcessResultDto;
import com.srs.common.exception.ServiceException;
import com.srs.common.utils.DateUtils;
import com.srs.common.utils.SecurityUtils;
import com.srs.common.utils.WeChatUtil;
import com.srs.flowable.service.IFlowDefinitionService;
import com.srs.routine.domain.RtStuDisciplinaryApplication;
import com.srs.routine.mapper.RtStuDisciplinaryApplicationMapper;
import com.srs.system.mapper.SysUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.IdentityService;
import org.flowable.engine.TaskService;
@@ -40,6 +44,12 @@ public class RtStuDisciplinaryRelieveServiceImpl extends ServiceImpl<RtStuDiscip
@Autowired
RtStuDisciplinaryApplicationMapper rtStuDisciplinaryApplicationMapper;
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
public WeChatUtil weChatUtil;
/**
* 查询学生解除处分申请
*
@@ -208,12 +218,49 @@ public class RtStuDisciplinaryRelieveServiceImpl extends ServiceImpl<RtStuDiscip
// 保存审核结果到任务变量中
variables.put("approved", true);
// 标记是否有任务被完成
boolean taskCompleted = false;
// 完成待办任务列表
for (Task task : tasks) {
String taskId = task.getId();
String applicationId = taskService.getVariable(taskId, "relieveId").toString();
if (applicationId.equals(rtStuDisciplinaryRelieve.getRelieveId().toString())) {
taskService.complete(task.getId(), variables);
taskCompleted = true;
}
}
// 所有相关任务完成后,发送企业微信消息
if (taskCompleted) {
try {
// 获取申请人信息
String applicantName = rtStuDisciplinaryRelieve.getStuName();
String politicalStatus = rtStuDisciplinaryRelieve.getPoliticalStatus();
SysUser sysUser = new SysUser();
sysUser.setRoleId(118L);
List<SysUser> sysUsers = sysUserMapper.selectAllocatedList(sysUser);
// 提取二级学院的书记的账号
List<String> userNames = sysUsers.stream()
.map(SysUser::getUserName)
.collect(Collectors.toList());
// 只有政治面貌是团员时才发送企业微信消息
if ("团员".equals(politicalStatus)) {
// 消息内容
String messageContent = "【解除处分】" + applicantName + "的解除处分申请已通过审核。";
int batchSize = 10;
for (int i = 0; i < userNames.size(); i += batchSize) {
List<String> batch = userNames.subList(i, Math.min(i + batchSize, userNames.size()));
// 拼接成"user1|user2|user3"格式
String toUser = String.join("|", batch);
// 调用企业微信发送消息方法
weChatUtil.sendTextMessage(toUser, messageContent);
}
}
} catch (Exception e) {
log.error("发送企业微信消息失败:", e);
}
}
return dto;

View File

@@ -117,7 +117,7 @@ public class RtStuMultiLevelReviewServiceImpl extends ServiceImpl<RtStuMultiLeve
if (result > 0) {
String messageContent = rtStuMultiLevelReview.getNotes();
if (messageContent == null || messageContent.trim().isEmpty()) {
messageContent = "你申请办理的学生证制作完成,长堽校区前往xxx领取里建校区前往xxx领取";
messageContent = "你申请办理的学生证制作完成,长堽校区前往经管楼学工处1-1办公室领取里建校区前往“一站式”学生社区大厅领取";
}
weChatUtil.sendTextMessage(rtStuMultiLevelReview.getStuNo(), messageContent);

View File

@@ -34,6 +34,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="mz" column="mz" />
<result property="birthday" column="birthday" />
<result property="jg" column="jg" />
<result property="politicalStatus" column="political_status" />
<result property="hksz2" column="hksz2" />
<result property="dispositionService" column="disposition_service" />
<result property="letterService" column="letter_service" />
@@ -44,7 +45,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select application_id, applicant_id, applicant_name, penalty_number, stu_no, stu_name, stu_id, penalty_type,
penalty_status, violation_date, expiration_date, evidence_upload, penalty_recommendation, violation_regulations,
submission_status, process_instance_id, deploy_id, create_by, create_time, update_by, update_time, remark, gender,
department_Name, grade_name, class_name, mz, birthday, jg, hksz2,disposition_service,letter_service,disciplinary_date from rt_stu_disciplinary_application
department_Name, grade_name, class_name, mz, birthday, jg,political_status,hksz2,disposition_service,letter_service,disciplinary_date from rt_stu_disciplinary_application
</sql>
<select id="selectRtStuDisciplinaryApplicationList" parameterType="RtStuDisciplinaryApplication" resultMap="RtStuDisciplinaryApplicationResult">
@@ -73,6 +74,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="mz != null and mz != ''"> and mz = #{mz}</if>
<if test="birthday != null and birthday != ''"> and birthday = #{birthday}</if>
<if test="jg != null and jg != ''"> and jg = #{jg}</if>
<if test="politicalStatus != null and politicalStatus != ''"> and political_status = #{politicalStatus}</if>
<if test="hksz2 != null and hksz2 != ''"> and hksz2 = #{hksz2}</if>
</where>
ORDER BY create_time desc
@@ -116,7 +118,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select application_id, applicant_id, applicant_name, penalty_number, stu_no, stu_name, stu_id, penalty_type,penalty_status,
violation_date, expiration_date, evidence_upload, penalty_recommendation, violation_regulations,submission_status,
process_instance_id, deploy_id, a.create_by, a.create_time, a.update_by, a.update_time, a.remark, a.gender,a.department_Name,
a.grade_name, a.class_name, a.mz, a.birthday, a.jg, a.hksz2,a.disposition_service,a.letter_service,a.disciplinary_date from rt_stu_disciplinary_application a
a.grade_name, a.class_name, a.mz, a.birthday, a.jg,a.political_status,a.hksz2,a.disposition_service,a.letter_service,a.disciplinary_date from rt_stu_disciplinary_application a
LEFT JOIN sys_user b ON a.applicant_id = b.user_id
LEFT JOIN cph_teacher c ON b.user_name = c.employee_id
<where>
@@ -175,6 +177,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="mz != null">mz,</if>
<if test="birthday != null">birthday,</if>
<if test="jg != null">jg,</if>
<if test="politicalStatus != null">political_status,</if>
<if test="hksz2 != null">hksz2,</if>
<if test="dispositionService != null">disposition_service,</if>
<if test="letterService != null">letter_service,</if>
@@ -208,6 +211,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="mz != null">#{mz},</if>
<if test="birthday != null">#{birthday},</if>
<if test="jg != null">#{jg},</if>
<if test="politicalStatus != null">#{politicalStatus},</if>
<if test="hksz2 != null">#{hksz2},</if>
<if test="dispositionService != null">#{dispositionService},</if>
<if test="letterService != null">#{letterService},</if>
@@ -245,6 +249,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="mz != null">mz = #{mz},</if>
<if test="birthday != null">birthday = #{birthday},</if>
<if test="jg != null">jg = #{jg},</if>
<if test="politicalStatus != null">political_status = #{politicalStatus},</if>
<if test="hksz2 != null">hksz2 = #{hksz2},</if>
<if test="dispositionService != null">disposition_service = #{dispositionService},</if>
<if test="letterService != null">letter_service = #{letterService},</if>

View File

@@ -33,6 +33,7 @@
<result property="mz" column="mz"/>
<result property="birthday" column="birthday"/>
<result property="jg" column="jg"/>
<result property="politicalStatus" column="political_status" />
<result property="hksz2" column="hksz2"/>
<result property="relieveService" column="relieve_service"/>
</resultMap>
@@ -65,6 +66,7 @@
mz,
birthday,
jg,
political_status,
hksz2,
relieve_service,
applicant_name
@@ -107,6 +109,7 @@
<if test="mz != null and mz != ''">and mz = #{mz}</if>
<if test="birthday != null and birthday != ''">and birthday = #{birthday}</if>
<if test="jg != null and jg != ''">and jg = #{jg}</if>
<if test="politicalStatus != null and politicalStatus != ''">and political_status = #{politicalStatus}</if>
<if test="hksz2 != null and hksz2 != ''">and hksz2 = #{hksz2}</if>
<if test="relieveService != null and relieveService != ''">and relieve_service = #{relieveService}</if>
</where>
@@ -147,6 +150,7 @@
a.mz,
a.birthday,
a.jg,
a.political_status,
a.hksz2,
a.relieve_service,
a.applicant_name
@@ -187,6 +191,7 @@
<if test="mz != null and mz != ''">and a.mz = #{mz}</if>
<if test="birthday != null and birthday != ''">and a.birthday = #{birthday}</if>
<if test="jg != null and jg != ''">and a.jg = #{jg}</if>
<if test="politicalStatus != null and politicalStatus != ''">and a.political_status = #{politicalStatus}</if>
<if test="hksz2 != null and hksz2 != ''">and a.hksz2 = #{hksz2}</if>
<if test="relieveService != null and relieveService != ''">and a.relieve_service = #{relieveService}</if>
<if test="params.employeeId != null and params.employeeId != ''">and d.employee_id = #{params.employeeId}</if>
@@ -222,6 +227,7 @@
a.mz,
a.birthday,
a.jg,
a.political_status,
a.hksz2,
a.relieve_service,
a.applicant_name
@@ -262,6 +268,7 @@
<if test="mz != null and mz != ''">and a.mz = #{mz}</if>
<if test="birthday != null and birthday != ''">and a.birthday = #{birthday}</if>
<if test="jg != null and jg != ''">and a.jg = #{jg}</if>
<if test="politicalStatus != null and politicalStatus != ''">and a.political_status = #{politicalStatus}</if>
<if test="hksz2 != null and hksz2 != ''">and a.hksz2 = #{hksz2}</if>
<if test="relieveService != null and relieveService != ''">and a.relieve_service = #{relieveService}</if>
<if test="params.employeeId != null and params.employeeId != ''">and d.employee_id = #{params.employeeId}</if>
@@ -300,6 +307,7 @@
<if test="mz != null">mz,</if>
<if test="birthday != null">birthday,</if>
<if test="jg != null">jg,</if>
<if test="politicalStatus != null">political_status,</if>
<if test="hksz2 != null">hksz2,</if>
<if test="relieveService != null">relieve_service,</if>
<if test="applicantName != null">applicant_name,</if>
@@ -331,6 +339,7 @@
<if test="mz != null">#{mz},</if>
<if test="birthday != null">#{birthday},</if>
<if test="jg != null">#{jg},</if>
<if test="politicalStatus != null">#{politicalStatus},</if>
<if test="hksz2 != null">#{hksz2},</if>
<if test="relieveService != null">#{relieveService},</if>
<if test="applicantName != null">#{applicantName},</if>
@@ -366,6 +375,7 @@
<if test="mz != null">mz = #{mz},</if>
<if test="birthday != null">birthday = #{birthday},</if>
<if test="jg != null">jg = #{jg},</if>
<if test="politicalStatus != null">political_status = #{politicalStatus},</if>
<if test="hksz2 != null">hksz2 = #{hksz2},</if>
<if test="relieveService != null">relieve_service = #{relieveService},</if>
<if test="applicantName != null">applicant_name = #{applicantName},</if>

View File

@@ -4,6 +4,7 @@ import com.srs.system.domain.StudentMentalRating;
import com.srs.system.domain.vo.StudentMentalRatingVo;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.util.List;
public interface StudentMentalRatingMapper {
@@ -47,5 +48,13 @@ public interface StudentMentalRatingMapper {
/** 单学号全部记录 */
List<StudentMentalRating> selectByStuNo(@Param("stuNo") String stuNo);
/**
* 查询今天的心理评级记录 陈冠元
* @param studentId 学生ID
* @param today 今天的日期
* @return 记录数
*/
int countTodayByStudentId(@Param("studentId") String studentId, @Param("today") LocalDate today);
}

View File

@@ -94,4 +94,12 @@
WHERE student_id = #{stuNo}
ORDER BY created_time DESC
</select>
<!-- 查询今天该学生是否已经填写过心理评测 陈冠元 -->
<select id="countTodayByStudentId" resultType="int">
SELECT COUNT(*)
FROM student_mental_rating
WHERE student_id = #{studentId}
AND DATE(created_time) = #{today}
</select>
</mapper>