Forráskód Böngészése

开发订单相关的接口

jinshihui 6 napja
szülő
commit
8198afebc4

+ 19 - 5
nightFragrance-admin/src/main/java/com/ylx/web/controller/massage/ProductOrderController.java

@@ -5,10 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ylx.common.core.controller.BaseController;
 import com.ylx.common.core.domain.R;
 import com.ylx.massage.domain.Product;
-import com.ylx.massage.domain.dto.ProductOrderCreateRequest;
-import com.ylx.massage.domain.dto.ProductOrderOperateDTO;
-import com.ylx.massage.domain.dto.ProductOrderPageDTO;
-import com.ylx.massage.domain.dto.ProductOrderPayRequest;
+import com.ylx.massage.domain.dto.*;
 import com.ylx.massage.domain.vo.ProductOrderDetailVo;
 import com.ylx.massage.domain.vo.ProductOrderListVo;
 import com.ylx.massage.domain.vo.ProductOrderPageVo;
@@ -72,7 +69,7 @@ public class ProductOrderController extends BaseController {
      *
      * @param page   分页参数
      * @param openId 用户openId
-     * @return R<Page<ProductOrderListVo>> 订单分页列表
+     * @return R<Page < ProductOrderListVo>> 订单分页列表
      */
     @GetMapping("/list")
     public R<Page<ProductOrderListVo>> getProductOrderList(Page<ProductOrderListVo> page, @RequestParam("openId") String openId) {
@@ -105,6 +102,23 @@ public class ProductOrderController extends BaseController {
         }
     }
 
+    /**
+     * 取消订单
+     *
+     * @param request 取消订单请求
+     * @return R 取消结果
+     */
+    @PostMapping("/cancel")
+    public R<?> cancelProductOrder(@RequestBody ProductOrderCancelRequest request) {
+        try {
+            log.info("取消商品订单,请求参数:{}", JSON.toJSONString(request));
+            return productOrderInfoService.cancelOrder(request);
+        } catch (Exception e) {
+            log.error("取消商品订单失败:{}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
     @ApiOperation("订单确认收货接口")
     @PutMapping("/confirm/receipt")
     public R confirmReceipt(@Validated @RequestBody ProductOrderOperateDTO dto) {

+ 1 - 0
nightFragrance-massage/pom.xml

@@ -31,6 +31,7 @@
             <artifactId>IJPay-WxPay</artifactId>
             <version>2.9.10</version>
         </dependency>
+
         <dependency>
             <groupId>com.github.wechatpay-apiv3</groupId>
             <artifactId>wechatpay-java</artifactId>

+ 4 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/ProductOrderInfo.java

@@ -133,6 +133,10 @@ public class ProductOrderInfo implements Serializable {
      */
     private String buyerRemark;
 
+    /**
+     * 取消原因
+     */
+    private String cancelReason;
 
     /**
      * 是否删除 0否 1是

+ 33 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/ProductOrderCancelRequest.java

@@ -0,0 +1,33 @@
+package com.ylx.massage.domain.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * 商品订单取消请求DTO
+ */
+@Data
+public class ProductOrderCancelRequest implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 订单编号
+     */
+    @NotBlank(message = "订单编号不能为空")
+    private String orderNo;
+
+    /**
+     * 用户openId
+     */
+    @NotBlank(message = "用户openId不能为空")
+    private String openId;
+
+    /**
+     * 取消原因
+     */
+    @NotBlank(message = "取消原因不能为空")
+    private String cancelReason;
+}

+ 20 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/ProductOrderListVo.java

@@ -40,11 +40,21 @@ public class ProductOrderListVo implements Serializable {
      */
     private Integer payStatus;
 
+    /**
+     * 支付状态描述
+     */
+    private String payStatusName;
+
     /**
      * 商品总金额
      */
     private BigDecimal totalAmount;
 
+    /**
+     * 使用的积分
+     */
+    private Integer pointsUsed;
+
     /**
      * 创建时间
      */
@@ -60,4 +70,14 @@ public class ProductOrderListVo implements Serializable {
      * 商品名称
      */
     private String productName;
+
+    /**
+     * 商品的付款类型
+     */
+    private Integer paymentType;
+
+    /**
+     * 商品的规格
+     */
+    private String skuSpec;
 }

+ 50 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/enums/PaymentStatusEnum.java

@@ -0,0 +1,50 @@
+package com.ylx.massage.enums;
+
+import lombok.Getter;
+
+/**
+ * 支付状态枚举
+ */
+@Getter
+public enum PaymentStatusEnum {
+
+    /**
+     * 未支付
+     */
+    UNPAID(0, "未支付"),
+
+    /**
+     * 已支付
+     */
+    PAID(1, "已支付"),
+
+    /**
+     * 支付失败
+     */
+    PAYMENT_FAILED(2, "支付失败"),
+
+    /**
+     * 已退款
+     */
+    REFUNDED(3, "已退款");
+
+    private final Integer code;
+    private final String info;
+
+    PaymentStatusEnum(Integer code, String info) {
+        this.code = code;
+        this.info = info;
+    }
+
+    public static String getDescByCode(Integer code) {
+        if (code == null) {
+            return "";
+        }
+        for (PaymentStatusEnum value : PaymentStatusEnum.values()) {
+            if (value.getCode().equals(code)) {
+                return value.getInfo();
+            }
+        }
+        return "";
+    }
+}

+ 6 - 1
nightFragrance-massage/src/main/java/com/ylx/massage/enums/ProductOrderStatusEnum.java

@@ -65,7 +65,12 @@ public enum ProductOrderStatusEnum {
     /**
      * 退款完成
      */
-    REFUND_COMPLETE(10, "退款完成");
+    REFUND_COMPLETE(10, "退款完成"),
+
+    /**
+     * 已取消
+     */
+    CANCELLED(11, "已取消");
 
     private final Integer code;
     private final String info;

+ 13 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductSkuMapper.java

@@ -2,6 +2,9 @@ package com.ylx.massage.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.ylx.massage.domain.ProductSku;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Update;
 
 /**
  * 商品SKU表(ProductSku)表数据库访问层
@@ -9,6 +12,16 @@ import com.ylx.massage.domain.ProductSku;
  * @author ylx
  * @since 2026-03-26
  */
+@Mapper
 public interface ProductSkuMapper extends BaseMapper<ProductSku> {
 
+    /**
+     * 释放SKU库存(乐观锁防并发超卖)
+     *
+     * @param skuId SKU ID
+     * @param quantity 释放数量
+     * @return int 影响行数,0表示库存不足或版本冲突
+     */
+    @Update("UPDATE product_sku SET stock = stock + #{quantity} WHERE id = #{skuId} AND stock >=0 ")
+    int releaseStock(@Param("skuId") Long skuId, @Param("quantity") Integer quantity);
 }

+ 9 - 4
nightFragrance-massage/src/main/java/com/ylx/massage/service/IProductOrderInfoService.java

@@ -5,10 +5,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import com.ylx.common.core.domain.R;
 import com.ylx.massage.domain.Product;
 import com.ylx.massage.domain.ProductOrderInfo;
-import com.ylx.massage.domain.dto.ProductOrderCreateRequest;
-import com.ylx.massage.domain.dto.ProductOrderOperateDTO;
-import com.ylx.massage.domain.dto.ProductOrderPageDTO;
-import com.ylx.massage.domain.dto.ProductOrderPayRequest;
+import com.ylx.massage.domain.dto.*;
 import com.ylx.massage.domain.vo.ProductOrderDetailVo;
 import com.ylx.massage.domain.vo.ProductOrderListVo;
 import com.ylx.massage.domain.vo.ProductOrderPageVo;
@@ -52,6 +49,14 @@ public interface IProductOrderInfoService extends IService<ProductOrderInfo> {
      */
     ProductOrderDetailVo getProductOrderDetail(String orderNo, String openId);
 
+    /**
+     * 取消订单
+     *
+     * @param request 订单取消请求
+     * @return R<?> 取消结果
+     */
+    R<?> cancelOrder(ProductOrderCancelRequest request);
+
     boolean confirmReceipt(ProductOrderOperateDTO dto);
 
     boolean cancelReturn(ProductOrderOperateDTO dto);

+ 13 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/TRechargeService.java

@@ -90,4 +90,17 @@ public interface TRechargeService extends IService<TRecharge> {
      * @return 在指定时间范围内的累计充值金额
      */
     BigDecimal getRecharge(String jsid, Date startDate, Date endDate);
+
+    /**
+     * 关闭微信支付订单
+     * <p>
+     * 调用微信支付关单接口,关闭未支付订单的支付链路。
+     * 如果订单从未唤起过支付(用户刚下单就取消),微信侧无订单,
+     * 关单会返回特定错误码,此时忽略错误即可。
+     * </p>
+     *
+     * @param outTradeNo 商户订单号
+     * @return boolean 关闭成功返回true,失败返回false(特定错误码视为成功)
+     */
+    boolean closeWeChatOrder(String outTradeNo);
 }

+ 95 - 5
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductOrderInfoServiceImpl.java

@@ -11,18 +11,17 @@ import com.ylx.common.core.domain.model.WxLoginUser;
 import com.ylx.common.exception.ServiceException;
 import com.ylx.common.utils.SecurityUtils;
 import com.ylx.massage.domain.*;
-import com.ylx.massage.domain.dto.ProductOrderCreateRequest;
-import com.ylx.massage.domain.dto.ProductOrderOperateDTO;
-import com.ylx.massage.domain.dto.ProductOrderPageDTO;
-import com.ylx.massage.domain.dto.ProductOrderPayRequest;
+import com.ylx.massage.domain.dto.*;
 import com.ylx.massage.domain.vo.ProductOrderDetailVo;
 import com.ylx.massage.domain.vo.ProductOrderListVo;
 import com.ylx.massage.domain.vo.ProductOrderPageVo;
 import com.ylx.massage.enums.BillTypeEnum;
+import com.ylx.massage.enums.PaymentStatusEnum;
 import com.ylx.massage.enums.ProductOrderStatusEnum;
 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.service.IProductOrderInfoService;
 import com.ylx.massage.service.TConsumptionLogService;
 import com.ylx.massage.service.TRechargeService;
@@ -71,6 +70,9 @@ public class ProductOrderInfoServiceImpl extends ServiceImpl<ProductOrderInfoMap
     @Autowired
     private IProductOrderAddressService productOrderAddressService;
 
+    @Autowired
+    private ProductSkuMapper productSkuMapper;
+
 
     /**
      * 创建商品订单
@@ -312,9 +314,12 @@ public class ProductOrderInfoServiceImpl extends ServiceImpl<ProductOrderInfoMap
             throw new ServiceException("用户openId不能为空");
         }
         Page<ProductOrderListVo> result = productOrderInfoMapper.selectProductOrderListByOpenId(page, openId);
-        // 设置订单状态名称
+
         for (ProductOrderListVo vo : result.getRecords()) {
+            // 设置订单状态名称
             vo.setOrderStatusName(ProductOrderStatusEnum.getDescByCode(vo.getOrderStatus()));
+            //设置支付状态名称
+            vo.setPayStatusName(PaymentStatusEnum.getDescByCode(vo.getPayStatus()));
         }
         return result;
     }
@@ -351,6 +356,91 @@ public class ProductOrderInfoServiceImpl extends ServiceImpl<ProductOrderInfoMap
         return detail;
     }
 
+
+    /**
+     * 取消商品订单
+     *
+     * @param request 取消订单请求
+     * @return R<?> 取消结果
+     */
+    @Override
+    public R<?> cancelOrder(ProductOrderCancelRequest request) {
+        String orderNo = request.getOrderNo();
+        String openId = request.getOpenId();
+        String cancelReason = request.getCancelReason();
+        log.info("取消商品订单,订单号:{},openId:{},取消原因:{}", orderNo, openId, cancelReason);
+
+        // 1. 查询订单
+        LambdaQueryWrapper<ProductOrderInfo> queryWrapper = new LambdaQueryWrapper<ProductOrderInfo>()
+                .eq(ProductOrderInfo::getOrderNo, orderNo)
+                .eq(ProductOrderInfo::getOpenId, openId);
+        ProductOrderInfo order = productOrderInfoMapper.selectOne(queryWrapper);
+        if (order == null) {
+            throw new ServiceException("订单不存在");
+        }
+
+        // 2. 幂等处理:已是已取消状态,直接返回成功
+        if (ProductOrderStatusEnum.CANCELLED.getCode().equals(order.getOrderStatus())) {
+            log.info("订单已是取消状态,无需重复处理,订单号:{}", orderNo);
+            return R.ok("订单已取消");
+        }
+
+        // 3. 前置校验:订单状态必须是待付款
+        if (!ProductOrderStatusEnum.WAIT_PAY.getCode().equals(order.getOrderStatus())) {
+            throw new ServiceException("只有待付款状态的订单才能取消");
+        }
+
+        // 4. 关闭微信支付订单(事务外,网络调用)
+        try {
+            rechargeService.closeWeChatOrder(orderNo);
+        } catch (Exception e) {
+            log.warn("关闭微信订单异常,继续执行取消流程,订单号:{},错误:{}", orderNo, e.getMessage());
+        }
+
+        // 5. 查询订单商品明细
+        LambdaQueryWrapper<ProductOrderItem> itemQueryWrapper = new LambdaQueryWrapper<ProductOrderItem>()
+                .eq(ProductOrderItem::getOrderNo, orderNo);
+        ProductOrderItem orderItem = productOrderItemMapper.selectOne(itemQueryWrapper);
+
+        // 6. 事务内:释放库存 + 更新订单状态
+        releaseStockAndUpdateOrderStatus(order, orderItem, cancelReason);
+        log.info("订单取消成功,订单号:{}", orderNo);
+        return R.ok("订单取消成功");
+    }
+
+
+    /**
+     * 释放库存并更新订单状态(事务内执行)
+     *
+     * @param order 订单信息
+     * @param orderItem 订单商品明细
+     * @param cancelReason 取消原因
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void releaseStockAndUpdateOrderStatus(ProductOrderInfo order, ProductOrderItem orderItem, String cancelReason) {
+        // 释放库存
+        if (orderItem != null && orderItem.getSkuId() != null && orderItem.getQuantity() != null) {
+            int affectedRows = productSkuMapper.releaseStock(orderItem.getSkuId(), orderItem.getQuantity());
+            if (affectedRows == 0) {
+                throw new ServiceException("库存释放失败,库存不足");
+            }
+            log.info("库存释放成功,SKU:{},数量:{}", orderItem.getSkuId(), orderItem.getQuantity());
+        }
+
+        // 更新订单状态为已取消
+        ProductOrderInfo updateOrder = new ProductOrderInfo();
+        updateOrder.setOrderStatus(ProductOrderStatusEnum.CANCELLED.getCode());
+        updateOrder.setCancelReason(cancelReason);
+        int updateRows = productOrderInfoMapper.update(updateOrder,
+                new LambdaQueryWrapper<ProductOrderInfo>()
+                        .eq(ProductOrderInfo::getId, order.getId())
+                        .eq(ProductOrderInfo::getOrderStatus, ProductOrderStatusEnum.WAIT_PAY.getCode()));
+        if (updateRows == 0) {
+            throw new ServiceException("订单状态已变更,取消失败");
+        }
+        log.info("订单状态更新为已取消,订单号:{}", order.getOrderNo());
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean confirmReceipt(ProductOrderOperateDTO dto) {

+ 41 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/TRechargeServiceImpl.java

@@ -17,6 +17,7 @@ import com.ijpay.core.utils.DateTimeZoneUtil;
 import com.ijpay.wxpay.WxPayApi;
 import com.ijpay.wxpay.enums.WxDomainEnum;
 import com.ijpay.wxpay.enums.v3.BasePayApiEnum;
+import com.ijpay.wxpay.model.CloseOrderModel;
 import com.ijpay.wxpay.model.v3.*;
 import com.ylx.common.config.WechatAccountConfig;
 import com.ylx.common.config.WxPayConfig;
@@ -250,6 +251,46 @@ public class TRechargeServiceImpl extends ServiceImpl<TRechargeMapper, TRecharge
         return rechargeMapper.getRecharge(jsid, startDate, endDate);
     }
 
+    @Override
+    public boolean closeWeChatOrder(String outTradeNo) {
+        try {
+            // 关闭订单API路径:/v3/pay/transactions/out-trade-no/{out_trade_no}/close
+            String closeOrderApi = String.format("/v3/pay/transactions/out-trade-no/%s/close", outTradeNo);
+            log.info("关闭微信订单,订单号:{},API路径:{}", outTradeNo, closeOrderApi);
+            CloseOrderModel closeOrderModel = new CloseOrderModel();
+            closeOrderModel.setMch_id(wxPayProperties.getMchId());
+            IJPayHttpResponse response = WxPayApi.v3(
+                    RequestMethodEnum.POST,
+                    WxDomainEnum.CHINA.toString(),
+                    closeOrderApi,
+                    wxPayProperties.getMchId(),
+                    getSerialNumber(),
+                    null,
+                    wxPayProperties.getCertKeyPath(),
+                    JSONUtil.toJsonStr(closeOrderModel)
+            );
+            log.info("关闭微信订单响应:{},状态码:{}", response.getBody(), response.getStatus());
+            // 200表示成功
+            if (response.getStatus() == SUCCESS) {
+                return true;
+            }
+            // 微信侧无订单时返回ORDERNOTEXIST错误码(订单已支付或从未创建),视为关闭成功
+            if (response.getStatus() == 400 && response.getBody() != null && response.getBody().contains("ORDERNOTEXIST")) {
+                log.info("微信侧无此订单,无需关闭,订单号:{}", outTradeNo);
+                return true;
+            }
+            return false;
+        } catch (Exception e) {
+            log.error("关闭微信订单异常,订单号:{},错误:{}", outTradeNo, e.getMessage(), e);
+            // 微信关单失败不影响后续流程,忽略错误
+            return true;
+        }
+    }
+
+    /**
+     * 获取证书序列号
+     * @return String 证书序列号
+     */
     private String getSerialNumber() {
         if (StrUtil.isEmpty(serialNo)) {
             // 获取证书序列号

+ 15 - 5
nightFragrance-massage/src/main/resources/mapper/massage/ProductOrderInfoMapper.xml

@@ -35,6 +35,9 @@
         <result property="createTime" column="create_time"/>
         <result property="productId" column="product_id"/>
         <result property="productName" column="product_name"/>
+        <result property="paymentType" column="payment_type"/>
+        <result property="skuSpec" column="sku_spec"/>
+        <result property="pointsUsed" column="points_used"/>
     </resultMap>
 
     <!-- 分页查询用户订单列表 -->
@@ -45,13 +48,20 @@
             poi.order_status,
             poi.pay_status,
             poi.total_amount,
+            poi.points_used,
             poi.create_time,
             poit.product_id,
-            poit.product_name
-        FROM product_order_info poi
-        JOIN product_order_item poit ON poi.id = poit.order_id
-        WHERE poi.open_id = #{openId}
-        ORDER BY poi.create_time DESC
+            poit.product_name,
+            p.payment_type,
+            poit.sku_spec
+        FROM
+            product_order_info poi
+                LEFT JOIN product_order_item poit ON poi.id = poit.order_id
+                LEFT JOIN product p ON poit.product_id = p.id
+        WHERE
+            poi.open_id = #{openId}
+        ORDER BY
+            poi.create_time DESC
     </select>
 
     <!-- 查询订单详情 -->