首次
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# 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`(交给数据库默认值)。
|
||||
Reference in New Issue
Block a user