diff --git a/backend/services/planting-service/src/infrastructure/external/wallet-service.client.ts b/backend/services/planting-service/src/infrastructure/external/wallet-service.client.ts index b09e9027..c2698fcc 100644 --- a/backend/services/planting-service/src/infrastructure/external/wallet-service.client.ts +++ b/backend/services/planting-service/src/infrastructure/external/wallet-service.client.ts @@ -406,6 +406,50 @@ export class WalletServiceClient { } } + /** + * [2026-03-01] 预种购买后触发待领取→可结算转换 + * + * 调用 wallet-service 内部 API,将用户标记为 hasPlanted=true, + * 并结算所有 PENDING 奖励为 SETTLED。 + * 幂等:重复调用安全,不会重复结算。 + */ + async settleAfterPrePlanting(accountSequence: string): Promise<{ + markedAsPlanted: boolean; + settledCount: number; + totalUsdt: number; + totalHashpower: number; + }> { + try { + return await this.withRetry( + `settleAfterPrePlanting(${accountSequence})`, + async () => { + const response = await firstValueFrom( + this.httpService.post<{ + markedAsPlanted: boolean; + settledCount: number; + totalUsdt: number; + totalHashpower: number; + }>( + `${this.baseUrl}/api/v1/wallets/settle-after-pre-planting`, + { accountSequence }, + ), + ); + return response.data; + }, + ); + } catch (error) { + this.logger.error( + `Failed to settle after pre-planting for ${accountSequence}`, + error, + ); + if (this.configService.get('NODE_ENV') === 'development') { + this.logger.warn('Development mode: simulating successful settle-after-pre-planting'); + return { markedAsPlanted: false, settledCount: 0, totalUsdt: 0, totalHashpower: 0 }; + } + throw error; + } + } + /** * 注入底池 */ diff --git a/backend/services/planting-service/src/pre-planting/application/services/pre-planting-application.service.ts b/backend/services/planting-service/src/pre-planting/application/services/pre-planting-application.service.ts index 84df442c..3037ca4d 100644 --- a/backend/services/planting-service/src/pre-planting/application/services/pre-planting-application.service.ts +++ b/backend/services/planting-service/src/pre-planting/application/services/pre-planting-application.service.ts @@ -182,6 +182,25 @@ export class PrePlantingApplicationService { provinceCode, cityCode, ); + + // [2026-03-01] 预种购买后触发待领取→可结算转换 + // 将用户标记为 hasPlanted=true,并结算所有 PENDING 奖励 + // 幂等:重复调用安全。失败不阻塞购买流程(奖励仍在 PENDING,不会丢失) + try { + const settleResult = await this.walletClient.settleAfterPrePlanting(accountSequence); + this.logger.log( + `[PRE-PLANTING] Settle after purchase: order=${orderNo}, ` + + `markedAsPlanted=${settleResult.markedAsPlanted}, ` + + `settled=${settleResult.settledCount} rewards, usdt=${settleResult.totalUsdt}`, + ); + } catch (settleError) { + // 结算失败不影响购买结果,奖励仍在 PENDING 状态,用户可后续手动领取 + this.logger.error( + `[PRE-PLANTING] Failed to settle after purchase for order ${orderNo}, ` + + `accountSequence=${accountSequence}. Rewards remain in PENDING status.`, + settleError, + ); + } } catch (error) { // 事务失败,解冻余额 this.logger.error( diff --git a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts index f4327056..4c66dcdc 100644 --- a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts +++ b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts @@ -137,6 +137,30 @@ export class InternalWalletController { return { success }; } + /** + * [2026-03-01] 预种购买后触发待领取→可结算转换(内部API) + * + * planting-service 在用户购买预种份额后调用此端点: + * 1. markUserAsPlanted(accountSequence) — 设置 hasPlanted=true + * 2. settleUserPendingRewards(accountSequence) — 结算所有 PENDING 奖励 + * 两步均幂等,重复调用安全。 + */ + @Post('settle-after-pre-planting') + @Public() + @ApiOperation({ summary: '预种购买后结算待领取奖励(内部API) - 幂等' }) + @ApiResponse({ status: 200, description: '结算结果' }) + async settleAfterPrePlanting( + @Body() dto: { accountSequence: string }, + ) { + this.logger.log(`========== settle-after-pre-planting 请求 ==========`); + this.logger.log(`accountSequence: ${dto.accountSequence}`); + + const result = await this.walletService.settleAfterPrePlanting(dto.accountSequence); + + this.logger.log(`预种结算结果: ${JSON.stringify(result)}`); + return result; + } + @Post('allocate-funds') @Public() @ApiOperation({ summary: '资金分配(内部API)' }) 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 2abda856..64f3c26e 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 @@ -2490,6 +2490,44 @@ export class WalletApplicationService { this.logger.log(`[markUserAsPlanted] User ${accountSequence} marked as planted`); } + /** + * [2026-03-01] 预种购买后触发待领取→可结算转换 + * + * 用户购买预种份额后,planting-service 调用此方法, + * 将该用户标记为 hasPlanted=true,并结算所有 PENDING 奖励为 SETTLED。 + * 内部复用已有幂等方法:markUserAsPlanted + settleUserPendingRewards。 + */ + async settleAfterPrePlanting(accountSequence: string): Promise<{ + markedAsPlanted: boolean; + settledCount: number; + totalUsdt: number; + totalHashpower: number; + }> { + this.logger.log(`[settleAfterPrePlanting] Processing for ${accountSequence}`); + + // Step 1: 标记为已认种(幂等) + const walletBefore = await this.walletRepo.findByAccountSequence(accountSequence); + const wasAlreadyPlanted = walletBefore?.hasPlanted ?? false; + + await this.markUserAsPlanted(accountSequence); + + // Step 2: 结算所有 PENDING 奖励(幂等) + const settleResult = await this.settleUserPendingRewards(accountSequence); + + this.logger.log( + `[settleAfterPrePlanting] Done for ${accountSequence}: ` + + `wasAlreadyPlanted=${wasAlreadyPlanted}, settled=${settleResult.settledCount} rewards, ` + + `usdt=${settleResult.totalUsdt}, hashpower=${settleResult.totalHashpower}`, + ); + + return { + markedAsPlanted: !wasAlreadyPlanted, + settledCount: settleResult.settledCount, + totalUsdt: settleResult.totalUsdt, + totalHashpower: settleResult.totalHashpower, + }; + } + /** * 处理过期奖励 * 定时任务调用,将已过期的 PENDING 奖励标记为 EXPIRED