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:
parent
d20ff9e9b5
commit
ebbf2d971a
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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: [] };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue