feat(ledger): add detailed ledger entry views with source tracking
实现账本流水详情功能,支持点击查看各类型流水的详细信息。 ## reward-service 后端 ### 数据库 - 新增 `source_account_sequence` 字段到 `reward_ledger_entries` 表 - 添加索引 `idx_source_account_seq` 提升查询性能 - 字段可空,兼容历史数据 ### 领域层 - `RewardSource` 值对象新增 `sourceAccountSequence` 属性 - `RewardCalculationService` 传递 `sourceAccountSequence` ### 应用层 - 新增 `getSettlementHistory` 方法查询结算历史 - 新增 `SettlementRecordRepository` 仓储实现 ### API层 - 新增 `GET /settlements/history` 接口 - 新增 `SettlementHistoryQueryDTO` 和 `SettlementHistoryDTO` ## mobile-app 前端 ### 服务层 - `RewardService` 新增结算历史相关模型和方法: - `SettlementHistoryItem` 结算记录模型 - `SettlementRewardEntry` 关联奖励条目模型 - `getSettlementHistory()` 获取结算历史 - `WalletService` 新增: - `LedgerEntry.payloadJson` 字段及辅助方法 - `counterpartyAccountSequence` 获取转账对手方ID - `counterpartyUserId` 获取转账对手方用户ID - `transferFee` 获取转账手续费 ### 账本详情页 - 结算流水详情:显示结算金额、币种、涉及奖励明细(含来源用户) - 提现流水详情:显示提现订单信息、状态、手续费等 - 转账流水详情:显示转入来源/转出目标用户信息 ### 交互优化 - REWARD_SETTLED、WITHDRAWAL、TRANSFER_IN、TRANSFER_OUT 类型可点击 - 使用底部弹窗展示详情,支持滚动查看长列表 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
35a812c058
commit
17b9c09381
|
|
@ -0,0 +1,10 @@
|
|||
-- Add source_account_sequence column to reward_ledger_entries
|
||||
-- This stores the account sequence of the user who triggered the reward (the planter)
|
||||
|
||||
ALTER TABLE "reward_ledger_entries" ADD COLUMN "source_account_sequence" VARCHAR(20);
|
||||
|
||||
-- Create index for source_account_sequence
|
||||
CREATE INDEX "idx_source_account_seq" ON "reward_ledger_entries"("source_account_sequence");
|
||||
|
||||
-- Note: This column is nullable for backward compatibility with existing records
|
||||
-- New records should always have this field populated
|
||||
|
|
@ -17,9 +17,10 @@ model RewardLedgerEntry {
|
|||
accountSequence String @map("account_sequence") @db.VarChar(20) // 账户序列号
|
||||
|
||||
// === 奖励来源 ===
|
||||
sourceOrderNo String @map("source_order_no") @db.VarChar(50) // 来源认种订单号(字符串格式如PLT1765391584505Q0Q6QD)
|
||||
sourceUserId BigInt @map("source_user_id") // 触发奖励的用户ID(认种者)
|
||||
rightType String @map("right_type") @db.VarChar(50) // 权益类型
|
||||
sourceOrderNo String @map("source_order_no") @db.VarChar(50) // 来源认种订单号(字符串格式如PLT1765391584505Q0Q6QD)
|
||||
sourceUserId BigInt @map("source_user_id") // 触发奖励的用户ID(认种者)
|
||||
sourceAccountSequence String? @map("source_account_sequence") @db.VarChar(20) // 触发奖励的用户账户序列号
|
||||
rightType String @map("right_type") @db.VarChar(50) // 权益类型
|
||||
|
||||
// === 奖励金额 ===
|
||||
usdtAmount Decimal @map("usdt_amount") @db.Decimal(20, 8)
|
||||
|
|
@ -45,6 +46,7 @@ model RewardLedgerEntry {
|
|||
@@index([accountSequence, createdAt(sort: Desc)], name: "idx_account_created")
|
||||
@@index([sourceOrderNo], name: "idx_source_order")
|
||||
@@index([sourceUserId], name: "idx_source_user")
|
||||
@@index([sourceAccountSequence], name: "idx_source_account_seq")
|
||||
@@index([rightType], name: "idx_right_type")
|
||||
@@index([rewardStatus], name: "idx_status")
|
||||
@@index([expireAt], name: "idx_expire")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { Controller, Post, Body, UseGuards, Request } from '@nestjs/common';
|
||||
import { Controller, Post, Get, Body, Query, UseGuards, Request } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../shared/guards/jwt-auth.guard';
|
||||
import { RewardApplicationService } from '../../application/services/reward-application.service';
|
||||
import { SettleRewardsDto } from '../dto/request/settle-rewards.dto';
|
||||
import { SettlementHistoryQueryDto } from '../dto/request/settlement-history-query.dto';
|
||||
import { SettlementResultDto } from '../dto/response/settlement-result.dto';
|
||||
import { SettlementHistoryResponseDto } from '../dto/response/settlement-history.dto';
|
||||
|
||||
@ApiTags('Settlement')
|
||||
@Controller('rewards')
|
||||
|
|
@ -27,4 +29,19 @@ export class SettlementController {
|
|||
settleCurrency: dto.settleCurrency,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('settlement-history')
|
||||
@ApiOperation({ summary: '获取结算历史记录' })
|
||||
@ApiResponse({ status: 200, description: '成功', type: SettlementHistoryResponseDto })
|
||||
async getSettlementHistory(
|
||||
@Request() req,
|
||||
@Query() query: SettlementHistoryQueryDto,
|
||||
): Promise<SettlementHistoryResponseDto> {
|
||||
const accountSequence = req.user.accountSequence;
|
||||
|
||||
return this.rewardService.getSettlementHistory(accountSequence, {
|
||||
page: query.page || 1,
|
||||
pageSize: query.pageSize || 20,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsOptional, IsInt, Min, Max } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class SettlementHistoryQueryDto {
|
||||
@ApiProperty({ description: '页码', example: 1, required: false, default: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@ApiProperty({ description: '每页数量', example: 20, required: false, default: 20 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
pageSize?: number = 20;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class SettlementHistoryItemDto {
|
||||
@ApiProperty({ description: '结算记录ID', example: '123' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '结算USDT金额', example: 1000 })
|
||||
usdtAmount: number;
|
||||
|
||||
@ApiProperty({ description: '结算算力', example: 0 })
|
||||
hashpowerAmount: number;
|
||||
|
||||
@ApiProperty({ description: '结算币种', example: 'USDT' })
|
||||
settleCurrency: string;
|
||||
|
||||
@ApiProperty({ description: '实际收到金额', example: 1000 })
|
||||
receivedAmount: number;
|
||||
|
||||
@ApiProperty({ description: '兑换哈希', example: '0x...', nullable: true })
|
||||
swapTxHash: string | null;
|
||||
|
||||
@ApiProperty({ description: '兑换汇率', example: 1.0, nullable: true })
|
||||
swapRate: number | null;
|
||||
|
||||
@ApiProperty({ description: '状态', example: 'SUCCESS' })
|
||||
status: string;
|
||||
|
||||
@ApiProperty({ description: '创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({ description: '完成时间', nullable: true })
|
||||
completedAt: Date | null;
|
||||
|
||||
@ApiProperty({ description: '涉及的奖励条目数量', example: 5 })
|
||||
rewardEntryCount: number;
|
||||
}
|
||||
|
||||
export class SettlementHistoryPaginationDto {
|
||||
@ApiProperty({ description: '当前页', example: 1 })
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ description: '每页数量', example: 20 })
|
||||
pageSize: number;
|
||||
|
||||
@ApiProperty({ description: '总数', example: 100 })
|
||||
total: number;
|
||||
}
|
||||
|
||||
export class SettlementHistoryResponseDto {
|
||||
@ApiProperty({ description: '结算记录列表', type: [SettlementHistoryItemDto] })
|
||||
data: SettlementHistoryItemDto[];
|
||||
|
||||
@ApiProperty({ description: '分页信息', type: SettlementHistoryPaginationDto })
|
||||
pagination: SettlementHistoryPaginationDto;
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import { Hashpower } from '../../domain/value-objects/hashpower.vo';
|
|||
import { EventPublisherService } from '../../infrastructure/kafka/event-publisher.service';
|
||||
import { WalletServiceClient } from '../../infrastructure/external/wallet-service/wallet-service.client';
|
||||
import { OutboxRepository, OutboxEventData } from '../../infrastructure/persistence/repositories/outbox.repository';
|
||||
import { SettlementRecordRepository, SettlementRecordDto } from '../../infrastructure/persistence/repositories/settlement-record.repository';
|
||||
import { RewardSummary } from '../../domain/aggregates/reward-summary/reward-summary.aggregate';
|
||||
|
||||
// 总部社区账户ID
|
||||
|
|
@ -32,6 +33,7 @@ export class RewardApplicationService {
|
|||
private readonly eventPublisher: EventPublisherService,
|
||||
private readonly walletService: WalletServiceClient,
|
||||
private readonly outboxRepository: OutboxRepository,
|
||||
private readonly settlementRecordRepository: SettlementRecordRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
@ -669,6 +671,8 @@ export class RewardApplicationService {
|
|||
createdAt: r.createdAt,
|
||||
expireAt: r.expireAt,
|
||||
remainingTimeMs: r.getRemainingTimeMs(),
|
||||
sourceOrderNo: r.rewardSource.sourceOrderNo,
|
||||
sourceAccountSequence: r.rewardSource.sourceAccountSequence,
|
||||
memo: r.memo,
|
||||
}));
|
||||
}
|
||||
|
|
@ -687,6 +691,7 @@ export class RewardApplicationService {
|
|||
createdAt: r.createdAt,
|
||||
claimedAt: r.claimedAt,
|
||||
sourceOrderNo: r.rewardSource.sourceOrderNo,
|
||||
sourceAccountSequence: r.rewardSource.sourceAccountSequence,
|
||||
memo: r.memo,
|
||||
}));
|
||||
}
|
||||
|
|
@ -822,4 +827,61 @@ export class RewardApplicationService {
|
|||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取结算历史记录(新增接口)
|
||||
*/
|
||||
async getSettlementHistory(
|
||||
accountSequence: string,
|
||||
pagination?: { page: number; pageSize: number },
|
||||
): Promise<{
|
||||
data: Array<{
|
||||
id: string;
|
||||
usdtAmount: number;
|
||||
hashpowerAmount: number;
|
||||
settleCurrency: string;
|
||||
receivedAmount: number;
|
||||
swapTxHash: string | null;
|
||||
swapRate: number | null;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
completedAt: Date | null;
|
||||
rewardEntryCount: number;
|
||||
}>;
|
||||
pagination: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
}> {
|
||||
const page = pagination?.page || 1;
|
||||
const pageSize = pagination?.pageSize || 20;
|
||||
|
||||
const records = await this.settlementRecordRepository.findByAccountSequence(
|
||||
accountSequence,
|
||||
{ page, pageSize },
|
||||
);
|
||||
const total = await this.settlementRecordRepository.countByAccountSequence(accountSequence);
|
||||
|
||||
return {
|
||||
data: records.map(r => ({
|
||||
id: r.id,
|
||||
usdtAmount: r.usdtAmount,
|
||||
hashpowerAmount: r.hashpowerAmount,
|
||||
settleCurrency: r.settleCurrency,
|
||||
receivedAmount: r.receivedAmount,
|
||||
swapTxHash: r.swapTxHash,
|
||||
swapRate: r.swapRate,
|
||||
status: r.status,
|
||||
createdAt: r.createdAt,
|
||||
completedAt: r.completedAt,
|
||||
rewardEntryCount: r.rewardEntryIds.length,
|
||||
})),
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ export class RewardCalculationService {
|
|||
const costFeeReward = this.calculateCostFee(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.treeCount,
|
||||
);
|
||||
rewards.push(costFeeReward);
|
||||
|
|
@ -118,6 +119,7 @@ export class RewardCalculationService {
|
|||
const operationFeeReward = this.calculateOperationFee(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.treeCount,
|
||||
);
|
||||
rewards.push(operationFeeReward);
|
||||
|
|
@ -126,6 +128,7 @@ export class RewardCalculationService {
|
|||
const headquartersBaseFeeReward = this.calculateHeadquartersBaseFee(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.treeCount,
|
||||
);
|
||||
rewards.push(headquartersBaseFeeReward);
|
||||
|
|
@ -134,6 +137,7 @@ export class RewardCalculationService {
|
|||
const rwadPoolReward = this.calculateRwadPoolInjection(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.treeCount,
|
||||
);
|
||||
rewards.push(rwadPoolReward);
|
||||
|
|
@ -218,6 +222,7 @@ export class RewardCalculationService {
|
|||
private calculateCostFee(
|
||||
sourceOrderNo: string,
|
||||
sourceUserId: bigint,
|
||||
sourceAccountSequence: string | undefined,
|
||||
treeCount: number,
|
||||
): RewardLedgerEntry {
|
||||
const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.COST_FEE];
|
||||
|
|
@ -228,6 +233,7 @@ export class RewardCalculationService {
|
|||
RightType.COST_FEE,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
return RewardLedgerEntry.createSettleable({
|
||||
|
|
@ -236,7 +242,7 @@ export class RewardCalculationService {
|
|||
rewardSource,
|
||||
usdtAmount,
|
||||
hashpowerAmount: hashpower,
|
||||
memo: `成本费:来自用户${sourceUserId}的认种,${treeCount}棵树`,
|
||||
memo: `成本费:来自用户${sourceAccountSequence || sourceUserId}的认种,${treeCount}棵树`,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -247,6 +253,7 @@ export class RewardCalculationService {
|
|||
private calculateOperationFee(
|
||||
sourceOrderNo: string,
|
||||
sourceUserId: bigint,
|
||||
sourceAccountSequence: string | undefined,
|
||||
treeCount: number,
|
||||
): RewardLedgerEntry {
|
||||
const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.OPERATION_FEE];
|
||||
|
|
@ -257,6 +264,7 @@ export class RewardCalculationService {
|
|||
RightType.OPERATION_FEE,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
return RewardLedgerEntry.createSettleable({
|
||||
|
|
@ -265,7 +273,7 @@ export class RewardCalculationService {
|
|||
rewardSource,
|
||||
usdtAmount,
|
||||
hashpowerAmount: hashpower,
|
||||
memo: `运营费:来自用户${sourceUserId}的认种,${treeCount}棵树`,
|
||||
memo: `运营费:来自用户${sourceAccountSequence || sourceUserId}的认种,${treeCount}棵树`,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -276,6 +284,7 @@ export class RewardCalculationService {
|
|||
private calculateHeadquartersBaseFee(
|
||||
sourceOrderNo: string,
|
||||
sourceUserId: bigint,
|
||||
sourceAccountSequence: string | undefined,
|
||||
treeCount: number,
|
||||
): RewardLedgerEntry {
|
||||
const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.HEADQUARTERS_BASE_FEE];
|
||||
|
|
@ -286,6 +295,7 @@ export class RewardCalculationService {
|
|||
RightType.HEADQUARTERS_BASE_FEE,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
return RewardLedgerEntry.createSettleable({
|
||||
|
|
@ -294,7 +304,7 @@ export class RewardCalculationService {
|
|||
rewardSource,
|
||||
usdtAmount,
|
||||
hashpowerAmount: hashpower,
|
||||
memo: `总部社区基础费:来自用户${sourceUserId}的认种,${treeCount}棵树`,
|
||||
memo: `总部社区基础费:来自用户${sourceAccountSequence || sourceUserId}的认种,${treeCount}棵树`,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -305,6 +315,7 @@ export class RewardCalculationService {
|
|||
private calculateRwadPoolInjection(
|
||||
sourceOrderNo: string,
|
||||
sourceUserId: bigint,
|
||||
sourceAccountSequence: string | undefined,
|
||||
treeCount: number,
|
||||
): RewardLedgerEntry {
|
||||
const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.RWAD_POOL_INJECTION];
|
||||
|
|
@ -315,6 +326,7 @@ export class RewardCalculationService {
|
|||
RightType.RWAD_POOL_INJECTION,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
return RewardLedgerEntry.createSettleable({
|
||||
|
|
@ -323,7 +335,7 @@ export class RewardCalculationService {
|
|||
rewardSource,
|
||||
usdtAmount,
|
||||
hashpowerAmount: hashpower,
|
||||
memo: `RWAD底池注入:来自用户${sourceUserId}的认种,${treeCount}棵树,5760U注入1号底池`,
|
||||
memo: `RWAD底池注入:来自用户${sourceAccountSequence || sourceUserId}的认种,${treeCount}棵树,5760U注入1号底池`,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -350,6 +362,7 @@ export class RewardCalculationService {
|
|||
RightType.SHARE_RIGHT,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
// 使用 accountSequence 获取推荐链(优先),否则用 userId
|
||||
|
|
@ -440,6 +453,7 @@ export class RewardCalculationService {
|
|||
RightType.PROVINCE_TEAM_RIGHT,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
rewards.push(
|
||||
|
|
@ -497,6 +511,7 @@ export class RewardCalculationService {
|
|||
RightType.PROVINCE_AREA_RIGHT,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
rewards.push(
|
||||
|
|
@ -553,6 +568,7 @@ export class RewardCalculationService {
|
|||
RightType.CITY_TEAM_RIGHT,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
rewards.push(
|
||||
|
|
@ -610,6 +626,7 @@ export class RewardCalculationService {
|
|||
RightType.CITY_AREA_RIGHT,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
rewards.push(
|
||||
|
|
@ -666,6 +683,7 @@ export class RewardCalculationService {
|
|||
RightType.COMMUNITY_RIGHT,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
rewards.push(
|
||||
|
|
@ -725,6 +743,7 @@ export class RewardCalculationService {
|
|||
rewards.push(this.calculateCostFee(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.treeCount,
|
||||
));
|
||||
|
||||
|
|
@ -732,6 +751,7 @@ export class RewardCalculationService {
|
|||
rewards.push(this.calculateOperationFee(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.treeCount,
|
||||
));
|
||||
|
||||
|
|
@ -739,6 +759,7 @@ export class RewardCalculationService {
|
|||
rewards.push(this.calculateHeadquartersBaseFee(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.treeCount,
|
||||
));
|
||||
|
||||
|
|
@ -746,6 +767,7 @@ export class RewardCalculationService {
|
|||
rewards.push(this.calculateRwadPoolInjection(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.treeCount,
|
||||
));
|
||||
|
||||
|
|
@ -757,6 +779,7 @@ export class RewardCalculationService {
|
|||
rewards.push(this.calculateShareRightToSystemAccount(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.treeCount,
|
||||
));
|
||||
|
||||
|
|
@ -764,6 +787,7 @@ export class RewardCalculationService {
|
|||
rewards.push(this.calculateProvinceTeamRightToSystemAccount(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.provinceCode,
|
||||
params.treeCount,
|
||||
));
|
||||
|
|
@ -772,6 +796,7 @@ export class RewardCalculationService {
|
|||
rewards.push(this.calculateProvinceAreaRightToSystemAccount(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.provinceCode,
|
||||
params.treeCount,
|
||||
));
|
||||
|
|
@ -780,6 +805,7 @@ export class RewardCalculationService {
|
|||
rewards.push(this.calculateCityTeamRightToSystemAccount(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.cityCode,
|
||||
params.treeCount,
|
||||
));
|
||||
|
|
@ -788,6 +814,7 @@ export class RewardCalculationService {
|
|||
rewards.push(this.calculateCityAreaRightToSystemAccount(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.cityCode,
|
||||
params.treeCount,
|
||||
));
|
||||
|
|
@ -796,6 +823,7 @@ export class RewardCalculationService {
|
|||
rewards.push(this.calculateCommunityRightToSystemAccount(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.treeCount,
|
||||
));
|
||||
|
||||
|
|
@ -816,6 +844,7 @@ export class RewardCalculationService {
|
|||
private calculateShareRightToSystemAccount(
|
||||
sourceOrderNo: string,
|
||||
sourceUserId: bigint,
|
||||
sourceAccountSequence: string | undefined,
|
||||
treeCount: number,
|
||||
): RewardLedgerEntry {
|
||||
const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.SHARE_RIGHT];
|
||||
|
|
@ -826,6 +855,7 @@ export class RewardCalculationService {
|
|||
RightType.SHARE_RIGHT,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
return RewardLedgerEntry.createSettleable({
|
||||
|
|
@ -844,6 +874,7 @@ export class RewardCalculationService {
|
|||
private calculateProvinceTeamRightToSystemAccount(
|
||||
sourceOrderNo: string,
|
||||
sourceUserId: bigint,
|
||||
sourceAccountSequence: string | undefined,
|
||||
provinceCode: string,
|
||||
treeCount: number,
|
||||
): RewardLedgerEntry {
|
||||
|
|
@ -855,6 +886,7 @@ export class RewardCalculationService {
|
|||
RightType.PROVINCE_TEAM_RIGHT,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
// 省团队默认账户: 7 + 省代码
|
||||
|
|
@ -876,6 +908,7 @@ export class RewardCalculationService {
|
|||
private calculateProvinceAreaRightToSystemAccount(
|
||||
sourceOrderNo: string,
|
||||
sourceUserId: bigint,
|
||||
sourceAccountSequence: string | undefined,
|
||||
provinceCode: string,
|
||||
treeCount: number,
|
||||
): RewardLedgerEntry {
|
||||
|
|
@ -887,6 +920,7 @@ export class RewardCalculationService {
|
|||
RightType.PROVINCE_AREA_RIGHT,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
// 省区域账户: 9 + 省代码
|
||||
|
|
@ -908,6 +942,7 @@ export class RewardCalculationService {
|
|||
private calculateCityTeamRightToSystemAccount(
|
||||
sourceOrderNo: string,
|
||||
sourceUserId: bigint,
|
||||
sourceAccountSequence: string | undefined,
|
||||
cityCode: string,
|
||||
treeCount: number,
|
||||
): RewardLedgerEntry {
|
||||
|
|
@ -919,6 +954,7 @@ export class RewardCalculationService {
|
|||
RightType.CITY_TEAM_RIGHT,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
// 市团队默认账户: 6 + 市代码
|
||||
|
|
@ -940,6 +976,7 @@ export class RewardCalculationService {
|
|||
private calculateCityAreaRightToSystemAccount(
|
||||
sourceOrderNo: string,
|
||||
sourceUserId: bigint,
|
||||
sourceAccountSequence: string | undefined,
|
||||
cityCode: string,
|
||||
treeCount: number,
|
||||
): RewardLedgerEntry {
|
||||
|
|
@ -951,6 +988,7 @@ export class RewardCalculationService {
|
|||
RightType.CITY_AREA_RIGHT,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
// 市区域账户: 8 + 市代码
|
||||
|
|
@ -972,6 +1010,7 @@ export class RewardCalculationService {
|
|||
private calculateCommunityRightToSystemAccount(
|
||||
sourceOrderNo: string,
|
||||
sourceUserId: bigint,
|
||||
sourceAccountSequence: string | undefined,
|
||||
treeCount: number,
|
||||
): RewardLedgerEntry {
|
||||
const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.COMMUNITY_RIGHT];
|
||||
|
|
@ -982,6 +1021,7 @@ export class RewardCalculationService {
|
|||
RightType.COMMUNITY_RIGHT,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
return RewardLedgerEntry.createSettleable({
|
||||
|
|
|
|||
|
|
@ -5,14 +5,16 @@ export class RewardSource {
|
|||
public readonly rightType: RightType,
|
||||
public readonly sourceOrderNo: string, // 订单号是字符串格式如 PLT1765391584505Q0Q6QD
|
||||
public readonly sourceUserId: bigint,
|
||||
public readonly sourceAccountSequence: string | null, // 触发奖励的用户账户序列号
|
||||
) {}
|
||||
|
||||
static create(
|
||||
rightType: RightType,
|
||||
sourceOrderNo: string,
|
||||
sourceUserId: bigint,
|
||||
sourceAccountSequence?: string | null,
|
||||
): RewardSource {
|
||||
return new RewardSource(rightType, sourceOrderNo, sourceUserId);
|
||||
return new RewardSource(rightType, sourceOrderNo, sourceUserId, sourceAccountSequence ?? null);
|
||||
}
|
||||
|
||||
equals(other: RewardSource): boolean {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config';
|
|||
import { PrismaService } from './persistence/prisma/prisma.service';
|
||||
import { RewardLedgerEntryRepositoryImpl } from './persistence/repositories/reward-ledger-entry.repository.impl';
|
||||
import { RewardSummaryRepositoryImpl } from './persistence/repositories/reward-summary.repository.impl';
|
||||
import { SettlementRecordRepository } from './persistence/repositories/settlement-record.repository';
|
||||
import { OutboxRepository } from './persistence/repositories/outbox.repository';
|
||||
import { ReferralServiceClient } from './external/referral-service/referral-service.client';
|
||||
import { AuthorizationServiceClient } from './external/authorization-service/authorization-service.client';
|
||||
|
|
@ -18,6 +19,7 @@ import { REFERRAL_SERVICE_CLIENT, AUTHORIZATION_SERVICE_CLIENT } from '../domain
|
|||
providers: [
|
||||
PrismaService,
|
||||
OutboxRepository,
|
||||
SettlementRecordRepository,
|
||||
{
|
||||
provide: REWARD_LEDGER_ENTRY_REPOSITORY,
|
||||
useClass: RewardLedgerEntryRepositoryImpl,
|
||||
|
|
@ -43,6 +45,7 @@ import { REFERRAL_SERVICE_CLIENT, AUTHORIZATION_SERVICE_CLIENT } from '../domain
|
|||
exports: [
|
||||
PrismaService,
|
||||
OutboxRepository,
|
||||
SettlementRecordRepository,
|
||||
REWARD_LEDGER_ENTRY_REPOSITORY,
|
||||
REWARD_SUMMARY_REPOSITORY,
|
||||
REFERRAL_SERVICE_CLIENT,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export class RewardLedgerEntryMapper {
|
|||
raw.rightType as RightType,
|
||||
raw.sourceOrderNo,
|
||||
raw.sourceUserId,
|
||||
raw.sourceAccountSequence,
|
||||
),
|
||||
usdtAmount: Number(raw.usdtAmount),
|
||||
hashpowerAmount: Number(raw.hashpowerAmount),
|
||||
|
|
@ -34,6 +35,7 @@ export class RewardLedgerEntryMapper {
|
|||
accountSequence: entry.accountSequence,
|
||||
sourceOrderNo: entry.rewardSource.sourceOrderNo,
|
||||
sourceUserId: entry.rewardSource.sourceUserId,
|
||||
sourceAccountSequence: entry.rewardSource.sourceAccountSequence,
|
||||
rightType: entry.rewardSource.rightType,
|
||||
usdtAmount: new Prisma.Decimal(entry.usdtAmount.amount),
|
||||
hashpowerAmount: new Prisma.Decimal(entry.hashpowerAmount.value),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { SettlementRecord as PrismaSettlementRecord } from '@prisma/client';
|
||||
|
||||
export interface SettlementRecordDto {
|
||||
id: string;
|
||||
userId: string;
|
||||
accountSequence: string;
|
||||
usdtAmount: number;
|
||||
hashpowerAmount: number;
|
||||
settleCurrency: string;
|
||||
receivedAmount: number;
|
||||
swapTxHash: string | null;
|
||||
swapRate: number | null;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
completedAt: Date | null;
|
||||
rewardEntryIds: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SettlementRecordRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* 查询用户的结算历史记录
|
||||
*/
|
||||
async findByAccountSequence(
|
||||
accountSequence: string,
|
||||
pagination?: { page: number; pageSize: number },
|
||||
): Promise<SettlementRecordDto[]> {
|
||||
const skip = pagination ? (pagination.page - 1) * pagination.pageSize : undefined;
|
||||
const take = pagination?.pageSize;
|
||||
|
||||
const rawList = await this.prisma.settlementRecord.findMany({
|
||||
where: { accountSequence },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
|
||||
return rawList.map(this.toDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户的结算记录数量
|
||||
*/
|
||||
async countByAccountSequence(accountSequence: string): Promise<number> {
|
||||
return this.prisma.settlementRecord.count({
|
||||
where: { accountSequence },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询结算记录
|
||||
*/
|
||||
async findById(id: bigint): Promise<SettlementRecordDto | null> {
|
||||
const raw = await this.prisma.settlementRecord.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
return raw ? this.toDto(raw) : null;
|
||||
}
|
||||
|
||||
private toDto(raw: PrismaSettlementRecord): SettlementRecordDto {
|
||||
return {
|
||||
id: raw.id.toString(),
|
||||
userId: raw.userId.toString(),
|
||||
accountSequence: raw.accountSequence,
|
||||
usdtAmount: Number(raw.usdtAmount),
|
||||
hashpowerAmount: Number(raw.hashpowerAmount),
|
||||
settleCurrency: raw.settleCurrency,
|
||||
receivedAmount: Number(raw.receivedAmount),
|
||||
swapTxHash: raw.swapTxHash,
|
||||
swapRate: raw.swapRate ? Number(raw.swapRate) : null,
|
||||
status: raw.status,
|
||||
createdAt: raw.createdAt,
|
||||
completedAt: raw.completedAt,
|
||||
rewardEntryIds: raw.rewardEntryIds.map(id => id.toString()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,8 @@ class PendingRewardItem {
|
|||
final DateTime createdAt;
|
||||
final DateTime expireAt;
|
||||
final int remainingTimeMs;
|
||||
final String sourceOrderNo;
|
||||
final String? sourceAccountSequence; // 触发奖励的用户账户序列号
|
||||
final String memo;
|
||||
|
||||
PendingRewardItem({
|
||||
|
|
@ -40,6 +42,8 @@ class PendingRewardItem {
|
|||
required this.createdAt,
|
||||
required this.expireAt,
|
||||
required this.remainingTimeMs,
|
||||
required this.sourceOrderNo,
|
||||
this.sourceAccountSequence,
|
||||
required this.memo,
|
||||
});
|
||||
|
||||
|
|
@ -52,6 +56,8 @@ class PendingRewardItem {
|
|||
createdAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
|
||||
expireAt: DateTime.tryParse(json['expireAt'] ?? '') ?? DateTime.now(),
|
||||
remainingTimeMs: (json['remainingTimeMs'] ?? 0).toInt(),
|
||||
sourceOrderNo: json['sourceOrderNo'] ?? '',
|
||||
sourceAccountSequence: json['sourceAccountSequence'],
|
||||
memo: json['memo'] ?? '',
|
||||
);
|
||||
}
|
||||
|
|
@ -187,6 +193,96 @@ class SettleToBalanceResult {
|
|||
}
|
||||
}
|
||||
|
||||
/// 结算历史记录条目 (从 GET /rewards/settlement-history 获取)
|
||||
class SettlementHistoryItem {
|
||||
final String id;
|
||||
final double usdtAmount;
|
||||
final double hashpowerAmount;
|
||||
final String settleCurrency;
|
||||
final double receivedAmount;
|
||||
final String? swapTxHash;
|
||||
final double? swapRate;
|
||||
final String status;
|
||||
final DateTime createdAt;
|
||||
final DateTime? completedAt;
|
||||
final int rewardEntryCount;
|
||||
|
||||
SettlementHistoryItem({
|
||||
required this.id,
|
||||
required this.usdtAmount,
|
||||
required this.hashpowerAmount,
|
||||
required this.settleCurrency,
|
||||
required this.receivedAmount,
|
||||
this.swapTxHash,
|
||||
this.swapRate,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
this.completedAt,
|
||||
required this.rewardEntryCount,
|
||||
});
|
||||
|
||||
factory SettlementHistoryItem.fromJson(Map<String, dynamic> json) {
|
||||
return SettlementHistoryItem(
|
||||
id: json['id']?.toString() ?? '',
|
||||
usdtAmount: (json['usdtAmount'] ?? 0).toDouble(),
|
||||
hashpowerAmount: (json['hashpowerAmount'] ?? 0).toDouble(),
|
||||
settleCurrency: json['settleCurrency'] ?? '',
|
||||
receivedAmount: (json['receivedAmount'] ?? 0).toDouble(),
|
||||
swapTxHash: json['swapTxHash'],
|
||||
swapRate: json['swapRate']?.toDouble(),
|
||||
status: json['status'] ?? '',
|
||||
createdAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
|
||||
completedAt: json['completedAt'] != null ? DateTime.tryParse(json['completedAt']) : null,
|
||||
rewardEntryCount: (json['rewardEntryCount'] ?? 0).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取状态的中文名称
|
||||
String get statusName {
|
||||
switch (status) {
|
||||
case 'SUCCESS':
|
||||
return '成功';
|
||||
case 'PENDING':
|
||||
return '处理中';
|
||||
case 'FAILED':
|
||||
return '失败';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 结算历史分页结果
|
||||
class SettlementHistoryResult {
|
||||
final List<SettlementHistoryItem> data;
|
||||
final int page;
|
||||
final int pageSize;
|
||||
final int total;
|
||||
|
||||
SettlementHistoryResult({
|
||||
required this.data,
|
||||
required this.page,
|
||||
required this.pageSize,
|
||||
required this.total,
|
||||
});
|
||||
|
||||
factory SettlementHistoryResult.fromJson(Map<String, dynamic> json) {
|
||||
final dataList = json['data'] as List<dynamic>? ?? [];
|
||||
final pagination = json['pagination'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
return SettlementHistoryResult(
|
||||
data: dataList
|
||||
.map((item) => SettlementHistoryItem.fromJson(item as Map<String, dynamic>))
|
||||
.toList(),
|
||||
page: (pagination['page'] ?? 1).toInt(),
|
||||
pageSize: (pagination['pageSize'] ?? 20).toInt(),
|
||||
total: (pagination['total'] ?? 0).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
bool get hasMore => page * pageSize < total;
|
||||
}
|
||||
|
||||
/// 奖励汇总信息 (从 reward-service 获取)
|
||||
class RewardSummary {
|
||||
final double pendingUsdt;
|
||||
|
|
@ -454,6 +550,51 @@ class RewardService {
|
|||
}
|
||||
}
|
||||
|
||||
/// 获取结算历史记录
|
||||
///
|
||||
/// 调用 GET /rewards/settlement-history (reward-service)
|
||||
/// 返回历史结算记录列表
|
||||
Future<SettlementHistoryResult> getSettlementHistory({int page = 1, int pageSize = 20}) async {
|
||||
try {
|
||||
debugPrint('[RewardService] ========== 获取结算历史记录 ==========');
|
||||
debugPrint('[RewardService] 请求: GET /rewards/settlement-history?page=$page&pageSize=$pageSize');
|
||||
|
||||
final response = await _apiClient.get(
|
||||
'/rewards/settlement-history',
|
||||
queryParameters: {'page': page, 'pageSize': pageSize},
|
||||
);
|
||||
|
||||
debugPrint('[RewardService] 响应状态码: ${response.statusCode}');
|
||||
debugPrint('[RewardService] 响应数据类型: ${response.data.runtimeType}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = response.data as Map<String, dynamic>;
|
||||
debugPrint('[RewardService] 原始响应数据: $responseData');
|
||||
|
||||
// 解包可能的 data 字段
|
||||
final data = responseData['data'] != null && responseData['pagination'] == null
|
||||
? responseData['data'] as Map<String, dynamic>
|
||||
: responseData;
|
||||
|
||||
final result = SettlementHistoryResult.fromJson(data);
|
||||
debugPrint('[RewardService] 解析到 ${result.data.length} 条结算记录');
|
||||
debugPrint('[RewardService] 分页: page=${result.page}, pageSize=${result.pageSize}, total=${result.total}');
|
||||
debugPrint('[RewardService] ================================');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
debugPrint('[RewardService] 请求失败,状态码: ${response.statusCode}');
|
||||
debugPrint('[RewardService] 响应内容: ${response.data}');
|
||||
throw Exception('获取结算历史记录失败: ${response.statusCode}');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('[RewardService] !!!!!!!!!! 获取结算历史记录异常 !!!!!!!!!!');
|
||||
debugPrint('[RewardService] 错误: $e');
|
||||
debugPrint('[RewardService] 堆栈: $stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取已过期奖励列表
|
||||
///
|
||||
/// 调用 GET /wallet/expired-rewards (wallet-service)
|
||||
|
|
|
|||
|
|
@ -221,6 +221,85 @@ class FeeConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// 提现记录项 (从 GET /wallet/withdrawals 获取)
|
||||
class WithdrawalHistoryItem {
|
||||
final String orderNo;
|
||||
final double amount;
|
||||
final double fee;
|
||||
final double netAmount;
|
||||
final String chainType;
|
||||
final String toAddress;
|
||||
final String? txHash;
|
||||
final String status;
|
||||
final DateTime createdAt;
|
||||
|
||||
WithdrawalHistoryItem({
|
||||
required this.orderNo,
|
||||
required this.amount,
|
||||
required this.fee,
|
||||
required this.netAmount,
|
||||
required this.chainType,
|
||||
required this.toAddress,
|
||||
this.txHash,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory WithdrawalHistoryItem.fromJson(Map<String, dynamic> json) {
|
||||
return WithdrawalHistoryItem(
|
||||
orderNo: json['orderNo'] ?? '',
|
||||
amount: (json['amount'] ?? 0).toDouble(),
|
||||
fee: (json['fee'] ?? 0).toDouble(),
|
||||
netAmount: (json['netAmount'] ?? 0).toDouble(),
|
||||
chainType: json['chainType'] ?? '',
|
||||
toAddress: json['toAddress'] ?? '',
|
||||
txHash: json['txHash'],
|
||||
status: json['status'] ?? '',
|
||||
createdAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取状态的中文名称
|
||||
String get statusName {
|
||||
switch (status) {
|
||||
case 'PENDING':
|
||||
return '待处理';
|
||||
case 'FROZEN':
|
||||
return '已冻结';
|
||||
case 'CONFIRMED':
|
||||
return '已确认';
|
||||
case 'COMPLETED':
|
||||
return '已完成';
|
||||
case 'FAILED':
|
||||
return '失败';
|
||||
case 'CANCELLED':
|
||||
return '已取消';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取链类型的中文名称
|
||||
String get chainTypeName {
|
||||
switch (chainType) {
|
||||
case 'KAVA':
|
||||
return 'KAVA';
|
||||
case 'BSC':
|
||||
return 'BSC';
|
||||
case 'INTERNAL':
|
||||
return '内部划转';
|
||||
default:
|
||||
return chainType;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取截断的地址显示
|
||||
String get shortAddress {
|
||||
if (toAddress.length <= 16) return toAddress;
|
||||
return '${toAddress.substring(0, 8)}...${toAddress.substring(toAddress.length - 6)}';
|
||||
}
|
||||
}
|
||||
|
||||
/// 钱包服务
|
||||
///
|
||||
/// 提供钱包余额查询、奖励领取等功能
|
||||
|
|
@ -992,6 +1071,7 @@ class LedgerEntry {
|
|||
final String? refTxHash;
|
||||
final String? memo;
|
||||
final String? allocationType;
|
||||
final Map<String, dynamic>? payloadJson;
|
||||
final DateTime createdAt;
|
||||
|
||||
LedgerEntry({
|
||||
|
|
@ -1004,6 +1084,7 @@ class LedgerEntry {
|
|||
this.refTxHash,
|
||||
this.memo,
|
||||
this.allocationType,
|
||||
this.payloadJson,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
|
|
@ -1018,12 +1099,47 @@ class LedgerEntry {
|
|||
refTxHash: json['refTxHash'],
|
||||
memo: json['memo'],
|
||||
allocationType: json['allocationType'],
|
||||
payloadJson: json['payloadJson'] as Map<String, dynamic>?,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.tryParse(json['createdAt']) ?? DateTime.now()
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
// === 转账相关辅助方法 ===
|
||||
|
||||
/// 获取转账对方的账户序列号
|
||||
String? get counterpartyAccountSequence {
|
||||
if (payloadJson == null) return null;
|
||||
if (entryType == 'TRANSFER_IN') {
|
||||
return payloadJson!['fromAccountSequence'] as String?;
|
||||
} else if (entryType == 'TRANSFER_OUT') {
|
||||
return payloadJson!['toAccountSequence'] as String?;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 获取转账对方的用户ID
|
||||
String? get counterpartyUserId {
|
||||
if (payloadJson == null) return null;
|
||||
if (entryType == 'TRANSFER_IN') {
|
||||
return payloadJson!['fromUserId']?.toString();
|
||||
} else if (entryType == 'TRANSFER_OUT') {
|
||||
return payloadJson!['toUserId']?.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 获取转账手续费(仅转出时有)
|
||||
double? get transferFee {
|
||||
if (payloadJson == null || entryType != 'TRANSFER_OUT') return null;
|
||||
final fee = payloadJson!['fee'];
|
||||
if (fee == null) return null;
|
||||
if (fee is num) return fee.toDouble();
|
||||
if (fee is String) return double.tryParse(fee);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 获取流水类型中文名
|
||||
String get entryTypeName {
|
||||
const nameMap = {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:share_plus/share_plus.dart';
|
|||
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../../core/services/wallet_service.dart';
|
||||
import '../../../../core/services/reward_service.dart';
|
||||
import '../../../../core/utils/date_utils.dart';
|
||||
|
||||
/// 账本明细页面 - 显示用户的流水账、统计图表和筛选功能
|
||||
|
|
@ -938,10 +939,13 @@ class _LedgerDetailPageState extends ConsumerState<LedgerDetailPage>
|
|||
/// 构建流水项
|
||||
Widget _buildLedgerItem(LedgerEntry entry) {
|
||||
final isIncome = entry.isIncome;
|
||||
// 可点击查看详情的类型:认种支付、权益分配
|
||||
// 可点击查看详情的类型:认种支付、权益分配、结算、提现、转入、转出
|
||||
final bool isPlantPayment = entry.entryType == 'PLANT_PAYMENT' && entry.refOrderId != null;
|
||||
final bool isRewardEntry = entry.allocationType != null;
|
||||
final bool isClickable = isPlantPayment || isRewardEntry;
|
||||
final bool isSettlementEntry = entry.entryType == 'REWARD_SETTLED';
|
||||
final bool isWithdrawalEntry = entry.entryType == 'WITHDRAWAL';
|
||||
final bool isTransferEntry = entry.entryType == 'TRANSFER_IN' || entry.entryType == 'TRANSFER_OUT';
|
||||
final bool isClickable = isPlantPayment || isRewardEntry || isSettlementEntry || isWithdrawalEntry || isTransferEntry;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: isClickable ? () => _showEntryDetail(entry) : null,
|
||||
|
|
@ -1057,12 +1061,57 @@ class _LedgerDetailPageState extends ConsumerState<LedgerDetailPage>
|
|||
if (entry.entryType == 'PLANT_PAYMENT' && entry.refOrderId != null) {
|
||||
// 认种支付 - 显示合同详情
|
||||
_showTransactionDetail(entry);
|
||||
} else if (entry.entryType == 'REWARD_SETTLED') {
|
||||
// 结算 - 显示结算详情
|
||||
_showSettlementDetail(entry);
|
||||
} else if (entry.entryType == 'WITHDRAWAL') {
|
||||
// 提现 - 显示提现详情
|
||||
_showWithdrawalDetail(entry);
|
||||
} else if (entry.entryType == 'TRANSFER_IN' || entry.entryType == 'TRANSFER_OUT') {
|
||||
// 转入/转出 - 显示转账详情
|
||||
_showTransferDetail(entry);
|
||||
} else if (entry.allocationType != null) {
|
||||
// 权益分配 - 显示权益详情
|
||||
_showRewardDetail(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示转账详情弹窗(转入/转出)
|
||||
void _showTransferDetail(LedgerEntry entry) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => _TransferDetailSheet(entry: entry),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示提现详情弹窗
|
||||
void _showWithdrawalDetail(LedgerEntry entry) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => _WithdrawalDetailSheet(
|
||||
entry: entry,
|
||||
walletService: ref.read(walletServiceProvider),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示结算详情弹窗
|
||||
void _showSettlementDetail(LedgerEntry entry) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => _SettlementDetailSheet(
|
||||
entry: entry,
|
||||
rewardService: ref.read(rewardServiceProvider),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示交易详情弹窗(认种支付)
|
||||
Future<void> _showTransactionDetail(LedgerEntry entry) async {
|
||||
if (entry.refOrderId == null) return;
|
||||
|
|
@ -1617,3 +1666,755 @@ class _RewardDetailSheet extends StatelessWidget {
|
|||
return DateTimeUtils.formatDateTime(date);
|
||||
}
|
||||
}
|
||||
|
||||
/// 结算详情底部弹窗
|
||||
class _SettlementDetailSheet extends StatefulWidget {
|
||||
final LedgerEntry entry;
|
||||
final RewardService rewardService;
|
||||
|
||||
const _SettlementDetailSheet({
|
||||
required this.entry,
|
||||
required this.rewardService,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_SettlementDetailSheet> createState() => _SettlementDetailSheetState();
|
||||
}
|
||||
|
||||
class _SettlementDetailSheetState extends State<_SettlementDetailSheet> {
|
||||
bool _isLoading = true;
|
||||
SettlementHistoryItem? _settlementDetail;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSettlementDetail();
|
||||
}
|
||||
|
||||
/// 加载结算详情
|
||||
Future<void> _loadSettlementDetail() async {
|
||||
try {
|
||||
// 尝试从结算历史中查找匹配的记录
|
||||
final result = await widget.rewardService.getSettlementHistory(page: 1, pageSize: 50);
|
||||
|
||||
// 尝试通过金额和时间匹配
|
||||
SettlementHistoryItem? matchedItem;
|
||||
for (final item in result.data) {
|
||||
// 比较金额(考虑浮点数精度)
|
||||
final amountMatch = (item.receivedAmount - widget.entry.amount).abs() < 0.01;
|
||||
// 比较时间(允许2分钟误差)
|
||||
final timeDiff = item.createdAt.difference(widget.entry.createdAt).inMinutes.abs();
|
||||
final timeMatch = timeDiff < 2;
|
||||
|
||||
if (amountMatch && timeMatch) {
|
||||
matchedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_settlementDetail = matchedItem;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[_SettlementDetailSheet] 加载结算详情失败: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 拖拽指示器
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE0E0E0),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
// 标题区域
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x1A4CAF50),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.check_circle_outline,
|
||||
size: 24,
|
||||
color: Color(0xFF4CAF50),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'结算详情',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'结算到钱包余额',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0x995D4037),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 分隔线
|
||||
Container(
|
||||
height: 1,
|
||||
color: const Color(0x1A8B5A2B),
|
||||
),
|
||||
// 详情内容
|
||||
_isLoading
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildDetailRow(
|
||||
'结算金额',
|
||||
'+${_formatAmount(widget.entry.amount)} 绿积分',
|
||||
color: const Color(0xFF4CAF50),
|
||||
),
|
||||
if (widget.entry.balanceAfter != null)
|
||||
_buildDetailRow(
|
||||
'结算后余额',
|
||||
'${_formatAmount(widget.entry.balanceAfter!)} 绿积分',
|
||||
),
|
||||
_buildDetailRow(
|
||||
'结算时间',
|
||||
_formatDateTime(widget.entry.createdAt),
|
||||
),
|
||||
if (_settlementDetail != null) ...[
|
||||
_buildDetailRow(
|
||||
'涉及奖励',
|
||||
'${_settlementDetail!.rewardEntryCount} 笔',
|
||||
),
|
||||
_buildDetailRow(
|
||||
'结算状态',
|
||||
_settlementDetail!.statusName,
|
||||
color: _settlementDetail!.status == 'SUCCESS'
|
||||
? const Color(0xFF4CAF50)
|
||||
: const Color(0xFFFF9800),
|
||||
),
|
||||
],
|
||||
if (widget.entry.memo != null && widget.entry.memo!.isNotEmpty)
|
||||
_buildDetailRow('备注', widget.entry.memo!),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 底部关闭按钮
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFD4AF37),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value, {Color? color}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0x995D4037),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color ?? const Color(0xFF5D4037),
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatAmount(double amount) {
|
||||
final formatter = NumberFormat('#,##0.00', 'zh_CN');
|
||||
return formatter.format(amount);
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime date) {
|
||||
return DateTimeUtils.formatDateTime(date);
|
||||
}
|
||||
}
|
||||
|
||||
/// 提现详情底部弹窗
|
||||
class _WithdrawalDetailSheet extends StatefulWidget {
|
||||
final LedgerEntry entry;
|
||||
final WalletService walletService;
|
||||
|
||||
const _WithdrawalDetailSheet({
|
||||
required this.entry,
|
||||
required this.walletService,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_WithdrawalDetailSheet> createState() => _WithdrawalDetailSheetState();
|
||||
}
|
||||
|
||||
class _WithdrawalDetailSheetState extends State<_WithdrawalDetailSheet> {
|
||||
bool _isLoading = true;
|
||||
WithdrawRecord? _withdrawalDetail;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadWithdrawalDetail();
|
||||
}
|
||||
|
||||
/// 加载提现详情
|
||||
Future<void> _loadWithdrawalDetail() async {
|
||||
try {
|
||||
// 获取提现记录列表
|
||||
final records = await widget.walletService.getWithdrawals();
|
||||
|
||||
// 尝试通过金额和时间匹配
|
||||
WithdrawRecord? matchedRecord;
|
||||
for (final record in records) {
|
||||
// 比较金额(考虑浮点数精度,使用 netAmount 或 amount)
|
||||
final amountMatch = (record.netAmount - widget.entry.amount.abs()).abs() < 0.01 ||
|
||||
(record.amount - widget.entry.amount.abs()).abs() < 0.01;
|
||||
// 比较时间(允许2分钟误差)
|
||||
final timeDiff = record.createdAt.difference(widget.entry.createdAt).inMinutes.abs();
|
||||
final timeMatch = timeDiff < 2;
|
||||
|
||||
if (amountMatch && timeMatch) {
|
||||
matchedRecord = record;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_withdrawalDetail = matchedRecord;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[_WithdrawalDetailSheet] 加载提现详情失败: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 拖拽指示器
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE0E0E0),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
// 标题区域
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x1AE53935),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.arrow_upward,
|
||||
size: 24,
|
||||
color: Color(0xFFE53935),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'提现详情',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'提取到外部钱包',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0x995D4037),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 分隔线
|
||||
Container(
|
||||
height: 1,
|
||||
color: const Color(0x1A8B5A2B),
|
||||
),
|
||||
// 详情内容
|
||||
_isLoading
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildWithdrawalRow(
|
||||
'提现金额',
|
||||
'-${_formatWithdrawalAmount(widget.entry.amount.abs())} 绿积分',
|
||||
color: const Color(0xFFE53935),
|
||||
),
|
||||
if (widget.entry.balanceAfter != null)
|
||||
_buildWithdrawalRow(
|
||||
'提现后余额',
|
||||
'${_formatWithdrawalAmount(widget.entry.balanceAfter!)} 绿积分',
|
||||
),
|
||||
_buildWithdrawalRow(
|
||||
'提现时间',
|
||||
_formatWithdrawalDateTime(widget.entry.createdAt),
|
||||
),
|
||||
if (_withdrawalDetail != null) ...[
|
||||
_buildWithdrawalRow(
|
||||
'订单编号',
|
||||
_withdrawalDetail!.orderNo,
|
||||
),
|
||||
_buildWithdrawalRow(
|
||||
'提现状态',
|
||||
_getWithdrawalStatusName(_withdrawalDetail!.status),
|
||||
color: _getWithdrawalStatusColor(_withdrawalDetail!.status),
|
||||
),
|
||||
_buildWithdrawalRow(
|
||||
'提现网络',
|
||||
_getWithdrawalChainName(_withdrawalDetail!.chainType),
|
||||
),
|
||||
if (_withdrawalDetail!.fee > 0)
|
||||
_buildWithdrawalRow(
|
||||
'手续费',
|
||||
'${_formatWithdrawalAmount(_withdrawalDetail!.fee)} 绿积分',
|
||||
),
|
||||
_buildWithdrawalRow(
|
||||
'到账地址',
|
||||
_getWithdrawalShortAddress(_withdrawalDetail!.toAddress),
|
||||
),
|
||||
if (_withdrawalDetail!.txHash != null &&
|
||||
_withdrawalDetail!.txHash!.isNotEmpty)
|
||||
_buildWithdrawalRow(
|
||||
'交易哈希',
|
||||
_getWithdrawalShortAddress(_withdrawalDetail!.txHash!),
|
||||
),
|
||||
],
|
||||
if (widget.entry.memo != null && widget.entry.memo!.isNotEmpty)
|
||||
_buildWithdrawalRow('备注', widget.entry.memo!),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 底部关闭按钮
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFD4AF37),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWithdrawalRow(String label, String value, {Color? color}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0x995D4037),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color ?? const Color(0xFF5D4037),
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatWithdrawalAmount(double amount) {
|
||||
final formatter = NumberFormat('#,##0.00', 'zh_CN');
|
||||
return formatter.format(amount);
|
||||
}
|
||||
|
||||
String _formatWithdrawalDateTime(DateTime date) {
|
||||
return DateTimeUtils.formatDateTime(date);
|
||||
}
|
||||
|
||||
String _getWithdrawalStatusName(String status) {
|
||||
switch (status) {
|
||||
case 'PENDING':
|
||||
return '待处理';
|
||||
case 'FROZEN':
|
||||
return '已冻结';
|
||||
case 'CONFIRMED':
|
||||
return '已确认';
|
||||
case 'COMPLETED':
|
||||
return '已完成';
|
||||
case 'FAILED':
|
||||
return '失败';
|
||||
case 'CANCELLED':
|
||||
return '已取消';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getWithdrawalStatusColor(String status) {
|
||||
switch (status) {
|
||||
case 'COMPLETED':
|
||||
return const Color(0xFF4CAF50);
|
||||
case 'PENDING':
|
||||
case 'FROZEN':
|
||||
case 'CONFIRMED':
|
||||
return const Color(0xFFFF9800);
|
||||
case 'FAILED':
|
||||
case 'CANCELLED':
|
||||
return const Color(0xFFE53935);
|
||||
default:
|
||||
return const Color(0xFF5D4037);
|
||||
}
|
||||
}
|
||||
|
||||
String _getWithdrawalChainName(String chainType) {
|
||||
switch (chainType) {
|
||||
case 'KAVA':
|
||||
return 'KAVA';
|
||||
case 'BSC':
|
||||
return 'BSC';
|
||||
case 'INTERNAL':
|
||||
return '内部划转';
|
||||
default:
|
||||
return chainType;
|
||||
}
|
||||
}
|
||||
|
||||
String _getWithdrawalShortAddress(String address) {
|
||||
if (address.length <= 16) return address;
|
||||
return '${address.substring(0, 8)}...${address.substring(address.length - 6)}';
|
||||
}
|
||||
}
|
||||
|
||||
/// 转账详情底部弹窗(转入/转出)
|
||||
class _TransferDetailSheet extends StatelessWidget {
|
||||
final LedgerEntry entry;
|
||||
|
||||
const _TransferDetailSheet({required this.entry});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isTransferIn = entry.entryType == 'TRANSFER_IN';
|
||||
final isIncome = entry.isIncome;
|
||||
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 拖拽指示器
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE0E0E0),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
// 标题区域
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: isIncome
|
||||
? const Color(0x1A4CAF50)
|
||||
: const Color(0x1AE53935),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
isTransferIn ? Icons.call_received : Icons.call_made,
|
||||
size: 24,
|
||||
color: isIncome ? const Color(0xFF4CAF50) : const Color(0xFFE53935),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isTransferIn ? '转入详情' : '转出详情',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
isTransferIn ? '来自其他用户的转账' : '转账给其他用户',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0x995D4037),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 分隔线
|
||||
Container(
|
||||
height: 1,
|
||||
color: const Color(0x1A8B5A2B),
|
||||
),
|
||||
// 详情内容
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildTransferRow(
|
||||
isTransferIn ? '转入金额' : '转出金额',
|
||||
'${isIncome ? '+' : ''}${_formatTransferAmount(entry.amount)} 绿积分',
|
||||
color: isIncome ? const Color(0xFF4CAF50) : const Color(0xFFE53935),
|
||||
),
|
||||
if (entry.balanceAfter != null)
|
||||
_buildTransferRow(
|
||||
isTransferIn ? '转入后余额' : '转出后余额',
|
||||
'${_formatTransferAmount(entry.balanceAfter!)} 绿积分',
|
||||
),
|
||||
_buildTransferRow(
|
||||
isTransferIn ? '转入时间' : '转出时间',
|
||||
_formatTransferDateTime(entry.createdAt),
|
||||
),
|
||||
// 对方账户信息
|
||||
if (entry.counterpartyAccountSequence != null)
|
||||
_buildTransferRow(
|
||||
isTransferIn ? '来自账户' : '转至账户',
|
||||
entry.counterpartyAccountSequence!,
|
||||
),
|
||||
// 手续费(仅转出有)
|
||||
if (entry.transferFee != null && entry.transferFee! > 0)
|
||||
_buildTransferRow(
|
||||
'手续费',
|
||||
'${_formatTransferAmount(entry.transferFee!)} 绿积分',
|
||||
),
|
||||
// 关联订单
|
||||
if (entry.refOrderId != null && entry.refOrderId!.isNotEmpty)
|
||||
_buildTransferRow('订单编号', entry.refOrderId!),
|
||||
// 交易哈希
|
||||
if (entry.refTxHash != null && entry.refTxHash!.isNotEmpty)
|
||||
_buildTransferRow(
|
||||
'交易哈希',
|
||||
_getTransferShortAddress(entry.refTxHash!),
|
||||
),
|
||||
// 备注
|
||||
if (entry.memo != null && entry.memo!.isNotEmpty)
|
||||
_buildTransferRow('备注', entry.memo!),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 底部关闭按钮
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFD4AF37),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTransferRow(String label, String value, {Color? color}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0x995D4037),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color ?? const Color(0xFF5D4037),
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTransferAmount(double amount) {
|
||||
final formatter = NumberFormat('#,##0.00', 'zh_CN');
|
||||
return formatter.format(amount);
|
||||
}
|
||||
|
||||
String _formatTransferDateTime(DateTime date) {
|
||||
return DateTimeUtils.formatDateTime(date);
|
||||
}
|
||||
|
||||
String _getTransferShortAddress(String address) {
|
||||
if (address.length <= 16) return address;
|
||||
return '${address.substring(0, 8)}...${address.substring(address.length - 6)}';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue