1191 lines
28 KiB
Markdown
1191 lines
28 KiB
Markdown
# 首页统计数据接口 - 前端对接文档
|
||
|
||
## 📋 目录
|
||
|
||
- [接口概述](#接口概述)
|
||
- [快速开始](#快速开始)
|
||
- [接口详情](#接口详情)
|
||
- [请求参数](#请求参数)
|
||
- [响应数据](#响应数据)
|
||
- [请求示例](#请求示例)
|
||
- [错误处理](#错误处理)
|
||
- [权限说明](#权限说明)
|
||
- [注意事项](#注意事项)
|
||
|
||
---
|
||
|
||
## 接口概述
|
||
|
||
### 基本信息
|
||
|
||
| 项目 | 内容 |
|
||
|------|------|
|
||
| **接口名称** | 获取首页统计数据 |
|
||
| **接口路径** | `/admin/statistics/home-data` |
|
||
| **请求方法** | `GET` |
|
||
| **需要认证** | ✅ 是(需要JWT Token) |
|
||
| **权限要求** | 所有登录用户可访问 |
|
||
| **版本** | v3 |
|
||
|
||
### 功能说明
|
||
|
||
获取首页展示的统计数据,包括:
|
||
- ✅ 今日待审核文件数
|
||
- ✅ 当前周期已审核文件数
|
||
- ✅ 审核文件同比增长
|
||
- ✅ 当前周期审核通过率
|
||
- ✅ 通过率同比增长
|
||
- ✅ 检测到的问题总数
|
||
- ✅ 问题数量同比增长
|
||
|
||
### 核心特性
|
||
|
||
1. **三级权限隔离**
|
||
- 省级管理员:查看全省数据
|
||
- 地市管理员:查看本地市数据
|
||
- 普通用户:只能查看自己的数据
|
||
|
||
2. **灵活时间范围**
|
||
- 支持今天、近3天、近7天、近30天、本月
|
||
- 自动对比上一个相同周期
|
||
|
||
3. **类型筛选**
|
||
- 支持按文档类型ID筛选
|
||
- 支持多个类型ID组合
|
||
|
||
4. **性能优化**
|
||
- Redis缓存20分钟
|
||
- 响应时间 < 200ms
|
||
|
||
---
|
||
|
||
## 快速开始
|
||
|
||
### 最简单的调用
|
||
|
||
```javascript
|
||
// 获取本月统计数据(默认)
|
||
fetch('/admin/statistics/home-data', {
|
||
headers: {
|
||
'Authorization': 'Bearer YOUR_JWT_TOKEN'
|
||
}
|
||
})
|
||
.then(res => res.json())
|
||
.then(data => console.log(data));
|
||
```
|
||
|
||
### Vue 3 示例
|
||
|
||
```vue
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue'
|
||
import axios from 'axios'
|
||
|
||
const stats = ref({})
|
||
const loading = ref(false)
|
||
|
||
const fetchHomeStats = async (timeRange = null, typeIds = null) => {
|
||
loading.value = true
|
||
try {
|
||
const params = {}
|
||
if (timeRange) params.time_range = timeRange
|
||
if (typeIds) params.type_ids = typeIds
|
||
|
||
const { data } = await axios.get('/admin/statistics/home-data', { params })
|
||
stats.value = data
|
||
} catch (error) {
|
||
console.error('获取统计数据失败:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchHomeStats()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="!loading">
|
||
<h2>今日待审核: {{ stats.today_pending_files }}</h2>
|
||
<h2>已审核: {{ stats.monthly_reviewed_files }}</h2>
|
||
<h2>通过率: {{ stats.monthly_pass_rate }}%</h2>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
### React 示例
|
||
|
||
```jsx
|
||
import { useState, useEffect } from 'react'
|
||
import axios from 'axios'
|
||
|
||
function HomeStatistics() {
|
||
const [stats, setStats] = useState({})
|
||
const [loading, setLoading] = useState(false)
|
||
|
||
const fetchHomeStats = async (timeRange = null, typeIds = null) => {
|
||
setLoading(true)
|
||
try {
|
||
const params = {}
|
||
if (timeRange) params.time_range = timeRange
|
||
if (typeIds) params.type_ids = typeIds
|
||
|
||
const { data } = await axios.get('/admin/statistics/home-data', { params })
|
||
setStats(data)
|
||
} catch (error) {
|
||
console.error('获取统计数据失败:', error)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
fetchHomeStats()
|
||
}, [])
|
||
|
||
return (
|
||
<div>
|
||
{!loading && (
|
||
<>
|
||
<h2>今日待审核: {stats.today_pending_files}</h2>
|
||
<h2>已审核: {stats.monthly_reviewed_files}</h2>
|
||
<h2>通过率: {stats.monthly_pass_rate}%</h2>
|
||
</>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 接口详情
|
||
|
||
### 完整URL
|
||
|
||
```
|
||
GET {BASE_URL}/admin/statistics/home-data
|
||
```
|
||
|
||
**示例**:
|
||
```
|
||
https://api.example.com/admin/statistics/home-data
|
||
http://localhost:8073/admin/statistics/home-data
|
||
```
|
||
|
||
### 请求头
|
||
|
||
| Header | 必填 | 类型 | 说明 | 示例 |
|
||
|--------|------|------|------|------|
|
||
| `Authorization` | ✅ 是 | String | JWT Token | `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...` |
|
||
| `Content-Type` | ❌ 否 | String | 内容类型 | `application/json` |
|
||
|
||
---
|
||
|
||
## 请求参数
|
||
|
||
### Query 参数
|
||
|
||
| 参数名 | 必填 | 类型 | 默认值 | 说明 |
|
||
|--------|------|------|--------|------|
|
||
| `type_ids` | ❌ 否 | String | `null` | 文档类型ID,多个用逗号分隔 |
|
||
| `time_range` | ❌ 否 | String | `null` | 时间范围,见下表 |
|
||
|
||
### time_range 参数详解
|
||
|
||
| 值 | 说明 | 当前周期 | 对比周期 | 使用场景 |
|
||
|----|------|----------|----------|----------|
|
||
| 不传参数 | 本月数据(默认) | 本月1日 ~ 当前时间 | 上月1日 ~ 上月最后一天 | 默认首页统计 |
|
||
| `today` | 今天 | 今天00:00 ~ 当前时间 | 昨天00:00 ~ 昨天23:59 | 查看今日数据 |
|
||
| `3days` | 近3天 | 当前时间向前72小时 | 再往前72小时 | 短期趋势 |
|
||
| `7days` | 近7天 | 当前时间向前7天 | 再往前7天 | 周统计 |
|
||
| `30days` | 近30天 | 当前时间向前30天 | 再往前30天 | 月统计 |
|
||
|
||
### type_ids 参数详解
|
||
|
||
**格式**: 逗号分隔的整数
|
||
|
||
**示例**:
|
||
- 单个类型: `type_ids=1`
|
||
- 多个类型: `type_ids=1,2,3`
|
||
- 所有类型: 不传参数或传 `null`
|
||
|
||
**有效值** (根据你的业务配置):
|
||
```javascript
|
||
1 // 类型1
|
||
2 // 类型2
|
||
3 // 类型3
|
||
// ... 更多类型ID
|
||
```
|
||
|
||
---
|
||
|
||
## 响应数据
|
||
|
||
### 响应结构
|
||
|
||
```typescript
|
||
interface HomeStatisticsResponse {
|
||
today_pending_files: number; // 今日待审核文件数
|
||
monthly_reviewed_files: number; // 当前周期已审核文件数
|
||
monthly_review_growth: GrowthData; // 审核数同比增长
|
||
monthly_pass_rate: number; // 当前周期通过率(百分比)
|
||
pass_rate_growth: GrowthData; // 通过率同比增长
|
||
issues_detected: number; // 检测到的问题总数
|
||
issues_growth: GrowthData; // 问题数同比增长
|
||
}
|
||
|
||
interface GrowthData {
|
||
value: number; // 增长百分比(绝对值)
|
||
is_up: boolean; // true=增长, false=下降
|
||
}
|
||
```
|
||
|
||
### 完整响应示例
|
||
|
||
```json
|
||
{
|
||
"today_pending_files": 15,
|
||
"monthly_reviewed_files": 120,
|
||
"monthly_review_growth": {
|
||
"value": 25.5,
|
||
"is_up": true
|
||
},
|
||
"monthly_pass_rate": 85.3,
|
||
"pass_rate_growth": {
|
||
"value": 3.2,
|
||
"is_up": true
|
||
},
|
||
"issues_detected": 42,
|
||
"issues_growth": {
|
||
"value": 12.8,
|
||
"is_up": false
|
||
}
|
||
}
|
||
```
|
||
|
||
### 字段说明
|
||
|
||
#### 1. today_pending_files(今日待审核文件数)
|
||
|
||
- **类型**: `number`
|
||
- **说明**: 今天新增的待审核文件数(audit_status = 0 或 null)
|
||
- **范围**: >= 0
|
||
- **示例**: `15`
|
||
|
||
#### 2. monthly_reviewed_files(当前周期已审核文件数)
|
||
|
||
- **类型**: `number`
|
||
- **说明**: 当前时间范围内已审核的文件数
|
||
- **包括状态**:
|
||
- ✅ `audit_status = 1` (通过)
|
||
- 🔄 `audit_status = 2` (审核中)
|
||
- ❌ `audit_status = -1` (拒绝)
|
||
- ⚠️ `audit_status = -2` (警告)
|
||
- **范围**: >= 0
|
||
- **示例**: `120`
|
||
|
||
#### 3. monthly_review_growth(审核数同比增长)
|
||
|
||
- **类型**: `GrowthData`
|
||
- **说明**: 当前周期 vs 上个周期的审核数量变化
|
||
- **计算公式**: `|(当前周期 - 上个周期) / 上个周期| * 100`
|
||
- **示例**:
|
||
```json
|
||
{
|
||
"value": 25.5, // 增长了25.5%
|
||
"is_up": true // 增长趋势
|
||
}
|
||
```
|
||
|
||
#### 4. monthly_pass_rate(当前周期通过率)
|
||
|
||
- **类型**: `number`
|
||
- **说明**: 当前周期审核通过的文件占比
|
||
- **计算公式**: `(通过数量 / 已审核总数) * 100`
|
||
- **范围**: 0.0 ~ 100.0
|
||
- **保留**: 小数点后1位
|
||
- **示例**: `85.3` (表示85.3%)
|
||
|
||
#### 5. pass_rate_growth(通过率同比增长)
|
||
|
||
- **类型**: `GrowthData`
|
||
- **说明**: 当前周期 vs 上个周期的通过率变化
|
||
- **计算公式**: `|(当前通过率 - 上期通过率) / 上期通过率| * 100`
|
||
- **示例**:
|
||
```json
|
||
{
|
||
"value": 3.2, // 通过率提升了3.2%
|
||
"is_up": true
|
||
}
|
||
```
|
||
|
||
#### 6. issues_detected(检测到的问题总数)
|
||
|
||
- **类型**: `number`
|
||
- **说明**: 当前周期内所有评查点发现的问题总数
|
||
- **来源**: `evaluation_results` 表中 `result='false'` 的记录
|
||
- **范围**: >= 0
|
||
- **示例**: `42`
|
||
|
||
#### 7. issues_growth(问题数同比增长)
|
||
|
||
- **类型**: `GrowthData`
|
||
- **说明**: 当前周期 vs 上个周期的问题数变化
|
||
- **示例**:
|
||
```json
|
||
{
|
||
"value": 12.8, // 问题数减少了12.8%
|
||
"is_up": false // 下降趋势(好事)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 请求示例
|
||
|
||
### 1. 获取本月统计数据(默认)
|
||
|
||
**请求**:
|
||
```bash
|
||
curl -X GET "http://localhost:8073/admin/statistics/home-data" \
|
||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||
```
|
||
|
||
**JavaScript**:
|
||
```javascript
|
||
const response = await fetch('/admin/statistics/home-data', {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
})
|
||
const data = await response.json()
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"today_pending_files": 10,
|
||
"monthly_reviewed_files": 150,
|
||
"monthly_review_growth": {
|
||
"value": 15.5,
|
||
"is_up": true
|
||
},
|
||
"monthly_pass_rate": 82.0,
|
||
"pass_rate_growth": {
|
||
"value": 2.5,
|
||
"is_up": true
|
||
},
|
||
"issues_detected": 35,
|
||
"issues_growth": {
|
||
"value": 8.0,
|
||
"is_up": false
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 获取今天的统计数据
|
||
|
||
**请求**:
|
||
```bash
|
||
curl -X GET "http://localhost:8073/admin/statistics/home-data?time_range=today" \
|
||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||
```
|
||
|
||
**JavaScript**:
|
||
```javascript
|
||
const response = await fetch('/admin/statistics/home-data?time_range=today', {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
})
|
||
const data = await response.json()
|
||
```
|
||
|
||
**Axios**:
|
||
```javascript
|
||
const { data } = await axios.get('/admin/statistics/home-data', {
|
||
params: { time_range: 'today' }
|
||
})
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"today_pending_files": 5,
|
||
"monthly_reviewed_files": 12,
|
||
"monthly_review_growth": {
|
||
"value": 20.0,
|
||
"is_up": true
|
||
},
|
||
"monthly_pass_rate": 75.0,
|
||
"pass_rate_growth": {
|
||
"value": 5.0,
|
||
"is_up": false
|
||
},
|
||
"issues_detected": 8,
|
||
"issues_growth": {
|
||
"value": 10.0,
|
||
"is_up": false
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. 获取近7天统计数据
|
||
|
||
**请求**:
|
||
```bash
|
||
curl -X GET "http://localhost:8073/admin/statistics/home-data?time_range=7days" \
|
||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||
```
|
||
|
||
**JavaScript**:
|
||
```javascript
|
||
const response = await fetch('/admin/statistics/home-data?time_range=7days', {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
})
|
||
const data = await response.json()
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"today_pending_files": 8,
|
||
"monthly_reviewed_files": 85,
|
||
"monthly_review_growth": {
|
||
"value": 30.5,
|
||
"is_up": true
|
||
},
|
||
"monthly_pass_rate": 88.2,
|
||
"pass_rate_growth": {
|
||
"value": 4.0,
|
||
"is_up": true
|
||
},
|
||
"issues_detected": 25,
|
||
"issues_growth": {
|
||
"value": 15.0,
|
||
"is_up": false
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4. 按文档类型筛选(单个类型)
|
||
|
||
**请求**:
|
||
```bash
|
||
curl -X GET "http://localhost:8073/admin/statistics/home-data?type_ids=1" \
|
||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||
```
|
||
|
||
**JavaScript**:
|
||
```javascript
|
||
const response = await fetch('/admin/statistics/home-data?type_ids=1', {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
})
|
||
const data = await response.json()
|
||
```
|
||
|
||
**Axios**:
|
||
```javascript
|
||
const { data } = await axios.get('/admin/statistics/home-data', {
|
||
params: { type_ids: '1' }
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
### 5. 按文档类型筛选(多个类型)
|
||
|
||
**请求**:
|
||
```bash
|
||
curl -X GET "http://localhost:8073/admin/statistics/home-data?type_ids=1,2,3" \
|
||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||
```
|
||
|
||
**JavaScript**:
|
||
```javascript
|
||
const typeIds = [1, 2, 3]
|
||
const response = await fetch(`/admin/statistics/home-data?type_ids=${typeIds.join(',')}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
})
|
||
const data = await response.json()
|
||
```
|
||
|
||
**Axios**:
|
||
```javascript
|
||
const { data } = await axios.get('/admin/statistics/home-data', {
|
||
params: { type_ids: '1,2,3' }
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
### 6. 组合筛选:时间范围 + 文档类型
|
||
|
||
**请求**:
|
||
```bash
|
||
curl -X GET "http://localhost:8073/admin/statistics/home-data?time_range=7days&type_ids=1,2" \
|
||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||
```
|
||
|
||
**JavaScript**:
|
||
```javascript
|
||
const params = new URLSearchParams({
|
||
time_range: '7days',
|
||
type_ids: '1,2'
|
||
})
|
||
|
||
const response = await fetch(`/admin/statistics/home-data?${params}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
})
|
||
const data = await response.json()
|
||
```
|
||
|
||
**Axios**:
|
||
```javascript
|
||
const { data } = await axios.get('/admin/statistics/home-data', {
|
||
params: {
|
||
time_range: '7days',
|
||
type_ids: '1,2'
|
||
}
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
### 7. 完整的Vue组件示例(带下拉选择器)
|
||
|
||
```vue
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue'
|
||
import axios from 'axios'
|
||
|
||
const stats = ref({})
|
||
const loading = ref(false)
|
||
const timeRange = ref(null)
|
||
const typeIds = ref(null)
|
||
|
||
// 时间范围选项
|
||
const timeRangeOptions = [
|
||
{ label: '本月', value: null },
|
||
{ label: '今天', value: 'today' },
|
||
{ label: '近3天', value: '3days' },
|
||
{ label: '近7天', value: '7days' },
|
||
{ label: '近30天', value: '30days' }
|
||
]
|
||
|
||
// 文档类型选项(根据实际业务调整)
|
||
const typeOptions = [
|
||
{ label: '全部类型', value: null },
|
||
{ label: '类型1', value: '1' },
|
||
{ label: '类型2', value: '2' },
|
||
{ label: '类型1+2', value: '1,2' }
|
||
]
|
||
|
||
const fetchStats = async () => {
|
||
loading.value = true
|
||
try {
|
||
const params = {}
|
||
if (timeRange.value) params.time_range = timeRange.value
|
||
if (typeIds.value) params.type_ids = typeIds.value
|
||
|
||
const { data } = await axios.get('/admin/statistics/home-data', { params })
|
||
stats.value = data
|
||
} catch (error) {
|
||
console.error('获取统计数据失败:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchStats()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="home-statistics">
|
||
<!-- 筛选器 -->
|
||
<div class="filters">
|
||
<select v-model="timeRange" @change="fetchStats">
|
||
<option v-for="opt in timeRangeOptions" :key="opt.value" :value="opt.value">
|
||
{{ opt.label }}
|
||
</option>
|
||
</select>
|
||
|
||
<select v-model="typeIds" @change="fetchStats">
|
||
<option v-for="opt in typeOptions" :key="opt.value" :value="opt.value">
|
||
{{ opt.label }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 统计数据展示 -->
|
||
<div v-if="!loading" class="stats-cards">
|
||
<div class="stat-card">
|
||
<h3>今日待审核</h3>
|
||
<p class="number">{{ stats.today_pending_files }}</p>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<h3>已审核文件</h3>
|
||
<p class="number">{{ stats.monthly_reviewed_files }}</p>
|
||
<p class="growth" :class="{ up: stats.monthly_review_growth?.is_up }">
|
||
{{ stats.monthly_review_growth?.is_up ? '↑' : '↓' }}
|
||
{{ stats.monthly_review_growth?.value }}%
|
||
</p>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<h3>审核通过率</h3>
|
||
<p class="number">{{ stats.monthly_pass_rate }}%</p>
|
||
<p class="growth" :class="{ up: stats.pass_rate_growth?.is_up }">
|
||
{{ stats.pass_rate_growth?.is_up ? '↑' : '↓' }}
|
||
{{ stats.pass_rate_growth?.value }}%
|
||
</p>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<h3>检测到的问题</h3>
|
||
<p class="number">{{ stats.issues_detected }}</p>
|
||
<p class="growth" :class="{ down: !stats.issues_growth?.is_up }">
|
||
{{ stats.issues_growth?.is_up ? '↑' : '↓' }}
|
||
{{ stats.issues_growth?.value }}%
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="loading">加载中...</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.filters {
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.stats-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.stat-card {
|
||
padding: 20px;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.number {
|
||
font-size: 32px;
|
||
font-weight: bold;
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.growth {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.growth.up {
|
||
color: #52c41a;
|
||
}
|
||
|
||
.growth.down {
|
||
color: #f5222d;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
### 8. 完整的React组件示例(带Ant Design)
|
||
|
||
```jsx
|
||
import { useState, useEffect } from 'react'
|
||
import { Card, Select, Spin, Statistic, Row, Col } from 'antd'
|
||
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
|
||
import axios from 'axios'
|
||
|
||
const { Option } = Select
|
||
|
||
function HomeStatistics() {
|
||
const [stats, setStats] = useState({})
|
||
const [loading, setLoading] = useState(false)
|
||
const [timeRange, setTimeRange] = useState(null)
|
||
const [typeIds, setTypeIds] = useState(null)
|
||
|
||
// 时间范围选项
|
||
const timeRangeOptions = [
|
||
{ label: '本月', value: null },
|
||
{ label: '今天', value: 'today' },
|
||
{ label: '近3天', value: '3days' },
|
||
{ label: '近7天', value: '7days' },
|
||
{ label: '近30天', value: '30days' }
|
||
]
|
||
|
||
const fetchStats = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const params = {}
|
||
if (timeRange) params.time_range = timeRange
|
||
if (typeIds) params.type_ids = typeIds
|
||
|
||
const { data } = await axios.get('/admin/statistics/home-data', { params })
|
||
setStats(data)
|
||
} catch (error) {
|
||
console.error('获取统计数据失败:', error)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
fetchStats()
|
||
}, [timeRange, typeIds])
|
||
|
||
return (
|
||
<div style={{ padding: '24px' }}>
|
||
{/* 筛选器 */}
|
||
<div style={{ marginBottom: '24px', display: 'flex', gap: '16px' }}>
|
||
<Select
|
||
placeholder="选择时间范围"
|
||
value={timeRange}
|
||
onChange={setTimeRange}
|
||
style={{ width: 200 }}
|
||
>
|
||
{timeRangeOptions.map(opt => (
|
||
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
||
))}
|
||
</Select>
|
||
|
||
<Select
|
||
placeholder="选择文档类型"
|
||
value={typeIds}
|
||
onChange={setTypeIds}
|
||
style={{ width: 200 }}
|
||
>
|
||
<Option value={null}>全部类型</Option>
|
||
<Option value="1">类型1</Option>
|
||
<Option value="2">类型2</Option>
|
||
<Option value="1,2">类型1+2</Option>
|
||
</Select>
|
||
</div>
|
||
|
||
{/* 统计卡片 */}
|
||
<Spin spinning={loading}>
|
||
<Row gutter={16}>
|
||
<Col span={6}>
|
||
<Card>
|
||
<Statistic
|
||
title="今日待审核"
|
||
value={stats.today_pending_files}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
|
||
<Col span={6}>
|
||
<Card>
|
||
<Statistic
|
||
title="已审核文件"
|
||
value={stats.monthly_reviewed_files}
|
||
suffix={
|
||
stats.monthly_review_growth && (
|
||
<span style={{
|
||
color: stats.monthly_review_growth.is_up ? '#52c41a' : '#f5222d',
|
||
fontSize: '14px'
|
||
}}>
|
||
{stats.monthly_review_growth.is_up ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
|
||
{stats.monthly_review_growth.value}%
|
||
</span>
|
||
)
|
||
}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
|
||
<Col span={6}>
|
||
<Card>
|
||
<Statistic
|
||
title="审核通过率"
|
||
value={stats.monthly_pass_rate}
|
||
precision={1}
|
||
suffix="%"
|
||
valueStyle={{ color: '#3f8600' }}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
|
||
<Col span={6}>
|
||
<Card>
|
||
<Statistic
|
||
title="检测到的问题"
|
||
value={stats.issues_detected}
|
||
valueStyle={{ color: '#cf1322' }}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
</Spin>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default HomeStatistics
|
||
```
|
||
|
||
---
|
||
|
||
## 错误处理
|
||
|
||
### 常见错误码
|
||
|
||
| HTTP状态码 | 错误场景 | 响应示例 | 处理建议 |
|
||
|-----------|----------|----------|----------|
|
||
| `200` | ✅ 成功 | 正常数据 | - |
|
||
| `400` | 参数错误 | `{"detail": "time_range 参数值无效..."}` | 检查参数格式 |
|
||
| `401` | 未认证 | `{"detail": "未提供认证凭据"}` | 检查Token是否传递 |
|
||
| `403` | 权限不足 | `{"detail": "此接口仅限管理员访问"}` | 检查用户权限 |
|
||
| `500` | 服务器错误 | `{"detail": "获取首页统计数据失败..."}` | 联系后端排查 |
|
||
|
||
### 错误响应格式
|
||
|
||
```json
|
||
{
|
||
"detail": "错误描述信息"
|
||
}
|
||
```
|
||
|
||
### 错误示例
|
||
|
||
#### 1. time_range 参数无效
|
||
|
||
**请求**:
|
||
```bash
|
||
curl "http://localhost:8073/admin/statistics/home-data?time_range=invalid"
|
||
```
|
||
|
||
**响应**: HTTP 400
|
||
```json
|
||
{
|
||
"detail": "time_range 参数值无效,应为: today, 3days, 7days, 30days,实际为: invalid"
|
||
}
|
||
```
|
||
|
||
#### 2. type_ids 参数格式错误
|
||
|
||
**请求**:
|
||
```bash
|
||
curl "http://localhost:8073/admin/statistics/home-data?type_ids=abc"
|
||
```
|
||
|
||
**响应**: HTTP 400
|
||
```json
|
||
{
|
||
"detail": "type_ids 参数格式错误,应为逗号分隔的整数,实际为: abc"
|
||
}
|
||
```
|
||
|
||
#### 3. Token过期或无效
|
||
|
||
**请求**:
|
||
```bash
|
||
curl "http://localhost:8073/admin/statistics/home-data" \
|
||
-H "Authorization: Bearer INVALID_TOKEN"
|
||
```
|
||
|
||
**响应**: HTTP 401
|
||
```json
|
||
{
|
||
"detail": "认证凭据无效或已过期"
|
||
}
|
||
```
|
||
|
||
### 前端错误处理示例
|
||
|
||
```javascript
|
||
const fetchStats = async () => {
|
||
try {
|
||
const { data } = await axios.get('/admin/statistics/home-data', {
|
||
params: { time_range: 'today' }
|
||
})
|
||
return data
|
||
} catch (error) {
|
||
// 处理不同的错误情况
|
||
if (error.response) {
|
||
switch (error.response.status) {
|
||
case 400:
|
||
console.error('参数错误:', error.response.data.detail)
|
||
// 提示用户检查筛选条件
|
||
break
|
||
case 401:
|
||
console.error('未认证,请重新登录')
|
||
// 跳转到登录页
|
||
window.location.href = '/login'
|
||
break
|
||
case 403:
|
||
console.error('权限不足')
|
||
// 提示用户权限不足
|
||
break
|
||
case 500:
|
||
console.error('服务器错误:', error.response.data.detail)
|
||
// 显示错误提示
|
||
break
|
||
default:
|
||
console.error('未知错误:', error)
|
||
}
|
||
} else if (error.request) {
|
||
console.error('网络错误,请检查网络连接')
|
||
} else {
|
||
console.error('请求配置错误:', error.message)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 权限说明
|
||
|
||
### 三级权限体系
|
||
|
||
| 用户角色 | 权限范围 | 能看到的数据 | 示例 |
|
||
|---------|---------|------------|------|
|
||
| **省级管理员** | 全省 | 所有地市的数据汇总 | 可以看到广州+深圳+梅州...所有地市 |
|
||
| **地市管理员** | 本地市 | 所在地市的所有用户数据 | 梅州管理员只能看梅州的数据 |
|
||
| **普通用户** | 个人 | 仅自己创建的文档数据 | 张三只能看张三的数据 |
|
||
|
||
### 权限判断逻辑
|
||
|
||
系统通过以下步骤判断用户权限:
|
||
|
||
1. **查询用户角色**
|
||
```sql
|
||
SELECT role_key FROM user_role
|
||
JOIN roles ON user_role.role_id = roles.id
|
||
WHERE user_role.user_id = {current_user_id}
|
||
```
|
||
|
||
2. **权限分配**
|
||
- `role_key = 'province_admin'` → 省级管理员
|
||
- `user_role = 'admin'` 或 `is_leader = true` → 地市管理员
|
||
- 其他 → 普通用户
|
||
|
||
3. **数据过滤**
|
||
- 省级管理员:不过滤地市(`area = null`)
|
||
- 地市管理员:过滤地市(`area = '梅州'`)
|
||
- 普通用户:过滤用户ID(`user_id = 123`)
|
||
|
||
### 权限验证示例
|
||
|
||
**省级管理员查询**:
|
||
```javascript
|
||
// Token中包含: { user_role: 'provincial_admin', area: '省级' }
|
||
const { data } = await axios.get('/admin/statistics/home-data')
|
||
// 返回全省所有数据
|
||
```
|
||
|
||
**地市管理员查询**:
|
||
```javascript
|
||
// Token中包含: { user_role: 'admin', area: '梅州' }
|
||
const { data } = await axios.get('/admin/statistics/home-data')
|
||
// 只返回梅州的数据
|
||
```
|
||
|
||
**普通用户查询**:
|
||
```javascript
|
||
// Token中包含: { user_id: 123, user_role: 'common', area: '梅州' }
|
||
const { data } = await axios.get('/admin/statistics/home-data')
|
||
// 只返回用户123的数据
|
||
```
|
||
|
||
---
|
||
|
||
## 注意事项
|
||
|
||
### ⚠️ 重要提示
|
||
|
||
1. **Token必须有效**
|
||
- Token必须包含在Authorization头中
|
||
- Token格式: `Bearer {token}`
|
||
- Token过期需要重新登录
|
||
|
||
2. **参数大小写敏感**
|
||
- `time_range=today` ✅
|
||
- `time_range=Today` ❌
|
||
- `time_range=TODAY` ❌
|
||
|
||
3. **type_ids必须是整数**
|
||
- `type_ids=1,2,3` ✅
|
||
- `type_ids=1, 2, 3` ✅ (自动trim空格)
|
||
- `type_ids=a,b,c` ❌
|
||
|
||
4. **缓存机制**
|
||
- 数据缓存20分钟
|
||
- 相同参数20分钟内返回缓存数据
|
||
- 不同参数会触发新的查询
|
||
|
||
5. **时间范围对比**
|
||
- `today` vs 昨天
|
||
- `3days` vs 前3天
|
||
- `7days` vs 前7天
|
||
- `30days` vs 前30天
|
||
- 默认(本月)vs 上月
|
||
|
||
### 📊 数据统计说明
|
||
|
||
1. **"已审核"的定义**
|
||
- 包括以下状态的文档:
|
||
- ✅ `audit_status = 1` (通过)
|
||
- 🔄 `audit_status = 2` (审核中)
|
||
- ❌ `audit_status = -1` (拒绝)
|
||
- ⚠️ `audit_status = -2` (警告)
|
||
- **不包括**:
|
||
- `audit_status = 0` (待审核)
|
||
- `audit_status = null` (未知)
|
||
|
||
2. **"今日待审核"的定义**
|
||
- 仅统计今天00:00之后创建的文档
|
||
- `audit_status = 0` 或 `audit_status = null`
|
||
|
||
3. **通过率计算**
|
||
- 公式: `(通过数量 / 已审核总数) * 100`
|
||
- 只有`audit_status = 1`算通过
|
||
- 如果已审核总数为0,通过率为0.0%
|
||
|
||
4. **增长率计算**
|
||
- 使用绝对值(不带正负号)
|
||
- 通过`is_up`判断增长/下降
|
||
- 如果上期数据为0,增长率固定为100%
|
||
|
||
### 🔄 刷新策略建议
|
||
|
||
```javascript
|
||
// 推荐的刷新策略
|
||
// 1. 首次加载
|
||
onMounted(() => fetchStats())
|
||
|
||
// 2. 定时刷新(5分钟)
|
||
setInterval(() => fetchStats(), 5 * 60 * 1000)
|
||
|
||
// 3. 用户切换筛选条件时立即刷新
|
||
watch([timeRange, typeIds], () => fetchStats())
|
||
|
||
// 4. 用户手动刷新
|
||
const handleRefresh = () => {
|
||
// 清除缓存的方式:随机添加时间戳(不推荐,会绕过缓存)
|
||
// const timestamp = Date.now()
|
||
// fetchStats({ _t: timestamp })
|
||
|
||
// 推荐:直接调用,利用缓存
|
||
fetchStats()
|
||
}
|
||
```
|
||
|
||
### 🎨 UI展示建议
|
||
|
||
1. **增长指标展示**
|
||
```javascript
|
||
// 使用颜色区分增长/下降
|
||
const getGrowthColor = (growth) => {
|
||
return growth.is_up ? '#52c41a' : '#f5222d'
|
||
}
|
||
|
||
// 使用图标
|
||
const getGrowthIcon = (growth) => {
|
||
return growth.is_up ? '↑' : '↓'
|
||
}
|
||
```
|
||
|
||
2. **数值格式化**
|
||
```javascript
|
||
// 大数值使用千分位
|
||
const formatNumber = (num) => {
|
||
return num.toLocaleString('zh-CN')
|
||
}
|
||
|
||
// 示例: 1234 → 1,234
|
||
```
|
||
|
||
3. **空数据处理**
|
||
```javascript
|
||
// 当数据为0时的友好提示
|
||
const displayValue = (value) => {
|
||
return value > 0 ? value : '暂无数据'
|
||
}
|
||
```
|
||
|
||
### 🐛 常见问题 FAQ
|
||
|
||
**Q1: 为什么返回的数据都是0?**
|
||
|
||
A: 可能的原因:
|
||
- 时间范围内没有文档(如选择"今天"但今天没创建文档)
|
||
- 文档类型筛选过滤掉了所有数据
|
||
- 用户权限限制(普通用户只能看自己的数据)
|
||
- 数据库中确实没有符合条件的数据
|
||
|
||
**Q2: 为什么切换时间范围后数据没变化?**
|
||
|
||
A: 可能是Redis缓存,等待20分钟缓存过期或者联系后端清除缓存。
|
||
|
||
**Q3: 增长率为什么总是100%?**
|
||
|
||
A: 当上个周期数据为0,当前周期有数据时,增长率固定显示100%。
|
||
|
||
**Q4: 省级管理员和地市管理员看到的数据一样?**
|
||
|
||
A: 如果省级管理员的地市也是"梅州",且数据库中只有梅州的数据,两者看到的会一样。省级管理员应该看到所有地市的汇总。
|
||
|
||
**Q5: 通过率计算是否包含"审核中"的文档?**
|
||
|
||
A: 通过率 = 通过数量 / 已审核总数。
|
||
- 已审核总数包含"审核中"(status=2)
|
||
- 但通过数量只计算status=1的文档
|
||
- 所以"审核中"会降低通过率
|
||
|
||
---
|
||
|
||
## 版本历史
|
||
|
||
| 版本 | 日期 | 更新内容 |
|
||
|------|------|---------|
|
||
| v3.0 | 2025-11-22 | 🎉 新增时间范围筛选功能(today/3days/7days/30days) |
|
||
| v2.1 | 2025-11-22 | ✨ "已审核"统计包含"审核中"状态(audit_status=2) |
|
||
| v2.0 | 2025-11-20 | 🔒 完善三级权限隔离机制 |
|
||
| v1.0 | 2025-11-15 | 🚀 初始版本发布 |
|
||
|
||
---
|
||
|
||
## 技术支持
|
||
|
||
如有疑问,请联系后端开发团队或查看:
|
||
|
||
- 📖 完整API文档: `/docs/api/`
|
||
- 🐛 问题反馈: [GitHub Issues](https://github.com/your-repo/issues)
|
||
- 💬 技术讨论: 开发团队微信群
|
||
|
||
---
|
||
|
||
**文档生成时间**: 2025-11-22
|
||
**文档维护**: 后端开发团队
|
||
**最后更新**: Claude Code 🤖
|