Files
1/CLAUDE.md
2026-09-21 20:16:09 +08:00

10 KiB
Raw Permalink Blame History

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(交给数据库默认值)。