diff --git a/backend/services/wallet-service/prisma/migrations/20241223000000_add_has_planted/migration.sql b/backend/services/wallet-service/prisma/migrations/20241223000000_add_has_planted/migration.sql new file mode 100644 index 00000000..9ce51134 --- /dev/null +++ b/backend/services/wallet-service/prisma/migrations/20241223000000_add_has_planted/migration.sql @@ -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; diff --git a/backend/services/wallet-service/prisma/schema.prisma b/backend/services/wallet-service/prisma/schema.prisma index 55f06456..739b2343 100644 --- a/backend/services/wallet-service/prisma/schema.prisma +++ b/backend/services/wallet-service/prisma/schema.prisma @@ -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") diff --git a/backend/services/wallet-service/src/application/event-handlers/planting-created.handler.ts b/backend/services/wallet-service/src/application/event-handlers/planting-created.handler.ts index 28c8aaef..a8b3f2cc 100644 --- a/backend/services/wallet-service/src/application/event-handlers/planting-created.handler.ts +++ b/backend/services/wallet-service/src/application/event-handlers/planting-created.handler.ts @@ -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 消费 } } 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 c9a4ef0b..767931e2 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 @@ -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 { + 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 diff --git a/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts b/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts index 9bbad821..912a5a86 100644 --- a/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts +++ b/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts @@ -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(); diff --git a/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts b/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts index 56245fa0..96a47c4b 100644 --- a/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts +++ b/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts @@ -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, }); diff --git a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart index 094876ec..5ddd1c97 100644 --- a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart +++ b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart @@ -1838,7 +1838,7 @@ class _ProfilePageState extends ConsumerState { initialTarget: _authCityCompanyInitialTarget, monthlyTarget: _authCityCompanyMonthlyTarget, monthIndex: _authCityCompanyMonthIndex, - rewardDescription: '每新增认种 1 棵可获得 40 绿积分', + rewardDescription: '每新增认种 1 棵可获得 288 绿积分', ), ], // 省团队权益考核 @@ -1852,7 +1852,7 @@ class _ProfilePageState extends ConsumerState { initialTarget: _authProvinceCompanyInitialTarget, monthlyTarget: _authProvinceCompanyMonthlyTarget, monthIndex: _authProvinceCompanyMonthIndex, - rewardDescription: '每新增认种 1 棵可获得 20 绿积分', + rewardDescription: '每新增认种 1 棵可获得 144 绿积分', ), ], // 市区域权益考核 @@ -1866,7 +1866,7 @@ class _ProfilePageState extends ConsumerState { initialTarget: _cityCompanyInitialTarget, monthlyTarget: _cityCompanyMonthlyTarget, monthIndex: _cityCompanyMonthIndex, - rewardDescription: '每新增认种 1 棵可获得 35 绿积分', + rewardDescription: '每新增认种 1 棵可获得 252 绿积分', ), ], // 省区域权益考核 @@ -1880,7 +1880,7 @@ class _ProfilePageState extends ConsumerState { initialTarget: _provinceCompanyInitialTarget, monthlyTarget: _provinceCompanyMonthlyTarget, monthIndex: _provinceCompanyMonthIndex, - rewardDescription: '每新增认种 1 棵可获得 15 绿积分', + rewardDescription: '每新增认种 1 棵可获得 108 绿积分', ), ], const SizedBox(height: 16), @@ -3537,7 +3537,7 @@ class _ProfilePageState extends ConsumerState { Expanded( child: Text( _communityBenefitActive - ? '每新增认种 1 棵可获得 80 绿积分' + ? '每新增认种 1 棵可获得 576 绿积分' : '需团队认种达到 $_communityInitialTarget 棵激活', style: TextStyle( fontSize: 14, diff --git a/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart b/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart index d39481e5..68163738 100644 --- a/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart +++ b/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart @@ -130,6 +130,11 @@ class _LedgerDetailPageState extends ConsumerState /// 筛选流水类型 Future _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 _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 _ledger = newLedger; _isLoading = false; }); + + // 数据加载完成后再次确保滚动位置正确 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_filterScrollController.hasClients) { + _filterScrollController.jumpTo(scrollOffset); + } + }); } } catch (e) { debugPrint('[LedgerDetailPage] 筛选失败: $e');