[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:
parent
e8b3185b8a
commit
fc21186a53
@ -10,7 +10,7 @@ plugins {
|
||||
// 避免不同尺寸機台間版本號產生衝突。
|
||||
// =========================================================================
|
||||
def verMinor = 1
|
||||
def verPatch = 76
|
||||
def verPatch = 79
|
||||
|
||||
|
||||
|
||||
|
||||
@ -610,6 +610,15 @@ public class StarCloudAPI {
|
||||
void onFailure(String message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 來店禮驗證回呼(B690):成功回傳折扣資訊。
|
||||
* discountType:amount(折抵金額)/ 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 登入核發的使用者 Token(auth: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
|
||||
|
||||
|
||||
@ -219,6 +219,12 @@ public class BuyDialog extends DialogAbstract {
|
||||
this.binding.buttonFreeGift.setVisibility(
|
||||
MyApp.getInstance().getDevSet().isWelcomeGift() ? View.VISIBLE : View.GONE);
|
||||
|
||||
// 通行碼入口(付款格內 buttonAccessCode,flowType 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());
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user