diff --git a/backend/services/trading-service/src/api/controllers/admin.controller.ts b/backend/services/trading-service/src/api/controllers/admin.controller.ts index d5821d3a..49108192 100644 --- a/backend/services/trading-service/src/api/controllers/admin.controller.ts +++ b/backend/services/trading-service/src/api/controllers/admin.controller.ts @@ -1,12 +1,16 @@ -import { Controller, Get } from '@nestjs/common'; -import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { Controller, Get, Post, HttpCode, HttpStatus } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; +import { TradingConfigRepository } from '../../infrastructure/persistence/repositories/trading-config.repository'; import { Public } from '../../shared/guards/jwt-auth.guard'; @ApiTags('Admin') @Controller('admin') export class AdminController { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly tradingConfigRepository: TradingConfigRepository, + ) {} @Get('accounts/sync') @Public() @@ -41,4 +45,93 @@ export class AdminController { total: accounts.length, }; } + + @Get('trading/status') + @Public() + @ApiOperation({ summary: '获取交易系统状态' }) + @ApiResponse({ status: 200, description: '返回交易系统配置状态' }) + async getTradingStatus() { + const config = await this.tradingConfigRepository.getConfig(); + if (!config) { + return { + initialized: false, + isActive: false, + activatedAt: null, + message: '交易系统未初始化', + }; + } + + return { + initialized: true, + isActive: config.isActive, + activatedAt: config.activatedAt, + totalShares: config.totalShares.toFixed(8), + burnTarget: config.burnTarget.toFixed(8), + burnPeriodMinutes: config.burnPeriodMinutes, + minuteBurnRate: config.minuteBurnRate.toFixed(18), + message: config.isActive ? '交易系统已激活' : '交易系统未激活', + }; + } + + @Post('trading/activate') + @Public() // TODO: 生产环境应添加管理员权限验证 + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '激活交易/销毁系统' }) + @ApiResponse({ status: 200, description: '交易系统已激活' }) + @ApiResponse({ status: 400, description: '交易系统未初始化' }) + async activateTrading() { + const config = await this.tradingConfigRepository.getConfig(); + if (!config) { + return { + success: false, + message: '交易系统未初始化,请先启动服务', + }; + } + + if (config.isActive) { + return { + success: true, + message: '交易系统已处于激活状态', + activatedAt: config.activatedAt, + }; + } + + await this.tradingConfigRepository.activate(); + const updatedConfig = await this.tradingConfigRepository.getConfig(); + + return { + success: true, + message: '交易系统已激活,每分钟销毁已开始运行', + activatedAt: updatedConfig?.activatedAt, + }; + } + + @Post('trading/deactivate') + @Public() // TODO: 生产环境应添加管理员权限验证 + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '关闭交易/销毁系统' }) + @ApiResponse({ status: 200, description: '交易系统已关闭' }) + async deactivateTrading() { + const config = await this.tradingConfigRepository.getConfig(); + if (!config) { + return { + success: false, + message: '交易系统未初始化', + }; + } + + if (!config.isActive) { + return { + success: true, + message: '交易系统已处于关闭状态', + }; + } + + await this.tradingConfigRepository.deactivate(); + + return { + success: true, + message: '交易系统已关闭,每分钟销毁已暂停', + }; + } } diff --git a/frontend/mining-admin-web/.env.production b/frontend/mining-admin-web/.env.production index 75f94b61..1be8ff7f 100644 --- a/frontend/mining-admin-web/.env.production +++ b/frontend/mining-admin-web/.env.production @@ -1,2 +1,3 @@ NEXT_PUBLIC_API_URL=https://rwaapi.szaiai.com/api/v2/mining-admin +TRADING_SERVICE_URL=https://rwaapi.szaiai.com/api/v2/trading NEXT_PUBLIC_APP_NAME=挖矿管理后台 diff --git a/frontend/mining-admin-web/next.config.js b/frontend/mining-admin-web/next.config.js index 2dcb9a1b..659554aa 100644 --- a/frontend/mining-admin-web/next.config.js +++ b/frontend/mining-admin-web/next.config.js @@ -6,9 +6,17 @@ const nextConfig = { // NEXT_PUBLIC_API_URL 应该是后端服务的基础 URL,如 http://mining-admin-service:3023 // 前端请求 /api/xxx 会被转发到 {API_URL}/api/v2/xxx const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3023'; + const tradingServiceUrl = process.env.TRADING_SERVICE_URL || 'http://localhost:3020'; // 移除末尾可能存在的 /api/v2 避免重复 const cleanUrl = apiBaseUrl.replace(/\/api\/v2\/?$/, ''); + const cleanTradingUrl = tradingServiceUrl.replace(/\/api\/v2\/?$/, ''); return [ + // trading-service 路由 + { + source: '/api/trading/:path*', + destination: `${cleanTradingUrl}/api/v2/:path*`, + }, + // mining-admin-service 路由 (默认) { source: '/api/:path*', destination: `${cleanUrl}/api/v2/:path*`, diff --git a/frontend/mining-admin-web/src/app/(dashboard)/trading/page.tsx b/frontend/mining-admin-web/src/app/(dashboard)/trading/page.tsx new file mode 100644 index 00000000..2d693d91 --- /dev/null +++ b/frontend/mining-admin-web/src/app/(dashboard)/trading/page.tsx @@ -0,0 +1,373 @@ +'use client'; + +import { useState } from 'react'; +import { PageHeader } from '@/components/layout/page-header'; +import { + useTradingStatus, + useActivateTrading, + useDeactivateTrading, + useBurnStatus, + useBurnRecords, + useMarketOverview, +} from '@/features/trading/hooks/use-trading'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Play, + Pause, + AlertCircle, + CheckCircle2, + TrendingUp, + Flame, + DollarSign, + Activity, + RefreshCw, +} from 'lucide-react'; +import { formatDistanceToNow } from 'date-fns'; +import { zhCN } from 'date-fns/locale'; + +export default function TradingPage() { + const { data: tradingStatus, isLoading: tradingLoading, refetch: refetchTrading } = useTradingStatus(); + const { data: burnStatus, isLoading: burnLoading, refetch: refetchBurn } = useBurnStatus(); + const { data: marketOverview, isLoading: marketLoading, refetch: refetchMarket } = useMarketOverview(); + const activateTrading = useActivateTrading(); + const deactivateTrading = useDeactivateTrading(); + + const [recordsPage, setRecordsPage] = useState(1); + const [recordsFilter, setRecordsFilter] = useState<'ALL' | 'MINUTE_BURN' | 'SELL_BURN'>('ALL'); + const { data: burnRecords, isLoading: recordsLoading } = useBurnRecords( + recordsPage, + 20, + recordsFilter === 'ALL' ? undefined : recordsFilter + ); + + const formatNumber = (value: string | undefined, decimals: number = 2) => { + if (!value) return '0'; + const num = parseFloat(value); + if (isNaN(num)) return '0'; + return num.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); + }; + + const formatPrice = (value: string | undefined) => { + if (!value) return '0'; + const num = parseFloat(value); + if (isNaN(num)) return '0'; + return num.toFixed(18); + }; + + const handleRefresh = () => { + refetchTrading(); + refetchBurn(); + refetchMarket(); + }; + + return ( +
交易系统未初始化
+请确保 trading-service 已正常启动
+总积分股
+{formatNumber(tradingStatus.totalShares, 0)}
+销毁目标
+{formatNumber(tradingStatus.burnTarget, 0)}
+销毁周期
+{tradingStatus.burnPeriodMinutes?.toLocaleString()} 分钟
+每分钟销毁率
+{formatNumber(tradingStatus.minuteBurnRate, 8)}
++ 激活时间: {new Date(tradingStatus.activatedAt).toLocaleString()} ( + {formatDistanceToNow(new Date(tradingStatus.activatedAt), { addSuffix: true, locale: zhCN })}) +
+无法获取市场数据
+绿积分
+{formatNumber(marketOverview.greenPoints, 8)}
+流通池
+{formatNumber(marketOverview.circulationPool, 8)}
+有效分母
+{formatNumber(marketOverview.effectiveDenominator, 0)}
+销毁倍数
+{formatNumber(marketOverview.burnMultiplier, 8)}
+无法获取销毁数据
+已销毁
+{formatNumber(burnStatus.totalBurned, 8)}
+剩余目标
+{formatNumber(burnStatus.remainingBurn, 8)}
+剩余分钟
+{burnStatus.remainingMinutes?.toLocaleString()}
+每分钟销毁
+{formatNumber(burnStatus.minuteBurnRate, 8)}
++ 最后销毁: {new Date(burnStatus.lastBurnMinute).toLocaleString()} +
+ )} +暂无销毁记录
+共 {burnRecords.total} 条记录
+