feat(planting-service): add global stats API for data verification
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 <noreply@anthropic.com>
This commit is contained in:
parent
8148f7a52a
commit
46b68e8652
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -509,4 +509,53 @@ export class PlantingApplicationService {
|
|||
|
||||
return events;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局认种统计数据
|
||||
* 直接从订单表聚合查询,确保数据可靠性
|
||||
*/
|
||||
async getGlobalStats(): Promise<GlobalStatsResult> {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,44 @@ export interface IPlantingOrderRepository {
|
|||
* 用于用户完成 KYC 后补创建合同
|
||||
*/
|
||||
findPaidOrdersWithoutContract(userId: bigint): Promise<PlantingOrder[]>;
|
||||
|
||||
/**
|
||||
* 获取全局统计数据(从订单表实时聚合)
|
||||
*/
|
||||
getGlobalStats(): Promise<GlobalPlantingStats>;
|
||||
|
||||
/**
|
||||
* 获取今日统计数据
|
||||
*/
|
||||
getTodayStats(): Promise<DailyPlantingStats>;
|
||||
|
||||
/**
|
||||
* 按状态获取订单分布统计
|
||||
*/
|
||||
getStatusDistribution(): Promise<StatusDistribution>;
|
||||
}
|
||||
|
||||
/** 全局认种统计 */
|
||||
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');
|
||||
|
|
|
|||
|
|
@ -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<GlobalPlantingStats> {
|
||||
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<DailyPlantingStats> {
|
||||
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<StatusDistribution> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue