feat(pre-planting+mobile): 预种奖励在"我"页面展示

## 问题
预种奖励直接走 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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-28 22:03:53 -08:00
parent f32748c1d5
commit 3be7b47678
4 changed files with 173 additions and 5 deletions

View File

@ -19,6 +19,7 @@ import {
} from '@nestjs/swagger'; } from '@nestjs/swagger';
import { Response } from 'express'; import { Response } from 'express';
import { PrePlantingApplicationService } from '../../application/services/pre-planting-application.service'; 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 { PurchasePrePlantingDto } from '../dto/request/purchase-pre-planting.dto';
import { SignPrePlantingContractDto } from '../dto/request/sign-pre-planting-contract.dto'; import { SignPrePlantingContractDto } from '../dto/request/sign-pre-planting-contract.dto';
import { JwtAuthGuard } from '../../../api/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../../api/guards/jwt-auth.guard';
@ -34,6 +35,7 @@ interface AuthenticatedRequest {
export class PrePlantingController { export class PrePlantingController {
constructor( constructor(
private readonly prePlantingService: PrePlantingApplicationService, private readonly prePlantingService: PrePlantingApplicationService,
private readonly rewardEntryRepo: PrePlantingRewardEntryRepository,
) {} ) {}
@Post('purchase') @Post('purchase')
@ -103,6 +105,40 @@ export class PrePlantingController {
return this.prePlantingService.getMerges(userId); 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') @Get('merges/:mergeNo/contract-pdf')
@ApiOperation({ summary: '下载合并合同预览 PDF' }) @ApiOperation({ summary: '下载合并合同预览 PDF' })
@ApiResponse({ status: HttpStatus.OK, description: 'PDF 文件' }) @ApiResponse({ status: HttpStatus.OK, description: 'PDF 文件' })

View File

@ -1,6 +1,7 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { PrePlantingRewardStatus } from '../../domain/value-objects/pre-planting-reward-status.enum'; import { PrePlantingRewardStatus } from '../../domain/value-objects/pre-planting-reward-status.enum';
import { PrismaService } from '../../../infrastructure/persistence/prisma/prisma.service';
export interface PrePlantingRewardEntryData { export interface PrePlantingRewardEntryData {
sourceOrderNo: string; sourceOrderNo: string;
@ -16,6 +17,8 @@ export interface PrePlantingRewardEntryData {
export class PrePlantingRewardEntryRepository { export class PrePlantingRewardEntryRepository {
private readonly logger = new Logger(PrePlantingRewardEntryRepository.name); private readonly logger = new Logger(PrePlantingRewardEntryRepository.name);
constructor(private readonly prisma: PrismaService) {}
async saveMany( async saveMany(
tx: Prisma.TransactionClient, tx: Prisma.TransactionClient,
entries: PrePlantingRewardEntryData[], entries: PrePlantingRewardEntryData[],
@ -75,4 +78,26 @@ export class PrePlantingRewardEntryRepository {
memo: r.memo || undefined, 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,
}));
}
} }

View File

@ -545,4 +545,89 @@ class PrePlantingService {
rethrow; rethrow;
} }
} }
/// "我收到的"
///
/// { summary: { settleableUsdt }, settleableRewards: [...] }
Future<PrePlantingMyRewards> getMyRewards() async {
try {
debugPrint('[PrePlantingService] 获取我的预种奖励');
final response = await _apiClient.get('/pre-planting/my-rewards');
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
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<PrePlantingRewardItem> settleableRewards;
PrePlantingMyRewards({
required this.settleableUsdt,
required this.settleableRewards,
});
factory PrePlantingMyRewards.empty() => PrePlantingMyRewards(
settleableUsdt: 0,
settleableRewards: [],
);
factory PrePlantingMyRewards.fromJson(Map<String, dynamic> json) {
final summary = json['summary'] as Map<String, dynamic>? ?? {};
final rewards = json['settleableRewards'] as List<dynamic>? ?? [];
return PrePlantingMyRewards(
settleableUsdt: (summary['settleableUsdt'] ?? 0).toDouble(),
settleableRewards: rewards
.map((r) => PrePlantingRewardItem.fromJson(r as Map<String, dynamic>))
.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<String, dynamic> 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'] ?? '',
);
}
} }

View File

@ -14,6 +14,7 @@ import '../../../../core/di/injection_container.dart';
import '../../../../core/storage/storage_keys.dart'; import '../../../../core/storage/storage_keys.dart';
import '../../../../core/services/referral_service.dart'; import '../../../../core/services/referral_service.dart';
import '../../../../core/services/reward_service.dart'; import '../../../../core/services/reward_service.dart';
import '../../../../core/services/pre_planting_service.dart';
import '../../../../core/services/wallet_service.dart'; import '../../../../core/services/wallet_service.dart';
import '../../../../core/services/notification_service.dart'; import '../../../../core/services/notification_service.dart';
import '../../../../core/providers/notification_badge_provider.dart'; import '../../../../core/providers/notification_badge_provider.dart';
@ -771,25 +772,46 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
}); });
} }
debugPrint('[ProfilePage] 获取 rewardServiceProvider walletServiceProvider...'); debugPrint('[ProfilePage] 获取 rewardServiceProvider / prePlantingServiceProvider / walletServiceProvider...');
final rewardService = ref.read(rewardServiceProvider); final rewardService = ref.read(rewardServiceProvider);
final prePlantingService = ref.read(prePlantingServiceProvider);
final walletService = ref.read(walletServiceProvider); 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([ final results = await Future.wait([
rewardService.getMyRewardSummary(), rewardService.getMyRewardSummary(),
rewardService.getPendingRewards(), rewardService.getPendingRewards(),
rewardService.getSettleableRewards(), rewardService.getSettleableRewards(),
rewardService.getExpiredRewards(), rewardService.getExpiredRewards(),
walletService.getLedgerStatistics(), walletService.getLedgerStatistics(),
prePlantingService.getMyRewards(), //
]); ]);
final summary = results[0] as RewardSummary; final summary = results[0] as RewardSummary;
final pendingRewards = results[1] as List<PendingRewardItem>; final pendingRewards = results[1] as List<PendingRewardItem>;
final settleableRewards = results[2] as List<SettleableRewardItem>; var settleableRewards = results[2] as List<SettleableRewardItem>;
final expiredRewards = results[3] as List<ExpiredRewardItem>; final expiredRewards = results[3] as List<ExpiredRewardItem>;
final ledgerStats = results[4] as LedgerStatistics; 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 "已结算" // REWARD_SETTLED "已结算"
// summary.settledTotalUsdt // summary.settledTotalUsdt
@ -820,7 +842,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
setState(() { setState(() {
_pendingUsdt = summary.pendingUsdt; _pendingUsdt = summary.pendingUsdt;
_pendingPower = summary.pendingHashpower; _pendingPower = summary.pendingHashpower;
_settleableUsdt = summary.settleableUsdt; _settleableUsdt = summary.settleableUsdt + prePlantingRewards.settleableUsdt;
// 使 // 使
_settledUsdt = settledFromLedger; _settledUsdt = settledFromLedger;
_expiredUsdt = summary.expiredTotalUsdt; _expiredUsdt = summary.expiredTotalUsdt;