Merge remote-tracking branch 'origin/feat/general-cart-and-payment' into ui-design

This commit is contained in:
TaiwanStar Developer 2026-06-15 16:24:58 +08:00
commit be3c25bbf5
26 changed files with 353 additions and 107 deletions

9
.claude/settings.json Normal file
View File

@ -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)"
]
}
}

View File

@ -10,7 +10,7 @@ plugins {
//
// =========================================================================
def verMinor = 1
def verPatch = 87
def verPatch = 91

View File

@ -115,11 +115,15 @@ public class MyApp extends Application {
// 購買流程模式basic多品項購物車/ employee_card員工卡/ pickup_sheet取貨單
// 由雲端 B014 ShoppingMode 下發取代舊 project flavor 分家預設 basic
private String shoppingMode = "basic";
// 上傳 log 的寫檔門檻等級預設 INFOdebug.txt 只留 INFO/WARNING/ERROR/UPDATE過濾掉 DEBUG/IGNORE 雜訊
// 工程師現場除錯硬體時可設為 DEBUG hex/協議細節重新寫檔logcat 一律照印不受此值影響
private String logLevel = "INFO";
// 機台顯示語系由雲端 B014 LangSet 下發標準標籤(zh-TW/en/...)有序 0 個為預設
private java.util.List<String> machineLanguages = new java.util.ArrayList<>();
private String defaultLanguage = "";
// 商品名稱多語 mapB012 t060v01_i18nkey=商品IDvalue=底線代碼名稱 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<String> 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());
}
// 商品規格多語 blobB012 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<String> 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;

View File

@ -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

View File

@ -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");

View File

@ -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");

View File

@ -18,6 +18,10 @@ import java.util.Date;
import java.util.Locale;
public class Logs {
// 單筆寫檔上限超過即截斷避免整包 JSON/payload dump debug.txt 撐爆淹沒有用事件
// 只影響寫入 debug.txt那份logcatLog.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) {

View File

@ -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;

View File

@ -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()) {

View File

@ -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) {}
}
}
// 新增商品規格多語 mapB012 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());

View File

@ -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();
}

View File

@ -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{" +

View File

@ -35,6 +35,14 @@ public class TransactionFinalizePayload {
/** 每個貨道的出貨結果列表 */
private List<DispenseRecord> 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 {

View File

@ -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()

View File

@ -114,8 +114,11 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
holder.binding.imageProductPicture.setFocusable(false);
holder.binding.imageProductPicture.setFocusableInTouchMode(false);
holder.binding.imageProductPicture.setOnClickListener(new imageProductPictureOnClickListener(position));
// 整張卡片皆可點開商品詳情不限圖片避免點到名稱/價格區沒反應
holder.binding.getRoot().setOnClickListener(new imageProductPictureOnClickListener(position));
} else {
holder.binding.imageProductPicture.setOnClickListener(null);
holder.binding.getRoot().setOnClickListener(null);
}
// 商品卡右下角+快速加購僅購物車功能開啟時顯示無購物車時隱藏只能點商品開詳情頁

View File

@ -387,7 +387,7 @@ public class BuyDialog extends DialogAbstract {
public void gotoDialog() {
closeDialogCountdown.shutdown();
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaa"+flowType);
getLogs().debug("buy flowType:" + flowType);
// if (flowType == 5 || flowType == 7 || flowType == 30) {
if (flowType == 5 || flowType == 7) {
changeCheckout();

View File

@ -100,25 +100,20 @@ public class CheckoutEsunPayDialog extends DialogAbstract {
case TRANSACTION_SUCCESSFUL:
if (isTransaction) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
// exception(context, e.getErrorCode(), e.getLocalizedMessage());
getLogs().warning(e);
}
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo)
// 直接進出貨流程 DispatchDialog MQTT Finalize 統一上報結案含發票/扣庫存
// payment_type=3掃碼支付已於 BuyDialog 設定
isTransaction = false;
changeDispatch();
}
break;
case TRANSACTION_FAILED:
if (isTransaction) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
// exception(context, e.getErrorCode(), e.getLocalizedMessage());
getLogs().warning(e);
}
// [MQTT Finalize 重構] 交易失敗不再經 B600直接提示並返回購買頁不進出貨
isTransaction = false;
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
changeBuy();
}
break;
case CHANGE_DISPATCH:

View File

@ -101,25 +101,20 @@ public class CheckoutLinePayDialog extends DialogAbstract {
case TRANSACTION_SUCCESSFUL:
if (isTransaction) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
// exception(context, e.getErrorCode(), e.getLocalizedMessage());
getLogs().warning(e);
}
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo)
// 直接進出貨流程 DispatchDialog MQTT Finalize 統一上報結案含發票/扣庫存
// payment_type=70LINE Pay 官方已於 BuyDialog 設定
isTransaction = false;
changeDispatch();
}
break;
case TRANSACTION_FAILED:
if (isTransaction) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
// exception(context, e.getErrorCode(), e.getLocalizedMessage());
getLogs().warning(e);
}
// [MQTT Finalize 重構] 交易失敗不再經 B600直接提示並返回購買頁不進出貨
isTransaction = false;
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
changeBuy();
}
break;
case CHANGE_DISPATCH:

View File

@ -109,11 +109,10 @@ public class CheckoutNfcCardDialog extends DialogAbstract {
if (message.equals("0000")) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
getLogs().warning(e);
}
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo)
// 直接進出貨流程 DispatchDialog MQTT Finalize 統一上報結案含發票/扣庫存
// payment_type=1信用卡已於 BuyDialog 設定
changeDispatch();
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));

View File

@ -108,11 +108,13 @@ public class CheckoutNfcPhoneDialog extends DialogAbstract {
if (message.equals("0000")) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
getLogs().warning(e);
}
// NfcPhone(手機支付)刷卡機 NCCC 電文無法區分手機錢包/實體卡
// 依使用者所選付款鈕歸類為 payment_type=10手機支付
// 路由鍵 flowType=4 僅用於 BuyDialog 開啟本 Dialog這裡覆寫為實際上報值
MyApp.getInstance().getReportFlowInfoData().setFlowType(10);
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo)
// 直接進出貨流程 DispatchDialog MQTT Finalize 統一上報結案含發票/扣庫存
changeDispatch();
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));

View File

@ -108,11 +108,10 @@ public class CheckoutNfcRechargeDialog extends DialogAbstract {
if (message.equals("0000")) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
getLogs().warning(e);
}
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo)
// 直接進出貨流程 DispatchDialog MQTT Finalize 統一上報結案含發票/扣庫存
// payment_type=2悠遊卡/一卡通已於 BuyDialog 設定
changeDispatch();
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));

View File

@ -123,22 +123,19 @@ public class CheckoutTapPayDialog extends DialogAbstract {
if (isTransaction) {
isTransaction = false; // 🚨 馬上關閉避免送出第二筆
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
getLogs().warning(e);
}
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo)
// 直接進出貨流程 DispatchDialog MQTT Finalize 統一上報結案含發票/扣庫存
// payment_type=30~34TapPay 子項已於本 Dialog 選擇時設定
changeDispatch();
}
break;
case TRANSACTION_FAILED:
if (isTransaction) {
isTransaction = false; // 🚨 馬上關閉
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
try {
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
} catch (LogsSettingEmptyException | LogsParseException e) {
getLogs().warning(e);
}
// [MQTT Finalize 重構] 交易失敗不再經 B600直接提示並返回購買頁不進出貨
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
changeBuy();
}
break;
case TRANSACTION_AGAIN:

View File

@ -28,10 +28,15 @@ public class DialogProductDetail extends DialogAbstract {
private DialogProductDetailBinding binding;
private final SlotStructure slot;
// 建構子傳入的原始 Context真正的 FontendActivity
// 不可用 onCreate getContext()那是 AlertDialog 包裝過的 ContextThemeWrapper
// instanceof FontendActivity 會是 false導致購物車頁/結帳入口取不到 SaleFlowHandler
private final Context hostContext;
private int quantity = 1;
public DialogProductDetail(Context context, HandlerMain srcHandlerMain, SlotStructure slot) {
super(context, srcHandlerMain);
this.hostContext = context;
this.slot = slot;
}
@ -72,6 +77,15 @@ public class DialogProductDetail extends DialogAbstract {
binding.slotNumber.setText("貨道" + slot.getSlot());
binding.inventoryNumber.setText("庫存" + slot.getCount());
// 商品規格描述多語純文字含換行後台空白則隱藏整塊
// setText 原樣呈現換行與空格縮排版面已設 gravity=start故縮排不會被置中打亂
String spec = slot.getProduct().getLocalizedSpec(context);
if (spec == null || spec.trim().isEmpty()) {
binding.textProductDescription.setVisibility(android.view.View.GONE);
} else {
binding.textProductDescription.setText(spec);
}
// 依購物車開關決定按鈕呈現
boolean cartOn = MyApp.getInstance().getDevSet().isShoppingCar();
if (!cartOn) {
@ -119,14 +133,16 @@ public class DialogProductDetail extends DialogAbstract {
binding.btnBuyNow.setOnClickListener(v -> {
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();
}
}

View File

@ -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()

View File

@ -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" />
</LinearLayout>

View File

@ -9,10 +9,12 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerBuy"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- pickup_sheet原 Cmuh模式專用掃描單據按鈕其餘模式隱藏由 VmcFragment 依 shopping_mode 切換可見性)。 -->