2 Коміти 7b699eb90e ... 7979d3645a

Автор SHA1 Опис Дата
  wangzhijun 7979d3645a 按摩支付、积分商城支付增加抽奖次数 1 тиждень тому
  wangzhijun e5d1097c0d getOrderItemVoByOrderId添加返回productId 1 тиждень тому

+ 3 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/OrderItemVo.java

@@ -16,6 +16,9 @@ public class OrderItemVo {
     @ApiModelProperty("商品规格图片")
     private String skuImage;
 
+    @ApiModelProperty("商品id")
+    private Long productId;
+
     @ApiModelProperty("商品名称")
     private String productName;
 

+ 226 - 10
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductOrderInfoServiceImpl.java

@@ -1,6 +1,7 @@
 package com.ylx.massage.service.impl;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
@@ -15,30 +16,40 @@ import com.ylx.common.core.domain.R;
 import com.ylx.common.core.domain.model.WxLoginUser;
 import com.ylx.common.exception.ServiceException;
 import com.ylx.common.utils.SecurityUtils;
+import com.ylx.lottery.domain.LotteryCountLog;
+import com.ylx.lottery.domain.vo.LocalActivityTableVO;
+import com.ylx.lottery.domain.vo.LotteryActivityRulesProductVO;
+import com.ylx.lottery.domain.vo.LotteryActivityVO;
+import com.ylx.lottery.domain.vo.LotteryStatVO;
+import com.ylx.lottery.service.LotteryCountLogService;
+import com.ylx.lottery.service.LotteryCountService;
 import com.ylx.massage.domain.*;
 import com.ylx.massage.domain.dto.*;
 import com.ylx.massage.domain.vo.*;
 import com.ylx.massage.enums.*;
-import com.ylx.massage.mapper.AfterSaleOrderMapper;
-import com.ylx.massage.mapper.ProductMapper;
-import com.ylx.massage.mapper.ProductOrderInfoMapper;
-import com.ylx.massage.mapper.ProductOrderItemMapper;
-import com.ylx.massage.mapper.ProductSkuMapper;
+import com.ylx.massage.mapper.*;
 import com.ylx.massage.service.*;
 import com.ylx.massage.utils.OrderNumberGenerator;
 import com.ylx.point.enums.TaskNameEnum;
 import com.ylx.point.service.IPointAccountService;
+import com.ylx.usercenter.domain.dto.UnifiedUserCenterDTO;
+import com.ylx.usercenter.service.UnifiedUserCenterService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
 
 import javax.annotation.Resource;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 商品订单信息Service实现类
@@ -79,6 +90,13 @@ public class ProductOrderInfoServiceImpl extends ServiceImpl<ProductOrderInfoMap
 
     @Resource
     private IPointAccountService pointAccountService;
+    @Resource
+    private UnifiedUserCenterService unifiedUserCenterService;
+    @Resource
+    private LotteryCountService lotteryCountService;
+    @Resource
+    private LotteryCountLogService lotteryCountLogService;
+
 
     /**
      * 创建商品订单
@@ -263,7 +281,7 @@ public class ProductOrderInfoServiceImpl extends ServiceImpl<ProductOrderInfoMap
         }
 
         // 4、校验支付金额是否正确
-        if(productOrderPayRequest.getPayType().equals(MassageConstants.INTEGER_ONE) || productOrderPayRequest.getPayType().equals(MassageConstants.INTEGER_TWO)){
+        if (productOrderPayRequest.getPayType().equals(MassageConstants.INTEGER_ONE) || productOrderPayRequest.getPayType().equals(MassageConstants.INTEGER_TWO)) {
             if (orderInfo.getTotalAmount().compareTo(productOrderPayRequest.getTotalPrice()) != 0) {
                 throw new ServiceException("支付金额错误");
             }
@@ -293,7 +311,7 @@ public class ProductOrderInfoServiceImpl extends ServiceImpl<ProductOrderInfoMap
                 orderPayManage(user, orderInfo);
                 return R.ok();
             }
-        }else{
+        } else {
             //积分支付
             pointAccountService.deductPoints(orderInfo.getOpenId(), orderInfo.getPointsUsed(), orderInfo.getOrderNo(), TaskNameEnum.GOODS_CONSUME.getInfo());
             return R.ok();
@@ -342,6 +360,12 @@ public class ProductOrderInfoServiceImpl extends ServiceImpl<ProductOrderInfoMap
         productOrderInfo.setPayTime(LocalDateTime.now());
         //更新订单表
         updateById(productOrderInfo);
+
+        // 任务奖励 或者 消费奖励 —— 发放抽奖次数日志
+        grantLotteryCountForPay(user, orderInfo);
+
+        // 已绑定一账通 → 异步同步抽奖次数
+        syncLotteryCountIfNeeded(user);
     }
 
     /**
@@ -646,16 +670,16 @@ public class ProductOrderInfoServiceImpl extends ServiceImpl<ProductOrderInfoMap
         queryWrapper.eq(AfterSaleOrder::getOrderId, productOrderInfo.getId())
                 .eq(AfterSaleOrder::getOpenId, loginUser.getCOpenid())
                 .ne(AfterSaleOrder::getAfterSaleStatus, AfterSaleStatusEnum.CANCELLED.getCode());
-        
+
         List<AfterSaleOrder> afterSaleOrders = this.afterSaleOrderService.list(queryWrapper);
         log.info("订单{}的售后单列表:{}", productOrderInfo.getId(), afterSaleOrders);
-        
+
         if (CollectionUtil.isEmpty(afterSaleOrders)) {
             log.warn("订单{}没有符合条件的售后单,跳过售后单状态更新", productOrderInfo.getId());
             // 没有售后单的情况,不抛出异常,直接返回
             return;
         }
-        
+
         // 更新售后单状态
         LambdaUpdateWrapper<AfterSaleOrder> updateWrapper = new LambdaUpdateWrapper<>();
         updateWrapper.eq(AfterSaleOrder::getOrderId, productOrderInfo.getId())
@@ -835,4 +859,196 @@ public class ProductOrderInfoServiceImpl extends ServiceImpl<ProductOrderInfoMap
         log.info("商品订单微信支付回调处理完成,订单号:{},订单状态更新为待发货,支付状态更新为已支付", orderNo);
     }
 
+    private void grantLotteryCountForPay(TWxUser user, ProductOrderInfo orderInfo) {
+
+        // 获取按摩订单中的商品ID
+        OrderItemVo orderItemVo = productOrderItemMapper.getOrderItemVoByOrderId(orderInfo.getId());
+
+        if (ObjectUtil.isNull(orderItemVo)) {
+            return;
+        }
+        Long productId = orderItemVo.getProductId();
+
+        if (ObjectUtil.isNull(productId)) {
+            return;
+        }
+
+        List<LotteryActivityVO> activityList = lotteryCountService.queryActivityRules();
+        if (CollUtil.isEmpty(activityList)) {
+            return;
+        }
+
+        // 查询抽奖次数统计
+        LambdaQueryWrapper<LotteryCountLog> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(LotteryCountLog::getUserId, user.getId());
+        queryWrapper.eq(LotteryCountLog::getIsDelete, 0);
+        List<LotteryStatVO> lotteryStatVOS = lotteryCountLogService.selectSumByGroup(queryWrapper);
+
+        // 生成活动ID -> 已获次数 的 Map
+        Map<String, Integer> activityGrantedCountMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(lotteryStatVOS)) {
+            for (LotteryStatVO statVO : lotteryStatVOS) {
+                activityGrantedCountMap.put(statVO.getLocalActivityTableId(), statVO.getTotalNum());
+            }
+        }
+
+        for (LotteryActivityVO activity : activityList) {
+
+            String activityId = activity.getId();
+
+            // 用户获取的最大抽奖次数
+            Integer productUserMaxNum = activity.getProductUserMaxNum();
+
+            // 检查 productUserMaxNum 是否为 null,如果是,则认为无限制
+            boolean hasLimit = !ObjectUtil.isNull(productUserMaxNum);
+
+            if (ObjectUtil.equals(activity.getParticipationRules(), 3)) { // 任务奖励类型
+
+                List<LocalActivityTableVO> localTables = activity.getLocalActivityTables();
+                if (CollUtil.isEmpty(localTables)) {
+                    continue;
+                }
+
+                // 类型=2 → 支付有礼
+                for (LocalActivityTableVO table : localTables) {
+                    if (ObjectUtil.equals(table.getType(), 2) && ObjectUtil.equals(table.getIsProduct(), 1)) {
+                        if (ObjectUtil.equals(table.getProductId(), productId)) {
+
+                            Integer userLotteryCount = activityGrantedCountMap.getOrDefault(activityId, 0);
+
+                            // 本次请求发放的数量
+                            Integer requestedLotteryNum = 1;
+
+                            // 计算实际可发放的数量
+                            int actualLotteryNumToGrant = calculateActualGrant(hasLimit, userLotteryCount, requestedLotteryNum, productUserMaxNum);
+
+                            if (actualLotteryNumToGrant > 0) {
+                                // 发放计算后的数量
+                                saveLotteryLog(user, activityId, actualLotteryNumToGrant, 2);
+                            }
+                            break;
+                        }
+                    }
+                }
+            } else if (ObjectUtil.equals(activity.getParticipationRules(), 4)) { // 消费奖励类型
+                if (ObjectUtil.isNull(activity.getProductRestriction())) {
+                    continue;
+                }
+
+                Integer requestedLotteryNum = null;
+                boolean conditionMet = false;
+
+                if (ObjectUtil.equals(activity.getProductRestriction(), 1)) {
+                    List<LotteryActivityRulesProductVO> lotteryActivityRulesProducts = activity.getLotteryActivityRulesProducts();
+                    if (CollUtil.isEmpty(lotteryActivityRulesProducts)) {
+                        continue;
+                    }
+
+                    requestedLotteryNum = activity.getProductLotteryNum();
+
+                    if (!ObjectUtil.isNull(requestedLotteryNum)) {
+                        for (LotteryActivityRulesProductVO activityRulesProduct : lotteryActivityRulesProducts) {
+                            if (ObjectUtil.equals(activityRulesProduct.getProductId(), productId)) {
+                                conditionMet = true;
+                                break;
+                            }
+                        }
+                    }
+
+                } else if (ObjectUtil.equals(activity.getProductRestriction(), 2)) {
+
+                    // 用户消费满几元
+                    Integer consumeAmount = activity.getConsumeAmount();
+                    // 消费奖励 消费满几元的几次抽奖机会
+                    requestedLotteryNum = activity.getConsumeAmountLottery();
+
+                    if (!ObjectUtil.isNull(consumeAmount) && !ObjectUtil.isNull(requestedLotteryNum) && !ObjectUtil.isNull(orderInfo.getPayAmount())) {
+                        BigDecimal thresholdAmount = new BigDecimal(consumeAmount);
+                        BigDecimal totalMoney = orderInfo.getPayAmount();
+                        conditionMet = totalMoney.compareTo(thresholdAmount) >= 0;
+                    }
+                }
+
+                // 如果条件满足,则进行次数检查并发放
+                if (conditionMet && !ObjectUtil.isNull(requestedLotteryNum)) {
+                    // --- 修复: 使用 getOrDefault 避免 NPE ---
+                    Integer userLotteryCount = activityGrantedCountMap.getOrDefault(activityId, 0);
+
+                    // --- 新增: 计算实际可发放的数量 ---
+                    int actualLotteryNumToGrant = calculateActualGrant(hasLimit, userLotteryCount, requestedLotteryNum, productUserMaxNum);
+
+                    if (actualLotteryNumToGrant > 0) {
+                        saveLotteryLog(user, activityId, actualLotteryNumToGrant, 3);
+                    }
+                }
+            }
+
+        }
+    }
+
+    private void syncLotteryCountIfNeeded(TWxUser user) {
+        if (ObjectUtil.equals(user.getIsBind(), 1) && com.ylx.common.utils.StringUtils.isNotBlank(user.getLocalLiveUserId())) {
+
+            UnifiedUserCenterDTO dto = new UnifiedUserCenterDTO();
+            dto.setTargetUserId(user.getLocalLiveUserId());
+            dto.setSourceUserId(user.getId());
+
+            // 👇 事务提交后再执行异步
+            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                @Override
+                public void afterCommit() {
+                    // 事务提交完成!现在调用异步,一定能查到数据
+                    unifiedUserCenterService.syncLotteryCount(dto);
+                }
+            });
+
+        }
+    }
+
+    /**
+     * 统一保存抽奖次数日志
+     */
+    private void saveLotteryLog(TWxUser user, String activityId, Integer lotteryNum, Integer activityType) {
+        LotteryCountLog log = new LotteryCountLog();
+        log.setOpenId(user.getcOpenid());
+        log.setUserId(user.getId());
+        log.setUserPhone(user.getcPhone());
+        log.setActivityType(activityType);
+        log.setLocalActivityTableId(activityId);
+        log.setLotteryNum(lotteryNum);
+        log.setReceiveTime(new Date());
+        log.setIsDelete("0");
+        log.setStatus(0); // 未同步
+        log.setIsLottery(0);
+        log.setType(2);
+        lotteryCountLogService.save(log);
+    }
+
+    /**
+     * 根据已有次数、请求发放次数和最大次数限制,计算实际可发放的次数
+     *
+     * @param hasLimit       是否有限制
+     * @param currentCount   当前已获得次数
+     * @param requestedCount 请求发放次数
+     * @param maxCount       最大次数限制
+     * @return 实际可发放次数
+     */
+    private int calculateActualGrant(boolean hasLimit, Integer currentCount, Integer requestedCount, Integer maxCount) {
+        if (!hasLimit) {
+            // 如果没有限制,直接返回请求的次数
+            return requestedCount;
+        }
+
+        // 如果有限制
+        int current = currentCount != null ? currentCount : 0;
+        int max = maxCount != null ? maxCount : 0;
+        int requested = requestedCount != null ? requestedCount : 0;
+
+        // 计算还能发放多少次
+        int remainingQuota = max - current;
+
+        // 实际发放次数为:请求次数 和 剩余配额 的较小值
+        return Math.max(0, Math.min(requested, remainingQuota));
+    }
+
 }

+ 233 - 51
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/TOrderServiceImpl.java

@@ -1,6 +1,8 @@
 package com.ylx.massage.service.impl;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
@@ -14,20 +16,30 @@ import com.ylx.common.constant.MassageConstants;
 import com.ylx.common.core.domain.R;
 import com.ylx.common.exception.ServiceException;
 import com.ylx.common.utils.SecurityUtils;
+import com.ylx.lottery.domain.LotteryCountLog;
+import com.ylx.lottery.domain.vo.LocalActivityTableVO;
+import com.ylx.lottery.domain.vo.LotteryActivityRulesProductVO;
+import com.ylx.lottery.domain.vo.LotteryActivityVO;
+import com.ylx.lottery.domain.vo.LotteryStatVO;
+import com.ylx.lottery.service.LotteryCountLogService;
+import com.ylx.lottery.service.LotteryCountService;
 import com.ylx.massage.domain.*;
 import com.ylx.massage.domain.vo.*;
 import com.ylx.massage.enums.BillTypeEnum;
-import com.ylx.massage.enums.DiscountTypeEnum;
 import com.ylx.massage.enums.JsStatusEnum;
 import com.ylx.massage.enums.OrderStatusEnum;
 import com.ylx.massage.mapper.TOrderMapper;
 import com.ylx.massage.service.*;
 import com.ylx.massage.utils.*;
 import com.ylx.point.service.IPointUserActivityTaskCompletionService;
+import com.ylx.usercenter.domain.dto.UnifiedUserCenterDTO;
+import com.ylx.usercenter.service.UnifiedUserCenterService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.compress.utils.Lists;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
 
 import javax.annotation.Resource;
 import java.math.BigDecimal;
@@ -106,6 +118,12 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
 
     @Resource
     private CancelOrderApplicationService cancelOrderApplicationService;
+    @Resource
+    private UnifiedUserCenterService unifiedUserCenterService;
+    @Resource
+    private LotteryCountService lotteryCountService;
+    @Resource
+    private LotteryCountLogService lotteryCountLogService;
 
     /**
      * 判断是否免车费
@@ -163,12 +181,12 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         }
         Integer techType = js.getTechType();
         // 虚拟技师
-        if(techType.equals(1)){
+        if (techType.equals(1)) {
             //虚拟技师订单
             order.setVirtualOrderFlag(1);
             //虚拟技师订单未分配
             order.setVirtualOrderAllocation(1);
-        }else{
+        } else {
             //真实订单
             order.setVirtualOrderFlag(0);
             order.setVirtualOrderAllocation(0);
@@ -435,9 +453,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
             }
             oldTechnicianStatusAfter = JsStatusEnum.JS_SERVICEABLE.getCode();
 
-            log.info("更新技师状态完成 - 新技师:{} {}→{}, 原技师:{} {}→{}",
-                newTechnicianName, getStatusName(newTechnicianStatusBefore), getStatusName(newTechnicianStatusAfter),
-                oldTechnicianName, getStatusName(oldTechnicianStatusBefore), getStatusName(oldTechnicianStatusAfter));
+            log.info("更新技师状态完成 - 新技师:{} {}→{}, 原技师:{} {}→{}", newTechnicianName, getStatusName(newTechnicianStatusBefore), getStatusName(newTechnicianStatusAfter), oldTechnicianName, getStatusName(oldTechnicianStatusBefore), getStatusName(oldTechnicianStatusAfter));
 
             // ========== 第8步:获取操作人信息 ==========
             operatorId = SecurityUtils.getUserId() != null ? SecurityUtils.getUserId().toString() : "ADMIN";
@@ -463,8 +479,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
             try {
                 // 只有在获取到基本信息后才记录日志
                 if (orderId != null && orderNo != null && oldTechnicianId != null && newTechnicianId != null) {
-                    allocationLogService.recordTransferOrder(
-                            orderId,                     // orderId
+                    allocationLogService.recordTransferOrder(orderId,                     // orderId
                             orderNo,                     // orderNo
                             oldTechnicianId,             // oldTechnicianId
                             oldTechnicianName,           // oldTechnicianName
@@ -639,7 +654,6 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
     }
 
 
-
     /**
      * 新订单通知
      *
@@ -697,7 +711,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         if (orderNew.getPayType().equals(MassageConstants.INTEGER_TWO)) {
             tConsumptionLog.setBillType(BillTypeEnum.BALANCE_PAYMENT.getCode());
             tConsumptionLog.setNote("余额支付");
-        } else if(orderNew.getPayType().equals(MassageConstants.INTEGER_ONE)){
+        } else if (orderNew.getPayType().equals(MassageConstants.INTEGER_ONE)) {
             tConsumptionLog.setBillType(BillTypeEnum.WX_PAY.getCode());
             tConsumptionLog.setNote("微信支付");
         } else {
@@ -736,6 +750,12 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         //电话通知
         TJs js = jsService.getById(orderNew.getcJsId());
         Sendvoice.sendPhone(js.getcPhone());
+
+        // 任务奖励 或者 消费奖励 —— 发放抽奖次数日志
+        grantLotteryCountForPay(user, orderParam);
+
+        // 已绑定一账通 → 异步同步抽奖次数
+        syncLotteryCountIfNeeded(user);
     }
 
     /**
@@ -833,9 +853,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         jsParam.setId(orderNew.getcJsId());
         jsParam.setnStatus(JsStatusEnum.JS_SERVICEABLE.getCode());
         //判断热度标识
-        List<TOrder> list = list(new LambdaQueryWrapper<TOrder>().eq(TOrder::getcJsId, orderNew.getcJsId())
-                .ge(TOrder::getDtCreateTime, DateTimeUtils.addDays(new Date(), -3))
-                .ge(TOrder::getnStatus, OrderStatusEnum.WAIT_EVALUATE.getCode()));
+        List<TOrder> list = list(new LambdaQueryWrapper<TOrder>().eq(TOrder::getcJsId, orderNew.getcJsId()).ge(TOrder::getDtCreateTime, DateTimeUtils.addDays(new Date(), -3)).ge(TOrder::getnStatus, OrderStatusEnum.WAIT_EVALUATE.getCode()));
         if (list.size() >= 2) {
             // 设置热度标识:1
             jsParam.setnB3(MassageConstants.INTEGER_ONE);
@@ -888,20 +906,20 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
 
         // 添加订单完成消息通知(用户侧)
         orderNotificationService.sendCompletedNotification(orderNew);
-        
+
         // 完成积分任务(按照新手活动、每日活动、每月活动的优先级顺序)
         try {
             this.pointUserActivityTaskCompletionService.completeOrderTaskByPriority(orderNew.getcOpenId());
         } catch (Exception e) {
             log.error("完成积分任务失败 - 订单号:{}, 错误信息:{}", orderNew.getOrderNo(), e.getMessage(), e);
         }
-        
+
         return true;
     }
 
     private void extracted(TOrder orderNew, TWxUser jsUp) {
         log.info("TOrderServiceImpl->extracted->jsUp,{}", JSONUtil.toJsonStr(jsUp));
-        log.info("TOrderServiceImpl->extracted->orderNew,{}",JSONUtil.toJsonStr(orderNew));
+        log.info("TOrderServiceImpl->extracted->orderNew,{}", JSONUtil.toJsonStr(orderNew));
         BigDecimal up = orderNew.getdTotalMoney().multiply(new BigDecimal("10"));
         up = up.divide(new BigDecimal(100), 2, RoundingMode.HALF_UP);
         // 更新余额
@@ -925,7 +943,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
      * 获取技师当天可预约时间
      *
      * @param technicianId 技师ID
-     * @param dateStr 查询日期(格式:yyyy-MM-dd),为null则查询当天
+     * @param dateStr      查询日期(格式:yyyy-MM-dd),为null则查询当天
      * @return TechnicianAvailabilityVo 技师当天可预约时间VO
      */
     @Override
@@ -957,14 +975,8 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         List<TimeSlotVo> timeSlots = new ArrayList<>();
         for (int hour = 0; hour < 24; hour++) {
             // 每小时生成两个时间段:xx:00 和 xx:30
-            timeSlots.add(TimeSlotVo.builder()
-                    .time(String.format("%02d:00", hour))
-                    .available(true)
-                    .build());
-            timeSlots.add(TimeSlotVo.builder()
-                    .time(String.format("%02d:30", hour))
-                    .available(true)
-                    .build());
+            timeSlots.add(TimeSlotVo.builder().time(String.format("%02d:00", hour)).available(true).build());
+            timeSlots.add(TimeSlotVo.builder().time(String.format("%02d:30", hour)).available(true).build());
         }
 
         // 5. 查询技师当天所有进行中的订单
@@ -975,15 +987,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         log.info("开始时间:{},结束时间:{}", startOfDay, endOfDay);
 
         LambdaQueryWrapper<TOrder> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.eq(TOrder::getcJsId, technicianId)
-                .in(TOrder::getnStatus, OrderStatusEnum.WAIT_JD.getCode(),
-                        OrderStatusEnum.RECEIVED_ORDER.getCode(),
-                        OrderStatusEnum.DEPART.getCode(),
-                        OrderStatusEnum.ARRIVED.getCode(),
-                        OrderStatusEnum.SERVICE.getCode())
-                .ge(TOrder::getDtCreateTime, startOfDay)
-                .lt(TOrder::getDtCreateTime, endOfDay)
-                .eq(TOrder::getIsDelete, 0);
+        queryWrapper.eq(TOrder::getcJsId, technicianId).in(TOrder::getnStatus, OrderStatusEnum.WAIT_JD.getCode(), OrderStatusEnum.RECEIVED_ORDER.getCode(), OrderStatusEnum.DEPART.getCode(), OrderStatusEnum.ARRIVED.getCode(), OrderStatusEnum.SERVICE.getCode()).ge(TOrder::getDtCreateTime, startOfDay).lt(TOrder::getDtCreateTime, endOfDay).eq(TOrder::getIsDelete, 0);
 
         List<TOrder> orders = this.list(queryWrapper);
         log.info("技师{},在{}天共有 {} 个进行中的订单", technicianId, queryDate, orders.size());
@@ -1018,21 +1022,16 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         }
         // 查询日期是未来的日期,所有时间段默认可预约,无需处理
         // 8. 构建返回结果
-        return TechnicianAvailabilityVo.builder()
-                .date(queryDate.toString())
-                .technicianId(technicianId)
-                .technicianName(js.getcName())
-                .timeSlots(timeSlots)
-                .build();
+        return TechnicianAvailabilityVo.builder().date(queryDate.toString()).technicianId(technicianId).technicianName(js.getcName()).timeSlots(timeSlots).build();
     }
 
     /**
      * 标记指定时间范围内的时间段为不可预约
      *
      * @param timeSlots 时间段列表
-     * @param start 开始时间
-     * @param end 结束时间
-     * @param orderNo 订单号
+     * @param start     开始时间
+     * @param end       结束时间
+     * @param orderNo   订单号
      */
     private void markTimeSlotsUnavailable(List<TimeSlotVo> timeSlots, LocalDateTime start, LocalDateTime end, String orderNo) {
         LocalTime startTime = start.toLocalTime();
@@ -1055,7 +1054,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
      * 标记所有时间段为不可预约
      *
      * @param timeSlots 时间段列表
-     * @param reason 不可预约原因
+     * @param reason    不可预约原因
      */
     private void markAllTimeSlotsUnavailable(List<TimeSlotVo> timeSlots, String reason) {
         for (TimeSlotVo slot : timeSlots) {
@@ -1069,7 +1068,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
      * 标记过去的时间段为不可预约
      *
      * @param timeSlots 时间段列表
-     * @param now 当前时间
+     * @param now       当前时间
      */
     private void markPastTimeSlotsUnavailable(List<TimeSlotVo> timeSlots, LocalDateTime now) {
         LocalTime currentTime = now.toLocalTime();
@@ -1291,6 +1290,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
 
     /**
      * 更新技师状态
+     *
      * @param orderNew
      */
     private void updateJs(TOrder orderNew) {
@@ -1317,7 +1317,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
 
     /**
      * 申请取消订单(退单申请)
-     *
+     * <p>
      * 业务流程:
      * 1. 校验订单状态(仅进行中的订单可申请退单)
      * 2. 创建退单申请记录
@@ -1366,7 +1366,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
     /**
      * 取消退单申请
      * 用户主动取消退单申请,恢复订单状态
-     *
+     * <p>
      * 业务流程:
      * 1. 参数校验(订单ID不能为空)
      * 2. 查询订单和退单申请记录
@@ -1441,11 +1441,193 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         // 统计用户在指定时间之前完成的订单数量
         // 完成状态包括:4-待评价(已完成)和5-已完成(已评价)
         LambdaQueryWrapper<TOrder> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.eq(TOrder::getcOpenId, openId)
-                .in(TOrder::getnStatus, OrderStatusEnum.WAIT_EVALUATE.getCode(), OrderStatusEnum.COMPLETE.getCode())
-                .lt(TOrder::getEndTime, queryTime);
-        
+        queryWrapper.eq(TOrder::getcOpenId, openId).in(TOrder::getnStatus, OrderStatusEnum.WAIT_EVALUATE.getCode(), OrderStatusEnum.COMPLETE.getCode()).lt(TOrder::getEndTime, queryTime);
+
         return Math.toIntExact(this.count(queryWrapper));
     }
 
+    private void grantLotteryCountForPay(TWxUser user, TOrder orderParam) {
+
+        // 获取按摩订单中的商品ID
+        JSONArray array = orderParam.getcGoods();
+        JSONObject obj = array.getJSONObject(0);
+        String cId = obj.getString("cId");
+
+        List<LotteryActivityVO> activityList = lotteryCountService.queryActivityRules();
+        if (CollUtil.isEmpty(activityList)) {
+            return;
+        }
+
+        // 查询抽奖次数统计
+        LambdaQueryWrapper<LotteryCountLog> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(LotteryCountLog::getUserId, user.getId());
+        queryWrapper.eq(LotteryCountLog::getIsDelete, 0);
+        List<LotteryStatVO> lotteryStatVOS = lotteryCountLogService.selectSumByGroup(queryWrapper);
+
+        // 生成活动ID -> 已获次数 的 Map
+        Map<String, Integer> activityGrantedCountMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(lotteryStatVOS)) {
+            for (LotteryStatVO statVO : lotteryStatVOS) {
+                activityGrantedCountMap.put(statVO.getLocalActivityTableId(), statVO.getTotalNum());
+            }
+        }
+
+        for (LotteryActivityVO activity : activityList) {
+
+            String activityId = activity.getId();
+
+            // 用户获取的最大抽奖次数
+            Integer productUserMaxNum = activity.getProductUserMaxNum();
+
+            // 检查 productUserMaxNum 是否为 null,如果是,则认为无限制
+            boolean hasLimit = !ObjectUtil.isNull(productUserMaxNum);
+
+            if (ObjectUtil.equals(activity.getParticipationRules(), 3)) { // 任务奖励类型
+
+                List<LocalActivityTableVO> localTables = activity.getLocalActivityTables();
+                if (CollUtil.isEmpty(localTables)) {
+                    continue;
+                }
+
+                // 类型=2 → 支付有礼
+                for (LocalActivityTableVO table : localTables) {
+                    if (ObjectUtil.equals(table.getType(), 2) && ObjectUtil.equals(table.getIsProduct(), 1)) {
+                        if (ObjectUtil.equals(table.getProductId(), cId)) {
+
+                            Integer userLotteryCount = activityGrantedCountMap.getOrDefault(activityId, 0);
+
+                            // 本次请求发放的数量
+                            Integer requestedLotteryNum = 1;
+
+                            // 计算实际可发放的数量
+                            int actualLotteryNumToGrant = calculateActualGrant(hasLimit, userLotteryCount, requestedLotteryNum, productUserMaxNum);
+
+                            if (actualLotteryNumToGrant > 0) {
+                                // 发放计算后的数量
+                                saveLotteryLog(user, activityId, actualLotteryNumToGrant, 2);
+                            }
+                            break;
+                        }
+                    }
+                }
+            } else if (ObjectUtil.equals(activity.getParticipationRules(), 4)) { // 消费奖励类型
+                if (ObjectUtil.isNull(activity.getProductRestriction())) {
+                    continue;
+                }
+
+                Integer requestedLotteryNum = null;
+                boolean conditionMet = false;
+
+                if (ObjectUtil.equals(activity.getProductRestriction(), 1)) {
+                    List<LotteryActivityRulesProductVO> lotteryActivityRulesProducts = activity.getLotteryActivityRulesProducts();
+                    if (CollUtil.isEmpty(lotteryActivityRulesProducts)) {
+                        continue;
+                    }
+
+                    requestedLotteryNum = activity.getProductLotteryNum();
+
+                    if (!ObjectUtil.isNull(requestedLotteryNum)) {
+                        for (LotteryActivityRulesProductVO activityRulesProduct : lotteryActivityRulesProducts) {
+                            if (ObjectUtil.equals(activityRulesProduct.getProductId(), cId)) {
+                                conditionMet = true;
+                                break;
+                            }
+                        }
+                    }
+
+                } else if (ObjectUtil.equals(activity.getProductRestriction(), 2)) {
+
+                    // 用户消费满几元
+                    Integer consumeAmount = activity.getConsumeAmount();
+                    // 消费奖励 消费满几元的几次抽奖机会
+                    requestedLotteryNum = activity.getConsumeAmountLottery();
+
+                    if (!ObjectUtil.isNull(consumeAmount) && !ObjectUtil.isNull(requestedLotteryNum) && !ObjectUtil.isNull(orderParam.getdTotalMoney())) {
+                        BigDecimal thresholdAmount = new BigDecimal(consumeAmount);
+                        BigDecimal totalMoney = orderParam.getdTotalMoney();
+                        conditionMet = totalMoney.compareTo(thresholdAmount) >= 0;
+                    }
+                }
+
+                // 如果条件满足,则进行次数检查并发放
+                if (conditionMet && !ObjectUtil.isNull(requestedLotteryNum)) {
+                    // --- 修复: 使用 getOrDefault 避免 NPE ---
+                    Integer userLotteryCount = activityGrantedCountMap.getOrDefault(activityId, 0);
+
+                    // --- 新增: 计算实际可发放的数量 ---
+                    int actualLotteryNumToGrant = calculateActualGrant(hasLimit, userLotteryCount, requestedLotteryNum, productUserMaxNum);
+
+                    if (actualLotteryNumToGrant > 0) {
+                        saveLotteryLog(user, activityId, actualLotteryNumToGrant, 3);
+                    }
+                }
+            }
+
+        }
+    }
+
+    private void syncLotteryCountIfNeeded(TWxUser user) {
+        if (ObjectUtil.equals(user.getIsBind(), 1) && com.ylx.common.utils.StringUtils.isNotBlank(user.getLocalLiveUserId())) {
+
+            UnifiedUserCenterDTO dto = new UnifiedUserCenterDTO();
+            dto.setTargetUserId(user.getLocalLiveUserId());
+            dto.setSourceUserId(user.getId());
+
+            // 👇 事务提交后再执行异步
+            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                @Override
+                public void afterCommit() {
+                    // 事务提交完成!现在调用异步,一定能查到数据
+                    unifiedUserCenterService.syncLotteryCount(dto);
+                }
+            });
+
+        }
+    }
+
+    /**
+     * 统一保存抽奖次数日志
+     */
+    private void saveLotteryLog(TWxUser user, String activityId, Integer lotteryNum, Integer activityType) {
+        LotteryCountLog log = new LotteryCountLog();
+        log.setOpenId(user.getcOpenid());
+        log.setUserId(user.getId());
+        log.setUserPhone(user.getcPhone());
+        log.setActivityType(activityType);
+        log.setLocalActivityTableId(activityId);
+        log.setLotteryNum(lotteryNum);
+        log.setReceiveTime(new Date());
+        log.setIsDelete("0");
+        log.setStatus(0); // 未同步
+        log.setIsLottery(0);
+        log.setType(1);
+        lotteryCountLogService.save(log);
+    }
+
+    /**
+     * 根据已有次数、请求发放次数和最大次数限制,计算实际可发放的次数
+     * @param hasLimit 是否有限制
+     * @param currentCount 当前已获得次数
+     * @param requestedCount 请求发放次数
+     * @param maxCount 最大次数限制
+     * @return 实际可发放次数
+     */
+    private int calculateActualGrant(boolean hasLimit, Integer currentCount, Integer requestedCount, Integer maxCount) {
+        if (!hasLimit) {
+            // 如果没有限制,直接返回请求的次数
+            return requestedCount;
+        }
+
+        // 如果有限制
+        int current = currentCount != null ? currentCount : 0;
+        int max = maxCount != null ? maxCount : 0;
+        int requested = requestedCount != null ? requestedCount : 0;
+
+        // 计算还能发放多少次
+        int remainingQuota = max - current;
+
+        // 实际发放次数为:请求次数 和 剩余配额 的较小值
+        return Math.max(0, Math.min(requested, remainingQuota));
+    }
+
 }

+ 1 - 0
nightFragrance-massage/src/main/resources/mapper/massage/ProductOrderItemMapper.xml

@@ -8,6 +8,7 @@
             poi.sku_image,
             ps.spec_combo AS specName,
             poi.product_name,
+            poi.product_id,
             ps.price_money AS payAmount,
             ps.price_point AS pointsUsed,
             ps.origin_price,