feat: 跨服务使用 accountSequence 查询推荐链 + 系统账户动态创建

1. reward-service 使用 accountSequence 查询推荐链
   - event-consumer.controller.ts: 优先使用 accountSequence 作为用户标识
   - reward-calculation.service.ts: 使用 accountSequence 查询推荐关系
   - referral-service.client.ts: 参数从 userId 改为 accountSequence

2. referral-service 支持 accountSequence 格式的推荐链查询
   - referral.controller.ts: /chain/:identifier 同时支持 userId 和 accountSequence

3. wallet-service 系统账户动态创建
   - wallet-application.service.ts: allocateToUserWallet 使用 getOrCreate
   - 支持省区域(9+code)和市区域(8+code)账户自动创建
   - 新增 migration seed: 4个固定系统账户 (S0000000001-S0000000004)

4. planting-service 事件增强
   - 事件中添加 accountSequence 字段用于跨服务关联

系统账户格式:
- S0000000001: 总部社区 (基础费9U + 兜底权益)
- S0000000002: 成本费账户 (400U)
- S0000000003: 运营费账户 (300U)
- S0000000004: RWAD底池账户 (800U)
- 9+provinceCode: 省区域系统账户 (动态创建)
- 8+cityCode: 市区域系统账户 (动态创建)

🤖 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-12 23:22:01 -08:00
parent d20ff9e9b5
commit ebbf2d971a
9 changed files with 145 additions and 30 deletions

View File

@ -267,7 +267,7 @@ export class PlantingApplicationService {
// 8. 添加 Outbox 事件(在同一事务中保存)
// 使用 Outbox Pattern 保证事件发布的可靠性
const outboxEvents = this.buildOutboxEvents(order, selection);
const outboxEvents = this.buildOutboxEvents(order, selection, accountSequence);
uow.addOutboxEvents(outboxEvents);
// 提交 Outbox 事件(在事务中保存到数据库)
@ -478,6 +478,7 @@ export class PlantingApplicationService {
private buildOutboxEvents(
order: PlantingOrder,
selection: { provinceCode: string; cityCode: string },
accountSequence?: string,
): OutboxEventData[] {
const events: OutboxEventData[] = [];
@ -488,11 +489,12 @@ export class PlantingApplicationService {
events.push({
eventType: 'planting.planting.created',
topic: 'planting.planting.created',
key: order.userId.toString(),
key: accountSequence || order.userId.toString(),
payload: {
eventName: 'planting.created',
data: {
userId: order.userId.toString(),
accountSequence: accountSequence || order.userId.toString(), // 添加 accountSequence 用于跨服务关联
treeCount: order.treeCount.value,
provinceCode: selection.provinceCode,
cityCode: selection.cityCode,

View File

@ -97,13 +97,19 @@ export class ReferralController {
};
}
private readonly logger = new Logger(ReferralController.name);
/**
* API reward-service
*
*
* @param accountSequence
* - accountSequence 格式: D25121300006 25121300006 (D前缀)
* - userId 格式: 2 ()
*/
@Get('chain/:userId')
@Get('chain/:accountSequence')
@ApiOperation({ summary: '获取用户推荐链内部API' })
@ApiParam({ name: 'userId', description: '用户ID' })
@ApiParam({ name: 'accountSequence', description: '用户标识 (accountSequence 或 userId)' })
@ApiResponse({
status: 200,
description: '推荐链数据',
@ -116,6 +122,7 @@ export class ReferralController {
type: 'object',
properties: {
userId: { type: 'string' },
accountSequence: { type: 'string' },
hasPlanted: { type: 'boolean' },
},
},
@ -124,13 +131,33 @@ export class ReferralController {
},
})
async getReferralChainForReward(
@Param('userId') userId: string,
): Promise<{ ancestors: Array<{ userId: string; hasPlanted: boolean }> }> {
const userIdBigInt = BigInt(userId);
@Param('accountSequence') accountSequence: string,
): Promise<{ ancestors: Array<{ userId: string; accountSequence: string; hasPlanted: boolean }> }> {
this.logger.log(`[INTERNAL] getReferralChain: accountSequence=${accountSequence}`);
let relationship;
// 判断传入的是 accountSequence 还是 userId
// accountSequence 格式: D25121300006 或 25121300006 (11位数字或D+11位数字)
if (accountSequence.startsWith('D') || accountSequence.length >= 11) {
// 使用 accountSequence 查询
const normalizedSequence = accountSequence.startsWith('D') ? accountSequence : `D${accountSequence}`;
this.logger.log(`Using accountSequence query: ${normalizedSequence}`);
relationship = await this.referralRepo.findByAccountSequence(normalizedSequence);
} else {
// 尝试作为 userId 查询 (向后兼容)
this.logger.log(`Using userId query: ${accountSequence}`);
try {
const userIdBigInt = BigInt(accountSequence);
relationship = await this.referralRepo.findByUserId(userIdBigInt);
} catch {
this.logger.warn(`Invalid userId format: ${accountSequence}`);
return { ancestors: [] };
}
}
// 获取用户的推荐关系
const relationship = await this.referralRepo.findByUserId(userIdBigInt);
if (!relationship || !relationship.referrerId) {
this.logger.log(`No referral found for accountSequence: ${accountSequence}`);
return { ancestors: [] };
}
@ -139,10 +166,17 @@ export class ReferralController {
const referrerStats = await this.teamStatsRepo.findByUserId(referrerId);
const hasPlanted = referrerStats ? referrerStats.personalPlantingCount > 0 : false;
// 获取推荐人的 accountSequence
const referrerRelationship = await this.referralRepo.findByUserId(referrerId);
const referrerAccountSequence = referrerRelationship?.accountSequence || referrerId.toString();
this.logger.log(`Found referrer: userId=${referrerId}, accountSequence=${referrerAccountSequence}, hasPlanted=${hasPlanted}`);
return {
ancestors: [
{
userId: referrerId.toString(),
accountSequence: referrerAccountSequence,
hasPlanted,
},
],

View File

@ -8,6 +8,7 @@ interface PlantingCreatedEvent {
eventName: string;
data: {
userId: string;
accountSequence: string; // 跨服务关联标识(优先使用)
treeCount: number;
provinceCode: string;
cityCode: string;
@ -138,12 +139,13 @@ export class PlantingCreatedHandler implements OnModuleInit {
try {
await this.kafkaService.publish({
topic: 'planting.order.paid',
key: event.data.userId,
key: event.data.accountSequence || event.data.userId,
value: {
eventName: 'planting.order.paid',
data: {
orderId: event.data.orderId,
userId: event.data.userId,
accountSequence: event.data.accountSequence, // 跨服务关联标识
treeCount: event.data.treeCount,
provinceCode: event.data.provinceCode,
cityCode: event.data.cityCode,

View File

@ -45,11 +45,12 @@ export class RewardApplicationService {
async distributeRewards(params: {
sourceOrderNo: string; // 订单号是字符串格式如 PLT1765391584505Q0Q6QD
sourceUserId: bigint;
sourceAccountSequence?: string; // 跨服务关联标识
treeCount: number;
provinceCode: string;
cityCode: string;
}): Promise<void> {
this.logger.log(`Distributing rewards for order ${params.sourceOrderNo}`);
this.logger.log(`Distributing rewards for order ${params.sourceOrderNo}, accountSequence=${params.sourceAccountSequence}`);
// 1. 计算所有奖励(包含考核逻辑,调用 authorization-service
const rewards = await this.rewardCalculationService.calculateRewards(params);

View File

@ -7,8 +7,8 @@ import { Hashpower } from '../value-objects/hashpower.vo';
// 外部服务接口 (防腐层)
export interface IReferralServiceClient {
getReferralChain(userId: bigint): Promise<{
ancestors: Array<{ userId: bigint; hasPlanted: boolean }>;
getReferralChain(accountSequence: string): Promise<{
ancestors: Array<{ userId: bigint; accountSequence: string; hasPlanted: boolean }>;
}>;
}
@ -77,6 +77,7 @@ export class RewardCalculationService {
async calculateRewards(params: {
sourceOrderNo: string; // 订单号是字符串格式
sourceUserId: bigint;
sourceAccountSequence?: string; // 跨服务关联标识
treeCount: number;
provinceCode: string;
cityCode: string;
@ -132,6 +133,7 @@ export class RewardCalculationService {
const shareRewards = await this.calculateShareRights(
params.sourceOrderNo,
params.sourceUserId,
params.sourceAccountSequence,
params.treeCount,
);
rewards.push(...shareRewards);
@ -317,9 +319,10 @@ export class RewardCalculationService {
private async calculateShareRights(
sourceOrderNo: string,
sourceUserId: bigint,
sourceAccountSequence: string | undefined,
treeCount: number,
): Promise<RewardLedgerEntry[]> {
this.logger.debug(`[calculateShareRights] userId=${sourceUserId}, treeCount=${treeCount}`);
this.logger.debug(`[calculateShareRights] userId=${sourceUserId}, accountSequence=${sourceAccountSequence}, treeCount=${treeCount}`);
const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.SHARE_RIGHT];
const usdtAmount = Money.USDT(usdt * treeCount);
@ -331,8 +334,10 @@ export class RewardCalculationService {
sourceUserId,
);
// 获取推荐链
const referralChain = await this.referralService.getReferralChain(sourceUserId);
// 使用 accountSequence 获取推荐链(优先),否则用 userId
const queryId = sourceAccountSequence || sourceUserId.toString();
this.logger.debug(`[calculateShareRights] querying referral chain with: ${queryId}`);
const referralChain = await this.referralService.getReferralChain(queryId);
if (referralChain.ancestors.length > 0) {
const directReferrer = referralChain.ancestors[0];
@ -340,28 +345,28 @@ export class RewardCalculationService {
if (directReferrer.hasPlanted) {
// 推荐人已认种,直接可结算
this.logger.debug(
`[calculateShareRights] referrer=${directReferrer.userId} hasPlanted=true -> SETTLEABLE`,
`[calculateShareRights] referrer=${directReferrer.userId} (${directReferrer.accountSequence}) hasPlanted=true -> SETTLEABLE`,
);
return [RewardLedgerEntry.createSettleable({
userId: directReferrer.userId,
accountSequence: directReferrer.userId.toString(),
accountSequence: directReferrer.accountSequence,
rewardSource,
usdtAmount,
hashpowerAmount: hashpower,
memo: `分享权益:来自用户${sourceUserId}的认种`,
memo: `分享权益:来自用户${sourceAccountSequence || sourceUserId}的认种`,
})];
} else {
// 推荐人未认种进入待领取24h倒计时
this.logger.debug(
`[calculateShareRights] referrer=${directReferrer.userId} hasPlanted=false -> PENDING (24h)`,
`[calculateShareRights] referrer=${directReferrer.userId} (${directReferrer.accountSequence}) hasPlanted=false -> PENDING (24h)`,
);
return [RewardLedgerEntry.createPending({
userId: directReferrer.userId,
accountSequence: directReferrer.userId.toString(),
accountSequence: directReferrer.accountSequence,
rewardSource,
usdtAmount,
hashpowerAmount: hashpower,
memo: `分享权益:来自用户${sourceUserId}的认种24h内认种可领取`,
memo: `分享权益:来自用户${sourceAccountSequence || sourceUserId}的认种24h内认种可领取`,
})];
}
} else {

View File

@ -11,27 +11,30 @@ export class ReferralServiceClient implements IReferralServiceClient {
this.baseUrl = this.configService.get<string>('REFERRAL_SERVICE_URL', 'http://localhost:3004');
}
async getReferralChain(userId: bigint): Promise<{
ancestors: Array<{ userId: bigint; hasPlanted: boolean }>;
async getReferralChain(accountSequence: string): Promise<{
ancestors: Array<{ userId: bigint; accountSequence: string; hasPlanted: boolean }>;
}> {
try {
const response = await fetch(`${this.baseUrl}/api/v1/referral/chain/${userId}`);
this.logger.log(`Fetching referral chain for accountSequence: ${accountSequence}`);
const response = await fetch(`${this.baseUrl}/api/v1/referral/chain/${accountSequence}`);
if (!response.ok) {
this.logger.warn(`Failed to get referral chain for user ${userId}: ${response.status}`);
this.logger.warn(`Failed to get referral chain for accountSequence ${accountSequence}: ${response.status}`);
return { ancestors: [] };
}
const data = await response.json();
this.logger.log(`Referral chain response: ${JSON.stringify(data)}`);
return {
ancestors: (data.ancestors || []).map((a: any) => ({
userId: BigInt(a.userId),
accountSequence: a.accountSequence || a.userId,
hasPlanted: a.hasPlanted ?? false,
})),
};
} catch (error) {
this.logger.error(`Error fetching referral chain for user ${userId}:`, error);
this.logger.error(`Error fetching referral chain for accountSequence ${accountSequence}:`, error);
return { ancestors: [] };
}
}

View File

@ -8,6 +8,7 @@ interface PlantingOrderPaidEvent {
data?: {
orderId: string;
userId: string;
accountSequence?: string; // 跨服务关联标识(优先使用)
treeCount: number;
provinceCode: string;
cityCode: string;
@ -16,6 +17,7 @@ interface PlantingOrderPaidEvent {
// 兼容旧格式
orderId?: string;
userId?: string;
accountSequence?: string;
treeCount?: number;
provinceCode?: string;
cityCode?: string;
@ -48,12 +50,17 @@ export class EventConsumerController {
const eventData = message.data || {
orderId: message.orderId!,
userId: message.userId!,
accountSequence: message.accountSequence,
treeCount: message.treeCount!,
provinceCode: message.provinceCode!,
cityCode: message.cityCode!,
paidAt: message.paidAt!,
};
// 优先使用 accountSequence如果未提供则使用 userId
const userIdentifier = eventData.accountSequence || eventData.userId;
this.logger.log(`Processing event with userIdentifier: ${userIdentifier} (accountSequence: ${eventData.accountSequence}, userId: ${eventData.userId})`);
// B方案提取 outbox 信息用于发送确认
const outboxInfo = message._outbox;
const eventId = outboxInfo?.aggregateId || eventData.orderId;
@ -63,6 +70,7 @@ export class EventConsumerController {
await this.rewardService.distributeRewards({
sourceOrderNo: eventData.orderId, // orderId 实际是 orderNo 字符串格式
sourceUserId: BigInt(eventData.userId),
sourceAccountSequence: userIdentifier, // 优先使用 accountSequence
treeCount: eventData.treeCount,
provinceCode: eventData.provinceCode,
cityCode: eventData.cityCode,

View File

@ -0,0 +1,36 @@
-- Seed system accounts for reward distribution
-- These accounts receive fixed allocations from each planting order
-- Note: Province/City area accounts (9+provinceCode, 8+cityCode) are dynamically created
-- when users select their planting location
-- S0000000001: 总部社区 (Headquarters Community)
-- Receives: 基础费 9 USDT, 无推荐人的分享权益 500 USDT, 无授权的团队权益等
INSERT INTO "wallet_accounts" (
"account_sequence", "user_id", "status", "created_at", "updated_at"
) VALUES (
'S0000000001', 1, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
) ON CONFLICT ("account_sequence") DO NOTHING;
-- S0000000002: 成本费账户 (Cost Fee Account)
-- Receives: 成本费 400 USDT per tree
INSERT INTO "wallet_accounts" (
"account_sequence", "user_id", "status", "created_at", "updated_at"
) VALUES (
'S0000000002', 2, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
) ON CONFLICT ("account_sequence") DO NOTHING;
-- S0000000003: 运营费账户 (Operation Fee Account)
-- Receives: 运营费 300 USDT per tree
INSERT INTO "wallet_accounts" (
"account_sequence", "user_id", "status", "created_at", "updated_at"
) VALUES (
'S0000000003', 3, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
) ON CONFLICT ("account_sequence") DO NOTHING;
-- S0000000004: RWAD底池账户 (RWAD Pool Injection Account)
-- Receives: 底池注入 800 USDT per tree
INSERT INTO "wallet_accounts" (
"account_sequence", "user_id", "status", "created_at", "updated_at"
) VALUES (
'S0000000004', 4, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
) ON CONFLICT ("account_sequence") DO NOTHING;

View File

@ -614,15 +614,39 @@ export class WalletApplicationService {
/**
*
* (D++) (S+, 9+, 8+)
*/
private async allocateToUserWallet(
allocation: FundAllocationItem,
orderId: string,
): Promise<void> {
// targetId 是 accountSequence (如 D2512120001),优先用它查找钱包
const wallet = await this.walletRepo.findByAccountSequence(allocation.targetId);
// targetId 是 accountSequence
// - 用户账户: D2512120001
// - 固定系统账户: S0000000001
// - 省区域账户: 9440000 (9 + provinceCode)
// - 市区域账户: 8440100 (8 + cityCode)
// 为系统账户生成 userId (用于数据库约束)
// 省区域: 使用 9 + provinceCode 作为 userId
// 市区域: 使用 8 + cityCode 作为 userId
let systemUserId = BigInt(0);
const targetId = allocation.targetId;
if (targetId.startsWith('9')) {
// 省区域账户: 9440000 -> userId = 9440000
systemUserId = BigInt(targetId);
} else if (targetId.startsWith('8')) {
// 市区域账户: 8440100 -> userId = 8440100
systemUserId = BigInt(targetId);
} else if (targetId.startsWith('S')) {
// 固定系统账户: S0000000001 -> userId 从后面的数字提取
const numPart = targetId.slice(1);
systemUserId = BigInt(parseInt(numPart, 10));
}
// 使用 getOrCreate 自动创建不存在的账户(包括省/市区域账户)
const wallet = await this.walletRepo.getOrCreate(targetId, systemUserId);
if (!wallet) {
this.logger.warn(`Wallet not found for accountSequence ${allocation.targetId}, skipping allocation`);
this.logger.warn(`Failed to get or create wallet for accountSequence ${allocation.targetId}`);
return;
}