diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..233387b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(git fetch *)", + "Bash(git switch *)", + "Bash(grep -n 'is_spring_slot\\\\|protected $fillable\\\\|protected $casts\\\\|'\\\\''settings'\\\\''' app/Models/Machine/Machine.php)" + ] + } +} diff --git a/app/build.gradle b/app/build.gradle index 56d87a4..aab448f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ plugins { // 避免不同尺寸機台間版本號產生衝突。 // ========================================================================= def verMinor = 1 -def verPatch = 87 +def verPatch = 91 diff --git a/app/src/main/java/com/unibuy/smartdevice/MyApp.java b/app/src/main/java/com/unibuy/smartdevice/MyApp.java index 24ad351..b32e764 100644 --- a/app/src/main/java/com/unibuy/smartdevice/MyApp.java +++ b/app/src/main/java/com/unibuy/smartdevice/MyApp.java @@ -115,11 +115,15 @@ public class MyApp extends Application { // 購買流程模式:basic(多品項購物車)/ employee_card(員工卡)/ pickup_sheet(取貨單), // 由雲端 B014 ShoppingMode 下發,取代舊 project flavor 分家。預設 basic。 private String shoppingMode = "basic"; + // 上傳 log 的寫檔門檻等級。預設 INFO:debug.txt 只留 INFO/WARNING/ERROR/UPDATE,過濾掉 DEBUG/IGNORE 雜訊。 + // 工程師現場除錯硬體時可設為 DEBUG,讓 hex/協議細節重新寫檔(logcat 一律照印,不受此值影響)。 + private String logLevel = "INFO"; // 機台顯示語系(由雲端 B014 LangSet 下發)。標準標籤(zh-TW/en/...),有序,第 0 個為預設。 private java.util.List machineLanguages = new java.util.ArrayList<>(); private String defaultLanguage = ""; // 商品名稱多語 map(B012 t060v01_i18n),key=商品ID,value=底線代碼→名稱 的 JSONObject。 private JSONObject productNameI18nBlob = new JSONObject(); + private JSONObject productSpecI18nBlob = new JSONObject(); private GreenInvoiceStructure greenInvoice; private GreenpaySettingStructure greenpaySetting; private GreenpayMerchantStructure greenpayMerchant; @@ -387,6 +391,7 @@ public class MyApp extends Application { MyApp.getInstance().getDevSet().setMemberSystem(jsonObject.optBoolean("MemberSystem", false)); MyApp.getInstance().getDevSet().setAmbientTemp(jsonObject.optBoolean("AmbientTemp", false)); MyApp.getInstance().setShoppingMode(jsonObject.optString("ShoppingMode", "basic")); + MyApp.getInstance().setLogLevel(jsonObject.optString("LogLevel", "INFO")); // 機台顯示語系(B014 LangSet) java.util.List langs = new java.util.ArrayList<>(); JSONArray langArr = jsonObject.optJSONArray("MachineLanguages"); @@ -408,6 +413,11 @@ public class MyApp extends Application { MyApp.getInstance().setProductNameI18nBlob(memoDao.getOnlyOne("ProductNameI18n").getData()); } + // 商品規格多語 blob(B012 t060v03_i18n),離線重啟時供 fallback + if (memoDao.existOnlyOne("ProductSpecI18n")) { + MyApp.getInstance().setProductSpecI18nBlob(memoDao.getOnlyOne("ProductSpecI18n").getData()); + } + if (memoDao.existOnlyOne(MyApp.getInstance().getCashSetStructure().getClass().getSimpleName())) { JSONObject jsonObject = memoDao.getOnlyOne(MyApp.getInstance().getCashSetStructure().getClass().getSimpleName()).getData(); logs.debug("initialSystemSet:" + jsonObject.toString()); @@ -470,6 +480,7 @@ public class MyApp extends Application { systemSetByJsonObject.put("MemberSystem", MyApp.getInstance().getDevSet().isMemberSystem()); systemSetByJsonObject.put("AmbientTemp", MyApp.getInstance().getDevSet().isAmbientTemp()); systemSetByJsonObject.put("ShoppingMode", MyApp.getInstance().getShoppingMode()); + systemSetByJsonObject.put("LogLevel", MyApp.getInstance().getLogLevel()); systemSetByJsonObject.put("MachineLanguages", new JSONArray(MyApp.getInstance().getMachineLanguages())); systemSetByJsonObject.put("DefaultLanguage", MyApp.getInstance().getDefaultLanguage()); @@ -535,6 +546,20 @@ public class MyApp extends Application { } } + // 持久化商品規格多語 blob(不動 products 表 schema,存於 MemoDao),供離線重啟 fallback + public static void saveProductSpecI18n(Context context) { + MemoDao memoDao = new MemoDao(context); + try { + memoDao.insertOnlyOne(new MemoStructure( + "ProductSpecI18n", + MyApp.getInstance().getProductSpecI18nBlob(), + new JSONObject())); + memoDao.mapUpdate(); + } catch (Exception e) { + new Logs(MyApp.class).warning(e); + } + } + public static void updateLog(Context context) { OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(UploadLogOneTimeWorker.class) .setInitialDelay(5, TimeUnit.SECONDS) @@ -816,7 +841,7 @@ public class MyApp extends Application { int i = 0; for (SlotStructure slotData: getSlotList(field)) { i += 1; - logs.info("AAAAAAAAAAAAAAAAAAAAAAAAAAAA0"+i); + logs.debug("slot index:" + i); if (slotData.getProduct().getProductID().equals(String.valueOf(pid))) { if(slotData.getCount() > 0) { return slotData; @@ -1016,6 +1041,14 @@ public class MyApp extends Application { this.shoppingMode = (shoppingMode == null || shoppingMode.isEmpty()) ? "basic" : shoppingMode; } + public String getLogLevel() { + return logLevel; + } + + public void setLogLevel(String logLevel) { + this.logLevel = (logLevel == null || logLevel.isEmpty()) ? "INFO" : logLevel.toUpperCase(java.util.Locale.US); + } + public java.util.List getMachineLanguages() { return machineLanguages; } @@ -1047,6 +1080,21 @@ public class MyApp extends Application { return (obj == null) ? null : obj.toString(); } + public void setProductSpecI18nBlob(JSONObject blob) { + this.productSpecI18nBlob = (blob == null) ? new JSONObject() : blob; + } + + public JSONObject getProductSpecI18nBlob() { + return productSpecI18nBlob; + } + + // 取某商品的規格多語 JSON 字串(底線代碼 key),無則回 null。供 ProductStructure 離線時 fallback。 + public String getProductSpecI18nJson(String productId) { + if (productId == null || productSpecI18nBlob == null) return null; + JSONObject obj = productSpecI18nBlob.optJSONObject(productId); + return (obj == null) ? null : obj.toString(); + } + public ReportAccessCodeStructure getReportAccessCodeStructure() { return reportAccessCodeStructure; diff --git a/app/src/main/java/com/unibuy/smartdevice/controller/DevNFCPayController.java b/app/src/main/java/com/unibuy/smartdevice/controller/DevNFCPayController.java index 2471eef..d47afb2 100644 --- a/app/src/main/java/com/unibuy/smartdevice/controller/DevNFCPayController.java +++ b/app/src/main/java/com/unibuy/smartdevice/controller/DevNFCPayController.java @@ -155,7 +155,7 @@ public class DevNFCPayController extends HandlerMainScheduler { byte[] buffer = comPort2.readListen(30, 200); if (buffer.length > 0) { - getLogs().info("NFC 收到資料: " + PortTools.showHex(buffer)); + getLogs().debug("NFC 收到資料: " + PortTools.showHex(buffer)); String rxData = new String(buffer, StandardCharsets.UTF_8); if (rxData.contains(String.valueOf((char) 0x03))) { // ETX diff --git a/app/src/main/java/com/unibuy/smartdevice/controller/DevXinYuanController.java b/app/src/main/java/com/unibuy/smartdevice/controller/DevXinYuanController.java index fc73d2c..0f2d7a6 100644 --- a/app/src/main/java/com/unibuy/smartdevice/controller/DevXinYuanController.java +++ b/app/src/main/java/com/unibuy/smartdevice/controller/DevXinYuanController.java @@ -56,7 +56,7 @@ public class DevXinYuanController extends DevController { try { this.rs232Dev = new Rs232Dev(MyApp.getInstance().getComPort1ByXinYuan()); devXinYuan.setRun(true); - logs.debug("下位機連接"); + logs.info("下位機連接"); } catch (LogsUnsupportedOperationException | LogsIOException e) { // getLogs().error(ErrorCode.DEV_ERROR, "@下位機連接錯誤", handlerMain); getLogs().warning(e); @@ -123,9 +123,9 @@ public class DevXinYuanController extends DevController { Byte key = Byte.valueOf(vmcCmd[0]); if (!vmcMap.containsKey(key)) { - getLogs().info("XinYuan vmcCmd:" + PortTools.showHex(vmcCmd)); - getLogs().info("XinYuan vmcData:" + PortTools.showHex(vmcData)); - getLogs().info("XinYuan vmcXor:" + PortTools.showHex(vmcXor)); + getLogs().debug("XinYuan vmcCmd:" + PortTools.showHex(vmcCmd)); + getLogs().debug("XinYuan vmcData:" + PortTools.showHex(vmcData)); + getLogs().debug("XinYuan vmcXor:" + PortTools.showHex(vmcXor)); getLogs().info("XinYuan vmc not found"); continue; } @@ -133,9 +133,9 @@ public class DevXinYuanController extends DevController { DevXinYuan.VMC vmc = vmcMap.get(key); if (vmc != DevXinYuan.VMC.VMC_41_POLL) { - getLogs().info("XinYuan vmcCmd:" + PortTools.showHex(vmcCmd)); - getLogs().info("XinYuan vmcData:" + PortTools.showHex(vmcData)); - getLogs().info("XinYuan vmcXor:" + PortTools.showHex(vmcXor)); + getLogs().debug("XinYuan vmcCmd:" + PortTools.showHex(vmcCmd)); + getLogs().debug("XinYuan vmcData:" + PortTools.showHex(vmcData)); + getLogs().debug("XinYuan vmcXor:" + PortTools.showHex(vmcXor)); getLogs().info("XinYuan vmc:" + vmc.name()); } @@ -143,12 +143,12 @@ public class DevXinYuanController extends DevController { case VMC_41_POLL: if (getCountBySendBuffer() > 0) { byte[] cmdBytes = getSendBufferByFirst(); - getLogs().info("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"+bytesToHex(cmdBytes)); + getLogs().debug("XinYuan cmd hex:" + bytesToHex(cmdBytes)); if (cmdBytes.length == (4+cmdBytes[3]+1)) { for (int sendCount=1; sendCount <2; sendCount++) { rs232Dev.send(cmdBytes); - getLogs().info("XinYuan Send:" + PortTools.showHex(cmdBytes)); + getLogs().debug("XinYuan Send:" + PortTools.showHex(cmdBytes)); byte[] vmcBuffer = rs232Dev.readListen(0, 1); if (vmcBuffer.length > 2) { @@ -247,7 +247,7 @@ public class DevXinYuanController extends DevController { break; case VMC_52_CHECK_VMC_STATUS: //溫度:vmcData[7] - getLogs().info("下位機訊號"+PortTools.showHex(vmcData)); + getLogs().debug("下位機訊號"+PortTools.showHex(vmcData)); if (PortTools.showHex(vmcData).isEmpty()) { getLogs().info("下位機失連,重啟APP"); diff --git a/app/src/main/java/com/unibuy/smartdevice/devices/DevXinYuan.java b/app/src/main/java/com/unibuy/smartdevice/devices/DevXinYuan.java index 7ecb081..565ce5b 100644 --- a/app/src/main/java/com/unibuy/smartdevice/devices/DevXinYuan.java +++ b/app/src/main/java/com/unibuy/smartdevice/devices/DevXinYuan.java @@ -267,7 +267,7 @@ public class DevXinYuan { proBytes = PortTools.concatByteArrays(proBytes, XORByte); if (isShow) { - logs.info("XinYuan ipcCmd:" + cmd.name() + " " + PortTools.showHex(proBytes)); + logs.debug("XinYuan ipcCmd:" + cmd.name() + " " + PortTools.showHex(proBytes)); } return proBytes; @@ -371,18 +371,18 @@ public class DevXinYuan { } public CMD readCMD(byte[] buffer) throws LogsEmptyException { - logs.info(PortTools.showHex(buffer)); + logs.debug(PortTools.showHex(buffer)); for (CMD cmd : CMD.values()) { if (cmd.getBytes().length >= 2 && buffer.length >= 2) { if (cmd.getBytes()[0] == buffer[0]) { if (cmd.equals(CMD.CMD_FAFB_STX)) { if (cmd.getBytes()[1] == buffer[1]) { - logs.info("CMD:" + cmd.name() + " " + PortTools.showHex(cmd.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); + logs.debug("CMD:" + cmd.name() + " " + PortTools.showHex(cmd.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); return cmd; } } else { - logs.info("CMD:" + cmd.name() + " " + PortTools.showHex(cmd.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); + logs.debug("CMD:" + cmd.name() + " " + PortTools.showHex(cmd.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); return cmd; } } @@ -393,18 +393,18 @@ public class DevXinYuan { } public VMC readVMC(byte[] buffer) throws LogsEmptyException { - logs.info(PortTools.showHex(buffer)); + logs.debug(PortTools.showHex(buffer)); for (VMC vmc : VMC.values()) { if (vmc.getBytes().length >= 2 && buffer.length >= 2) { if (vmc.getBytes()[0] == buffer[0]) { if (vmc.equals(VMC.VMC_FAFB_STX)) { if (vmc.getBytes()[1] == buffer[1]) { - logs.info("VMC:" + vmc.name() + " " + PortTools.showHex(vmc.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); + logs.debug("VMC:" + vmc.name() + " " + PortTools.showHex(vmc.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); return vmc; } } else { - logs.info("VMC:" + vmc.name() + " " + PortTools.showHex(vmc.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); + logs.debug("VMC:" + vmc.name() + " " + PortTools.showHex(vmc.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); return vmc; } } @@ -416,7 +416,7 @@ public class DevXinYuan { public byte readPackNo(byte[] buffer) { byte packNo = buffer[0]; - logs.info("Read packNo:" + PortTools.showHex(packNo)); + logs.debug("Read packNo:" + PortTools.showHex(packNo)); return packNo; } @@ -424,7 +424,7 @@ public class DevXinYuan { for (BUY_STATUS_04 buyStatus : BUY_STATUS_04.values()) { if (buyStatus.getBytes().length == 1 && buffer.length > 0) { if (buyStatus.getBytes()[0] == buffer[1]) { - logs.info(BUY_STATUS_04.class.getSimpleName()+":" + buyStatus.name() + " " + PortTools.showHex(buyStatus.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); + logs.debug(BUY_STATUS_04.class.getSimpleName()+":" + buyStatus.name() + " " + PortTools.showHex(buyStatus.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); return buyStatus; } } @@ -437,7 +437,7 @@ public class DevXinYuan { for (SLOT_STATUS_02 slotStatus : SLOT_STATUS_02.values()) { if (slotStatus.getBytes().length == 1 && buffer.length > 0) { if (slotStatus.getBytes()[0] == buffer[1]) { - logs.info(SLOT_STATUS_02.class.getSimpleName()+":" + slotStatus.name() + " " + PortTools.showHex(slotStatus.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); + logs.debug(SLOT_STATUS_02.class.getSimpleName()+":" + slotStatus.name() + " " + PortTools.showHex(slotStatus.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); return slotStatus; } } @@ -451,7 +451,7 @@ public class DevXinYuan { for (GIVE_MONEY_26 giveMoney : GIVE_MONEY_26.values()) { if (giveMoney.getBytes().length == 1 && buffer.length > 0) { if (giveMoney.getBytes()[0] == buffer[1]) { - logs.info(GIVE_MONEY_26.class.getSimpleName()+":" + giveMoney.name() + " " + PortTools.showHex(giveMoney.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); + logs.debug(GIVE_MONEY_26.class.getSimpleName()+":" + giveMoney.name() + " " + PortTools.showHex(giveMoney.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]})); return giveMoney; } } @@ -475,10 +475,10 @@ public class DevXinYuan { byte XORByte = buffer[buffer.length -1]; proBytes = PortTools.concatByteArrays(proBytes, cmdLenData); byte XORByte2 = getXOR(proBytes); - logs.info("Length:"+length+" XOR1:" + PortTools.showHex(new byte[] {XORByte}) + " XOR2:" + PortTools.showHex(new byte[] {XORByte2})); + logs.debug("Length:"+length+" XOR1:" + PortTools.showHex(new byte[] {XORByte}) + " XOR2:" + PortTools.showHex(new byte[] {XORByte2})); if (buffer.length > (2+length)) { byte[] dataBytes = PortTools.catByteArrays(buffer, 2, length); - logs.info("data:"+PortTools.showHex(dataBytes)); + logs.debug("data:"+PortTools.showHex(dataBytes)); return dataBytes; } else { throw new LogsEmptyException(logs, "data array length insufficient"); diff --git a/app/src/main/java/com/unibuy/smartdevice/exception/Logs.java b/app/src/main/java/com/unibuy/smartdevice/exception/Logs.java index 913288b..c3e50b7 100644 --- a/app/src/main/java/com/unibuy/smartdevice/exception/Logs.java +++ b/app/src/main/java/com/unibuy/smartdevice/exception/Logs.java @@ -18,6 +18,10 @@ import java.util.Date; import java.util.Locale; public class Logs { + // 單筆寫檔上限:超過即截斷,避免整包 JSON/payload dump 把 debug.txt 撐爆、淹沒有用事件。 + // 只影響「寫入 debug.txt」那份;logcat(Log.d/i/...)維持完整不截斷。 + private static final int MAX_LINE = 1000; + private String message; private String className; private String methodName; @@ -46,6 +50,10 @@ public class Logs { } String dateTime = Tools.getTimeStampByNow("yyyy-MM-dd HH:mm:ss.SSS"); + // 招 1:超長行截斷(止血整包 JSON/payload dump),保留事件本身與原始長度資訊。 + if (data.length() > MAX_LINE) { + data = data.substring(0, MAX_LINE) + " …[truncated " + data.length() + " chars]"; + } data = dateTime + " " + data; try (FileOutputStream fos = new FileOutputStream(txtDebug, true)) { // try-with-resources 自動 close() @@ -61,10 +69,20 @@ public class Logs { return this.className+"."+this.methodName+"("+this.lineNumber+")"; } + // 招 2:是否把 DEBUG/IGNORE 寫進 debug.txt。預設(門檻 INFO)不寫,過濾掉開發雜訊; + // 雲端/現場把 LogLevel 設為 DEBUG 時才寫檔。MyApp 尚未就緒時一律不寫,維持乾淨預設。 + private static boolean writeDebugToFile() { + try { + return "DEBUG".equals(com.unibuy.smartdevice.MyApp.getInstance().getLogLevel()); + } catch (Exception e) { + return false; + } + } + public void debug(String message) { this.message = message; Log.d(this.className, "DEBUG ["+getClassName()+"]"+this.message); - writeFile("DEBUG ["+getClassName()+"]"+this.message); + if (writeDebugToFile()) writeFile("DEBUG ["+getClassName()+"]"+this.message); } public void info(String message) { @@ -87,7 +105,7 @@ public class Logs { public void ignore(String message) { this.message = message; Log.w(this.className, "IGNORE ["+getClassName()+"/"+getInfo()+"]"+this.message); - writeFile("IGNORE ["+getClassName()+"/"+getInfo()+"]"+this.message); + if (writeDebugToFile()) writeFile("IGNORE ["+getClassName()+"/"+getInfo()+"]"+this.message); } public void warning(Exception e) { diff --git a/app/src/main/java/com/unibuy/smartdevice/external/HttpAPI.java b/app/src/main/java/com/unibuy/smartdevice/external/HttpAPI.java index ca950be..e751227 100644 --- a/app/src/main/java/com/unibuy/smartdevice/external/HttpAPI.java +++ b/app/src/main/java/com/unibuy/smartdevice/external/HttpAPI.java @@ -493,7 +493,6 @@ public class HttpAPI { public void response(int responseCode, String receive, HandlerMain handlerMain) { if (responseCode == 200) { try { - getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); MyApp.getInstance().getMachine().setMachineID(""); MyApp.getInstance().getEsunpay().setStoreID(""); @@ -656,9 +655,7 @@ public class HttpAPI { MyApp.getInstance().getGreenpayMerchant().getAtomeMerchantId() })); - getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa0"); SettingActivity.updateProgressUI(10, "下載中..."); - getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa10"); settingsDao.close(); } catch (JSONException e) { getLogs().warning(e); @@ -720,7 +717,7 @@ public class HttpAPI { * t060v06 "https://unibuy.com.tw/Unibuy/dist/img/products/53317506.png", */ - getLogs().info("AAAAAAAAAAAAAAAAAAAAAAA" + productData); + getLogs().debug("productData:" + productData); if (productData.has("t060v00") && productData.has("t060v06") && @@ -783,7 +780,6 @@ public class HttpAPI { productsDao.close(); SettingActivity.updateProgressUI(10, "下載中..."); - getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa30"); handlerMain.start(getClass().getSimpleName(), Option.DOWNLOAD_PRODUCT_RESPONSE.getOption(), "成功載入" + MyApp.getInstance().getProductList().size() + "筆商品資料"); @@ -1053,9 +1049,8 @@ public class HttpAPI { JSONObject receiveByJsonObject = new JSONObject(receive); String workApi = receiveByJsonObject.getString("workApi"); int status = receiveByJsonObject.getInt("status"); - getLogs().info("bbbbbbbbbbbbbbbbbbbbbb status" + status); String message = receiveByJsonObject.getString("message"); - getLogs().info("bbbbbbbbbbbbbbbbbbbbbb message" + message); + getLogs().debug("reportFlow status:" + status + " message:" + message); if (status == 0) { handlerMain.start(getClass().getSimpleName(), Option.REPORT_FLOW_RESPONSE.getOption(), @@ -2142,7 +2137,7 @@ public class HttpAPI { */ jsonObject2.put("data", jsonArray); - getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=" + jsonObject2.toString()); + getLogs().debug("B009 supplementary:" + jsonObject2.toString()); formPutRunMain.start(domainNameByUnibuy + "products/supplementary/B009", jsonObject2.toString(), null, null, HttpConnect.ContentType.JSON); @@ -2228,7 +2223,7 @@ public class HttpAPI { int slotid = Integer.parseInt(deliveryData.getString("tid")); int slotnum = Integer.parseInt(deliveryData.getString("num")); - getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + slotid + " " + slotnum); + getLogs().info("dispatch slotid:" + slotid + " num:" + slotnum); slotid = slotid; diff --git a/app/src/main/java/com/unibuy/smartdevice/external/HttpTapPay.java b/app/src/main/java/com/unibuy/smartdevice/external/HttpTapPay.java index 1ac2131..6d571aa 100644 --- a/app/src/main/java/com/unibuy/smartdevice/external/HttpTapPay.java +++ b/app/src/main/java/com/unibuy/smartdevice/external/HttpTapPay.java @@ -174,11 +174,11 @@ public class HttpTapPay { int payType = MyApp.getInstance().getReportFlowInfoData().getFlowType(); GreenpaySettingStructure greenpaySetting = MyApp.getInstance().getGreenpaySetting(); String greenpayMerchantId = MyApp.getInstance().getMachine().getMachineID(); - logs.info("aaaaaaaaaaaaaaaaaaaaaa1"+ MyApp.getInstance().getGreenpayMerchant().getLineMerchantId()); - logs.info("aaaaaaaaaaaaaaaaaaaaaa2"+ MyApp.getInstance().getGreenpayMerchant().getJkoMerchantId()); - logs.info("aaaaaaaaaaaaaaaaaaaaaa3"+ MyApp.getInstance().getGreenpayMerchant().getPiMerchantId()); - logs.info("aaaaaaaaaaaaaaaaaaaaaa4"+ MyApp.getInstance().getGreenpayMerchant().getPsMerchantId()); - logs.info("aaaaaaaaaaaaaaaaaaaaaa5"+ MyApp.getInstance().getGreenpayMerchant().getEasyMerchantId()); + logs.debug("merchantId line:" + MyApp.getInstance().getGreenpayMerchant().getLineMerchantId() + + " jko:" + MyApp.getInstance().getGreenpayMerchant().getJkoMerchantId() + + " pi:" + MyApp.getInstance().getGreenpayMerchant().getPiMerchantId() + + " ps:" + MyApp.getInstance().getGreenpayMerchant().getPsMerchantId() + + " easy:" + MyApp.getInstance().getGreenpayMerchant().getEasyMerchantId()); switch (payType) { case 30: if (MyApp.getInstance().getGreenpayMerchant() != null && !MyApp.getInstance().getGreenpayMerchant().getLineMerchantId().isEmpty()) { 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 a5100f8..f579926 100644 --- a/app/src/main/java/com/unibuy/smartdevice/external/StarCloudAPI.java +++ b/app/src/main/java/com/unibuy/smartdevice/external/StarCloudAPI.java @@ -1169,6 +1169,8 @@ public class StarCloudAPI { MyApp.getInstance().getProductList().clear(); // 商品名稱多語 blob:商品ID → t060v01_i18n(底線代碼 key),存於 MemoDao 持久化(不動 products 表) JSONObject nameI18nBlob = new JSONObject(); + // 商品規格多語 blob:商品ID → t060v03_i18n(純文字含換行) + JSONObject specI18nBlob = new JSONObject(); for (int i = 0; i < products.length(); i++) { JSONObject p = products.getJSONObject(i); @@ -1195,11 +1197,23 @@ public class StarCloudAPI { try { nameI18nBlob.put(pid, nameI18n); } catch (JSONException ignore) {} } } + + // 新增:商品規格多語 map(B012 t060v03_i18n,純文字含換行) + JSONObject specI18n = p.optJSONObject("t060v03_i18n"); + if (specI18n != null) { + product.setSpecI18nJson(specI18n.toString()); + String pid = p.optString("t060v00", ""); + if (!pid.isEmpty()) { + try { specI18nBlob.put(pid, specI18n); } catch (JSONException ignore) {} + } + } MyApp.getInstance().getProductList().add(product); } // 持久化商品多語 blob,供離線重啟時 fallback MyApp.getInstance().setProductNameI18nBlob(nameI18nBlob); MyApp.saveProductNameI18n(handlerMain.getContext()); + MyApp.getInstance().setProductSpecI18nBlob(specI18nBlob); + MyApp.saveProductSpecI18n(handlerMain.getContext()); // 將商品同步至資料庫 ProductsDao productsDao = new ProductsDao(handlerMain.getContext()); diff --git a/app/src/main/java/com/unibuy/smartdevice/structure/ProductStructure.java b/app/src/main/java/com/unibuy/smartdevice/structure/ProductStructure.java index 4b956c5..90b0a32 100644 --- a/app/src/main/java/com/unibuy/smartdevice/structure/ProductStructure.java +++ b/app/src/main/java/com/unibuy/smartdevice/structure/ProductStructure.java @@ -171,6 +171,47 @@ public class ProductStructure { } } + // B012 t060v03_i18n 原始 JSON 字串(底線代碼 key,如 {"zh_TW":..,"en":..})。 + // 內容為純文字含換行(\n),顯示時以 setText 原樣呈現(保留換行/空格/縮排)。 + // 由 B012 解析時設入;離線從 DB 重新載入商品時此欄為空,改向 MyApp 持久化 blob 取。 + private String specI18nJson; + + public String getSpecI18nJson() { + return specI18nJson; + } + + public void setSpecI18nJson(String specI18nJson) { + this.specI18nJson = specI18nJson; + } + + // 依目前語言取商品規格/描述。順序:對應語系 → zh_TW → 空字串(無描述)。 + public String getLocalizedSpec(Context context) { + String canonical = LanguageHelper.getSavedLanguage(context); + String backendLocale = LanguageHelper.toBackendLocale(canonical); + + String fromMap = lookupSpecI18n(backendLocale); + if (notBlank(fromMap)) return fromMap; + + if (!"zh_TW".equals(backendLocale)) { + String zh = lookupSpecI18n("zh_TW"); + if (notBlank(zh)) return zh; + } + return ""; + } + + private String lookupSpecI18n(String backendLocale) { + String json = specI18nJson; + if (json == null || json.isEmpty()) { + json = com.unibuy.smartdevice.MyApp.getInstance().getProductSpecI18nJson(productID); + } + if (json == null || json.isEmpty()) return null; + try { + return new org.json.JSONObject(json).optString(backendLocale, null); + } catch (org.json.JSONException e) { + return null; + } + } + private static boolean notBlank(String s) { return s != null && !s.trim().isEmpty(); } diff --git a/app/src/main/java/com/unibuy/smartdevice/structure/ReportInvoiceInfoStructure.java b/app/src/main/java/com/unibuy/smartdevice/structure/ReportInvoiceInfoStructure.java index 188cdff..90e5a70 100644 --- a/app/src/main/java/com/unibuy/smartdevice/structure/ReportInvoiceInfoStructure.java +++ b/app/src/main/java/com/unibuy/smartdevice/structure/ReportInvoiceInfoStructure.java @@ -8,6 +8,9 @@ public class ReportInvoiceInfoStructure { String invoiceDate; String randomNumber; String loveCode; + // 綠界 RelateNumber(冪等鍵,= machineID + flow_id)。由 DispatchDialog 開票前產生, + // 開票請求與 finalize 上報共用同一值,後台據此查詢/補開避免重複開立。 + String relateNumber = ""; public ReportInvoiceInfoStructure(String reportFlowId, String rtnCode, String rtnMsg, String invoiceNo, String invoiceDate, String randomNumber, String loveCode) { this.reportFlowId = reportFlowId; @@ -75,6 +78,14 @@ public class ReportInvoiceInfoStructure { this.loveCode = loveCode; } + public String getRelateNumber() { + return relateNumber; + } + + public void setRelateNumber(String relateNumber) { + this.relateNumber = relateNumber; + } + @Override public String toString() { return "ReportInvoiceInfoStructure{" + diff --git a/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/TransactionFinalizePayload.java b/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/TransactionFinalizePayload.java index f3d10bf..df517f7 100644 --- a/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/TransactionFinalizePayload.java +++ b/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/TransactionFinalizePayload.java @@ -35,6 +35,14 @@ public class TransactionFinalizePayload { /** 每個貨道的出貨結果列表 */ private List dispense; + /** + * 電子發票開立結果(選填)。 + * 機台在進入出貨流程時平行向綠界開立,結果折進此物件隨 finalize 一起上報; + * 若開票尚未回應則 status=pending,後台依 relate_number 去綠界查詢/補開。 + * 無發票需求(未帶發票資訊)時此欄為 null,後台不建立發票。 + */ + private InvoicePayload invoice; + // =========== Getters & Setters =========== public String getAction() { @@ -82,6 +90,74 @@ public class TransactionFinalizePayload { this.dispense = dispense; } + public InvoicePayload getInvoice() { + return invoice; + } + + public void setInvoice(InvoicePayload invoice) { + this.invoice = invoice; + } + + // =========== 內部類別:電子發票開立結果 =========== + + public static class InvoicePayload { + /** 開立狀態:issued(已開)/ pending(已送出未回應)/ failed(綠界回失敗) */ + private String status; + /** 綠界 RelateNumber(= machineID + flow_id),後台查詢/補開用 */ + private String relate_number; + /** 發票號碼(pending/failed 時為空) */ + private String invoice_no; + /** 綠界回應碼(1=成功) */ + private String rtn_code; + /** 綠界回應訊息 */ + private String rtn_msg; + /** 發票開立日期 */ + private String invoice_date; + /** 隨機碼 */ + private String random_number; + /** 愛心碼(捐贈時) */ + private String love_code; + /** 載具號碼 */ + private String carrier_id; + /** 統一編號(B2B 統編,有打統編時) */ + private String business_tax_id; + /** 發票金額 */ + private int amount; + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getRelate_number() { return relate_number; } + public void setRelate_number(String relate_number) { this.relate_number = relate_number; } + + public String getInvoice_no() { return invoice_no; } + public void setInvoice_no(String invoice_no) { this.invoice_no = invoice_no; } + + public String getRtn_code() { return rtn_code; } + public void setRtn_code(String rtn_code) { this.rtn_code = rtn_code; } + + public String getRtn_msg() { return rtn_msg; } + public void setRtn_msg(String rtn_msg) { this.rtn_msg = rtn_msg; } + + public String getInvoice_date() { return invoice_date; } + public void setInvoice_date(String invoice_date) { this.invoice_date = invoice_date; } + + public String getRandom_number() { return random_number; } + public void setRandom_number(String random_number) { this.random_number = random_number; } + + public String getLove_code() { return love_code; } + public void setLove_code(String love_code) { this.love_code = love_code; } + + public String getCarrier_id() { return carrier_id; } + public void setCarrier_id(String carrier_id) { this.carrier_id = carrier_id; } + + public String getBusiness_tax_id() { return business_tax_id; } + public void setBusiness_tax_id(String business_tax_id) { this.business_tax_id = business_tax_id; } + + public int getAmount() { return amount; } + public void setAmount(int amount) { this.amount = amount; } + } + // =========== 內部類別:訂單資訊 =========== public static class OrderPayload { diff --git a/app/src/main/java/com/unibuy/smartdevice/tools/TransactionFinalizeBuilder.java b/app/src/main/java/com/unibuy/smartdevice/tools/TransactionFinalizeBuilder.java index 71885d6..fab5ca3 100644 --- a/app/src/main/java/com/unibuy/smartdevice/tools/TransactionFinalizeBuilder.java +++ b/app/src/main/java/com/unibuy/smartdevice/tools/TransactionFinalizeBuilder.java @@ -5,6 +5,7 @@ import android.util.Log; import com.unibuy.smartdevice.MyApp; import com.unibuy.smartdevice.structure.BuyStructure; +import com.unibuy.smartdevice.structure.InvoiceStructure; import com.unibuy.smartdevice.structure.ReportFlowInfoStructure; import com.unibuy.smartdevice.structure.mqtt.TransactionFinalizePayload; @@ -59,6 +60,10 @@ public class TransactionFinalizeBuilder { this.flowId = flowId; } + public String getFlowId() { + return flowId; + } + public void setOrderNo(String orderNo) { this.orderNo = orderNo; } @@ -133,6 +138,24 @@ public class TransactionFinalizeBuilder { // --- 出貨記錄 --- payload.setDispense(new ArrayList<>(dispenseRecords)); + // --- 電子發票(改由後台開立)--- + // 機台不開票,只把「開立輸入」(載具/愛心碼/統編/金額)帶上去,狀態固定 pending; + // 後台建 pending 發票後派 Job 去綠界開立。無發票需求(invoiceInfo 為空)時不帶,後台不建立發票。 + if (flowInfo.getInvoiceInfo() != null && !flowInfo.getInvoiceInfo().isEmpty()) { + TransactionFinalizePayload.InvoicePayload invoicePayload = + new TransactionFinalizePayload.InvoicePayload(); + invoicePayload.setStatus("pending"); + invoicePayload.setAmount(flowInfo.getTotalPrice()); + + InvoiceStructure invoiceData = MyApp.getInstance().getInvoiceData(); + if (invoiceData != null) { + invoicePayload.setCarrier_id(invoiceData.getCarrier()); + invoicePayload.setBusiness_tax_id(invoiceData.getCustomerIdentifier()); + invoicePayload.setLove_code(invoiceData.getLoveCode()); + } + payload.setInvoice(invoicePayload); + } + Log.i(TAG, "TransactionFinalizePayload built: flow_id=" + flowId + ", order_no=" + order.getOrder_no() + ", items=" + order.getItems().size() diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/RecyclerVmcBuyListAdpter.java b/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/RecyclerVmcBuyListAdpter.java index 0276229..ff04fdd 100644 --- a/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/RecyclerVmcBuyListAdpter.java +++ b/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/RecyclerVmcBuyListAdpter.java @@ -114,8 +114,11 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter { if (cartOn) { addSelectedQuantityToCart(); + // 先切到購物車品項頁(於詳情頁視窗後方),再關閉詳情頁顯示出來, + // 避免「關閉對話框」與「fragment 切換」時序互相干擾導致沒跳頁。 + openCartList(); cancel(); - openCartList(context); } else { MyApp.getInstance().getBuyList().clear(); MyApp.getInstance().getBuyList().add( new BuyStructure(slot.getField(), slot.getSlot(), slot.getProduct(), quantity)); cancel(); - requestBuy(context); + requestBuy(); } }); } @@ -139,9 +155,9 @@ public class DialogProductDetail extends DialogAbstract { } /** 前往購物車列表頁(沿用各 flavor 的購物車入口)。 */ - private void openCartList(Context context) { - if (context instanceof FontendActivity) { - SaleFlowHandler handler = ((FontendActivity) context).getSaleFlowHandler(); + private void openCartList() { + if (hostContext instanceof FontendActivity) { + SaleFlowHandler handler = ((FontendActivity) hostContext).getSaleFlowHandler(); if (handler != null) { handler.onShoppingCartRequested(); } @@ -155,14 +171,14 @@ public class DialogProductDetail extends DialogAbstract { } /** 沿用各 flavor 的結帳入口(basic→BuyDialog);非 FontendActivity 時直接開 BuyDialog。 */ - private void requestBuy(Context context) { - if (context instanceof FontendActivity) { - SaleFlowHandler handler = ((FontendActivity) context).getSaleFlowHandler(); + private void requestBuy() { + if (hostContext instanceof FontendActivity) { + SaleFlowHandler handler = ((FontendActivity) hostContext).getSaleFlowHandler(); if (handler != null) { handler.onBuyRequested(); return; } } - new BuyDialog(context, getSrcHandlerMain()).show(); + new BuyDialog(hostContext, getSrcHandlerMain()).show(); } } diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/DispatchDialog.java b/app/src/main/java/com/unibuy/smartdevice/ui/dialog/DispatchDialog.java index 67cb6d0..6ddb946 100644 --- a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/DispatchDialog.java +++ b/app/src/main/java/com/unibuy/smartdevice/ui/dialog/DispatchDialog.java @@ -262,6 +262,10 @@ public class DispatchDialog extends DialogAbstract { MyApp.getInstance().getReportFlowInfoData().setFlowType(42); } + // [MQTT Finalize 重構] 電子發票改由「後台」開立:機台不再直連綠界, + // 僅把發票輸入(載具/愛心碼/統編/金額)隨 finalize 的 invoice 物件上報(TransactionFinalizeBuilder 組裝)。 + // 後台收到 finalize 後建 pending 發票並派 Job 去綠界開立,避免機台網路不確定性與等待。 + MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, "")); getLogs().info("BuyList size:" + MyApp.getInstance().getBuyList().size()); @@ -494,7 +498,7 @@ public class DispatchDialog extends DialogAbstract { public void readBufferOnScheduler(DevController devController, HandlerMain handlerMain) { if (isCheckSlotByVMC) return; - getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA006"); + getLogs().debug("dispatch flow 006"); isCheckSlotByVMC = true; // 進來立即設定true,可以判斷掉重複出貨 boolean isCMD = false; @@ -669,7 +673,7 @@ public class DispatchDialog extends DialogAbstract { getSrcHandlerMain().send(getClass().getSimpleName(), Option.NEXT_PROCESS_GOODS.getOption(), "next process goods"); } else { - getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA002"); + getLogs().debug("dispatch flow 002"); MyApp.getInstance().getDevXinYuanController() .getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot), 5000L); } @@ -682,7 +686,7 @@ public class DispatchDialog extends DialogAbstract { getCtx().getString(R.string.product_delivery_issues_contact_the_counter_window)); } } else { - getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA003"); + getLogs().debug("dispatch flow 003"); MyApp.getInstance().getDevXinYuanController() .getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot), 5000L); MyApp.getInstance().getDevXinYuanController() diff --git a/app/src/main/res/layout/dialog_product_detail.xml b/app/src/main/res/layout/dialog_product_detail.xml index 4c73592..1701d59 100644 --- a/app/src/main/res/layout/dialog_product_detail.xml +++ b/app/src/main/res/layout/dialog_product_detail.xml @@ -138,8 +138,7 @@ android:ellipsize="end" android:gravity="start" android:lineSpacingExtra="4dp" - android:maxLines="3" - android:text="無" + android:maxLines="8" android:textColor="#757575" android:textSize="28sp" /> diff --git a/app/src/main/res/layout/fragment_vmc.xml b/app/src/main/res/layout/fragment_vmc.xml index 0700956..63f1ebf 100644 --- a/app/src/main/res/layout/fragment_vmc.xml +++ b/app/src/main/res/layout/fragment_vmc.xml @@ -9,10 +9,12 @@