[FIX] 優化 MQTT 連線生命週期與重啟邏輯
1. 新增 gracefulDisconnect 模式:重啟前主動發布 'restarting' 狀態並正常斷線,抑制 LWT 離線警告。 2. 消除冗餘重啟循環:修改 HomeActivity 與 RestartActivity 協作邏輯,進入購物頁面僅釋放硬體而不殺掉進程,避免多次連線恢復。 3. 遠端指令校準:確保遠端重啟指令後 App 回到 HomeActivity 初始畫面。 4. 解決硬體資源衝突:保留 RestartActivity 的串口釋放流程,修復 App 跳轉卡死問題。
This commit is contained in:
parent
7a990ffc12
commit
5579d937e3
44
.agents/workflows/compiler-apk.md
Normal file
44
.agents/workflows/compiler-apk.md
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
description: 自動編譯 Android APK、輸出至專屬目錄並安裝至實體手機
|
||||
---
|
||||
|
||||
# 編譯與安裝 APK 工作流 (compiler-apk)
|
||||
|
||||
這個 Workflow 主要是協助您一鍵完成 Android 專案的編譯、匯出與安裝。當您呼叫 `/compiler-apk` 時,AI 會自動為您執行以下步驟:
|
||||
|
||||
## 步驟一:設定環境並編譯 Debug APK
|
||||
使用 Java 17 與 Gradle 編譯專屬 Flavor (`S_12_XY_U_Standard_Debug`) 的程式碼。
|
||||
|
||||
```bash
|
||||
// turbo
|
||||
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
||||
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
|
||||
./gradlew :app:assembleS_12_XY_U_Standard_Debug
|
||||
```
|
||||
|
||||
## 步驟二:導出 APK 到 outputs 資料夾
|
||||
將編譯出來的深層 APK 複製到專案最外層的 `outputs` 資料夾,並統一命名為 `TaiwanStar_debug.apk`。
|
||||
|
||||
```bash
|
||||
// turbo
|
||||
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
|
||||
mkdir -p outputs
|
||||
cp app/build/outputs/apk/S_12_XY_U_Standard_/debug/app-S_12_XY_U_Standard_-10_08_2_R-debug.apk outputs/TaiwanStar_debug.apk
|
||||
echo "✅ APK 已經複製到專案根目錄的 outputs 資料夾中"
|
||||
```
|
||||
|
||||
## 步驟三:透過 Windows ADB 安裝到手機 / 機台
|
||||
利用 WSL 串接 Windows 端的 ADB,將熱騰騰的 APK 覆蓋安裝到目前連接的設備上。
|
||||
|
||||
```bash
|
||||
// turbo
|
||||
/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe install -r -d -t /home/mama/projects/TaiwanStar_general_size_s_Chengwai/outputs/TaiwanStar_debug.apk
|
||||
```
|
||||
|
||||
## 步驟四:自動啟動 App
|
||||
安裝完成後,直接在手機上喚醒並啟動應用程式 (`HomeActivity`)。
|
||||
|
||||
```bash
|
||||
// turbo
|
||||
/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe shell am start -n com.unibuy.smartdevice/com.unibuy.smartdevice.HomeActivity
|
||||
```
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,3 +13,4 @@
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
/outputs
|
||||
|
||||
@ -215,6 +215,8 @@ public class HomeActivity extends AppCompatActivityAbstract {
|
||||
httpAPI = new HttpAPI(getHandlerMain());
|
||||
|
||||
binding.startButton.setOnClickListener(v -> {
|
||||
// 經過 RestartActivity 釋放硬體資源(RS232 等),再跳購物頁面
|
||||
// RestartActivity 已修改為不再 gracefulDisconnect + killProcess,MQTT 連線不中斷
|
||||
getTools().gotoActivity(RestartActivity.class);
|
||||
finish();
|
||||
});
|
||||
|
||||
@ -638,6 +638,7 @@ public class MainActivity extends AppCompatActivityAbstract {
|
||||
finishAffinity();
|
||||
// 建議先 log 再 kill
|
||||
getLogs().info("🔄MainActivity - App restarting...");
|
||||
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
// 完全不需要 killProcess()
|
||||
}
|
||||
|
||||
@ -417,7 +417,14 @@ public class MyApp extends Application {
|
||||
serviceIntent.putExtra("apiToken", apiKey);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent);
|
||||
try {
|
||||
startForegroundService(serviceIntent);
|
||||
} catch (Exception e) {
|
||||
// Android 12+ 在 AlarmManager 喚醒時不允許直接啟動 ForegroundService
|
||||
// 改用普通 startService 讓 Service 先啟動,等 Activity 進前台後再提升
|
||||
logs.info("ForegroundService start blocked (background state), falling back to startService");
|
||||
startService(serviceIntent);
|
||||
}
|
||||
} else {
|
||||
startService(serviceIntent);
|
||||
}
|
||||
|
||||
@ -439,6 +439,111 @@ public class StarCloudAPI {
|
||||
/**
|
||||
* 將下載回來的設定儲存到 MyApp 中
|
||||
*/
|
||||
/**
|
||||
* B680: 員工卡身分驗證
|
||||
* 驗證成功後會回傳 code_id (staff_cards 資料表的 ID)
|
||||
*/
|
||||
public void verifyStaffCard(final String cardNo, final VerificationCallback callback) {
|
||||
final String machineId = MyApp.getInstance().getMachine().getMachineID();
|
||||
final String url = BASE_URL + "machine/staff/verify/B680";
|
||||
Log.i(TAG, "Starting B680 request for card: " + cardNo);
|
||||
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int responseCode = -1;
|
||||
String receive = "";
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
// 根據 API 規格,參數名應為 "code"
|
||||
json.put("code", cardNo);
|
||||
|
||||
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");
|
||||
|
||||
// 加入機台 Token 驗證
|
||||
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);
|
||||
|
||||
byte[] bodyBytes = json.toString().getBytes("UTF-8");
|
||||
conn.getOutputStream().write(bodyBytes);
|
||||
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, "B680 Response (" + responseCode + "): " + receive);
|
||||
} catch (final Exception e) {
|
||||
Log.e(TAG, "B680 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);
|
||||
// 根據規格,後台回傳 success: true 且包含 code_id
|
||||
if (res.optBoolean("success", false)) {
|
||||
// 規格顯示 code_id 可能在 data 層級或直接在根層級,這裡做相容處理
|
||||
String codeId = "";
|
||||
if (res.has("data")) {
|
||||
codeId = res.optJSONObject("data").optString("code_id", "");
|
||||
} else {
|
||||
codeId = res.optString("code_id", "");
|
||||
}
|
||||
callback.onSuccess(codeId);
|
||||
} else {
|
||||
callback.onFailure(res.optString("message", "驗證失敗"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
callback.onFailure("解析錯誤: " + finalCode);
|
||||
}
|
||||
} else if (finalCode == 404) {
|
||||
callback.onFailure("查無此員工卡,請確認卡片是否已註冊");
|
||||
} else if (finalCode == 403) {
|
||||
callback.onFailure("此卡片目前無權限使用");
|
||||
} else if (finalCode == 401) {
|
||||
callback.onFailure("系統認證失效,請聯繫管理員");
|
||||
} else {
|
||||
// 嘗試從錯誤訊息中提取 message
|
||||
try {
|
||||
JSONObject res = new JSONObject(finalReceive);
|
||||
callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
|
||||
} catch (Exception e) {
|
||||
callback.onFailure("系統連線錯誤 (" + finalCode + ")");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public interface VerificationCallback {
|
||||
void onSuccess(String codeId);
|
||||
void onFailure(String message);
|
||||
}
|
||||
|
||||
private void saveSettings(JSONObject config) {
|
||||
// 使用 optString 避免欄位缺失導致 JSONException
|
||||
|
||||
|
||||
@ -13,7 +13,9 @@ import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.controller.DevXinYuanController;
|
||||
import com.unibuy.smartdevice.structure.mqtt.CommandAckPayload;
|
||||
import com.unibuy.smartdevice.structure.mqtt.CommandPayload;
|
||||
import com.unibuy.smartdevice.structure.mqtt.ErrorPayload;
|
||||
import com.unibuy.smartdevice.structure.mqtt.HeartbeatPayload;
|
||||
import com.unibuy.smartdevice.structure.mqtt.TransactionFinalizePayload;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
@ -136,6 +138,9 @@ public class MqttManager {
|
||||
}, 0, 60, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 一般斷線(內部或非計畫性使用)。
|
||||
*/
|
||||
public void disconnect() {
|
||||
if (heartbeatExecutor != null && !heartbeatExecutor.isShutdown()) {
|
||||
heartbeatExecutor.shutdownNow();
|
||||
@ -145,6 +150,64 @@ public class MqttManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 優雅重啟斷線(Graceful Disconnect)。
|
||||
*
|
||||
* 依照 MQTT 3.1.1 規範:Client 主動發送 DISCONNECT 封包後正常離線,
|
||||
* Broker 不會觸發 LWT 遺囑訊息,因此可避免管理後台誤報「離線」。
|
||||
*
|
||||
* 呼叫流程:
|
||||
* 1. publishStatus("restarting") — 告知後台為計畫性重啟
|
||||
* 2. 等發布完成後呼叫 client.disconnect() — 正常斷線,LWT 不觸發
|
||||
* 3. 執行 onComplete callback — 呼叫方在此執行 killProcess()
|
||||
*
|
||||
* @param onComplete 斷線完成後的回呼,用來執行 killProcess/System.exit 等後續動作
|
||||
*/
|
||||
public void gracefulDisconnect(Runnable onComplete) {
|
||||
// 先停止心跳,避免斷線期間還在發送
|
||||
if (heartbeatExecutor != null && !heartbeatExecutor.isShutdown()) {
|
||||
heartbeatExecutor.shutdownNow();
|
||||
}
|
||||
|
||||
if (client == null || !client.getState().isConnected()) {
|
||||
// 如果本來就沒有連線,直接執行後續動作
|
||||
Log.i(TAG, "gracefulDisconnect: MQTT not connected, skip directly.");
|
||||
if (onComplete != null) onComplete.run();
|
||||
return;
|
||||
}
|
||||
|
||||
// 發布 restarting 狀態(retain=true,後台可感知這是計畫性重啟)
|
||||
String payload = "{\"status\":\"restarting\"}";
|
||||
String topic = "machine/" + serialNo + "/status";
|
||||
Log.i(TAG, "gracefulDisconnect: publishing restarting status...");
|
||||
|
||||
client.publishWith()
|
||||
.topic(topic)
|
||||
.payload(payload.getBytes(StandardCharsets.UTF_8))
|
||||
.qos(MqttQos.AT_LEAST_ONCE)
|
||||
.retain(true)
|
||||
.send()
|
||||
.whenComplete((publishResult, pubThrowable) -> {
|
||||
if (pubThrowable != null) {
|
||||
Log.w(TAG, "gracefulDisconnect: failed to publish restarting status, disconnecting anyway.");
|
||||
} else {
|
||||
Log.i(TAG, "gracefulDisconnect: restarting status published.");
|
||||
}
|
||||
|
||||
// 無論發布成否,都執行正常 DISCONNECT(避免 LWT 觸發)
|
||||
client.disconnect()
|
||||
.whenComplete((v, disconnThrowable) -> {
|
||||
if (disconnThrowable != null) {
|
||||
Log.w(TAG, "gracefulDisconnect: disconnect error: " + disconnThrowable.getMessage());
|
||||
} else {
|
||||
Log.i(TAG, "gracefulDisconnect: disconnected cleanly, LWT suppressed.");
|
||||
}
|
||||
// 回呼呼叫方,可以安全地執行 killProcess
|
||||
if (onComplete != null) onComplete.run();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void subscribeToCommandTopic() {
|
||||
if (client == null || serialNo == null) return;
|
||||
|
||||
@ -240,6 +303,58 @@ public class MqttManager {
|
||||
});
|
||||
}
|
||||
|
||||
public void publishError(int tid, String errorCode) {
|
||||
if (client == null || !client.getState().isConnected()) return;
|
||||
|
||||
ErrorPayload payloadObj = new ErrorPayload(tid, errorCode);
|
||||
String payload = gson.toJson(payloadObj);
|
||||
String topic = "machine/" + serialNo + "/error";
|
||||
|
||||
client.publishWith()
|
||||
.topic(topic)
|
||||
.payload(payload.getBytes(StandardCharsets.UTF_8))
|
||||
.qos(MqttQos.AT_LEAST_ONCE)
|
||||
.send()
|
||||
.whenComplete((publishResult, throwable) -> {
|
||||
if (throwable != null) {
|
||||
Log.e(TAG, "Failed to publish error to MQTT", throwable);
|
||||
} else {
|
||||
Log.i(TAG, "Hardware error published to " + topic + ": " + payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 發送交易結案訊息 (MQTT Finalize)。
|
||||
* 在 App 端完成所有本地動作(支付 + 出貨)後呼叫。
|
||||
* Topic: machine/[MachineID]/transaction
|
||||
* 取代舊的 B600/B601/B602 分段 HTTP 上報機制。
|
||||
*
|
||||
* @param payload 由 TransactionFinalizeBuilder.build() 產生的完整 payload
|
||||
*/
|
||||
public void publishTransactionFinalize(TransactionFinalizePayload payload) {
|
||||
if (client == null || !client.getState().isConnected()) {
|
||||
Log.e(TAG, "Cannot publish finalize: MQTT not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
String payloadJson = gson.toJson(payload);
|
||||
String topic = "machine/" + serialNo + "/transaction";
|
||||
|
||||
client.publishWith()
|
||||
.topic(topic)
|
||||
.payload(payloadJson.getBytes(StandardCharsets.UTF_8))
|
||||
.qos(MqttQos.AT_LEAST_ONCE)
|
||||
.send()
|
||||
.whenComplete((publishResult, throwable) -> {
|
||||
if (throwable != null) {
|
||||
Log.e(TAG, "Failed to publish transaction finalize", throwable);
|
||||
} else {
|
||||
Log.i(TAG, "Transaction finalize published to " + topic + ": " + payloadJson);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface MqttCommandListener {
|
||||
void onCommandReceived(CommandPayload command);
|
||||
}
|
||||
|
||||
@ -78,15 +78,19 @@ public class MqttService extends Service {
|
||||
if ("reboot".equals(cmd)) {
|
||||
Log.i(TAG, "Executing remote reboot command...");
|
||||
mqttManager.publishCommandAck(cmdId, "success", "Rebooting soon...");
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
|
||||
Intent restartIntent = new Intent(getApplicationContext(), com.unibuy.smartdevice.HomeActivity.class);
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(restartIntent);
|
||||
|
||||
// 徹底殺掉當前進程,讓系統乾淨重啟
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
}, 500); // 縮短為 0.5 秒,確保 ACK 已發出即可
|
||||
// 🚩 優雅重啟:ACK 發出後先 gracefulDisconnect(不觸發 LWT),再 killProcess
|
||||
// 重啟到初始畫面 (HomeActivity);按「購物去」再經 RestartActivity 釋放硬體,不会再次 killProcess
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() ->
|
||||
mqttManager.gracefulDisconnect(() -> {
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
Intent restartIntent = new Intent(getApplicationContext(), com.unibuy.smartdevice.HomeActivity.class);
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(restartIntent);
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
});
|
||||
})
|
||||
, 500); // 0.5 秒確保 ACK 先發出
|
||||
} else if ("lock".equals(cmd)) {
|
||||
Log.i(TAG, "Executing remote lock command...");
|
||||
com.unibuy.smartdevice.database.SettingsDao settingsDao = new com.unibuy.smartdevice.database.SettingsDao(getApplicationContext());
|
||||
@ -108,12 +112,19 @@ public class MqttService extends Service {
|
||||
|
||||
mqttManager.publishCommandAck(cmdId, "success", "Machine unlocked, restarting...");
|
||||
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
|
||||
Intent restartIntent = new Intent(getApplicationContext(), com.unibuy.smartdevice.HomeActivity.class);
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(restartIntent);
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
}, 500);
|
||||
// 🚩 優雅重啟:先 gracefulDisconnect(不觸發 LWT),再 killProcess
|
||||
// 解鎖後重啟到初始畫面 (HomeActivity)
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() ->
|
||||
mqttManager.gracefulDisconnect(() -> {
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
Intent restartIntent = new Intent(getApplicationContext(), com.unibuy.smartdevice.HomeActivity.class);
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(restartIntent);
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
});
|
||||
})
|
||||
, 500);
|
||||
} else if ("nfc_reset".equals(cmd)) {
|
||||
Log.i(TAG, "Executing remote NFC reset command...");
|
||||
try {
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
package com.unibuy.smartdevice.structure.mqtt;
|
||||
|
||||
public class ErrorPayload {
|
||||
private int tid;
|
||||
private String error_code;
|
||||
|
||||
public ErrorPayload(int tid, String error_code) {
|
||||
this.tid = tid;
|
||||
this.error_code = error_code;
|
||||
}
|
||||
|
||||
public int getTid() {
|
||||
return tid;
|
||||
}
|
||||
|
||||
public void setTid(int tid) {
|
||||
this.tid = tid;
|
||||
}
|
||||
|
||||
public String getError_code() {
|
||||
return error_code;
|
||||
}
|
||||
|
||||
public void setError_code(String error_code) {
|
||||
this.error_code = error_code;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,219 @@
|
||||
package com.unibuy.smartdevice.structure.mqtt;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MQTT Finalize 交易結案訊息的資料結構。
|
||||
* 對應後台 machine/[ID]/transaction Topic 的 payload 格式。
|
||||
*
|
||||
* 此訊息在 App 端完成所有本地動作(支付 + 出貨)後,統一發送一次。
|
||||
* 取代原本的分段式 HTTP 上報 (B600/B601/B602)。
|
||||
*/
|
||||
public class TransactionFinalizePayload {
|
||||
|
||||
/** 固定為 "finalize" */
|
||||
private final String action = "finalize";
|
||||
|
||||
/** 機台流水號(由 reportFlowId 填入,若無則以 order_no 代替) */
|
||||
private String flow_id;
|
||||
|
||||
/**
|
||||
* 特定交易的資料庫原始 ID。
|
||||
* B660(取貨碼) / B670(通行碼) / B680(員工卡) 驗證成功後,後台返回的 ID。
|
||||
* 一般現金/電子支付交易此欄位填 0。
|
||||
*/
|
||||
private int code_id;
|
||||
|
||||
/** 訂單資訊 */
|
||||
private OrderPayload order;
|
||||
|
||||
/** 每個貨道的出貨結果列表 */
|
||||
private List<DispenseRecord> dispense;
|
||||
|
||||
// =========== Getters & Setters ===========
|
||||
|
||||
public String getAction() { return action; }
|
||||
|
||||
public String getFlow_id() { return flow_id; }
|
||||
public void setFlow_id(String flow_id) { this.flow_id = flow_id; }
|
||||
|
||||
public int getCode_id() { return code_id; }
|
||||
public void setCode_id(int code_id) { this.code_id = code_id; }
|
||||
|
||||
public OrderPayload getOrder() { return order; }
|
||||
public void setOrder(OrderPayload order) { this.order = order; }
|
||||
|
||||
public List<DispenseRecord> getDispense() { return dispense; }
|
||||
public void setDispense(List<DispenseRecord> dispense) { this.dispense = dispense; }
|
||||
|
||||
// =========== 內部類別:訂單資訊 ===========
|
||||
|
||||
public static class OrderPayload {
|
||||
/** 訂單編號(本地時間戳 UUID) */
|
||||
private String order_no;
|
||||
|
||||
/** 商品原始總金額 */
|
||||
private int original_amount;
|
||||
|
||||
/** 折扣金額 */
|
||||
private int discount_amount;
|
||||
|
||||
/** 實際應付金額 */
|
||||
private int total_amount;
|
||||
|
||||
/** 實際支付金額 */
|
||||
private int pay_amount;
|
||||
|
||||
/** 找零金額 */
|
||||
private int change_amount;
|
||||
|
||||
/** 使用點數 */
|
||||
private int points_used;
|
||||
|
||||
/**
|
||||
* 支付類型代碼:
|
||||
* 1:信用卡, 2:悠遊卡/一卡通, 3:掃碼支付, 4:紙鈔機, 5:通行碼, 6:取貨碼,
|
||||
* 7:來店禮, 8:問卷, 9:零錢, 30:LINE Pay, 31:街口, 32:悠遊付, 33:Pi,
|
||||
* 34:全盈+, 40:會員驗證取貨, 41:員工卡, 60:點數全額折抵。
|
||||
*/
|
||||
private int payment_type;
|
||||
|
||||
/**
|
||||
* 支付狀態:1 = 成功
|
||||
*/
|
||||
private int payment_status;
|
||||
|
||||
/** 支付 Request 原始內容 (JSON String) */
|
||||
private String payment_request;
|
||||
|
||||
/** 支付 Response 原始內容 (JSON String) */
|
||||
private String payment_response;
|
||||
|
||||
/** 會員條碼 */
|
||||
private String member_barcode;
|
||||
|
||||
/** 發票資訊 */
|
||||
private String invoice_info;
|
||||
|
||||
/** 機台本地時間 (yyyy-MM-dd HH:mm:ss) */
|
||||
private String machine_time;
|
||||
|
||||
/** 購買商品列表 */
|
||||
private List<ItemRecord> items;
|
||||
|
||||
// Getters & Setters
|
||||
public String getOrder_no() { return order_no; }
|
||||
public void setOrder_no(String order_no) { this.order_no = order_no; }
|
||||
|
||||
public int getOriginal_amount() { return original_amount; }
|
||||
public void setOriginal_amount(int original_amount) { this.original_amount = original_amount; }
|
||||
|
||||
public int getDiscount_amount() { return discount_amount; }
|
||||
public void setDiscount_amount(int discount_amount) { this.discount_amount = discount_amount; }
|
||||
|
||||
public int getTotal_amount() { return total_amount; }
|
||||
public void setTotal_amount(int total_amount) { this.total_amount = total_amount; }
|
||||
|
||||
public int getPay_amount() { return pay_amount; }
|
||||
public void setPay_amount(int pay_amount) { this.pay_amount = pay_amount; }
|
||||
|
||||
public int getChange_amount() { return change_amount; }
|
||||
public void setChange_amount(int change_amount) { this.change_amount = change_amount; }
|
||||
|
||||
public int getPoints_used() { return points_used; }
|
||||
public void setPoints_used(int points_used) { this.points_used = points_used; }
|
||||
|
||||
public int getPayment_type() { return payment_type; }
|
||||
public void setPayment_type(int payment_type) { this.payment_type = payment_type; }
|
||||
|
||||
public int getPayment_status() { return payment_status; }
|
||||
public void setPayment_status(int payment_status) { this.payment_status = payment_status; }
|
||||
|
||||
public String getPayment_request() { return payment_request; }
|
||||
public void setPayment_request(String payment_request) { this.payment_request = payment_request; }
|
||||
|
||||
public String getPayment_response() { return payment_response; }
|
||||
public void setPayment_response(String payment_response) { this.payment_response = payment_response; }
|
||||
|
||||
public String getMember_barcode() { return member_barcode; }
|
||||
public void setMember_barcode(String member_barcode) { this.member_barcode = member_barcode; }
|
||||
|
||||
public String getInvoice_info() { return invoice_info; }
|
||||
public void setInvoice_info(String invoice_info) { this.invoice_info = invoice_info; }
|
||||
|
||||
public String getMachine_time() { return machine_time; }
|
||||
public void setMachine_time(String machine_time) { this.machine_time = machine_time; }
|
||||
|
||||
public List<ItemRecord> getItems() { return items; }
|
||||
public void setItems(List<ItemRecord> items) { this.items = items; }
|
||||
}
|
||||
|
||||
// =========== 內部類別:購買商品記錄 ===========
|
||||
|
||||
public static class ItemRecord {
|
||||
/** 商品 ID(後台數字 ID,由 productID 字串轉換;若無法解析則填 0) */
|
||||
private int product_id;
|
||||
|
||||
/** 單價 */
|
||||
private int price;
|
||||
|
||||
/** 數量 */
|
||||
private int quantity;
|
||||
|
||||
public ItemRecord(int product_id, int price, int quantity) {
|
||||
this.product_id = product_id;
|
||||
this.price = price;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
public int getProduct_id() { return product_id; }
|
||||
public int getPrice() { return price; }
|
||||
public int getQuantity() { return quantity; }
|
||||
}
|
||||
|
||||
// =========== 內部類別:出貨記錄 ===========
|
||||
|
||||
public static class DispenseRecord {
|
||||
/** 貨道號碼(字串型態) */
|
||||
private String slot_no;
|
||||
|
||||
/** 商品 ID */
|
||||
private int product_id;
|
||||
|
||||
/** 商品金額 */
|
||||
private int amount;
|
||||
|
||||
/** 使用點數 */
|
||||
private int points_used;
|
||||
|
||||
/**
|
||||
* 出貨狀態:1 = 成功, 0 = 失敗
|
||||
*/
|
||||
private int dispense_status;
|
||||
|
||||
/** 出貨後剩餘庫存 */
|
||||
private int remaining_stock;
|
||||
|
||||
/** 出貨完成時的機台本地時間 (yyyy-MM-dd HH:mm:ss) */
|
||||
private String machine_time;
|
||||
|
||||
public DispenseRecord(String slot_no, int product_id, int amount, int points_used,
|
||||
int dispense_status, int remaining_stock, String machine_time) {
|
||||
this.slot_no = slot_no;
|
||||
this.product_id = product_id;
|
||||
this.amount = amount;
|
||||
this.points_used = points_used;
|
||||
this.dispense_status = dispense_status;
|
||||
this.remaining_stock = remaining_stock;
|
||||
this.machine_time = machine_time;
|
||||
}
|
||||
|
||||
public String getSlot_no() { return slot_no; }
|
||||
public int getProduct_id() { return product_id; }
|
||||
public int getAmount() { return amount; }
|
||||
public int getPoints_used() { return points_used; }
|
||||
public int getDispense_status() { return dispense_status; }
|
||||
public int getRemaining_stock() { return remaining_stock; }
|
||||
public String getMachine_time() { return machine_time; }
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import com.unibuy.smartdevice.external.mqtt.MqttManager;
|
||||
import com.unibuy.smartdevice.ui.FontendActivity;
|
||||
|
||||
public class AppUtils {
|
||||
@ -18,6 +19,8 @@ public class AppUtils {
|
||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
alarmManager.setExact(AlarmManager.RTC, System.currentTimeMillis() + 500, pendingIntent);
|
||||
|
||||
|
||||
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
package com.unibuy.smartdevice.tools;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 機台本地交易流水號生成器。
|
||||
*
|
||||
* 流水號格式:YYYYMMDDXXXX
|
||||
* - YYYYMMDD:當日日期
|
||||
* - XXXX:當日從 0001 起算的 4 位序號(最多 9999 筆/日,超過後循環)
|
||||
*
|
||||
* 範例:202605110001、202605110002、...
|
||||
*
|
||||
* 每天重置:當偵測到日期改變時,序號自動從 0001 重新開始。
|
||||
* 持久化:流水號存放於 SharedPreferences,App 重啟後不會遺失。
|
||||
*/
|
||||
public class FlowIdGenerator {
|
||||
|
||||
private static final String PREFS_NAME = "FlowIdPrefs";
|
||||
private static final String KEY_LAST_DATE = "last_date";
|
||||
private static final String KEY_COUNTER = "counter";
|
||||
private static final SimpleDateFormat DATE_FORMAT =
|
||||
new SimpleDateFormat("yyyyMMdd", Locale.getDefault());
|
||||
|
||||
/**
|
||||
* 取得下一個流水號(每次呼叫自動遞增)。
|
||||
*
|
||||
* @param context Android Context
|
||||
* @return 格式為 "YYYYMMDDXXXX" 的流水號字串,例如 "202605110001"
|
||||
*/
|
||||
public static synchronized String next(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
String today = DATE_FORMAT.format(new Date());
|
||||
String lastDate = prefs.getString(KEY_LAST_DATE, "");
|
||||
int counter;
|
||||
|
||||
if (!today.equals(lastDate)) {
|
||||
// 新的一天,重置計數器
|
||||
counter = 1;
|
||||
} else {
|
||||
// 同一天,遞增計數器(超過 9999 後循環回 1)
|
||||
counter = prefs.getInt(KEY_COUNTER, 0) + 1;
|
||||
if (counter > 9999) counter = 1;
|
||||
}
|
||||
|
||||
// 持久化
|
||||
prefs.edit()
|
||||
.putString(KEY_LAST_DATE, today)
|
||||
.putInt(KEY_COUNTER, counter)
|
||||
.apply();
|
||||
|
||||
// 格式化:日期 + 4 位補零序號
|
||||
return today + String.format(Locale.getDefault(), "%04d", counter);
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@ import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.controller.DevController;
|
||||
import com.unibuy.smartdevice.controller.DevXinYuanController;
|
||||
import com.unibuy.smartdevice.devices.PortTools;
|
||||
import com.unibuy.smartdevice.external.mqtt.MqttManager;
|
||||
|
||||
/**
|
||||
* 遠端出貨管理器
|
||||
@ -57,6 +58,7 @@ public class ProductDispenseManager {
|
||||
public void readBufferOnScheduler(DevController devController, HandlerMain handlerMain) {
|
||||
boolean isCMD = false;
|
||||
boolean isStatusOk = false;
|
||||
String errorDetail = "";
|
||||
|
||||
for (byte[] buffer : devController.getReadBufferByMax()) {
|
||||
if (buffer[0] == 0x02) { // VMC_02_CHECK_SLOT_STATUS
|
||||
@ -65,6 +67,8 @@ public class ProductDispenseManager {
|
||||
// 01: OK, 02: PDT_EMPTY (有些機型空也會允許出貨指令), 04: TMP_USE
|
||||
if (buffer[1] == 0x01 || buffer[1] == 0x02 || buffer[1] == 0x04) {
|
||||
isStatusOk = true;
|
||||
} else {
|
||||
errorDetail = String.format("%02X%02X", buffer[0], buffer[1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -74,6 +78,10 @@ public class ProductDispenseManager {
|
||||
if (isStatusOk) {
|
||||
mainHandler.post(() -> executeActualDispense(slot));
|
||||
} else {
|
||||
// Error during CMD 01
|
||||
if (!errorDetail.isEmpty()) {
|
||||
MqttManager.getInstance().publishError(slot, errorDetail);
|
||||
}
|
||||
mainHandler.post(() -> listener.onFailure("貨道狀態異常,無法出貨"));
|
||||
}
|
||||
} else {
|
||||
@ -139,6 +147,10 @@ public class ProductDispenseManager {
|
||||
listener.onSuccess();
|
||||
});
|
||||
} else if (isFailed) {
|
||||
// Error during CMD 04: construct error code e.g. "0403"
|
||||
String errCode = String.format("04%s", errorDetail);
|
||||
MqttManager.getInstance().publishError(slot, errCode);
|
||||
|
||||
String finalErrorDetail = errorDetail;
|
||||
mainHandler.post(() -> listener.onFailure("出貨失敗,錯誤代碼: " + finalErrorDetail));
|
||||
} else {
|
||||
|
||||
@ -0,0 +1,172 @@
|
||||
package com.unibuy.smartdevice.tools;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.structure.ReportFlowInfoStructure;
|
||||
import com.unibuy.smartdevice.structure.mqtt.TransactionFinalizePayload;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 交易結案 Payload 的組裝工具。
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 在支付成功進入出貨流程前,建立此 Builder 實例。
|
||||
* 2. 每次一個貨道出貨完成,呼叫 {@link #addDispenseRecord} 加入結果。
|
||||
* 3. 全部出貨完成後,呼叫 {@link #build()} 取得完整的 Payload。
|
||||
*
|
||||
* 此 Builder 取代舊的 B600 / B601 / B602 分段 HTTP 上報機制。
|
||||
*/
|
||||
public class TransactionFinalizeBuilder {
|
||||
|
||||
private static final String TAG = "TransactionFinalizeBuilder";
|
||||
private static final SimpleDateFormat SDF =
|
||||
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||
|
||||
/** 訂單建立時間(支付完成時的時間點) */
|
||||
private final String orderMachineTime;
|
||||
|
||||
/** 機台本地流水號(格式:YYYYMMDDXXXX,例如 202605110001) */
|
||||
private final String flowId;
|
||||
|
||||
/** 累積的出貨結果清單 */
|
||||
private final List<TransactionFinalizePayload.DispenseRecord> dispenseRecords = new ArrayList<>();
|
||||
|
||||
public TransactionFinalizeBuilder() {
|
||||
this.orderMachineTime = SDF.format(new Date());
|
||||
// 在進入出貨流程時立即生成本次交易流水號(一次交易只生成一次)
|
||||
this.flowId = FlowIdGenerator.next(MyApp.getInstance());
|
||||
Log.i(TAG, "TransactionFinalizeBuilder created, flow_id=" + this.flowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入一筆出貨結果記錄(每個貨道出貨後呼叫一次)。
|
||||
*
|
||||
* @param slotNo 貨道號碼(整數)
|
||||
* @param productId 商品 ID(整數,後台 ID)
|
||||
* @param amount 此貨道的商品金額
|
||||
* @param pointsUsed 使用點數
|
||||
* @param isSuccess 是否出貨成功
|
||||
* @param remainingStock 出貨後剩餘庫存
|
||||
*/
|
||||
public void addDispenseRecord(int slotNo, int productId, int amount,
|
||||
int pointsUsed, boolean isSuccess, int remainingStock) {
|
||||
TransactionFinalizePayload.DispenseRecord record = new TransactionFinalizePayload.DispenseRecord(
|
||||
String.valueOf(slotNo),
|
||||
productId,
|
||||
amount,
|
||||
pointsUsed,
|
||||
isSuccess ? 1 : 0,
|
||||
remainingStock,
|
||||
SDF.format(new Date())
|
||||
);
|
||||
dispenseRecords.add(record);
|
||||
Log.d(TAG, "Added dispense record: slot=" + slotNo + " status=" + (isSuccess ? "success" : "failed"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 組裝完整的 TransactionFinalizePayload。
|
||||
* 應在所有出貨流程結束後呼叫。
|
||||
*
|
||||
* @return 完整的 MQTT finalize payload
|
||||
*/
|
||||
public TransactionFinalizePayload build() {
|
||||
ReportFlowInfoStructure flowInfo = MyApp.getInstance().getReportFlowInfoData();
|
||||
|
||||
TransactionFinalizePayload payload = new TransactionFinalizePayload();
|
||||
|
||||
// --- flow_id:機台本地流水號 (YYYYMMDDXXXX) ---
|
||||
// 由 FlowIdGenerator 在建立 Builder 時生成,不依賴後端 API 回傳。
|
||||
payload.setFlow_id(flowId);
|
||||
|
||||
// --- code_id:特殊驗證碼 ID (B660/B670/B680 取得) ---
|
||||
// 一般現金/電子支付交易為 0。
|
||||
String accessCodeId = MyApp.getInstance().getReportAccessCodeStructure().getAccessCodeId();
|
||||
try {
|
||||
payload.setCode_id(accessCodeId != null && !accessCodeId.isEmpty()
|
||||
? Integer.parseInt(accessCodeId) : 0);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Cannot parse code_id: " + accessCodeId);
|
||||
payload.setCode_id(0);
|
||||
}
|
||||
|
||||
// --- 組裝 order ---
|
||||
TransactionFinalizePayload.OrderPayload order = buildOrderPayload(flowInfo);
|
||||
payload.setOrder(order);
|
||||
|
||||
// --- 出貨記錄 ---
|
||||
payload.setDispense(new ArrayList<>(dispenseRecords));
|
||||
|
||||
Log.i(TAG, "TransactionFinalizePayload built: flow_id=" + flowId
|
||||
+ ", items=" + order.getItems().size()
|
||||
+ ", dispense=" + dispenseRecords.size());
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 從 MyApp 的全域狀態組裝 OrderPayload。
|
||||
*/
|
||||
private TransactionFinalizePayload.OrderPayload buildOrderPayload(ReportFlowInfoStructure flowInfo) {
|
||||
TransactionFinalizePayload.OrderPayload order = new TransactionFinalizePayload.OrderPayload();
|
||||
|
||||
order.setOrder_no(flowInfo.getOrderId());
|
||||
|
||||
// 金額資訊
|
||||
int totalAmount = flowInfo.getTotalPrice();
|
||||
order.setOriginal_amount(totalAmount);
|
||||
order.setDiscount_amount(0); // TODO: 如有折扣活動需進一步填入
|
||||
order.setTotal_amount(totalAmount);
|
||||
order.setPay_amount(totalAmount);
|
||||
order.setChange_amount(flowInfo.getGiveChange());
|
||||
order.setPoints_used(flowInfo.getUsePoints());
|
||||
|
||||
// 支付類型:flowType 直接對應 payment_type
|
||||
order.setPayment_type(flowInfo.getFlowType());
|
||||
order.setPayment_status(1); // 走到出貨流程代表支付已成功
|
||||
|
||||
// 支付流程原始資料
|
||||
order.setPayment_request(
|
||||
flowInfo.getFlowSendInfo() != null ? flowInfo.getFlowSendInfo() : "");
|
||||
order.setPayment_response(
|
||||
flowInfo.getFlowRequestInfo() != null ? flowInfo.getFlowRequestInfo() : "");
|
||||
|
||||
// 會員資訊
|
||||
order.setMember_barcode(
|
||||
flowInfo.getFlowBarCode() != null ? flowInfo.getFlowBarCode() : "");
|
||||
|
||||
// 發票資訊
|
||||
order.setInvoice_info(
|
||||
flowInfo.getInvoiceInfo() != null ? flowInfo.getInvoiceInfo() : "");
|
||||
|
||||
// 機台時間
|
||||
order.setMachine_time(orderMachineTime);
|
||||
|
||||
// --- 組裝購買商品清單 (items) ---
|
||||
List<TransactionFinalizePayload.ItemRecord> items = new ArrayList<>();
|
||||
List<BuyStructure> buyList = MyApp.getInstance().getBuyList();
|
||||
if (buyList != null) {
|
||||
for (BuyStructure buy : buyList) {
|
||||
int productId = 0;
|
||||
try {
|
||||
productId = Integer.parseInt(buy.getProduct().getProductID());
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Cannot parse productID to int: " + buy.getProduct().getProductID());
|
||||
}
|
||||
int price = buy.getProduct().getRealPrice();
|
||||
int quantity = buy.getCount() > 0 ? buy.getCount() : 1;
|
||||
items.add(new TransactionFinalizePayload.ItemRecord(productId, price, quantity));
|
||||
}
|
||||
}
|
||||
order.setItems(items);
|
||||
|
||||
return order;
|
||||
}
|
||||
}
|
||||
@ -387,6 +387,7 @@ public class FontendActivity extends AppCompatActivityAbstract {
|
||||
finishAffinity();
|
||||
// 建議先 log 再 kill
|
||||
getLogs().info("🔄MainActivity - App restarting...");
|
||||
|
||||
Process.killProcess(Process.myPid());
|
||||
}
|
||||
|
||||
@ -755,6 +756,7 @@ public class FontendActivity extends AppCompatActivityAbstract {
|
||||
Intent intent = new Intent(getApplicationContext(), com.unibuy.smartdevice.HomeActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(intent);
|
||||
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
});
|
||||
|
||||
@ -1,15 +1,11 @@
|
||||
package com.unibuy.smartdevice.ui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.Settings;
|
||||
|
||||
import com.unibuy.smartdevice.AppCompatActivityAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
@ -17,6 +13,7 @@ import com.unibuy.smartdevice.databinding.ActivityRestartBinding;
|
||||
import com.unibuy.smartdevice.devices.Rs232Dev;
|
||||
import com.unibuy.smartdevice.exception.LogsIOException;
|
||||
import com.unibuy.smartdevice.exception.LogsUnsupportedOperationException;
|
||||
import com.unibuy.smartdevice.external.mqtt.MqttManager;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
|
||||
@ -95,32 +92,15 @@ public class RestartActivity extends AppCompatActivityAbstract {
|
||||
restartApp();
|
||||
}
|
||||
|
||||
@SuppressLint("ScheduleExactAlarm")
|
||||
private void restartApp() {
|
||||
// Intent intent = new Intent(getApplicationContext(), FontendActivity.class);
|
||||
// intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
//
|
||||
// PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
// getApplicationContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE);
|
||||
//
|
||||
// AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
|
||||
// alarmManager.setExact(AlarmManager.RTC, System.currentTimeMillis() + 1000, pendingIntent);
|
||||
//
|
||||
// android.os.Process.killProcess(android.os.Process.myPid());
|
||||
|
||||
// 🚩 硬體資源已在 onCreate() 釋放完畢,直接跳購物頁面
|
||||
// 不需要 gracefulDisconnect + killProcess:MQTT 已在線,保持連線不中斷
|
||||
Intent intent = new Intent(getApplicationContext(), FontendActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
getApplicationContext().startActivity(intent);
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
}, 200);
|
||||
|
||||
//finishAffinity();
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
|
||||
public static class DeviceResourceManager {
|
||||
|
||||
private static DeviceResourceManager instance;
|
||||
|
||||
@ -56,28 +56,7 @@ public class SystemSetActivity extends AppCompatActivityAbstract {
|
||||
finish();
|
||||
});
|
||||
|
||||
// IC 卡讀取功能實作
|
||||
binding.editTextCardNo.requestFocus();
|
||||
binding.buttonClearCardNo.setOnClickListener(v -> {
|
||||
binding.editTextCardNo.setText("");
|
||||
binding.editTextCardNo.requestFocus();
|
||||
});
|
||||
|
||||
binding.editTextCardNo.setOnKeyListener((v, keyCode, event) -> {
|
||||
if (keyCode == android.view.KeyEvent.KEYCODE_ENTER && event.getAction() == android.view.KeyEvent.ACTION_UP) {
|
||||
// 當讀卡機輸入完成並送出 Enter 時的處理 (如果需要)
|
||||
getLogs().info("IC 卡號輸入完成: " + binding.editTextCardNo.getText().toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// 避免點擊其他地方導致失去焦點無法讀卡,可在必要時重新獲取焦點
|
||||
binding.editTextCardNo.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
// 如果需要強制聚焦,可以在這裡處理,但通常讓使用者手動點選回欄位即可
|
||||
}
|
||||
});
|
||||
// IC 卡讀取功能已移除
|
||||
}
|
||||
|
||||
private void updateCheckBoxes() {
|
||||
|
||||
@ -86,6 +86,37 @@ public class RecyclerVmcSlotListAdpter extends RecyclerView.Adapter<RecyclerVmcS
|
||||
holder.binding.editCount.setOnEditorActionListener(new editCountOnEditorActionListener(holder.binding, position));
|
||||
holder.binding.editCount.setOnClickListener(new editCountOnClickListener(holder.binding, slot.getCount()));
|
||||
|
||||
// 修正:移除不穩定的失去焦點邏輯,改用即時監聽 (TextWatcher)
|
||||
// 先移除舊的監聽器(如果有),避免 RecyclerView 重用 View 時產生衝突
|
||||
if (holder.binding.editCount.getTag() instanceof android.text.TextWatcher) {
|
||||
holder.binding.editCount.removeTextChangedListener((android.text.TextWatcher) holder.binding.editCount.getTag());
|
||||
}
|
||||
|
||||
android.text.TextWatcher textWatcher = new android.text.TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(android.text.Editable s) {
|
||||
// 只有在輸入框有焦點(使用者正在打字)時才更新資料,避免 setText 觸發循環
|
||||
if (holder.binding.editCount.hasFocus()) {
|
||||
int newCount = 0;
|
||||
if (s.length() > 0) {
|
||||
try {
|
||||
newCount = Integer.parseInt(s.toString());
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
slotList.get(position).setCount(newCount);
|
||||
holder.binding.textCount.setText(String.valueOf(newCount));
|
||||
}
|
||||
}
|
||||
};
|
||||
holder.binding.editCount.addTextChangedListener(textWatcher);
|
||||
holder.binding.editCount.setTag(textWatcher); // 將監聽器存入 Tag 以便下次重用時移除
|
||||
|
||||
tools.setImmHideByFocusChange(holder.binding.editCount);
|
||||
}
|
||||
|
||||
|
||||
@ -112,6 +112,12 @@ public class VmcSlotListActivity extends AppCompatActivityAbstract {
|
||||
binding.buttonSave.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// 強制清除目前焦點,確保輸入框 (editCount) 觸發失去焦點儲存事件
|
||||
View currentFocus = getCurrentFocus();
|
||||
if (currentFocus != null) {
|
||||
currentFocus.clearFocus();
|
||||
}
|
||||
|
||||
// 1. 本地儲存
|
||||
saveData();
|
||||
|
||||
|
||||
@ -230,109 +230,36 @@ public class CashPayDialog extends DialogAbstract {
|
||||
}
|
||||
|
||||
/**
|
||||
* 顯示出貨對話框,設置金流類型並調用 B600 接口
|
||||
* 在 B600 調用後延遲 2 秒再顯示 DispatchDialog,以確保 B600 完成後再調用 B602
|
||||
* [MQTT Finalize 重構]
|
||||
* 顯示出貨對話框。
|
||||
* 原本會先呼叫 B600 再延遲 2 秒跳轉,此設計因 Race Condition 風險已廢棄。
|
||||
* 現在改為直接將支付資訊寫入 MyApp 全域狀態,再跳轉至 DispatchDialog。
|
||||
* 完整的交易資料將在所有出貨動作完成後,由 DispatchDialog 統一透過 MQTT Finalize 送出。
|
||||
*/
|
||||
private void showDispatchDialog() {
|
||||
if (getCtx() != null) {
|
||||
// 設置現金支付的 flowType 為 9
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowType(9);
|
||||
if (getCtx() == null) return;
|
||||
|
||||
// 調用 B600 接口
|
||||
callB600AndProceed();
|
||||
// 填入現金支付資訊至全域狀態(供 TransactionFinalizeBuilder 使用)
|
||||
com.unibuy.smartdevice.structure.ReportFlowInfoStructure flowInfo =
|
||||
MyApp.getInstance().getReportFlowInfoData();
|
||||
|
||||
// 延遲 2 秒後再顯示 DispatchDialog,確保 B600 完成後再調用 B602
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
flowInfo.setFlowType(9); // 現金支付
|
||||
flowInfo.setFlowSendInfo("收現金:" + inCashSAVE + "元/找零:" + redundantCash + "元");
|
||||
flowInfo.setFlowRequestInfo(""); // 現金無原始請求字串
|
||||
flowInfo.setOrderId(com.unibuy.smartdevice.tools.Tools.generateTimeBasedUUID());
|
||||
flowInfo.setTotalPrice(getTotalPrice());
|
||||
flowInfo.setFlowBarCode(""); // 現金交易無條碼
|
||||
flowInfo.setFlowStatusType(1); // 1 = 成功
|
||||
flowInfo.setGiveChange(redundantCash);
|
||||
flowInfo.setUsePoints(0);
|
||||
|
||||
if (!isDispatchDialogShown) {
|
||||
isDispatchDialogShown = true;
|
||||
changeDialog(DispatchDialog.class);
|
||||
}
|
||||
|
||||
}, 2000);
|
||||
// 現金支付無需等待後端確認,直接跳轉出貨
|
||||
if (!isDispatchDialogShown) {
|
||||
isDispatchDialogShown = true;
|
||||
changeDialog(DispatchDialog.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 調用 B600 接口,上傳交易資訊
|
||||
*/
|
||||
private void callB600AndProceed() {
|
||||
try {
|
||||
// 1. 準備 B600 請求參數
|
||||
ReportFlowInfoStructure reportFlowInfoData = MyApp.getInstance().getReportFlowInfoData();
|
||||
|
||||
// 設置 B600 參數
|
||||
reportFlowInfoData.setFlowType(9); // 現金支付
|
||||
reportFlowInfoData.setFlowSendInfo("收現金:" + inCashSAVE + "元/找零:" + redundantCash + "元"); // req4 空字串
|
||||
reportFlowInfoData.setFlowRequestInfo(""); // req5 空字串
|
||||
MyApp.getInstance().getReportFlowInfoData().setOrderId(Tools.generateTimeBasedUUID());
|
||||
MyApp.getInstance().getPostInvoiceByGreenData()
|
||||
.setOrderId(MyApp.getInstance().getReportFlowInfoData().getOrderId());
|
||||
reportFlowInfoData.setProductId(MyApp.getInstance().getBuyList().get(0).getProduct().getProductID()); // req6
|
||||
// 商品ID
|
||||
reportFlowInfoData.setTotalPrice(getTotalPrice()); // req7 商品價格
|
||||
// ✅ 保留已有的發票信息,不強制清空
|
||||
// reportFlowInfoData.setInvoiceInfo(""); // 移除這行,保留使用者在發票對話框輸入的資料
|
||||
reportFlowInfoData.setOrderId(Tools.generateTimeBasedUUID()); // req9 訂單ID
|
||||
reportFlowInfoData.setFlowBarCode(""); // req10 空字串
|
||||
reportFlowInfoData.setFlowStatusType(1); // req12 設為1表示成功
|
||||
reportFlowInfoData.setGiveChange(0); // req13 找零金額
|
||||
reportFlowInfoData.setUsePoints(0); // req14 設為0
|
||||
|
||||
// 設置購物車信息
|
||||
JSONArray shoppingCartArray = new JSONArray();
|
||||
JSONObject cartItem = new JSONObject();
|
||||
cartItem.put("pid", "0");
|
||||
cartItem.put("amount", "0");
|
||||
cartItem.put("num", "0");
|
||||
shoppingCartArray.put(cartItem);
|
||||
reportFlowInfoData.setShoppingCartInfoByJsonArray(shoppingCartArray); // req16
|
||||
|
||||
// 2. 調用 B600
|
||||
|
||||
final HttpAPI[] httpAPIRef = new HttpAPI[1];
|
||||
httpAPIRef[0] = new HttpAPI(new HandlerMain(getCtx(), getLogs()) {
|
||||
@Override
|
||||
protected void execute(Context context, int commandCode, String message) {
|
||||
if (commandCode == HttpAPI.Option.REPORT_FLOW_RESPONSE.getOption()) {
|
||||
// 保存金流ID到 MyApp
|
||||
String flowId = message;
|
||||
MyApp.getInstance().getReportFlowInfoData().setReportFlowId(flowId);
|
||||
|
||||
// 同時保存金流ID到 ReportFlowInfoData
|
||||
ReportFlowInfoStructure flowInfo = MyApp.getInstance().getReportFlowInfoData();
|
||||
if (flowInfo != null) {
|
||||
flowInfo.setReportFlowId(flowId);
|
||||
}
|
||||
|
||||
// ✅ 檢查是否有發票信息,有的話調用綠界科技開立發票
|
||||
if (!MyApp.getInstance().getReportFlowInfoData().getInvoiceInfo().isEmpty()) {
|
||||
try {
|
||||
httpAPIRef[0].postInvoiceByGreen(MyApp.getInstance().getPostInvoiceByGreenData());
|
||||
} catch (Exception e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
}
|
||||
} else if (commandCode == HttpAPI.Option.GREEN_INVOICE_RESPONSE.getOption()) {
|
||||
// ✅ 綠界發票開立成功
|
||||
getLogs().info("綠界發票開立成功(現金支付): " + message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 調用 B600
|
||||
httpAPIRef[0].reportFlowInfo(reportFlowInfoData);
|
||||
|
||||
} catch (Exception e) {
|
||||
getLogs().error(ErrorCode.UNKNOWN_ERROR, "調用 B600 失敗: " + e.getMessage());
|
||||
// 如果調用失敗,仍然進入出貨流程,但可能會有問題
|
||||
if (!isDispatchDialogShown) {
|
||||
isDispatchDialogShown = true;
|
||||
changeDialog(DispatchDialog.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// ////
|
||||
|
||||
// 接收設備的回應
|
||||
/**
|
||||
|
||||
@ -23,6 +23,7 @@ import com.unibuy.smartdevice.controller.DevController;
|
||||
import com.unibuy.smartdevice.controller.DevDigitalDisplayController;
|
||||
import com.unibuy.smartdevice.controller.DevElectricController;
|
||||
import com.unibuy.smartdevice.database.SlotsDao;
|
||||
|
||||
import com.unibuy.smartdevice.databinding.DialogDispatchBinding;
|
||||
import com.unibuy.smartdevice.devices.ComPort;
|
||||
import com.unibuy.smartdevice.devices.PortTools;
|
||||
@ -35,6 +36,7 @@ import com.unibuy.smartdevice.exception.LogsSecurityException;
|
||||
import com.unibuy.smartdevice.exception.LogsSettingEmptyException;
|
||||
import com.unibuy.smartdevice.exception.LogsUnsupportedOperationException;
|
||||
import com.unibuy.smartdevice.external.HttpAPI;
|
||||
import com.unibuy.smartdevice.external.mqtt.MqttManager;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.structure.DispatchStructure;
|
||||
import com.unibuy.smartdevice.structure.ProductStructure;
|
||||
@ -44,6 +46,7 @@ import com.unibuy.smartdevice.structure.SlotStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
import com.unibuy.smartdevice.tools.TransactionFinalizeBuilder;
|
||||
import com.unibuy.smartdevice.ui.FontendActivity;
|
||||
import com.unibuy.smartdevice.ui.recycler.RecyclerDialogDispatchListAdpter;
|
||||
import com.unibuy.smartdevice.ui.tools.ImageGlide;
|
||||
@ -144,14 +147,9 @@ public class DispatchDialog extends DialogAbstract {
|
||||
closeButtonOk();
|
||||
break;
|
||||
case REPORT_DISPATCH:
|
||||
if (isReportDispatch) {
|
||||
try {
|
||||
isReportDispatch = false;
|
||||
new HttpAPI(getHandlerMain()).reportDispatchInfo(MyApp.getInstance().getReportDispatchInfoData());
|
||||
} catch (LogsSettingEmptyException | LogsParseException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
}
|
||||
// [已停用] 舊的 B602 HTTP 分段上報,改由 MQTT Finalize 統一結案
|
||||
// 此事件仍保留以維持向下相容,但不執行任何 HTTP 呼叫
|
||||
getLogs().debug("REPORT_DISPATCH event received but skipped (MQTT Finalize mode)");
|
||||
break;
|
||||
case SET_MESSAGE_TEXT:
|
||||
setTextViewSmall(message);
|
||||
@ -223,6 +221,8 @@ public class DispatchDialog extends DialogAbstract {
|
||||
private HandlerMainCountdown buttonControlCountdown;
|
||||
private HttpAPI httpAPI;
|
||||
private final List<String> deliveryLogs = new ArrayList<>();
|
||||
/** MQTT Finalize 模式:累積本次交易的所有出貨記錄,最後統一送出 */
|
||||
private TransactionFinalizeBuilder finalizeBuilder;
|
||||
|
||||
public DispatchDialog(Context context, HandlerMain srcHandlerMain) {
|
||||
super(context, srcHandlerMain);
|
||||
@ -239,6 +239,8 @@ public class DispatchDialog extends DialogAbstract {
|
||||
isReportDispatch = true;
|
||||
tvCountdown = binding.tvCountdown;
|
||||
httpAPI = new HttpAPI(getHandlerMain());
|
||||
// 初始化 Finalize Builder(在支付完成、進入出貨流程時建立)
|
||||
finalizeBuilder = new TransactionFinalizeBuilder();
|
||||
|
||||
MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
|
||||
|
||||
@ -418,6 +420,7 @@ public class DispatchDialog extends DialogAbstract {
|
||||
|
||||
boolean isCMD = false;
|
||||
boolean isStatus = false;
|
||||
String errorDetail = "";
|
||||
|
||||
for (byte[] buffer: devController.getReadBufferByMax()) {
|
||||
if (buffer[0] == 0x02) {
|
||||
@ -427,6 +430,8 @@ public class DispatchDialog extends DialogAbstract {
|
||||
if (buffer[1] == 0x01 || buffer[1] == 0x02 || buffer[1] == 0x04) {
|
||||
isStatus = true;
|
||||
break;
|
||||
} else {
|
||||
errorDetail = String.format("%02X%02X", buffer[0], buffer[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -440,6 +445,9 @@ public class DispatchDialog extends DialogAbstract {
|
||||
MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot), 5000);
|
||||
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().getProduct(slot));
|
||||
} else {
|
||||
if (!errorDetail.isEmpty()) {
|
||||
MqttManager.getInstance().publishError(slot, errorDetail);
|
||||
}
|
||||
buttonControlCountdown.shutdown();
|
||||
getSrcHandlerMain().start(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(), getCtx().getString(R.string.check_whether_goods_have_been_taken_out_before_confirming_collection));//請檢查商品有無取出再確認取貨
|
||||
}
|
||||
@ -487,6 +495,7 @@ public class DispatchDialog extends DialogAbstract {
|
||||
boolean isCMD = false;
|
||||
boolean inTheMiddle = false;
|
||||
boolean isSuccess = false;
|
||||
String errorDetail = "";
|
||||
|
||||
for (byte[] buffer: devController.getReadBufferByMax()) {
|
||||
if (buffer[0] == 0x04) {
|
||||
@ -510,6 +519,9 @@ public class DispatchDialog extends DialogAbstract {
|
||||
isSuccess = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// If it's a known error code
|
||||
errorDetail = String.format("04%02X", buffer[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -550,6 +562,9 @@ public class DispatchDialog extends DialogAbstract {
|
||||
MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot),5000L);
|
||||
}
|
||||
} else {
|
||||
if (!errorDetail.isEmpty()) {
|
||||
MqttManager.getInstance().publishError(slot, errorDetail);
|
||||
}
|
||||
buttonControlCountdown.shutdown();
|
||||
getSrcHandlerMain().send(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(), getCtx().getString(R.string.product_delivery_issues_contact_the_counter_window));
|
||||
}
|
||||
@ -794,38 +809,20 @@ public class DispatchDialog extends DialogAbstract {
|
||||
* 3)dispatchStatusType欄位寫入0,代表成功
|
||||
* */
|
||||
int price = product.getRealPrice() * count;
|
||||
MyApp.getInstance().setReportDispatchInfoData(new ReportDispatchInfoStructure(
|
||||
MyApp.getInstance().getReportFlowInfoData().getReportFlowId(),
|
||||
MyApp.getInstance().getReportFlowInfoData().getFlowType(),
|
||||
product.getProductID(),
|
||||
slot,
|
||||
price*increase,
|
||||
MyApp.getInstance().getReportFlowInfoData().getOrderId(),
|
||||
remaining,
|
||||
MyApp.getInstance().getReportFlowInfoData().getFlowBarCode(),
|
||||
"",
|
||||
0,//dispatchStatusType欄位寫入0,代表成功
|
||||
price,
|
||||
0
|
||||
));
|
||||
|
||||
isReportDispatch = true;
|
||||
if (isReportDispatch) {
|
||||
try {
|
||||
isReportDispatch = false;
|
||||
httpAPI.reportDispatchInfo(MyApp.getInstance().getReportDispatchInfoData());
|
||||
} catch (LogsSettingEmptyException | LogsParseException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
}
|
||||
// [MQTT Finalize 模式] 累積出貨成功記錄,最後統一送出
|
||||
int productIdInt = 0;
|
||||
try { productIdInt = Integer.parseInt(product.getProductID()); } catch (Exception ignored) {}
|
||||
finalizeBuilder.addDispenseRecord(slot, productIdInt, price * increase, 0, true, remaining);
|
||||
getLogs().info("[Finalize] 已累積出貨記錄: slot=" + slot + ", product=" + productIdInt + ", remaining=" + remaining);
|
||||
|
||||
getLogs().info("count:" + count +" quantity:" + quantity);
|
||||
|
||||
if (count == quantity) { //貨物是否已經出貨完畢
|
||||
//偵測是否還有下個出貨商品
|
||||
if (this.dispatchIndex >= this.dispatchList.size()-1) { //沒有貨
|
||||
// ✅ 所有商品出貨完畢,結束流程
|
||||
getLogs().info("所有商品出貨完畢,結束出貨流程。");
|
||||
// ✅ 所有商品出貨完畢,發送 MQTT Finalize 結案訊息
|
||||
getLogs().info("所有商品出貨完畢,發送 MQTT Finalize 結案訊息。");
|
||||
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
|
||||
getSrcHandlerMain().send(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(), "update data");
|
||||
saveData();
|
||||
|
||||
|
||||
@ -18,6 +18,9 @@ import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
import com.unibuy.smartdevice.ui.FontendActivity;
|
||||
import com.unibuy.smartdevice.external.StarCloudAPI;
|
||||
import com.unibuy.smartdevice.exception.ErrorCode;
|
||||
import com.unibuy.smartdevice.tools.Tools;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@ -176,36 +179,48 @@ public class MemberCardPickupDialog extends DialogAbstract {
|
||||
private void handleCardDetected(String cardNo) {
|
||||
if (!cardNo.isEmpty()) {
|
||||
getLogs().info("MemberCardPickupDialog - Card Detected: " + cardNo);
|
||||
|
||||
// 檢查是否為指定的授權卡號
|
||||
if (cardNo.equals("0640741922")) {
|
||||
getLogs().info("MemberCardPickupDialog - Authorized Card Confirmed: 0640741922");
|
||||
MyApp.getInstance().getReportFlowInfoData().setReportFlowId(cardNo);
|
||||
|
||||
// 跳轉到出貨
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
getHandlerMain().send(getClass().getSimpleName(), Option.CHANGE_DISPATCH.getOption(), "change dispatch");
|
||||
}, 300);
|
||||
} else {
|
||||
getLogs().info("MemberCardPickupDialog - Unauthorized Card: " + cardNo);
|
||||
// 顯示錯誤訊息在畫面上
|
||||
binding.editCardNo.setText("");
|
||||
binding.editCardNo.setHint("無效卡號,將於5秒後關閉");
|
||||
cardBuffer.setLength(0); // 清空緩衝區
|
||||
|
||||
// 使用 Toast 提示
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.TOAST.getOption(), "此卡號無領取權限,視窗即將關閉");
|
||||
binding.editCardNo.setHint("正在驗證卡片...");
|
||||
binding.editCardNo.setText("");
|
||||
|
||||
// 5 秒後自動關閉對話框
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
if (isShowing()) {
|
||||
dialogCancel();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
// 呼叫 B680 遠端驗證
|
||||
new StarCloudAPI(getHandlerMain(), getHandlerMain()).verifyStaffCard(cardNo, new StarCloudAPI.VerificationCallback() {
|
||||
@Override
|
||||
public void onSuccess(String codeId) {
|
||||
getLogs().info("MemberCardPickupDialog - B680 Success, code_id: " + codeId);
|
||||
|
||||
// 設定支付資訊
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowType(41); // 員工卡
|
||||
MyApp.getInstance().getReportFlowInfoData().setReportFlowId(cardNo);
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowBarCode(cardNo);
|
||||
|
||||
// 生成訂單編號 (避免後台找不到 order_no)
|
||||
MyApp.getInstance().getReportFlowInfoData().setOrderId(Tools.generateTimeBasedUUID());
|
||||
|
||||
// 設定交易總金額 (員工卡設為 0 元)
|
||||
MyApp.getInstance().getReportFlowInfoData().setTotalPrice(0);
|
||||
|
||||
// 儲存 code_id (用於 MQTT finalize)
|
||||
MyApp.getInstance().getReportAccessCodeStructure().setAccessCodeId(codeId);
|
||||
|
||||
// 跳轉到出貨
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
getHandlerMain().send(getClass().getSimpleName(), Option.CHANGE_DISPATCH.getOption(), "change dispatch");
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String message) {
|
||||
getLogs().error(ErrorCode.NETWORK_UNKNOWN_ERROR, "MemberCardPickupDialog - B680 Failed: " + message);
|
||||
binding.editCardNo.setText("");
|
||||
binding.editCardNo.setHint(message);
|
||||
cardBuffer.setLength(0);
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.TOAST.getOption(), message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void changeDispatch() {
|
||||
if (isChangeDispatch) {
|
||||
isChangeDispatch = false;
|
||||
|
||||
@ -291,41 +291,7 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="26sp"
|
||||
android:text="IC卡讀取:" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_text_card_no"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="26sp"
|
||||
android:hint="請感應卡片"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:background="@null"
|
||||
android:inputType="none"
|
||||
android:singleLine="true" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_clear_card_no"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="清除"
|
||||
android:textSize="20sp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:backgroundTint="@color/black" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
|
||||
Loading…
Reference in New Issue
Block a user