客户超额消息通知

This commit is contained in:
wuweihua
2026-10-09 17:06:40 +08:00
parent 94e7a18e0b
commit 037d4b7cdf
20 changed files with 2065 additions and 7 deletions
@@ -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;
/**
* 消息通知管理(系统管理)
*
* <p>规则/消息管理需 system:notify:* 权限;“我的消息”系列接口所有登录用户可用。</p>
*
* @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()));
}
}
@@ -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<NotifyReceive> receives;
}
@@ -0,0 +1,38 @@
package com.bms.notify.domain;
import java.math.BigDecimal;
import lombok.Data;
/**
* 超额客户扫描结果(只读DTO,不落表)。
*
* <p>可用额度口径与客户资金状态保持一致:
* 余额 + 当日有效的信用额度 + 当日有效的临时额度,&lt; 0 即超额(欠费)。</p>
*
* @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;
}
@@ -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;
}
@@ -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
*
* <p>当前仅一种事件:{@link #EVENT_CUSTOMER_OVERDUE}(客户已超额/欠费)。
* 每条规则可配置多个通知对象(用户/角色),见 {@link NotifyRuleTarget}。</p>
*
* @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<NotifyRuleTarget> targets;
}
@@ -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;
}
@@ -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<NotifyRule> 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<NotifyRuleTarget> selectTargetsByRuleId(@Param("ruleId") Long ruleId);
List<NotifyRuleTarget> selectTargetsByRuleIds(@Param("ruleIds") List<Long> ruleIds);
int insertTarget(NotifyRuleTarget target);
int deleteTargetsByRuleId(@Param("ruleId") Long ruleId);
/** 校验用户存在且为启用状态 */
int countEnabledUser(@Param("userId") Long userId);
/** 校验角色存在且为启用状态 */
int countEnabledRole(@Param("roleId") Long roleId);
/** 汇总所有启用规则的去重接收用户(直选用户 + 选中角色下用户) */
List<Long> selectRecipientUserIds(@Param("eventType") String eventType);
/* ================= 消息与接收 ================= */
List<NotifyMessage> selectMessageList(NotifyMessage query);
NotifyMessage selectMessageById(@Param("messageId") Long messageId);
int insertMessage(NotifyMessage message);
int insertReceive(NotifyReceive receive);
List<NotifyReceive> 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<Long> customerIds);
/** 查询所有当前超额客户(CNY账户,有效额度口径) */
List<NotifyOverdueCustomer> selectOverdueCustomers();
/* ================= 我的消息 ================= */
/** 登录待弹屏消息(popup_flag=N) */
List<NotifyReceive> selectPopupReceives(@Param("userId") Long userId);
/** 取弹屏列表后置 popup_flag=Y(服务器时间,一次登录只弹一次) */
int markPopupShown(@Param("userId") Long userId);
/** 我的消息列表(readFlag 为空=全部) */
List<NotifyReceive> 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);
}
@@ -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<NotifyRule> selectRuleList(NotifyRule query);
/** 规则详情(含通知对象) */
NotifyRule selectRuleById(Long ruleId);
/** 新增规则(含通知对象) */
int insertRule(NotifyRule rule);
/** 修改规则(全量重建通知对象) */
int updateRule(NotifyRule rule);
/** 删除规则 */
int deleteRuleByIds(Long[] ruleIds);
/** 通知消息分页列表 */
List<NotifyMessage> selectMessageList(NotifyMessage query);
/** 消息详情(含接收人已读情况) */
NotifyMessage selectMessageById(Long messageId);
/**
* 扫描超额客户并按启用规则投递消息(定时任务/手工触发)。
* 同一客户当天只通知一次;客户恢复正常后再次超额可立即再通知。
* @return 本次新生成的消息条数
*/
int scanCustomerOverdue();
/** 登录后取待弹屏消息,并置 popup_flag=Y(一次登录只弹一次) */
List<NotifyReceive> popupAndMarkShown(Long userId);
/** 我的消息列表(readFlag 为空=全部) */
List<NotifyReceive> selectMineList(Long userId, String readFlag);
/** 我的未读数 */
int countUnread(Long userId);
/** 标记单条已读 */
int markRead(Long receiveId, Long userId);
/** 全部已读 */
int markAllRead(Long userId);
}
@@ -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<NotifyRule> selectRuleList(NotifyRule query)
{
List<NotifyRule> rules = notifyMapper.selectRuleList(query);
if (rules.isEmpty())
{
return rules;
}
List<Long> ids = rules.stream().map(NotifyRule::getRuleId).toList();
Map<Long, List<NotifyRuleTarget>> 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<NotifyRuleTarget> raw = rule.getTargets();
if (raw == null || raw.isEmpty())
{
throw new ServiceException("请至少选择一个通知用户或角色");
}
// 去重 + 校验对象存在且启用
Map<String, NotifyRuleTarget> 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<Long, List<NotifyRuleTarget>> loadTargets(List<Long> ruleIds)
{
Map<Long, List<NotifyRuleTarget>> map = new LinkedHashMap<>();
for (NotifyRuleTarget t : notifyMapper.selectTargetsByRuleIds(ruleIds))
{
map.computeIfAbsent(t.getRuleId(), k -> new ArrayList<>()).add(t);
}
return map;
}
/* ================= 消息 ================= */
@Override
public List<NotifyMessage> 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<Long> recipients = notifyMapper.selectRecipientUserIds(eventType);
if (recipients.isEmpty())
{
log.info("客户超额通知扫描:启用规则未匹配到有效接收人,跳过");
return 0;
}
List<NotifyOverdueCustomer> overdueList = notifyMapper.selectOverdueCustomers();
List<Long> 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<NotifyReceive> popupAndMarkShown(Long userId)
{
List<NotifyReceive> list = notifyMapper.selectPopupReceives(userId);
if (!list.isEmpty())
{
// 服务器端置位:一次登录只弹一次,刷新页面不再弹,未读消息可从“我的通知”再次查看
notifyMapper.markPopupShown(userId);
}
return list;
}
@Override
public List<NotifyReceive> 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);
}
}
@@ -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;
/**
* 客户超额通知定时任务。
*
* <p>纳入 Quartz(sys_job)统一管理,调用目标:
* {@code notifyCustomerTask.scanOverdue()},cron/启停在「系统监控-定时任务」配置。</p>
*
* <p>本任务只做消息投递,不改动任何客户余额/额度数据。</p>
*
* @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);
}
}
@@ -0,0 +1,323 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bms.notify.mapper.NotifyMapper">
<!-- ================= 规则 ================= -->
<resultMap id="NotifyRuleResult" type="com.bms.notify.domain.NotifyRule">
<id property="ruleId" column="rule_id" />
<result property="ruleName" column="rule_name" />
<result property="eventType" column="event_type" />
<result property="status" column="status" />
<result property="remark" column="remark" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="delFlag" column="del_flag" />
</resultMap>
<resultMap id="NotifyRuleTargetResult" type="com.bms.notify.domain.NotifyRuleTarget">
<id property="targetId" column="target_id" />
<result property="ruleId" column="rule_id" />
<result property="targetType" column="target_type" />
<result property="refId" column="ref_id" />
<result property="refName" column="ref_name" />
<result property="createTime" column="create_time" />
</resultMap>
<select id="selectRuleList" parameterType="com.bms.notify.domain.NotifyRule" resultMap="NotifyRuleResult">
select rule_id, rule_name, event_type, status, remark, create_by, create_time, update_by, update_time
from sys_notify_rule
where del_flag = '0'
<if test="ruleName != null and ruleName != ''"> and rule_name like concat('%', #{ruleName}, '%')</if>
<if test="eventType != null and eventType != ''"> and event_type = #{eventType}</if>
<if test="status != null and status != ''"> and status = #{status}</if>
order by rule_id desc
</select>
<select id="selectRuleById" resultMap="NotifyRuleResult">
select rule_id, rule_name, event_type, status, remark, create_by, create_time, update_by, update_time, del_flag
from sys_notify_rule
where rule_id = #{ruleId} and del_flag = '0'
</select>
<insert id="insertRule" parameterType="com.bms.notify.domain.NotifyRule" useGeneratedKeys="true" keyProperty="ruleId">
insert into sys_notify_rule (rule_name, event_type, status, remark, create_by, create_time)
values (#{ruleName}, #{eventType}, #{status}, #{remark}, #{createBy}, sysdate())
</insert>
<update id="updateRule" parameterType="com.bms.notify.domain.NotifyRule">
update sys_notify_rule
<set>
<if test="ruleName != null">rule_name = #{ruleName},</if>
<if test="eventType != null">event_type = #{eventType},</if>
<if test="status != null">status = #{status},</if>
<if test="remark != null">remark = #{remark},</if>
update_by = #{updateBy},
update_time = sysdate()
</set>
where rule_id = #{ruleId} and del_flag = '0'
</update>
<update id="deleteRuleByIds">
update sys_notify_rule
set del_flag = '2', update_by = #{updateBy}, update_time = sysdate()
where rule_id in
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
</update>
<select id="countEnabledRules" resultType="int">
select count(1) from sys_notify_rule
where del_flag = '0' and status = '0'
<if test="eventType != null and eventType != ''"> and event_type = #{eventType}</if>
</select>
<!-- ================= 规则对象 ================= -->
<select id="selectTargetsByRuleId" resultMap="NotifyRuleTargetResult">
select target_id, rule_id, target_type, ref_id, ref_name, create_time
from sys_notify_rule_target
where rule_id = #{ruleId}
order by target_type, ref_id
</select>
<select id="selectTargetsByRuleIds" resultMap="NotifyRuleTargetResult">
select target_id, rule_id, target_type, ref_id, ref_name, create_time
from sys_notify_rule_target
where rule_id in
<foreach collection="ruleIds" item="id" open="(" separator="," close=")">#{id}</foreach>
order by rule_id, target_type, ref_id
</select>
<insert id="insertTarget" parameterType="com.bms.notify.domain.NotifyRuleTarget" useGeneratedKeys="true" keyProperty="targetId">
insert into sys_notify_rule_target (rule_id, target_type, ref_id, ref_name, create_time)
values (#{ruleId}, #{targetType}, #{refId}, #{refName}, sysdate())
</insert>
<delete id="deleteTargetsByRuleId">
delete from sys_notify_rule_target where rule_id = #{ruleId}
</delete>
<select id="countEnabledUser" resultType="int">
select count(1) from sys_user
where user_id = #{userId} and status = '0' and del_flag = '0'
</select>
<select id="countEnabledRole" resultType="int">
select count(1) from sys_role
where role_id = #{roleId} and status = '0' and del_flag = '0'
</select>
<select id="selectRecipientUserIds" resultType="long">
select distinct u.user_id
from sys_user u
where u.status = '0' and u.del_flag = '0'
and (
u.user_id in (
select t.ref_id from sys_notify_rule_target t
join sys_notify_rule r on r.rule_id = t.rule_id
where t.target_type = 'USER' and r.del_flag = '0' and r.status = '0'
<if test="eventType != null and eventType != ''"> and r.event_type = #{eventType}</if>
)
or u.user_id in (
select ur.user_id from sys_user_role ur
where ur.role_id in (
select t.ref_id from sys_notify_rule_target t
join sys_notify_rule r on r.rule_id = t.rule_id
where t.target_type = 'ROLE' and r.del_flag = '0' and r.status = '0'
<if test="eventType != null and eventType != ''"> and r.event_type = #{eventType}</if>
)
)
)
</select>
<!-- ================= 消息与接收 ================= -->
<resultMap id="NotifyMessageResult" type="com.bms.notify.domain.NotifyMessage">
<id property="messageId" column="message_id" />
<result property="eventType" column="event_type" />
<result property="title" column="title" />
<result property="content" column="content" />
<result property="customerId" column="customer_id" />
<result property="customerCode" column="customer_code" />
<result property="customerName" column="customer_name" />
<result property="overdueAmount" column="overdue_amount" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="receiveCount" column="receive_count" />
<result property="readCount" column="read_count" />
</resultMap>
<select id="selectMessageList" parameterType="com.bms.notify.domain.NotifyMessage" resultMap="NotifyMessageResult">
select m.message_id, m.event_type, m.title, m.content, m.customer_id, m.customer_code,
m.customer_name, m.overdue_amount, m.create_by, m.create_time,
(select count(1) from sys_notify_receive rv where rv.message_id = m.message_id) as receive_count,
(select count(1) from sys_notify_receive rv where rv.message_id = m.message_id and rv.read_flag = '1') as read_count
from sys_notify_message m
<where>
<if test="eventType != null and eventType != ''"> and m.event_type = #{eventType}</if>
<if test="customerName != null and customerName != ''"> and m.customer_name like concat('%', #{customerName}, '%')</if>
<if test="title != null and title != ''"> and m.title like concat('%', #{title}, '%')</if>
</where>
order by m.message_id desc
</select>
<select id="selectMessageById" resultMap="NotifyMessageResult">
select m.message_id, m.event_type, m.title, m.content, m.customer_id, m.customer_code,
m.customer_name, m.overdue_amount, m.create_by, m.create_time,
(select count(1) from sys_notify_receive rv where rv.message_id = m.message_id) as receive_count,
(select count(1) from sys_notify_receive rv where rv.message_id = m.message_id and rv.read_flag = '1') as read_count
from sys_notify_message m
where m.message_id = #{messageId}
</select>
<insert id="insertMessage" parameterType="com.bms.notify.domain.NotifyMessage" useGeneratedKeys="true" keyProperty="messageId">
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>
<insert id="insertReceive" parameterType="com.bms.notify.domain.NotifyReceive">
insert ignore into sys_notify_receive (message_id, user_id, read_flag, popup_flag, create_time)
values (#{messageId}, #{userId}, '0', 'N', sysdate())
</insert>
<resultMap id="NotifyReceiveResult" type="com.bms.notify.domain.NotifyReceive">
<id property="receiveId" column="receive_id" />
<result property="messageId" column="message_id" />
<result property="userId" column="user_id" />
<result property="readFlag" column="read_flag" />
<result property="readTime" column="read_time" />
<result property="popupFlag" column="popup_flag" />
<result property="createTime" column="create_time" />
<result property="title" column="title" />
<result property="content" column="content" />
<result property="eventType" column="event_type" />
<result property="customerName" column="customer_name" />
<result property="customerCode" column="customer_code" />
<result property="overdueAmount" column="overdue_amount" />
<result property="nickName" column="nick_name" />
<result property="userName" column="user_name" />
</resultMap>
<select id="selectReceivesByMessageId" resultMap="NotifyReceiveResult">
select rv.receive_id, rv.message_id, rv.user_id, rv.read_flag, rv.read_time, rv.popup_flag, rv.create_time,
u.nick_name, u.user_name
from sys_notify_receive rv
left join sys_user u on u.user_id = rv.user_id
where rv.message_id = #{messageId}
order by rv.read_flag asc, rv.receive_id asc
</select>
<!-- ================= 扫描日志 ================= -->
<select id="selectScanLogStatus" resultType="string">
select last_status from sys_notify_scan_log
where event_type = #{eventType} and customer_id = #{customerId}
</select>
<select id="selectScanLogNotifyDate" resultType="java.util.Date">
select last_notify_date from sys_notify_scan_log
where event_type = #{eventType} and customer_id = #{customerId}
</select>
<insert id="insertScanLog">
insert into sys_notify_scan_log (event_type, customer_id, last_status, last_notify_date, update_time)
values (#{eventType}, #{customerId}, #{status},
<choose><when test="notify">curdate()</when><otherwise>null</otherwise></choose>,
sysdate())
</insert>
<update id="updateScanLog">
update sys_notify_scan_log
set last_status = #{status},
last_notify_date = <choose><when test="notify">curdate()</when><otherwise>last_notify_date</otherwise></choose>,
update_time = sysdate()
where event_type = #{eventType} and customer_id = #{customerId}
</update>
<update id="resetNormalForCustomers">
update sys_notify_scan_log
set last_status = 'NORMAL', update_time = sysdate()
where event_type = #{eventType} and last_status = 'OVERDUE'
<if test="customerIds != null and customerIds.size() > 0">
and customer_id not in
<foreach collection="customerIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</if>
</update>
<!-- 当前超额客户:CNY账户,可用额度=余额+当日有效信用额度+当日有效临时额度 < 0 -->
<resultMap id="OverdueCustomerResult" type="com.bms.notify.domain.NotifyOverdueCustomer">
<id property="customerId" column="customer_id" />
<result property="customerCode" column="customer_code" />
<result property="customerName" column="customer_name" />
<result property="isEnable" column="is_enable" />
<result property="balance" column="balance" />
<result property="available" column="available" />
<result property="overdueAmount" column="overdue_amount" />
</resultMap>
<select id="selectOverdueCustomers" resultMap="OverdueCustomerResult">
select t.customer_id, t.customer_code, t.customer_name, t.is_enable, t.balance, t.available,
(0 - t.available) as overdue_amount
from (
select ca.customer_id, c.customer_code, c.customer_name, c.is_enable, ca.balance,
(
ca.balance
+ case when (ca.credit_effective_date is null or ca.credit_effective_date &lt;= curdate())
and (ca.credit_expire_date is null or ca.credit_expire_date &gt;= curdate())
then ifnull(ca.total_credit, 0) else 0 end
+ case when (ca.extra_effective_date is null or ca.extra_effective_date &lt;= curdate())
and (ca.extra_expire_date is null or ca.extra_expire_date &gt;= curdate())
then ifnull(ca.extra_credit, 0) else 0 end
) as available
from ctr_customer_account ca
join ctr_customer c on c.customer_id = ca.customer_id and c.del_flag = '0'
where ca.currency = 'CNY' and ca.del_flag = '0'
) t
where t.available &lt; 0
order by t.available asc
</select>
<!-- ================= 我的消息 ================= -->
<select id="selectPopupReceives" resultMap="NotifyReceiveResult">
select rv.receive_id, rv.message_id, rv.user_id, rv.read_flag, rv.read_time, rv.popup_flag, rv.create_time,
m.title, m.content, m.event_type, m.customer_name, m.customer_code, m.overdue_amount
from sys_notify_receive rv
join sys_notify_message m on m.message_id = rv.message_id
where rv.user_id = #{userId} and rv.popup_flag = 'N'
order by rv.receive_id desc
</select>
<update id="markPopupShown">
update sys_notify_receive set popup_flag = 'Y'
where user_id = #{userId} and popup_flag = 'N'
</update>
<select id="selectMineList" resultMap="NotifyReceiveResult">
select rv.receive_id, rv.message_id, rv.user_id, rv.read_flag, rv.read_time, rv.popup_flag, rv.create_time,
m.title, m.content, m.event_type, m.customer_name, m.customer_code, m.overdue_amount
from sys_notify_receive rv
join sys_notify_message m on m.message_id = rv.message_id
where rv.user_id = #{userId}
<if test="readFlag != null and readFlag != ''"> and rv.read_flag = #{readFlag}</if>
order by rv.receive_id desc
</select>
<select id="countUnread" resultType="int">
select count(1) from sys_notify_receive
where user_id = #{userId} and read_flag = '0'
</select>
<update id="markRead">
update sys_notify_receive
set read_flag = '1', read_time = sysdate()
where receive_id = #{receiveId} and user_id = #{userId} and read_flag = '0'
</update>
<update id="markAllRead">
update sys_notify_receive
set read_flag = '1', read_time = sysdate()
where user_id = #{userId} and read_flag = '0'
</update>
</mapper>
+98
View File
@@ -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(), '扫描已超额(欠费)客户并按通知规则投递消息');