From 906234665017c599f315b8b2e4f89372adf63982 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 20 Jan 2026 21:29:01 -0800 Subject: [PATCH] =?UTF-8?q?refactor(system-accounts):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=20baseType=20=E5=AD=97=E6=AE=B5=EF=BC=8C=E4=BD=BF=E7=94=A8=20a?= =?UTF-8?q?ccountType+regionCode=20=E5=A4=8D=E5=90=88=E5=94=AF=E4=B8=80?= =?UTF-8?q?=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 主要变更 ### 数据模型简化 - 移除冗余的 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 --- .../prisma/migrations/0001_init/migration.sql | 7 +- .../contribution-service/prisma/schema.prisma | 10 +- .../src/api/controllers/admin.controller.ts | 5 +- .../contribution-calculation.service.ts | 24 ++-- ...ribution-distribution-publisher.service.ts | 12 +- .../events/system-account-synced.event.ts | 7 +- .../contribution-calculator.service.ts | 59 +++------- .../repositories/system-account.repository.ts | 109 +++++++----------- .../prisma/migrations/0001_init/migration.sql | 19 ++- .../mining-admin-service/prisma/schema.prisma | 21 ++-- .../controllers/system-accounts.controller.ts | 23 +++- .../services/system-accounts.service.ts | 107 ++++++++++------- .../infrastructure/kafka/cdc-sync.service.ts | 38 +++--- .../prisma/migrations/0001_init/migration.sql | 35 +++--- .../mining-service/prisma/schema.prisma | 40 +++---- .../src/api/controllers/admin.controller.ts | 76 +++++++++--- .../contribution-event.handler.ts | 3 +- .../services/mining-distribution.service.ts | 68 +++++++---- .../services/network-sync.service.ts | 37 ++---- .../system-mining-account.repository.ts | 97 +++++++++------- 20 files changed, 410 insertions(+), 387 deletions(-) diff --git a/backend/services/contribution-service/prisma/migrations/0001_init/migration.sql b/backend/services/contribution-service/prisma/migrations/0001_init/migration.sql index e1c68e1a..a7286bc7 100644 --- a/backend/services/contribution-service/prisma/migrations/0001_init/migration.sql +++ b/backend/services/contribution-service/prisma/migrations/0001_init/migration.sql @@ -222,13 +222,12 @@ CREATE INDEX "unallocated_contributions_unalloc_type_idx" ON "unallocated_contri CREATE INDEX "unallocated_contributions_status_idx" ON "unallocated_contributions"("status"); -- ============================================ --- 6. 系统账户表 (支持按省市细分) +-- 6. 系统账户表 -- ============================================ CREATE TABLE "system_accounts" ( "id" BIGSERIAL NOT NULL, "account_type" TEXT NOT NULL, - "base_type" TEXT NOT NULL, "region_code" TEXT, "name" TEXT NOT NULL, "contribution_balance" DECIMAL(30,10) NOT NULL DEFAULT 0, @@ -240,8 +239,8 @@ CREATE TABLE "system_accounts" ( CONSTRAINT "system_accounts_pkey" PRIMARY KEY ("id") ); -CREATE UNIQUE INDEX "system_accounts_account_type_key" ON "system_accounts"("account_type"); -CREATE INDEX "system_accounts_base_type_idx" ON "system_accounts"("base_type"); +CREATE UNIQUE INDEX "system_accounts_account_type_region_code_key" ON "system_accounts"("account_type", "region_code"); +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 TABLE "system_contribution_records" ( diff --git a/backend/services/contribution-service/prisma/schema.prisma b/backend/services/contribution-service/prisma/schema.prisma index 79f95860..5c76580e 100644 --- a/backend/services/contribution-service/prisma/schema.prisma +++ b/backend/services/contribution-service/prisma/schema.prisma @@ -298,13 +298,10 @@ model UnallocatedContribution { } // 系统账户(运营/省/市/总部) -// accountType 格式: OPERATION, PROVINCE, CITY, HEADQUARTERS (汇总账户) -// PROVINCE_440000, CITY_440100 等 (按省市细分的账户) model SystemAccount { id BigInt @id @default(autoincrement()) - accountType String @unique @map("account_type") // 组合键: PROVINCE_440000, CITY_440100 等 - baseType String @map("base_type") // 基础类型: OPERATION / PROVINCE / CITY / HEADQUARTERS - regionCode String? @map("region_code") // 区域代码: 省/市代码,如 440000, 440100 + accountType String @map("account_type") // OPERATION / PROVINCE / CITY / HEADQUARTERS + regionCode String? @map("region_code") // 省/市代码,如 440000, 440100 name String contributionBalance Decimal @default(0) @map("contribution_balance") @db.Decimal(30, 10) @@ -317,7 +314,8 @@ model SystemAccount { records SystemContributionRecord[] - @@index([baseType]) + @@unique([accountType, regionCode]) + @@index([accountType]) @@index([regionCode]) @@map("system_accounts") } diff --git a/backend/services/contribution-service/src/api/controllers/admin.controller.ts b/backend/services/contribution-service/src/api/controllers/admin.controller.ts index 9aef0cb2..40c2133b 100644 --- a/backend/services/contribution-service/src/api/controllers/admin.controller.ts +++ b/backend/services/contribution-service/src/api/controllers/admin.controller.ts @@ -438,8 +438,7 @@ export class AdminController { const events = systemAccounts.map((account) => { const event = new SystemAccountSyncedEvent( account.accountType, - account.baseType, // 基础类型 - account.regionCode, // 区域代码 + account.regionCode, account.name, account.contributionBalance.toString(), account.createdAt, @@ -447,7 +446,7 @@ export class AdminController { return { aggregateType: SystemAccountSyncedEvent.AGGREGATE_TYPE, - aggregateId: account.accountType, + aggregateId: `${account.accountType}:${account.regionCode || 'null'}`, eventType: SystemAccountSyncedEvent.EVENT_TYPE, payload: event.toPayload(), }; diff --git a/backend/services/contribution-service/src/application/services/contribution-calculation.service.ts b/backend/services/contribution-service/src/application/services/contribution-calculation.service.ts index a85acd3b..91a33bda 100644 --- a/backend/services/contribution-service/src/application/services/contribution-calculation.service.ts +++ b/backend/services/contribution-service/src/application/services/contribution-calculation.service.ts @@ -332,44 +332,46 @@ export class ContributionCalculationService { } } - // 5. 保存系统账户算力并发布同步事件(支持按省市细分) + // 5. 保存系统账户算力并发布同步事件 if (result.systemContributions.length > 0) { await this.systemAccountRepository.ensureSystemAccountsExist(); for (const sys of result.systemContributions) { - // 动态创建/更新系统账户(支持省市细分) + // 动态创建/更新系统账户 await this.systemAccountRepository.addContribution( sys.accountType, - sys.baseType, sys.regionCode, sys.amount, ); - // 保存算力明细记录并获取保存后的记录(带ID) + // 保存算力明细记录 const savedRecord = await this.systemAccountRepository.saveContributionRecord({ - systemAccountType: sys.accountType, + accountType: sys.accountType, + regionCode: sys.regionCode, sourceAdoptionId, sourceAccountSequence, distributionRate: sys.rate.value.toNumber(), amount: sys.amount, effectiveDate, - expireDate: null, // System account contributions never expire based on the schema's contributionNeverExpires field + expireDate: null, }); // 发布系统账户同步事件(用于 mining-service 同步系统账户算力) - const systemAccount = await this.systemAccountRepository.findByType(sys.accountType); + const systemAccount = await this.systemAccountRepository.findByTypeAndRegion( + sys.accountType, + sys.regionCode, + ); if (systemAccount) { const event = new SystemAccountSyncedEvent( sys.accountType, - sys.baseType, // 新增:基础类型 - sys.regionCode, // 新增:区域代码 + sys.regionCode, systemAccount.name, systemAccount.contributionBalance.value.toString(), systemAccount.createdAt, ); await this.outboxRepository.save({ aggregateType: SystemAccountSyncedEvent.AGGREGATE_TYPE, - aggregateId: sys.accountType, + aggregateId: `${sys.accountType}:${sys.regionCode || 'null'}`, eventType: SystemAccountSyncedEvent.EVENT_TYPE, payload: event.toPayload(), }); @@ -383,7 +385,7 @@ export class ContributionCalculationService { sys.rate.value.toNumber(), sys.amount.value.toString(), effectiveDate, - null, // System account contributions never expire + null, savedRecord.createdAt, ); await this.outboxRepository.save({ diff --git a/backend/services/contribution-service/src/application/services/contribution-distribution-publisher.service.ts b/backend/services/contribution-service/src/application/services/contribution-distribution-publisher.service.ts index 4f9d2b09..c093675e 100644 --- a/backend/services/contribution-service/src/application/services/contribution-distribution-publisher.service.ts +++ b/backend/services/contribution-service/src/application/services/contribution-distribution-publisher.service.ts @@ -121,11 +121,15 @@ export class ContributionDistributionPublisherService { return result.systemContributions.map((sys) => ({ accountType: sys.accountType, amount: sys.amount.value.toString(), + // 直接使用 regionCode,没有就用传入的参数 provinceCode: - sys.accountType === 'PROVINCE' || sys.accountType === 'CITY' - ? provinceCode - : undefined, - cityCode: sys.accountType === 'CITY' ? cityCode : undefined, + sys.accountType === 'PROVINCE' + ? sys.regionCode || provinceCode + : sys.accountType === 'CITY' + ? sys.regionCode || cityCode + : undefined, + cityCode: + sys.accountType === 'CITY' ? sys.regionCode || cityCode : undefined, neverExpires: sys.accountType === 'OPERATION', // 运营账户永不过期 })); } diff --git a/backend/services/contribution-service/src/domain/events/system-account-synced.event.ts b/backend/services/contribution-service/src/domain/events/system-account-synced.event.ts index 9d5ec04e..d22c6133 100644 --- a/backend/services/contribution-service/src/domain/events/system-account-synced.event.ts +++ b/backend/services/contribution-service/src/domain/events/system-account-synced.event.ts @@ -1,16 +1,14 @@ /** * 系统账户算力同步事件 * 用于将系统账户(运营、省、市、总部)的算力同步到 mining-service - * 支持按省市细分的账户(如 PROVINCE_440000, CITY_440100) */ export class SystemAccountSyncedEvent { static readonly EVENT_TYPE = 'SystemAccountSynced'; static readonly AGGREGATE_TYPE = 'SystemAccount'; constructor( - 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 accountType: string, // OPERATION / PROVINCE / CITY / HEADQUARTERS + public readonly regionCode: string | null, // 省/市代码,如 440000, 440100 public readonly name: string, public readonly contributionBalance: string, public readonly createdAt: Date, @@ -20,7 +18,6 @@ export class SystemAccountSyncedEvent { return { eventType: SystemAccountSyncedEvent.EVENT_TYPE, accountType: this.accountType, - baseType: this.baseType, regionCode: this.regionCode, name: this.name, contributionBalance: this.contributionBalance, diff --git a/backend/services/contribution-service/src/domain/services/contribution-calculator.service.ts b/backend/services/contribution-service/src/domain/services/contribution-calculator.service.ts index 64a4c624..dc594e09 100644 --- a/backend/services/contribution-service/src/domain/services/contribution-calculator.service.ts +++ b/backend/services/contribution-service/src/domain/services/contribution-calculator.service.ts @@ -5,16 +5,12 @@ import { ContributionAccountAggregate, ContributionSourceType } from '../aggrega import { ContributionRecordAggregate } from '../aggregates/contribution-record.aggregate'; 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; // 区域代码 + accountType: 'OPERATION' | 'PROVINCE' | 'CITY' | 'HEADQUARTERS'; + regionCode: string | null; // 省市代码,如 440000、440100 rate: DistributionRate; amount: ContributionAmount; } @@ -98,7 +94,6 @@ export class ContributionCalculatorService { // 运营账户(全国)- 12% result.systemContributions.push({ accountType: 'OPERATION', - baseType: 'OPERATION', regionCode: null, rate: DistributionRate.OPERATION, amount: totalContribution.multiply(DistributionRate.OPERATION.value), @@ -106,47 +101,21 @@ export class ContributionCalculatorService { // 省公司账户 - 1%(按认种选择的省份) 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({ - accountType: 'PROVINCE', - baseType: 'PROVINCE', - regionCode: null, - rate: DistributionRate.PROVINCE, - amount: totalContribution.multiply(DistributionRate.PROVINCE.value), - }); - } + result.systemContributions.push({ + accountType: 'PROVINCE', + regionCode: provinceCode || 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, - 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), - }); - } + result.systemContributions.push({ + accountType: 'CITY', + regionCode: cityCode || null, + rate: DistributionRate.CITY, + amount: totalContribution.multiply(DistributionRate.CITY.value), + }); // 3. 团队贡献值 (15%) this.distributeTeamContribution( diff --git a/backend/services/contribution-service/src/infrastructure/persistence/repositories/system-account.repository.ts b/backend/services/contribution-service/src/infrastructure/persistence/repositories/system-account.repository.ts index 6dc2ee16..b9f47bb9 100644 --- a/backend/services/contribution-service/src/infrastructure/persistence/repositories/system-account.repository.ts +++ b/backend/services/contribution-service/src/infrastructure/persistence/repositories/system-account.repository.ts @@ -2,14 +2,12 @@ import { Injectable } from '@nestjs/common'; import { ContributionAmount } from '../../../domain/value-objects/contribution-amount.vo'; import { UnitOfWork, TransactionClient } from '../unit-of-work/unit-of-work'; -// 基础类型枚举 -export type SystemAccountBaseType = 'OPERATION' | 'PROVINCE' | 'CITY' | 'HEADQUARTERS'; +export type SystemAccountType = 'OPERATION' | 'PROVINCE' | 'CITY' | 'HEADQUARTERS'; export interface SystemAccount { id: bigint; - accountType: string; // 组合键: PROVINCE_440000, CITY_440100 等 - baseType: SystemAccountBaseType; // 基础类型 - regionCode: string | null; // 区域代码 + accountType: SystemAccountType; + regionCode: string | null; // 省/市代码 name: string; contributionBalance: ContributionAmount; contributionNeverExpires: boolean; @@ -40,11 +38,16 @@ export class SystemAccountRepository { } /** - * 根据 accountType(组合键)查找系统账户 + * 根据 accountType + regionCode 查找系统账户 */ - async findByType(accountType: string): Promise { + async findByTypeAndRegion( + accountType: SystemAccountType, + regionCode: string | null, + ): Promise { const record = await this.client.systemAccount.findUnique({ - where: { accountType }, + where: { + accountType_regionCode: { accountType, regionCode }, + }, }); if (!record) { @@ -55,12 +58,12 @@ export class SystemAccountRepository { } /** - * 根据基础类型查找所有账户(如查找所有 CITY 类型账户) + * 根据类型查找所有账户(如查找所有 CITY 类型账户) */ - async findByBaseType(baseType: SystemAccountBaseType): Promise { + async findByType(accountType: SystemAccountType): Promise { const records = await this.client.systemAccount.findMany({ - where: { baseType }, - orderBy: { accountType: 'asc' }, + where: { accountType }, + orderBy: { regionCode: 'asc' }, }); return records.map((r) => this.toSystemAccount(r)); @@ -68,29 +71,28 @@ export class SystemAccountRepository { async findAll(): Promise { const records = await this.client.systemAccount.findMany({ - orderBy: { accountType: 'asc' }, + orderBy: [{ accountType: 'asc' }, { regionCode: 'asc' }], }); return records.map((r) => this.toSystemAccount(r)); } /** - * 确保基础的汇总账户存在(向后兼容) + * 确保基础账户存在 */ async ensureSystemAccountsExist(): Promise { - const accounts: { accountType: string; baseType: SystemAccountBaseType; name: string }[] = [ - { accountType: 'OPERATION', baseType: 'OPERATION', name: '运营账户' }, - { accountType: 'PROVINCE', baseType: 'PROVINCE', name: '省公司账户' }, - { accountType: 'CITY', baseType: 'CITY', name: '市公司账户' }, - { accountType: 'HEADQUARTERS', baseType: 'HEADQUARTERS', name: '总部账户' }, + const accounts: { accountType: SystemAccountType; name: string }[] = [ + { accountType: 'OPERATION', name: '运营账户' }, + { accountType: 'HEADQUARTERS', name: '总部账户' }, ]; for (const account of accounts) { await this.client.systemAccount.upsert({ - where: { accountType: account.accountType }, + where: { + accountType_regionCode: { accountType: account.accountType, regionCode: null }, + }, create: { accountType: account.accountType, - baseType: account.baseType, regionCode: null, name: account.name, 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( - accountType: string, - baseType: SystemAccountBaseType, + accountType: SystemAccountType, regionCode: string | null, amount: ContributionAmount, ): Promise { - const name = this.getAccountName(baseType, regionCode); + const name = this.getAccountName(accountType, regionCode); await this.client.systemAccount.upsert({ - where: { accountType }, + where: { + accountType_regionCode: { accountType, regionCode }, + }, create: { accountType, - baseType, regionCode, name, 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) { - const names: Record = { + const names: Record = { OPERATION: '运营账户', PROVINCE: '省公司账户', CITY: '市公司账户', HEADQUARTERS: '总部账户', }; - return names[baseType] || baseType; + return names[accountType] || accountType; } return `${regionCode}账户`; } async saveContributionRecord(record: { - systemAccountType: string; // 改为 string 支持组合键 + accountType: SystemAccountType; + regionCode: string | null; sourceAdoptionId: bigint; sourceAccountSequence: string; distributionRate: number; @@ -157,9 +156,9 @@ export class SystemAccountRepository { effectiveDate: Date; expireDate?: Date | null; }): Promise { - const systemAccount = await this.findByType(record.systemAccountType); + const systemAccount = await this.findByTypeAndRegion(record.accountType, record.regionCode); 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({ @@ -177,42 +176,13 @@ export class SystemAccountRepository { return this.toContributionRecord(created); } - async saveContributionRecords(records: { - systemAccountType: string; // 改为 string 支持组合键 - sourceAdoptionId: bigint; - sourceAccountSequence: string; - distributionRate: number; - amount: ContributionAmount; - effectiveDate: Date; - expireDate?: Date | null; - }[]): Promise { - if (records.length === 0) return; - - const systemAccounts = await this.findAll(); - const accountMap = new Map(); - 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( - systemAccountType: string, // 改为 string 支持组合键 + accountType: SystemAccountType, + regionCode: string | null, page: number, pageSize: number, ): Promise<{ data: SystemContributionRecord[]; total: number }> { - const systemAccount = await this.findByType(systemAccountType); + const systemAccount = await this.findByTypeAndRegion(accountType, regionCode); if (!systemAccount) { return { data: [], total: 0 }; } @@ -238,8 +208,7 @@ export class SystemAccountRepository { private toSystemAccount(record: any): SystemAccount { return { id: record.id, - accountType: record.accountType, - baseType: record.baseType as SystemAccountBaseType, + accountType: record.accountType as SystemAccountType, regionCode: record.regionCode, name: record.name, contributionBalance: new ContributionAmount(record.contributionBalance), diff --git a/backend/services/mining-admin-service/prisma/migrations/0001_init/migration.sql b/backend/services/mining-admin-service/prisma/migrations/0001_init/migration.sql index 496c547c..2831bd91 100644 --- a/backend/services/mining-admin-service/prisma/migrations/0001_init/migration.sql +++ b/backend/services/mining-admin-service/prisma/migrations/0001_init/migration.sql @@ -302,11 +302,10 @@ CREATE TABLE "synced_circulation_pools" ( CONSTRAINT "synced_circulation_pools_pkey" PRIMARY KEY ("id") ); --- CreateTable +-- CreateTable: 系统账户算力 (from contribution-service) CREATE TABLE "synced_system_contributions" ( "id" TEXT NOT NULL, "accountType" TEXT NOT NULL, - "base_type" TEXT NOT NULL DEFAULT '', "region_code" TEXT, "name" TEXT NOT NULL, "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 CREATE UNIQUE INDEX "synced_day_klines_klineDate_key" ON "synced_day_klines"("klineDate"); --- CreateIndex -CREATE UNIQUE INDEX "synced_system_contributions_accountType_key" ON "synced_system_contributions"("accountType"); - --- CreateIndex (base_type 和 region_code 索引) -CREATE INDEX "synced_system_contributions_base_type_idx" ON "synced_system_contributions"("base_type"); +-- CreateIndex: synced_system_contributions +-- 使用 accountType + region_code 复合唯一键 +CREATE UNIQUE INDEX "synced_system_contributions_accountType_region_code_key" ON "synced_system_contributions"("accountType", "region_code"); +CREATE INDEX "synced_system_contributions_accountType_idx" ON "synced_system_contributions"("accountType"); CREATE INDEX "synced_system_contributions_region_code_idx" ON "synced_system_contributions"("region_code"); -- CreateIndex @@ -869,11 +867,12 @@ ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_adminId_fkey" FOREIGN KEY (" -- 用于存储从 contribution-service 同步的系统账户算力来源明细 -- ============================================================================ --- CreateTable +-- CreateTable: 系统账户算力明细 (from contribution-service) CREATE TABLE "synced_system_contribution_records" ( "id" TEXT 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_account_sequence" TEXT NOT NULL, "distribution_rate" DECIMAL(10,6) NOT NULL, @@ -890,7 +889,7 @@ CREATE TABLE "synced_system_contribution_records" ( -- CreateIndex 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_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); diff --git a/backend/services/mining-admin-service/prisma/schema.prisma b/backend/services/mining-admin-service/prisma/schema.prisma index 1f231a28..828a01cb 100644 --- a/backend/services/mining-admin-service/prisma/schema.prisma +++ b/backend/services/mining-admin-service/prisma/schema.prisma @@ -422,19 +422,16 @@ model SyncedCirculationPool { model SyncedSystemContribution { id String @id @default(uuid()) - 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 + accountType String // OPERATION / PROVINCE / CITY / HEADQUARTERS + regionCode String? @map("region_code") // 省/市代码,如 440000, 440100 name String contributionBalance Decimal @db.Decimal(30, 8) @default(0) contributionNeverExpires Boolean @default(false) syncedAt DateTime @default(now()) updatedAt DateTime @updatedAt - // 关联算力明细记录 - records SyncedSystemContributionRecord[] - - @@index([baseType]) + @@unique([accountType, regionCode]) + @@index([accountType]) @@index([regionCode]) @@map("synced_system_contributions") } @@ -446,7 +443,10 @@ model SyncedSystemContribution { model SyncedSystemContributionRecord { id String @id @default(uuid()) 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 @@ -465,10 +465,7 @@ model SyncedSystemContributionRecord { syncedAt DateTime @default(now()) updatedAt DateTime @updatedAt - // 关联系统账户 - systemContribution SyncedSystemContribution @relation(fields: [systemAccountType], references: [accountType]) - - @@index([systemAccountType]) + @@index([accountType, regionCode]) @@index([sourceAdoptionId]) @@index([sourceAccountSequence]) @@index([createdAt(sort: Desc)]) diff --git a/backend/services/mining-admin-service/src/api/controllers/system-accounts.controller.ts b/backend/services/mining-admin-service/src/api/controllers/system-accounts.controller.ts index 0f43536d..183c371d 100644 --- a/backend/services/mining-admin-service/src/api/controllers/system-accounts.controller.ts +++ b/backend/services/mining-admin-service/src/api/controllers/system-accounts.controller.ts @@ -22,16 +22,19 @@ export class SystemAccountsController { @Get(':accountType/records') @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: 'pageSize', required: false, type: Number }) async getSystemAccountMiningRecords( @Param('accountType') accountType: string, + @Query('regionCode') regionCode?: string, @Query('page') page?: number, @Query('pageSize') pageSize?: number, ) { return this.systemAccountsService.getSystemAccountMiningRecords( accountType, + regionCode || null, page ?? 1, pageSize ?? 20, ); @@ -39,16 +42,19 @@ export class SystemAccountsController { @Get(':accountType/transactions') @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: 'pageSize', required: false, type: Number }) async getSystemAccountTransactions( @Param('accountType') accountType: string, + @Query('regionCode') regionCode?: string, @Query('page') page?: number, @Query('pageSize') pageSize?: number, ) { return this.systemAccountsService.getSystemAccountTransactions( accountType, + regionCode || null, page ?? 1, pageSize ?? 20, ); @@ -57,22 +63,25 @@ export class SystemAccountsController { @Get(':accountType/contributions') @ApiOperation({ summary: '获取系统账户算力来源明细', - description: '显示该账户的每笔算力来自哪个认种订单,支持按省市细分的账户类型(如 CITY_440100, PROVINCE_440000)', + description: '显示该账户的每笔算力来自哪个认种订单', }) @ApiParam({ name: 'accountType', 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: 'pageSize', required: false, type: Number, description: '每页数量,默认20' }) async getSystemAccountContributionRecords( @Param('accountType') accountType: string, + @Query('regionCode') regionCode?: string, @Query('page') page?: number, @Query('pageSize') pageSize?: number, ) { return this.systemAccountsService.getSystemAccountContributionRecords( accountType, + regionCode || null, page ?? 1, pageSize ?? 20, ); @@ -86,11 +95,13 @@ export class SystemAccountsController { @ApiParam({ name: 'accountType', type: String, - description: '系统账户类型(组合键),如 OPERATION, PROVINCE_440000, CITY_440100', + description: '系统账户类型(OPERATION/PROVINCE/CITY/HEADQUARTERS)', }) + @ApiQuery({ name: 'regionCode', required: false, type: String, description: '区域代码(省/市代码)' }) async getSystemAccountContributionStats( @Param('accountType') accountType: string, + @Query('regionCode') regionCode?: string, ) { - return this.systemAccountsService.getSystemAccountContributionStats(accountType); + return this.systemAccountsService.getSystemAccountContributionStats(accountType, regionCode || null); } } diff --git a/backend/services/mining-admin-service/src/application/services/system-accounts.service.ts b/backend/services/mining-admin-service/src/application/services/system-accounts.service.ts index 481a0304..3aba403b 100644 --- a/backend/services/mining-admin-service/src/application/services/system-accounts.service.ts +++ b/backend/services/mining-admin-service/src/application/services/system-accounts.service.ts @@ -5,9 +5,9 @@ import { firstValueFrom } from 'rxjs'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; interface MiningServiceSystemAccount { - accountType: string; // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等 - baseType: string; // 基础类型: OPERATION / PROVINCE / CITY / HEADQUARTERS - regionCode: string | null; // 区域代码: 省/市代码,如 440000, 440100 + id: string; + accountType: string; // OPERATION / PROVINCE / CITY / HEADQUARTERS + regionCode: string | null; // 省/市代码,如 440000, 440100 name: string; totalMined: string; availableBalance: string; @@ -48,7 +48,11 @@ export class SystemAccountsService { const miningDataMap = new Map(); 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; @@ -82,12 +86,13 @@ export class SystemAccountsService { // 从 mining-service 获取挖矿数据 const miningDataMap = await this.fetchMiningServiceSystemAccounts(); - // 构建算力数据映射 - 支持两种匹配方式 - // 1. 直接用 accountType 匹配(如 OPERATION, HEADQUARTERS) - // 2. 用组合键匹配(如 CITY_440100, PROVINCE_440000) + // 构建算力数据映射 - 使用 accountType:regionCode 格式 const contributionMap = new Map(); 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 miningData = null; - // 1. 尝试用 regionCode 匹配(针对省市账户) + // 1. 尝试用 accountType: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); + // 使用 accountType:regionCode 格式(如 CITY:440100) + const matchKey = `${account.accountType}:${regionCode}`; + contrib = contributionMap.get(matchKey); + miningData = miningDataMap.get(matchKey); } } - // 2. 回退到直接 accountType 匹配(汇总账户) + // 2. 回退到直接 accountType 匹配(汇总账户,如 OPERATION, HEADQUARTERS) if (!contrib) { 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( accountType: string, + regionCode: string | null, page: number = 1, pageSize: number = 20, ) { @@ -253,12 +263,15 @@ export class SystemAccountsService { ); try { + const params: Record = { page, pageSize }; + if (regionCode) { + params.regionCode = regionCode; + } + const response = await firstValueFrom( this.httpService.get( `${miningServiceUrl}/admin/system-accounts/${accountType}/records`, - { - params: { page, pageSize }, - }, + { params }, ), ); @@ -267,15 +280,20 @@ export class SystemAccountsService { this.logger.warn( `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( accountType: string, + regionCode: string | null, page: number = 1, pageSize: number = 20, ) { @@ -285,12 +303,15 @@ export class SystemAccountsService { ); try { + const params: Record = { page, pageSize }; + if (regionCode) { + params.regionCode = regionCode; + } + const response = await firstValueFrom( this.httpService.get( `${miningServiceUrl}/admin/system-accounts/${accountType}/transactions`, - { - params: { page, pageSize }, - }, + { params }, ), ); @@ -299,7 +320,7 @@ export class SystemAccountsService { this.logger.warn( `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 pageSize 每页数量 */ async getSystemAccountContributionRecords( accountType: string, + regionCode: string | null, page: number = 1, pageSize: number = 20, ) { + const whereClause = regionCode + ? { accountType, regionCode } + : { accountType, regionCode: null }; + const [records, total] = await Promise.all([ this.prisma.syncedSystemContributionRecord.findMany({ - where: { systemAccountType: accountType }, + where: whereClause, skip: (page - 1) * pageSize, take: pageSize, orderBy: { createdAt: 'desc' }, }), this.prisma.syncedSystemContributionRecord.count({ - where: { systemAccountType: accountType }, + where: whereClause, }), ]); return { records: records.map((record) => ({ - id: record.id, originalRecordId: record.originalRecordId.toString(), - systemAccountType: record.systemAccountType, + accountType: record.accountType, + regionCode: record.regionCode, sourceAdoptionId: record.sourceAdoptionId.toString(), sourceAccountSequence: record.sourceAccountSequence, 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({ - where: { accountType }, + where: { accountType_regionCode: { accountType, regionCode } }, }); + const whereClause = regionCode + ? { accountType, regionCode } + : { accountType, regionCode: null }; + // 获取明细记录统计 const recordStats = await this.prisma.syncedSystemContributionRecord.aggregate({ - where: { systemAccountType: accountType }, + where: whereClause, _count: true, _sum: { amount: true }, }); @@ -370,20 +401,19 @@ export class SystemAccountsService { // 获取来源认种订单数量(去重) const uniqueAdoptions = await this.prisma.syncedSystemContributionRecord.groupBy({ by: ['sourceAdoptionId'], - where: { systemAccountType: accountType }, + where: whereClause, }); // 获取来源用户数量(去重) const uniqueUsers = await this.prisma.syncedSystemContributionRecord.groupBy({ by: ['sourceAccountSequence'], - where: { systemAccountType: accountType }, + where: whereClause, }); return { accountType, + regionCode, 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', @@ -391,13 +421,4 @@ export class SystemAccountsService { uniqueUserCount: uniqueUsers.length, }; } - - /** - * 从 accountType 提取 baseType - */ - private extractBaseTypeFromAccountType(accountType: string): string { - if (accountType.startsWith('PROVINCE_')) return 'PROVINCE'; - if (accountType.startsWith('CITY_')) return 'CITY'; - return accountType; - } } diff --git a/backend/services/mining-admin-service/src/infrastructure/kafka/cdc-sync.service.ts b/backend/services/mining-admin-service/src/infrastructure/kafka/cdc-sync.service.ts index 49e3a3bd..c29f58dc 100644 --- a/backend/services/mining-admin-service/src/infrastructure/kafka/cdc-sync.service.ts +++ b/backend/services/mining-admin-service/src/infrastructure/kafka/cdc-sync.service.ts @@ -559,44 +559,33 @@ export class CdcSyncService implements OnModuleInit { /** * 处理 SystemAccountSynced 事件 - 同步系统账户算力 * 来自 contribution-service 的系统账户(运营、省、市、总部)算力同步 - * 支持组合键账户类型(如 PROVINCE_440000, CITY_440100) + * accountType: OPERATION / PROVINCE / CITY / HEADQUARTERS + * regionCode: 省/市代码,如 440000, 440100 */ private async handleSystemAccountSynced(event: ServiceEvent, tx: TransactionClient): Promise { const { payload } = event; - // 从 accountType 提取 baseType(向后兼容) - const baseType = payload.baseType || this.extractBaseType(payload.accountType); + const accountType = payload.accountType; // OPERATION / PROVINCE / CITY / HEADQUARTERS const regionCode = payload.regionCode || null; + // 使用 accountType + regionCode 作为复合唯一键 await tx.syncedSystemContribution.upsert({ - where: { accountType: payload.accountType }, + where: { + accountType_regionCode: { accountType, regionCode }, + }, create: { - accountType: payload.accountType, - baseType, + accountType, regionCode, name: payload.name, contributionBalance: payload.contributionBalance || 0, contributionNeverExpires: true, // 系统账户算力永不过期 }, update: { - baseType, - regionCode, name: payload.name, 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,记录每笔算力的来源信息 @@ -604,11 +593,15 @@ export class CdcSyncService implements OnModuleInit { private async handleSystemContributionRecordCreated(event: ServiceEvent, tx: TransactionClient): Promise { const { payload } = event; + const accountType = payload.accountType; + const regionCode = payload.regionCode || null; + await tx.syncedSystemContributionRecord.upsert({ where: { originalRecordId: BigInt(payload.recordId) }, create: { originalRecordId: BigInt(payload.recordId), - systemAccountType: payload.systemAccountType, + accountType, + regionCode, sourceAdoptionId: BigInt(payload.sourceAdoptionId), sourceAccountSequence: payload.sourceAccountSequence, distributionRate: payload.distributionRate, @@ -619,7 +612,8 @@ export class CdcSyncService implements OnModuleInit { createdAt: new Date(payload.createdAt), }, update: { - systemAccountType: payload.systemAccountType, + accountType, + regionCode, sourceAdoptionId: BigInt(payload.sourceAdoptionId), sourceAccountSequence: payload.sourceAccountSequence, distributionRate: payload.distributionRate, @@ -630,7 +624,7 @@ export class CdcSyncService implements OnModuleInit { }); 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}`, ); } diff --git a/backend/services/mining-service/prisma/migrations/0001_init/migration.sql b/backend/services/mining-service/prisma/migrations/0001_init/migration.sql index 443e3c81..99a405f9 100644 --- a/backend/services/mining-service/prisma/migrations/0001_init/migration.sql +++ b/backend/services/mining-service/prisma/migrations/0001_init/migration.sql @@ -99,11 +99,10 @@ CREATE TABLE "mining_transactions" ( CONSTRAINT "mining_transactions_pkey" PRIMARY KEY ("id") ); --- CreateTable: 系统挖矿账户 (直接使用 TEXT 组合键) +-- CreateTable: 系统挖矿账户 CREATE TABLE "system_mining_accounts" ( "id" TEXT NOT NULL, "account_type" TEXT NOT NULL, - "base_type" TEXT NOT NULL, "region_code" TEXT, "name" TEXT NOT NULL, "totalMined" DECIMAL(30, 8) NOT NULL DEFAULT 0, @@ -119,7 +118,7 @@ CREATE TABLE "system_mining_accounts" ( -- CreateTable: 系统账户挖矿记录 CREATE TABLE "system_mining_records" ( "id" TEXT NOT NULL, - "account_type" TEXT NOT NULL, + "system_account_id" TEXT NOT NULL, "mining_minute" TIMESTAMP(3) NOT NULL, "contribution_ratio" DECIMAL(30, 18) NOT NULL, "total_contribution" DECIMAL(30, 8) NOT NULL, @@ -133,7 +132,7 @@ CREATE TABLE "system_mining_records" ( -- CreateTable: 系统账户交易流水 CREATE TABLE "system_mining_transactions" ( "id" TEXT NOT NULL, - "account_type" TEXT NOT NULL, + "system_account_id" TEXT NOT NULL, "type" TEXT NOT NULL, "amount" 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"); -- 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_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"); -- 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"); -- 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 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; -- AddForeignKey: system_mining_records -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; +ALTER TABLE "system_mining_records" ADD CONSTRAINT "system_mining_records_system_account_id_fkey" + FOREIGN KEY ("system_account_id") REFERENCES "system_mining_accounts"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey: system_mining_transactions -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; +ALTER TABLE "system_mining_transactions" ADD CONSTRAINT "system_mining_transactions_system_account_id_fkey" + FOREIGN KEY ("system_account_id") REFERENCES "system_mining_accounts"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey: pending_mining_records 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 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; --- 初始化系统账户 -INSERT INTO "system_mining_accounts" ("id", "account_type", "base_type", "name", "totalMined", "availableBalance", "totalContribution", "updated_at") +-- 初始化系统账户 (无 regionCode 的汇总账户) +INSERT INTO "system_mining_accounts" ("id", "account_type", "region_code", "name", "totalMined", "availableBalance", "totalContribution", "updated_at") VALUES - (gen_random_uuid(), 'OPERATION', 'OPERATION', '运营账户', 0, 0, 0, NOW()), - (gen_random_uuid(), 'PROVINCE', 'PROVINCE', '省公司账户', 0, 0, 0, NOW()), - (gen_random_uuid(), 'CITY', 'CITY', '市公司账户', 0, 0, 0, NOW()), - (gen_random_uuid(), 'HEADQUARTERS', 'HEADQUARTERS', '总部账户', 0, 0, 0, NOW()) -ON CONFLICT ("account_type") DO NOTHING; + (gen_random_uuid(), 'OPERATION', NULL, '运营账户', 0, 0, 0, NOW()), + (gen_random_uuid(), 'HEADQUARTERS', NULL, '总部账户', 0, 0, 0, NOW()) +ON CONFLICT ("account_type", "region_code") DO NOTHING; diff --git a/backend/services/mining-service/prisma/schema.prisma b/backend/services/mining-service/prisma/schema.prisma index add519be..0e5f272e 100644 --- a/backend/services/mining-service/prisma/schema.prisma +++ b/backend/services/mining-service/prisma/schema.prisma @@ -53,13 +53,10 @@ model MiningEra { // ==================== 系统账户(运营/省/市/总部)==================== // 系统挖矿账户 -// accountType 格式: OPERATION, PROVINCE, CITY, HEADQUARTERS (汇总账户) -// PROVINCE_440000, CITY_440100 等 (按省市细分的账户) model SystemMiningAccount { id String @id @default(uuid()) - accountType String @unique @map("account_type") // 组合键 - baseType String @map("base_type") // 基础类型: OPERATION/PROVINCE/CITY/HEADQUARTERS - regionCode String? @map("region_code") // 区域代码 + accountType String @map("account_type") // OPERATION/PROVINCE/CITY/HEADQUARTERS + regionCode String? @map("region_code") // 省市代码,如 440000、440100 name String totalMined Decimal @default(0) @db.Decimal(30, 8) // 总挖到的积分股 availableBalance Decimal @default(0) @db.Decimal(30, 8) // 可用余额 @@ -71,7 +68,8 @@ model SystemMiningAccount { records SystemMiningRecord[] transactions SystemMiningTransaction[] - @@index([baseType]) + @@unique([accountType, regionCode]) + @@index([accountType]) @@index([regionCode]) @@index([totalContribution(sort: Desc)]) @@map("system_mining_accounts") @@ -80,7 +78,7 @@ model SystemMiningAccount { // 系统账户挖矿记录(分钟级别汇总) model SystemMiningRecord { id String @id @default(uuid()) - accountType String @map("account_type") // 组合键 + systemAccountId String @map("system_account_id") miningMinute DateTime @map("mining_minute") contributionRatio Decimal @db.Decimal(30, 18) @map("contribution_ratio") totalContribution Decimal @db.Decimal(30, 8) @map("total_contribution") @@ -88,29 +86,29 @@ model SystemMiningRecord { minedAmount Decimal @db.Decimal(30, 18) @map("mined_amount") 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]) @@map("system_mining_records") } // 系统账户交易流水 model SystemMiningTransaction { - id String @id @default(uuid()) - accountType String @map("account_type") // 组合键 - type String // MINE, TRANSFER_OUT, ADJUSTMENT - amount Decimal @db.Decimal(30, 8) - balanceBefore Decimal @db.Decimal(30, 8) @map("balance_before") - balanceAfter Decimal @db.Decimal(30, 8) @map("balance_after") - referenceId String? @map("reference_id") - referenceType String? @map("reference_type") - memo String? @db.Text - createdAt DateTime @default(now()) @map("created_at") + id String @id @default(uuid()) + systemAccountId String @map("system_account_id") + type String // MINE, TRANSFER_OUT, ADJUSTMENT + amount Decimal @db.Decimal(30, 8) + balanceBefore Decimal @db.Decimal(30, 8) @map("balance_before") + balanceAfter Decimal @db.Decimal(30, 8) @map("balance_after") + referenceId String? @map("reference_id") + referenceType String? @map("reference_type") + memo String? @db.Text + 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") } diff --git a/backend/services/mining-service/src/api/controllers/admin.controller.ts b/backend/services/mining-service/src/api/controllers/admin.controller.ts index 9cfca557..b102e165 100644 --- a/backend/services/mining-service/src/api/controllers/admin.controller.ts +++ b/backend/services/mining-service/src/api/controllers/admin.controller.ts @@ -163,14 +163,14 @@ export class AdminController { @ApiOperation({ summary: '获取系统账户挖矿状态' }) async getSystemAccounts() { const accounts = await this.prisma.systemMiningAccount.findMany({ - orderBy: { accountType: 'asc' }, + orderBy: [{ accountType: 'asc' }, { regionCode: 'asc' }], }); return { accounts: accounts.map((acc) => ({ + id: acc.id, accountType: acc.accountType, - baseType: acc.baseType, // 新增:基础类型 - regionCode: acc.regionCode, // 新增:区域代码 + regionCode: acc.regionCode, name: acc.name, totalMined: acc.totalMined.toString(), availableBalance: acc.availableBalance.toString(), @@ -184,35 +184,58 @@ export class AdminController { @Get('system-accounts/:accountType/records') @Public() @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: 'pageSize', required: false, type: Number }) async getSystemAccountMiningRecords( @Param('accountType') accountType: string, + @Query('regionCode') regionCode?: string, @Query('page') page?: number, @Query('pageSize') pageSize?: number, ) { const pageNum = page ?? 1; const pageSizeNum = pageSize ?? 20; 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([ this.prisma.systemMiningRecord.findMany({ - where: { accountType }, + where: { systemAccountId: account.id }, orderBy: { miningMinute: 'desc' }, skip, take: pageSizeNum, }), this.prisma.systemMiningRecord.count({ - where: { accountType }, + where: { systemAccountId: account.id }, }), ]); return { records: records.map((record) => ({ id: record.id, - accountType: record.accountType, + accountType, + regionCode: regionCode || null, miningMinute: record.miningMinute, contributionRatio: record.contributionRatio.toString(), totalContribution: record.totalContribution.toString(), @@ -223,41 +246,66 @@ export class AdminController { total, page: pageNum, pageSize: pageSizeNum, + accountType, + regionCode: regionCode || null, }; } @Get('system-accounts/:accountType/transactions') @Public() @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: 'pageSize', required: false, type: Number }) async getSystemAccountTransactions( @Param('accountType') accountType: string, + @Query('regionCode') regionCode?: string, @Query('page') page?: number, @Query('pageSize') pageSize?: number, ) { const pageNum = page ?? 1; const pageSizeNum = pageSize ?? 20; 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([ this.prisma.systemMiningTransaction.findMany({ - where: { accountType }, + where: { systemAccountId: account.id }, orderBy: { createdAt: 'desc' }, skip, take: pageSizeNum, }), this.prisma.systemMiningTransaction.count({ - where: { accountType }, + where: { systemAccountId: account.id }, }), ]); return { transactions: transactions.map((tx) => ({ id: tx.id, - accountType: tx.accountType, + accountType, + regionCode: regionCode || null, type: tx.type, amount: tx.amount.toString(), balanceBefore: tx.balanceBefore.toString(), @@ -270,6 +318,8 @@ export class AdminController { total, page: pageNum, pageSize: pageSizeNum, + accountType, + regionCode: regionCode || null, }; } diff --git a/backend/services/mining-service/src/application/event-handlers/contribution-event.handler.ts b/backend/services/mining-service/src/application/event-handlers/contribution-event.handler.ts index 9698111f..71ea8ebc 100644 --- a/backend/services/mining-service/src/application/event-handlers/contribution-event.handler.ts +++ b/backend/services/mining-service/src/application/event-handlers/contribution-event.handler.ts @@ -89,12 +89,11 @@ export class ContributionEventHandler implements OnModuleInit { }); } else if (eventType === 'SystemAccountSynced') { 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({ accountType: eventPayload.accountType, - baseType: eventPayload.baseType || eventPayload.accountType, // 向后兼容 regionCode: eventPayload.regionCode || null, name: eventPayload.name, contributionBalance: eventPayload.contributionBalance, diff --git a/backend/services/mining-service/src/application/services/mining-distribution.service.ts b/backend/services/mining-service/src/application/services/mining-distribution.service.ts index fe8c0917..f409f723 100644 --- a/backend/services/mining-service/src/application/services/mining-distribution.service.ts +++ b/backend/services/mining-service/src/application/services/mining-distribution.service.ts @@ -12,8 +12,8 @@ import { MiningCalculatorService } from '../../domain/services/mining-calculator import { ShareAmount } from '../../domain/value-objects/share-amount.vo'; 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) { await this.accumulateSystemMinuteData( data.accountType, + data.regionCode, currentMinute, data.reward, data.contribution, @@ -207,7 +208,8 @@ export class MiningDistributionService { new ShareAmount(0), ); await this.accumulateSystemMinuteData( - HEADQUARTERS_BASE_TYPE, + HEADQUARTERS_ACCOUNT_TYPE, + null, // HEADQUARTERS 没有 regionCode currentMinute, headquartersTotalReward, headquartersTotalContribution, @@ -351,7 +353,8 @@ export class MiningDistributionService { // 计算所有系统账户的挖矿奖励 const systemRewards: Array<{ - accountType: string; // 改为字符串支持组合键 + accountType: string; + regionCode: string | null; reward: ShareAmount; contribution: ShareAmount; memo: string; @@ -359,7 +362,7 @@ export class MiningDistributionService { for (const systemAccount of systemAccounts) { // 总部账户不直接参与挖矿,它只接收未解锁算力的收益 - if (systemAccount.baseType === HEADQUARTERS_BASE_TYPE) { + if (systemAccount.accountType === HEADQUARTERS_ACCOUNT_TYPE) { continue; } @@ -376,6 +379,7 @@ export class MiningDistributionService { if (!reward.isZero()) { systemRewards.push({ accountType: systemAccount.accountType, + regionCode: systemAccount.regionCode, reward, contribution: systemAccount.totalContribution, memo: `秒挖矿 ${currentSecond.getTime()}`, @@ -417,8 +421,8 @@ export class MiningDistributionService { // 在单个事务中执行所有系统账户和待解锁算力的挖矿 await this.prisma.$transaction(async (tx) => { // 处理系统账户挖矿 - for (const { accountType, reward, memo } of systemRewards) { - await this.systemMiningAccountRepository.mine(accountType, reward, memo, tx); + for (const { accountType, regionCode, reward, memo } of systemRewards) { + await this.systemMiningAccountRepository.mine(accountType as any, regionCode, reward, memo, tx); systemDistributed = systemDistributed.add(reward); systemParticipantCount++; } @@ -438,7 +442,8 @@ export class MiningDistributionService { // 一次性更新总部账户(而不是每个待解锁算力单独更新) if (!headquartersTotal.isZero()) { await this.systemMiningAccountRepository.mine( - HEADQUARTERS_BASE_TYPE, + HEADQUARTERS_ACCOUNT_TYPE as any, + null, // HEADQUARTERS 没有 regionCode headquartersTotal, `秒挖矿 ${currentSecond.getTime()} - 待解锁算力汇总 (${pendingRewards.length}笔)`, tx, @@ -447,9 +452,10 @@ export class MiningDistributionService { }); // 事务成功后,累积 Redis 数据(Redis 操作不需要在事务内) - for (const { accountType, reward, contribution } of systemRewards) { + for (const { accountType, regionCode, reward, contribution } of systemRewards) { await this.accumulateSystemMinuteData( accountType, + regionCode, currentMinute, reward, contribution, @@ -469,7 +475,8 @@ export class MiningDistributionService { new ShareAmount(0), ); await this.accumulateSystemMinuteData( - HEADQUARTERS_BASE_TYPE, + HEADQUARTERS_ACCOUNT_TYPE, + null, // HEADQUARTERS 没有 regionCode currentMinute, headquartersTotalReward, headquartersTotalContribution, @@ -508,7 +515,8 @@ export class MiningDistributionService { systemParticipantCount: number; pendingParticipantCount: number; systemRedisData: Array<{ - accountType: string; // 改为字符串支持组合键 + accountType: string; + regionCode: string | null; reward: ShareAmount; contribution: ShareAmount; }>; @@ -541,7 +549,8 @@ export class MiningDistributionService { // 计算所有系统账户的挖矿奖励 const systemRewards: Array<{ - accountType: string; // 改为字符串支持组合键 + accountType: string; + regionCode: string | null; reward: ShareAmount; contribution: ShareAmount; memo: string; @@ -549,7 +558,7 @@ export class MiningDistributionService { for (const systemAccount of systemAccounts) { // 总部账户不直接参与挖矿,它只接收未解锁算力的收益 - if (systemAccount.baseType === HEADQUARTERS_BASE_TYPE) { + if (systemAccount.accountType === HEADQUARTERS_ACCOUNT_TYPE) { continue; } @@ -566,6 +575,7 @@ export class MiningDistributionService { if (!reward.isZero()) { systemRewards.push({ accountType: systemAccount.accountType, + regionCode: systemAccount.regionCode, reward, contribution: systemAccount.totalContribution, memo: `秒挖矿 ${currentSecond.getTime()}`, @@ -612,11 +622,11 @@ export class MiningDistributionService { } // 处理系统账户挖矿(复用外部事务) - for (const { accountType, reward, contribution, memo } of systemRewards) { - await this.systemMiningAccountRepository.mine(accountType, reward, memo, tx); + for (const { accountType, regionCode, reward, contribution, memo } of systemRewards) { + await this.systemMiningAccountRepository.mine(accountType as any, regionCode, reward, memo, tx); systemDistributed = systemDistributed.add(reward); systemParticipantCount++; - systemRedisData.push({ accountType, reward, contribution }); + systemRedisData.push({ accountType, regionCode, reward, contribution }); } // 处理待解锁算力挖矿(归总部账户) @@ -633,7 +643,8 @@ export class MiningDistributionService { // 一次性更新总部账户(而不是每个待解锁算力单独更新) if (!headquartersTotal.isZero()) { await this.systemMiningAccountRepository.mine( - HEADQUARTERS_BASE_TYPE, + HEADQUARTERS_ACCOUNT_TYPE as any, + null, // HEADQUARTERS 没有 regionCode headquartersTotal, `秒挖矿 ${currentSecond.getTime()} - 待解锁算力汇总 (${pendingRewards.length}笔)`, tx, @@ -696,20 +707,26 @@ export class MiningDistributionService { /** * 累积系统账户每分钟的挖矿数据到Redis - * @param accountType 账户类型(组合键,如 PROVINCE_440000, CITY_440100) + * @param accountType 账户类型(OPERATION/PROVINCE/CITY/HEADQUARTERS) + * @param regionCode 区域代码(省市代码,如 440000, 440100) */ private async accumulateSystemMinuteData( accountType: string, + regionCode: string | null, minuteTime: Date, reward: ShareAmount, accountContribution: ShareAmount, totalContribution: ShareAmount, secondDistribution: ShareAmount, ): Promise { - 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); let accumulated: { + accountType: string; + regionCode: string | null; minedAmount: string; contributionRatio: string; totalContribution: string; @@ -728,6 +745,8 @@ export class MiningDistributionService { accumulated.accountContribution = accountContribution.value.toString(); } else { accumulated = { + accountType, + regionCode, minedAmount: reward.value.toString(), contributionRatio: accountContribution.value.dividedBy(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 { try { @@ -807,12 +827,14 @@ export class MiningDistributionService { if (!data) continue; const accumulated = JSON.parse(data); - // accountType 现在是字符串类型,支持组合键 - const accountType = key.split(':').pop()!; + // 从 accumulated 中获取 accountType 和 regionCode + const accountType = accumulated.accountType; + const regionCode = accumulated.regionCode; // 使用 repository 的 saveMinuteRecord 方法(支持 upsert) await this.systemMiningAccountRepository.saveMinuteRecord( - accountType, + accountType as any, + regionCode, minuteTime, new ShareAmount(accumulated.contributionRatio), new ShareAmount(accumulated.totalContribution), diff --git a/backend/services/mining-service/src/application/services/network-sync.service.ts b/backend/services/mining-service/src/application/services/network-sync.service.ts index 90c55d8a..cf4a387b 100644 --- a/backend/services/mining-service/src/application/services/network-sync.service.ts +++ b/backend/services/mining-service/src/application/services/network-sync.service.ts @@ -2,15 +2,14 @@ import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; import { SystemMiningAccountRepository, - SystemAccountBaseType, + SystemAccountType, } from '../../infrastructure/persistence/repositories/system-mining-account.repository'; import { ShareAmount } from '../../domain/value-objects/share-amount.vo'; import Decimal from 'decimal.js'; interface SystemAccountSyncedData { - accountType: string; // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等 - baseType: string; // 基础类型: OPERATION / PROVINCE / CITY / HEADQUARTERS - regionCode: string | null; // 区域代码: 省/市代码,如 440000, 440100 + accountType: string; // OPERATION / PROVINCE / CITY / HEADQUARTERS + regionCode: string | null; // 省/市代码,如 440000, 440100 name: 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 { try { - // 验证 baseType 是否有效 - const validBaseTypes: SystemAccountBaseType[] = ['OPERATION', 'PROVINCE', 'CITY', 'HEADQUARTERS']; - const baseType = (data.baseType || this.extractBaseType(data.accountType)) as SystemAccountBaseType; + // 验证 accountType 是否有效 + const validTypes: SystemAccountType[] = ['OPERATION', 'PROVINCE', 'CITY', 'HEADQUARTERS']; + const accountType = data.accountType as SystemAccountType; - if (!validBaseTypes.includes(baseType)) { - this.logger.warn(`Unknown system account base type: ${baseType} for ${data.accountType}`); + if (!validTypes.includes(accountType)) { + this.logger.warn(`Unknown system account type: ${accountType}`); return; } @@ -68,15 +68,14 @@ export class NetworkSyncService { // 使用 upsert 动态创建或更新账户 await this.systemAccountRepository.updateContribution( - data.accountType, - baseType, + accountType, data.regionCode, data.name, contribution, ); 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) { 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) { await this.handleSystemAccountSynced({ accountType: account.accountType, - baseType: account.baseType || account.accountType, // 向后兼容 regionCode: account.regionCode || null, name: account.name, contributionBalance: account.contributionBalance, diff --git a/backend/services/mining-service/src/infrastructure/persistence/repositories/system-mining-account.repository.ts b/backend/services/mining-service/src/infrastructure/persistence/repositories/system-mining-account.repository.ts index 13f36abf..805a9413 100644 --- a/backend/services/mining-service/src/infrastructure/persistence/repositories/system-mining-account.repository.ts +++ b/backend/services/mining-service/src/infrastructure/persistence/repositories/system-mining-account.repository.ts @@ -3,13 +3,12 @@ import { PrismaService } from '../prisma/prisma.service'; import { ShareAmount } from '../../../domain/value-objects/share-amount.vo'; import { TransactionClient } from '../unit-of-work/unit-of-work'; -// 基础类型定义(不再使用 Prisma 枚举) -export type SystemAccountBaseType = 'OPERATION' | 'PROVINCE' | 'CITY' | 'HEADQUARTERS'; +export type SystemAccountType = 'OPERATION' | 'PROVINCE' | 'CITY' | 'HEADQUARTERS'; export interface SystemMiningAccountSnapshot { - accountType: string; // 组合键: OPERATION, PROVINCE_440000, CITY_440100 等 - baseType: SystemAccountBaseType; // 基础类型 - regionCode: string | null; // 区域代码 + id: string; + accountType: SystemAccountType; + regionCode: string | null; name: string; totalMined: ShareAmount; availableBalance: ShareAmount; @@ -21,9 +20,14 @@ export interface SystemMiningAccountSnapshot { export class SystemMiningAccountRepository { constructor(private readonly prisma: PrismaService) {} - async findByType(accountType: string): Promise { + async findByTypeAndRegion( + accountType: SystemAccountType, + regionCode: string | null, + ): Promise { const record = await this.prisma.systemMiningAccount.findUnique({ - where: { accountType }, + where: { + accountType_regionCode: { accountType, regionCode }, + }, }); if (!record) { @@ -34,12 +38,12 @@ export class SystemMiningAccountRepository { } /** - * 根据基础类型查找所有账户 + * 根据类型查找所有账户 */ - async findByBaseType(baseType: SystemAccountBaseType): Promise { + async findByType(accountType: SystemAccountType): Promise { const records = await this.prisma.systemMiningAccount.findMany({ - where: { baseType }, - orderBy: { accountType: 'asc' }, + where: { accountType }, + orderBy: { regionCode: 'asc' }, }); return records.map((r) => this.toSnapshot(r)); @@ -47,29 +51,28 @@ export class SystemMiningAccountRepository { async findAll(): Promise { const records = await this.prisma.systemMiningAccount.findMany({ - orderBy: { accountType: 'asc' }, + orderBy: [{ accountType: 'asc' }, { regionCode: 'asc' }], }); return records.map((r) => this.toSnapshot(r)); } /** - * 确保基础汇总账户存在 + * 确保基础账户存在 */ async ensureSystemAccountsExist(): Promise { - const accounts: { accountType: string; baseType: SystemAccountBaseType; name: string }[] = [ - { accountType: 'OPERATION', baseType: 'OPERATION', name: '运营账户' }, - { accountType: 'PROVINCE', baseType: 'PROVINCE', name: '省公司账户' }, - { accountType: 'CITY', baseType: 'CITY', name: '市公司账户' }, - { accountType: 'HEADQUARTERS', baseType: 'HEADQUARTERS', name: '总部账户' }, + const accounts: { accountType: SystemAccountType; name: string }[] = [ + { accountType: 'OPERATION', name: '运营账户' }, + { accountType: 'HEADQUARTERS', name: '总部账户' }, ]; for (const account of accounts) { await this.prisma.systemMiningAccount.upsert({ - where: { accountType: account.accountType }, + where: { + accountType_regionCode: { accountType: account.accountType, regionCode: null }, + }, create: { accountType: account.accountType, - baseType: account.baseType, regionCode: null, name: account.name, totalMined: 0, @@ -82,20 +85,20 @@ export class SystemMiningAccountRepository { } /** - * 更新系统账户算力(支持动态创建省市账户) + * 更新系统账户算力(动态创建省市账户) */ async updateContribution( - accountType: string, - baseType: SystemAccountBaseType, + accountType: SystemAccountType, regionCode: string | null, name: string, contribution: ShareAmount, ): Promise { await this.prisma.systemMiningAccount.upsert({ - where: { accountType }, + where: { + accountType_regionCode: { accountType, regionCode }, + }, create: { accountType, - baseType, regionCode, name, totalContribution: contribution.value, @@ -117,25 +120,24 @@ export class SystemMiningAccountRepository { } /** - * 执行系统账户挖矿(带外部事务支持) - * @param accountType 账户类型(组合键) - * @param amount 挖矿数量 - * @param memo 备注 - * @param tx 可选的外部事务客户端,如果不传则自动创建事务 + * 执行系统账户挖矿 */ async mine( - accountType: string, + accountType: SystemAccountType, + regionCode: string | null, amount: ShareAmount, memo: string, tx?: TransactionClient, ): Promise { const executeInTx = async (client: TransactionClient) => { const account = await client.systemMiningAccount.findUnique({ - where: { accountType }, + where: { + accountType_regionCode: { accountType, regionCode }, + }, }); if (!account) { - throw new Error(`System account ${accountType} not found`); + throw new Error(`System account ${accountType}:${regionCode} not found`); } const balanceBefore = account.availableBalance; @@ -143,7 +145,7 @@ export class SystemMiningAccountRepository { const totalMined = account.totalMined.plus(amount.value); await client.systemMiningAccount.update({ - where: { accountType }, + where: { id: account.id }, data: { totalMined, availableBalance: balanceAfter, @@ -152,7 +154,7 @@ export class SystemMiningAccountRepository { await client.systemMiningTransaction.create({ data: { - accountType, + systemAccountId: account.id, type: 'MINE', amount: amount.value, balanceBefore, @@ -163,31 +165,40 @@ export class SystemMiningAccountRepository { }; if (tx) { - // 使用外部事务 await executeInTx(tx); } else { - // 自动创建事务(向后兼容) await this.prisma.$transaction(executeInTx); } } async saveMinuteRecord( - accountType: string, + accountType: SystemAccountType, + regionCode: string | null, miningMinute: Date, contributionRatio: ShareAmount, totalContribution: ShareAmount, secondDistribution: ShareAmount, minedAmount: ShareAmount, ): Promise { + 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({ where: { - accountType_miningMinute: { - accountType, + systemAccountId_miningMinute: { + systemAccountId: account.id, miningMinute, }, }, create: { - accountType, + systemAccountId: account.id, miningMinute, contributionRatio: contributionRatio.value, totalContribution: totalContribution.value, @@ -202,8 +213,8 @@ export class SystemMiningAccountRepository { private toSnapshot(record: any): SystemMiningAccountSnapshot { return { - accountType: record.accountType, - baseType: record.baseType as SystemAccountBaseType, + id: record.id, + accountType: record.accountType as SystemAccountType, regionCode: record.regionCode, name: record.name, totalMined: new ShareAmount(record.totalMined),