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

View File

@ -97,13 +97,19 @@ export class ReferralController {
}; };
} }
private readonly logger = new Logger(ReferralController.name);
/** /**
* API reward-service * API reward-service
* *
*
* @param accountSequence
* - accountSequence 格式: D25121300006 25121300006 (D前缀)
* - userId 格式: 2 ()
*/ */
@Get('chain/:userId') @Get('chain/:accountSequence')
@ApiOperation({ summary: '获取用户推荐链内部API' }) @ApiOperation({ summary: '获取用户推荐链内部API' })
@ApiParam({ name: 'userId', description: '用户ID' }) @ApiParam({ name: 'accountSequence', description: '用户标识 (accountSequence 或 userId)' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: '推荐链数据', description: '推荐链数据',
@ -116,6 +122,7 @@ export class ReferralController {
type: 'object', type: 'object',
properties: { properties: {
userId: { type: 'string' }, userId: { type: 'string' },
accountSequence: { type: 'string' },
hasPlanted: { type: 'boolean' }, hasPlanted: { type: 'boolean' },
}, },
}, },
@ -124,13 +131,33 @@ export class ReferralController {
}, },
}) })
async getReferralChainForReward( async getReferralChainForReward(
@Param('userId') userId: string, @Param('accountSequence') accountSequence: string,
): Promise<{ ancestors: Array<{ userId: string; hasPlanted: boolean }> }> { ): Promise<{ ancestors: Array<{ userId: string; accountSequence: string; hasPlanted: boolean }> }> {
const userIdBigInt = BigInt(userId); 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) { if (!relationship || !relationship.referrerId) {
this.logger.log(`No referral found for accountSequence: ${accountSequence}`);
return { ancestors: [] }; return { ancestors: [] };
} }
@ -139,10 +166,17 @@ export class ReferralController {
const referrerStats = await this.teamStatsRepo.findByUserId(referrerId); const referrerStats = await this.teamStatsRepo.findByUserId(referrerId);
const hasPlanted = referrerStats ? referrerStats.personalPlantingCount > 0 : false; 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 { return {
ancestors: [ ancestors: [
{ {
userId: referrerId.toString(), userId: referrerId.toString(),
accountSequence: referrerAccountSequence,
hasPlanted, hasPlanted,
}, },
], ],

View File

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

View File

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

View File

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

View File

@ -11,27 +11,30 @@ export class ReferralServiceClient implements IReferralServiceClient {
this.baseUrl = this.configService.get<string>('REFERRAL_SERVICE_URL', 'http://localhost:3004'); this.baseUrl = this.configService.get<string>('REFERRAL_SERVICE_URL', 'http://localhost:3004');
} }
async getReferralChain(userId: bigint): Promise<{ async getReferralChain(accountSequence: string): Promise<{
ancestors: Array<{ userId: bigint; hasPlanted: boolean }>; ancestors: Array<{ userId: bigint; accountSequence: string; hasPlanted: boolean }>;
}> { }> {
try { 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) { 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: [] }; return { ancestors: [] };
} }
const data = await response.json(); const data = await response.json();
this.logger.log(`Referral chain response: ${JSON.stringify(data)}`);
return { return {
ancestors: (data.ancestors || []).map((a: any) => ({ ancestors: (data.ancestors || []).map((a: any) => ({
userId: BigInt(a.userId), userId: BigInt(a.userId),
accountSequence: a.accountSequence || a.userId,
hasPlanted: a.hasPlanted ?? false, hasPlanted: a.hasPlanted ?? false,
})), })),
}; };
} catch (error) { } 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: [] }; return { ancestors: [] };
} }
} }

View File

@ -8,6 +8,7 @@ interface PlantingOrderPaidEvent {
data?: { data?: {
orderId: string; orderId: string;
userId: string; userId: string;
accountSequence?: string; // 跨服务关联标识(优先使用)
treeCount: number; treeCount: number;
provinceCode: string; provinceCode: string;
cityCode: string; cityCode: string;
@ -16,6 +17,7 @@ interface PlantingOrderPaidEvent {
// 兼容旧格式 // 兼容旧格式
orderId?: string; orderId?: string;
userId?: string; userId?: string;
accountSequence?: string;
treeCount?: number; treeCount?: number;
provinceCode?: string; provinceCode?: string;
cityCode?: string; cityCode?: string;
@ -48,12 +50,17 @@ export class EventConsumerController {
const eventData = message.data || { const eventData = message.data || {
orderId: message.orderId!, orderId: message.orderId!,
userId: message.userId!, userId: message.userId!,
accountSequence: message.accountSequence,
treeCount: message.treeCount!, treeCount: message.treeCount!,
provinceCode: message.provinceCode!, provinceCode: message.provinceCode!,
cityCode: message.cityCode!, cityCode: message.cityCode!,
paidAt: message.paidAt!, 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 信息用于发送确认 // B方案提取 outbox 信息用于发送确认
const outboxInfo = message._outbox; const outboxInfo = message._outbox;
const eventId = outboxInfo?.aggregateId || eventData.orderId; const eventId = outboxInfo?.aggregateId || eventData.orderId;
@ -63,6 +70,7 @@ export class EventConsumerController {
await this.rewardService.distributeRewards({ await this.rewardService.distributeRewards({
sourceOrderNo: eventData.orderId, // orderId 实际是 orderNo 字符串格式 sourceOrderNo: eventData.orderId, // orderId 实际是 orderNo 字符串格式
sourceUserId: BigInt(eventData.userId), sourceUserId: BigInt(eventData.userId),
sourceAccountSequence: userIdentifier, // 优先使用 accountSequence
treeCount: eventData.treeCount, treeCount: eventData.treeCount,
provinceCode: eventData.provinceCode, provinceCode: eventData.provinceCode,
cityCode: eventData.cityCode, 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( private async allocateToUserWallet(
allocation: FundAllocationItem, allocation: FundAllocationItem,
orderId: string, orderId: string,
): Promise<void> { ): Promise<void> {
// targetId 是 accountSequence (如 D2512120001),优先用它查找钱包 // targetId 是 accountSequence
const wallet = await this.walletRepo.findByAccountSequence(allocation.targetId); // - 用户账户: 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) { 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; return;
} }