Prechádzať zdrojové kódy

发送到店提醒代码

zhangchen 1 mesiac pred
rodič
commit
f129517a94

+ 9 - 0
alien-entity/src/main/java/shop/alien/mapper/UserReservationOrderMapper.java

@@ -9,6 +9,8 @@ import shop.alien.entity.store.UserReservationOrder;
 import shop.alien.entity.store.vo.ReservationOrderCountsDto;
 import shop.alien.entity.store.vo.ReservationOrderListDto;
 
+import java.util.List;
+
 /**
  * 用户预订订单表 Mapper 接口
  *
@@ -47,4 +49,11 @@ public interface UserReservationOrderMapper extends BaseMapper<UserReservationOr
      * @return 各状态数量
      */
     ReservationOrderCountsDto selectOrderCountsByUserId(@Param("userId") Integer userId);
+
+    /**
+     * 查询待使用订单且预定开始时间在「当前时间~当前时间+30分钟」内的订单ID列表,用于到店提醒短信
+     *
+     * @return 订单ID列表
+     */
+    List<Integer> listOrderIdsForArrivalReminder();
 }

+ 12 - 0
alien-entity/src/main/resources/mapper/UserReservationOrderMapper.xml

@@ -70,4 +70,16 @@
         WHERE o.delete_flag = 0
           AND o.user_id = #{userId}
     </select>
+
+    <!-- 到店提醒:待使用订单且预定开始时间在 当前时间~当前时间+30分钟 内的订单ID。start_time 格式为 yyyy-MM-dd HH:mm -->
+    <select id="listOrderIdsForArrivalReminder" resultType="java.lang.Integer">
+        SELECT o.id
+        FROM user_reservation_order o
+        INNER JOIN user_reservation r ON o.reservation_id = r.id AND r.delete_flag = 0
+        WHERE o.delete_flag = 0
+          AND o.order_status = 1
+          AND r.status = 1
+          AND r.start_time IS NOT NULL AND TRIM(r.start_time) != ''
+          AND STR_TO_DATE(TRIM(r.start_time), '%Y-%m-%d %H:%i') BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 30 MINUTE)
+    </select>
 </mapper>

+ 8 - 0
alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java

@@ -77,4 +77,12 @@ public interface AlienStoreFeign {
     @org.springframework.web.bind.annotation.PostMapping("/reservation/job/markTimeout")
     R<Integer> markReservationTimeoutByEndTime();
 
+    /**
+     * 到店提醒定时任务:订单待使用且距预定开始时间≤30分钟时发送短信提醒
+     *
+     * @return R.data 为本次发送短信条数
+     */
+    @org.springframework.web.bind.annotation.PostMapping("/reservation/job/sendArrivalReminder")
+    R<Integer> sendArrivalReminder();
+
 }

+ 33 - 0
alien-job/src/main/java/shop/alien/job/store/ReservationArrivalReminderJob.java

@@ -0,0 +1,33 @@
+package shop.alien.job.store;
+
+import com.xxl.job.core.handler.annotation.XxlJob;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.result.R;
+import shop.alien.job.feign.AlienStoreFeign;
+
+/**
+ * 到店提醒定时任务
+ * 订单状态为待使用且当前时间距离预定开始时间(user_reservation.reservation_date + start_time)≤30分钟时,发送到店提醒短信
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class ReservationArrivalReminderJob {
+
+    private final AlienStoreFeign alienStoreFeign;
+
+    @XxlJob("reservationArrivalReminderJob")
+    public void reservationArrivalReminderJob() {
+        log.info("【定时任务】到店提醒:开始执行");
+        try {
+            R<Integer> result = alienStoreFeign.sendArrivalReminder();
+            int count = (result != null && result.getData() != null) ? result.getData() : 0;
+            log.info("【定时任务】到店提醒:执行完成,本次发送短信条数={}", count);
+        } catch (Exception e) {
+            log.error("【定时任务】到店提醒:执行异常", e);
+            throw e;
+        }
+    }
+}

+ 9 - 0
alien-store/src/main/java/shop/alien/store/controller/ReservationJobController.java

@@ -30,4 +30,13 @@ public class ReservationJobController {
         log.info("reservation job: markTimeout 结束,更新条数={}", count);
         return R.data(count);
     }
+
+    @ApiOperation("到店提醒:订单待使用且距预定开始时间≤30分钟时发送短信提醒")
+    @PostMapping("/sendArrivalReminder")
+    public R<Integer> sendArrivalReminder() {
+        log.info("reservation job: sendArrivalReminder 开始");
+        int count = userReservationService.sendArrivalReminderSms();
+        log.info("reservation job: sendArrivalReminder 结束,发送条数={}", count);
+        return R.data(count);
+    }
 }

+ 16 - 0
alien-store/src/main/java/shop/alien/store/service/UserReservationService.java

@@ -190,4 +190,20 @@ public interface UserReservationService extends IService<UserReservation> {
      * @return 本次更新的预约数量(即更新的 order 数量)
      */
     int markReservationTimeoutByEndTime();
+
+    /**
+     * 定时任务:订单状态为待使用且当前时间距离预定开始时间小于等于30分钟时,发送到店提醒短信。
+     * 短信名称:到店提醒。内容:您在14:00预订了xxx(店铺名称)A01(桌号或名称)的桌位,请您及时到店
+     *
+     * @return 本次发送短信条数
+     */
+    int sendArrivalReminderSms();
+
+    /**
+     * 为单个订单发送到店提醒短信(供定时任务或 Redis 过期回调调用,不校验 Redis 防重)
+     *
+     * @param orderId 订单ID
+     * @return true 发送成功,false 未发送或发送失败
+     */
+    boolean sendArrivalReminderSmsForOrder(Integer orderId);
 }

+ 89 - 0
alien-store/src/main/java/shop/alien/store/service/impl/UserReservationServiceImpl.java

@@ -26,7 +26,9 @@ import shop.alien.store.vo.ReservationOrderDetailVo;
 import shop.alien.store.vo.ReservationOrderPageVo;
 import shop.alien.mapper.UserReservationMapper;
 import shop.alien.mapper.UserReservationTableMapper;
+import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.service.*;
+import shop.alien.store.util.ali.AliSms;
 import shop.alien.util.common.UniqueRandomNumGenerator;
 
 import java.math.BigDecimal;
@@ -67,6 +69,9 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
 
     private final EssentialHolidayComparisonMapper essentialHolidayComparisonMapper;
 
+    private final BaseRedisService baseRedisService;
+    private final AliSms aliSms;
+
     private ReservationOrderPageService reservationOrderPageService;
 
     @Autowired
@@ -90,6 +95,10 @@ 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;
 
     @Override
     public Integer add(UserReservationDTO dto) {
@@ -1115,6 +1124,86 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
         return toUpdateReservationIds.size();
     }
 
+    @Override
+    public int sendArrivalReminderSms() {
+        List<Integer> orderIds = userReservationOrderMapper.listOrderIdsForArrivalReminder();
+        if (orderIds == null || orderIds.isEmpty()) {
+            return 0;
+        }
+        int sentCount = 0;
+        for (Integer orderId : orderIds) {
+            try {
+                String redisKey = REDIS_KEY_ARRIVAL_REMINDER_PREFIX + orderId;
+                if (baseRedisService.hasKey(redisKey)) {
+                    continue;
+                }
+                if (sendArrivalReminderSmsForOrder(orderId)) {
+                    baseRedisService.setString(redisKey, "1", Long.valueOf(ARRIVAL_REMINDER_SENT_TTL_SECONDS));
+                    sentCount++;
+                }
+            } catch (Exception e) {
+                log.error("到店提醒短信处理异常,orderId={}", orderId, e);
+            }
+        }
+        log.info("到店提醒定时任务结束,符合条件订单数={}, 发送短信数={}", orderIds.size(), sentCount);
+        return sentCount;
+    }
+
+    @Override
+    public boolean sendArrivalReminderSmsForOrder(Integer orderId) {
+        if (orderId == null) {
+            return false;
+        }
+        UserReservationOrder order = userReservationOrderService.getById(orderId);
+        if (order == null || order.getReservationId() == null) {
+            return false;
+        }
+        UserReservation reservation = this.getById(order.getReservationId());
+        if (reservation == null) {
+            return false;
+        }
+        if (order.getOrderStatus() == null || order.getOrderStatus() != ORDER_STATUS_TO_USE) {
+            return false;
+        }
+        String phone = reservation.getReservationUserPhone();
+        if (phone == null || phone.trim().isEmpty()) {
+            log.warn("到店提醒跳过:预约人电话为空,orderId={}", orderId);
+            return false;
+        }
+        String startTimeStr = reservation.getStartTime() != null ? reservation.getStartTime().trim() : "";
+        if (startTimeStr.isEmpty()) {
+            startTimeStr = "未知时间";
+        }
+        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);
+            }
+        }
+
+        Integer smsResult = aliSms.sendArrivalReminderSms(phone, startTimeStr, storeName, tableNumber);
+        if (smsResult != null && smsResult == 1) {
+            log.info("到店提醒短信发送成功,orderId={}, orderSn={}, phone={}", orderId, order.getOrderSn(), phone);
+            return true;
+        }
+        return false;
+    }
+
     /** 将页面 VO 字段复制到详情 VO,供前端订单详情页与 page 接口一致展示 */
     private void copyPageVoToDetailVo(ReservationOrderPageVo page, ReservationOrderDetailVo detail) {
         detail.setOrderId(page.getOrderId());

+ 79 - 0
alien-store/src/main/java/shop/alien/store/util/ali/AliSms.java

@@ -66,6 +66,10 @@ public class AliSms {
     @Value("${ali.sms.templatereRefundCode:}")
     private String templatereRefundCode;
 
+    /** 到店提醒短信模板(内容:您在${time}预订了${storeName}${tableNumber}的桌位,请您及时到店) */
+    @Value("${ali.sms.templateArrivalReminderCode:}")
+    private String templateArrivalReminderCode;
+
 
 
 
@@ -545,4 +549,79 @@ public class AliSms {
         }
     }
 
+    /**
+     * 发送到店提醒短信
+     * 名称:到店提醒。内容:您在${time}预订了${storeName}${tableNumber}的桌位,请您及时到店
+     *
+     * @param phone       用户手机号
+     * @param timeStr     预订开始时间(如 14:00)
+     * @param storeName   店铺名称
+     * @param tableNumber 桌号或名称(如 A01)
+     * @return 1-发送成功, null-发送失败
+     */
+    public Integer sendArrivalReminderSms(String phone, String timeStr, String storeName, String tableNumber) {
+        log.info("AliSms.sendArrivalReminderSms?phone={}, timeStr={}, storeName={}, tableNumber={}",
+                phone, timeStr, storeName, tableNumber);
+        try {
+            if (phone == null || phone.trim().isEmpty()) {
+                log.warn("发送到店提醒短信失败:手机号不能为空");
+                return null;
+            }
+            if (timeStr == null || timeStr.trim().isEmpty()) {
+                log.warn("发送到店提醒短信失败:预订时间不能为空");
+                return null;
+            }
+            if (storeName == null) {
+                storeName = "";
+            }
+            if (tableNumber == null) {
+                tableNumber = "";
+            }
+
+            String templateCodeArrival = templateArrivalReminderCode;
+            if (templateCodeArrival == null || templateCodeArrival.trim().isEmpty()) {
+                log.warn("发送到店提醒短信失败:模板代码未配置 ali.sms.templateArrivalReminderCode");
+                return null;
+            }
+
+            String templateParam = String.format("{\"time\":\"%s\",\"storeName\":\"%s\",\"tableNumber\":\"%s\"}",
+                    escapeJsonString(timeStr),
+                    escapeJsonString(storeName),
+                    escapeJsonString(tableNumber));
+
+            SendSmsRequest sendSmsRequest = new SendSmsRequest()
+                    .setSignName(signName)
+                    .setTemplateCode(templateCodeArrival)
+                    .setPhoneNumbers(phone)
+                    .setTemplateParam(templateParam);
+
+            Config config = new Config()
+                    .setEndpoint(endPoint)
+                    .setAccessKeyId(accessKeyId)
+                    .setAccessKeySecret(accessKeySecret);
+            RuntimeOptions runtime = new RuntimeOptions();
+            Client client = new Client(config);
+            SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, runtime);
+
+            String responseCode = sendSmsResponse.getBody().getCode();
+            String responseMessage = sendSmsResponse.getBody().getMessage();
+            String bizId = sendSmsResponse.getBody().getBizId();
+
+            log.info("AliSms.sendArrivalReminderSms API响应,phone={}, responseCode={}, responseMessage={}, bizId={}",
+                    phone, responseCode, responseMessage, bizId);
+
+            if (!"OK".equals(responseCode)) {
+                log.error("AliSms.sendArrivalReminderSms 短信发送失败,phone={}, templateParam={}, responseCode={}, responseMessage={}",
+                        phone, templateParam, responseCode, responseMessage);
+                return null;
+            }
+            log.info("AliSms.sendArrivalReminderSms 短信发送成功,phone={}, bizId={}", phone, bizId);
+            return 1;
+        } catch (Exception e) {
+            log.error("AliSms.sendArrivalReminderSms ERROR phone={}, timeStr={}, storeName={}, tableNumber={}, Msg={}",
+                    phone, timeStr, storeName, tableNumber, e.getMessage(), e);
+            return null;
+        }
+    }
+
 }