Przeglądaj źródła

feat(coupon): implement coupon verification and management features

- Add CouponManageController with endpoints for order verification and coupon redemption
- Create CouponManageService interface and implementation for handling coupon logic
- Implement order pre-validation and coupon verification workflows
- Add validation for coupon validity periods, unavailable dates, and usage times
- Include single-use quantity checks and group-buy coupon validations
- Implement distributed locking mechanism to prevent duplicate redemptions
- Add income calculation and store balance update logic
- Integrate with existing entities and mappers for coupon and order data
- Handle transactional operations for coupon status updates and financial records
- Add logging and error handling throughout the coupon management process
wxd 3 tygodni temu
rodzic
commit
d0920a5433

+ 64 - 0
alien-store-platform/src/main/java/shop/alien/storeplatform/controller/CouponManageController.java

@@ -0,0 +1,64 @@
+package shop.alien.storeplatform.controller;
+
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.storeplatform.service.CouponManageService;
+
+import java.util.Map;
+
+/**
+ * web端商户优惠券管理 前端控制器
+ *
+ * @author ssk
+ * @since 2025-11-13
+ */
+@Slf4j
+@Api(tags = {"web端商户优惠券管理"})
+@ApiSort(7)
+@CrossOrigin
+@RestController
+@RequestMapping("/couponManage")
+@RequiredArgsConstructor
+public class CouponManageController {
+
+    private final CouponManageService couponManageService;
+
+    @ApiOperation("核销订单前效验")
+    @ApiOperationSupport(order = 1)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "orderCode", value = "劵code", dataType = "String", paramType = "query", required = true)
+    })
+    @GetMapping("/verifyOrder")
+    public R<String> verifyOrder(@RequestParam("orderCode") String orderCode) {
+        log.info("CouponManageController.verifyOrder?orderCode={}", orderCode);
+        try {
+            return couponManageService.verifyOrder(orderCode);
+        } catch (Exception e) {
+            log.error("CouponManageController.verifyOrder ERROR: {}", e.getMessage(), e);
+            return R.fail("效验失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperation("核销订单")
+    @ApiOperationSupport(order = 2)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "quanCode", value = "券码", dataType = "String", paramType = "query", required = true)
+    })
+    @GetMapping("/verifyCoupon")
+    public R<Map<String, String>> verifyCoupon(@RequestParam("storeId") String storeId,
+                                                @RequestParam("quanCode") String quanCode) {
+        log.info("CouponManageController.verifyCoupon?storeId={}, quanCode={}", storeId, quanCode);
+        try {
+            Map<String, String> result = couponManageService.verifyCoupon(storeId, quanCode);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("CouponManageController.verifyCoupon ERROR: {}", e.getMessage(), e);
+            return R.fail("核销失败:" + e.getMessage());
+        }
+    }
+}
+

+ 32 - 0
alien-store-platform/src/main/java/shop/alien/storeplatform/service/CouponManageService.java

@@ -0,0 +1,32 @@
+package shop.alien.storeplatform.service;
+
+import shop.alien.entity.result.R;
+
+import java.util.Map;
+
+/**
+ * web端商户优惠券管理 Service接口
+ *
+ * @author ssk
+ * @since 2025-11-13
+ */
+public interface CouponManageService {
+
+    /**
+     * 核销订单前效验
+     *
+     * @param orderCode 劵code
+     * @return 效验结果
+     */
+    R<String> verifyOrder(String orderCode);
+
+    /**
+     * 核销订单
+     *
+     * @param storeId  门店ID
+     * @param quanCode 券码
+     * @return 核销结果
+     */
+    Map<String, String> verifyCoupon(String storeId, String quanCode);
+}
+

+ 718 - 0
alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/CouponManageServiceImpl.java

@@ -0,0 +1,718 @@
+package shop.alien.storeplatform.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.config.redis.BaseRedisService;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.*;
+import shop.alien.mapper.*;
+import shop.alien.storeplatform.service.CouponManageService;
+import shop.alien.util.common.constant.OrderStatusEnum;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.time.format.TextStyle;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * web端商户优惠券管理 Service实现
+ *
+ * @author ssk
+ * @since 2025-11-13
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CouponManageServiceImpl implements CouponManageService {
+
+    private final OrderCouponMiddleMapper orderCouponMiddleMapper;
+    private final LifeUserOrderMapper lifeUserOrderMapper;
+    private final LifeCouponMapper lifeCouponMapper;
+    private final LifeGroupBuyMainMapper lifeGroupBuyMainMapper;
+    private final EssentialHolidayComparisonMapper essentialHolidayComparisonMapper;
+    private final BaseRedisService baseRedisService;
+    private final LifeDiscountCouponMapper lifeDiscountCouponMapper;
+    private final StoreInfoMapper storeInfoMapper;
+    private final StoreIncomeDetailsRecordMapper storeIncomeDetailsRecordMapper;
+    private final StoreUserMapper storeUserMapper;
+
+    /**
+     * 核销订单前效验
+     *
+     * @param orderCode 劵code
+     * @return 效验结果
+     */
+    @Override
+    public R<String> verifyOrder(String orderCode) {
+        log.info("CouponManageServiceImpl.verifyOrder - 开始效验订单: orderCode={}", orderCode);
+
+        // 1. 查询订单券中间表
+        OrderCouponMiddle orderCouponMiddle = orderCouponMiddleMapper.selectOne(
+                new LambdaQueryWrapper<OrderCouponMiddle>()
+                        .eq(OrderCouponMiddle::getCouponCode, orderCode)
+        );
+
+        // 2. 检查订单券是否存在且状态为待使用
+        if (orderCouponMiddle != null && orderCouponMiddle.getStatus() == 1) {
+            // 3. 查询用户订单
+            LifeUserOrder lifeUserOrder = lifeUserOrderMapper.selectOne(
+                    new LambdaQueryWrapper<LifeUserOrder>()
+                            .eq(LifeUserOrder::getId, orderCouponMiddle.getOrderId())
+            );
+
+            if (lifeUserOrder == null) {
+                log.warn("CouponManageServiceImpl.verifyOrder - 订单不存在: orderId={}", orderCouponMiddle.getOrderId());
+                return R.fail("订单不存在");
+            }
+
+            // 4. 根据券类型进行不同的验证
+            // 类型为:1 代金券订单
+            if (lifeUserOrder.getCouponType() == 1) {
+                log.info("CouponManageServiceImpl.verifyOrder - 代金券验证: couponId={}", orderCouponMiddle.getCouponId());
+                return verifyCoupon(orderCouponMiddle);
+            }
+            // 类型为:2 团购订单
+            else if (lifeUserOrder.getCouponType() == 2) {
+                log.info("CouponManageServiceImpl.verifyOrder - 团购券验证: couponId={}", orderCouponMiddle.getCouponId());
+                return verifyGroupBuy(orderCouponMiddle);
+            }
+        }
+
+        log.warn("CouponManageServiceImpl.verifyOrder - 该劵不是待使用状态: orderCode={}", orderCode);
+        return R.fail("该劵不是待使用状态");
+    }
+
+    /**
+     * 代金券验证
+     *
+     * @param orderCouponMiddle 订单券中间表
+     * @return 验证结果
+     */
+    private R<String> verifyCoupon(OrderCouponMiddle orderCouponMiddle) {
+        log.info("CouponManageServiceImpl.verifyCoupon - 开始代金券验证: couponId={}", orderCouponMiddle.getCouponId());
+
+        // 1. 查询代金券信息
+        LifeCoupon lifeCoupon = lifeCouponMapper.selectOne(
+                new LambdaQueryWrapper<LifeCoupon>()
+                        .eq(LifeCoupon::getId, orderCouponMiddle.getCouponId())
+        );
+
+        if (lifeCoupon == null) {
+            return R.fail("代金券不存在");
+        }
+
+        // 2. 验证有效期
+        R<String> validityResult = validateCouponValidity(lifeCoupon, orderCouponMiddle);
+        if (!validityResult.isSuccess()) {
+            return validityResult;
+        }
+
+        // 3. 验证不可用日期
+        R<String> unavailableDateResult = validateCouponUnavailableDate(lifeCoupon);
+        if (!unavailableDateResult.isSuccess()) {
+            return unavailableDateResult;
+        }
+
+        // 4. 验证使用时间段
+        R<String> useTimeResult = validateCouponUseTime(lifeCoupon);
+        if (!useTimeResult.isSuccess()) {
+            return useTimeResult;
+        }
+
+        // 5. 验证单次核销数量
+        R<String> singleUseResult = validateCouponSingleUse(lifeCoupon, orderCouponMiddle);
+        if (!singleUseResult.isSuccess()) {
+            return singleUseResult;
+        }
+
+        log.info("CouponManageServiceImpl.verifyCoupon - 代金券验证通过: couponId={}", orderCouponMiddle.getCouponId());
+        return R.success("效验通过");
+    }
+
+    /**
+     * 团购券验证
+     *
+     * @param orderCouponMiddle 订单券中间表
+     * @return 验证结果
+     */
+    private R<String> verifyGroupBuy(OrderCouponMiddle orderCouponMiddle) {
+        log.info("CouponManageServiceImpl.verifyGroupBuy - 开始团购券验证: couponId={}", orderCouponMiddle.getCouponId());
+
+        // 1. 查询团购信息
+        LifeGroupBuyMain lifeGroupBuyMain = lifeGroupBuyMainMapper.selectOne(
+                new LambdaQueryWrapper<LifeGroupBuyMain>()
+                        .eq(LifeGroupBuyMain::getId, orderCouponMiddle.getCouponId())
+        );
+
+        if (lifeGroupBuyMain == null) {
+            return R.fail("团购券不存在");
+        }
+
+        // 2. 验证有效期
+        R<String> validityResult = validateGroupBuyValidity(lifeGroupBuyMain, orderCouponMiddle);
+        if (!validityResult.isSuccess()) {
+            return validityResult;
+        }
+
+        // 3. 验证不可用日期
+        R<String> unavailableDateResult = validateGroupBuyUnavailableDate(lifeGroupBuyMain);
+        if (!unavailableDateResult.isSuccess()) {
+            return unavailableDateResult;
+        }
+
+        log.info("CouponManageServiceImpl.verifyGroupBuy - 团购券验证通过: couponId={}", orderCouponMiddle.getCouponId());
+        return R.success("效验通过");
+    }
+
+    /**
+     * 验证代金券有效期
+     */
+    private R<String> validateCouponValidity(LifeCoupon lifeCoupon, OrderCouponMiddle orderCouponMiddle) {
+        // 类型为:1 指定天数
+        if ("1".equals(lifeCoupon.getExpirationType())) {
+            LifeUserOrder lifeUserOrder = lifeUserOrderMapper.selectOne(
+                    new LambdaQueryWrapper<LifeUserOrder>()
+                            .eq(LifeUserOrder::getId, orderCouponMiddle.getOrderId())
+            );
+
+            if (lifeUserOrder == null || lifeUserOrder.getPayTime() == null) {
+                return R.fail("订单支付时间异常");
+            }
+
+            LocalDate localDate = lifeUserOrder.getPayTime().toInstant()
+                    .atZone(ZoneId.systemDefault()).toLocalDate();
+            LocalDate validityPeriod = localDate.plusDays(lifeCoupon.getExpirationDate());
+            LocalDate nowDate = LocalDate.now();
+
+            if (nowDate.isAfter(validityPeriod)) {
+                log.warn("代金券已过期: couponId={}, validityPeriod={}, nowDate={}", 
+                        lifeCoupon.getId(), validityPeriod, nowDate);
+                return R.fail("该劵不在有效期内");
+            }
+        }
+        // 类型为:2 指定时间段
+        else if ("2".equals(lifeCoupon.getExpirationType())) {
+            if (StringUtils.isEmpty(lifeCoupon.getValidityPeriod())) {
+                return R.fail("有效期配置异常");
+            }
+
+            String[] strings = lifeCoupon.getValidityPeriod().split(",");
+            if (strings.length < 2) {
+                return R.fail("有效期格式异常");
+            }
+
+            long start = Long.parseLong(strings[0]);
+            long end = Long.parseLong(strings[1]);
+            LocalDate startDate = Instant.ofEpochMilli(start)
+                    .atZone(ZoneId.systemDefault()).toLocalDate();
+            LocalDate endDate = Instant.ofEpochMilli(end)
+                    .atZone(ZoneId.systemDefault()).toLocalDate();
+            LocalDate nowDate = LocalDate.now();
+
+            if (nowDate.isAfter(endDate) || nowDate.isBefore(startDate)) {
+                log.warn("代金券不在有效期内: couponId={}, startDate={}, endDate={}, nowDate={}", 
+                        lifeCoupon.getId(), startDate, endDate, nowDate);
+                return R.fail("该劵不在有效期内");
+            }
+        }
+
+        return R.success("有效期验证通过");
+    }
+
+    /**
+     * 验证代金券不可用日期
+     */
+    private R<String> validateCouponUnavailableDate(LifeCoupon lifeCoupon) {
+        // 限制日期: 1234567;节日id
+        if ("2".equals(lifeCoupon.getUnusedType())) {
+            if (StringUtils.isEmpty(lifeCoupon.getUnavaiLableDate())) {
+                return R.success("无不可用日期限制");
+            }
+
+            LocalDate nowDate = LocalDate.now();
+            DayOfWeek dayOfWeek = nowDate.getDayOfWeek();
+            String week = dayOfWeek.getDisplayName(TextStyle.FULL, Locale.CHINA);
+
+            // 验证不可用星期
+            String beforeSemicolon = lifeCoupon.getUnavaiLableDate().split(";")[0];
+            if (StringUtils.isNotEmpty(beforeSemicolon)) {
+                List<String> collectUnavailableDate = Arrays.stream(beforeSemicolon.split(","))
+                        .map(String::trim)
+                        .collect(Collectors.toList());
+                boolean isExist = collectUnavailableDate.stream().anyMatch(s -> s.equals(week));
+                if (isExist) {
+                    log.warn("代金券在不可用星期: couponId={}, week={}", lifeCoupon.getId(), week);
+                    return R.fail("该劵在不可用日期内");
+                }
+            }
+
+            // 验证不可用节日
+            String[] strings = lifeCoupon.getUnavaiLableDate().split(";");
+            if (strings.length > 1) {
+                String afterSemicolon = strings[1];
+                if (StringUtils.isNotEmpty(afterSemicolon)) {
+                    List<String> collectUnavailableDate = Arrays.stream(afterSemicolon.split(","))
+                            .map(String::trim)
+                            .collect(Collectors.toList());
+                    List<EssentialHolidayComparison> essentialHolidayComparisons = essentialHolidayComparisonMapper
+                            .selectList(new LambdaQueryWrapper<EssentialHolidayComparison>()
+                                    .in(EssentialHolidayComparison::getId, collectUnavailableDate));
+                    boolean isExist = essentialHolidayComparisons.stream()
+                            .anyMatch(s -> !nowDate.isBefore(s.getStartTime().toInstant()
+                                    .atZone(ZoneId.systemDefault()).toLocalDate())
+                                    && !nowDate.isAfter(s.getEndTime().toInstant()
+                                    .atZone(ZoneId.systemDefault()).toLocalDate()));
+                    if (isExist) {
+                        log.warn("代金券在不可用节日: couponId={}, nowDate={}", lifeCoupon.getId(), nowDate);
+                        return R.fail("该劵在不可用日期内");
+                    }
+                }
+            }
+        }
+        // 自定义不可用日期
+        else if ("3".equals(lifeCoupon.getUnusedType())) {
+            if (StringUtils.isNotEmpty(lifeCoupon.getUnavaiLableDate())) {
+                String[] customDate = lifeCoupon.getUnavaiLableDate().split(";");
+                boolean isExist = isCurrentDateInAnyRange(customDate);
+                if (isExist) {
+                    log.warn("代金券在自定义不可用日期: couponId={}", lifeCoupon.getId());
+                    return R.fail("该劵在不可用日期内");
+                }
+            }
+        }
+
+        return R.success("不可用日期验证通过");
+    }
+
+    /**
+     * 验证代金券使用时间段
+     */
+    private R<String> validateCouponUseTime(LifeCoupon lifeCoupon) {
+        if (StringUtils.isEmpty(lifeCoupon.getBuyUseStartTime()) 
+                || StringUtils.isEmpty(lifeCoupon.getBuyUseEndTime())) {
+            return R.success("无使用时间限制");
+        }
+
+        try {
+            Integer buyUseStartTime = Integer.parseInt(lifeCoupon.getBuyUseStartTime());
+            Integer buyUseEndTime = Integer.parseInt(lifeCoupon.getBuyUseEndTime());
+
+            // 获取当前小时
+            LocalTime now = LocalTime.now();
+            int currentHour = now.getHour();
+
+            // 验证输入的小时是否有效
+            if (buyUseStartTime < 0 || buyUseStartTime > 24 || buyUseEndTime < 0 || buyUseEndTime > 24) {
+                return R.fail("小时必须在0-23之间");
+            }
+
+            // 处理跨天的情况,例如22点到次日3点
+            if (buyUseStartTime >= buyUseEndTime) {
+                if (currentHour < buyUseStartTime && currentHour > buyUseEndTime) {
+                    log.warn("代金券不在使用时间段(跨天): couponId={}, currentHour={}, startTime={}, endTime={}", 
+                            lifeCoupon.getId(), currentHour, buyUseStartTime, buyUseEndTime);
+                    return R.fail("该劵不在有效期内");
+                }
+            } else {
+                if (!(currentHour >= buyUseStartTime && currentHour <= buyUseEndTime)) {
+                    log.warn("代金券不在使用时间段: couponId={}, currentHour={}, startTime={}, endTime={}", 
+                            lifeCoupon.getId(), currentHour, buyUseStartTime, buyUseEndTime);
+                    return R.fail("该劵不在有效期内");
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.error("使用时间格式异常: couponId={}, error={}", lifeCoupon.getId(), e.getMessage());
+            return R.fail("使用时间配置异常");
+        }
+
+        return R.success("使用时间验证通过");
+    }
+
+    /**
+     * 验证代金券单次核销数量
+     */
+    private R<String> validateCouponSingleUse(LifeCoupon lifeCoupon, OrderCouponMiddle orderCouponMiddle) {
+        if (StringUtils.isEmpty(lifeCoupon.getSingleCanUse()) || "0".equals(lifeCoupon.getSingleCanUse())) {
+            return R.success("无单次核销限制");
+        }
+
+        try {
+            // 获取当天开始时间(00:00:00)
+            LocalDateTime todayStart = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
+            // 获取当天结束时间(23:59:59.999)
+            LocalDateTime todayEnd = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
+
+            Integer orderNum = orderCouponMiddleMapper.selectCount(
+                    new LambdaQueryWrapper<OrderCouponMiddle>()
+                            .eq(OrderCouponMiddle::getOrderId, orderCouponMiddle.getOrderId())
+                            .between(OrderCouponMiddle::getUsedTime, todayStart, todayEnd)
+                            .eq(OrderCouponMiddle::getStatus, 2)
+            );
+
+            int maxSingleUse = Integer.parseInt(lifeCoupon.getSingleCanUse());
+            if (orderNum >= maxSingleUse) {
+                log.warn("超过今日单次核销数量: couponId={}, orderNum={}, maxSingleUse={}", 
+                        lifeCoupon.getId(), orderNum, maxSingleUse);
+                return R.fail("该订单已经超过今日单次核销数量");
+            }
+        } catch (NumberFormatException e) {
+            log.error("单次核销数量格式异常: couponId={}, error={}", lifeCoupon.getId(), e.getMessage());
+            return R.fail("单次核销配置异常");
+        }
+
+        return R.success("单次核销验证通过");
+    }
+
+    /**
+     * 验证团购券有效期
+     */
+    private R<String> validateGroupBuyValidity(LifeGroupBuyMain lifeGroupBuyMain, OrderCouponMiddle orderCouponMiddle) {
+        // 类型为:0 指定天数
+        if (lifeGroupBuyMain.getEffectiveDateType() == 0) {
+            LifeUserOrder lifeUserOrder = lifeUserOrderMapper.selectOne(
+                    new LambdaQueryWrapper<LifeUserOrder>()
+                            .eq(LifeUserOrder::getId, orderCouponMiddle.getOrderId())
+            );
+
+            if (lifeUserOrder == null || lifeUserOrder.getPayTime() == null) {
+                return R.fail("订单支付时间异常");
+            }
+
+            LocalDate localDate = lifeUserOrder.getPayTime().toInstant()
+                    .atZone(ZoneId.systemDefault()).toLocalDate();
+            LocalDate validityPeriod = localDate.plusDays(Long.parseLong(lifeGroupBuyMain.getEffectiveDateValue()));
+            LocalDate nowDate = LocalDate.now();
+
+            if (nowDate.isAfter(validityPeriod)) {
+                log.warn("团购券已过期: groupBuyId={}, validityPeriod={}, nowDate={}", 
+                        lifeGroupBuyMain.getId(), validityPeriod, nowDate);
+                return R.fail("该劵不在有效期内");
+            }
+        }
+        // 类型为:1 指定时间段
+        else if (lifeGroupBuyMain.getEffectiveDateType() == 1) {
+            if (StringUtils.isEmpty(lifeGroupBuyMain.getEffectiveDateValue())) {
+                return R.fail("有效期配置异常");
+            }
+
+            String[] strings = lifeGroupBuyMain.getEffectiveDateValue().split(",");
+            if (strings.length < 2) {
+                return R.fail("有效期格式异常");
+            }
+
+            String startDate = strings[0];
+            String endDate = strings[1];
+            LocalDate localStartDate = LocalDate.parse(startDate);
+            LocalDate localEndDate = LocalDate.parse(endDate);
+            LocalDate nowDate = LocalDate.now();
+
+            if (nowDate.isAfter(localEndDate) || nowDate.isBefore(localStartDate)) {
+                log.warn("团购券不在有效期内: groupBuyId={}, startDate={}, endDate={}, nowDate={}", 
+                        lifeGroupBuyMain.getId(), localStartDate, localEndDate, nowDate);
+                return R.fail("该劵不在有效期内");
+            }
+        }
+
+        return R.success("有效期验证通过");
+    }
+
+    /**
+     * 验证团购券不可用日期
+     */
+    private R<String> validateGroupBuyUnavailableDate(LifeGroupBuyMain lifeGroupBuyMain) {
+        // 限制日期: 1234567;节日id
+        if (lifeGroupBuyMain.getDisableDateType() == 1) {
+            if (StringUtils.isEmpty(lifeGroupBuyMain.getDisableDateValue())) {
+                return R.success("无不可用日期限制");
+            }
+
+            LocalDate nowDate = LocalDate.now();
+            DayOfWeek dayOfWeek = nowDate.getDayOfWeek();
+            String week = dayOfWeek.getDisplayName(TextStyle.FULL, Locale.CHINA);
+
+            // 验证不可用星期
+            String beforeSemicolon = lifeGroupBuyMain.getDisableDateValue().split(";")[0];
+            if (StringUtils.isNotEmpty(beforeSemicolon)) {
+                List<String> collectUnavailableDate = Arrays.stream(beforeSemicolon.split(","))
+                        .map(String::trim)
+                        .collect(Collectors.toList());
+                boolean isExist = collectUnavailableDate.stream().anyMatch(s -> s.equals(week));
+                if (isExist) {
+                    log.warn("团购券在不可用星期: groupBuyId={}, week={}", lifeGroupBuyMain.getId(), week);
+                    return R.fail("该劵在不可用日期内");
+                }
+            }
+
+            // 验证不可用节日
+            String[] strings = lifeGroupBuyMain.getDisableDateValue().split(";");
+            if (strings.length > 1) {
+                String afterSemicolon = strings[1];
+                if (StringUtils.isNotEmpty(afterSemicolon)) {
+                    List<String> collectUnavailableDate = Arrays.stream(afterSemicolon.split(","))
+                            .map(String::trim)
+                            .collect(Collectors.toList());
+                    List<EssentialHolidayComparison> essentialHolidayComparisons = essentialHolidayComparisonMapper
+                            .selectList(new LambdaQueryWrapper<EssentialHolidayComparison>()
+                                    .in(EssentialHolidayComparison::getId, collectUnavailableDate));
+                    boolean isExist = essentialHolidayComparisons.stream()
+                            .anyMatch(s -> !nowDate.isBefore(s.getStartTime().toInstant()
+                                    .atZone(ZoneId.systemDefault()).toLocalDate())
+                                    && !nowDate.isAfter(s.getEndTime().toInstant()
+                                    .atZone(ZoneId.systemDefault()).toLocalDate()));
+                    if (isExist) {
+                        log.warn("团购券在不可用节日: groupBuyId={}, nowDate={}", lifeGroupBuyMain.getId(), nowDate);
+                        return R.fail("该劵在不可用日期内");
+                    }
+                }
+            }
+        }
+        // 自定义不可用日期
+        else if (lifeGroupBuyMain.getDisableDateType() == 2) {
+            if (StringUtils.isNotEmpty(lifeGroupBuyMain.getDisableDateValue())) {
+                String[] customDate = lifeGroupBuyMain.getDisableDateValue().split(";");
+                boolean isExist = isCurrentDateInAnyRange(customDate);
+                if (isExist) {
+                    log.warn("团购券在自定义不可用日期: groupBuyId={}", lifeGroupBuyMain.getId());
+                    return R.fail("该劵在不可用日期内");
+                }
+            }
+        }
+
+        return R.success("不可用日期验证通过");
+    }
+
+        /**
+         * 判断当前日期是否在任意日期范围内
+         *
+         * @param dateRanges 日期范围数组
+         * @return true-在范围内,false-不在范围内
+         */
+        private static boolean isCurrentDateInAnyRange(String[] dateRanges) {
+            // 获取当前日期
+            LocalDate currentDate = LocalDate.now();
+            // 定义日期格式
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+        // 遍历数组中的每一对日期范围
+        for (String range : dateRanges) {
+            // 分割每一对日期(开始日期和结束日期)
+            String[] dates = range.split(",");
+            if (dates.length != 2) {
+                // 格式不正确,跳过这一对
+                continue;
+            }
+            try {
+                // 解析开始日期和结束日期
+                LocalDate startDate = LocalDate.parse(dates[0], formatter);
+                LocalDate endDate = LocalDate.parse(dates[1], formatter);
+
+                // 检查当前日期是否在范围内(包括开始和结束日期)
+                if (!currentDate.isBefore(startDate) && !currentDate.isAfter(endDate)) {
+                    return true;
+                }
+            } catch (Exception e) {
+                // 日期解析错误,跳过这一对
+                continue;
+            }
+        }
+        // 没有任何一对日期范围包含当前日期
+        return false;
+    }
+
+    /**
+     * 核销订单
+     *
+     * @param storeId  门店ID
+     * @param quanCode 券码
+     * @return 核销结果
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, String> verifyCoupon(String storeId, String quanCode) {
+        log.info("CouponManageServiceImpl.verifyCoupon - 开始核销订单: storeId={}, quanCode={}", storeId, quanCode);
+
+        Map<String, String> resultMap = new HashMap<>();
+        String lockKey = "coupon:use:" + quanCode;
+
+        // 1. Redis分布式锁,防止重复核销
+        String lockValue = baseRedisService.getString(lockKey);
+        if (StringUtils.isNotEmpty(lockValue)) {
+            log.warn("CouponManageServiceImpl.verifyCoupon - 请勿重复核销: quanCode={}", quanCode);
+            resultMap.put("code", "false");
+            resultMap.put("message", "请勿重复核销");
+            return resultMap;
+        }
+
+        // 设置锁
+        baseRedisService.setListRight(lockKey, quanCode);
+
+        try {
+            // 2. 查询订单券中间表(状态:待使用 或 退款失败)
+            OrderCouponMiddle orderCouponMiddle = orderCouponMiddleMapper.selectOne(
+                    new LambdaQueryWrapper<OrderCouponMiddle>()
+                            .eq(OrderCouponMiddle::getCouponCode, quanCode)
+                            .in(OrderCouponMiddle::getStatus,
+                                    OrderStatusEnum.WAIT_USE.getStatus(),
+                                    OrderStatusEnum.REFUND_FAILED.getStatus())
+            );
+
+            if (orderCouponMiddle == null) {
+                log.warn("CouponManageServiceImpl.verifyCoupon - 券不存在或状态不正确: quanCode={}", quanCode);
+                resultMap.put("code", "false");
+                resultMap.put("message", "核销失败");
+                return resultMap;
+            }
+
+            // 3. 更新订单券状态为已使用
+            orderCouponMiddle.setStatus(OrderStatusEnum.USED.getStatus());
+            orderCouponMiddle.setUsedTime(new Date());
+            orderCouponMiddleMapper.updateById(orderCouponMiddle);
+
+            // 4. 查询用户订单
+            LifeUserOrder lifeUserOrder = lifeUserOrderMapper.selectOne(
+                    new LambdaQueryWrapper<LifeUserOrder>()
+                            .eq(LifeUserOrder::getId, orderCouponMiddle.getOrderId())
+            );
+
+            if (lifeUserOrder == null) {
+                log.error("CouponManageServiceImpl.verifyCoupon - 订单不存在: orderId={}", orderCouponMiddle.getOrderId());
+                resultMap.put("code", "false");
+                resultMap.put("message", "订单不存在");
+                return resultMap;
+            }
+
+            // 5. 检查是否使用了平台优惠券
+            Boolean platFormCoupon = false;
+            String quanId = lifeUserOrder.getQuanId();
+            if (StringUtils.isNotEmpty(quanId)) {
+                LifeDiscountCoupon lifeDiscountCoupon = lifeDiscountCouponMapper.selectById(quanId);
+                if (ObjectUtils.isNotEmpty(lifeDiscountCoupon) && lifeDiscountCoupon.getType() == 3) {
+                    platFormCoupon = true;
+                    log.info("CouponManageServiceImpl.verifyCoupon - 使用了平台优惠券: quanId={}", quanId);
+                }
+            }
+
+            // 6. 查询该订单下的所有券
+            List<OrderCouponMiddle> couponMiddleList = orderCouponMiddleMapper.selectList(
+                    new LambdaQueryWrapper<OrderCouponMiddle>()
+                            .eq(OrderCouponMiddle::getOrderId, lifeUserOrder.getId())
+            );
+
+            // 7. 检查是否所有券都已核销
+            boolean allUsed = couponMiddleList.stream()
+                    .allMatch(str -> str.getStatus() == OrderStatusEnum.USED.getStatus());
+
+            // 筛选出未完成的券
+            List<OrderCouponMiddle> unfinishedList = couponMiddleList.stream()
+                    .filter(x -> x.getStatus() == OrderStatusEnum.WAIT_USE.getStatus()
+                            || x.getStatus() == OrderStatusEnum.REFUND_FAILED.getStatus())
+                    .collect(Collectors.toList());
+
+            // 8. 如果所有券都已核销,或者没有未完成的券,更新订单状态为已完成
+            if (allUsed || unfinishedList.size() == 0) {
+                lifeUserOrder.setStatus(OrderStatusEnum.COMPLETE.getStatus());
+                lifeUserOrder.setFinishTime(new Date());
+                lifeUserOrder.setCreatedTime(new Date());
+                lifeUserOrderMapper.updateById(lifeUserOrder);
+
+                log.info("CouponManageServiceImpl.verifyCoupon - 订单完成: orderId={}", lifeUserOrder.getId());
+                // 注:发放好友优惠券功能需要通过 Feign 调用 alien-store 服务实现
+                // 或在 web 端实现该功能
+            }
+
+            // 9. 查询门店信息(获取抽成比例)
+            StoreInfo storeInfo = storeInfoMapper.selectOne(
+                    new LambdaQueryWrapper<StoreInfo>()
+                            .eq(StoreInfo::getId, lifeUserOrder.getStoreId())
+            );
+
+            if (storeInfo == null) {
+                log.error("CouponManageServiceImpl.verifyCoupon - 门店不存在: storeId={}", lifeUserOrder.getStoreId());
+                resultMap.put("code", "false");
+                resultMap.put("message", "门店不存在");
+                return resultMap;
+            }
+
+            // 10. 计算抽成和收入
+            // 抽成比例转换为小数
+            BigDecimal commissionRate = new BigDecimal(storeInfo.getCommissionRate())
+                    .divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP);
+
+            // 计算抽成金额(单位:分)
+            BigDecimal commission = orderCouponMiddle.getPrice()
+                    .multiply(new BigDecimal("100"))
+                    .multiply(commissionRate)
+                    .setScale(2, RoundingMode.HALF_UP);
+
+            // 计算实际收入(单位:分)
+            BigDecimal money;
+            if (platFormCoupon) {
+                // 如果使用了平台优惠券,需要加上平均优惠券价格
+                money = orderCouponMiddle.getPrice()
+                        .add(orderCouponMiddle.getAvgDiscountCouponPrice())
+                        .multiply(new BigDecimal("100"))
+                        .subtract(commission);
+                log.info("CouponManageServiceImpl.verifyCoupon - 使用平台券计算: 原价={}, 优惠={}, 抽成={}, 收入={}",
+                        orderCouponMiddle.getPrice(), orderCouponMiddle.getAvgDiscountCouponPrice(),
+                        commission, money);
+            } else {
+                money = orderCouponMiddle.getPrice()
+                        .multiply(new BigDecimal("100"))
+                        .subtract(commission);
+                log.info("CouponManageServiceImpl.verifyCoupon - 普通计算: 原价={}, 抽成={}, 收入={}",
+                        orderCouponMiddle.getPrice(), commission, money);
+            }
+
+            // 11. 插入收入明细记录
+            StoreIncomeDetailsRecord record = new StoreIncomeDetailsRecord();
+            record.setStoreId(Integer.parseInt(storeId));
+            record.setUserOrderId(orderCouponMiddle.getId());
+            record.setIncomeType(lifeUserOrder.getCouponType());
+            record.setBusinessId(orderCouponMiddle.getCouponId());
+            record.setCommission(commission.intValue());
+            record.setMoney(money.intValue());
+            storeIncomeDetailsRecordMapper.insert(record);
+
+            log.info("CouponManageServiceImpl.verifyCoupon - 插入收入记录: storeId={}, money={}, commission={}",
+                    storeId, money.intValue(), commission.intValue());
+
+            // 12. 更新店铺账户余额
+            UpdateWrapper<StoreUser> updateWrapper = new UpdateWrapper<>();
+            updateWrapper.eq("store_id", storeId);
+            updateWrapper.eq("delete_flag", 0);
+            updateWrapper.setSql("money = money + " + money.intValue());
+            int updateCount = storeUserMapper.update(null, updateWrapper);
+
+            log.info("CouponManageServiceImpl.verifyCoupon - 更新店铺余额: storeId={}, 增加金额={}, 影响行数={}",
+                    storeId, money.intValue(), updateCount);
+
+            // 13. 返回成功结果
+            resultMap.put("code", "true");
+            resultMap.put("message", "核销成功");
+
+            log.info("CouponManageServiceImpl.verifyCoupon - 核销完成: quanCode={}", quanCode);
+            return resultMap;
+
+        } catch (Exception e) {
+            log.error("CouponManageServiceImpl.verifyCoupon - 核销异常: quanCode={}, error={}",
+                    quanCode, e.getMessage(), e);
+            resultMap.put("code", "false");
+            resultMap.put("message", "核销失败:" + e.getMessage());
+            return resultMap;
+        } finally {
+            // 14. 释放分布式锁
+            baseRedisService.delete(lockKey);
+            log.debug("CouponManageServiceImpl.verifyCoupon - 释放锁: lockKey={}", lockKey);
+        }
+    }
+}
+

+ 4 - 0
alien-store-platform/接口文档/21-检查支付密码接口.md

@@ -887,3 +887,7 @@ if (response.data.data.data === true) { ... }
 **最后更新**: 2025-11-17  
 **维护人员**: ssk
 
+
+
+
+

+ 4 - 0
alien-store-platform/接口文档/22-批量标记通知已读接口-DTO版.md

@@ -760,3 +760,7 @@ ADD INDEX idx_receiver_read_type (receiver_id, is_read, notice_type);
 **最后更新**: 2025-11-17  
 **维护人员**: ssk
 
+
+
+
+