Преглед на файлове

fix: 消除自主注册用户充值生成的幻影站点 000 分账记录

充值侧:stationId 无效(null/"000")时不再创建 SplitRecord,
改为在用户首次消费结算时通过 backfillUnattributedRecharges 追溯补建,
归属到消费时确定的真实站点。月结同步跳过 "000" 站点,防止历史
残留数据被当成真实站点结算。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline преди 2 дни
родител
ревизия
7a475aa149

+ 53 - 0
car-wash-entity/src/main/resources/sql/v8_clean_phantom_station.sql

@@ -0,0 +1,53 @@
+-- ====================================================
+-- V8 清理幻影站点 "000" 历史数据
+-- 背景:自主注册用户充值时代码硬编码 stationId="000",
+-- 导致 t_split_record / t_settlement_record 积攒了无效站点数据
+-- 发布日期:2026-06-05
+-- ====================================================
+
+-- ----------------------------
+-- 1. 迁移可追溯的分账记录:将已归属用户的 "000" SplitRecord 更新为实际站点
+--    通过 t_pay_log 和 t_user 关联找到用户的真实 station_id
+-- ----------------------------
+UPDATE t_split_record sr
+INNER JOIN t_pay_log pl ON sr.trade_no = pl.transaction_id
+INNER JOIN t_user u ON pl.user_id = u.id
+SET sr.from_station_id = u.station_id,
+    sr.to_station_id   = u.station_id
+WHERE sr.type = 1
+  AND sr.from_station_id = '000'
+  AND sr.to_station_id   = '000'
+  AND u.station_id IS NOT NULL
+  AND u.station_id != ''
+  AND u.station_id != '000';
+
+-- ----------------------------
+-- 2. 删除无法追溯的残余 "000" 分账记录
+--    即便残留全部删除也无业务影响:
+--    - 用户再次消费时会触发 OrderSettlementServiceImpl.backfillUnattributedRecharges 补建
+--    - 不消费的用户充值余额始终在钱包中,无需分账
+-- ----------------------------
+DELETE FROM t_split_record
+WHERE from_station_id = '000'
+   OR to_station_id   = '000';
+
+-- ----------------------------
+-- 3. 清理 "000" 站点的结算记录
+-- ----------------------------
+DELETE FROM t_settlement_record
+WHERE station_id = '000';
+
+-- ----------------------------
+-- 4. 清理 "000" 站点的账户记录
+-- ----------------------------
+DELETE FROM t_station_account
+WHERE station_id = '000';
+
+-- ----------------------------
+-- 5. 清理 "000" 站点的充值配置分组关联(如有)
+-- ----------------------------
+DELETE FROM t_recharge_config_station_group
+WHERE station_id = '000';
+
+DELETE FROM t_recharge_config_group
+WHERE name = '000';

+ 2 - 2
car-wash-miniapp/src/main/java/com/kym/miniapp/controller/PaymentController.java

@@ -35,7 +35,7 @@ public class PaymentController {
     @ApiLog("用户充值微信支付")
     @GetMapping("/wxPay")
     @ResponseBody
-    R<?> prepay(@RequestParam Long rechargeConfigId, @RequestParam(value = "stationId", defaultValue = "000") String stationId) { //todo 前端更新后修改
+    R<?> prepay(@RequestParam Long rechargeConfigId, @RequestParam(value = "stationId", required = false) String stationId) {
         return R.success(wxPayService.wxPay(rechargeConfigId, stationId));
     }
 
@@ -55,7 +55,7 @@ public class PaymentController {
     @GetMapping("/promotionPay")
     @ResponseBody
     R<?> promotionPay(@RequestParam String promotionToken,
-                      @RequestParam(value = "stationId", defaultValue = "000") String stationId) {
+                      @RequestParam(value = "stationId", required = false) String stationId) {
         return R.success(wxPayService.promotionPay(promotionToken, stationId));
     }
 

+ 6 - 5
car-wash-mp/src/pages-user/wallet/promotion.vue

@@ -109,11 +109,12 @@ const confirm = () => {
   paying.value = true;
   uni.showLoading({ title: "支付中..." });
 
-  const homeStationId = getApp<any>().globalData.user?.stationId || "000";
-  get("/payment/promotionPay", {
-    promotionToken: state.promotionToken,
-    stationId: homeStationId,
-  })
+  const homeStationId = getApp<any>().globalData.user?.stationId;
+  const params: any = { promotionToken: state.promotionToken };
+  if (homeStationId) {
+    params.stationId = homeStationId;
+  }
+  get("/payment/promotionPay", params)
     .then((res: any) => {
       // #ifdef MP-WEIXIN
       wx.requestPayment({

+ 6 - 5
car-wash-mp/src/pages-user/wallet/recharge.vue

@@ -56,7 +56,7 @@ import { get } from "@/utils/https";
 const initState = () => ({
   configList: [] as any[],
   chosenIdx: -1,
-  stationId: "000",
+  stationId: "",
   rechargeItem: { grantsAmount: 0 } as any,
 });
 
@@ -114,10 +114,11 @@ const confirm = () => {
   uni.showLoading({ title: "支付中..." });
 
   const recharge = state.configList[state.chosenIdx];
-  get("/payment/wxPay", {
-    rechargeConfigId: recharge.id,
-    stationId: state.stationId,
-  })
+  const params: any = { rechargeConfigId: recharge.id };
+  if (state.stationId) {
+    params.stationId = state.stationId;
+  }
+  get("/payment/wxPay", params)
     .then((res: any) => {
       // #ifdef MP-WEIXIN
       wx.requestPayment({

+ 52 - 1
car-wash-service/src/main/java/com/kym/service/impl/OrderSettlementServiceImpl.java

@@ -16,6 +16,7 @@ import java.time.Duration;
 import java.time.LocalDateTime;
 import java.util.List;
 import java.util.UUID;
+import java.util.stream.Collectors;
 
 /**
  * 订单结算服务实现
@@ -32,15 +33,17 @@ public class OrderSettlementServiceImpl implements OrderSettlementService {
     private final WalletDetailService walletDetailService;
     private final AccountService accountService;
     private final SplitRecordService splitRecordService;
+    private final PayLogService payLogService;
     private final MpMsgTemplateService mpMsgTemplateService;
 
     public OrderSettlementServiceImpl(WashOrderService washOrderService, WalletDetailService walletDetailService,
                                       AccountService accountService, SplitRecordService splitRecordService,
-                                      MpMsgTemplateService mpMsgTemplateService) {
+                                      PayLogService payLogService, MpMsgTemplateService mpMsgTemplateService) {
         this.washOrderService = washOrderService;
         this.walletDetailService = walletDetailService;
         this.accountService = accountService;
         this.splitRecordService = splitRecordService;
+        this.payLogService = payLogService;
         this.mpMsgTemplateService = mpMsgTemplateService;
     }
 
@@ -93,6 +96,8 @@ public class OrderSettlementServiceImpl implements OrderSettlementService {
 
         washOrderService.updateById(washOrder);
 
+        backfillUnattributedRecharges(washOrder.getUserId());
+
         if (orderInfo.getAmount() == 0) {
             return;
         }
@@ -169,4 +174,50 @@ public class OrderSettlementServiceImpl implements OrderSettlementService {
         splitRecordService.saveBatch(List.of(crossExpend, crossIncome));
         log.info("订单:{},跨店分账完成", washOrder.getOrderId());
     }
+
+    private void backfillUnattributedRecharges(Long userId) {
+        var userStationId = KymCache.INSTANCE.getUserStationId(userId);
+        if (userStationId == null) {
+            return;
+        }
+
+        var walletDetails = walletDetailService.lambdaQuery()
+                .eq(WalletDetail::getUserId, userId)
+                .eq(WalletDetail::getType, WalletDetail.TYPE_充值)
+                .eq(WalletDetail::getStatus, WalletDetail.STATUS_已确认)
+                .list();
+
+        if (walletDetails.isEmpty()) {
+            return;
+        }
+
+        var outTradeNos = walletDetails.stream().map(WalletDetail::getOrderNo).toList();
+        var payLogs = payLogService.lambdaQuery().in(PayLog::getOutTradeNo, outTradeNos).list();
+        if (payLogs.isEmpty()) {
+            return;
+        }
+
+        var allTxIds = payLogs.stream().map(PayLog::getTransactionId).collect(Collectors.toSet());
+        var existingTxIds = splitRecordService.lambdaQuery()
+                .in(SplitRecord::getTradeNo, allTxIds)
+                .eq(SplitRecord::getType, SplitRecord.TYPE_RECHARGE)
+                .list()
+                .stream()
+                .map(SplitRecord::getTradeNo)
+                .collect(Collectors.toSet());
+
+        for (var payLog : payLogs) {
+            if (!existingTxIds.contains(payLog.getTransactionId())) {
+                var splitRecord = new SplitRecord()
+                        .setFromStationId(userStationId)
+                        .setToStationId(userStationId)
+                        .setTradeNo(payLog.getTransactionId())
+                        .setAmount(payLog.getTotal())
+                        .setType(SplitRecord.TYPE_RECHARGE);
+                splitRecordService.save(splitRecord);
+                log.info("追溯补建充值分账:用户={}, 站点={}, 交易号={}, 金额={}",
+                        userId, userStationId, payLog.getTransactionId(), payLog.getTotal());
+            }
+        }
+    }
 }

+ 3 - 0
car-wash-service/src/main/java/com/kym/service/impl/SettlementServiceImpl.java

@@ -87,6 +87,9 @@ public class SettlementServiceImpl extends MyBaseServiceImpl<SettlementRecordMap
                 .collect(Collectors.toSet());
 
         for (String stationId : stationIds) {
+            if ("000".equals(stationId)) {
+                continue;
+            }
             doStationSettlement(stationId, period, allRecords);
         }
 

+ 10 - 9
car-wash-service/src/main/java/com/kym/service/wechat/impl/WxPayServiceImpl.java

@@ -419,15 +419,16 @@ public class WxPayServiceImpl implements WxPayService {
                 payLogService.save(payLog);
 
                 // V2 结算方案:充值资金记录分账流水,结算日统一处理,不再即时转入站点账户
-
-                // 分账记录(结算时按此汇总)
-                var splitRecord = new SplitRecord()
-                        .setFromStationId(stationId)
-                        .setToStationId(stationId)
-                        .setTradeNo(transaction.getTransactionId())
-                        .setAmount(totalAmount)
-                        .setType(SplitRecord.TYPE_RECHARGE);
-                splitRecordService.save(splitRecord);
+                // 无归属站点的用户充值暂不生成分账记录,首次消费时追溯补建
+                if (CommUtil.isNotEmptyAndNull(stationId) && !"000".equals(stationId)) {
+                    var splitRecord = new SplitRecord()
+                            .setFromStationId(stationId)
+                            .setToStationId(stationId)
+                            .setTradeNo(transaction.getTransactionId())
+                            .setAmount(totalAmount)
+                            .setType(SplitRecord.TYPE_RECHARGE);
+                    splitRecordService.save(splitRecord);
+                }
 
                 // 发送公众号消息
                 mpMsgTemplateService.sendPaymentSuccessMsg(payLog, walletDetail.getAfterBalance());