人员与人脸同步一致性修复设计
1. 目标
修复 AIDishRecognition 当前人员主档库、人脸 SQLite 库和百度 SDK 内存索引之间的明显一致性问题,使启动全量、启动增量、定时增量、心跳补偿和 MQTT 实时消息遵守同一套同步语义。
本次设计确认以下协议约定:
| 值 | 同步类型 |
|---|---|
0 |
增量同步 |
1 |
全量同步 |
| 人员状态 | 处理语义 |
|---|---|
1 |
新增;本地已存在时按最新数据替换 |
2 |
更新;本地不存在时允许按新增落库 |
3 |
删除;本地不存在时按幂等成功处理 |
2. 当前问题
2.1 增量同步清空人脸库
FaceDataUpdaters.performUpdate("0") 在收到非空响应后无条件调用 FaceApi.userClean()。如果服务端返回的是变更集,未变化人员会被删除,只剩本次变更人员。
2.2 数据库成功与同步成功混为一谈
当前人脸批处理不统计单条失败,最终始终回调成功。人员批处理虽然统计失败数,也仍然调用成功回调。启动页和 WorkManager 因此可能在数据不完整时记录“同步完成”并继续后续流程。
2.3 更新操作先删除旧数据
人脸新增或更新先删除旧记录,再解码新特征并插入。新数据为空、Base64 非法或数据库插入失败时,旧的可用数据已经丢失。
2.4 SDK 内存索引未跟随删除
FaceApi.userDelete() 只维护 SQLite 和 FaceApi.users。FaceSDKManager.initPush() 只推送当前人员,不删除 SDK 中已不存在的 ID,旧特征仍可能进入 Top 1 搜索结果。
2.5 人员主档更新和删除不完整
人员主档按 staff_uuid 查旧记录,却按新记录的 id 删除;当服务端 ID 变化时会留下重复数据。Customer.status 只存储不执行删除,全量同步也不清理服务端已不存在的本地人员,刷卡查询又不检查状态。
2.6 启动阶段重复写人脸库
/p/api/userdata 批处理在 FaceApi 初始化前调用 CustomerSyncManager.syncFace(),随后 /p/api/facedata 又重新同步。前一次写入可能失败或污染内存列表,且职责与人脸接口重复。
2.7 MQTT 和后台任务误报完成
MQTT 人员消息未检查保存结果就反馈成功。定时任务和心跳任务忽略 CountDownLatch.await() 的超时返回值,未收到回调时可能仍返回成功。
2.8 多入口并发写人脸库
启动页、定时任务、心跳和 MQTT 使用不同线程写同一数据库及 SDK 索引,缺少统一写入边界,可能产生交错删除、重复替换和刷新顺序不确定。
3. 设计原则
- 增量不清库:
type=0只处理响应中的变更记录。 - 全量原子替换:
type=1必须先完整校验,再在数据库事务中替换全量数据;失败时保留旧库。 - 先验证后写入:所有新增和更新必须先验证 UUID、服务端 ID、状态和人脸特征,再修改旧数据。
- 单一写入口:HTTP 与 MQTT 的人脸落库都进入同一个串行协调器。
- 三层一致:每次变更同时明确人员主档、FaceApi SQLite 和 SDK 内存索引的结果。
- 真实失败:超时、单条失败、数据库失败和 SDK 刷新失败不得被报告为成功。
- 幂等:重复新增、重复更新和重复删除不会产生重复记录或无意义失败。
- 控制范围:不修改服务端接口路径、请求字段、人脸算法阈值、支付协议或 UI。
4. 组件设计
4.1 FaceSyncPolicy
新增无 Android 依赖的纯 Java 策略类,集中定义:
TYPE_INCREMENTAL = "0";TYPE_FULL = "1";STATUS_ADD = "1";STATUS_UPDATE = "2";STATUS_DELETE = "3";- 同步类型和人员状态校验;
- 空响应判定:增量空列表为成功且变更数为 0;全量空列表表示有效的“全量无人”快照,但只有接口业务码成功且数据对象存在时才允许清空本地人脸库;缺失数据对象仍为失败;
- 是否需要完整人脸特征的判断。
该类只解释协议,不访问数据库、网络或 SDK,使用 JVM 单元测试覆盖。
4.2 FaceSyncResult
新增不可变结果对象,记录:
- 请求总数;
- 成功数;
- 失败数;
- 新增、更新、删除、跳过数量;
- 首个明确错误原因;
- 是否为完整成功。
FaceDataUpdaters 不再用输入列表长度冒充成功数量。只有所有必要操作成功时才调用 updateSuccess;失败时调用 updateFail 并保留详细日志。
现有 FaceDataUpdateCallback 暂不修改接口签名,避免扩大页面改造范围;详细计数通过日志和内部结果对象保留,成功回调的 count 表示真实成功数量。
4.3 FaceSyncCoordinator
新增进程级人脸写入协调器,持有单线程 ExecutorService,职责是:
- 串行执行 HTTP 全量或增量结果落库;
- 串行执行 MQTT 单条人脸变更;
- 保证一次落库及对应 SDK 索引更新不可与另一条写任务交错;
- 捕获任务异常并回调明确失败;
- 不负责发起网络请求,不持有 Activity。
网络请求可以并发返回,但所有本地写操作按照进入协调器的先后顺序执行。该设计先解决数据库和 SDK 的物理并发写问题,不假设服务端消息含有可比较的版本号。后续如果协议提供版本号,再增加过期响应丢弃能力。
4.4 FaceApi 数据库原子操作
在 DBManager 和 FaceApi 中补充两个清晰操作:
replaceUser(User user):在单个 SQLite 事务内按user_id删除旧行并插入新行;插入失败时事务回滚,旧数据保留。replaceAllUsers(List<User> users):在单个 SQLite 事务内清理并写入完整快照;任一插入失败时整体回滚。
事务成功后重新从数据库加载 FaceApi.users,不再通过用户名查询后向列表添加元素。这样同时修复:
- 数据库插入失败仍向内存列表加入
null; - 同名人员可能查到错误对象;
- 手工维护列表与数据库不一致。
删除操作返回真实受影响行数语义;删除不存在的数据按业务层幂等成功处理,但不伪造“实际删除一条”。
4.5 SDK 内存索引一致性
FaceSDKManager 增加有职责注释的索引维护接口:
- 单条替换:删除旧数据库 ID 对应的索引,再推送新 ID 和特征;
- 单条删除:删除旧数据库 ID 对应的索引;
- 全量替换:先删除同步前快照中的全部旧 ID,再推送事务提交后的全部新记录。
只有人脸模型和 FaceSearch 已初始化时执行即时索引操作;未初始化时数据库仍可落库,后续 SDK 初始化通过现有加载流程重建索引。
4.6 FaceDataUpdaters 流程
增量 type=0
- 接口业务成功、数据对象存在且列表为空:返回成功,数量 0,不清库。
- 逐条校验状态和必要字段。
- 删除状态只要求有效 UUID;找到旧记录时同时删除 SQLite 和 SDK 索引,不存在时按幂等成功。
- 新增或更新必须先完成 Base64 解码和
User构建,再执行事务替换。 - 每条结果计入
FaceSyncResult。 - 任一失败时整体回调失败,但已经成功提交的独立增量记录不回滚。
全量 type=1
- 在写数据库前校验并构建整个有效快照。
- 任一必要记录无效时拒绝替换,保留旧人脸库。
- 全部校验通过后调用
replaceAllUsers()原子替换 SQLite。 - 数据库提交成功后按同步前、同步后 ID 集合重建 SDK 索引。
- 业务成功且明确返回空数组时,按“全量无人”处理;数据对象或数组字段缺失时不清库。
4.7 人员主档同步
CustomerDatabaseManager 统一按 staff_uuid 处理:
- 新增和更新:事务内删除同 UUID 的所有旧行,再插入新行;
- 删除:按
staff_uuid删除,重复删除按幂等成功; - 不再按新数据的
id删除旧记录。
BatchCustomerSyncManager 接收同步类型:
- 增量:逐条应用
status=1/2/3; - 全量:先校验全部人员,再在事务中替换完整人员快照;空快照允许清空人员库;
- 只有失败数为 0 才回调成功;部分失败必须进入失败回调并记录成功、失败计数。
/p/api/userdata 只维护人员与卡号库,不再调用 CustomerSyncManager.syncFace()。人脸数据只由 /p/api/facedata 和 MQTT 人脸消息维护。
刷卡本地查询只返回有效人员。由于删除状态会直接删除本地行,正常情况下不会读到 status=3;查询层仍增加有效状态保护,兼容升级前遗留数据。
4.8 MQTT 消息确认
type=1人脸消息提交给FaceSyncCoordinator,数据库和 SDK 更新成功后才调用反馈接口。type=4人员消息按状态保存或删除,确认本地操作成功且消息 ID 非空后才反馈。- 解析失败、字段无效、本地写失败或消息 ID 缺失时不反馈成功,并写入明确日志。
- MQTT 不直接调用同步类的内部同步方法。
4.9 WorkManager 与心跳
- 等待前将结果初始化为失败状态。
- 检查
await(timeout, unit)返回值;超时返回Result.retry()。 - 回调到达后按照真实同步结果返回成功或重试。
- 未启用人脸识别时,不执行人脸定时同步与心跳触发的人脸更新。
- 保持现有 120 秒人脸检查和 30 秒心跳间隔,不在本次调整调度周期。
5. 数据流
5.1 启动流程
LaunchActivity
→ /p/api/userdata(type=0/1)
→ CustomerDatabaseManager 增量应用或全量替换
→ 人员主档完整成功
→ 若未启用人脸:标记初始化完成并进入主页
→ 若启用人脸:激活并初始化 Face SDK/FaceApi
→ /p/api/facedata(type=0/1)
→ FaceSyncCoordinator
→ FaceApi SQLite 事务
→ FaceSDK 内存索引
→ 全部成功后标记初始化完成
5.2 运行期流程
WorkManager / 心跳
→ /p/api/facedata(type=0)
→ FaceSyncCoordinator
MQTT type=1
→ FaceSyncCoordinator
→ 成功后反馈
MQTT type=4
→ CustomerDatabaseManager
→ 成功后反馈
6. 错误处理
- 网络失败:不修改本地数据,调用失败回调。
- 数据对象缺失:不清库,调用失败回调。
- 增量空列表:成功,数量 0。
- 全量明确空快照:事务清空对应本地库并同步清理 SDK 旧索引。
- 非法状态、空 UUID、非法 ID、空或非法人脸特征:该条失败;全量拒绝整个快照,增量继续统计其他记录。
- SQLite 事务失败:回滚并保留旧数据。
- SDK 未初始化:数据库结果保留,跳过即时索引更新,等待 SDK 初始化统一加载。
- SDK 已初始化但索引更新失败:同步结果记为失败并记录错误,不伪报完成;数据库保持已提交状态,下一次同步或重建索引可恢复。
- Worker 超时:返回重试,不返回成功。
7. 测试设计
7.1 JVM 单元测试
新增纯逻辑测试,覆盖:
0识别为增量、1识别为全量,其他值无效;- 增量空列表为成功且不要求清库;
- 全量明确空快照允许替换为空;
- 状态
1/2/3的必要字段规则; - Base64 非法时在删除旧数据前失败;
- 部分失败结果不能判定为完整成功;
- Worker 等待超时判定为重试;
- 人员状态删除和有效状态判断;
- 全量 UUID 对账不会在删除列表时跳过相邻人员。
7.2 数据库与编译验证
Android SQLite 与百度 SDK 依赖不在普通 JVM 测试中模拟。通过以下方式验证集成边界:
- 为事务输入计划和结果聚合保持纯逻辑单测;
- 编译 Debug APK,验证 Android 和百度 SDK API 调用签名;
- 使用
git diff --check检查补丁格式; - 实机验收全量、增量、删除、MQTT 与刷卡场景。
执行命令:
./gradlew :app:testDebugUnitTest :app:assembleDebug
git diff --check
8. 实机验收标准
- 初次启动全量同步后,人员库和人脸库数量与服务端一致。
- 增量新增一个人员后,旧人员全部保留,新人员可刷卡或识别。
- 增量更新人脸后,新特征生效,旧特征不再命中。
- 增量删除人员后,本地刷卡查询失败、旧人脸不再进入识别 Top 1。
- 增量返回空列表时不弹失败、不重试、不改变本地人数。
- 非法特征更新不会删除旧的可用人脸。
- 全量中存在非法记录时整次拒绝替换,旧库完整保留。
- MQTT 本地保存失败时不反馈处理完成;成功时只反馈一次。
- 模拟网络无回调或超时,Worker 返回重试。
- 心跳、定时和 MQTT 同时到达时无崩溃、无重复人员、无数据库锁异常。
9. 影响范围
计划修改:
app/src/main/java/com/cpt/aidishrecognition/utils/FaceDataUpdaters.javaapp/src/main/java/com/cpt/aidishrecognition/manager/FaceSDKManager.javaapp/src/main/java/com/cpt/aidishrecognition/manager/CustomerSyncManager.javaapp/src/main/java/com/cpt/aidishrecognition/manager/BatchCustomerSyncManager.javaapp/src/main/java/com/cpt/aidishrecognition/dataabase/CustomerDatabaseManager.javaapp/src/main/java/com/cpt/aidishrecognition/dataabase/CustomerDBHelper.javaapp/src/main/java/com/cpt/aidishrecognition/mqtt/MqttSimple.javaapp/src/main/java/com/cpt/aidishrecognition/LaunchActivity.javaapp/src/main/java/com/cpt/aidishrecognition/workmanager/UpdateFaceWorker.javaapp/src/main/java/com/cpt/aidishrecognition/workmanager/HeartBeatWorker.javadatalibrary/src/main/java/com/example/datalibrary/api/FaceApi.javadatalibrary/src/main/java/com/example/datalibrary/db/DBManager.java- 新增同步策略、协调器、结果对象及对应单元测试。
明确不修改:
- 菜品学习、菜品识别和相机相关的现有未提交修改;
- 服务端接口路径和请求字段;
- 人脸算法模型、阈值和授权逻辑;
- 页面布局与视觉样式;
- Git 分支、暂存区、提交和远端状态。
10. 文档与交付
- 设计和实施计划保存在提示词工程
work_android/AIDishRecognition/docs/superpowers/下,并同步维护 Markdown 与 HTML。 - 实际项目文件发生变更后,在
work_android/AIDishRecognition/change_records/生成同 basename 的 Markdown 与 HTML 变更记录。 - 业务源码仓库不创建
docs/、doc/、prompt/或其他文档产物目录。 - 本次只编辑和验证工作区,不执行 Git 暂存、提交、推送、切换分支、合并或变基。