From 4c7a1e06a8eeef20e2a62ea1f9c172812600623f Mon Sep 17 00:00:00 2001 From: xiaoquan <838115837@qq.com> Date: Fri, 22 May 2026 18:54:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BF=AD=E4=BB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/miniapp/src/pages/login/index.vue | 9 +- apps/miniapp/src/utils/request.ts | 8 + .../src/pages/finance/Dashboard.tsx | 61 +- .../src/entities/platform-account.entity.ts | 9 +- .../src/entities/system-account.entity.ts | 40 + .../src/entities/system-transaction.entity.ts | 48 + .../modules/app/activity/activity.module.ts | 2 + .../modules/app/activity/activity.service.ts | 28 + .../src/modules/app/order/order.service.ts | 10 +- .../modules/shared/finance/account.service.ts | 308 ++++- .../modules/shared/finance/finance.module.ts | 6 + .../modules/shared/finance/refund.service.ts | 49 +- .../modules/shared/finance/report.service.ts | 40 +- .../shared/finance/settlement.service.ts | 9 +- .../shared/finance/withdrawal.service.ts | 31 +- database/migrations/001_init_schema.sql | 69 +- docs/README.md | 84 +- docs/database/finance-database.md | 598 +++++++--- docs/features/finance-system.md | 1058 +++++++++++++++++ .../features/settlement-and-finance-system.md | 789 ------------ 20 files changed, 2169 insertions(+), 1087 deletions(-) create mode 100644 apps/server/src/entities/system-account.entity.ts create mode 100644 apps/server/src/entities/system-transaction.entity.ts create mode 100644 docs/features/finance-system.md delete mode 100644 docs/features/settlement-and-finance-system.md diff --git a/apps/miniapp/src/pages/login/index.vue b/apps/miniapp/src/pages/login/index.vue index 0e40907..75885b7 100644 --- a/apps/miniapp/src/pages/login/index.vue +++ b/apps/miniapp/src/pages/login/index.vue @@ -335,9 +335,14 @@ async function handlePhoneLogin() { setTimeout(() => { uni.switchTab({ url: '/pages/index/index' }); }, 500); - } catch (e) { + } catch (error: any) { uni.hideLoading(); - console.error(e); + console.error('手机号登录失败', error); + uni.showToast({ + title: error.message || '登录失败,请重试', + icon: 'none', + duration: 2000 + }); } } diff --git a/apps/miniapp/src/utils/request.ts b/apps/miniapp/src/utils/request.ts index 7acb431..383d093 100644 --- a/apps/miniapp/src/utils/request.ts +++ b/apps/miniapp/src/utils/request.ts @@ -83,6 +83,14 @@ export function request(options: RequestOptions): Promise { {overview && ( <> - {/* 平台总账户余额 - 突出显示 */} + {/* 系统总账户金额 - 突出显示 */} 平台总账户余额} - value={Number(overview.platformBalance || 0) + Number(overview.totalMerchantBalance || 0) + Number(overview.totalUserBalance || 0)} + title={系统总账户金额} + value={Number(overview.systemTotalAmount || 0)} prefix={} suffix={} precision={2} valueStyle={{ color: '#fff', fontSize: 36, fontWeight: 'bold' }} />
- 平台内所有未提现的资金总额(包含平台钱包、商家账户、用户账户) + 用户实付(¥{Number(overview.totalUserPaid || 0).toFixed(2)})- 退款(¥{Number(overview.totalRefund || 0).toFixed(2)})- 提现(¥{Number(overview.totalWithdrawn || 0).toFixed(2)}) +
+
+ = 商家账户余额(¥{Number(overview.totalMerchantBalance || 0).toFixed(2)})+ 用户账户余额(¥{Number(overview.totalUserBalance || 0).toFixed(2)})+ 平台净收益(¥{Number(overview.platformBalance || 0).toFixed(2)})
@@ -127,13 +135,16 @@ const FinanceDashboard: React.FC = () => { } suffix="元" valueStyle={{ color: '#1890ff' }} /> +
+ 服务费收入 - 邀请返现 +
@@ -146,6 +157,9 @@ const FinanceDashboard: React.FC = () => { suffix="元" valueStyle={{ color: '#52c41a' }} /> +
+ {overview.userCount} 个用户账户 +
@@ -158,23 +172,25 @@ const FinanceDashboard: React.FC = () => { suffix="元" valueStyle={{ color: '#faad14' }} /> +
+ {overview.merchantCount} 个商家账户 +
- + { /> - + { /> + + + = 0 ? '#52c41a' : '#ff4d4f' + }} + /> + + , @InjectRepository(User) private userRepo: Repository, + private accountService: AccountService, + private transactionService: TransactionService, private dataSource: DataSource, ) {} @@ -415,6 +419,30 @@ export class ActivityService { ); } + // 账户操作:给邀请人用户账户增加返现金额,从平台账户扣减 + const transactionNo = this.transactionService.generateTransactionNo(); + + // 给用户账户增加返现 + await this.accountService.addUserBalance( + invitation.inviterId, + amount, + transactionNo, + 'invite_cashback', + cashback.id, + order.orderNo, + `邀请返现 - 订单${order.orderNo}第${orderIndex}单`, + ); + + // 从平台账户扣减返现支出 + await this.accountService.deductPlatformCashback( + amount, + transactionNo, + 'invite_cashback', + cashback.id, + order.orderNo, + `邀请返现支出 - 用户${invitation.inviterId}订单${order.orderNo}`, + ); + await queryRunner.commitTransaction(); this.logger.log( `订单 ${order.orderNo} 完成,邀请人 ${invitation.inviterId} 获得第${orderIndex}单返现 ${amount} 元(比例${rate * 100}%)`, diff --git a/apps/server/src/modules/app/order/order.service.ts b/apps/server/src/modules/app/order/order.service.ts index db27a0e..214872b 100644 --- a/apps/server/src/modules/app/order/order.service.ts +++ b/apps/server/src/modules/app/order/order.service.ts @@ -329,11 +329,10 @@ export class OrderService { paidAt: new Date(), }); - // 记录平台账户收入 + // 记录系统总账户收入(用户实付金额) const transactionNo = `TXN${Date.now()}${Math.floor(Math.random() * 10000)}`; - await this.accountService.addPlatformBalance( + await this.accountService.addSystemIncome( order.payAmount, - order.serviceFee || 0, transactionNo, 'order_payment', order.id, @@ -378,11 +377,10 @@ export class OrderService { paidAt: new Date(), }); - // 记录平台账户收入 + // 记录系统总账户收入(用户实付金额) const transactionNo = `TXN${Date.now()}${Math.floor(Math.random() * 10000)}`; - await this.accountService.addPlatformBalance( + await this.accountService.addSystemIncome( order.payAmount, - order.serviceFee || 0, transactionNo, 'order_payment', order.id, diff --git a/apps/server/src/modules/shared/finance/account.service.ts b/apps/server/src/modules/shared/finance/account.service.ts index d47272d..4b153b7 100644 --- a/apps/server/src/modules/shared/finance/account.service.ts +++ b/apps/server/src/modules/shared/finance/account.service.ts @@ -4,9 +4,11 @@ import { Repository, DataSource, FindOptionsWhere } from 'typeorm'; import { UserAccount } from '@/entities/user-account.entity'; import { MerchantAccount } from '@/entities/merchant-account.entity'; import { PlatformAccount } from '@/entities/platform-account.entity'; +import { SystemAccount } from '@/entities/system-account.entity'; import { UserTransaction } from '@/entities/user-transaction.entity'; import { MerchantTransaction } from '@/entities/merchant-transaction.entity'; import { PlatformTransaction } from '@/entities/platform-transaction.entity'; +import { SystemTransaction } from '@/entities/system-transaction.entity'; import { QueryUserAccountsDto, QueryMerchantAccountsDto } from './dto/account.dto'; @Injectable() @@ -18,12 +20,16 @@ export class AccountService { private merchantAccountRepo: Repository, @InjectRepository(PlatformAccount) private platformAccountRepo: Repository, + @InjectRepository(SystemAccount) + private systemAccountRepo: Repository, @InjectRepository(UserTransaction) private userTransactionRepo: Repository, @InjectRepository(MerchantTransaction) private merchantTransactionRepo: Repository, @InjectRepository(PlatformTransaction) private platformTransactionRepo: Repository, + @InjectRepository(SystemTransaction) + private systemTransactionRepo: Repository, private dataSource: DataSource, ) {} @@ -429,10 +435,9 @@ export class AccountService { } /** - * 平台账户增加余额(订单收入) + * 平台账户增加服务费收入 */ - async addPlatformBalance( - amount: number, + async addPlatformServiceFee( serviceFee: number, transactionNo: string, businessType: string, @@ -454,14 +459,12 @@ export class AccountService { throw new NotFoundException('平台账户不存在'); } - const balanceBefore = Number(account.balance); - const amountNum = Number(amount); const serviceFeeNum = Number(serviceFee); - const balanceAfter = parseFloat((balanceBefore + amountNum).toFixed(2)); + const balanceBefore = Number(account.balance); + const balanceAfter = parseFloat((balanceBefore + serviceFeeNum).toFixed(2)); account.balance = balanceAfter; - account.total_income = parseFloat((Number(account.total_income) + amountNum).toFixed(2)); - account.total_service_fee = parseFloat((Number(account.total_service_fee) + serviceFeeNum).toFixed(2)); + account.total_income = parseFloat((Number(account.total_income) + serviceFeeNum).toFixed(2)); account.version += 1; await queryRunner.manager.save(account); @@ -470,10 +473,10 @@ export class AccountService { transaction_no: transactionNo, account_id: account.id, direction: 'income', - amount, + amount: serviceFeeNum, balance_before: balanceBefore, balance_after: balanceAfter, - transaction_type: '订单收入', + transaction_type: '服务费收入', business_type: businessType, business_id: businessId, business_no: businessNo, @@ -491,12 +494,11 @@ export class AccountService { } /** - * 平台账户扣减余额(结算/返现/提现) + * 平台账户扣减邀请返现支出 */ - async deductPlatformBalance( - amount: number, + async deductPlatformCashback( + cashbackAmount: number, transactionNo: string, - transactionType: string, businessType: string, businessId: number, businessNo: string, @@ -516,21 +518,12 @@ export class AccountService { throw new NotFoundException('平台账户不存在'); } + const cashbackNum = Number(cashbackAmount); const balanceBefore = Number(account.balance); - if (balanceBefore < amount) { - throw new BadRequestException('平台账户余额不足'); - } - - const balanceAfter = balanceBefore - amount; + const balanceAfter = parseFloat((balanceBefore - cashbackNum).toFixed(2)); account.balance = balanceAfter; - account.total_expense = Number(account.total_expense) + amount; - - if (transactionType === '商家结算') { - } else if (transactionType === '邀请返现') { - } else if (transactionType === '提现') { - } - + account.total_expense = parseFloat((Number(account.total_expense) + cashbackNum).toFixed(2)); account.version += 1; await queryRunner.manager.save(account); @@ -539,10 +532,73 @@ export class AccountService { transaction_no: transactionNo, account_id: account.id, direction: 'expense', - amount, + amount: cashbackNum, balance_before: balanceBefore, balance_after: balanceAfter, - transaction_type: transactionType, + transaction_type: '邀请返现', + business_type: businessType, + business_id: businessId, + business_no: businessNo, + remark, + }); + + await queryRunner.manager.save(transaction); + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + + /** + * 平台账户扣减提现 + */ + async deductPlatformWithdrawal( + amount: number, + transactionNo: string, + businessType: string, + businessId: number, + businessNo: string, + remark: string, + ): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const account = await queryRunner.manager.findOne(PlatformAccount, { + where: { account_name: '主账户' }, + lock: { mode: 'pessimistic_write' }, + }); + + if (!account) { + throw new NotFoundException('平台账户不存在'); + } + + const amountNum = Number(amount); + const balanceBefore = Number(account.balance); + + if (balanceBefore < amountNum) { + throw new BadRequestException('平台账户余额不足'); + } + + const balanceAfter = parseFloat((balanceBefore - amountNum).toFixed(2)); + + account.balance = balanceAfter; + account.version += 1; + + await queryRunner.manager.save(account); + + const transaction = this.platformTransactionRepo.create({ + transaction_no: transactionNo, + account_id: account.id, + direction: 'expense', + amount: amountNum, + balance_before: balanceBefore, + balance_after: balanceAfter, + transaction_type: '提现', business_type: businessType, business_id: businessId, business_no: businessNo, @@ -709,4 +765,200 @@ export class AccountService { totalPendingSettlement: Number(result?.totalPendingSettlement || 0), }; } + + // ============================================================ + // 系统总账户操作方法 + // ============================================================ + + /** + * 获取系统总账户 + */ + async getSystemAccount(accountName: string = '主账户'): Promise { + const account = await this.systemAccountRepo.findOne({ where: { account_name: accountName } }); + + if (!account) { + throw new NotFoundException( + `系统总账户"${accountName}"不存在,请先执行数据库初始化脚本创建系统总账户` + ); + } + + return account; + } + + /** + * 系统总账户增加收入(用户支付订单) + */ + async addSystemIncome( + amount: number, + transactionNo: string, + businessType: string, + businessId: number, + businessNo: string, + remark: string, + ): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const account = await queryRunner.manager.findOne(SystemAccount, { + where: { account_name: '主账户' }, + lock: { mode: 'pessimistic_write' }, + }); + + if (!account) { + throw new NotFoundException('系统总账户不存在'); + } + + const amountNum = Number(amount); + const balanceBefore = Number(account.balance); + const balanceAfter = parseFloat((balanceBefore + amountNum).toFixed(2)); + + account.balance = balanceAfter; + account.total_income = parseFloat((Number(account.total_income) + amountNum).toFixed(2)); + account.version += 1; + + await queryRunner.manager.save(account); + + const transaction = this.systemTransactionRepo.create({ + transactionNo: transactionNo, + accountId: account.id, + direction: 'income', + amount: amountNum, + balanceBefore: balanceBefore, + balanceAfter: balanceAfter, + transactionType: '用户支付', + businessType: businessType, + businessId: businessId, + businessNo: businessNo, + remark, + }); + + await queryRunner.manager.save(transaction); + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + + /** + * 系统总账户增加退款支出 + */ + async addSystemRefund( + amount: number, + transactionNo: string, + businessType: string, + businessId: number, + businessNo: string, + remark: string, + ): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const account = await queryRunner.manager.findOne(SystemAccount, { + where: { account_name: '主账户' }, + lock: { mode: 'pessimistic_write' }, + }); + + if (!account) { + throw new NotFoundException('系统总账户不存在'); + } + + const amountNum = Number(amount); + const balanceBefore = Number(account.balance); + const balanceAfter = parseFloat((balanceBefore - amountNum).toFixed(2)); + + account.balance = balanceAfter; + account.total_refund = parseFloat((Number(account.total_refund) + amountNum).toFixed(2)); + account.version += 1; + + await queryRunner.manager.save(account); + + const transaction = this.systemTransactionRepo.create({ + transactionNo: transactionNo, + accountId: account.id, + direction: 'expense', + amount: amountNum, + balanceBefore: balanceBefore, + balanceAfter: balanceAfter, + transactionType: '退款', + businessType: businessType, + businessId: businessId, + businessNo: businessNo, + remark, + }); + + await queryRunner.manager.save(transaction); + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + + /** + * 系统总账户增加提现支出 + */ + async addSystemWithdrawal( + amount: number, + transactionNo: string, + businessType: string, + businessId: number, + businessNo: string, + remark: string, + ): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const account = await queryRunner.manager.findOne(SystemAccount, { + where: { account_name: '主账户' }, + lock: { mode: 'pessimistic_write' }, + }); + + if (!account) { + throw new NotFoundException('系统总账户不存在'); + } + + const amountNum = Number(amount); + const balanceBefore = Number(account.balance); + const balanceAfter = parseFloat((balanceBefore - amountNum).toFixed(2)); + + account.balance = balanceAfter; + account.total_withdrawn = parseFloat((Number(account.total_withdrawn) + amountNum).toFixed(2)); + account.version += 1; + + await queryRunner.manager.save(account); + + const transaction = this.systemTransactionRepo.create({ + transactionNo: transactionNo, + accountId: account.id, + direction: 'expense', + amount: amountNum, + balanceBefore: balanceBefore, + balanceAfter: balanceAfter, + transactionType: '提现', + businessType: businessType, + businessId: businessId, + businessNo: businessNo, + remark, + }); + + await queryRunner.manager.save(transaction); + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } } diff --git a/apps/server/src/modules/shared/finance/finance.module.ts b/apps/server/src/modules/shared/finance/finance.module.ts index 4ad32b4..d6f608e 100644 --- a/apps/server/src/modules/shared/finance/finance.module.ts +++ b/apps/server/src/modules/shared/finance/finance.module.ts @@ -5,12 +5,15 @@ import { SettlementItem } from '@/entities/settlement-item.entity'; import { UserWithdrawal } from '@/entities/user-withdrawal.entity'; import { MerchantWithdrawal } from '@/entities/merchant-withdrawal.entity'; import { PlatformWithdrawal } from '@/entities/platform-withdrawal.entity'; +import { MktInviteWithdrawal } from '@/entities/mkt-invite-withdrawal.entity'; import { UserAccount } from '@/entities/user-account.entity'; import { MerchantAccount } from '@/entities/merchant-account.entity'; import { PlatformAccount } from '@/entities/platform-account.entity'; +import { SystemAccount } from '@/entities/system-account.entity'; import { UserTransaction } from '@/entities/user-transaction.entity'; import { MerchantTransaction } from '@/entities/merchant-transaction.entity'; import { PlatformTransaction } from '@/entities/platform-transaction.entity'; +import { SystemTransaction } from '@/entities/system-transaction.entity'; import { DailyReconciliation } from '@/entities/daily-reconciliation.entity'; import { PlatformConfig } from '@/entities/platform-config.entity'; import { Merchant } from '@/entities/merchant.entity'; @@ -33,12 +36,15 @@ import { MerchantModule } from '@/modules/merchant/merchant.module'; UserWithdrawal, MerchantWithdrawal, PlatformWithdrawal, + MktInviteWithdrawal, UserAccount, MerchantAccount, PlatformAccount, + SystemAccount, UserTransaction, MerchantTransaction, PlatformTransaction, + SystemTransaction, DailyReconciliation, PlatformConfig, Merchant, diff --git a/apps/server/src/modules/shared/finance/refund.service.ts b/apps/server/src/modules/shared/finance/refund.service.ts index fabf133..e2de355 100644 --- a/apps/server/src/modules/shared/finance/refund.service.ts +++ b/apps/server/src/modules/shared/finance/refund.service.ts @@ -3,8 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository, DataSource } from 'typeorm'; import { ConfigService } from '@nestjs/config'; import { Order } from '@/entities/order.entity'; -import { PlatformAccount } from '@/entities/platform-account.entity'; -import { PlatformTransaction } from '@/entities/platform-transaction.entity'; +import { AccountService } from './account.service'; +import { TransactionService } from './transaction.service'; import Wechatpay = require('wechatpay-node-v3'); @Injectable() @@ -14,10 +14,8 @@ export class RefundService { constructor( @InjectRepository(Order) private orderRepo: Repository, - @InjectRepository(PlatformAccount) - private platformAccountRepo: Repository, - @InjectRepository(PlatformTransaction) - private platformTransactionRepo: Repository, + private accountService: AccountService, + private transactionService: TransactionService, private dataSource: DataSource, private configService: ConfigService, ) { @@ -179,7 +177,7 @@ export class RefundService { } /** - * 平台账户记录退款支出(仅用于记账) + * 记录系统总账户退款 * @param order 订单信息 * @param queryRunner 事务查询器 */ @@ -187,34 +185,17 @@ export class RefundService { order: Order, queryRunner: any, ): Promise { - // 获取平台主账户 - const platformAccount = await queryRunner.manager.findOne(PlatformAccount, { - where: { accountType: 'main' }, - }); + const transactionNo = this.transactionService.generateTransactionNo(); - if (!platformAccount) { - throw new BadRequestException('平台账户不存在'); - } - - // 创建退款支出交易记录(仅记账,实际退款通过第三方支付) - const transaction = queryRunner.manager.create(PlatformTransaction, { - accountId: platformAccount.id, - type: 'refund_expense', - amount: order.payAmount, - balance: platformAccount.balance, // 余额不变,仅记录 - relatedType: 'order', - relatedId: order.id, - description: `订单退款 - ${order.orderNo}`, - metadata: { - orderId: order.id, - orderNo: order.orderNo, - userId: order.userId, - paymentMethod: order.paymentMethod, - refundAmount: order.payAmount, - }, - }); - - await queryRunner.manager.save(transaction); + // 记录系统总账户退款 + await this.accountService.addSystemRefund( + order.payAmount, + transactionNo, + 'order_refund', + order.id, + order.orderNo, + `订单退款 - ${order.orderNo}`, + ); } /** diff --git a/apps/server/src/modules/shared/finance/report.service.ts b/apps/server/src/modules/shared/finance/report.service.ts index 8ff3810..bf25952 100644 --- a/apps/server/src/modules/shared/finance/report.service.ts +++ b/apps/server/src/modules/shared/finance/report.service.ts @@ -1,17 +1,24 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { SystemAccount } from '@/entities/system-account.entity'; import { PlatformAccount } from '@/entities/platform-account.entity'; import { UserAccount } from '@/entities/user-account.entity'; import { MerchantAccount } from '@/entities/merchant-account.entity'; import { PlatformTransaction } from '@/entities/platform-transaction.entity'; import { Order } from '@/entities/order.entity'; import { Settlement } from '@/entities/settlement.entity'; +import { MerchantWithdrawal } from '@/entities/merchant-withdrawal.entity'; +import { UserWithdrawal } from '@/entities/user-withdrawal.entity'; +import { PlatformWithdrawal } from '@/entities/platform-withdrawal.entity'; +import { MktInviteWithdrawal } from '@/entities/mkt-invite-withdrawal.entity'; import dayjs from 'dayjs'; @Injectable() export class ReportService { constructor( + @InjectRepository(SystemAccount) + private systemAccountRepo: Repository, @InjectRepository(PlatformAccount) private platformAccountRepo: Repository, @InjectRepository(UserAccount) @@ -24,12 +31,26 @@ export class ReportService { private orderRepo: Repository, @InjectRepository(Settlement) private settlementRepo: Repository, + @InjectRepository(MerchantWithdrawal) + private merchantWithdrawalRepo: Repository, + @InjectRepository(UserWithdrawal) + private userWithdrawalRepo: Repository, + @InjectRepository(PlatformWithdrawal) + private platformWithdrawalRepo: Repository, + @InjectRepository(MktInviteWithdrawal) + private mktInviteWithdrawalRepo: Repository, ) {} /** * 财务总览 */ async getOverview() { + // 获取系统总账户 + const systemAccount = await this.systemAccountRepo.findOne({ + where: { account_name: '系统总账户' }, + }); + + // 获取平台账户 const platformAccount = await this.platformAccountRepo.findOne({ where: { account_name: '主账户' }, }); @@ -68,11 +89,26 @@ export class ReportService { .getRawOne(); return { + // 系统总账户金额(用户实付 - 退款 - 提现) + systemTotalAmount: Number(systemAccount?.balance || 0), + totalUserPaid: Number(systemAccount?.total_income || 0), + totalRefund: Number(systemAccount?.total_refund || 0), + totalWithdrawn: Number(systemAccount?.total_withdrawn || 0), + + // 平台净收益(服务费 - 邀请返现) platformBalance: Number(platformAccount?.balance || 0), - merchantTotalBalance: Number(merchantStats?.totalBalance || 0), + platformTotalIncome: Number(platformAccount?.total_income || 0), + platformTotalExpense: Number(platformAccount?.total_expense || 0), + + // 商家账户统计 + totalMerchantBalance: Number(merchantStats?.totalBalance || 0), merchantCount: Number(merchantStats?.count || 0), - userTotalBalance: Number(userStats?.totalBalance || 0), + + // 用户账户统计 + totalUserBalance: Number(userStats?.totalBalance || 0), userCount: Number(userStats?.count || 0), + + // 今日统计 todayOrders, todayIncome: Number(todayIncome?.sum || 0), todayExpense: Number(todayExpense?.sum || 0), diff --git a/apps/server/src/modules/shared/finance/settlement.service.ts b/apps/server/src/modules/shared/finance/settlement.service.ts index 6725708..3ec6e38 100644 --- a/apps/server/src/modules/shared/finance/settlement.service.ts +++ b/apps/server/src/modules/shared/finance/settlement.service.ts @@ -167,6 +167,7 @@ export class SettlementService { const transactionNo = this.transactionService.generateTransactionNo(); + // 给商家账户增加余额(订单金额 - 服务费) await this.accountService.addMerchantBalance( merchantId, settlementAmount, @@ -177,14 +178,14 @@ export class SettlementService { `商家周结算:${periodStart} ~ ${periodEnd}` ); - await this.accountService.deductPlatformBalance( - settlementAmount, + // 给平台账户增加服务费收入 + await this.accountService.addPlatformServiceFee( + serviceFee, transactionNo, - '商家结算', 'settlement', settlement.id, settlementNo, - `商家 ${merchantId} 周结算:${periodStart} ~ ${periodEnd}` + `商家 ${merchantId} 周结算服务费:${periodStart} ~ ${periodEnd}` ); await queryRunner.commitTransaction(); diff --git a/apps/server/src/modules/shared/finance/withdrawal.service.ts b/apps/server/src/modules/shared/finance/withdrawal.service.ts index 539624f..2ab80f2 100644 --- a/apps/server/src/modules/shared/finance/withdrawal.service.ts +++ b/apps/server/src/modules/shared/finance/withdrawal.service.ts @@ -155,10 +155,10 @@ export class WithdrawalService { const account = await this.accountService.getPlatformAccount(); - // 计算可提现金额 = 累计服务费 - 冻结余额 - // 注意:这里使用 total_service_fee 是为了确保只提现平台收入 + // 计算可提现金额 = 累计收入 - 冻结余额 + // 注意:这里使用 total_income 是为了确保只提现平台收入 // 未来如果有其他收入(广告费、会员费等),需要累加到可提现金额中 - const availableAmount = Number(account.total_service_fee) - Number(account.frozen_balance); + const availableAmount = Number(account.total_income) - Number(account.frozen_balance); if (availableAmount < amount) { throw new BadRequestException(`可提现金额不足,当前可提现:${availableAmount.toFixed(2)}元`); @@ -294,6 +294,7 @@ export class WithdrawalService { try { const transactionNo = this.transactionService.generateTransactionNo(); + // 扣减用户账户余额 await this.accountService.deductUserBalance( withdrawal.userId, Number(withdrawal.actualAmount), @@ -304,6 +305,16 @@ export class WithdrawalService { `用户提现 - ${withdrawal.paymentChannel}` ); + // 记录系统总账户提现 + await this.accountService.addSystemWithdrawal( + Number(withdrawal.actualAmount), + transactionNo, + 'user_withdraw', + withdrawal.id, + withdrawal.withdrawNo, + `用户 ${withdrawal.userId} 提现 - ${withdrawal.paymentChannel}`, + ); + await queryRunner.manager.update(UserWithdrawal, id, { status: 'paid', paymentNo, @@ -415,6 +426,7 @@ export class WithdrawalService { try { const transactionNo = this.transactionService.generateTransactionNo(); + // 扣减商家账户余额 await this.accountService.deductMerchantBalance( withdrawal.merchantId, Number(withdrawal.actualAmount), @@ -425,6 +437,16 @@ export class WithdrawalService { `商家提现 - ${withdrawal.bankName}` ); + // 记录系统总账户提现 + await this.accountService.addSystemWithdrawal( + Number(withdrawal.actualAmount), + transactionNo, + 'merchant_withdraw', + withdrawal.id, + withdrawal.withdrawNo, + `商家 ${withdrawal.merchantId} 提现 - ${withdrawal.bankName}`, + ); + await queryRunner.manager.update(MerchantWithdrawal, id, { status: 'paid', paymentNo, @@ -536,10 +558,9 @@ export class WithdrawalService { try { const transactionNo = this.transactionService.generateTransactionNo(); - await this.accountService.deductPlatformBalance( + await this.accountService.deductPlatformWithdrawal( Number(withdrawal.actualAmount), transactionNo, - '提现', 'withdraw', withdrawal.id, withdrawal.withdrawNo, diff --git a/database/migrations/001_init_schema.sql b/database/migrations/001_init_schema.sql index 7cb7682..2059d06 100644 --- a/database/migrations/001_init_schema.sql +++ b/database/migrations/001_init_schema.sql @@ -645,19 +645,14 @@ CREATE TABLE IF NOT EXISTS `merchant_accounts` ( KEY `idx_last_settlement_at` (`last_settlement_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商家账户表'; --- 3. 平台账户表 -CREATE TABLE IF NOT EXISTS `platform_accounts` ( +-- 3. 系统总账户表(记录整个系统的资金流入流出) +CREATE TABLE IF NOT EXISTS `system_accounts` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '账户ID', - `account_name` VARCHAR(50) NOT NULL COMMENT '账户名称(如:主账户、备用账户)', - `balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '可用余额', - `frozen_balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '冻结余额(提现中)', - `total_income` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计收入(订单收入)', - `total_expense` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计支出(结算+返现+提现)', - `total_order_income` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计订单收入', - `total_service_fee` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计服务费收入', - `total_settlement` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计商家结算支出', - `total_cashback` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计返现支出', - `total_withdraw` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计提现金额', + `account_name` VARCHAR(50) NOT NULL COMMENT '账户名称(如:主账户)', + `balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '可用余额(total_income - total_refund - total_withdrawn)', + `total_income` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计收入(用户实付总额)', + `total_refund` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计退款', + `total_withdrawn` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计提现(所有提现)', `version` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', `status` ENUM('active','frozen','closed') NOT NULL DEFAULT 'active' COMMENT '账户状态', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', @@ -665,13 +660,53 @@ CREATE TABLE IF NOT EXISTS `platform_accounts` ( PRIMARY KEY (`id`), UNIQUE KEY `uk_account_name` (`account_name`), KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台账户表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统总账户表(资金守恒验证:balance应等于所有用户账户+商家账户+平台账户余额之和)'; + +-- 4. 平台账户表(记录平台净收益) +CREATE TABLE IF NOT EXISTS `platform_accounts` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '账户ID', + `account_name` VARCHAR(50) NOT NULL COMMENT '账户名称(如:主账户、备用账户)', + `balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '可用余额(平台净收益 = total_income - total_expense)', + `frozen_balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '冻结余额(提现中)', + `total_income` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计收入(服务费收入)', + `total_expense` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计支出(邀请返现支出)', + `version` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + `status` ENUM('active','frozen','closed') NOT NULL DEFAULT 'active' COMMENT '账户状态', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_account_name` (`account_name`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台账户表(平台净收益 = 服务费 - 邀请返现)'; -- ============================================================ -- 第二部分:交易流水表(按角色拆分) -- ============================================================ --- 4. 用户交易流水表 +-- 4. 系统总账户交易流水表 +CREATE TABLE IF NOT EXISTS `system_transactions` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '流水ID', + `transaction_no` VARCHAR(32) NOT NULL COMMENT '交易流水号(全局唯一)', + `account_id` BIGINT UNSIGNED NOT NULL COMMENT '系统账户ID', + `direction` ENUM('income','expense') NOT NULL COMMENT '方向:income-收入/expense-支出', + `amount` DECIMAL(12,2) NOT NULL COMMENT '金额(正数)', + `balance_before` DECIMAL(12,2) NOT NULL COMMENT '交易前余额', + `balance_after` DECIMAL(12,2) NOT NULL COMMENT '交易后余额', + `transaction_type` VARCHAR(50) NOT NULL COMMENT '交易类型', + `business_type` VARCHAR(50) NOT NULL COMMENT '业务类型:order_payment/refund/withdraw', + `business_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '业务ID(订单ID/退款ID/提现ID等)', + `business_no` VARCHAR(32) DEFAULT NULL COMMENT '业务单号', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_transaction_no` (`transaction_no`), + KEY `idx_account_id` (`account_id`), + KEY `idx_transaction_type` (`transaction_type`), + KEY `idx_business` (`business_type`, `business_id`), + KEY `idx_created_at` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统总账户交易流水表'; + +-- 5. 用户交易流水表 CREATE TABLE IF NOT EXISTS `user_transactions` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '流水ID', `transaction_no` VARCHAR(32) NOT NULL COMMENT '交易流水号(全局唯一)', @@ -698,7 +733,7 @@ CREATE TABLE IF NOT EXISTS `user_transactions` ( KEY `idx_created_at` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户交易流水表'; --- 5. 商家交易流水表 +-- 6. 商家交易流水表 CREATE TABLE IF NOT EXISTS `merchant_transactions` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '流水ID', `transaction_no` VARCHAR(32) NOT NULL COMMENT '交易流水号(全局唯一)', @@ -725,7 +760,7 @@ CREATE TABLE IF NOT EXISTS `merchant_transactions` ( KEY `idx_created_at` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商家交易流水表'; --- 6. 平台交易流水表 +-- 7. 平台交易流水表 CREATE TABLE IF NOT EXISTS `platform_transactions` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '流水ID', `transaction_no` VARCHAR(32) NOT NULL COMMENT '交易流水号(全局唯一)', @@ -735,7 +770,7 @@ CREATE TABLE IF NOT EXISTS `platform_transactions` ( `balance_before` DECIMAL(12,2) NOT NULL COMMENT '交易前余额', `balance_after` DECIMAL(12,2) NOT NULL COMMENT '交易后余额', `transaction_type` VARCHAR(50) NOT NULL COMMENT '交易类型', - `business_type` VARCHAR(50) NOT NULL COMMENT '业务类型:order/settlement/cashback/withdraw/refund', + `business_type` VARCHAR(50) NOT NULL COMMENT '业务类型:service_fee/cashback/withdraw', `business_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '业务ID', `business_no` VARCHAR(32) DEFAULT NULL COMMENT '业务单号', `related_account_type` ENUM('user','merchant') DEFAULT NULL COMMENT '对方账户类型', diff --git a/docs/README.md b/docs/README.md index 46bc47f..993fd36 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,9 +17,9 @@ ### 配置指南 - 💳 [微信支付配置](WECHAT_PAYMENT_SETUP.md) - 微信支付接入指南 -### 功能模块 +### 核心系统设计 +- 💰 [财务系统完整设计](finance-system.md) - 账户体系、资金流转、核心服务、安全保障(v2.0 重构版) - 🎁 [邀请码系统](features/invite-system.md) - 邀请码生成、绑定和返现机制 -- 💰 [结算系统](features/settlement-system.md) - 订单支付、资金流转、周结算和商家提现 --- @@ -28,6 +28,7 @@ ``` docs/ ├── README.md # 本文件 - 文档索引 +├── finance-system.md # 财务系统完整设计文档(v2.0) ├── DEVELOPMENT_SUMMARY.md # 开发总结(项目完成情况) ├── WECHAT_PAYMENT_SETUP.md # 微信支付配置指南 ├── requirements/ @@ -35,17 +36,33 @@ docs/ ├── planning/ │ └── TODO2.md # 任务清单(已完成100%) ├── database/ -│ └── finance-database.md # 财务数据库设计 +│ └── finance-database.md # 财务数据库设计(v2.0) └── features/ - ├── invite-system.md # 邀请码系统设计文档 - └── settlement-system.md # 结算系统设计文档 + └── invite-system.md # 邀请码系统设计文档 ``` --- -## 📖 核心文档说明 +### 1. [财务系统完整设计](features/finance-system.md) - v3.0 合并版 +**内容**: +- 四层账户体系设计(系统总账户、平台账户、商家账户、用户账户) +- 完整的资金流转流程(订单支付、结算、退款、提现) +- 结算周期和服务费计算 +- 核心服务详解(AccountService、SettlementService、RefundService、WithdrawalService、ReportService) +- 数据库表结构和SQL定义 +- API接口文档 +- 前端页面实现(5个页面) +- 扩展方案(未来增加其他收入) +- 安全保障机制(事务、悲观锁、乐观锁、金额精度控制) +- 数据一致性保证 +- 测试验证和部署说明 +- 常见问题解答 -### 1. [项目需求文档](requirements/项目需求文档.md) +**适合**:后端开发人员、架构师、财务人员、测试人员 + +--- + +### 2. [项目需求文档](requirements/项目需求文档.md) **内容**: - 项目概述和技术栈 - 系统架构设计 @@ -58,18 +75,19 @@ docs/ --- -### 2. [财务数据库设计](database/finance-database.md) +### 3. [财务数据库设计](database/finance-database.md) - v2.0 更新 **内容**: -- 完整的数据库表结构 -- 账户体系设计(用户、商家、平台) -- 资金流转逻辑 -- 财务交易记录 +- 完整的数据库表结构(v2.0 重构版) +- 四层账户体系(system_accounts、platform_accounts、merchant_accounts、user_accounts) +- 资金流转逻辑和业务流程 +- 数据一致性保证机制 +- 索引说明和版本更新记录 **适合**:后端开发人员、数据库管理员 --- -### 3. [任务清单 TODO2.md](planning/TODO2.md) +### 4. [任务清单 TODO2.md](planning/TODO2.md) **内容**: - 项目开发任务分解 - 各阶段完成情况(100%) @@ -80,7 +98,7 @@ docs/ --- -### 4. [开发总结](DEVELOPMENT_SUMMARY.md) +### 5. [开发总结](DEVELOPMENT_SUMMARY.md) **内容**: - 项目整体完成情况 - 已实现的功能模块 @@ -91,7 +109,7 @@ docs/ --- -### 5. [微信支付配置](WECHAT_PAYMENT_SETUP.md) +### 6. [微信支付配置](WECHAT_PAYMENT_SETUP.md) **内容**: - 微信支付商户配置 - API密钥设置 @@ -102,7 +120,7 @@ docs/ --- -### 6. [邀请码系统](features/invite-system.md) +### 7. [邀请码系统](features/invite-system.md) **内容**: - 邀请码生成规则(6位数字+大写字母) - 邀请关系绑定流程 @@ -114,37 +132,38 @@ docs/ --- -### 7. [结算系统](features/settlement-system.md) -**内容**: -- 完整的资金流转流程 -- 周结算机制(每周一凌晨2点自动执行) -- 服务费计算规则和配置 -- 订单支付、商家结算、提现流程 -- 数据库设计和API接口 -- 问题修复记录 - -**适合**:产品经理、后端开发人员、财务人员 - ---- +## 🗄️ 数据库初始化 ### 初始化脚本 位置:`database/migrations/001_init_schema.sql` **包含内容**: -- 所有数据库表结构 +- 所有数据库表结构(v2.0 重构版) +- 系统总账户表(system_accounts) +- 平台账户表(platform_accounts) +- 商家账户表(merchant_accounts) +- 用户账户表(user_accounts) +- 各类交易流水表 - 索引和外键约束 - 初始化数据 -- 常住人表(guests) -- 实名认证字段 +- 触发器 ### 执行方式 ```bash -# 方式1:命令行执行 +# 方式1:命令行执行(会清空所有数据) mysql -u root -p < database/migrations/001_init_schema.sql # 方式2:登录后执行 mysql -u root -p source database/migrations/001_init_schema.sql; + +# 方式3:初始化系统总账户和平台账户 +USE your_database_name; +INSERT INTO system_accounts (account_name, balance, total_income, total_refund, total_withdrawn) +VALUES ('系统总账户', 0.00, 0.00, 0.00, 0.00); + +INSERT INTO platform_accounts (account_name, balance, total_income, total_expense) +VALUES ('主账户', 0.00, 0.00, 0.00); ``` --- @@ -196,6 +215,7 @@ source database/migrations/001_init_schema.sql; | 日期 | 版本 | 说明 | |------|------|------| +| 2024-01-XX | v5.0 | 财务系统账户体系重构(v2.0),新增系统总账户,重构平台账户 | | 2026-05-13 | v4.2 | 新增结算系统设计文档,修复结算逻辑问题 | | 2026-05-13 | v4.1 | 新增邀请码系统设计文档 | | 2026-05-13 | v4.0 | 整合文档,删除重复内容,项目完成100% | diff --git a/docs/database/finance-database.md b/docs/database/finance-database.md index 0c3759e..46d533f 100644 --- a/docs/database/finance-database.md +++ b/docs/database/finance-database.md @@ -1,43 +1,175 @@ -# 财务系统数据库表结构说明 +# 财务系统数据库设计文档 + +> **最后更新**: 2024-01-XX - 账户体系重构完成 +> **版本**: v2.0 + +## 概述 + +本文档描述了租房平台财务系统的数据库设计,包括账户体系、资金流转和数据一致性保障机制。 ## 数据库表与 Entity 映射关系 | 数据库表 | Entity 文件 | 说明 | 状态 | |---------|------------|------|------| -| `user_accounts` | `user-account.entity.ts` | 用户账户表 | ✅ 已完成 | -| `merchant_accounts` | `merchant-account.entity.ts` | 商家账户表 | ✅ 已完成 | -| `platform_accounts` | `platform-account.entity.ts` | 平台账户表 | ✅ 已完成 | -| `user_transactions` | `user-transaction.entity.ts` | 用户交易流水表 | ✅ 已完成 | -| `merchant_transactions` | `merchant-transaction.entity.ts` | 商家交易流水表 | ✅ 已完成 | +| `system_accounts` | `system-account.entity.ts` | 系统总账户表 | ✅ v2.0 新增 | +| `system_transactions` | `system-transaction.entity.ts` | 系统总账户交易流水表 | ✅ v2.0 新增 | +| `platform_accounts` | `platform-account.entity.ts` | 平台账户表 | ✅ v2.0 重构 | | `platform_transactions` | `platform-transaction.entity.ts` | 平台交易流水表 | ✅ 已完成 | +| `merchant_accounts` | `merchant-account.entity.ts` | 商家账户表 | ✅ 已完成 | +| `merchant_transactions` | `merchant-transaction.entity.ts` | 商家交易流水表 | ✅ 已完成 | +| `user_accounts` | `user-account.entity.ts` | 用户账户表 | ✅ 已完成 | +| `user_transactions` | `user-transaction.entity.ts` | 用户交易流水表 | ✅ 已完成 | | `user_withdrawals` | `user-withdrawal.entity.ts` | 用户提现表 | ✅ 已完成 | | `merchant_withdrawals` | `merchant-withdrawal.entity.ts` | 商家提现表 | ✅ 已完成 | -| `platform_withdrawals` | `platform-withdrawal.entity.ts` | 平台提现表 | ✅ 已完成 | | `settlements` | `settlement.entity.ts` | 结算单表 | ✅ 已完成 | | `settlement_items` | `settlement-item.entity.ts` | 结算明细表 | ✅ 已完成 | -| `daily_reconciliations` | `daily-reconciliation.entity.ts` | 日对账表 | ✅ 已完成 | -## 数据库初始化说明 +## 账户体系设计 -### 1. 迁移脚本位置 +### 四层账户结构 -`database/migrations/001_init_schema.sql` 已包含完整的财务系统表结构(第 606-901 行) +系统采用四层账户结构,确保资金流转清晰、职责明确: -### 2. 自动创建的数据 +#### 1. 系统总账户 (System Account) - v2.0 新增 -执行迁移脚本后会自动创建: +**职责**: 记录平台内所有未提现的资金总额 -1. **触发器** - - `trg_user_create_account`: 用户注册时自动创建用户账户 - - `trg_merchant_create_account`: 商家审核通过时自动创建商家账户 +**计算公式**: +``` +系统总账户余额 = 用户实付总额 - 累计退款 - 累计提现 +``` -### 3. 表结构版本说明 +**字段说明**: +- `balance`: 当前余额 +- `total_income`: 累计收入(用户实付总额) +- `total_refund`: 累计退款 +- `total_withdrawn`: 累计提现 -⚠️ **重要**: 旧版本的 `settlements` 和 `withdrawals` 表定义已删除。 +#### 2. 平台账户 (Platform Account) - v2.0 重构 -**当前使用**: 按角色分离的账户和交易表(user_accounts、merchant_accounts、platform_accounts 等) +**职责**: 记录平台净收益(服务费收入 - 邀请返现支出) -## 字段说明 +**计算公式**: +``` +平台账户余额 = 累计服务费收入 - 累计邀请返现支出 +``` + +**字段说明**: +- `balance`: 当前余额 +- `total_income`: 累计收入(服务费收入) +- `total_expense`: 累计支出(邀请返现支出) + +**v2.0 变更**: +- ❌ 删除字段: `total_order_income`, `total_service_fee`, `total_cashback`, `total_settlement` +- ✅ 字段含义变更: `total_income` 从"订单收入"改为"服务费收入" +- ✅ 字段含义变更: `total_expense` 从"所有支出"改为"邀请返现支出" + +#### 3. 商家账户 (Merchant Account) + +**职责**: 记录商家的订单收入和提现 + +**计算公式**: +``` +商家账户余额 = 累计订单收入 - 累计提现 +订单收入 = 订单金额 - 服务费 +``` + +#### 4. 用户账户 (User Account) + +**职责**: 记录用户的邀请返现收入和提现 + +**计算公式**: +``` +用户账户余额 = 累计邀请返现 - 累计提现 +``` + +### 资金守恒验证 + +系统通过以下公式验证资金守恒: + +``` +系统总账户余额 = 商家账户余额总和 + 用户账户余额总和 + 平台账户余额 +``` + +这个公式确保平台内所有资金都有明确的归属,不会出现资金丢失或凭空产生的情况。 + +## 数据表结构详解 + +### system_accounts(系统总账户表)- v2.0 新增 + +```sql +CREATE TABLE IF NOT EXISTS `system_accounts` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `account_name` VARCHAR(50) NOT NULL COMMENT '账户名称', + `balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '可用余额', + `total_income` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计收入(用户实付总额)', + `total_refund` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计退款', + `total_withdrawn` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计提现', + `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_account_name` (`account_name`) +) COMMENT='系统总账户表'; +``` + +**关键点**: +- 记录平台内所有未提现的资金 +- `balance` = `total_income` - `total_refund` - `total_withdrawn` +- 用于验证资金守恒 + +### system_transactions(系统总账户交易流水表)- v2.0 新增 + +```sql +CREATE TABLE IF NOT EXISTS `system_transactions` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `transaction_no` VARCHAR(64) NOT NULL COMMENT '交易流水号', + `account_id` BIGINT UNSIGNED NOT NULL COMMENT '账户ID', + `direction` ENUM('income','expense') NOT NULL COMMENT '交易方向', + `amount` DECIMAL(12,2) NOT NULL COMMENT '交易金额', + `balance_before` DECIMAL(12,2) NOT NULL COMMENT '交易前余额', + `balance_after` DECIMAL(12,2) NOT NULL COMMENT '交易后余额', + `transaction_type` VARCHAR(50) NOT NULL COMMENT '交易类型', + `business_type` VARCHAR(50) NOT NULL COMMENT '业务类型', + `business_id` BIGINT UNSIGNED NOT NULL COMMENT '业务ID', + `business_no` VARCHAR(64) NOT NULL COMMENT '业务单号', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_transaction_no` (`transaction_no`), + KEY `idx_account_id` (`account_id`), + KEY `idx_business` (`business_type`, `business_id`), + KEY `idx_created_at` (`created_at`) +) COMMENT='系统总账户交易流水表'; +``` + +**交易类型**: +- `用户支付`: 用户支付订单(income) +- `订单退款`: 订单退款(expense) +- `用户提现`: 用户提现(expense) +- `商家提现`: 商家提现(expense) + +### platform_accounts(平台账户表)- v2.0 重构 + +```sql +CREATE TABLE IF NOT EXISTS `platform_accounts` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `account_name` VARCHAR(50) NOT NULL COMMENT '账户名称', + `balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '可用余额', + `total_income` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计收入(服务费收入)', + `total_expense` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计支出(邀请返现支出)', + `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_account_name` (`account_name`) +) COMMENT='平台账户表'; +``` + +**v2.0 变更**: +- `total_income` 含义变更: 从"订单收入"改为"服务费收入" +- `total_expense` 含义变更: 从"所有支出"改为"邀请返现支出" +- 删除冗余字段: `total_order_income`, `total_service_fee`, `total_cashback`, `total_settlement` ### user_accounts(用户账户表) @@ -56,6 +188,11 @@ CREATE TABLE `user_accounts` ( ); ``` +**关键点**: +- 用户账户主要用于邀请返现 +- `balance` = `total_income` - `total_expense` +- 提现时先冻结余额,打款后扣减 + ### merchant_accounts(商家账户表) ```sql @@ -78,6 +215,121 @@ CREATE TABLE `merchant_accounts` ( ); ``` +**关键点**: +- 商家账户记录订单结算收入 +- 结算金额 = 订单金额 - 服务费 +- 每周一凌晨2点自动执行周结算 + +## 资金流转流程 + +### 1. 订单支付流程 - v2.0 更新 + +``` +用户支付订单 + ↓ +系统总账户 +实付金额 (income) + ↓ +订单状态:pending_pay → pending_confirm +``` + +**涉及表**: +- `system_accounts`: balance += 实付金额, total_income += 实付金额 +- `system_transactions`: 插入交易流水(income) + +**代码位置**: `apps/server/src/modules/app/order/order.service.ts` + +### 2. 订单结算流程 - v2.0 更新 + +``` +订单完成(用户离店) + ↓ +商家账户 +(实付金额 - 服务费) + ↓ +平台账户 +服务费 + ↓ +如果有邀请关系: + 用户账户(邀请人)+返现金额 + 平台账户 -返现金额 +``` + +**涉及表**: +- `merchant_accounts`: balance += 结算金额 +- `merchant_transactions`: 插入交易流水 +- `platform_accounts`: balance += 服务费, total_income += 服务费 +- `platform_transactions`: 插入交易流水 +- `user_accounts`: balance += 返现金额(如有邀请) +- `user_transactions`: 插入交易流水(如有邀请) +- `platform_accounts`: balance -= 返现金额, total_expense += 返现金额(如有邀请) +- `settlements`: 插入结算单记录 +- `settlement_items`: 插入结算明细 + +**代码位置**: +- `apps/server/src/modules/shared/finance/settlement.service.ts` +- `apps/server/src/modules/app/activity/activity.service.ts` + +### 3. 订单退款流程 - v2.0 更新 + +``` +用户申请退款 + ↓ +调用第三方支付退款API + ↓ +系统总账户 +退款金额 (expense) + ↓ +订单状态:pending_confirm/pending_checkin → refunded +``` + +**涉及表**: +- `system_accounts`: balance -= 退款金额, total_refund += 退款金额 +- `system_transactions`: 插入交易流水(expense) + +**代码位置**: `apps/server/src/modules/shared/finance/refund.service.ts` + +### 4. 提现流程 - v2.0 更新 + +#### 用户提现 + +``` +用户申请提现 + ↓ +用户账户:balance → frozen_balance + ↓ +管理员审核通过 + ↓ +管理员确认打款 + ↓ +用户账户 -提现金额(从frozen_balance扣减) +系统总账户 +提现金额 (expense) + ↓ +提现状态:pending → approved → paid +``` + +#### 商家提现 + +``` +商家申请提现 + ↓ +商家账户:balance → frozen_balance + ↓ +管理员审核通过 + ↓ +管理员确认打款 + ↓ +商家账户 -提现金额(从frozen_balance扣减) +系统总账户 +提现金额 (expense) + ↓ +提现状态:pending → approved → paid +``` + +**涉及表**: +- `user_accounts` / `merchant_accounts`: frozen_balance → balance扣减 +- `user_transactions` / `merchant_transactions`: 插入交易流水 +- `system_accounts`: balance -= 提现金额, total_withdrawn += 提现金额 +- `system_transactions`: 插入交易流水 +- `user_withdrawals` / `merchant_withdrawals`: 更新提现状态 + +**代码位置**: `apps/server/src/modules/shared/finance/withdrawal.service.ts` + ### platform_accounts(平台账户表) ```sql @@ -126,9 +378,9 @@ CREATE TABLE `user_transactions` ( ``` **关键点**: -- 复式记账:每笔转账生成两条流水(一条支出 + 一条收入) -- `transaction_no` 相同的两条流水表示同一笔转账 -- `related_account_type` 和 `related_account_id` 关联对方账户 +- 所有账户操作都记录详细的交易流水 +- `transaction_no` 全局唯一,用于追踪资金流向 +- 记录交易前后余额,便于对账和审计 ### settlements(结算单表) @@ -152,123 +404,71 @@ CREATE TABLE `settlements` ( **关键点**: - 每周一凌晨2点自动生成上周的结算单 - `settlement_amount` = `order_amount` - `service_fee` -- 结算时执行转账:平台账户 → 商家账户 - -### daily_reconciliations(日对账表) - -```sql -CREATE TABLE `daily_reconciliations` ( - `id` BIGINT UNSIGNED AUTO_INCREMENT, - `reconciliation_date` DATE UNIQUE, -- 对账日期 - `platform_balance` DECIMAL(12,2), -- 平台余额 - `merchant_balance_sum` DECIMAL(12,2), -- 商家余额总和 - `user_balance_sum` DECIMAL(12,2), -- 用户余额总和 - `total_balance` DECIMAL(12,2), -- 总余额 - `order_income` DECIMAL(12,2) DEFAULT 0.00, -- 订单收入 - `service_fee` DECIMAL(12,2) DEFAULT 0.00, -- 服务费 - `merchant_settlement` DECIMAL(12,2) DEFAULT 0.00, -- 商家结算 - `cashback_expense` DECIMAL(12,2) DEFAULT 0.00, -- 返现支出 - `withdraw_expense` DECIMAL(12,2) DEFAULT 0.00, -- 提现支出 - `refund_expense` DECIMAL(12,2) DEFAULT 0.00, -- 退款支出 - `status` ENUM('balanced','unbalanced'), -- 状态 - `error_message` TEXT, -- 异常信息 - PRIMARY KEY (`id`) -); -``` - -**关键点**: -- 每天凌晨3点自动执行对账 -- 检查借贷平衡:收入总额 = 支出总额 -- 如有异常记录 `error_message` 并发送告警 - -## 业务流程与数据库操作 - -### 1. 用户支付订单 - -``` -1. 用户支付 → 平台账户收入 -2. 插入 transactions 记录(平台账户 income) -3. 更新 accounts.balance(平台账户) -``` - -### 2. 周结算 - -``` -1. 查询上周已完成订单 -2. 按商家分组计算结算金额 -3. 插入 settlements 记录 -4. 插入 settlement_items 记录(订单明细) -5. 执行转账:平台账户 → 商家账户 - - 插入 2 条 transactions 记录(复式记账) - - 更新 2 个 accounts.balance -``` - -### 3. 商家提现 - -``` -1. 商家申请提现 - - 插入 merchant_withdrawals 记录 - - 冻结商家账户余额(frozen_balance += amount) -2. 管理员审核通过 - - 更新 merchant_withdrawals.status = 'approved' -3. 管理员确认打款 - - 扣减余额(balance -= amount, frozen_balance -= amount) - - 插入 transactions 记录(商家账户 expense) - - 更新 merchant_withdrawals.status = 'paid' -``` - -### 4. 日对账 - -``` -1. 统计各账户余额 - - SELECT SUM(balance) FROM accounts WHERE account_type = 'platform' - - SELECT SUM(balance) FROM accounts WHERE account_type = 'merchant' - - SELECT SUM(balance) FROM accounts WHERE account_type = 'user' -2. 统计当日交易金额 - - SELECT SUM(amount) FROM transactions WHERE direction = 'income' - - SELECT SUM(amount) FROM transactions WHERE direction = 'expense' -3. 检查借贷平衡 - - 收入总额 = 支出总额(允许 0.01 元误差) -4. 插入 daily_reconciliations 记录 -``` - -## 索引说明 - -### 关键索引 - -1. **accounts 表** - - `uk_type_owner`: 唯一索引,确保每个用户/商家只有一个账户 - - `idx_status`: 按状态查询 - -2. **transactions 表** - - `uk_transaction_no`: 唯一索引,防止重复交易 - - `idx_account_id`: 按账户查询流水 - - `idx_business`: 按业务类型和业务ID查询 - - `idx_created_at`: 按时间范围查询 - -3. **settlements 表** - - `uk_settlement_no`: 唯一索引 - - `idx_merchant_id`: 按商家查询 - - `idx_period`: 按周期查询 - -4. **withdrawals 表** - - `uk_withdraw_no`: 唯一索引 - - `idx_status`: 按状态查询(待审核、已打款等) +- 结算时执行转账:给商家账户增加余额,给平台账户增加服务费 ## 数据一致性保证 ### 1. 事务保证 所有涉及金额变动的操作都在事务中执行: -- 转账操作(账户余额变动 + 交易流水插入) -- 提现操作(余额冻结/扣减 + 提现记录更新) -- 结算操作(结算单生成 + 转账执行) -### 2. 乐观锁 +```typescript +const queryRunner = this.dataSource.createQueryRunner(); +await queryRunner.connect(); +await queryRunner.startTransaction(); -`accounts` 表使用 `version` 字段实现乐观锁,防止并发更新导致余额错误。 +try { + // 账户操作 + await queryRunner.commitTransaction(); +} catch (error) { + await queryRunner.rollbackTransaction(); + throw error; +} finally { + await queryRunner.release(); +} +``` -### 3. CHECK 约束 +### 2. 悲观锁 + +在查询账户时使用悲观锁,防止并发修改: + +```typescript +const account = await queryRunner.manager.findOne(SystemAccount, { + where: { account_name: '系统总账户' }, + lock: { mode: 'pessimistic_write' }, +}); +``` + +### 3. 乐观锁 + +使用 `version` 字段实现乐观锁,检测并发冲突: + +```typescript +account.version += 1; +await queryRunner.manager.save(account); +``` + +### 4. 金额精度控制 + +所有金额计算都使用 `Number()` 转换和 `toFixed(2)` 保留两位小数: + +```typescript +const amountNum = Number(amount); +const balanceBefore = Number(account.balance); +const balanceAfter = parseFloat((balanceBefore + amountNum).toFixed(2)); +``` + +### 5. 余额校验 + +在扣减余额前检查余额是否足够: + +```typescript +if (balanceBefore < amount) { + throw new BadRequestException('账户余额不足'); +} +``` + +### 6. CHECK 约束 ```sql CONSTRAINT `chk_balance` CHECK (`balance` >= 0) @@ -277,29 +477,135 @@ CONSTRAINT `chk_frozen_balance` CHECK (`frozen_balance` >= 0) 确保余额不会为负数。 -### 4. 触发器 +### 7. 触发器 - 用户注册时自动创建账户 - 商家审核通过时自动创建账户 -## 下一步工作 +## 数据库初始化 -1. ✅ 数据库表结构已完成 -2. ✅ Entity 层已完成 -3. ✅ Service 层已完成 -4. ✅ Controller 层已完成 -5. ✅ DTO 层已完成 -6. ⏳ 执行数据库迁移脚本 -7. ⏳ 集成到订单模块(订单支付时调用账户服务) -8. ⏳ 编写单元测试 -9. ⏳ 前端页面开发 +### 1. 迁移脚本位置 + +`database/migrations/001_init_schema.sql` 包含完整的财务系统表结构 + +### 2. 初始化步骤 + +```bash +# 1. 执行数据库迁移脚本(会清空所有数据) +mysql -u root -p < database/migrations/001_init_schema.sql + +# 2. 初始化系统总账户 +mysql -u root -p +USE your_database_name; +INSERT INTO system_accounts (account_name, balance, total_income, total_refund, total_withdrawn) +VALUES ('系统总账户', 0.00, 0.00, 0.00, 0.00); + +# 3. 初始化平台账户 +INSERT INTO platform_accounts (account_name, balance, total_income, total_expense) +VALUES ('主账户', 0.00, 0.00, 0.00); +``` + +### 3. 自动创建的数据 + +执行迁移脚本后会自动创建: + +1. **触发器** + - `trg_user_create_account`: 用户注册时自动创建用户账户 + - `trg_merchant_create_account`: 商家审核通过时自动创建商家账户 + +## 索引说明 + +### 关键索引 + +1. **system_accounts 表** + - `uk_account_name`: 唯一索引,确保账户名称唯一 + +2. **system_transactions 表** + - `uk_transaction_no`: 唯一索引,防止重复交易 + - `idx_account_id`: 按账户查询流水 + - `idx_business`: 按业务类型和业务ID查询 + - `idx_created_at`: 按时间范围查询 + +3. **platform_accounts 表** + - `uk_account_name`: 唯一索引,确保账户名称唯一 + +4. **user_accounts / merchant_accounts 表** + - `uk_user_id` / `uk_merchant_id`: 唯一索引,确保每个用户/商家只有一个账户 + - `idx_status`: 按状态查询 + +5. **transactions 表** + - `uk_transaction_no`: 唯一索引,防止重复交易 + - `idx_account_id`: 按账户查询流水 + - `idx_business`: 按业务类型和业务ID查询 + - `idx_created_at`: 按时间范围查询 + +6. **settlements 表** + - `uk_settlement_no`: 唯一索引 + - `idx_merchant_id`: 按商家查询 + - `idx_period`: 按周期查询 + +7. **withdrawals 表** + - `uk_withdraw_no`: 唯一索引 + - `idx_status`: 按状态查询(待审核、已打款等) + +## 版本更新记录 + +### v2.0 (2024-01-XX) - 账户体系重构 + +**重大变更**: +1. ✅ 新增 `system_accounts` 表 - 记录系统总资金 +2. ✅ 新增 `system_transactions` 表 - 系统总账户交易流水 +3. ✅ 重构 `platform_accounts` 表 - 只记录平台净收益 + - 字段含义变更: `total_income` 改为"服务费收入" + - 字段含义变更: `total_expense` 改为"邀请返现支出" + - 删除冗余字段: `total_order_income`, `total_service_fee`, `total_cashback`, `total_settlement` +4. ✅ 修改订单支付逻辑 - 更新系统总账户 +5. ✅ 修改订单结算逻辑 - 同时更新商家账户和平台账户 +6. ✅ 修改邀请返现逻辑 - 从平台账户扣减 +7. ✅ 修改退款逻辑 - 更新系统总账户 +8. ✅ 修改提现逻辑 - 更新系统总账户 + +**资金守恒验证**: +``` +系统总账户余额 = 商家账户余额总和 + 用户账户余额总和 + 平台账户余额 +``` + +**升级步骤**: +1. 备份现有数据库 +2. 执行新的数据库迁移脚本 +3. 初始化系统总账户和平台账户 +4. 更新后端代码 +5. 重启服务 +6. 验证资金守恒 + +### v1.0 (2024-04-24) - 初始版本 + +**功能**: +- 用户账户、商家账户、平台账户 +- 交易流水记录 +- 周结算功能 +- 提现功能 +- 日对账功能 + +## 相关文档 + +- [财务系统完整设计文档](../finance-system.md) - 包含业务流程、核心服务、前端展示等完整内容 +- [项目需求文档](../requirements/项目需求文档.md) - 了解项目功能和业务逻辑 +- [开发总结](../DEVELOPMENT_SUMMARY.md) - 查看项目完成情况 ## 注意事项 -1. **删除旧版 settlements 表定义**: 数据库脚本中第 394-417 行的旧版本定义应该删除,避免混淆。 +1. **执行迁移脚本**: 确保按顺序执行 `001_init_schema.sql`,会自动创建所有表和触发器。 -2. **执行迁移脚本**: 确保按顺序执行 `001_init_schema.sql`,会自动创建所有表和触发器。 +2. **初始化账户**: 迁移脚本执行后需要手动初始化系统总账户和平台账户。 -3. **平台账户**: 迁移脚本会自动创建平台账户(`owner_id=0`),无需手动创建。 +3. **定时任务**: 需要在 NestJS 中启用 `@nestjs/schedule` 模块,定时任务才会执行。 -4. **定时任务**: 需要在 NestJS 中启用 `@nestjs/schedule` 模块,定时任务才会执行。 +4. **资金安全**: 所有账户操作都使用事务、悲观锁和乐观锁保证数据一致性。 + +5. **金额精度**: 所有金额计算使用 `Number()` 和 `toFixed(2)` 确保精度。 + +--- + +**维护团队**: 开发团队 +**最后更新**: 2024-01-XX diff --git a/docs/features/finance-system.md b/docs/features/finance-system.md new file mode 100644 index 0000000..69de904 --- /dev/null +++ b/docs/features/finance-system.md @@ -0,0 +1,1058 @@ +# 财务系统设计文档 + +> **版本**:v3.0(合并版) +> **最后更新**:2024-01-XX +> **状态**:✅ 已实现并优化 + +--- + +## 📋 目录 + +1. [概述](#概述) +2. [账户体系](#账户体系) +3. [资金流转](#资金流转) +4. [结算周期](#结算周期) +5. [服务费计算](#服务费计算) +6. [核心服务](#核心服务) +7. [数据库表结构](#数据库表结构) +8. [前端展示](#前端展示) +9. [API接口](#api接口) +10. [扩展方案](#扩展方案) +11. [安全保障](#安全保障) +12. [数据一致性保证](#数据一致性保证) +13. [测试验证](#测试验证) +14. [部署说明](#部署说明) +15. [常见问题](#常见问题) +16. [更新日志](#更新日志) + +--- + +## 概述 + +本文档描述了租房平台的财务系统架构,包括账户体系、资金流转、结算逻辑和安全保障机制。 + +**核心特性**: +- ✅ 四层账户体系(系统总账户、平台账户、商家账户、用户账户) +- ✅ 资金守恒验证机制 +- ✅ 订单支付、结算、退款、提现完整流程 +- ✅ 自动周结算(每周一凌晨2点) +- ✅ 邀请返现机制 +- ✅ 多重安全保障(事务、悲观锁、乐观锁、复式记账) +- ✅ 完整的交易流水记录 + +--- + +## 账户体系 + +### 四层账户结构 + +系统采用四层账户结构,确保资金流转清晰、职责明确: + +#### 1. 系统总账户 (System Account) + +**职责**:记录平台内所有未提现的资金总额 + +**计算公式**: +``` +系统总账户余额 = 用户实付总额 - 累计退款 - 累计提现 +``` + +**字段说明**: +- `balance`: 当前余额 +- `total_income`: 累计收入(用户实付总额) +- `total_refund`: 累计退款 +- `total_withdrawn`: 累计提现 + +**数据表**:`system_accounts` + +#### 2. 平台账户 (Platform Account) + +**职责**:记录平台净收益(服务费收入 - 邀请返现支出) + +**计算公式**: +``` +平台账户余额 = 累计服务费收入 - 累计邀请返现支出 +``` + +**字段说明**: +- `balance`: 当前余额 +- `total_income`: 累计收入(服务费收入) +- `total_expense`: 累计支出(邀请返现支出) + +**数据表**:`platform_accounts` + +#### 3. 商家账户 (Merchant Account) + +**职责**:记录商家的订单收入和提现 + +**计算公式**: +``` +商家账户余额 = 累计订单收入 - 累计提现 +订单收入 = 订单金额 - 服务费 +``` + +**字段说明**: +- `balance`: 可用余额 +- `frozen_balance`: 冻结余额(提现申请中) +- `total_income`: 累计收入 +- `total_expense`: 累计支出(提现) + +**数据表**:`merchant_accounts` + +#### 4. 用户账户 (User Account) + +**职责**:记录用户的邀请返现收入和提现 + +**计算公式**: +``` +用户账户余额 = 累计邀请返现 - 累计提现 +``` + +**字段说明**: +- `balance`: 可用余额 +- `frozen_balance`: 冻结余额(提现申请中) +- `total_income`: 累计收入(邀请返现) +- `total_expense`: 累计支出(提现) + +**数据表**:`user_accounts` + +### 资金守恒验证 + +系统通过以下公式验证资金守恒: + +``` +系统总账户余额 = 商家账户余额总和 + 用户账户余额总和 + 平台账户余额 +``` + +这个公式确保平台内所有资金都有明确的归属,不会出现资金丢失或凭空产生的情况。 + +## 资金流转 + +### 1. 订单支付流程 + +``` +用户支付订单 + ↓ +系统总账户 +实付金额 + ↓ +订单状态:pending_pay → pending_confirm +``` + +**涉及文件**: +- `apps/server/src/modules/app/order/order.service.ts` (payByOrderNo, pay 方法) +- `apps/server/src/modules/shared/finance/account.service.ts` (addSystemIncome 方法) + +**关键代码**: +```typescript +// 记录系统总账户收入(用户实付金额) +await this.accountService.addSystemIncome( + order.payAmount, + transactionNo, + 'order_payment', + order.id, + order.orderNo, + `用户支付订单:${order.orderNo}`, +); +``` + +### 2. 订单结算流程 + +``` +订单完成(用户离店) + ↓ +商家账户 +(实付金额 - 服务费) + ↓ +平台账户 +服务费 + ↓ +如果有邀请关系: + 用户账户(邀请人)+返现金额 + 平台账户 -返现金额 +``` + +**涉及文件**: +- `apps/server/src/modules/shared/finance/settlement.service.ts` (createSettlement 方法) +- `apps/server/src/modules/app/activity/activity.service.ts` (handleOrderCompleted 方法) + +**关键代码**: +```typescript +// 给商家账户增加余额 +await this.accountService.addMerchantBalance( + merchantId, + settlementAmount, // 订单金额 - 服务费 + transactionNo, + 'settlement', + settlement.id, + settlementNo, + `商家周结算:${periodStart} ~ ${periodEnd}` +); + +// 给平台账户增加服务费收入 +await this.accountService.addPlatformBalance( + serviceFee, + transactionNo, + 'settlement', + settlement.id, + settlementNo, + `商家 ${merchantId} 周结算服务费:${periodStart} ~ ${periodEnd}` +); + +// 邀请返现(如果有) +await this.accountService.addUserBalance( + invitation.inviterId, + amount, + transactionNo, + 'invite_cashback', + cashback.id, + order.orderNo, + `邀请返现 - 订单${order.orderNo}第${orderIndex}单`, +); + +await this.accountService.deductPlatformCashback( + amount, + transactionNo, + 'invite_cashback', + cashback.id, + order.orderNo, + `邀请返现支出 - 用户${invitation.inviterId}订单${order.orderNo}`, +); +``` + +### 3. 订单退款流程 + +``` +用户申请退款 + ↓ +调用第三方支付退款API + ↓ +系统总账户 +退款金额 + ↓ +订单状态:pending_confirm/pending_checkin → refunded +``` + +**涉及文件**: +- `apps/server/src/modules/shared/finance/refund.service.ts` (processRefund 方法) + +**关键代码**: +```typescript +// 记录系统总账户退款 +await this.accountService.addSystemRefund( + order.payAmount, + transactionNo, + 'order_refund', + order.id, + order.orderNo, + `订单退款 - ${order.orderNo}`, +); +``` + +### 4. 提现流程 + +#### 用户提现 + +``` +用户申请提现 + ↓ +用户账户:balance → frozen_balance + ↓ +管理员审核通过 + ↓ +管理员确认打款 + ↓ +用户账户 -提现金额(从frozen_balance扣减) +系统总账户 +提现金额 + ↓ +提现状态:pending → approved → paid +``` + +#### 商家提现 + +``` +商家申请提现 + ↓ +商家账户:balance → frozen_balance + ↓ +管理员审核通过 + ↓ +管理员确认打款 + ↓ +商家账户 -提现金额(从frozen_balance扣减) +系统总账户 +提现金额 + ↓ +提现状态:pending → approved → paid +``` + +**涉及文件**: +- `apps/server/src/modules/shared/finance/withdrawal.service.ts` (payUserWithdrawal, payMerchantWithdrawal 方法) + +**关键代码**: +```typescript +// 扣减用户账户余额 +await this.accountService.deductUserBalance( + withdrawal.userId, + Number(withdrawal.actualAmount), + transactionNo, + 'withdraw', + withdrawal.id, + withdrawal.withdrawNo, + `用户提现 - ${withdrawal.paymentChannel}` +); + +// 记录系统总账户提现 +await this.accountService.addSystemWithdrawal( + Number(withdrawal.actualAmount), + transactionNo, + 'user_withdraw', + withdrawal.id, + withdrawal.withdrawNo, + `用户 ${withdrawal.userId} 提现 - ${withdrawal.paymentChannel}`, +); +``` + +## 结算周期 + +### 周结算机制 + +**执行时间**:每周一凌晨 2:00 +**结算周期**:上周一 00:00:00 ~ 上周日 23:59:59 +**结算对象**:状态为 `completed` 的订单 +**判断依据**:订单的 `checkout_at`(离店时间)字段 + +### 定时任务配置 + +**文件位置**:`apps/server/src/schedule/settlement.schedule.ts` + +```typescript +@Cron('0 2 * * 1') // 每周一凌晨2点 +async handleWeeklySettlement() { + await this.settlementService.handleWeeklySettlement(); +} +``` + +### 结算逻辑 + +1. **查询上周已完成订单** + ```typescript + 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. **执行资金转账** + - 商家账户增加 + - 平台账户增加(服务费) + - 复式记账 + +### 防止重复结算 + +```typescript +// 检查该周期是否已经结算过 +const existingSettlements = await this.settlementRepo.count({ + where: { + periodStart: lastWeekStart, + periodEnd: lastWeekEnd + } +}); + +if (existingSettlements > 0) { + throw new Error(`该周期 ${lastWeekStart} ~ ${lastWeekEnd} 已经结算过,无法重复结算`); +} +``` + +--- + +## 服务费计算 + +### 计算公式 + +```typescript +// 用户实付金额 +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元 +``` + +--- + +## 核心服务 + +### AccountService + +账户操作核心服务,提供所有账户的增减操作。 + +**位置**:`apps/server/src/modules/shared/finance/account.service.ts` + +**主要方法**: + +#### 系统总账户操作 +- `getSystemAccount()` - 获取系统总账户 +- `addSystemIncome(amount, ...)` - 记录用户实付收入 +- `addSystemRefund(amount, ...)` - 记录退款 +- `addSystemWithdrawal(amount, ...)` - 记录提现 + +#### 平台账户操作 +- `addPlatformBalance(serviceFee, ...)` - 记录服务费收入 +- `deductPlatformCashback(amount, ...)` - 记录邀请返现支出 + +**核心代码示例**: + +```typescript +// 平台账户增加余额(服务费收入) +async addPlatformBalance( + amount: number, + transactionNo: string, + businessType: string, + businessId: number, + businessNo: string, + remark: string, +): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // 使用悲观锁查询账户 + const account = await queryRunner.manager.findOne(PlatformAccount, { + where: { account_name: '主账户' }, + lock: { mode: 'pessimistic_write' }, + }); + + // 金额精度控制 + const amountNum = Number(amount); + const balanceBefore = Number(account.balance); + const balanceAfter = parseFloat((balanceBefore + amountNum).toFixed(2)); + + // 更新账户 + account.balance = balanceAfter; + account.total_income = parseFloat((Number(account.total_income) + amountNum).toFixed(2)); + account.version += 1; + + await queryRunner.manager.save(account); + + // 记录交易流水 + const transaction = queryRunner.manager.create(PlatformTransaction, { + transactionNo, + accountId: account.id, + direction: 'income', + amount: amountNum, + balanceBefore, + balanceAfter, + transactionType: 'service_fee', + businessType, + businessId, + businessNo, + remark, + }); + + await queryRunner.manager.save(transaction); + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } +} +``` + +#### 商家账户操作 +- `addMerchantBalance(merchantId, amount, ...)` - 增加商家余额 +- `deductMerchantBalance(merchantId, amount, ...)` - 扣减商家余额 +- `freezeMerchantBalance(merchantId, amount)` - 冻结商家余额 +- `unfreezeMerchantBalance(merchantId, amount)` - 解冻商家余额 + +#### 用户账户操作 +- `addUserBalance(userId, amount, ...)` - 增加用户余额 +- `deductUserBalance(userId, amount, ...)` - 扣减用户余额 +- `freezeUserBalance(userId, amount)` - 冻结用户余额 +- `unfreezeUserBalance(userId, amount)` - 解冻用户余额 + +**安全机制**: +- 使用数据库事务确保操作原子性 +- 使用悲观锁(`pessimistic_write`)防止并发修改 +- 使用乐观锁(`version` 字段)检测并发冲突 +- 所有金额计算使用 `Number()` 和 `toFixed(2)` 确保精度 + +### SettlementService + +订单结算服务,负责商家的周结算。 + +**位置**:`apps/server/src/modules/shared/finance/settlement.service.ts` + +**主要功能**: +- 每周一凌晨2点自动执行周结算 +- 统计上周已完成订单 +- 按商家分组计算结算金额 +- 更新商家账户和平台账户 + +### RefundService + +退款服务,负责订单退款处理。 + +**位置**:`apps/server/src/modules/shared/finance/refund.service.ts` + +**主要功能**: +- 调用第三方支付退款API(微信支付、支付宝) +- 更新订单状态 +- 记录系统总账户退款 + +### WithdrawalService + +提现服务,负责用户和商家的提现申请、审核、打款。 + +**位置**:`apps/server/src/modules/shared/finance/withdrawal.service.ts` + +**主要功能**: +- 提现申请(冻结余额) +- 提现审核(通过/拒绝) +- 提现打款(扣减余额,记录系统总账户提现) + +### ReportService + +财务报表服务,提供财务数据统计。 + +**位置**:`apps/server/src/modules/shared/finance/report.service.ts` + +**主要功能**: +- 财务总览(系统总账户、平台账户、商家账户、用户账户统计) +- 财务趋势(按日统计收入支出) +- 资金守恒验证 + +## 数据库表结构 + +### system_accounts(系统总账户表) + +```sql +CREATE TABLE `system_accounts` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `account_name` VARCHAR(50) NOT NULL COMMENT '账户名称', + `balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '可用余额', + `total_income` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计收入(用户实付总额)', + `total_refund` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计退款', + `total_withdrawn` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计提现', + `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_account_name` (`account_name`) +) COMMENT='系统总账户表'; +``` + +### system_transactions(系统总账户交易流水表) + +```sql +CREATE TABLE `system_transactions` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `transaction_no` VARCHAR(64) NOT NULL COMMENT '交易流水号', + `account_id` BIGINT UNSIGNED NOT NULL COMMENT '账户ID', + `direction` ENUM('income','expense') NOT NULL COMMENT '交易方向', + `amount` DECIMAL(12,2) NOT NULL COMMENT '交易金额', + `balance_before` DECIMAL(12,2) NOT NULL COMMENT '交易前余额', + `balance_after` DECIMAL(12,2) NOT NULL COMMENT '交易后余额', + `transaction_type` VARCHAR(50) NOT NULL COMMENT '交易类型', + `business_type` VARCHAR(50) NOT NULL COMMENT '业务类型', + `business_id` BIGINT UNSIGNED NOT NULL COMMENT '业务ID', + `business_no` VARCHAR(64) NOT NULL COMMENT '业务单号', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_transaction_no` (`transaction_no`), + KEY `idx_account_id` (`account_id`), + KEY `idx_business` (`business_type`, `business_id`), + KEY `idx_created_at` (`created_at`) +) COMMENT='系统总账户交易流水表'; +``` + +### platform_accounts(平台账户表) + +```sql +CREATE TABLE `platform_accounts` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `account_name` VARCHAR(50) NOT NULL COMMENT '账户名称', + `balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '可用余额', + `total_income` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计收入(服务费收入)', + `total_expense` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计支出(邀请返现支出)', + `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_account_name` (`account_name`) +) COMMENT='平台账户表'; +``` + +## 前端展示 + +### 1. 财务总览页面 + +**位置**:`apps/platform-admin/src/pages/finance/Dashboard.tsx` + +**展示内容**: + +1. **系统总账户金额**(突出显示) + - 计算公式:用户实付 - 退款 - 提现 + - 资金守恒验证:= 商家账户余额 + 用户账户余额 + 平台净收益 + +2. **平台净收益** + - 服务费收入 - 邀请返现支出 + +3. **用户总余额** + - 所有用户账户余额总和 + - 用户账户数量 + +4. **商家总余额** + - 所有商家账户余额总和 + - 商家账户数量 + +5. **今日统计** + - 今日订单数 + - 今日收入 + - 今日支出 + - 今日净收益 + +### 2. 平台钱包页面 + +**位置**:`apps/platform-admin/src/pages/finance/PlatformWallet.tsx` + +**功能特性**: +- 钱包余额、可提现金额、冻结金额、服务费收入展示 +- 账户总收入、总支出、其他收入统计 +- 钱包详情查看 +- 申请提现功能(集成在页面内) + +### 3. 平台交易记录 + +**位置**:`apps/platform-admin/src/pages/finance/PlatformTransactions.tsx` + +**功能特性**: +- 交易流水查询(按流水号、方向、类型、时间筛选) +- 交易详情展示 +- 分页展示 + +### 4. 结算管理 + +**位置**:`apps/platform-admin/src/pages/finance/Settlements.tsx` + +**功能特性**: +- 结算单列表查询 +- 结算单详情查看 +- 预览周结算数据 +- 手动执行周结算 + +### 5. 商家提现审核 + +**位置**:`apps/platform-admin/src/pages/finance/Withdrawals.tsx` + +**功能特性**: +- 商家提现申请列表 +- 审核通过/拒绝 +- 确认打款 + +--- + +## API接口 + +### 平台钱包接口 + +#### 1. 查询平台账户 + +**接口**:`GET /api/admin/finance/platform-accounts` +**权限**:平台管理员 + +**响应示例**: +```json +{ + "id": 1, + "account_name": "主账户", + "balance": 40.00, + "frozen_balance": 0.00, + "total_income": 1000.00, + "total_expense": 960.00, + "status": "active" +} +``` + +#### 2. 平台申请提现 + +**接口**:`POST /api/admin/finance/platform-withdrawals` +**权限**:平台管理员 + +**请求参数**: +```json +{ + "amount": 30.00, + "bankName": "中国工商银行", + "bankAccount": "6222021234567890123", + "accountName": "某某科技有限公司", + "remark": "提现备注" +} +``` + +### 结算管理接口 + +#### 3. 预览周结算 + +**接口**:`GET /api/admin/finance/settlements/preview-weekly` +**权限**:平台管理员 + +**响应示例**: +```json +{ + "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. 数据库增加字段 + +```sql +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. 后端增加方法 + +```typescript +// 广告收入 +async addPlatformAdRevenue(amount: number, ...): Promise { + account.balance += amount; + account.total_income += amount; + account.total_ad_revenue += amount; +} + +// 会员费收入 +async addPlatformMembershipFee(amount: number, ...): Promise { + account.balance += amount; + account.total_income += amount; + account.total_membership_fee += amount; +} +``` + +#### 3. 前端增加展示 + +```typescript + + + +``` + +#### 4. 调整可提现计算 + +```typescript +// 方案1:继续使用各项收入累加 +const withdrawableAmount = ( + total_service_fee + + total_ad_revenue + + total_membership_fee +) - frozen_balance; + +// 方案2:直接使用钱包余额(推荐) +const withdrawableAmount = balance - frozen_balance; +``` + +**推荐方案2**,因为: +- `balance` 已经包含了所有收入和支出 +- 不需要每次增加新收入类型都修改代码 +- 更简洁、更易维护 + +--- + +## 前端展示 + +## 安全保障 + +### 1. 数据库事务 + +所有涉及账户操作的方法都使用数据库事务,确保操作的原子性: + +```typescript +const queryRunner = this.dataSource.createQueryRunner(); +await queryRunner.connect(); +await queryRunner.startTransaction(); + +try { + // 账户操作 + await queryRunner.commitTransaction(); +} catch (error) { + await queryRunner.rollbackTransaction(); + throw error; +} finally { + await queryRunner.release(); +} +``` + +### 2. 悲观锁 + +在查询账户时使用悲观锁,防止并发修改: + +```typescript +const account = await queryRunner.manager.findOne(SystemAccount, { + where: { account_name: '系统总账户' }, + lock: { mode: 'pessimistic_write' }, +}); +``` + +### 3. 乐观锁 + +使用 `version` 字段实现乐观锁,检测并发冲突: + +```typescript +account.version += 1; +await queryRunner.manager.save(account); +``` + +### 4. 金额精度控制 + +所有金额计算都使用 `Number()` 转换和 `toFixed(2)` 保留两位小数: + +```typescript +const amountNum = Number(amount); +const balanceBefore = Number(account.balance); +const balanceAfter = parseFloat((balanceBefore + amountNum).toFixed(2)); +``` + +### 5. 余额校验 + +在扣减余额前检查余额是否足够: + +```typescript +if (balanceBefore < amount) { + throw new BadRequestException('账户余额不足'); +} +``` + +### 6. 交易流水 + +所有账户操作都记录详细的交易流水,包括: +- 交易流水号(唯一) +- 交易前后余额 +- 交易类型和业务类型 +- 关联的业务ID和业务单号 +- 备注信息 + +## 测试验证 + +### 资金守恒验证 + +定期执行以下查询验证资金守恒: + +```sql +-- 查询系统总账户余额 +SELECT balance FROM system_accounts WHERE account_name = '系统总账户'; + +-- 查询商家账户余额总和 +SELECT SUM(balance) FROM merchant_accounts; + +-- 查询用户账户余额总和 +SELECT SUM(balance) FROM user_accounts; + +-- 查询平台账户余额 +SELECT balance FROM platform_accounts WHERE account_name = '主账户'; + +-- 验证:系统总账户余额 = 商家账户余额 + 用户账户余额 + 平台账户余额 +``` + +### 完整流程测试 + +1. **订单支付测试** + - 创建订单并支付 + - 验证系统总账户余额增加 + +2. **订单结算测试** + - 订单完成后触发结算 + - 验证商家账户余额增加 + - 验证平台账户余额增加(服务费) + - 如果有邀请关系,验证用户账户余额增加(返现) + +3. **订单退款测试** + - 申请退款 + - 验证系统总账户退款金额增加 + +4. **提现测试** + - 申请提现 + - 审核通过 + - 确认打款 + - 验证账户余额扣减 + - 验证系统总账户提现金额增加 + +5. **资金守恒测试** + - 执行多个订单流程 + - 验证资金守恒公式始终成立 + +## 部署说明 + +### 数据库初始化 + +1. 执行数据库迁移脚本: +```bash +# 重新初始化数据库(会清空所有数据) +mysql -u root -p < database/migrations/001_init_schema.sql +``` + +2. 初始化系统总账户: +```sql +INSERT INTO system_accounts (account_name, balance, total_income, total_refund, total_withdrawn) +VALUES ('系统总账户', 0.00, 0.00, 0.00, 0.00); +``` + +3. 初始化平台账户: +```sql +INSERT INTO platform_accounts (account_name, balance, total_income, total_expense) +VALUES ('主账户', 0.00, 0.00, 0.00); +``` + +### 服务启动 + +```bash +# 安装依赖 +pnpm install + +# 启动后端服务 +cd apps/server +pnpm run start:dev + +# 启动平台管理后台 +cd apps/platform-admin +pnpm run dev +``` + +## 常见问题 + +### Q1: 为什么要分系统总账户和平台账户? + +A: 系统总账户记录的是平台内所有未提现的资金总额,用于验证资金守恒。平台账户记录的是平台的净收益(服务费 - 邀请返现),用于计算平台的实际盈利。两者职责不同,分开管理更清晰。 + +### Q2: 订单支付时为什么只更新系统总账户,不更新商家账户? + +A: 订单支付时资金进入平台,但还未分配给商家。只有订单完成(用户离店)后才进行结算,将资金分配给商家。这样可以防止用户退款时出现商家已收款的情况。 + +### Q3: 邀请返现为什么要从平台账户扣减? + +A: 邀请返现是平台的营销成本,应该从平台的收益中扣除。平台的净收益 = 服务费收入 - 邀请返现支出。 + +### Q4: 如何确保资金安全? + +A: 系统采用多重安全机制: +- 数据库事务确保操作原子性 +- 悲观锁和乐观锁防止并发问题 +- 详细的交易流水记录 +- 资金守恒验证 +- 余额校验 + +### Q5: 如果发现账户余额不对怎么办? + +A: +1. 查看交易流水,定位问题操作 +2. 检查是否有并发操作导致的问题 +3. 验证资金守恒公式是否成立 +4. 如果是数据错误,需要手动调整并记录原因 + +## 更新日志 + +### 2024-01-XX - 账户体系重构 + +**重大变更**: +- 新增系统总账户,记录平台内所有未提现的资金 +- 修改平台账户职责,只记录平台净收益 +- 修改订单支付逻辑,更新系统总账户 +- 修改订单结算逻辑,同时更新商家账户和平台账户 +- 修改邀请返现逻辑,从平台账户扣减 +- 修改退款逻辑,更新系统总账户 +- 修改提现逻辑,更新系统总账户 +- 修改财务报表,使用新的账户数据 +- 修改前端页面,展示新的账户结构 + +**影响范围**: +- 数据库表结构变更,需要重新初始化数据库 +- 所有涉及账户操作的代码都需要更新 +- 前端页面需要更新 + +**升级步骤**: +1. 备份现有数据库 +2. 执行新的数据库迁移脚本 +3. 更新后端代码 +4. 更新前端代码 +5. 重启服务 +6. 验证资金守恒 + +## 联系方式 + +如有问题,请联系开发团队。 + +--- + +## 相关文档 + +- [财务数据库设计](../database/finance-database.md) +- [邀请码系统设计](./invite-system.md) +- [项目文档索引](../README.md) + +--- + +**版本**:v3.0(合并版) +**最后更新**:2024-01-XX +**维护团队**:开发团队 diff --git a/docs/features/settlement-and-finance-system.md b/docs/features/settlement-and-finance-system.md deleted file mode 100644 index a12c262..0000000 --- a/docs/features/settlement-and-finance-system.md +++ /dev/null @@ -1,789 +0,0 @@ -# 结算与财务系统设计文档 - -> **版本**:v2.0 -> **最后更新**:2026-05-21 -> **状态**:✅ 已实现并优化 - ---- - -## 📋 目录 - -1. [系统概述](#系统概述) -2. [核心概念](#核心概念) -3. [资金流转流程](#资金流转流程) -4. [平台钱包设计](#平台钱包设计) -5. [结算周期](#结算周期) -6. [服务费计算](#服务费计算) -7. [数据库设计](#数据库设计) -8. [API接口](#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: +50(5%) -└─ 商家账户:无变化(等待结算) -``` - -**代码位置**:[order.service.ts](../../apps/server/src/modules/app/order/order.service.ts) - -```typescript -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](../../apps/server/src/modules/shared/finance/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 -``` - ---- - -## 平台钱包设计 - -### 钱包余额计算 - -```typescript -// 钱包余额 = 账户总收入 - 账户总支出 -balance = total_income - total_expense - -// 展开计算 -balance = (订单总额) - (商家结算 + 邀请返现) - = (商家应得 + 服务费) - 商家应得 - 邀请返现 - = 服务费 - 邀请返现 -``` - -### 可提现金额计算 - -**当前实现:** -```typescript -// 可提现金额 = 累计服务费 - 冻结余额 -const withdrawableAmount = total_service_fee - frozen_balance; -``` - -**未来扩展:** -```typescript -// 方案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](../../apps/server/src/schedule/settlement.schedule.ts) - -```typescript -@Cron('0 2 * * 1') // 每周一凌晨2点 -async handleWeeklySettlement() { - await this.settlementService.handleWeeklySettlement(); -} -``` - -### 结算逻辑 - -1. **查询上周已完成订单** - ```typescript - 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. **执行资金转账** - - 平台账户扣减 - - 商家账户增加 - - 复式记账 - -### 防止重复结算 - -```typescript -// 检查该周期是否已经结算过 -const existingSettlements = await this.settlementRepo.count({ - where: { - periodStart: lastWeekStart, - periodEnd: lastWeekEnd - } -}); - -if (existingSettlements > 0) { - throw new Error(`该周期 ${lastWeekStart} ~ ${lastWeekEnd} 已经结算过,无法重复结算`); -} -``` - ---- - -## 服务费计算 - -### 计算公式 - -```typescript -// 用户实付金额 -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(平台账户表) - -```sql -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(商家账户表) - -```sql -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(结算单表) - -```sql -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(平台交易流水表) - -```sql -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` -**权限**:平台管理员 - -**响应示例**: -```json -{ - "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` -**权限**:平台管理员 - -**请求参数**: -```json -{ - "amount": 30.00, - "bankName": "中国工商银行", - "bankAccount": "6222021234567890123", - "accountName": "某某科技有限公司", - "remark": "提现备注" -} -``` - -### 结算管理接口 - -#### 3. 预览周结算 - -**接口**:`GET /api/admin/finance/settlements/preview-weekly` -**权限**:平台管理员 - -**响应示例**: -```json -{ - "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](../../apps/server/src/modules/shared/finance/account.service.ts) - -**核心方法**: - -```typescript -// 平台账户增加余额(订单支付) -async addPlatformBalance( - amount: number, - serviceFee: number, - transactionNo: string, - businessType: string, - businessId: number, - businessNo: string, - remark: string, -): Promise { - // 更新字段: - // - 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 { - // 更新字段: - // - balance: -amount - // - total_expense: +amount -} -``` - -#### 2. WithdrawalService(提现服务) - -**文件位置**:[withdrawal.service.ts](../../apps/server/src/modules/shared/finance/withdrawal.service.ts) - -**平台提现逻辑**: - -```typescript -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; - - // 创建提现记录 - // ... -} -``` - -### 并发控制 - -#### 悲观锁 - -```typescript -const account = await queryRunner.manager.findOne(PlatformAccount, { - where: { account_name: '主账户' }, - lock: { mode: 'pessimistic_write' } // 行级锁 -}); -``` - -#### 乐观锁 - -```typescript -await this.accountRepo.update( - { id: accountId, version: currentVersion }, - { balance: newBalance, version: currentVersion + 1 } -); -``` - -### 复式记账 - -每笔转账生成两条流水记录: -- 一条支出记录(平台账户) -- 一条收入记录(商家账户) - -两条流水的 `transaction_no` 相同,通过 `related_account_type` 和 `related_account_id` 关联。 - ---- - -## 前端页面 - -### 1. 平台钱包页面 - -**文件位置**:[PlatformWallet.tsx](../../apps/platform-admin/src/pages/finance/PlatformWallet.tsx) - -**功能特性**: -- 钱包余额、可提现金额、冻结金额、服务费收入展示 -- 账户总收入、总支出、其他收入统计 -- 钱包详情查看 -- 申请提现功能(集成在页面内) - -### 2. 平台交易记录 - -**文件位置**:[PlatformTransactions.tsx](../../apps/platform-admin/src/pages/finance/PlatformTransactions.tsx) - -**功能特性**: -- 交易流水查询(按流水号、方向、类型、时间筛选) -- 交易详情展示 -- 分页展示 - -### 3. 结算管理 - -**文件位置**:[Settlements.tsx](../../apps/platform-admin/src/pages/finance/Settlements.tsx) - -**功能特性**: -- 结算单列表查询 -- 结算单详情查看 -- 预览周结算数据 -- 手动执行周结算 - -### 4. 商家提现审核 - -**文件位置**:[Withdrawals.tsx](../../apps/platform-admin/src/pages/finance/Withdrawals.tsx) - -**功能特性**: -- 商家提现申请列表 -- 审核通过/拒绝 -- 确认打款 - ---- - -## 扩展方案 - -### 未来增加其他收入 - -#### 1. 数据库增加字段 - -```sql -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. 后端增加方法 - -```typescript -// 广告收入 -async addPlatformAdRevenue(amount: number, ...): Promise { - account.balance += amount; - account.total_income += amount; - account.total_ad_revenue += amount; -} - -// 会员费收入 -async addPlatformMembershipFee(amount: number, ...): Promise { - account.balance += amount; - account.total_income += amount; - account.total_membership_fee += amount; -} -``` - -#### 3. 前端增加展示 - -```typescript - - - -``` - -#### 4. 调整可提现计算 - -```typescript -// 方案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 约束 - -```sql -CONSTRAINT `chk_balance` CHECK (`balance` >= 0) -CONSTRAINT `chk_frozen_balance` CHECK (`frozen_balance` >= 0) -``` - ---- - -## 相关文档 - -- [项目需求文档](../requirements/项目需求文档.md) -- [数据库设计](../database/finance-database.md) -- [开发总结](../DEVELOPMENT_SUMMARY.md) - ---- - -**维护团队**:开发团队 -**最后更新**:2026-05-21