feat(pre-planting): 预种购买后自动触发待领取→可结算转换

购买预种份额后,planting-service 调用 wallet-service 新增的内部 API,
将用户标记为 hasPlanted=true 并结算所有 PENDING 奖励为 SETTLED。
纯新增代码,不修改任何现有方法逻辑,两步均幂等,失败不阻塞购买流程。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-01 08:51:03 -08:00
parent e9b9896317
commit 722c124cc9
4 changed files with 125 additions and 0 deletions

View File

@ -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;
}
}
/** /**
* *
*/ */

View File

@ -182,6 +182,25 @@ export class PrePlantingApplicationService {
provinceCode, provinceCode,
cityCode, 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) { } catch (error) {
// 事务失败,解冻余额 // 事务失败,解冻余额
this.logger.error( this.logger.error(

View File

@ -137,6 +137,30 @@ export class InternalWalletController {
return { success }; 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') @Post('allocate-funds')
@Public() @Public()
@ApiOperation({ summary: '资金分配(内部API)' }) @ApiOperation({ summary: '资金分配(内部API)' })

View File

@ -2490,6 +2490,44 @@ export class WalletApplicationService {
this.logger.log(`[markUserAsPlanted] User ${accountSequence} marked as planted`); 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 * PENDING EXPIRED