Files
student_manage_system/docs/05-optimization-plan.md
T

453 lines
17 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.
# 学生管理系统 — 优化文档 v1.0
> 本文档针对当前 V1.0 代码实现,梳理存在的问题、提出优化方案,按优先级排序,供后续迭代参考。
---
## 一、当前问题清单
### 1. 数据库层
#### [P0] database.py 硬编码 SQLite,未支持多数据源
**现状:**
```python
DATABASE_URL = "sqlite:///./sms.db"
```
**问题:** 开发/测试/生产环境共用同一配置,切换数据库需改代码重启。
**影响:** README 中明确规划了 dev→prod 的多数据源适配,当前未实现。
**优化方案:**
- 引入 `pydantic-settings` 或 `python-dotenv` 读取 `.env` 环境变量
- 根据 `ENV` 环境变量自动切换连接串
- 支持 MySQL / PostgreSQL 驱动切换
#### [P0] 软删除未更新 delete_time
**现状:** `students.py`、`scores.py`、`employment.py` 中软删除只设 `is_deleted=True`,`delete_time` 始终为 NULL;仅 `teachers.py` 正确设置了 `delete_time`。
**问题:** 审计字段不一致,删除追溯能力缺失。
**优化方案:** 统一在 BaseModel 或基类方法中处理 `delete_time` 赋值,所有删除接口复用同一逻辑。
#### [P1] 缺少 Alembic 迁移工具
**现状:** 建表依赖 `Base.metadata.create_all()`,每次启动都会执行,生产环境不安全。
**问题:** 无法做版本控制、灰度发布、回滚。
**优化方案:** 引入 Alembic,将 `init_db()` 改为迁移脚本,seed 数据单独管理。
#### [P1] 索引设计有冗余和缺失
**现状:**
- `teachers.num` 同时有 `unique=True`(自带索引)和代码中手动加 `index=True`,重复
- `students` 表缺少 `(is_deleted, class_id)` 联合索引的显式定义(虽然代码中写了但 ORM 未强制)
- `employment` 表缺少 `(class_id, is_deleted)` 复合索引的实际生效确认
**优化方案:** 清理重复索引,在模型 `__table_args__` 中统一声明复合索引,与 DDL 文档对齐。
---
### 2. API 层
#### [P0] 缺少统一异常处理和响应格式
**现状:** 每个 router 独立 `raise HTTPException`,错误响应格式不一致(有的返回 `{"detail": "..."}`,有的返回 plain string)。
**问题:** 前端难以统一处理错误,移动端/第三方接入体验差。
**优化方案:**
```python
# 新增 utils/exceptions.py
class SMSException(Exception):
def __init__(self, code: int, message: str, detail: Any = None):
self.code = code
self.message = message
self.detail = detail
@app.exception_handler(SMSException)
async def sms_exception_handler(request, exc):
return JSONResponse(
status_code=exc.code,
content={"code": exc.code, "message": exc.message, "detail": exc.detail},
)
```
#### [P0] 分页参数 skip/limit 未做边界校验
**现状:** `limit` 最大无上限,攻击者可传 `?limit=999999` 导致内存溢出。
**问题:** 缺乏防护,存在 DoS 风险。
**优化方案:**
```python
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200), # 默认50,最大200
```
#### [P1] 列表接口无总数返回
**现状:** GET 列表只返回 `List[Model]`,前端无法实现完整分页(无 total count)。
**问题:** 需要额外发一次 `COUNT` 请求,增加网络开销。
**优化方案:** 返回 `{ "data": [...], "total": int, "skip": int, "limit": int }` 结构。
#### [P1] CORS 全开放,生产不安全
**现状:**
```python
allow_origins=["*"], allow_credentials=True
```
**问题:** `*` 与 `allow_credentials=True` 冲突(浏览器会拒绝),且生产环境不应放行所有来源。
**优化方案:** 通过环境变量配置允许的来源列表,生产环境严格限制。
#### [P1] 统计接口存在 N+1 查询问题
**现状:** `stats.py` 的 `employment_stats` 中,对每个班级单独再发一次查询计算 `avg_duration_days`:
```python
for r in results:
durations = db.query(...).filter(Employment.class_id == r.class_id, ...).all()
```
**问题:** N 个班级触发 N+1 次查询,数据量增长后性能急剧下降。
**优化方案:** 一次查询获取所有班级的周期数据,在内存中分组聚合。
---
### 3. 业务逻辑层
#### [P0] 缺少用户认证与权限控制
**现状:** 所有接口无需任何凭证即可访问和修改数据。
**问题:** 任何知道 URL 的人都可以增删改查全部数据,存在严重安全隐患。
**优化方案(V2.0):**
- 引入 JWT OAuth2 认证(FastAPI 内置 `OAuth2PasswordBearer`)
- 基于角色的访问控制(RBAC):教务管理员 / 老师 / 顾问
- 敏感接口(写操作)强制鉴权,统计接口可匿名访问
#### [P1] 缺少数据校验规则
**现状:** Pydantic Schema 未对关键字段做格式约束。
**问题示例:**
- `age` 可以为负数或超大值
- `score` 可以为负数或超过合理范围(如 200 分)
- `num`(编号)无格式校验,可能输入非法字符
**优化方案:**
```python
from pydantic import Field, field_validator
class StudentCreate(BaseModel):
age: int = Field(6, ge=6, le=100)
sex: int = Field(0, ge=0, le=2)
score: Decimal = Field(..., ge=0, le=100)
@field_validator('num')
@classmethod
def validate_num(cls, v):
if not re.match(r'^[A-Za-z0-9_-]{1,30}$', v):
raise ValueError('编号只能包含字母、数字、下划线和短横线')
return v
```
#### [P1] 删除操作无二次确认(业务层)
**现状:** 删除学生时,级联删除其所有成绩和就业记录,无前置检查。
**问题:** 一旦误删,数据不可恢复(软删除也无法区分级联删除和手动删除)。
**优化方案:**
- 删除学生前检查是否有关联成绩/就业记录,给出警告
- 提供"彻底删除"和"软删除"两种模式供管理员选择
#### [P2] seed 数据硬编码在 Python 中,不可维护
**现状:** `seed.py` 包含大量内联数据,与 DDL 文档中的造数 SQL 分离。
**问题:** 两份数据容易不同步,新成员难以快速理解测试数据结构。
**优化方案:** 将种子数据统一迁移到 SQL 文件(`seed/data.sql`),由 Alembic 迁移或独立脚本导入。
---
### 4. 前端层
#### [P1] 静态页面与后端耦合度高
**现状:** `static/index.html` 中 `API = 'http://localhost:8000/api'` 硬编码。
**问题:** 部署到测试/生产环境需改代码重新构建。
**优化方案:**
- 通过环境变量注入 API 地址:`const API_BASE = window.API_BASE || 'http://localhost:8000/api';`
- 或在 Nginx 层做代理转发,前端直接请求相对路径 `/api/...`
#### [P1] 缺少加载状态和错误提示
**现状:** 所有接口调用无 loading 状态,失败时无用户提示。
**问题:** 用户体验差,网络异常时页面卡死无反馈。
**优化方案:** 添加统一的请求拦截器,显示 loading spinner 和错误 toast。
#### [P2] 前端无路由,单页切换靠 display 控制
**现状:** 所有模块在一个 HTML 文件中,用 `style.display` 切换。
**问题:** 无法书签共享特定页面,SEO 不支持,体积随功能增长而膨胀。
**优化方案:** V2.0 迁移至 Vue3 + Vue Router,组件化开发。
---
### 5. 运维与工程化
#### [P0] 缺少健康检查端点
**现状:** 无 `/health` 接口,负载均衡器无法检测服务状态。
**问题:** 容器编排(K8s/Docker Swarm)无法做存活探针,故障实例无法自动剔除。
**优化方案:**
```python
from sqlalchemy import text
@app.get("/health")
async def health_check(db: Session = Depends(get_db)):
db.execute(text("SELECT 1"))
return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
```
#### [P1] 缺少日志系统
**现状:** 无日志配置,生产环境无法追踪请求链路。
**问题:** 故障排查只能靠打印到终端,线上无法定位问题。
**优化方案:**
- 引入 `loguru`,配置请求日志中间件
- 记录:请求方法、路径、状态码、耗时、客户端 IP
- 日志按天轮转,保留 30 天
#### [P1] 缺少单元测试
**现状:** 无任何测试代码。
**问题:** 修改功能无法验证回归,重构风险高。
**优化方案:**
- 引入 `pytest` + `httpx`(异步测试客户端)
- 覆盖核心 CRUD 接口和统计接口
- CI 流水线中自动运行测试
#### [P2] 缺少 API 文档自动化
**现状:** FastAPI 自动生成 Swagger,但无 Postman/OpenAPI 导出。
**问题:** 前端联调时无法快速导入接口列表。
**优化方案:**
- 启动时导出 OpenAPI JSON 到 `docs/openapi.json`
- 提供 Postman Collection 下载端点
---
## 二、优化实施优先级矩阵
| 优先级 | 类别 | 优化项 | 预计工作量 | 建议阶段 |
|--------|------|--------|-----------|----------|
| P0 | 安全 | 用户认证(JWT/OAuth2) | 3-5天 | V2.0 |
| P0 | 安全 | 统一异常处理 | 0.5天 | V2.0 第1周 |
| P0 | 数据 | 多数据源配置(.env) | 1天 | V2.0 第1周 |
| P0 | 数据 | 统一软删除 delete_time | 0.5天 | V2.0 第1周 |
| P0 | 运维 | 健康检查端点 | 0.5天 | V2.0 第1周 |
| P0 | 性能 | 统计接口 N+1 查询修复 | 1天 | V2.0 第2周 |
| P1 | API | 分页参数边界校验 | 0.5天 | V2.0 第1周 |
| P1 | API | 列表接口返回总数 | 1天 | V2.0 第2周 |
| P1 | API | CORS 生产环境收紧 | 0.5天 | V2.0 第1周 |
| P1 | 业务 | 数据校验规则完善 | 2天 | V2.0 第2周 |
| P1 | 业务 | 删除前关联检查 | 1天 | V2.0 第2周 |
| P1 | 数据库 | Alembic 迁移工具 | 2天 | V2.0 第3周 |
| P1 | 数据库 | 索引清理与对齐 | 0.5天 | V2.0 第1周 |
| P1 | 运维 | 日志系统(loguru) | 1天 | V2.0 第2周 |
| P1 | 运维 | 单元测试框架 | 2天 | V2.0 第3周 |
| P2 | 前端 | API 地址环境变量化 | 0.5天 | V2.0 第1周 |
| P2 | 前端 | 加载状态与错误提示 | 1天 | V2.0 第2周 |
| P2 | 前端 | Vue3 重构(可选) | 10-15天 | V3.0 |
| P2 | 运维 | 种子数据 SQL 化 | 1天 | V2.0 第2周 |
| P2 | 运维 | API 文档导出 | 0.5天 | V2.0 第1周 |
---
## 三、V2.0 版本规划(特色需求)
### 3.1 模块新增
| 模块 | 说明 | 接口前缀 |
|------|------|----------|
| 用户认证 | JWT Token 登录/刷新/注销 | `/api/auth` |
| 部门管理 | 组织架构树,老师归属部门 | `/api/departments` |
| 顾问管理 | 顾问老师与学生的绑定关系(当前 advisor_id 已存在,需独立模块化管理) | `/api/advisors` |
| 数据源配置 | 多环境数据库配置管理(仅管理员可见) | `/api/config` |
### 3.2 统计增强
| 功能 | 说明 |
|------|------|
| 趋势分析 | 成绩/就业率按月/季度趋势折线图数据接口 |
| 导出功能 | 统计结果导出为 Excel/CSV |
| 不及格预警 | 连续两次不及格学生自动标记,推送消息 |
### 3.3 AI 能力(V3.0 预留)
| 功能 | 说明 |
|------|------|
| 自然语言查询 | 输入"三班平均分多少",自动转为 SQL 查询 |
| 智能报告 | 定期自动生成班级学情报告(PDF) |
| 知识库 | 教培行业常见问题 FAQ 智能问答 |
---
## 四、数据库优化专项
### 4.1 字典表落地(V2.0)
当前 `sex`、`education`、`coach_area` 等字段存的是裸值,需升级为字典表驱动:
```sql
-- 迁移脚本(在 Alembic migration 中执行)
ALTER TABLE students ADD COLUMN sex_code VARCHAR(10) AFTER sex;
ALTER TABLE students ADD COLUMN education_code VARCHAR(20) AFTER education;
UPDATE students SET sex_code = CAST(sex AS CHAR) WHERE sex IS NOT NULL;
UPDATE students SET education_code =
CASE education
WHEN '本科' THEN 'bachelor'
WHEN '硕士' THEN 'master'
WHEN '博士' THEN 'phd'
ELSE education_code
END;
-- 后续版本再移除裸值字段
```
### 4.2 大表归档策略(V3.0)
当学生表超过 100 万行时启用:
```sql
-- 创建归档表
CREATE TABLE students_archive LIKE students;
CREATE TABLE scores_archive LIKE scores;
-- 归档毕业超过 2 年的学生
INSERT INTO students_archive
SELECT * FROM students
WHERE is_deleted = 0
AND graduate_time IS NOT NULL
AND graduate_time < DATE_SUB(NOW(), INTERVAL 2 YEAR);
-- 归档后从主表删除(谨慎操作,先备份)
DELETE FROM students
WHERE id IN (SELECT id FROM students_archive);
```
---
## 五、运维优化专项
### 5.1 项目目录结构调整
```
student_manage_system/
├── app/ # 应用主体(重构后)
│ ├── __init__.py
│ ├── main.py # 应用入口
│ ├── config.py # 配置管理(.env 读取)
│ ├── database.py # DB 连接
│ ├── models/ # ORM 模型(按模块分包)
│ │ ├── __init__.py
│ │ ├── teacher.py
│ │ ├── student.py
│ │ ├── class.py
│ │ ├── score.py
│ │ └── employment.py
│ ├── schemas/ # Pydantic Schema
│ │ ├── __init__.py
│ │ └── ...
│ ├── routers/ # API 路由
│ │ ├── __init__.py
│ │ ├── auth.py # 新增
│ │ └── ...
│ ├── services/ # 业务逻辑层(新增)
│ │ └── ...
│ └── utils/ # 工具函数
│ ├── exceptions.py # 新增:统一异常
│ └── logging.py # 新增:日志配置
├── tests/ # 新增:测试目录
│ ├── __init__.py
│ ├── test_teachers.py
│ ├── test_students.py
│ └── conftest.py
├── alembic/ # 新增:数据库迁移
│ ├── versions/
│ └── env.py
├── seed/ # 新增:种子数据 SQL
│ └── data.sql
├── static/ # 保持现有
├── docs/ # 保持现有
├── .env.example # 新增:环境变量模板
├── .env.local # 新增:本地开发配置(.gitignore)
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
└── README.md
```
### 5.2 .env 配置模板
```env
# 环境:development / staging / production
APP_ENV=development
# 数据库
DATABASE_URL=mysql+pymysql://sms_user:SMS_pass@db-host:3306/sms
# 或开发环境
# DATABASE_URL=sqlite:///./sms.db
# 应用
APP_HOST=0.0.0.0
APP_PORT=8000
DEBUG=true
# JWT(生产必须配置)
SECRET_KEY=your-random-secret-key-here
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=1440
# CORS(生产填写具体域名)
ALLOWED_ORIGINS=https://sms.example.com,https://admin.sms.example.com
# 日志
LOG_LEVEL=INFO
LOG_FILE=/var/log/sms/app.log
```
### 5.3 Docker Compose 生产版
```yaml
version: "3.9"
services:
app:
build: .
environment:
- DATABASE_URL=${DATABASE_URL}
- SECRET_KEY=${SECRET_KEY}
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS}
volumes:
- ./logs:/app/logs
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: sms
MYSQL_USER: sms_user
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- sms_mysql_data:/var/lib/mysql
- ./seed/data.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "127.0.0.1:3306:3306" # 仅本地可访问
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "sms_user", "-p${MYSQL_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
sms_mysql_data:
driver: local
```
---
## 六、接口变更清单(V2.0)
| 接口 | 变更类型 | 变更内容 |
|------|----------|----------|
| `POST /api/auth/login` | **新增** | 用户名密码登录,返回 JWT |
| `POST /api/auth/refresh` | **新增** | 刷新 Token |
| `POST /api/auth/logout` | **新增** | 注销(黑名单 Token) |
| `GET /api/teachers/` | **修改** | 需要 Token,返回加上 `total` 字段 |
| `GET /api/students/` | **修改** | 需要 Token,分页参数加 `ge/le` 校验 |
| `GET /api/stats/classes` | **修改** | 修复 N+1 查询,性能优化 |
| `GET /health` | **新增** | 健康检查端点 |
---
## 七、总结
| 维度 | 当前状态 | V2.0 目标 |
|------|----------|-----------|
| 安全性 | 无认证,CORS 全开放 | JWT 认证 + RBAC + 严格 CORS |
| 数据一致性 | 软删除字段不完整 | 全量统一审计字段 |
| 性能 | 统计接口 N+1 查询 | 单次聚合查询 |
| 可维护性 | 单体结构,无测试 | 分层架构 + 单元测试覆盖 |
| 可观测性 | 无日志无监控 | loguru 日志 + health endpoint |
| 部署 | 手动启动 | Docker + .env 配置化管理 |