From 46b68e8652a643ec272c5a63344f55c98470498f Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 4 Jan 2026 06:55:08 -0800 Subject: [PATCH] feat(planting-service): add global stats API for data verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new endpoint GET /api/v1/planting/stats/global to query planting statistics directly from the database, providing reliable data source for verifying reporting-service statistics. New features: - GlobalPlantingStats: total tree count, order count, amount - StatusDistribution: breakdown by order status (PAID to MINING_ENABLED) - TodayStats: daily statistics with tree count, order count, amount Implementation: - Pure additive changes, no modifications to existing code - Read-only aggregate queries using Prisma aggregate/groupBy - No database schema changes required 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../planting-service/src/api/api.module.ts | 2 + .../src/api/controllers/index.ts | 1 + .../controllers/planting-stats.controller.ts | 72 ++++++++++ .../dto/response/planting-stats.response.ts | 58 ++++++++ .../services/planting-application.service.ts | 49 +++++++ .../planting-order.repository.interface.ts | 38 +++++ .../planting-order.repository.impl.ts | 136 +++++++++++++++++- 7 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 backend/services/planting-service/src/api/controllers/planting-stats.controller.ts create mode 100644 backend/services/planting-service/src/api/dto/response/planting-stats.response.ts diff --git a/backend/services/planting-service/src/api/api.module.ts b/backend/services/planting-service/src/api/api.module.ts index 0f136fe2..ea391d67 100644 --- a/backend/services/planting-service/src/api/api.module.ts +++ b/backend/services/planting-service/src/api/api.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { PlantingOrderController } from './controllers/planting-order.controller'; import { PlantingPositionController } from './controllers/planting-position.controller'; +import { PlantingStatsController } from './controllers/planting-stats.controller'; import { HealthController } from './controllers/health.controller'; import { ContractSigningController, @@ -14,6 +15,7 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard'; controllers: [ PlantingOrderController, PlantingPositionController, + PlantingStatsController, HealthController, ContractSigningController, ContractSigningConfigController, diff --git a/backend/services/planting-service/src/api/controllers/index.ts b/backend/services/planting-service/src/api/controllers/index.ts index ea137c8e..d9146cb7 100644 --- a/backend/services/planting-service/src/api/controllers/index.ts +++ b/backend/services/planting-service/src/api/controllers/index.ts @@ -1,4 +1,5 @@ export * from './planting-order.controller'; export * from './planting-position.controller'; +export * from './planting-stats.controller'; export * from './health.controller'; export * from './contract-signing.controller'; diff --git a/backend/services/planting-service/src/api/controllers/planting-stats.controller.ts b/backend/services/planting-service/src/api/controllers/planting-stats.controller.ts new file mode 100644 index 00000000..785b35ed --- /dev/null +++ b/backend/services/planting-service/src/api/controllers/planting-stats.controller.ts @@ -0,0 +1,72 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, +} from '@nestjs/swagger'; +import { PlantingApplicationService } from '../../application/services/planting-application.service'; +import { JwtAuthGuard } from '../guards/jwt-auth.guard'; +import { GlobalStatsResponseDto } from '../dto/response/planting-stats.response'; + +/** + * 认种统计控制器 + * 提供全局统计数据 API,数据直接从订单表聚合查询 + */ +@ApiTags('认种统计') +@Controller('planting/stats') +export class PlantingStatsController { + constructor( + private readonly plantingService: PlantingApplicationService, + ) {} + + /** + * 获取全局认种统计(公开接口,无需认证) + * 此接口可用于验证 reporting-service 的统计数据准确性 + */ + @Get('global') + @ApiOperation({ + summary: '获取全局认种统计', + description: '从订单表实时聚合统计数据,可用于验证其他服务的统计准确性', + }) + @ApiResponse({ + status: 200, + description: '全局统计数据', + type: GlobalStatsResponseDto, + }) + async getGlobalStats(): Promise<{ code: number; message: string; data: GlobalStatsResponseDto }> { + const stats = await this.plantingService.getGlobalStats(); + + return { + code: 0, + message: 'success', + data: stats, + }; + } + + /** + * 获取全局认种统计(需要认证) + * 管理后台调用此接口 + */ + @Get('admin/global') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ + summary: '获取全局认种统计(管理员)', + description: '从订单表实时聚合统计数据,需要管理员权限', + }) + @ApiResponse({ + status: 200, + description: '全局统计数据', + type: GlobalStatsResponseDto, + }) + async getAdminGlobalStats(): Promise<{ code: number; message: string; data: GlobalStatsResponseDto }> { + const stats = await this.plantingService.getGlobalStats(); + + return { + code: 0, + message: 'success', + data: stats, + }; + } +} diff --git a/backend/services/planting-service/src/api/dto/response/planting-stats.response.ts b/backend/services/planting-service/src/api/dto/response/planting-stats.response.ts new file mode 100644 index 00000000..8361db84 --- /dev/null +++ b/backend/services/planting-service/src/api/dto/response/planting-stats.response.ts @@ -0,0 +1,58 @@ +import { ApiProperty } from '@nestjs/swagger'; + +/** + * 状态分布 DTO + */ +export class StatusDistributionDto { + @ApiProperty({ description: '已支付状态的认种数', example: 100 }) + paid: number; + + @ApiProperty({ description: '资金已分配状态的认种数', example: 80 }) + fundAllocated: number; + + @ApiProperty({ description: '底池已排期状态的认种数', example: 50 }) + poolScheduled: number; + + @ApiProperty({ description: '底池已注入状态的认种数', example: 30 }) + poolInjected: number; + + @ApiProperty({ description: '挖矿已开启状态的认种数(有效认种)', example: 500 }) + miningEnabled: number; +} + +/** + * 今日统计 DTO + */ +export class TodayStatsDto { + @ApiProperty({ description: '今日认种棵数', example: 10 }) + treeCount: number; + + @ApiProperty({ description: '今日订单数', example: 5 }) + orderCount: number; + + @ApiProperty({ description: '今日认种金额', example: '1000.00' }) + amount: string; +} + +/** + * 全局统计响应 DTO + */ +export class GlobalStatsResponseDto { + @ApiProperty({ description: '总认种棵数(PAID及之后状态)', example: 760 }) + totalTreeCount: number; + + @ApiProperty({ description: '总订单数', example: 150 }) + totalOrderCount: number; + + @ApiProperty({ description: '总认种金额', example: '76000.00' }) + totalAmount: string; + + @ApiProperty({ description: '按状态分布的认种数', type: StatusDistributionDto }) + statusDistribution: StatusDistributionDto; + + @ApiProperty({ description: '今日统计', type: TodayStatsDto }) + todayStats: TodayStatsDto; + + @ApiProperty({ description: '统计计算时间', example: '2026-01-04T12:00:00.000Z' }) + calculatedAt: string; +} diff --git a/backend/services/planting-service/src/application/services/planting-application.service.ts b/backend/services/planting-service/src/application/services/planting-application.service.ts index c8ad2762..041b56c4 100644 --- a/backend/services/planting-service/src/application/services/planting-application.service.ts +++ b/backend/services/planting-service/src/application/services/planting-application.service.ts @@ -509,4 +509,53 @@ export class PlantingApplicationService { return events; } + + /** + * 获取全局认种统计数据 + * 直接从订单表聚合查询,确保数据可靠性 + */ + async getGlobalStats(): Promise { + this.logger.log('Getting global planting stats from database'); + + const [globalStats, todayStats, statusDistribution] = await Promise.all([ + this.orderRepository.getGlobalStats(), + this.orderRepository.getTodayStats(), + this.orderRepository.getStatusDistribution(), + ]); + + return { + totalTreeCount: globalStats.totalTreeCount, + totalOrderCount: globalStats.totalOrderCount, + totalAmount: globalStats.totalAmount, + statusDistribution, + todayStats, + calculatedAt: new Date().toISOString(), + }; + } +} + +/** 全局统计结果 */ +export interface GlobalStatsResult { + /** 总认种棵数(PAID及之后状态) */ + totalTreeCount: number; + /** 总订单数 */ + totalOrderCount: number; + /** 总金额 */ + totalAmount: string; + /** 按状态分布的认种数 */ + statusDistribution: { + paid: number; + fundAllocated: number; + poolScheduled: number; + poolInjected: number; + miningEnabled: number; + }; + /** 今日统计 */ + todayStats: { + treeCount: number; + orderCount: number; + amount: string; + }; + /** 统计时间 */ + calculatedAt: string; } diff --git a/backend/services/planting-service/src/domain/repositories/planting-order.repository.interface.ts b/backend/services/planting-service/src/domain/repositories/planting-order.repository.interface.ts index 5a0625d7..873a6f7c 100644 --- a/backend/services/planting-service/src/domain/repositories/planting-order.repository.interface.ts +++ b/backend/services/planting-service/src/domain/repositories/planting-order.repository.interface.ts @@ -24,6 +24,44 @@ export interface IPlantingOrderRepository { * 用于用户完成 KYC 后补创建合同 */ findPaidOrdersWithoutContract(userId: bigint): Promise; + + /** + * 获取全局统计数据(从订单表实时聚合) + */ + getGlobalStats(): Promise; + + /** + * 获取今日统计数据 + */ + getTodayStats(): Promise; + + /** + * 按状态获取订单分布统计 + */ + getStatusDistribution(): Promise; +} + +/** 全局认种统计 */ +export interface GlobalPlantingStats { + totalTreeCount: number; + totalOrderCount: number; + totalAmount: string; +} + +/** 每日认种统计 */ +export interface DailyPlantingStats { + treeCount: number; + orderCount: number; + amount: string; +} + +/** 订单状态分布 */ +export interface StatusDistribution { + paid: number; + fundAllocated: number; + poolScheduled: number; + poolInjected: number; + miningEnabled: number; } export const PLANTING_ORDER_REPOSITORY = Symbol('IPlantingOrderRepository'); diff --git a/backend/services/planting-service/src/infrastructure/persistence/repositories/planting-order.repository.impl.ts b/backend/services/planting-service/src/infrastructure/persistence/repositories/planting-order.repository.impl.ts index 3bd7de19..8f6e844e 100644 --- a/backend/services/planting-service/src/infrastructure/persistence/repositories/planting-order.repository.impl.ts +++ b/backend/services/planting-service/src/infrastructure/persistence/repositories/planting-order.repository.impl.ts @@ -1,7 +1,12 @@ import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; -import { IPlantingOrderRepository } from '../../../domain/repositories/planting-order.repository.interface'; +import { + IPlantingOrderRepository, + GlobalPlantingStats, + DailyPlantingStats, + StatusDistribution, +} from '../../../domain/repositories/planting-order.repository.interface'; import { PlantingOrder } from '../../../domain/aggregates/planting-order.aggregate'; import { PlantingOrderStatus } from '../../../domain/value-objects/planting-order-status.enum'; import { PlantingOrderMapper } from '../mappers/planting-order.mapper'; @@ -224,4 +229,133 @@ export class PlantingOrderRepositoryImpl implements IPlantingOrderRepository { return orders.map(PlantingOrderMapper.toDomain); } + + /** + * 获取全局统计数据(从订单表实时聚合) + * 只统计已支付(PAID)及之后状态的订单 + */ + async getGlobalStats(): Promise { + const paidStatuses = [ + PlantingOrderStatus.PAID, + PlantingOrderStatus.FUND_ALLOCATED, + PlantingOrderStatus.POOL_SCHEDULED, + PlantingOrderStatus.POOL_INJECTED, + PlantingOrderStatus.MINING_ENABLED, + ]; + + const result = await this.prisma.plantingOrder.aggregate({ + where: { + status: { in: paidStatuses }, + }, + _sum: { + treeCount: true, + totalAmount: true, + }, + _count: { + id: true, + }, + }); + + return { + totalTreeCount: result._sum.treeCount || 0, + totalOrderCount: result._count.id || 0, + totalAmount: result._sum.totalAmount?.toString() || '0', + }; + } + + /** + * 获取今日统计数据 + */ + async getTodayStats(): Promise { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const tomorrow = new Date(today); + tomorrow.setDate(tomorrow.getDate() + 1); + + const paidStatuses = [ + PlantingOrderStatus.PAID, + PlantingOrderStatus.FUND_ALLOCATED, + PlantingOrderStatus.POOL_SCHEDULED, + PlantingOrderStatus.POOL_INJECTED, + PlantingOrderStatus.MINING_ENABLED, + ]; + + const result = await this.prisma.plantingOrder.aggregate({ + where: { + status: { in: paidStatuses }, + paidAt: { + gte: today, + lt: tomorrow, + }, + }, + _sum: { + treeCount: true, + totalAmount: true, + }, + _count: { + id: true, + }, + }); + + return { + treeCount: result._sum.treeCount || 0, + orderCount: result._count.id || 0, + amount: result._sum.totalAmount?.toString() || '0', + }; + } + + /** + * 按状态获取订单分布统计 + */ + async getStatusDistribution(): Promise { + const statusCounts = await this.prisma.plantingOrder.groupBy({ + by: ['status'], + _sum: { + treeCount: true, + }, + where: { + status: { + in: [ + PlantingOrderStatus.PAID, + PlantingOrderStatus.FUND_ALLOCATED, + PlantingOrderStatus.POOL_SCHEDULED, + PlantingOrderStatus.POOL_INJECTED, + PlantingOrderStatus.MINING_ENABLED, + ], + }, + }, + }); + + const distribution: StatusDistribution = { + paid: 0, + fundAllocated: 0, + poolScheduled: 0, + poolInjected: 0, + miningEnabled: 0, + }; + + for (const item of statusCounts) { + const count = item._sum.treeCount || 0; + switch (item.status) { + case PlantingOrderStatus.PAID: + distribution.paid = count; + break; + case PlantingOrderStatus.FUND_ALLOCATED: + distribution.fundAllocated = count; + break; + case PlantingOrderStatus.POOL_SCHEDULED: + distribution.poolScheduled = count; + break; + case PlantingOrderStatus.POOL_INJECTED: + distribution.poolInjected = count; + break; + case PlantingOrderStatus.MINING_ENABLED: + distribution.miningEnabled = count; + break; + } + } + + return distribution; + } }