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:
Create:
app/src/main/java/com/cpt/aidishrecognition/sync/FaceSyncPolicy.javaCreate:
app/src/main/java/com/cpt/aidishrecognition/sync/FaceSyncResult.javaCreate:
app/src/main/java/com/cpt/aidishrecognition/sync/WorkerSyncWaitPolicy.javaCreate:
app/src/main/java/com/cpt/aidishrecognition/sync/CustomerSyncPolicy.javaCreate:
app/src/test/java/com/cpt/aidishrecognition/sync/FaceSyncPolicyTest.javaCreate:
app/src/test/java/com/cpt/aidishrecognition/sync/FaceSyncResultTest.javaCreate:
app/src/test/java/com/cpt/aidishrecognition/sync/WorkerSyncWaitPolicyTest.javaCreate:
app/src/test/java/com/cpt/aidishrecognition/sync/CustomerSyncPolicyTest.javaStep 1: Write failing tests for protocol constants and validation
@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, ""));
}
- Step 2: Write failing tests for result aggregation and timeout decisions
@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));
}
- Step 3: Run the focused tests and verify RED
Run:
./gradlew :app:testDebugUnitTest --tests 'com.cpt.aidishrecognition.sync.*'
Expected: compilation fails because the four production policy classes do not exist.
- Step 4: Implement minimal pure Java policies
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.
- Step 5: Run focused tests and verify GREEN
Run the command from Step 3. Expected: all sync policy tests pass.
- Step 6: Inspect only the new files
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:
Modify:
datalibrary/src/main/java/com/example/datalibrary/db/DBManager.javaModify:
datalibrary/src/main/java/com/example/datalibrary/api/FaceApi.javaStep 1: Add one
ContentValuesbuilder for user rows
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;
}
- Step 2: Implement single-user replacement in one SQLite transaction
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.
- Step 3: Implement full-snapshot replacement in one SQLite 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.
- Step 4: Make delete return real affected-row semantics
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.
- Step 5: Add FaceApi wrappers that reload memory from SQLite after commit
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.
- Step 6: Compile both modules
Run:
./gradlew :datalibrary:assembleDebug :app:compileDebugJavaWithJavac
Expected: BUILD SUCCESSFUL.
Task 3: Serialize face writes and implement correct full/incremental behavior
Files:
Create:
app/src/main/java/com/cpt/aidishrecognition/sync/FaceSyncCoordinator.javaModify:
app/src/main/java/com/cpt/aidishrecognition/utils/FaceDataUpdaters.javaModify:
app/src/main/java/com/cpt/aidishrecognition/manager/FaceSDKManager.javaTest:
app/src/test/java/com/cpt/aidishrecognition/sync/FaceSyncPolicyTest.javaTest:
app/src/test/java/com/cpt/aidishrecognition/sync/FaceSyncResultTest.javaStep 1: Add RED tests for adjacent full-snapshot reconciliation and invalid feature input
Test that two adjacent obsolete UUIDs are both selected for removal, and that invalid Base64 validation fails before the write plan is marked applicable.
- Step 2: Implement
FaceSyncCoordinator
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.
- Step 3: Build and validate
Userbefore changing SQLite
In FaceDataUpdaters, validate fields through FaceSyncPolicy, decode Base64 inside try/catch, and return a failed result without deleting the old user when validation fails.
- Step 4: Implement incremental apply without
userClean()
For type=0:
empty list returns success count 0;
status 1/2 uses
FaceApi.userReplace();status 3 looks up the old user, deletes SQLite by UUID, and removes the old SDK ID;
every item updates
FaceSyncResult;any failure produces
updateFail, notupdateSuccess.Step 5: Implement atomic full-snapshot apply
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.
- Step 6: Add explicit SDK index methods
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.
- Step 7: Make HTTP response semantics exact
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.
- Step 8: Run sync policy tests and compile
./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:
Modify:
app/src/main/java/com/cpt/aidishrecognition/dataabase/CustomerDBHelper.javaModify:
app/src/main/java/com/cpt/aidishrecognition/dataabase/CustomerDatabaseManager.javaModify:
app/src/main/java/com/cpt/aidishrecognition/manager/CustomerSyncManager.javaModify:
app/src/main/java/com/cpt/aidishrecognition/manager/BatchCustomerSyncManager.javaModify:
app/src/main/java/com/cpt/aidishrecognition/LaunchActivity.javaTest:
app/src/test/java/com/cpt/aidishrecognition/sync/CustomerSyncPolicyTest.javaStep 1: Add customer transaction helpers
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.
- Step 2: Apply status semantics in
CustomerDatabaseManager
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.
- Step 3: Remove face writes from customer synchronization
Delete the CustomerSyncManager.syncFace() call from BatchCustomerSyncManager; retire syncCustomerAndFace() if no call sites remain. /userdata must only update customer/card data.
- Step 4: Make batch success honest
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.
- Step 5: Pass the startup sync type from LaunchActivity
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.
- Step 6: Protect card lookup from legacy deleted rows
Make getCustomerByCardId() select only rows whose status is not delete status, while remaining compatible with null/0 legacy active values.
- Step 7: Compile and run customer policy tests
./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:
Modify:
app/src/main/java/com/cpt/aidishrecognition/mqtt/MqttSimple.javaModify:
app/src/main/java/com/cpt/aidishrecognition/workmanager/UpdateFaceWorker.javaModify:
app/src/main/java/com/cpt/aidishrecognition/workmanager/HeartBeatWorker.javaModify:
app/src/main/java/com/cpt/aidishrecognition/utils/SyncStrategyManager.javaTest:
app/src/test/java/com/cpt/aidishrecognition/sync/WorkerSyncWaitPolicyTest.javaStep 1: Route MQTT face messages through the coordinator
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.
- Step 2: Validate personnel MQTT acknowledgement
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.
- Step 3: Gate timed face work on face-payment configuration
Update SyncStrategyManager.shouldUseTimedIncrementSync() so it requires device code and the face-recognition preference. Apply the same guard to heartbeat-triggered face updates.
- Step 4: Fix timeout decisions
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.
- Step 5: Run focused tests and compile
./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:
Create:
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/AIDishRecognition/change_records/2026-09-24-personnel-sync-consistency-fix.mdCreate:
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/AIDishRecognition/change_records/2026-09-24-personnel-sync-consistency-fix.htmlStep 1: Run the full JVM test suite
./gradlew :app:testDebugUnitTest
Expected: BUILD SUCCESSFUL, zero failing tests.
- Step 2: Build the Debug APK
./gradlew :app:assembleDebug
Expected: BUILD SUCCESSFUL and APK generated under app/build/outputs/apk/debug/.
- Step 3: Run patch hygiene checks
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.
- Step 4: Verify hardcoding and comments
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.
- Step 5: Write the paired change record
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.
- Step 6: Verify document pairing and source-repo cleanliness
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.