Browse Source

跨店分账优化:赠款参与分账 + 充值配置站点化

- 跨店分账基数由仅充值余额改为充值余额+赠款余额,统一按70%分账
- RechargeConfig 新增 stationId 字段,支持按站点配置充值套餐
- 查询充值配置时优先返回站点专属配置,无则回退默认配置
- 创建站点时自动复制默认充值配置到新站点
- 新增管理后台充值配置 CRUD 接口
- 小程序充值页按归属站查询套餐

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 4 ngày trước cách đây
mục cha
commit
97e349c9c3

+ 75 - 0
car-wash-admin/src/main/java/com/kym/admin/controller/RechargeConfigController.java

@@ -0,0 +1,75 @@
+package com.kym.admin.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.kym.common.R;
+import com.kym.common.annotation.SysLog;
+import com.kym.entity.RechargeConfig;
+import com.kym.service.RechargeConfigService;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 充值配置管理(支持按站点维度)
+ *
+ * @author skyline
+ */
+@RestController
+@RequestMapping("/rechargeConfig")
+public class RechargeConfigController {
+
+    private final RechargeConfigService rechargeConfigService;
+
+    public RechargeConfigController(RechargeConfigService rechargeConfigService) {
+        this.rechargeConfigService = rechargeConfigService;
+    }
+
+    /**
+     * 充值配置列表
+     */
+    @SaCheckPermission("rechargeConfig.list")
+    @GetMapping("/list")
+    R<?> list(@RequestParam(required = false) String stationId) {
+        return R.success(rechargeConfigService.listByStationId(stationId));
+    }
+
+    /**
+     * 查询全部充值配置
+     */
+    @SaCheckPermission("rechargeConfig.list")
+    @GetMapping("/listAll")
+    R<?> listAll() {
+        return R.success(rechargeConfigService.list());
+    }
+
+    /**
+     * 新增充值配置
+     */
+    @SaCheckPermission("rechargeConfig.add")
+    @SysLog("新增充值配置")
+    @PostMapping("/add")
+    R<?> add(@RequestBody RechargeConfig config) {
+        rechargeConfigService.save(config);
+        return R.success();
+    }
+
+    /**
+     * 修改充值配置
+     */
+    @SaCheckPermission("rechargeConfig.modify")
+    @SysLog("修改充值配置")
+    @PostMapping("/modify")
+    R<?> modify(@RequestBody RechargeConfig config) {
+        rechargeConfigService.updateById(config);
+        return R.success();
+    }
+
+    /**
+     * 删除充值配置
+     */
+    @SaCheckPermission("rechargeConfig.remove")
+    @SysLog("删除充值配置")
+    @GetMapping("/remove/{id}")
+    R<?> remove(@PathVariable long id) {
+        rechargeConfigService.removeById(id);
+        return R.success();
+    }
+}

+ 5 - 0
car-wash-entity/src/main/java/com/kym/entity/RechargeConfig.java

@@ -35,4 +35,9 @@ public class RechargeConfig extends BaseEntity {
      * 文字标签(用于悬浮在充值金额旁)
      */
     private String label;
+
+    /**
+     * 站点ID,NULL表示平台默认配置(新站点自动使用)
+     */
+    private String stationId;
 }

+ 11 - 0
car-wash-entity/src/main/resources/sql/v2_settlement.sql

@@ -2,6 +2,7 @@
 -- 结算方案 V2 数据库变更脚本
 -- 按月结算模式:每月15日结算上月
 -- 发布日期:2026-05-14
+-- 修订日期:2026-05-26(V2.1 充值配置站点化 + 跨店分账含赠款)
 -- ====================================================
 
 -- ----------------------------
@@ -107,3 +108,13 @@ CREATE TABLE IF NOT EXISTS `t_platform_revenue_record` (
 -- 6. 清除虚拟站点(开发环境)
 -- ----------------------------
 -- DELETE FROM `t_station_account` WHERE `station_id` = '0';
+
+-- ----------------------------
+-- 7. V2.1 充值配置站点化(2026-05-26)
+-- ----------------------------
+ALTER TABLE `t_recharge_config`
+    ADD COLUMN `station_id` VARCHAR(64) DEFAULT NULL COMMENT '站点ID,NULL表示平台默认配置'
+    AFTER `label`;
+
+-- 为现有配置追加唯一索引(不包含NULL值)
+-- CREATE UNIQUE INDEX `uk_station_recharge_amount` ON `t_recharge_config` (`station_id`, `recharge_amount`);

+ 4 - 3
car-wash-miniapp/src/main/java/com/kym/miniapp/controller/CommonController.java

@@ -5,6 +5,7 @@ import com.kym.service.ContactService;
 import com.kym.service.RechargeConfigService;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 /**
@@ -37,12 +38,12 @@ public class CommonController {
 
 
     /**
-     * 充值金额配置项
+     * 充值金额配置项(按站点查询,优先返回站点专属配置,若无则返回默认配置)
      *
      * @return
      */
     @GetMapping("/rechargeConfig")
-    R<?> rechargeConfig() {
-        return R.success(rechargeConfigService.list());
+    R<?> rechargeConfig(@RequestParam(required = false) String stationId) {
+        return R.success(rechargeConfigService.listByStationId(stationId));
     }
 }

+ 2 - 1
car-wash-mp/src/pages-user/wallet/recharge.vue

@@ -87,7 +87,8 @@ const handleRechargeClick = (recharge: any, idx: number) => {
 };
 
 const loadRechargeConfig = () => {
-  get("/common/rechargeConfig")
+  const homeStationId = getApp<any>().globalData.user?.stationId;
+  get("/common/rechargeConfig", { stationId: homeStationId })
     .then((res: any) => {
       state.configList = res;
     })

+ 7 - 0
car-wash-service/src/main/java/com/kym/service/RechargeConfigService.java

@@ -11,7 +11,14 @@ import com.kym.service.mybatisplus.MyBaseService;
  * @author skyline
  * @since 2024-11-15
  */
+import java.util.List;
+
 public interface RechargeConfigService extends MyBaseService<RechargeConfig> {
 
     RechargeConfig getRechargeConfigByAmount(Integer amount);
+
+    /**
+     * 根据站点查询充值配置,优先返回站点专属配置,若无则返回默认配置
+     */
+    List<RechargeConfig> listByStationId(String stationId);
 }

+ 9 - 8
car-wash-service/src/main/java/com/kym/service/awoara/event/handle/OrderCloseEventHandler.java

@@ -163,25 +163,26 @@ public class OrderCloseEventHandler implements AwoaraEventHandler<OrderInfoObjec
 
     /**
      * 跨店消费分账 — V2 结算方案
-     * 仅记录跨店支出(充值款实收 × 70%)和跨店收入(充值款实收 × 70%)
+     * 以订单实收总额(充值余额 + 赠款余额)的 70% 为跨店转账基数
      * 平台服务费由结算日统一计算,70:30 拉新奖励比例在此体现
-     * 注意:按充值款实收金额 (rechargePayment) 计算,赠款为平台促销金,不参与站点间转账
+     * 赠款为归属站点自行发起的营销成本,参与跨店分账,由归属站承担
      *
      * @param washOrder     洗车订单
      * @param userStationId 用户归属站点(充值方)
      */
     @Transactional
     protected void doCrossSplit(WashOrder washOrder, String userStationId) {
-        // 仅以充值款实收金额为跨店转账基数,赠款为平台促销金,不参与站点间转账
         int rechargePayment = washOrder.getRechargePayment();
-        if (rechargePayment == 0) {
-            log.info("订单:{},充值款支付为0(全部为赠款支付),不产生跨店转账", washOrder.getOrderId());
+        int grantsPayment = washOrder.getGrantsPayment();
+        int totalPayment = rechargePayment + grantsPayment;
+        if (totalPayment == 0) {
+            log.info("订单:{},支付金额为0,不产生跨店转账", washOrder.getOrderId());
             return;
         }
         BigDecimal crossRate = new BigDecimal("0.7");
-        int crossAmount = crossRate.multiply(BigDecimal.valueOf(rechargePayment)).setScale(0, RoundingMode.DOWN).intValue();
-        log.info("订单:{},执行跨店分账,归属站:{},消费站:{},充值款实收:{},转账金额:{}",
-                washOrder.getOrderId(), userStationId, washOrder.getStationId(), rechargePayment, crossAmount);
+        int crossAmount = crossRate.multiply(BigDecimal.valueOf(totalPayment)).setScale(0, RoundingMode.DOWN).intValue();
+        log.info("订单:{},执行跨店分账,归属站:{},消费站:{},充值款实收:{},赠款实收:{},转账金额:{}",
+                washOrder.getOrderId(), userStationId, washOrder.getStationId(), rechargePayment, grantsPayment, crossAmount);
 
         // 归属站点跨店支出
         var crossExpend = new SplitRecord()

+ 13 - 0
car-wash-service/src/main/java/com/kym/service/impl/RechargeConfigServiceImpl.java

@@ -4,6 +4,7 @@ import com.kym.entity.RechargeConfig;
 import com.kym.mapper.RechargeConfigMapper;
 import com.kym.service.RechargeConfigService;
 import com.kym.service.mybatisplus.MyBaseServiceImpl;
+import java.util.List;
 import org.springframework.stereotype.Service;
 
 /**
@@ -21,4 +22,16 @@ public class RechargeConfigServiceImpl extends MyBaseServiceImpl<RechargeConfigM
     public RechargeConfig getRechargeConfigByAmount(Integer amount) {
         return lambdaQuery().eq(RechargeConfig::getRechargeAmount, amount).one();
     }
+
+    @Override
+    public List<RechargeConfig> listByStationId(String stationId) {
+        // 优先返回站点专属配置,若无则回退到默认配置
+        List<RechargeConfig> stationConfigs = lambdaQuery()
+                .eq(RechargeConfig::getStationId, stationId)
+                .list();
+        if (!stationConfigs.isEmpty()) {
+            return stationConfigs;
+        }
+        return lambdaQuery().isNull(RechargeConfig::getStationId).list();
+    }
 }

+ 19 - 1
car-wash-service/src/main/java/com/kym/service/impl/WashStationServiceImpl.java

@@ -3,11 +3,13 @@ package com.kym.service.impl;
 import com.github.pagehelper.PageHelper;
 import com.kym.common.utils.CommUtil;
 import com.kym.entity.common.PageBean;
+import com.kym.entity.RechargeConfig;
 import com.kym.entity.WashDevice;
 import com.kym.entity.WashStation;
 import com.kym.entity.queryParams.StationQueryParams;
 import com.kym.entity.vo.WashStationVo;
 import com.kym.mapper.WashStationMapper;
+import com.kym.service.RechargeConfigService;
 import com.kym.service.WashDeviceService;
 import com.kym.service.WashStationService;
 import com.kym.service.cache.KymCache;
@@ -37,9 +39,11 @@ public class WashStationServiceImpl extends MyBaseServiceImpl<WashStationMapper,
     }
 
     private final WashDeviceService washDeviceService;
+    private final RechargeConfigService rechargeConfigService;
 
-    public WashStationServiceImpl(WashDeviceService washDeviceService) {
+    public WashStationServiceImpl(WashDeviceService washDeviceService, RechargeConfigService rechargeConfigService) {
         this.washDeviceService = washDeviceService;
+        this.rechargeConfigService = rechargeConfigService;
     }
 
 
@@ -51,6 +55,20 @@ public class WashStationServiceImpl extends MyBaseServiceImpl<WashStationMapper,
         CommUtil.asserts(count == 0, "站点已存在");
         save(station);
 
+        // 自动使用默认充值配置:将平台默认配置复制一份关联到新站点
+        var defaultConfigs = rechargeConfigService.lambdaQuery()
+                .isNull(RechargeConfig::getStationId).list();
+        if (!defaultConfigs.isEmpty()) {
+            var stationConfigs = defaultConfigs.stream().map(c -> {
+                var config = new RechargeConfig();
+                config.setRechargeAmount(c.getRechargeAmount());
+                config.setGrantsAmount(c.getGrantsAmount());
+                config.setLabel(c.getLabel());
+                config.setStationId(station.getStationId());
+                return config;
+            }).toList();
+            rechargeConfigService.saveBatch(stationConfigs);
+        }
     }
 
     @Override

+ 10 - 2
docs/结算方案-业务说明.md

@@ -42,7 +42,8 @@
 **跨店转账比例**:70:30
 
 - 用户在 A 站充值、去 B 站消费 1,000 元时,A 站向 B 站转账 700 元(消费金额的 70%),A 站保留 300 元(30%)作为拉新奖励
-- 即跨店支出/收入以 70% 记账
+- 即跨店支出/收入以订单实收总额(充值余额 + 赠款余额)的 70% 记账
+- 赠款为归属站点自行发起的营销成本,参与跨店分账,由归属站承担
 
 **关键规则**:
 
@@ -118,11 +119,17 @@
   → 本站消费(A = B):
       记入 SplitRecord(type=消费, from=站点, to=站点),金额为消费全额
   → 跨店消费(A ≠ B):
-      记入 SplitRecord(type=跨店支出, from=A, to=B, amount=700)  ← 消费金额 × 70%
+      以订单实收总额(充值余额 + 赠款余额)的 70% 为跨店转账基数
+      记入 SplitRecord(type=跨店支出, from=A, to=B, amount=700)  ← 实收总额 × 70%
       记入 SplitRecord(type=跨店收入, from=A, to=B, amount=700)  ← 同上
       A 站保留 300 元(30%)作为拉新奖励
+      赠款为归属站点自行发起的营销成本,参与跨店分账,由归属站承担
 ```
 
+### 4.2.1 充值配置站点化
+
+每个站点可有独立的充值套餐配置(`t_recharge_config` 增加 `station_id` 字段)。`station_id` 为 NULL 的记录为平台默认配置,新站点创建时自动复制默认配置。用户充值页面按归属站展示对应套餐,无专属配置时回退到默认配置。
+
 ### 4.3 退款
 
 ```
@@ -232,4 +239,5 @@
 
 | 版本 | 日期 | 说明 |
 |---|---|---|
+| V2.1 | 2026-05-26 | 跨店分账基数调整为充值余额+赠款余额;充值配置站点化(`RechargeConfig.stationId`)+ 默认配置回退 |
 | V2-draft | 2026-05-14 | 初稿,基于业务讨论整理 |