设备申请模块 - 出错点和解决方案
设备申请模块 - 出错点和解决方案
一、环境搭建与启动问题
1.1 MySQL 数据库不存在
问题原因:MySQL 服务已启动,但 labsystem 数据库尚未创建。
解决方法:
1 | /usr/local/mysql/bin/mysql -u root -p5222039@Lst -e "CREATE DATABASE IF NOT EXISTS labsystem DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;" |
1.2 后端启动命令
激活虚拟环境后运行
1 | cd /Users/ergou/code/LabManagementSystem |
二、Pydantic 模型问题
2.1 默认值问题:default=None vs default=””
核心原则:编辑接口的字段要用 default=None,而非 default="" 或 default=0
| 写法 | 含义 | 适用场景 |
|---|---|---|
name: str = Field(..., min_length=1) |
必填字段,不传报错 | 创建接口的必填项 |
name: str = Field(default="") |
可选,默认空字符串 | 创建接口的可选项 |
name: Optional[str] = None |
可选,默认 None | 编辑接口的所有字段 |
为什么编辑接口要用 default=None?
编辑时需要区分「用户传了空字符串」和「用户没传」:
default="":无法区分,前端不传时默认为空字符串,会覆盖原有值default=None:可以判断if body.name is not None,只更新传了的字段
2.2 类型声明:Optional[int] vs int
错误写法:
1 | totalQuantity: int = Field(default=None) # ❌ 类型是 int,默认值是 None |
正确写法:
1 | totalQuantity: Optional[int] = Field(default=None) # ✅ 明确允许 None |
问题原因:虽然写了 default=None,但类型声明是 int,Pydantic 会把”没传”的字段当作需要验证的 int 类型,可能产生隐式转换。
三、SQLAlchemy 查询问题
3.1 查询对象创建
错误写法:
1 | query = db.query # ❌ 缺少括号和模型 |
正确写法:
1 | query = db.query(Device) # ✅ 创建针对 Device 表的查询对象 |
3.2 链式调用顺序
正确顺序:
1 | query = db.query(Device) # 1. 创建查询 |
3.3 重复校验(编辑时排除自身)
创建时的校验(不排除):
1 | exist = db.query(Device).filter(Device.serial_number == new_serial).first() |
编辑时的校验(排除自身):
1 | exist = db.query(Device).filter( |
四、语法错误
4.1 HTTPException 分行
错误写法:
1 | raise |
正确写法:
1 | raise HTTPException(status_code=404, detail="未找到该设备") # ✅ 一行 |
4.2 字符串格式化
错误写法:
1 | like = "%{q}%" # ❌ 普通字符串,{q} 不会被替换 |
正确写法:
1 | like = f"%{q}%" # ✅ f-string 格式化 |
4.3 字典键缺少引号
错误写法:
1 | return { |
正确写法:
1 | return { |
五、逻辑错误
5.1 可用数量为 0 时的误判
错误写法:
1 | if not avail or avail is None: # ❌ 当 avail=0 时,not avail 为 True |
问题场景:用户明确传 availableQuantity: 0(表示无库存),会被错误地改为 totalQuantity。
正确写法:
1 | if avail is None: # ✅ 只判断是否为 None |
5.2 db.commit() 和 db.refresh() 顺序
错误顺序:
1 | db.refresh(row) # ❌ 先刷新,再提交 |
正确顺序:
1 | db.commit() # ✅ 先提交到数据库 |
六、参数校验
6.1 Query 参数枚举限制
错误写法:
1 | scope: str = Query("public"|"admin", default="public") # ❌ 字符串不能用位或 |
正确写法(使用 regex):
1 | scope: str = Query("public", regex="^(public|admin)$") |
七、数据库模型字段
7.1 字段命名:驼峰 vs 下划线
| 前端 JSON(驼峰) | 数据库字段(下划线) |
|---|---|
serialNumber |
serial_number |
totalQuantity |
total_quantity |
availableQuantity |
available_quantity |
className |
class_name |
八、状态映射逻辑
| 数据库 status 值 | 返回的 status_text | 含义 |
|---|---|---|
| 0 + available_quantity > 0 | "available" |
可借 |
| 0 + available_quantity <= 0 | "unavailable" |
无库存 |
| 1 | "maintenance" |
维修中 |
| 2 | "retired" |
已报废 |
九、接口汇总
| 接口 | 方法 | 功能 | 参数 | 返回 |
|---|---|---|---|---|
/api/devices |
GET | 设备列表 | category, q, page, page_size, scope | {data, pagination} |
/api/devices/{id} |
GET | 设备详情 | device_id | device 对象 |
/api/devices |
POST | 创建设备 | DeviceCreateBody | {ok, id} |
/api/devices/{id} |
PUT | 更新设备 | device_id, DeviceUpdateBody | {ok, data} |
十、注意事项
- 编辑接口的字段校验:只校验传了的字段(非 None 的),未传的保持原值
- 数量校验:
availableQuantity不能超过totalQuantity - 编号唯一:创建时检查所有设备,编辑时排除自身
- 状态值范围:
category03,2status0 - 软下架:不使用 DELETE,改为
PUT修改status=2或availableQuantity=0 - 用户端过滤:
scope=public只显示status=0的设备
文档生成时间:2026年7月15日
十一、测试过程中遇到的问题
11.1 Query 参数类型不匹配
错误信息:Input should be a valid integer, unable to parse string as an integer
错误信息:Input should be a valid date or datetime, input is too short
问题原因:Query 参数类型定义错误:
1 | # ❌ 错误 |
解决方法:
1 | # ✅ 正确 |
11.2 SQLAlchemy 过滤条件缺少模型前缀
错误信息:NameError: name “start_date” is not defined
问题原因:SQLAlchemy 查询必须使用 Model.field 的形式:
1 | # ❌ 错误 |
11.3 日期字符串缺少转换
错误信息:TypeError: can only compare datetime.date to datetime.date
问题原因:Query 参数传入的是字符串,需要转为 date 对象才能和数据库字段比较:
1 | # ❌ 错误 |
11.4 日期格式错误缺少异常处理
错误信息:ValueError: time data “2026/07/17” does not match format “%Y-%m-%d”
问题原因:用户传入错误的日期格式(如用斜杠),没有捕获异常:
1 | # ❌ 错误 |
11.5 参数名拼写错误
错误信息:NameError: name “data_from” is not defined
问题原因:参数名和变量名不一致:
1 | # ❌ 错误 |
11.6 测试验证流程
以下是完整的测试验证流程,所有步骤均已通过:
- 创建设备:POST /api/devices → 返回 ok, id
- 管理端查看:GET /api/devices?scope=admin → 显示所有设备
- 用户端查看:GET /api/devices → 只显示 status=0 的设备
- 类别筛选:GET /api/devices?category=0 → 只显示对应类别
- 创建预约:POST /api/device-bookings → 返回 ok, id, status=0
- 审核通过:PATCH /review {action:”approve”} → status=1
- 审核拒绝:PATCH /review {action:”reject”, rejectReason:”…”} → status=2
- 取消申请:PATCH /cancel → status=3
- 库存不足:设 availableQuantity=0,新申请返回 400
- 设备报废:PUT status=2,用户端不再显示该设备
11.7 测试结果
所有测试用例通过!
设备 CRUD、预约流程、审核流程、库存校验、软下架功能均正常工作。
十二、前后端联调遇到的问题
12.1 负责教师从下拉选择改为输入框
需求变更:负责教师字段从预设选项改为自由输入
修改文件:
frontend/src/components/EquipmentApplyModal.vue:删除teacherOptions数组,下拉框改输入框,form.teacherId→form.teacherNamebackend/app/api/routes/device_booking.py:删除teacherId字段,teacherName改为必填
12.2 学号、姓名、专业、班级改为必填项
需求变更:四个字段从可选改为必填
前端修改:
- 标签添加红色星号
* - 输入框添加错误状态样式
errors对象新增四个字段validate()函数添加空值校验
后端修改:
DeviceBookingCreateBody中四个字段从Field(default="")改为Field(..., description="xxx")
12.3 申请人姓名显示为破折号
问题现象:设备借用详情弹窗中”申请人”显示为 —
问题原因:前后端字段名不匹配
解决方法:修改后端返回字段名为 user_name
12.4 拒绝原因不显示
问题现象:申请被拒绝后,详情弹窗中不显示拒绝原因
问题原因:前端传递详情数据时缺少 reject_reason 字段
解决方法:
- 在
openEquipmentDetail()中添加rejectReason: item.reject_reason || - 在详情组件中添加拒绝原因显示区块,条件显示(状态为 rejected 且有拒绝原因)
12.5 筛选器选择后不自动搜索
问题现象:管理端页面选择分类/状态后,列表没有刷新,仍显示全部数据
问题原因:<select> 和 <input type="date"> 缺少 @change 事件
解决方法:为筛选控件添加 @change="search" 事件
涉及页面:
admin/src/views/AdminDeviceBookingsView.vue:状态选择框、日期选择框admin/src/views/AdminDevicesView.vue:分类选择框
12.6 经验总结
注意事项:
- 前后端字段名必须严格一致,否则数据无法正确传递
- 修改前端表单时,后端数据模型也要同步更新
- 下拉框和日期选择器需要添加
@change事件才会触发搜索 - 详情弹窗需要传递完整的数据字段,避免遗漏关键信息
12.7 录入设备500错误测试
问题现象:录入设备时,当 totalQuantity 传入超大数值(如 999999999999)会报 500 Internal Server Error
问题原因:
- 数据库
total_quantity字段类型是Integer - MySQL Integer 最大值是
2147483647(约 21 亿) - 传入
999999999999(1 万亿)超出范围,导致数据库写入失败
测试结果:
| 测试场景 | 状态码 | 结果 |
|---|---|---|
| 正常录入 | 200 | ✅ 通过 |
| 缺少必填字段 | 422 | ✅ 返回字段缺失错误 |
| 空名称 | 422 | ✅ 返回长度不足错误 |
| 重复编号 | 400 | ✅ 返回编号已存在 |
| category超出范围 | 422 | ✅ 返回范围错误 |
| status超出范围 | 422 | ✅ 返回范围错误 |
| 超大totalQuantity(修复前) | 500 | ❌ 数据库溢出 |
| 超大totalQuantity(修复后) | 422 | ✅ 返回范围错误 |
| availableQuantity超过totalQuantity | 400 | ✅ 返回数量校验错误 |
| availableQuantity负数 | 422 | ✅ 返回范围错误 |
| 特殊字符名称 | 200 | ✅ 通过(SQLAlchemy自动转义) |
| 空请求体 | 422 | ✅ 返回字段缺失错误 |
| 无效JSON | 422 | ✅ 返回JSON解析错误 |
修复方法:
经验总结:
- 数据库字段类型有限制,前端/API 需要做相应的校验
- Integer 类型最大值约 21 亿,实际业务中设备数量不可能这么大,设为 10000 足够
- 字符串字段要限制长度,避免数据库字段截断或报错
- 500 错误是服务器内部错误,应该尽量避免,转为 4xx 客户端错误
(注:部分内容由豆包工作 AI 生成)