feat: 迭代

This commit is contained in:
2026-05-22 18:54:30 +08:00
parent ca22542c7a
commit 4c7a1e06a8
20 changed files with 2169 additions and 1087 deletions
+7 -2
View File
@@ -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
});
}
}
</script>
+8
View File
@@ -83,6 +83,14 @@ export function request<T = any>(options: RequestOptions): Promise<ApiResponse<T
return;
}
// 检查是否有 token,如果没有 token 说明是登录接口返回的 401,直接返回原始错误消息
const hasToken = useSellerToken ? uni.getStorageSync('sellerToken') : uni.getStorageSync('token');
if (!hasToken) {
// 登录接口的 401 错误,直接返回后端的错误消息
reject(new Error(responseData.message || '认证失败'));
return;
}
// 正常 401 处理,只清除对应账户的 token,不互相干扰
if (useSellerToken) {
// 商家 token 失效,跳转到商家登录页
@@ -5,15 +5,20 @@ import { getFinancialOverview, getPlatformTransactions } from '@/api/finance';
import type { ColumnsType } from 'antd/es/table';
interface FinancialOverview {
systemTotalAmount: number;
totalUserPaid: number;
totalRefund: number;
totalWithdrawn: number;
platformBalance: number;
platformTotalIncome: number;
platformTotalExpense: number;
totalUserBalance: number;
totalMerchantBalance: number;
userCount: number;
merchantCount: number;
todayIncome: number;
todayExpense: number;
monthIncome: number;
monthExpense: number;
pendingWithdrawals: number;
pendingSettlements: number;
todayOrders: number;
}
interface Transaction {
@@ -108,18 +113,21 @@ const FinanceDashboard: React.FC = () => {
{overview && (
<>
{/* 平台总账户额 - 突出显示 */}
{/* 系统总账户额 - 突出显示 */}
<Card style={{ marginBottom: 24, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
<Statistic
title={<span style={{ color: '#fff', fontSize: 16 }}></span>}
value={Number(overview.platformBalance || 0) + Number(overview.totalMerchantBalance || 0) + Number(overview.totalUserBalance || 0)}
title={<span style={{ color: '#fff', fontSize: 16 }}></span>}
value={Number(overview.systemTotalAmount || 0)}
prefix={<WalletOutlined style={{ color: '#fff' }} />}
suffix={<span style={{ color: '#fff' }}></span>}
precision={2}
valueStyle={{ color: '#fff', fontSize: 36, fontWeight: 'bold' }}
/>
<div style={{ color: 'rgba(255,255,255,0.8)', fontSize: 12, marginTop: 8 }}>
¥{Number(overview.totalUserPaid || 0).toFixed(2)}- 退¥{Number(overview.totalRefund || 0).toFixed(2)}- ¥{Number(overview.totalWithdrawn || 0).toFixed(2)}
</div>
<div style={{ color: 'rgba(255,255,255,0.6)', fontSize: 11, marginTop: 4 }}>
= ¥{Number(overview.totalMerchantBalance || 0).toFixed(2)}+ ¥{Number(overview.totalUserBalance || 0).toFixed(2)}+ ¥{Number(overview.platformBalance || 0).toFixed(2)}
</div>
</Card>
@@ -127,13 +135,16 @@ const FinanceDashboard: React.FC = () => {
<Col span={6}>
<Card>
<Statistic
title="平台账户余额"
title="平台净收益"
value={overview.platformBalance}
precision={2}
prefix={<WalletOutlined />}
suffix="元"
valueStyle={{ color: '#1890ff' }}
/>
<div style={{ fontSize: 12, color: '#999', marginTop: 8 }}>
-
</div>
</Card>
</Col>
<Col span={6}>
@@ -146,6 +157,9 @@ const FinanceDashboard: React.FC = () => {
suffix="元"
valueStyle={{ color: '#52c41a' }}
/>
<div style={{ fontSize: 12, color: '#999', marginTop: 8 }}>
{overview.userCount}
</div>
</Card>
</Col>
<Col span={6}>
@@ -158,23 +172,25 @@ const FinanceDashboard: React.FC = () => {
suffix="元"
valueStyle={{ color: '#faad14' }}
/>
<div style={{ fontSize: 12, color: '#999', marginTop: 8 }}>
{overview.merchantCount}
</div>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="待处理提现"
value={overview.pendingWithdrawals}
precision={2}
suffix="元"
valueStyle={{ color: '#ff4d4f' }}
title="今日订单数"
value={overview.todayOrders}
suffix="单"
valueStyle={{ color: '#722ed1' }}
/>
</Card>
</Col>
</Row>
<Row gutter={24} style={{ marginBottom: 24 }}>
<Col span={6}>
<Col span={8}>
<Card>
<Statistic
title="今日收入"
@@ -186,7 +202,7 @@ const FinanceDashboard: React.FC = () => {
/>
</Card>
</Col>
<Col span={6}>
<Col span={8}>
<Card>
<Statistic
title="今日支出"
@@ -198,6 +214,19 @@ const FinanceDashboard: React.FC = () => {
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="今日净收益"
value={Number(overview.todayIncome || 0) - Number(overview.todayExpense || 0)}
precision={2}
suffix="元"
valueStyle={{
color: (Number(overview.todayIncome || 0) - Number(overview.todayExpense || 0)) >= 0 ? '#52c41a' : '#ff4d4f'
}}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
@@ -8,21 +8,18 @@ export class PlatformAccount {
@Column({ type: 'varchar', length: 50, comment: '账户名称(如:主账户、备用账户)' })
account_name: string;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '可用余额' })
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '可用余额(平台净收益 = total_income - total_expense' })
balance: number;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '冻结余额(提现中)' })
frozen_balance: number;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '累计收入(订单收入)' })
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '累计收入(服务费收入)' })
total_income: number;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '累计支出(商家结算+退款' })
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '累计支出(邀请返现支出' })
total_expense: number;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '累计服务费收入' })
total_service_fee: number;
@VersionColumn({ comment: '乐观锁版本号' })
version: number;
@@ -0,0 +1,40 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, VersionColumn } from 'typeorm';
@Entity('system_accounts')
export class SystemAccount {
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
id: number;
@Column({ type: 'varchar', length: 50, comment: '账户名称(如:主账户)' })
account_name: string;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '可用余额(total_income - total_refund - total_withdrawn' })
balance: number;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '累计收入(用户实付总额)' })
total_income: number;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '累计退款' })
total_refund: number;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '累计提现(所有提现)' })
total_withdrawn: number;
@VersionColumn({ comment: '乐观锁版本号' })
version: number;
@Column({
type: 'enum',
enum: ['active', 'frozen', 'closed'],
default: 'active',
comment: '状态'
})
@Index()
status: 'active' | 'frozen' | 'closed';
@CreateDateColumn({ comment: '创建时间' })
created_at: Date;
@UpdateDateColumn({ comment: '更新时间' })
updated_at: Date;
}
@@ -0,0 +1,48 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index } from 'typeorm';
@Entity('system_transactions')
@Index('uk_transaction_no', ['transactionNo'], { unique: true })
@Index('idx_account_id', ['accountId'])
@Index('idx_transaction_type', ['transactionType'])
@Index('idx_business', ['businessType', 'businessId'])
@Index('idx_created_at', ['createdAt'])
export class SystemTransaction {
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true, comment: '流水ID' })
id: number;
@Column({ type: 'varchar', length: 32, name: 'transaction_no', comment: '交易流水号(全局唯一)' })
transactionNo: string;
@Column({ type: 'bigint', unsigned: true, name: 'account_id', comment: '系统账户ID' })
accountId: number;
@Column({ type: 'enum', enum: ['income', 'expense'], comment: '方向:income-收入/expense-支出' })
direction: 'income' | 'expense';
@Column({ type: 'decimal', precision: 12, scale: 2, comment: '金额(正数)' })
amount: number;
@Column({ type: 'decimal', precision: 12, scale: 2, name: 'balance_before', comment: '交易前余额' })
balanceBefore: number;
@Column({ type: 'decimal', precision: 12, scale: 2, name: 'balance_after', comment: '交易后余额' })
balanceAfter: number;
@Column({ type: 'varchar', length: 50, name: 'transaction_type', comment: '交易类型' })
transactionType: string;
@Column({ type: 'varchar', length: 50, name: 'business_type', comment: '业务类型:order_payment/refund/withdraw' })
businessType: string;
@Column({ type: 'bigint', unsigned: true, nullable: true, name: 'business_id', comment: '业务ID(订单ID/退款ID/提现ID等)' })
businessId: number;
@Column({ type: 'varchar', length: 32, nullable: true, name: 'business_no', comment: '业务单号' })
businessNo: string;
@Column({ type: 'varchar', length: 500, nullable: true, comment: '备注' })
remark: string;
@CreateDateColumn({ type: 'datetime', name: 'created_at', comment: '创建时间' })
createdAt: Date;
}
@@ -9,6 +9,7 @@ import { MktUserInviteStats } from '@/entities/mkt-user-invite-stats.entity';
import { MktInviteWithdrawal } from '@/entities/mkt-invite-withdrawal.entity';
import { Order } from '@/entities/order.entity';
import { User } from '@/entities/user.entity';
import { FinanceModule } from '@/modules/shared/finance/finance.module';
@Module({
imports: [
@@ -21,6 +22,7 @@ import { User } from '@/entities/user.entity';
Order,
User,
]),
FinanceModule,
],
controllers: [ActivityController],
providers: [ActivityService],
@@ -8,6 +8,8 @@ import { MktUserInviteStats } from '@/entities/mkt-user-invite-stats.entity';
import { MktInviteWithdrawal } from '@/entities/mkt-invite-withdrawal.entity';
import { Order } from '@/entities/order.entity';
import { User } from '@/entities/user.entity';
import { AccountService } from '@/modules/shared/finance/account.service';
import { TransactionService } from '@/modules/shared/finance/transaction.service';
import {
BindInvitationDto,
CreateInviteWithdrawalDto,
@@ -33,6 +35,8 @@ export class ActivityService {
private orderRepo: Repository<Order>,
@InjectRepository(User)
private userRepo: Repository<User>,
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}%`,
@@ -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,
@@ -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<MerchantAccount>,
@InjectRepository(PlatformAccount)
private platformAccountRepo: Repository<PlatformAccount>,
@InjectRepository(SystemAccount)
private systemAccountRepo: Repository<SystemAccount>,
@InjectRepository(UserTransaction)
private userTransactionRepo: Repository<UserTransaction>,
@InjectRepository(MerchantTransaction)
private merchantTransactionRepo: Repository<MerchantTransaction>,
@InjectRepository(PlatformTransaction)
private platformTransactionRepo: Repository<PlatformTransaction>,
@InjectRepository(SystemTransaction)
private systemTransactionRepo: Repository<SystemTransaction>,
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<void> {
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<SystemAccount> {
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<void> {
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<void> {
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<void> {
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();
}
}
}
@@ -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,
@@ -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<Order>,
@InjectRepository(PlatformAccount)
private platformAccountRepo: Repository<PlatformAccount>,
@InjectRepository(PlatformTransaction)
private platformTransactionRepo: Repository<PlatformTransaction>,
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<void> {
// 获取平台主账户
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}`,
);
}
/**
@@ -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<SystemAccount>,
@InjectRepository(PlatformAccount)
private platformAccountRepo: Repository<PlatformAccount>,
@InjectRepository(UserAccount)
@@ -24,12 +31,26 @@ export class ReportService {
private orderRepo: Repository<Order>,
@InjectRepository(Settlement)
private settlementRepo: Repository<Settlement>,
@InjectRepository(MerchantWithdrawal)
private merchantWithdrawalRepo: Repository<MerchantWithdrawal>,
@InjectRepository(UserWithdrawal)
private userWithdrawalRepo: Repository<UserWithdrawal>,
@InjectRepository(PlatformWithdrawal)
private platformWithdrawalRepo: Repository<PlatformWithdrawal>,
@InjectRepository(MktInviteWithdrawal)
private mktInviteWithdrawalRepo: Repository<MktInviteWithdrawal>,
) {}
/**
* 财务总览
*/
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),
@@ -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();
@@ -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,
+52 -17
View File
@@ -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 '对方账户类型',
+52 -32
View File
@@ -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% |
+452 -146
View File
@@ -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
File diff suppressed because it is too large Load Diff
@@ -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: +505%
└─ 商家账户:无变化(等待结算)
```
**代码位置**[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<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](../../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<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. 前端增加展示
```typescript
<Statistic
title="广告收入"
value={account.total_ad_revenue}
precision={2}
suffix="元"
/>
<Statistic
title="会员费收入"
value={account.total_membership_fee}
precision={2}
suffix="元"
/>
```
#### 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