# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 沃林学生管理系统(FastAPI + SQLAlchemy + MySQL),课程小组作业,按模块分工:学生/班级/成绩/老师/顾问/就业/统计。注释、commit、接口 summary 全部是中文,改动时保持中文注释风格。 ## 运行 ```powershell 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 里即可,重启自动重建,不用写迁移脚本。 ### 常用验证方式 没有测试套件,实践中靠路由级导入 + 起服务打接口: ```powershell 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`,所以没有用它挂载静态目录): ```powershell 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、无打包,全部是普通 `