[FEAT] 來店禮串接 B690 + 掃碼即時回饋與空窗修正、通行碼購物車模式封堵

1. 來店禮改串新端點 B690(取代舊 HttpAPI.freeGiftCode):
   - StarCloudAPI 新增 verifyWelcomeGift(),POST machine/welcome-gift/verify/B690(Bearer 機台 token,body {code})。
   - CheckoutFreeGiftCodeDialog 手動輸入與掃碼皆改走 B690;折扣映射 amount→mode1(折抵金額)、percentage→mode2(折扣趴數換算總價乘數,八折20→0.8)。
   - 加 isWelcomeGift API 防護;非 amount/percentage 型別擋下提示。
   - 注意:B690 無舊「免費商品」型,純折扣。
2. 掃碼 UX 空窗修正(取貨碼/通行碼/來店禮三個對話框):
   - 掃描器啟動延遲 5 秒→0,對話框一彈出即監聽,解決「剛彈出馬上掃會整個丟失、時好時壞、無回饋」。
   - 讀取窗口 7→9 次(約 45 秒),覆蓋 40 秒對話框存活時間。
   - 新增 SET_SCAN_INPUT:掃到的碼即時顯示在輸入框;送出前顯示「驗證中…」。
3. 通行碼購物車模式封堵:
   - BuyDialog 購物車模式的付款清單中,通行碼(buttonAccessCode)從未 gating;改在 onCreate 加權威判斷,需 isPickupModule && isPassCode 才顯示,涵蓋購物車/單品所有模式。
4. 版號 verPatch 76→79。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sky121113 2026-06-11 15:09:42 +08:00
parent e8b3185b8a
commit fc21186a53
6 changed files with 176 additions and 25 deletions

View File

@ -10,7 +10,7 @@ plugins {
//
// =========================================================================
def verMinor = 1
def verPatch = 76
def verPatch = 79

View File

@ -610,6 +610,15 @@ public class StarCloudAPI {
void onFailure(String message);
}
/**
* 來店禮驗證回呼B690成功回傳折扣資訊
* discountTypeamount折抵金額/ percentage折扣百分比discountValue 為折扣趴數八折=20
*/
public interface WelcomeGiftCallback {
void onSuccess(String codeId, String discountType, int discountValue, String discountLabel, String name);
void onFailure(String message);
}
/**
* B016系統方identity=system回寫機台系統設定到雲端 star-cloud
* 使用 B000 登入核發的使用者 Tokenauth:sanctum後端再驗一次系統方權限
@ -876,6 +885,97 @@ public class StarCloudAPI {
}).start();
}
/**
* B690: 來店禮驗證邏輯比照 B660/B670驗證通道不同
* 成功回傳 data.code_id 與折扣資訊discount_type/discount_value/discount_label/name
*/
public void verifyWelcomeGift(final String code, final WelcomeGiftCallback callback) {
final String url = getBaseUrl() + "machine/welcome-gift/verify/B690";
Log.i(TAG, "Starting B690 request for code: " + code);
new Thread(new Runnable() {
@Override
public void run() {
int responseCode = -1;
String receive = "";
try {
JSONObject json = new JSONObject();
json.put("code", code);
java.net.URL reqUrl = new java.net.URL(url);
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
conn.setRequestProperty("Accept", "application/json");
String machineToken = MyApp.getInstance().getMachine().getApiKey();
if (machineToken != null && !machineToken.isEmpty()) {
conn.setRequestProperty("Authorization", "Bearer " + machineToken);
}
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
conn.setDoOutput(true);
conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
conn.getOutputStream().flush();
responseCode = conn.getResponseCode();
java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream() : conn.getErrorStream();
if (is != null) {
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
receive = sb.toString();
}
conn.disconnect();
Log.d(TAG, "B690 Response (" + responseCode + "): " + receive);
} catch (final Exception e) {
Log.e(TAG, "B690 Network Error", e);
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> callback.onFailure("連線失敗: " + e.getMessage()));
return;
}
final int finalCode = responseCode;
final String finalReceive = receive;
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
if (finalCode >= 200 && finalCode < 300) {
try {
JSONObject res = new JSONObject(finalReceive);
if (res.optBoolean("success", false)) {
JSONObject data = res.optJSONObject("data");
String codeId = data != null ? data.optString("code_id", "") : "";
String discountType = data != null ? data.optString("discount_type", "") : "";
int discountValue = data != null ? data.optInt("discount_value", 0) : 0;
String discountLabel = data != null ? data.optString("discount_label", "") : "";
String name = data != null ? data.optString("name", "") : "";
callback.onSuccess(codeId, discountType, discountValue, discountLabel, name);
} else {
callback.onFailure(res.optString("message", "來店禮驗證失敗"));
}
} catch (Exception e) {
callback.onFailure("解析錯誤: " + finalCode);
}
} else if (finalCode == 404) {
callback.onFailure("查無此來店禮或已失效");
} else if (finalCode == 429) {
callback.onFailure("錯誤次數過多,請稍後再試");
} else if (finalCode == 401) {
callback.onFailure("系統認證失效,請聯繫管理員");
} else if (finalCode == 400) {
callback.onFailure("來店禮格式錯誤(需 8 碼)");
} else {
try {
JSONObject res = new JSONObject(finalReceive);
callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
} catch (Exception e) {
callback.onFailure("系統連線錯誤 (" + finalCode + ")");
}
}
});
}
}).start();
}
private void saveSettings(JSONObject config) {
// 使用 optString 避免欄位缺失導致 JSONException

View File

@ -219,6 +219,12 @@ public class BuyDialog extends DialogAbstract {
this.binding.buttonFreeGift.setVisibility(
MyApp.getInstance().getDevSet().isWelcomeGift() ? View.VISIBLE : View.GONE);
// 通行碼入口付款格內 buttonAccessCodeflowType 5權威判斷涵蓋購物車/單品所有模式
// 從屬取貨模組PickupModule PassCode 任一關閉即隱藏購物車模式下方不會再覆寫此值
this.binding.buttonAccessCode.setVisibility(
(MyApp.getInstance().getDevSet().isPickupModule()
&& MyApp.getInstance().getDevSet().isPassCode()) ? View.VISIBLE : View.GONE);
this.binding.buttonAddProduct.setOnClickListener(v -> gotoAddProduct());
this.binding.buttonCancel.setOnClickListener(v -> dialogCancel());

View File

@ -247,7 +247,8 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
closeDialogCountdown.setCountdown(40);
});
new ReadBarCodeOnScheduler(getHandlerMain()).start(5, TimeUnit.SECONDS);
// 立即開始聽掃描器原本延遲 5 導致對話框剛彈出時掃碼被整個丟掉 時好時壞無回饋
new ReadBarCodeOnScheduler(getHandlerMain()).start(0, TimeUnit.SECONDS);
}
public class ReadBarCodeOnScheduler extends HandlerMainScheduler {
@ -271,7 +272,7 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
byte[] buffer = new byte[1000];
usbDev.read(buffer, 100);
for (int i=0; i<7; i++) {
for (int i=0; i<9; i++) { // 45 秒讀取窗口覆蓋 40 秒對話框存活時間
if (isRun() && openBarcode) {
int size = usbDev.read(buffer, 5000);
if (size > 5) {

View File

@ -39,6 +39,7 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
CHANGE_DISPATCH(6),
CHANGE_BUY(7),
SET_STATUS_TEXT(8),
SET_SCAN_INPUT(9),
;
private int option;
@ -106,6 +107,11 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
case SET_STATUS_TEXT:
setStatusText(message);
break;
case SET_SCAN_INPUT:
// 掃碼即時回饋把掃到的內容顯示在輸入框讓使用者確認有掃到
binding.editTextNumber.setText(message);
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
break;
}
}
}
@ -232,7 +238,8 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
closeDialogCountdown.setCountdown(40);
});
new ReadBarCodeOnScheduler(getHandlerMain()).start(5, TimeUnit.SECONDS);
// 立即開始聽掃描器原本延遲 5 導致對話框剛彈出時掃碼被整個丟掉 時好時壞無回饋
new ReadBarCodeOnScheduler(getHandlerMain()).start(0, TimeUnit.SECONDS);
}
public class ReadBarCodeOnScheduler extends HandlerMainScheduler {
@ -256,20 +263,14 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
byte[] buffer = new byte[1000];
usbDev.read(buffer, 100);
for (int i=0; i<7; i++) {
for (int i=0; i<9; i++) { // 45 秒讀取窗口覆蓋 40 秒對話框存活時間
if (isRun() && openBarcode) {
int size = usbDev.read(buffer, 5000);
if (size > 5) {
String barCodeString = new String(buffer, 0, size);
getHandlerMain().start(getLogs().getClassName(), getCtx().getString(R.string.receive_barcode)+""+ Tools.randomReplace(barCodeString, 0.3)+""+getCtx().getString(R.string.send_transaction));//收到條碼,送出交易
try {
MyApp.getInstance().getFreeGiftStructure().setPassCode(barCodeString);
httpAPI.freeGiftCode(MyApp.getInstance().getFreeGiftStructure());
} catch (LogsParseException | LogsSettingEmptyException e) {
getLogs().warning(e);
}
String barCodeString = new String(buffer, 0, size).trim();
// 掃碼即時回饋先把掃到的內容顯示在輸入框再送出驗證
getHandlerMain().start(getClass().getSimpleName(), Option.SET_SCAN_INPUT.getOption(), barCodeString);
submitWelcomeGift(barCodeString);
openBarcode = false;
}
}
@ -304,15 +305,57 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
}
public void changeOk() {
try {
String number = binding.editTextNumber.getText().toString();
MyApp.getInstance().getFreeGiftStructure().setPassCode(number);
httpAPI.freeGiftCode(MyApp.getInstance().getFreeGiftStructure());
} catch (LogsParseException e) {
throw new RuntimeException(e);
} catch (LogsSettingEmptyException e) {
throw new RuntimeException(e);
String number = binding.editTextNumber.getText().toString().trim();
if (number.isEmpty() || number.length() < 8) {
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "來店禮代碼需 8 碼");
return;
}
submitWelcomeGift(number);
}
/**
* 來店禮驗證改走 StarCloudAPI B690取代舊 HttpAPI.freeGiftCode
* B690 只回折扣amount/percentage免費商品成功即套折扣並返回 BuyDialog 結帳
*/
private void submitWelcomeGift(String code) {
// 防護來店禮關閉即不開放呼叫 B690即使 UI 漏出入口也擋下
if (!MyApp.getInstance().getDevSet().isWelcomeGift()) {
openBarcode = true;
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "來店禮功能未開放");
return;
}
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "驗證中…");
new com.unibuy.smartdevice.external.StarCloudAPI(getHandlerMain(), getHandlerMain())
.verifyWelcomeGift(code, new com.unibuy.smartdevice.external.StarCloudAPI.WelcomeGiftCallback() {
@Override
public void onSuccess(String codeId, String discountType, int discountValue, String discountLabel, String name) {
// 映射到既有折扣機制mode 1=折抵金額mode 2=折扣百分比乘數
MyApp.getInstance().getReportFreeGiftCodeStructure().setFreeGiftId(codeId);
MyApp.getInstance().getReportFreeGiftCodeStructure().setPassCode(code);
MyApp.getInstance().getReportFlowInfoData().setReportFlowId(code);
MyApp.getInstance().getReportFlowInfoData().setFlowBarCode(code);
if ("amount".equals(discountType)) {
MyApp.getInstance().getReportFreeGiftCodeStructure().setMode(1);
MyApp.getInstance().getReportFreeGiftCodeStructure().setDiscountAmount(discountValue);
} else if ("percentage".equals(discountType)) {
MyApp.getInstance().getReportFreeGiftCodeStructure().setMode(2);
// discountValue 為折扣趴數八折=20 換算成總價乘數 0.8
MyApp.getInstance().getReportFreeGiftCodeStructure()
.setDiscountPercentage((100 - discountValue) / 100f);
} else {
openBarcode = true;
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "不支援的折扣類型");
return;
}
getHandlerMain().start(getClass().getSimpleName(), Option.CHANGE_BUY.getOption(), "change buy");
}
@Override
public void onFailure(String message) {
openBarcode = true; // 允許重新掃碼/輸入
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), message);
}
});
}
public void changeBuy() {

View File

@ -284,7 +284,8 @@ public class FontendPickupCodeDialog extends DialogAbstract {
closeDialogCountdown.setCountdown(40);
});
new ReadBarCodeOnScheduler(getHandlerMain()).start(5, TimeUnit.SECONDS);
// 立即開始聽掃描器原本延遲 5 導致對話框剛彈出時掃碼被整個丟掉 時好時壞無回饋
new ReadBarCodeOnScheduler(getHandlerMain()).start(0, TimeUnit.SECONDS);
// 進入頁面時立即檢查取貨口 (使用貨道1作為代表)
// callback null表示檢查通過後不做額外動作僅在檢查失敗時顯示警告
@ -315,7 +316,7 @@ public class FontendPickupCodeDialog extends DialogAbstract {
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
byte[] buffer = new byte[1000];
usbDev.read(buffer, 100);
for (int i=0; i<7; i++) {
for (int i=0; i<9; i++) { // 45 秒讀取窗口覆蓋 40 秒對話框存活時間
if (isRun() && openBarcode) {
int size = usbDev.read(buffer, 5000);
if (size > 5) {