Files
rent/docs/features/settlement-and-finance-system.md
T
2026-05-21 19:01:49 +08:00

20 KiB
Raw Blame History

结算与财务系统设计文档

版本v2.0
最后更新2026-05-21
状态 已实现并优化


📋 目录

  1. 系统概述
  2. 核心概念
  3. 资金流转流程
  4. 平台钱包设计
  5. 结算周期
  6. 服务费计算
  7. 数据库设计
  8. API接口
  9. 技术实现
  10. 前端页面
  11. 扩展方案

系统概述

结算与财务系统是平台的核心模块,负责处理订单支付、资金流转、商家结算、平台钱包和提现等业务。

核心功能

  • 订单支付时记录平台账户收入
  • 自动周结算(每周一凌晨2点)
  • 手动执行周结算(管理员触发)
  • 结算预览功能
  • 防止重复结算
  • 服务费自动计算和扣除
  • 平台钱包管理
  • 商家账户余额管理
  • 提现申请和审核
  • 完整的交易流水记录

核心概念

平台账户 vs 平台钱包

平台账户(Platform Account

  • 定位:资金汇总账户,记录所有资金流转
  • 包含内容
    • 用户支付的订单金额(全额)
    • 待结算给商家的金额
    • 平台的收入(服务费、广告费等)
    • 平台的支出(商家结算、邀请返现等)

平台钱包(Platform Wallet

  • 定位:平台可支配的资金
  • 包含内容
    • 服务费收入
    • 其他收入(广告费、会员费等)
    • 扣除:邀请返现等支出
  • 计算公式
    钱包余额 = 账户总收入 - 账户总支出
             = (订单总额) - (商家结算 + 邀请返现)
             = (商家应得 + 服务费) - 商家应得 - 邀请返现
             = 服务费 + 其他收入 - 邀请返现
    

三方账户体系

系统账户体系:
├─ 平台账户(PlatformAccount)
│  └─ 主账户(全局唯一,资金中转)
├─ 商家账户(MerchantAccount)
│  └─ 每个商家一个账户
└─ 用户账户(UserAccount)
   └─ 每个用户一个账户(用于返现、提现)

可提现金额

角色 可提现金额计算 说明
商家 balance - frozen_balance 已结算金额(扣除服务费后)
用户 balance - frozen_balance 邀请返现所得
平台 total_service_fee - frozen_balance 服务费收入(未来可扩展其他收入)

资金流转流程

完整流程图

┌─────────────┐
│  用户支付   │
└──────┬──────┘
       │ 订单金额(全额)
       ↓
┌─────────────────────┐
│   平台账户收入      │
│ - 记录交易流水      │
│ - 更新账户余额      │
│ - 累加服务费        │
└──────┬──────────────┘
       │ 等待订单完成
       ↓
┌─────────────────────┐
│   商家确认离店      │
│ - 订单状态: completed│
│ - 记录离店时间      │
└──────┬──────────────┘
       │ 等待周结算
       ↓
┌─────────────────────┐
│   周结算(自动)    │
│ - 每周一凌晨2点     │
│ - 统计上周完成订单  │
│ - 计算结算金额      │
└──────┬──────────────┘
       │ 结算金额 = 订单金额 - 服务费
       ↓
┌─────────────────────┐
│   商家账户入账      │
│ - 平台账户扣减      │
│ - 商家账户增加      │
│ - 复式记账          │
└──────┬──────────────┘
       │ 商家可提现
       ↓
┌─────────────────────┐
│   商家申请提现      │
│ - 冻结余额          │
│ - 管理员审核        │
│ - 确认打款          │
└─────────────────────┘

详细流程说明

1. 用户支付订单

用户支付 1000元
├─ 平台账户收入:+1000元
│  ├─ balance: +1000
│  ├─ total_income: +1000
│  └─ total_service_fee: +505%
└─ 商家账户:无变化(等待结算)

代码位置order.service.ts

await this.accountService.addPlatformBalance(
  order.payAmount,           // 订单金额(全额)
  order.serviceFee,          // 服务费(5%
  transactionNo,
  'order_payment',
  order.id,
  order.orderNo,
  `订单 ${order.orderNo} 支付收入`
);

2. 订单完成后周结算

每周一凌晨2点自动结算
├─ 计算商家应得:1000 × 95% = 950元
├─ 平台账户支出:-950元
│  ├─ balance: 1000 - 950 = 50
│  └─ total_expense: +950
├─ 商家账户收入:+950元
│  ├─ balance: +950
│  └─ total_income: +950
└─ 平台保留服务费:50元

代码位置settlement.service.ts

3. 邀请返现(可选)

用户邀请新用户注册
├─ 平台账户支出:-10元
│  ├─ balance: 50 - 10 = 40
│  └─ total_expense: +10
└─ 用户账户收入:+10元
   ├─ balance: +10
   └─ total_income: +10

4. 各方提现

商家提现:500元
├─ 商家账户:balance -500, frozen_balance +500
└─ 审核通过后:frozen_balance -500, total_expense +500

用户提现:10元
├─ 用户账户:balance -10, frozen_balance +10
└─ 审核通过后:frozen_balance -10, total_expense +10

平台提现:30元
├─ 平台账户:balance -30, frozen_balance +30
└─ 审核通过后:frozen_balance -30, total_expense +30

平台钱包设计

钱包余额计算

// 钱包余额 = 账户总收入 - 账户总支出
balance = total_income - total_expense

// 展开计算
balance = (订单总额) - (商家结算 + 邀请返现)
        = (商家应得 + 服务费) - 商家应得 - 邀请返现
        = 服务费 - 邀请返现

可提现金额计算

当前实现:

// 可提现金额 = 累计服务费 - 冻结余额
const withdrawableAmount = total_service_fee - frozen_balance;

未来扩展:

// 方案1:使用钱包余额(推荐)
const withdrawableAmount = balance - frozen_balance;

// 方案2:累加所有收入类型
const withdrawableAmount = (total_service_fee + total_ad_revenue + ...) - frozen_balance;

前端展示

核心指标卡片

┌─────────────┬─────────────┬─────────────┬─────────────┐
│ 钱包余额    │ 可提现金额  │ 冻结金额    │ 服务费收入  │
│ 40.00元     │ 40.00元     │ 0.00元      │ 50.00元     │
│ 平台可支配  │ 余额-冻结   │ 提现申请中  │ 累计服务费  │
└─────────────┴─────────────┴─────────────┴─────────────┘

收入支出统计

┌─────────────────┬─────────────────┬─────────────────┐
│ 账户总收入      │ 账户总支出      │ 其他收入        │
│ 1000.00元       │ 960.00元        │ 0.00元          │
│ 所有订单支付    │ 结算+返现       │ 广告费、会员费  │
└─────────────────┴─────────────────┴─────────────────┘

结算周期

周结算机制

执行时间:每周一凌晨 2:00
结算周期:上周一 00:00:00 ~ 上周日 23:59:59
结算对象:状态为 completed 的订单
判断依据:订单的 checkout_at(离店时间)字段

定时任务配置

文件位置settlement.schedule.ts

@Cron('0 2 * * 1') // 每周一凌晨2点
async handleWeeklySettlement() {
  await this.settlementService.handleWeeklySettlement();
}

结算逻辑

  1. 查询上周已完成订单

    const orders = await this.orderRepo
      .createQueryBuilder('o')
      .where('o.status = :status', { status: 'completed' })
      .andWhere('o.checkout_at BETWEEN :start AND :end', {
        start: `${lastWeekStart} 00:00:00`,
        end: `${lastWeekEnd} 23:59:59`
      })
      .getMany();
    
  2. 按商家分组统计

    • 订单数量:orderCount
    • 订单总额:orderAmount
    • 服务费总额:serviceFee
    • 结算金额:settlementAmount = orderAmount - serviceFee
  3. 生成结算单

    • 创建 Settlement 记录
    • 创建 SettlementItem 记录
  4. 执行资金转账

    • 平台账户扣减
    • 商家账户增加
    • 复式记账

防止重复结算

// 检查该周期是否已经结算过
const existingSettlements = await this.settlementRepo.count({
  where: {
    periodStart: lastWeekStart,
    periodEnd: lastWeekEnd
  }
});

if (existingSettlements > 0) {
  throw new Error(`该周期 ${lastWeekStart} ~ ${lastWeekEnd} 已经结算过,无法重复结算`);
}

服务费计算

计算公式

// 用户实付金额
payAmount = totalAmount - couponDiscount

// 服务费
serviceFee = Math.round(payAmount * serviceFeeRate * 100) / 100

// 商家实收金额
merchantIncome = payAmount - serviceFee

服务费率配置

默认值5% (0.05)
配置位置platform_configs
配置键service_fee_rate

示例计算

房费总额:1000元
优惠券抵扣:100元
服务费率:5%

用户实付:1000 - 100 = 900元
服务费:900 × 0.05 = 45元
商家实收:900 - 45 = 855元
平台收入:45元

数据库设计

核心表结构

1. platform_accounts(平台账户表)

CREATE TABLE `platform_accounts` (
  `id` BIGINT UNSIGNED AUTO_INCREMENT,
  `account_name` VARCHAR(50) COMMENT '账户名称',
  `balance` DECIMAL(12,2) DEFAULT 0 COMMENT '钱包余额',
  `frozen_balance` DECIMAL(12,2) DEFAULT 0 COMMENT '冻结余额',
  `total_income` DECIMAL(12,2) DEFAULT 0 COMMENT '账户总收入',
  `total_expense` DECIMAL(12,2) DEFAULT 0 COMMENT '账户总支出',
  `total_service_fee` DECIMAL(12,2) DEFAULT 0 COMMENT '累计服务费',
  `version` INT UNSIGNED DEFAULT 0 COMMENT '乐观锁版本号',
  `status` ENUM('active', 'frozen', 'closed') DEFAULT 'active',
  PRIMARY KEY (`id`)
);

字段说明:

字段 说明 当前用途 未来扩展
balance 钱包余额 服务费 - 邀请返现 所有收入 - 所有支出
frozen_balance 冻结余额 提现申请中的金额 同左
total_income 账户总收入 所有订单支付金额 订单 + 广告 + 会员等
total_expense 账户总支出 商家结算 + 邀请返现 同左 + 其他支出
total_service_fee 服务费收入 订单服务费累计 同左

2. merchant_accounts(商家账户表)

CREATE TABLE `merchant_accounts` (
  `id` BIGINT UNSIGNED AUTO_INCREMENT,
  `merchant_id` BIGINT UNSIGNED,
  `balance` DECIMAL(12,2) DEFAULT 0 COMMENT '可用余额',
  `frozen_balance` DECIMAL(12,2) DEFAULT 0 COMMENT '冻结余额',
  `debt_amount` DECIMAL(12,2) DEFAULT 0 COMMENT '欠款金额',
  `total_income` DECIMAL(12,2) DEFAULT 0 COMMENT '累计收入',
  `total_expense` DECIMAL(12,2) DEFAULT 0 COMMENT '累计支出',
  `total_settlement` DECIMAL(12,2) DEFAULT 0 COMMENT '累计结算',
  `total_withdraw` DECIMAL(12,2) DEFAULT 0 COMMENT '累计提现',
  `pending_settlement` DECIMAL(12,2) DEFAULT 0 COMMENT '待结算',
  `last_settlement_at` DATETIME COMMENT '最后结算时间',
  `version` INT UNSIGNED DEFAULT 0,
  `status` ENUM('active', 'frozen', 'closed') DEFAULT 'active',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_merchant_id` (`merchant_id`)
);

3. settlements(结算单表)

CREATE TABLE `settlements` (
  `id` BIGINT UNSIGNED AUTO_INCREMENT,
  `settlement_no` VARCHAR(32) UNIQUE,
  `merchant_id` BIGINT UNSIGNED,
  `period_start` DATE,
  `period_end` DATE,
  `order_count` INT UNSIGNED DEFAULT 0,
  `order_amount` DECIMAL(12,2) DEFAULT 0,
  `service_fee` DECIMAL(12,2) DEFAULT 0,
  `settlement_amount` DECIMAL(12,2) DEFAULT 0,
  `status` ENUM('pending','settled','failed'),
  `settled_at` DATETIME,
  PRIMARY KEY (`id`),
  KEY `idx_merchant_id` (`merchant_id`),
  KEY `idx_period` (`period_start`, `period_end`)
);

4. platform_transactions(平台交易流水表)

CREATE TABLE `platform_transactions` (
  `id` BIGINT UNSIGNED AUTO_INCREMENT,
  `transaction_no` VARCHAR(32) UNIQUE,
  `account_id` BIGINT UNSIGNED,
  `direction` ENUM('income','expense'),
  `amount` DECIMAL(12,2),
  `balance_before` DECIMAL(12,2),
  `balance_after` DECIMAL(12,2),
  `transaction_type` VARCHAR(50),
  `business_type` VARCHAR(50),
  `business_id` BIGINT UNSIGNED,
  `business_no` VARCHAR(32),
  `related_account_type` ENUM('merchant','user'),
  `related_account_id` BIGINT UNSIGNED,
  `remark` VARCHAR(255),
  PRIMARY KEY (`id`),
  KEY `idx_account_id` (`account_id`),
  KEY `idx_business` (`business_type`, `business_id`)
);

API接口

平台钱包接口

1. 查询平台账户

接口GET /api/admin/finance/platform-accounts
权限:平台管理员

响应示例

{
  "id": 1,
  "account_name": "主账户",
  "balance": 40.00,
  "frozen_balance": 0.00,
  "total_income": 1000.00,
  "total_expense": 960.00,
  "total_service_fee": 50.00,
  "status": "active"
}

2. 平台申请提现

接口POST /api/admin/finance/platform-withdrawals
权限:平台管理员

请求参数

{
  "amount": 30.00,
  "bankName": "中国工商银行",
  "bankAccount": "6222021234567890123",
  "accountName": "某某科技有限公司",
  "remark": "提现备注"
}

结算管理接口

3. 预览周结算

接口GET /api/admin/finance/settlements/preview-weekly
权限:平台管理员

响应示例

{
  "periodStart": "2026-05-13",
  "periodEnd": "2026-05-19",
  "totalOrders": 45,
  "totalAmount": 35000.00,
  "totalServiceFee": 1750.00,
  "totalSettlementAmount": 33250.00,
  "merchants": [...]
}

4. 执行周结算

接口POST /api/admin/finance/settlements/execute-weekly
权限:平台管理员


技术实现

核心服务

1. AccountService(账户服务)

文件位置account.service.ts

核心方法

// 平台账户增加余额(订单支付)
async addPlatformBalance(
  amount: number,
  serviceFee: number,
  transactionNo: string,
  businessType: string,
  businessId: number,
  businessNo: string,
  remark: string,
): Promise<void> {
  // 更新字段:
  // - balance: +amount
  // - total_income: +amount
  // - total_service_fee: +serviceFee
}

// 平台账户扣减余额(结算/返现)
async deductPlatformBalance(
  amount: number,
  transactionNo: string,
  transactionType: string,
  businessType: string,
  businessId: number,
  businessNo: string,
  remark: string,
): Promise<void> {
  // 更新字段:
  // - balance: -amount
  // - total_expense: +amount
}

2. WithdrawalService(提现服务)

文件位置withdrawal.service.ts

平台提现逻辑

async createPlatformWithdrawal(dto: {
  amount: number;
  bankName: string;
  bankAccount: string;
  accountName: string;
}) {
  const account = await this.accountService.getPlatformAccount();

  // 计算可提现金额
  const availableAmount = Number(account.total_service_fee) - Number(account.frozen_balance);

  if (availableAmount < amount) {
    throw new BadRequestException(`可提现金额不足,当前可提现:${availableAmount.toFixed(2)}元`);
  }

  // 冻结余额
  account.balance -= amount;
  account.frozen_balance += amount;
  
  // 创建提现记录
  // ...
}

并发控制

悲观锁

const account = await queryRunner.manager.findOne(PlatformAccount, {
  where: { account_name: '主账户' },
  lock: { mode: 'pessimistic_write' }  // 行级锁
});

乐观锁

await this.accountRepo.update(
  { id: accountId, version: currentVersion },
  { balance: newBalance, version: currentVersion + 1 }
);

复式记账

每笔转账生成两条流水记录:

  • 一条支出记录(平台账户)
  • 一条收入记录(商家账户)

两条流水的 transaction_no 相同,通过 related_account_typerelated_account_id 关联。


前端页面

1. 平台钱包页面

文件位置PlatformWallet.tsx

功能特性

  • 钱包余额、可提现金额、冻结金额、服务费收入展示
  • 账户总收入、总支出、其他收入统计
  • 钱包详情查看
  • 申请提现功能(集成在页面内)

2. 平台交易记录

文件位置PlatformTransactions.tsx

功能特性

  • 交易流水查询(按流水号、方向、类型、时间筛选)
  • 交易详情展示
  • 分页展示

3. 结算管理

文件位置Settlements.tsx

功能特性

  • 结算单列表查询
  • 结算单详情查看
  • 预览周结算数据
  • 手动执行周结算

4. 商家提现审核

文件位置Withdrawals.tsx

功能特性

  • 商家提现申请列表
  • 审核通过/拒绝
  • 确认打款

扩展方案

未来增加其他收入

1. 数据库增加字段

ALTER TABLE platform_accounts 
ADD COLUMN total_ad_revenue DECIMAL(12,2) DEFAULT 0 COMMENT '累计广告收入',
ADD COLUMN total_membership_fee DECIMAL(12,2) DEFAULT 0 COMMENT '累计会员费收入',
ADD COLUMN total_other_income DECIMAL(12,2) DEFAULT 0 COMMENT '累计其他收入';

2. 后端增加方法

// 广告收入
async addPlatformAdRevenue(amount: number, ...): Promise<void> {
  account.balance += amount;
  account.total_income += amount;
  account.total_ad_revenue += amount;
}

// 会员费收入
async addPlatformMembershipFee(amount: number, ...): Promise<void> {
  account.balance += amount;
  account.total_income += amount;
  account.total_membership_fee += amount;
}

3. 前端增加展示

<Statistic
  title="广告收入"
  value={account.total_ad_revenue}
  precision={2}
  suffix="元"
/>

<Statistic
  title="会员费收入"
  value={account.total_membership_fee}
  precision={2}
  suffix="元"
/>

4. 调整可提现计算

// 方案1:继续使用各项收入累加
const withdrawableAmount = (
  total_service_fee + 
  total_ad_revenue + 
  total_membership_fee
) - frozen_balance;

// 方案2:直接使用钱包余额(推荐)
const withdrawableAmount = balance - frozen_balance;

推荐方案2,因为:

  • balance 已经包含了所有收入和支出
  • 不需要每次增加新收入类型都修改代码
  • 更简洁、更易维护

数据一致性保证

1. 事务保证

所有涉及金额变动的操作都在事务中执行:

  • 订单支付:订单状态更新 + 平台账户收入
  • 结算转账:平台账户扣减 + 商家账户增加 + 结算单生成
  • 提现:余额冻结/扣减 + 提现记录更新

2. 乐观锁

账户表使用 version 字段实现乐观锁,防止并发更新导致余额错误。

3. 复式记账

每笔转账生成两条流水记录,确保资金流向可追溯。

4. CHECK 约束

CONSTRAINT `chk_balance` CHECK (`balance` >= 0)
CONSTRAINT `chk_frozen_balance` CHECK (`frozen_balance` >= 0)

相关文档


维护团队:开发团队
最后更新2026-05-21