From 3be7b4767821a0d1fb6127b648ad9a9d5b006843 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 28 Feb 2026 22:03:53 -0800 Subject: [PATCH] =?UTF-8?q?feat(pre-planting+mobile):=20=E9=A2=84=E7=A7=8D?= =?UTF-8?q?=E5=A5=96=E5=8A=B1=E5=9C=A8"=E6=88=91"=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题 预种奖励直接走 planting-service → wallet-service,绕过 reward-service, 导致前端"我"页面读 reward-service 的 summary/settleable 数据时看不到预种奖励。 ## 方案 前端同时读 reward-service(正常认种)和 planting-service(预种),合并展示。 ## 后端(planting-service) - PrePlantingRewardEntryRepository: 新增 findByRecipientAccountSequence() 方法, 按收款方账户查询预种奖励记录(注入 PrismaService 替代事务 client) - PrePlantingController: 新增 GET /pre-planting/my-rewards 端点, 返回当前用户作为收款方收到的预种奖励汇总+明细列表 格式与 reward-service 的 settleable 对齐(id, rightType, usdtAmount, sourceOrderNo 等) ## 前端(Flutter mobile-app) - PrePlantingService: 新增 getMyRewards() 方法 + PrePlantingMyRewards/PrePlantingRewardItem 数据类 - profile_page.dart: 并行调用 prePlantingService.getMyRewards(), 将预种奖励转为 SettleableRewardItem 合并到可结算列表, summary.settleableUsdt 也加上预种金额 Co-Authored-By: Claude Opus 4.6 --- .../controllers/pre-planting.controller.ts | 36 ++++++++ .../pre-planting-reward-entry.repository.ts | 25 ++++++ .../core/services/pre_planting_service.dart | 85 +++++++++++++++++++ .../presentation/pages/profile_page.dart | 32 +++++-- 4 files changed, 173 insertions(+), 5 deletions(-) diff --git a/backend/services/planting-service/src/pre-planting/api/controllers/pre-planting.controller.ts b/backend/services/planting-service/src/pre-planting/api/controllers/pre-planting.controller.ts index daddcba9..59677a59 100644 --- a/backend/services/planting-service/src/pre-planting/api/controllers/pre-planting.controller.ts +++ b/backend/services/planting-service/src/pre-planting/api/controllers/pre-planting.controller.ts @@ -19,6 +19,7 @@ import { } from '@nestjs/swagger'; import { Response } from 'express'; import { PrePlantingApplicationService } from '../../application/services/pre-planting-application.service'; +import { PrePlantingRewardEntryRepository } from '../../infrastructure/repositories/pre-planting-reward-entry.repository'; import { PurchasePrePlantingDto } from '../dto/request/purchase-pre-planting.dto'; import { SignPrePlantingContractDto } from '../dto/request/sign-pre-planting-contract.dto'; import { JwtAuthGuard } from '../../../api/guards/jwt-auth.guard'; @@ -34,6 +35,7 @@ interface AuthenticatedRequest { export class PrePlantingController { constructor( private readonly prePlantingService: PrePlantingApplicationService, + private readonly rewardEntryRepo: PrePlantingRewardEntryRepository, ) {} @Post('purchase') @@ -103,6 +105,40 @@ export class PrePlantingController { return this.prePlantingService.getMerges(userId); } + /** + * 获取"我收到的"预种奖励(作为收款方) + * 返回格式与 reward-service 的 summary + settleable 对齐,方便前端合并 + */ + @Get('my-rewards') + @ApiOperation({ summary: '获取我收到的预种奖励' }) + @ApiResponse({ status: HttpStatus.OK, description: '预种奖励汇总+明细' }) + async getMyRewards(@Req() req: AuthenticatedRequest) { + const accountSequence = req.user.accountSequence; + const entries = await this.rewardEntryRepo.findByRecipientAccountSequence(accountSequence); + + // 只返回用户账户收到的奖励(D 开头),排除系统账户 + const userEntries = entries.filter(e => e.recipientAccountSequence.startsWith('D')); + + const settleableUsdt = userEntries.reduce((sum, e) => sum + e.usdtAmount, 0); + + return { + summary: { + settleableUsdt, + }, + settleableRewards: userEntries.map(e => ({ + id: `pp-${e.sourceOrderNo}-${e.rightType}`, + rightType: e.rightType, + usdtAmount: e.usdtAmount, + hashpowerAmount: 0, + createdAt: e.createdAt.toISOString(), + claimedAt: null, + sourceOrderNo: e.sourceOrderNo, + sourceAccountSequence: e.sourceAccountSequence, + memo: e.memo || '', + })), + }; + } + @Get('merges/:mergeNo/contract-pdf') @ApiOperation({ summary: '下载合并合同预览 PDF' }) @ApiResponse({ status: HttpStatus.OK, description: 'PDF 文件' }) diff --git a/backend/services/planting-service/src/pre-planting/infrastructure/repositories/pre-planting-reward-entry.repository.ts b/backend/services/planting-service/src/pre-planting/infrastructure/repositories/pre-planting-reward-entry.repository.ts index 499722bd..c3552e49 100644 --- a/backend/services/planting-service/src/pre-planting/infrastructure/repositories/pre-planting-reward-entry.repository.ts +++ b/backend/services/planting-service/src/pre-planting/infrastructure/repositories/pre-planting-reward-entry.repository.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrePlantingRewardStatus } from '../../domain/value-objects/pre-planting-reward-status.enum'; +import { PrismaService } from '../../../infrastructure/persistence/prisma/prisma.service'; export interface PrePlantingRewardEntryData { sourceOrderNo: string; @@ -16,6 +17,8 @@ export interface PrePlantingRewardEntryData { export class PrePlantingRewardEntryRepository { private readonly logger = new Logger(PrePlantingRewardEntryRepository.name); + constructor(private readonly prisma: PrismaService) {} + async saveMany( tx: Prisma.TransactionClient, entries: PrePlantingRewardEntryData[], @@ -75,4 +78,26 @@ export class PrePlantingRewardEntryRepository { memo: r.memo || undefined, })); } + + /** + * 按收款方账户查询(用于用户查看"我收到的预种奖励") + */ + async findByRecipientAccountSequence( + accountSequence: string, + ): Promise<(PrePlantingRewardEntryData & { createdAt: Date })[]> { + const records = await this.prisma.prePlantingRewardEntry.findMany({ + where: { recipientAccountSequence: accountSequence }, + orderBy: { createdAt: 'desc' }, + }); + return records.map((r) => ({ + sourceOrderNo: r.sourceOrderNo, + sourceAccountSequence: r.sourceAccountSequence, + recipientAccountSequence: r.recipientAccountSequence, + rightType: r.rightType, + usdtAmount: Number(r.usdtAmount), + rewardStatus: r.rewardStatus as PrePlantingRewardStatus, + memo: r.memo || undefined, + createdAt: r.createdAt, + })); + } } diff --git a/frontend/mobile-app/lib/core/services/pre_planting_service.dart b/frontend/mobile-app/lib/core/services/pre_planting_service.dart index a3f35b34..29809314 100644 --- a/frontend/mobile-app/lib/core/services/pre_planting_service.dart +++ b/frontend/mobile-app/lib/core/services/pre_planting_service.dart @@ -545,4 +545,89 @@ class PrePlantingService { rethrow; } } + + /// 获取"我收到的"预种奖励 + /// + /// 返回 { summary: { settleableUsdt }, settleableRewards: [...] } + Future getMyRewards() async { + try { + debugPrint('[PrePlantingService] 获取我的预种奖励'); + final response = await _apiClient.get('/pre-planting/my-rewards'); + + if (response.statusCode == 200) { + final data = response.data as Map; + return PrePlantingMyRewards.fromJson(data); + } + + debugPrint('[PrePlantingService] 获取预种奖励失败: ${response.statusCode}'); + return PrePlantingMyRewards.empty(); + } catch (e) { + debugPrint('[PrePlantingService] 获取预种奖励异常: $e'); + return PrePlantingMyRewards.empty(); + } + } +} + +/// 预种奖励返回数据 +class PrePlantingMyRewards { + final double settleableUsdt; + final List settleableRewards; + + PrePlantingMyRewards({ + required this.settleableUsdt, + required this.settleableRewards, + }); + + factory PrePlantingMyRewards.empty() => PrePlantingMyRewards( + settleableUsdt: 0, + settleableRewards: [], + ); + + factory PrePlantingMyRewards.fromJson(Map json) { + final summary = json['summary'] as Map? ?? {}; + final rewards = json['settleableRewards'] as List? ?? []; + + return PrePlantingMyRewards( + settleableUsdt: (summary['settleableUsdt'] ?? 0).toDouble(), + settleableRewards: rewards + .map((r) => PrePlantingRewardItem.fromJson(r as Map)) + .toList(), + ); + } +} + +/// 预种奖励条目 +class PrePlantingRewardItem { + final String id; + final String rightType; + final double usdtAmount; + final double hashpowerAmount; + final DateTime createdAt; + final String sourceOrderNo; + final String? sourceAccountSequence; + final String memo; + + PrePlantingRewardItem({ + required this.id, + required this.rightType, + required this.usdtAmount, + required this.hashpowerAmount, + required this.createdAt, + required this.sourceOrderNo, + this.sourceAccountSequence, + required this.memo, + }); + + factory PrePlantingRewardItem.fromJson(Map json) { + return PrePlantingRewardItem( + id: json['id']?.toString() ?? '', + rightType: json['rightType'] ?? '', + usdtAmount: (json['usdtAmount'] ?? 0).toDouble(), + hashpowerAmount: (json['hashpowerAmount'] ?? 0).toDouble(), + createdAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(), + sourceOrderNo: json['sourceOrderNo'] ?? '', + sourceAccountSequence: json['sourceAccountSequence'], + memo: json['memo'] ?? '', + ); + } } 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 4580b191..d7700e48 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 @@ -14,6 +14,7 @@ import '../../../../core/di/injection_container.dart'; import '../../../../core/storage/storage_keys.dart'; import '../../../../core/services/referral_service.dart'; import '../../../../core/services/reward_service.dart'; +import '../../../../core/services/pre_planting_service.dart'; import '../../../../core/services/wallet_service.dart'; import '../../../../core/services/notification_service.dart'; import '../../../../core/providers/notification_badge_provider.dart'; @@ -771,25 +772,46 @@ class _ProfilePageState extends ConsumerState { }); } - debugPrint('[ProfilePage] 获取 rewardServiceProvider 和 walletServiceProvider...'); + debugPrint('[ProfilePage] 获取 rewardServiceProvider / prePlantingServiceProvider / walletServiceProvider...'); final rewardService = ref.read(rewardServiceProvider); + final prePlantingService = ref.read(prePlantingServiceProvider); final walletService = ref.read(walletServiceProvider); - // 并行加载汇总数据、待领取列表、可结算列表、已过期列表、流水统计 - debugPrint('[ProfilePage] 调用 getMyRewardSummary()、getPendingRewards()、getSettleableRewards()、getExpiredRewards()、getLedgerStatistics()...'); + // 并行加载:正常认种奖励 + 预种奖励 + 流水统计 + debugPrint('[ProfilePage] 并行调用 reward-service + planting-service(预种) + wallet-service...'); final results = await Future.wait([ rewardService.getMyRewardSummary(), rewardService.getPendingRewards(), rewardService.getSettleableRewards(), rewardService.getExpiredRewards(), walletService.getLedgerStatistics(), + prePlantingService.getMyRewards(), // 预种奖励 ]); final summary = results[0] as RewardSummary; final pendingRewards = results[1] as List; - final settleableRewards = results[2] as List; + var settleableRewards = results[2] as List; final expiredRewards = results[3] as List; final ledgerStats = results[4] as LedgerStatistics; + final prePlantingRewards = results[5] as PrePlantingMyRewards; + + // 合并预种可结算奖励到列表中 + // 预种奖励转为 SettleableRewardItem 格式,与正常认种统一展示 + final prePlantingSettleable = prePlantingRewards.settleableRewards.map((r) => + SettleableRewardItem( + id: r.id, + rightType: r.rightType, + usdtAmount: r.usdtAmount, + hashpowerAmount: r.hashpowerAmount, + createdAt: r.createdAt, + claimedAt: null, + sourceOrderNo: r.sourceOrderNo, + sourceAccountSequence: r.sourceAccountSequence, + memo: '[预种] ${r.memo}', + ), + ).toList(); + settleableRewards = [...settleableRewards, ...prePlantingSettleable]; + debugPrint('[ProfilePage] 预种可结算奖励: ${prePlantingSettleable.length} 条, 金额: ${prePlantingRewards.settleableUsdt}'); // 从流水统计中获取 REWARD_SETTLED 类型的总金额作为"已结算"数据 // 这比 summary.settledTotalUsdt 更准确,因为直接来自交易流水 @@ -820,7 +842,7 @@ class _ProfilePageState extends ConsumerState { setState(() { _pendingUsdt = summary.pendingUsdt; _pendingPower = summary.pendingHashpower; - _settleableUsdt = summary.settleableUsdt; + _settleableUsdt = summary.settleableUsdt + prePlantingRewards.settleableUsdt; // 使用流水统计的数据,更准确 _settledUsdt = settledFromLedger; _expiredUsdt = summary.expiredTotalUsdt;