浏览代码

Merge remote-tracking branch 'origin/dev' into dev

jinshihui 2 天之前
父节点
当前提交
335b40266d
共有 24 个文件被更改,包括 935 次插入83 次删除
  1. 27 0
      nightFragrance-admin/src/main/java/com/ylx/web/controller/massage/PayController.java
  2. 3 3
      nightFragrance-massage/src/main/java/com/ylx/giftCard/service/impl/GiftCardOrderServiceImpl.java
  3. 25 0
      nightFragrance-massage/src/main/java/com/ylx/massage/mapper/CouponReceiveMapper.java
  4. 11 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/CouponReceiveService.java
  5. 8 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/CouponService.java
  6. 26 4
      nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/CouponReceiveServiceImpl.java
  7. 109 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/CouponServiceImpl.java
  8. 23 3
      nightFragrance-massage/src/main/java/com/ylx/order/controller/OrderController.java
  9. 4 4
      nightFragrance-massage/src/main/java/com/ylx/order/domain/OrderStatusFlow.java
  10. 3 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/TOrder.java
  11. 50 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/OrderDateQueryDTO.java
  12. 21 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/OrderDeleteDTO.java
  13. 2 2
      nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/OrderSubmitDTO.java
  14. 29 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/OrderUpdateStatusDTO.java
  15. 80 0
      nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/OrderDateQueryVo.java
  16. 44 0
      nightFragrance-massage/src/main/java/com/ylx/order/enums/OrderStatusEnum.java
  17. 19 0
      nightFragrance-massage/src/main/java/com/ylx/order/enums/PaymentMethodEnum.java
  18. 2 2
      nightFragrance-massage/src/main/java/com/ylx/order/service/OrderStatusFlowService.java
  19. 25 1
      nightFragrance-massage/src/main/java/com/ylx/order/service/TOrderService.java
  20. 2 2
      nightFragrance-massage/src/main/java/com/ylx/order/service/impl/OrderStatusFlowServiceImpl.java
  21. 329 61
      nightFragrance-massage/src/main/java/com/ylx/order/service/impl/TOrderServiceImpl.java
  22. 0 1
      nightFragrance-massage/src/main/java/com/ylx/shopingfundsdetail/domain/vo/ShoppingFundsDetailAddDto.java
  23. 19 0
      nightFragrance-massage/src/main/java/com/ylx/shopingfundsdetail/enums/ShoppingFundsExpenseTypeEnum.java
  24. 74 0
      nightFragrance-massage/src/main/resources/mapper/massage/CouponReceiveMapper.xml

+ 27 - 0
nightFragrance-admin/src/main/java/com/ylx/web/controller/massage/PayController.java

@@ -34,6 +34,9 @@ import com.ylx.massage.enums.BillTypeEnum;
 import com.ylx.massage.service.RefundVoucherService;
 import com.ylx.massage.service.TRechargeService;
 import com.ylx.massage.service.TWxUserService;
+import com.ylx.order.domain.TOrder;
+import com.ylx.order.enums.OrderStatusEnum;
+import com.ylx.order.service.TOrderService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.slf4j.Slf4j;
@@ -75,6 +78,8 @@ public class PayController {
     private WxPayService wxPayService;
     @Resource
     private IGiftCardOrderService giftCardOrderService;
+    @Resource
+    private TOrderService orderService;
 
     /**
      * 小程序微信支付的第一步,统一下单
@@ -229,6 +234,28 @@ public class PayController {
                     this.giftCardOrderService.processGiftCardPayment(result, wxUser, cardOrder);
                 } else if (WxPayTypeEnum.EMOTION_GOODS.getCode().equals(attach)) {
                     log.info("检测到情感服务商品支付成功,订单号: {}", outTradeNo);
+
+                    // 3.1 更新订单支付状态
+                    LambdaQueryWrapper<TOrder> wrapper = new LambdaQueryWrapper<>();
+                    wrapper.eq(TOrder::getOrderNo, outTradeNo);
+                    TOrder order = this.orderService.getOne(wrapper);
+                    if (ObjectUtil.isNull(order)) {
+                        log.error("订单不存在,订单号: {}", outTradeNo);
+                        resp.put("code", "FAIL");
+                        resp.put("message", "订单不存在");
+                        return resp;
+                    }
+                    // 3.2 检查是否已处理
+                    if (ObjectUtil.equals(OrderStatusEnum.PENDING_DISPATCH.getCode(), order.getStatus())) {
+                        log.warn("订单已处理过:{}", outTradeNo);
+                        resp.put("code", "SUCCESS");
+                        resp.put("message", "OK");
+                        return resp;
+                    }
+
+                    // 3.3 处理订单相关数据
+                    this.orderService.processOrderPayment(result, wxUser, order);
+
                 }
             }
 

+ 3 - 3
nightFragrance-massage/src/main/java/com/ylx/giftCard/service/impl/GiftCardOrderServiceImpl.java

@@ -18,6 +18,7 @@ import com.ylx.massage.domain.TWxUser;
 import com.ylx.massage.service.TJsService;
 import com.ylx.massage.service.TWxUserService;
 import com.ylx.shopingfundsdetail.domain.vo.ShoppingFundsDetailAddDto;
+import com.ylx.shopingfundsdetail.enums.ShoppingFundsExpenseTypeEnum;
 import com.ylx.shopingfundsdetail.service.ShoppingFundsDetailService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -37,8 +38,6 @@ public class GiftCardOrderServiceImpl extends ServiceImpl<GiftCardOrderMapper, G
     private TWxUserService wxUserService;
     @Resource
     private ShoppingFundsDetailService shoppingFundsDetailService;
-    // 充值
-    public static final int EXPENSE_TYPE_RECHARGE = 0;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -107,6 +106,7 @@ public class GiftCardOrderServiceImpl extends ServiceImpl<GiftCardOrderMapper, G
         log.info("订单取消成功,订单号:{},购物卡ID:{}", order.getOrderNo(), order.getGiftCardId());
     }
 
+    @Override
     @Transactional(rollbackFor = Exception.class)
     public void processGiftCardPayment(WxPayOrderNotifyV3Result.DecryptNotifyResult result, TWxUser wxUser,GiftCardOrder cardOrder) {
 
@@ -133,7 +133,7 @@ public class GiftCardOrderServiceImpl extends ServiceImpl<GiftCardOrderMapper, G
         dto.setUserId(wxUser.getId());
         dto.setAmount(payAmount);
         dto.setOrderNo(result.getOutTradeNo());
-        dto.setExpenseType(EXPENSE_TYPE_RECHARGE);
+        dto.setExpenseType(ShoppingFundsExpenseTypeEnum.RECHARGE.getCode());
         dto.setBalance(newBalance);
         dto.setGiftCardId(cardOrder.getGiftCardId());
         shoppingFundsDetailService.addShoppingFundsDetail(dto);

+ 25 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/mapper/CouponReceiveMapper.java

@@ -1,11 +1,14 @@
 package com.ylx.massage.mapper;
 
+import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Map;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.ylx.massage.domain.vo.CouponReceiveVo;
 import org.apache.ibatis.annotations.Param;
 import com.ylx.massage.domain.CouponReceive;
+import org.apache.ibatis.annotations.Select;
 
 /**
  * 优惠券领取表(CouponReceive)表数据库访问层
@@ -34,5 +37,27 @@ public interface CouponReceiveMapper extends BaseMapper<CouponReceive> {
 
 
     List<CouponReceiveVo> getByOpenId(String openid);
+
+    Map<String, Object> selectCouponDetailForCalc(@Param("couponId") String couponId,
+                                                  @Param("openId") String openId);
+
+    // 乐观锁核销
+    int useCouponOptimisticLock(@Param("couponId") String couponId,
+                                @Param("openId") String openId,
+                                @Param("orderId") Long orderId,
+                                @Param("orderType") Integer orderType,
+                                @Param("useTime") LocalDateTime useTime);
+
+    // 使用数量 +1
+    int incrementUsedNum(@Param("couponId") String couponId);
+
+    // 乐观锁退还
+    int returnCouponOptimisticLock(@Param("couponId") String couponId,
+                                   @Param("openId") String openId,
+                                   @Param("orderId") Long orderId);
+
+    // 使用数量 -1
+    int decrementUsedNum(@Param("couponId") String couponId);
+
 }
 

+ 11 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/CouponReceiveService.java

@@ -8,7 +8,9 @@ import com.ylx.massage.domain.vo.ClaimCouponRequestVO;
 import com.ylx.massage.domain.vo.CouponReceiveVo;
 import com.ylx.massage.domain.vo.CouponReceivesVO;
 
+import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 优惠券领取表(CouponReceive)表服务接口
@@ -48,5 +50,14 @@ public interface CouponReceiveService extends IService<CouponReceive> {
     List<Coupon> couponWindows(CouponReceive couponReceive);
 
 
+    Map<String, Object> selectCouponDetailForCalc(String couponId, String openId);
+
+    int useCouponOptimisticLock(String couponId, String openId, Long orderId, Integer orderType, LocalDateTime now);
+
+    int incrementUsedNum(String couponId);
+
+    int returnCouponOptimisticLock(String couponId, String openId, Long orderId);
+
+    void decrementUsedNum(String couponId);
 }
 

+ 8 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/CouponService.java

@@ -4,6 +4,8 @@ package com.ylx.massage.service;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.ylx.massage.domain.Coupon;
 
+import java.math.BigDecimal;
+
 /**
  * 优惠券的规则信息(Coupon)表服务接口
  *
@@ -12,5 +14,11 @@ import com.ylx.massage.domain.Coupon;
  */
 public interface CouponService extends IService<Coupon> {
 
+    BigDecimal calculateDiscountAmount(String couponId, String openId,BigDecimal orderAmount);
+
+    void useCoupon(String couponId, String openId, Long orderId, Integer orderType);
+
+    void returnCoupon(String couponId, String openId, Long orderId);
+
 }
 

+ 26 - 4
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/CouponReceiveServiceImpl.java

@@ -29,10 +29,7 @@ import javax.annotation.Resource;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
@@ -266,5 +263,30 @@ public class CouponReceiveServiceImpl extends ServiceImpl<CouponReceiveMapper, C
             return Collections.emptyList();
         }
     }
+
+    @Override
+    public Map<String, Object> selectCouponDetailForCalc(String couponId, String openId) {
+        return this.baseMapper.selectCouponDetailForCalc(couponId, openId);
+    }
+
+    @Override
+    public int useCouponOptimisticLock(String couponId, String openId, Long orderId, Integer orderType, LocalDateTime now) {
+        return this.baseMapper.useCouponOptimisticLock(couponId, openId, orderId, orderType, now);
+    }
+
+    @Override
+    public int incrementUsedNum(String couponId) {
+        return this.baseMapper.incrementUsedNum(couponId);
+    }
+
+    @Override
+    public int returnCouponOptimisticLock(String couponId, String openId, Long orderId) {
+        return this.baseMapper.returnCouponOptimisticLock(couponId,openId,orderId);
+    }
+
+    @Override
+    public void decrementUsedNum(String couponId) {
+        this.baseMapper.decrementUsedNum(couponId);
+    }
 }
 

+ 109 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/CouponServiceImpl.java

@@ -1,11 +1,26 @@
 package com.ylx.massage.service.impl;
 
 
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ylx.common.exception.ServiceException;
 import com.ylx.massage.domain.Coupon;
+import com.ylx.massage.domain.CouponReceive;
 import com.ylx.massage.mapper.CouponMapper;
+import com.ylx.massage.service.CouponReceiveService;
 import com.ylx.massage.service.CouponService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
 
 /**
  * 优惠券的规则信息(Coupon)表服务实现类
@@ -13,8 +28,102 @@ import org.springframework.stereotype.Service;
  * @author makejava
  * @since 2024-05-13 16:32:59
  */
+@Slf4j
 @Service("couponService")
 public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> implements CouponService {
 
+    @Resource
+    private CouponReceiveService couponReceiveService;
+
+    @Override
+    public BigDecimal calculateDiscountAmount(String couponId, String openId, BigDecimal orderAmount) {
+
+        // 1. 查询优惠券领取详情
+        Map<String, Object> detail = couponReceiveService.selectCouponDetailForCalc(couponId, openId);
+
+        if (CollUtil.isEmpty(detail)) {
+            throw new ServiceException("未查询到该用户的优惠券信息");
+        }
+
+        // 3. 校验优惠券状态(必须是 0:待使用)
+        Integer couponStatus = (Integer) detail.get("coupon_status");
+        if (couponStatus == null || couponStatus != 0) {
+            throw new ServiceException("优惠券状态异常,不可使用");
+        }
+
+        // 4. 校验有效期
+        LocalDate validStart = ((java.sql.Date) detail.get("valid_start_time")).toLocalDate();
+        LocalDate expiration = ((java.sql.Date) detail.get("expiration_time")).toLocalDate();
+        LocalDate today = LocalDate.now();
+        if (today.isBefore(validStart) || today.isAfter(expiration)) {
+            throw new ServiceException("优惠券不在有效期内");
+        }
+
+        // 5. 提取优惠规则字段
+        Integer discountType = (Integer) detail.get("discount_type");
+        BigDecimal discountValue = (BigDecimal) detail.get("discount_value");
+        BigDecimal rebValue = (BigDecimal) detail.get("reb_value");
+        BigDecimal thresholdAmount = (BigDecimal) detail.get("threshold_amount");
+
+        // 6. 根据类型计算抵扣金额
+        BigDecimal discountAmount = BigDecimal.ZERO;
+        switch (discountType) {
+            case 1: // 无门槛
+                discountAmount = discountValue;
+                break;
+            case 2: // 折扣 (reb_value 例如 0.8 代表8折)
+                // 抵扣金额 = 订单金额 * (1 - 折扣值)
+                discountAmount = orderAmount.multiply(BigDecimal.ONE.subtract(rebValue));
+                break;
+            case 3: // 满减
+                if (orderAmount.compareTo(thresholdAmount) < 0) {
+                    throw new ServiceException("订单金额未达到满减门槛");
+                }
+                discountAmount = discountValue;
+                break;
+            default:
+                throw new ServiceException("未知的优惠券类型: " + discountType);
+        }
+
+        // 7. 兜底逻辑:抵扣金额不能大于订单实际金额
+        if (discountAmount.compareTo(orderAmount) > 0) {
+            discountAmount = orderAmount;
+        }
+
+        // 8. 保留两位小数,四舍五入
+        return discountAmount.setScale(2, RoundingMode.HALF_UP);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void useCoupon(String couponId, String openId, Long orderId, Integer orderType) {
+        // 1. 乐观锁核销用户领取记录
+        int updatedRows = couponReceiveService.useCouponOptimisticLock(couponId, openId, orderId, orderType, LocalDateTime.now());
+        if (updatedRows == 0) {
+            throw new ServiceException("优惠券核销失败:状态异常或已被使用");
+        }
+
+        // 2. 规则表使用数量 +1
+        int ruleUpdatedRows = couponReceiveService.incrementUsedNum(couponId);
+        if (ruleUpdatedRows == 0) {
+            throw new ServiceException("优惠券规则状态异常,核销中止");
+        }
+        log.info("优惠券核销成功: couponId={}, openId={}, orderId={}", couponId, openId, orderId);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void returnCoupon(String couponId, String openId, Long orderId) {
+        // 1. 将已使用的券退回到待使用状态
+        int rows = couponReceiveService.returnCouponOptimisticLock(couponId, openId, orderId);
+        if (rows == 0) {
+            log.warn("退还优惠券失败,可能券已过期或不属于该订单: couponId={}, orderId={}", couponId, orderId);
+            return;
+        }
+
+        // 2. 规则表使用数量 -1
+        this.couponReceiveService.decrementUsedNum(couponId);
+        log.info("优惠券退还成功: couponId={}, orderId={}", couponId, orderId);
+    }
 }
 

+ 23 - 3
nightFragrance-massage/src/main/java/com/ylx/order/controller/OrderController.java

@@ -1,7 +1,11 @@
 package com.ylx.order.controller;
 
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ylx.common.core.domain.R;
+import com.ylx.order.domain.dto.OrderDateQueryDTO;
+import com.ylx.order.domain.dto.OrderDeleteDTO;
 import com.ylx.order.domain.dto.OrderSubmitDTO;
+import com.ylx.order.domain.vo.OrderDateQueryVo;
 import com.ylx.order.service.TOrderService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -14,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
+import java.util.Map;
 
 @RestController
 @RequestMapping("/order")
@@ -27,9 +32,24 @@ public class OrderController {
     @PreAuthorize("@customerAuth.isCustomer()")
     @ApiOperation("客户端用户提交订单")
     @PostMapping("/submit")
-    public R<?> submitOrder(@Validated @RequestBody OrderSubmitDTO dto) {
-        this.orderService.submitOrder(dto);
-        return R.ok();
+    public R<Map<String, Object>> submitOrder(@Validated @RequestBody OrderSubmitDTO dto) {
+        Map<String, Object> data = this.orderService.submitOrder(dto);
+        return R.ok(data);
     }
 
+    @PreAuthorize("@customerAuth.isCustomer()")
+    @ApiOperation("根据日期范围等条件查询订单")
+    @PostMapping("/queryByDate")
+    public R<Page<OrderDateQueryVo>> queryOrderByDate(@Validated @RequestBody OrderDateQueryDTO dto) {
+        Page<OrderDateQueryVo> page = orderService.queryOrderList(dto);
+        return R.ok(page);
+    }
+
+    @PreAuthorize("@customerAuth.isCustomer()")
+    @ApiOperation("逻辑删除订单(移入回收站)")
+    @PostMapping("/delete")
+    public R<?> deleteOrder(@Validated @RequestBody OrderDeleteDTO dto) {
+        orderService.logicDeleteOrder(dto.getId());
+        return R.ok("删除成功");
+    }
 }

+ 4 - 4
nightFragrance-massage/src/main/java/com/ylx/order/domain/OrderStatusFlow.java

@@ -30,11 +30,11 @@ public class OrderStatusFlow extends BaseEntity implements Serializable  {
     @ApiModelProperty("主键")
     private Long id;
     /*
-     * 订单
+     * 订单id
      */
-    @TableField("order_no")
-    @ApiModelProperty("订单")
-    private String orderNo;
+    @TableField("order_id")
+    @ApiModelProperty("订单id")
+    private Long orderId;
     /**
      * 流转状态
      */

+ 3 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/TOrder.java

@@ -186,4 +186,7 @@ public class TOrder extends BaseEntity {
 
     @ApiModelProperty("派单情况:0=未派单 1=已派单")
     private Integer dispatchedStatus;
+
+    @ApiModelProperty("优惠券id")
+    private String couponId;
 }

+ 50 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/OrderDateQueryDTO.java

@@ -0,0 +1,50 @@
+package com.ylx.order.domain.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.Min;
+import java.time.LocalDate;
+
+/**
+ * 类描述:用户端订单列表DTO
+ *
+ * @author Administrator
+ * @version 1.0
+ * @date 2026/6/8 9:26
+ */
+@Data
+public class OrderDateQueryDTO {
+    @ApiModelProperty("页码,默认1")
+    @Min(value = 1, message = "页码最小为1")
+    private Integer pageNum = 1;
+
+    @ApiModelProperty("每页条数,默认10")
+    @Min(value = 1, message = "每页条数最小为1")
+    private Integer pageSize = 10;
+    /**
+     * 起始日期(可选),例如 2025-11-11
+     */
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    @ApiModelProperty("起始日期(可选)")
+    private LocalDate startDate;
+
+    /**
+     *  /终止日期(可选)
+     */
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    @ApiModelProperty("终止日期(可选)")
+    private LocalDate endDate;
+
+    /**
+     * 项目名称
+     */
+    @ApiModelProperty("项目名称")
+    private String projectName;
+    /**
+     * 商户昵称
+     */
+    @ApiModelProperty("商户昵称")
+    private String merchantNickName;
+}

+ 21 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/OrderDeleteDTO.java

@@ -0,0 +1,21 @@
+package com.ylx.order.domain.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 类描述:用户端逻辑删除订单
+ *
+ * @author Administrator
+ * @version 1.0
+ * @date 2026/6/8 10:26
+ */
+@Data
+public class OrderDeleteDTO {
+
+    @NotNull(message = "订单ID不能为空")
+    @ApiModelProperty("主键ID")
+    private Long id;
+}

+ 2 - 2
nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/OrderSubmitDTO.java

@@ -33,8 +33,8 @@ public class OrderSubmitDTO implements Serializable {
     @ApiModelProperty("联系人地址ID")
     private Long addressId;
 
-    @ApiModelProperty("优惠券抵扣金额")
-    private BigDecimal couponDiscount;
+    @ApiModelProperty("优惠券id")
+    private String couponId;
 
     @ApiModelProperty("交通费")
     private BigDecimal trafficFee;

+ 29 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/OrderUpdateStatusDTO.java

@@ -0,0 +1,29 @@
+package com.ylx.order.domain.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 类描述:修改订单表所有状态时DTO
+ *
+ * @author Administrator
+ * @version 1.0
+ * @date 2026/6/8 15:35
+ */
+@Data
+public class OrderUpdateStatusDTO {
+    /*
+     * 订单id
+     */
+    @NotNull(message = "订单id不能为空")
+    @ApiModelProperty("订单id")
+    private Long orderId;
+    /**
+     * 流转状态
+     */
+    @NotNull(message = "流转状态不能为空")
+    @ApiModelProperty("同步订单表状态")
+    private Integer status;
+}

+ 80 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/vo/OrderDateQueryVo.java

@@ -0,0 +1,80 @@
+package com.ylx.order.domain.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 类描述:用户端订单查询列表
+ *
+ * @author Administrator
+ * @version 1.0
+ * @date 2026/6/8 9:43
+ */
+@Data
+public class OrderDateQueryVo {
+    /**
+     * 商户头像
+     */
+    @ApiModelProperty("商户头像")
+    private String merchantAvatar;
+
+    /**
+     * 服务时间范围
+     */
+    @ApiModelProperty("服务时间范围")
+    private String serviceTime;
+
+    /**
+     * 订单状态
+     */
+    @ApiModelProperty("订单状态")
+    private Integer orderStatus;
+
+    /**
+     * 订单状态中文
+     */
+    @ApiModelProperty("订单状态中文")
+    private String orderStatusName;
+
+    /**
+     * 项目名称
+     */
+    @ApiModelProperty("项目名称")
+    private String projectName;
+
+    /**
+     * 项目时长
+     */
+    @ApiModelProperty("项目时长")
+    private Integer projectDuration;
+    /**
+     * 项目封面图
+     */
+    @ApiModelProperty("项目封面图")
+    private String projectCover;
+    /**
+     * 项目亮点
+     */
+    @ApiModelProperty("项目亮点")
+    private String highlight;
+    /**
+     * 最终应付/实付金额
+     */
+    @ApiModelProperty("最终应付/实付金额")
+    private BigDecimal finalAmount;
+    /**
+     * 交通费
+     */
+    @ApiModelProperty("交通费")
+    private BigDecimal trafficFee;
+
+    /**
+     * 项目标价/售价
+     */
+    @ApiModelProperty("项目标价/售价")
+    private BigDecimal basePrice;
+
+
+}

+ 44 - 0
nightFragrance-massage/src/main/java/com/ylx/order/enums/OrderStatusEnum.java

@@ -0,0 +1,44 @@
+package com.ylx.order.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum OrderStatusEnum {
+
+    PENDING_PAYMENT(0, "待付款"),
+    PENDING_DISPATCH(1, "待派单"),
+    PENDING_ACCEPT(2, "待接单"),
+    PENDING_SERVICE(3, "待服务"),
+    IN_SERVICE(4, "服务中"),
+    IN_AFTER_SALE(5, "售后中"),
+    COMPLETED(6, "已完成"),
+    REFUNDED(7, "已退款"),
+    CANCELLED(8, "已取消"),
+    CLOSED(9, "已关闭"),
+    REJECTED(10, "拒绝接单");
+
+    private final Integer code;
+    private final String info;
+
+    OrderStatusEnum(Integer code, String info) {
+        this.code = code;
+        this.info = info;
+    }
+
+    public static OrderStatusEnum fromCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (OrderStatusEnum status : values()) {
+            if (status.code.equals(code)) {
+                return status;
+            }
+        }
+        return null;
+    }
+
+    public static String getInfoByCode(Integer code) {
+        OrderStatusEnum status = fromCode(code);
+        return status == null ? "未知" : status.getInfo();
+    }
+}

+ 19 - 0
nightFragrance-massage/src/main/java/com/ylx/order/enums/PaymentMethodEnum.java

@@ -0,0 +1,19 @@
+package com.ylx.order.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum PaymentMethodEnum {
+
+    BALANCE(0, "余额支付"),
+    THIRD_PARTY(1, "第三方支付");
+
+
+    private final Integer code;
+    private final String info;
+
+    PaymentMethodEnum(Integer code, String info) {
+        this.code = code;
+        this.info = info;
+    }
+}

+ 2 - 2
nightFragrance-massage/src/main/java/com/ylx/order/service/OrderStatusFlowService.java

@@ -7,9 +7,9 @@ public interface OrderStatusFlowService extends IService<OrderStatusFlow> {
 
     /**
      * 记录订单状态流转
-     * @param orderNo
+     * @param orderId
      * @param status
      * @return
      */
-    public void recordFlow(String orderNo, Integer status);
+    public void recordFlow(Long orderId, Integer status);
 }

+ 25 - 1
nightFragrance-massage/src/main/java/com/ylx/order/service/TOrderService.java

@@ -2,13 +2,17 @@ package com.ylx.order.service;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
 import com.ylx.common.core.domain.R;
 import com.ylx.order.domain.TOrder;
 import com.ylx.massage.domain.TWxUser;
 import com.ylx.massage.domain.vo.HomeBlock;
 import com.ylx.massage.domain.vo.OrderVerificationVo;
 import com.ylx.massage.domain.vo.TechnicianAvailabilityVo;
+import com.ylx.order.domain.dto.OrderDateQueryDTO;
 import com.ylx.order.domain.dto.OrderSubmitDTO;
+import com.ylx.order.domain.dto.OrderUpdateStatusDTO;
+import com.ylx.order.domain.vo.OrderDateQueryVo;
 
 import java.math.BigDecimal;
 import java.util.Date;
@@ -151,5 +155,25 @@ public interface TOrderService extends IService<TOrder> {
      */
     public List<Map<String, Object>> myIncomeDetail(TWxUser user, Integer wStatus);
 
-    void submitOrder(OrderSubmitDTO dto);
+    Map<String, Object> submitOrder(OrderSubmitDTO dto);
+
+    /**
+     * 用户端订单列表
+     * @param dto
+     * @return
+     */
+    Page<OrderDateQueryVo> queryOrderList(OrderDateQueryDTO dto);
+
+    /*
+     * 逻辑删除订单
+     */
+    void logicDeleteOrder(Long orderId);
+
+    void processOrderPayment(WxPayOrderNotifyV3Result.DecryptNotifyResult result, TWxUser wxUser, TOrder order);
+
+    /**
+     * 修改订单表状态必须使用此接口(此接口里面会插入订单流转记录表)
+     * @param dto
+     */
+    void updateOrderStatus(OrderUpdateStatusDTO dto);
 }

+ 2 - 2
nightFragrance-massage/src/main/java/com/ylx/order/service/impl/OrderStatusFlowServiceImpl.java

@@ -17,9 +17,9 @@ import org.springframework.stereotype.Service;
 public class OrderStatusFlowServiceImpl extends ServiceImpl<OrderStatusFlowMapper, OrderStatusFlow> implements OrderStatusFlowService {
 
     @Override
-    public void recordFlow(String orderNo, Integer status) {
+    public void recordFlow(Long orderId, Integer status) {
         OrderStatusFlow orderStatusFlow = new OrderStatusFlow();
-        orderStatusFlow.setOrderNo(orderNo);
+        orderStatusFlow.setOrderId(orderId);
         orderStatusFlow.setStatus(status);
         this.save(orderStatusFlow);
     }

+ 329 - 61
nightFragrance-massage/src/main/java/com/ylx/order/service/impl/TOrderServiceImpl.java

@@ -2,37 +2,59 @@ package com.ylx.order.service.impl;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
 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.DateUtils;
 import com.ylx.common.utils.SecurityUtils;
+import com.ylx.common.weixinPay.enums.WxPayTypeEnum;
+import com.ylx.common.weixinPay.service.WxPayV3Service;
 import com.ylx.massage.domain.MaTechnician;
 import com.ylx.massage.domain.TAddress;
 import com.ylx.massage.domain.TWxUser;
 import com.ylx.massage.domain.vo.HomeBlock;
 import com.ylx.massage.domain.vo.OrderVerificationVo;
 import com.ylx.massage.domain.vo.TechnicianAvailabilityVo;
+import com.ylx.massage.service.CouponService;
 import com.ylx.massage.service.IMaTechnicianService;
 import com.ylx.massage.service.TAddressService;
+import com.ylx.massage.service.TWxUserService;
 import com.ylx.massage.utils.OrderNumberGenerator;
+import com.ylx.order.domain.OrderStatusFlow;
 import com.ylx.order.domain.TOrder;
+import com.ylx.order.domain.dto.OrderDateQueryDTO;
 import com.ylx.order.domain.dto.OrderSubmitDTO;
+import com.ylx.order.domain.dto.OrderUpdateStatusDTO;
+import com.ylx.order.domain.vo.OrderDateQueryVo;
+import com.ylx.order.enums.OrderStatusEnum;
+import com.ylx.order.enums.PaymentMethodEnum;
+import com.ylx.order.mapper.OrderStatusFlowMapper;
 import com.ylx.order.mapper.TOrderMapper;
+import com.ylx.order.service.OrderStatusFlowService;
 import com.ylx.order.service.TOrderService;
 import com.ylx.project.domain.Project;
 import com.ylx.project.service.ProjectService;
+import com.ylx.shopingfundsdetail.domain.vo.ShoppingFundsDetailAddDto;
+import com.ylx.shopingfundsdetail.enums.ShoppingFundsExpenseTypeEnum;
+import com.ylx.shopingfundsdetail.service.ShoppingFundsDetailService;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
+import java.util.stream.Collectors;
 
 
 /**
@@ -42,6 +64,8 @@ import java.util.*;
 @Slf4j
 public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> implements TOrderService {
 
+    private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("M月d日");
+    private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
     @Resource
     private ProjectService projectService;
     @Resource
@@ -50,6 +74,17 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
     private IMaTechnicianService maTechnicianService;
     @Resource
     private OrderNumberGenerator orderNumberGenerator;
+    @Resource
+    private TWxUserService wxUserService;
+    @Resource
+    private ShoppingFundsDetailService shoppingFundsDetailService;
+    @Resource
+    private CouponService couponService;
+    @Resource
+    private WxPayV3Service wxPayV3Service;
+
+    @Resource
+    private OrderStatusFlowService orderStatusFlowService;
 
     @Override
     public TOrder addOrder(TOrder order) {
@@ -183,113 +218,346 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void submitOrder(OrderSubmitDTO dto) {
-
-        TOrder order = new TOrder();
-        String orderNo = orderNumberGenerator.generateNextOrderNumber("");
-        order.setOrderNo(orderNo);
+    public Map<String, Object> submitOrder(OrderSubmitDTO dto) {
+        Map<String, Object> map = new HashMap<>();
 
-        // 1. 获取当前用户
+        // 1. 获取并校验当前用户
         WxLoginUser wxLoginUser = SecurityUtils.getWxLoginUser();
         if (ObjectUtil.isNull(wxLoginUser)) {
             throw new ServiceException("用户未登录");
         }
+        Long userId = Long.parseLong(wxLoginUser.getId());
+        String openId = wxLoginUser.getCOpenid();
 
-        order.setUserId(Long.parseLong(wxLoginUser.getId()));
-
-        // 2. 获取项目信息
+        // 2. 获取并校验项目信息
         Project project = this.projectService.getById(dto.getProjectId());
         if (ObjectUtil.isNull(project)) {
             throw new ServiceException("项目不存在");
         }
 
-        order.setProjectId(dto.getProjectId());
-        order.setProjectType(project.getType());
-        order.setProjectName(project.getTitle());
-        order.setProjectCover(project.getCover());
-        order.setHighlight(project.getHighlight());
-        order.setAppointmentStartTime(dto.getAppointmentStartTime());
-        order.setProjectDuration(project.getStandardDuration());
-
-        // 3. 获取商户信息
+        // 3. 获取并校验商户信息
         MaTechnician maTechnician = this.maTechnicianService.getById(dto.getMerchantId());
         if (ObjectUtil.isNull(maTechnician)) {
             throw new ServiceException("商户不存在");
         }
 
-        List<TAddress> merchantAddresslist = this.addressService.list(new LambdaQueryWrapper<TAddress>()
+        // 4. 获取联系人信息
+        TAddress address = this.addressService.getById(dto.getAddressId());
+        if (ObjectUtil.isNull(address)) {
+            throw new ServiceException("客户联系地址不存在");
+        }
+
+        // 5. 获取商户地址信息
+        List<TAddress> merchantAddressList = this.addressService.list(new LambdaQueryWrapper<TAddress>()
                 .eq(TAddress::getMerchantId, dto.getMerchantId())
                 .eq(TAddress::getUserType, 2)
                 .eq(TAddress::getIsDelete, 0));
-        if (CollUtil.isEmpty(merchantAddresslist)) {
+        if (CollUtil.isEmpty(merchantAddressList)) {
             throw new ServiceException("商户地址不存在");
         }
 
-        order.setMerchantId(dto.getMerchantId());
-        order.setMerchantType(maTechnician.getTechType());
-        order.setMerchantNickName(maTechnician.getTeNickName());
-        order.setMerchantAvatar(maTechnician.getTeAvatar());
+        // 6. 计算价格与优惠券核销 (核心优化:先算钱、扣券,再落库)
+        BigDecimal basePrice = Optional.ofNullable(project.getPrice()).orElse(BigDecimal.ZERO);
+        BigDecimal trafficFee = Optional.ofNullable(dto.getTrafficFee()).orElse(BigDecimal.ZERO);
+        BigDecimal couponDiscount = BigDecimal.ZERO;
 
-        // 4. 获取联系人信息
-        TAddress address = this.addressService.getById(dto.getAddressId());
-        if (ObjectUtil.isNull(address)) {
-            throw new ServiceException("客户联系地址不存在");
+        if (ObjectUtil.isNotNull(dto.getCouponId())) {
+            // 计算抵扣金额
+            couponDiscount = this.couponService.calculateDiscountAmount(dto.getCouponId(), openId, basePrice);
+            // 立即核销优惠券,保证与订单在同一事务中
+            this.couponService.useCoupon(dto.getCouponId(), openId, null, 1);
         }
 
-        order.setContactPersonName(address.getUserName());
-        order.setContactPhoneNumber(address.getPhone());
-        order.setContactAddressInfo(address.getDetailAddress());
+        // 实付金额 = 商品原价 - 优惠券优惠 + 车费
+        BigDecimal finalAmount = basePrice.subtract(couponDiscount).add(trafficFee)
+                .setScale(2, RoundingMode.HALF_UP);
 
-        // 5. 设置价格信息
-        LocalDateTime appointmentStartTime = dto.getAppointmentStartTime();
+        // 7. 组装订单对象
+        TOrder order = buildOrder(dto, project, maTechnician, address, merchantAddressList,
+                basePrice, trafficFee, couponDiscount, finalAmount, userId);
 
-        Integer paymentMethod = dto.getPaymentMethod();
-        // 优惠券优惠
-        BigDecimal couponDiscount = Optional.ofNullable(dto.getCouponDiscount()).orElse(BigDecimal.ZERO);
-        // 车费
-        BigDecimal trafficFee = Optional.ofNullable(dto.getTrafficFee()).orElse(BigDecimal.ZERO);
-        // 商品原价
-        BigDecimal price = Optional.ofNullable(project.getPrice()).orElse(BigDecimal.ZERO);
+        // 8. 保存订单
+        boolean saveResult = this.save(order);
+        if (!saveResult) {
+            throw new ServiceException("添加订单失败");
+        }
 
-        // 实付金额 = 商品原价 - 优惠券优惠 + 车费
-        BigDecimal finalAmount = price.subtract(couponDiscount).add(trafficFee);
+        // 9. 更新优惠券关联的真实订单ID (因为刚才核销时订单还没生成,这里补上)
+        if (ObjectUtil.isNotNull(dto.getCouponId())) {
+            this.couponService.useCoupon(dto.getCouponId(), openId, order.getId(), 1);
+        }
+
+        // 10. 处理余额支付逻辑
+        if (ObjectUtil.equals(PaymentMethodEnum.BALANCE.getCode(), dto.getPaymentMethod())) {
+            handleBalancePayment(userId, finalAmount, order.getOrderNo());
+            map.put("orderId", order.getId());
+            return map;
+        } else {
+            return createWxPayOrder(order, wxLoginUser);
+        }
+    }
+
+    /**
+     * 用户端订单列表
+     *
+     * @param dto
+     * @return
+     */
+    @Override
+    public Page<OrderDateQueryVo> queryOrderList(OrderDateQueryDTO dto) {
+        // 1. 构造分页对象
+        Page<TOrder> page = new Page<>(dto.getPageNum(), dto.getPageSize());
+
+        // 2. 构造查询条件
+        LambdaQueryWrapper<TOrder> wrapper = new LambdaQueryWrapper<>();
+        if (dto.getStartDate() != null) {
+            LocalDateTime start = dto.getStartDate().atStartOfDay();
+            wrapper.ge(TOrder::getCreateTime, start);
+        }
+        if (dto.getEndDate() != null) {
+            LocalDateTime end = dto.getEndDate().atTime(LocalTime.MAX);
+            wrapper.le(TOrder::getCreateTime, end);
+        }
+        if (StrUtil.isNotBlank(dto.getProjectName())) {
+            wrapper.like(TOrder::getProjectName, dto.getProjectName());
+        }
+        if (StrUtil.isNotBlank(dto.getMerchantNickName())) {
+            wrapper.like(TOrder::getMerchantNickName, dto.getMerchantNickName());
+        }
+        wrapper.eq(TOrder::getIsDelete, 0);
+        wrapper.orderByDesc(TOrder::getCreateTime);
+
+        // 3. 执行分页查询
+        Page<TOrder> orderPage = baseMapper.selectPage(page, wrapper);
+
+        // 4. 转换 VO
+        List<OrderDateQueryVo> voList = orderPage.getRecords().stream()
+                .map(this::convertToVo)
+                .collect(Collectors.toList());
+
+
+        // 5. 返回分页结果
+
+        Page<OrderDateQueryVo> voPage = new Page<>(orderPage.getCurrent(), orderPage.getSize(), orderPage.getTotal());
+        voPage.setRecords(voList);
 
-        order.setBasePrice(price);
+        return voPage;
+    }
+
+    /**
+     * 用户端逻辑删除
+     *
+     * @param orderId
+     */
+    @Override
+    public void logicDeleteOrder(Long orderId) {
+        // 1. 查询订单是否存在
+        TOrder order = getById(orderId);
+        if (order == null) {
+            throw new ServiceException("订单不存在");
+        }
+
+        // 3. 如果已经是删除状态,可提示或直接返回
+        if (Integer.valueOf(1).equals(order.getIsDelete())) {
+            throw new ServiceException("订单已在回收站中");
+        }
+
+        // 4. 执行逻辑删除
+        order.setIsDelete(1);
+        order.setDeletedTime(LocalDateTime.now());
+        boolean updated = updateById(order);
+        if (!updated) {
+            throw new ServiceException("删除失败,请稍后重试");
+        }
+        // 5. 事务成功,返回(Controller中会返回 success 信息)
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void processOrderPayment(WxPayOrderNotifyV3Result.DecryptNotifyResult result, TWxUser wxUser, TOrder order) {
+        // 更新订单状态
+        this.lambdaUpdate()
+                .set(TOrder::getStatus, OrderStatusEnum.PENDING_DISPATCH.getCode())
+                .eq(TOrder::getId, order.getId())
+                .update();
+    }
+
+    /**
+     *
+     * 更新订单状态(修改订单表状态必须使用此接口)
+     * @param dto
+     */
+    @Override
+    public void updateOrderStatus(OrderUpdateStatusDTO dto) {
+        Long orderId = dto.getOrderId();
+        Integer newStatus = dto.getStatus();
+
+        // 1. 查询订单
+        TOrder order = this.baseMapper.selectById(orderId);
+        if (order == null) {
+            throw new ServiceException("订单不存在");
+        }
+        Integer oldStatus = order.getStatus();
+
+        // 2. 更新订单状态
+        order.setStatus(newStatus);
+        // 可根据状态同步更新对应时间字段(例如:支付完成时间、接单时间等),此处省略具体业务逻辑
+        int updateCount = this.baseMapper.updateById(order);
+        if (updateCount == 0) {
+            throw new ServiceException("更新订单状态失败");
+        }
+
+        // 3. 记录状态流转流水
+        OrderStatusFlow flow = new OrderStatusFlow();
+        flow.setOrderId(orderId);
+        flow.setStatus(newStatus);
+        orderStatusFlowService.getBaseMapper().insert(flow);
+
+        log.info("订单状态变更: orderId={}, oldStatus={}, newStatus={}", orderId, oldStatus, newStatus);
+    }
+
+
+    /**
+     * 将 TOrder 转换为 OrderDateQueryVo,并处理 serviceTime 字段
+     */
+    private OrderDateQueryVo convertToVo(TOrder order) {
+        OrderDateQueryVo vo = new OrderDateQueryVo();
+        // 拷贝相同字段
+        BeanUtils.copyProperties(order, vo);
+        // 手动设置字段名不一致或需要特殊处理的
+        vo.setOrderStatus(order.getStatus());
+        vo.setOrderStatusName(getOrderStatusName(order.getStatus()));
+        // 服务时间范围:预约开始时间 + 项目时长(分钟)
+        // 使用 startTime 和 completedTime 构建服务时间展示
+        String serviceTime = buildServiceTime(order.getStartTime(), order.getCompletedTime());
+        vo.setServiceTime(serviceTime);
+
+        return vo;
+    }
+
+    /**
+     * 订单状态码 -> 中文描述
+     */
+    private String getOrderStatusName(Integer status) {
+        return OrderStatusEnum.getInfoByCode(status);
+    }
+
+    /**
+     * 拼接服务时间字符串,例如 “2026-06-08 14:30 (90分钟)”
+     */
+    private String buildServiceTime(LocalDateTime startTime, LocalDateTime completedTime) {
+        if (startTime == null || completedTime == null) {
+            return "";
+        }
+        String date = startTime.format(dateFormatter);
+        String start = startTime.format(timeFormatter);
+        String end = completedTime.format(timeFormatter);
+        return date + " " + start + "-" + end;
+    }
+
+    /**
+     * 组装订单对象
+     */
+    private TOrder buildOrder(OrderSubmitDTO dto, Project project, MaTechnician maTechnician,
+                              TAddress address, List<TAddress> merchantAddressList,
+                              BigDecimal basePrice, BigDecimal trafficFee, BigDecimal couponDiscount,
+                              BigDecimal finalAmount, Long userId) {
+        TOrder order = new TOrder();
+        order.setOrderNo(orderNumberGenerator.generateNextOrderNumber(""));
+        order.setUserId(userId);
+        order.setProjectId(dto.getProjectId());
+        order.setProjectType(project.getType());
+        order.setProjectName(project.getTitle());
+        order.setProjectCover(project.getCover());
+        order.setHighlight(project.getHighlight());
+        order.setAppointmentStartTime(dto.getAppointmentStartTime());
+        order.setProjectDuration(project.getStandardDuration());
+        order.setMerchantId(dto.getMerchantId());
+        order.setMerchantType(maTechnician.getTechType());
+        order.setMerchantNickName(maTechnician.getTeNickName());
+        order.setMerchantAvatar(maTechnician.getTeAvatar());
+        order.setContactPersonName(address.getUserName());
+        order.setContactPhoneNumber(address.getPhone());
+        order.setContactAddressInfo(address.getDetailAddress());
+        order.setBasePrice(basePrice);
         order.setCouponDiscount(couponDiscount);
         order.setTrafficFee(trafficFee);
         order.setFinalAmount(finalAmount);
-        order.setPaymentMethod(paymentMethod);
-
+        order.setCouponId(dto.getCouponId());
         order.setCreateTime(DateUtils.getNowDate());
-        order.setAppointmentStartTime(appointmentStartTime);
+        order.setAppointmentStartTime(dto.getAppointmentStartTime());
         order.setStatus(0);
         order.setExecStatus(0);
         order.setDispatchedStatus(0);
+        order.setPaymentMethod(dto.getPaymentMethod());
 
-        // 6.经纬度信息
+        // 经纬度安全赋值
         order.setUserLatitude(new BigDecimal(address.getLatitude()));
         order.setUserLongitude(new BigDecimal(address.getLongitude()));
-        //取默认真实地址(type=1)
-        Optional<TAddress> realAddrOpt = merchantAddresslist.stream()
-                .filter(a -> a.getType() == 1)
-                .findFirst();
-        //取默认虚拟地址(type=2)
-        Optional<TAddress> virtualAddrOpt = merchantAddresslist.stream()
-                .filter(a -> a.getType() == 2)
-                .findFirst();
-
-        realAddrOpt.ifPresent(addr -> {
+
+        merchantAddressList.stream().filter(a -> a.getType() == 1).findFirst().ifPresent(addr -> {
             order.setMerchantLongitude(new BigDecimal(addr.getLongitude()));
             order.setMerchantLatitude(new BigDecimal(addr.getLatitude()));
         });
-        virtualAddrOpt.ifPresent(addr -> {
+        merchantAddressList.stream().filter(a -> a.getType() == 2).findFirst().ifPresent(addr -> {
             order.setVirtualLongitude(new BigDecimal(addr.getLongitude()));
             order.setVirtualLatitude(new BigDecimal(addr.getLatitude()));
         });
 
-        boolean saveResult = this.save(order);
-        if (!saveResult) {
-            throw new ServiceException("添加订单失败");
+        // 设置支付状态
+        if (ObjectUtil.equals(PaymentMethodEnum.BALANCE.getCode(), dto.getPaymentMethod())) {
+            order.setStatus(OrderStatusEnum.PENDING_DISPATCH.getCode());
+            order.setPaidTime(LocalDateTime.now());
+        } else {
+            order.setStatus(OrderStatusEnum.PENDING_PAYMENT.getCode());
+        }
+        return order;
+    }
+
+    /**
+     * 处理余额支付 (核心优化:数据库原子操作防并发)
+     */
+    private void handleBalancePayment(Long userId, BigDecimal finalAmount, String orderNo) {
+        // 1. 数据库层面原子扣减余额,并校验余额是否充足
+        boolean deductSuccess = this.wxUserService.lambdaUpdate()
+                .setSql("d_balance = d_balance - " + finalAmount)
+                .eq(TWxUser::getId, userId)
+                // 防止余额超扣的核心:当前余额必须大于等于扣减金额
+                .apply("d_balance >= {0}", finalAmount)
+                .update();
+
+        if (!deductSuccess) {
+            throw new ServiceException("余额不足,扣款失败");
+        }
+
+        // 2. 重新查询最新余额用于记录流水
+        TWxUser user = this.wxUserService.getById(userId);
+
+        // 3. 记录购物金明细
+        ShoppingFundsDetailAddDto detailDto = new ShoppingFundsDetailAddDto();
+        detailDto.setUserId(userId.toString());
+        detailDto.setAmount(finalAmount);
+        detailDto.setOrderNo(orderNo);
+        detailDto.setExpenseType(ShoppingFundsExpenseTypeEnum.CONSUMPTION.getCode());
+        detailDto.setBalance(user.getdBalance());
+        this.shoppingFundsDetailService.addShoppingFundsDetail(detailDto);
+    }
+
+    /**
+     * 创建微信支付订单(事务外执行,减少事务时长)
+     */
+    private Map<String, Object> createWxPayOrder(TOrder order, WxLoginUser wxLoginUser) {
+        try {
+            return wxPayV3Service.createV3JsapiOrder(
+                    order.getOrderNo(),
+                    order.getFinalAmount(),
+                    "购买情绪价值商品",
+                    wxLoginUser.getCOpenid(),
+                    WxPayTypeEnum.EMOTION_GOODS.getCode()
+            );
+        } catch (Exception e) {
+            log.error("微信支付下单失败,订单号: {}", order.getOrderNo(), e);
+            throw new ServiceException("支付服务异常,请稍后重试");
         }
     }
 }

+ 0 - 1
nightFragrance-massage/src/main/java/com/ylx/shopingfundsdetail/domain/vo/ShoppingFundsDetailAddDto.java

@@ -32,7 +32,6 @@ public class ShoppingFundsDetailAddDto {
     /**
      * 购物卡id
      */
-    @NotNull(message = "购物卡id不能为空")
     @ApiModelProperty("购物卡id")
     private Long giftCardId;
     /**

+ 19 - 0
nightFragrance-massage/src/main/java/com/ylx/shopingfundsdetail/enums/ShoppingFundsExpenseTypeEnum.java

@@ -0,0 +1,19 @@
+package com.ylx.shopingfundsdetail.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum ShoppingFundsExpenseTypeEnum {
+
+    RECHARGE(0, "充值"),
+    CONSUMPTION(1, "消费");
+
+    private final Integer code;
+    private final String info;
+
+    ShoppingFundsExpenseTypeEnum(Integer code, String info) {
+        this.code = code;
+        this.info = info;
+    }
+
+}

+ 74 - 0
nightFragrance-massage/src/main/resources/mapper/massage/CouponReceiveMapper.xml

@@ -80,5 +80,79 @@
         and a.receive_open_id = #{openid}
     </select>
 
+    <!-- 1. 核销优惠券 (乐观锁) -->
+    <update id="useCouponOptimisticLock">
+        UPDATE coupon_receive
+        SET coupon_status = 2,
+        use_time = #{useTime},
+        order_id = #{orderId},
+        order_type = #{orderType},
+        update_time = NOW()
+        WHERE coupon_id = #{couponId}
+        AND receive_open_id = #{openId}
+        AND coupon_status = 0  <!-- 核心:只有状态为 0(待使用) 才能被更新 -->
+        AND is_delete = 0
+    </update>
+
+    <!-- 2. 优惠券规则表使用数量 +1 -->
+    <update id="incrementUsedNum">
+        UPDATE coupon
+        SET used_num = used_num + 1,
+            update_time = NOW()
+        WHERE id = #{couponId}
+          AND is_delete = 0
+    </update>
+
+    <!-- 3. 退还优惠券 (乐观锁) -->
+    <update id="returnCouponOptimisticLock">
+        UPDATE coupon_receive
+        SET coupon_status = 0,
+        use_time = NULL,
+        order_id = NULL,
+        order_type = NULL,
+        update_time = NOW()
+        WHERE coupon_id = #{couponId}
+        AND receive_open_id = #{openId}
+        AND order_id = #{orderId} <!-- 核心:必须匹配原订单,防止退错 -->
+        AND coupon_status = 2     <!-- 核心:只有状态为 2(已使用) 才能退回 -->
+        AND is_delete = 0
+    </update>
+
+    <!-- 4. 优惠券规则表使用数量 -1 -->
+    <update id="decrementUsedNum">
+        UPDATE coupon
+        SET used_num = used_num - 1,
+        update_time = NOW()
+        WHERE id = #{couponId}
+        AND used_num > 0  <!-- 核心防御:防止减为负数 -->
+        AND is_delete = 0
+    </update>
+
+    <!-- 查询优惠券领取详情 -->
+    <select id="selectCouponDetailForCalc" resultType="java.util.Map">
+        SELECT
+        cr.id,
+        cr.coupon_status,
+        cr.valid_start_time,
+        cr.expiration_time,
+        c.discount_type,
+        c.discount_value,
+        c.reb_value,
+        c.threshold_amount
+        FROM coupon_receive cr
+        JOIN coupon c ON cr.coupon_id = c.id
+        <where>
+            cr.is_delete = 0
+            AND c.is_delete = 0
+            <if test="couponId != null and couponId != ''">
+                AND cr.coupon_id = #{couponId}
+            </if>
+            <if test="openId != null and openId != ''">
+                AND cr.receive_open_id = #{openId}
+            </if>
+        </where>
+        LIMIT 1
+    </select>
+
 </mapper>