Files
1/CLAUDE.md
2026-09-14 10:23:36 +08:00

106 lines
10 KiB
Markdown
Raw Permalink 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.
# 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、无打包,全部是普通 `<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`(交给数据库默认值)。