Personnel Sync Consistency Fix Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make full, incremental, scheduled, heartbeat, and MQTT personnel synchronization preserve consistent customer, face-database, and SDK-index state without clearing unchanged incremental data or reporting partial failures as success.

Architecture: Extract protocol decisions and result aggregation into pure Java policies covered by JVM tests. Route every face write through one process-level serial coordinator; use SQLite transactions for single-person replacement and full-snapshot replacement, then explicitly reconcile the Baidu SDK search index. Keep /userdata responsible for customer/card data and /facedata responsible for face data.

Tech Stack: Java 8, Android SQLite, WorkManager 2.7.1, Retrofit/RxJava, Eclipse Paho MQTT, Baidu Face SDK, JUnit 4.

Repository constraints: Work in the existing checkout, preserve unrelated dish-learning changes, do not stage or commit, and store design/plan/change-record documents only in the AIDishRecognition prompt mirror as paired Markdown and HTML.


Task 1: Add protocol and result policies with RED tests

Files:

@Test public void incrementalTypeDoesNotReplaceWholeDatabase() {
    assertTrue(FaceSyncPolicy.isIncremental("0"));
    assertFalse(FaceSyncPolicy.shouldReplaceWholeDatabase("0"));
}

@Test public void fullTypeReplacesWholeDatabaseOnlyAfterValidSnapshot() {
    assertTrue(FaceSyncPolicy.isFull("1"));
    assertTrue(FaceSyncPolicy.shouldReplaceWholeDatabase("1"));
}

@Test public void deleteRequiresOnlyUuidButUpsertRequiresIdAndFeature() {
    assertNull(FaceSyncPolicy.validateRecord("3", "u-1", 0, null));
    assertNotNull(FaceSyncPolicy.validateRecord("2", "u-1", 1, ""));
}
@Test public void partialFailureIsNotSuccess() {
    FaceSyncResult result = new FaceSyncResult(2);
    result.recordSuccess("1");
    result.recordFailure("bad feature");
    assertFalse(result.isCompleteSuccess());
    assertEquals(1, result.getSuccessCount());
    assertEquals(1, result.getFailureCount());
}

@Test public void timeoutAlwaysRequestsRetry() {
    assertTrue(WorkerSyncWaitPolicy.shouldRetry(false, true));
    assertTrue(WorkerSyncWaitPolicy.shouldRetry(true, false));
    assertFalse(WorkerSyncWaitPolicy.shouldRetry(true, true));
}

Run:

./gradlew :app:testDebugUnitTest --tests 'com.cpt.aidishrecognition.sync.*'

Expected: compilation fails because the four production policy classes do not exist.

FaceSyncPolicy must define named constants and return a concrete validation message instead of throwing:

public static String validateRecord(String status, String uuid, int id, String feature) {
    if (!isSupportedStatus(status)) return "不支持的人员状态:" + status;
    if (isBlank(uuid)) return "人员UUID为空";
    if (isDelete(status)) return null;
    if (id <= 0) return "人员ID无效";
    if (isBlank(feature)) return "人脸特征为空";
    return null;
}

FaceSyncResult must count success/failure/status and preserve the first failure message. WorkerSyncWaitPolicy must make timeout override the callback success flag. CustomerSyncPolicy must treat status 3 as delete and statuses null/0/1/2 as storable for backward-compatible snapshots.

Run the command from Step 3. Expected: all sync policy tests pass.

Run:

git diff --check -- app/src/main/java/com/cpt/aidishrecognition/sync app/src/test/java/com/cpt/aidishrecognition/sync

Expected: no output.

Task 2: Add atomic face-database operations and safe FaceApi memory refresh

Files:

Extract all user columns currently written by addUser() into:

private ContentValues toUserValues(User user) {
    ContentValues values = new ContentValues();
    values.put("_id", user.getId());
    values.put("user_id", user.getUserId());
    values.put("user_name", user.getUserName());
    values.put("user_info", user.getUserInfo());
    values.put("group_id", GROUP_ID);
    values.put("face_token", user.getFaceToken());
    values.put("feature", user.getFeature());
    values.put("image_name", user.getImageName());
    values.put("ctime", user.getCtime() > 0 ? user.getCtime() : System.currentTimeMillis());
    values.put("head_uuid", user.getHeadUUID());
    values.put("head_img", user.getHeadImg());
    values.put("update_time", System.currentTimeMillis());
    return values;
}

Add replaceUser(User) that validates initialized DB state, begins a transaction, deletes rows matching either the same user_id or _id, inserts the new row, and commits only when insert succeeds. Catch exceptions, log, and return false; always end the transaction.

Add replaceAllUsers(List<User>). Begin the transaction, delete TABLE_USER, insert every validated row, and commit only after every insert succeeds. An empty list is valid and commits an empty table.

Add deleteUserAndReturnCount(String userId) for business logic that needs to distinguish “deleted” from “already absent”. Keep the old boolean method for compatibility, implemented through the count method where possible.

Add:

public synchronized boolean userReplace(User user)
public synchronized boolean usersReplaceAll(List<User> newUsers)
public synchronized void reloadUsersFromDatabase()

On success, set users from DBManager.queryAllUsers(). Change userAdd() so it never adds null and never changes the in-memory list when DBManager.addUser() fails.

Run:

./gradlew :datalibrary:assembleDebug :app:compileDebugJavaWithJavac

Expected: BUILD SUCCESSFUL.

Task 3: Serialize face writes and implement correct full/incremental behavior

Files:

Test that two adjacent obsolete UUIDs are both selected for removal, and that invalid Base64 validation fails before the write plan is marked applicable.

Use a singleton with a named single-thread executor and a documented responsibility boundary:

public final class FaceSyncCoordinator {
    public interface Completion { void onComplete(FaceSyncResult result); }
    public void execute(Runnable task)
    public void applySingle(SingleStaffDataBean bean, Completion callback)
}

The coordinator must catch every Throwable at the executor boundary and convert it to a failed FaceSyncResult. It must not hold an Activity or start network requests.

In FaceDataUpdaters, validate fields through FaceSyncPolicy, decode Base64 inside try/catch, and return a failed result without deleting the old user when validation fails.

For type=0:

For type=1, validate and build the whole list before calling usersReplaceAll(). Capture old user IDs before commit. After a successful commit, remove all old SDK IDs and push all current users. A missing data object/array fails without changing the DB; an explicitly present empty array replaces the DB with an empty snapshot.

Add methods with null/model guards:

public boolean removeFaceIndex(int dbId)
public boolean replaceFaceIndex(int oldDbId, User newUser)
public boolean rebuildFaceIndexes(List<Integer> oldDbIds, List<User> currentUsers)

When FaceSearch is not initialized, treat immediate index work as deferred rather than crashing; the later SDK initialization loads SQLite.

FaceDataUpdaters.performUpdate() must distinguish missing payload from a present empty array, enqueue all DB work through the coordinator, and dispatch callbacks with true success/failure counts.

./gradlew :app:testDebugUnitTest --tests 'com.cpt.aidishrecognition.sync.*' :app:compileDebugJavaWithJavac

Expected: tests pass and compilation succeeds.

Task 4: Make customer/card synchronization status-aware and atomic

Files:

In CustomerDBHelper, add:

public long replaceCustomerByStaffUuid(Customer customer)
public boolean replaceAllCustomers(List<Customer> customers)

Single replacement deletes every row with the same staff_uuid and inserts within one transaction. Full replacement clears and inserts the validated snapshot in one transaction. Roll back on any exception.

Return a structured boolean/result from save logic: status 3 deletes by UUID idempotently; status null/0/1/2 replaces by UUID. Reject null customer, empty UUID, or missing ID for non-delete records.

Delete the CustomerSyncManager.syncFace() call from BatchCustomerSyncManager; retire syncCustomerAndFace() if no call sites remain. /userdata must only update customer/card data.

Pass the sync type into syncCustomerListInBatches. For full sync, call atomic full replacement after validation. For incremental sync, apply each status. If any item fails, call onSyncFailed with total/success/failure counts and do not call onSyncSuccess.

Change the call to:

syncManager.syncCustomerListInBatches(customerList, type);

An explicitly empty full snapshot clears the customer DB; an empty incremental response is success with zero changes.

Make getCustomerByCardId() select only rows whose status is not delete status, while remaining compatible with null/0 legacy active values.

./gradlew :app:testDebugUnitTest --tests 'com.cpt.aidishrecognition.sync.CustomerSyncPolicyTest' :app:compileDebugJavaWithJavac

Expected: test and compile success.

Task 5: Correct MQTT acknowledgement and background timeout behavior

Files:

Replace the synchronous saveSingleBeanToDB() call with an asynchronous coordinator call. Refresh/log/feedback only after completion. Do not call initPush() independently because the coordinated apply owns index updates.

Require non-empty message ID. Call the status-aware customer save method and invoke feedbackMsg() only when it returns success. On failure, log and retain the message for broker/server retry behavior.

Update SyncStrategyManager.shouldUseTimedIncrementSync() so it requires device code and the face-recognition preference. Apply the same guard to heartbeat-triggered face updates.

Initialize callback success to false, capture the boolean returned from await, and use:

boolean completed = latch.await(90, TimeUnit.SECONDS);
return WorkerSyncWaitPolicy.shouldRetry(completed, success.get())
        ? Result.retry() : Result.success();

Use the same rule for heartbeat waits. Log explicit timeout messages.

./gradlew :app:testDebugUnitTest --tests 'com.cpt.aidishrecognition.sync.*' :app:compileDebugJavaWithJavac

Expected: all sync tests pass and Java compilation succeeds.

Task 6: Full verification, change record, and repository hygiene

Files:

./gradlew :app:testDebugUnitTest

Expected: BUILD SUCCESSFUL, zero failing tests.

./gradlew :app:assembleDebug

Expected: BUILD SUCCESSFUL and APK generated under app/build/outputs/apk/debug/.

git diff --check
git status --short

Expected: no whitespace errors; status contains the pre-existing dish-learning edits plus only the intended personnel-sync source/test files.

Check touched files for raw sync/status strings outside FaceSyncPolicy and CustomerSyncPolicy. Ensure new coordinator, policy, transaction, and SDK-index methods have responsibility/rationale comments.

Record the confirmed root causes, changed files, behavior matrix, RED/GREEN evidence, final verification commands, remaining real-device acceptance items, and explicit statement that no Git mutations were performed. Generate matching browser-facing HTML.

find /Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/AIDishRecognition -type f -name '*.md'
find /Users/liang/AndroidStudioProjects/AIDishRecognition -maxdepth 2 -type d \( -name docs -o -name doc -o -name prompt \)

Expected: every new formal Markdown file has a same-directory HTML peer; no new source-repository documentation directory exists.