人员与人脸同步一致性修复设计

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. 设计原则

  1. 增量不清库:type=0 只处理响应中的变更记录。
  2. 全量原子替换:type=1 必须先完整校验,再在数据库事务中替换全量数据;失败时保留旧库。
  3. 先验证后写入:所有新增和更新必须先验证 UUID、服务端 ID、状态和人脸特征,再修改旧数据。
  4. 单一写入口:HTTP 与 MQTT 的人脸落库都进入同一个串行协调器。
  5. 三层一致:每次变更同时明确人员主档、FaceApi SQLite 和 SDK 内存索引的结果。
  6. 真实失败:超时、单条失败、数据库失败和 SDK 刷新失败不得被报告为成功。
  7. 幂等:重复新增、重复更新和重复删除不会产生重复记录或无意义失败。
  8. 控制范围:不修改服务端接口路径、请求字段、人脸算法阈值、支付协议或 UI。

4. 组件设计

4.1 FaceSyncPolicy

新增无 Android 依赖的纯 Java 策略类,集中定义:

该类只解释协议,不访问数据库、网络或 SDK,使用 JVM 单元测试覆盖。

4.2 FaceSyncResult

新增不可变结果对象,记录:

FaceDataUpdaters 不再用输入列表长度冒充成功数量。只有所有必要操作成功时才调用 updateSuccess;失败时调用 updateFail 并保留详细日志。

现有 FaceDataUpdateCallback 暂不修改接口签名,避免扩大页面改造范围;详细计数通过日志和内部结果对象保留,成功回调的 count 表示真实成功数量。

4.3 FaceSyncCoordinator

新增进程级人脸写入协调器,持有单线程 ExecutorService,职责是:

网络请求可以并发返回,但所有本地写操作按照进入协调器的先后顺序执行。该设计先解决数据库和 SDK 的物理并发写问题,不假设服务端消息含有可比较的版本号。后续如果协议提供版本号,再增加过期响应丢弃能力。

4.4 FaceApi 数据库原子操作

在 DBManager 和 FaceApi 中补充两个清晰操作:

  1. replaceUser(User user):在单个 SQLite 事务内按 user_id 删除旧行并插入新行;插入失败时事务回滚,旧数据保留。
  2. replaceAllUsers(List<User> users):在单个 SQLite 事务内清理并写入完整快照;任一插入失败时整体回滚。

事务成功后重新从数据库加载 FaceApi.users,不再通过用户名查询后向列表添加元素。这样同时修复:

删除操作返回真实受影响行数语义;删除不存在的数据按业务层幂等成功处理,但不伪造“实际删除一条”。

4.5 SDK 内存索引一致性

FaceSDKManager 增加有职责注释的索引维护接口:

只有人脸模型和 FaceSearch 已初始化时执行即时索引操作;未初始化时数据库仍可落库,后续 SDK 初始化通过现有加载流程重建索引。

4.6 FaceDataUpdaters 流程

增量 type=0

  1. 接口业务成功、数据对象存在且列表为空:返回成功,数量 0,不清库。
  2. 逐条校验状态和必要字段。
  3. 删除状态只要求有效 UUID;找到旧记录时同时删除 SQLite 和 SDK 索引,不存在时按幂等成功。
  4. 新增或更新必须先完成 Base64 解码和 User 构建,再执行事务替换。
  5. 每条结果计入 FaceSyncResult。
  6. 任一失败时整体回调失败,但已经成功提交的独立增量记录不回滚。

全量 type=1

  1. 在写数据库前校验并构建整个有效快照。
  2. 任一必要记录无效时拒绝替换,保留旧人脸库。
  3. 全部校验通过后调用 replaceAllUsers() 原子替换 SQLite。
  4. 数据库提交成功后按同步前、同步后 ID 集合重建 SDK 索引。
  5. 业务成功且明确返回空数组时,按“全量无人”处理;数据对象或数组字段缺失时不清库。

4.7 人员主档同步

CustomerDatabaseManager 统一按 staff_uuid 处理:

BatchCustomerSyncManager 接收同步类型:

/p/api/userdata 只维护人员与卡号库,不再调用 CustomerSyncManager.syncFace()。人脸数据只由 /p/api/facedata 和 MQTT 人脸消息维护。

刷卡本地查询只返回有效人员。由于删除状态会直接删除本地行,正常情况下不会读到 status=3;查询层仍增加有效状态保护,兼容升级前遗留数据。

4.8 MQTT 消息确认

4.9 WorkManager 与心跳

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. 错误处理

7. 测试设计

7.1 JVM 单元测试

新增纯逻辑测试,覆盖:

  1. 0 识别为增量、1 识别为全量,其他值无效;
  2. 增量空列表为成功且不要求清库;
  3. 全量明确空快照允许替换为空;
  4. 状态 1/2/3 的必要字段规则;
  5. Base64 非法时在删除旧数据前失败;
  6. 部分失败结果不能判定为完整成功;
  7. Worker 等待超时判定为重试;
  8. 人员状态删除和有效状态判断;
  9. 全量 UUID 对账不会在删除列表时跳过相邻人员。

7.2 数据库与编译验证

Android SQLite 与百度 SDK 依赖不在普通 JVM 测试中模拟。通过以下方式验证集成边界:

执行命令:

./gradlew :app:testDebugUnitTest :app:assembleDebug
git diff --check

8. 实机验收标准

  1. 初次启动全量同步后,人员库和人脸库数量与服务端一致。
  2. 增量新增一个人员后,旧人员全部保留,新人员可刷卡或识别。
  3. 增量更新人脸后,新特征生效,旧特征不再命中。
  4. 增量删除人员后,本地刷卡查询失败、旧人脸不再进入识别 Top 1。
  5. 增量返回空列表时不弹失败、不重试、不改变本地人数。
  6. 非法特征更新不会删除旧的可用人脸。
  7. 全量中存在非法记录时整次拒绝替换,旧库完整保留。
  8. MQTT 本地保存失败时不反馈处理完成;成功时只反馈一次。
  9. 模拟网络无回调或超时,Worker 返回重试。
  10. 心跳、定时和 MQTT 同时到达时无崩溃、无重复人员、无数据库锁异常。

9. 影响范围

计划修改:

明确不修改:

10. 文档与交付