fix(wallet-service): 修复系统账户资金分配功能

问题:
- 认种订单支付后,系统账户(成本费、运营费、总部社区、RWA底池)余额始终为0
- reward-service 正确计算分配,但 wallet-service 未实际执行系统账户的资金转移

根本原因:
1. allocateToSystemAccount() 方法只打印日志,未执行任何数据库操作(遗留的 TODO)
2. UserId 值对象不允许负数,而系统账户 user_id 为负数(-1 到 -4)

修复内容:

1. wallet-application.service.ts - allocateToSystemAccount()
   - 实现完整的系统账户资金分配逻辑
   - 通过 findByAccountSequence() 获取系统账户
   - 调用 addAvailableBalance() 直接增加可用余额
   - 创建 SYSTEM_ALLOCATION 类型的流水记录

2. wallet-account.aggregate.ts
   - 新增 addAvailableBalance(amount: Money) 方法
   - 用于系统账户直接增加余额(无需待领取/过期机制)

3. ledger-entry-type.enum.ts
   - 新增 SYSTEM_ALLOCATION 枚举值,用于系统账户分配流水

4. user-id.vo.ts
   - 移除负数校验,允许系统账户使用负数 user_id
   - 系统账户约定:-1(总部社区)、-2(成本费)、-3(运营费)、-4(RWA底池)

验证结果(认种1棵树=2199 USDT):
- S0000000001 总部社区: 9 USDT ✓
- S0000000002 成本费账户: 400 USDT ✓
- S0000000003 运营费账户: 300 USDT ✓
- S0000000004 RWA底池: 800 USDT ✓

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-13 02:01:48 -08:00
parent 98d8bee20d
commit 4d3290f029
4 changed files with 117 additions and 72 deletions

View File

@ -685,31 +685,63 @@ export class WalletApplicationService {
);
}
/**
*
* authorization-service
*/
private async allocateToSystemAccount(
allocation: FundAllocationItem,
orderId: string,
): Promise<void> {
// 记录系统账户分配流水(用于审计和对账)
// 系统账户不通过 wallet-service 管理余额,而是发送事件通知 authorization-service
this.logger.debug(
`System account allocation: ${allocation.amount} USDT to ${allocation.targetId} for ${allocation.allocationType}`,
);
// TODO: 发布 Kafka 事件通知 authorization-service 更新系统账户余额
// await this.eventPublisher.publish('system-account.funds-allocated', {
// targetAccountType: allocation.targetId,
// amount: allocation.amount,
// allocationType: allocation.allocationType,
// sourceOrderId: orderId,
// hashpowerPercent: allocation.hashpowerPercent,
// metadata: allocation.metadata,
// });
}
/**
*
* S开头 migration seed
*
*
* - S0000000001: 总部社区 (user_id = -1)
* - S0000000002: 成本费账户 (user_id = -2)
* - S0000000003: 运营费账户 (user_id = -3)
* - S0000000004: RWA底池 (user_id = -4)
*/
private async allocateToSystemAccount(
allocation: FundAllocationItem,
orderId: string,
): Promise<void> {
this.logger.debug(
`System account allocation: ${allocation.amount} USDT to ${allocation.targetId} for ${allocation.allocationType}`,
);
const targetId = allocation.targetId;
if (!targetId.startsWith('S')) {
this.logger.warn(`Invalid system account format: ${targetId}`);
return;
}
// 获取系统账户(已由 migration seed 创建)
const wallet = await this.walletRepo.findByAccountSequence(targetId);
if (!wallet) {
this.logger.error(`System account not found: ${targetId}`);
return;
}
const amount = Money.USDT(allocation.amount);
// 系统账户直接增加可用余额(不需要待领取/过期机制)
wallet.addAvailableBalance(amount);
await this.walletRepo.save(wallet);
// 记录流水
const ledgerEntry = LedgerEntry.create({
accountSequence: wallet.accountSequence,
userId: wallet.userId,
entryType: LedgerEntryType.SYSTEM_ALLOCATION,
amount,
refOrderId: orderId,
memo: `${allocation.allocationType} - system account allocation`,
payloadJson: {
allocationType: allocation.allocationType,
metadata: allocation.metadata,
},
});
await this.ledgerRepo.save(ledgerEntry);
this.logger.debug(
`Allocated ${allocation.amount} USDT to system account ${targetId} for ${allocation.allocationType}`,
);
}
// =============== Region Accounts ===============
/**

View File

@ -186,6 +186,16 @@ export class WalletAccount {
}));
}
// 直接增加可用余额(用于系统账户分配)
addAvailableBalance(amount: Money): void {
this.ensureActive();
const balance = this.getBalance(amount.currency as AssetType);
const newBalance = balance.add(amount);
this.setBalance(amount.currency as AssetType, newBalance);
this._updatedAt = new Date();
}
// 扣款 (如认种支付)
deduct(amount: Money, reason: string, refOrderId?: string): void {
this.ensureActive();

View File

@ -1,18 +1,19 @@
export enum LedgerEntryType {
DEPOSIT_KAVA = 'DEPOSIT_KAVA',
DEPOSIT_BSC = 'DEPOSIT_BSC',
PLANT_PAYMENT = 'PLANT_PAYMENT',
PLANT_FREEZE = 'PLANT_FREEZE', // 认种冻结
PLANT_UNFREEZE = 'PLANT_UNFREEZE', // 认种解冻(失败回滚)
REWARD_PENDING = 'REWARD_PENDING',
REWARD_TO_SETTLEABLE = 'REWARD_TO_SETTLEABLE',
REWARD_EXPIRED = 'REWARD_EXPIRED',
REWARD_SETTLED = 'REWARD_SETTLED',
TRANSFER_TO_POOL = 'TRANSFER_TO_POOL',
SWAP_EXECUTED = 'SWAP_EXECUTED',
WITHDRAWAL = 'WITHDRAWAL',
TRANSFER_IN = 'TRANSFER_IN',
TRANSFER_OUT = 'TRANSFER_OUT',
FREEZE = 'FREEZE',
UNFREEZE = 'UNFREEZE',
}
export enum LedgerEntryType {
DEPOSIT_KAVA = 'DEPOSIT_KAVA',
DEPOSIT_BSC = 'DEPOSIT_BSC',
PLANT_PAYMENT = 'PLANT_PAYMENT',
PLANT_FREEZE = 'PLANT_FREEZE', // 认种冻结
PLANT_UNFREEZE = 'PLANT_UNFREEZE', // 认种解冻(失败回滚)
REWARD_PENDING = 'REWARD_PENDING',
REWARD_TO_SETTLEABLE = 'REWARD_TO_SETTLEABLE',
REWARD_EXPIRED = 'REWARD_EXPIRED',
REWARD_SETTLED = 'REWARD_SETTLED',
TRANSFER_TO_POOL = 'TRANSFER_TO_POOL',
SWAP_EXECUTED = 'SWAP_EXECUTED',
WITHDRAWAL = 'WITHDRAWAL',
TRANSFER_IN = 'TRANSFER_IN',
TRANSFER_OUT = 'TRANSFER_OUT',
FREEZE = 'FREEZE',
UNFREEZE = 'UNFREEZE',
SYSTEM_ALLOCATION = 'SYSTEM_ALLOCATION', // 系统账户分配
}

View File

@ -1,29 +1,31 @@
import { DomainError } from '@/shared/exceptions/domain.exception';
export class UserId {
private readonly _value: bigint;
private constructor(value: bigint) {
this._value = value;
}
static create(value: bigint | number | string): UserId {
const bigintValue = typeof value === 'bigint' ? value : BigInt(value);
if (bigintValue < 0) {
throw new DomainError('UserId cannot be negative');
}
return new UserId(bigintValue);
}
get value(): bigint {
return this._value;
}
equals(other: UserId): boolean {
return this._value === other._value;
}
toString(): string {
return this._value.toString();
}
}
import { DomainError } from '@/shared/exceptions/domain.exception';
export class UserId {
private readonly _value: bigint;
private constructor(value: bigint) {
this._value = value;
}
static create(value: bigint | number | string): UserId {
const bigintValue = typeof value === 'bigint' ? value : BigInt(value);
// 允许负数 userId用于系统账户
// -1: 总部社区 (S0000000001)
// -2: 成本费账户 (S0000000002)
// -3: 运营费账户 (S0000000003)
// -4: RWA底池 (S0000000004)
return new UserId(bigintValue);
}
get value(): bigint {
return this._value;
}
equals(other: UserId): boolean {
return this._value === other._value;
}
toString(): string {
return this._value.toString();
}
}