推送项目重构代码
This commit is contained in:
@@ -76,5 +76,9 @@ public class OaRequirementsBo extends BaseEntity {
|
||||
*/
|
||||
private String accessory;
|
||||
|
||||
|
||||
/**
|
||||
* 状态多选筛选:逗号分隔的状态值,如 "0,1"。用于"未完成"等组合 tab。
|
||||
* 与 status 同时存在时优先生效。
|
||||
*/
|
||||
private String statusIn;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -29,6 +29,8 @@ public class OpenImClient {
|
||||
|
||||
/** 自定义消息 contentType,OpenIM 约定 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");
|
||||
|
||||
@@ -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);
|
||||
//根据创建时间倒叙
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user