Przeglądaj źródła

平台账户功能从站点账户中(虚拟站点)独立

skyline 1 tydzień temu
rodzic
commit
2d01d5f142

+ 22 - 1
car-wash-admin/src/main/java/com/kym/admin/controller/FinanceController.java

@@ -24,16 +24,21 @@ public class FinanceController {
     private final SettlementService settlementService;
     private final WxPayService wxPayService;
     private final RefundLogService refundLogService;
+    private final PlatformAccountService platformAccountService;
+    private final PlatformRevenueRecordService platformRevenueRecordService;
 
     public FinanceController(SplitRecordService splitRecordService, StationAccountService stationAccountService,
                              WithdrawnRecordService withdrawnRecordService, SettlementService settlementService,
-                             WxPayService wxPayService, RefundLogService refundLogService) {
+                             WxPayService wxPayService, RefundLogService refundLogService,
+                             PlatformAccountService platformAccountService, PlatformRevenueRecordService platformRevenueRecordService) {
         this.splitRecordService = splitRecordService;
         this.stationAccountService = stationAccountService;
         this.withdrawnRecordService = withdrawnRecordService;
         this.settlementService = settlementService;
         this.wxPayService = wxPayService;
         this.refundLogService = refundLogService;
+        this.platformAccountService = platformAccountService;
+        this.platformRevenueRecordService = platformRevenueRecordService;
     }
 
     /**
@@ -164,4 +169,20 @@ public class FinanceController {
         return R.success();
     }
 
+    /**
+     * 平台账户信息
+     */
+    @PostMapping("/platformAccount")
+    public R<?> platformAccount() {
+        return R.success(platformAccountService.getPlatformAccount());
+    }
+
+    /**
+     * 平台收入记录列表
+     */
+    @PostMapping("/platformRevenueRecords")
+    public R<?> platformRevenueRecords(@RequestBody CommonQueryParam params) {
+        return R.success(platformRevenueRecordService.listRevenueRecords(params));
+    }
+
 }

+ 46 - 0
car-wash-entity/src/main/java/com/kym/entity/PlatformAccount.java

@@ -0,0 +1,46 @@
+package com.kym.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.annotation.Version;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 平台账户
+ *
+ * @author skyline
+ * @since 2026-05-18
+ */
+@Getter
+@Setter
+@TableName("t_platform_account")
+public class PlatformAccount extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 累计服务费收入(分)
+     */
+    private Integer totalRevenue;
+
+    /**
+     * 可用余额(分)
+     */
+    private Integer availableBalance;
+
+    /**
+     * 提现冻结金额(分)
+     */
+    private Integer withdrawnFrozenAmount;
+
+    /**
+     * 已提现金额(分)
+     */
+    private Integer withdrawnAmount;
+
+    /**
+     * 乐观锁版本号
+     */
+    @Version
+    private Integer version;
+}

+ 41 - 0
car-wash-entity/src/main/java/com/kym/entity/PlatformRevenueRecord.java

@@ -0,0 +1,41 @@
+package com.kym.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+
+/**
+ * 平台服务费收入记录
+ *
+ * @author skyline
+ * @since 2026-05-18
+ */
+@Getter
+@Setter
+@Accessors(chain = true)
+@TableName("t_platform_revenue_record")
+public class PlatformRevenueRecord extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 关联结算单ID
+     */
+    private Long settlementRecordId;
+
+    /**
+     * 费用来源站点ID
+     */
+    private String stationId;
+
+    /**
+     * 结算周期
+     */
+    private String settlementPeriod;
+
+    /**
+     * 收入金额(分)
+     */
+    private Integer amount;
+}

+ 2 - 1
car-wash-entity/src/main/java/com/kym/entity/SplitRecord.java

@@ -22,8 +22,9 @@ public class SplitRecord extends BaseEntity {
     private static final long serialVersionUID = 1L;
 
     /**
-     * 交易类型(0-平台技术服务费,1-充值 2-消费 3-解冻(已废弃) 4-跨店支出 5-退款 6-跨店收入)
+     * @deprecated V2 结算方案中平台服务费由 t_platform_revenue_record 独立记录,不再使用此类型
      */
+    @Deprecated
     public static final Integer TYPE_PLATFORM = 0;
     public static final Integer TYPE_RECHARGE = 1;
     public static final Integer TYPE_CONSUME = 2;

+ 1 - 8
car-wash-entity/src/main/java/com/kym/entity/StationAccount.java

@@ -15,15 +15,8 @@ import lombok.Setter;
 @TableName("t_station_account")
 public class StationAccount extends BaseEntity {
 
-    /**
-     * 平台账户ID
-     */
-    public static final Integer PLATFORM_ACCOUNT_ID = 0;
-    /**
-     * 平台站点ID
-     */
-    public static final String PLATFORM_STATION_ID = "0";
     private static final long serialVersionUID = 1L;
+
     /**
      * 站点ID
      */

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

@@ -63,3 +63,47 @@ DROP PROCEDURE IF EXISTS add_available_balance;
 -- DELETE FROM `t_station_fee_rate`;
 -- DELETE FROM `t_platform_fee_rate` WHERE name != '默认费率';
 -- INSERT INTO `t_platform_fee_rate` (name, fee_rate, withdrawal_fee_rate) VALUES ('默认费率', 0.1, 0.006);
+
+-- ----------------------------
+-- 4. 新增平台账户表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_platform_account` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `total_revenue` INT NOT NULL DEFAULT 0 COMMENT '累计服务费收入(分)',
+    `available_balance` INT NOT NULL DEFAULT 0 COMMENT '可用余额(分)',
+    `withdrawn_frozen_amount` INT NOT NULL DEFAULT 0 COMMENT '提现冻结金额(分)',
+    `withdrawn_amount` INT NOT NULL DEFAULT 0 COMMENT '已提现金额(分)',
+    `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司(租户)ID',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台账户表';
+
+-- 初始化平台账户(仅当表为空时插入)
+INSERT INTO `t_platform_account` (`id`, `total_revenue`, `available_balance`, `withdrawn_frozen_amount`, `withdrawn_amount`, `version`)
+SELECT 1, 0, 0, 0, 0, 0
+WHERE NOT EXISTS (SELECT 1 FROM `t_platform_account` LIMIT 1);
+
+-- ----------------------------
+-- 5. 新增平台收入记录表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_platform_revenue_record` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `settlement_record_id` BIGINT NOT NULL COMMENT '关联结算单ID',
+    `station_id` VARCHAR(64) NOT NULL COMMENT '费用来源站点ID',
+    `settlement_period` VARCHAR(7) NOT NULL COMMENT '结算周期',
+    `amount` INT NOT NULL DEFAULT 0 COMMENT '收入金额(分)',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司(租户)ID',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_settlement_record_id` (`settlement_record_id`),
+    KEY `idx_station_id` (`station_id`),
+    KEY `idx_settlement_period` (`settlement_period`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台服务费收入记录表';
+
+-- ----------------------------
+-- 6. 清除虚拟站点(开发环境)
+-- ----------------------------
+-- DELETE FROM `t_station_account` WHERE `station_id` = '0';

+ 13 - 0
car-wash-mapper/src/main/java/com/kym/mapper/PlatformAccountMapper.java

@@ -0,0 +1,13 @@
+package com.kym.mapper;
+
+import com.kym.entity.PlatformAccount;
+import com.kym.mapper.mybatisplus.MyBaseMapper;
+
+/**
+ * 平台账户表 Mapper 接口
+ *
+ * @author skyline
+ * @since 2026-05-18
+ */
+public interface PlatformAccountMapper extends MyBaseMapper<PlatformAccount> {
+}

+ 13 - 0
car-wash-mapper/src/main/java/com/kym/mapper/PlatformRevenueRecordMapper.java

@@ -0,0 +1,13 @@
+package com.kym.mapper;
+
+import com.kym.entity.PlatformRevenueRecord;
+import com.kym.mapper.mybatisplus.MyBaseMapper;
+
+/**
+ * 平台收入记录表 Mapper 接口
+ *
+ * @author skyline
+ * @since 2026-05-18
+ */
+public interface PlatformRevenueRecordMapper extends MyBaseMapper<PlatformRevenueRecord> {
+}

+ 30 - 0
car-wash-service/src/main/java/com/kym/service/PlatformAccountService.java

@@ -0,0 +1,30 @@
+package com.kym.service;
+
+import com.kym.entity.PlatformAccount;
+import com.kym.entity.common.PageBean;
+import com.kym.entity.queryParams.CommonQueryParam;
+import com.kym.service.mybatisplus.MyBaseService;
+
+/**
+ * 平台账户服务
+ *
+ * @author skyline
+ * @since 2026-05-18
+ */
+public interface PlatformAccountService extends MyBaseService<PlatformAccount> {
+
+    /**
+     * 获取平台账户(单行)
+     */
+    PlatformAccount getPlatformAccount();
+
+    /**
+     * 增加平台收入
+     */
+    void addRevenue(int amount);
+
+    /**
+     * 平台账户列表(管理端)
+     */
+    PageBean<PlatformAccount> listPlatformAccounts(CommonQueryParam params);
+}

+ 26 - 0
car-wash-service/src/main/java/com/kym/service/PlatformRevenueRecordService.java

@@ -0,0 +1,26 @@
+package com.kym.service;
+
+import com.kym.entity.PlatformRevenueRecord;
+import com.kym.entity.SettlementRecord;
+import com.kym.entity.common.PageBean;
+import com.kym.entity.queryParams.CommonQueryParam;
+import com.kym.service.mybatisplus.MyBaseService;
+
+/**
+ * 平台收入记录服务
+ *
+ * @author skyline
+ * @since 2026-05-18
+ */
+public interface PlatformRevenueRecordService extends MyBaseService<PlatformRevenueRecord> {
+
+    /**
+     * 创建平台收入记录
+     */
+    void saveRecord(SettlementRecord settlement, String stationId);
+
+    /**
+     * 分页查询平台收入记录
+     */
+    PageBean<PlatformRevenueRecord> listRevenueRecords(CommonQueryParam params);
+}

+ 59 - 0
car-wash-service/src/main/java/com/kym/service/impl/PlatformAccountServiceImpl.java

@@ -0,0 +1,59 @@
+package com.kym.service.impl;
+
+import com.github.pagehelper.PageHelper;
+import com.kym.common.exception.BusinessException;
+import com.kym.entity.PlatformAccount;
+import com.kym.entity.common.PageBean;
+import com.kym.entity.queryParams.CommonQueryParam;
+import com.kym.mapper.PlatformAccountMapper;
+import com.kym.service.PlatformAccountService;
+import com.kym.service.mybatisplus.MyBaseServiceImpl;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 平台账户服务实现
+ *
+ * @author skyline
+ * @since 2026-05-18
+ */
+@Service
+public class PlatformAccountServiceImpl extends MyBaseServiceImpl<PlatformAccountMapper, PlatformAccount> implements PlatformAccountService {
+
+    @Override
+    public PlatformAccount getPlatformAccount() {
+        var account = lambdaQuery().last("LIMIT 1").one();
+        if (account == null) {
+            account = new PlatformAccount();
+            account.setTotalRevenue(0);
+            account.setAvailableBalance(0);
+            account.setWithdrawnFrozenAmount(0);
+            account.setWithdrawnAmount(0);
+            save(account);
+        }
+        return account;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void addRevenue(int amount) {
+        if (amount <= 0) {
+            return;
+        }
+        boolean updated = lambdaUpdate()
+                .setSql("total_revenue = total_revenue + {0}", amount)
+                .setSql("available_balance = available_balance + {0}", amount)
+                .eq(PlatformAccount::getId, getPlatformAccount().getId())
+                .update();
+        if (!updated) {
+            throw new BusinessException("平台账户更新失败");
+        }
+    }
+
+    @Override
+    public PageBean<PlatformAccount> listPlatformAccounts(CommonQueryParam params) {
+        PageHelper.startPage(params.getPageNum(), params.getPageSize());
+        var list = lambdaQuery().list();
+        return new PageBean<>(list);
+    }
+}

+ 42 - 0
car-wash-service/src/main/java/com/kym/service/impl/PlatformRevenueRecordServiceImpl.java

@@ -0,0 +1,42 @@
+package com.kym.service.impl;
+
+import com.github.pagehelper.PageHelper;
+import com.kym.entity.PlatformRevenueRecord;
+import com.kym.entity.SettlementRecord;
+import com.kym.entity.common.PageBean;
+import com.kym.entity.queryParams.CommonQueryParam;
+import com.kym.mapper.PlatformRevenueRecordMapper;
+import com.kym.service.PlatformRevenueRecordService;
+import com.kym.service.mybatisplus.MyBaseServiceImpl;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 平台收入记录服务实现
+ *
+ * @author skyline
+ * @since 2026-05-18
+ */
+@Service
+public class PlatformRevenueRecordServiceImpl extends MyBaseServiceImpl<PlatformRevenueRecordMapper, PlatformRevenueRecord> implements PlatformRevenueRecordService {
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void saveRecord(SettlementRecord settlement, String stationId) {
+        var record = new PlatformRevenueRecord()
+                .setSettlementRecordId(settlement.getId())
+                .setStationId(stationId)
+                .setSettlementPeriod(settlement.getSettlementPeriod())
+                .setAmount(settlement.getPlatformFee());
+        save(record);
+    }
+
+    @Override
+    public PageBean<PlatformRevenueRecord> listRevenueRecords(CommonQueryParam params) {
+        PageHelper.startPage(params.getPageNum(), params.getPageSize());
+        var list = lambdaQuery()
+                .orderByDesc(PlatformRevenueRecord::getId)
+                .list();
+        return new PageBean<>(list);
+    }
+}

+ 19 - 5
car-wash-service/src/main/java/com/kym/service/impl/SettlementServiceImpl.java

@@ -10,6 +10,8 @@ import com.kym.entity.queryParams.SettlementQueryParam;
 import com.kym.entity.vo.SettlementRecordVo;
 import com.kym.mapper.SettlementRecordMapper;
 import com.kym.service.SettlementService;
+import com.kym.service.PlatformAccountService;
+import com.kym.service.PlatformRevenueRecordService;
 import com.kym.service.SplitRecordService;
 import com.kym.service.StationAccountService;
 import com.kym.service.mybatisplus.MyBaseServiceImpl;
@@ -45,10 +47,15 @@ public class SettlementServiceImpl extends MyBaseServiceImpl<SettlementRecordMap
 
     private final SplitRecordService splitRecordService;
     private final StationAccountService stationAccountService;
+    private final PlatformAccountService platformAccountService;
+    private final PlatformRevenueRecordService platformRevenueRecordService;
 
-    public SettlementServiceImpl(SplitRecordService splitRecordService, StationAccountService stationAccountService) {
+    public SettlementServiceImpl(SplitRecordService splitRecordService, StationAccountService stationAccountService,
+                                 PlatformAccountService platformAccountService, PlatformRevenueRecordService platformRevenueRecordService) {
         this.splitRecordService = splitRecordService;
         this.stationAccountService = stationAccountService;
+        this.platformAccountService = platformAccountService;
+        this.platformRevenueRecordService = platformRevenueRecordService;
     }
 
     @Override
@@ -70,7 +77,6 @@ public class SettlementServiceImpl extends MyBaseServiceImpl<SettlementRecordMap
         // 站点 -> {recharge, refund, crossExpend, crossIncome}
         Set<String> stationIds = allRecords.stream()
                 .flatMap(r -> List.of(r.getFromStationId(), r.getToStationId()).stream())
-                .filter(id -> !StationAccount.PLATFORM_STATION_ID.equals(id))
                 .collect(Collectors.toSet());
 
         for (String stationId : stationIds) {
@@ -123,7 +129,15 @@ public class SettlementServiceImpl extends MyBaseServiceImpl<SettlementRecordMap
                     .eq(StationAccount::getStationId, stationId)
                     .update();
 
-            log.info("站点 {} 结算成功,金额:{} 分", stationId, available);
+            save(record);
+
+            // 平台服务费收入记录
+            if (platformFee > 0) {
+                platformRevenueRecordService.saveRecord(record, stationId);
+                platformAccountService.addRevenue(platformFee);
+            }
+
+            log.info("站点 {} 结算成功,金额:{} 分,平台费:{} 分", stationId, available, platformFee);
         } else {
             // 异常结算
             record.setSettlementAmount(0)
@@ -131,10 +145,10 @@ public class SettlementServiceImpl extends MyBaseServiceImpl<SettlementRecordMap
                     .setStatus(SettlementRecord.STATUS_异常结算)
                     .setRemark("结算金额为负,结转至下期");
 
+            save(record);
+
             log.warn("站点 {} 异常结算,结转金额:{} 分", stationId, available);
         }
-
-        save(record);
     }
 
     /**

+ 7 - 1
car-wash-service/src/test/java/com/kym/service/impl/SettlementServiceImplTest.java

@@ -3,6 +3,8 @@ package com.kym.service.impl;
 import com.kym.entity.SettlementRecord;
 import com.kym.entity.SplitRecord;
 import com.kym.entity.StationAccount;
+import com.kym.service.PlatformAccountService;
+import com.kym.service.PlatformRevenueRecordService;
 import com.kym.service.SplitRecordService;
 import com.kym.service.StationAccountService;
 import org.junit.jupiter.api.BeforeEach;
@@ -33,13 +35,17 @@ class SettlementServiceImplTest {
     private SplitRecordService splitRecordService;
     @Mock
     private StationAccountService stationAccountService;
+    @Mock
+    private PlatformAccountService platformAccountService;
+    @Mock
+    private PlatformRevenueRecordService platformRevenueRecordService;
 
     private SettlementServiceImpl settlementService;
     private List<SplitRecord> records;
 
     @BeforeEach
     void setUp() {
-        settlementService = new SettlementServiceImpl(splitRecordService, stationAccountService);
+        settlementService = new SettlementServiceImpl(splitRecordService, stationAccountService, platformAccountService, platformRevenueRecordService);
         records = new ArrayList<>();
     }
 

+ 228 - 0
docs/平台账户设计说明.md

@@ -0,0 +1,228 @@
+# 自助洗车 — 平台独立账户设计说明
+
+## 1. 背景与动机
+
+### 1.1 旧方案的问题
+
+V2 结算方案落地后,平台账户以"虚拟站点"的形式存在:
+
+- `t_station_account` 中 `station_id = "0"` 的行即为平台账户
+- `StationAccount` 类中定义了两个常量:`PLATFORM_ACCOUNT_ID = 0`、`PLATFORM_STATION_ID = "0"`
+- 月结时对各站点计算的 `platformFee`(10% 服务费)仅保存在 `SettlementRecord.platform_fee` 字段中,不产生任何平台收入流水
+- 平台累计收入无法从数据库直接查询,只能遍历所有结算记录求和得出
+
+**核心矛盾**:平台和站点在业务上是两个不同的主体,但在代码和数据模型上被强行捏合在一起。平台不是站点,不应和站点共用一张表、一套逻辑。
+
+### 1.2 改造成因
+
+随着 V2 结算方案对模型的简化(废弃冻结/解冻、废弃 70% 即时结算),旧方案中平台作为"虚拟站点"参与分账的必要性已经消失。新方案下平台只需做一件事:**在每次站点结算时,记录应收取的软件服务费收入**。
+
+这是一个独立的、只与平台自身相关的能力,理应由独立的模型承载。
+
+## 2. 设计目标
+
+1. **平台账户独立**:平台拥有自己的账户实体和数据表,与站点账户彻底解耦
+2. **收入显式记录**:每次结算产生的平台服务费,生成一条可追溯的收入记录
+3. **审计友好**:`结算单 → 平台收入记录 → 平台账户余额` 形成完整的资金链路
+4. **对现有流程侵入最小**:结算公式、分账逻辑、提现流程均不变
+
+## 3. 数据模型
+
+### 3.1 平台账户 — `t_platform_account`
+
+单行表,整个平台只有一个账户。字段设计对标 `t_station_account`,预留在途提现相关字段供后续迭代。
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| `id` | bigint | 主键 |
+| `total_revenue` | int | 累计服务费收入(分),只增不减 |
+| `available_balance` | int | 可用余额(分),当前已结算可动用的服务费收入 |
+| `withdrawn_frozen_amount` | int | 提现冻结金额(分),预留 |
+| `withdrawn_amount` | int | 已提现金额(分),预留 |
+| `version` | int | 乐观锁版本号,防并发写 |
+| `create_time` | datetime | 创建时间 |
+| `update_time` | datetime | 更新时间 |
+
+**初始化策略**:SQL 脚本中用 `INSERT ... WHERE NOT EXISTS` 在首次部署时自动创建一行全零记录;Java 层 `getPlatformAccount()` 也做了懒初始化兜底。
+
+### 3.2 平台收入记录 — `t_platform_revenue_record`
+
+每次正常结算产生一条,与结算单一一对应(`settlement_record_id`),可追溯每笔平台费的来源站点和所属结算周期。
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| `id` | bigint | 主键 |
+| `settlement_record_id` | bigint | 关联的结算单 ID |
+| `station_id` | varchar | 费用来源站点 |
+| `settlement_period` | varchar | 结算周期(如 `2026-05`) |
+| `amount` | int | 收入金额(分) |
+| `create_time` | datetime | 创建时间 |
+
+索引:
+- `idx_settlement_record_id` — 按结算单反查
+- `idx_station_id` — 按来源站点汇总
+- `idx_settlement_period` — 按周期汇总
+
+### 3.3 与结算单的关系
+
+```
+t_settlement_record(结算单)
+    │
+    │  settlement_record_id ──────┐
+    │  platform_fee               │
+    │                              │
+    ▼                              ▼
+t_platform_revenue_record(平台收入记录)
+    │  settlement_record_id  ← 外键关联
+    │  station_id            ← 费用来源
+    │  settlement_period     ← 结算周期
+    │  amount                ← 对应 platform_fee
+    │
+    ▼
+t_platform_account(平台账户)
+    │  total_revenue   += platform_fee
+    │  available_balance += platform_fee
+```
+
+## 4. 业务流程
+
+### 4.1 结算时平台收入记录
+
+```
+每月 15 日定时任务 / 手动触发结算
+  │
+  ├── 汇总各站点上月 SplitRecord(充值、退款、跨店收支)
+  ├── 计算平台费基数、平台费、结算金额(公式不变)
+  │
+  ├── 正常结算(available > 0):
+  │     ├── 更新站点 availableBalance
+  │     ├── 保存 SettlementRecord
+  │     ├── [新增] 创建 PlatformRevenueRecord(platformFee > 0 时)
+  │     └── [新增] 更新 PlatformAccount.total_revenue += platformFee
+  │                 更新 PlatformAccount.available_balance += platformFee
+  │
+  └── 异常结算(available ≤ 0):
+        ├── 保存 SettlementRecord(status=异常结算)
+        └── 不产生平台收入(当期无实际结算金额)
+```
+
+### 4.2 不变的部分
+
+- 充值、消费、退款、跨店消费的分账记录(`t_split_record`)生成逻辑完全不变
+- 结算公式不变:`平台费基数 = 总充值 - 总退款 - 跨店支出 + 跨店收入`
+- 10% 费率不变(`PLATFORM_FEE_RATE = 0.1`)
+- 站点账户的提现流程不变
+
+## 5. 关键设计决策
+
+### 5.1 为什么不用 `SplitRecord.TYPE_PLATFORM` 记录平台收入
+
+`SplitRecord`(`t_split_record`)的语义是"分账流水"——记录资金在站点之间的流转关系。平台收入并非"站点间分账",而是"站点向平台缴纳的服务费",属于不同业务域。混入同一张表会导致:
+
+- 查询分账记录时需要额外过滤掉平台记录
+- 分账汇总逻辑需要感知平台概念(这正是本次要消除的问题)
+- `fromStationId` / `toStationId` 字段对平台记录没有实际意义
+
+因此选择了独立的 `t_platform_revenue_record`。`SplitRecord.TYPE_PLATFORM = 0` 保留但标记 `@Deprecated`,防止未来有人误用。
+
+### 5.2 为什么异常结算不产生平台收入
+
+异常结算意味着站点当期结算金额为负(`available ≤ 0`),平台费虽然计算出来了但并未实际收取——站点没有收到可提现金额,平台也不应确认收入。负金额结转到下期(`closingPendingBalance`),待下期资金回正后一并结算。
+
+### 5.3 为什么平台账户使用乐观锁
+
+`t_platform_account` 是单行热点数据——每次站点结算成功都会更新。虽然当前结算流程是单事务串行(一个站点接一个站点),但预留乐观锁(`@Version`)可以在未来引入并行结算或手动运维操作时避免数据写覆盖。
+
+### 5.4 为什么预留提现字段
+
+平台账户当前是纯记账模型(只记录累计收入和可用余额),但平台未来可能需要将可用余额提现到企业银行账户。预留 `withdrawn_frozen_amount`、`withdrawn_amount` 字段使数据结构与站点账户保持一致,未来实现平台提现时无需改表。
+
+## 6. API 接口
+
+两个新端点均挂载在 `FinanceController` 下,复用现有权限体系。
+
+| 方法 | 路径 | 说明 |
+|---|---|---|
+| POST | `/finance/platformAccount` | 查询平台账户信息(单行) |
+| POST | `/finance/platformRevenueRecords` | 分页查询平台收入记录,支持 `pageNum`/`pageSize` |
+
+响应示例:
+
+```json
+// POST /finance/platformAccount
+{
+  "code": 200,
+  "data": {
+    "id": 1,
+    "totalRevenue": 150000,
+    "availableBalance": 150000,
+    "withdrawnFrozenAmount": 0,
+    "withdrawnAmount": 0,
+    "version": 15
+  }
+}
+
+// POST /finance/platformRevenueRecords
+{
+  "code": 200,
+  "data": {
+    "total": 15,
+    "records": [
+      {
+        "id": 1,
+        "settlementRecordId": 42,
+        "stationId": "10001",
+        "settlementPeriod": "2026-05",
+        "amount": 930
+      }
+    ]
+  }
+}
+```
+
+## 7. 数据对账
+
+平台总收入应与结算单中的平台费汇总一致:
+
+```sql
+-- 平台账户累计收入
+SELECT total_revenue FROM t_platform_account WHERE id = 1;
+
+-- 结算单平台费合计(应等于上述值)
+SELECT SUM(platform_fee) FROM t_settlement_record WHERE status = 1;
+
+-- 平台收入记录合计(应等于上述值)
+SELECT SUM(amount) FROM t_platform_revenue_record;
+```
+
+三条查询结果应始终相等。如有偏差,说明存在直接操作数据库的旁路行为。
+
+## 8. 注意事项
+
+1. **`StationAccount.PLATFORM_ACCOUNT_ID` 和 `PLATFORM_STATION_ID` 已删除**。任何引用这些常量的旧代码需要同步移除。
+2. **`t_station_account` 中 `station_id = '0'` 的行应在部署时手动清除**(SQL 脚本中有注释掉的 DELETE 语句,按需执行)。
+3. **`SplitRecord.TYPE_PLATFORM = 0` 已废弃**,禁止在新增代码中使用。
+4. **平台账户不支持直接写入**,所有收入必须通过结算流程产生(`platformAccountService.addRevenue()` 仅在 `SettlementServiceImpl` 中调用)。
+5. **当前不支持平台提现**,`withdrawn_frozen_amount` 和 `withdrawn_amount` 字段仅供后续迭代使用。
+
+## 9. 文件清单
+
+| 文件 | 说明 |
+|---|---|
+| `car-wash-entity/.../entity/PlatformAccount.java` | 平台账户实体 |
+| `car-wash-entity/.../entity/PlatformRevenueRecord.java` | 平台收入记录实体 |
+| `car-wash-mapper/.../mapper/PlatformAccountMapper.java` | 平台账户 Mapper |
+| `car-wash-mapper/.../mapper/PlatformRevenueRecordMapper.java` | 平台收入记录 Mapper |
+| `car-wash-service/.../service/PlatformAccountService.java` | 平台账户服务接口 |
+| `car-wash-service/.../service/PlatformRevenueRecordService.java` | 平台收入记录服务接口 |
+| `car-wash-service/.../service/impl/PlatformAccountServiceImpl.java` | 平台账户服务实现 |
+| `car-wash-service/.../service/impl/PlatformRevenueRecordServiceImpl.java` | 平台收入记录服务实现 |
+| `car-wash-service/.../service/impl/SettlementServiceImpl.java` | 结算服务(核心改动点) |
+| `car-wash-admin/.../controller/FinanceController.java` | 财务接口(新增两个端点) |
+| `car-wash-entity/.../resources/sql/v2_settlement.sql` | V2 结算 SQL(新增两张表) |
+
+## 10. 版本记录
+
+| 版本 | 日期 | 说明 |
+|---|---|---|
+| V1 | 2026-05-18 | 初稿,平台独立账户功能落地 |