From bf772967f5f5671e0cf0f744f3371d18a05f2823 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 1 Feb 2026 04:25:20 -0800 Subject: [PATCH] =?UTF-8?q?feat(mining):=20=E6=89=B9=E9=87=8F=E8=A1=A5?= =?UTF-8?q?=E5=8F=9130%=E5=88=86=E9=85=8D=E5=88=B0=E8=BF=90=E8=90=A5?= =?UTF-8?q?=E5=92=8C=E6=80=BB=E9=83=A8=E8=B4=A6=E6=88=B7=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=A4=E6=98=93=E7=AD=9B=E9=80=89=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 批量补发时将剩余30%分配到运营(12%)和总部(18%)系统账户 - SystemMiningAccountRepository.mine()支持referenceId/referenceType参数 - BatchMiningExecution新增operationAmount/headquartersAmount字段(含DB迁移) - 三层架构(mining-service→admin-service→admin-web)全链路支持referenceType筛选 - 系统账户交易记录页面增加"全部/批量补发"筛选按钮 Co-Authored-By: Claude Opus 4.5 --- .../controllers/system-accounts.controller.ts | 3 + .../services/system-accounts.service.ts | 4 + .../migration.sql | 3 + .../mining-service/prisma/schema.prisma | 4 +- .../src/api/controllers/admin.controller.ts | 13 ++- .../services/batch-mining.service.ts | 87 +++++++++++++++++-- .../system-mining-account.repository.ts | 4 + .../system-accounts/[accountType]/page.tsx | 50 ++++++++--- .../api/system-accounts.api.ts | 9 +- .../hooks/use-system-accounts.ts | 7 +- 10 files changed, 154 insertions(+), 30 deletions(-) create mode 100644 backend/services/mining-service/prisma/migrations/0005_add_system_amounts_to_batch_mining/migration.sql diff --git a/backend/services/mining-admin-service/src/api/controllers/system-accounts.controller.ts b/backend/services/mining-admin-service/src/api/controllers/system-accounts.controller.ts index 183c371d..07a8da9e 100644 --- a/backend/services/mining-admin-service/src/api/controllers/system-accounts.controller.ts +++ b/backend/services/mining-admin-service/src/api/controllers/system-accounts.controller.ts @@ -44,11 +44,13 @@ export class SystemAccountsController { @ApiOperation({ summary: '获取系统账户交易记录' }) @ApiParam({ name: 'accountType', type: String, description: '系统账户类型(OPERATION/PROVINCE/CITY/HEADQUARTERS)' }) @ApiQuery({ name: 'regionCode', required: false, type: String, description: '区域代码(省/市代码)' }) + @ApiQuery({ name: 'referenceType', required: false, type: String, description: '关联类型筛选(如 BATCH_MINING)' }) @ApiQuery({ name: 'page', required: false, type: Number }) @ApiQuery({ name: 'pageSize', required: false, type: Number }) async getSystemAccountTransactions( @Param('accountType') accountType: string, @Query('regionCode') regionCode?: string, + @Query('referenceType') referenceType?: string, @Query('page') page?: number, @Query('pageSize') pageSize?: number, ) { @@ -57,6 +59,7 @@ export class SystemAccountsController { regionCode || null, page ?? 1, pageSize ?? 20, + referenceType || null, ); } diff --git a/backend/services/mining-admin-service/src/application/services/system-accounts.service.ts b/backend/services/mining-admin-service/src/application/services/system-accounts.service.ts index 98923e77..3d67b1da 100644 --- a/backend/services/mining-admin-service/src/application/services/system-accounts.service.ts +++ b/backend/services/mining-admin-service/src/application/services/system-accounts.service.ts @@ -342,6 +342,7 @@ export class SystemAccountsService { regionCode: string | null, page: number = 1, pageSize: number = 20, + referenceType: string | null = null, ) { const miningServiceUrl = this.configService.get( 'MINING_SERVICE_URL', @@ -353,6 +354,9 @@ export class SystemAccountsService { if (regionCode) { params.regionCode = regionCode; } + if (referenceType) { + params.referenceType = referenceType; + } const response = await firstValueFrom( this.httpService.get( diff --git a/backend/services/mining-service/prisma/migrations/0005_add_system_amounts_to_batch_mining/migration.sql b/backend/services/mining-service/prisma/migrations/0005_add_system_amounts_to_batch_mining/migration.sql new file mode 100644 index 00000000..5bf832f5 --- /dev/null +++ b/backend/services/mining-service/prisma/migrations/0005_add_system_amounts_to_batch_mining/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable: 批量补发执行记录增加系统账户分配金额 +ALTER TABLE "batch_mining_executions" ADD COLUMN "operation_amount" DECIMAL(30,8) NOT NULL DEFAULT 0; +ALTER TABLE "batch_mining_executions" ADD COLUMN "headquarters_amount" DECIMAL(30,8) NOT NULL DEFAULT 0; diff --git a/backend/services/mining-service/prisma/schema.prisma b/backend/services/mining-service/prisma/schema.prisma index 69a2fa56..80707a03 100644 --- a/backend/services/mining-service/prisma/schema.prisma +++ b/backend/services/mining-service/prisma/schema.prisma @@ -605,7 +605,9 @@ model BatchMiningExecution { totalBatches Int @map("total_batches") successCount Int @default(0) @map("success_count") failedCount Int @default(0) @map("failed_count") - totalAmount Decimal @default(0) @db.Decimal(30, 8) @map("total_amount") + totalAmount Decimal @default(0) @db.Decimal(30, 8) @map("total_amount") + operationAmount Decimal @default(0) @db.Decimal(30, 8) @map("operation_amount") + headquartersAmount Decimal @default(0) @db.Decimal(30, 8) @map("headquarters_amount") executedAt DateTime @map("executed_at") createdAt DateTime @default(now()) @map("created_at") diff --git a/backend/services/mining-service/src/api/controllers/admin.controller.ts b/backend/services/mining-service/src/api/controllers/admin.controller.ts index 2fb1cdfe..cbb19a2f 100644 --- a/backend/services/mining-service/src/api/controllers/admin.controller.ts +++ b/backend/services/mining-service/src/api/controllers/admin.controller.ts @@ -258,11 +258,13 @@ export class AdminController { @ApiOperation({ summary: '获取系统账户交易记录' }) @ApiParam({ name: 'accountType', type: String, description: '系统账户类型(OPERATION/PROVINCE/CITY/HEADQUARTERS)' }) @ApiQuery({ name: 'regionCode', required: false, type: String, description: '区域代码(省/市代码)' }) + @ApiQuery({ name: 'referenceType', required: false, type: String, description: '关联类型筛选(如 BATCH_MINING)' }) @ApiQuery({ name: 'page', required: false, type: Number }) @ApiQuery({ name: 'pageSize', required: false, type: Number }) async getSystemAccountTransactions( @Param('accountType') accountType: string, @Query('regionCode') regionCode?: string, + @Query('referenceType') referenceType?: string, @Query('page') page?: number, @Query('pageSize') pageSize?: number, ) { @@ -291,16 +293,19 @@ export class AdminController { }; } + const where: any = { systemAccountId: account.id }; + if (referenceType) { + where.referenceType = referenceType; + } + const [transactions, total] = await Promise.all([ this.prisma.systemMiningTransaction.findMany({ - where: { systemAccountId: account.id }, + where, orderBy: { createdAt: 'desc' }, skip, take: pageSizeNum, }), - this.prisma.systemMiningTransaction.count({ - where: { systemAccountId: account.id }, - }), + this.prisma.systemMiningTransaction.count({ where }), ]); return { diff --git a/backend/services/mining-service/src/application/services/batch-mining.service.ts b/backend/services/mining-service/src/application/services/batch-mining.service.ts index 9b51daa6..95b9996c 100644 --- a/backend/services/mining-service/src/application/services/batch-mining.service.ts +++ b/backend/services/mining-service/src/application/services/batch-mining.service.ts @@ -1,6 +1,7 @@ import { Injectable, Logger, ConflictException, BadRequestException } from '@nestjs/common'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; import { MiningConfigRepository } from '../../infrastructure/persistence/repositories/mining-config.repository'; +import { SystemMiningAccountRepository } from '../../infrastructure/persistence/repositories/system-mining-account.repository'; import { ShareAmount } from '../../domain/value-objects/share-amount.vo'; import Decimal from 'decimal.js'; @@ -54,6 +55,8 @@ export interface BatchMiningResult { successCount: number; failedCount: number; totalAmount: string; + operationAmount: string; // 运营账户12%金额 + headquartersAmount: string; // 总部账户18%金额 results: BatchMiningItemResult[]; message: string; } @@ -83,6 +86,8 @@ export interface BatchMiningPreviewResult { batchTotalAmount: string; }[]; grandTotalAmount: string; + operationAmount: string; // 运营账户12%金额 + headquartersAmount: string; // 总部账户18%金额 message: string; calculatedStartDate: string; // 计算使用的起始日期 (YYYY-MM-DD) } @@ -93,6 +98,10 @@ const BASE_CONTRIBUTION_PER_TREE = new Decimal('22617'); // 每棵树的基础 const SECONDS_PER_DAY = 86400; // 每天产出的70%分给补发用户 const DAILY_DISTRIBUTION_RATIO = new Decimal('0.70'); +// 每天产出的12%分给运营账户 +const OPERATION_DISTRIBUTION_RATIO = new Decimal('0.12'); +// 每天产出的18%分给总部账户 +const HEADQUARTERS_DISTRIBUTION_RATIO = new Decimal('0.18'); /** * 挖矿阶段信息 @@ -111,10 +120,11 @@ interface MiningPhase { * * 核心逻辑(分阶段计算): * 1. 根据各批次的挖矿开始时间,划分挖矿阶段 - * 2. 每天产出的70%固定分给当前参与的用户 + * 2. 每天产出的70%固定分给当前参与的用户,12%分给运营账户,18%分给总部账户 * 3. 用户算力 = 认种棵数 × 基础算力/棵 * 4. 用户在每个阶段的收益 = 每日产出 × 70% × 阶段天数 × (用户算力 / 当前参与总算力) * 5. 用户总收益 = 用户参与的各阶段收益之和 + * 6. 运营/总部收益 = 每日产出 × 对应比例 × 总挖矿天数 */ @Injectable() export class BatchMiningService { @@ -123,6 +133,7 @@ export class BatchMiningService { constructor( private readonly prisma: PrismaService, private readonly miningConfigRepository: MiningConfigRepository, + private readonly systemMiningAccountRepository: SystemMiningAccountRepository, ) {} /** @@ -159,6 +170,8 @@ export class BatchMiningService { totalUsers: 0, batches: [], grandTotalAmount: '0', + operationAmount: '0', + headquartersAmount: '0', message: `批量补发已于 ${existing?.executedAt?.toISOString()} 执行过,操作人: ${existing?.operatorName}`, calculatedStartDate: DEFAULT_MINING_START_DATE, }; @@ -235,10 +248,15 @@ export class BatchMiningService { participatingContribution: p.participatingContribution.toFixed(2) })))}`); - // 每天补发额度 = 日产出 × 70% + // 每天补发额度 = 日产出 × 70%(用户)/ 12%(运营)/ 18%(总部) const dailyDistribution = secondDistribution.times(SECONDS_PER_DAY); const dailyAllocation = dailyDistribution.times(DAILY_DISTRIBUTION_RATIO); - this.logger.log(`[preview] 每日产出: ${dailyDistribution.toFixed(8)}, 每日补发额度(70%): ${dailyAllocation.toFixed(8)}`); + const dailyOperationAllocation = dailyDistribution.times(OPERATION_DISTRIBUTION_RATIO); + const dailyHeadquartersAllocation = dailyDistribution.times(HEADQUARTERS_DISTRIBUTION_RATIO); + this.logger.log(`[preview] 每日产出: ${dailyDistribution.toFixed(8)}, 用户70%: ${dailyAllocation.toFixed(8)}, 运营12%: ${dailyOperationAllocation.toFixed(8)}, 总部18%: ${dailyHeadquartersAllocation.toFixed(8)}`); + + // 计算总挖矿天数(用于系统账户分配计算) + const totalMiningDays = phases.reduce((sum, p) => sum + p.daysInPhase, 0); // 计算每个用户在各阶段的收益 // 使用 Set 记录已处理的用户,避免重复计算 @@ -349,14 +367,20 @@ export class BatchMiningService { }); } - const result = { + // 计算运营和总部账户的补发金额 + const operationAmount = dailyOperationAllocation.times(totalMiningDays); + const headquartersAmount = dailyHeadquartersAllocation.times(totalMiningDays); + + const result: BatchMiningPreviewResult = { canExecute: true, alreadyExecuted: false, totalBatches: sortedBatches.length, totalUsers: items.length, batches: batchResults, grandTotalAmount: grandTotalAmount.toFixed(8), - message: `预览成功: ${sortedBatches.length} 个批次, ${items.length} 个用户, 总补发金额 ${grandTotalAmount.toFixed(8)}`, + operationAmount: operationAmount.toFixed(8), + headquartersAmount: headquartersAmount.toFixed(8), + message: `预览成功: ${sortedBatches.length} 个批次, ${items.length} 个用户, 用户补发 ${grandTotalAmount.toFixed(8)}, 运营 ${operationAmount.toFixed(8)}, 总部 ${headquartersAmount.toFixed(8)}`, calculatedStartDate, }; this.logger.log(`[preview] 预览完成: ${result.message}, 起始日期: ${calculatedStartDate}`); @@ -485,6 +509,10 @@ export class BatchMiningService { const secondDistribution = config.secondDistribution.value; const now = new Date(); + // 预检查: 确保系统账户存在 + await this.systemMiningAccountRepository.ensureSystemAccountsExist(); + this.logger.log('[execute] 系统账户预检查通过'); + // 按批次分组并排序 const batchGroups = this.groupByBatch(items); const sortedBatches = Array.from(batchGroups.keys()).sort((a, b) => a - b); @@ -532,9 +560,17 @@ export class BatchMiningService { // 构建挖矿阶段 const phases = this.buildMiningPhases(items, sortedBatches, batchContributions, calculatedStartDate); - // 每天补发额度 = 日产出 × 70% + // 每天补发额度 = 日产出 × 70%(用户)/ 12%(运营)/ 18%(总部) const dailyDistribution = secondDistribution.times(SECONDS_PER_DAY); const dailyAllocation = dailyDistribution.times(DAILY_DISTRIBUTION_RATIO); + const dailyOperationAllocation = dailyDistribution.times(OPERATION_DISTRIBUTION_RATIO); + const dailyHeadquartersAllocation = dailyDistribution.times(HEADQUARTERS_DISTRIBUTION_RATIO); + + // 计算总挖矿天数(用于系统账户分配) + const totalMiningDays = phases.reduce((sum, p) => sum + p.daysInPhase, 0); + const operationTotalAmount = dailyOperationAllocation.times(totalMiningDays); + const headquartersTotalAmount = dailyHeadquartersAllocation.times(totalMiningDays); + this.logger.log(`[execute] 系统账户分配: 运营=${operationTotalAmount.toFixed(8)}, 总部=${headquartersTotalAmount.toFixed(8)}, 总挖矿天数=${totalMiningDays}`); // 计算每个用户在各阶段的收益 const userAmounts = new Map(); @@ -793,6 +829,37 @@ export class BatchMiningService { } } + // 系统账户补发 (30%部分) + // 运营账户 12% + if (!operationTotalAmount.isZero()) { + const operationMemo = `批量补发挖矿 - 运营账户12%份额 - ${totalMiningDays}天 × ${dailyOperationAllocation.toFixed(8)}/天 - 操作人:${operatorName} - ${reason}`; + await this.systemMiningAccountRepository.mine( + 'OPERATION', + null, + new ShareAmount(operationTotalAmount), + operationMemo, + tx, + execution.id, + 'BATCH_MINING', + ); + this.logger.log(`[execute] 运营账户补发完成: ${operationTotalAmount.toFixed(8)}`); + } + + // 总部账户 18% + if (!headquartersTotalAmount.isZero()) { + const headquartersMemo = `批量补发挖矿 - 总部账户18%份额 - ${totalMiningDays}天 × ${dailyHeadquartersAllocation.toFixed(8)}/天 - 操作人:${operatorName} - ${reason}`; + await this.systemMiningAccountRepository.mine( + 'HEADQUARTERS', + null, + new ShareAmount(headquartersTotalAmount), + headquartersMemo, + tx, + execution.id, + 'BATCH_MINING', + ); + this.logger.log(`[execute] 总部账户补发完成: ${headquartersTotalAmount.toFixed(8)}`); + } + // 更新执行记录 await tx.batchMiningExecution.update({ where: { id: execution.id }, @@ -800,6 +867,8 @@ export class BatchMiningService { successCount, failedCount, totalAmount, + operationAmount: operationTotalAmount, + headquartersAmount: headquartersTotalAmount, }, }); @@ -809,7 +878,7 @@ export class BatchMiningService { }); this.logger.log( - `Batch mining executed: batchId=${batchId}, total=${items.length}, success=${successCount}, failed=${failedCount}, amount=${totalAmount.toFixed(8)}`, + `Batch mining executed: batchId=${batchId}, total=${items.length}, success=${successCount}, failed=${failedCount}, 用户=${totalAmount.toFixed(8)}, 运营=${operationTotalAmount.toFixed(8)}, 总部=${headquartersTotalAmount.toFixed(8)}`, ); return { @@ -819,8 +888,10 @@ export class BatchMiningService { successCount, failedCount, totalAmount: totalAmount.toFixed(8), + operationAmount: operationTotalAmount.toFixed(8), + headquartersAmount: headquartersTotalAmount.toFixed(8), results, - message: `批量补发完成: 成功 ${successCount} 个, 失败 ${failedCount} 个, 总金额 ${totalAmount.toFixed(8)}`, + message: `批量补发完成: 成功 ${successCount} 个, 失败 ${failedCount} 个, 用户 ${totalAmount.toFixed(8)}, 运营 ${operationTotalAmount.toFixed(8)}, 总部 ${headquartersTotalAmount.toFixed(8)}`, }; } diff --git a/backend/services/mining-service/src/infrastructure/persistence/repositories/system-mining-account.repository.ts b/backend/services/mining-service/src/infrastructure/persistence/repositories/system-mining-account.repository.ts index 67ba2ebb..5a0743a0 100644 --- a/backend/services/mining-service/src/infrastructure/persistence/repositories/system-mining-account.repository.ts +++ b/backend/services/mining-service/src/infrastructure/persistence/repositories/system-mining-account.repository.ts @@ -147,6 +147,8 @@ export class SystemMiningAccountRepository { amount: ShareAmount, memo: string, tx?: TransactionClient, + referenceId?: string, + referenceType?: string, ): Promise { const executeInTx = async (client: TransactionClient) => { // 使用 findFirst 替代 findUnique,因为 regionCode 可以为 null @@ -180,6 +182,8 @@ export class SystemMiningAccountRepository { amount: amount.value, balanceBefore, balanceAfter, + referenceId: referenceId ?? null, + referenceType: referenceType ?? null, memo, }, }); diff --git a/frontend/mining-admin-web/src/app/(dashboard)/system-accounts/[accountType]/page.tsx b/frontend/mining-admin-web/src/app/(dashboard)/system-accounts/[accountType]/page.tsx index bdd47dd2..d4f8b950 100644 --- a/frontend/mining-admin-web/src/app/(dashboard)/system-accounts/[accountType]/page.tsx +++ b/frontend/mining-admin-web/src/app/(dashboard)/system-accounts/[accountType]/page.tsx @@ -56,6 +56,7 @@ export default function SystemAccountDetailPage() { const [miningPage, setMiningPage] = useState(1); const [transactionPage, setTransactionPage] = useState(1); + const [transactionFilter, setTransactionFilter] = useState(null); const [contributionPage, setContributionPage] = useState(1); const pageSize = 20; @@ -77,7 +78,7 @@ export default function SystemAccountDetailPage() { data: transactions, isLoading: transactionsLoading, error: transactionsError, - } = useSystemAccountTransactions(accountType, transactionPage, pageSize); + } = useSystemAccountTransactions(accountType, transactionPage, pageSize, transactionFilter); // 获取当前账户的 regionCode const regionCode = currentAccount?.regionCode ?? null; @@ -353,14 +354,32 @@ export default function SystemAccountDetailPage() { - - 交易记录 - {transactions && ( - - 共 {transactions.total} 条 - - )} - +
+ + 交易记录 + {transactions && ( + + 共 {transactions.total} 条 + + )} + +
+ + +
+
{transactionsError ? ( @@ -419,9 +438,16 @@ export default function SystemAccountDetailPage() { )} - - {typeInfo.label} - +
+ + {typeInfo.label} + + {tx.referenceType === 'BATCH_MINING' && ( + + 批量补发 + + )} +
=> { + const params: Record = { page, pageSize }; + if (referenceType) { + params.referenceType = referenceType; + } const response = await apiClient.get( `/system-accounts/${accountType}/transactions`, - { params: { page, pageSize } } + { params } ); return response.data.data; }, diff --git a/frontend/mining-admin-web/src/features/system-accounts/hooks/use-system-accounts.ts b/frontend/mining-admin-web/src/features/system-accounts/hooks/use-system-accounts.ts index 4e6039e0..54f953c0 100644 --- a/frontend/mining-admin-web/src/features/system-accounts/hooks/use-system-accounts.ts +++ b/frontend/mining-admin-web/src/features/system-accounts/hooks/use-system-accounts.ts @@ -68,11 +68,12 @@ export function useSystemAccountMiningRecords( export function useSystemAccountTransactions( accountType: string, page: number = 1, - pageSize: number = 20 + pageSize: number = 20, + referenceType?: string | null, ) { return useQuery({ - queryKey: ['system-accounts', accountType, 'transactions', page, pageSize], - queryFn: () => systemAccountsApi.getTransactions(accountType, page, pageSize), + queryKey: ['system-accounts', accountType, 'transactions', page, pageSize, referenceType], + queryFn: () => systemAccountsApi.getTransactions(accountType, page, pageSize, referenceType), enabled: !!accountType, }); }