客户超额消息通知
This commit is contained in:
+142
@@ -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>可用额度口径与客户资金状态保持一致:
|
||||||
|
* 余额 + 当日有效的信用额度 + 当日有效的临时额度,< 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 <= curdate())
|
||||||
|
and (ca.credit_expire_date is null or ca.credit_expire_date >= curdate())
|
||||||
|
then ifnull(ca.total_credit, 0) else 0 end
|
||||||
|
+ case when (ca.extra_effective_date is null or ca.extra_effective_date <= curdate())
|
||||||
|
and (ca.extra_expire_date is null or ca.extra_expire_date >= 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 < 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>
|
||||||
@@ -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(), '扫描已超额(欠费)客户并按通知规则投递消息');
|
||||||
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="notifyState.visible" :title="dialogTitle" width="660px" append-to-body
|
||||||
|
:close-on-click-modal="false" class="notify-popup-dialog" @open="loadList">
|
||||||
|
<div class="np-toolbar">
|
||||||
|
<el-radio-group v-model="readFilter" size="small" @change="loadList">
|
||||||
|
<el-radio-button value="">全部</el-radio-button>
|
||||||
|
<el-radio-button value="0">未读</el-radio-button>
|
||||||
|
<el-radio-button value="1">已读</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
<el-button type="primary" link :disabled="!notifyState.unreadCount" @click="handleReadAll">全部标记已读</el-button>
|
||||||
|
</div>
|
||||||
|
<el-scrollbar max-height="420px">
|
||||||
|
<div v-for="item in list" :key="item.receiveId" class="np-item" :class="{ unread: item.readFlag === '0' }">
|
||||||
|
<div class="np-item-head">
|
||||||
|
<el-badge is-dot :hidden="item.readFlag === '1'" type="danger">
|
||||||
|
<span class="np-title">{{ item.title }}</span>
|
||||||
|
</el-badge>
|
||||||
|
<span class="np-time">{{ parseTime(item.createTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="np-content">{{ item.content }}</div>
|
||||||
|
<div class="np-foot">
|
||||||
|
<el-tag v-if="item.eventType === 'CUSTOMER_OVERDUE'" type="danger" size="small">客户已超额</el-tag>
|
||||||
|
<span class="np-amount" v-if="item.overdueAmount">超额金额:{{ formatMoney(item.overdueAmount) }} CNY</span>
|
||||||
|
<el-button v-if="item.readFlag === '0'" link type="primary" @click="handleRead(item)">标记已读</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="!list.length" :description="readFilter === '0' ? '暂无未读通知' : '暂无通知'" :image-size="80" />
|
||||||
|
</el-scrollbar>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="notifyState.visible = false">关 闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="NotifyPopup">
|
||||||
|
import { onMounted, ref, computed } from 'vue'
|
||||||
|
import { popupNotify, listMyNotify, countMyNotify, readMyNotify, readAllMyNotify } from '@/api/system/notify'
|
||||||
|
import { notifyState } from './notifyState'
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
const parseTime = proxy.parseTime
|
||||||
|
const formatMoney = (v) => (v === null || v === undefined || v === '' ? '0.00' : Number(v).toFixed(2))
|
||||||
|
|
||||||
|
const list = ref([])
|
||||||
|
const readFilter = ref('')
|
||||||
|
const autoOpened = ref(false)
|
||||||
|
const dialogTitle = computed(() => '我的通知' + (notifyState.unreadCount > 0 ? `(${notifyState.unreadCount} 条未读)` : ''))
|
||||||
|
|
||||||
|
function refreshCount() {
|
||||||
|
countMyNotify().then(res => { notifyState.unreadCount = Number(res.data) || 0 }).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadList() {
|
||||||
|
listMyNotify(readFilter.value).then(res => { list.value = res.data || [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRead(item) {
|
||||||
|
readMyNotify(item.receiveId).then(() => {
|
||||||
|
item.readFlag = '1'
|
||||||
|
refreshCount()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReadAll() {
|
||||||
|
readAllMyNotify().then(() => {
|
||||||
|
proxy.$modal.msgSuccess('已全部标记为已读')
|
||||||
|
refreshCount()
|
||||||
|
loadList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 对外暴露手动打开入口 */
|
||||||
|
notifyState.open = () => {
|
||||||
|
readFilter.value = ''
|
||||||
|
notifyState.visible = true
|
||||||
|
}
|
||||||
|
notifyState.refreshCount = refreshCount
|
||||||
|
notifyState.loadList = loadList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录后自动弹屏:
|
||||||
|
* 1) 先让路由/首页渲染稳定;
|
||||||
|
* 2) 若“初始/过期密码修改”提示正在展示(user.js 写 session 标记),等待其关闭后再弹,避免双框冲突;
|
||||||
|
* 3) 待弹屏消息只弹一次(后端拉取即置 popup_flag=Y),关闭后可从头像菜单“我的通知”再次查看。
|
||||||
|
*/
|
||||||
|
function waitPasswordPromptThenRun(run) {
|
||||||
|
const START_DELAY = 1200
|
||||||
|
const POLL_INTERVAL = 800
|
||||||
|
const MAX_WAIT = 60000
|
||||||
|
setTimeout(() => {
|
||||||
|
const begin = Date.now()
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
const pwdShowing = sessionStorage.getItem('bmsPwdPrompt') === '1'
|
||||||
|
if (!pwdShowing) {
|
||||||
|
clearInterval(timer)
|
||||||
|
setTimeout(run, 300)
|
||||||
|
} else if (Date.now() - begin > MAX_WAIT) {
|
||||||
|
// 用户长时间未处理改密框,超时也弹出通知,避免消息永远看不到
|
||||||
|
clearInterval(timer)
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
}, POLL_INTERVAL)
|
||||||
|
}, START_DELAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshCount()
|
||||||
|
waitPasswordPromptThenRun(() => {
|
||||||
|
if (autoOpened.value) return
|
||||||
|
popupNotify().then(res => {
|
||||||
|
if ((res.data || []).length) {
|
||||||
|
autoOpened.value = true
|
||||||
|
readFilter.value = ''
|
||||||
|
notifyState.visible = true
|
||||||
|
refreshCount()
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.np-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.np-item {
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: var(--el-fill-color-blank);
|
||||||
|
|
||||||
|
&.unread {
|
||||||
|
border-left: 3px solid var(--el-color-danger);
|
||||||
|
background: var(--el-color-danger-light-9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.np-item-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.np-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.np-time {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.np-content {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.np-foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.np-amount {
|
||||||
|
color: #f56c6c;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知弹屏共享状态(单例):
|
||||||
|
* - 组件挂载于全局 Layout,负责登录后自动拉取待弹屏消息
|
||||||
|
* - Navbar “我的通知”入口通过 open() 打开同一弹窗
|
||||||
|
*/
|
||||||
|
export const notifyState = reactive({
|
||||||
|
visible: false,
|
||||||
|
unreadCount: 0
|
||||||
|
})
|
||||||
@@ -42,6 +42,11 @@
|
|||||||
<router-link to="/user/profile">
|
<router-link to="/user/profile">
|
||||||
<el-dropdown-item>个人中心</el-dropdown-item>
|
<el-dropdown-item>个人中心</el-dropdown-item>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<el-dropdown-item command="myNotify">
|
||||||
|
<el-badge :value="notifyState.unreadCount" :hidden="!notifyState.unreadCount" :max="99" type="danger">
|
||||||
|
<span>我的通知</span>
|
||||||
|
</el-badge>
|
||||||
|
</el-dropdown-item>
|
||||||
<el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
|
<el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
|
||||||
<span>布局设置</span>
|
<span>布局设置</span>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
@@ -72,6 +77,7 @@ import useAppStore from '@/store/modules/app'
|
|||||||
import useUserStore from '@/store/modules/user'
|
import useUserStore from '@/store/modules/user'
|
||||||
import useLockStore from '@/store/modules/lock'
|
import useLockStore from '@/store/modules/lock'
|
||||||
import useSettingsStore from '@/store/modules/settings'
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
|
import { notifyState } from '@/components/NotifyPopup/notifyState'
|
||||||
// import HeaderNotice from './HeaderNotice' // 消息通知铃铛已隐藏
|
// import HeaderNotice from './HeaderNotice' // 消息通知铃铛已隐藏
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -93,6 +99,9 @@ function handleCommand(command) {
|
|||||||
case "lockScreen":
|
case "lockScreen":
|
||||||
lockScreen()
|
lockScreen()
|
||||||
break
|
break
|
||||||
|
case "myNotify":
|
||||||
|
notifyState.open && notifyState.open()
|
||||||
|
break
|
||||||
case "logout":
|
case "logout":
|
||||||
logout()
|
logout()
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
<app-main />
|
<app-main />
|
||||||
<settings ref="settingRef" />
|
<settings ref="settingRef" />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 全局消息通知:登录后未读弹屏 + 头像菜单“我的通知”入口 -->
|
||||||
|
<notify-popup />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -17,6 +19,7 @@
|
|||||||
import { useWindowSize } from '@vueuse/core'
|
import { useWindowSize } from '@vueuse/core'
|
||||||
import Sidebar from './components/Sidebar/index.vue'
|
import Sidebar from './components/Sidebar/index.vue'
|
||||||
import { AppMain, Navbar, Settings, TagsView } from './components'
|
import { AppMain, Navbar, Settings, TagsView } from './components'
|
||||||
|
import NotifyPopup from '@/components/NotifyPopup/index.vue'
|
||||||
import useAppStore from '@/store/modules/app'
|
import useAppStore from '@/store/modules/app'
|
||||||
import useSettingsStore from '@/store/modules/settings'
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
|
|
||||||
|
|||||||
@@ -58,17 +58,22 @@ const useUserStore = defineStore(
|
|||||||
this.nickName = user.nickName
|
this.nickName = user.nickName
|
||||||
this.avatar = avatar
|
this.avatar = avatar
|
||||||
cache.session.set('pwrChrtype', res.pwdChrtype)
|
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) {
|
if(res.isDefaultModifyPwd) {
|
||||||
ElMessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
|
showPwdPrompt('您的密码还是初始密码,请修改密码!')
|
||||||
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
}
|
||||||
/* 过期密码提示 */
|
/* 过期密码提示 */
|
||||||
if(!res.isDefaultModifyPwd && res.isPasswordExpired) {
|
if(!res.isDefaultModifyPwd && res.isPasswordExpired) {
|
||||||
ElMessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
|
showPwdPrompt('您的密码已过期,请尽快修改密码!')
|
||||||
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
}
|
||||||
resolve(res)
|
resolve(res)
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
|||||||
@@ -180,7 +180,7 @@
|
|||||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="290" fixed="right">
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="290" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['customersupplier:customerinfo:edit']">修改</el-button>
|
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['customersupplier:customerinfo:edit']">修改</el-button>
|
||||||
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['customersupplier:customerinfo:remove']">删除</el-button>
|
<!-- <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['customersupplier:customerinfo:remove']">删除</el-button>-->
|
||||||
<el-button link type="primary" icon="Wallet" @click="handleCustomerMoney(scope.row)">客户余额</el-button>
|
<el-button link type="primary" icon="Wallet" @click="handleCustomerMoney(scope.row)">客户余额</el-button>
|
||||||
<el-button link type="primary" icon="Document" @click="handleViewLog(scope.row)" v-hasPermi="['customersupplier:customerinfo:query']">日志</el-button>
|
<el-button link type="primary" icon="Document" @click="handleViewLog(scope.row)" v-hasPermi="['customersupplier:customerinfo:query']">日志</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,399 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-tabs v-model="activeTab" class="notify-tabs">
|
||||||
|
<!-- ============ 通知规则 ============ -->
|
||||||
|
<el-tab-pane label="通知配置" name="rule">
|
||||||
|
<el-form :model="ruleQuery" ref="ruleQueryRef" :inline="true" v-show="showSearch" label-width="80px">
|
||||||
|
<el-form-item label="规则名称" prop="ruleName">
|
||||||
|
<el-input v-model="ruleQuery.ruleName" placeholder="请输入规则名称" clearable style="width: 200px"
|
||||||
|
@keyup.enter="handleRuleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="ruleQuery.status" placeholder="全部" clearable style="width: 140px">
|
||||||
|
<el-option v-for="d in sys_normal_disable" :key="d.value" :label="d.label" :value="d.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="Search" @click="handleRuleQuery">搜索</el-button>
|
||||||
|
<el-button icon="Refresh" @click="resetRuleQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-row :gutter="10" class="mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="primary" plain icon="Plus" @click="handleAddRule" v-hasPermi="['system:notify:add']">新增配置</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="warning" plain icon="RefreshRight" :loading="scanLoading"
|
||||||
|
@click="handleScanNow" v-hasPermi="['system:notify:edit']">立即扫描超额客户</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button icon="Refresh" @click="getRuleList">刷新</el-button>
|
||||||
|
</el-col>
|
||||||
|
<right-toolbar v-model:showSearch="showSearch" @queryTable="getRuleList" />
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-table v-loading="ruleLoading" :data="ruleList" border>
|
||||||
|
<el-table-column label="规则名称" prop="ruleName" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="通知事件" align="center" width="150">
|
||||||
|
<template #default>
|
||||||
|
<el-tag type="danger" size="small">客户已超额(欠费)</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="通知对象" align="left" min-width="260">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-for="t in scope.row.targets" :key="t.targetType + t.refId"
|
||||||
|
:type="t.targetType === 'USER' ? 'primary' : 'success'" size="small"
|
||||||
|
style="margin: 2px;">
|
||||||
|
{{ t.targetType === 'USER' ? '用户' : '角色' }}:{{ t.refName }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" align="center" width="90">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1"
|
||||||
|
@change="handleStatusChange(scope.row)" :disabled="!checkPermi(['system:notify:edit'])" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
|
||||||
|
<template #default="scope">{{ parseTime(scope.row.createTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button link type="primary" icon="Edit" @click="handleEditRule(scope.row)"
|
||||||
|
v-hasPermi="['system:notify:edit']">修改</el-button>
|
||||||
|
<el-button link type="primary" icon="Delete" @click="handleDeleteRule(scope.row)"
|
||||||
|
v-hasPermi="['system:notify:remove']">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination v-show="ruleTotal > 0" :total="ruleTotal" v-model:page="ruleQuery.pageNum"
|
||||||
|
v-model:limit="ruleQuery.pageSize" @pagination="getRuleList" />
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- ============ 通知记录 ============ -->
|
||||||
|
<el-tab-pane label="通知记录" name="message">
|
||||||
|
<el-form :model="messageQuery" :inline="true" label-width="90px">
|
||||||
|
<el-form-item label="客户名称">
|
||||||
|
<el-input v-model="messageQuery.customerName" placeholder="客户名称" clearable style="width: 180px"
|
||||||
|
@keyup.enter="getMessageList" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标题">
|
||||||
|
<el-input v-model="messageQuery.title" placeholder="标题关键字" clearable style="width: 180px"
|
||||||
|
@keyup.enter="getMessageList" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="Search" @click="handleMessageQuery">搜索</el-button>
|
||||||
|
<el-button icon="Refresh" @click="resetMessageQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table v-loading="messageLoading" :data="messageList" border>
|
||||||
|
<el-table-column label="标题" prop="title" min-width="220" show-overflow-tooltip />
|
||||||
|
<el-table-column label="客户编号" align="center" prop="customerCode" width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="客户名称" align="center" prop="customerName" width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column label="超额金额(CNY)" align="center" width="130">
|
||||||
|
<template #default="scope"><span class="money-out">{{ formatMoney(scope.row.overdueAmount) }}</span></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="已读/接收" align="center" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.readCount === scope.row.receiveCount ? 'info' : 'warning'" size="small">
|
||||||
|
{{ scope.row.readCount }}/{{ scope.row.receiveCount }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="通知时间" align="center" prop="createTime" width="160">
|
||||||
|
<template #default="scope">{{ parseTime(scope.row.createTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" width="80">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button link type="primary" @click="handleViewMessage(scope.row)"
|
||||||
|
v-hasPermi="['system:notify:query']">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination v-show="messageTotal > 0" :total="messageTotal" v-model:page="messageQuery.pageNum"
|
||||||
|
v-model:limit="messageQuery.pageSize" @pagination="getMessageList" />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<!-- ====== 规则新增/修改弹窗 ====== -->
|
||||||
|
<el-dialog :title="ruleDialog.title" v-model="ruleDialog.visible" width="640px" append-to-body>
|
||||||
|
<el-form ref="ruleFormRef" :model="ruleDialog.form" :rules="ruleRules" label-width="100px">
|
||||||
|
<el-form-item label="规则名称" prop="ruleName">
|
||||||
|
<el-input v-model="ruleDialog.form.ruleName" placeholder="例如:超额通知-财务与销售" maxlength="100" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="通知事件">
|
||||||
|
<el-tag type="danger">客户已超额(欠费)</el-tag>
|
||||||
|
<span class="form-tip">每日 09:00 自动扫描,客户恢复正常后再次超额会再次通知;同一客户每天最多通知一次。</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="通知用户" prop="userIds">
|
||||||
|
<el-select v-model="ruleDialog.form.userIds" multiple filterable collapse-tags collapse-tags-tooltip
|
||||||
|
placeholder="选择要通知的用户(可多选)" style="width: 100%;">
|
||||||
|
<el-option v-for="u in userOptions" :key="u.userId"
|
||||||
|
:label="(u.nickName || u.userName) + '(' + u.userName + ')'" :value="u.userId" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="通知角色" prop="roleIds">
|
||||||
|
<el-select v-model="ruleDialog.form.roleIds" multiple filterable collapse-tags collapse-tags-tooltip
|
||||||
|
placeholder="选择角色后,该角色下所有启用用户都会收到" style="width: 100%;">
|
||||||
|
<el-option v-for="r in roleOptions" :key="r.roleId" :label="r.roleName" :value="r.roleId" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="ruleDialog.form.status">
|
||||||
|
<el-radio v-for="d in sys_normal_disable" :key="d.value" :value="d.value">{{ d.label }}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="ruleDialog.form.remark" type="textarea" :rows="2" maxlength="500" />
|
||||||
|
</el-form-item>
|
||||||
|
<div class="form-tip" style="margin-left: 100px;">
|
||||||
|
用户与角色合并去重通知;被停用的用户/角色不能选择。通知本身不涉及任何余额、额度变动。
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" :loading="ruleDialog.loading" @click="submitRule">确 定</el-button>
|
||||||
|
<el-button @click="ruleDialog.visible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- ====== 消息详情弹窗 ====== -->
|
||||||
|
<el-dialog title="通知详情" v-model="messageDialog.visible" width="680px" append-to-body>
|
||||||
|
<el-descriptions :column="2" border size="small" v-if="messageDialog.data">
|
||||||
|
<el-descriptions-item label="标题" :span="2">{{ messageDialog.data.title }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="客户">{{ messageDialog.data.customerName }}({{ messageDialog.data.customerCode }})</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="超额金额(CNY)">{{ formatMoney(messageDialog.data.overdueAmount) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="通知时间" :span="2">{{ parseTime(messageDialog.data.createTime) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="内容" :span="2">{{ messageDialog.data.content }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<div class="recv-title">接收情况</div>
|
||||||
|
<el-table :data="messageDialog.receives" border size="small" max-height="260">
|
||||||
|
<el-table-column label="接收人" prop="nickName" align="center" width="160">
|
||||||
|
<template #default="scope">{{ scope.row.nickName || scope.row.userName }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="账号" prop="userName" align="center" width="160" />
|
||||||
|
<el-table-column label="状态" align="center" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.readFlag === '1' ? 'success' : 'info'" size="small">
|
||||||
|
{{ scope.row.readFlag === '1' ? '已读' : '未读' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="阅读时间" align="center">
|
||||||
|
<template #default="scope">{{ scope.row.readTime ? parseTime(scope.row.readTime) : '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="messageDialog.visible = false">关 闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="Notify">
|
||||||
|
import { optionUserList } from '@/api/system/user'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
import {
|
||||||
|
listNotifyRule, getNotifyRule, addNotifyRule, updateNotifyRule, delNotifyRule,
|
||||||
|
scanOverdueNow, listNotifyMessage, getNotifyMessage
|
||||||
|
} from '@/api/system/notify'
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
const { sys_normal_disable } = useDict('sys_normal_disable')
|
||||||
|
const checkPermi = (keys) => proxy.$auth.hasPermi(keys)
|
||||||
|
const parseTime = proxy.parseTime
|
||||||
|
const formatMoney = (v) => (v === null || v === undefined || v === '' ? '0.00' : Number(v).toFixed(2))
|
||||||
|
|
||||||
|
const activeTab = ref('rule')
|
||||||
|
const showSearch = ref(true)
|
||||||
|
|
||||||
|
/* ---------- 规则列表 ---------- */
|
||||||
|
const ruleLoading = ref(false)
|
||||||
|
const ruleTotal = ref(0)
|
||||||
|
const ruleList = ref([])
|
||||||
|
const ruleQuery = reactive({ pageNum: 1, pageSize: 10, ruleName: '', status: '' })
|
||||||
|
|
||||||
|
function getRuleList() {
|
||||||
|
ruleLoading.value = true
|
||||||
|
listNotifyRule(ruleQuery).then(res => {
|
||||||
|
ruleList.value = res.rows
|
||||||
|
ruleTotal.value = res.total
|
||||||
|
}).finally(() => { ruleLoading.value = false })
|
||||||
|
}
|
||||||
|
function handleRuleQuery() { ruleQuery.pageNum = 1; getRuleList() }
|
||||||
|
function resetRuleQuery() { proxy.resetForm('ruleQueryRef'); handleRuleQuery() }
|
||||||
|
|
||||||
|
/* ---------- 规则编辑 ---------- */
|
||||||
|
const ruleFormRef = ref(null)
|
||||||
|
const ruleDialog = reactive({ visible: false, title: '', loading: false, form: {} })
|
||||||
|
const userOptions = ref([])
|
||||||
|
const roleOptions = ref([])
|
||||||
|
const ruleRules = {
|
||||||
|
ruleName: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
|
||||||
|
userIds: [{ type: 'array', required: false }],
|
||||||
|
roleIds: [{ type: 'array', required: false }]
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadOptions() {
|
||||||
|
if (!userOptions.value.length) {
|
||||||
|
optionUserList().then(res => { userOptions.value = res.data || [] })
|
||||||
|
}
|
||||||
|
if (!roleOptions.value.length) {
|
||||||
|
request({ url: '/system/role/optionselect', method: 'get' })
|
||||||
|
.then(res => { roleOptions.value = res.data || [] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetRuleForm() {
|
||||||
|
ruleDialog.form = {
|
||||||
|
ruleId: undefined,
|
||||||
|
ruleName: '',
|
||||||
|
eventType: 'CUSTOMER_OVERDUE',
|
||||||
|
status: '0',
|
||||||
|
remark: '',
|
||||||
|
userIds: [],
|
||||||
|
roleIds: [],
|
||||||
|
targets: []
|
||||||
|
}
|
||||||
|
proxy.resetForm('ruleFormRef')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddRule() {
|
||||||
|
resetRuleForm()
|
||||||
|
loadOptions()
|
||||||
|
ruleDialog.title = '新增通知配置'
|
||||||
|
ruleDialog.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditRule(row) {
|
||||||
|
resetRuleForm()
|
||||||
|
loadOptions()
|
||||||
|
getNotifyRule(row.ruleId).then(res => {
|
||||||
|
const d = res.data || {}
|
||||||
|
ruleDialog.form = {
|
||||||
|
ruleId: d.ruleId,
|
||||||
|
ruleName: d.ruleName,
|
||||||
|
eventType: d.eventType || 'CUSTOMER_OVERDUE',
|
||||||
|
status: d.status,
|
||||||
|
remark: d.remark || '',
|
||||||
|
userIds: (d.targets || []).filter(t => t.targetType === 'USER').map(t => t.refId),
|
||||||
|
roleIds: (d.targets || []).filter(t => t.targetType === 'ROLE').map(t => t.refId),
|
||||||
|
targets: d.targets || []
|
||||||
|
}
|
||||||
|
ruleDialog.title = '修改通知配置'
|
||||||
|
ruleDialog.visible = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitRule() {
|
||||||
|
ruleFormRef.value.validate(valid => {
|
||||||
|
if (!valid) return
|
||||||
|
const f = ruleDialog.form
|
||||||
|
if (!f.userIds.length && !f.roleIds.length) {
|
||||||
|
proxy.$modal.msgError('请至少选择一个通知用户或角色')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const targets = []
|
||||||
|
f.userIds.forEach(uid => {
|
||||||
|
const u = userOptions.value.find(x => x.userId === uid)
|
||||||
|
targets.push({ targetType: 'USER', refId: uid, refName: u ? (u.nickName || u.userName) : String(uid) })
|
||||||
|
})
|
||||||
|
f.roleIds.forEach(rid => {
|
||||||
|
const r = roleOptions.value.find(x => x.roleId === rid)
|
||||||
|
targets.push({ targetType: 'ROLE', refId: rid, refName: r ? r.roleName : String(rid) })
|
||||||
|
})
|
||||||
|
const payload = {
|
||||||
|
ruleId: f.ruleId,
|
||||||
|
ruleName: f.ruleName,
|
||||||
|
eventType: 'CUSTOMER_OVERDUE',
|
||||||
|
status: f.status,
|
||||||
|
remark: f.remark,
|
||||||
|
targets
|
||||||
|
}
|
||||||
|
ruleDialog.loading = true
|
||||||
|
const action = f.ruleId ? updateNotifyRule(payload) : addNotifyRule(payload)
|
||||||
|
action.then(() => {
|
||||||
|
proxy.$modal.msgSuccess(f.ruleId ? '修改成功' : '新增成功')
|
||||||
|
ruleDialog.visible = false
|
||||||
|
getRuleList()
|
||||||
|
}).finally(() => { ruleDialog.loading = false })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStatusChange(row) {
|
||||||
|
const text = row.status === '0' ? '启用' : '停用'
|
||||||
|
proxy.$modal.confirm('确认' + text + '规则「' + row.ruleName + '」?').then(() => {
|
||||||
|
return updateNotifyRule({
|
||||||
|
ruleId: row.ruleId,
|
||||||
|
ruleName: row.ruleName,
|
||||||
|
eventType: row.eventType,
|
||||||
|
status: row.status,
|
||||||
|
targets: (row.targets || []).map(t => ({ targetType: t.targetType, refId: t.refId, refName: t.refName }))
|
||||||
|
})
|
||||||
|
}).then(() => {
|
||||||
|
proxy.$modal.msgSuccess(text + '成功')
|
||||||
|
}).catch(() => {
|
||||||
|
row.status = row.status === '0' ? '1' : '0'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteRule(row) {
|
||||||
|
proxy.$modal.confirm('确认删除规则「' + row.ruleName + '」?删除后不再产生新通知,历史通知记录保留。').then(() => {
|
||||||
|
return delNotifyRule(row.ruleId)
|
||||||
|
}).then(() => {
|
||||||
|
proxy.$modal.msgSuccess('删除成功')
|
||||||
|
getRuleList()
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const scanLoading = ref(false)
|
||||||
|
function handleScanNow() {
|
||||||
|
proxy.$modal.confirm('立即扫描所有超额(欠费)客户并按启用配置投递消息?同一客户当天只通知一次。').then(() => {
|
||||||
|
scanLoading.value = true
|
||||||
|
return scanOverdueNow()
|
||||||
|
}).then(res => {
|
||||||
|
proxy.$modal.msgSuccess(res.msg || '扫描完成')
|
||||||
|
if (activeTab.value === 'message') getMessageList()
|
||||||
|
}).catch(() => {}).finally(() => { scanLoading.value = false })
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 消息记录 ---------- */
|
||||||
|
const messageLoading = ref(false)
|
||||||
|
const messageTotal = ref(0)
|
||||||
|
const messageList = ref([])
|
||||||
|
const messageQuery = reactive({ pageNum: 1, pageSize: 10, customerName: '', title: '' })
|
||||||
|
|
||||||
|
function getMessageList() {
|
||||||
|
messageLoading.value = true
|
||||||
|
listNotifyMessage(messageQuery).then(res => {
|
||||||
|
messageList.value = res.rows
|
||||||
|
messageTotal.value = res.total
|
||||||
|
}).finally(() => { messageLoading.value = false })
|
||||||
|
}
|
||||||
|
function handleMessageQuery() { messageQuery.pageNum = 1; getMessageList() }
|
||||||
|
function resetMessageQuery() {
|
||||||
|
messageQuery.customerName = ''
|
||||||
|
messageQuery.title = ''
|
||||||
|
handleMessageQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageDialog = reactive({ visible: false, data: null, receives: [] })
|
||||||
|
function handleViewMessage(row) {
|
||||||
|
getNotifyMessage(row.messageId).then(res => {
|
||||||
|
messageDialog.data = res.data
|
||||||
|
messageDialog.receives = (res.data && res.data.receives) || []
|
||||||
|
messageDialog.visible = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getRuleList()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.notify-tabs { margin-top: -8px; }
|
||||||
|
.mb8 { margin-bottom: 12px; }
|
||||||
|
.money-out { color: #f56c6c; font-weight: 600; }
|
||||||
|
.form-tip { color: #909399; font-size: 12px; line-height: 1.6; margin-left: 8px; }
|
||||||
|
.recv-title { font-weight: 600; margin: 14px 0 8px; }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user