feat: 完成消息通知中心全功能开发

1. 新增消息通知相关实体、Mapper、Service、控制器与前端页面
2. 实现审批通知、报价到期提醒等通知发送逻辑
3. 完成通知菜单配置与路由注册
4. 修复通知数据与跳转路径问题
5. 新增配套SQL脚本与定时任务
This commit is contained in:
2026-06-21 04:20:44 +08:00
parent 41b2e3e772
commit 8bdb8d7c23
27 changed files with 1817 additions and 132 deletions

View File

@@ -0,0 +1,52 @@
package com.ruoyi.system.domain.bid;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 消息通知 biz_notify_message
*/
public class BizNotifyMessage extends BaseEntity {
private static final long serialVersionUID = 1L;
private Long messageId;
private Long tenantId;
private Long userId;
private String noticeType;
private Integer priority;
private String title;
private String content;
private String bizType;
private Long bizId;
private String bizUrl;
private String isRead;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date readTime;
public Long getMessageId() { return messageId; }
public void setMessageId(Long messageId) { this.messageId = messageId; }
public Long getTenantId() { return tenantId; }
public void setTenantId(Long tenantId) { this.tenantId = tenantId; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getNoticeType() { return noticeType; }
public void setNoticeType(String noticeType) { this.noticeType = noticeType; }
public Integer getPriority() { return priority; }
public void setPriority(Integer priority) { this.priority = priority; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getBizType() { return bizType; }
public void setBizType(String bizType) { this.bizType = bizType; }
public Long getBizId() { return bizId; }
public void setBizId(Long bizId) { this.bizId = bizId; }
public String getBizUrl() { return bizUrl; }
public void setBizUrl(String bizUrl) { this.bizUrl = bizUrl; }
public String getIsRead() { return isRead; }
public void setIsRead(String isRead) { this.isRead = isRead; }
public Date getReadTime() { return readTime; }
public void setReadTime(Date readTime) { this.readTime = readTime; }
}

View File

@@ -0,0 +1,36 @@
package com.ruoyi.system.domain.bid;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 通知规则配置 biz_notify_rule
*/
public class BizNotifyRule extends BaseEntity {
private static final long serialVersionUID = 1L;
private Long ruleId;
private Long tenantId;
private String ruleName;
private String noticeType;
private String bizType;
private String triggerCondition;
private Integer advanceDays;
private String enabled;
public Long getRuleId() { return ruleId; }
public void setRuleId(Long ruleId) { this.ruleId = ruleId; }
public Long getTenantId() { return tenantId; }
public void setTenantId(Long tenantId) { this.tenantId = tenantId; }
public String getRuleName() { return ruleName; }
public void setRuleName(String ruleName) { this.ruleName = ruleName; }
public String getNoticeType() { return noticeType; }
public void setNoticeType(String noticeType) { this.noticeType = noticeType; }
public String getBizType() { return bizType; }
public void setBizType(String bizType) { this.bizType = bizType; }
public String getTriggerCondition() { return triggerCondition; }
public void setTriggerCondition(String triggerCondition) { this.triggerCondition = triggerCondition; }
public Integer getAdvanceDays() { return advanceDays; }
public void setAdvanceDays(Integer advanceDays) { this.advanceDays = advanceDays; }
public String getEnabled() { return enabled; }
public void setEnabled(String enabled) { this.enabled = enabled; }
}

View File

@@ -16,4 +16,24 @@ public interface BizApprovalActionMapper {
@Param("pk") String pk,
@Param("statusCol") String statusCol,
@Param("id") Long id);
/** 查询业务单据的 create_by提交人用户名 */
java.util.Map<String, Object> selectBizCreateInfo(@Param("table") String table,
@Param("pk") String pk,
@Param("id") Long id);
/** 查询采购订单详情(含供应商名称、询价标题、金额) */
java.util.Map<String, Object> selectPurchaseOrderInfo(@Param("id") Long id);
/** 查询客户报价详情(含客户名称、询价标题、金额) */
java.util.Map<String, Object> selectClientQuoteInfo(@Param("id") Long id);
/** 查询供应商报价详情(含供应商名称、询价标题、金额) */
java.util.Map<String, Object> selectQuotationInfo(@Param("id") Long id);
/** 查询发货单详情(含供应商/客户名称、金额) */
java.util.Map<String, Object> selectDeliveryOrderInfo(@Param("id") Long id);
/** 查询订单异议详情(含供应商名称、采购单号) */
java.util.Map<String, Object> selectObjectionInfo(@Param("id") Long id);
}

View File

@@ -0,0 +1,32 @@
package com.ruoyi.system.mapper.bid;
import java.util.List;
import java.util.Map;
import com.ruoyi.system.domain.bid.BizNotifyMessage;
import org.apache.ibatis.annotations.Param;
public interface BizNotifyMessageMapper {
public List<BizNotifyMessage> selectNotifyMessageList(BizNotifyMessage query);
public BizNotifyMessage selectNotifyMessageById(Long messageId);
public int insertNotifyMessage(BizNotifyMessage message);
public int batchInsertNotifyMessage(@Param("list") List<BizNotifyMessage> list);
public int updateNotifyMessage(BizNotifyMessage message);
public int markAsRead(@Param("messageId") Long messageId, @Param("userId") Long userId);
public int markAllAsRead(@Param("userId") Long userId);
public int deleteNotifyMessageByIds(@Param("ids") Long[] ids);
public int selectUnreadCount(@Param("userId") Long userId);
public List<Map<String, Object>> selectNotifyStats(@Param("userId") Long userId);
public List<BizNotifyMessage> selectTopUnread(@Param("userId") Long userId, @Param("limit") int limit);
public List<BizNotifyMessage> selectTopNotify(@Param("userId") Long userId, @Param("limit") int limit);
}

View File

@@ -0,0 +1,18 @@
package com.ruoyi.system.mapper.bid;
import java.util.List;
import com.ruoyi.system.domain.bid.BizNotifyRule;
public interface BizNotifyRuleMapper {
public List<BizNotifyRule> selectNotifyRuleList(BizNotifyRule query);
public BizNotifyRule selectNotifyRuleById(Long ruleId);
public int insertNotifyRule(BizNotifyRule rule);
public int updateNotifyRule(BizNotifyRule rule);
public int deleteNotifyRuleByIds(Long[] ids);
public List<BizNotifyRule> selectEnabledRules();
}

View File

@@ -0,0 +1,73 @@
package com.ruoyi.system.service.bid;
import java.util.List;
import java.util.Map;
import com.ruoyi.system.domain.bid.BizNotifyMessage;
/**
* 消息通知服务接口
*/
public interface IBizNotifyMessageService {
public List<BizNotifyMessage> selectNotifyMessageList(BizNotifyMessage query);
public BizNotifyMessage selectNotifyMessageById(Long messageId);
public int insertNotifyMessage(BizNotifyMessage message);
public int batchInsertNotifyMessage(List<BizNotifyMessage> list);
public int markAsRead(Long messageId, Long userId);
public int markAllAsRead(Long userId);
public int deleteNotifyMessageByIds(Long[] ids);
public int selectUnreadCount(Long userId);
public List<Map<String, Object>> selectNotifyStats(Long userId);
public List<BizNotifyMessage> selectTopUnread(Long userId, int limit);
/**
* 查询最近N条通知含已读和未读用于铃铛面板
*/
public List<BizNotifyMessage> selectTopNotify(Long userId, int limit);
/**
* 发送审批结果通知
* @param bizType 业务类型
* @param bizId 业务ID
* @param bizTitle 业务标题
* @param approved 是否通过
* @param reason 驳回原因
* @param submitUserId 提交人用户ID
* @param approverName 审批人姓名
*/
public void sendApprovalNotification(String bizType, Long bizId, String bizTitle,
boolean approved, String reason, Long submitUserId, String approverName);
/**
* 发送报价到期提醒
* @param quotationId 报价单ID
* @param quotationNo 报价单号
* @param supplierName 供应商名称
* @param daysRemaining 剩余天数
* @param userId 接收人用户ID
*/
public void sendQuotationExpireNotification(Long quotationId, String quotationNo,
String supplierName, int daysRemaining, Long userId);
/**
* 发送报价到期提醒(含详情)
* @param quotationId 报价单ID
* @param quotationNo 报价单号
* @param supplierName 供应商名称
* @param rfqTitle 询价标题
* @param totalAmount 报价金额
* @param daysRemaining 剩余天数
* @param userId 接收人用户ID
*/
public void sendQuotationExpireNotification(Long quotationId, String quotationNo,
String supplierName, String rfqTitle, java.math.BigDecimal totalAmount,
int daysRemaining, Long userId);
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.system.service.bid;
import java.util.List;
import com.ruoyi.system.domain.bid.BizNotifyRule;
public interface IBizNotifyRuleService {
public List<BizNotifyRule> selectNotifyRuleList(BizNotifyRule query);
public BizNotifyRule selectNotifyRuleById(Long ruleId);
public int insertNotifyRule(BizNotifyRule rule);
public int updateNotifyRule(BizNotifyRule rule);
public int deleteNotifyRuleByIds(Long[] ids);
public List<BizNotifyRule> selectEnabledRules();
}

View File

@@ -4,6 +4,11 @@ import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.mapper.bid.BizApprovalActionMapper;
import com.ruoyi.system.service.bid.IBizApprovalActionService;
import com.ruoyi.system.service.bid.IBizApprovalConfigService;
import com.ruoyi.system.service.bid.IBizNotifyMessageService;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.system.domain.bid.BizApprovalConfig;
import com.ruoyi.system.domain.bid.BizNotifyMessage;
import com.ruoyi.system.mapper.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -18,24 +23,26 @@ public class BizApprovalActionServiceImpl implements IBizApprovalActionService {
@Autowired private BizApprovalActionMapper mapper;
@Autowired private IBizApprovalConfigService configService;
@Autowired private IBizNotifyMessageService notifyMessageService;
@Autowired private SysUserMapper sysUserMapper;
/** 业务类型 -> 表/主键/状态列/审批通过的目标状态/可提交的初始状态 */
/** 业务类型 -> 表/主键/状态列/审批通过的目标状态/可提交的初始状态/业务名称 */
static class Meta {
final String table, pk, statusCol, approvedStatus;
final String table, pk, statusCol, approvedStatus, bizName;
final List<String> draftStatuses;
Meta(String t, String pk, String sc, String approved, List<String> drafts) {
Meta(String t, String pk, String sc, String approved, List<String> drafts, String bizName) {
this.table = t; this.pk = pk; this.statusCol = sc;
this.approvedStatus = approved; this.draftStatuses = drafts;
this.approvedStatus = approved; this.draftStatuses = drafts; this.bizName = bizName;
}
}
private static final Map<String, Meta> META = new HashMap<>();
static {
META.put("PURCHASE_ORDER", new Meta("biz_purchase_order", "po_id", "status", "confirmed", Arrays.asList("draft")));
META.put("CLIENT_QUOTE", new Meta("biz_client_quote", "quote_id", "status", "confirmed", Arrays.asList("draft")));
META.put("QUOTATION", new Meta("biz_quotation", "quotation_id", "status", "accepted", Arrays.asList("draft", "submitted")));
META.put("DELIVERY_ORDER", new Meta("biz_delivery_order", "do_id", "delivery_status", "confirmed", Arrays.asList("pending")));
META.put("ORDER_OBJECTION", new Meta("biz_order_objection", "objection_id", "status", "resolved", Arrays.asList("pending")));
META.put("PURCHASE_ORDER", new Meta("biz_purchase_order", "po_id", "status", "confirmed", Arrays.asList("draft"), "采购订单"));
META.put("CLIENT_QUOTE", new Meta("biz_client_quote", "quote_id", "status", "confirmed", Arrays.asList("draft"), "客户报价"));
META.put("QUOTATION", new Meta("biz_quotation", "quotation_id", "status", "accepted", Arrays.asList("draft", "submitted"), "供应商报价"));
META.put("DELIVERY_ORDER", new Meta("biz_delivery_order", "do_id", "delivery_status", "confirmed", Arrays.asList("pending"), "发货单"));
META.put("ORDER_OBJECTION", new Meta("biz_order_objection", "objection_id", "status", "resolved", Arrays.asList("pending"), "订单异议"));
}
private Meta meta(String bizType) {
@@ -49,6 +56,13 @@ public class BizApprovalActionServiceImpl implements IBizApprovalActionService {
Meta m = meta(bizType);
int rows = mapper.updateStatus(m.table, m.pk, m.statusCol, id, "10", m.draftStatuses, username);
if (rows == 0) throw new ServiceException("当前状态不允许提交审批");
// 通知审批人:有新的待审批申请
try {
sendSubmitNotification(bizType, m, id, username);
} catch (Exception e) {
// 通知发送失败不影响审批操作
}
return rows;
}
@@ -61,6 +75,13 @@ public class BizApprovalActionServiceImpl implements IBizApprovalActionService {
int rows = mapper.updateStatus(m.table, m.pk, m.statusCol, id,
m.approvedStatus, Collections.singletonList("10"), username);
if (rows == 0) throw new ServiceException("单据非审批中, 无法通过");
// 发送审批通过通知给提交人
try {
sendApprovalNotification(bizType, m, id, true, null, username);
} catch (Exception e) {
// 通知发送失败不影响审批操作
}
return rows;
}
@@ -73,6 +94,203 @@ public class BizApprovalActionServiceImpl implements IBizApprovalActionService {
int rows = mapper.updateStatus(m.table, m.pk, m.statusCol, id,
"rejected", Collections.singletonList("10"), username);
if (rows == 0) throw new ServiceException("单据非审批中, 无法驳回");
// 发送审批驳回通知给提交人
try {
sendApprovalNotification(bizType, m, id, false, reason, username);
} catch (Exception e) {
// 通知发送失败不影响审批操作
}
return rows;
}
/**
* 查询业务详情,返回统一格式: bizNo, partnerName, bizTitle, totalAmount, currency, createTime, createBy
*/
private Map<String, Object> getBizDetail(String bizType, Long bizId) {
switch (bizType) {
case "PURCHASE_ORDER": return mapper.selectPurchaseOrderInfo(bizId);
case "CLIENT_QUOTE": return mapper.selectClientQuoteInfo(bizId);
case "QUOTATION": return mapper.selectQuotationInfo(bizId);
case "DELIVERY_ORDER": return mapper.selectDeliveryOrderInfo(bizId);
case "ORDER_OBJECTION": return mapper.selectObjectionInfo(bizId);
default: return null;
}
}
/**
* 构建审批通知内容(包含单号、客户/供应商、金额等关键信息)
*/
private String buildApprovalContent(String bizType, Meta m, Map<String, Object> detail,
boolean approved, String reason, String approverName) {
StringBuilder sb = new StringBuilder();
sb.append("您提交的【").append(m.bizName).append("");
// 单号
if (detail.get("bizNo") != null) {
sb.append("(单号: ").append(detail.get("bizNo")).append("");
}
if (approved) {
sb.append("已审批通过。");
} else {
sb.append("被驳回。");
}
// 客户/供应商名称
String partnerLabel = getPartnerLabel(bizType);
if (detail.get("partnerName") != null) {
sb.append(partnerLabel).append(": ").append(detail.get("partnerName")).append("");
}
// 发货单额外显示客户名称
if ("DELIVERY_ORDER".equals(bizType) && detail.get("clientName") != null) {
sb.append("客户: ").append(detail.get("clientName")).append("");
}
// 询价标题/异议原因
if (detail.get("bizTitle") != null) {
sb.append("标题: ").append(detail.get("bizTitle")).append("");
}
// 金额
if (detail.get("totalAmount") != null) {
sb.append("金额: ").append(detail.get("totalAmount"));
if (detail.get("currency") != null) {
sb.append(" ").append(detail.get("currency"));
}
sb.append("");
}
sb.append("审批人: ").append(approverName);
// 驳回原因
if (!approved && reason != null && !reason.isEmpty()) {
sb.append(";驳回原因: ").append(reason);
}
return sb.toString();
}
/**
* 构建提交审批通知内容(发给审批人)
*/
private String buildSubmitContent(String bizType, Meta m, Map<String, Object> detail, String submitterName) {
StringBuilder sb = new StringBuilder();
sb.append("您有新的【").append(m.bizName).append("】审批申请待处理。");
if (detail.get("bizNo") != null) {
sb.append("单号: ").append(detail.get("bizNo")).append("");
}
String partnerLabel = getPartnerLabel(bizType);
if (detail.get("partnerName") != null) {
sb.append(partnerLabel).append(": ").append(detail.get("partnerName")).append("");
}
if ("DELIVERY_ORDER".equals(bizType) && detail.get("clientName") != null) {
sb.append("客户: ").append(detail.get("clientName")).append("");
}
if (detail.get("bizTitle") != null) {
sb.append("标题: ").append(detail.get("bizTitle")).append("");
}
if (detail.get("totalAmount") != null) {
sb.append("金额: ").append(detail.get("totalAmount"));
if (detail.get("currency") != null) {
sb.append(" ").append(detail.get("currency"));
}
sb.append("");
}
sb.append("提交人: ").append(submitterName);
return sb.toString();
}
/** 根据业务类型返回关联方标签 */
private String getPartnerLabel(String bizType) {
switch (bizType) {
case "PURCHASE_ORDER":
case "QUOTATION":
case "DELIVERY_ORDER":
case "ORDER_OBJECTION":
return "供应商";
case "CLIENT_QUOTE":
return "客户";
default:
return "关联方";
}
}
/**
* 发送审批结果通知
*/
private void sendApprovalNotification(String bizType, Meta m, Long bizId, boolean approved, String reason, String approverName) {
Map<String, Object> detail = getBizDetail(bizType, bizId);
if (detail == null || detail.get("createBy") == null) return;
String createBy = (String) detail.get("createBy");
SysUser submitUser = sysUserMapper.selectUserByUserName(createBy);
if (submitUser == null) return;
String bizNo = detail.get("bizNo") != null ? String.valueOf(detail.get("bizNo")) : String.valueOf(bizId);
String titlePrefix = approved ? "审批通过" : "审批驳回";
String title = titlePrefix + ": " + m.bizName + " " + bizNo;
String content = buildApprovalContent(bizType, m, detail, approved, reason, approverName);
BizNotifyMessage msg = new BizNotifyMessage();
msg.setUserId(submitUser.getUserId());
msg.setNoticeType("approval");
msg.setPriority(approved ? 1 : 2);
msg.setBizType(bizType);
msg.setBizId(bizId);
msg.setBizUrl(getBizUrl(bizType, bizId));
msg.setCreateBy(approverName);
msg.setTitle(title);
msg.setContent(content);
msg.setIsRead("0");
notifyMessageService.insertNotifyMessage(msg);
}
/**
* 发送提交审批通知给审批人
*/
private void sendSubmitNotification(String bizType, Meta m, Long bizId, String submitterName) {
BizApprovalConfig config = configService.selectByBizType(bizType);
if (config == null || config.getUserIds() == null || config.getUserIds().isEmpty()) return;
Map<String, Object> detail = getBizDetail(bizType, bizId);
String bizNo = (detail != null && detail.get("bizNo") != null) ? String.valueOf(detail.get("bizNo")) : String.valueOf(bizId);
String title = "审批待处理: " + m.bizName + " " + bizNo;
String content = (detail != null) ? buildSubmitContent(bizType, m, detail, submitterName)
: "您有新的【" + m.bizName + "】审批申请待处理,提交人: " + submitterName;
for (Long approverUserId : config.getUserIds()) {
if (approverUserId == null) continue;
BizNotifyMessage msg = new BizNotifyMessage();
msg.setUserId(approverUserId);
msg.setNoticeType("approval");
msg.setPriority(1);
msg.setBizType(bizType);
msg.setBizId(bizId);
msg.setCreateBy(submitterName);
msg.setTitle(title);
msg.setContent(content);
msg.setBizUrl(getBizUrl(bizType, bizId));
msg.setIsRead("0");
notifyMessageService.insertNotifyMessage(msg);
}
}
private String getBizUrl(String bizType, Long bizId) {
if (bizType == null || bizId == null) return null;
switch (bizType) {
case "PURCHASE_ORDER": return "/quote/purchaseorder?id=" + bizId;
case "CLIENT_QUOTE": return "/bid/clientquote/detail?id=" + bizId;
case "QUOTATION": return "/quote/quotation?quotationId=" + bizId;
case "DELIVERY_ORDER": return "/bid/order/pending?id=" + bizId;
case "ORDER_OBJECTION": return "/bid/order/objection?id=" + bizId;
default: return null;
}
}
}

View File

@@ -0,0 +1,155 @@
package com.ruoyi.system.service.bid.impl;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.system.domain.bid.BizNotifyMessage;
import com.ruoyi.system.mapper.bid.BizNotifyMessageMapper;
import com.ruoyi.system.service.bid.IBizNotifyMessageService;
@Service
public class BizNotifyMessageServiceImpl implements IBizNotifyMessageService {
@Autowired
private BizNotifyMessageMapper notifyMessageMapper;
@Override
public List<BizNotifyMessage> selectNotifyMessageList(BizNotifyMessage query) {
return notifyMessageMapper.selectNotifyMessageList(query);
}
@Override
public BizNotifyMessage selectNotifyMessageById(Long messageId) {
return notifyMessageMapper.selectNotifyMessageById(messageId);
}
@Override
public int insertNotifyMessage(BizNotifyMessage message) {
return notifyMessageMapper.insertNotifyMessage(message);
}
@Override
public int batchInsertNotifyMessage(List<BizNotifyMessage> list) {
return notifyMessageMapper.batchInsertNotifyMessage(list);
}
@Override
public int markAsRead(Long messageId, Long userId) {
return notifyMessageMapper.markAsRead(messageId, userId);
}
@Override
public int markAllAsRead(Long userId) {
return notifyMessageMapper.markAllAsRead(userId);
}
@Override
public int deleteNotifyMessageByIds(Long[] ids) {
return notifyMessageMapper.deleteNotifyMessageByIds(ids);
}
@Override
public int selectUnreadCount(Long userId) {
return notifyMessageMapper.selectUnreadCount(userId);
}
@Override
public List<Map<String, Object>> selectNotifyStats(Long userId) {
return notifyMessageMapper.selectNotifyStats(userId);
}
@Override
public List<BizNotifyMessage> selectTopUnread(Long userId, int limit) {
return notifyMessageMapper.selectTopUnread(userId, limit);
}
@Override
public List<BizNotifyMessage> selectTopNotify(Long userId, int limit) {
return notifyMessageMapper.selectTopNotify(userId, limit);
}
@Override
public void sendApprovalNotification(String bizType, Long bizId, String bizTitle,
boolean approved, String reason, Long submitUserId, String approverName) {
BizNotifyMessage msg = new BizNotifyMessage();
msg.setUserId(submitUserId);
msg.setNoticeType("approval");
msg.setPriority(approved ? 1 : 2);
msg.setBizType(bizType);
msg.setBizId(bizId);
msg.setCreateBy(approverName);
if (approved) {
msg.setTitle("审批通过: " + bizTitle);
msg.setContent("您提交的【" + bizTitle + "】已审批通过。审批人: " + approverName);
} else {
msg.setTitle("审批驳回: " + bizTitle);
msg.setContent("您提交的【" + bizTitle + "】被驳回。审批人: " + approverName + ";驳回原因: " + (reason != null ? reason : ""));
}
msg.setIsRead("0");
msg.setBizUrl(getBizUrl(bizType, bizId));
notifyMessageMapper.insertNotifyMessage(msg);
}
@Override
public void sendQuotationExpireNotification(Long quotationId, String quotationNo,
String supplierName, int daysRemaining, Long userId) {
sendQuotationExpireNotification(quotationId, quotationNo, supplierName, null, null, daysRemaining, userId);
}
@Override
public void sendQuotationExpireNotification(Long quotationId, String quotationNo,
String supplierName, String rfqTitle, java.math.BigDecimal totalAmount,
int daysRemaining, Long userId) {
BizNotifyMessage msg = new BizNotifyMessage();
msg.setUserId(userId);
msg.setNoticeType("quotation_expire");
msg.setBizType("QUOTATION");
msg.setBizId(quotationId);
msg.setBizUrl("/quote/quotation?quotationId=" + quotationId);
msg.setCreateBy("system");
if (daysRemaining <= 0) {
msg.setPriority(2);
msg.setTitle("报价已过期: " + quotationNo);
} else if (daysRemaining <= 1) {
msg.setPriority(2);
msg.setTitle("报价今日到期: " + quotationNo);
} else {
msg.setPriority(1);
msg.setTitle("报价即将到期: " + quotationNo);
}
StringBuilder content = new StringBuilder();
content.append("供应商【").append(supplierName).append("】的报价单【").append(quotationNo).append("");
if (daysRemaining <= 0) {
content.append("已过期,请及时处理。");
} else {
content.append("将在").append(daysRemaining).append("天后到期。");
}
if (rfqTitle != null && !rfqTitle.isEmpty()) {
content.append("询价标题: ").append(rfqTitle).append("");
}
if (totalAmount != null) {
content.append("报价金额: ").append(totalAmount).append("");
}
msg.setContent(content.toString());
msg.setIsRead("0");
notifyMessageMapper.insertNotifyMessage(msg);
}
private String getBizUrl(String bizType, Long bizId) {
if (bizType == null || bizId == null) return null;
switch (bizType) {
case "PURCHASE_ORDER": return "/quote/purchaseorder?id=" + bizId;
case "CLIENT_QUOTE": return "/bid/clientquote/detail?id=" + bizId;
case "QUOTATION": return "/quote/quotation?quotationId=" + bizId;
case "DELIVERY_ORDER": return "/bid/order/pending?id=" + bizId;
case "ORDER_OBJECTION": return "/bid/order/objection?id=" + bizId;
default: return null;
}
}
}

View File

@@ -0,0 +1,45 @@
package com.ruoyi.system.service.bid.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.system.domain.bid.BizNotifyRule;
import com.ruoyi.system.mapper.bid.BizNotifyRuleMapper;
import com.ruoyi.system.service.bid.IBizNotifyRuleService;
@Service
public class BizNotifyRuleServiceImpl implements IBizNotifyRuleService {
@Autowired
private BizNotifyRuleMapper notifyRuleMapper;
@Override
public List<BizNotifyRule> selectNotifyRuleList(BizNotifyRule query) {
return notifyRuleMapper.selectNotifyRuleList(query);
}
@Override
public BizNotifyRule selectNotifyRuleById(Long ruleId) {
return notifyRuleMapper.selectNotifyRuleById(ruleId);
}
@Override
public int insertNotifyRule(BizNotifyRule rule) {
return notifyRuleMapper.insertNotifyRule(rule);
}
@Override
public int updateNotifyRule(BizNotifyRule rule) {
return notifyRuleMapper.updateNotifyRule(rule);
}
@Override
public int deleteNotifyRuleByIds(Long[] ids) {
return notifyRuleMapper.deleteNotifyRuleByIds(ids);
}
@Override
public List<BizNotifyRule> selectEnabledRules() {
return notifyRuleMapper.selectEnabledRules();
}
}

View File

@@ -0,0 +1,95 @@
package com.ruoyi.system.task;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.service.bid.IBizNotifyMessageService;
import com.ruoyi.system.mapper.bid.BizQuotationMapper;
import com.ruoyi.system.domain.bid.BizQuotation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 定时任务 - 报价到期提醒
* 每日执行,检查即将到期和已过期的报价单,发送站内通知
*
* Quartz 调用示例: quotationExpireTask.checkQuotationExpire()
* 推荐表达式: 0 0 9 * * ? (每天上午9点执行)
*/
@Component("quotationExpireTask")
public class QuotationExpireTask {
private static final Logger log = LoggerFactory.getLogger(QuotationExpireTask.class);
@Autowired
private BizQuotationMapper quotationMapper;
@Autowired
private IBizNotifyMessageService notifyMessageService;
@Autowired
private SysUserMapper sysUserMapper;
/**
* 每日报价到期检查
* 检查所有未完成报价单(草稿、审批中),基于 submitTime + validDays 计算到期日,
* 在到期前3天、1天、到期当天发送提醒。
*/
public void checkQuotationExpire() {
log.info("=== 开始执行报价到期检查 ===");
// 查询所有报价单(不限制状态),在循环中过滤已完成的
BizQuotation query = new BizQuotation();
List<BizQuotation> quotations = quotationMapper.selectBizQuotationList(query);
int remindCount = 0;
for (BizQuotation q : quotations) {
// 跳过已完成的报价单
if ("accepted".equals(q.getStatus()) || "rejected".equals(q.getStatus())) {
continue;
}
// 必须设置了有效期
if (q.getValidDays() == null || q.getValidDays() <= 0) continue;
// 使用 submitTime 计算到期,草稿单据用 createTime 回退
java.util.Date baseTime = q.getSubmitTime() != null ? q.getSubmitTime() : q.getCreateTime();
if (baseTime == null) continue;
long expireTime = baseTime.getTime() + (long) q.getValidDays() * 24 * 60 * 60 * 1000L;
long now = System.currentTimeMillis();
int daysRemaining = (int) Math.ceil((expireTime - now) / (24.0 * 60 * 60 * 1000));
// 提前3天、1天提醒过期当天提醒
if (daysRemaining == 3 || daysRemaining == 1 || daysRemaining == 0) {
try {
if (StringUtils.isNotEmpty(q.getCreateBy())) {
SysUser createUser = sysUserMapper.selectUserByUserName(q.getCreateBy());
if (createUser != null) {
String supplierName = q.getSupplierName() != null ? q.getSupplierName() : "供应商-" + q.getSupplierId();
String rfqTitle = q.getRfqTitle();
BigDecimal totalAmount = q.getTotalAmount();
notifyMessageService.sendQuotationExpireNotification(
q.getQuotationId(),
q.getQuoteNo(),
supplierName,
rfqTitle,
totalAmount,
daysRemaining,
createUser.getUserId()
);
remindCount++;
}
}
} catch (Exception e) {
log.error("报价到期提醒发送失败, quotationId={}", q.getQuotationId(), e);
}
}
}
log.info("=== 报价到期检查完成, 共发送 {} 条提醒 ===", remindCount);
}
}

View File

@@ -17,4 +17,58 @@
<select id="selectStatus" resultType="java.lang.String">
SELECT ${statusCol} FROM ${table} WHERE ${pk}=#{id}
</select>
<select id="selectBizCreateInfo" resultType="java.util.Map">
SELECT t.create_by AS createBy FROM ${table} t WHERE ${pk}=#{id}
</select>
<!-- 查询采购订单详情 -->
<select id="selectPurchaseOrderInfo" resultType="java.util.Map">
SELECT p.po_no AS bizNo, s.supplier_name AS partnerName, r.rfq_title AS bizTitle,
p.total_amount AS totalAmount, p.currency, p.create_time AS createTime, p.create_by AS createBy
FROM biz_purchase_order p
LEFT JOIN biz_supplier s ON p.supplier_id = s.supplier_id
LEFT JOIN biz_rfq r ON p.rfq_id = r.rfq_id
WHERE p.po_id = #{id}
</select>
<!-- 查询客户报价详情 -->
<select id="selectClientQuoteInfo" resultType="java.util.Map">
SELECT q.quote_no AS bizNo, q.client_name AS partnerName, q.rfq_title AS bizTitle,
q.total_amount AS totalAmount, q.currency, q.create_time AS createTime, q.create_by AS createBy
FROM biz_client_quote q
WHERE q.quote_id = #{id}
</select>
<!-- 查询供应商报价详情 -->
<select id="selectQuotationInfo" resultType="java.util.Map">
SELECT q.quote_no AS bizNo, s.supplier_name AS partnerName, r.rfq_title AS bizTitle,
q.total_amount AS totalAmount, q.currency, q.submit_time AS createTime, q.create_by AS createBy
FROM biz_quotation q
LEFT JOIN biz_supplier s ON q.supplier_id = s.supplier_id
LEFT JOIN biz_rfq r ON q.rfq_id = r.rfq_id
WHERE q.quotation_id = #{id}
</select>
<!-- 查询发货单详情 -->
<select id="selectDeliveryOrderInfo" resultType="java.util.Map">
SELECT d.do_no AS bizNo, s.supplier_name AS partnerName,
COALESCE(cl.client_name, cq.client_name) AS clientName,
d.total_amount AS totalAmount, d.currency, d.create_time AS createTime, d.create_by AS createBy
FROM biz_delivery_order d
LEFT JOIN biz_supplier s ON d.supplier_id = s.supplier_id
LEFT JOIN biz_client_quote cq ON d.client_quote_id = cq.quote_id
LEFT JOIN biz_client cl ON cq.client_id = cl.client_id
WHERE d.do_id = #{id}
</select>
<!-- 查询订单异议详情 -->
<select id="selectObjectionInfo" resultType="java.util.Map">
SELECT o.reason AS bizTitle, s.supplier_name AS partnerName, p.po_no AS bizNo,
o.create_time AS createTime, o.create_by AS createBy
FROM biz_order_objection o
LEFT JOIN biz_supplier s ON o.supplier_id = s.supplier_id
LEFT JOIN biz_purchase_order p ON o.po_id = p.po_id
WHERE o.objection_id = #{id}
</select>
</mapper>

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.bid.BizNotifyMessageMapper">
<resultMap id="NotifyMessageRM" type="com.ruoyi.system.domain.bid.BizNotifyMessage">
<id property="messageId" column="message_id"/>
<result property="tenantId" column="tenant_id"/>
<result property="userId" column="user_id"/>
<result property="noticeType" column="notice_type"/>
<result property="priority" column="priority"/>
<result property="title" column="title"/>
<result property="content" column="content"/>
<result property="bizType" column="biz_type"/>
<result property="bizId" column="biz_id"/>
<result property="bizUrl" column="biz_url"/>
<result property="isRead" column="is_read"/>
<result property="readTime" column="read_time"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
</resultMap>
<sql id="selectColumns">
message_id, tenant_id, user_id, notice_type, priority, title, content,
biz_type, biz_id, biz_url, is_read, read_time, create_by, create_time
</sql>
<select id="selectNotifyMessageList" parameterType="com.ruoyi.system.domain.bid.BizNotifyMessage" resultMap="NotifyMessageRM">
SELECT <include refid="selectColumns"/>
FROM biz_notify_message
<where>
<if test="userId != null"> AND user_id = #{userId} </if>
<if test="noticeType != null and noticeType != ''"> AND notice_type = #{noticeType} </if>
<if test="isRead != null and isRead != ''"> AND is_read = #{isRead} </if>
<if test="bizType != null and bizType != ''"> AND biz_type = #{bizType} </if>
<if test="priority != null"> AND priority = #{priority} </if>
<if test="params.beginTime != null and params.beginTime != ''"> AND create_time &gt;= #{params.beginTime} </if>
<if test="params.endTime != null and params.endTime != ''"> AND create_time &lt;= #{params.endTime} </if>
</where>
ORDER BY create_time DESC
</select>
<select id="selectNotifyMessageById" parameterType="Long" resultMap="NotifyMessageRM">
SELECT <include refid="selectColumns"/> FROM biz_notify_message WHERE message_id = #{messageId}
</select>
<insert id="insertNotifyMessage" parameterType="com.ruoyi.system.domain.bid.BizNotifyMessage" useGeneratedKeys="true" keyProperty="messageId">
INSERT INTO biz_notify_message
(tenant_id, user_id, notice_type, priority, title, content, biz_type, biz_id, biz_url, is_read, create_by, create_time)
VALUES
(#{tenantId}, #{userId}, #{noticeType}, #{priority}, #{title}, #{content}, #{bizType}, #{bizId}, #{bizUrl}, #{isRead}, #{createBy}, sysdate())
</insert>
<insert id="batchInsertNotifyMessage" parameterType="java.util.List">
INSERT INTO biz_notify_message
(tenant_id, user_id, notice_type, priority, title, content, biz_type, biz_id, biz_url, is_read, create_by, create_time)
VALUES
<foreach collection="list" item="m" separator=",">
(#{m.tenantId}, #{m.userId}, #{m.noticeType}, #{m.priority}, #{m.title}, #{m.content}, #{m.bizType}, #{m.bizId}, #{m.bizUrl}, '0', #{m.createBy}, sysdate())
</foreach>
</insert>
<update id="updateNotifyMessage" parameterType="com.ruoyi.system.domain.bid.BizNotifyMessage">
UPDATE biz_notify_message
<set>
<if test="isRead != null and isRead != ''"> is_read = #{isRead}, </if>
<if test="readTime != null"> read_time = #{readTime}, </if>
</set>
WHERE message_id = #{messageId}
</update>
<update id="markAsRead">
UPDATE biz_notify_message SET is_read = '1', read_time = sysdate()
WHERE message_id = #{messageId} AND user_id = #{userId} AND is_read = '0'
</update>
<update id="markAllAsRead">
UPDATE biz_notify_message SET is_read = '1', read_time = sysdate()
WHERE user_id = #{userId} AND is_read = '0'
</update>
<delete id="deleteNotifyMessageByIds">
DELETE FROM biz_notify_message WHERE message_id IN
<foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach>
</delete>
<select id="selectUnreadCount" resultType="int">
SELECT COUNT(*) FROM biz_notify_message WHERE user_id = #{userId} AND is_read = '0'
</select>
<select id="selectNotifyStats" resultType="java.util.Map">
SELECT
notice_type AS noticeType,
SUM(CASE WHEN is_read = '0' THEN 1 ELSE 0 END) AS unreadCount,
COUNT(*) AS totalCount
FROM biz_notify_message
WHERE user_id = #{userId}
GROUP BY notice_type
</select>
<select id="selectTopUnread" resultMap="NotifyMessageRM">
SELECT <include refid="selectColumns"/>
FROM biz_notify_message
WHERE user_id = #{userId} AND is_read = '0'
ORDER BY priority DESC, create_time DESC
LIMIT #{limit}
</select>
<select id="selectTopNotify" resultMap="NotifyMessageRM">
SELECT <include refid="selectColumns"/>
FROM biz_notify_message
WHERE user_id = #{userId}
ORDER BY priority DESC, create_time DESC
LIMIT #{limit}
</select>
</mapper>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.bid.BizNotifyRuleMapper">
<resultMap id="NotifyRuleRM" type="com.ruoyi.system.domain.bid.BizNotifyRule">
<id property="ruleId" column="rule_id"/>
<result property="tenantId" column="tenant_id"/>
<result property="ruleName" column="rule_name"/>
<result property="noticeType" column="notice_type"/>
<result property="bizType" column="biz_type"/>
<result property="triggerCondition" column="trigger_condition"/>
<result property="advanceDays" column="advance_days"/>
<result property="enabled" column="enabled"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
</resultMap>
<select id="selectNotifyRuleList" parameterType="com.ruoyi.system.domain.bid.BizNotifyRule" resultMap="NotifyRuleRM">
SELECT rule_id, tenant_id, rule_name, notice_type, biz_type, trigger_condition, advance_days, enabled, create_by, create_time
FROM biz_notify_rule
<where>
<if test="noticeType != null and noticeType != ''"> AND notice_type = #{noticeType} </if>
<if test="bizType != null and bizType != ''"> AND biz_type = #{bizType} </if>
<if test="enabled != null and enabled != ''"> AND enabled = #{enabled} </if>
</where>
ORDER BY rule_id
</select>
<select id="selectNotifyRuleById" parameterType="Long" resultMap="NotifyRuleRM">
SELECT rule_id, tenant_id, rule_name, notice_type, biz_type, trigger_condition, advance_days, enabled, create_by, create_time
FROM biz_notify_rule WHERE rule_id = #{ruleId}
</select>
<insert id="insertNotifyRule" parameterType="com.ruoyi.system.domain.bid.BizNotifyRule" useGeneratedKeys="true" keyProperty="ruleId">
INSERT INTO biz_notify_rule (tenant_id, rule_name, notice_type, biz_type, trigger_condition, advance_days, enabled, create_by, create_time)
VALUES (#{tenantId}, #{ruleName}, #{noticeType}, #{bizType}, #{triggerCondition}, #{advanceDays}, #{enabled}, #{createBy}, sysdate())
</insert>
<update id="updateNotifyRule" parameterType="com.ruoyi.system.domain.bid.BizNotifyRule">
UPDATE biz_notify_rule
<set>
<if test="ruleName != null and ruleName != ''"> rule_name = #{ruleName}, </if>
<if test="noticeType != null and noticeType != ''"> notice_type = #{noticeType}, </if>
<if test="bizType != null"> biz_type = #{bizType}, </if>
<if test="triggerCondition != null"> trigger_condition = #{triggerCondition}, </if>
<if test="advanceDays != null"> advance_days = #{advanceDays}, </if>
<if test="enabled != null and enabled != ''"> enabled = #{enabled}, </if>
</set>
WHERE rule_id = #{ruleId}
</update>
<delete id="deleteNotifyRuleByIds">
DELETE FROM biz_notify_rule WHERE rule_id IN
<foreach collection="array" item="id" open="(" separator="," close=")"> #{id} </foreach>
</delete>
<select id="selectEnabledRules" resultMap="NotifyRuleRM">
SELECT rule_id, tenant_id, rule_name, notice_type, biz_type, trigger_condition, advance_days, enabled, create_by, create_time
FROM biz_notify_rule WHERE enabled = '1'
</select>
</mapper>