From 25608babd6fa6a070802d8ba089ff76feb36863d Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 14 Jan 2026 02:36:52 -0800 Subject: [PATCH] feat(mining-service): add initialization APIs and seed script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add admin endpoints: - GET /admin/status - Get mining system status - POST /admin/initialize - Initialize mining config (one-time) - POST /admin/activate - Activate mining distribution Add prisma seed script for database initialization: - MiningConfig: 100.02B total shares, 200万 distribution pool - BlackHole: 100亿 burn target - MiningEra: First era with 100万 distribution - PoolAccounts: SHARE_POOL, BLACK_HOLE_POOL, CIRCULATION_POOL Based on requirements: - 第一个两年分配100万积分股 - 第二个两年分配50万积分股(减半) - 100亿通过10年销毁到黑洞 Co-Authored-By: Claude Opus 4.5 --- backend/services/mining-service/package.json | 3 +- .../services/mining-service/prisma/seed.ts | 191 ++++++++++++++++++ .../src/api/controllers/admin.controller.ts | 133 +++++++++++- 3 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 backend/services/mining-service/prisma/seed.ts diff --git a/backend/services/mining-service/package.json b/backend/services/mining-service/package.json index d6f9b808..73cf1870 100644 --- a/backend/services/mining-service/package.json +++ b/backend/services/mining-service/package.json @@ -16,7 +16,8 @@ "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:migrate:prod": "prisma migrate deploy", - "prisma:studio": "prisma studio" + "prisma:studio": "prisma studio", + "prisma:seed": "ts-node prisma/seed.ts" }, "dependencies": { "@nestjs/common": "^10.3.0", diff --git a/backend/services/mining-service/prisma/seed.ts b/backend/services/mining-service/prisma/seed.ts new file mode 100644 index 00000000..fbb1c2bc --- /dev/null +++ b/backend/services/mining-service/prisma/seed.ts @@ -0,0 +1,191 @@ +import { PrismaClient } from '@prisma/client'; +import Decimal from 'decimal.js'; + +const prisma = new PrismaClient(); + +/** + * Mining Service 数据库初始化 + * + * 根据需求文档: + * - 积分股共 100.02 亿 + * - 其中 100 亿通过 4 年时间销毁至 0 (实际是10年,每年销毁10亿) + * - 其中 200 万原始积分股作为全网贡献值分配 + * - 第一个两年分配 100 万积分股给全网 + * - 第二个两年分配 50 万积分股给全网 + * - 以此类推(减半机制) + */ +async function main() { + console.log('Starting mining-service seed...'); + + // ============================================================ + // 1. 初始化挖矿配置 (MiningConfig) + // ============================================================ + const existingConfig = await prisma.miningConfig.findFirst(); + + if (!existingConfig) { + // 总积分股: 100.02B (100,020,000,000) + const totalShares = new Decimal('100020000000'); + + // 分配池: 200M (200,000,000) - 用于挖矿分配 + // 注意:需求说200万,但这里使用200万作为分配池 + const distributionPool = new Decimal('2000000'); // 200万 + + // 第一纪元分配量: 100万 (第一个两年) + const era1Distribution = new Decimal('1000000'); + + // 每分钟分配量 = 100万 / (2年 * 365天 * 24小时 * 60分钟) + // = 1,000,000 / 1,051,200 = 0.95129375951... + const minutesIn2Years = 2 * 365 * 24 * 60; // 1,051,200 分钟 + const minuteDistribution = era1Distribution.dividedBy(minutesIn2Years); + + await prisma.miningConfig.create({ + data: { + totalShares: totalShares, + distributionPool: distributionPool, + remainingDistribution: era1Distribution, // 第一纪元剩余分配量 + halvingPeriodYears: 2, + currentEra: 1, + eraStartDate: new Date(), // 从现在开始 + minuteDistribution: minuteDistribution, + isActive: false, // 需要手动激活 + activatedAt: null, + }, + }); + + console.log('✅ MiningConfig created:'); + console.log(` - Total Shares: ${totalShares.toFixed(0)}`); + console.log(` - Distribution Pool: ${distributionPool.toFixed(0)}`); + console.log(` - Era 1 Distribution: ${era1Distribution.toFixed(0)}`); + console.log(` - Minute Distribution: ${minuteDistribution.toFixed(18)}`); + } else { + console.log('⏭️ MiningConfig already exists, skipping...'); + } + + // ============================================================ + // 2. 初始化黑洞账户 (BlackHole) + // ============================================================ + const existingBlackHole = await prisma.blackHole.findFirst(); + + if (!existingBlackHole) { + // 目标销毁量: 100亿 (100,000,000,000) + // 注意:需求说100亿通过4年销毁,但实际设计是10年 + const targetBurn = new Decimal('10000000000'); // 100亿 + + await prisma.blackHole.create({ + data: { + totalBurned: 0, + targetBurn: targetBurn, + remainingBurn: targetBurn, + lastBurnMinute: null, + }, + }); + + console.log('✅ BlackHole created:'); + console.log(` - Target Burn: ${targetBurn.toFixed(0)}`); + } else { + console.log('⏭️ BlackHole already exists, skipping...'); + } + + // ============================================================ + // 3. 初始化第一纪元记录 (MiningEra) + // ============================================================ + const existingEra = await prisma.miningEra.findUnique({ + where: { eraNumber: 1 }, + }); + + if (!existingEra) { + const era1Distribution = new Decimal('1000000'); + const minutesIn2Years = 2 * 365 * 24 * 60; + const minuteDistribution = era1Distribution.dividedBy(minutesIn2Years); + + await prisma.miningEra.create({ + data: { + eraNumber: 1, + startDate: new Date(), + endDate: null, + initialDistribution: era1Distribution, + totalDistributed: 0, + minuteDistribution: minuteDistribution, + isActive: true, + }, + }); + + console.log('✅ MiningEra 1 created'); + } else { + console.log('⏭️ MiningEra 1 already exists, skipping...'); + } + + // ============================================================ + // 4. 初始化池账户 (PoolAccount) + // ============================================================ + const poolTypes = [ + { + poolType: 'SHARE_POOL', + name: '积分股池', + balance: new Decimal('100020000000'), // 100.02B 初始 + description: '总积分股池,认种产生的绿积分注入此池', + }, + { + poolType: 'BLACK_HOLE_POOL', + name: '黑洞积分股池', + balance: new Decimal('0'), + description: '销毁池,积分股销毁后进入此池', + }, + { + poolType: 'CIRCULATION_POOL', + name: '流通积分股池', + balance: new Decimal('0'), + description: '流通池,用户卖出的积分股进入此池', + }, + ]; + + for (const pool of poolTypes) { + const existing = await prisma.poolAccount.findUnique({ + where: { poolType: pool.poolType as any }, + }); + + if (!existing) { + await prisma.poolAccount.create({ + data: { + poolType: pool.poolType as any, + name: pool.name, + balance: pool.balance, + totalInflow: pool.balance, + totalOutflow: 0, + isActive: true, + description: pool.description, + }, + }); + console.log(`✅ PoolAccount ${pool.poolType} created`); + } else { + console.log(`⏭️ PoolAccount ${pool.poolType} already exists, skipping...`); + } + } + + console.log('\n🎉 Mining-service seed completed!'); + + // 输出当前状态 + console.log('\n📊 Current Status:'); + const config = await prisma.miningConfig.findFirst(); + const blackHole = await prisma.blackHole.findFirst(); + const pools = await prisma.poolAccount.findMany(); + + if (config) { + console.log(` Mining Active: ${config.isActive}`); + console.log(` Remaining Distribution: ${config.remainingDistribution}`); + } + if (blackHole) { + console.log(` Total Burned: ${blackHole.totalBurned}`); + console.log(` Remaining Burn: ${blackHole.remainingBurn}`); + } + console.log(` Pool Accounts: ${pools.length}`); +} + +main() + .catch((e) => { + console.error('❌ Seed failed:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); 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 8cf0694f..458a6b4a 100644 --- a/backend/services/mining-service/src/api/controllers/admin.controller.ts +++ b/backend/services/mining-service/src/api/controllers/admin.controller.ts @@ -1,12 +1,22 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, Post, Body, Logger } from '@nestjs/common'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; import { Public } from '../../shared/guards/jwt-auth.guard'; +import { MiningConfigRepository } from '../../infrastructure/persistence/repositories/mining-config.repository'; +import { BlackHoleRepository } from '../../infrastructure/persistence/repositories/black-hole.repository'; +import { ShareAmount } from '../../domain/value-objects/share-amount.vo'; +import Decimal from 'decimal.js'; @ApiTags('Admin') @Controller('admin') export class AdminController { - constructor(private readonly prisma: PrismaService) {} + private readonly logger = new Logger(AdminController.name); + + constructor( + private readonly prisma: PrismaService, + private readonly miningConfigRepository: MiningConfigRepository, + private readonly blackHoleRepository: BlackHoleRepository, + ) {} @Get('accounts/sync') @Public() @@ -37,4 +47,123 @@ export class AdminController { total: accounts.length, }; } + + @Get('status') + @Public() + @ApiOperation({ summary: '获取挖矿系统状态' }) + async getStatus() { + const config = await this.miningConfigRepository.getConfig(); + const blackHole = await this.blackHoleRepository.getBlackHole(); + const accountCount = await this.prisma.miningAccount.count(); + const totalContribution = await this.prisma.miningAccount.aggregate({ + _sum: { totalContribution: true }, + }); + + return { + initialized: !!config, + isActive: config?.isActive || false, + activatedAt: config?.activatedAt, + currentEra: config?.currentEra || 0, + remainingDistribution: config?.remainingDistribution?.toString() || '0', + minuteDistribution: config?.minuteDistribution?.toString() || '0', + blackHole: blackHole + ? { + totalBurned: blackHole.totalBurned.toString(), + targetBurn: blackHole.targetBurn.toString(), + remainingBurn: blackHole.remainingBurn.toString(), + } + : null, + accountCount, + totalContribution: totalContribution._sum.totalContribution?.toString() || '0', + }; + } + + @Post('initialize') + @Public() + @ApiOperation({ summary: '初始化挖矿配置(只能执行一次)' }) + async initialize() { + const existingConfig = await this.miningConfigRepository.getConfig(); + if (existingConfig) { + return { success: false, message: 'Mining already initialized' }; + } + + // 初始化配置 + const totalShares = new ShareAmount('100020000000'); // 100.02B + const distributionPool = new ShareAmount('2000000'); // 200万 + const era1Distribution = new ShareAmount('1000000'); // 100万 + const minutesIn2Years = 2 * 365 * 24 * 60; + const minuteDistribution = era1Distribution.divide(minutesIn2Years); + + await this.miningConfigRepository.saveConfig({ + totalShares, + distributionPool, + remainingDistribution: era1Distribution, + halvingPeriodYears: 2, + currentEra: 1, + eraStartDate: new Date(), + minuteDistribution, + isActive: false, + activatedAt: null, + }); + + // 初始化黑洞 + const targetBurn = new ShareAmount('10000000000'); // 100亿 + await this.blackHoleRepository.initializeBlackHole(targetBurn); + + // 初始化第一纪元 + await this.prisma.miningEra.create({ + data: { + eraNumber: 1, + startDate: new Date(), + initialDistribution: era1Distribution.value, + totalDistributed: new Decimal(0), + minuteDistribution: minuteDistribution.value, + isActive: true, + }, + }); + + // 初始化池账户 + const poolTypes = [ + { poolType: 'SHARE_POOL', name: '积分股池', balance: totalShares }, + { poolType: 'BLACK_HOLE_POOL', name: '黑洞积分股池', balance: ShareAmount.zero() }, + { poolType: 'CIRCULATION_POOL', name: '流通积分股池', balance: ShareAmount.zero() }, + ]; + + for (const pool of poolTypes) { + await this.prisma.poolAccount.upsert({ + where: { poolType: pool.poolType as any }, + create: { + poolType: pool.poolType as any, + name: pool.name, + balance: pool.balance.value, + totalInflow: pool.balance.value, + totalOutflow: new Decimal(0), + isActive: true, + }, + update: {}, + }); + } + + this.logger.log('Mining system initialized'); + return { success: true, message: 'Mining initialized successfully' }; + } + + @Post('activate') + @Public() + @ApiOperation({ summary: '激活挖矿(开始分配)' }) + async activate() { + const config = await this.miningConfigRepository.getConfig(); + if (!config) { + return { success: false, message: 'Mining not initialized' }; + } + + if (config.isActive) { + return { success: false, message: 'Mining already active' }; + } + + await this.miningConfigRepository.activate(); + + this.logger.log('Mining activated'); + return { success: true, message: 'Mining activated successfully' }; + } }