Explorar o código

ReservationArrivalReminderJob除了发送短信之外,还要给用户发送通知

zhangchen hai 4 semanas
pai
achega
818e9e74bb

+ 25 - 0
alien-store/src/main/java/shop/alien/store/service/ArrivalReminderNoticeService.java

@@ -0,0 +1,25 @@
+package shop.alien.store.service;
+
+/**
+ * 预订到店提醒:向用户发送站内通知(与短信独立,供定时任务调用)
+ */
+public interface ArrivalReminderNoticeService {
+
+    /**
+     * 写入一条「到店提醒」站内通知(LifeNotice)
+     * <p>标题:到店提醒</p>
+     * <p>内容:您在HH:mm预订了{店铺名}{桌号}的桌位,请您及时到店</p>
+     *
+     * @param userPhone      预约人手机号(用于 receiverId=user_{phone})
+     * @param orderId        预订订单 id(与 context 中 orderId 一致)
+     * @param businessOrderId 业务主键,写入 life_notice.business_id
+     * @param reservationId  预约 id
+     * @param startTimeRaw   预约开始时间原始串,如 2026-01-01 14:00
+     * @param storeName      店铺名称
+     * @param tableNumbers   桌号文案,多桌逗号拼接
+     * @return 落库成功返回 true
+     */
+    boolean sendArrivalReminderNotice(String userPhone, Integer orderId, Integer businessOrderId,
+                                      Integer reservationId, String startTimeRaw,
+                                      String storeName, String tableNumbers);
+}

+ 79 - 0
alien-store/src/main/java/shop/alien/store/service/impl/ArrivalReminderNoticeServiceImpl.java

@@ -0,0 +1,79 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.LifeNotice;
+import shop.alien.mapper.LifeNoticeMapper;
+import shop.alien.store.service.ArrivalReminderNoticeService;
+
+/**
+ * 到店提醒用户站内通知:独立实现,便于维护与测试
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ArrivalReminderNoticeServiceImpl implements ArrivalReminderNoticeService {
+
+    private static final String TITLE = "到店提醒";
+    private static final int NOTICE_TYPE_ORDER = 2;
+
+    private final LifeNoticeMapper lifeNoticeMapper;
+
+    @Override
+    public boolean sendArrivalReminderNotice(String userPhone, Integer orderId, Integer businessOrderId,
+                                             Integer reservationId, String startTimeRaw,
+                                             String storeName, String tableNumbers) {
+        if (StringUtils.isBlank(userPhone) || orderId == null) {
+            return false;
+        }
+        String phone = userPhone.trim();
+        String hm = extractHourMinute(startTimeRaw);
+        String shop = StringUtils.isNotBlank(storeName) ? storeName.trim() : "未知店铺";
+        String tables = StringUtils.isNotBlank(tableNumbers) ? tableNumbers.trim() : "未知桌号";
+        String message = "您在" + hm + "预订了" + shop + tables + "的桌位,请您及时到店";
+
+        try {
+            JSONObject ctx = new JSONObject();
+            ctx.put("message", message);
+            ctx.put("orderId", orderId);
+            ctx.put("reservationId", reservationId);
+
+            LifeNotice notice = new LifeNotice();
+            notice.setSenderId("system");
+            notice.setReceiverId("user_" + phone);
+            notice.setBusinessId(businessOrderId);
+            notice.setTitle(TITLE);
+            notice.setContext(ctx.toJSONString());
+            notice.setNoticeType(NOTICE_TYPE_ORDER);
+            notice.setIsRead(0);
+            lifeNoticeMapper.insert(notice);
+            log.info("到店提醒站内通知已写入,orderId={}, receiverId=user_{}", orderId, phone);
+            return true;
+        } catch (Exception e) {
+            log.error("到店提醒站内通知写入失败,orderId={}", orderId, e);
+            return false;
+        }
+    }
+
+    /**
+     * 从预约开始时间解析 HH:mm;无法解析时返回「未知时间」
+     */
+    private static String extractHourMinute(String startTimeRaw) {
+        if (StringUtils.isBlank(startTimeRaw)) {
+            return "未知时间";
+        }
+        String s = startTimeRaw.trim();
+        int space = s.lastIndexOf(' ');
+        if (space >= 0 && space + 1 < s.length()) {
+            String tail = s.substring(space + 1).trim();
+            if (tail.length() >= 5 && tail.charAt(2) == ':') {
+                return tail.substring(0, 5);
+            }
+            return tail.length() > 5 ? tail.substring(0, 5) : tail;
+        }
+        return s.length() >= 5 ? s.substring(0, 5) : s;
+    }
+}

+ 57 - 33
alien-store/src/main/java/shop/alien/store/service/impl/UserReservationServiceImpl.java

@@ -71,6 +71,7 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
     private final BaseRedisService baseRedisService;
     private final AliSms aliSms;
     private final ReservationNoticeAsyncService reservationNoticeAsyncService;
+    private final ArrivalReminderNoticeService arrivalReminderNoticeService;
 
     private ReservationOrderPageService reservationOrderPageService;
 
@@ -95,10 +96,11 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
     private static final int MAX_DAYS_TO_CHECK = 366;
     /** 全天预订时使用的结束分钟数(24*60,即到次日0点) */
     private static final int MINUTES_DAY_END = 24 * 60;
-    /** 到店提醒 Redis key 前缀,用于防重复发送 */
-    private static final String REDIS_KEY_ARRIVAL_REMINDER_PREFIX = "reservation:arrival:reminder:order:";
-    /** 到店提醒已发送标记的 Redis 过期时间(25 分钟) */
-    private static final long ARRIVAL_REMINDER_SENT_TTL_SECONDS = 30 * 60;
+    /** 到店提醒:短信防重(保持与原 key 一致,避免已标记订单重复发短信) */
+    private static final String REDIS_KEY_ARRIVAL_SMS_PREFIX = "reservation:arrival:reminder:order:";
+    /** 到店提醒:站内通知防重(与短信独立,短信失败可多次重试) */
+    private static final String REDIS_KEY_ARRIVAL_NOTICE_PREFIX = "reservation:arrival:notice:order:";
+    private static final long ARRIVAL_REMINDER_REDIS_TTL_SECONDS = 30 * 60;
 
     @Override
     public Integer add(UserReservationDTO dto) {
@@ -1189,25 +1191,30 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
         if (orderIds == null || orderIds.isEmpty()) {
             return 0;
         }
-        int sentCount = 0;
+        int smsSuccessCount = 0;
         for (Integer orderId : orderIds) {
             try {
-                String redisKey = REDIS_KEY_ARRIVAL_REMINDER_PREFIX + orderId;
-                if (baseRedisService.hasKey(redisKey)) {
+                String noticeKey = REDIS_KEY_ARRIVAL_NOTICE_PREFIX + orderId;
+                String smsKey = REDIS_KEY_ARRIVAL_SMS_PREFIX + orderId;
+                if (baseRedisService.hasKey(noticeKey) && baseRedisService.hasKey(smsKey)) {
                     continue;
                 }
                 if (sendArrivalReminderSmsForOrder(orderId)) {
-                    baseRedisService.setString(redisKey, "1", Long.valueOf(ARRIVAL_REMINDER_SENT_TTL_SECONDS));
-                    sentCount++;
+                    smsSuccessCount++;
                 }
             } catch (Exception e) {
-                log.error("到店提醒短信处理异常,orderId={}", orderId, e);
+                log.error("到店提醒任务处理异常,orderId={}", orderId, e);
             }
         }
-        log.info("到店提醒定时任务结束,符合条件订单数={}, 发送短信数={}", orderIds.size(), sentCount);
-        return sentCount;
+        log.info("到店提醒定时任务结束,符合条件订单数={}, 短信成功数={}", orderIds.size(), smsSuccessCount);
+        return smsSuccessCount;
     }
 
+    /**
+     * 单订单到店提醒:先写站内通知(与短信是否成功无关,各自 Redis 防重),再发短信。
+     *
+     * @return 本次是否短信发送成功(用于统计)
+     */
     @Override
     public boolean sendArrivalReminderSmsForOrder(Integer orderId) {
         if (orderId == null) {
@@ -1229,40 +1236,57 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
             log.warn("到店提醒跳过:预约人电话为空,orderId={}", orderId);
             return false;
         }
-        String startTimeStr = reservation.getStartTime() != null ? reservation.getStartTime().trim() : "";
-        if (startTimeStr.isEmpty()) {
-            startTimeStr = "未知时间";
+
+        String startTimeForSms = reservation.getStartTime() != null ? reservation.getStartTime().trim() : "";
+        if (startTimeForSms.isEmpty()) {
+            startTimeForSms = "未知时间";
         }
         StoreInfo storeInfo = storeInfoService.getById(reservation.getStoreId());
         String storeName = storeInfo != null && storeInfo.getStoreName() != null ? storeInfo.getStoreName() : "未知店铺";
-
-        LambdaQueryWrapper<UserReservationTable> tableWrapper = new LambdaQueryWrapper<>();
-        tableWrapper.eq(UserReservationTable::getReservationId, reservation.getId())
-                .eq(UserReservationTable::getDeleteFlag, 0)
-                .orderByAsc(UserReservationTable::getSort);
-        List<UserReservationTable> reservationTables = userReservationTableMapper.selectList(tableWrapper);
-        String tableNumber = "未知桌号";
-        if (reservationTables != null && !reservationTables.isEmpty()) {
-            List<String> tableNumbers = reservationTables.stream()
-                    .map(rt -> {
-                        StoreBookingTable table = storeBookingTableService.getById(rt.getTableId());
-                        return table != null && table.getTableNumber() != null ? table.getTableNumber() : null;
-                    })
-                    .filter(tn -> tn != null && !tn.trim().isEmpty())
-                    .collect(Collectors.toList());
-            if (!tableNumbers.isEmpty()) {
-                tableNumber = String.join(",", tableNumbers);
+        String tableNumbers = resolveReservationTableNumbers(reservation.getId());
+
+        String noticeKey = REDIS_KEY_ARRIVAL_NOTICE_PREFIX + orderId;
+        if (!baseRedisService.hasKey(noticeKey)) {
+            boolean noticeOk = arrivalReminderNoticeService.sendArrivalReminderNotice(
+                    phone, orderId, order.getId(), reservation.getId(),
+                    reservation.getStartTime(), storeName, tableNumbers);
+            if (noticeOk) {
+                baseRedisService.setString(noticeKey, "1", Long.valueOf(ARRIVAL_REMINDER_REDIS_TTL_SECONDS));
             }
         }
 
-        Integer smsResult = aliSms.sendArrivalReminderSms(phone, startTimeStr, storeName, tableNumber);
+        String smsKey = REDIS_KEY_ARRIVAL_SMS_PREFIX + orderId;
+        if (baseRedisService.hasKey(smsKey)) {
+            return false;
+        }
+        Integer smsResult = aliSms.sendArrivalReminderSms(phone, startTimeForSms, storeName, tableNumbers);
         if (smsResult != null && smsResult == 1) {
             log.info("到店提醒短信发送成功,orderId={}, orderSn={}, phone={}", orderId, order.getOrderSn(), phone);
+            baseRedisService.setString(smsKey, "1", Long.valueOf(ARRIVAL_REMINDER_REDIS_TTL_SECONDS));
             return true;
         }
         return false;
     }
 
+    private String resolveReservationTableNumbers(Integer reservationId) {
+        LambdaQueryWrapper<UserReservationTable> w = new LambdaQueryWrapper<>();
+        w.eq(UserReservationTable::getReservationId, reservationId)
+                .eq(UserReservationTable::getDeleteFlag, 0)
+                .orderByAsc(UserReservationTable::getSort);
+        List<UserReservationTable> links = userReservationTableMapper.selectList(w);
+        if (links == null || links.isEmpty()) {
+            return "未知桌号";
+        }
+        List<String> nums = links.stream()
+                .map(rt -> {
+                    StoreBookingTable t = storeBookingTableService.getById(rt.getTableId());
+                    return t != null && t.getTableNumber() != null ? t.getTableNumber().trim() : null;
+                })
+                .filter(n -> n != null && !n.isEmpty())
+                .collect(Collectors.toList());
+        return nums.isEmpty() ? "未知桌号" : String.join(",", nums);
+    }
+
     /** 将页面 VO 字段复制到详情 VO,供前端订单详情页与 page 接口一致展示 */
     private void copyPageVoToDetailVo(ReservationOrderPageVo page, ReservationOrderDetailVo detail) {
         detail.setOrderId(page.getOrderId());