feat(system-accounts): 实现系统账户按省市细分算力和挖矿分配
## 核心功能 ### 1. 算力按省市细分分配 - accountType 从枚举改为组合键字符串:PROVINCE_440000, CITY_440100 - 新增 baseType (基础类型) 和 regionCode (区域代码) 字段 - 认种时根据 selectedProvince/selectedCity 分配到具体省市账户 - 无省市信息时归入汇总账户 ### 2. 系统账户参与挖矿 - 运营、省、市账户按各自 totalContribution 参与挖矿 - 总部账户(HEADQUARTERS)不直接参与,接收待解锁算力收益 - 待解锁算力 100% 参与挖矿,收益归总部 ### 3. 算力来源明细追溯 - 新增 SystemContributionRecord 记录每笔算力来源 - 新增 SystemContributionRecordCreatedEvent 事件同步明细 - 前端新增"算力来源"标签页展示明细 ## 修改的服务 ### contribution-service - schema: SystemAccount 新增 baseType, regionCode - contribution-calculator: 按省市生成组合键 - system-account.repository: 支持动态创建省市账户 - 新增 SystemContributionRecordCreatedEvent 事件 ### mining-service - schema: SystemMiningAccount 从枚举改为字符串 - network-sync: 处理带 baseType/regionCode 的同步事件 - mining-distribution: 系统账户和待解锁算力参与挖矿 ### mining-admin-service - schema: 新增 SyncedSystemContributionRecord 表 - cdc-sync: 处理 SystemContributionRecordCreated 事件 - system-accounts.service: 新增算力来源明细和统计 API ### mining-admin-web - 新增 ContributionRecordsTable 组件 - 系统账户详情页新增"算力来源"标签页 - 显示来源认种ID、用户、分配比例、金额 ## 数据库迁移 - contribution-service: 20250120000001_add_region_to_system_accounts - mining-service: 20250120000001_add_region_to_system_mining_accounts - mining-admin-service: 20250120000001, 20250120000002 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5fa0fd5d1a
commit
09b0bc077e
|
|
@ -792,7 +792,9 @@
|
||||||
"Bash(where:*)",
|
"Bash(where:*)",
|
||||||
"Bash(npx md-to-pdf:*)",
|
"Bash(npx md-to-pdf:*)",
|
||||||
"Bash(ssh -J ceshi@103.39.231.231 ceshi@192.168.1.111 \"curl -s ''http://localhost:3000/api/price/klines?period=1h&limit=5'' | head -500\")",
|
"Bash(ssh -J ceshi@103.39.231.231 ceshi@192.168.1.111 \"curl -s ''http://localhost:3000/api/price/klines?period=1h&limit=5'' | head -500\")",
|
||||||
"Bash(dir /b /ad \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\")"
|
"Bash(dir /b /ad \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\")",
|
||||||
|
"Bash(timeout 30 cat:*)",
|
||||||
|
"Bash(npm run lint)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
-- 系统账户按省市细分: 添加 base_type 和 region_code 字段
|
||||||
|
-- 将 accountType 从简单枚举改为组合键(如 PROVINCE_440000, CITY_440100)
|
||||||
|
|
||||||
|
-- 1. 添加新字段
|
||||||
|
ALTER TABLE "system_accounts" ADD COLUMN "base_type" VARCHAR(20);
|
||||||
|
ALTER TABLE "system_accounts" ADD COLUMN "region_code" VARCHAR(10);
|
||||||
|
|
||||||
|
-- 2. 设置现有数据的 base_type(与 account_type 相同)
|
||||||
|
UPDATE "system_accounts" SET "base_type" = "account_type" WHERE "base_type" IS NULL;
|
||||||
|
|
||||||
|
-- 3. 将 base_type 设置为非空
|
||||||
|
ALTER TABLE "system_accounts" ALTER COLUMN "base_type" SET NOT NULL;
|
||||||
|
|
||||||
|
-- 4. 修改 account_type 字段长度以支持组合键
|
||||||
|
ALTER TABLE "system_accounts" ALTER COLUMN "account_type" TYPE VARCHAR(50);
|
||||||
|
|
||||||
|
-- 5. 创建索引(Prisma 默认命名格式: {table}_{field}_idx)
|
||||||
|
CREATE INDEX IF NOT EXISTS "system_accounts_base_type_idx" ON "system_accounts"("base_type");
|
||||||
|
CREATE INDEX IF NOT EXISTS "system_accounts_region_code_idx" ON "system_accounts"("region_code");
|
||||||
|
|
@ -298,10 +298,14 @@ model UnallocatedContribution {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 系统账户(运营/省/市/总部)
|
// 系统账户(运营/省/市/总部)
|
||||||
|
// accountType 格式: OPERATION, PROVINCE, CITY, HEADQUARTERS (汇总账户)
|
||||||
|
// PROVINCE_440000, CITY_440100 等 (按省市细分的账户)
|
||||||
model SystemAccount {
|
model SystemAccount {
|
||||||
id BigInt @id @default(autoincrement())
|
id BigInt @id @default(autoincrement())
|
||||||
accountType String @unique @map("account_type") @db.VarChar(20) // OPERATION / PROVINCE / CITY / HEADQUARTERS
|
accountType String @unique @map("account_type") @db.VarChar(50) // 组合键: PROVINCE_440000, CITY_440100 等
|
||||||
name String @db.VarChar(100)
|
baseType String @map("base_type") @db.VarChar(20) // 基础类型: OPERATION / PROVINCE / CITY / HEADQUARTERS
|
||||||
|
regionCode String? @map("region_code") @db.VarChar(10) // 区域代码: 省/市代码,如 440000, 440100
|
||||||
|
name String @db.VarChar(100)
|
||||||
|
|
||||||
contributionBalance Decimal @default(0) @map("contribution_balance") @db.Decimal(30, 10)
|
contributionBalance Decimal @default(0) @map("contribution_balance") @db.Decimal(30, 10)
|
||||||
contributionNeverExpires Boolean @default(false) @map("contribution_never_expires")
|
contributionNeverExpires Boolean @default(false) @map("contribution_never_expires")
|
||||||
|
|
@ -313,6 +317,8 @@ model SystemAccount {
|
||||||
|
|
||||||
records SystemContributionRecord[]
|
records SystemContributionRecord[]
|
||||||
|
|
||||||
|
@@index([baseType])
|
||||||
|
@@index([regionCode])
|
||||||
@@map("system_accounts")
|
@@map("system_accounts")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { SyncedReferral } from '../../domain/repositories/synced-data.repository
|
||||||
import { ContributionDistributionPublisherService } from './contribution-distribution-publisher.service';
|
import { ContributionDistributionPublisherService } from './contribution-distribution-publisher.service';
|
||||||
import { ContributionRateService } from './contribution-rate.service';
|
import { ContributionRateService } from './contribution-rate.service';
|
||||||
import { BonusClaimService } from './bonus-claim.service';
|
import { BonusClaimService } from './bonus-claim.service';
|
||||||
import { ContributionRecordSyncedEvent, NetworkProgressUpdatedEvent, ContributionAccountUpdatedEvent, SystemAccountSyncedEvent, UnallocatedContributionSyncedEvent } from '../../domain/events';
|
import { ContributionRecordSyncedEvent, NetworkProgressUpdatedEvent, ContributionAccountUpdatedEvent, SystemAccountSyncedEvent, SystemContributionRecordCreatedEvent, UnallocatedContributionSyncedEvent } from '../../domain/events';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 算力计算应用服务
|
* 算力计算应用服务
|
||||||
|
|
@ -285,13 +285,20 @@ export class ContributionCalculationService {
|
||||||
(sum, u) => sum.add(u.amount),
|
(sum, u) => sum.add(u.amount),
|
||||||
new ContributionAmount(0),
|
new ContributionAmount(0),
|
||||||
);
|
);
|
||||||
await this.systemAccountRepository.addContribution('HEADQUARTERS', totalUnallocatedAmount);
|
await this.systemAccountRepository.addContribution(
|
||||||
|
'HEADQUARTERS',
|
||||||
|
'HEADQUARTERS',
|
||||||
|
null,
|
||||||
|
totalUnallocatedAmount,
|
||||||
|
);
|
||||||
|
|
||||||
// 发布 HEADQUARTERS 账户同步事件
|
// 发布 HEADQUARTERS 账户同步事件
|
||||||
const headquartersAccount = await this.systemAccountRepository.findByType('HEADQUARTERS');
|
const headquartersAccount = await this.systemAccountRepository.findByType('HEADQUARTERS');
|
||||||
if (headquartersAccount) {
|
if (headquartersAccount) {
|
||||||
const hqEvent = new SystemAccountSyncedEvent(
|
const hqEvent = new SystemAccountSyncedEvent(
|
||||||
'HEADQUARTERS',
|
'HEADQUARTERS',
|
||||||
|
'HEADQUARTERS', // 新增:基础类型
|
||||||
|
null, // 新增:区域代码(总部没有区域)
|
||||||
headquartersAccount.name,
|
headquartersAccount.name,
|
||||||
headquartersAccount.contributionBalance.value.toString(),
|
headquartersAccount.contributionBalance.value.toString(),
|
||||||
headquartersAccount.createdAt,
|
headquartersAccount.createdAt,
|
||||||
|
|
@ -325,13 +332,21 @@ export class ContributionCalculationService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 保存系统账户算力并发布同步事件
|
// 5. 保存系统账户算力并发布同步事件(支持按省市细分)
|
||||||
if (result.systemContributions.length > 0) {
|
if (result.systemContributions.length > 0) {
|
||||||
await this.systemAccountRepository.ensureSystemAccountsExist();
|
await this.systemAccountRepository.ensureSystemAccountsExist();
|
||||||
|
|
||||||
for (const sys of result.systemContributions) {
|
for (const sys of result.systemContributions) {
|
||||||
await this.systemAccountRepository.addContribution(sys.accountType, sys.amount);
|
// 动态创建/更新系统账户(支持省市细分)
|
||||||
await this.systemAccountRepository.saveContributionRecord({
|
await this.systemAccountRepository.addContribution(
|
||||||
|
sys.accountType,
|
||||||
|
sys.baseType,
|
||||||
|
sys.regionCode,
|
||||||
|
sys.amount,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 保存算力明细记录并获取保存后的记录(带ID)
|
||||||
|
const savedRecord = await this.systemAccountRepository.saveContributionRecord({
|
||||||
systemAccountType: sys.accountType,
|
systemAccountType: sys.accountType,
|
||||||
sourceAdoptionId,
|
sourceAdoptionId,
|
||||||
sourceAccountSequence,
|
sourceAccountSequence,
|
||||||
|
|
@ -346,6 +361,8 @@ export class ContributionCalculationService {
|
||||||
if (systemAccount) {
|
if (systemAccount) {
|
||||||
const event = new SystemAccountSyncedEvent(
|
const event = new SystemAccountSyncedEvent(
|
||||||
sys.accountType,
|
sys.accountType,
|
||||||
|
sys.baseType, // 新增:基础类型
|
||||||
|
sys.regionCode, // 新增:区域代码
|
||||||
systemAccount.name,
|
systemAccount.name,
|
||||||
systemAccount.contributionBalance.value.toString(),
|
systemAccount.contributionBalance.value.toString(),
|
||||||
systemAccount.createdAt,
|
systemAccount.createdAt,
|
||||||
|
|
@ -356,6 +373,25 @@ export class ContributionCalculationService {
|
||||||
eventType: SystemAccountSyncedEvent.EVENT_TYPE,
|
eventType: SystemAccountSyncedEvent.EVENT_TYPE,
|
||||||
payload: event.toPayload(),
|
payload: event.toPayload(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 发布系统账户算力明细事件(用于 mining-admin-service 同步明细记录)
|
||||||
|
const recordEvent = new SystemContributionRecordCreatedEvent(
|
||||||
|
savedRecord.id,
|
||||||
|
sys.accountType,
|
||||||
|
sourceAdoptionId,
|
||||||
|
sourceAccountSequence,
|
||||||
|
sys.rate.value.toNumber(),
|
||||||
|
sys.amount.value.toString(),
|
||||||
|
effectiveDate,
|
||||||
|
null, // System account contributions never expire
|
||||||
|
savedRecord.createdAt,
|
||||||
|
);
|
||||||
|
await this.outboxRepository.save({
|
||||||
|
aggregateType: SystemContributionRecordCreatedEvent.AGGREGATE_TYPE,
|
||||||
|
aggregateId: savedRecord.id.toString(),
|
||||||
|
eventType: SystemContributionRecordCreatedEvent.EVENT_TYPE,
|
||||||
|
payload: recordEvent.toPayload(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,5 @@ export * from './adoption-synced.event';
|
||||||
export * from './contribution-record-synced.event';
|
export * from './contribution-record-synced.event';
|
||||||
export * from './network-progress-updated.event';
|
export * from './network-progress-updated.event';
|
||||||
export * from './system-account-synced.event';
|
export * from './system-account-synced.event';
|
||||||
|
export * from './system-contribution-record-created.event';
|
||||||
export * from './unallocated-contribution-synced.event';
|
export * from './unallocated-contribution-synced.event';
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
/**
|
/**
|
||||||
* 系统账户算力同步事件
|
* 系统账户算力同步事件
|
||||||
* 用于将系统账户(运营、省、市、总部)的算力同步到 mining-service
|
* 用于将系统账户(运营、省、市、总部)的算力同步到 mining-service
|
||||||
|
* 支持按省市细分的账户(如 PROVINCE_440000, CITY_440100)
|
||||||
*/
|
*/
|
||||||
export class SystemAccountSyncedEvent {
|
export class SystemAccountSyncedEvent {
|
||||||
static readonly EVENT_TYPE = 'SystemAccountSynced';
|
static readonly EVENT_TYPE = 'SystemAccountSynced';
|
||||||
static readonly AGGREGATE_TYPE = 'SystemAccount';
|
static readonly AGGREGATE_TYPE = 'SystemAccount';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly accountType: string, // OPERATION / PROVINCE / CITY / HEADQUARTERS
|
public readonly accountType: string, // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等
|
||||||
|
public readonly baseType: string, // 基础类型: OPERATION / PROVINCE / CITY / HEADQUARTERS
|
||||||
|
public readonly regionCode: string | null, // 区域代码: 省/市代码,如 440000, 440100
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly contributionBalance: string,
|
public readonly contributionBalance: string,
|
||||||
public readonly createdAt: Date,
|
public readonly createdAt: Date,
|
||||||
|
|
@ -17,6 +20,8 @@ export class SystemAccountSyncedEvent {
|
||||||
return {
|
return {
|
||||||
eventType: SystemAccountSyncedEvent.EVENT_TYPE,
|
eventType: SystemAccountSyncedEvent.EVENT_TYPE,
|
||||||
accountType: this.accountType,
|
accountType: this.accountType,
|
||||||
|
baseType: this.baseType,
|
||||||
|
regionCode: this.regionCode,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
contributionBalance: this.contributionBalance,
|
contributionBalance: this.contributionBalance,
|
||||||
createdAt: this.createdAt.toISOString(),
|
createdAt: this.createdAt.toISOString(),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* 系统账户算力明细创建事件
|
||||||
|
* 用于将系统账户的每笔算力来源明细同步到 mining-admin-service
|
||||||
|
*/
|
||||||
|
export class SystemContributionRecordCreatedEvent {
|
||||||
|
static readonly EVENT_TYPE = 'SystemContributionRecordCreated';
|
||||||
|
static readonly AGGREGATE_TYPE = 'SystemContributionRecord';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly recordId: bigint, // 明细记录ID
|
||||||
|
public readonly systemAccountType: string, // 系统账户类型(组合键)
|
||||||
|
public readonly sourceAdoptionId: bigint, // 来源认种ID
|
||||||
|
public readonly sourceAccountSequence: string, // 认种人账号
|
||||||
|
public readonly distributionRate: number, // 分配比例
|
||||||
|
public readonly amount: string, // 算力金额
|
||||||
|
public readonly effectiveDate: Date, // 生效日期
|
||||||
|
public readonly expireDate: Date | null, // 过期日期
|
||||||
|
public readonly createdAt: Date, // 创建时间
|
||||||
|
) {}
|
||||||
|
|
||||||
|
toPayload(): Record<string, any> {
|
||||||
|
return {
|
||||||
|
eventType: SystemContributionRecordCreatedEvent.EVENT_TYPE,
|
||||||
|
recordId: this.recordId.toString(),
|
||||||
|
systemAccountType: this.systemAccountType,
|
||||||
|
sourceAdoptionId: this.sourceAdoptionId.toString(),
|
||||||
|
sourceAccountSequence: this.sourceAccountSequence,
|
||||||
|
distributionRate: this.distributionRate,
|
||||||
|
amount: this.amount,
|
||||||
|
effectiveDate: this.effectiveDate.toISOString(),
|
||||||
|
expireDate: this.expireDate?.toISOString() ?? null,
|
||||||
|
createdAt: this.createdAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,20 @@ import { ContributionAccountAggregate, ContributionSourceType } from '../aggrega
|
||||||
import { ContributionRecordAggregate } from '../aggregates/contribution-record.aggregate';
|
import { ContributionRecordAggregate } from '../aggregates/contribution-record.aggregate';
|
||||||
import { SyncedAdoption, SyncedReferral } from '../repositories/synced-data.repository.interface';
|
import { SyncedAdoption, SyncedReferral } from '../repositories/synced-data.repository.interface';
|
||||||
|
|
||||||
|
// 基础类型枚举
|
||||||
|
export type SystemAccountBaseType = 'OPERATION' | 'PROVINCE' | 'CITY' | 'HEADQUARTERS';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统账户贡献值分配
|
||||||
|
*/
|
||||||
|
export interface SystemContributionAllocation {
|
||||||
|
accountType: string; // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等
|
||||||
|
baseType: SystemAccountBaseType; // 基础类型
|
||||||
|
regionCode: string | null; // 区域代码
|
||||||
|
rate: DistributionRate;
|
||||||
|
amount: ContributionAmount;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 算力分配结果
|
* 算力分配结果
|
||||||
*/
|
*/
|
||||||
|
|
@ -27,12 +41,8 @@ export interface ContributionDistributionResult {
|
||||||
reason: string;
|
reason: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
// 系统账户贡献值
|
// 系统账户贡献值(支持按省市细分)
|
||||||
systemContributions: {
|
systemContributions: SystemContributionAllocation[];
|
||||||
accountType: 'OPERATION' | 'PROVINCE' | 'CITY';
|
|
||||||
rate: DistributionRate;
|
|
||||||
amount: ContributionAmount;
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -85,23 +95,58 @@ export class ContributionCalculatorService {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 系统账户贡献值 (15%)
|
// 2. 系统账户贡献值 (15%)
|
||||||
result.systemContributions = [
|
// 运营账户(全国)- 12%
|
||||||
{
|
result.systemContributions.push({
|
||||||
accountType: 'OPERATION',
|
accountType: 'OPERATION',
|
||||||
rate: DistributionRate.OPERATION,
|
baseType: 'OPERATION',
|
||||||
amount: totalContribution.multiply(DistributionRate.OPERATION.value),
|
regionCode: null,
|
||||||
},
|
rate: DistributionRate.OPERATION,
|
||||||
{
|
amount: totalContribution.multiply(DistributionRate.OPERATION.value),
|
||||||
accountType: 'PROVINCE',
|
});
|
||||||
|
|
||||||
|
// 省公司账户 - 1%(按认种选择的省份)
|
||||||
|
const provinceCode = adoption.selectedProvince;
|
||||||
|
if (provinceCode) {
|
||||||
|
// 有省份时分配到具体省份账户
|
||||||
|
result.systemContributions.push({
|
||||||
|
accountType: `PROVINCE_${provinceCode}`,
|
||||||
|
baseType: 'PROVINCE',
|
||||||
|
regionCode: provinceCode,
|
||||||
rate: DistributionRate.PROVINCE,
|
rate: DistributionRate.PROVINCE,
|
||||||
amount: totalContribution.multiply(DistributionRate.PROVINCE.value),
|
amount: totalContribution.multiply(DistributionRate.PROVINCE.value),
|
||||||
},
|
});
|
||||||
{
|
} else {
|
||||||
accountType: 'CITY',
|
// 无省份时归汇总账户
|
||||||
|
result.systemContributions.push({
|
||||||
|
accountType: 'PROVINCE',
|
||||||
|
baseType: 'PROVINCE',
|
||||||
|
regionCode: null,
|
||||||
|
rate: DistributionRate.PROVINCE,
|
||||||
|
amount: totalContribution.multiply(DistributionRate.PROVINCE.value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 市公司账户 - 2%(按认种选择的城市)
|
||||||
|
const cityCode = adoption.selectedCity;
|
||||||
|
if (cityCode) {
|
||||||
|
// 有城市时分配到具体城市账户
|
||||||
|
result.systemContributions.push({
|
||||||
|
accountType: `CITY_${cityCode}`,
|
||||||
|
baseType: 'CITY',
|
||||||
|
regionCode: cityCode,
|
||||||
rate: DistributionRate.CITY,
|
rate: DistributionRate.CITY,
|
||||||
amount: totalContribution.multiply(DistributionRate.CITY.value),
|
amount: totalContribution.multiply(DistributionRate.CITY.value),
|
||||||
},
|
});
|
||||||
];
|
} else {
|
||||||
|
// 无城市时归汇总账户
|
||||||
|
result.systemContributions.push({
|
||||||
|
accountType: 'CITY',
|
||||||
|
baseType: 'CITY',
|
||||||
|
regionCode: null,
|
||||||
|
rate: DistributionRate.CITY,
|
||||||
|
amount: totalContribution.multiply(DistributionRate.CITY.value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 团队贡献值 (15%)
|
// 3. 团队贡献值 (15%)
|
||||||
this.distributeTeamContribution(
|
this.distributeTeamContribution(
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,14 @@ import { Injectable } from '@nestjs/common';
|
||||||
import { ContributionAmount } from '../../../domain/value-objects/contribution-amount.vo';
|
import { ContributionAmount } from '../../../domain/value-objects/contribution-amount.vo';
|
||||||
import { UnitOfWork, TransactionClient } from '../unit-of-work/unit-of-work';
|
import { UnitOfWork, TransactionClient } from '../unit-of-work/unit-of-work';
|
||||||
|
|
||||||
export type SystemAccountType = 'OPERATION' | 'PROVINCE' | 'CITY' | 'HEADQUARTERS';
|
// 基础类型枚举
|
||||||
|
export type SystemAccountBaseType = 'OPERATION' | 'PROVINCE' | 'CITY' | 'HEADQUARTERS';
|
||||||
|
|
||||||
export interface SystemAccount {
|
export interface SystemAccount {
|
||||||
id: bigint;
|
id: bigint;
|
||||||
accountType: SystemAccountType;
|
accountType: string; // 组合键: PROVINCE_440000, CITY_440100 等
|
||||||
|
baseType: SystemAccountBaseType; // 基础类型
|
||||||
|
regionCode: string | null; // 区域代码
|
||||||
name: string;
|
name: string;
|
||||||
contributionBalance: ContributionAmount;
|
contributionBalance: ContributionAmount;
|
||||||
contributionNeverExpires: boolean;
|
contributionNeverExpires: boolean;
|
||||||
|
|
@ -36,7 +39,10 @@ export class SystemAccountRepository {
|
||||||
return this.unitOfWork.getClient();
|
return this.unitOfWork.getClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByType(accountType: SystemAccountType): Promise<SystemAccount | null> {
|
/**
|
||||||
|
* 根据 accountType(组合键)查找系统账户
|
||||||
|
*/
|
||||||
|
async findByType(accountType: string): Promise<SystemAccount | null> {
|
||||||
const record = await this.client.systemAccount.findUnique({
|
const record = await this.client.systemAccount.findUnique({
|
||||||
where: { accountType },
|
where: { accountType },
|
||||||
});
|
});
|
||||||
|
|
@ -48,6 +54,18 @@ export class SystemAccountRepository {
|
||||||
return this.toSystemAccount(record);
|
return this.toSystemAccount(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据基础类型查找所有账户(如查找所有 CITY 类型账户)
|
||||||
|
*/
|
||||||
|
async findByBaseType(baseType: SystemAccountBaseType): Promise<SystemAccount[]> {
|
||||||
|
const records = await this.client.systemAccount.findMany({
|
||||||
|
where: { baseType },
|
||||||
|
orderBy: { accountType: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return records.map((r) => this.toSystemAccount(r));
|
||||||
|
}
|
||||||
|
|
||||||
async findAll(): Promise<SystemAccount[]> {
|
async findAll(): Promise<SystemAccount[]> {
|
||||||
const records = await this.client.systemAccount.findMany({
|
const records = await this.client.systemAccount.findMany({
|
||||||
orderBy: { accountType: 'asc' },
|
orderBy: { accountType: 'asc' },
|
||||||
|
|
@ -56,12 +74,15 @@ export class SystemAccountRepository {
|
||||||
return records.map((r) => this.toSystemAccount(r));
|
return records.map((r) => this.toSystemAccount(r));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保基础的汇总账户存在(向后兼容)
|
||||||
|
*/
|
||||||
async ensureSystemAccountsExist(): Promise<void> {
|
async ensureSystemAccountsExist(): Promise<void> {
|
||||||
const accounts: { accountType: SystemAccountType; name: string }[] = [
|
const accounts: { accountType: string; baseType: SystemAccountBaseType; name: string }[] = [
|
||||||
{ accountType: 'OPERATION', name: '运营账户' },
|
{ accountType: 'OPERATION', baseType: 'OPERATION', name: '运营账户' },
|
||||||
{ accountType: 'PROVINCE', name: '省公司账户' },
|
{ accountType: 'PROVINCE', baseType: 'PROVINCE', name: '省公司账户' },
|
||||||
{ accountType: 'CITY', name: '市公司账户' },
|
{ accountType: 'CITY', baseType: 'CITY', name: '市公司账户' },
|
||||||
{ accountType: 'HEADQUARTERS', name: '总部账户' },
|
{ accountType: 'HEADQUARTERS', baseType: 'HEADQUARTERS', name: '总部账户' },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
|
|
@ -69,31 +90,41 @@ export class SystemAccountRepository {
|
||||||
where: { accountType: account.accountType },
|
where: { accountType: account.accountType },
|
||||||
create: {
|
create: {
|
||||||
accountType: account.accountType,
|
accountType: account.accountType,
|
||||||
|
baseType: account.baseType,
|
||||||
|
regionCode: null,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
contributionBalance: 0,
|
contributionBalance: 0,
|
||||||
|
contributionNeverExpires: true,
|
||||||
},
|
},
|
||||||
update: {},
|
update: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加算力到系统账户(动态创建账户)
|
||||||
|
* @param accountType 组合键,如 PROVINCE_440000, CITY_440100
|
||||||
|
* @param baseType 基础类型: OPERATION, PROVINCE, CITY, HEADQUARTERS
|
||||||
|
* @param regionCode 区域代码: 省/市代码,如 440000, 440100
|
||||||
|
* @param amount 算力金额
|
||||||
|
*/
|
||||||
async addContribution(
|
async addContribution(
|
||||||
accountType: SystemAccountType,
|
accountType: string,
|
||||||
|
baseType: SystemAccountBaseType,
|
||||||
|
regionCode: string | null,
|
||||||
amount: ContributionAmount,
|
amount: ContributionAmount,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const accountNames: Record<SystemAccountType, string> = {
|
const name = this.getAccountName(baseType, regionCode);
|
||||||
OPERATION: '运营账户',
|
|
||||||
PROVINCE: '省公司账户',
|
|
||||||
CITY: '市公司账户',
|
|
||||||
HEADQUARTERS: '总部账户',
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.client.systemAccount.upsert({
|
await this.client.systemAccount.upsert({
|
||||||
where: { accountType },
|
where: { accountType },
|
||||||
create: {
|
create: {
|
||||||
accountType,
|
accountType,
|
||||||
name: accountNames[accountType],
|
baseType,
|
||||||
|
regionCode,
|
||||||
|
name,
|
||||||
contributionBalance: amount.value,
|
contributionBalance: amount.value,
|
||||||
|
contributionNeverExpires: true,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
contributionBalance: { increment: amount.value },
|
contributionBalance: { increment: amount.value },
|
||||||
|
|
@ -101,21 +132,37 @@ export class SystemAccountRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成账户名称
|
||||||
|
*/
|
||||||
|
private getAccountName(baseType: SystemAccountBaseType, regionCode: string | null): string {
|
||||||
|
if (!regionCode) {
|
||||||
|
const names: Record<SystemAccountBaseType, string> = {
|
||||||
|
OPERATION: '运营账户',
|
||||||
|
PROVINCE: '省公司账户',
|
||||||
|
CITY: '市公司账户',
|
||||||
|
HEADQUARTERS: '总部账户',
|
||||||
|
};
|
||||||
|
return names[baseType] || baseType;
|
||||||
|
}
|
||||||
|
return `${regionCode}账户`;
|
||||||
|
}
|
||||||
|
|
||||||
async saveContributionRecord(record: {
|
async saveContributionRecord(record: {
|
||||||
systemAccountType: SystemAccountType;
|
systemAccountType: string; // 改为 string 支持组合键
|
||||||
sourceAdoptionId: bigint;
|
sourceAdoptionId: bigint;
|
||||||
sourceAccountSequence: string;
|
sourceAccountSequence: string;
|
||||||
distributionRate: number;
|
distributionRate: number;
|
||||||
amount: ContributionAmount;
|
amount: ContributionAmount;
|
||||||
effectiveDate: Date;
|
effectiveDate: Date;
|
||||||
expireDate?: Date | null;
|
expireDate?: Date | null;
|
||||||
}): Promise<void> {
|
}): Promise<SystemContributionRecord> {
|
||||||
const systemAccount = await this.findByType(record.systemAccountType);
|
const systemAccount = await this.findByType(record.systemAccountType);
|
||||||
if (!systemAccount) {
|
if (!systemAccount) {
|
||||||
throw new Error(`System account ${record.systemAccountType} not found`);
|
throw new Error(`System account ${record.systemAccountType} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.client.systemContributionRecord.create({
|
const created = await this.client.systemContributionRecord.create({
|
||||||
data: {
|
data: {
|
||||||
systemAccountId: systemAccount.id,
|
systemAccountId: systemAccount.id,
|
||||||
sourceAdoptionId: record.sourceAdoptionId,
|
sourceAdoptionId: record.sourceAdoptionId,
|
||||||
|
|
@ -126,10 +173,12 @@ export class SystemAccountRepository {
|
||||||
expireDate: record.expireDate ?? null,
|
expireDate: record.expireDate ?? null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return this.toContributionRecord(created);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveContributionRecords(records: {
|
async saveContributionRecords(records: {
|
||||||
systemAccountType: SystemAccountType;
|
systemAccountType: string; // 改为 string 支持组合键
|
||||||
sourceAdoptionId: bigint;
|
sourceAdoptionId: bigint;
|
||||||
sourceAccountSequence: string;
|
sourceAccountSequence: string;
|
||||||
distributionRate: number;
|
distributionRate: number;
|
||||||
|
|
@ -140,7 +189,7 @@ export class SystemAccountRepository {
|
||||||
if (records.length === 0) return;
|
if (records.length === 0) return;
|
||||||
|
|
||||||
const systemAccounts = await this.findAll();
|
const systemAccounts = await this.findAll();
|
||||||
const accountMap = new Map<SystemAccountType, bigint>();
|
const accountMap = new Map<string, bigint>();
|
||||||
for (const account of systemAccounts) {
|
for (const account of systemAccounts) {
|
||||||
accountMap.set(account.accountType, account.id);
|
accountMap.set(account.accountType, account.id);
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +208,7 @@ export class SystemAccountRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async findContributionRecords(
|
async findContributionRecords(
|
||||||
systemAccountType: SystemAccountType,
|
systemAccountType: string, // 改为 string 支持组合键
|
||||||
page: number,
|
page: number,
|
||||||
pageSize: number,
|
pageSize: number,
|
||||||
): Promise<{ data: SystemContributionRecord[]; total: number }> {
|
): Promise<{ data: SystemContributionRecord[]; total: number }> {
|
||||||
|
|
@ -189,7 +238,9 @@ export class SystemAccountRepository {
|
||||||
private toSystemAccount(record: any): SystemAccount {
|
private toSystemAccount(record: any): SystemAccount {
|
||||||
return {
|
return {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
accountType: record.accountType as SystemAccountType,
|
accountType: record.accountType,
|
||||||
|
baseType: record.baseType as SystemAccountBaseType,
|
||||||
|
regionCode: record.regionCode,
|
||||||
name: record.name,
|
name: record.name,
|
||||||
contributionBalance: new ContributionAmount(record.contributionBalance),
|
contributionBalance: new ContributionAmount(record.contributionBalance),
|
||||||
contributionNeverExpires: record.contributionNeverExpires,
|
contributionNeverExpires: record.contributionNeverExpires,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
-- 同步的系统账户算力表按省市细分: 添加 base_type 和 region_code 字段
|
||||||
|
-- 支持组合键(如 PROVINCE_440000, CITY_440100)
|
||||||
|
|
||||||
|
-- 1. 添加新字段
|
||||||
|
ALTER TABLE "synced_system_contributions" ADD COLUMN "base_type" VARCHAR(20) DEFAULT '';
|
||||||
|
ALTER TABLE "synced_system_contributions" ADD COLUMN "region_code" VARCHAR(10);
|
||||||
|
|
||||||
|
-- 2. 设置现有数据的 base_type(根据 account_type 提取)
|
||||||
|
UPDATE "synced_system_contributions"
|
||||||
|
SET "base_type" = CASE
|
||||||
|
WHEN "account_type" LIKE 'PROVINCE_%' THEN 'PROVINCE'
|
||||||
|
WHEN "account_type" LIKE 'CITY_%' THEN 'CITY'
|
||||||
|
ELSE "account_type"
|
||||||
|
END
|
||||||
|
WHERE "base_type" = '' OR "base_type" IS NULL;
|
||||||
|
|
||||||
|
-- 3. 将 base_type 设置为非空
|
||||||
|
ALTER TABLE "synced_system_contributions" ALTER COLUMN "base_type" SET NOT NULL;
|
||||||
|
|
||||||
|
-- 4. 创建索引(Prisma 默认命名格式: {table}_{field}_idx)
|
||||||
|
CREATE INDEX IF NOT EXISTS "synced_system_contributions_base_type_idx" ON "synced_system_contributions"("base_type");
|
||||||
|
CREATE INDEX IF NOT EXISTS "synced_system_contributions_region_code_idx" ON "synced_system_contributions"("region_code");
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
-- 添加系统账户算力明细同步表
|
||||||
|
-- 用于存储从 contribution-service 同步的系统账户算力来源明细
|
||||||
|
|
||||||
|
-- 1. 创建 synced_system_contribution_records 表
|
||||||
|
CREATE TABLE IF NOT EXISTS "synced_system_contribution_records" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"original_record_id" BIGINT NOT NULL,
|
||||||
|
"system_account_type" TEXT NOT NULL,
|
||||||
|
"source_adoption_id" BIGINT NOT NULL,
|
||||||
|
"source_account_sequence" TEXT NOT NULL,
|
||||||
|
"distribution_rate" DECIMAL(10,6) NOT NULL,
|
||||||
|
"amount" DECIMAL(30,10) NOT NULL,
|
||||||
|
"effective_date" DATE NOT NULL,
|
||||||
|
"expire_date" DATE,
|
||||||
|
"is_expired" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "synced_system_contribution_records_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 创建唯一索引(原始记录ID唯一)
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "synced_system_contribution_records_original_record_id_key" ON "synced_system_contribution_records"("original_record_id");
|
||||||
|
|
||||||
|
-- 3. 创建查询索引
|
||||||
|
CREATE INDEX IF NOT EXISTS "synced_system_contribution_records_system_account_type_idx" ON "synced_system_contribution_records"("system_account_type");
|
||||||
|
CREATE INDEX IF NOT EXISTS "synced_system_contribution_records_source_adoption_id_idx" ON "synced_system_contribution_records"("source_adoption_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "synced_system_contribution_records_source_account_sequence_idx" ON "synced_system_contribution_records"("source_account_sequence");
|
||||||
|
CREATE INDEX IF NOT EXISTS "synced_system_contribution_records_created_at_idx" ON "synced_system_contribution_records"("created_at" DESC);
|
||||||
|
|
@ -422,16 +422,59 @@ model SyncedCirculationPool {
|
||||||
|
|
||||||
model SyncedSystemContribution {
|
model SyncedSystemContribution {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
accountType String @unique // OPERATION, PROVINCE, CITY, HEADQUARTERS
|
accountType String @unique // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等
|
||||||
|
baseType String @default("") @map("base_type") // 基础类型: OPERATION / PROVINCE / CITY / HEADQUARTERS
|
||||||
|
regionCode String? @map("region_code") // 区域代码: 省/市代码,如 440000, 440100
|
||||||
name String
|
name String
|
||||||
contributionBalance Decimal @db.Decimal(30, 8) @default(0)
|
contributionBalance Decimal @db.Decimal(30, 8) @default(0)
|
||||||
contributionNeverExpires Boolean @default(false)
|
contributionNeverExpires Boolean @default(false)
|
||||||
syncedAt DateTime @default(now())
|
syncedAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// 关联算力明细记录
|
||||||
|
records SyncedSystemContributionRecord[]
|
||||||
|
|
||||||
|
@@index([baseType])
|
||||||
|
@@index([regionCode])
|
||||||
@@map("synced_system_contributions")
|
@@map("synced_system_contributions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CDC 同步表 - 系统账户算力明细 (from contribution-service)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
model SyncedSystemContributionRecord {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
originalRecordId BigInt @unique @map("original_record_id") // contribution-service 中的原始 ID
|
||||||
|
systemAccountType String @map("system_account_type") // 关联的系统账户类型 (组合键)
|
||||||
|
|
||||||
|
// 来源信息
|
||||||
|
sourceAdoptionId BigInt @map("source_adoption_id") // 来源认种ID
|
||||||
|
sourceAccountSequence String @map("source_account_sequence") // 认种人账号
|
||||||
|
|
||||||
|
// 分配参数
|
||||||
|
distributionRate Decimal @map("distribution_rate") @db.Decimal(10, 6) // 分配比例
|
||||||
|
amount Decimal @map("amount") @db.Decimal(30, 10) // 算力金额
|
||||||
|
|
||||||
|
// 有效期
|
||||||
|
effectiveDate DateTime @map("effective_date") @db.Date // 生效日期
|
||||||
|
expireDate DateTime? @map("expire_date") @db.Date // 过期日期(系统账户一般为null,永不过期)
|
||||||
|
isExpired Boolean @default(false) @map("is_expired")
|
||||||
|
|
||||||
|
createdAt DateTime @map("created_at") // 原始记录创建时间
|
||||||
|
syncedAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// 关联系统账户
|
||||||
|
systemContribution SyncedSystemContribution @relation(fields: [systemAccountType], references: [accountType])
|
||||||
|
|
||||||
|
@@index([systemAccountType])
|
||||||
|
@@index([sourceAdoptionId])
|
||||||
|
@@index([sourceAccountSequence])
|
||||||
|
@@index([createdAt(sort: Desc)])
|
||||||
|
@@map("synced_system_contribution_records")
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// CDC 同步进度跟踪
|
// CDC 同步进度跟踪
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -53,4 +53,44 @@ export class SystemAccountsController {
|
||||||
pageSize ?? 20,
|
pageSize ?? 20,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':accountType/contributions')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取系统账户算力来源明细',
|
||||||
|
description: '显示该账户的每笔算力来自哪个认种订单,支持按省市细分的账户类型(如 CITY_440100, PROVINCE_440000)',
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'accountType',
|
||||||
|
type: String,
|
||||||
|
description: '系统账户类型(组合键),如 OPERATION, PROVINCE_440000, CITY_440100',
|
||||||
|
})
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number, description: '页码,默认1' })
|
||||||
|
@ApiQuery({ name: 'pageSize', required: false, type: Number, description: '每页数量,默认20' })
|
||||||
|
async getSystemAccountContributionRecords(
|
||||||
|
@Param('accountType') accountType: string,
|
||||||
|
@Query('page') page?: number,
|
||||||
|
@Query('pageSize') pageSize?: number,
|
||||||
|
) {
|
||||||
|
return this.systemAccountsService.getSystemAccountContributionRecords(
|
||||||
|
accountType,
|
||||||
|
page ?? 1,
|
||||||
|
pageSize ?? 20,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':accountType/contribution-stats')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取系统账户算力明细统计',
|
||||||
|
description: '显示算力来源的汇总信息,包括记录数、来源认种订单数、来源用户数等',
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'accountType',
|
||||||
|
type: String,
|
||||||
|
description: '系统账户类型(组合键),如 OPERATION, PROVINCE_440000, CITY_440100',
|
||||||
|
})
|
||||||
|
async getSystemAccountContributionStats(
|
||||||
|
@Param('accountType') accountType: string,
|
||||||
|
) {
|
||||||
|
return this.systemAccountsService.getSystemAccountContributionStats(accountType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ import { firstValueFrom } from 'rxjs';
|
||||||
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
||||||
|
|
||||||
interface MiningServiceSystemAccount {
|
interface MiningServiceSystemAccount {
|
||||||
accountType: string;
|
accountType: string; // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等
|
||||||
|
baseType: string; // 基础类型: OPERATION / PROVINCE / CITY / HEADQUARTERS
|
||||||
|
regionCode: string | null; // 区域代码: 省/市代码,如 440000, 440100
|
||||||
name: string;
|
name: string;
|
||||||
totalMined: string;
|
totalMined: string;
|
||||||
availableBalance: string;
|
availableBalance: string;
|
||||||
|
|
@ -61,6 +63,11 @@ export class SystemAccountsService {
|
||||||
/**
|
/**
|
||||||
* 获取系统账户列表
|
* 获取系统账户列表
|
||||||
* 从 CDC 同步的钱包系统账户表读取数据,并合并挖矿数据
|
* 从 CDC 同步的钱包系统账户表读取数据,并合并挖矿数据
|
||||||
|
*
|
||||||
|
* 数据匹配逻辑:
|
||||||
|
* 1. 钱包账户的 code 格式为 "CITY-440100"、"PROVINCE-440000" 等
|
||||||
|
* 2. 算力/挖矿账户的 accountType 格式为 "CITY_440100"、"PROVINCE_440000" 等
|
||||||
|
* 3. 需要从 code 提取区域代码,然后构建组合键进行匹配
|
||||||
*/
|
*/
|
||||||
async getSystemAccounts() {
|
async getSystemAccounts() {
|
||||||
// 从 CDC 同步的 SyncedWalletSystemAccount 表获取数据
|
// 从 CDC 同步的 SyncedWalletSystemAccount 表获取数据
|
||||||
|
|
@ -75,7 +82,9 @@ export class SystemAccountsService {
|
||||||
// 从 mining-service 获取挖矿数据
|
// 从 mining-service 获取挖矿数据
|
||||||
const miningDataMap = await this.fetchMiningServiceSystemAccounts();
|
const miningDataMap = await this.fetchMiningServiceSystemAccounts();
|
||||||
|
|
||||||
// 构建算力数据映射
|
// 构建算力数据映射 - 支持两种匹配方式
|
||||||
|
// 1. 直接用 accountType 匹配(如 OPERATION, HEADQUARTERS)
|
||||||
|
// 2. 用组合键匹配(如 CITY_440100, PROVINCE_440000)
|
||||||
const contributionMap = new Map<string, any>();
|
const contributionMap = new Map<string, any>();
|
||||||
for (const contrib of syncedContributions) {
|
for (const contrib of syncedContributions) {
|
||||||
contributionMap.set(contrib.accountType, contrib);
|
contributionMap.set(contrib.accountType, contrib);
|
||||||
|
|
@ -83,8 +92,29 @@ export class SystemAccountsService {
|
||||||
|
|
||||||
// 构建返回数据
|
// 构建返回数据
|
||||||
const accounts = syncedAccounts.map((account) => {
|
const accounts = syncedAccounts.map((account) => {
|
||||||
const contrib = contributionMap.get(account.accountType);
|
// 尝试匹配算力数据
|
||||||
const miningData = miningDataMap.get(account.accountType);
|
let contrib = null;
|
||||||
|
let miningData = null;
|
||||||
|
|
||||||
|
// 1. 尝试用 regionCode 匹配(针对省市账户)
|
||||||
|
if (account.code) {
|
||||||
|
// 从 code 提取区域代码(如 "CITY-440100" -> "440100")
|
||||||
|
const regionCode = this.extractRegionCodeFromCode(account.code);
|
||||||
|
if (regionCode) {
|
||||||
|
// 构建组合键(如 CITY_440100)
|
||||||
|
const accountTypeKey = `${account.accountType}_${regionCode}`;
|
||||||
|
contrib = contributionMap.get(accountTypeKey);
|
||||||
|
miningData = miningDataMap.get(accountTypeKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 回退到直接 accountType 匹配(汇总账户)
|
||||||
|
if (!contrib) {
|
||||||
|
contrib = contributionMap.get(account.accountType);
|
||||||
|
}
|
||||||
|
if (!miningData) {
|
||||||
|
miningData = miningDataMap.get(account.accountType);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: account.originalId,
|
id: account.originalId,
|
||||||
|
|
@ -120,6 +150,18 @@ export class SystemAccountsService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从钱包账户的 code 提取区域代码
|
||||||
|
* 例如: "CITY-440100" -> "440100", "PROVINCE-440000" -> "440000"
|
||||||
|
* 对于非区域账户(如 "HEADQUARTERS")返回 null
|
||||||
|
*/
|
||||||
|
private extractRegionCodeFromCode(code: string): string | null {
|
||||||
|
if (!code) return null;
|
||||||
|
// 匹配 CITY-XXXXXX, PROVINCE-XXXXXX, PROV-XXXXXX 格式
|
||||||
|
const match = code.match(/^(?:CITY|PROVINCE|PROV)-(\d+)$/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取系统账户汇总
|
* 获取系统账户汇总
|
||||||
*/
|
*/
|
||||||
|
|
@ -260,4 +302,102 @@ export class SystemAccountsService {
|
||||||
return { transactions: [], total: 0, page, pageSize };
|
return { transactions: [], total: 0, page, pageSize };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统账户算力来源明细
|
||||||
|
* 显示该账户的每笔算力来自哪个认种订单
|
||||||
|
*
|
||||||
|
* @param accountType 系统账户类型(组合键),如 CITY_440100, PROVINCE_440000, OPERATION
|
||||||
|
* @param page 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
*/
|
||||||
|
async getSystemAccountContributionRecords(
|
||||||
|
accountType: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20,
|
||||||
|
) {
|
||||||
|
const [records, total] = await Promise.all([
|
||||||
|
this.prisma.syncedSystemContributionRecord.findMany({
|
||||||
|
where: { systemAccountType: accountType },
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
}),
|
||||||
|
this.prisma.syncedSystemContributionRecord.count({
|
||||||
|
where: { systemAccountType: accountType },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
records: records.map((record) => ({
|
||||||
|
id: record.id,
|
||||||
|
originalRecordId: record.originalRecordId.toString(),
|
||||||
|
systemAccountType: record.systemAccountType,
|
||||||
|
sourceAdoptionId: record.sourceAdoptionId.toString(),
|
||||||
|
sourceAccountSequence: record.sourceAccountSequence,
|
||||||
|
distributionRate: record.distributionRate.toString(),
|
||||||
|
amount: record.amount.toString(),
|
||||||
|
effectiveDate: record.effectiveDate,
|
||||||
|
expireDate: record.expireDate,
|
||||||
|
isExpired: record.isExpired,
|
||||||
|
createdAt: record.createdAt,
|
||||||
|
syncedAt: record.syncedAt,
|
||||||
|
})),
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages: Math.ceil(total / pageSize),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统账户算力明细统计
|
||||||
|
* 用于显示算力来源的汇总信息
|
||||||
|
*/
|
||||||
|
async getSystemAccountContributionStats(accountType: string) {
|
||||||
|
// 获取算力账户信息
|
||||||
|
const contribution = await this.prisma.syncedSystemContribution.findUnique({
|
||||||
|
where: { accountType },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取明细记录统计
|
||||||
|
const recordStats = await this.prisma.syncedSystemContributionRecord.aggregate({
|
||||||
|
where: { systemAccountType: accountType },
|
||||||
|
_count: true,
|
||||||
|
_sum: { amount: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取来源认种订单数量(去重)
|
||||||
|
const uniqueAdoptions = await this.prisma.syncedSystemContributionRecord.groupBy({
|
||||||
|
by: ['sourceAdoptionId'],
|
||||||
|
where: { systemAccountType: accountType },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取来源用户数量(去重)
|
||||||
|
const uniqueUsers = await this.prisma.syncedSystemContributionRecord.groupBy({
|
||||||
|
by: ['sourceAccountSequence'],
|
||||||
|
where: { systemAccountType: accountType },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accountType,
|
||||||
|
name: contribution?.name || accountType,
|
||||||
|
baseType: contribution?.baseType || this.extractBaseTypeFromAccountType(accountType),
|
||||||
|
regionCode: contribution?.regionCode || null,
|
||||||
|
totalContribution: contribution?.contributionBalance?.toString() || '0',
|
||||||
|
recordCount: recordStats._count,
|
||||||
|
sumFromRecords: recordStats._sum?.amount?.toString() || '0',
|
||||||
|
uniqueAdoptionCount: uniqueAdoptions.length,
|
||||||
|
uniqueUserCount: uniqueUsers.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 accountType 提取 baseType
|
||||||
|
*/
|
||||||
|
private extractBaseTypeFromAccountType(accountType: string): string {
|
||||||
|
if (accountType.startsWith('PROVINCE_')) return 'PROVINCE';
|
||||||
|
if (accountType.startsWith('CITY_')) return 'CITY';
|
||||||
|
return accountType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,11 @@ export class CdcSyncService implements OnModuleInit {
|
||||||
'SystemAccountSynced',
|
'SystemAccountSynced',
|
||||||
this.withIdempotency(this.handleSystemAccountSynced.bind(this)),
|
this.withIdempotency(this.handleSystemAccountSynced.bind(this)),
|
||||||
);
|
);
|
||||||
|
// SystemContributionRecordCreated 事件 - 同步系统账户算力明细(来自 contribution-service)
|
||||||
|
this.cdcConsumer.registerServiceHandler(
|
||||||
|
'SystemContributionRecordCreated',
|
||||||
|
this.withIdempotency(this.handleSystemContributionRecordCreated.bind(this)),
|
||||||
|
);
|
||||||
// ReferralSynced 事件 - 同步推荐关系
|
// ReferralSynced 事件 - 同步推荐关系
|
||||||
this.cdcConsumer.registerServiceHandler(
|
this.cdcConsumer.registerServiceHandler(
|
||||||
'ReferralSynced',
|
'ReferralSynced',
|
||||||
|
|
@ -554,24 +559,81 @@ export class CdcSyncService implements OnModuleInit {
|
||||||
/**
|
/**
|
||||||
* 处理 SystemAccountSynced 事件 - 同步系统账户算力
|
* 处理 SystemAccountSynced 事件 - 同步系统账户算力
|
||||||
* 来自 contribution-service 的系统账户(运营、省、市、总部)算力同步
|
* 来自 contribution-service 的系统账户(运营、省、市、总部)算力同步
|
||||||
|
* 支持组合键账户类型(如 PROVINCE_440000, CITY_440100)
|
||||||
*/
|
*/
|
||||||
private async handleSystemAccountSynced(event: ServiceEvent, tx: TransactionClient): Promise<void> {
|
private async handleSystemAccountSynced(event: ServiceEvent, tx: TransactionClient): Promise<void> {
|
||||||
const { payload } = event;
|
const { payload } = event;
|
||||||
|
// 从 accountType 提取 baseType(向后兼容)
|
||||||
|
const baseType = payload.baseType || this.extractBaseType(payload.accountType);
|
||||||
|
const regionCode = payload.regionCode || null;
|
||||||
|
|
||||||
await tx.syncedSystemContribution.upsert({
|
await tx.syncedSystemContribution.upsert({
|
||||||
where: { accountType: payload.accountType },
|
where: { accountType: payload.accountType },
|
||||||
create: {
|
create: {
|
||||||
accountType: payload.accountType,
|
accountType: payload.accountType,
|
||||||
|
baseType,
|
||||||
|
regionCode,
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
contributionBalance: payload.contributionBalance || 0,
|
contributionBalance: payload.contributionBalance || 0,
|
||||||
contributionNeverExpires: true, // 系统账户算力永不过期
|
contributionNeverExpires: true, // 系统账户算力永不过期
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
|
baseType,
|
||||||
|
regionCode,
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
contributionBalance: payload.contributionBalance,
|
contributionBalance: payload.contributionBalance,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 accountType 提取基础类型(向后兼容)
|
||||||
|
* 例如: PROVINCE_440000 -> PROVINCE, CITY_440100 -> CITY
|
||||||
|
*/
|
||||||
|
private extractBaseType(accountType: string): string {
|
||||||
|
if (accountType.startsWith('PROVINCE_')) return 'PROVINCE';
|
||||||
|
if (accountType.startsWith('CITY_')) return 'CITY';
|
||||||
|
// 如果没有下划线,则 accountType 本身就是基础类型
|
||||||
|
return accountType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 SystemContributionRecordCreated 事件 - 同步系统账户算力明细
|
||||||
|
* 来自 contribution-service,记录每笔算力的来源信息
|
||||||
|
*/
|
||||||
|
private async handleSystemContributionRecordCreated(event: ServiceEvent, tx: TransactionClient): Promise<void> {
|
||||||
|
const { payload } = event;
|
||||||
|
|
||||||
|
await tx.syncedSystemContributionRecord.upsert({
|
||||||
|
where: { originalRecordId: BigInt(payload.recordId) },
|
||||||
|
create: {
|
||||||
|
originalRecordId: BigInt(payload.recordId),
|
||||||
|
systemAccountType: payload.systemAccountType,
|
||||||
|
sourceAdoptionId: BigInt(payload.sourceAdoptionId),
|
||||||
|
sourceAccountSequence: payload.sourceAccountSequence,
|
||||||
|
distributionRate: payload.distributionRate,
|
||||||
|
amount: payload.amount,
|
||||||
|
effectiveDate: new Date(payload.effectiveDate),
|
||||||
|
expireDate: payload.expireDate ? new Date(payload.expireDate) : null,
|
||||||
|
isExpired: false,
|
||||||
|
createdAt: new Date(payload.createdAt),
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
systemAccountType: payload.systemAccountType,
|
||||||
|
sourceAdoptionId: BigInt(payload.sourceAdoptionId),
|
||||||
|
sourceAccountSequence: payload.sourceAccountSequence,
|
||||||
|
distributionRate: payload.distributionRate,
|
||||||
|
amount: payload.amount,
|
||||||
|
effectiveDate: new Date(payload.effectiveDate),
|
||||||
|
expireDate: payload.expireDate ? new Date(payload.expireDate) : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Synced system contribution record: recordId=${payload.recordId}, account=${payload.systemAccountType}, amount=${payload.amount}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理 ReferralSynced 事件 - 同步推荐关系
|
* 处理 ReferralSynced 事件 - 同步推荐关系
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
-- 系统挖矿账户按省市细分: 移除枚举类型,改用字符串组合键
|
||||||
|
-- 将 accountType 从枚举改为字符串(如 PROVINCE_440000, CITY_440100)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 步骤 1: 删除现有外键约束(如果存在)
|
||||||
|
-- ============================================================
|
||||||
|
ALTER TABLE "system_mining_records" DROP CONSTRAINT IF EXISTS "system_mining_records_account_type_fkey";
|
||||||
|
ALTER TABLE "system_mining_transactions" DROP CONSTRAINT IF EXISTS "system_mining_transactions_account_type_fkey";
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 步骤 2: 修改 system_mining_accounts 主表
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 2.1 添加新列
|
||||||
|
ALTER TABLE "system_mining_accounts" ADD COLUMN "account_type_new" VARCHAR(50);
|
||||||
|
ALTER TABLE "system_mining_accounts" ADD COLUMN "base_type" VARCHAR(20);
|
||||||
|
ALTER TABLE "system_mining_accounts" ADD COLUMN "region_code" VARCHAR(10);
|
||||||
|
|
||||||
|
-- 2.2 迁移数据:将枚举值转为字符串
|
||||||
|
UPDATE "system_mining_accounts"
|
||||||
|
SET "account_type_new" = "account_type"::TEXT,
|
||||||
|
"base_type" = "account_type"::TEXT
|
||||||
|
WHERE "account_type_new" IS NULL;
|
||||||
|
|
||||||
|
-- 2.3 删除旧的唯一约束和列
|
||||||
|
DROP INDEX IF EXISTS "system_mining_accounts_account_type_key";
|
||||||
|
ALTER TABLE "system_mining_accounts" DROP COLUMN "account_type";
|
||||||
|
|
||||||
|
-- 2.4 重命名新列
|
||||||
|
ALTER TABLE "system_mining_accounts" RENAME COLUMN "account_type_new" TO "account_type";
|
||||||
|
|
||||||
|
-- 2.5 添加非空约束和唯一约束
|
||||||
|
ALTER TABLE "system_mining_accounts" ALTER COLUMN "account_type" SET NOT NULL;
|
||||||
|
ALTER TABLE "system_mining_accounts" ALTER COLUMN "base_type" SET NOT NULL;
|
||||||
|
CREATE UNIQUE INDEX "system_mining_accounts_account_type_key" ON "system_mining_accounts"("account_type");
|
||||||
|
|
||||||
|
-- 2.6 创建索引(Prisma 默认命名格式: {table}_{field}_idx)
|
||||||
|
CREATE INDEX IF NOT EXISTS "system_mining_accounts_base_type_idx" ON "system_mining_accounts"("base_type");
|
||||||
|
CREATE INDEX IF NOT EXISTS "system_mining_accounts_region_code_idx" ON "system_mining_accounts"("region_code");
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 步骤 3: 修改 system_mining_records 表
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 3.1 添加新列并迁移数据
|
||||||
|
ALTER TABLE "system_mining_records" ADD COLUMN "account_type_new" VARCHAR(50);
|
||||||
|
UPDATE "system_mining_records" SET "account_type_new" = "account_type"::TEXT;
|
||||||
|
|
||||||
|
-- 3.2 删除旧的唯一索引和列
|
||||||
|
DROP INDEX IF EXISTS "system_mining_records_account_type_mining_minute_key";
|
||||||
|
ALTER TABLE "system_mining_records" DROP COLUMN "account_type";
|
||||||
|
|
||||||
|
-- 3.3 重命名新列并添加约束
|
||||||
|
ALTER TABLE "system_mining_records" RENAME COLUMN "account_type_new" TO "account_type";
|
||||||
|
ALTER TABLE "system_mining_records" ALTER COLUMN "account_type" SET NOT NULL;
|
||||||
|
|
||||||
|
-- 3.4 重建复合唯一索引
|
||||||
|
CREATE UNIQUE INDEX "system_mining_records_account_type_mining_minute_key" ON "system_mining_records"("account_type", "mining_minute");
|
||||||
|
|
||||||
|
-- 3.5 重建外键约束
|
||||||
|
ALTER TABLE "system_mining_records"
|
||||||
|
ADD CONSTRAINT "system_mining_records_account_type_fkey"
|
||||||
|
FOREIGN KEY ("account_type") REFERENCES "system_mining_accounts"("account_type")
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 步骤 4: 修改 system_mining_transactions 表
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 4.1 添加新列并迁移数据
|
||||||
|
ALTER TABLE "system_mining_transactions" ADD COLUMN "account_type_new" VARCHAR(50);
|
||||||
|
UPDATE "system_mining_transactions" SET "account_type_new" = "account_type"::TEXT;
|
||||||
|
|
||||||
|
-- 4.2 删除旧列
|
||||||
|
ALTER TABLE "system_mining_transactions" DROP COLUMN "account_type";
|
||||||
|
|
||||||
|
-- 4.3 重命名新列并添加约束
|
||||||
|
ALTER TABLE "system_mining_transactions" RENAME COLUMN "account_type_new" TO "account_type";
|
||||||
|
ALTER TABLE "system_mining_transactions" ALTER COLUMN "account_type" SET NOT NULL;
|
||||||
|
|
||||||
|
-- 4.4 重建索引(Prisma 默认命名格式: {table}_{field(s)}_idx)
|
||||||
|
CREATE INDEX IF NOT EXISTS "system_mining_transactions_account_type_createdAt_idx" ON "system_mining_transactions"("account_type", "created_at" DESC);
|
||||||
|
|
||||||
|
-- 4.5 重建外键约束
|
||||||
|
ALTER TABLE "system_mining_transactions"
|
||||||
|
ADD CONSTRAINT "system_mining_transactions_account_type_fkey"
|
||||||
|
FOREIGN KEY ("account_type") REFERENCES "system_mining_accounts"("account_type")
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 步骤 5: 清理旧的枚举类型(如果不再使用)
|
||||||
|
-- ============================================================
|
||||||
|
-- DROP TYPE IF EXISTS "SystemAccountType";
|
||||||
|
|
@ -52,43 +52,41 @@ model MiningEra {
|
||||||
|
|
||||||
// ==================== 系统账户(运营/省/市/总部)====================
|
// ==================== 系统账户(运营/省/市/总部)====================
|
||||||
|
|
||||||
// 系统账户类型枚举
|
|
||||||
enum SystemAccountType {
|
|
||||||
OPERATION // 运营账户 12%
|
|
||||||
PROVINCE // 省公司账户 1%
|
|
||||||
CITY // 市公司账户 2%
|
|
||||||
HEADQUARTERS // 总部账户(收取未解锁的收益)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 系统挖矿账户
|
// 系统挖矿账户
|
||||||
|
// accountType 格式: OPERATION, PROVINCE, CITY, HEADQUARTERS (汇总账户)
|
||||||
|
// PROVINCE_440000, CITY_440100 等 (按省市细分的账户)
|
||||||
model SystemMiningAccount {
|
model SystemMiningAccount {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
accountType SystemAccountType @unique @map("account_type")
|
accountType String @unique @map("account_type") @db.VarChar(50) // 组合键
|
||||||
name String @db.VarChar(100)
|
baseType String @map("base_type") @db.VarChar(20) // 基础类型: OPERATION/PROVINCE/CITY/HEADQUARTERS
|
||||||
totalMined Decimal @default(0) @db.Decimal(30, 8) // 总挖到的积分股
|
regionCode String? @map("region_code") @db.VarChar(10) // 区域代码
|
||||||
availableBalance Decimal @default(0) @db.Decimal(30, 8) // 可用余额
|
name String @db.VarChar(100)
|
||||||
totalContribution Decimal @default(0) @db.Decimal(30, 8) // 当前算力(从 contribution-service 同步)
|
totalMined Decimal @default(0) @db.Decimal(30, 8) // 总挖到的积分股
|
||||||
lastSyncedAt DateTime? @map("last_synced_at")
|
availableBalance Decimal @default(0) @db.Decimal(30, 8) // 可用余额
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
totalContribution Decimal @default(0) @db.Decimal(30, 8) // 当前算力(从 contribution-service 同步)
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
lastSyncedAt DateTime? @map("last_synced_at")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
records SystemMiningRecord[]
|
records SystemMiningRecord[]
|
||||||
transactions SystemMiningTransaction[]
|
transactions SystemMiningTransaction[]
|
||||||
|
|
||||||
|
@@index([baseType])
|
||||||
|
@@index([regionCode])
|
||||||
@@index([totalContribution(sort: Desc)])
|
@@index([totalContribution(sort: Desc)])
|
||||||
@@map("system_mining_accounts")
|
@@map("system_mining_accounts")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 系统账户挖矿记录(分钟级别汇总)
|
// 系统账户挖矿记录(分钟级别汇总)
|
||||||
model SystemMiningRecord {
|
model SystemMiningRecord {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
accountType SystemAccountType @map("account_type")
|
accountType String @map("account_type") @db.VarChar(50) // 组合键
|
||||||
miningMinute DateTime @map("mining_minute")
|
miningMinute DateTime @map("mining_minute")
|
||||||
contributionRatio Decimal @db.Decimal(30, 18) @map("contribution_ratio")
|
contributionRatio Decimal @db.Decimal(30, 18) @map("contribution_ratio")
|
||||||
totalContribution Decimal @db.Decimal(30, 8) @map("total_contribution")
|
totalContribution Decimal @db.Decimal(30, 8) @map("total_contribution")
|
||||||
secondDistribution Decimal @db.Decimal(30, 18) @map("second_distribution")
|
secondDistribution Decimal @db.Decimal(30, 18) @map("second_distribution")
|
||||||
minedAmount Decimal @db.Decimal(30, 18) @map("mined_amount")
|
minedAmount Decimal @db.Decimal(30, 18) @map("mined_amount")
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
account SystemMiningAccount @relation(fields: [accountType], references: [accountType])
|
account SystemMiningAccount @relation(fields: [accountType], references: [accountType])
|
||||||
|
|
||||||
|
|
@ -99,16 +97,16 @@ model SystemMiningRecord {
|
||||||
|
|
||||||
// 系统账户交易流水
|
// 系统账户交易流水
|
||||||
model SystemMiningTransaction {
|
model SystemMiningTransaction {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
accountType SystemAccountType @map("account_type")
|
accountType String @map("account_type") @db.VarChar(50) // 组合键
|
||||||
type String // MINE, TRANSFER_OUT, ADJUSTMENT
|
type String // MINE, TRANSFER_OUT, ADJUSTMENT
|
||||||
amount Decimal @db.Decimal(30, 8)
|
amount Decimal @db.Decimal(30, 8)
|
||||||
balanceBefore Decimal @db.Decimal(30, 8) @map("balance_before")
|
balanceBefore Decimal @db.Decimal(30, 8) @map("balance_before")
|
||||||
balanceAfter Decimal @db.Decimal(30, 8) @map("balance_after")
|
balanceAfter Decimal @db.Decimal(30, 8) @map("balance_after")
|
||||||
referenceId String? @map("reference_id")
|
referenceId String? @map("reference_id")
|
||||||
referenceType String? @map("reference_type")
|
referenceType String? @map("reference_type")
|
||||||
memo String? @db.Text
|
memo String? @db.Text
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
account SystemMiningAccount @relation(fields: [accountType], references: [accountType])
|
account SystemMiningAccount @relation(fields: [accountType], references: [accountType])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Controller, Get, Post, Body, Query, Param, HttpException, HttpStatus } from '@nestjs/common';
|
import { Controller, Get, Post, Body, Query, Param, HttpException, HttpStatus } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBody, ApiQuery, ApiParam } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBody, ApiQuery, ApiParam } from '@nestjs/swagger';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { SystemAccountType } from '@prisma/client';
|
|
||||||
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
||||||
import { NetworkSyncService } from '../../application/services/network-sync.service';
|
import { NetworkSyncService } from '../../application/services/network-sync.service';
|
||||||
import { ManualMiningService } from '../../application/services/manual-mining.service';
|
import { ManualMiningService } from '../../application/services/manual-mining.service';
|
||||||
|
|
@ -170,6 +169,8 @@ export class AdminController {
|
||||||
return {
|
return {
|
||||||
accounts: accounts.map((acc) => ({
|
accounts: accounts.map((acc) => ({
|
||||||
accountType: acc.accountType,
|
accountType: acc.accountType,
|
||||||
|
baseType: acc.baseType, // 新增:基础类型
|
||||||
|
regionCode: acc.regionCode, // 新增:区域代码
|
||||||
name: acc.name,
|
name: acc.name,
|
||||||
totalMined: acc.totalMined.toString(),
|
totalMined: acc.totalMined.toString(),
|
||||||
availableBalance: acc.availableBalance.toString(),
|
availableBalance: acc.availableBalance.toString(),
|
||||||
|
|
@ -183,7 +184,7 @@ export class AdminController {
|
||||||
@Get('system-accounts/:accountType/records')
|
@Get('system-accounts/:accountType/records')
|
||||||
@Public()
|
@Public()
|
||||||
@ApiOperation({ summary: '获取系统账户挖矿记录' })
|
@ApiOperation({ summary: '获取系统账户挖矿记录' })
|
||||||
@ApiParam({ name: 'accountType', type: String, description: '系统账户类型 (OPERATION, PROVINCE, CITY, HEADQUARTERS)' })
|
@ApiParam({ name: 'accountType', type: String, description: '系统账户类型 (OPERATION, PROVINCE_440000, CITY_440100, HEADQUARTERS 等)' })
|
||||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||||
async getSystemAccountMiningRecords(
|
async getSystemAccountMiningRecords(
|
||||||
|
|
@ -194,17 +195,17 @@ export class AdminController {
|
||||||
const pageNum = page ?? 1;
|
const pageNum = page ?? 1;
|
||||||
const pageSizeNum = pageSize ?? 20;
|
const pageSizeNum = pageSize ?? 20;
|
||||||
const skip = (pageNum - 1) * pageSizeNum;
|
const skip = (pageNum - 1) * pageSizeNum;
|
||||||
const accountTypeEnum = accountType as SystemAccountType;
|
// accountType 现在是字符串类型,支持组合键(如 PROVINCE_440000, CITY_440100)
|
||||||
|
|
||||||
const [records, total] = await Promise.all([
|
const [records, total] = await Promise.all([
|
||||||
this.prisma.systemMiningRecord.findMany({
|
this.prisma.systemMiningRecord.findMany({
|
||||||
where: { accountType: accountTypeEnum },
|
where: { accountType },
|
||||||
orderBy: { miningMinute: 'desc' },
|
orderBy: { miningMinute: 'desc' },
|
||||||
skip,
|
skip,
|
||||||
take: pageSizeNum,
|
take: pageSizeNum,
|
||||||
}),
|
}),
|
||||||
this.prisma.systemMiningRecord.count({
|
this.prisma.systemMiningRecord.count({
|
||||||
where: { accountType: accountTypeEnum },
|
where: { accountType },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -228,7 +229,7 @@ export class AdminController {
|
||||||
@Get('system-accounts/:accountType/transactions')
|
@Get('system-accounts/:accountType/transactions')
|
||||||
@Public()
|
@Public()
|
||||||
@ApiOperation({ summary: '获取系统账户交易记录' })
|
@ApiOperation({ summary: '获取系统账户交易记录' })
|
||||||
@ApiParam({ name: 'accountType', type: String, description: '系统账户类型 (OPERATION, PROVINCE, CITY, HEADQUARTERS)' })
|
@ApiParam({ name: 'accountType', type: String, description: '系统账户类型 (OPERATION, PROVINCE_440000, CITY_440100, HEADQUARTERS 等)' })
|
||||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||||
async getSystemAccountTransactions(
|
async getSystemAccountTransactions(
|
||||||
|
|
@ -239,17 +240,17 @@ export class AdminController {
|
||||||
const pageNum = page ?? 1;
|
const pageNum = page ?? 1;
|
||||||
const pageSizeNum = pageSize ?? 20;
|
const pageSizeNum = pageSize ?? 20;
|
||||||
const skip = (pageNum - 1) * pageSizeNum;
|
const skip = (pageNum - 1) * pageSizeNum;
|
||||||
const accountTypeEnum = accountType as SystemAccountType;
|
// accountType 现在是字符串类型,支持组合键(如 PROVINCE_440000, CITY_440100)
|
||||||
|
|
||||||
const [transactions, total] = await Promise.all([
|
const [transactions, total] = await Promise.all([
|
||||||
this.prisma.systemMiningTransaction.findMany({
|
this.prisma.systemMiningTransaction.findMany({
|
||||||
where: { accountType: accountTypeEnum },
|
where: { accountType },
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
skip,
|
skip,
|
||||||
take: pageSizeNum,
|
take: pageSizeNum,
|
||||||
}),
|
}),
|
||||||
this.prisma.systemMiningTransaction.count({
|
this.prisma.systemMiningTransaction.count({
|
||||||
where: { accountType: accountTypeEnum },
|
where: { accountType },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,10 +88,14 @@ export class ContributionEventHandler implements OnModuleInit {
|
||||||
activeAccounts: eventPayload.activeAccounts,
|
activeAccounts: eventPayload.activeAccounts,
|
||||||
});
|
});
|
||||||
} else if (eventType === 'SystemAccountSynced') {
|
} else if (eventType === 'SystemAccountSynced') {
|
||||||
this.logger.log(`Received SystemAccountSynced for ${eventPayload.accountType}`);
|
this.logger.log(
|
||||||
|
`Received SystemAccountSynced for ${eventPayload.accountType} (baseType=${eventPayload.baseType}, regionCode=${eventPayload.regionCode})`,
|
||||||
|
);
|
||||||
|
|
||||||
await this.networkSyncService.handleSystemAccountSynced({
|
await this.networkSyncService.handleSystemAccountSynced({
|
||||||
accountType: eventPayload.accountType,
|
accountType: eventPayload.accountType,
|
||||||
|
baseType: eventPayload.baseType || eventPayload.accountType, // 向后兼容
|
||||||
|
regionCode: eventPayload.regionCode || null,
|
||||||
name: eventPayload.name,
|
name: eventPayload.name,
|
||||||
contributionBalance: eventPayload.contributionBalance,
|
contributionBalance: eventPayload.contributionBalance,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,11 @@ import { UnitOfWork, TransactionClient } from '../../infrastructure/persistence/
|
||||||
import { RedisService } from '../../infrastructure/redis/redis.service';
|
import { RedisService } from '../../infrastructure/redis/redis.service';
|
||||||
import { MiningCalculatorService } from '../../domain/services/mining-calculator.service';
|
import { MiningCalculatorService } from '../../domain/services/mining-calculator.service';
|
||||||
import { ShareAmount } from '../../domain/value-objects/share-amount.vo';
|
import { ShareAmount } from '../../domain/value-objects/share-amount.vo';
|
||||||
import { SystemAccountType } from '@prisma/client';
|
|
||||||
import Decimal from 'decimal.js';
|
import Decimal from 'decimal.js';
|
||||||
|
|
||||||
|
// 系统账户基础类型常量(替代 Prisma 枚举)
|
||||||
|
const HEADQUARTERS_BASE_TYPE = 'HEADQUARTERS';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 挖矿分配服务
|
* 挖矿分配服务
|
||||||
* 负责每秒执行挖矿分配
|
* 负责每秒执行挖矿分配
|
||||||
|
|
@ -205,7 +207,7 @@ export class MiningDistributionService {
|
||||||
new ShareAmount(0),
|
new ShareAmount(0),
|
||||||
);
|
);
|
||||||
await this.accumulateSystemMinuteData(
|
await this.accumulateSystemMinuteData(
|
||||||
SystemAccountType.HEADQUARTERS,
|
HEADQUARTERS_BASE_TYPE,
|
||||||
currentMinute,
|
currentMinute,
|
||||||
headquartersTotalReward,
|
headquartersTotalReward,
|
||||||
headquartersTotalContribution,
|
headquartersTotalContribution,
|
||||||
|
|
@ -349,7 +351,7 @@ export class MiningDistributionService {
|
||||||
|
|
||||||
// 计算所有系统账户的挖矿奖励
|
// 计算所有系统账户的挖矿奖励
|
||||||
const systemRewards: Array<{
|
const systemRewards: Array<{
|
||||||
accountType: SystemAccountType;
|
accountType: string; // 改为字符串支持组合键
|
||||||
reward: ShareAmount;
|
reward: ShareAmount;
|
||||||
contribution: ShareAmount;
|
contribution: ShareAmount;
|
||||||
memo: string;
|
memo: string;
|
||||||
|
|
@ -357,7 +359,7 @@ export class MiningDistributionService {
|
||||||
|
|
||||||
for (const systemAccount of systemAccounts) {
|
for (const systemAccount of systemAccounts) {
|
||||||
// 总部账户不直接参与挖矿,它只接收未解锁算力的收益
|
// 总部账户不直接参与挖矿,它只接收未解锁算力的收益
|
||||||
if (systemAccount.accountType === SystemAccountType.HEADQUARTERS) {
|
if (systemAccount.baseType === HEADQUARTERS_BASE_TYPE) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -436,7 +438,7 @@ export class MiningDistributionService {
|
||||||
// 一次性更新总部账户(而不是每个待解锁算力单独更新)
|
// 一次性更新总部账户(而不是每个待解锁算力单独更新)
|
||||||
if (!headquartersTotal.isZero()) {
|
if (!headquartersTotal.isZero()) {
|
||||||
await this.systemMiningAccountRepository.mine(
|
await this.systemMiningAccountRepository.mine(
|
||||||
SystemAccountType.HEADQUARTERS,
|
HEADQUARTERS_BASE_TYPE,
|
||||||
headquartersTotal,
|
headquartersTotal,
|
||||||
`秒挖矿 ${currentSecond.getTime()} - 待解锁算力汇总 (${pendingRewards.length}笔)`,
|
`秒挖矿 ${currentSecond.getTime()} - 待解锁算力汇总 (${pendingRewards.length}笔)`,
|
||||||
tx,
|
tx,
|
||||||
|
|
@ -467,7 +469,7 @@ export class MiningDistributionService {
|
||||||
new ShareAmount(0),
|
new ShareAmount(0),
|
||||||
);
|
);
|
||||||
await this.accumulateSystemMinuteData(
|
await this.accumulateSystemMinuteData(
|
||||||
SystemAccountType.HEADQUARTERS,
|
HEADQUARTERS_BASE_TYPE,
|
||||||
currentMinute,
|
currentMinute,
|
||||||
headquartersTotalReward,
|
headquartersTotalReward,
|
||||||
headquartersTotalContribution,
|
headquartersTotalContribution,
|
||||||
|
|
@ -506,7 +508,7 @@ export class MiningDistributionService {
|
||||||
systemParticipantCount: number;
|
systemParticipantCount: number;
|
||||||
pendingParticipantCount: number;
|
pendingParticipantCount: number;
|
||||||
systemRedisData: Array<{
|
systemRedisData: Array<{
|
||||||
accountType: SystemAccountType;
|
accountType: string; // 改为字符串支持组合键
|
||||||
reward: ShareAmount;
|
reward: ShareAmount;
|
||||||
contribution: ShareAmount;
|
contribution: ShareAmount;
|
||||||
}>;
|
}>;
|
||||||
|
|
@ -521,7 +523,7 @@ export class MiningDistributionService {
|
||||||
let systemParticipantCount = 0;
|
let systemParticipantCount = 0;
|
||||||
let pendingParticipantCount = 0;
|
let pendingParticipantCount = 0;
|
||||||
const systemRedisData: Array<{
|
const systemRedisData: Array<{
|
||||||
accountType: SystemAccountType;
|
accountType: string; // 改为字符串支持组合键
|
||||||
reward: ShareAmount;
|
reward: ShareAmount;
|
||||||
contribution: ShareAmount;
|
contribution: ShareAmount;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
@ -539,7 +541,7 @@ export class MiningDistributionService {
|
||||||
|
|
||||||
// 计算所有系统账户的挖矿奖励
|
// 计算所有系统账户的挖矿奖励
|
||||||
const systemRewards: Array<{
|
const systemRewards: Array<{
|
||||||
accountType: SystemAccountType;
|
accountType: string; // 改为字符串支持组合键
|
||||||
reward: ShareAmount;
|
reward: ShareAmount;
|
||||||
contribution: ShareAmount;
|
contribution: ShareAmount;
|
||||||
memo: string;
|
memo: string;
|
||||||
|
|
@ -547,7 +549,7 @@ export class MiningDistributionService {
|
||||||
|
|
||||||
for (const systemAccount of systemAccounts) {
|
for (const systemAccount of systemAccounts) {
|
||||||
// 总部账户不直接参与挖矿,它只接收未解锁算力的收益
|
// 总部账户不直接参与挖矿,它只接收未解锁算力的收益
|
||||||
if (systemAccount.accountType === SystemAccountType.HEADQUARTERS) {
|
if (systemAccount.baseType === HEADQUARTERS_BASE_TYPE) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -631,7 +633,7 @@ export class MiningDistributionService {
|
||||||
// 一次性更新总部账户(而不是每个待解锁算力单独更新)
|
// 一次性更新总部账户(而不是每个待解锁算力单独更新)
|
||||||
if (!headquartersTotal.isZero()) {
|
if (!headquartersTotal.isZero()) {
|
||||||
await this.systemMiningAccountRepository.mine(
|
await this.systemMiningAccountRepository.mine(
|
||||||
SystemAccountType.HEADQUARTERS,
|
HEADQUARTERS_BASE_TYPE,
|
||||||
headquartersTotal,
|
headquartersTotal,
|
||||||
`秒挖矿 ${currentSecond.getTime()} - 待解锁算力汇总 (${pendingRewards.length}笔)`,
|
`秒挖矿 ${currentSecond.getTime()} - 待解锁算力汇总 (${pendingRewards.length}笔)`,
|
||||||
tx,
|
tx,
|
||||||
|
|
@ -694,9 +696,10 @@ export class MiningDistributionService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 累积系统账户每分钟的挖矿数据到Redis
|
* 累积系统账户每分钟的挖矿数据到Redis
|
||||||
|
* @param accountType 账户类型(组合键,如 PROVINCE_440000, CITY_440100)
|
||||||
*/
|
*/
|
||||||
private async accumulateSystemMinuteData(
|
private async accumulateSystemMinuteData(
|
||||||
accountType: SystemAccountType,
|
accountType: string,
|
||||||
minuteTime: Date,
|
minuteTime: Date,
|
||||||
reward: ShareAmount,
|
reward: ShareAmount,
|
||||||
accountContribution: ShareAmount,
|
accountContribution: ShareAmount,
|
||||||
|
|
@ -792,6 +795,7 @@ export class MiningDistributionService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 写入系统账户每分钟汇总的挖矿记录
|
* 写入系统账户每分钟汇总的挖矿记录
|
||||||
|
* 支持组合键账户类型(如 PROVINCE_440000, CITY_440100)
|
||||||
*/
|
*/
|
||||||
private async writeSystemMinuteRecords(minuteTime: Date): Promise<void> {
|
private async writeSystemMinuteRecords(minuteTime: Date): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
|
@ -803,18 +807,18 @@ export class MiningDistributionService {
|
||||||
if (!data) continue;
|
if (!data) continue;
|
||||||
|
|
||||||
const accumulated = JSON.parse(data);
|
const accumulated = JSON.parse(data);
|
||||||
const accountType = key.split(':').pop() as SystemAccountType;
|
// accountType 现在是字符串类型,支持组合键
|
||||||
|
const accountType = key.split(':').pop()!;
|
||||||
|
|
||||||
await this.prisma.systemMiningRecord.create({
|
// 使用 repository 的 saveMinuteRecord 方法(支持 upsert)
|
||||||
data: {
|
await this.systemMiningAccountRepository.saveMinuteRecord(
|
||||||
accountType,
|
accountType,
|
||||||
miningMinute: minuteTime,
|
minuteTime,
|
||||||
contributionRatio: new Decimal(accumulated.contributionRatio),
|
new ShareAmount(accumulated.contributionRatio),
|
||||||
totalContribution: new Decimal(accumulated.totalContribution),
|
new ShareAmount(accumulated.totalContribution),
|
||||||
secondDistribution: new Decimal(accumulated.secondDistribution),
|
new ShareAmount(accumulated.secondDistribution),
|
||||||
minedAmount: new Decimal(accumulated.minedAmount),
|
new ShareAmount(accumulated.minedAmount),
|
||||||
},
|
);
|
||||||
});
|
|
||||||
|
|
||||||
await this.redis.del(key);
|
await this.redis.del(key);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
||||||
import { SystemMiningAccountRepository } from '../../infrastructure/persistence/repositories/system-mining-account.repository';
|
import {
|
||||||
|
SystemMiningAccountRepository,
|
||||||
|
SystemAccountBaseType,
|
||||||
|
} from '../../infrastructure/persistence/repositories/system-mining-account.repository';
|
||||||
import { ShareAmount } from '../../domain/value-objects/share-amount.vo';
|
import { ShareAmount } from '../../domain/value-objects/share-amount.vo';
|
||||||
import { SystemAccountType } from '@prisma/client';
|
|
||||||
import Decimal from 'decimal.js';
|
import Decimal from 'decimal.js';
|
||||||
|
|
||||||
interface SystemAccountSyncedData {
|
interface SystemAccountSyncedData {
|
||||||
accountType: string;
|
accountType: string; // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等
|
||||||
|
baseType: string; // 基础类型: OPERATION / PROVINCE / CITY / HEADQUARTERS
|
||||||
|
regionCode: string | null; // 区域代码: 省/市代码,如 440000, 440100
|
||||||
name: string;
|
name: string;
|
||||||
contributionBalance: string;
|
contributionBalance: string;
|
||||||
}
|
}
|
||||||
|
|
@ -47,20 +51,32 @@ export class NetworkSyncService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理系统账户同步事件
|
* 处理系统账户同步事件
|
||||||
|
* 支持组合键账户类型(如 PROVINCE_440000, CITY_440100)
|
||||||
*/
|
*/
|
||||||
async handleSystemAccountSynced(data: SystemAccountSyncedData): Promise<void> {
|
async handleSystemAccountSynced(data: SystemAccountSyncedData): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const accountType = this.mapAccountType(data.accountType);
|
// 验证 baseType 是否有效
|
||||||
if (!accountType) {
|
const validBaseTypes: SystemAccountBaseType[] = ['OPERATION', 'PROVINCE', 'CITY', 'HEADQUARTERS'];
|
||||||
this.logger.warn(`Unknown system account type: ${data.accountType}`);
|
const baseType = (data.baseType || this.extractBaseType(data.accountType)) as SystemAccountBaseType;
|
||||||
|
|
||||||
|
if (!validBaseTypes.includes(baseType)) {
|
||||||
|
this.logger.warn(`Unknown system account base type: ${baseType} for ${data.accountType}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contribution = new ShareAmount(data.contributionBalance);
|
const contribution = new ShareAmount(data.contributionBalance);
|
||||||
await this.systemAccountRepository.updateContribution(accountType, contribution);
|
|
||||||
|
// 使用 upsert 动态创建或更新账户
|
||||||
|
await this.systemAccountRepository.updateContribution(
|
||||||
|
data.accountType,
|
||||||
|
baseType,
|
||||||
|
data.regionCode,
|
||||||
|
data.name,
|
||||||
|
contribution,
|
||||||
|
);
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Synced system account ${data.accountType}: contribution=${data.contributionBalance}`,
|
`Synced system account ${data.accountType} (baseType=${baseType}, regionCode=${data.regionCode}): contribution=${data.contributionBalance}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to sync system account ${data.accountType}`, error);
|
this.logger.error(`Failed to sync system account ${data.accountType}`, error);
|
||||||
|
|
@ -68,6 +84,17 @@ export class NetworkSyncService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 accountType 提取基础类型(向后兼容)
|
||||||
|
* 例如: PROVINCE_440000 -> PROVINCE, CITY_440100 -> CITY
|
||||||
|
*/
|
||||||
|
private extractBaseType(accountType: string): string {
|
||||||
|
if (accountType.startsWith('PROVINCE_')) return 'PROVINCE';
|
||||||
|
if (accountType.startsWith('CITY_')) return 'CITY';
|
||||||
|
// 如果没有下划线,则 accountType 本身就是基础类型
|
||||||
|
return accountType;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理全网进度更新事件
|
* 处理全网进度更新事件
|
||||||
*/
|
*/
|
||||||
|
|
@ -132,6 +159,8 @@ export class NetworkSyncService {
|
||||||
for (const account of systemAccounts) {
|
for (const account of systemAccounts) {
|
||||||
await this.handleSystemAccountSynced({
|
await this.handleSystemAccountSynced({
|
||||||
accountType: account.accountType,
|
accountType: account.accountType,
|
||||||
|
baseType: account.baseType || account.accountType, // 向后兼容
|
||||||
|
regionCode: account.regionCode || null,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
contributionBalance: account.contributionBalance,
|
contributionBalance: account.contributionBalance,
|
||||||
});
|
});
|
||||||
|
|
@ -254,18 +283,4 @@ export class NetworkSyncService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapAccountType(type: string): SystemAccountType | null {
|
|
||||||
switch (type.toUpperCase()) {
|
|
||||||
case 'OPERATION':
|
|
||||||
return SystemAccountType.OPERATION;
|
|
||||||
case 'PROVINCE':
|
|
||||||
return SystemAccountType.PROVINCE;
|
|
||||||
case 'CITY':
|
|
||||||
return SystemAccountType.CITY;
|
|
||||||
case 'HEADQUARTERS':
|
|
||||||
return SystemAccountType.HEADQUARTERS;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { ShareAmount } from '../../../domain/value-objects/share-amount.vo';
|
import { ShareAmount } from '../../../domain/value-objects/share-amount.vo';
|
||||||
import { SystemAccountType } from '@prisma/client';
|
|
||||||
import { TransactionClient } from '../unit-of-work/unit-of-work';
|
import { TransactionClient } from '../unit-of-work/unit-of-work';
|
||||||
|
|
||||||
|
// 基础类型定义(不再使用 Prisma 枚举)
|
||||||
|
export type SystemAccountBaseType = 'OPERATION' | 'PROVINCE' | 'CITY' | 'HEADQUARTERS';
|
||||||
|
|
||||||
export interface SystemMiningAccountSnapshot {
|
export interface SystemMiningAccountSnapshot {
|
||||||
accountType: SystemAccountType;
|
accountType: string; // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等
|
||||||
|
baseType: SystemAccountBaseType; // 基础类型
|
||||||
|
regionCode: string | null; // 区域代码
|
||||||
name: string;
|
name: string;
|
||||||
totalMined: ShareAmount;
|
totalMined: ShareAmount;
|
||||||
availableBalance: ShareAmount;
|
availableBalance: ShareAmount;
|
||||||
|
|
@ -17,7 +21,7 @@ export interface SystemMiningAccountSnapshot {
|
||||||
export class SystemMiningAccountRepository {
|
export class SystemMiningAccountRepository {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
async findByType(accountType: SystemAccountType): Promise<SystemMiningAccountSnapshot | null> {
|
async findByType(accountType: string): Promise<SystemMiningAccountSnapshot | null> {
|
||||||
const record = await this.prisma.systemMiningAccount.findUnique({
|
const record = await this.prisma.systemMiningAccount.findUnique({
|
||||||
where: { accountType },
|
where: { accountType },
|
||||||
});
|
});
|
||||||
|
|
@ -29,6 +33,18 @@ export class SystemMiningAccountRepository {
|
||||||
return this.toSnapshot(record);
|
return this.toSnapshot(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据基础类型查找所有账户
|
||||||
|
*/
|
||||||
|
async findByBaseType(baseType: SystemAccountBaseType): Promise<SystemMiningAccountSnapshot[]> {
|
||||||
|
const records = await this.prisma.systemMiningAccount.findMany({
|
||||||
|
where: { baseType },
|
||||||
|
orderBy: { accountType: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return records.map((r) => this.toSnapshot(r));
|
||||||
|
}
|
||||||
|
|
||||||
async findAll(): Promise<SystemMiningAccountSnapshot[]> {
|
async findAll(): Promise<SystemMiningAccountSnapshot[]> {
|
||||||
const records = await this.prisma.systemMiningAccount.findMany({
|
const records = await this.prisma.systemMiningAccount.findMany({
|
||||||
orderBy: { accountType: 'asc' },
|
orderBy: { accountType: 'asc' },
|
||||||
|
|
@ -37,12 +53,15 @@ export class SystemMiningAccountRepository {
|
||||||
return records.map((r) => this.toSnapshot(r));
|
return records.map((r) => this.toSnapshot(r));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保基础汇总账户存在
|
||||||
|
*/
|
||||||
async ensureSystemAccountsExist(): Promise<void> {
|
async ensureSystemAccountsExist(): Promise<void> {
|
||||||
const accounts = [
|
const accounts: { accountType: string; baseType: SystemAccountBaseType; name: string }[] = [
|
||||||
{ accountType: SystemAccountType.OPERATION, name: '运营账户' },
|
{ accountType: 'OPERATION', baseType: 'OPERATION', name: '运营账户' },
|
||||||
{ accountType: SystemAccountType.PROVINCE, name: '省公司账户' },
|
{ accountType: 'PROVINCE', baseType: 'PROVINCE', name: '省公司账户' },
|
||||||
{ accountType: SystemAccountType.CITY, name: '市公司账户' },
|
{ accountType: 'CITY', baseType: 'CITY', name: '市公司账户' },
|
||||||
{ accountType: SystemAccountType.HEADQUARTERS, name: '总部账户' },
|
{ accountType: 'HEADQUARTERS', baseType: 'HEADQUARTERS', name: '总部账户' },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
|
|
@ -50,6 +69,8 @@ export class SystemMiningAccountRepository {
|
||||||
where: { accountType: account.accountType },
|
where: { accountType: account.accountType },
|
||||||
create: {
|
create: {
|
||||||
accountType: account.accountType,
|
accountType: account.accountType,
|
||||||
|
baseType: account.baseType,
|
||||||
|
regionCode: null,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
totalMined: 0,
|
totalMined: 0,
|
||||||
availableBalance: 0,
|
availableBalance: 0,
|
||||||
|
|
@ -60,15 +81,23 @@ export class SystemMiningAccountRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新系统账户算力(支持动态创建省市账户)
|
||||||
|
*/
|
||||||
async updateContribution(
|
async updateContribution(
|
||||||
accountType: SystemAccountType,
|
accountType: string,
|
||||||
|
baseType: SystemAccountBaseType,
|
||||||
|
regionCode: string | null,
|
||||||
|
name: string,
|
||||||
contribution: ShareAmount,
|
contribution: ShareAmount,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.prisma.systemMiningAccount.upsert({
|
await this.prisma.systemMiningAccount.upsert({
|
||||||
where: { accountType },
|
where: { accountType },
|
||||||
create: {
|
create: {
|
||||||
accountType,
|
accountType,
|
||||||
name: this.getAccountName(accountType),
|
baseType,
|
||||||
|
regionCode,
|
||||||
|
name,
|
||||||
totalContribution: contribution.value,
|
totalContribution: contribution.value,
|
||||||
lastSyncedAt: new Date(),
|
lastSyncedAt: new Date(),
|
||||||
},
|
},
|
||||||
|
|
@ -89,13 +118,13 @@ export class SystemMiningAccountRepository {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行系统账户挖矿(带外部事务支持)
|
* 执行系统账户挖矿(带外部事务支持)
|
||||||
* @param accountType 账户类型
|
* @param accountType 账户类型(组合键)
|
||||||
* @param amount 挖矿数量
|
* @param amount 挖矿数量
|
||||||
* @param memo 备注
|
* @param memo 备注
|
||||||
* @param tx 可选的外部事务客户端,如果不传则自动创建事务
|
* @param tx 可选的外部事务客户端,如果不传则自动创建事务
|
||||||
*/
|
*/
|
||||||
async mine(
|
async mine(
|
||||||
accountType: SystemAccountType,
|
accountType: string,
|
||||||
amount: ShareAmount,
|
amount: ShareAmount,
|
||||||
memo: string,
|
memo: string,
|
||||||
tx?: TransactionClient,
|
tx?: TransactionClient,
|
||||||
|
|
@ -143,7 +172,7 @@ export class SystemMiningAccountRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveMinuteRecord(
|
async saveMinuteRecord(
|
||||||
accountType: SystemAccountType,
|
accountType: string,
|
||||||
miningMinute: Date,
|
miningMinute: Date,
|
||||||
contributionRatio: ShareAmount,
|
contributionRatio: ShareAmount,
|
||||||
totalContribution: ShareAmount,
|
totalContribution: ShareAmount,
|
||||||
|
|
@ -171,24 +200,11 @@ export class SystemMiningAccountRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAccountName(accountType: SystemAccountType): string {
|
|
||||||
switch (accountType) {
|
|
||||||
case SystemAccountType.OPERATION:
|
|
||||||
return '运营账户';
|
|
||||||
case SystemAccountType.PROVINCE:
|
|
||||||
return '省公司账户';
|
|
||||||
case SystemAccountType.CITY:
|
|
||||||
return '市公司账户';
|
|
||||||
case SystemAccountType.HEADQUARTERS:
|
|
||||||
return '总部账户';
|
|
||||||
default:
|
|
||||||
return accountType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private toSnapshot(record: any): SystemMiningAccountSnapshot {
|
private toSnapshot(record: any): SystemMiningAccountSnapshot {
|
||||||
return {
|
return {
|
||||||
accountType: record.accountType,
|
accountType: record.accountType,
|
||||||
|
baseType: record.baseType as SystemAccountBaseType,
|
||||||
|
regionCode: record.regionCode,
|
||||||
name: record.name,
|
name: record.name,
|
||||||
totalMined: new ShareAmount(record.totalMined),
|
totalMined: new ShareAmount(record.totalMined),
|
||||||
availableBalance: new ShareAmount(record.availableBalance),
|
availableBalance: new ShareAmount(record.availableBalance),
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import {
|
||||||
Receipt,
|
Receipt,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
FileStack,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
@ -33,7 +34,10 @@ import {
|
||||||
useSystemAccounts,
|
useSystemAccounts,
|
||||||
useSystemAccountMiningRecords,
|
useSystemAccountMiningRecords,
|
||||||
useSystemAccountTransactions,
|
useSystemAccountTransactions,
|
||||||
|
useSystemAccountContributionRecords,
|
||||||
|
useSystemAccountContributionStats,
|
||||||
} from '@/features/system-accounts';
|
} from '@/features/system-accounts';
|
||||||
|
import { ContributionRecordsTable } from '@/features/system-accounts/components';
|
||||||
import { getAccountDisplayInfo } from '@/types/system-account';
|
import { getAccountDisplayInfo } from '@/types/system-account';
|
||||||
import { formatDecimal } from '@/lib/utils/format';
|
import { formatDecimal } from '@/lib/utils/format';
|
||||||
|
|
||||||
|
|
@ -51,6 +55,7 @@ export default function SystemAccountDetailPage() {
|
||||||
|
|
||||||
const [miningPage, setMiningPage] = useState(1);
|
const [miningPage, setMiningPage] = useState(1);
|
||||||
const [transactionPage, setTransactionPage] = useState(1);
|
const [transactionPage, setTransactionPage] = useState(1);
|
||||||
|
const [contributionPage, setContributionPage] = useState(1);
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
|
|
||||||
// 获取账户列表以找到当前账户信息
|
// 获取账户列表以找到当前账户信息
|
||||||
|
|
@ -73,6 +78,16 @@ export default function SystemAccountDetailPage() {
|
||||||
error: transactionsError,
|
error: transactionsError,
|
||||||
} = useSystemAccountTransactions(accountType, transactionPage, pageSize);
|
} = useSystemAccountTransactions(accountType, transactionPage, pageSize);
|
||||||
|
|
||||||
|
// 获取算力来源明细
|
||||||
|
const {
|
||||||
|
data: contributionRecords,
|
||||||
|
isLoading: contributionLoading,
|
||||||
|
error: contributionError,
|
||||||
|
} = useSystemAccountContributionRecords(accountType, contributionPage, pageSize);
|
||||||
|
|
||||||
|
// 获取算力明细统计
|
||||||
|
const { data: contributionStats } = useSystemAccountContributionStats(accountType);
|
||||||
|
|
||||||
const displayInfo = getAccountDisplayInfo(accountType);
|
const displayInfo = getAccountDisplayInfo(accountType);
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
|
|
@ -208,6 +223,10 @@ export default function SystemAccountDetailPage() {
|
||||||
<Receipt className="h-4 w-4" />
|
<Receipt className="h-4 w-4" />
|
||||||
交易记录
|
交易记录
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="contributions" className="flex items-center gap-2">
|
||||||
|
<FileStack className="h-4 w-4" />
|
||||||
|
算力来源
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* 挖矿记录 Tab */}
|
{/* 挖矿记录 Tab */}
|
||||||
|
|
@ -458,6 +477,27 @@ export default function SystemAccountDetailPage() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 算力来源 Tab */}
|
||||||
|
<TabsContent value="contributions">
|
||||||
|
{contributionError ? (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>加载算力来源记录失败</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<ContributionRecordsTable
|
||||||
|
records={contributionRecords?.records || []}
|
||||||
|
stats={contributionStats}
|
||||||
|
isLoading={contributionLoading}
|
||||||
|
page={contributionPage}
|
||||||
|
pageSize={pageSize}
|
||||||
|
total={contributionRecords?.total || 0}
|
||||||
|
totalPages={contributionRecords ? Math.ceil(contributionRecords.total / pageSize) : 0}
|
||||||
|
onPageChange={setContributionPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,43 @@ export interface SystemTransactionsResponse {
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 算力来源明细记录
|
||||||
|
export interface SystemContributionRecord {
|
||||||
|
id: string;
|
||||||
|
originalRecordId: string;
|
||||||
|
systemAccountType: string;
|
||||||
|
sourceAdoptionId: string;
|
||||||
|
sourceAccountSequence: string;
|
||||||
|
distributionRate: string;
|
||||||
|
amount: string;
|
||||||
|
effectiveDate: string;
|
||||||
|
expireDate: string | null;
|
||||||
|
isExpired: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
syncedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemContributionRecordsResponse {
|
||||||
|
records: SystemContributionRecord[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 算力明细统计
|
||||||
|
export interface SystemContributionStats {
|
||||||
|
accountType: string;
|
||||||
|
name: string;
|
||||||
|
baseType: string;
|
||||||
|
regionCode: string | null;
|
||||||
|
totalContribution: string;
|
||||||
|
recordCount: number;
|
||||||
|
sumFromRecords: string;
|
||||||
|
uniqueAdoptionCount: number;
|
||||||
|
uniqueUserCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const systemAccountsApi = {
|
export const systemAccountsApi = {
|
||||||
/**
|
/**
|
||||||
* Get all system accounts (merged local + synced data)
|
* Get all system accounts (merged local + synced data)
|
||||||
|
|
@ -89,6 +126,34 @@ export const systemAccountsApi = {
|
||||||
);
|
);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get system account contribution records (算力来源明细)
|
||||||
|
* 显示该账户的每笔算力来自哪个认种订单
|
||||||
|
*/
|
||||||
|
getContributionRecords: async (
|
||||||
|
accountType: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20
|
||||||
|
): Promise<SystemContributionRecordsResponse> => {
|
||||||
|
const response = await apiClient.get(
|
||||||
|
`/system-accounts/${accountType}/contributions`,
|
||||||
|
{ params: { page, pageSize } }
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get system account contribution stats (算力明细统计)
|
||||||
|
*/
|
||||||
|
getContributionStats: async (
|
||||||
|
accountType: string
|
||||||
|
): Promise<SystemContributionStats> => {
|
||||||
|
const response = await apiClient.get(
|
||||||
|
`/system-accounts/${accountType}/contribution-stats`
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to categorize accounts for display
|
// Helper to categorize accounts for display
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { formatDecimal } from '@/lib/utils/format';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { zhCN } from 'date-fns/locale';
|
||||||
|
import { ChevronLeft, ChevronRight, Users, FileStack, TrendingUp } from 'lucide-react';
|
||||||
|
import type { SystemContributionRecord, SystemContributionStats } from '../api/system-accounts.api';
|
||||||
|
|
||||||
|
interface ContributionRecordsTableProps {
|
||||||
|
records: SystemContributionRecord[];
|
||||||
|
stats?: SystemContributionStats;
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
isLoading?: boolean;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContributionRecordsTable({
|
||||||
|
records,
|
||||||
|
stats,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
isLoading = false,
|
||||||
|
onPageChange,
|
||||||
|
}: ContributionRecordsTableProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
{stats && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardDescription className="flex items-center gap-2">
|
||||||
|
<FileStack className="h-4 w-4" />
|
||||||
|
算力明细记录数
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.recordCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
合计: {formatDecimal(stats.sumFromRecords, 4)}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardDescription className="flex items-center gap-2">
|
||||||
|
<TrendingUp className="h-4 w-4" />
|
||||||
|
来源认种订单数
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.uniqueAdoptionCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
去重后的认种订单
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardDescription className="flex items-center gap-2">
|
||||||
|
<Users className="h-4 w-4" />
|
||||||
|
来源用户数
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.uniqueUserCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
去重后的认种用户
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 表格 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
算力来源明细
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
共 {total} 条记录
|
||||||
|
</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
显示该账户的每笔算力来自哪个认种订单
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>来源认种ID</TableHead>
|
||||||
|
<TableHead>认种用户</TableHead>
|
||||||
|
<TableHead className="text-right">分配比例</TableHead>
|
||||||
|
<TableHead className="text-right">算力金额</TableHead>
|
||||||
|
<TableHead>生效日期</TableHead>
|
||||||
|
<TableHead>创建时间</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{isLoading ? (
|
||||||
|
[...Array(5)].map((_, i) => (
|
||||||
|
<TableRow key={i}>
|
||||||
|
{[...Array(6)].map((_, j) => (
|
||||||
|
<TableCell key={j}>
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : records.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={6}
|
||||||
|
className="text-center text-muted-foreground py-8"
|
||||||
|
>
|
||||||
|
暂无算力来源记录
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
records.map((record) => (
|
||||||
|
<TableRow key={record.id}>
|
||||||
|
<TableCell>
|
||||||
|
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
||||||
|
#{record.sourceAdoptionId}
|
||||||
|
</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<code className="text-xs bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded">
|
||||||
|
{record.sourceAccountSequence}
|
||||||
|
</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Badge variant="outline" className="font-mono">
|
||||||
|
{(Number(record.distributionRate) * 100).toFixed(2)}%
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-mono text-green-600">
|
||||||
|
+{formatDecimal(record.amount, 4)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground text-sm">
|
||||||
|
{format(new Date(record.effectiveDate), 'yyyy-MM-dd', { locale: zhCN })}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground text-sm">
|
||||||
|
{format(new Date(record.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-t">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
第 {page} / {totalPages} 页,共 {total} 条
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(page - 1)}
|
||||||
|
disabled={page <= 1}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
上一页
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(page + 1)}
|
||||||
|
disabled={page >= totalPages}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export { AccountCard } from './account-card';
|
export { AccountCard } from './account-card';
|
||||||
export { AccountsTable } from './accounts-table';
|
export { AccountsTable } from './accounts-table';
|
||||||
|
export { ContributionRecordsTable } from './contribution-records-table';
|
||||||
export { SummaryCards } from './summary-cards';
|
export { SummaryCards } from './summary-cards';
|
||||||
|
|
|
||||||
|
|
@ -76,3 +76,29 @@ export function useSystemAccountTransactions(
|
||||||
enabled: !!accountType,
|
enabled: !!accountType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to fetch system account contribution records (算力来源明细)
|
||||||
|
*/
|
||||||
|
export function useSystemAccountContributionRecords(
|
||||||
|
accountType: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['system-accounts', accountType, 'contributions', page, pageSize],
|
||||||
|
queryFn: () => systemAccountsApi.getContributionRecords(accountType, page, pageSize),
|
||||||
|
enabled: !!accountType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to fetch system account contribution stats (算力明细统计)
|
||||||
|
*/
|
||||||
|
export function useSystemAccountContributionStats(accountType: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['system-accounts', accountType, 'contribution-stats'],
|
||||||
|
queryFn: () => systemAccountsApi.getContributionStats(accountType),
|
||||||
|
enabled: !!accountType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ export {
|
||||||
type SystemMiningRecordsResponse,
|
type SystemMiningRecordsResponse,
|
||||||
type SystemTransaction,
|
type SystemTransaction,
|
||||||
type SystemTransactionsResponse,
|
type SystemTransactionsResponse,
|
||||||
|
type SystemContributionRecord,
|
||||||
|
type SystemContributionRecordsResponse,
|
||||||
|
type SystemContributionStats,
|
||||||
} from './api/system-accounts.api';
|
} from './api/system-accounts.api';
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
|
|
@ -15,7 +18,9 @@ export {
|
||||||
useCategorizedAccounts,
|
useCategorizedAccounts,
|
||||||
useSystemAccountMiningRecords,
|
useSystemAccountMiningRecords,
|
||||||
useSystemAccountTransactions,
|
useSystemAccountTransactions,
|
||||||
|
useSystemAccountContributionRecords,
|
||||||
|
useSystemAccountContributionStats,
|
||||||
} from './hooks/use-system-accounts';
|
} from './hooks/use-system-accounts';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
export { AccountCard, AccountsTable, SummaryCards } from './components';
|
export { AccountCard, AccountsTable, SummaryCards, ContributionRecordsTable } from './components';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue