同步架构文档与下一步优化计划

This commit is contained in:
geeker
2026-09-23 08:43:29 +08:00
parent 8bb8c2d7b8
commit 4c813d6575
7 changed files with 1887 additions and 1 deletions
+452
View File
@@ -0,0 +1,452 @@
# 学生管理系统 — 优化文档 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 配置化管理 |