10 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
沃林学生管理系统(FastAPI + SQLAlchemy + MySQL),课程小组作业,按模块分工:学生/班级/成绩/老师/顾问/就业/统计。注释、commit、接口 summary 全部是中文,改动时保持中文注释风格。
运行
python main.py # uvicorn main:app,0.0.0.0:8001,reload=True
- 无 requirements.txt / 测试 / lint 配置。依赖:fastapi、uvicorn、sqlalchemy 2.x、pydantic 2.x、pymysql。
- 交互式文档:http://localhost:8001/docs
- 数据库连接硬编码在
database.py:10(root:123456@localhost:3306/wl_student_manager_system)。库需要事先手动建好,代码不建库。echo=True会打印全部 SQL。
启动即清库(最重要的一条)
main.py:10-11 在模块顶层执行 Base.metadata.drop_all(engine) → create_all()。因此:
- import main 就等于清空数据库。验证改动时不要
python -c "import main",只导入具体模块,例如python -c "from api import wl_teacher_api"。 reload=True的监听范围只有*.py(uvicorn 默认includes = ["*.py"])。改后端.py= 重载 + 丢数据;改static/下的.html/.css/.js不会触发重载,数据安全——录完数据后可以放心反复调前端。没有迁移和种子数据,数据只能重新调接口录。- 表结构改动(加字段/改类型)直接写在 model 里即可,重启自动重建,不用写迁移脚本。
常用验证方式
没有测试套件,实践中靠路由级导入 + 起服务打接口:
python -c "from api import wl_advisor_api; print([r.path for r in wl_advisor_api.router.routes])"
python -c "from dao.wl_statistics_dao import StatisticDao; print('ok')"
分层架构
api/ → dao/ → model/,DTO 独立在 scheme/(注意是 scheme 不是 schema)。命令式命名约定:wl_<领域>_{api,dao,model,scheme}.py。
- api/:只做路由声明、
Depends(get_db)注入、404/400 抛错。业务逻辑不写在这里。 - dao/:全部查询逻辑。函数第一个参数统一是
db: Session,自己db.commit()/db.refresh()。两种写法并存:wl_student_dao/wl_teacher_dao/wl_advisor_dao是模块级函数,wl_emp_dao/wl_statistics_dao是class XxxDAO+@staticmethod。改哪个文件就跟哪个文件的写法。 - model/:ORM 表定义。
Class_带下划线是因为Class名字冲突;表名与类名不对应(Class_→class_info,Emp→wl_emp)。 - scheme/:Pydantic 入参/出参。入参类名
XxxCreate/XxxUpdate,出参XxxOut或XxxResponse(不统一,看模块)。
路由前缀不统一(改接口时注意)
| 模块 | 文件 | prefix | 细节 |
|---|---|---|---|
| 学生 | api/wl_student_api.py |
/students |
路径是 "" 不带斜杠;按 stu_no 定位,不是 id |
| 班级 | api/wl_class_api.py |
/classes |
路径带尾斜杠 /classes/;按 class_id |
| 老师 | api/wl_teacher_api.py |
/teacher |
单数;/generate-no、/no/{t_no}、/{t_id} 三个查询入口 |
| 顾问 | api/wl_advisor_api.py |
/advisor |
单数;列表路由必须写在 /{adv_id} 之前,否则 "" 会被当成 adv_id |
| 就业 | api/wl_emp_api.py |
/emp |
按 stu_no 定位 |
| 成绩 | api/wl_score_api.py |
无 prefix | 路径 /score/add 等,对外按 stu_no+exam_order,参数走 query 而不是 JSON body;出参 ScoreOut(不含 stu_id) |
| 统计 | api/statistics.py |
/statistics |
只读聚合接口 |
跨模块约定
逻辑删除:除 wl_score 外每张表都有 is_deleted(0 正常 / 1 删除),所有查询都要带 filter(X.is_deleted == 0)。删除接口把标记置 1,不做物理删除。唯一例外是 dao/wl_score_dao.py:delete_score 用 delete() 语句真删。
级联保护:删除前检查引用并 raise HTTPException(400),且这个检查写在 DAO 层而不是 api 层(见 wl_class_dao.delete_class 查学生/老师、wl_advisor_dao.delete_advisor 查学生)。
学生身份键:stu_no(业务学号)是唯一的对外标识,所有 api 出入参都用它。stu_id 是 wl_student 的自增内部主键,只在学生模块自己的 SQL 里出现,成绩模块已经完全不用它了:
wl_score.stu_no既是主键也是外键,直接指向wl_student.stu_no(和wl_emp.stu_no一个套路)。这张表里没有stu_id列,Score模型里也没有这个字段。- 所以
/score/*查出来的 ORM 对象直接读stu_no就有值,不需要任何 setattr 挂载。ScoreOut只是把出参形状钉死。 dao/wl_score_dao.py里的get_student_by_id/get_student_by_name是遗留函数(查的还是Student.stu_id,全项目没人调用),别拿它们当stu_no版本的样板。
学号生成:wl_student_dao.generate_stu_no = 班级 start_time 的 %Y%m%d + 3 位序号(该班学生数 +1)。文件里留了一行 # date_str = "20260912" 的测试硬编码,演示/测试时会切换成注释状态,看到它别当成 bug 删掉。老师工号 generate_teacher_no = T + 年月日 + 3 位随机数(dao/wl_teacher_dao.py)。
DAO 往 ORM 对象上挂非字段属性:为了前端展示,查出来后会手动 setattr,比如 Emp.stu_name(wl_emp_dao)、Class_.head_teacher_name / course_teacher_names(wl_class_dao.get_classes)。对应出参 scheme 里声明了这些字段。这是有意为之的答辩点,不要"清理"掉。
就业记录复活:Emp.stu_no 是主键,软删的记录仍占着主键,所以 create_emp 先连已删除的一起查,命中就把 is_deleted 置 0 复用,否则会主键冲突。
前端(static/,免构建)
前后端分开起服务,靠 CORS 中间件放行(项目里没学过 StaticFiles,所以没有用它挂载静态目录):
python main.py # 后端 :8001
cd static; python -m http.server 8080 # 前端 :8080,另开一个窗口
两个入口都能开:http://localhost:8080/,或直接双击 static/index.html(file://,浏览器发的 Origin: null,allow_origins=["*"] 一样放行)。
main.py里是app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])。allow_origins=["*"]不能和allow_credentials=True同时用,浏览器会直接拒绝;本项目接口无登录态、不带 cookie,所以可以放心用*。js/api.js的API_BASE因此必须写后端的绝对地址http://127.0.0.1:8001,不能留空(留空就指向前端自己的 8080 了)。同理app.js里侧边栏的/docs链接也得写成API_BASE + '/docs'。
无 npm、无打包,全部是普通 <script>:vendor/(vue 3.5 全量版含模板编译器、element-plus 3.0.0、echarts 5.6)→ js/util.js → js/api.js → js/pages/*.js → js/app.js(最后,负责 createApp().mount())。加载顺序不能动。
- 顶层
const共享全局词法作用域:这些文件不是 ES Module(file://下import会被 CORS 拦),util.js里const { ref, computed, ... } = Vue的解构对后面所有文件可见,各页面直接用ref、onMounted。window.PAGES.push({key,title,sub,group,icon,component})注册页面,注册顺序即菜单顺序,dashboard必须第一个(外壳取pages[0]当默认页)。 js/api.js是唯一网络出口,页面里不出现fetch。四个接口怪癖都在这一层消化:尾斜杠写死(错则 307)、DELETE /classes/{id}删除失败也返回 200 只是 body 里code=404、DELETE /teacher|emp失败返回 200 +null、以及"响应不是 JSON 就报错"——路径写错现在拿到的是后端的 JSON 404(正常报错),这条分支主要是给API_BASE配错的情况兜底:请求会打到 8080 上,拿回http.server的 HTML 404,不拦就会在JSON.parse那儿抛个看不懂的语法错误。- 班级没有名字列,
className()/classLabel()统一显示成编号1 · 2026-09-01 开班。 - Element Plus 是 3.0:
el-radio(-button)的值用value而不是label,按钮的link取代了type="text",el-dialog用header插槽取代title插槽。 - 图表用
v-if挂载(不是v-show),隐藏容器里 ECharts 初始宽度为 0,切回来是空白图。.chart-box的高度写在css/app.css,删掉它所有图都会变成 0 高。
容易踩的坑
聚合查询的 ON vs WHERE:左外连接里,逻辑删除条件和"是否就业"的过滤必须写在 outerjoin(..., and_(...)) 的 ON 条件里;写进 .filter() 会退化成 INNER JOIN,把空班级/未就业学生整行滤掉。dao/wl_statistics_dao.py 里有两处注释专门说明(statistic_classes_count、get_class_avg_emp_time),照抄该模式。
统计口径:get_emp_time 用 coalesce(datediff(...), 0) 把未就业学生兜成 0;get_class_avg_emp_time 则保留 None 表示"该班无就业学生"(AVG 忽略 NULL,COUNT(表达式) 作分母)。性别取值在 wl_statistics_dao.py:15-16 的 MALE_VALUE / FEMALE_VALUE("男"/"女"),和库里实际存的值对齐时只改这两行——库里存了别的值(如 "M")时,/statistics/classes/count 的 total 会算上它,但 male_count/female_count 都是 0。EmpTopOut 里带了 salary(前端薪资 TopN 要画薪资,不是只排名次)。
model 导入顺序:create_all 依赖 metadata 里已注册全部表,main.py:8 的显式导入不能删。wl_emp_model 尤其危险——它原本只靠 statistics 路由传递导入,一旦统计路由被拿掉,wl_emp 表漏建,Student.emp 关系随即报错。
被注释掉的 relationship:Student.advisor、Advisor.students 是注释状态,因为 join 条件写错会让 SQLAlchemy 在映射配置阶段直接抛 ArgumentError。要恢复得先修好 primaryjoin / foreign_keys,别只取消注释。
Pydantic v1/v2 风格混用:老文件用 class Config: from_attributes = True,新文件用 model_config = ConfigDict(from_attributes=True)。聚合查询返回的是 SQLAlchemy Row 而不是 ORM 对象,这类出参必须开 from_attributes(见 scheme/wl_statistics_scheme.py)。更新写法用 model_dump(exclude_unset=True)(部分更新,前端没传的字段保持原值)或 exclude_none=True(交给数据库默认值)。