refactor(system-accounts): 移除 baseType 字段,使用 accountType+regionCode 复合唯一键

## 主要变更

### 数据模型简化
- 移除冗余的 baseType 字段,accountType 已包含类型信息
- 使用 accountType (OPERATION/PROVINCE/CITY/HEADQUARTERS) + regionCode (省市代码) 作为复合唯一键
- 所有查询改用 accountType+regionCode,100% 弃用数据库自增 ID

### contribution-service
- SystemAccount 表移除 baseType,改用 accountType+regionCode 唯一约束
- 修改算力分配逻辑,省市账户使用对应 regionCode
- 事件发布增加 regionCode 字段

### mining-service
- SystemMiningAccount 表使用 accountType+regionCode 唯一约束
- API 改为 /system-accounts/:accountType/records?regionCode=xxx 格式
- 挖矿分配逻辑支持按省市细分

### mining-admin-service
- SyncedSystemContribution 表使用 accountType+regionCode 唯一约束
- CDC 同步处理器适配新格式
- API 统一使用 accountType+regionCode 查询

## API 示例
- 运营账户: GET /admin/system-accounts/OPERATION/records
- 广东省: GET /admin/system-accounts/PROVINCE/records?regionCode=440000
- 广州市: GET /admin/system-accounts/CITY/records?regionCode=440100
- 总部: GET /admin/system-accounts/HEADQUARTERS/records

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-20 21:29:01 -08:00
parent 81b2e7a4c2
commit 9062346650
20 changed files with 410 additions and 387 deletions

View File

@ -222,13 +222,12 @@ CREATE INDEX "unallocated_contributions_unalloc_type_idx" ON "unallocated_contri
CREATE INDEX "unallocated_contributions_status_idx" ON "unallocated_contributions"("status"); CREATE INDEX "unallocated_contributions_status_idx" ON "unallocated_contributions"("status");
-- ============================================ -- ============================================
-- 6. 系统账户表 (支持按省市细分) -- 6. 系统账户表
-- ============================================ -- ============================================
CREATE TABLE "system_accounts" ( CREATE TABLE "system_accounts" (
"id" BIGSERIAL NOT NULL, "id" BIGSERIAL NOT NULL,
"account_type" TEXT NOT NULL, "account_type" TEXT NOT NULL,
"base_type" TEXT NOT NULL,
"region_code" TEXT, "region_code" TEXT,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"contribution_balance" DECIMAL(30,10) NOT NULL DEFAULT 0, "contribution_balance" DECIMAL(30,10) NOT NULL DEFAULT 0,
@ -240,8 +239,8 @@ CREATE TABLE "system_accounts" (
CONSTRAINT "system_accounts_pkey" PRIMARY KEY ("id") CONSTRAINT "system_accounts_pkey" PRIMARY KEY ("id")
); );
CREATE UNIQUE INDEX "system_accounts_account_type_key" ON "system_accounts"("account_type"); CREATE UNIQUE INDEX "system_accounts_account_type_region_code_key" ON "system_accounts"("account_type", "region_code");
CREATE INDEX "system_accounts_base_type_idx" ON "system_accounts"("base_type"); CREATE INDEX "system_accounts_account_type_idx" ON "system_accounts"("account_type");
CREATE INDEX "system_accounts_region_code_idx" ON "system_accounts"("region_code"); CREATE INDEX "system_accounts_region_code_idx" ON "system_accounts"("region_code");
CREATE TABLE "system_contribution_records" ( CREATE TABLE "system_contribution_records" (

View File

@ -298,13 +298,10 @@ 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") // 组合键: PROVINCE_440000, CITY_440100 等 accountType String @map("account_type") // OPERATION / PROVINCE / CITY / HEADQUARTERS
baseType String @map("base_type") // 基础类型: OPERATION / PROVINCE / CITY / HEADQUARTERS regionCode String? @map("region_code") // 省/市代码,如 440000, 440100
regionCode String? @map("region_code") // 区域代码: 省/市代码,如 440000, 440100
name String name String
contributionBalance Decimal @default(0) @map("contribution_balance") @db.Decimal(30, 10) contributionBalance Decimal @default(0) @map("contribution_balance") @db.Decimal(30, 10)
@ -317,7 +314,8 @@ model SystemAccount {
records SystemContributionRecord[] records SystemContributionRecord[]
@@index([baseType]) @@unique([accountType, regionCode])
@@index([accountType])
@@index([regionCode]) @@index([regionCode])
@@map("system_accounts") @@map("system_accounts")
} }

View File

@ -438,8 +438,7 @@ export class AdminController {
const events = systemAccounts.map((account) => { const events = systemAccounts.map((account) => {
const event = new SystemAccountSyncedEvent( const event = new SystemAccountSyncedEvent(
account.accountType, account.accountType,
account.baseType, // 基础类型 account.regionCode,
account.regionCode, // 区域代码
account.name, account.name,
account.contributionBalance.toString(), account.contributionBalance.toString(),
account.createdAt, account.createdAt,
@ -447,7 +446,7 @@ export class AdminController {
return { return {
aggregateType: SystemAccountSyncedEvent.AGGREGATE_TYPE, aggregateType: SystemAccountSyncedEvent.AGGREGATE_TYPE,
aggregateId: account.accountType, aggregateId: `${account.accountType}:${account.regionCode || 'null'}`,
eventType: SystemAccountSyncedEvent.EVENT_TYPE, eventType: SystemAccountSyncedEvent.EVENT_TYPE,
payload: event.toPayload(), payload: event.toPayload(),
}; };

View File

@ -332,44 +332,46 @@ 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( await this.systemAccountRepository.addContribution(
sys.accountType, sys.accountType,
sys.baseType,
sys.regionCode, sys.regionCode,
sys.amount, sys.amount,
); );
// 保存算力明细记录并获取保存后的记录带ID // 保存算力明细记录
const savedRecord = await this.systemAccountRepository.saveContributionRecord({ const savedRecord = await this.systemAccountRepository.saveContributionRecord({
systemAccountType: sys.accountType, accountType: sys.accountType,
regionCode: sys.regionCode,
sourceAdoptionId, sourceAdoptionId,
sourceAccountSequence, sourceAccountSequence,
distributionRate: sys.rate.value.toNumber(), distributionRate: sys.rate.value.toNumber(),
amount: sys.amount, amount: sys.amount,
effectiveDate, effectiveDate,
expireDate: null, // System account contributions never expire based on the schema's contributionNeverExpires field expireDate: null,
}); });
// 发布系统账户同步事件(用于 mining-service 同步系统账户算力) // 发布系统账户同步事件(用于 mining-service 同步系统账户算力)
const systemAccount = await this.systemAccountRepository.findByType(sys.accountType); const systemAccount = await this.systemAccountRepository.findByTypeAndRegion(
sys.accountType,
sys.regionCode,
);
if (systemAccount) { if (systemAccount) {
const event = new SystemAccountSyncedEvent( const event = new SystemAccountSyncedEvent(
sys.accountType, sys.accountType,
sys.baseType, // 新增:基础类型 sys.regionCode,
sys.regionCode, // 新增:区域代码
systemAccount.name, systemAccount.name,
systemAccount.contributionBalance.value.toString(), systemAccount.contributionBalance.value.toString(),
systemAccount.createdAt, systemAccount.createdAt,
); );
await this.outboxRepository.save({ await this.outboxRepository.save({
aggregateType: SystemAccountSyncedEvent.AGGREGATE_TYPE, aggregateType: SystemAccountSyncedEvent.AGGREGATE_TYPE,
aggregateId: sys.accountType, aggregateId: `${sys.accountType}:${sys.regionCode || 'null'}`,
eventType: SystemAccountSyncedEvent.EVENT_TYPE, eventType: SystemAccountSyncedEvent.EVENT_TYPE,
payload: event.toPayload(), payload: event.toPayload(),
}); });
@ -383,7 +385,7 @@ export class ContributionCalculationService {
sys.rate.value.toNumber(), sys.rate.value.toNumber(),
sys.amount.value.toString(), sys.amount.value.toString(),
effectiveDate, effectiveDate,
null, // System account contributions never expire null,
savedRecord.createdAt, savedRecord.createdAt,
); );
await this.outboxRepository.save({ await this.outboxRepository.save({

View File

@ -121,11 +121,15 @@ export class ContributionDistributionPublisherService {
return result.systemContributions.map((sys) => ({ return result.systemContributions.map((sys) => ({
accountType: sys.accountType, accountType: sys.accountType,
amount: sys.amount.value.toString(), amount: sys.amount.value.toString(),
// 直接使用 regionCode没有就用传入的参数
provinceCode: provinceCode:
sys.accountType === 'PROVINCE' || sys.accountType === 'CITY' sys.accountType === 'PROVINCE'
? provinceCode ? sys.regionCode || provinceCode
: sys.accountType === 'CITY'
? sys.regionCode || cityCode
: undefined, : undefined,
cityCode: sys.accountType === 'CITY' ? cityCode : undefined, cityCode:
sys.accountType === 'CITY' ? sys.regionCode || cityCode : undefined,
neverExpires: sys.accountType === 'OPERATION', // 运营账户永不过期 neverExpires: sys.accountType === 'OPERATION', // 运营账户永不过期
})); }));
} }

View File

@ -1,16 +1,14 @@
/** /**
* *
* 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_440000, CITY_440100 等 public readonly accountType: string, // OPERATION / PROVINCE / CITY / HEADQUARTERS
public readonly baseType: string, // 基础类型: OPERATION / PROVINCE / CITY / HEADQUARTERS public readonly regionCode: string | null, // 省/市代码,如 440000, 440100
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,
@ -20,7 +18,6 @@ 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, regionCode: this.regionCode,
name: this.name, name: this.name,
contributionBalance: this.contributionBalance, contributionBalance: this.contributionBalance,

View File

@ -5,16 +5,12 @@ 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 { export interface SystemContributionAllocation {
accountType: string; // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等 accountType: 'OPERATION' | 'PROVINCE' | 'CITY' | 'HEADQUARTERS';
baseType: SystemAccountBaseType; // 基础类型 regionCode: string | null; // 省市代码,如 440000、440100
regionCode: string | null; // 区域代码
rate: DistributionRate; rate: DistributionRate;
amount: ContributionAmount; amount: ContributionAmount;
} }
@ -98,7 +94,6 @@ export class ContributionCalculatorService {
// 运营账户(全国)- 12% // 运营账户(全国)- 12%
result.systemContributions.push({ result.systemContributions.push({
accountType: 'OPERATION', accountType: 'OPERATION',
baseType: 'OPERATION',
regionCode: null, regionCode: null,
rate: DistributionRate.OPERATION, rate: DistributionRate.OPERATION,
amount: totalContribution.multiply(DistributionRate.OPERATION.value), amount: totalContribution.multiply(DistributionRate.OPERATION.value),
@ -106,47 +101,21 @@ export class ContributionCalculatorService {
// 省公司账户 - 1%(按认种选择的省份) // 省公司账户 - 1%(按认种选择的省份)
const provinceCode = adoption.selectedProvince; const provinceCode = adoption.selectedProvince;
if (provinceCode) {
// 有省份时分配到具体省份账户
result.systemContributions.push({
accountType: `PROVINCE_${provinceCode}`,
baseType: 'PROVINCE',
regionCode: provinceCode,
rate: DistributionRate.PROVINCE,
amount: totalContribution.multiply(DistributionRate.PROVINCE.value),
});
} else {
// 无省份时归汇总账户
result.systemContributions.push({ result.systemContributions.push({
accountType: 'PROVINCE', accountType: 'PROVINCE',
baseType: 'PROVINCE', regionCode: provinceCode || null,
regionCode: null,
rate: DistributionRate.PROVINCE, rate: DistributionRate.PROVINCE,
amount: totalContribution.multiply(DistributionRate.PROVINCE.value), amount: totalContribution.multiply(DistributionRate.PROVINCE.value),
}); });
}
// 市公司账户 - 2%(按认种选择的城市) // 市公司账户 - 2%(按认种选择的城市)
const cityCode = adoption.selectedCity; const cityCode = adoption.selectedCity;
if (cityCode) {
// 有城市时分配到具体城市账户
result.systemContributions.push({
accountType: `CITY_${cityCode}`,
baseType: 'CITY',
regionCode: cityCode,
rate: DistributionRate.CITY,
amount: totalContribution.multiply(DistributionRate.CITY.value),
});
} else {
// 无城市时归汇总账户
result.systemContributions.push({ result.systemContributions.push({
accountType: 'CITY', accountType: 'CITY',
baseType: 'CITY', regionCode: cityCode || null,
regionCode: null,
rate: DistributionRate.CITY, rate: DistributionRate.CITY,
amount: totalContribution.multiply(DistributionRate.CITY.value), amount: totalContribution.multiply(DistributionRate.CITY.value),
}); });
}
// 3. 团队贡献值 (15%) // 3. 团队贡献值 (15%)
this.distributeTeamContribution( this.distributeTeamContribution(

View File

@ -2,14 +2,12 @@ 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: string; // 组合键: PROVINCE_440000, CITY_440100 等 accountType: SystemAccountType;
baseType: SystemAccountBaseType; // 基础类型 regionCode: string | null; // 省/市代码
regionCode: string | null; // 区域代码
name: string; name: string;
contributionBalance: ContributionAmount; contributionBalance: ContributionAmount;
contributionNeverExpires: boolean; contributionNeverExpires: boolean;
@ -40,11 +38,16 @@ export class SystemAccountRepository {
} }
/** /**
* accountType * accountType + regionCode
*/ */
async findByType(accountType: string): Promise<SystemAccount | null> { async findByTypeAndRegion(
accountType: SystemAccountType,
regionCode: string | null,
): Promise<SystemAccount | null> {
const record = await this.client.systemAccount.findUnique({ const record = await this.client.systemAccount.findUnique({
where: { accountType }, where: {
accountType_regionCode: { accountType, regionCode },
},
}); });
if (!record) { if (!record) {
@ -55,12 +58,12 @@ export class SystemAccountRepository {
} }
/** /**
* CITY * CITY
*/ */
async findByBaseType(baseType: SystemAccountBaseType): Promise<SystemAccount[]> { async findByType(accountType: SystemAccountType): Promise<SystemAccount[]> {
const records = await this.client.systemAccount.findMany({ const records = await this.client.systemAccount.findMany({
where: { baseType }, where: { accountType },
orderBy: { accountType: 'asc' }, orderBy: { regionCode: 'asc' },
}); });
return records.map((r) => this.toSystemAccount(r)); return records.map((r) => this.toSystemAccount(r));
@ -68,29 +71,28 @@ export class SystemAccountRepository {
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' }, { regionCode: 'asc' }],
}); });
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: string; baseType: SystemAccountBaseType; name: string }[] = [ const accounts: { accountType: SystemAccountType; name: string }[] = [
{ accountType: 'OPERATION', baseType: 'OPERATION', name: '运营账户' }, { accountType: 'OPERATION', name: '运营账户' },
{ accountType: 'PROVINCE', baseType: 'PROVINCE', name: '省公司账户' }, { accountType: 'HEADQUARTERS', name: '总部账户' },
{ accountType: 'CITY', baseType: 'CITY', name: '市公司账户' },
{ accountType: 'HEADQUARTERS', baseType: 'HEADQUARTERS', name: '总部账户' },
]; ];
for (const account of accounts) { for (const account of accounts) {
await this.client.systemAccount.upsert({ await this.client.systemAccount.upsert({
where: { accountType: account.accountType }, where: {
accountType_regionCode: { accountType: account.accountType, regionCode: null },
},
create: { create: {
accountType: account.accountType, accountType: account.accountType,
baseType: account.baseType,
regionCode: null, regionCode: null,
name: account.name, name: account.name,
contributionBalance: 0, contributionBalance: 0,
@ -103,24 +105,20 @@ export class SystemAccountRepository {
/** /**
* *
* @param accountType PROVINCE_440000, CITY_440100
* @param baseType 基础类型: OPERATION, PROVINCE, CITY, HEADQUARTERS
* @param regionCode 区域代码: / 440000, 440100
* @param amount
*/ */
async addContribution( async addContribution(
accountType: string, accountType: SystemAccountType,
baseType: SystemAccountBaseType,
regionCode: string | null, regionCode: string | null,
amount: ContributionAmount, amount: ContributionAmount,
): Promise<void> { ): Promise<void> {
const name = this.getAccountName(baseType, regionCode); const name = this.getAccountName(accountType, regionCode);
await this.client.systemAccount.upsert({ await this.client.systemAccount.upsert({
where: { accountType }, where: {
accountType_regionCode: { accountType, regionCode },
},
create: { create: {
accountType, accountType,
baseType,
regionCode, regionCode,
name, name,
contributionBalance: amount.value, contributionBalance: amount.value,
@ -135,21 +133,22 @@ export class SystemAccountRepository {
/** /**
* *
*/ */
private getAccountName(baseType: SystemAccountBaseType, regionCode: string | null): string { private getAccountName(accountType: SystemAccountType, regionCode: string | null): string {
if (!regionCode) { if (!regionCode) {
const names: Record<SystemAccountBaseType, string> = { const names: Record<SystemAccountType, string> = {
OPERATION: '运营账户', OPERATION: '运营账户',
PROVINCE: '省公司账户', PROVINCE: '省公司账户',
CITY: '市公司账户', CITY: '市公司账户',
HEADQUARTERS: '总部账户', HEADQUARTERS: '总部账户',
}; };
return names[baseType] || baseType; return names[accountType] || accountType;
} }
return `${regionCode}账户`; return `${regionCode}账户`;
} }
async saveContributionRecord(record: { async saveContributionRecord(record: {
systemAccountType: string; // 改为 string 支持组合键 accountType: SystemAccountType;
regionCode: string | null;
sourceAdoptionId: bigint; sourceAdoptionId: bigint;
sourceAccountSequence: string; sourceAccountSequence: string;
distributionRate: number; distributionRate: number;
@ -157,9 +156,9 @@ export class SystemAccountRepository {
effectiveDate: Date; effectiveDate: Date;
expireDate?: Date | null; expireDate?: Date | null;
}): Promise<SystemContributionRecord> { }): Promise<SystemContributionRecord> {
const systemAccount = await this.findByType(record.systemAccountType); const systemAccount = await this.findByTypeAndRegion(record.accountType, record.regionCode);
if (!systemAccount) { if (!systemAccount) {
throw new Error(`System account ${record.systemAccountType} not found`); throw new Error(`System account ${record.accountType}:${record.regionCode} not found`);
} }
const created = await this.client.systemContributionRecord.create({ const created = await this.client.systemContributionRecord.create({
@ -177,42 +176,13 @@ export class SystemAccountRepository {
return this.toContributionRecord(created); return this.toContributionRecord(created);
} }
async saveContributionRecords(records: {
systemAccountType: string; // 改为 string 支持组合键
sourceAdoptionId: bigint;
sourceAccountSequence: string;
distributionRate: number;
amount: ContributionAmount;
effectiveDate: Date;
expireDate?: Date | null;
}[]): Promise<void> {
if (records.length === 0) return;
const systemAccounts = await this.findAll();
const accountMap = new Map<string, bigint>();
for (const account of systemAccounts) {
accountMap.set(account.accountType, account.id);
}
await this.client.systemContributionRecord.createMany({
data: records.map((r) => ({
systemAccountId: accountMap.get(r.systemAccountType)!,
sourceAdoptionId: r.sourceAdoptionId,
sourceAccountSequence: r.sourceAccountSequence,
distributionRate: r.distributionRate,
amount: r.amount.value,
effectiveDate: r.effectiveDate,
expireDate: r.expireDate ?? null,
})),
});
}
async findContributionRecords( async findContributionRecords(
systemAccountType: string, // 改为 string 支持组合键 accountType: SystemAccountType,
regionCode: string | null,
page: number, page: number,
pageSize: number, pageSize: number,
): Promise<{ data: SystemContributionRecord[]; total: number }> { ): Promise<{ data: SystemContributionRecord[]; total: number }> {
const systemAccount = await this.findByType(systemAccountType); const systemAccount = await this.findByTypeAndRegion(accountType, regionCode);
if (!systemAccount) { if (!systemAccount) {
return { data: [], total: 0 }; return { data: [], total: 0 };
} }
@ -238,8 +208,7 @@ export class SystemAccountRepository {
private toSystemAccount(record: any): SystemAccount { private toSystemAccount(record: any): SystemAccount {
return { return {
id: record.id, id: record.id,
accountType: record.accountType, accountType: record.accountType as SystemAccountType,
baseType: record.baseType as SystemAccountBaseType,
regionCode: record.regionCode, regionCode: record.regionCode,
name: record.name, name: record.name,
contributionBalance: new ContributionAmount(record.contributionBalance), contributionBalance: new ContributionAmount(record.contributionBalance),

View File

@ -302,11 +302,10 @@ CREATE TABLE "synced_circulation_pools" (
CONSTRAINT "synced_circulation_pools_pkey" PRIMARY KEY ("id") CONSTRAINT "synced_circulation_pools_pkey" PRIMARY KEY ("id")
); );
-- CreateTable -- CreateTable: 系统账户算力 (from contribution-service)
CREATE TABLE "synced_system_contributions" ( CREATE TABLE "synced_system_contributions" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"accountType" TEXT NOT NULL, "accountType" TEXT NOT NULL,
"base_type" TEXT NOT NULL DEFAULT '',
"region_code" TEXT, "region_code" TEXT,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"contributionBalance" DECIMAL(30,8) NOT NULL DEFAULT 0, "contributionBalance" DECIMAL(30,8) NOT NULL DEFAULT 0,
@ -689,11 +688,10 @@ CREATE UNIQUE INDEX "synced_daily_mining_stats_statDate_key" ON "synced_daily_mi
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "synced_day_klines_klineDate_key" ON "synced_day_klines"("klineDate"); CREATE UNIQUE INDEX "synced_day_klines_klineDate_key" ON "synced_day_klines"("klineDate");
-- CreateIndex -- CreateIndex: synced_system_contributions
CREATE UNIQUE INDEX "synced_system_contributions_accountType_key" ON "synced_system_contributions"("accountType"); -- 使用 accountType + region_code 复合唯一键
CREATE UNIQUE INDEX "synced_system_contributions_accountType_region_code_key" ON "synced_system_contributions"("accountType", "region_code");
-- CreateIndex (base_type 和 region_code 索引) CREATE INDEX "synced_system_contributions_accountType_idx" ON "synced_system_contributions"("accountType");
CREATE INDEX "synced_system_contributions_base_type_idx" ON "synced_system_contributions"("base_type");
CREATE INDEX "synced_system_contributions_region_code_idx" ON "synced_system_contributions"("region_code"); CREATE INDEX "synced_system_contributions_region_code_idx" ON "synced_system_contributions"("region_code");
-- CreateIndex -- CreateIndex
@ -869,11 +867,12 @@ ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_adminId_fkey" FOREIGN KEY ("
-- 用于存储从 contribution-service 同步的系统账户算力来源明细 -- 用于存储从 contribution-service 同步的系统账户算力来源明细
-- ============================================================================ -- ============================================================================
-- CreateTable -- CreateTable: 系统账户算力明细 (from contribution-service)
CREATE TABLE "synced_system_contribution_records" ( CREATE TABLE "synced_system_contribution_records" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"original_record_id" BIGINT NOT NULL, "original_record_id" BIGINT NOT NULL,
"system_account_type" TEXT NOT NULL, "account_type" TEXT NOT NULL,
"region_code" TEXT,
"source_adoption_id" BIGINT NOT NULL, "source_adoption_id" BIGINT NOT NULL,
"source_account_sequence" TEXT NOT NULL, "source_account_sequence" TEXT NOT NULL,
"distribution_rate" DECIMAL(10,6) NOT NULL, "distribution_rate" DECIMAL(10,6) NOT NULL,
@ -890,7 +889,7 @@ CREATE TABLE "synced_system_contribution_records" (
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "synced_system_contribution_records_original_record_id_key" ON "synced_system_contribution_records"("original_record_id"); CREATE UNIQUE INDEX "synced_system_contribution_records_original_record_id_key" ON "synced_system_contribution_records"("original_record_id");
CREATE INDEX "synced_system_contribution_records_system_account_type_idx" ON "synced_system_contribution_records"("system_account_type"); CREATE INDEX "synced_system_contribution_records_account_type_region_code_idx" ON "synced_system_contribution_records"("account_type", "region_code");
CREATE INDEX "synced_system_contribution_records_source_adoption_id_idx" ON "synced_system_contribution_records"("source_adoption_id"); CREATE INDEX "synced_system_contribution_records_source_adoption_id_idx" ON "synced_system_contribution_records"("source_adoption_id");
CREATE INDEX "synced_system_contribution_records_source_account_sequence_idx" ON "synced_system_contribution_records"("source_account_sequence"); CREATE INDEX "synced_system_contribution_records_source_account_sequence_idx" ON "synced_system_contribution_records"("source_account_sequence");
CREATE INDEX "synced_system_contribution_records_created_at_idx" ON "synced_system_contribution_records"("created_at" DESC); CREATE INDEX "synced_system_contribution_records_created_at_idx" ON "synced_system_contribution_records"("created_at" DESC);

View File

@ -422,19 +422,16 @@ model SyncedCirculationPool {
model SyncedSystemContribution { model SyncedSystemContribution {
id String @id @default(uuid()) id String @id @default(uuid())
accountType String @unique // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等 accountType String // OPERATION / PROVINCE / CITY / HEADQUARTERS
baseType String @default("") @map("base_type") // 基础类型: OPERATION / PROVINCE / CITY / HEADQUARTERS regionCode String? @map("region_code") // 省/市代码,如 440000, 440100
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
// 关联算力明细记录 @@unique([accountType, regionCode])
records SyncedSystemContributionRecord[] @@index([accountType])
@@index([baseType])
@@index([regionCode]) @@index([regionCode])
@@map("synced_system_contributions") @@map("synced_system_contributions")
} }
@ -446,7 +443,10 @@ model SyncedSystemContribution {
model SyncedSystemContributionRecord { model SyncedSystemContributionRecord {
id String @id @default(uuid()) id String @id @default(uuid())
originalRecordId BigInt @unique @map("original_record_id") // contribution-service 中的原始 ID originalRecordId BigInt @unique @map("original_record_id") // contribution-service 中的原始 ID
systemAccountType String @map("system_account_type") // 关联的系统账户类型 (组合键)
// 系统账户信息(冗余存储,便于查询)
accountType String @map("account_type") // OPERATION / PROVINCE / CITY / HEADQUARTERS
regionCode String? @map("region_code") // 省/市代码
// 来源信息 // 来源信息
sourceAdoptionId BigInt @map("source_adoption_id") // 来源认种ID sourceAdoptionId BigInt @map("source_adoption_id") // 来源认种ID
@ -465,10 +465,7 @@ model SyncedSystemContributionRecord {
syncedAt DateTime @default(now()) syncedAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
// 关联系统账户 @@index([accountType, regionCode])
systemContribution SyncedSystemContribution @relation(fields: [systemAccountType], references: [accountType])
@@index([systemAccountType])
@@index([sourceAdoptionId]) @@index([sourceAdoptionId])
@@index([sourceAccountSequence]) @@index([sourceAccountSequence])
@@index([createdAt(sort: Desc)]) @@index([createdAt(sort: Desc)])

View File

@ -22,16 +22,19 @@ export class SystemAccountsController {
@Get(':accountType/records') @Get(':accountType/records')
@ApiOperation({ summary: '获取系统账户挖矿记录' }) @ApiOperation({ summary: '获取系统账户挖矿记录' })
@ApiParam({ name: 'accountType', type: String, description: '系统账户类型' }) @ApiParam({ name: 'accountType', type: String, description: '系统账户类型OPERATION/PROVINCE/CITY/HEADQUARTERS' })
@ApiQuery({ name: 'regionCode', required: false, type: String, description: '区域代码(省/市代码)' })
@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(
@Param('accountType') accountType: string, @Param('accountType') accountType: string,
@Query('regionCode') regionCode?: string,
@Query('page') page?: number, @Query('page') page?: number,
@Query('pageSize') pageSize?: number, @Query('pageSize') pageSize?: number,
) { ) {
return this.systemAccountsService.getSystemAccountMiningRecords( return this.systemAccountsService.getSystemAccountMiningRecords(
accountType, accountType,
regionCode || null,
page ?? 1, page ?? 1,
pageSize ?? 20, pageSize ?? 20,
); );
@ -39,16 +42,19 @@ export class SystemAccountsController {
@Get(':accountType/transactions') @Get(':accountType/transactions')
@ApiOperation({ summary: '获取系统账户交易记录' }) @ApiOperation({ summary: '获取系统账户交易记录' })
@ApiParam({ name: 'accountType', type: String, description: '系统账户类型' }) @ApiParam({ name: 'accountType', type: String, description: '系统账户类型OPERATION/PROVINCE/CITY/HEADQUARTERS' })
@ApiQuery({ name: 'regionCode', required: false, type: String, description: '区域代码(省/市代码)' })
@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(
@Param('accountType') accountType: string, @Param('accountType') accountType: string,
@Query('regionCode') regionCode?: string,
@Query('page') page?: number, @Query('page') page?: number,
@Query('pageSize') pageSize?: number, @Query('pageSize') pageSize?: number,
) { ) {
return this.systemAccountsService.getSystemAccountTransactions( return this.systemAccountsService.getSystemAccountTransactions(
accountType, accountType,
regionCode || null,
page ?? 1, page ?? 1,
pageSize ?? 20, pageSize ?? 20,
); );
@ -57,22 +63,25 @@ export class SystemAccountsController {
@Get(':accountType/contributions') @Get(':accountType/contributions')
@ApiOperation({ @ApiOperation({
summary: '获取系统账户算力来源明细', summary: '获取系统账户算力来源明细',
description: '显示该账户的每笔算力来自哪个认种订单,支持按省市细分的账户类型(如 CITY_440100, PROVINCE_440000', description: '显示该账户的每笔算力来自哪个认种订单',
}) })
@ApiParam({ @ApiParam({
name: 'accountType', name: 'accountType',
type: String, type: String,
description: '系统账户类型(组合键),如 OPERATION, PROVINCE_440000, CITY_440100', description: '系统账户类型(OPERATION/PROVINCE/CITY/HEADQUARTERS',
}) })
@ApiQuery({ name: 'regionCode', required: false, type: String, description: '区域代码(省/市代码)' })
@ApiQuery({ name: 'page', required: false, type: Number, description: '页码默认1' }) @ApiQuery({ name: 'page', required: false, type: Number, description: '页码默认1' })
@ApiQuery({ name: 'pageSize', required: false, type: Number, description: '每页数量默认20' }) @ApiQuery({ name: 'pageSize', required: false, type: Number, description: '每页数量默认20' })
async getSystemAccountContributionRecords( async getSystemAccountContributionRecords(
@Param('accountType') accountType: string, @Param('accountType') accountType: string,
@Query('regionCode') regionCode?: string,
@Query('page') page?: number, @Query('page') page?: number,
@Query('pageSize') pageSize?: number, @Query('pageSize') pageSize?: number,
) { ) {
return this.systemAccountsService.getSystemAccountContributionRecords( return this.systemAccountsService.getSystemAccountContributionRecords(
accountType, accountType,
regionCode || null,
page ?? 1, page ?? 1,
pageSize ?? 20, pageSize ?? 20,
); );
@ -86,11 +95,13 @@ export class SystemAccountsController {
@ApiParam({ @ApiParam({
name: 'accountType', name: 'accountType',
type: String, type: String,
description: '系统账户类型(组合键),如 OPERATION, PROVINCE_440000, CITY_440100', description: '系统账户类型(OPERATION/PROVINCE/CITY/HEADQUARTERS',
}) })
@ApiQuery({ name: 'regionCode', required: false, type: String, description: '区域代码(省/市代码)' })
async getSystemAccountContributionStats( async getSystemAccountContributionStats(
@Param('accountType') accountType: string, @Param('accountType') accountType: string,
@Query('regionCode') regionCode?: string,
) { ) {
return this.systemAccountsService.getSystemAccountContributionStats(accountType); return this.systemAccountsService.getSystemAccountContributionStats(accountType, regionCode || null);
} }
} }

View File

@ -5,9 +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; // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等 id: string;
baseType: string; // 基础类型: OPERATION / PROVINCE / CITY / HEADQUARTERS accountType: string; // OPERATION / PROVINCE / CITY / HEADQUARTERS
regionCode: string | null; // 区域代码: 省/市代码,如 440000, 440100 regionCode: string | null; // 省/市代码,如 440000, 440100
name: string; name: string;
totalMined: string; totalMined: string;
availableBalance: string; availableBalance: string;
@ -48,7 +48,11 @@ export class SystemAccountsService {
const miningDataMap = new Map<string, MiningServiceSystemAccount>(); const miningDataMap = new Map<string, MiningServiceSystemAccount>();
for (const account of response.data.accounts) { for (const account of response.data.accounts) {
miningDataMap.set(account.accountType, account); // 使用 accountType:regionCode 作为 key与 contribution 表一致
const key = account.regionCode
? `${account.accountType}:${account.regionCode}`
: account.accountType;
miningDataMap.set(key, account);
} }
return miningDataMap; return miningDataMap;
@ -82,12 +86,13 @@ export class SystemAccountsService {
// 从 mining-service 获取挖矿数据 // 从 mining-service 获取挖矿数据
const miningDataMap = await this.fetchMiningServiceSystemAccounts(); const miningDataMap = await this.fetchMiningServiceSystemAccounts();
// 构建算力数据映射 - 支持两种匹配方式 // 构建算力数据映射 - 使用 accountType:regionCode 格式
// 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); const key = contrib.regionCode
? `${contrib.accountType}:${contrib.regionCode}`
: contrib.accountType;
contributionMap.set(key, contrib);
} }
// 构建返回数据 // 构建返回数据
@ -96,19 +101,19 @@ export class SystemAccountsService {
let contrib = null; let contrib = null;
let miningData = null; let miningData = null;
// 1. 尝试用 regionCode 匹配(针对省市账户) // 1. 尝试用 accountType:regionCode 匹配(针对省市账户)
if (account.code) { if (account.code) {
// 从 code 提取区域代码(如 "CITY-440100" -> "440100" // 从 code 提取区域代码(如 "CITY-440100" -> "440100"
const regionCode = this.extractRegionCodeFromCode(account.code); const regionCode = this.extractRegionCodeFromCode(account.code);
if (regionCode) { if (regionCode) {
// 构建组合键(如 CITY_440100 // 使用 accountType:regionCode 格式(如 CITY:440100
const accountTypeKey = `${account.accountType}_${regionCode}`; const matchKey = `${account.accountType}:${regionCode}`;
contrib = contributionMap.get(accountTypeKey); contrib = contributionMap.get(matchKey);
miningData = miningDataMap.get(accountTypeKey); miningData = miningDataMap.get(matchKey);
} }
} }
// 2. 回退到直接 accountType 匹配(汇总账户 // 2. 回退到直接 accountType 匹配(汇总账户,如 OPERATION, HEADQUARTERS
if (!contrib) { if (!contrib) {
contrib = contributionMap.get(account.accountType); contrib = contributionMap.get(account.accountType);
} }
@ -241,9 +246,14 @@ export class SystemAccountsService {
/** /**
* *
* @param accountType OPERATION/PROVINCE/CITY/HEADQUARTERS
* @param regionCode / 440000, 440100
* @param page
* @param pageSize
*/ */
async getSystemAccountMiningRecords( async getSystemAccountMiningRecords(
accountType: string, accountType: string,
regionCode: string | null,
page: number = 1, page: number = 1,
pageSize: number = 20, pageSize: number = 20,
) { ) {
@ -253,12 +263,15 @@ export class SystemAccountsService {
); );
try { try {
const params: Record<string, any> = { page, pageSize };
if (regionCode) {
params.regionCode = regionCode;
}
const response = await firstValueFrom( const response = await firstValueFrom(
this.httpService.get( this.httpService.get(
`${miningServiceUrl}/admin/system-accounts/${accountType}/records`, `${miningServiceUrl}/admin/system-accounts/${accountType}/records`,
{ { params },
params: { page, pageSize },
},
), ),
); );
@ -267,15 +280,20 @@ export class SystemAccountsService {
this.logger.warn( this.logger.warn(
`Failed to fetch system account mining records: ${error.message}`, `Failed to fetch system account mining records: ${error.message}`,
); );
return { records: [], total: 0, page, pageSize }; return { records: [], total: 0, page, pageSize, accountType, regionCode };
} }
} }
/** /**
* *
* @param accountType OPERATION/PROVINCE/CITY/HEADQUARTERS
* @param regionCode / 440000, 440100
* @param page
* @param pageSize
*/ */
async getSystemAccountTransactions( async getSystemAccountTransactions(
accountType: string, accountType: string,
regionCode: string | null,
page: number = 1, page: number = 1,
pageSize: number = 20, pageSize: number = 20,
) { ) {
@ -285,12 +303,15 @@ export class SystemAccountsService {
); );
try { try {
const params: Record<string, any> = { page, pageSize };
if (regionCode) {
params.regionCode = regionCode;
}
const response = await firstValueFrom( const response = await firstValueFrom(
this.httpService.get( this.httpService.get(
`${miningServiceUrl}/admin/system-accounts/${accountType}/transactions`, `${miningServiceUrl}/admin/system-accounts/${accountType}/transactions`,
{ { params },
params: { page, pageSize },
},
), ),
); );
@ -299,7 +320,7 @@ export class SystemAccountsService {
this.logger.warn( this.logger.warn(
`Failed to fetch system account transactions: ${error.message}`, `Failed to fetch system account transactions: ${error.message}`,
); );
return { transactions: [], total: 0, page, pageSize }; return { transactions: [], total: 0, page, pageSize, accountType, regionCode };
} }
} }
@ -307,32 +328,38 @@ export class SystemAccountsService {
* *
* *
* *
* @param accountType CITY_440100, PROVINCE_440000, OPERATION * @param accountType OPERATION/PROVINCE/CITY/HEADQUARTERS
* @param regionCode / 440000, 440100
* @param page * @param page
* @param pageSize * @param pageSize
*/ */
async getSystemAccountContributionRecords( async getSystemAccountContributionRecords(
accountType: string, accountType: string,
regionCode: string | null,
page: number = 1, page: number = 1,
pageSize: number = 20, pageSize: number = 20,
) { ) {
const whereClause = regionCode
? { accountType, regionCode }
: { accountType, regionCode: null };
const [records, total] = await Promise.all([ const [records, total] = await Promise.all([
this.prisma.syncedSystemContributionRecord.findMany({ this.prisma.syncedSystemContributionRecord.findMany({
where: { systemAccountType: accountType }, where: whereClause,
skip: (page - 1) * pageSize, skip: (page - 1) * pageSize,
take: pageSize, take: pageSize,
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
}), }),
this.prisma.syncedSystemContributionRecord.count({ this.prisma.syncedSystemContributionRecord.count({
where: { systemAccountType: accountType }, where: whereClause,
}), }),
]); ]);
return { return {
records: records.map((record) => ({ records: records.map((record) => ({
id: record.id,
originalRecordId: record.originalRecordId.toString(), originalRecordId: record.originalRecordId.toString(),
systemAccountType: record.systemAccountType, accountType: record.accountType,
regionCode: record.regionCode,
sourceAdoptionId: record.sourceAdoptionId.toString(), sourceAdoptionId: record.sourceAdoptionId.toString(),
sourceAccountSequence: record.sourceAccountSequence, sourceAccountSequence: record.sourceAccountSequence,
distributionRate: record.distributionRate.toString(), distributionRate: record.distributionRate.toString(),
@ -354,15 +381,19 @@ export class SystemAccountsService {
* *
* *
*/ */
async getSystemAccountContributionStats(accountType: string) { async getSystemAccountContributionStats(accountType: string, regionCode: string | null) {
// 获取算力账户信息 // 获取算力账户信息
const contribution = await this.prisma.syncedSystemContribution.findUnique({ const contribution = await this.prisma.syncedSystemContribution.findUnique({
where: { accountType }, where: { accountType_regionCode: { accountType, regionCode } },
}); });
const whereClause = regionCode
? { accountType, regionCode }
: { accountType, regionCode: null };
// 获取明细记录统计 // 获取明细记录统计
const recordStats = await this.prisma.syncedSystemContributionRecord.aggregate({ const recordStats = await this.prisma.syncedSystemContributionRecord.aggregate({
where: { systemAccountType: accountType }, where: whereClause,
_count: true, _count: true,
_sum: { amount: true }, _sum: { amount: true },
}); });
@ -370,20 +401,19 @@ export class SystemAccountsService {
// 获取来源认种订单数量(去重) // 获取来源认种订单数量(去重)
const uniqueAdoptions = await this.prisma.syncedSystemContributionRecord.groupBy({ const uniqueAdoptions = await this.prisma.syncedSystemContributionRecord.groupBy({
by: ['sourceAdoptionId'], by: ['sourceAdoptionId'],
where: { systemAccountType: accountType }, where: whereClause,
}); });
// 获取来源用户数量(去重) // 获取来源用户数量(去重)
const uniqueUsers = await this.prisma.syncedSystemContributionRecord.groupBy({ const uniqueUsers = await this.prisma.syncedSystemContributionRecord.groupBy({
by: ['sourceAccountSequence'], by: ['sourceAccountSequence'],
where: { systemAccountType: accountType }, where: whereClause,
}); });
return { return {
accountType, accountType,
regionCode,
name: contribution?.name || accountType, name: contribution?.name || accountType,
baseType: contribution?.baseType || this.extractBaseTypeFromAccountType(accountType),
regionCode: contribution?.regionCode || null,
totalContribution: contribution?.contributionBalance?.toString() || '0', totalContribution: contribution?.contributionBalance?.toString() || '0',
recordCount: recordStats._count, recordCount: recordStats._count,
sumFromRecords: recordStats._sum?.amount?.toString() || '0', sumFromRecords: recordStats._sum?.amount?.toString() || '0',
@ -391,13 +421,4 @@ export class SystemAccountsService {
uniqueUserCount: uniqueUsers.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;
}
} }

View File

@ -559,44 +559,33 @@ export class CdcSyncService implements OnModuleInit {
/** /**
* SystemAccountSynced - * SystemAccountSynced -
* contribution-service * contribution-service
* PROVINCE_440000, CITY_440100 * accountType: OPERATION / PROVINCE / CITY / HEADQUARTERS
* regionCode: / 440000, 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 accountType = payload.accountType; // OPERATION / PROVINCE / CITY / HEADQUARTERS
const baseType = payload.baseType || this.extractBaseType(payload.accountType);
const regionCode = payload.regionCode || null; const regionCode = payload.regionCode || null;
// 使用 accountType + regionCode 作为复合唯一键
await tx.syncedSystemContribution.upsert({ await tx.syncedSystemContribution.upsert({
where: { accountType: payload.accountType }, where: {
accountType_regionCode: { accountType, regionCode },
},
create: { create: {
accountType: payload.accountType, accountType,
baseType,
regionCode, 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 - * SystemContributionRecordCreated -
* contribution-service * contribution-service
@ -604,11 +593,15 @@ export class CdcSyncService implements OnModuleInit {
private async handleSystemContributionRecordCreated(event: ServiceEvent, tx: TransactionClient): Promise<void> { private async handleSystemContributionRecordCreated(event: ServiceEvent, tx: TransactionClient): Promise<void> {
const { payload } = event; const { payload } = event;
const accountType = payload.accountType;
const regionCode = payload.regionCode || null;
await tx.syncedSystemContributionRecord.upsert({ await tx.syncedSystemContributionRecord.upsert({
where: { originalRecordId: BigInt(payload.recordId) }, where: { originalRecordId: BigInt(payload.recordId) },
create: { create: {
originalRecordId: BigInt(payload.recordId), originalRecordId: BigInt(payload.recordId),
systemAccountType: payload.systemAccountType, accountType,
regionCode,
sourceAdoptionId: BigInt(payload.sourceAdoptionId), sourceAdoptionId: BigInt(payload.sourceAdoptionId),
sourceAccountSequence: payload.sourceAccountSequence, sourceAccountSequence: payload.sourceAccountSequence,
distributionRate: payload.distributionRate, distributionRate: payload.distributionRate,
@ -619,7 +612,8 @@ export class CdcSyncService implements OnModuleInit {
createdAt: new Date(payload.createdAt), createdAt: new Date(payload.createdAt),
}, },
update: { update: {
systemAccountType: payload.systemAccountType, accountType,
regionCode,
sourceAdoptionId: BigInt(payload.sourceAdoptionId), sourceAdoptionId: BigInt(payload.sourceAdoptionId),
sourceAccountSequence: payload.sourceAccountSequence, sourceAccountSequence: payload.sourceAccountSequence,
distributionRate: payload.distributionRate, distributionRate: payload.distributionRate,
@ -630,7 +624,7 @@ export class CdcSyncService implements OnModuleInit {
}); });
this.logger.debug( this.logger.debug(
`Synced system contribution record: recordId=${payload.recordId}, account=${payload.systemAccountType}, amount=${payload.amount}`, `Synced system contribution record: recordId=${payload.recordId}, account=${accountType}:${regionCode}, amount=${payload.amount}`,
); );
} }

View File

@ -99,11 +99,10 @@ CREATE TABLE "mining_transactions" (
CONSTRAINT "mining_transactions_pkey" PRIMARY KEY ("id") CONSTRAINT "mining_transactions_pkey" PRIMARY KEY ("id")
); );
-- CreateTable: 系统挖矿账户 (直接使用 TEXT 组合键) -- CreateTable: 系统挖矿账户
CREATE TABLE "system_mining_accounts" ( CREATE TABLE "system_mining_accounts" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"account_type" TEXT NOT NULL, "account_type" TEXT NOT NULL,
"base_type" TEXT NOT NULL,
"region_code" TEXT, "region_code" TEXT,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"totalMined" DECIMAL(30, 8) NOT NULL DEFAULT 0, "totalMined" DECIMAL(30, 8) NOT NULL DEFAULT 0,
@ -119,7 +118,7 @@ CREATE TABLE "system_mining_accounts" (
-- CreateTable: 系统账户挖矿记录 -- CreateTable: 系统账户挖矿记录
CREATE TABLE "system_mining_records" ( CREATE TABLE "system_mining_records" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"account_type" TEXT NOT NULL, "system_account_id" TEXT NOT NULL,
"mining_minute" TIMESTAMP(3) NOT NULL, "mining_minute" TIMESTAMP(3) NOT NULL,
"contribution_ratio" DECIMAL(30, 18) NOT NULL, "contribution_ratio" DECIMAL(30, 18) NOT NULL,
"total_contribution" DECIMAL(30, 8) NOT NULL, "total_contribution" DECIMAL(30, 8) NOT NULL,
@ -133,7 +132,7 @@ CREATE TABLE "system_mining_records" (
-- CreateTable: 系统账户交易流水 -- CreateTable: 系统账户交易流水
CREATE TABLE "system_mining_transactions" ( CREATE TABLE "system_mining_transactions" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"account_type" TEXT NOT NULL, "system_account_id" TEXT NOT NULL,
"type" TEXT NOT NULL, "type" TEXT NOT NULL,
"amount" DECIMAL(30, 8) NOT NULL, "amount" DECIMAL(30, 8) NOT NULL,
"balance_before" DECIMAL(30, 8) NOT NULL, "balance_before" DECIMAL(30, 8) NOT NULL,
@ -407,17 +406,17 @@ CREATE INDEX "mining_transactions_counterparty_account_seq_idx" ON "mining_trans
CREATE INDEX "mining_transactions_counterparty_user_id_idx" ON "mining_transactions"("counterparty_user_id"); CREATE INDEX "mining_transactions_counterparty_user_id_idx" ON "mining_transactions"("counterparty_user_id");
-- CreateIndex: system_mining_accounts -- CreateIndex: system_mining_accounts
CREATE UNIQUE INDEX "system_mining_accounts_account_type_key" ON "system_mining_accounts"("account_type"); CREATE UNIQUE INDEX "system_mining_accounts_account_type_region_code_key" ON "system_mining_accounts"("account_type", "region_code");
CREATE INDEX "system_mining_accounts_totalContribution_idx" ON "system_mining_accounts"("totalContribution" DESC); CREATE INDEX "system_mining_accounts_totalContribution_idx" ON "system_mining_accounts"("totalContribution" DESC);
CREATE INDEX "system_mining_accounts_base_type_idx" ON "system_mining_accounts"("base_type"); CREATE INDEX "system_mining_accounts_account_type_idx" ON "system_mining_accounts"("account_type");
CREATE INDEX "system_mining_accounts_region_code_idx" ON "system_mining_accounts"("region_code"); CREATE INDEX "system_mining_accounts_region_code_idx" ON "system_mining_accounts"("region_code");
-- CreateIndex: system_mining_records -- CreateIndex: system_mining_records
CREATE UNIQUE INDEX "system_mining_records_account_type_mining_minute_key" ON "system_mining_records"("account_type", "mining_minute"); CREATE UNIQUE INDEX "system_mining_records_system_account_id_mining_minute_key" ON "system_mining_records"("system_account_id", "mining_minute");
CREATE INDEX "system_mining_records_mining_minute_idx" ON "system_mining_records"("mining_minute"); CREATE INDEX "system_mining_records_mining_minute_idx" ON "system_mining_records"("mining_minute");
-- CreateIndex: system_mining_transactions -- CreateIndex: system_mining_transactions
CREATE INDEX "system_mining_transactions_account_type_created_at_idx" ON "system_mining_transactions"("account_type", "created_at" DESC); CREATE INDEX "system_mining_transactions_system_account_id_created_at_idx" ON "system_mining_transactions"("system_account_id", "created_at" DESC);
-- CreateIndex: pending_contribution_mining -- CreateIndex: pending_contribution_mining
CREATE UNIQUE INDEX "pending_contribution_mining_source_adoption_id_would_be_acco_key" CREATE UNIQUE INDEX "pending_contribution_mining_source_adoption_id_would_be_acco_key"
@ -533,12 +532,12 @@ ALTER TABLE "mining_records" ADD CONSTRAINT "mining_records_accountSequence_fkey
ALTER TABLE "mining_transactions" ADD CONSTRAINT "mining_transactions_accountSequence_fkey" FOREIGN KEY ("accountSequence") REFERENCES "mining_accounts"("accountSequence") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "mining_transactions" ADD CONSTRAINT "mining_transactions_accountSequence_fkey" FOREIGN KEY ("accountSequence") REFERENCES "mining_accounts"("accountSequence") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey: system_mining_records -- AddForeignKey: system_mining_records
ALTER TABLE "system_mining_records" ADD CONSTRAINT "system_mining_records_account_type_fkey" ALTER TABLE "system_mining_records" ADD CONSTRAINT "system_mining_records_system_account_id_fkey"
FOREIGN KEY ("account_type") REFERENCES "system_mining_accounts"("account_type") ON DELETE RESTRICT ON UPDATE CASCADE; FOREIGN KEY ("system_account_id") REFERENCES "system_mining_accounts"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey: system_mining_transactions -- AddForeignKey: system_mining_transactions
ALTER TABLE "system_mining_transactions" ADD CONSTRAINT "system_mining_transactions_account_type_fkey" ALTER TABLE "system_mining_transactions" ADD CONSTRAINT "system_mining_transactions_system_account_id_fkey"
FOREIGN KEY ("account_type") REFERENCES "system_mining_accounts"("account_type") ON DELETE RESTRICT ON UPDATE CASCADE; FOREIGN KEY ("system_account_id") REFERENCES "system_mining_accounts"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey: pending_mining_records -- AddForeignKey: pending_mining_records
ALTER TABLE "pending_mining_records" ADD CONSTRAINT "pending_mining_records_pending_contribution_id_fkey" ALTER TABLE "pending_mining_records" ADD CONSTRAINT "pending_mining_records_pending_contribution_id_fkey"
@ -550,11 +549,9 @@ ALTER TABLE "burn_records" ADD CONSTRAINT "burn_records_blackHoleId_fkey" FOREIG
-- AddForeignKey -- AddForeignKey
ALTER TABLE "pool_transactions" ADD CONSTRAINT "pool_transactions_pool_account_id_fkey" FOREIGN KEY ("pool_account_id") REFERENCES "pool_accounts"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "pool_transactions" ADD CONSTRAINT "pool_transactions_pool_account_id_fkey" FOREIGN KEY ("pool_account_id") REFERENCES "pool_accounts"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- 初始化系统账户 -- 初始化系统账户 (无 regionCode 的汇总账户)
INSERT INTO "system_mining_accounts" ("id", "account_type", "base_type", "name", "totalMined", "availableBalance", "totalContribution", "updated_at") INSERT INTO "system_mining_accounts" ("id", "account_type", "region_code", "name", "totalMined", "availableBalance", "totalContribution", "updated_at")
VALUES VALUES
(gen_random_uuid(), 'OPERATION', 'OPERATION', '运营账户', 0, 0, 0, NOW()), (gen_random_uuid(), 'OPERATION', NULL, '运营账户', 0, 0, 0, NOW()),
(gen_random_uuid(), 'PROVINCE', 'PROVINCE', '省公司账户', 0, 0, 0, NOW()), (gen_random_uuid(), 'HEADQUARTERS', NULL, '总部账户', 0, 0, 0, NOW())
(gen_random_uuid(), 'CITY', 'CITY', '市公司账户', 0, 0, 0, NOW()), ON CONFLICT ("account_type", "region_code") DO NOTHING;
(gen_random_uuid(), 'HEADQUARTERS', 'HEADQUARTERS', '总部账户', 0, 0, 0, NOW())
ON CONFLICT ("account_type") DO NOTHING;

View File

@ -53,13 +53,10 @@ model MiningEra {
// ==================== 系统账户(运营/省/市/总部)==================== // ==================== 系统账户(运营/省/市/总部)====================
// 系统挖矿账户 // 系统挖矿账户
// accountType 格式: OPERATION, PROVINCE, CITY, HEADQUARTERS (汇总账户)
// PROVINCE_440000, CITY_440100 等 (按省市细分的账户)
model SystemMiningAccount { model SystemMiningAccount {
id String @id @default(uuid()) id String @id @default(uuid())
accountType String @unique @map("account_type") // 组合键 accountType String @map("account_type") // OPERATION/PROVINCE/CITY/HEADQUARTERS
baseType String @map("base_type") // 基础类型: OPERATION/PROVINCE/CITY/HEADQUARTERS regionCode String? @map("region_code") // 省市代码,如 440000、440100
regionCode String? @map("region_code") // 区域代码
name String name String
totalMined Decimal @default(0) @db.Decimal(30, 8) // 总挖到的积分股 totalMined Decimal @default(0) @db.Decimal(30, 8) // 总挖到的积分股
availableBalance Decimal @default(0) @db.Decimal(30, 8) // 可用余额 availableBalance Decimal @default(0) @db.Decimal(30, 8) // 可用余额
@ -71,7 +68,8 @@ model SystemMiningAccount {
records SystemMiningRecord[] records SystemMiningRecord[]
transactions SystemMiningTransaction[] transactions SystemMiningTransaction[]
@@index([baseType]) @@unique([accountType, regionCode])
@@index([accountType])
@@index([regionCode]) @@index([regionCode])
@@index([totalContribution(sort: Desc)]) @@index([totalContribution(sort: Desc)])
@@map("system_mining_accounts") @@map("system_mining_accounts")
@ -80,7 +78,7 @@ model SystemMiningAccount {
// 系统账户挖矿记录(分钟级别汇总) // 系统账户挖矿记录(分钟级别汇总)
model SystemMiningRecord { model SystemMiningRecord {
id String @id @default(uuid()) id String @id @default(uuid())
accountType String @map("account_type") // 组合键 systemAccountId String @map("system_account_id")
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")
@ -88,9 +86,9 @@ model SystemMiningRecord {
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: [systemAccountId], references: [id])
@@unique([accountType, miningMinute]) @@unique([systemAccountId, miningMinute])
@@index([miningMinute]) @@index([miningMinute])
@@map("system_mining_records") @@map("system_mining_records")
} }
@ -98,7 +96,7 @@ model SystemMiningRecord {
// 系统账户交易流水 // 系统账户交易流水
model SystemMiningTransaction { model SystemMiningTransaction {
id String @id @default(uuid()) id String @id @default(uuid())
accountType String @map("account_type") // 组合键 systemAccountId String @map("system_account_id")
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")
@ -108,9 +106,9 @@ model SystemMiningTransaction {
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: [systemAccountId], references: [id])
@@index([accountType, createdAt(sort: Desc)]) @@index([systemAccountId, createdAt(sort: Desc)])
@@map("system_mining_transactions") @@map("system_mining_transactions")
} }

View File

@ -163,14 +163,14 @@ export class AdminController {
@ApiOperation({ summary: '获取系统账户挖矿状态' }) @ApiOperation({ summary: '获取系统账户挖矿状态' })
async getSystemAccounts() { async getSystemAccounts() {
const accounts = await this.prisma.systemMiningAccount.findMany({ const accounts = await this.prisma.systemMiningAccount.findMany({
orderBy: { accountType: 'asc' }, orderBy: [{ accountType: 'asc' }, { regionCode: 'asc' }],
}); });
return { return {
accounts: accounts.map((acc) => ({ accounts: accounts.map((acc) => ({
id: acc.id,
accountType: acc.accountType, accountType: acc.accountType,
baseType: acc.baseType, // 新增:基础类型 regionCode: acc.regionCode,
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(),
@ -184,35 +184,58 @@ 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_440000, CITY_440100, HEADQUARTERS 等)' }) @ApiParam({ name: 'accountType', type: String, description: '系统账户类型OPERATION/PROVINCE/CITY/HEADQUARTERS' })
@ApiQuery({ name: 'regionCode', required: false, type: String, description: '区域代码(省/市代码)' })
@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(
@Param('accountType') accountType: string, @Param('accountType') accountType: string,
@Query('regionCode') regionCode?: string,
@Query('page') page?: number, @Query('page') page?: number,
@Query('pageSize') pageSize?: number, @Query('pageSize') pageSize?: number,
) { ) {
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;
// accountType 现在是字符串类型,支持组合键(如 PROVINCE_440000, CITY_440100
// 先通过 accountType + regionCode 查找系统账户
const account = await this.prisma.systemMiningAccount.findUnique({
where: {
accountType_regionCode: {
accountType,
regionCode: regionCode || null,
},
},
});
if (!account) {
return {
records: [],
total: 0,
page: pageNum,
pageSize: pageSizeNum,
accountType,
regionCode: regionCode || null,
};
}
const [records, total] = await Promise.all([ const [records, total] = await Promise.all([
this.prisma.systemMiningRecord.findMany({ this.prisma.systemMiningRecord.findMany({
where: { accountType }, where: { systemAccountId: account.id },
orderBy: { miningMinute: 'desc' }, orderBy: { miningMinute: 'desc' },
skip, skip,
take: pageSizeNum, take: pageSizeNum,
}), }),
this.prisma.systemMiningRecord.count({ this.prisma.systemMiningRecord.count({
where: { accountType }, where: { systemAccountId: account.id },
}), }),
]); ]);
return { return {
records: records.map((record) => ({ records: records.map((record) => ({
id: record.id, id: record.id,
accountType: record.accountType, accountType,
regionCode: regionCode || null,
miningMinute: record.miningMinute, miningMinute: record.miningMinute,
contributionRatio: record.contributionRatio.toString(), contributionRatio: record.contributionRatio.toString(),
totalContribution: record.totalContribution.toString(), totalContribution: record.totalContribution.toString(),
@ -223,41 +246,66 @@ export class AdminController {
total, total,
page: pageNum, page: pageNum,
pageSize: pageSizeNum, pageSize: pageSizeNum,
accountType,
regionCode: regionCode || null,
}; };
} }
@Get('system-accounts/:accountType/transactions') @Get('system-accounts/:accountType/transactions')
@Public() @Public()
@ApiOperation({ summary: '获取系统账户交易记录' }) @ApiOperation({ summary: '获取系统账户交易记录' })
@ApiParam({ name: 'accountType', type: String, description: '系统账户类型 (OPERATION, PROVINCE_440000, CITY_440100, HEADQUARTERS 等)' }) @ApiParam({ name: 'accountType', type: String, description: '系统账户类型OPERATION/PROVINCE/CITY/HEADQUARTERS' })
@ApiQuery({ name: 'regionCode', required: false, type: String, description: '区域代码(省/市代码)' })
@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(
@Param('accountType') accountType: string, @Param('accountType') accountType: string,
@Query('regionCode') regionCode?: string,
@Query('page') page?: number, @Query('page') page?: number,
@Query('pageSize') pageSize?: number, @Query('pageSize') pageSize?: number,
) { ) {
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;
// accountType 现在是字符串类型,支持组合键(如 PROVINCE_440000, CITY_440100
// 先通过 accountType + regionCode 查找系统账户
const account = await this.prisma.systemMiningAccount.findUnique({
where: {
accountType_regionCode: {
accountType,
regionCode: regionCode || null,
},
},
});
if (!account) {
return {
transactions: [],
total: 0,
page: pageNum,
pageSize: pageSizeNum,
accountType,
regionCode: regionCode || null,
};
}
const [transactions, total] = await Promise.all([ const [transactions, total] = await Promise.all([
this.prisma.systemMiningTransaction.findMany({ this.prisma.systemMiningTransaction.findMany({
where: { accountType }, where: { systemAccountId: account.id },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
skip, skip,
take: pageSizeNum, take: pageSizeNum,
}), }),
this.prisma.systemMiningTransaction.count({ this.prisma.systemMiningTransaction.count({
where: { accountType }, where: { systemAccountId: account.id },
}), }),
]); ]);
return { return {
transactions: transactions.map((tx) => ({ transactions: transactions.map((tx) => ({
id: tx.id, id: tx.id,
accountType: tx.accountType, accountType,
regionCode: regionCode || null,
type: tx.type, type: tx.type,
amount: tx.amount.toString(), amount: tx.amount.toString(),
balanceBefore: tx.balanceBefore.toString(), balanceBefore: tx.balanceBefore.toString(),
@ -270,6 +318,8 @@ export class AdminController {
total, total,
page: pageNum, page: pageNum,
pageSize: pageSizeNum, pageSize: pageSizeNum,
accountType,
regionCode: regionCode || null,
}; };
} }

View File

@ -89,12 +89,11 @@ export class ContributionEventHandler implements OnModuleInit {
}); });
} else if (eventType === 'SystemAccountSynced') { } else if (eventType === 'SystemAccountSynced') {
this.logger.log( this.logger.log(
`Received SystemAccountSynced for ${eventPayload.accountType} (baseType=${eventPayload.baseType}, regionCode=${eventPayload.regionCode})`, `Received SystemAccountSynced for ${eventPayload.accountType} (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, regionCode: eventPayload.regionCode || null,
name: eventPayload.name, name: eventPayload.name,
contributionBalance: eventPayload.contributionBalance, contributionBalance: eventPayload.contributionBalance,

View File

@ -12,8 +12,8 @@ import { MiningCalculatorService } from '../../domain/services/mining-calculator
import { ShareAmount } from '../../domain/value-objects/share-amount.vo'; import { ShareAmount } from '../../domain/value-objects/share-amount.vo';
import Decimal from 'decimal.js'; import Decimal from 'decimal.js';
// 系统账户基础类型常量(替代 Prisma 枚举) // 系统账户类型常量
const HEADQUARTERS_BASE_TYPE = 'HEADQUARTERS'; const HEADQUARTERS_ACCOUNT_TYPE = 'HEADQUARTERS';
/** /**
* *
@ -188,6 +188,7 @@ export class MiningDistributionService {
for (const data of result.systemRedisData) { for (const data of result.systemRedisData) {
await this.accumulateSystemMinuteData( await this.accumulateSystemMinuteData(
data.accountType, data.accountType,
data.regionCode,
currentMinute, currentMinute,
data.reward, data.reward,
data.contribution, data.contribution,
@ -207,7 +208,8 @@ export class MiningDistributionService {
new ShareAmount(0), new ShareAmount(0),
); );
await this.accumulateSystemMinuteData( await this.accumulateSystemMinuteData(
HEADQUARTERS_BASE_TYPE, HEADQUARTERS_ACCOUNT_TYPE,
null, // HEADQUARTERS 没有 regionCode
currentMinute, currentMinute,
headquartersTotalReward, headquartersTotalReward,
headquartersTotalContribution, headquartersTotalContribution,
@ -351,7 +353,8 @@ export class MiningDistributionService {
// 计算所有系统账户的挖矿奖励 // 计算所有系统账户的挖矿奖励
const systemRewards: Array<{ const systemRewards: Array<{
accountType: string; // 改为字符串支持组合键 accountType: string;
regionCode: string | null;
reward: ShareAmount; reward: ShareAmount;
contribution: ShareAmount; contribution: ShareAmount;
memo: string; memo: string;
@ -359,7 +362,7 @@ export class MiningDistributionService {
for (const systemAccount of systemAccounts) { for (const systemAccount of systemAccounts) {
// 总部账户不直接参与挖矿,它只接收未解锁算力的收益 // 总部账户不直接参与挖矿,它只接收未解锁算力的收益
if (systemAccount.baseType === HEADQUARTERS_BASE_TYPE) { if (systemAccount.accountType === HEADQUARTERS_ACCOUNT_TYPE) {
continue; continue;
} }
@ -376,6 +379,7 @@ export class MiningDistributionService {
if (!reward.isZero()) { if (!reward.isZero()) {
systemRewards.push({ systemRewards.push({
accountType: systemAccount.accountType, accountType: systemAccount.accountType,
regionCode: systemAccount.regionCode,
reward, reward,
contribution: systemAccount.totalContribution, contribution: systemAccount.totalContribution,
memo: `秒挖矿 ${currentSecond.getTime()}`, memo: `秒挖矿 ${currentSecond.getTime()}`,
@ -417,8 +421,8 @@ export class MiningDistributionService {
// 在单个事务中执行所有系统账户和待解锁算力的挖矿 // 在单个事务中执行所有系统账户和待解锁算力的挖矿
await this.prisma.$transaction(async (tx) => { await this.prisma.$transaction(async (tx) => {
// 处理系统账户挖矿 // 处理系统账户挖矿
for (const { accountType, reward, memo } of systemRewards) { for (const { accountType, regionCode, reward, memo } of systemRewards) {
await this.systemMiningAccountRepository.mine(accountType, reward, memo, tx); await this.systemMiningAccountRepository.mine(accountType as any, regionCode, reward, memo, tx);
systemDistributed = systemDistributed.add(reward); systemDistributed = systemDistributed.add(reward);
systemParticipantCount++; systemParticipantCount++;
} }
@ -438,7 +442,8 @@ export class MiningDistributionService {
// 一次性更新总部账户(而不是每个待解锁算力单独更新) // 一次性更新总部账户(而不是每个待解锁算力单独更新)
if (!headquartersTotal.isZero()) { if (!headquartersTotal.isZero()) {
await this.systemMiningAccountRepository.mine( await this.systemMiningAccountRepository.mine(
HEADQUARTERS_BASE_TYPE, HEADQUARTERS_ACCOUNT_TYPE as any,
null, // HEADQUARTERS 没有 regionCode
headquartersTotal, headquartersTotal,
`秒挖矿 ${currentSecond.getTime()} - 待解锁算力汇总 (${pendingRewards.length}笔)`, `秒挖矿 ${currentSecond.getTime()} - 待解锁算力汇总 (${pendingRewards.length}笔)`,
tx, tx,
@ -447,9 +452,10 @@ export class MiningDistributionService {
}); });
// 事务成功后,累积 Redis 数据Redis 操作不需要在事务内) // 事务成功后,累积 Redis 数据Redis 操作不需要在事务内)
for (const { accountType, reward, contribution } of systemRewards) { for (const { accountType, regionCode, reward, contribution } of systemRewards) {
await this.accumulateSystemMinuteData( await this.accumulateSystemMinuteData(
accountType, accountType,
regionCode,
currentMinute, currentMinute,
reward, reward,
contribution, contribution,
@ -469,7 +475,8 @@ export class MiningDistributionService {
new ShareAmount(0), new ShareAmount(0),
); );
await this.accumulateSystemMinuteData( await this.accumulateSystemMinuteData(
HEADQUARTERS_BASE_TYPE, HEADQUARTERS_ACCOUNT_TYPE,
null, // HEADQUARTERS 没有 regionCode
currentMinute, currentMinute,
headquartersTotalReward, headquartersTotalReward,
headquartersTotalContribution, headquartersTotalContribution,
@ -508,7 +515,8 @@ export class MiningDistributionService {
systemParticipantCount: number; systemParticipantCount: number;
pendingParticipantCount: number; pendingParticipantCount: number;
systemRedisData: Array<{ systemRedisData: Array<{
accountType: string; // 改为字符串支持组合键 accountType: string;
regionCode: string | null;
reward: ShareAmount; reward: ShareAmount;
contribution: ShareAmount; contribution: ShareAmount;
}>; }>;
@ -541,7 +549,8 @@ export class MiningDistributionService {
// 计算所有系统账户的挖矿奖励 // 计算所有系统账户的挖矿奖励
const systemRewards: Array<{ const systemRewards: Array<{
accountType: string; // 改为字符串支持组合键 accountType: string;
regionCode: string | null;
reward: ShareAmount; reward: ShareAmount;
contribution: ShareAmount; contribution: ShareAmount;
memo: string; memo: string;
@ -549,7 +558,7 @@ export class MiningDistributionService {
for (const systemAccount of systemAccounts) { for (const systemAccount of systemAccounts) {
// 总部账户不直接参与挖矿,它只接收未解锁算力的收益 // 总部账户不直接参与挖矿,它只接收未解锁算力的收益
if (systemAccount.baseType === HEADQUARTERS_BASE_TYPE) { if (systemAccount.accountType === HEADQUARTERS_ACCOUNT_TYPE) {
continue; continue;
} }
@ -566,6 +575,7 @@ export class MiningDistributionService {
if (!reward.isZero()) { if (!reward.isZero()) {
systemRewards.push({ systemRewards.push({
accountType: systemAccount.accountType, accountType: systemAccount.accountType,
regionCode: systemAccount.regionCode,
reward, reward,
contribution: systemAccount.totalContribution, contribution: systemAccount.totalContribution,
memo: `秒挖矿 ${currentSecond.getTime()}`, memo: `秒挖矿 ${currentSecond.getTime()}`,
@ -612,11 +622,11 @@ export class MiningDistributionService {
} }
// 处理系统账户挖矿(复用外部事务) // 处理系统账户挖矿(复用外部事务)
for (const { accountType, reward, contribution, memo } of systemRewards) { for (const { accountType, regionCode, reward, contribution, memo } of systemRewards) {
await this.systemMiningAccountRepository.mine(accountType, reward, memo, tx); await this.systemMiningAccountRepository.mine(accountType as any, regionCode, reward, memo, tx);
systemDistributed = systemDistributed.add(reward); systemDistributed = systemDistributed.add(reward);
systemParticipantCount++; systemParticipantCount++;
systemRedisData.push({ accountType, reward, contribution }); systemRedisData.push({ accountType, regionCode, reward, contribution });
} }
// 处理待解锁算力挖矿(归总部账户) // 处理待解锁算力挖矿(归总部账户)
@ -633,7 +643,8 @@ export class MiningDistributionService {
// 一次性更新总部账户(而不是每个待解锁算力单独更新) // 一次性更新总部账户(而不是每个待解锁算力单独更新)
if (!headquartersTotal.isZero()) { if (!headquartersTotal.isZero()) {
await this.systemMiningAccountRepository.mine( await this.systemMiningAccountRepository.mine(
HEADQUARTERS_BASE_TYPE, HEADQUARTERS_ACCOUNT_TYPE as any,
null, // HEADQUARTERS 没有 regionCode
headquartersTotal, headquartersTotal,
`秒挖矿 ${currentSecond.getTime()} - 待解锁算力汇总 (${pendingRewards.length}笔)`, `秒挖矿 ${currentSecond.getTime()} - 待解锁算力汇总 (${pendingRewards.length}笔)`,
tx, tx,
@ -696,20 +707,26 @@ export class MiningDistributionService {
/** /**
* Redis * Redis
* @param accountType PROVINCE_440000, CITY_440100 * @param accountType OPERATION/PROVINCE/CITY/HEADQUARTERS
* @param regionCode 440000, 440100
*/ */
private async accumulateSystemMinuteData( private async accumulateSystemMinuteData(
accountType: string, accountType: string,
regionCode: string | null,
minuteTime: Date, minuteTime: Date,
reward: ShareAmount, reward: ShareAmount,
accountContribution: ShareAmount, accountContribution: ShareAmount,
totalContribution: ShareAmount, totalContribution: ShareAmount,
secondDistribution: ShareAmount, secondDistribution: ShareAmount,
): Promise<void> { ): Promise<void> {
const key = `mining:system:minute:${minuteTime.getTime()}:${accountType}`; // Redis key 使用 accountType:regionCode 格式区分不同的省市账户
const keyId = regionCode ? `${accountType}:${regionCode}` : accountType;
const key = `mining:system:minute:${minuteTime.getTime()}:${keyId}`;
const existing = await this.redis.get(key); const existing = await this.redis.get(key);
let accumulated: { let accumulated: {
accountType: string;
regionCode: string | null;
minedAmount: string; minedAmount: string;
contributionRatio: string; contributionRatio: string;
totalContribution: string; totalContribution: string;
@ -728,6 +745,8 @@ export class MiningDistributionService {
accumulated.accountContribution = accountContribution.value.toString(); accumulated.accountContribution = accountContribution.value.toString();
} else { } else {
accumulated = { accumulated = {
accountType,
regionCode,
minedAmount: reward.value.toString(), minedAmount: reward.value.toString(),
contributionRatio: accountContribution.value.dividedBy(totalContribution.value).toString(), contributionRatio: accountContribution.value.dividedBy(totalContribution.value).toString(),
totalContribution: totalContribution.value.toString(), totalContribution: totalContribution.value.toString(),
@ -795,7 +814,8 @@ export class MiningDistributionService {
/** /**
* *
* PROVINCE_440000, CITY_440100 * accountType: OPERATION/PROVINCE/CITY/HEADQUARTERS
* regionCode: 区域代码
*/ */
private async writeSystemMinuteRecords(minuteTime: Date): Promise<void> { private async writeSystemMinuteRecords(minuteTime: Date): Promise<void> {
try { try {
@ -807,12 +827,14 @@ export class MiningDistributionService {
if (!data) continue; if (!data) continue;
const accumulated = JSON.parse(data); const accumulated = JSON.parse(data);
// accountType 现在是字符串类型,支持组合键 // 从 accumulated 中获取 accountType 和 regionCode
const accountType = key.split(':').pop()!; const accountType = accumulated.accountType;
const regionCode = accumulated.regionCode;
// 使用 repository 的 saveMinuteRecord 方法(支持 upsert // 使用 repository 的 saveMinuteRecord 方法(支持 upsert
await this.systemMiningAccountRepository.saveMinuteRecord( await this.systemMiningAccountRepository.saveMinuteRecord(
accountType, accountType as any,
regionCode,
minuteTime, minuteTime,
new ShareAmount(accumulated.contributionRatio), new ShareAmount(accumulated.contributionRatio),
new ShareAmount(accumulated.totalContribution), new ShareAmount(accumulated.totalContribution),

View File

@ -2,15 +2,14 @@ import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
import { import {
SystemMiningAccountRepository, SystemMiningAccountRepository,
SystemAccountBaseType, SystemAccountType,
} from '../../infrastructure/persistence/repositories/system-mining-account.repository'; } 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 Decimal from 'decimal.js'; import Decimal from 'decimal.js';
interface SystemAccountSyncedData { interface SystemAccountSyncedData {
accountType: string; // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等 accountType: string; // OPERATION / PROVINCE / CITY / HEADQUARTERS
baseType: string; // 基础类型: OPERATION / PROVINCE / CITY / HEADQUARTERS regionCode: string | null; // 省/市代码,如 440000, 440100
regionCode: string | null; // 区域代码: 省/市代码,如 440000, 440100
name: string; name: string;
contributionBalance: string; contributionBalance: string;
} }
@ -51,16 +50,17 @@ export class NetworkSyncService {
/** /**
* *
* PROVINCE_440000, CITY_440100 * accountType: OPERATION / PROVINCE / CITY / HEADQUARTERS
* regionCode: / 440000, 440100
*/ */
async handleSystemAccountSynced(data: SystemAccountSyncedData): Promise<void> { async handleSystemAccountSynced(data: SystemAccountSyncedData): Promise<void> {
try { try {
// 验证 baseType 是否有效 // 验证 accountType 是否有效
const validBaseTypes: SystemAccountBaseType[] = ['OPERATION', 'PROVINCE', 'CITY', 'HEADQUARTERS']; const validTypes: SystemAccountType[] = ['OPERATION', 'PROVINCE', 'CITY', 'HEADQUARTERS'];
const baseType = (data.baseType || this.extractBaseType(data.accountType)) as SystemAccountBaseType; const accountType = data.accountType as SystemAccountType;
if (!validBaseTypes.includes(baseType)) { if (!validTypes.includes(accountType)) {
this.logger.warn(`Unknown system account base type: ${baseType} for ${data.accountType}`); this.logger.warn(`Unknown system account type: ${accountType}`);
return; return;
} }
@ -68,15 +68,14 @@ export class NetworkSyncService {
// 使用 upsert 动态创建或更新账户 // 使用 upsert 动态创建或更新账户
await this.systemAccountRepository.updateContribution( await this.systemAccountRepository.updateContribution(
data.accountType, accountType,
baseType,
data.regionCode, data.regionCode,
data.name, data.name,
contribution, contribution,
); );
this.logger.log( this.logger.log(
`Synced system account ${data.accountType} (baseType=${baseType}, regionCode=${data.regionCode}): contribution=${data.contributionBalance}`, `Synced system account ${accountType} (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);
@ -84,17 +83,6 @@ 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;
}
/** /**
* *
*/ */
@ -159,7 +147,6 @@ 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, regionCode: account.regionCode || null,
name: account.name, name: account.name,
contributionBalance: account.contributionBalance, contributionBalance: account.contributionBalance,

View File

@ -3,13 +3,12 @@ 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 { TransactionClient } from '../unit-of-work/unit-of-work'; import { TransactionClient } from '../unit-of-work/unit-of-work';
// 基础类型定义(不再使用 Prisma 枚举) export type SystemAccountType = 'OPERATION' | 'PROVINCE' | 'CITY' | 'HEADQUARTERS';
export type SystemAccountBaseType = 'OPERATION' | 'PROVINCE' | 'CITY' | 'HEADQUARTERS';
export interface SystemMiningAccountSnapshot { export interface SystemMiningAccountSnapshot {
accountType: string; // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等 id: string;
baseType: SystemAccountBaseType; // 基础类型 accountType: SystemAccountType;
regionCode: string | null; // 区域代码 regionCode: string | null;
name: string; name: string;
totalMined: ShareAmount; totalMined: ShareAmount;
availableBalance: ShareAmount; availableBalance: ShareAmount;
@ -21,9 +20,14 @@ export interface SystemMiningAccountSnapshot {
export class SystemMiningAccountRepository { export class SystemMiningAccountRepository {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
async findByType(accountType: string): Promise<SystemMiningAccountSnapshot | null> { async findByTypeAndRegion(
accountType: SystemAccountType,
regionCode: string | null,
): Promise<SystemMiningAccountSnapshot | null> {
const record = await this.prisma.systemMiningAccount.findUnique({ const record = await this.prisma.systemMiningAccount.findUnique({
where: { accountType }, where: {
accountType_regionCode: { accountType, regionCode },
},
}); });
if (!record) { if (!record) {
@ -34,12 +38,12 @@ export class SystemMiningAccountRepository {
} }
/** /**
* *
*/ */
async findByBaseType(baseType: SystemAccountBaseType): Promise<SystemMiningAccountSnapshot[]> { async findByType(accountType: SystemAccountType): Promise<SystemMiningAccountSnapshot[]> {
const records = await this.prisma.systemMiningAccount.findMany({ const records = await this.prisma.systemMiningAccount.findMany({
where: { baseType }, where: { accountType },
orderBy: { accountType: 'asc' }, orderBy: { regionCode: 'asc' },
}); });
return records.map((r) => this.toSnapshot(r)); return records.map((r) => this.toSnapshot(r));
@ -47,29 +51,28 @@ export class SystemMiningAccountRepository {
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' }, { regionCode: 'asc' }],
}); });
return records.map((r) => this.toSnapshot(r)); return records.map((r) => this.toSnapshot(r));
} }
/** /**
* *
*/ */
async ensureSystemAccountsExist(): Promise<void> { async ensureSystemAccountsExist(): Promise<void> {
const accounts: { accountType: string; baseType: SystemAccountBaseType; name: string }[] = [ const accounts: { accountType: SystemAccountType; name: string }[] = [
{ accountType: 'OPERATION', baseType: 'OPERATION', name: '运营账户' }, { accountType: 'OPERATION', name: '运营账户' },
{ accountType: 'PROVINCE', baseType: 'PROVINCE', name: '省公司账户' }, { accountType: 'HEADQUARTERS', name: '总部账户' },
{ accountType: 'CITY', baseType: 'CITY', name: '市公司账户' },
{ accountType: 'HEADQUARTERS', baseType: 'HEADQUARTERS', name: '总部账户' },
]; ];
for (const account of accounts) { for (const account of accounts) {
await this.prisma.systemMiningAccount.upsert({ await this.prisma.systemMiningAccount.upsert({
where: { accountType: account.accountType }, where: {
accountType_regionCode: { accountType: account.accountType, regionCode: null },
},
create: { create: {
accountType: account.accountType, accountType: account.accountType,
baseType: account.baseType,
regionCode: null, regionCode: null,
name: account.name, name: account.name,
totalMined: 0, totalMined: 0,
@ -82,20 +85,20 @@ export class SystemMiningAccountRepository {
} }
/** /**
* *
*/ */
async updateContribution( async updateContribution(
accountType: string, accountType: SystemAccountType,
baseType: SystemAccountBaseType,
regionCode: string | null, regionCode: string | null,
name: string, name: string,
contribution: ShareAmount, contribution: ShareAmount,
): Promise<void> { ): Promise<void> {
await this.prisma.systemMiningAccount.upsert({ await this.prisma.systemMiningAccount.upsert({
where: { accountType }, where: {
accountType_regionCode: { accountType, regionCode },
},
create: { create: {
accountType, accountType,
baseType,
regionCode, regionCode,
name, name,
totalContribution: contribution.value, totalContribution: contribution.value,
@ -117,25 +120,24 @@ export class SystemMiningAccountRepository {
} }
/** /**
* *
* @param accountType
* @param amount
* @param memo
* @param tx
*/ */
async mine( async mine(
accountType: string, accountType: SystemAccountType,
regionCode: string | null,
amount: ShareAmount, amount: ShareAmount,
memo: string, memo: string,
tx?: TransactionClient, tx?: TransactionClient,
): Promise<void> { ): Promise<void> {
const executeInTx = async (client: TransactionClient) => { const executeInTx = async (client: TransactionClient) => {
const account = await client.systemMiningAccount.findUnique({ const account = await client.systemMiningAccount.findUnique({
where: { accountType }, where: {
accountType_regionCode: { accountType, regionCode },
},
}); });
if (!account) { if (!account) {
throw new Error(`System account ${accountType} not found`); throw new Error(`System account ${accountType}:${regionCode} not found`);
} }
const balanceBefore = account.availableBalance; const balanceBefore = account.availableBalance;
@ -143,7 +145,7 @@ export class SystemMiningAccountRepository {
const totalMined = account.totalMined.plus(amount.value); const totalMined = account.totalMined.plus(amount.value);
await client.systemMiningAccount.update({ await client.systemMiningAccount.update({
where: { accountType }, where: { id: account.id },
data: { data: {
totalMined, totalMined,
availableBalance: balanceAfter, availableBalance: balanceAfter,
@ -152,7 +154,7 @@ export class SystemMiningAccountRepository {
await client.systemMiningTransaction.create({ await client.systemMiningTransaction.create({
data: { data: {
accountType, systemAccountId: account.id,
type: 'MINE', type: 'MINE',
amount: amount.value, amount: amount.value,
balanceBefore, balanceBefore,
@ -163,31 +165,40 @@ export class SystemMiningAccountRepository {
}; };
if (tx) { if (tx) {
// 使用外部事务
await executeInTx(tx); await executeInTx(tx);
} else { } else {
// 自动创建事务(向后兼容)
await this.prisma.$transaction(executeInTx); await this.prisma.$transaction(executeInTx);
} }
} }
async saveMinuteRecord( async saveMinuteRecord(
accountType: string, accountType: SystemAccountType,
regionCode: string | null,
miningMinute: Date, miningMinute: Date,
contributionRatio: ShareAmount, contributionRatio: ShareAmount,
totalContribution: ShareAmount, totalContribution: ShareAmount,
secondDistribution: ShareAmount, secondDistribution: ShareAmount,
minedAmount: ShareAmount, minedAmount: ShareAmount,
): Promise<void> { ): Promise<void> {
const account = await this.prisma.systemMiningAccount.findUnique({
where: {
accountType_regionCode: { accountType, regionCode },
},
});
if (!account) {
throw new Error(`System account ${accountType}:${regionCode} not found`);
}
await this.prisma.systemMiningRecord.upsert({ await this.prisma.systemMiningRecord.upsert({
where: { where: {
accountType_miningMinute: { systemAccountId_miningMinute: {
accountType, systemAccountId: account.id,
miningMinute, miningMinute,
}, },
}, },
create: { create: {
accountType, systemAccountId: account.id,
miningMinute, miningMinute,
contributionRatio: contributionRatio.value, contributionRatio: contributionRatio.value,
totalContribution: totalContribution.value, totalContribution: totalContribution.value,
@ -202,8 +213,8 @@ export class SystemMiningAccountRepository {
private toSnapshot(record: any): SystemMiningAccountSnapshot { private toSnapshot(record: any): SystemMiningAccountSnapshot {
return { return {
accountType: record.accountType, id: record.id,
baseType: record.baseType as SystemAccountBaseType, accountType: record.accountType as SystemAccountType,
regionCode: record.regionCode, regionCode: record.regionCode,
name: record.name, name: record.name,
totalMined: new ShareAmount(record.totalMined), totalMined: new ShareAmount(record.totalMined),