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:
parent
f32748c1d5
commit
3be7b47678
|
|
@ -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 文件' })
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue