feat: 已认种用户分享权益直接进入可结算状态
- 新增 hasPlanted 字段标记用户是否已认种 - 已认种用户的分享权益直接进入可结算余额,无需待领取 - 修正前端权益考核数值(576/288/252/144/108 绿积分) - 修复账本明细筛选栏选择后滚动位置重置问题 🤖 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
723d70e4b8
commit
3f5203c142
|
|
@ -0,0 +1,8 @@
|
|||
-- 添加 has_planted 字段,标记用户是否已认种过
|
||||
-- 认种后的用户,分享权益直接进入可结算状态,无需待领取
|
||||
|
||||
ALTER TABLE "wallet_accounts" ADD COLUMN "has_planted" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- 为已有认种记录的用户设置 has_planted = true
|
||||
-- 通过检查 settled_total_usdt > 0 来推断(有结算记录说明曾经认种过)
|
||||
UPDATE "wallet_accounts" SET "has_planted" = true WHERE "settled_total_usdt" > 0;
|
||||
|
|
@ -58,6 +58,9 @@ model WalletAccount {
|
|||
// 状态
|
||||
status String @default("ACTIVE") @map("status") @db.VarChar(20)
|
||||
|
||||
// 是否已认种过(认种后分享权益直接进入可结算)
|
||||
hasPlanted Boolean @default(false) @map("has_planted")
|
||||
|
||||
// 乐观锁版本号
|
||||
version Int @default(0) @map("version")
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { WalletApplicationService } from '@/application/services';
|
|||
* 处理认种创建事件
|
||||
*
|
||||
* 当用户认种一棵树后:
|
||||
* 1. 结算该用户所有待领取奖励(从 PENDING 变为 SETTLED)
|
||||
* 2. 待领取 → 可结算,算力生效
|
||||
* 1. 标记用户为已认种(后续分享权益直接进入可结算)
|
||||
* 2. 结算该用户所有待领取奖励(从 PENDING 变为 SETTLED)
|
||||
*/
|
||||
@Injectable()
|
||||
export class PlantingCreatedHandler implements OnModuleInit {
|
||||
|
|
@ -29,7 +29,10 @@ export class PlantingCreatedHandler implements OnModuleInit {
|
|||
this.logger.log(`[PLANTING] User ${accountSequence} planted ${treeCount} tree(s), order: ${orderNo}`);
|
||||
|
||||
try {
|
||||
// 用户认种后,结算其所有待领取奖励
|
||||
// 1. 标记用户为已认种(后续分享权益直接进入可结算)
|
||||
await this.walletService.markUserAsPlanted(accountSequence);
|
||||
|
||||
// 2. 结算用户所有待领取奖励
|
||||
const result = await this.walletService.settleUserPendingRewards(accountSequence);
|
||||
|
||||
if (result.settledCount > 0) {
|
||||
|
|
@ -41,7 +44,7 @@ export class PlantingCreatedHandler implements OnModuleInit {
|
|||
this.logger.debug(`[PLANTING] No pending rewards to settle for ${accountSequence}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`[PLANTING] Failed to settle pending rewards for ${accountSequence}`, error);
|
||||
this.logger.error(`[PLANTING] Failed to process planting for ${accountSequence}`, error);
|
||||
// 不抛出异常,避免阻塞 Kafka 消费
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -956,7 +956,7 @@ export class WalletApplicationService {
|
|||
return;
|
||||
}
|
||||
|
||||
// 分享权益 (SHARE_RIGHT) - 写入 pending_rewards 表待领取
|
||||
// 分享权益 (SHARE_RIGHT)
|
||||
// targetId 是 accountSequence (用户账户: D2512120001)
|
||||
const targetId = allocation.targetId;
|
||||
|
||||
|
|
@ -967,45 +967,73 @@ export class WalletApplicationService {
|
|||
return;
|
||||
}
|
||||
|
||||
// 添加待领取奖励(24小时后过期)- 写入 pending_rewards 表
|
||||
const expireAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
// 根据用户是否已认种,决定分配方式
|
||||
if (wallet.hasPlanted) {
|
||||
// 已认种用户:直接进入可结算余额
|
||||
wallet.addSettleableReward(amount, Hashpower.create(0));
|
||||
await this.walletRepo.save(wallet);
|
||||
|
||||
const pendingReward = PendingReward.create({
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: wallet.userId,
|
||||
usdtAmount: amount,
|
||||
hashpowerAmount: Hashpower.create(0),
|
||||
sourceOrderId: orderId,
|
||||
allocationType: allocation.allocationType,
|
||||
expireAt,
|
||||
});
|
||||
await this.pendingRewardRepo.save(pendingReward);
|
||||
// 记录流水 - 直接可结算
|
||||
const ledgerEntry = LedgerEntry.create({
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: wallet.userId,
|
||||
entryType: LedgerEntryType.REWARD_TO_SETTLEABLE,
|
||||
amount,
|
||||
refOrderId: orderId,
|
||||
memo: `${allocation.allocationType} allocation (direct settleable)`,
|
||||
payloadJson: {
|
||||
allocationType: allocation.allocationType,
|
||||
metadata: allocation.metadata,
|
||||
},
|
||||
});
|
||||
await this.ledgerRepo.save(ledgerEntry);
|
||||
|
||||
// 同步更新 wallet_accounts 表的 pending_usdt 字段
|
||||
wallet.addPendingReward(amount, Hashpower.create(0), expireAt, orderId);
|
||||
await this.walletRepo.save(wallet);
|
||||
await this.walletCacheService.invalidateWallet(wallet.userId.value);
|
||||
|
||||
// 记录流水
|
||||
const ledgerEntry = LedgerEntry.create({
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: wallet.userId,
|
||||
entryType: LedgerEntryType.REWARD_PENDING,
|
||||
amount,
|
||||
refOrderId: orderId,
|
||||
memo: `${allocation.allocationType} allocation`,
|
||||
payloadJson: {
|
||||
this.logger.debug(
|
||||
`Allocated ${allocation.amount} USDT to user ${allocation.targetId} for ${allocation.allocationType} (direct settleable)`,
|
||||
);
|
||||
} else {
|
||||
// 未认种用户:写入 pending_rewards 表待领取
|
||||
const expireAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
|
||||
const pendingReward = PendingReward.create({
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: wallet.userId,
|
||||
usdtAmount: amount,
|
||||
hashpowerAmount: Hashpower.create(0),
|
||||
sourceOrderId: orderId,
|
||||
allocationType: allocation.allocationType,
|
||||
expireAt: expireAt.toISOString(),
|
||||
metadata: allocation.metadata,
|
||||
},
|
||||
});
|
||||
await this.ledgerRepo.save(ledgerEntry);
|
||||
expireAt,
|
||||
});
|
||||
await this.pendingRewardRepo.save(pendingReward);
|
||||
|
||||
await this.walletCacheService.invalidateWallet(wallet.userId.value);
|
||||
// 同步更新 wallet_accounts 表的 pending_usdt 字段
|
||||
wallet.addPendingReward(amount, Hashpower.create(0), expireAt, orderId);
|
||||
await this.walletRepo.save(wallet);
|
||||
|
||||
this.logger.debug(
|
||||
`Allocated ${allocation.amount} USDT to user ${allocation.targetId} for ${allocation.allocationType} (pending)`,
|
||||
);
|
||||
// 记录流水
|
||||
const ledgerEntry = LedgerEntry.create({
|
||||
accountSequence: wallet.accountSequence,
|
||||
userId: wallet.userId,
|
||||
entryType: LedgerEntryType.REWARD_PENDING,
|
||||
amount,
|
||||
refOrderId: orderId,
|
||||
memo: `${allocation.allocationType} allocation`,
|
||||
payloadJson: {
|
||||
allocationType: allocation.allocationType,
|
||||
expireAt: expireAt.toISOString(),
|
||||
metadata: allocation.metadata,
|
||||
},
|
||||
});
|
||||
await this.ledgerRepo.save(ledgerEntry);
|
||||
|
||||
await this.walletCacheService.invalidateWallet(wallet.userId.value);
|
||||
|
||||
this.logger.debug(
|
||||
`Allocated ${allocation.amount} USDT to user ${allocation.targetId} for ${allocation.allocationType} (pending)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1790,6 +1818,29 @@ export class WalletApplicationService {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记用户为已认种
|
||||
* 认种后的用户,分享权益直接进入可结算状态
|
||||
*/
|
||||
async markUserAsPlanted(accountSequence: string): Promise<void> {
|
||||
const wallet = await this.walletRepo.findByAccountSequence(accountSequence);
|
||||
if (!wallet) {
|
||||
this.logger.warn(`[markUserAsPlanted] Wallet not found for ${accountSequence}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wallet.hasPlanted) {
|
||||
this.logger.debug(`[markUserAsPlanted] User ${accountSequence} already marked as planted`);
|
||||
return;
|
||||
}
|
||||
|
||||
wallet.markAsPlanted();
|
||||
await this.walletRepo.save(wallet);
|
||||
await this.walletCacheService.invalidateWallet(wallet.userId.value);
|
||||
|
||||
this.logger.log(`[markUserAsPlanted] User ${accountSequence} marked as planted`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理过期奖励
|
||||
* 定时任务调用,将已过期的 PENDING 奖励标记为 EXPIRED
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export class WalletAccount {
|
|||
private _hashpower: Hashpower;
|
||||
private _rewards: WalletRewards;
|
||||
private _status: WalletStatus;
|
||||
private _hasPlanted: boolean; // 是否已认种过
|
||||
private readonly _createdAt: Date;
|
||||
private _updatedAt: Date;
|
||||
private _domainEvents: DomainEvent[] = [];
|
||||
|
|
@ -51,6 +52,7 @@ export class WalletAccount {
|
|||
hashpower: Hashpower,
|
||||
rewards: WalletRewards,
|
||||
status: WalletStatus,
|
||||
hasPlanted: boolean,
|
||||
createdAt: Date,
|
||||
updatedAt: Date,
|
||||
) {
|
||||
|
|
@ -61,6 +63,7 @@ export class WalletAccount {
|
|||
this._hashpower = hashpower;
|
||||
this._rewards = rewards;
|
||||
this._status = status;
|
||||
this._hasPlanted = hasPlanted;
|
||||
this._createdAt = createdAt;
|
||||
this._updatedAt = updatedAt;
|
||||
}
|
||||
|
|
@ -75,6 +78,7 @@ export class WalletAccount {
|
|||
get status(): WalletStatus { return this._status; }
|
||||
get createdAt(): Date { return this._createdAt; }
|
||||
get updatedAt(): Date { return this._updatedAt; }
|
||||
get hasPlanted(): boolean { return this._hasPlanted; }
|
||||
get isActive(): boolean { return this._status === WalletStatus.ACTIVE; }
|
||||
get domainEvents(): DomainEvent[] { return [...this._domainEvents]; }
|
||||
|
||||
|
|
@ -104,6 +108,7 @@ export class WalletAccount {
|
|||
expiredTotalHashpower: Hashpower.zero(),
|
||||
},
|
||||
WalletStatus.ACTIVE,
|
||||
false, // hasPlanted
|
||||
now,
|
||||
now,
|
||||
);
|
||||
|
|
@ -134,6 +139,7 @@ export class WalletAccount {
|
|||
expiredTotalUsdt: Decimal;
|
||||
expiredTotalHashpower: Decimal;
|
||||
status: string;
|
||||
hasPlanted: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}): WalletAccount {
|
||||
|
|
@ -161,11 +167,18 @@ export class WalletAccount {
|
|||
expiredTotalHashpower: Hashpower.create(params.expiredTotalHashpower),
|
||||
},
|
||||
params.status as WalletStatus,
|
||||
params.hasPlanted,
|
||||
params.createdAt,
|
||||
params.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
// 标记为已认种
|
||||
markAsPlanted(): void {
|
||||
this._hasPlanted = true;
|
||||
this._updatedAt = new Date();
|
||||
}
|
||||
|
||||
// 充值入账
|
||||
deposit(amount: Money, chainType: string, txHash: string): void {
|
||||
this.ensureActive();
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export class WalletAccountRepositoryImpl implements IWalletAccountRepository {
|
|||
expiredTotalUsdt: wallet.rewards.expiredTotalUsdt.toDecimal(),
|
||||
expiredTotalHashpower: wallet.rewards.expiredTotalHashpower.decimal,
|
||||
status: wallet.status,
|
||||
hasPlanted: wallet.hasPlanted,
|
||||
};
|
||||
|
||||
if (wallet.walletId.value === BigInt(0)) {
|
||||
|
|
@ -118,6 +119,7 @@ export class WalletAccountRepositoryImpl implements IWalletAccountRepository {
|
|||
expiredTotalUsdt: Decimal;
|
||||
expiredTotalHashpower: Decimal;
|
||||
status: string;
|
||||
hasPlanted: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}): WalletAccount {
|
||||
|
|
@ -146,6 +148,7 @@ export class WalletAccountRepositoryImpl implements IWalletAccountRepository {
|
|||
expiredTotalUsdt: new Decimal(record.expiredTotalUsdt.toString()),
|
||||
expiredTotalHashpower: new Decimal(record.expiredTotalHashpower.toString()),
|
||||
status: record.status,
|
||||
hasPlanted: record.hasPlanted,
|
||||
createdAt: record.createdAt,
|
||||
updatedAt: record.updatedAt,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1838,7 +1838,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
initialTarget: _authCityCompanyInitialTarget,
|
||||
monthlyTarget: _authCityCompanyMonthlyTarget,
|
||||
monthIndex: _authCityCompanyMonthIndex,
|
||||
rewardDescription: '每新增认种 1 棵可获得 40 绿积分',
|
||||
rewardDescription: '每新增认种 1 棵可获得 288 绿积分',
|
||||
),
|
||||
],
|
||||
// 省团队权益考核
|
||||
|
|
@ -1852,7 +1852,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
initialTarget: _authProvinceCompanyInitialTarget,
|
||||
monthlyTarget: _authProvinceCompanyMonthlyTarget,
|
||||
monthIndex: _authProvinceCompanyMonthIndex,
|
||||
rewardDescription: '每新增认种 1 棵可获得 20 绿积分',
|
||||
rewardDescription: '每新增认种 1 棵可获得 144 绿积分',
|
||||
),
|
||||
],
|
||||
// 市区域权益考核
|
||||
|
|
@ -1866,7 +1866,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
initialTarget: _cityCompanyInitialTarget,
|
||||
monthlyTarget: _cityCompanyMonthlyTarget,
|
||||
monthIndex: _cityCompanyMonthIndex,
|
||||
rewardDescription: '每新增认种 1 棵可获得 35 绿积分',
|
||||
rewardDescription: '每新增认种 1 棵可获得 252 绿积分',
|
||||
),
|
||||
],
|
||||
// 省区域权益考核
|
||||
|
|
@ -1880,7 +1880,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
initialTarget: _provinceCompanyInitialTarget,
|
||||
monthlyTarget: _provinceCompanyMonthlyTarget,
|
||||
monthIndex: _provinceCompanyMonthIndex,
|
||||
rewardDescription: '每新增认种 1 棵可获得 15 绿积分',
|
||||
rewardDescription: '每新增认种 1 棵可获得 108 绿积分',
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -3537,7 +3537,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||
Expanded(
|
||||
child: Text(
|
||||
_communityBenefitActive
|
||||
? '每新增认种 1 棵可获得 80 绿积分'
|
||||
? '每新增认种 1 棵可获得 576 绿积分'
|
||||
: '需团队认种达到 $_communityInitialTarget 棵激活',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
|
|
|
|||
|
|
@ -130,6 +130,11 @@ class _LedgerDetailPageState extends ConsumerState<LedgerDetailPage>
|
|||
|
||||
/// 筛选流水类型
|
||||
Future<void> _filterByEntryType(String? entryType, int index) async {
|
||||
// 保存当前滚动位置
|
||||
final scrollOffset = _filterScrollController.hasClients
|
||||
? _filterScrollController.offset
|
||||
: 0.0;
|
||||
|
||||
setState(() {
|
||||
_selectedEntryType = entryType?.isEmpty == true ? null : entryType;
|
||||
_selectedFilterIndex = index;
|
||||
|
|
@ -137,6 +142,13 @@ class _LedgerDetailPageState extends ConsumerState<LedgerDetailPage>
|
|||
_isLoading = true;
|
||||
});
|
||||
|
||||
// 恢复滚动位置
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_filterScrollController.hasClients) {
|
||||
_filterScrollController.jumpTo(scrollOffset);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
final walletService = ref.read(walletServiceProvider);
|
||||
final newLedger = await walletService.getLedger(
|
||||
|
|
@ -150,6 +162,13 @@ class _LedgerDetailPageState extends ConsumerState<LedgerDetailPage>
|
|||
_ledger = newLedger;
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
// 数据加载完成后再次确保滚动位置正确
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_filterScrollController.hasClients) {
|
||||
_filterScrollController.jumpTo(scrollOffset);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[LedgerDetailPage] 筛选失败: $e');
|
||||
|
|
|
|||
Loading…
Reference in New Issue