# 统计分析模块 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 ``` **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 ``` **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 ``` --- ## 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 开发团队