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:
parent
e9b9896317
commit
722c124cc9
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注入底池
|
* 注入底池
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)' })
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue