feat(statistics): 认种统计改为真实数据并显示积分

后端变更(planting-service):
- 添加 getMonthStats() 方法获取本月认种统计
- 更新 GlobalStatsResult 接口添加 monthStats 字段
- 添加 MonthStatsDto 响应类型

前端变更(admin-web):
- 更新 PlantingGlobalStats 类型定义
- statistics 页面调用真实 API 获取认种统计
- 显示认种总量、今日、本月的棵数和积分

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-06 09:36:46 -08:00
parent 898521d236
commit 2be9a2d9c2
7 changed files with 199 additions and 7 deletions

View File

@ -34,8 +34,24 @@ export class TodayStatsDto {
amount: string;
}
/**
* DTO
* [2026-01-06]
*/
export class MonthStatsDto {
@ApiProperty({ description: '本月认种棵数', example: 150 })
treeCount: number;
@ApiProperty({ description: '本月订单数', example: 50 })
orderCount: number;
@ApiProperty({ description: '本月认种金额', example: '15000.00' })
amount: string;
}
/**
* DTO
* [2026-01-06]
*/
export class GlobalStatsResponseDto {
@ApiProperty({ description: '总认种棵数PAID及之后状态', example: 760 })
@ -53,6 +69,9 @@ export class GlobalStatsResponseDto {
@ApiProperty({ description: '今日统计', type: TodayStatsDto })
todayStats: TodayStatsDto;
@ApiProperty({ description: '本月统计', type: MonthStatsDto })
monthStats: MonthStatsDto;
@ApiProperty({ description: '统计计算时间', example: '2026-01-04T12:00:00.000Z' })
calculatedAt: string;
}

View File

@ -513,13 +513,15 @@ export class PlantingApplicationService {
/**
*
*
* [2026-01-06]
*/
async getGlobalStats(): Promise<GlobalStatsResult> {
this.logger.log('Getting global planting stats from database');
const [globalStats, todayStats, statusDistribution] = await Promise.all([
const [globalStats, todayStats, monthStats, statusDistribution] = await Promise.all([
this.orderRepository.getGlobalStats(),
this.orderRepository.getTodayStats(),
this.orderRepository.getMonthStats(),
this.orderRepository.getStatusDistribution(),
]);
@ -529,6 +531,7 @@ export class PlantingApplicationService {
totalAmount: globalStats.totalAmount,
statusDistribution,
todayStats,
monthStats,
calculatedAt: new Date().toISOString(),
};
}
@ -556,6 +559,12 @@ export interface GlobalStatsResult {
orderCount: number;
amount: string;
};
/** 本月统计 [2026-01-06] 新增 */
monthStats: {
treeCount: number;
orderCount: number;
amount: string;
};
/** 统计时间 */
calculatedAt: string;
}

View File

@ -35,6 +35,12 @@ export interface IPlantingOrderRepository {
*/
getTodayStats(): Promise<DailyPlantingStats>;
/**
*
* [2026-01-06]
*/
getMonthStats(): Promise<DailyPlantingStats>;
/**
*
*/

View File

@ -305,6 +305,47 @@ export class PlantingOrderRepositoryImpl implements IPlantingOrderRepository {
};
}
/**
*
* [2026-01-06]
*/
async getMonthStats(): Promise<DailyPlantingStats> {
const now = new Date();
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 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: monthStart,
lt: monthEnd,
},
},
_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',
};
}
/**
*
*/

View File

@ -1,15 +1,18 @@
/**
*
* [2026-01-04] Tab
* [2026-01-06]
* SystemAccountsTab mainTab state
*/
'use client';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { PageContainer } from '@/components/layout';
import { cn } from '@/utils/helpers';
// [2026-01-04] 新增系统账户报表Tab
import { SystemAccountsTab } from '@/components/features/system-account-report';
import { dashboardService } from '@/services/dashboardService';
import type { PlantingGlobalStats } from '@/types';
import styles from './statistics.module.scss';
/**
@ -99,10 +102,27 @@ const metricsData = [
{ label: '每月累计认种\n提成', value: '¥8,900' },
];
/**
*
*/
function formatNumber(num: number): string {
return num.toLocaleString('zh-CN');
}
/**
*
*/
function formatAmount(amount: string): string {
const num = parseFloat(amount);
if (isNaN(num)) return '0';
return num.toLocaleString('zh-CN', { maximumFractionDigits: 2 });
}
/**
*
* UIPro Figma
* [2026-01-04] Tab
* [2026-01-06]
*/
export default function StatisticsPage() {
// [2026-01-04] 新增主Tab切换 - 数据统计 vs 系统账户
@ -114,6 +134,33 @@ export default function StatisticsPage() {
// 区域统计维度
const [regionType, setRegionType] = useState<'province' | 'city'>('province');
// [2026-01-06] 新增:认种统计数据状态
const [plantingStats, setPlantingStats] = useState<PlantingGlobalStats | null>(null);
const [statsLoading, setStatsLoading] = useState(true);
const [statsError, setStatsError] = useState<string | null>(null);
// [2026-01-06] 新增:获取认种统计数据
useEffect(() => {
async function fetchPlantingStats() {
try {
setStatsLoading(true);
setStatsError(null);
const response = await dashboardService.getPlantingStats();
if (response.code === 0 && response.data) {
setPlantingStats(response.data);
} else {
setStatsError(response.message || '获取统计数据失败');
}
} catch (error) {
console.error('获取认种统计失败:', error);
setStatsError('获取统计数据失败');
} finally {
setStatsLoading(false);
}
}
fetchPlantingStats();
}, []);
return (
<PageContainer title="数据统计">
<div className={styles.statistics}>
@ -142,19 +189,58 @@ export default function StatisticsPage() {
{/* 原有统计内容 - 仅在 statistics tab 显示 */}
{mainTab === 'statistics' && (
<>
{/* 统计概览卡片 */}
{/* 统计概览卡片 - [2026-01-06] 改为真实数据 */}
<section className={styles.statistics__overview}>
<div className={styles.statistics__overviewCard}>
<div className={styles.statistics__overviewLabel}></div>
<h1 className={styles.statistics__overviewValue}>12,345</h1>
{statsLoading ? (
<h1 className={styles.statistics__overviewValue}>...</h1>
) : statsError ? (
<h1 className={styles.statistics__overviewValue}>--</h1>
) : (
<>
<h1 className={styles.statistics__overviewValue}>
{formatNumber(plantingStats?.totalTreeCount ?? 0)}
</h1>
<div className={styles.statistics__overviewSubValue}>
: {formatAmount(plantingStats?.totalAmount ?? '0')}
</div>
</>
)}
</div>
<div className={styles.statistics__overviewCard}>
<div className={styles.statistics__overviewLabel}></div>
<h1 className={styles.statistics__overviewValue}>123</h1>
{statsLoading ? (
<h1 className={styles.statistics__overviewValue}>...</h1>
) : statsError ? (
<h1 className={styles.statistics__overviewValue}>--</h1>
) : (
<>
<h1 className={styles.statistics__overviewValue}>
{formatNumber(plantingStats?.todayStats?.treeCount ?? 0)}
</h1>
<div className={styles.statistics__overviewSubValue}>
: {formatAmount(plantingStats?.todayStats?.amount ?? '0')}
</div>
</>
)}
</div>
<div className={styles.statistics__overviewCard}>
<div className={styles.statistics__overviewLabel}></div>
<h1 className={styles.statistics__overviewValue}>1,456</h1>
{statsLoading ? (
<h1 className={styles.statistics__overviewValue}>...</h1>
) : statsError ? (
<h1 className={styles.statistics__overviewValue}>--</h1>
) : (
<>
<h1 className={styles.statistics__overviewValue}>
{formatNumber(plantingStats?.monthStats?.treeCount ?? 0)}
</h1>
<div className={styles.statistics__overviewSubValue}>
: {formatAmount(plantingStats?.monthStats?.amount ?? '0')}
</div>
</>
)}
</div>
</section>

View File

@ -98,6 +98,15 @@
color: #0f172a;
}
/* [2026-01-06] 新增:积分副标题样式 */
.statistics__overviewSubValue {
align-self: stretch;
font-size: 14px;
line-height: 20px;
color: #9ca3af;
margin-top: 4px;
}
/* 通用卡片样式 */
.statistics__card {
align-self: stretch;

View File

@ -88,9 +88,31 @@ export interface DashboardActivitiesParams {
limit?: number;
}
/** 认种全局统计数据(来自 planting-service */
/** 认种周期统计数据 */
export interface PlantingPeriodStats {
treeCount: number;
orderCount: number;
amount: string;
}
/** 状态分布统计 */
export interface PlantingStatusDistribution {
paid: number;
fundAllocated: number;
poolScheduled: number;
poolInjected: number;
miningEnabled: number;
}
/** planting-service
* [2026-01-06]
*/
export interface PlantingGlobalStats {
totalTreeCount: number;
totalOrderCount: number;
totalAmount: string;
statusDistribution: PlantingStatusDistribution;
todayStats: PlantingPeriodStats;
monthStats: PlantingPeriodStats;
calculatedAt: string;
}