From 8bdb8d7c230a369e499a0a56fd0442b524c87814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=96=87=E6=98=8A?= Date: Sun, 21 Jun 2026 04:20:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E4=B8=AD=E5=BF=83=E5=85=A8=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增消息通知相关实体、Mapper、Service、控制器与前端页面 2. 实现审批通知、报价到期提醒等通知发送逻辑 3. 完成通知菜单配置与路由注册 4. 修复通知数据与跳转路径问题 5. 新增配套SQL脚本与定时任务 --- .../controller/bid/BizNotifyController.java | 127 +++++++ .../system/domain/bid/BizNotifyMessage.java | 52 +++ .../system/domain/bid/BizNotifyRule.java | 36 ++ .../mapper/bid/BizApprovalActionMapper.java | 20 ++ .../mapper/bid/BizNotifyMessageMapper.java | 32 ++ .../mapper/bid/BizNotifyRuleMapper.java | 18 + .../service/bid/IBizNotifyMessageService.java | 73 ++++ .../service/bid/IBizNotifyRuleService.java | 13 + .../impl/BizApprovalActionServiceImpl.java | 236 ++++++++++++- .../bid/impl/BizNotifyMessageServiceImpl.java | 155 +++++++++ .../bid/impl/BizNotifyRuleServiceImpl.java | 45 +++ .../system/task/QuotationExpireTask.java | 95 ++++++ .../mapper/bid/BizApprovalActionMapper.xml | 54 +++ .../mapper/bid/BizNotifyMessageMapper.xml | 116 +++++++ .../mapper/bid/BizNotifyRuleMapper.xml | 62 ++++ ruoyi-ui/src/api/bid/notify.js | 71 ++++ .../layout/components/HeaderNotice/index.vue | 316 +++++++++++------- ruoyi-ui/src/layout/components/Navbar.vue | 4 +- ruoyi-ui/src/router/index.js | 15 + ruoyi-ui/src/views/bid/notify/index.vue | 270 +++++++++++++++ sql/_q2.sql | 5 + sql/_q3.sql | 1 + sql/_query.sql | 3 + sql/bid_notify.sql | 72 ++++ sql/bid_notify_fix.sql | 20 ++ sql/bid_notify_fix2.sql | 21 ++ sql/bid_notify_fix3.sql | 17 + 27 files changed, 1817 insertions(+), 132 deletions(-) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/bid/BizNotifyController.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/bid/BizNotifyMessage.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/bid/BizNotifyRule.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizNotifyMessageMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizNotifyRuleMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/bid/IBizNotifyMessageService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/bid/IBizNotifyRuleService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizNotifyMessageServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizNotifyRuleServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/task/QuotationExpireTask.java create mode 100644 ruoyi-system/src/main/resources/mapper/bid/BizNotifyMessageMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/bid/BizNotifyRuleMapper.xml create mode 100644 ruoyi-ui/src/api/bid/notify.js create mode 100644 ruoyi-ui/src/views/bid/notify/index.vue create mode 100644 sql/_q2.sql create mode 100644 sql/_q3.sql create mode 100644 sql/_query.sql create mode 100644 sql/bid_notify.sql create mode 100644 sql/bid_notify_fix.sql create mode 100644 sql/bid_notify_fix2.sql create mode 100644 sql/bid_notify_fix3.sql diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/bid/BizNotifyController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/bid/BizNotifyController.java new file mode 100644 index 00000000..4467b1f7 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/bid/BizNotifyController.java @@ -0,0 +1,127 @@ +package com.ruoyi.web.controller.bid; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.system.domain.bid.BizNotifyMessage; +import com.ruoyi.system.domain.bid.BizNotifyRule; +import com.ruoyi.system.service.bid.IBizNotifyMessageService; +import com.ruoyi.system.service.bid.IBizNotifyRuleService; +import com.ruoyi.common.utils.SecurityUtils; + +@RestController +@RequestMapping("/bid/notify") +public class BizNotifyController extends BaseController { + + @Autowired + private IBizNotifyMessageService notifyMessageService; + + @Autowired + private IBizNotifyRuleService notifyRuleService; + + // ==================== 消息通知接口 ==================== + + @PreAuthorize("@ss.hasPermi('bid:notify:list')") + @GetMapping("/list") + public TableDataInfo list(BizNotifyMessage query) { + // 强制按当前用户过滤 + query.setUserId(SecurityUtils.getUserId()); + startPage(); + List list = notifyMessageService.selectNotifyMessageList(query); + return getDataTable(list); + } + + @PreAuthorize("@ss.hasPermi('bid:notify:list')") + @GetMapping("/unreadCount") + public AjaxResult unreadCount() { + return success(notifyMessageService.selectUnreadCount(SecurityUtils.getUserId())); + } + + @PreAuthorize("@ss.hasPermi('bid:notify:list')") + @GetMapping("/topUnread") + public AjaxResult topUnread(@RequestParam(defaultValue = "5") int limit) { + return success(notifyMessageService.selectTopUnread(SecurityUtils.getUserId(), limit)); + } + + @PreAuthorize("@ss.hasPermi('bid:notify:list')") + @GetMapping("/topNotify") + public AjaxResult topNotify(@RequestParam(defaultValue = "10") int limit) { + return success(notifyMessageService.selectTopNotify(SecurityUtils.getUserId(), limit)); + } + + @PreAuthorize("@ss.hasPermi('bid:notify:list')") + @GetMapping("/stats") + public AjaxResult stats() { + return success(notifyMessageService.selectNotifyStats(SecurityUtils.getUserId())); + } + + @PreAuthorize("@ss.hasPermi('bid:notify:query')") + @GetMapping("/{messageId}") + public AjaxResult getInfo(@PathVariable("messageId") Long messageId) { + return success(notifyMessageService.selectNotifyMessageById(messageId)); + } + + @PreAuthorize("@ss.hasPermi('bid:notify:read')") + @PutMapping("/read/{messageId}") + public AjaxResult markRead(@PathVariable("messageId") Long messageId) { + return toAjax(notifyMessageService.markAsRead(messageId, SecurityUtils.getUserId())); + } + + @PreAuthorize("@ss.hasPermi('bid:notify:read')") + @PutMapping("/readAll") + public AjaxResult markAllRead() { + return toAjax(notifyMessageService.markAllAsRead(SecurityUtils.getUserId())); + } + + @PreAuthorize("@ss.hasPermi('bid:notify:remove')") + @Log(title = "消息通知", businessType = BusinessType.DELETE) + @DeleteMapping("/{messageIds}") + public AjaxResult remove(@PathVariable Long[] messageIds) { + return toAjax(notifyMessageService.deleteNotifyMessageByIds(messageIds)); + } + + // ==================== 通知规则接口 ==================== + + @PreAuthorize("@ss.hasPermi('bid:notify:rule')") + @GetMapping("/rule/list") + public TableDataInfo ruleList(BizNotifyRule query) { + startPage(); + List list = notifyRuleService.selectNotifyRuleList(query); + return getDataTable(list); + } + + @PreAuthorize("@ss.hasPermi('bid:notify:rule')") + @GetMapping("/rule/{ruleId}") + public AjaxResult getRule(@PathVariable("ruleId") Long ruleId) { + return success(notifyRuleService.selectNotifyRuleById(ruleId)); + } + + @PreAuthorize("@ss.hasPermi('bid:notify:rule')") + @Log(title = "通知规则", businessType = BusinessType.INSERT) + @PostMapping("/rule") + public AjaxResult addRule(@RequestBody BizNotifyRule rule) { + rule.setCreateBy(getUsername()); + return toAjax(notifyRuleService.insertNotifyRule(rule)); + } + + @PreAuthorize("@ss.hasPermi('bid:notify:rule')") + @Log(title = "通知规则", businessType = BusinessType.UPDATE) + @PutMapping("/rule") + public AjaxResult editRule(@RequestBody BizNotifyRule rule) { + rule.setUpdateBy(getUsername()); + return toAjax(notifyRuleService.updateNotifyRule(rule)); + } + + @PreAuthorize("@ss.hasPermi('bid:notify:rule')") + @Log(title = "通知规则", businessType = BusinessType.DELETE) + @DeleteMapping("/rule/{ruleIds}") + public AjaxResult removeRule(@PathVariable Long[] ruleIds) { + return toAjax(notifyRuleService.deleteNotifyRuleByIds(ruleIds)); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bid/BizNotifyMessage.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bid/BizNotifyMessage.java new file mode 100644 index 00000000..d25745bf --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bid/BizNotifyMessage.java @@ -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; } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bid/BizNotifyRule.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bid/BizNotifyRule.java new file mode 100644 index 00000000..1ad12b9c --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bid/BizNotifyRule.java @@ -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; } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizApprovalActionMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizApprovalActionMapper.java index 1e66cc43..6fb399c4 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizApprovalActionMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizApprovalActionMapper.java @@ -16,4 +16,24 @@ public interface BizApprovalActionMapper { @Param("pk") String pk, @Param("statusCol") String statusCol, @Param("id") Long id); + + /** 查询业务单据的 create_by(提交人用户名) */ + java.util.Map selectBizCreateInfo(@Param("table") String table, + @Param("pk") String pk, + @Param("id") Long id); + + /** 查询采购订单详情(含供应商名称、询价标题、金额) */ + java.util.Map selectPurchaseOrderInfo(@Param("id") Long id); + + /** 查询客户报价详情(含客户名称、询价标题、金额) */ + java.util.Map selectClientQuoteInfo(@Param("id") Long id); + + /** 查询供应商报价详情(含供应商名称、询价标题、金额) */ + java.util.Map selectQuotationInfo(@Param("id") Long id); + + /** 查询发货单详情(含供应商/客户名称、金额) */ + java.util.Map selectDeliveryOrderInfo(@Param("id") Long id); + + /** 查询订单异议详情(含供应商名称、采购单号) */ + java.util.Map selectObjectionInfo(@Param("id") Long id); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizNotifyMessageMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizNotifyMessageMapper.java new file mode 100644 index 00000000..f2a681d4 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizNotifyMessageMapper.java @@ -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 selectNotifyMessageList(BizNotifyMessage query); + + public BizNotifyMessage selectNotifyMessageById(Long messageId); + + public int insertNotifyMessage(BizNotifyMessage message); + + public int batchInsertNotifyMessage(@Param("list") List 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> selectNotifyStats(@Param("userId") Long userId); + + public List selectTopUnread(@Param("userId") Long userId, @Param("limit") int limit); + + public List selectTopNotify(@Param("userId") Long userId, @Param("limit") int limit); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizNotifyRuleMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizNotifyRuleMapper.java new file mode 100644 index 00000000..c08faee0 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizNotifyRuleMapper.java @@ -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 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 selectEnabledRules(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/IBizNotifyMessageService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/IBizNotifyMessageService.java new file mode 100644 index 00000000..330c3909 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/IBizNotifyMessageService.java @@ -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 selectNotifyMessageList(BizNotifyMessage query); + + public BizNotifyMessage selectNotifyMessageById(Long messageId); + + public int insertNotifyMessage(BizNotifyMessage message); + + public int batchInsertNotifyMessage(List 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> selectNotifyStats(Long userId); + + public List selectTopUnread(Long userId, int limit); + + /** + * 查询最近N条通知(含已读和未读),用于铃铛面板 + */ + public List 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); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/IBizNotifyRuleService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/IBizNotifyRuleService.java new file mode 100644 index 00000000..6d07a508 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/IBizNotifyRuleService.java @@ -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 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 selectEnabledRules(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizApprovalActionServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizApprovalActionServiceImpl.java index c5b53a22..c4a73228 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizApprovalActionServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizApprovalActionServiceImpl.java @@ -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 draftStatuses; - Meta(String t, String pk, String sc, String approved, List drafts) { + Meta(String t, String pk, String sc, String approved, List 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 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 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 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 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 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 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; + } + } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizNotifyMessageServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizNotifyMessageServiceImpl.java new file mode 100644 index 00000000..e5b1acf4 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizNotifyMessageServiceImpl.java @@ -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 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 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> selectNotifyStats(Long userId) { + return notifyMessageMapper.selectNotifyStats(userId); + } + + @Override + public List selectTopUnread(Long userId, int limit) { + return notifyMessageMapper.selectTopUnread(userId, limit); + } + + @Override + public List 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; + } + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizNotifyRuleServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizNotifyRuleServiceImpl.java new file mode 100644 index 00000000..c4569f2e --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizNotifyRuleServiceImpl.java @@ -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 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 selectEnabledRules() { + return notifyRuleMapper.selectEnabledRules(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/task/QuotationExpireTask.java b/ruoyi-system/src/main/java/com/ruoyi/system/task/QuotationExpireTask.java new file mode 100644 index 00000000..1634feaf --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/task/QuotationExpireTask.java @@ -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 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); + } +} diff --git a/ruoyi-system/src/main/resources/mapper/bid/BizApprovalActionMapper.xml b/ruoyi-system/src/main/resources/mapper/bid/BizApprovalActionMapper.xml index 04edc68b..ecf6118c 100644 --- a/ruoyi-system/src/main/resources/mapper/bid/BizApprovalActionMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/bid/BizApprovalActionMapper.xml @@ -17,4 +17,58 @@ + + + + + + + + + + + + + + + + + diff --git a/ruoyi-system/src/main/resources/mapper/bid/BizNotifyMessageMapper.xml b/ruoyi-system/src/main/resources/mapper/bid/BizNotifyMessageMapper.xml new file mode 100644 index 00000000..4b57fe7f --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/bid/BizNotifyMessageMapper.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + + + 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 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 + + (#{m.tenantId}, #{m.userId}, #{m.noticeType}, #{m.priority}, #{m.title}, #{m.content}, #{m.bizType}, #{m.bizId}, #{m.bizUrl}, '0', #{m.createBy}, sysdate()) + + + + + UPDATE biz_notify_message + + is_read = #{isRead}, + read_time = #{readTime}, + + WHERE message_id = #{messageId} + + + + UPDATE biz_notify_message SET is_read = '1', read_time = sysdate() + WHERE message_id = #{messageId} AND user_id = #{userId} AND is_read = '0' + + + + UPDATE biz_notify_message SET is_read = '1', read_time = sysdate() + WHERE user_id = #{userId} AND is_read = '0' + + + + DELETE FROM biz_notify_message WHERE message_id IN + #{id} + + + + + + + + + + + diff --git a/ruoyi-system/src/main/resources/mapper/bid/BizNotifyRuleMapper.xml b/ruoyi-system/src/main/resources/mapper/bid/BizNotifyRuleMapper.xml new file mode 100644 index 00000000..5e4b7461 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/bid/BizNotifyRuleMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + 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()) + + + + UPDATE biz_notify_rule + + rule_name = #{ruleName}, + notice_type = #{noticeType}, + biz_type = #{bizType}, + trigger_condition = #{triggerCondition}, + advance_days = #{advanceDays}, + enabled = #{enabled}, + + WHERE rule_id = #{ruleId} + + + + DELETE FROM biz_notify_rule WHERE rule_id IN + #{id} + + + + + diff --git a/ruoyi-ui/src/api/bid/notify.js b/ruoyi-ui/src/api/bid/notify.js new file mode 100644 index 00000000..809e414b --- /dev/null +++ b/ruoyi-ui/src/api/bid/notify.js @@ -0,0 +1,71 @@ +import request from '@/utils/request' + +// 查询消息通知列表 +export function listNotify(data) { + return request({ url: '/bid/notify/list', method: 'get', params: data }) +} + +// 查询未读数量 +export function getUnreadCount() { + return request({ url: '/bid/notify/unreadCount', method: 'get' }) +} + +// 查询最新未读消息(仅未读) +export function getTopUnread(limit) { + return request({ url: '/bid/notify/topUnread', method: 'get', params: { limit } }) +} + +// 查询最近通知(含已读和未读,用于铃铛面板) +export function getTopNotify(limit) { + return request({ url: '/bid/notify/topNotify', method: 'get', params: { limit } }) +} + +// 查询通知统计 +export function getNotifyStats() { + return request({ url: '/bid/notify/stats', method: 'get' }) +} + +// 查询消息详情 +export function getNotify(messageId) { + return request({ url: '/bid/notify/' + messageId, method: 'get' }) +} + +// 标记单条已读 +export function markRead(messageId) { + return request({ url: '/bid/notify/read/' + messageId, method: 'put' }) +} + +// 全部已读 +export function markAllRead() { + return request({ url: '/bid/notify/readAll', method: 'put' }) +} + +// 删除消息 +export function delNotify(messageIds) { + return request({ url: '/bid/notify/' + messageIds, method: 'delete' }) +} + +// 通知规则列表 +export function listNotifyRule(data) { + return request({ url: '/bid/notify/rule/list', method: 'get', params: data }) +} + +// 通知规则详情 +export function getNotifyRule(ruleId) { + return request({ url: '/bid/notify/rule/' + ruleId, method: 'get' }) +} + +// 新增规则 +export function addNotifyRule(data) { + return request({ url: '/bid/notify/rule', method: 'post', data }) +} + +// 修改规则 +export function updateNotifyRule(data) { + return request({ url: '/bid/notify/rule', method: 'put', data }) +} + +// 删除规则 +export function delNotifyRule(ruleIds) { + return request({ url: '/bid/notify/rule/' + ruleIds, method: 'delete' }) +} diff --git a/ruoyi-ui/src/layout/components/HeaderNotice/index.vue b/ruoyi-ui/src/layout/components/HeaderNotice/index.vue index ea3d8fbf..6e9e4357 100644 --- a/ruoyi-ui/src/layout/components/HeaderNotice/index.vue +++ b/ruoyi-ui/src/layout/components/HeaderNotice/index.vue @@ -1,110 +1,191 @@ + + diff --git a/ruoyi-ui/src/layout/components/Navbar.vue b/ruoyi-ui/src/layout/components/Navbar.vue index 5e8b2074..924e4004 100644 --- a/ruoyi-ui/src/layout/components/Navbar.vue +++ b/ruoyi-ui/src/layout/components/Navbar.vue @@ -18,9 +18,7 @@ - - - + diff --git a/ruoyi-ui/src/router/index.js b/ruoyi-ui/src/router/index.js index fde63a35..9db2f6cf 100644 --- a/ruoyi-ui/src/router/index.js +++ b/ruoyi-ui/src/router/index.js @@ -307,6 +307,21 @@ export const dynamicRoutes = [ }] }, + { + path: '/bizconfig/notify', + component: Layout, + hidden: true, + permissions: ['bid:notify:list'], + children: [ + { + path: '', + component: () => import('@/views/bid/notify/index'), + name: 'NotifyCenter', + meta: { title: '消息通知中心', activeMenu: '/bizconfig/notify' } + } + ] + }, + { path: '/system/user-auth', component: Layout, diff --git a/ruoyi-ui/src/views/bid/notify/index.vue b/ruoyi-ui/src/views/bid/notify/index.vue new file mode 100644 index 00000000..610e788f --- /dev/null +++ b/ruoyi-ui/src/views/bid/notify/index.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/sql/_q2.sql b/sql/_q2.sql new file mode 100644 index 00000000..c625025f --- /dev/null +++ b/sql/_q2.sql @@ -0,0 +1,5 @@ +SELECT menu_id, menu_name, parent_id, path FROM sys_menu WHERE parent_id=2000 ORDER BY menu_id; +SELECT '---bid_sub2---' AS ''; +SELECT m.menu_id, m.menu_name, m.parent_id, m.path, m.component FROM sys_menu m WHERE m.parent_id IN (SELECT menu_id FROM sys_menu WHERE parent_id=2000); +SELECT '---bizconfig---' AS ''; +SELECT menu_id, menu_name, parent_id, path, component FROM sys_menu WHERE parent_id=2130; diff --git a/sql/_q3.sql b/sql/_q3.sql new file mode 100644 index 00000000..1693c8bc --- /dev/null +++ b/sql/_q3.sql @@ -0,0 +1 @@ +SELECT menu_id, menu_name, parent_id, path, component FROM sys_menu WHERE parent_id=2060; diff --git a/sql/_query.sql b/sql/_query.sql new file mode 100644 index 00000000..4bc13579 --- /dev/null +++ b/sql/_query.sql @@ -0,0 +1,3 @@ +SELECT menu_id, menu_name, parent_id, order_num, path FROM sys_menu WHERE menu_name LIKE '%approval%' OR menu_name LIKE '%待我%' OR path LIKE '%approval%' OR path LIKE '%待我%'; +SELECT '---' AS 'all_menus'; +SELECT menu_id, menu_name, parent_id, order_num, path FROM sys_menu ORDER BY parent_id, order_num; diff --git a/sql/bid_notify.sql b/sql/bid_notify.sql new file mode 100644 index 00000000..3ca42a13 --- /dev/null +++ b/sql/bid_notify.sql @@ -0,0 +1,72 @@ +-- ════════════════════════════════════════════════════════════════════ +-- 消息通知中心 - 数据库表结构 +-- 功能:统一管理站内通知,支持审批结果、报价到期、任务分配等场景 +-- 执行方式: mysql --default-character-set=utf8mb4 -u root -p < bid_notify.sql +-- ════════════════════════════════════════════════════════════════════ +SET NAMES utf8mb4; + +-- 1. 消息通知表(存储所有通知消息) +DROP TABLE IF EXISTS biz_notify_message; +CREATE TABLE biz_notify_message ( + message_id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '消息ID', + tenant_id BIGINT DEFAULT 0 COMMENT '租户ID', + user_id BIGINT NOT NULL COMMENT '接收人用户ID', + notice_type VARCHAR(32) NOT NULL COMMENT '通知类型(approval-审批结果/quotation_expire-报价到期/rfq_deadline-RFQ截止/task-任务分配/system-系统公告/exception-异常提醒)', + priority TINYINT DEFAULT 0 COMMENT '优先级(0普通 1重要 2紧急)', + title VARCHAR(200) NOT NULL COMMENT '消息标题', + content TEXT COMMENT '消息内容', + biz_type VARCHAR(32) COMMENT '关联业务类型(PURCHASE_ORDER/CLIENT_QUOTE/QUOTATION/DELIVERY_ORDER/ORDER_OBJECTION)', + biz_id BIGINT COMMENT '关联业务ID', + biz_url VARCHAR(255) COMMENT '业务跳转URL', + is_read CHAR(1) DEFAULT '0' COMMENT '是否已读(0未读 1已读)', + read_time DATETIME COMMENT '阅读时间', + create_by VARCHAR(64) DEFAULT '' COMMENT '创建者', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_by VARCHAR(64) DEFAULT '' COMMENT '更新者', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + KEY idx_user_read (user_id, is_read), + KEY idx_user_type (user_id, notice_type), + KEY idx_tenant (tenant_id), + KEY idx_create_time (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息通知表'; + +-- 2. 通知规则配置表(定义各类通知的触发规则) +DROP TABLE IF EXISTS biz_notify_rule; +CREATE TABLE biz_notify_rule ( + rule_id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '规则ID', + tenant_id BIGINT DEFAULT 0 COMMENT '租户ID', + rule_name VARCHAR(100) NOT NULL COMMENT '规则名称', + notice_type VARCHAR(32) NOT NULL COMMENT '通知类型', + biz_type VARCHAR(32) COMMENT '业务类型', + trigger_condition VARCHAR(500) COMMENT '触发条件(JSON格式)', + advance_days INT DEFAULT 0 COMMENT '提前提醒天数(用于到期类提醒)', + enabled CHAR(1) DEFAULT '0' COMMENT '是否启用(0停用 1启用)', + create_by VARCHAR(64) DEFAULT '' COMMENT '创建者', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_by VARCHAR(64) DEFAULT '' COMMENT '更新者', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + KEY idx_tenant_type (tenant_id, notice_type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知规则配置表'; + +-- 3. 初始化默认通知规则 +INSERT INTO biz_notify_rule (rule_name, notice_type, biz_type, trigger_condition, advance_days, enabled, create_by) VALUES +('审批结果通知', 'approval', NULL, '{"event":"approve,reject"}', 0, '1', 'admin'), +('报价到期提醒', 'quotation_expire', 'QUOTATION', '{"field":"submit_time","unit":"day"}', 3, '1', 'admin'), +('报价到期紧急提醒', 'quotation_expire', 'QUOTATION', '{"field":"submit_time","unit":"day"}', 1, '1', 'admin'), +('RFQ截止提醒', 'rfq_deadline', NULL, '{"field":"deadline","unit":"day"}', 2, '1', 'admin'), +('系统公告', 'system', NULL, '{}', 0, '1', 'admin'); + +-- 4. 菜单权限初始化 +-- 消息通知中心菜单(挂在"系统配置"目录 menu_id=2130 path=bizconfig 下,与"待我审批"同级) +DELETE FROM sys_menu WHERE menu_id IN (2170, 2171, 2172, 2173, 2174); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES +(2170, '消息通知中心', 2130, 100, 'notify', 'bid/notify/index', 1, 0, 'C', '0', '0', 'bid:notify:list', 'message', 'admin', sysdate(), '消息通知中心'), +(2171, '通知查询', 2170, 1, '', '', 1, 0, 'F', '0', '0', 'bid:notify:query', '#', 'admin', sysdate(), ''), +(2172, '通知删除', 2170, 2, '', '', 1, 0, 'F', '0', '0', 'bid:notify:remove', '#', 'admin', sysdate(), ''), +(2173, '标记已读', 2170, 3, '', '', 1, 0, 'F', '0', '0', 'bid:notify:read', '#', 'admin', sysdate(), ''), +(2174, '通知规则配置', 2170, 4, '', '', 1, 0, 'F', '0', '0', 'bid:notify:rule', '#', 'admin', sysdate(), ''); + +-- 为 admin 角色授权 +DELETE FROM sys_role_menu WHERE role_id = 1 AND menu_id IN (2170, 2171, 2172, 2173, 2174); +INSERT INTO sys_role_menu (role_id, menu_id) VALUES +(1, 2170), (1, 2171), (1, 2172), (1, 2173), (1, 2174); diff --git a/sql/bid_notify_fix.sql b/sql/bid_notify_fix.sql new file mode 100644 index 00000000..1670f0b4 --- /dev/null +++ b/sql/bid_notify_fix.sql @@ -0,0 +1,20 @@ +-- ════════════════════════════════════════════════════════════════════ +-- 消息通知中心 - Bug 修复脚本 +-- 修复内容: +-- 1. 注册 Quartz 定时任务(报价到期检查) +-- 2. 将现有待审批的 CLIENT_QUOTE(quote_id=6) 状态重置为 draft +-- ════════════════════════════════════════════════════════════════════ +SET NAMES utf8mb4; + +-- 1. 注册 Quartz 定时任务(报价到期检查,每天上午9点执行) +-- 如果已通过 UI 注册则跳过 +DELETE FROM sys_job WHERE job_name = '报价到期检查' AND job_group = 'DEFAULT'; +INSERT INTO sys_job (job_id, job_name, job_group, invoke_target, cron_expression, + misfire_policy, concurrent, status, create_by, create_time, remark) +VALUES (100, '报价到期检查', 'DEFAULT', + 'quotationExpireTask.checkQuotationExpire()', + '0 0 9 * * ?', + '3', '1', '0', 'admin', sysdate(), '报价到期提醒,每天检查即将到期和已过期的报价单'); + +-- 2. 将 quote_id=6 的测试数据重置为 draft 方便重新测试审批提交通知 +UPDATE biz_client_quote SET status = 'draft', update_time = sysdate() WHERE quote_id = 6 AND status = '10'; diff --git a/sql/bid_notify_fix2.sql b/sql/bid_notify_fix2.sql new file mode 100644 index 00000000..0dffa7a2 --- /dev/null +++ b/sql/bid_notify_fix2.sql @@ -0,0 +1,21 @@ +-- ════════════════════════════════════════════════════════════════════ +-- 消息通知中心 - 修复脚本 #2 +-- 修复内容: +-- 1. 修复现有通知 is_read=NULL 的问题(导致铃铛未读数为0) +-- 2. 将消息通知中心菜单从"系统管理"下移到"系统配置"下,与"待我审批"同级 +-- ════════════════════════════════════════════════════════════════════ +SET NAMES utf8mb4; + +-- 1. 修复现有数据的 is_read=NULL → '0'(新消息默认未读) +UPDATE biz_notify_message SET is_read = '0' WHERE is_read IS NULL OR is_read = ''; + +-- 2. 移动"消息通知中心"菜单从"系统管理"(parent_id=1) 到"系统配置"(parent_id=2130) +-- 放到"待我审批"(order_num=99) 之后,order_num=100 +UPDATE sys_menu SET parent_id = 2130, order_num = 100 +WHERE menu_id = 2170 AND parent_id = 1; + +-- 3. 为 admin 角色重新授权(菜单路径变了,权限不受影响,但刷新一下角色-菜单关联) +-- 注: role_id=1(admin) 的关联已在初始化 SQL 中添加,这里不做重复插入 + +-- 4. 清理之前插入的测试通知记录(is_read 已修复,保留即可) +-- 无需操作 diff --git a/sql/bid_notify_fix3.sql b/sql/bid_notify_fix3.sql new file mode 100644 index 00000000..181a34a6 --- /dev/null +++ b/sql/bid_notify_fix3.sql @@ -0,0 +1,17 @@ +-- ════════════════════════════════════════════════════════════════════ +-- 消息通知中心 - 修复脚本 #3 +-- 修复内容: +-- 1. 修复通知菜单路径为绝对路径 /bid/notify(解决"查看全部" 404) +-- 2. 修复所有通知的 bizUrl 跳转路径(解决"查看详情" 404) +-- ════════════════════════════════════════════════════════════════════ +SET NAMES utf8mb4; + +-- 1. 修复通知菜单路径为绝对路径,使 /bid/notify 路由生效 +UPDATE sys_menu SET path = '/bid/notify' WHERE menu_id = 2170; + +-- 2. 修复现有通知的 bizUrl(将旧路径改为正确路径) +UPDATE biz_notify_message SET biz_url = REPLACE(biz_url, '/bid/purchaseorder?id=', '/quote/purchaseorder?id=') WHERE biz_url LIKE '/bid/purchaseorder?id=%'; +UPDATE biz_notify_message SET biz_url = REPLACE(biz_url, '/bid/clientquote?id=', '/bid/clientquote/detail?id=') WHERE biz_url LIKE '/bid/clientquote?id=%'; +UPDATE biz_notify_message SET biz_url = REPLACE(biz_url, '/bid/quotation?quotationId=', '/quote/quotation?quotationId=') WHERE biz_url LIKE '/bid/quotation?quotationId=%'; +UPDATE biz_notify_message SET biz_url = REPLACE(biz_url, '/bid/order/pending?id=', '/bid/order/pending?id=') WHERE biz_url LIKE '/bid/order/pending?id=%'; +UPDATE biz_notify_message SET biz_url = REPLACE(biz_url, '/bid/objection?id=', '/bid/order/objection?id=') WHERE biz_url LIKE '/bid/objection?id=%';