diff --git a/app/build.gradle b/app/build.gradle index 26e2eda..aebdce1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ plugins { // 避免不同尺寸機台間版本號產生衝突。 // ========================================================================= def verMinor = 2 -def verPatch = 19 +def verPatch = 20 diff --git a/app/src/main/java/com/unibuy/smartdevice/database/DatabaseHelper.java b/app/src/main/java/com/unibuy/smartdevice/database/DatabaseHelper.java index 7644bfc..fadc2ad 100644 --- a/app/src/main/java/com/unibuy/smartdevice/database/DatabaseHelper.java +++ b/app/src/main/java/com/unibuy/smartdevice/database/DatabaseHelper.java @@ -6,7 +6,7 @@ import android.util.Log; public class DatabaseHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "mydata.db"; - private static final int DATABASE_VERSION = 1; + private static final int DATABASE_VERSION = 2; public enum column { COLUMN_ID("INTEGER PRIMARY KEY"); @@ -164,6 +164,11 @@ public class DatabaseHelper extends SQLiteOpenHelper { COUNT("INTEGER NOT NULL"), LOCK("TINYINT NOT NULL"), OTHER_DATA("TEXT"), + // 雙向 LWW 同步序號(per-field) + LOCK_REV("INTEGER NOT NULL DEFAULT 0"), + LOCK_SYNCED_REV("INTEGER NOT NULL DEFAULT 0"), + EXPIRY_REV("INTEGER NOT NULL DEFAULT 0"), + EXPIRY_SYNCED_REV("INTEGER NOT NULL DEFAULT 0"), ; private String type; @@ -267,7 +272,11 @@ public class DatabaseHelper extends SQLiteOpenHelper { slot.PRODUCT_ID.getCreateSQL(", ") + slot.COUNT.getCreateSQL(", ") + slot.LOCK.getCreateSQL(", ") + - slot.OTHER_DATA.getCreateSQL(");") + slot.OTHER_DATA.getCreateSQL(", ") + + slot.LOCK_REV.getCreateSQL(", ") + + slot.LOCK_SYNCED_REV.getCreateSQL(", ") + + slot.EXPIRY_REV.getCreateSQL(", ") + + slot.EXPIRY_SYNCED_REV.getCreateSQL(");") ; private static final String CREATE_TABLE_MEMO = @@ -299,12 +308,31 @@ public class DatabaseHelper extends SQLiteOpenHelper { @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - exec(db, "DROP TABLE IF EXISTS " + settinglogs.TABLE.getName()); - exec(db, "DROP TABLE IF EXISTS " + settings.TABLE.getName()); - exec(db, "DROP TABLE IF EXISTS " + products.TABLE.getName()); - exec(db, "DROP TABLE IF EXISTS " + slot.TABLE.getName()); - exec(db, "DROP TABLE IF EXISTS " + memo.TABLE.getName()); - onCreate(db); + // v1→v2:雙向 LWW 同步序號。採「ALTER ADD COLUMN」增量升級,保留既有 slot/lock/效期/庫存資料, + // 不可 DROP 全表(否則本機補貨設定、現場鎖定狀態會全部遺失)。 + if (oldVersion < 2) { + tryAddColumn(db, slot.TABLE.getName(), slot.LOCK_REV.getName(), "INTEGER NOT NULL DEFAULT 0"); + tryAddColumn(db, slot.TABLE.getName(), slot.LOCK_SYNCED_REV.getName(), "INTEGER NOT NULL DEFAULT 0"); + tryAddColumn(db, slot.TABLE.getName(), slot.EXPIRY_REV.getName(), "INTEGER NOT NULL DEFAULT 0"); + tryAddColumn(db, slot.TABLE.getName(), slot.EXPIRY_SYNCED_REV.getName(), "INTEGER NOT NULL DEFAULT 0"); + // 既有「現場已鎖定」貨道:lock_rev 設為 1(synced 仍為 0 = 未上報), + // 升級後首次 B009 即以 1>0 透過常態 LWW 把現場鎖定遷移上雲,無需 app_uuid/重灌機制。 + try { + exec(db, "UPDATE " + slot.TABLE.getName() + " SET " + slot.LOCK_REV.getName() + + "=1 WHERE " + slot.LOCK.getName() + "=1"); + } catch (Exception e) { + Log.i("DatabaseHelper", "seed lock_rev on upgrade skipped: " + e.getMessage()); + } + } + } + + /** 安全新增欄位:欄位已存在時忽略例外,避免升級重入時報錯。 */ + private void tryAddColumn(SQLiteDatabase db, String table, String column, String type) { + try { + exec(db, "ALTER TABLE " + table + " ADD COLUMN " + column + " " + type); + } catch (Exception e) { + Log.i("DatabaseHelper", "add column skipped (" + table + "." + column + "): " + e.getMessage()); + } } } diff --git a/app/src/main/java/com/unibuy/smartdevice/database/SlotsDao.java b/app/src/main/java/com/unibuy/smartdevice/database/SlotsDao.java index 17db59d..95ddb03 100644 --- a/app/src/main/java/com/unibuy/smartdevice/database/SlotsDao.java +++ b/app/src/main/java/com/unibuy/smartdevice/database/SlotsDao.java @@ -38,10 +38,34 @@ public class SlotsDao { values.put(DatabaseHelper.slot.COUNT.getName(), data.getCount()); values.put(DatabaseHelper.slot.LOCK.getName(), data.isLock()); values.put(DatabaseHelper.slot.OTHER_DATA.getName(), data.getOtherData().toString()); + values.put(DatabaseHelper.slot.LOCK_REV.getName(), data.getLockRev()); + values.put(DatabaseHelper.slot.LOCK_SYNCED_REV.getName(), data.getLockSyncedRev()); + values.put(DatabaseHelper.slot.EXPIRY_REV.getName(), data.getExpiryRev()); + values.put(DatabaseHelper.slot.EXPIRY_SYNCED_REV.getName(), data.getExpirySyncedRev()); database.insert(DatabaseHelper.slot.TABLE.getName(), null, values); } + /** + * 單行更新一個貨道(count/lock/otherData/4 個 rev),依 field+slot 定位。 + * 取代 MQTT 下行/局部更新時 deleteAll()+insertAll() 全表重寫,避免與 UI 存檔互相 race。 + */ + public void updateSlot(SlotStructure data) { + ContentValues values = new ContentValues(); + values.put(DatabaseHelper.slot.PRODUCT_ID.getName(), data.getProduct().getProductID()); + values.put(DatabaseHelper.slot.COUNT.getName(), data.getCount()); + values.put(DatabaseHelper.slot.LOCK.getName(), data.isLock()); + values.put(DatabaseHelper.slot.OTHER_DATA.getName(), data.getOtherData() != null ? data.getOtherData().toString() : "{}"); + values.put(DatabaseHelper.slot.LOCK_REV.getName(), data.getLockRev()); + values.put(DatabaseHelper.slot.LOCK_SYNCED_REV.getName(), data.getLockSyncedRev()); + values.put(DatabaseHelper.slot.EXPIRY_REV.getName(), data.getExpiryRev()); + values.put(DatabaseHelper.slot.EXPIRY_SYNCED_REV.getName(), data.getExpirySyncedRev()); + + String whereClause = DatabaseHelper.slot.FIELD.getName() + "=? AND " + DatabaseHelper.slot.SLOT.getName() + "=?"; + String[] whereArgs = { String.valueOf(data.getField()), String.valueOf(data.getSlot()) }; + database.update(DatabaseHelper.slot.TABLE.getName(), values, whereClause, whereArgs); + } + // 更新指定 field 和 slot 的數量(count) public void updateCount(int field, int slot, int newCount) { ContentValues values = new ContentValues(); @@ -77,6 +101,29 @@ public class SlotsDao { } } + /** + * 以單一交易「清空後重建」整個 slot 表,避免 deleteAll() 與 insertAll() 之間的空窗被其他連線讀到空表, + * 或與背景下行寫入互相覆蓋。供 UI 存檔使用。 + */ + public void replaceAll(Map> slotMap) { + database.beginTransaction(); + try { + database.delete(DatabaseHelper.slot.TABLE.getName(), null, null); + for (SlotField slotField : SlotField.values()) { + if (slotMap.containsKey(slotField.getField())) { + List slotList = slotMap.get(slotField.getField()); + if (slotList == null) continue; + for (SlotStructure data : slotList) { + insertOne(data); + } + } + } + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + } + // 獲取所有 Products public Map> getAll() { Map> slotMap = new HashMap<>(); @@ -99,6 +146,10 @@ public class SlotsDao { int count = cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseHelper.slot.COUNT.getName())); boolean lock = cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseHelper.slot.LOCK.getName())) == 1; String otherData = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.slot.OTHER_DATA.getName())); + int lockRev = cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseHelper.slot.LOCK_REV.getName())); + int lockSyncedRev = cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseHelper.slot.LOCK_SYNCED_REV.getName())); + int expiryRev = cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseHelper.slot.EXPIRY_REV.getName())); + int expirySyncedRev = cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseHelper.slot.EXPIRY_SYNCED_REV.getName())); JSONObject jsonObject = new JSONObject(); try { @@ -117,7 +168,12 @@ public class SlotsDao { slotMap.put(field, new ArrayList<>()); } - slotMap.get(field).add(new SlotStructure(field, slot, product, count, lock, jsonObject)); + SlotStructure slotData = new SlotStructure(field, slot, product, count, lock, jsonObject); + slotData.setLockRev(lockRev); + slotData.setLockSyncedRev(lockSyncedRev); + slotData.setExpiryRev(expiryRev); + slotData.setExpirySyncedRev(expirySyncedRev); + slotMap.get(field).add(slotData); } cursor.close(); } diff --git a/app/src/main/java/com/unibuy/smartdevice/external/StarCloudAPI.java b/app/src/main/java/com/unibuy/smartdevice/external/StarCloudAPI.java index 9aadb17..4ba6ceb 100644 --- a/app/src/main/java/com/unibuy/smartdevice/external/StarCloudAPI.java +++ b/app/src/main/java/com/unibuy/smartdevice/external/StarCloudAPI.java @@ -411,17 +411,33 @@ public class StarCloudAPI { String receive = ""; try { JSONArray jsonArray = new JSONArray(); + // 記錄本次快照上報的 rev,成功後回填 synced_rev(用快照值而非當下值,避免吃掉上報期間的新編輯) + final java.util.List reportedRevs = new java.util.ArrayList<>(); for (SlotField slotField : SlotField.values()) { for (SlotStructure slotData : MyApp.getInstance().getSlotList(slotField.getField())) { - // 判斷是否有商品(若無商品 ID 為 "?",則不傳送,觸發後台全量同步移除邏輯) - if (slotData.getProduct() != null && !slotData.getProduct().getProductID().equals("?")) { - JSONObject item = new JSONObject(); - item.put("tid", String.valueOf(slotData.getSlot())); // 貨道編號轉字串 - item.put("t060v00", slotData.getProduct().getProductID()); // 商品 ID - item.put("num", String.valueOf(slotData.getCount())); // 實體庫存轉字串 - // 不送 status 參數(依據新文件,只送 tid, t060v00, num) - jsonArray.put(item); + boolean hasProduct = slotData.getProduct() != null + && !slotData.getProduct().getProductID().equals("?"); + // 即使無商品,只要有鎖定或 rev>0(現場改過)也要上報,否則被鎖的空貨道狀態無法同步 + boolean hasState = slotData.isLock() + || slotData.getLockRev() > 0 || slotData.getExpiryRev() > 0; + if (!hasProduct && !hasState) { + continue; // 無商品又無狀態 → 不送,維持後台全量同步移除語意 } + JSONObject item = new JSONObject(); + item.put("tid", String.valueOf(slotData.getSlot())); // 貨道編號轉字串 + item.put("t060v00", hasProduct ? slotData.getProduct().getProductID() : ""); // 商品 ID + item.put("num", String.valueOf(slotData.getCount())); // 實體庫存轉字串 + // 雙向 LWW:鎖定 + 效期/批號 + 各自的編輯序號 + item.put("is_locked", slotData.isLock()); + item.put("lock_rev", slotData.getLockRev()); + String exp = slotData.getExpiryDate(); + item.put("expiry_date", (exp == null || exp.isEmpty()) ? JSONObject.NULL : exp); + String batch = slotData.getBatchNo(); + item.put("batch_no", (batch == null || batch.isEmpty()) ? JSONObject.NULL : batch); + item.put("expiry_rev", slotData.getExpiryRev()); + jsonArray.put(item); + reportedRevs.add(new int[]{ slotField.getField(), slotData.getSlot(), + slotData.getLockRev(), slotData.getExpiryRev() }); } } @@ -469,6 +485,23 @@ public class StarCloudAPI { if (responseCode >= 200 && responseCode < 300) { Log.i(TAG, "B009 Success: " + receive); + // 回填 synced_rev = 本次快照上報的 rev(用快照值,期間若有新編輯則 rev>synced 仍視為未同步) + try { + com.unibuy.smartdevice.database.SlotsDao revDao = + new com.unibuy.smartdevice.database.SlotsDao(handlerMain.getContext()); + for (int[] r : reportedRevs) { + try { + SlotStructure s = MyApp.getInstance().getSlotData(r[0], r[1]); + if (s == null) continue; + if (s.getLockRev() == r[2]) s.setLockSyncedRev(r[2]); + if (s.getExpiryRev() == r[3]) s.setExpirySyncedRev(r[3]); + revDao.updateSlot(s); + } catch (Exception ignore) {} + } + revDao.close(); + } catch (Exception e) { + Log.e(TAG, "B009 synced_rev backfill error: " + e.getMessage()); + } } else { Log.e(TAG, "B009 Failed (" + responseCode + "): " + receive); } diff --git a/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java b/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java index 3240014..6ab0b95 100644 --- a/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java +++ b/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java @@ -356,7 +356,15 @@ public class MqttService extends Service { int slotNo = -1; int stock = -1; String expiryDate = null; + boolean hasExpiryKey = false; String batchNo = null; + boolean hasBatchKey = false; + boolean isLocked = false; + boolean hasLock = false; + int lockRev = 0; + boolean hasLockRev = false; + int expiryRev = 0; + boolean hasExpiryRev = false; if (command.getPayload() != null && command.getPayload().isJsonObject()) { com.google.gson.JsonObject payloadObj = command.getPayload().getAsJsonObject(); @@ -364,10 +372,28 @@ public class MqttService extends Service { slotNo = payloadObj.get("slot_no").getAsInt(); if (payloadObj.has("stock")) stock = payloadObj.get("stock").getAsInt(); - if (payloadObj.has("expiry_date") && !payloadObj.get("expiry_date").isJsonNull()) - expiryDate = payloadObj.get("expiry_date").getAsString(); - if (payloadObj.has("batch_no") && !payloadObj.get("batch_no").isJsonNull()) - batchNo = payloadObj.get("batch_no").getAsString(); + if (payloadObj.has("expiry_date")) { + hasExpiryKey = true; + if (!payloadObj.get("expiry_date").isJsonNull()) + expiryDate = payloadObj.get("expiry_date").getAsString(); + } + if (payloadObj.has("batch_no")) { + hasBatchKey = true; + if (!payloadObj.get("batch_no").isJsonNull()) + batchNo = payloadObj.get("batch_no").getAsString(); + } + if (payloadObj.has("is_locked") && !payloadObj.get("is_locked").isJsonNull()) { + hasLock = true; + isLocked = payloadObj.get("is_locked").getAsBoolean(); + } + if (payloadObj.has("lock_rev") && !payloadObj.get("lock_rev").isJsonNull()) { + hasLockRev = true; + lockRev = payloadObj.get("lock_rev").getAsInt(); + } + if (payloadObj.has("expiry_rev") && !payloadObj.get("expiry_rev").isJsonNull()) { + hasExpiryRev = true; + expiryRev = payloadObj.get("expiry_rev").getAsInt(); + } } if (slotNo <= 0 || stock < 0) { @@ -381,24 +407,57 @@ public class MqttService extends Service { if (slotData != null) { slotData.setCount(stock); - // 更新選填欄位至 otherData org.json.JSONObject otherData = slotData.getOtherData(); if (otherData == null) otherData = new org.json.JSONObject(); - try { - if (expiryDate != null) - otherData.put("expiry_date", expiryDate); - if (batchNo != null) - otherData.put("batch_no", batchNo); - } catch (org.json.JSONException e) { - logs.info("Failed to update JSON otherData: " + e.getMessage()); + + // 鎖定 LWW:本地 rev>synced_rev 表示有未上報的現場編輯 → 拒絕被下行覆蓋 + if (hasLock) { + if (slotData.getLockRev() <= slotData.getLockSyncedRev()) { + slotData.setLock(isLocked); + int r = hasLockRev ? lockRev : slotData.getLockSyncedRev(); + slotData.setLockRev(r); + slotData.setLockSyncedRev(r); + } else { + logs.info("update_inventory: 本地有未上報鎖定編輯,略過下行 is_locked slot=" + slotNo); + } + } + + // 效期/批號 LWW(只動「下行確實帶了該鍵」的欄位,避免缺鍵時誤清本地效期) + if (hasExpiryRev) { + if (slotData.getExpiryRev() <= slotData.getExpirySyncedRev()) { + try { + if (hasExpiryKey) { + if (expiryDate != null) otherData.put("expiry_date", expiryDate); + else otherData.remove("expiry_date"); + } + if (hasBatchKey) { + if (batchNo != null) otherData.put("batch_no", batchNo); + else otherData.remove("batch_no"); + } + } catch (org.json.JSONException e) { + logs.info("Failed to update JSON otherData: " + e.getMessage()); + } + slotData.setExpiryRev(expiryRev); + slotData.setExpirySyncedRev(expiryRev); + } else { + logs.info("update_inventory: 本地有未上報效期編輯,略過下行 expiry slot=" + slotNo); + } + } else { + // 舊版相容:無 expiry_rev,沿用原本「有給就寫」行為 + try { + if (expiryDate != null) otherData.put("expiry_date", expiryDate); + if (batchNo != null) otherData.put("batch_no", batchNo); + } catch (org.json.JSONException e) { + logs.info("Failed to update JSON otherData: " + e.getMessage()); + } } slotData.setOtherData(otherData); + // 單行更新,避免 deleteAll()+insertAll() 全表重寫與 UI 存檔 race com.unibuy.smartdevice.database.SlotsDao slotsDao = new com.unibuy.smartdevice.database.SlotsDao( getApplicationContext()); - slotsDao.deleteAll(); - slotsDao.insertAll(MyApp.getInstance().getSlotMap()); + slotsDao.updateSlot(slotData); slotsDao.close(); logs.info("Inventory updated: slot=" + slotNo + ", stock=" + stock); diff --git a/app/src/main/java/com/unibuy/smartdevice/structure/SlotStructure.java b/app/src/main/java/com/unibuy/smartdevice/structure/SlotStructure.java index fd342d5..bd07324 100644 --- a/app/src/main/java/com/unibuy/smartdevice/structure/SlotStructure.java +++ b/app/src/main/java/com/unibuy/smartdevice/structure/SlotStructure.java @@ -12,6 +12,15 @@ public class SlotStructure { private JSONObject otherData; public int cartSelectedCount;// 本地欄位:購物車已選數量(General flavor 多品項購物車使用) + // ── 雙向 LWW 同步序號(per-field)── + // rev:店員在貨道管理「存檔且實際變更」時才遞增;synced_rev:已被後台確認(B009 成功/下行採用)的 rev。 + // rev > synced_rev = 有未上報的現場編輯 → 收到下行時該欄位拒絕被覆蓋。 + // 注意:出貨扣到 0 的 clearExpiry / 自動扣庫存 / 一鍵補貨「不可」遞增 rev。 + private int lockRev; + private int lockSyncedRev; + private int expiryRev; + private int expirySyncedRev; + /* JSONObject jsonObject = new JSONObject(); jsonObject.put("key", apiKey); @@ -146,6 +155,7 @@ public class SlotStructure { /** * 清除效期與批號。通常在貨道庫存歸 0(賣完)時呼叫,避免舊批貨效期殘留, * 導致下次本機補新貨時被舊效期誤鎖成「暫不販售」。 + * 注意:這是「自動」清除,呼叫端不可遞增 expiryRev(否則賣完會把後台預設的下批效期洗掉)。 */ public void clearExpiry() { if (otherData == null) @@ -153,4 +163,42 @@ public class SlotStructure { otherData.remove("expiry_date"); otherData.remove("batch_no"); } + + /** 批號(otherData.batch_no),無則回空字串。 */ + public String getBatchNo() { + if (otherData == null || !otherData.has("batch_no")) + return ""; + try { + String b = otherData.getString("batch_no"); + return (b == null || "null".equalsIgnoreCase(b)) ? "" : b; + } catch (Exception e) { + return ""; + } + } + + /** + * 設定效期字串(yyyy-MM-dd)。供貨道管理 UI 編輯用;空字串/null 代表清除(無效期管控)。 + * 不在此遞增 rev —— rev 由 saveData 與 baseline 比對後統一遞增。 + */ + public void setExpiryDate(String expiry) { + if (otherData == null) + otherData = new JSONObject(); + try { + if (expiry == null || expiry.trim().isEmpty()) { + otherData.remove("expiry_date"); + } else { + otherData.put("expiry_date", expiry.trim()); + } + } catch (Exception ignored) { + } + } + + public int getLockRev() { return lockRev; } + public void setLockRev(int lockRev) { this.lockRev = lockRev; } + public int getLockSyncedRev() { return lockSyncedRev; } + public void setLockSyncedRev(int lockSyncedRev) { this.lockSyncedRev = lockSyncedRev; } + public int getExpiryRev() { return expiryRev; } + public void setExpiryRev(int expiryRev) { this.expiryRev = expiryRev; } + public int getExpirySyncedRev() { return expirySyncedRev; } + public void setExpirySyncedRev(int expirySyncedRev) { this.expirySyncedRev = expirySyncedRev; } } diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/RecyclerVmcSlotListAdpter.java b/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/RecyclerVmcSlotListAdpter.java index 2eeee83..844d96c 100644 --- a/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/RecyclerVmcSlotListAdpter.java +++ b/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/RecyclerVmcSlotListAdpter.java @@ -76,20 +76,20 @@ public class RecyclerVmcSlotListAdpter extends RecyclerView.Adapter showExpiryPicker(position)); if (slot.isLock()) { holder.binding.textLock.setChecked(false); @@ -296,6 +296,39 @@ public class RecyclerVmcSlotListAdpter extends RecyclerView.Adapter= 10) { + cal.set(Integer.parseInt(cur.substring(0, 4)), + Integer.parseInt(cur.substring(5, 7)) - 1, + Integer.parseInt(cur.substring(8, 10))); + } + } catch (Exception ignored) {} + + android.app.DatePickerDialog dialog = new android.app.DatePickerDialog( + context, + (view, year, month, day) -> { + String date = String.format(java.util.Locale.US, "%04d-%02d-%02d", year, month + 1, day); + slot.setExpiryDate(date); + notifyItemChanged(position); + }, + cal.get(java.util.Calendar.YEAR), + cal.get(java.util.Calendar.MONTH), + cal.get(java.util.Calendar.DAY_OF_MONTH)); + dialog.setButton(android.app.DatePickerDialog.BUTTON_NEUTRAL, "清除效期", (d, w) -> { + slot.setExpiryDate(""); + notifyItemChanged(position); + }); + dialog.show(); + } + public void delProductData(int position) { slotList.get(position).setProduct(new ProductStructure("?", "", "", 0, 0, 0, 0, "", "","","")); slotList.get(position).setCount(0); diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/VmcSlotListActivity.java b/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/VmcSlotListActivity.java index a7fe223..8143c6d 100644 --- a/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/VmcSlotListActivity.java +++ b/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/VmcSlotListActivity.java @@ -38,6 +38,9 @@ public class VmcSlotListActivity extends AppCompatActivityAbstract { private RecyclerVmcSlotListAdpter recyclerVmcSlotListAdpter; private HttpAPI httpAPI; private StarCloudAPI starCloudAPI; + // rev 遞增 baseline:進畫面時快照各貨道 lock/效期,存檔時與此比對,「實際變更」才遞增對應 rev。 + private final Map baselineLock = new HashMap<>(); + private final Map baselineExpiry = new HashMap<>(); @Override protected Context setCtx() { @@ -128,6 +131,7 @@ public class VmcSlotListActivity extends AppCompatActivityAbstract { recyclerVmcSlotListAdpter = new RecyclerVmcSlotListAdpter(this, resultLauncher, MyApp.getInstance().getSlotList(SlotField.VMC.getField())); binding.recyclerSlotList.setAdapter(recyclerVmcSlotListAdpter); binding.recyclerSlotList.setLayoutManager(new GridLayoutManager(this, 3)); + captureRevBaseline(); // dataShow(); @@ -216,11 +220,37 @@ public class VmcSlotListActivity extends AppCompatActivityAbstract { finish(); } + /** 進畫面/存檔後快照各貨道 lock 與效期,作為下次存檔判斷「是否實際變更」的 baseline。 */ + private void captureRevBaseline() { + baselineLock.clear(); + baselineExpiry.clear(); + for (SlotStructure s : MyApp.getInstance().getSlotList(SlotField.VMC.getField())) { + baselineLock.put(s.getSlot(), s.isLock()); + baselineExpiry.put(s.getSlot(), s.getExpiryDate()); + } + } + + /** 存檔時:與 baseline 比對,鎖定或效期「實際變更」才遞增對應 rev(每次存檔至多 +1,避免重複切換虛增)。 */ + private void applyRevBumps() { + for (SlotStructure s : MyApp.getInstance().getSlotList(SlotField.VMC.getField())) { + Boolean bLock = baselineLock.get(s.getSlot()); + if (bLock != null && bLock != s.isLock()) { + s.setLockRev(Math.max(s.getLockRev(), s.getLockSyncedRev()) + 1); + } + String bExp = baselineExpiry.get(s.getSlot()); + String curExp = s.getExpiryDate(); + if (bExp != null && !bExp.equals(curExp)) { + s.setExpiryRev(Math.max(s.getExpiryRev(), s.getExpirySyncedRev()) + 1); + } + } + } + private void saveData() { + applyRevBumps(); SlotsDao slotsDao = new SlotsDao(this); - slotsDao.deleteAll(); - slotsDao.insertAll(MyApp.getInstance().getSlotMap()); + slotsDao.replaceAll(MyApp.getInstance().getSlotMap()); // 單一交易清空重建,避免與背景下行寫入 race / 讀到空表 slotsDao.close(); + captureRevBaseline(); // 存檔後重設 baseline,避免同次連續存檔重複遞增 rev getLogs().info("save count:"+ MyApp.getInstance().getSlotList(SlotField.VMC.getField()).size()); }