diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysNotifyController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysNotifyController.java
new file mode 100644
index 00000000..04f55393
--- /dev/null
+++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysNotifyController.java
@@ -0,0 +1,142 @@
+package com.bms.web.controller.system;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import com.bms.common.annotation.Log;
+import com.bms.common.core.controller.BaseController;
+import com.bms.common.core.domain.AjaxResult;
+import com.bms.common.core.page.TableDataInfo;
+import com.bms.common.enums.BusinessType;
+import com.bms.common.utils.SecurityUtils;
+import com.bms.notify.domain.NotifyMessage;
+import com.bms.notify.domain.NotifyRule;
+import com.bms.notify.service.INotifyService;
+
+/**
+ * 消息通知管理(系统管理)
+ *
+ *
规则/消息管理需 system:notify:* 权限;“我的消息”系列接口所有登录用户可用。
+ *
+ * @author bms
+ * @date 2026-10-09
+ */
+@RestController
+@RequestMapping("/system/notify")
+public class SysNotifyController extends BaseController
+{
+ @Autowired
+ private INotifyService notifyService;
+
+ /* ================= 通知规则 ================= */
+
+ @PreAuthorize("@ss.hasPermi('system:notify:list')")
+ @GetMapping("/rule/list")
+ public TableDataInfo ruleList(NotifyRule query)
+ {
+ startPage();
+ return getDataTable(notifyService.selectRuleList(query));
+ }
+
+ @PreAuthorize("@ss.hasPermi('system:notify:query')")
+ @GetMapping("/rule/{ruleId}")
+ public AjaxResult getRule(@PathVariable Long ruleId)
+ {
+ return success(notifyService.selectRuleById(ruleId));
+ }
+
+ @PreAuthorize("@ss.hasPermi('system:notify:add')")
+ @Log(title = "消息通知规则", businessType = BusinessType.INSERT)
+ @PostMapping("/rule")
+ public AjaxResult addRule(@RequestBody NotifyRule rule)
+ {
+ return toAjax(notifyService.insertRule(rule));
+ }
+
+ @PreAuthorize("@ss.hasPermi('system:notify:edit')")
+ @Log(title = "消息通知规则", businessType = BusinessType.UPDATE)
+ @PutMapping("/rule")
+ public AjaxResult editRule(@RequestBody NotifyRule rule)
+ {
+ return toAjax(notifyService.updateRule(rule));
+ }
+
+ @PreAuthorize("@ss.hasPermi('system:notify:remove')")
+ @Log(title = "消息通知规则", businessType = BusinessType.DELETE)
+ @DeleteMapping("/rule/{ruleIds}")
+ public AjaxResult removeRule(@PathVariable Long[] ruleIds)
+ {
+ return toAjax(notifyService.deleteRuleByIds(ruleIds));
+ }
+
+ /** 立即执行一次超额扫描(测试/补发用) */
+ @PreAuthorize("@ss.hasPermi('system:notify:edit')")
+ @Log(title = "客户超额通知扫描", businessType = BusinessType.OTHER)
+ @PostMapping("/scan")
+ public AjaxResult scanNow()
+ {
+ int count = notifyService.scanCustomerOverdue();
+ return AjaxResult.success("扫描完成,新生成通知 " + count + " 条", count);
+ }
+
+ /* ================= 通知记录 ================= */
+
+ @PreAuthorize("@ss.hasPermi('system:notify:list')")
+ @GetMapping("/message/list")
+ public TableDataInfo messageList(NotifyMessage query)
+ {
+ startPage();
+ return getDataTable(notifyService.selectMessageList(query));
+ }
+
+ @PreAuthorize("@ss.hasPermi('system:notify:query')")
+ @GetMapping("/message/{messageId}")
+ public AjaxResult getMessage(@PathVariable Long messageId)
+ {
+ return success(notifyService.selectMessageById(messageId));
+ }
+
+ /* ================= 我的消息(所有登录用户) ================= */
+
+ /** 登录后待弹屏消息;拉取即置弹屏已展示,避免刷新/每次进首页重复打扰 */
+ @GetMapping("/mine/popup")
+ public AjaxResult minePopup()
+ {
+ return success(notifyService.popupAndMarkShown(SecurityUtils.getUserId()));
+ }
+
+ @GetMapping("/mine/list")
+ public AjaxResult mineList(@RequestParam(required = false) String readFlag)
+ {
+ return success(notifyService.selectMineList(SecurityUtils.getUserId(), readFlag));
+ }
+
+ @GetMapping("/mine/count")
+ public AjaxResult mineCount()
+ {
+ return success(notifyService.countUnread(SecurityUtils.getUserId()));
+ }
+
+ @Log(title = "我的通知-已读", businessType = BusinessType.OTHER)
+ @PutMapping("/mine/read/{receiveId}")
+ public AjaxResult mineRead(@PathVariable Long receiveId)
+ {
+ return toAjax(notifyService.markRead(receiveId, SecurityUtils.getUserId()));
+ }
+
+ @Log(title = "我的通知-全部已读", businessType = BusinessType.OTHER)
+ @PutMapping("/mine/readAll")
+ public AjaxResult mineReadAll()
+ {
+ return toAjax(notifyService.markAllRead(SecurityUtils.getUserId()));
+ }
+}
diff --git a/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyMessage.java b/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyMessage.java
new file mode 100644
index 00000000..84a8f79b
--- /dev/null
+++ b/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyMessage.java
@@ -0,0 +1,59 @@
+package com.bms.notify.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+/**
+ * 通知消息 sys_notify_message
+ *
+ * @author bms
+ * @date 2026-10-09
+ */
+@Data
+public class NotifyMessage
+{
+ /** 消息ID */
+ private Long messageId;
+
+ /** 事件类型 */
+ private String eventType;
+
+ /** 标题 */
+ private String title;
+
+ /** 内容 */
+ private String content;
+
+ /** 关联客户ID */
+ private Long customerId;
+
+ /** 客户编号快照 */
+ private String customerCode;
+
+ /** 客户名称快照 */
+ private String customerName;
+
+ /** 超额金额(CNY,正数) */
+ private BigDecimal overdueAmount;
+
+ /** 创建者(系统任务记为 system) */
+ private String createBy;
+
+ /** 创建时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date createTime;
+
+ /* ===== 管理页统计/明细字段 ===== */
+
+ /** 接收人数 */
+ private Integer receiveCount;
+
+ /** 已读人数 */
+ private Integer readCount;
+
+ /** 接收明细 */
+ private List receives;
+}
diff --git a/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyOverdueCustomer.java b/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyOverdueCustomer.java
new file mode 100644
index 00000000..968e4084
--- /dev/null
+++ b/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyOverdueCustomer.java
@@ -0,0 +1,38 @@
+package com.bms.notify.domain;
+
+import java.math.BigDecimal;
+import lombok.Data;
+
+/**
+ * 超额客户扫描结果(只读DTO,不落表)。
+ *
+ * 可用额度口径与客户资金状态保持一致:
+ * 余额 + 当日有效的信用额度 + 当日有效的临时额度,< 0 即超额(欠费)。
+ *
+ * @author bms
+ * @date 2026-10-09
+ */
+@Data
+public class NotifyOverdueCustomer
+{
+ /** 客户ID */
+ private Long customerId;
+
+ /** 客户编号 */
+ private String customerCode;
+
+ /** 客户名称 */
+ private String customerName;
+
+ /** 停用标志(0正常 1停用) */
+ private String isEnable;
+
+ /** 账户余额 */
+ private BigDecimal balance;
+
+ /** 当日有效额度合计后可用额度(负=超额) */
+ private BigDecimal available;
+
+ /** 超额金额(= -available,正数) */
+ private BigDecimal overdueAmount;
+}
diff --git a/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyReceive.java b/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyReceive.java
new file mode 100644
index 00000000..f7a8c41a
--- /dev/null
+++ b/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyReceive.java
@@ -0,0 +1,64 @@
+package com.bms.notify.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+/**
+ * 通知消息接收人 sys_notify_receive
+ *
+ * @author bms
+ * @date 2026-10-09
+ */
+@Data
+public class NotifyReceive
+{
+ /** 接收ID */
+ private Long receiveId;
+
+ /** 消息ID */
+ private Long messageId;
+
+ /** 接收用户ID */
+ private Long userId;
+
+ /** 已读标志(0未读 1已读) */
+ private String readFlag;
+
+ /** 阅读时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date readTime;
+
+ /** 登录弹屏是否已展示(N否 Y是) */
+ private String popupFlag;
+
+ /** 创建时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date createTime;
+
+ /* ===== 我的消息列表展示字段(来自 sys_notify_message / sys_user) ===== */
+
+ /** 标题 */
+ private String title;
+
+ /** 内容 */
+ private String content;
+
+ /** 事件类型 */
+ private String eventType;
+
+ /** 客户名称 */
+ private String customerName;
+
+ /** 客户编号 */
+ private String customerCode;
+
+ /** 超额金额(CNY) */
+ private java.math.BigDecimal overdueAmount;
+
+ /** 接收人昵称 */
+ private String nickName;
+
+ /** 接收人账号 */
+ private String userName;
+}
diff --git a/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyRule.java b/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyRule.java
new file mode 100644
index 00000000..cd72498e
--- /dev/null
+++ b/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyRule.java
@@ -0,0 +1,48 @@
+package com.bms.notify.domain;
+
+import java.util.List;
+import com.bms.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 消息通知规则 sys_notify_rule
+ *
+ * 当前仅一种事件:{@link #EVENT_CUSTOMER_OVERDUE}(客户已超额/欠费)。
+ * 每条规则可配置多个通知对象(用户/角色),见 {@link NotifyRuleTarget}。
+ *
+ * @author bms
+ * @date 2026-10-09
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class NotifyRule extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 事件类型:客户已超额(欠费) */
+ public static final String EVENT_CUSTOMER_OVERDUE = "CUSTOMER_OVERDUE";
+
+ /** 对象类型:用户 */
+ public static final String TARGET_USER = "USER";
+ /** 对象类型:角色 */
+ public static final String TARGET_ROLE = "ROLE";
+
+ /** 规则ID */
+ private Long ruleId;
+
+ /** 规则名称 */
+ private String ruleName;
+
+ /** 事件类型 */
+ private String eventType;
+
+ /** 状态(0启用 1停用) */
+ private String status;
+
+ /** 删除标志(0存在 2删除) */
+ private String delFlag;
+
+ /** 通知对象列表(保存/回显用) */
+ private List targets;
+}
diff --git a/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyRuleTarget.java b/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyRuleTarget.java
new file mode 100644
index 00000000..c601474b
--- /dev/null
+++ b/bms-back/bms-system/src/main/java/com/bms/notify/domain/NotifyRuleTarget.java
@@ -0,0 +1,34 @@
+package com.bms.notify.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+/**
+ * 通知规则对象 sys_notify_rule_target(USER 指定用户 / ROLE 指定角色)
+ *
+ * @author bms
+ * @date 2026-10-09
+ */
+@Data
+public class NotifyRuleTarget
+{
+ /** 对象ID */
+ private Long targetId;
+
+ /** 规则ID */
+ private Long ruleId;
+
+ /** 对象类型:USER/ROLE */
+ private String targetType;
+
+ /** 用户ID或角色ID */
+ private Long refId;
+
+ /** 用户名/角色名快照 */
+ private String refName;
+
+ /** 创建时间 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private Date createTime;
+}
diff --git a/bms-back/bms-system/src/main/java/com/bms/notify/mapper/NotifyMapper.java b/bms-back/bms-system/src/main/java/com/bms/notify/mapper/NotifyMapper.java
new file mode 100644
index 00000000..934ad391
--- /dev/null
+++ b/bms-back/bms-system/src/main/java/com/bms/notify/mapper/NotifyMapper.java
@@ -0,0 +1,107 @@
+package com.bms.notify.mapper;
+
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import com.bms.notify.domain.NotifyMessage;
+import com.bms.notify.domain.NotifyOverdueCustomer;
+import com.bms.notify.domain.NotifyReceive;
+import com.bms.notify.domain.NotifyRule;
+import com.bms.notify.domain.NotifyRuleTarget;
+
+/**
+ * 消息通知 Mapper(规则/对象/消息/接收/扫描日志)
+ *
+ * @author bms
+ * @date 2026-10-09
+ */
+public interface NotifyMapper
+{
+ /* ================= 规则 ================= */
+
+ List selectRuleList(NotifyRule query);
+
+ NotifyRule selectRuleById(@Param("ruleId") Long ruleId);
+
+ int insertRule(NotifyRule rule);
+
+ int updateRule(NotifyRule rule);
+
+ /** 软删除规则 */
+ int deleteRuleByIds(@Param("ids") Long[] ids, @Param("updateBy") String updateBy);
+
+ int countEnabledRules(@Param("eventType") String eventType);
+
+ /* ================= 规则对象 ================= */
+
+ List selectTargetsByRuleId(@Param("ruleId") Long ruleId);
+
+ List selectTargetsByRuleIds(@Param("ruleIds") List ruleIds);
+
+ int insertTarget(NotifyRuleTarget target);
+
+ int deleteTargetsByRuleId(@Param("ruleId") Long ruleId);
+
+ /** 校验用户存在且为启用状态 */
+ int countEnabledUser(@Param("userId") Long userId);
+
+ /** 校验角色存在且为启用状态 */
+ int countEnabledRole(@Param("roleId") Long roleId);
+
+ /** 汇总所有启用规则的去重接收用户(直选用户 + 选中角色下用户) */
+ List selectRecipientUserIds(@Param("eventType") String eventType);
+
+ /* ================= 消息与接收 ================= */
+
+ List selectMessageList(NotifyMessage query);
+
+ NotifyMessage selectMessageById(@Param("messageId") Long messageId);
+
+ int insertMessage(NotifyMessage message);
+
+ int insertReceive(NotifyReceive receive);
+
+ List selectReceivesByMessageId(@Param("messageId") Long messageId);
+
+ /* ================= 扫描日志 ================= */
+
+ String selectScanLogStatus(@Param("eventType") String eventType,
+ @Param("customerId") Long customerId);
+
+ java.util.Date selectScanLogNotifyDate(@Param("eventType") String eventType,
+ @Param("customerId") Long customerId);
+
+ int insertScanLog(@Param("eventType") String eventType,
+ @Param("customerId") Long customerId,
+ @Param("status") String status,
+ @Param("notify") boolean notify);
+
+ int updateScanLog(@Param("eventType") String eventType,
+ @Param("customerId") Long customerId,
+ @Param("status") String status,
+ @Param("notify") boolean notify);
+
+ /** 当前不超额但日志仍为OVERDUE的客户 -> 置NORMAL */
+ int resetNormalForCustomers(@Param("eventType") String eventType,
+ @Param("customerIds") List customerIds);
+
+ /** 查询所有当前超额客户(CNY账户,有效额度口径) */
+ List selectOverdueCustomers();
+
+ /* ================= 我的消息 ================= */
+
+ /** 登录待弹屏消息(popup_flag=N) */
+ List selectPopupReceives(@Param("userId") Long userId);
+
+ /** 取弹屏列表后置 popup_flag=Y(服务器时间,一次登录只弹一次) */
+ int markPopupShown(@Param("userId") Long userId);
+
+ /** 我的消息列表(readFlag 为空=全部) */
+ List selectMineList(@Param("userId") Long userId,
+ @Param("readFlag") String readFlag);
+
+ int countUnread(@Param("userId") Long userId);
+
+ int markRead(@Param("receiveId") Long receiveId, @Param("userId") Long userId);
+
+ int markAllRead(@Param("userId") Long userId);
+}
diff --git a/bms-back/bms-system/src/main/java/com/bms/notify/service/INotifyService.java b/bms-back/bms-system/src/main/java/com/bms/notify/service/INotifyService.java
new file mode 100644
index 00000000..36440366
--- /dev/null
+++ b/bms-back/bms-system/src/main/java/com/bms/notify/service/INotifyService.java
@@ -0,0 +1,58 @@
+package com.bms.notify.service;
+
+import java.util.List;
+import com.bms.notify.domain.NotifyMessage;
+import com.bms.notify.domain.NotifyReceive;
+import com.bms.notify.domain.NotifyRule;
+
+/**
+ * 消息通知 Service
+ *
+ * @author bms
+ * @date 2026-10-09
+ */
+public interface INotifyService
+{
+ /** 规则分页列表 */
+ List selectRuleList(NotifyRule query);
+
+ /** 规则详情(含通知对象) */
+ NotifyRule selectRuleById(Long ruleId);
+
+ /** 新增规则(含通知对象) */
+ int insertRule(NotifyRule rule);
+
+ /** 修改规则(全量重建通知对象) */
+ int updateRule(NotifyRule rule);
+
+ /** 删除规则 */
+ int deleteRuleByIds(Long[] ruleIds);
+
+ /** 通知消息分页列表 */
+ List selectMessageList(NotifyMessage query);
+
+ /** 消息详情(含接收人已读情况) */
+ NotifyMessage selectMessageById(Long messageId);
+
+ /**
+ * 扫描超额客户并按启用规则投递消息(定时任务/手工触发)。
+ * 同一客户当天只通知一次;客户恢复正常后再次超额可立即再通知。
+ * @return 本次新生成的消息条数
+ */
+ int scanCustomerOverdue();
+
+ /** 登录后取待弹屏消息,并置 popup_flag=Y(一次登录只弹一次) */
+ List popupAndMarkShown(Long userId);
+
+ /** 我的消息列表(readFlag 为空=全部) */
+ List selectMineList(Long userId, String readFlag);
+
+ /** 我的未读数 */
+ int countUnread(Long userId);
+
+ /** 标记单条已读 */
+ int markRead(Long receiveId, Long userId);
+
+ /** 全部已读 */
+ int markAllRead(Long userId);
+}
diff --git a/bms-back/bms-system/src/main/java/com/bms/notify/service/impl/NotifyServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/notify/service/impl/NotifyServiceImpl.java
new file mode 100644
index 00000000..4faf2bd2
--- /dev/null
+++ b/bms-back/bms-system/src/main/java/com/bms/notify/service/impl/NotifyServiceImpl.java
@@ -0,0 +1,342 @@
+package com.bms.notify.service.impl;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import com.bms.common.exception.ServiceException;
+import com.bms.common.utils.SecurityUtils;
+import com.bms.notify.domain.NotifyMessage;
+import com.bms.notify.domain.NotifyOverdueCustomer;
+import com.bms.notify.domain.NotifyReceive;
+import com.bms.notify.domain.NotifyRule;
+import com.bms.notify.domain.NotifyRuleTarget;
+import com.bms.notify.mapper.NotifyMapper;
+import com.bms.notify.service.INotifyService;
+
+/**
+ * 消息通知 Service实现
+ *
+ * @author bms
+ * @date 2026-10-09
+ */
+@Service
+public class NotifyServiceImpl implements INotifyService
+{
+ private static final Logger log = LoggerFactory.getLogger(NotifyServiceImpl.class);
+
+ @Autowired
+ private NotifyMapper notifyMapper;
+
+ /* ================= 规则 ================= */
+
+ @Override
+ public List selectRuleList(NotifyRule query)
+ {
+ List rules = notifyMapper.selectRuleList(query);
+ if (rules.isEmpty())
+ {
+ return rules;
+ }
+ List ids = rules.stream().map(NotifyRule::getRuleId).toList();
+ Map> targetMap = loadTargets(ids);
+ rules.forEach(r -> r.setTargets(targetMap.getOrDefault(r.getRuleId(), new ArrayList<>())));
+ return rules;
+ }
+
+ @Override
+ public NotifyRule selectRuleById(Long ruleId)
+ {
+ NotifyRule rule = notifyMapper.selectRuleById(ruleId);
+ if (rule != null)
+ {
+ rule.setTargets(notifyMapper.selectTargetsByRuleId(ruleId));
+ }
+ return rule;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int insertRule(NotifyRule rule)
+ {
+ prepareRule(rule);
+ String username = SecurityUtils.getUsername();
+ rule.setCreateBy(username);
+ notifyMapper.insertRule(rule);
+ saveTargets(rule);
+ return 1;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int updateRule(NotifyRule rule)
+ {
+ if (rule.getRuleId() == null || notifyMapper.selectRuleById(rule.getRuleId()) == null)
+ {
+ throw new ServiceException("通知规则不存在");
+ }
+ prepareRule(rule);
+ rule.setUpdateBy(SecurityUtils.getUsername());
+ int rows = notifyMapper.updateRule(rule);
+ notifyMapper.deleteTargetsByRuleId(rule.getRuleId());
+ saveTargets(rule);
+ return rows;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int deleteRuleByIds(Long[] ruleIds)
+ {
+ if (ruleIds == null || ruleIds.length == 0)
+ {
+ return 0;
+ }
+ return notifyMapper.deleteRuleByIds(ruleIds, SecurityUtils.getUsername());
+ }
+
+ private void prepareRule(NotifyRule rule)
+ {
+ if (!StringUtils.hasText(rule.getRuleName()))
+ {
+ throw new ServiceException("规则名称不能为空");
+ }
+ if (!StringUtils.hasText(rule.getEventType()))
+ {
+ rule.setEventType(NotifyRule.EVENT_CUSTOMER_OVERDUE);
+ }
+ if (!NotifyRule.EVENT_CUSTOMER_OVERDUE.equals(rule.getEventType()))
+ {
+ throw new ServiceException("暂不支持的通知事件类型");
+ }
+ if (!StringUtils.hasText(rule.getStatus()))
+ {
+ rule.setStatus("0");
+ }
+ List raw = rule.getTargets();
+ if (raw == null || raw.isEmpty())
+ {
+ throw new ServiceException("请至少选择一个通知用户或角色");
+ }
+ // 去重 + 校验对象存在且启用
+ Map uniq = new LinkedHashMap<>();
+ for (NotifyRuleTarget t : raw)
+ {
+ if (t == null || t.getRefId() == null || !StringUtils.hasText(t.getTargetType()))
+ {
+ continue;
+ }
+ String type = t.getTargetType().trim().toUpperCase();
+ if (!NotifyRule.TARGET_USER.equals(type) && !NotifyRule.TARGET_ROLE.equals(type))
+ {
+ throw new ServiceException("通知对象类型只能是用户或角色");
+ }
+ String key = type + "#" + t.getRefId();
+ if (uniq.containsKey(key))
+ {
+ continue;
+ }
+ int exists = NotifyRule.TARGET_USER.equals(type)
+ ? notifyMapper.countEnabledUser(t.getRefId())
+ : notifyMapper.countEnabledRole(t.getRefId());
+ if (exists == 0)
+ {
+ throw new ServiceException((NotifyRule.TARGET_USER.equals(type) ? "用户" : "角色")
+ + "不存在或已停用:" + (StringUtils.hasText(t.getRefName()) ? t.getRefName() : t.getRefId()));
+ }
+ t.setTargetType(type);
+ uniq.put(key, t);
+ }
+ if (uniq.isEmpty())
+ {
+ throw new ServiceException("请至少选择一个有效的通知用户或角色");
+ }
+ rule.setTargets(new ArrayList<>(uniq.values()));
+ }
+
+ private void saveTargets(NotifyRule rule)
+ {
+ for (NotifyRuleTarget t : rule.getTargets())
+ {
+ t.setTargetId(null);
+ t.setRuleId(rule.getRuleId());
+ notifyMapper.insertTarget(t);
+ }
+ }
+
+ private Map> loadTargets(List ruleIds)
+ {
+ Map> map = new LinkedHashMap<>();
+ for (NotifyRuleTarget t : notifyMapper.selectTargetsByRuleIds(ruleIds))
+ {
+ map.computeIfAbsent(t.getRuleId(), k -> new ArrayList<>()).add(t);
+ }
+ return map;
+ }
+
+ /* ================= 消息 ================= */
+
+ @Override
+ public List selectMessageList(NotifyMessage query)
+ {
+ return notifyMapper.selectMessageList(query);
+ }
+
+ @Override
+ public NotifyMessage selectMessageById(Long messageId)
+ {
+ NotifyMessage message = notifyMapper.selectMessageById(messageId);
+ if (message != null)
+ {
+ message.setReceives(notifyMapper.selectReceivesByMessageId(messageId));
+ }
+ return message;
+ }
+
+ /* ================= 超额扫描 ================= */
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int scanCustomerOverdue()
+ {
+ String eventType = NotifyRule.EVENT_CUSTOMER_OVERDUE;
+ if (notifyMapper.countEnabledRules(eventType) == 0)
+ {
+ log.info("客户超额通知扫描:无启用规则,跳过");
+ return 0;
+ }
+ List recipients = notifyMapper.selectRecipientUserIds(eventType);
+ if (recipients.isEmpty())
+ {
+ log.info("客户超额通知扫描:启用规则未匹配到有效接收人,跳过");
+ return 0;
+ }
+
+ List overdueList = notifyMapper.selectOverdueCustomers();
+ List overdueIds = overdueList.stream().map(NotifyOverdueCustomer::getCustomerId).toList();
+ // 曾超额但当前已恢复的客户:状态置 NORMAL,下次再超额可立即通知
+ notifyMapper.resetNormalForCustomers(eventType, overdueIds);
+
+ LocalDate today = LocalDate.now();
+ int generated = 0;
+ for (NotifyOverdueCustomer c : overdueList)
+ {
+ String lastStatus = notifyMapper.selectScanLogStatus(eventType, c.getCustomerId());
+ // 当日去重:今天已通知过且仍超额,跳过
+ if ("OVERDUE".equals(lastStatus) && today.equals(lastNotifyDate(eventType, c.getCustomerId())))
+ {
+ continue;
+ }
+
+ NotifyMessage message = new NotifyMessage();
+ message.setEventType(eventType);
+ boolean disabled = "1".equals(c.getIsEnable());
+ message.setTitle((disabled ? "【停用客户已超额】" : "【客户已超额】") + c.getCustomerName());
+ message.setContent(buildContent(c, disabled));
+ message.setCustomerId(c.getCustomerId());
+ message.setCustomerCode(c.getCustomerCode());
+ message.setCustomerName(c.getCustomerName());
+ message.setOverdueAmount(c.getOverdueAmount());
+ message.setCreateBy("system");
+ notifyMapper.insertMessage(message);
+
+ for (Long userId : recipients)
+ {
+ NotifyReceive rv = new NotifyReceive();
+ rv.setMessageId(message.getMessageId());
+ rv.setUserId(userId);
+ notifyMapper.insertReceive(rv);
+ }
+
+ if (lastStatus == null)
+ {
+ notifyMapper.insertScanLog(eventType, c.getCustomerId(), "OVERDUE", true);
+ }
+ else
+ {
+ notifyMapper.updateScanLog(eventType, c.getCustomerId(), "OVERDUE", true);
+ }
+ generated++;
+ }
+ log.info("客户超额通知扫描完成:超额客户 {} 户,新生成通知 {} 条,接收 {} 人",
+ overdueList.size(), generated, recipients.size());
+ return generated;
+ }
+
+ private LocalDate lastNotifyDate(String eventType, Long customerId)
+ {
+ Date d = notifyMapper.selectScanLogNotifyDate(eventType, customerId);
+ if (d == null)
+ {
+ return null;
+ }
+ return d.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+ }
+
+ private String buildContent(NotifyOverdueCustomer c, boolean disabled)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("客户「").append(c.getCustomerName()).append("」(").append(c.getCustomerCode()).append(")");
+ if (disabled)
+ {
+ sb.append("已停用且");
+ }
+ sb.append("已超额(欠费)。");
+ sb.append("当前可用额度 ").append(format(c.getAvailable())).append(" CNY,");
+ sb.append("超额金额 ").append(format(c.getOverdueAmount())).append(" CNY。");
+ sb.append("请及时催收回款,可在【客户财务管理-收款管理】登记收款并审核。");
+ return sb.toString();
+ }
+
+ private String format(java.math.BigDecimal v)
+ {
+ return v == null ? "0.00" : v.stripTrailingZeros().toPlainString();
+ }
+
+ /* ================= 我的消息 ================= */
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public List popupAndMarkShown(Long userId)
+ {
+ List list = notifyMapper.selectPopupReceives(userId);
+ if (!list.isEmpty())
+ {
+ // 服务器端置位:一次登录只弹一次,刷新页面不再弹,未读消息可从“我的通知”再次查看
+ notifyMapper.markPopupShown(userId);
+ }
+ return list;
+ }
+
+ @Override
+ public List selectMineList(Long userId, String readFlag)
+ {
+ return notifyMapper.selectMineList(userId, readFlag);
+ }
+
+ @Override
+ public int countUnread(Long userId)
+ {
+ return notifyMapper.countUnread(userId);
+ }
+
+ @Override
+ public int markRead(Long receiveId, Long userId)
+ {
+ return notifyMapper.markRead(receiveId, userId);
+ }
+
+ @Override
+ public int markAllRead(Long userId)
+ {
+ return notifyMapper.markAllRead(userId);
+ }
+}
diff --git a/bms-back/bms-system/src/main/java/com/bms/notify/task/NotifyCustomerTask.java b/bms-back/bms-system/src/main/java/com/bms/notify/task/NotifyCustomerTask.java
new file mode 100644
index 00000000..79039d98
--- /dev/null
+++ b/bms-back/bms-system/src/main/java/com/bms/notify/task/NotifyCustomerTask.java
@@ -0,0 +1,36 @@
+package com.bms.notify.task;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import com.bms.notify.service.INotifyService;
+
+/**
+ * 客户超额通知定时任务。
+ *
+ * 纳入 Quartz(sys_job)统一管理,调用目标:
+ * {@code notifyCustomerTask.scanOverdue()},cron/启停在「系统监控-定时任务」配置。
+ *
+ * 本任务只做消息投递,不改动任何客户余额/额度数据。
+ *
+ * @author bms
+ * @date 2026-10-09
+ */
+@Component("notifyCustomerTask")
+public class NotifyCustomerTask
+{
+ private static final Logger log = LoggerFactory.getLogger(NotifyCustomerTask.class);
+
+ @Autowired
+ private INotifyService notifyService;
+
+ /**
+ * 扫描已超额(欠费)客户并按通知规则投递消息(同一客户当天最多一条)。
+ */
+ public void scanOverdue()
+ {
+ int count = notifyService.scanCustomerOverdue();
+ log.info("客户超额通知任务执行完成,新生成通知 {} 条", count);
+ }
+}
diff --git a/bms-back/bms-system/src/main/resources/mapper/notify/NotifyMapper.xml b/bms-back/bms-system/src/main/resources/mapper/notify/NotifyMapper.xml
new file mode 100644
index 00000000..6e325beb
--- /dev/null
+++ b/bms-back/bms-system/src/main/resources/mapper/notify/NotifyMapper.xml
@@ -0,0 +1,323 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ insert into sys_notify_rule (rule_name, event_type, status, remark, create_by, create_time)
+ values (#{ruleName}, #{eventType}, #{status}, #{remark}, #{createBy}, sysdate())
+
+
+
+ update sys_notify_rule
+
+ rule_name = #{ruleName},
+ event_type = #{eventType},
+ status = #{status},
+ remark = #{remark},
+ update_by = #{updateBy},
+ update_time = sysdate()
+
+ where rule_id = #{ruleId} and del_flag = '0'
+
+
+
+ update sys_notify_rule
+ set del_flag = '2', update_by = #{updateBy}, update_time = sysdate()
+ where rule_id in
+ #{id}
+
+
+
+
+
+
+
+
+
+
+ insert into sys_notify_rule_target (rule_id, target_type, ref_id, ref_name, create_time)
+ values (#{ruleId}, #{targetType}, #{refId}, #{refName}, sysdate())
+
+
+
+ delete from sys_notify_rule_target where rule_id = #{ruleId}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ insert into sys_notify_message (event_type, title, content, customer_id, customer_code,
+ customer_name, overdue_amount, create_by, create_time)
+ values (#{eventType}, #{title}, #{content}, #{customerId}, #{customerCode},
+ #{customerName}, #{overdueAmount}, #{createBy}, sysdate())
+
+
+
+ insert ignore into sys_notify_receive (message_id, user_id, read_flag, popup_flag, create_time)
+ values (#{messageId}, #{userId}, '0', 'N', sysdate())
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ insert into sys_notify_scan_log (event_type, customer_id, last_status, last_notify_date, update_time)
+ values (#{eventType}, #{customerId}, #{status},
+ curdate()null,
+ sysdate())
+
+
+
+ update sys_notify_scan_log
+ set last_status = #{status},
+ last_notify_date = curdate()last_notify_date,
+ update_time = sysdate()
+ where event_type = #{eventType} and customer_id = #{customerId}
+
+
+
+ update sys_notify_scan_log
+ set last_status = 'NORMAL', update_time = sysdate()
+ where event_type = #{eventType} and last_status = 'OVERDUE'
+
+ and customer_id not in
+ #{id}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ update sys_notify_receive
+ set read_flag = '1', read_time = sysdate()
+ where receive_id = #{receiveId} and user_id = #{userId} and read_flag = '0'
+
+
+
+ update sys_notify_receive
+ set read_flag = '1', read_time = sysdate()
+ where user_id = #{userId} and read_flag = '0'
+
+
diff --git a/bms-back/sql/20261009_add_sys_notify.sql b/bms-back/sql/20261009_add_sys_notify.sql
new file mode 100644
index 00000000..e57acd99
--- /dev/null
+++ b/bms-back/sql/20261009_add_sys_notify.sql
@@ -0,0 +1,98 @@
+-- ============================================================
+-- 功能:系统管理 - 消息通知管理(客户超额通知)
+-- 日期:2026-10-09
+-- 说明:
+-- 1) sys_notify_rule 通知规则(当前事件类型:CUSTOMER_OVERDUE 客户已超额)
+-- 2) sys_notify_rule_target 规则通知对象(USER 指定用户 / ROLE 指定角色,可多个)
+-- 3) sys_notify_message 通知消息(一次超额扫描/客户/天最多一条,防重复打扰)
+-- 4) sys_notify_receive 接收人与已读/弹屏记录
+-- 5) sys_notify_scan_log 扫描状态日志(用于超额->恢复状态机与当日去重)
+-- 6) 菜单:系统管理 -> 消息通知管理(system/notify/index)
+-- 7) 定时任务:notifyCustomerTask.scanOverdue() 每日09:00
+-- 口径:通知与核销无关,不涉及任何余额/额度变动。
+-- ============================================================
+
+-- 1. 通知规则
+CREATE TABLE IF NOT EXISTS `sys_notify_rule` (
+ `rule_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '规则ID',
+ `rule_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '规则名称',
+ `event_type` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'CUSTOMER_OVERDUE' COMMENT '事件类型:CUSTOMER_OVERDUE客户已超额',
+ `status` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '状态(0启用 1停用)',
+ `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
+ `create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建者',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新者',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ `del_flag` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
+ PRIMARY KEY (`rule_id`),
+ KEY `idx_snr_event_status` (`event_type`,`status`,`del_flag`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息通知规则';
+
+-- 2. 规则通知对象
+CREATE TABLE IF NOT EXISTS `sys_notify_rule_target` (
+ `target_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '对象ID',
+ `rule_id` bigint(20) NOT NULL COMMENT '规则ID',
+ `target_type` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '对象类型:USER用户/ROLE角色',
+ `ref_id` bigint(20) NOT NULL COMMENT '用户ID或角色ID',
+ `ref_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户名/角色名快照',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ PRIMARY KEY (`target_id`),
+ UNIQUE KEY `uk_snrt_rule_type_ref` (`rule_id`,`target_type`,`ref_id`),
+ KEY `idx_snrt_type_ref` (`target_type`,`ref_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知规则对象(用户/角色)';
+
+-- 3. 通知消息
+CREATE TABLE IF NOT EXISTS `sys_notify_message` (
+ `message_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '消息ID',
+ `event_type` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'CUSTOMER_OVERDUE' COMMENT '事件类型',
+ `title` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标题',
+ `content` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '内容',
+ `customer_id` bigint(20) DEFAULT NULL COMMENT '关联客户ID',
+ `customer_code` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '客户编号快照',
+ `customer_name` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '客户名称快照',
+ `overdue_amount` decimal(18,2) DEFAULT NULL COMMENT '超额金额(CNY,正数)',
+ `create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建者(系统任务记为system)',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ PRIMARY KEY (`message_id`),
+ KEY `idx_snm_event_customer` (`event_type`,`customer_id`),
+ KEY `idx_snm_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知消息';
+
+-- 4. 接收人
+CREATE TABLE IF NOT EXISTS `sys_notify_receive` (
+ `receive_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '接收ID',
+ `message_id` bigint(20) NOT NULL COMMENT '消息ID',
+ `user_id` bigint(20) NOT NULL COMMENT '接收用户ID',
+ `read_flag` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '已读标志(0未读 1已读)',
+ `read_time` datetime DEFAULT NULL COMMENT '阅读时间',
+ `popup_flag` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'N' COMMENT '登录弹屏是否已展示(N否 Y是)',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ PRIMARY KEY (`receive_id`),
+ UNIQUE KEY `uk_snr_message_user` (`message_id`,`user_id`),
+ KEY `idx_snr_user_popup` (`user_id`,`popup_flag`,`read_flag`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知消息接收人';
+
+-- 5. 扫描状态日志(每客户每事件一行)
+CREATE TABLE IF NOT EXISTS `sys_notify_scan_log` (
+ `log_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID',
+ `event_type` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '事件类型',
+ `customer_id` bigint(20) NOT NULL COMMENT '客户ID',
+ `last_status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '最近状态:OVERDUE超额/NORMAL正常',
+ `last_notify_date` date DEFAULT NULL COMMENT '最近通知日期(当日去重)',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ PRIMARY KEY (`log_id`),
+ UNIQUE KEY `uk_snsl_event_customer` (`event_type`,`customer_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知扫描状态日志';
+
+-- 6. 菜单:系统管理 -> 消息通知管理
+INSERT INTO `sys_menu`(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
+VALUES (2450, '消息通知管理', 1, 9, 'notify', 'system/notify/index', NULL, 1, 0, 'C', '0', '0', 'system:notify:list', 'message', 'admin', sysdate(), '消息通知配置与记录');
+INSERT INTO `sys_menu`(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`) VALUES
+(2451, '通知规则查询', 2450, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:notify:query', '#', 'admin', sysdate(), ''),
+(2452, '通知规则新增', 2450, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:notify:add', '#', 'admin', sysdate(), ''),
+(2453, '通知规则修改', 2450, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:notify:edit', '#', 'admin', sysdate(), ''),
+(2454, '通知规则删除', 2450, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:notify:remove', '#', 'admin', sysdate(), '');
+
+-- 7. 定时任务:客户超额通知扫描(每日09:00,misfire不补偿、禁止并发)
+INSERT INTO `sys_job`(`job_id`, `job_name`, `job_group`, `invoke_target`, `cron_expression`, `misfire_policy`, `concurrent`, `status`, `create_by`, `create_time`, `remark`)
+VALUES (8, '客户超额通知扫描', 'DEFAULT', 'notifyCustomerTask.scanOverdue()', '0 0 9 * * ?', '3', '1', '0', 'admin', sysdate(), '扫描已超额(欠费)客户并按通知规则投递消息');
diff --git a/bms-front/src/api/system/notify.js b/bms-front/src/api/system/notify.js
new file mode 100644
index 00000000..2b403b33
--- /dev/null
+++ b/bms-front/src/api/system/notify.js
@@ -0,0 +1,110 @@
+import request from '@/utils/request'
+
+// 查询通知规则列表
+export function listNotifyRule(query) {
+ return request({
+ url: '/system/notify/rule/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询通知规则详情
+export function getNotifyRule(ruleId) {
+ return request({
+ url: '/system/notify/rule/' + ruleId,
+ method: 'get'
+ })
+}
+
+// 新增通知规则
+export function addNotifyRule(data) {
+ return request({
+ url: '/system/notify/rule',
+ method: 'post',
+ data
+ })
+}
+
+// 修改通知规则
+export function updateNotifyRule(data) {
+ return request({
+ url: '/system/notify/rule',
+ method: 'put',
+ data
+ })
+}
+
+// 删除通知规则
+export function delNotifyRule(ruleIds) {
+ return request({
+ url: '/system/notify/rule/' + ruleIds,
+ method: 'delete'
+ })
+}
+
+// 立即执行一次客户超额扫描
+export function scanOverdueNow() {
+ return request({
+ url: '/system/notify/scan',
+ method: 'post'
+ })
+}
+
+// 查询通知消息列表
+export function listNotifyMessage(query) {
+ return request({
+ url: '/system/notify/message/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询通知消息详情(含接收人已读情况)
+export function getNotifyMessage(messageId) {
+ return request({
+ url: '/system/notify/message/' + messageId,
+ method: 'get'
+ })
+}
+
+// 登录后待弹屏消息(拉取即置弹屏已展示)
+export function popupNotify() {
+ return request({
+ url: '/system/notify/mine/popup',
+ method: 'get'
+ })
+}
+
+// 我的通知列表
+export function listMyNotify(readFlag) {
+ return request({
+ url: '/system/notify/mine/list',
+ method: 'get',
+ params: { readFlag }
+ })
+}
+
+// 我的未读数
+export function countMyNotify() {
+ return request({
+ url: '/system/notify/mine/count',
+ method: 'get'
+ })
+}
+
+// 标记单条已读
+export function readMyNotify(receiveId) {
+ return request({
+ url: '/system/notify/mine/read/' + receiveId,
+ method: 'put'
+ })
+}
+
+// 全部已读
+export function readAllMyNotify() {
+ return request({
+ url: '/system/notify/mine/readAll',
+ method: 'put'
+ })
+}
diff --git a/bms-front/src/components/NotifyPopup/index.vue b/bms-front/src/components/NotifyPopup/index.vue
new file mode 100644
index 00000000..21b82ca2
--- /dev/null
+++ b/bms-front/src/components/NotifyPopup/index.vue
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
diff --git a/bms-front/src/components/NotifyPopup/notifyState.js b/bms-front/src/components/NotifyPopup/notifyState.js
new file mode 100644
index 00000000..6573decb
--- /dev/null
+++ b/bms-front/src/components/NotifyPopup/notifyState.js
@@ -0,0 +1,11 @@
+import { reactive } from 'vue'
+
+/**
+ * 通知弹屏共享状态(单例):
+ * - 组件挂载于全局 Layout,负责登录后自动拉取待弹屏消息
+ * - Navbar “我的通知”入口通过 open() 打开同一弹窗
+ */
+export const notifyState = reactive({
+ visible: false,
+ unreadCount: 0
+})
diff --git a/bms-front/src/layout/components/Navbar.vue b/bms-front/src/layout/components/Navbar.vue
index 60bea0fc..c0e5eae4 100644
--- a/bms-front/src/layout/components/Navbar.vue
+++ b/bms-front/src/layout/components/Navbar.vue
@@ -42,6 +42,11 @@
个人中心
+
+
+ 我的通知
+
+
布局设置
@@ -72,6 +77,7 @@ import useAppStore from '@/store/modules/app'
import useUserStore from '@/store/modules/user'
import useLockStore from '@/store/modules/lock'
import useSettingsStore from '@/store/modules/settings'
+import { notifyState } from '@/components/NotifyPopup/notifyState'
// import HeaderNotice from './HeaderNotice' // 消息通知铃铛已隐藏
const route = useRoute()
@@ -93,6 +99,9 @@ function handleCommand(command) {
case "lockScreen":
lockScreen()
break
+ case "myNotify":
+ notifyState.open && notifyState.open()
+ break
case "logout":
logout()
break
diff --git a/bms-front/src/layout/index.vue b/bms-front/src/layout/index.vue
index b7328edc..ca6e54f8 100644
--- a/bms-front/src/layout/index.vue
+++ b/bms-front/src/layout/index.vue
@@ -10,6 +10,8 @@
+
+
@@ -17,6 +19,7 @@
import { useWindowSize } from '@vueuse/core'
import Sidebar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components'
+import NotifyPopup from '@/components/NotifyPopup/index.vue'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
diff --git a/bms-front/src/store/modules/user.js b/bms-front/src/store/modules/user.js
index 89c6de20..3717e433 100644
--- a/bms-front/src/store/modules/user.js
+++ b/bms-front/src/store/modules/user.js
@@ -58,17 +58,22 @@ const useUserStore = defineStore(
this.nickName = user.nickName
this.avatar = avatar
cache.session.set('pwrChrtype', res.pwdChrtype)
+ /* 改密提示展示标记:通知弹屏组件读取该标记,等改密框关闭后再弹通知,避免双框冲突 */
+ const showPwdPrompt = (msg) => {
+ sessionStorage.setItem('bmsPwdPrompt', '1')
+ ElMessageBox.confirm(msg, '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
+ router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
+ }).catch(() => {}).finally(() => {
+ sessionStorage.removeItem('bmsPwdPrompt')
+ })
+ }
/* 初始密码提示 */
if(res.isDefaultModifyPwd) {
- ElMessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
- router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
- }).catch(() => {})
+ showPwdPrompt('您的密码还是初始密码,请修改密码!')
}
/* 过期密码提示 */
if(!res.isDefaultModifyPwd && res.isPasswordExpired) {
- ElMessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
- router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
- }).catch(() => {})
+ showPwdPrompt('您的密码已过期,请尽快修改密码!')
}
resolve(res)
}).catch(error => {
diff --git a/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue b/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue
index 77ee4eb2..15c9151d 100644
--- a/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue
+++ b/bms-front/src/views/customersuppliermanagement/customerinfo/index.vue
@@ -180,7 +180,7 @@
修改
- 删除
+
客户余额
日志
diff --git a/bms-front/src/views/system/notify/index.vue b/bms-front/src/views/system/notify/index.vue
new file mode 100644
index 00000000..8caf7ae6
--- /dev/null
+++ b/bms-front/src/views/system/notify/index.vue
@@ -0,0 +1,399 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增配置
+
+
+ 立即扫描超额客户
+
+
+ 刷新
+
+
+
+
+
+
+
+
+ 客户已超额(欠费)
+
+
+
+
+
+ {{ t.targetType === 'USER' ? '用户' : '角色' }}:{{ t.refName }}
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+ {{ formatMoney(scope.row.overdueAmount) }}
+
+
+
+
+ {{ scope.row.readCount }}/{{ scope.row.receiveCount }}
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+ 详情
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 客户已超额(欠费)
+ 每日 09:00 自动扫描,客户恢复正常后再次超额会再次通知;同一客户每天最多通知一次。
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ d.label }}
+
+
+
+
+
+
+ 用户与角色合并去重通知;被停用的用户/角色不能选择。通知本身不涉及任何余额、额度变动。
+
+
+
+ 确 定
+ 取 消
+
+
+
+
+
+
+ {{ messageDialog.data.title }}
+ {{ messageDialog.data.customerName }}({{ messageDialog.data.customerCode }})
+ {{ formatMoney(messageDialog.data.overdueAmount) }}
+ {{ parseTime(messageDialog.data.createTime) }}
+ {{ messageDialog.data.content }}
+
+ 接收情况
+
+
+ {{ scope.row.nickName || scope.row.userName }}
+
+
+
+
+
+ {{ scope.row.readFlag === '1' ? '已读' : '未读' }}
+
+
+
+
+ {{ scope.row.readTime ? parseTime(scope.row.readTime) : '-' }}
+
+
+
+ 关 闭
+
+
+
+
+
+
+
+