南沙项目物联网 - 月卡管理模块 Bug 排查记录
南沙项目物联网的月卡管理模块在日常使用中陆续发现了三个 Bug,涵盖前端接口调用错误、后端空指针异常、以及业务逻辑校验缺陷三类典型问题。本文记录这三个 Bug 的完整排查过程、根因分析及修复方案,供后续开发参考。
前言
月卡管理是停车系统的核心功能之一,涉及费用计算、车位绑定、审批流程等多个子模块。本次排查的三个 Bug 分别来自:
| 序号 | 问题 | 类型 | 影响 |
|---|---|---|---|
| 1 | 个人月卡拒绝功能 404 错误 | 前端接口路径不匹配 | 拒绝功能完全不可用 |
| 2 | 月卡变更审批拒绝 404 错误 | 后端空指针异常 | 拒绝操作触发 404 |
| 3 | 月卡变更车位占用校验失效 | 业务逻辑错误 | 空闲车位被误报占用 |
这三个问题虽然都表现为”404 错误”,但根因完全不同,体现了停车场系统中常见的几类典型缺陷。
问题一:个人月卡拒绝功能 404
问题现象
费用管理模块 → 个人月卡页面,点击”拒绝退款”或”拒绝变更”按钮时,系统报错 Request failed with status code 404。
排查过程
Step 1:定位后端接口
通过代码搜索找到后端 Controller:
1 | 文件: src/main/java/cn/flyrise/pai/nsrx/iva/controller/MonthPersonalController.java |
后端实际提供的接口是:
POST /v1/monthpersonal/changeRefuse— 拒绝变更(第 296-301 行)POST /v1/monthpersonal/refundRefuse— 拒绝退款(第 307-312 行)
Step 2:查看前端代码
前端 api/iva/monthPersonal.js 中的 API 定义如下:
1 | // 前端实际调用 |
Step 3:根因分析
| 功能 | 前端调用 | 后端实际接口 | 状态 |
|---|---|---|---|
| 拒绝变更 | /updateFlow |
/changeRefuse |
❌ 不匹配 |
| 拒绝退款 | /updateFlow |
/refundRefuse |
❌ 不匹配 |
前端调用了 /updateFlow,但后端根本没有这个接口。Spring Boot 在找不到对应路由时返回 404。

图1:前端调用 /updateFlow 但后端接口不匹配,导致 404 错误
修复方案
Step 1:在 monthPersonal.js 中添加两个新的 API 方法:
1 | // 拒绝变更 |
Step 2:修改 Vue 页面中的调用方法,将 updateFlow 替换为对应的拒绝接口:
1 | // 拒绝退款 |
经验总结
- 前后端接口命名需保持一致,建议通过 Swagger 文档同步接口
- 代码审查时检查接口匹配性
- 测试覆盖完整的业务流程(通过/拒绝两种路径)
问题二:月卡变更审批拒绝 404
问题现象
费用管理模块 → 月卡变更列表页面 → 点击”审核” → 选择”拒绝”时,系统报错 404。但选择”通过”时正常。
排查过程
Step 1:定位后端接口
Controller 路径为 src/main/java/cn/flyrise/pai/nsrx/iva/controller/MonthPersonListController.java,审核接口为 POST /v1/monthpersonlist/audit。
Step 2:分析 audit 方法
1 |
|
Step 3:根因分析
当审核状态为”拒绝”(state="4")时,代码通过 monthlyCardId 查询 MonthPersonalEntity,但未进行空值检查。
- 如果
monthlyCardId对应的月卡记录不存在,getById()返回null - 后续
detail.setState("11")抛出NullPointerException - BladeX 框架的全局异常处理器将 NPE 映射为 HTTP 404
为什么”通过”正常?
“通过”时 state="3",不进入 if("4".equals(...)) 分支,因此不会触发 NPE。

图2:state=4 时 getById 返回 null,detail.setState 触发 NPE,框架转换为 404
流程对比
1 | graph TD |
修复方案
在 getById 之后添加空值检查:
1 | if("4".equals(monthPersonList.getState())){ |
经验总结
- 数据库查询后必须进行空值检查
- 全局异常处理器会将未捕获的 RuntimeException 映射为特定 HTTP 状态码
- 同类代码对比:
MonthPersonalBiz.changeRefuse()做了空值检查,而MonthPersonListController.audit()遗漏了这个检查
问题三:月卡变更车位占用校验 Bug
问题现象
个人月卡变更固定车位时,从空闲车位列表中选择了一个空闲车位,但提交变更时系统返回”车位已经被占用”,无法提交申请。
排查过程
Step 1:定位报错位置
搜索后端”已占用”相关错误信息,找到 3 处:
| 文件 | 行号 | 错误信息 | 场景 |
|---|---|---|---|
UserMonthCompanyController.java |
185 | “车位XX已被占用” | 企业租赁修改 |
MouthPersonalBiz.java |
175 | “车位已占用” | 个人月卡申请 |
MouthPersonalBiz.java |
605 | “车位已经被占用” | 个人月卡变更 ← 本次问题 |
Step 2:分析 change() 方法
MouthPersonalBiz.java 第 594-641 行:
1 | public R change(MouthPersonalRefundApplyDTO dto) { |
Step 3:追踪车位状态
月卡申请成功后,车位会被设为占用状态(usingState = "4"):
1 | if(monthPersonal.getParkingSpotId()!=null){ |
根因分析
这个 Bug 包含 3 个子问题:
Bug 1:检查的是老车位而非新车位
change() 方法获取的是当前已绑定的老车位 ID。老车位在月卡申请时已被设为占用(usingState = "4"),所以 !spot.getUsingState().equals("1") 永远为 true。
Bug 2:DTO 缺少新车位 ID 字段
MouthPersonalRefundApplyDTO 只包含退款相关字段,没有接收新车位 ID 的字段。前端传入的新车位 ID 无法到达后端。
Bug 3:新车位用老 ID 查询
1 | ParkSlotEntity newSpot = parkSlotService.getById(monthPersonal.getParkingSpotId()); |

图3:检查老车位(错误 - 已被月卡绑定)vs 检查新车位(正确 - 应该检查目标车位是否空闲)
修复方案
Step 1:DTO 增加 newParkingSpotId 字段
1 |
|
Step 2:重写 change() 车位检查逻辑
1 | // 变更车位 |
Step 3:MonthPersonListEntity 增加 afterValueId 字段保存新车位 ID
Step 4:changePass() 审批通过时切换车位绑定
1 | // 释放老车位 |
经验总结
- 车位状态检查必须区分”当前占用车位”和”要变更到的新车位”
- DTO 设计需覆盖业务操作所需的全部参数
- 变更操作需要考虑完整的状态流转:提交 → 审批通过/拒绝
总结
本次排查的三个 Bug 覆盖了不同的根因类型:
| Bug | 类型 | 根因 |
|---|---|---|
| 个人月卡拒绝 404 | 前端 | 接口路径不匹配 |
| 月卡变更审批拒绝 404 | 后端 | 空指针异常(NPE) |
| 车位占用校验失效 | 业务逻辑 | 检查对象错误(老车位 vs 新车位) |
通用经验
- 前后端接口一致性:接口命名、参数结构必须保持一致,建议通过 Swagger 文档同步
- 空值检查:数据库查询后必须进行空值检查,避免 NPE
- 业务逻辑验证:校验逻辑必须验证正确的对象,不能混淆”当前状态”和”目标状态”
- 测试覆盖:必须覆盖完整的业务流程,包括”通过”和”拒绝”两种路径
💡 提示:月卡管理模块的问题往往涉及状态流转,建议在开发变更功能时同步梳理状态机,避免遗漏边界情况。