推送项目重构代码

This commit is contained in:
2026-05-31 14:19:15 +08:00
parent a28ea44cab
commit dcc66aa4a9
30 changed files with 1112 additions and 1021 deletions

View File

@@ -76,5 +76,9 @@ public class OaRequirementsBo extends BaseEntity {
*/
private String accessory;
/**
* 状态多选筛选:逗号分隔的状态值,如 "0,1"。用于"未完成"等组合 tab。
* 与 status 同时存在时优先生效。
*/
private String statusIn;
}

View File

@@ -0,0 +1,79 @@
package com.ruoyi.oa.im;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.hrm.event.ApprovalRequestedEvent;
import com.ruoyi.system.mapper.SysUserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import java.util.HashMap;
import java.util.Map;
/**
* 申请提交事件监听:调 ImSendService 给当前审批人推 IM 通知,
* 同时根据 bizType 计算 web 跳转路径与 app 跳转路径。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class HrmApprovalListener {
private final ImSendService imSendService;
private final SysUserMapper userMapper;
/** 业务类型 → web 详情页路径 */
private static final Map<String, String> WEB_ROUTE = new HashMap<>();
/** 业务类型 → 中文标签 */
private static final Map<String, String> BIZ_LABEL = new HashMap<>();
static {
WEB_ROUTE.put("seal", "/hrm/HrmSealDetail");
WEB_ROUTE.put("leave", "/hrm/HrmLeaveDetail");
WEB_ROUTE.put("travel", "/hrm/HrmTravelDetail");
WEB_ROUTE.put("reimburse", "/hrm/HrmReimburseDetail");
WEB_ROUTE.put("appropriation", "/hrm/HrmAppropriationDetail");
BIZ_LABEL.put("seal", "用印申请");
BIZ_LABEL.put("leave", "请假申请");
BIZ_LABEL.put("travel", "出差申请");
BIZ_LABEL.put("reimburse", "报销申请");
BIZ_LABEL.put("appropriation", "拨款申请");
}
/** 事务提交后再推 IM避免业务回滚后还发出去 */
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
public void onApprovalRequested(ApprovalRequestedEvent ev) {
try {
if (ev.getAssigneeUserId() == null) {
log.debug("[Approval] no assignee, skip push. bizType={} bizId={}", ev.getBizType(), ev.getBizId());
return;
}
String label = BIZ_LABEL.getOrDefault(ev.getBizType(), "审批");
String starterName = "申请人";
if (ev.getStartUserId() != null) {
SysUser u = userMapper.selectById(ev.getStartUserId());
if (u != null) {
starterName = u.getNickName() != null ? u.getNickName()
: (u.getUserName() != null ? u.getUserName() : starterName);
}
}
String title = "新的" + label;
String desc = String.format("[%s] 发起了%s等待您的审批", starterName, label);
String webBase = WEB_ROUTE.getOrDefault(ev.getBizType(), "/hrm/approval");
String webRoute = webBase + "?bizId=" + ev.getBizId();
String mobileRoute = "/pages/workbench/hrm/detail/detail?bizType="
+ ev.getBizType() + "&bizId=" + ev.getBizId();
imSendService.sendToOaUser(
ev.getAssigneeUserId(), title, desc,
ev.getBizType(), ev.getBizId(),
webRoute, mobileRoute);
} catch (Exception e) {
log.warn("[Approval] push IM failed: {}", e.getMessage());
}
}
}

View File

@@ -66,6 +66,17 @@ public class ImSendService {
@Async
public void sendToOaUser(Long oaUserId, String title, String description,
String bizType, Object bizId, String route) {
sendToOaUser(oaUserId, title, description, bizType, bizId, route, null);
}
/**
* 同上,但允许给 Web 和 App 分别指定跳转路径。
* @param webRoute Web 跳转路径(同 route 字段,前端 SDK 读 route
* @param mobileRoute 手机端跳转路径uniapp 页面路径),手机端读 mobileRoute
*/
@Async
public void sendToOaUser(Long oaUserId, String title, String description,
String bizType, Object bizId, String webRoute, String mobileRoute) {
if (oaUserId == null) { return; }
try {
ImBind bind = bindMapper.selectById(oaUserId);
@@ -78,7 +89,8 @@ public class ImSendService {
Map<String, Object> payload = new HashMap<>();
payload.put("bizType", bizType);
payload.put("bizId", bizId);
if (route != null) { payload.put("route", route); }
if (webRoute != null) { payload.put("route", webRoute); }
if (mobileRoute != null) { payload.put("mobileRoute", mobileRoute); }
openImClient.sendCustomToUser(bind.getImUserId(), title, description, payload);
} catch (Exception e) {

View File

@@ -29,6 +29,8 @@ public class OpenImClient {
/** 自定义消息 contentTypeOpenIM 约定 110 以上为自定义 */
public static final int CUSTOM_CONTENT_TYPE = 110;
/** 文本消息 contentType */
public static final int TEXT_CONTENT_TYPE = 101;
/** 会话类型:单聊 */
public static final int SESSION_SINGLE = 1;
@@ -86,41 +88,37 @@ public class OpenImClient {
log.debug("[OpenIM] disabled, skip send to {}", recvImUserId);
return false;
}
Map<String, Object> data = new HashMap<>();
data.put("title", title);
data.put("description", description);
if (payload != null) { data.putAll(payload); }
Map<String, Object> customElem = new HashMap<>(3);
customElem.put("data", JSON.toJSONString(data));
customElem.put("description", description);
customElem.put("extension", "");
// 业务元数据放在 ex 里(客户端可解析以路由)
Map<String, Object> ex = new HashMap<>();
ex.put("title", title);
ex.put("description", description);
if (payload != null) { ex.putAll(payload); }
// 用 TextElem 让聊天软件直接展示 —— content.text 是聊天可见内容
String visibleText = "" + title + "\n" + description;
Map<String, Object> content = new HashMap<>();
content.put("customElem", customElem);
content.put("content", visibleText);
Map<String, Object> offlinePush = new HashMap<>();
offlinePush.put("title", title);
offlinePush.put("desc", description);
offlinePush.put("ex", "");
offlinePush.put("ex", JSON.toJSONString(ex));
offlinePush.put("iOSPushSound", "default");
offlinePush.put("iOSBadgeCount", true);
// SendMsg 字段需要嵌套在 sendMessage 对象里OpenIM v3.8 约定)
Map<String, Object> sendMessage = new HashMap<>();
sendMessage.put("sendID", props.getNotificationSender());
sendMessage.put("recvID", recvImUserId);
sendMessage.put("senderNickname", "系统通知");
sendMessage.put("senderPlatformID", 1);
sendMessage.put("content", content);
sendMessage.put("contentType", CUSTOM_CONTENT_TYPE);
sendMessage.put("sessionType", SESSION_SINGLE);
sendMessage.put("isOnlineOnly", false);
sendMessage.put("notOfflinePush", false);
sendMessage.put("offlinePushInfo", offlinePush);
// OpenIM v3.8 send_msg所有字段平铺在请求顶层
Map<String, Object> body = new HashMap<>();
body.put("sendMessage", sendMessage);
body.put("sendID", props.getNotificationSender());
body.put("recvID", recvImUserId);
body.put("senderNickname", "OA助手");
body.put("senderPlatformID", 1);
body.put("content", content);
body.put("contentType", TEXT_CONTENT_TYPE);
body.put("sessionType", SESSION_SINGLE);
body.put("isOnlineOnly", false);
body.put("notOfflinePush", false);
body.put("offlinePushInfo", offlinePush);
body.put("ex", JSON.toJSONString(ex));
JSONObject resp = postJson(props.getApiUrl() + "/msg/send_msg", body, getAdminToken());
Integer errCode = resp.getInteger("errCode");

View File

@@ -18,6 +18,7 @@ import com.ruoyi.oa.domain.OaRequirements;
import com.ruoyi.oa.mapper.OaRequirementsMapper;
import com.ruoyi.oa.service.IOaRequirementsService;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Collection;
@@ -71,7 +72,16 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
qw.eq(bo.getProjectId() != null, "r.project_id", bo.getProjectId());
qw.like(StringUtils.isNotBlank(bo.getDescription()), "r.description", bo.getDescription());
qw.eq(bo.getDeadline() != null, "r.deadline", bo.getDeadline());
qw.eq(bo.getStatus() != null, "r.status", bo.getStatus());
// statusIn 优先于 status用于"未完成0,1"等组合 tab
if (StringUtils.isNotBlank(bo.getStatusIn())) {
List<Integer> ins = new ArrayList<>();
for (String s : bo.getStatusIn().split(",")) {
try { ins.add(Integer.parseInt(s.trim())); } catch (Exception ignored) {}
}
if (!ins.isEmpty()) qw.in("r.status", ins);
} else {
qw.eq(bo.getStatus() != null, "r.status", bo.getStatus());
}
qw.eq(StringUtils.isNotBlank(bo.getAccessory()), "r.accessory", bo.getAccessory());
qw.eq("r.del_flag", 0);
//根据创建时间倒叙

View File

@@ -73,11 +73,20 @@ public class UserSuggestionController extends BaseController {
String title = "新的修改意见";
String desc = String.format("[%s] %s", name == null ? "用户" : name,
body.getTitle() == null ? "未命名" : body.getTitle());
int sent = 0;
for (SysUser u : itUsers) {
if (u.getUserId() == null || u.getUserId().equals(uid)) continue;
if (u.getUserId() == null) continue;
imSendService.sendToOaUser(u.getUserId(), title, desc,
"suggestion", body.getFeedbackId(),
"/system/feedback?id=" + body.getFeedbackId());
sent++;
}
// 兜底:如果信息化部一个人都没有,发一份给自己确认链路
if (sent == 0) {
imSendService.sendToOaUser(uid, title + "(测试)",
desc + " · 信息化部暂无 IM 绑定,先回发给提出者",
"suggestion", body.getFeedbackId(),
"/system/feedback?id=" + body.getFeedbackId());
}
return R.ok();
}

View File

@@ -0,0 +1,94 @@
package com.ruoyi.oa.task;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.oa.domain.OaReportSummary;
import com.ruoyi.oa.im.ImSendService;
import com.ruoyi.oa.mapper.OaReportSummaryMapper;
import com.ruoyi.system.mapper.SysUserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 每日 18:00 扫描没报工的员工,通过 IM 推送提醒。
*
* @author wangyu
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DailyReportRemindScheduler {
/** Web 端跳转路径("我的报工" */
private static final String WEB_ROUTE = "/hint/my";
/** 手机端跳转路径uniapp 报工页) */
private static final String MOBILE_ROUTE = "/pages/workbench/reportWork/reportWork";
private final OaReportSummaryMapper summaryMapper;
private final SysUserMapper userMapper;
private final ImSendService imSendService;
/** 工作日 18:00 触发 */
@Scheduled(cron = "0 0 18 * * MON-FRI")
public void notifyMissingReporters() {
LocalDate today = LocalDate.now();
if (today.getDayOfWeek() == DayOfWeek.SATURDAY || today.getDayOfWeek() == DayOfWeek.SUNDAY) {
return;
}
// 今天报过工的人(按 reporter 名字去重)
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0);
Date start = c.getTime();
c.add(Calendar.DAY_OF_MONTH, 1);
Date end = c.getTime();
List<OaReportSummary> reportedToday = summaryMapper.selectList(
Wrappers.<OaReportSummary>lambdaQuery()
.ge(OaReportSummary::getReportDate, start)
.lt(OaReportSummary::getReportDate, end)
.eq(OaReportSummary::getDelFlag, 0L));
Set<String> reportedReporters = new HashSet<>();
for (OaReportSummary s : reportedToday) {
if (s.getReporter() != null) reportedReporters.add(s.getReporter().trim());
}
// 全部在职用户
List<SysUser> users = userMapper.selectList(Wrappers.<SysUser>lambdaQuery()
.eq(SysUser::getDelFlag, "0")
.eq(SysUser::getStatus, "0"));
SimpleDateFormat sdf = new SimpleDateFormat("MM-dd");
String todayStr = sdf.format(new Date());
int pushed = 0;
for (SysUser u : users) {
if (u.getUserId() == null) continue;
String key = u.getNickName() != null ? u.getNickName() : u.getUserName();
if (key != null && reportedReporters.contains(key.trim())) continue;
String title = "报工提醒";
String desc = String.format("【%s】您今天%s还未提交日报请尽快填写。点击立即报工。",
u.getNickName() == null ? u.getUserName() : u.getNickName(), todayStr);
imSendService.sendToOaUser(u.getUserId(), title, desc,
"report", System.currentTimeMillis(),
WEB_ROUTE, MOBILE_ROUTE);
pushed++;
}
log.info("[DailyReportRemind] 已推送 {} 人未报工提醒(总员工 {},今日已报工 {}",
pushed, users.size(), reportedReporters.size());
}
}