Files
leaudit-platform-frontend/auth_doc/系统概览.md
T
2025-12-05 00:09:32 +08:00

1150 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 统计分析模块 API 文档
> **路径**: `app/routes/v3/statistics.py`
> **路由前缀**: `/admin/v3/statistics`
> **功能**: 提供评查数据的统计分析,包括高频错误评查点和高风险用户排行
---
## 📋 目录
- [1. 模块概述](#1-模块概述)
- [2. 接口列表](#2-接口列表)
- [3. 详细接口说明](#3-详细接口说明)
- [3.1 获取高频错误评查点](#31-获取高频错误评查点)
- [3.2 获取高风险用户](#32-获取高风险用户)
- [4. 权限控制](#4-权限控制)
- [5. 地市隔离机制](#5-地市隔离机制)
- [6. 数据结构](#6-数据结构)
- [7. 使用示例](#7-使用示例)
- [8. 最佳实践](#8-最佳实践)
---
## 1. 模块概述
统计分析模块专为管理员提供数据洞察能力,帮助发现系统中的薄弱环节和高风险用户,从而进行针对性的培训和改进。
### 核心特性
1. **仅管理员访问**
- 所有统计接口仅限 `admin` 角色访问
- 普通用户无法查看统计数据
2. **地市隔离**
- 管理员只能查看自己地市的统计数据
- 从 JWT Token 中提取 `area` 字段进行数据过滤
- 确保数据安全和隐私
3. **时间范围过滤**
- 支持按时间范围统计(`start_date` - `end_date`
- 不提供时间参数时统计全部历史数据
4. **Top N 排行**
- 支持自定义返回数量(`limit` 参数)
- 高频错误评查点:最多返回 50 条
- 高风险用户:最多返回 20 条
### 应用场景
| 统计类型 | 应用场景 | 改进措施 |
|---------|---------|---------|
| **高频错误评查点** | 发现常见合规问题 | 加强培训、优化模板 |
| **高风险用户** | 识别需要培训的用户 | 针对性培训、质量把控 |
---
## 2. 接口列表
| 序号 | HTTP方法 | 路径 | 功能 | 认证要求 | 权限要求 |
|------|---------|------|------|---------|---------|
| 1 | GET | `/top-error-points` | 获取高频错误评查点 Top N | JWT必需 | `admin` |
| 2 | GET | `/top-risk-users` | 获取高风险用户 Top N | JWT必需 | `admin` |
---
## 3. 详细接口说明
### 3.1 获取高频错误评查点
统计最常出错的评查点,按累计出错用户数排序。
#### 接口信息
- **HTTP方法**: `GET`
- **路径**: `/admin/v3/statistics/top-error-points`
- **认证**: JWT Token(必需)
- **权限**: `admin`(管理员)
#### 请求参数
**Headers**:
```
Authorization: Bearer <JWT_TOKEN>
```
**Query 参数**:
| 参数 | 类型 | 必填 | 默认值 | 范围 | 说明 |
|-----|------|-----|--------|------|------|
| `limit` | `int` | 否 | `10` | `1-50` | 返回Top N条记录 |
| `start_date` | `str` | 否 | `null` | - | 开始时间(格式:`YYYY-MM-DD` |
| `end_date` | `str` | 否 | `null` | - | 结束时间(格式:`YYYY-MM-DD` |
#### 请求示例
**示例 1: 获取 Top 10 高频错误评查点(全部历史)**
**cURL**:
```bash
curl -X GET "http://localhost:8000/admin/v3/statistics/top-error-points?limit=10" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Python**:
```python
import requests
url = "http://localhost:8000/admin/v3/statistics/top-error-points"
headers = {
"Authorization": f"Bearer {jwt_token}"
}
params = {
"limit": 10
}
response = requests.get(url, headers=headers, params=params)
data = response.json()
print(f"高频错误评查点 Top {data['total']}:")
for item in data['items']:
print(f" {item['rank']}. {item['point_name']} - {item['error_user_count']} 人出错")
```
**JavaScript**:
```javascript
const params = new URLSearchParams({
limit: '10'
});
const response = await fetch(
`http://localhost:8000/admin/v3/statistics/top-error-points?${params}`,
{
headers: {
'Authorization': `Bearer ${jwtToken}`
}
}
);
const data = await response.json();
console.log(`高频错误评查点 Top ${data.total}:`);
data.items.forEach(item => {
console.log(` ${item.rank}. ${item.point_name} - ${item.error_user_count} 人出错`);
});
```
**示例 2: 获取 2025年11月的 Top 20 高频错误评查点**
**cURL**:
```bash
curl -X GET "http://localhost:8000/admin/v3/statistics/top-error-points?limit=20&start_date=2025-11-01&end_date=2025-11-30" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Python**:
```python
import requests
url = "http://localhost:8000/admin/v3/statistics/top-error-points"
headers = {
"Authorization": f"Bearer {jwt_token}"
}
params = {
"limit": 20,
"start_date": "2025-11-01",
"end_date": "2025-11-30"
}
response = requests.get(url, headers=headers, params=params)
data = response.json()
print(f"2025年11月高频错误评查点 Top {data['total']}:")
for item in data['items']:
print(f" {item['rank']}. {item['point_name']} (ID: {item['evaluation_point_id']})")
print(f" 累计出错用户数: {item['error_user_count']}")
```
**JavaScript**:
```javascript
const params = new URLSearchParams({
limit: '20',
start_date: '2025-11-01',
end_date: '2025-11-30'
});
const response = await fetch(
`http://localhost:8000/admin/v3/statistics/top-error-points?${params}`,
{
headers: {
'Authorization': `Bearer ${jwtToken}`
}
}
);
const data = await response.json();
console.log(`2025年11月高频错误评查点 Top ${data.total}`);
```
#### 响应示例
**成功响应** (200 OK):
```json
{
"total": 10,
"items": [
{
"rank": 1,
"evaluation_point_id": 45,
"point_name": "管辖地点明确性",
"error_user_count": 38
},
{
"rank": 2,
"evaluation_point_id": 67,
"point_name": "履约保证金比例合规性",
"error_user_count": 25
},
{
"rank": 3,
"evaluation_point_id": 23,
"point_name": "付款条款完整性",
"error_user_count": 19
},
{
"rank": 4,
"evaluation_point_id": 89,
"point_name": "违约责任条款明确性",
"error_user_count": 15
},
{
"rank": 5,
"evaluation_point_id": 12,
"point_name": "工程质量标准规范性",
"error_user_count": 12
}
]
}
```
**失败响应 - 权限不足** (403 Forbidden):
```json
{
"detail": "此接口仅限管理员访问"
}
```
**失败响应 - 缺少地市信息** (400 Bad Request):
```json
{
"detail": "用户缺少地市信息,无法进行统计查询"
}
```
**失败响应 - 时间格式错误** (400 Bad Request):
```json
{
"detail": "开始时间格式错误,应为 YYYY-MM-DD,实际为: 2025/11/01"
}
```
#### 业务逻辑
```
1. 验证JWT Token
2. 提取用户信息(user_id, user_role, area
3. ✅ 权限检查
├─ 验证user_role == "admin"
├─ 验证current_user.area存在
└─ 不满足则返回403或400
4. 📅 解析时间参数
├─ start_date: 转换为datetime对象
├─ end_date: 转换为datetime对象,并调整到23:59:59
└─ 格式错误则返回400
5. 🗂️ 调用服务层
├─ StatisticsService.get_top_error_evaluation_points()
├─ 传入参数: area, limit, start_date, end_date
└─ 查询evaluation_results表
6. 📊 统计逻辑
├─ 筛选条件:
│ ├─ result = 'false' (评查失败)
│ ├─ documents.ou_id = current_user.area (地市隔离)
│ └─ created_at BETWEEN start_date AND end_date (时间范围)
├─ 分组统计:
│ ├─ GROUP BY evaluation_point_id
│ └─ COUNT(DISTINCT user_id) AS error_user_count (去重用户)
└─ 排序: ORDER BY error_user_count DESC
7. 📤 构建响应
├─ 添加排名(rank
└─ 返回TopErrorPointsResponse
```
#### 统计说明
**去重规则**:
- 同一用户在多个文档中触发同一评查点错误,只计数一次
- 统计的是 "有多少人犯了这个错误",而不是 "这个错误发生了多少次"
**示例**:
```
用户A的文档1: 管辖地点明确性 - 失败
用户A的文档2: 管辖地点明确性 - 失败
用户B的文档1: 管辖地点明确性 - 失败
结果: 管辖地点明确性的error_user_count = 2(只计数A和B两个用户)
```
---
### 3.2 获取高风险用户
统计最容易出错的用户,按累计出错数排序。
#### 接口信息
- **HTTP方法**: `GET`
- **路径**: `/admin/v3/statistics/top-risk-users`
- **认证**: JWT Token(必需)
- **权限**: `admin`(管理员)
#### 请求参数
**Headers**:
```
Authorization: Bearer <JWT_TOKEN>
```
**Query 参数**:
| 参数 | 类型 | 必填 | 默认值 | 范围 | 说明 |
|-----|------|-----|--------|------|------|
| `limit` | `int` | 否 | `5` | `1-20` | 返回Top N条记录 |
| `start_date` | `str` | 否 | `null` | - | 开始时间(格式:`YYYY-MM-DD` |
| `end_date` | `str` | 否 | `null` | - | 结束时间(格式:`YYYY-MM-DD` |
#### 请求示例
**示例 1: 获取 Top 5 高风险用户(全部历史)**
**cURL**:
```bash
curl -X GET "http://localhost:8000/admin/v3/statistics/top-risk-users?limit=5" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Python**:
```python
import requests
url = "http://localhost:8000/admin/v3/statistics/top-risk-users"
headers = {
"Authorization": f"Bearer {jwt_token}"
}
params = {
"limit": 5
}
response = requests.get(url, headers=headers, params=params)
data = response.json()
print(f"高风险用户 Top {data['total']}:")
for item in data['items']:
print(f" {item['rank']}. {item['user_name']} ({item['department']})")
print(f" 累计出错: {item['total_errors']}")
print(f" 单文档平均出错: {item['avg_errors_per_doc']:.2f}")
```
**JavaScript**:
```javascript
const params = new URLSearchParams({
limit: '5'
});
const response = await fetch(
`http://localhost:8000/admin/v3/statistics/top-risk-users?${params}`,
{
headers: {
'Authorization': `Bearer ${jwtToken}`
}
}
);
const data = await response.json();
console.log(`高风险用户 Top ${data.total}:`);
data.items.forEach(item => {
console.log(` ${item.rank}. ${item.user_name} (${item.department})`);
console.log(` 累计出错: ${item.total_errors}`);
console.log(` 单文档平均出错: ${item.avg_errors_per_doc.toFixed(2)}`);
});
```
**示例 2: 获取 2025年11月的 Top 10 高风险用户**
**cURL**:
```bash
curl -X GET "http://localhost:8000/admin/v3/statistics/top-risk-users?limit=10&start_date=2025-11-01&end_date=2025-11-30" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Python**:
```python
import requests
url = "http://localhost:8000/admin/v3/statistics/top-risk-users"
headers = {
"Authorization": f"Bearer {jwt_token}"
}
params = {
"limit": 10,
"start_date": "2025-11-01",
"end_date": "2025-11-30"
}
response = requests.get(url, headers=headers, params=params)
data = response.json()
print(f"2025年11月高风险用户 Top {data['total']}:")
for item in data['items']:
print(f" {item['rank']}. {item['user_name']} (ID: {item['user_id']})")
print(f" 部门: {item['department']}")
print(f" 累计出错: {item['total_errors']}")
print(f" 单文档平均出错: {item['avg_errors_per_doc']:.2f}")
```
#### 响应示例
**成功响应** (200 OK):
```json
{
"total": 5,
"items": [
{
"rank": 1,
"user_id": 123,
"user_name": "张三",
"department": "工程部",
"total_errors": 45,
"avg_errors_per_doc": 3.75
},
{
"rank": 2,
"user_id": 456,
"user_name": "李四",
"department": "采购部",
"total_errors": 38,
"avg_errors_per_doc": 3.17
},
{
"rank": 3,
"user_id": 789,
"user_name": "王五",
"department": "法务部",
"total_errors": 32,
"avg_errors_per_doc": 2.67
},
{
"rank": 4,
"user_id": 234,
"user_name": "赵六",
"department": "工程部",
"total_errors": 28,
"avg_errors_per_doc": 2.33
},
{
"rank": 5,
"user_id": 567,
"user_name": "孙七",
"department": "采购部",
"total_errors": 25,
"avg_errors_per_doc": 2.08
}
]
}
```
**失败响应 - 权限不足** (403 Forbidden):
```json
{
"detail": "此接口仅限管理员访问"
}
```
#### 业务逻辑
```
1. 验证JWT Token
2. 提取用户信息(user_id, user_role, area
3. ✅ 权限检查
├─ 验证user_role == "admin"
├─ 验证current_user.area存在
└─ 不满足则返回403或400
4. 📅 解析时间参数(同高频错误评查点)
5. 🗂️ 调用服务层
├─ StatisticsService.get_top_risk_users()
├─ 传入参数: area, limit, start_date, end_date
└─ 查询evaluation_results + documents + users表
6. 📊 统计逻辑
├─ 筛选条件:
│ ├─ result = 'false' (评查失败)
│ ├─ documents.ou_id = current_user.area (地市隔离)
│ └─ created_at BETWEEN start_date AND end_date (时间范围)
├─ 分组统计:
│ ├─ GROUP BY documents.user_id
│ ├─ COUNT(*) AS total_errors (累计出错数)
│ └─ COUNT(DISTINCT document_id) AS doc_count (文档总数)
├─ 计算平均值:
│ └─ avg_errors_per_doc = total_errors / doc_count
└─ 排序: ORDER BY total_errors DESC
7. 📤 构建响应
├─ 添加排名(rank
├─ 联表查询用户名和部门
└─ 返回TopRiskUsersResponse
```
#### 统计说明
**计算公式**:
```
累计出错数 = 该用户所有文档的错误评查点总数
单文档平均出错数 = 累计出错数 / 文档总数
```
**示例**:
```
用户A:
- 文档1: 3个错误评查点
- 文档2: 5个错误评查点
- 文档3: 2个错误评查点
结果:
- total_errors = 10
- doc_count = 3
- avg_errors_per_doc = 10 / 3 = 3.33
```
---
## 4. 权限控制
### 4.1 权限验证函数
**代码位置**: `app/routes/v3/statistics.py:24-55`
```python
def require_admin(current_user: User = Depends(verify_token)) -> User:
"""
权限验证:要求管理员角色
Args:
current_user: 当前登录用户
Returns:
User: 验证通过的管理员用户
Raises:
HTTPException: 403 如果用户不是管理员
"""
if current_user.user_role != "admin":
logger.warning(
f"用户 {current_user.username} (角色:{current_user.user_role}) "
f"尝试访问管理员专属统计接口"
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="此接口仅限管理员访问"
)
# 检查用户是否有地市信息
if not hasattr(current_user, 'area') or not current_user.area:
logger.error(f"管理员用户 {current_user.username} 缺少地市(area)信息")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="用户缺少地市信息,无法进行统计查询"
)
return current_user
```
### 4.2 权限检查流程
```
1. JWT Token 验证
├─ 验证Token签名
├─ 验证Token有效期
└─ 提取用户信息(user_id, username, user_role, area
2. 角色验证
├─ 检查user_role == "admin"
└─ 不是管理员则返回403
3. 地市信息验证
├─ 检查current_user.area存在
└─ 不存在则返回400
```
### 4.3 权限级别
| 用户角色 | 访问权限 | 可见数据范围 |
|---------|---------|------------|
| `admin` | ✅ 允许访问 | 只能查看自己地市的统计数据 |
| `provincial_admin` | ❌ 禁止访问 | - |
| `user` | ❌ 禁止访问 | - |
| `guest` | ❌ 禁止访问 | - |
**注意**: 即使是省级管理员(`provincial_admin`)也无法访问统计接口,因为统计接口专为地市级管理员设计。
---
## 5. 地市隔离机制
### 5.1 地市隔离原理
**数据过滤逻辑**:
```sql
-- 高频错误评查点统计
SELECT
ep.id AS evaluation_point_id,
ep.name AS point_name,
COUNT(DISTINCT d.user_id) AS error_user_count
FROM evaluation_results er
JOIN evaluation_points ep ON er.evaluation_point_id = ep.id
JOIN documents d ON er.document_id = d.id
WHERE er.evaluated_results->>'result' = 'false'
AND d.ou_id = ${current_user.area} -- 地市隔离
AND er.created_at BETWEEN ${start_date} AND ${end_date}
GROUP BY ep.id, ep.name
ORDER BY error_user_count DESC
LIMIT ${limit};
```
**关键字段**:
- `documents.ou_id`: 文档所属地市(如: "梅州", "云浮", "揭阳", "潮州"
- `current_user.area`: 当前管理员所属地市
### 5.2 地市映射
| 地市代码 | 地市名称 | 说明 |
|---------|---------|------|
| `梅州` | 梅州市 | 地市级 |
| `云浮` | 云浮市 | 地市级 |
| `揭阳` | 揭阳市 | 地市级 |
| `潮州` | 潮州市 | 地市级 |
| `省级` | 广东省 | 省级(暂不支持统计接口) |
### 5.3 地市隔离示例
**场景**:
- 梅州管理员登录
- JWT Token中 `area = "梅州"`
**查询结果**:
- ✅ 包含:梅州市用户的所有文档评查数据
- ❌ 不包含:云浮、揭阳、潮州的文档评查数据
**效果**:
```
梅州管理员查询高频错误评查点:
1. 管辖地点明确性 - 38人出错(都是梅州市用户)
2. 履约保证金比例 - 25人出错(都是梅州市用户)
...
云浮管理员查询高频错误评查点:
1. 付款条款完整性 - 42人出错(都是云浮市用户)
2. 工程质量标准 - 30人出错(都是云浮市用户)
...
```
---
## 6. 数据结构
### 6.1 高频错误评查点
```typescript
interface TopErrorPointItem {
rank: number; // 排名(1, 2, 3...
evaluation_point_id: number; // 评查点ID
point_name: string; // 评查点名称
error_user_count: number; // 累计出错用户数(去重)
}
interface TopErrorPointsResponse {
total: number; // 返回记录数
items: TopErrorPointItem[]; // 评查点列表
}
```
### 6.2 高风险用户
```typescript
interface TopRiskUserItem {
rank: number; // 排名(1, 2, 3...
user_id: number; // 用户ID
user_name: string; // 用户真实姓名
department: string; // 所在部门
total_errors: number; // 累计出错数
avg_errors_per_doc: number; // 单文档平均出错数(保留2位小数)
}
interface TopRiskUsersResponse {
total: number; // 返回记录数
items: TopRiskUserItem[]; // 用户列表
}
```
---
## 7. 使用示例
### 7.1 完整的统计看板示例
**Vue 3 + Element Plus + ECharts**:
```vue
<template>
<div class="statistics-dashboard">
<el-row :gutter="20">
<!-- 时间范围选择器 -->
<el-col :span="24">
<el-card>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
@change="fetchAllStatistics"
/>
<el-button type="primary" style="margin-left: 10px;" @click="resetDateRange">
查看全部历史
</el-button>
</el-card>
</el-col>
<!-- 高频错误评查点 -->
<el-col :span="12">
<el-card>
<template #header>
<div class="card-header">
<span>高频错误评查点 Top 10</span>
<el-tag type="danger">重点关注</el-tag>
</div>
</template>
<div ref="errorPointsChart" style="height: 400px;"></div>
<el-table :data="errorPoints" style="margin-top: 20px;">
<el-table-column prop="rank" label="排名" width="80" />
<el-table-column prop="point_name" label="评查点名称" />
<el-table-column prop="error_user_count" label="出错人数" width="120">
<template #default="{ row }">
<el-tag type="danger">{{ row.error_user_count }} </el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<!-- 高风险用户 -->
<el-col :span="12">
<el-card>
<template #header>
<div class="card-header">
<span>高风险用户 Top 10</span>
<el-tag type="warning">需要培训</el-tag>
</div>
</template>
<div ref="riskUsersChart" style="height: 400px;"></div>
<el-table :data="riskUsers" style="margin-top: 20px;">
<el-table-column prop="rank" label="排名" width="80" />
<el-table-column prop="user_name" label="用户" />
<el-table-column prop="department" label="部门" />
<el-table-column prop="total_errors" label="累计出错" width="120">
<template #default="{ row }">
<el-tag type="warning">{{ row.total_errors }} </el-tag>
</template>
</el-table-column>
<el-table-column prop="avg_errors_per_doc" label="平均出错" width="120">
<template #default="{ row }">
{{ row.avg_errors_per_doc.toFixed(2) }}
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue';
import axios from 'axios';
import * as echarts from 'echarts';
import { ElMessage } from 'element-plus';
const dateRange = ref(null);
const errorPoints = ref([]);
const riskUsers = ref([]);
const errorPointsChart = ref(null);
const riskUsersChart = ref(null);
let errorPointsChartInstance = null;
let riskUsersChartInstance = null;
async function fetchErrorPoints() {
try {
const params = {
limit: 10
};
if (dateRange.value && dateRange.value.length === 2) {
params.start_date = dateRange.value[0];
params.end_date = dateRange.value[1];
}
const { data } = await axios.get('/admin/v3/statistics/top-error-points', { params });
errorPoints.value = data.items;
// 更新图表
renderErrorPointsChart(data.items);
} catch (error) {
ElMessage.error('获取高频错误评查点失败');
console.error(error);
}
}
async function fetchRiskUsers() {
try {
const params = {
limit: 10
};
if (dateRange.value && dateRange.value.length === 2) {
params.start_date = dateRange.value[0];
params.end_date = dateRange.value[1];
}
const { data } = await axios.get('/admin/v3/statistics/top-risk-users', { params });
riskUsers.value = data.items;
// 更新图表
renderRiskUsersChart(data.items);
} catch (error) {
ElMessage.error('获取高风险用户失败');
console.error(error);
}
}
async function fetchAllStatistics() {
await Promise.all([
fetchErrorPoints(),
fetchRiskUsers()
]);
}
function resetDateRange() {
dateRange.value = null;
fetchAllStatistics();
}
function renderErrorPointsChart(items) {
if (!errorPointsChartInstance) {
errorPointsChartInstance = echarts.init(errorPointsChart.value);
}
const option = {
title: {
text: '高频错误评查点统计'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
xAxis: {
type: 'value',
name: '出错人数'
},
yAxis: {
type: 'category',
data: items.map(item => item.point_name).reverse(),
axisLabel: {
formatter: (value) => {
return value.length > 10 ? value.substring(0, 10) + '...' : value;
}
}
},
series: [{
name: '出错人数',
type: 'bar',
data: items.map(item => item.error_user_count).reverse(),
itemStyle: {
color: '#f56c6c'
},
label: {
show: true,
position: 'right'
}
}]
};
errorPointsChartInstance.setOption(option);
}
function renderRiskUsersChart(items) {
if (!riskUsersChartInstance) {
riskUsersChartInstance = echarts.init(riskUsersChart.value);
}
const option = {
title: {
text: '高风险用户统计'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['累计出错数', '平均出错数']
},
xAxis: {
type: 'category',
data: items.map(item => item.user_name)
},
yAxis: [
{
type: 'value',
name: '累计出错数'
},
{
type: 'value',
name: '平均出错数'
}
],
series: [
{
name: '累计出错数',
type: 'bar',
data: items.map(item => item.total_errors),
itemStyle: {
color: '#e6a23c'
}
},
{
name: '平均出错数',
type: 'line',
yAxisIndex: 1,
data: items.map(item => item.avg_errors_per_doc),
itemStyle: {
color: '#f56c6c'
}
}
]
};
riskUsersChartInstance.setOption(option);
}
onMounted(() => {
nextTick(() => {
fetchAllStatistics();
});
// 响应式调整图表大小
window.addEventListener('resize', () => {
errorPointsChartInstance?.resize();
riskUsersChartInstance?.resize();
});
});
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
```
---
## 8. 最佳实践
### 8.1 定期统计分析
**建议频率**:
- **周报**: 每周一查看上周统计数据
- **月报**: 每月初查看上月统计数据
- **季度报**: 每季度末查看季度统计数据
**Python 定时任务示例**:
```python
import requests
from datetime import datetime, timedelta
import schedule
import time
def weekly_statistics_report(jwt_token: str):
"""
每周统计报告生成
"""
# 计算上周的日期范围
today = datetime.now()
last_monday = today - timedelta(days=today.weekday() + 7)
last_sunday = last_monday + timedelta(days=6)
start_date = last_monday.strftime('%Y-%m-%d')
end_date = last_sunday.strftime('%Y-%m-%d')
print(f"\n=== 上周统计报告 ({start_date} ~ {end_date}) ===\n")
# 获取高频错误评查点
error_points_resp = requests.get(
"http://localhost:8000/admin/v3/statistics/top-error-points",
headers={"Authorization": f"Bearer {jwt_token}"},
params={
"limit": 10,
"start_date": start_date,
"end_date": end_date
}
)
if error_points_resp.status_code == 200:
error_points = error_points_resp.json()['items']
print("高频错误评查点 Top 10:")
for item in error_points:
print(f" {item['rank']}. {item['point_name']} - {item['error_user_count']}")
# 获取高风险用户
risk_users_resp = requests.get(
"http://localhost:8000/admin/v3/statistics/top-risk-users",
headers={"Authorization": f"Bearer {jwt_token}"},
params={
"limit": 5,
"start_date": start_date,
"end_date": end_date
}
)
if risk_users_resp.status_code == 200:
risk_users = risk_users_resp.json()['items']
print("\n高风险用户 Top 5:")
for item in risk_users:
print(f" {item['rank']}. {item['user_name']} ({item['department']}) - {item['total_errors']}")
print("\n==========================================\n")
# 设置定时任务:每周一早上9点执行
schedule.every().monday.at("09:00").do(weekly_statistics_report, jwt_token="your_jwt_token")
while True:
schedule.run_pending()
time.sleep(60)
```
### 8.2 改进措施建议
**基于高频错误评查点**:
| 出错人数 | 改进措施 | 优先级 |
|---------|---------|--------|
| > 30人 | 全员培训、修订模板、优化流程 | 🔴 高 |
| 20-30人 | 针对性培训、发布指导文档 | 🟡 中 |
| 10-20人 | 内部分享、案例学习 | 🟢 低 |
| < 10人 | 个别指导 | - |
**基于高风险用户**:
| 累计出错数 | 改进措施 | 优先级 |
|-----------|---------|--------|
| > 40次 | 一对一培训、质量把控 | 🔴 高 |
| 30-40次 | 小组培训、定期复查 | 🟡 中 |
| 20-30次 | 案例分享、自我学习 | 🟢 低 |
| < 20次 | 日常指导 | - |
### 8.3 统计数据导出
**Python导出Excel示例**:
```python
import requests
import pandas as pd
from datetime import datetime
def export_statistics_to_excel(jwt_token: str, output_file: str):
"""
导出统计数据到Excel
"""
# 获取高频错误评查点
error_points_resp = requests.get(
"http://localhost:8000/admin/v3/statistics/top-error-points",
headers={"Authorization": f"Bearer {jwt_token}"},
params={"limit": 50}
)
error_points = error_points_resp.json()['items']
# 获取高风险用户
risk_users_resp = requests.get(
"http://localhost:8000/admin/v3/statistics/top-risk-users",
headers={"Authorization": f"Bearer {jwt_token}"},
params={"limit": 20}
)
risk_users = risk_users_resp.json()['items']
# 创建Excel文件
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
# 高频错误评查点
df_error_points = pd.DataFrame(error_points)
df_error_points.to_excel(
writer,
sheet_name='高频错误评查点',
index=False
)
# 高风险用户
df_risk_users = pd.DataFrame(risk_users)
df_risk_users.to_excel(
writer,
sheet_name='高风险用户',
index=False
)
print(f"统计数据已导出到: {output_file}")
# 使用示例
export_statistics_to_excel(
jwt_token="your_jwt_token",
output_file=f"统计报告_{datetime.now().strftime('%Y%m%d')}.xlsx"
)
```
---
## 相关文档
- [认证授权模块](../01_authentication/README.md) - JWT Token 获取和验证
- [评查管理模块](../05_evaluation/README.md) - 评查结果数据来源
- [评查点管理](../10_evaluation_points/README.md) - 评查点配置
- [用户组织管理](../07_users/README.md) - 用户和部门信息
---
**文档版本**: v1.0
**最后更新**: 2025-11-20
**维护者**: DocAuditAI 开发团队