From ebbf2d971ad0f306c77d2414e0f6e4688ec0adbf Mon Sep 17 00:00:00 2001 From: hailin Date: Fri, 12 Dec 2025 23:22:01 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B7=A8=E6=9C=8D=E5=8A=A1=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20accountSequence=20=E6=9F=A5=E8=AF=A2=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E9=93=BE=20+=20=E7=B3=BB=E7=BB=9F=E8=B4=A6=E6=88=B7?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E5=88=9B=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../services/planting-application.service.ts | 6 ++- .../api/controllers/referral.controller.ts | 48 ++++++++++++++++--- .../planting-created.handler.ts | 4 +- .../services/reward-application.service.ts | 3 +- .../services/reward-calculation.service.ts | 27 ++++++----- .../referral-service.client.ts | 13 +++-- .../kafka/event-consumer.controller.ts | 8 ++++ .../migration.sql | 36 ++++++++++++++ .../services/wallet-application.service.ts | 30 ++++++++++-- 9 files changed, 145 insertions(+), 30 deletions(-) create mode 100644 backend/services/wallet-service/prisma/migrations/20241213000000_seed_system_accounts/migration.sql diff --git a/backend/services/planting-service/src/application/services/planting-application.service.ts b/backend/services/planting-service/src/application/services/planting-application.service.ts index 552329bf..c257827a 100644 --- a/backend/services/planting-service/src/application/services/planting-application.service.ts +++ b/backend/services/planting-service/src/application/services/planting-application.service.ts @@ -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, diff --git a/backend/services/referral-service/src/api/controllers/referral.controller.ts b/backend/services/referral-service/src/api/controllers/referral.controller.ts index 4d1cf04e..729be4fc 100644 --- a/backend/services/referral-service/src/api/controllers/referral.controller.ts +++ b/backend/services/referral-service/src/api/controllers/referral.controller.ts @@ -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, }, ], diff --git a/backend/services/referral-service/src/application/event-handlers/planting-created.handler.ts b/backend/services/referral-service/src/application/event-handlers/planting-created.handler.ts index 2cb8aaf4..03539ce2 100644 --- a/backend/services/referral-service/src/application/event-handlers/planting-created.handler.ts +++ b/backend/services/referral-service/src/application/event-handlers/planting-created.handler.ts @@ -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, diff --git a/backend/services/reward-service/src/application/services/reward-application.service.ts b/backend/services/reward-service/src/application/services/reward-application.service.ts index d6e8a130..4c625bfb 100644 --- a/backend/services/reward-service/src/application/services/reward-application.service.ts +++ b/backend/services/reward-service/src/application/services/reward-application.service.ts @@ -45,11 +45,12 @@ export class RewardApplicationService { async distributeRewards(params: { sourceOrderNo: string; // 订单号是字符串格式如 PLT1765391584505Q0Q6QD sourceUserId: bigint; + sourceAccountSequence?: string; // 跨服务关联标识 treeCount: number; provinceCode: string; cityCode: string; }): Promise { - 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); diff --git a/backend/services/reward-service/src/domain/services/reward-calculation.service.ts b/backend/services/reward-service/src/domain/services/reward-calculation.service.ts index 19a0fb3c..c8edade1 100644 --- a/backend/services/reward-service/src/domain/services/reward-calculation.service.ts +++ b/backend/services/reward-service/src/domain/services/reward-calculation.service.ts @@ -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 { - 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 { diff --git a/backend/services/reward-service/src/infrastructure/external/referral-service/referral-service.client.ts b/backend/services/reward-service/src/infrastructure/external/referral-service/referral-service.client.ts index 2ddf20fb..2dbe14d3 100644 --- a/backend/services/reward-service/src/infrastructure/external/referral-service/referral-service.client.ts +++ b/backend/services/reward-service/src/infrastructure/external/referral-service/referral-service.client.ts @@ -11,27 +11,30 @@ export class ReferralServiceClient implements IReferralServiceClient { this.baseUrl = this.configService.get('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: [] }; } } diff --git a/backend/services/reward-service/src/infrastructure/kafka/event-consumer.controller.ts b/backend/services/reward-service/src/infrastructure/kafka/event-consumer.controller.ts index f2a80ac3..74d9568b 100644 --- a/backend/services/reward-service/src/infrastructure/kafka/event-consumer.controller.ts +++ b/backend/services/reward-service/src/infrastructure/kafka/event-consumer.controller.ts @@ -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, diff --git a/backend/services/wallet-service/prisma/migrations/20241213000000_seed_system_accounts/migration.sql b/backend/services/wallet-service/prisma/migrations/20241213000000_seed_system_accounts/migration.sql new file mode 100644 index 00000000..4a127133 --- /dev/null +++ b/backend/services/wallet-service/prisma/migrations/20241213000000_seed_system_accounts/migration.sql @@ -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; diff --git a/backend/services/wallet-service/src/application/services/wallet-application.service.ts b/backend/services/wallet-service/src/application/services/wallet-application.service.ts index 8e4dea76..300ece1b 100644 --- a/backend/services/wallet-service/src/application/services/wallet-application.service.ts +++ b/backend/services/wallet-service/src/application/services/wallet-application.service.ts @@ -614,15 +614,39 @@ export class WalletApplicationService { /** * 分配资金到用户钱包 + * 支持用户账户 (D+日期+序号) 和系统账户 (S+序号, 9+省代码, 8+市代码) */ private async allocateToUserWallet( allocation: FundAllocationItem, orderId: string, ): Promise { - // 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; }