Ver código fonte

fix: 支付分补扣功能 + 回调选择性更新防止覆盖 payScoreOrderId

1. 补扣接口改为指定订单修复,支持订单ID/订单编号/outOrderNo 回填
2. HahaCallbackServiceImpl 三处 updateById 全字段更新改为 LambdaUpdateWrapper 选择性更新,防止回调覆盖 payScoreOrderId/payChannel/payScoreState
3. 前端新增「支付分补扣」按钮,弹窗输入订单和可选 outOrderNo

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 6 dias atrás
pai
commit
465098c123

+ 5 - 0
haha-admin-web/src/api/order.ts

@@ -93,6 +93,11 @@ export const getLayerRecognize = (params: { orderId?: string; activityId?: strin
   return http.request<Result>("get", "/order/layer-recognize", { params });
 };
 
+// 修复指定订单的支付分扣费(orderId/outOrderNo 以字符串传输,避免长整型精度丢失)
+export const repairPayScorePayment = (orderId: string, outOrderNo?: string) => {
+  return http.request<Result>("post", "/order/repair-payscore", { data: { orderId, outOrderNo } });
+};
+
 // 查询回调投递状态
 export const getCallbackInfo = (params: { type: string; actId: string }) => {
   return http.request<Result>("get", "/callback/info", { params });

+ 9 - 0
haha-admin-web/src/views/order/index.vue

@@ -8,6 +8,7 @@ import View from "~icons/ep/view";
 import Refund from "~icons/ep/money";
 import Cancel from "~icons/ep/delete";
 import Search from "~icons/ep/search";
+import Setting from "~icons/ep/setting";
 
 defineOptions({
   name: "OrderManage"
@@ -28,6 +29,7 @@ const {
   handleRefund,
   handlePayScoreCancel,
   handleQueryFromHaha,
+  handleRepairPayScore,
   handleSizeChange,
   handleCurrentChange
 } = useOrder(tableRef);
@@ -155,6 +157,13 @@ async function doPayScoreCancel() {
         >
           支付分取消
         </el-button>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon(Setting)"
+          @click="handleRepairPayScore"
+        >
+          支付分补扣
+        </el-button>
       </el-form-item>
     </el-form>
 

+ 53 - 1
haha-admin-web/src/views/order/utils/hook.tsx

@@ -8,7 +8,8 @@ import {
   getOrderById,
   refundOrder,
   cancelPayScoreOrder,
-  queryOrderFromHaha
+  queryOrderFromHaha,
+  repairPayScorePayment
 } from "@/api/order";
 import { type Ref, ref, computed, reactive, onMounted } from "vue";
 import {
@@ -710,6 +711,56 @@ export function useOrder(tableRef: Ref) {
     });
   }
 
+  async function handleRepairPayScore() {
+    const repairForm = reactive({ orderId: "", outOrderNo: "" });
+
+    addDialog({
+      title: "修复支付分扣费",
+      width: "460px",
+      draggable: true,
+      closeOnClickModal: false,
+      contentRenderer: () => (
+        <div>
+          <div style="margin-bottom: 12px;">
+            <div style="margin-bottom: 4px; font-size: 13px; color: var(--el-text-color-regular);">
+              订单ID 或 订单编号 <span style="color: #f56c6c;">*</span>
+            </div>
+            <ElInput v-model={repairForm.orderId} placeholder="如 202607081727441260270668" />
+          </div>
+          <div>
+            <div style="margin-bottom: 4px; font-size: 13px; color: var(--el-text-color-regular);">
+              outOrderNo(选填)
+            </div>
+            <ElInput v-model={repairForm.outOrderNo} placeholder="如 PS1782351340B150534,本地丢失时从微信商户后台获取" />
+            <div style="margin-top: 4px; font-size: 12px; color: #909399;">
+              仅在本地 payScoreOrderId 丢失时填写,从微信商户后台「支付分订单」中查找
+            </div>
+          </div>
+        </div>
+      ),
+      beforeSure: async (done) => {
+        const orderId = repairForm.orderId.trim();
+        if (!orderId) {
+          message("请输入订单ID或订单编号", { type: "warning" });
+          return;
+        }
+        const outOrderNo = repairForm.outOrderNo.trim() || undefined;
+        try {
+          const res = await repairPayScorePayment(orderId, outOrderNo);
+          if (res.code === 200) {
+            message(res.message || "修复完成", { type: "success" });
+            onSearch();
+          } else {
+            message(res.message || "修复失败", { type: "error" });
+          }
+          done();
+        } catch {
+          message("修复请求失败", { type: "error" });
+        }
+      }
+    });
+  }
+
   async function handlePayScoreCancel(cancelForm: { outOrderNo: string; reason?: string }) {
     const outOrderNo = cancelForm.outOrderNo?.trim();
     if (!outOrderNo) {
@@ -746,6 +797,7 @@ export function useOrder(tableRef: Ref) {
     resetForm,
     handleDetail,
     handleRefund,
+    handleRepairPayScore,
     handlePayScoreCancel,
     handleQueryFromHaha,
     handleSizeChange,

+ 11 - 4
haha-admin/src/main/java/com/haha/admin/controller/OrderController.java

@@ -251,13 +251,20 @@ public class OrderController {
     }
 
     /**
-     * 修复漏掉的支付分扣费:查询有支付分服务订单但未完成扣费的订单,重新发起扣费
+     * 修复指定订单的支付分扣费
      */
     @RequirePermission("order:update")
     @PostMapping("/repair-payscore")
-    public Result<Map<String, Object>> repairPayScorePayments() {
+    public Result<Map<String, Object>> repairPayScorePayment(@RequestBody Map<String, Object> params) {
+        Object orderIdObj = params.get("orderId");
+        if (orderIdObj == null || orderIdObj.toString().trim().isEmpty()) {
+            return Result.error("orderId 不能为空");
+        }
+        String input = orderIdObj.toString().trim();
+        Object outOrderNoObj = params.get("outOrderNo");
+        String outOrderNo = outOrderNoObj != null ? outOrderNoObj.toString().trim() : null;
         com.haha.service.impl.OrderServiceImpl impl = (com.haha.service.impl.OrderServiceImpl) orderService;
-        Map<String, Object> data = impl.repairPayScorePayments();
-        return Result.success("支付分扣费修复完成", data);
+        Map<String, Object> data = impl.repairPayScorePayment(input, outOrderNo);
+        return (boolean) data.get("success") ? Result.success((String) data.get("message"), data) : Result.error((String) data.get("message"));
     }
 }

+ 25 - 12
haha-service/src/main/java/com/haha/service/impl/HahaCallbackServiceImpl.java

@@ -35,6 +35,7 @@ import com.haha.service.DoorRecordService;
 import com.haha.service.UserCouponService;
 import com.haha.service.CouponTemplateService;
 import com.haha.service.config.DeviceAlertProperties;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.haha.service.payment.payscore.PayScoreCreateRequest;
 import com.haha.service.payment.payscore.PayScoreResult;
 import com.haha.service.payment.payscore.PayScoreService;
@@ -419,10 +420,10 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
             return;
         }
 
-        // 设置订单类型(OUT-拿出/IN-放入)
+        // 设置订单类型(OUT-拿出/IN-放入)- 选择性更新,不覆盖 payScoreOrderId
         if (recognizeType != null && !recognizeType.isEmpty()) {
             order.setOrderType(recognizeType);
-            orderService.updateById(order);
+            orderService.lambdaUpdate().set(Order::getOrderType, recognizeType).eq(Order::getId, order.getId()).update();
             log.info("订单类型设置成功 - orderId: {}, orderType: {}", order.getId(), recognizeType);
 
             // IN类型(异物放入)触发告警,提醒管理员审核
@@ -798,18 +799,22 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
         // 解析视频URL
         String videoUrl = parseVideoUrl(resourceInfoStr);
 
-        // 更新订单信息:只更新商品列表和视频URL,不更新金额
+        // 选择性更新,只更新识别结果相关字段,不覆盖 payScoreOrderId/payChannel
+        LambdaUpdateWrapper<Order> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(Order::getId, order.getId());
+        updateWrapper.set(Order::getItems, skuListStr);
         order.setItems(skuListStr);
-        // 注意:不更新totalAmount,保持为0,等待订单回调时更新
 
         if (videoUrl != null) {
+            updateWrapper.set(Order::getVideoUrl, videoUrl);
             order.setVideoUrl(videoUrl);
         }
         if (confidence != null) {
+            updateWrapper.set(Order::getConfidence, confidence);
             order.setConfidence(confidence);
         }
 
-        boolean updated = orderService.updateById(order);
+        boolean updated = orderService.update(updateWrapper);
         if (updated) {
             log.info("订单识别信息更新成功 - orderId: {}, 等待回调更新金额", order.getId());
         }
@@ -887,20 +892,25 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
     private void updateOrderFromCallback(Order order, String orderId, String activityId,
                                           Object orderMoney, String orderDetail,
                                           String orderName, String orderGoodsStr) {
-        // 不覆盖本地订单号,哈哈 order_id 存入独立字段,用于后续 payStatus 同步
-        order.setHahaOrderId(orderId);
-        order.setActivityId(activityId);
+        // 使用选择性更新,只更新回调提供的字段,避免覆盖 payScoreOrderId/payChannel 等
+        LambdaUpdateWrapper<Order> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(Order::getId, order.getId());
+
+        updateWrapper.set(Order::getHahaOrderId, orderId);
+        updateWrapper.set(Order::getActivityId, activityId);
 
         if (orderName != null && !orderName.isEmpty()) {
-            order.setOrderName(orderName);
+            updateWrapper.set(Order::getOrderName, orderName);
             // 根据订单名称判断订单类型:包含"放入"的为IN类型(异物放入),其他为OUT类型(正常消费)
             if (orderName.contains("放入")) {
+                updateWrapper.set(Order::getOrderType, OpenType.IN.getCode());
                 order.setOrderType(OpenType.IN.getCode());
             }
         }
 
         // 如果订单类型仍为空,默认为OUT(正常消费)
         if (order.getOrderType() == null || order.getOrderType().isEmpty()) {
+            updateWrapper.set(Order::getOrderType, OpenType.OUT.getCode());
             order.setOrderType(OpenType.OUT.getCode());
         }
 
@@ -916,20 +926,23 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
         }
 
         if (orderDetail != null) {
+            updateWrapper.set(Order::getItems, orderDetail);
             order.setItems(orderDetail);
         }
 
         if (orderMoney != null) {
             // 设备端传来的金额是已经计算过营销活动优惠后的金额
             BigDecimal totalAmount = new BigDecimal(orderMoney.toString());
-            order.setTotalAmount(totalAmount);
-
+            updateWrapper.set(Order::getTotalAmount, totalAmount);
             // 初始化优惠金额和实付金额(后续会被processCouponDiscount更新)
+            updateWrapper.set(Order::getDiscountAmount, BigDecimal.ZERO);
+            updateWrapper.set(Order::getPaidAmount, totalAmount);
+            order.setTotalAmount(totalAmount);
             order.setDiscountAmount(BigDecimal.ZERO);
             order.setPaidAmount(totalAmount);
         }
 
-        boolean updated = orderService.updateById(order);
+        boolean updated = orderService.update(updateWrapper);
         if (updated) {
             log.info("订单信息更新成功: {}", orderId);
 

+ 56 - 39
haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java

@@ -1281,52 +1281,69 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
     }
 
     /**
-     * 修复漏掉的支付分扣费:查询有支付分服务订单但未完成扣费的订单,重新发起扣费
+     * 修复指定订单的支付分扣费,支持订单ID或订单编号
      *
-     * 适用场景:订单创建后因并发回调导致 payChannel 为 null,processPayScorePayment 跳过扣费
-     * 筛选条件:payScoreOrderId 已设置 + payScoreState 为 CREATED/DOING + payStatus 为 UNPAID + totalAmount > 0
-     *
-     * @return { total, success, failed }
+     * @param orderIdOrNo       订单ID(数字)或订单编号
+     * @param payScoreOrderId   微信支付分服务订单号(outOrderNo),为空则使用订单上的值
+     * @return { success: true/false, message: "..." }
      */
     @Transactional(rollbackFor = Exception.class)
-    public Map<String, Object> repairPayScorePayments() {
-        List<Order> orders = lambdaQuery()
-                .isNotNull(Order::getPayScoreOrderId)
-                .ne(Order::getPayScoreOrderId, "")
-                .in(Order::getPayScoreState, PayScoreState.CREATED.getCode(), PayScoreState.DOING.getCode())
-                .eq(Order::getPayStatus, OrderConstants.PAY_STATUS_UNPAID)
-                .gt(Order::getTotalAmount, BigDecimal.ZERO)
-                .list();
-
-        int success = 0, failed = 0;
-        for (Order order : orders) {
-            try {
-                BigDecimal amount = order.getPaidAmount() != null
-                        && order.getPaidAmount().compareTo(BigDecimal.ZERO) > 0
-                        ? order.getPaidAmount() : order.getTotalAmount();
+    public Map<String, Object> repairPayScorePayment(String orderIdOrNo, String payScoreOrderId) {
+        Order order;
+        if (orderIdOrNo.matches("^\\d{1,19}$")) {
+            order = this.getById(Long.parseLong(orderIdOrNo));
+        } else {
+            order = lambdaQuery().eq(Order::getOrderNo, orderIdOrNo).one();
+        }
+        if (order == null) {
+            return Map.of("success", false, "message", "订单不存在: " + orderIdOrNo);
+        }
 
-                boolean result = completeOrderWithPayScore(order.getId(), amount);
-                if (result) {
-                    success++;
-                    log.info("[支付分修复] 扣费成功 - orderId: {}, orderNo: {}, amount: {}",
-                            order.getId(), order.getOrderNo(), amount);
-                } else {
-                    failed++;
-                    log.warn("[支付分修复] 扣费失败 - orderId: {}, orderNo: {}",
-                            order.getId(), order.getOrderNo());
-                }
-            } catch (Exception e) {
-                failed++;
-                log.error("[支付分修复] 扣费异常 - orderId: {}, error: {}", order.getId(), e.getMessage());
+        // 如果提供了 outOrderNo,先回填到本地订单
+        if (payScoreOrderId != null && !payScoreOrderId.isEmpty()) {
+            if (order.getPayScoreOrderId() == null || order.getPayScoreOrderId().isEmpty()) {
+                order.setPayScoreOrderId(payScoreOrderId);
+                order.setPayScoreState(PayScoreState.DOING.getCode());
+                this.updateById(order);
+                log.info("[支付分修复] 回填 payScoreOrderId - orderId: {}, orderNo: {}, payScoreOrderId: {}",
+                        order.getId(), order.getOrderNo(), payScoreOrderId);
+            } else if (!payScoreOrderId.equals(order.getPayScoreOrderId())) {
+                return Map.of("success", false, "message",
+                        "提供的 outOrderNo 与订单上的不一致: " + order.getPayScoreOrderId());
             }
         }
 
-        Map<String, Object> result = new HashMap<>();
-        result.put("total", orders.size());
-        result.put("success", success);
-        result.put("failed", failed);
-        log.info("[支付分修复] 完成 - total: {}, success: {}, failed: {}", orders.size(), success, failed);
-        return result;
+        // 重新加载以获取最新状态
+        order = this.getById(order.getId());
+
+        if (order.getPayScoreOrderId() == null || order.getPayScoreOrderId().isEmpty()) {
+            return Map.of("success", false, "message",
+                    "订单未创建支付分服务订单,请在微信商户后台找到 outOrderNo 后输入");
+        }
+
+        if (PayScoreState.isFinished(order.getPayScoreState())) {
+            return Map.of("success", false, "message", "支付分订单已完结,无需修复: " + order.getPayScoreState());
+        }
+
+        if (!OrderConstants.PAY_STATUS_UNPAID.equals(order.getPayStatus())) {
+            return Map.of("success", false, "message", "订单已支付,无需修复");
+        }
+
+        if (order.getTotalAmount() == null || order.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) {
+            return Map.of("success", false, "message", "订单金额为0,无法扣费");
+        }
+
+        BigDecimal amount = order.getPaidAmount() != null
+                && order.getPaidAmount().compareTo(BigDecimal.ZERO) > 0
+                ? order.getPaidAmount() : order.getTotalAmount();
+
+        boolean result = completeOrderWithPayScore(order.getId(), amount);
+        log.info("[支付分修复] orderId: {}, orderNo: {}, payScoreOrderId: {}, amount: {}, result: {}",
+                order.getId(), order.getOrderNo(), order.getPayScoreOrderId(), amount, result);
+
+        return result
+                ? Map.of("success", true, "message", "扣费成功")
+                : Map.of("success", false, "message", "扣费失败,请查看服务端日志");
     }
 
     /**