黑名单审核权限回收修复实战:从时机错误到完整回收
在物联网门禁系统的黑名单审核流程中,我们发现了一个”审核未通过就删权”的严重 Bug。本文记录了从问题定位到完整修复的全过程,包括两次迭代:第一次解决”时机错误”,第二次解决”删权不完整”。
前言
在物联网门禁系统中,黑名单审核流程的核心逻辑应该是:
- 发起拉黑申请 → 仅进入待审核状态
- 审核通过 → 下发权限回收
- 审核拒绝 → 权限保持不变
然而在实际排查中发现,系统存在两个问题:
[!warning]
- 时机错误:拉黑申请阶段就提前下发了权限删除任务
- 删权不完整:审核通过后只回收了部分权限(卡、黑名单记录),漏掉了二维码、人脸和临时权限
本文详细记录了这两轮修复的完整过程。
问题背景
理想流程 vs 实际流程
| 阶段 | 理想流程 | 实际流程(修复前) |
|---|---|---|
| 拉黑申请 | 仅记录待审核状态 | ❌ 直接下发权限删除任务 |
| 审核通过 | 完整回收所有权限 | ⚠️ 只回收了部分权限 |
| 审核拒绝 | 保持权限不变 | ✓ 符合预期 |
业务影响
- 误删权限:未审核就删权,可能导致正常用户无法通行
- 数据不一致:审核通过后部分权限残留,黑名单用户仍可通过二维码/人脸进出
- 审核失败:历史脏数据导致空指针,审核接口返回
400 + msg:null

图1:Bug 根因分析 - 时机错误、空指针、删权不完整
第一轮修复:解决”时机错误”
问题定位
1. 拉黑申请阶段提前下发权限删除
排查 EmployeeInfoController.pullBlack() 发现:
1 | // 原代码逻辑(问题) |
问题分析:
- 定时任务只认”待下发记录”,不认”黑名单审核状态”
- 一旦创建下发任务,就会直接开始执行权限回收
- 这与”审核通过后才删权”的业务期望完全不符
2. 黑名单审核通过时空指针
接口 /v1/employeewhitelist/black-audit 返回 400 + msg:null,最终定位到:
1 | NullPointerException at DbServiceImpl.getSqlSegment(DbServiceImpl.java:225) |
根因:
blackAudit()最后会同步僵尸用户状态- 按
phone_no + company_id查询visitor_employee_zombie - 旧的黑名单记录里
companyId没有被写入 - 对
null做toString()导致空指针
修复方案
改动 1:移除申请阶段的权限下发
1 | // EmployeeInfoController.java |
改动 2:在审核通过时统一下发
1 | // EmployeeWhiteListServiceImpl.java |
改动 3:增强日志与容错
1 | // 新增多处日志,便于问题定位 |
第一轮验证
1 | mvn test -Dtest=BlacklistAuditTimingTest |
验证通过:
- ✓ 拉黑申请阶段不再调用
privilegeDeployService.deploy() - ✓ 新写入的黑名单记录会带上
companyId - ✓ 审核通过时才会创建
5 / 9 / 14任务 - ✓ 黑名单记录缺
companyId时,不再因为同步僵尸用户而抛异常
第二轮修复:解决”删权不完整”
问题发现
第一轮修复后,人工联调发现:
黑名单审核通过后,用户的门禁服务、梯控授权、设备授权状态看起来仍然正常。
排查发现:原实现只下发了部分回收动作:
| 操作类型 | 编码 | 是否下发 | 说明 |
|---|---|---|---|
| 注销卡 | 5 | ✓ | 已下发 |
| 黑名单 | 9 | ✓ | 已下发,带电梯 18 |
| 权限组删除门禁 | 14 | ✓ | 已下发 |
| 注销二维码 | 15 | ✗ | 遗漏 |
| 注销人脸 | 16 | ✗ | 遗漏 |
| 临时权限清理 | - | ✗ | 遗漏 |
影响:如果员工主要靠二维码或人脸通行,看起来就像”没删权限”。
根因分析
1. 下发语义不完整
原代码只处理了 5 / 9 / 14,没有处理 15 / 16,也没有清理 priviledge_temp 临时权限关系。
2. 纯删除设备请求被拦截
PrivilegeDeployServiceImpl.deploy() 有一个限制:
1 | // 原代码逻辑(问题) |
问题:黑名单审核后的删除门禁,语义应该是”只删不保留”。旧逻辑把”只有删除设备、没有保留设备”的请求拦掉了。
修复方案
改动 1:完整回收所有权限类型
1 | // EmployeeWhiteListServiceImpl.java |
改动 2:支持纯删除请求
1 | // PrivilegeDeployServiceImpl.java |
第二轮验证
1 | mvn -gs E:\maven363\pai-settings.xml -s E:\maven363\pai-settings.xml \ |
完整验证通过:
- ✓ 审核通过后完整回收卡、二维码、人脸、门禁、梯控
- ✓ 删除权限组关系和临时权限关系
- ✓ 纯删除设备任务能够落库(
is_delete=1) - ✓ 历史不完整黑名单数据不会导致审核崩溃
关键代码对比
修复前后流程对比
1 | 修复前: |

图2:修复前后流程对比 - 从”提前下发”到”审核后完整回收”
权限回收覆盖对比
| 权限类型 | 修复前 | 修复后 |
|---|---|---|
| 卡 | ✓ | ✓ |
| 二维码 | ✗ | ✓ |
| 人脸 | ✗ | ✓ |
| 黑名单记录 | ✓ | ✓ |
| 门禁删除 | ✓ | ✓ |
| 电梯注销 | ✓ | ✓ |
| 权限组关系 | ✓ | ✓ |
| 临时权限关系 | ✗ | ✓ |

图3:权限维度覆盖对比 - 修复后完整覆盖 8 个维度
经验总结
1. 时机把控是权限流程的核心
权限操作必须严格对齐业务状态流转,任何”提前”或”延迟”都可能导致严重的安全隐患。
2. 完整的权限维度清单
在设计权限回收功能时,必须有一份完整的”权限维度清单”:
1 | 物理凭证:卡、二维码、人脸 |
3. 历史数据兼容性设计
1 | // 好做法:对可能为空的字段做防御 |
4. 测试覆盖要全面
这次问题本质是”时机错误 + 历史脏数据兼容不足”,测试需要覆盖:
- 正常路径:新数据完整流程
- 边界条件:历史脏数据、缺失字段
- 异常输入:空指针、非法状态
5. 日志是排查问题的关键
1 | // 关键位置必须打日志 |
推荐后续动作
人工再验证一次完整业务流
- 发起拉黑申请
- 确认申请阶段不产生权限删除下发
- 审核通过后再确认产生
5 / 15 / 16 / 9 / 14 / 18
抽样检查历史黑名单数据
- 关注
visit_white_list.type = 2的记录里company_id是否存在为空的情况
- 关注
数据治理(如需要)
- 如果业务强依赖
visitor_employee_zombie的黑名单状态,建议增加一次性数据修复脚本,回填历史黑名单记录的company_id
- 如果业务强依赖
结语
本次修复经历了两轮迭代:
- 第一轮:解决了”时机错误”,确保权限回收只在审核通过后执行,同时兼容了历史脏数据
- 第二轮:解决了”删权不完整”,扩展了权限回收覆盖面,从”卡 + 黑名单 + 门禁”升级到”卡 + 二维码 + 人脸 + 临时权限 + 门禁 + 梯控”的完整回收
两次修复都遵循”最小影响面”原则,只修改黑名单审核这条链路,不动全局的停用、离职、注销等其他流程。这种精准打击的策略,既能快速修复问题,又能控制回归风险。