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:
hailin 2025-12-22 21:10:49 -08:00
parent 723d70e4b8
commit 3f5203c142
8 changed files with 143 additions and 43 deletions

View File

@ -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;

View File

@ -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")

View File

@ -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 消费
}
}

View File

@ -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

View File

@ -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();

View File

@ -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,
});

View File

@ -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,

View File

@ -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');