feat(mining-service): add initialization APIs and seed script

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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-14 02:36:52 -08:00
parent bd0f98cfb3
commit 25608babd6
3 changed files with 324 additions and 3 deletions

View File

@ -16,7 +16,8 @@
"prisma:generate": "prisma generate", "prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev", "prisma:migrate": "prisma migrate dev",
"prisma:migrate:prod": "prisma migrate deploy", "prisma:migrate:prod": "prisma migrate deploy",
"prisma:studio": "prisma studio" "prisma:studio": "prisma studio",
"prisma:seed": "ts-node prisma/seed.ts"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^10.3.0", "@nestjs/common": "^10.3.0",

View File

@ -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 (1010亿)
* - 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();
});

View File

@ -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 { ApiTags, ApiOperation } from '@nestjs/swagger';
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
import { Public } from '../../shared/guards/jwt-auth.guard'; 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') @ApiTags('Admin')
@Controller('admin') @Controller('admin')
export class AdminController { 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') @Get('accounts/sync')
@Public() @Public()
@ -37,4 +47,123 @@ export class AdminController {
total: accounts.length, 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' };
}
} }