liu
2 天以前 7c2b68615207bfae95e208d1f0f16eb0612e8394
feat(mes): 计量器具已过期提醒接入系统消息提示(站内信)

- 新增已过期器具查询方法 selectListByNextCheckDateBefore
- 已过期与即将到期使用独立 Redis 去重 key,器具过期后仍单独提醒一次(60 天内不重复)
- 新增已过期站内信模板 mes_metering_expire_overdue

Co-Authored-By: Claude <noreply@anthropic.com>
已修改4个文件
60 ■■■■ 文件已修改
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/metering/MesDvMeteringMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/RedisKeyConstants.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/dv/metering/MesDvMeteringRemindRedisDAO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindServiceImpl.java 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/metering/MesDvMeteringMapper.java
@@ -42,4 +42,10 @@
                .le(MesDvMeteringDO::getNextCheckDate, end));
    }
    default List<MesDvMeteringDO> selectListByNextCheckDateBefore(LocalDate date) {
        return selectList(new LambdaQueryWrapperX<MesDvMeteringDO>()
                .isNotNull(MesDvMeteringDO::getNextCheckDate)
                .lt(MesDvMeteringDO::getNextCheckDate, date));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/RedisKeyConstants.java
@@ -31,4 +31,14 @@
     */
    String METERING_EXPIRE_REMIND_SCAN = "mes:dv:metering:remind:scan:";
    /**
     * 计量器具已过期提醒去重
     *
     * KEY 格式:mes:dv:metering:overdue:remind:{meteringId}:{nextCheckDate}
     * VALUE 数据格式:1
     *
     * 与 {@link #METERING_EXPIRE_REMIND} 相互独立,保证器具过期后仍能单独提醒一次
     */
    String METERING_EXPIRE_REMIND_OVERDUE = "mes:dv:metering:overdue:remind:";
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/dv/metering/MesDvMeteringRemindRedisDAO.java
@@ -41,6 +41,20 @@
    }
    /**
     * 尝试标记已提醒(已过期场景,60 天内只提醒一次)
     *
     * 使用独立的 KEY,与 {@link #tryMarkReminded} 相互隔离,保证器具过期后仍能单独提醒一次
     *
     * @param meteringId 计量器具编号
     * @param nextCheckDate 到期日
     * @return true 表示首次标记成功,需要发送提醒
     */
    public boolean tryMarkOverdueReminded(Long meteringId, LocalDate nextCheckDate) {
        String key = RedisKeyConstants.METERING_EXPIRE_REMIND_OVERDUE + meteringId + ":" + nextCheckDate;
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "1", REMIND_TIMEOUT));
    }
    /**
     * 尝试获取当天的扫描门闩,避免多实例并发重复扫描
     *
     * @param today 当天日期
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindServiceImpl.java
@@ -34,9 +34,13 @@
     */
    private static final int DEFAULT_REMIND_DAYS = 30;
    /**
     * 到期提醒站内信模板编号
     * 即将到期提醒站内信模板编号
     */
    private static final String REMIND_TEMPLATE_CODE = "mes_metering_expire_remind";
    /**
     * 已过期提醒站内信模板编号
     */
    private static final String OVERDUE_TEMPLATE_CODE = "mes_metering_expire_overdue";
    @Resource
    private MesDvMeteringMapper meteringMapper;
@@ -57,18 +61,28 @@
            }
            // 2. 读取提醒天数(默认 30)
            int remindDays = getRemindDays();
            // 3. 查询 [today, today + remindDays] 到期的器具
            List<MesDvMeteringDO> list = meteringMapper.selectListByNextCheckDateBetween(
            // 3. 查询已过期(到期日 < 今天)与即将到期(到期日在 [today, today + remindDays])的器具
            List<MesDvMeteringDO> overdueList = meteringMapper.selectListByNextCheckDateBefore(today);
            List<MesDvMeteringDO> dueList = meteringMapper.selectListByNextCheckDateBetween(
                    today, today.plusDays(remindDays));
            // 4. 逐台发送站内信(按器具 + 到期日去重)
            for (MesDvMeteringDO metering : list) {
            // 4. 逐台发送站内信(按器具 + 到期日去重,两类相互独立)
            for (MesDvMeteringDO metering : overdueList) {
                if (metering.getChargeUserId() == null || metering.getNextCheckDate() == null) {
                    continue;
                }
                if (!meteringRemindRedisDAO.tryMarkOverdueReminded(metering.getId(), metering.getNextCheckDate())) {
                    continue;
                }
                sendRemindMessage(metering, OVERDUE_TEMPLATE_CODE);
            }
            for (MesDvMeteringDO metering : dueList) {
                if (metering.getChargeUserId() == null || metering.getNextCheckDate() == null) {
                    continue;
                }
                if (!meteringRemindRedisDAO.tryMarkReminded(metering.getId(), metering.getNextCheckDate())) {
                    continue;
                }
                sendRemindMessage(metering);
                sendRemindMessage(metering, REMIND_TEMPLATE_CODE);
            }
        } catch (Exception ex) {
            // best-effort:提醒失败不影响列表查询
@@ -89,14 +103,14 @@
        }
    }
    private void sendRemindMessage(MesDvMeteringDO metering) {
    private void sendRemindMessage(MesDvMeteringDO metering, String templateCode) {
        Map<String, Object> params = new HashMap<>();
        params.put("meteringCode", metering.getCode());
        params.put("meteringName", metering.getName());
        params.put("nextCheckDate", metering.getNextCheckDate().toString());
        NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO()
                .setUserId(metering.getChargeUserId())
                .setTemplateCode(REMIND_TEMPLATE_CODE)
                .setTemplateCode(templateCode)
                .setTemplateParams(params);
        notifyMessageSendApi.sendSingleMessageToAdmin(reqDTO);
    }