|
|
@@ -305,6 +305,14 @@ public class ProductOrderService {
|
|
|
order.setCreatedAt(new Date());
|
|
|
order.setUpdatedAt(new Date());
|
|
|
orderMapper.insert(order);
|
|
|
+ // 占用已选优惠券(避免 pending 期间其他订单重复使用同一张)
|
|
|
+ if (appliedCouponId != null) {
|
|
|
+ try {
|
|
|
+ couponService.reserve(appliedCouponId, order.getId());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("占用优惠券失败: orderNo={}, couponId={}, error={}", order.getOrderNo(), appliedCouponId, e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
// ===== 赠品校验与记录 =====
|
|
|
Result<Void> giftResult = processGifts(order, dto, product);
|
|
|
if (giftResult.getCode() != 200) {
|
|
|
@@ -642,6 +650,14 @@ public class ProductOrderService {
|
|
|
order.setCreatedAt(new Date());
|
|
|
order.setUpdatedAt(new Date());
|
|
|
orderMapper.insert(order);
|
|
|
+ // 占用已选优惠券(避免 pending 期间其他订单重复使用同一张)
|
|
|
+ if (appliedCouponId != null) {
|
|
|
+ try {
|
|
|
+ couponService.reserve(appliedCouponId, order.getId());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("占用优惠券失败: orderNo={}, couponId={}, error={}", order.getOrderNo(), appliedCouponId, e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
// ===== 赠品校验与记录(仅单商品下单支持赠品) =====
|
|
|
if (dto.getGiftItemIds() != null && !dto.getGiftItemIds().isEmpty()) {
|
|
|
if (dto.getItems().size() > 1) {
|
|
|
@@ -1004,14 +1020,8 @@ public class ProductOrderService {
|
|
|
order.setStatus("cancelled");
|
|
|
order.setUpdatedAt(new Date());
|
|
|
orderMapper.updateById(order);
|
|
|
- // 取消订单:归还优惠券
|
|
|
- if (order.getCouponId() != null) {
|
|
|
- try {
|
|
|
- couponService.revert(order.getCouponId());
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("取消订单归还优惠券失败: orderNo={}, couponId={}, error={}", orderNo, order.getCouponId(), e.getMessage());
|
|
|
- }
|
|
|
- }
|
|
|
+ // 取消订单:归还优惠券;CF 兑换来的券同时退还 CF 值
|
|
|
+ releaseCouponForCancelledOrder(order.getCouponId());
|
|
|
// Restore stock on cancel
|
|
|
productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
|
// 记录退款入库流水
|
|
|
@@ -1020,6 +1030,40 @@ public class ProductOrderService {
|
|
|
return Result.success("订单已取消");
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 订单取消/退款时的优惠券释放:先退还 CF 值(若该券为用户 CF 兑换而来),再归还券实例。
|
|
|
+ *
|
|
|
+ * 规则:
|
|
|
+ * <ul>
|
|
|
+ * <li>券实例在 family_platform_exchange_record 有 completed 兑换记录 → 用户用 CF 值兑换而来,
|
|
|
+ * 按原始消耗 cfCost 退还到家庭池(退还幂等,同一券实例只退一次);</li>
|
|
|
+ * <li>无兑换记录 → 平台赠送券(JOIN 开通会员 / POPULATION 新增成员 / MANUAL 手动发放 /
|
|
|
+ * 供应商临时券等),只归还券,不退还 CF 值。</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * 券归还与 CF 退还放在同一事务,避免「券已归还但 CF 未退」或相反的不一致。
|
|
|
+ * 与商品主流程解耦:优惠券释放失败只记日志,不阻断订单取消/退款主流程。
|
|
|
+ *
|
|
|
+ * @param userCouponId 券实例ID(family_coupon.id,即下单时保存的 product_orders.coupon_id)
|
|
|
+ */
|
|
|
+ @Transactional
|
|
|
+ public void releaseCouponForCancelledOrder(Long userCouponId) {
|
|
|
+ if (userCouponId == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ // 先退 CF:查询与 family_coupon 状态无关,顺序不影响判断结果
|
|
|
+ int cfRefunded = familyPlatformPointsService.refundCouponCf(userCouponId);
|
|
|
+ // 再归还券实例(USED → AVAILABLE)
|
|
|
+ couponService.revert(userCouponId);
|
|
|
+ if (cfRefunded > 0) {
|
|
|
+ log.info("取消/退款订单:优惠券已归还并退还CF值: familyCouponId={}, cfRefunded={}", userCouponId, cfRefunded);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("取消/退款订单:释放优惠券失败: familyCouponId={}, error={}", userCouponId, e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
public Result<String> ship(String orderNo, String trackingNumber, String expressCompany) {
|
|
|
return ship(orderNo, trackingNumber, expressCompany, null);
|
|
|
}
|
|
|
@@ -1215,23 +1259,38 @@ public class ProductOrderService {
|
|
|
if (order.getRefundStatus() == null || order.getRefundStatus() != 1) {
|
|
|
return Result.error("没有待处理的退款申请");
|
|
|
}
|
|
|
- try {
|
|
|
- paymentService.refund(orderNo, order.getTotalAmount(), order.getRefundReason());
|
|
|
- } catch (Exception e) {
|
|
|
- if (e.getMessage() != null && e.getMessage().contains("NOT_ENOUGH")) {
|
|
|
- pendingRefundService.createPendingRefund(order.getBuyerId(), null, order.getTotalAmount(), "product", orderNo);
|
|
|
- order.setStatus("refund_pending");
|
|
|
- order.setRefundStatus(4);
|
|
|
- order.setUpdatedAt(new Date());
|
|
|
- orderMapper.updateById(order);
|
|
|
- log.info("微信退款余额不足,已加入待退款队列: orderNo={}", orderNo);
|
|
|
- return Result.success("⚠️ 商户余额不足,退款已加入等待队列,到账后将自动退款");
|
|
|
+ // 实际退款金额 = 用户实付现金(money_amount 已扣除优惠券抵扣与积分抵扣)。
|
|
|
+ // 微信侧按订单实收金额退款:退 total_amount 会超过实收金额被微信拒绝,
|
|
|
+ // 且会把已抵扣的优惠券/积分折算成现金再退一次,与「归还优惠券」形成双重获益。
|
|
|
+ // 历史订单 money_amount 为空时退回 total_amount。
|
|
|
+ Integer refundAmount = order.getMoneyAmount();
|
|
|
+ if (refundAmount == null || refundAmount < 0) {
|
|
|
+ refundAmount = order.getTotalAmount();
|
|
|
+ }
|
|
|
+ if (refundAmount != null && refundAmount > 0) {
|
|
|
+ try {
|
|
|
+ paymentService.refund(orderNo, refundAmount, order.getRefundReason());
|
|
|
+ } catch (Exception e) {
|
|
|
+ if (e.getMessage() != null && e.getMessage().contains("NOT_ENOUGH")) {
|
|
|
+ pendingRefundService.createPendingRefund(order.getBuyerId(), null, refundAmount, "product", orderNo);
|
|
|
+ order.setStatus("refund_pending");
|
|
|
+ order.setRefundStatus(4);
|
|
|
+ order.setUpdatedAt(new Date());
|
|
|
+ orderMapper.updateById(order);
|
|
|
+ log.info("微信退款余额不足,已加入待退款队列: orderNo={}", orderNo);
|
|
|
+ // 注意:此处不归还优惠券/不退 CF —— 钱还没退出去。
|
|
|
+ // 待 RefundScheduledTasks.processProductRefund 实际退款成功后再释放,避免"券退了钱没退"。
|
|
|
+ return Result.success("⚠️ 商户余额不足,退款已加入等待队列,到账后将自动退款");
|
|
|
+ }
|
|
|
+ log.error("微信退款失败: orderNo={}, error={}", orderNo, e.getMessage());
|
|
|
+ return Result.error("退款失败: " + e.getMessage());
|
|
|
}
|
|
|
- log.error("微信退款失败: orderNo={}, error={}", orderNo, e.getMessage());
|
|
|
- return Result.error("退款失败: " + e.getMessage());
|
|
|
+ } else {
|
|
|
+ // 优惠券/积分全额抵扣、用户未付现金:直接走完退款流程,无需调微信退款接口
|
|
|
+ log.info("订单实付金额为0(优惠券/积分全额抵扣),跳过微信退款: orderNo={}", orderNo);
|
|
|
}
|
|
|
order.setRefundStatus(2);
|
|
|
- order.setRefundAmount(order.getTotalAmount());
|
|
|
+ order.setRefundAmount(refundAmount);
|
|
|
order.setRefundTime(new Date());
|
|
|
order.setStatus("refunded");
|
|
|
order.setUpdatedAt(new Date());
|
|
|
@@ -1239,6 +1298,8 @@ public class ProductOrderService {
|
|
|
productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
|
inventoryService.recordRefundInbound(order.getProductId(), null, order.getQuantity(),
|
|
|
"order", order.getId(), adminId, "退款:" + orderNo);
|
|
|
+ // 退款成功:归还优惠券;CF 兑换来的券同时退还 CF 值
|
|
|
+ releaseCouponForCancelledOrder(order.getCouponId());
|
|
|
syncAfterSalesRequestStatus(orderNo, "approved", adminId);
|
|
|
log.info("退款完成: orderNo={}, adminId={}", orderNo, adminId);
|
|
|
return Result.success("退款已处理");
|