扫码支付超时时间设置 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 在支付设置中提供默认 30 秒、范围 5~120 秒的扫码支付超时时间,并让该值只约束扫码支付整笔 HTTP 请求。
Architecture: 使用纯 Java 策略统一解析和校验超时值;扫码支付通过独立缓存的 Retrofit/OkHttp 客户端获得专用连接、读取和写入超时,再由 RxJava timeout 提供整笔请求总时限。普通 Retrofit 客户端、刷卡、刷脸、MQTT 倒计时和扫码枪收码延迟保持不变。
Tech Stack: Android Java 8、SharedPreferences、Retrofit 2.1.0、OkHttp 3.9.0、RxJava 2、JUnit 4、Android XML。
实施约束
- 业务代码仓库:
/Users/liang/AndroidStudioProjects/AIDishRecognition。 - 设计与计划归档:
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/AIDishRecognition。 - 不升级 Retrofit、OkHttp 或 RxJava;OkHttp 3.9.0 没有
callTimeout,整笔请求上限由 RxJavatimeout实现。 - 不执行
git add、git commit、git push、分支切换或其他 Git 写操作。每个任务结束只做差异检查。 - 不在业务代码仓库创建
docs/、doc/、prompt/等文档目录。 - 新增类必须有职责和边界注释;业务常量不得散落为魔法数字。
文件结构
新增业务文件
app/src/main/java/com/cpt/aidishrecognition/payment/ScanPaymentTimeoutPolicy.java:解析、校验和规范化扫码支付超时秒数,不访问 Android UI 或网络。app/src/main/java/com/cpt/aidishrecognition/network/ScanPaymentHttpClientFactory.java:从公共 OkHttpClient 克隆扫码支付专用客户端。app/src/main/java/com/cpt/aidishrecognition/network/ScanPaymentRequestTimeout.java:为扫码支付 Observable 应用整笔请求超时。app/src/test/java/com/cpt/aidishrecognition/payment/ScanPaymentTimeoutPolicyTest.java。app/src/test/java/com/cpt/aidishrecognition/network/ScanPaymentHttpClientFactoryTest.java。app/src/test/java/com/cpt/aidishrecognition/network/ScanPaymentRequestTimeoutTest.java。app/src/test/java/com/cpt/aidishrecognition/network/exception/ExceptionHandleTest.java。
修改业务文件
app/src/main/java/com/cpt/aidishrecognition/utils/Constants.java:配置键、默认值、上下限及支付方式编号。app/src/main/java/com/cpt/aidishrecognition/view/InputValueDialogFragment.java:增加可选数字键盘和提交前校验器。app/src/main/java/com/cpt/aidishrecognition/fragment/SettingsPaymentFragment.java:显示、编辑和保存超时值。app/src/main/res/layout/fragment_setting_payment.xml:新增超时设置行。app/src/main/res/values/strings.xml:新增界面和校验文案。app/src/main/java/com/cpt/aidishrecognition/network/RetrofitClient.java:独立缓存扫码支付 Retrofit 实例。app/src/main/java/com/cpt/aidishrecognition/network/RetrofitRequest.java:扫码支付专用请求入口和 RxJava 整体超时。app/src/main/java/com/cpt/aidishrecognition/network/exception/ExceptionHandle.java:识别 RxJavaTimeoutException。app/src/main/java/com/cpt/aidishrecognition/utils/PaymentManager.java:扫码支付选择专用 HTTP 入口。app/src/main/java/com/cpt/aidishrecognition/activity/DishRecognitionActivity.java:本地支付方式常量引用统一定义。app/src/main/java/com/cpt/aidishrecognition/activity/PaymentTestActivity.java:本地支付方式常量引用统一定义。
实施完成后新增记录
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/AIDishRecognition/change_records/2026-09-10-scan-payment-timeout-settings.md。/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/AIDishRecognition/change_records/2026-09-10-scan-payment-timeout-settings.html。
Task 1: 建立超时配置契约和纯逻辑策略
Files:
Modify:
app/src/main/java/com/cpt/aidishrecognition/utils/Constants.javaCreate:
app/src/main/java/com/cpt/aidishrecognition/payment/ScanPaymentTimeoutPolicy.javaTest:
app/src/test/java/com/cpt/aidishrecognition/payment/ScanPaymentTimeoutPolicyTest.javaStep 1: 写失败测试
创建测试文件:
package com.cpt.aidishrecognition.payment;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import com.cpt.aidishrecognition.utils.Constants;
import org.junit.Test;
public class ScanPaymentTimeoutPolicyTest {
@Test
public void parseSeconds_acceptsOnlyIntegerText() {
assertEquals(Integer.valueOf(30), ScanPaymentTimeoutPolicy.parseSeconds("30"));
assertEquals(Integer.valueOf(30), ScanPaymentTimeoutPolicy.parseSeconds(" 30 "));
assertNull(ScanPaymentTimeoutPolicy.parseSeconds(""));
assertNull(ScanPaymentTimeoutPolicy.parseSeconds("30.5"));
assertNull(ScanPaymentTimeoutPolicy.parseSeconds("abc"));
}
@Test
public void isValid_acceptsClosedConfiguredRange() {
assertTrue(ScanPaymentTimeoutPolicy.isValid(Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_MIN));
assertTrue(ScanPaymentTimeoutPolicy.isValid(Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_MAX));
assertFalse(ScanPaymentTimeoutPolicy.isValid(Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_MIN - 1));
assertFalse(ScanPaymentTimeoutPolicy.isValid(Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_MAX + 1));
}
@Test
public void normalize_fallsBackToThirtySecondsForInvalidValues() {
assertEquals(30, ScanPaymentTimeoutPolicy.normalize(30));
assertEquals(Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_DEFAULT,
ScanPaymentTimeoutPolicy.normalize(0));
assertEquals(Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_DEFAULT,
ScanPaymentTimeoutPolicy.normalize(121));
}
}
- Step 2: 运行测试并确认按预期失败
Run:
./gradlew :app:testDebugUnitTest --tests '*ScanPaymentTimeoutPolicyTest'
Expected: FAIL,提示 ScanPaymentTimeoutPolicy 或新增常量不存在。
- Step 3: 在
Constants.java中增加统一常量
放在现有支付方式配置附近:
public static final int PAYMENT_METHOD_CARD = 1;
public static final int PAYMENT_METHOD_SCAN_QR = 2;
public static final int PAYMENT_METHOD_FACE = 3;
public static final String SCAN_PAYMENT_TIMEOUT_SECONDS =
"scan_payment_timeout_seconds";
public static final int SCAN_PAYMENT_TIMEOUT_SECONDS_DEFAULT = 30;
public static final int SCAN_PAYMENT_TIMEOUT_SECONDS_MIN = 5;
public static final int SCAN_PAYMENT_TIMEOUT_SECONDS_MAX = 120;
- Step 4: 创建纯逻辑策略
package com.cpt.aidishrecognition.payment;
import com.cpt.aidishrecognition.utils.Constants;
/**
* 统一扫码支付超时值的解析和边界校验。
* 本类不读取偏好设置,也不操作界面或网络,调用方负责持久化和应用结果。
*/
public final class ScanPaymentTimeoutPolicy {
private ScanPaymentTimeoutPolicy() {
}
public static Integer parseSeconds(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
try {
return Integer.valueOf(value.trim());
} catch (NumberFormatException ignored) {
return null;
}
}
public static boolean isValid(int timeoutSeconds) {
return timeoutSeconds >= Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_MIN
&& timeoutSeconds <= Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_MAX;
}
public static int normalize(int timeoutSeconds) {
return isValid(timeoutSeconds)
? timeoutSeconds
: Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_DEFAULT;
}
}
- Step 5: 运行策略测试
Run:
./gradlew :app:testDebugUnitTest --tests '*ScanPaymentTimeoutPolicyTest'
Expected: PASS,3 个测试全部通过。
- Step 6: 检查本任务差异
Run:
git diff --check
git diff -- app/src/main/java/com/cpt/aidishrecognition/utils/Constants.java \
app/src/main/java/com/cpt/aidishrecognition/payment/ScanPaymentTimeoutPolicy.java \
app/src/test/java/com/cpt/aidishrecognition/payment/ScanPaymentTimeoutPolicyTest.java
Expected: git diff --check 无输出;差异只包含配置契约、策略和测试,不提交 Git。
Task 2: 增加设置页输入与持久化
Files:
Modify:
app/src/main/java/com/cpt/aidishrecognition/view/InputValueDialogFragment.javaModify:
app/src/main/java/com/cpt/aidishrecognition/fragment/SettingsPaymentFragment.javaModify:
app/src/main/res/layout/fragment_setting_payment.xmlModify:
app/src/main/res/values/strings.xmlStep 1: 给通用输入弹窗增加可选数字输入模式
在 InputValueDialogFragment 中引入 android.text.InputType,增加参数和工厂方法:
private static final String ARG_INPUT_TYPE = "input_type";
public static InputValueDialogFragment newNumberInstance(
String title, String hint, String value) {
InputValueDialogFragment fragment = newInstance(title, hint, value);
Bundle args = fragment.getArguments();
if (args != null) {
args.putInt(ARG_INPUT_TYPE, InputType.TYPE_CLASS_NUMBER);
}
return fragment;
}
在 onViewCreated() 读取参数后、设置文本前配置输入类型:
inputView.setInputType(args.getInt(ARG_INPUT_TYPE, InputType.TYPE_CLASS_TEXT));
inputView.setHint(args.getString(ARG_HINT, ""));
inputView.setText(args.getString(ARG_VALUE, ""));
- Step 2: 给弹窗增加提交前校验器
增加字段、设置方法和接口:
private OnValueValidateListener onValueValidateListener;
public void setOnValueValidateListener(OnValueValidateListener listener) {
this.onValueValidateListener = listener;
}
public interface OnValueValidateListener {
@Nullable
String validate(@NonNull String value);
}
将确认按钮分支改为先校验、后关闭:
if (id == R.id.input_dialog_confirm_view) {
String value = inputView.getText() == null
? "" : inputView.getText().toString().trim();
if (TextUtils.isEmpty(value)) {
com.cpt.aidishrecognition.utils.ToastUtils.toast(
requireContext(), R.string.setting_input_required, Toast.LENGTH_SHORT);
return;
}
if (onValueValidateListener != null) {
String validationMessage = onValueValidateListener.validate(value);
if (!TextUtils.isEmpty(validationMessage)) {
com.cpt.aidishrecognition.utils.ToastUtils.toast(
requireContext(), validationMessage, Toast.LENGTH_SHORT);
return;
}
}
dismiss();
if (onValueConfirmListener != null) {
onValueConfirmListener.onValueConfirm(value);
}
}
Expected: 现有字符串输入调用保持 TYPE_CLASS_TEXT,只有新工厂方法使用数字键盘;校验不通过时弹窗不关闭。
- Step 3: 新增字符串资源
在支付设置文案附近加入:
<string name="setting_scan_payment_timeout_title">扫码支付超时时间</string>
<string name="setting_scan_payment_timeout_value">%1$d 秒</string>
<string name="setting_scan_payment_timeout_input_hint">请输入 %1$d~%2$d 秒</string>
<string name="setting_scan_payment_timeout_invalid">请输入 %1$d~%2$d 之间的整数</string>
- Step 4: 在支付设置布局增加可点击设置行
在扫码、刷脸支付复选框之后、订单管理入口之前加入:
<LinearLayout
android:id="@+id/setting_scan_payment_timeout_entry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/setting_scan_payment_timeout_title"
android:textColor="@color/setting_title"
android:textSize="20sp" />
<TextView
android:id="@+id/setting_scan_payment_timeout_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/setting_hint"
android:textSize="20sp" />
</LinearLayout>
- Step 5: 在
SettingsPaymentFragment绑定显示与保存逻辑
增加导入:
import android.widget.TextView;
import com.cpt.aidishrecognition.payment.ScanPaymentTimeoutPolicy;
import com.cpt.aidishrecognition.view.InputValueDialogFragment;
在 onViewCreated() 中调用:
bindScanPaymentTimeout(view);
增加以下方法:
private void bindScanPaymentTimeout(@NonNull View rootView) {
TextView timeoutValueView = rootView.findViewById(
R.id.setting_scan_payment_timeout_value);
View timeoutEntry = rootView.findViewById(
R.id.setting_scan_payment_timeout_entry);
refreshScanPaymentTimeout(timeoutValueView);
timeoutEntry.setOnClickListener(view ->
showScanPaymentTimeoutInput(timeoutValueView));
}
private void refreshScanPaymentTimeout(@NonNull TextView timeoutValueView) {
int savedSeconds = PreferenceUtils.getInt(
Constants.SCAN_PAYMENT_TIMEOUT_SECONDS,
Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_DEFAULT);
int normalizedSeconds = ScanPaymentTimeoutPolicy.normalize(savedSeconds);
timeoutValueView.setText(getString(
R.string.setting_scan_payment_timeout_value, normalizedSeconds));
}
private void showScanPaymentTimeoutInput(@NonNull TextView timeoutValueView) {
int currentSeconds = ScanPaymentTimeoutPolicy.normalize(PreferenceUtils.getInt(
Constants.SCAN_PAYMENT_TIMEOUT_SECONDS,
Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_DEFAULT));
InputValueDialogFragment fragment = InputValueDialogFragment.newNumberInstance(
getString(R.string.setting_scan_payment_timeout_title),
getString(
R.string.setting_scan_payment_timeout_input_hint,
Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_MIN,
Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_MAX),
String.valueOf(currentSeconds));
fragment.setOnValueValidateListener(value -> {
Integer seconds = ScanPaymentTimeoutPolicy.parseSeconds(value);
return seconds != null && ScanPaymentTimeoutPolicy.isValid(seconds)
? null
: getString(
R.string.setting_scan_payment_timeout_invalid,
Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_MIN,
Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_MAX);
});
fragment.setOnValueConfirmListener(value -> {
Integer seconds = ScanPaymentTimeoutPolicy.parseSeconds(value);
if (seconds == null || !ScanPaymentTimeoutPolicy.isValid(seconds)) {
return;
}
PreferenceUtils.putInt(Constants.SCAN_PAYMENT_TIMEOUT_SECONDS, seconds);
refreshScanPaymentTimeout(timeoutValueView);
});
fragment.show(getChildFragmentManager(), "scan_payment_timeout_input");
}
- Step 6: 编译资源与页面代码
Run:
./gradlew :app:compileDebugJavaWithJavac
Expected: BUILD SUCCESSFUL;现有 InputValueDialogFragment.newInstance() 调用全部继续编译。
- Step 7: 检查本任务差异
Run:
git diff --check
git diff -- app/src/main/java/com/cpt/aidishrecognition/view/InputValueDialogFragment.java \
app/src/main/java/com/cpt/aidishrecognition/fragment/SettingsPaymentFragment.java \
app/src/main/res/layout/fragment_setting_payment.xml \
app/src/main/res/values/strings.xml
Expected: 无格式错误;设置文案均来自资源,超时边界均来自统一常量,不提交 Git。
Task 3: 创建扫码支付专用 HTTP 客户端
Files:
Create:
app/src/main/java/com/cpt/aidishrecognition/network/ScanPaymentHttpClientFactory.javaModify:
app/src/main/java/com/cpt/aidishrecognition/network/RetrofitClient.javaTest:
app/src/test/java/com/cpt/aidishrecognition/network/ScanPaymentHttpClientFactoryTest.javaStep 1: 写失败测试
package com.cpt.aidishrecognition.network;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import com.cpt.aidishrecognition.utils.Constants;
import org.junit.Test;
import okhttp3.OkHttpClient;
public class ScanPaymentHttpClientFactoryTest {
@Test
public void create_appliesConfiguredTimeoutWithoutReplacingSharedPool() {
OkHttpClient baseClient = new OkHttpClient.Builder().build();
OkHttpClient scanClient = ScanPaymentHttpClientFactory.create(baseClient, 30);
assertEquals(30_000, scanClient.connectTimeoutMillis());
assertEquals(30_000, scanClient.readTimeoutMillis());
assertEquals(30_000, scanClient.writeTimeoutMillis());
assertSame(baseClient.connectionPool(), scanClient.connectionPool());
assertSame(baseClient.dispatcher(), scanClient.dispatcher());
}
@Test
public void create_normalizesInvalidTimeoutToDefault() {
OkHttpClient scanClient = ScanPaymentHttpClientFactory.create(
new OkHttpClient.Builder().build(), 0);
int expectedMillis = Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_DEFAULT * 1_000;
assertEquals(expectedMillis, scanClient.connectTimeoutMillis());
assertEquals(expectedMillis, scanClient.readTimeoutMillis());
assertEquals(expectedMillis, scanClient.writeTimeoutMillis());
}
}
- Step 2: 运行测试并确认按预期失败
Run:
./gradlew :app:testDebugUnitTest --tests '*ScanPaymentHttpClientFactoryTest'
Expected: FAIL,提示 ScanPaymentHttpClientFactory 不存在。
- Step 3: 创建专用客户端工厂
package com.cpt.aidishrecognition.network;
import com.cpt.aidishrecognition.payment.ScanPaymentTimeoutPolicy;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
/**
* 从应用公共客户端派生扫码支付客户端,只覆盖连接、读取和写入超时。
* 连接池、调度器、拦截器、证书与缓存均继承公共客户端,整笔请求上限由 RxJava 层负责。
*/
public final class ScanPaymentHttpClientFactory {
private ScanPaymentHttpClientFactory() {
}
public static OkHttpClient create(OkHttpClient baseClient, int timeoutSeconds) {
if (baseClient == null) {
throw new IllegalArgumentException("baseClient cannot be null");
}
int normalizedSeconds = ScanPaymentTimeoutPolicy.normalize(timeoutSeconds);
return baseClient.newBuilder()
.connectTimeout(normalizedSeconds, TimeUnit.SECONDS)
.readTimeout(normalizedSeconds, TimeUnit.SECONDS)
.writeTimeout(normalizedSeconds, TimeUnit.SECONDS)
.build();
}
}
- Step 4: 在
RetrofitClient增加独立缓存
增加字段:
private static Retrofit scanPaymentRetrofit;
private static String scanPaymentBaseUrl;
private static int scanPaymentTimeoutSeconds = -1;
增加方法:
public static synchronized <T> T createScanPaymentApi(
Class<T> service, int timeoutSeconds) {
String baseUrl = PreferenceUtils.getString(
Constants.CUSTOMER_BASE_URL,
Constants.DEFAULT_CUSTOMER_BASE_URL);
int normalizedSeconds = ScanPaymentTimeoutPolicy.normalize(timeoutSeconds);
if (scanPaymentRetrofit == null
|| !baseUrl.equals(scanPaymentBaseUrl)
|| normalizedSeconds != scanPaymentTimeoutSeconds) {
OkHttpClient paymentClient = ScanPaymentHttpClientFactory.create(
CptApplication.getOkHttpClient(), normalizedSeconds);
scanPaymentRetrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.client(paymentClient)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MyGsonConverterFactory.create())
.build();
scanPaymentBaseUrl = baseUrl;
scanPaymentTimeoutSeconds = normalizedSeconds;
}
return scanPaymentRetrofit.create(service);
}
引入:
import com.cpt.aidishrecognition.payment.ScanPaymentTimeoutPolicy;
import okhttp3.OkHttpClient;
在 updateBaseUrl() 中同步清空扫码支付缓存:
scanPaymentRetrofit = null;
scanPaymentBaseUrl = null;
scanPaymentTimeoutSeconds = -1;
- Step 5: 运行客户端工厂测试
Run:
./gradlew :app:testDebugUnitTest --tests '*ScanPaymentHttpClientFactoryTest'
Expected: PASS,2 个测试全部通过。
- Step 6: 编译网络层
Run:
./gradlew :app:compileDebugJavaWithJavac
Expected: BUILD SUCCESSFUL;不得调用 OkHttp 3.9.0 不存在的 callTimeout。
Task 4: 实现 RxJava 整笔请求超时和错误映射
Files:
Create:
app/src/main/java/com/cpt/aidishrecognition/network/ScanPaymentRequestTimeout.javaModify:
app/src/main/java/com/cpt/aidishrecognition/network/RetrofitRequest.javaModify:
app/src/main/java/com/cpt/aidishrecognition/network/exception/ExceptionHandle.javaTest:
app/src/test/java/com/cpt/aidishrecognition/network/ScanPaymentRequestTimeoutTest.javaTest:
app/src/test/java/com/cpt/aidishrecognition/network/exception/ExceptionHandleTest.javaStep 1: 写整体超时失败测试
package com.cpt.aidishrecognition.network;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import io.reactivex.Observable;
import io.reactivex.observers.TestObserver;
import io.reactivex.schedulers.TestScheduler;
public class ScanPaymentRequestTimeoutTest {
@Test
public void apply_timesOutAtConfiguredWholeRequestLimit() {
TestScheduler scheduler = new TestScheduler();
TestObserver<Object> observer = ScanPaymentRequestTimeout.apply(
Observable.never(), 30, scheduler).test();
scheduler.advanceTimeBy(29, TimeUnit.SECONDS);
observer.assertNoErrors();
observer.assertNotComplete();
scheduler.advanceTimeBy(1, TimeUnit.SECONDS);
observer.assertError(TimeoutException.class);
}
}
- Step 2: 运行整体超时测试并确认失败
Run:
./gradlew :app:testDebugUnitTest --tests '*ScanPaymentRequestTimeoutTest'
Expected: FAIL,提示 ScanPaymentRequestTimeout 不存在。
- Step 3: 创建整体超时封装
package com.cpt.aidishrecognition.network;
import com.cpt.aidishrecognition.payment.ScanPaymentTimeoutPolicy;
import java.util.concurrent.TimeUnit;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.schedulers.Schedulers;
/**
* 为扫码支付响应链提供从订阅到终止的整体时间上限。
* 超时会处置 Retrofit RxJava 订阅,从而取消对应的底层 HTTP Call。
*/
public final class ScanPaymentRequestTimeout {
private ScanPaymentRequestTimeout() {
}
public static <T> Observable<T> apply(Observable<T> source, int timeoutSeconds) {
return apply(source, timeoutSeconds, Schedulers.computation());
}
static <T> Observable<T> apply(
Observable<T> source, int timeoutSeconds, Scheduler scheduler) {
if (source == null) {
throw new IllegalArgumentException("source cannot be null");
}
if (scheduler == null) {
throw new IllegalArgumentException("scheduler cannot be null");
}
int normalizedSeconds = ScanPaymentTimeoutPolicy.normalize(timeoutSeconds);
return source.timeout(normalizedSeconds, TimeUnit.SECONDS, scheduler);
}
}
- Step 4: 写错误映射失败测试
package com.cpt.aidishrecognition.network.exception;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import java.util.concurrent.TimeoutException;
public class ExceptionHandleTest {
@Test
public void handleException_mapsRxTimeoutToRequestTimeout() {
OkHttpException exception = ExceptionHandle.handleException(
new TimeoutException("scan payment timeout"));
assertEquals(-1, exception.getEcode());
assertEquals("请求超时", exception.getEmsg());
}
}
- Step 5: 运行错误映射测试并确认失败
Run:
./gradlew :app:testDebugUnitTest --tests '*ExceptionHandleTest'
Expected: FAIL,当前 TimeoutException 被映射成“未知错误”。
- Step 6: 扩展
ExceptionHandle
增加导入:
import java.util.concurrent.TimeoutException;
将超时分支改为:
} else if (e instanceof java.net.SocketTimeoutException
|| e instanceof TimeoutException) {
ex = new OkHttpException(TIMEOUT_ERROR, TIMEOUTMSG);
return ex;
- Step 7: 在
RetrofitRequest增加扫码支付入口
增加方法:
public static void orderScanPayment(
Context context,
IResponseCallBack<BaseBean<PaySuccessResponse>> callBack,
MqttOrderPaymentRequest request,
int timeoutSeconds) {
String authAccount = PreferenceUtils.getString(Constants.API_AUTH_ACCOUNT, "");
String authPwd = PreferenceUtils.getString(Constants.API_AUTH_PWD, "");
String authKey = PreferenceUtils.getString(Constants.API_AUTH_KEY, "");
Observable<BaseBean<PaySuccessResponse>> observable =
RetrofitClient.createScanPaymentApi(APIService.class, timeoutSeconds)
.orderPayment(authAccount, authPwd, authKey, request);
RetrofitClient.request(
context,
ScanPaymentRequestTimeout.apply(observable, timeoutSeconds),
callBack,
"orderScanPayment");
}
保留现有 orderPayment(...) 不变,供非扫码支付和订单补传继续调用。
- Step 8: 运行整体超时和异常映射测试
Run:
./gradlew :app:testDebugUnitTest \
--tests '*ScanPaymentRequestTimeoutTest' \
--tests '*ExceptionHandleTest'
Expected: PASS,两个测试类全部通过。
Task 5: 将扫码支付路由到专用超时请求
Files:
Modify:
app/src/main/java/com/cpt/aidishrecognition/utils/PaymentManager.javaModify:
app/src/main/java/com/cpt/aidishrecognition/activity/DishRecognitionActivity.javaModify:
app/src/main/java/com/cpt/aidishrecognition/activity/PaymentTestActivity.javaStep 1: 统一页面内支付方式常量来源
在 DishRecognitionActivity 和 PaymentTestActivity 中把本地数字定义改为统一常量引用:
private static final int METHOD_CARD = Constants.PAYMENT_METHOD_CARD;
private static final int METHOD_QR = Constants.PAYMENT_METHOD_SCAN_QR;
private static final int METHOD_FACE = Constants.PAYMENT_METHOD_FACE;
Expected: 页面行为不变,支付方式编号不再散落硬编码。
- Step 2: 在
PaymentManager增加扫码超时读取方法
增加导入:
import com.cpt.aidishrecognition.payment.ScanPaymentTimeoutPolicy;
增加方法:
private int getScanPaymentTimeoutSeconds() {
int savedSeconds = PreferenceUtils.getInt(
Constants.SCAN_PAYMENT_TIMEOUT_SECONDS,
Constants.SCAN_PAYMENT_TIMEOUT_SECONDS_DEFAULT);
return ScanPaymentTimeoutPolicy.normalize(savedSeconds);
}
- Step 3: 抽取 HTTP 支付回调,保持原有成功失败行为
把 executeHttpPayment() 中的匿名回调原样移动到以下方法,回调内部的加载关闭、成功保存、余额不足提示、声音和 pendingPayments.remove() 行为保持不变:
private IResponseCallBack<BaseBean<PaySuccessResponse>> createHttpPaymentCallback(
PaymentRequestData paymentRequest) {
return new IResponseCallBack<BaseBean<PaySuccessResponse>>() {
@Override
public void onSuccess(BaseBean<PaySuccessResponse> data) {
paymentRequest.callback.onPaymentLoading(paymentRequest.requestId, false);
if (data != null && data.getErrorCode() == 0 && data.getData() != null) {
PaySuccessResponse response = data.getData();
playSound(1);
saveOrder(paymentRequest, response);
paymentRequest.callback.onPaymentSuccess(
paymentRequest.requestId,
paymentRequest.recognitionTime,
response.staffName,
response.totalPrice,
response.payPrice,
response.remainBalance);
} else {
handlePaymentError(
paymentRequest,
data == null ? "支付失败" : safe(data.getErrorMsg(), "支付失败"));
}
pendingPayments.remove(paymentRequest.requestId);
}
@Override
public void onFail(OkHttpException failure) {
paymentRequest.callback.onPaymentLoading(paymentRequest.requestId, false);
String errorMsg = failure == null
? "未知错误!" : safe(failure.getEmsg(), "未知错误!");
if (failure != null && failure.getEcode() == 8001) {
errorMsg = "余额不足,请充值后再尝试!";
playSound(3);
} else {
playSound(2);
}
paymentRequest.callback.onPaymentFailed(paymentRequest.requestId, errorMsg);
pendingPayments.remove(paymentRequest.requestId);
}
@Override
public void onLoading() {
}
};
}
- Step 4: 按支付方式选择 HTTP 请求入口
用以下实现替换 executeHttpPayment():
private void executeHttpPayment(
PaymentRequestData paymentRequest, MqttOrderPaymentRequest request) {
paymentRequest.callback.onPaymentLoading(paymentRequest.requestId, true);
IResponseCallBack<BaseBean<PaySuccessResponse>> callback =
createHttpPaymentCallback(paymentRequest);
if (paymentRequest.paymentType == Constants.PAYMENT_METHOD_SCAN_QR) {
int timeoutSeconds = getScanPaymentTimeoutSeconds();
LogUtils.d("scan payment requestId=" + paymentRequest.requestId
+ ", timeoutSeconds=" + timeoutSeconds);
RetrofitRequest.orderScanPayment(
context, callback, request, timeoutSeconds);
return;
}
RetrofitRequest.orderPayment(context, callback, request);
}
日志只能包含请求 ID、支付方式和超时值,不记录完整付款码。
- Step 5: 修正 MQTT 路由附近的误导注释
保持现有条件不变,将:
// 刷卡支付不能走mqtt
if (paymentType == 2) {
改为:
// 当前扫码支付仍由 HTTP 接口处理,其他支付方式保持既有 MQTT 路由。
if (paymentType == Constants.PAYMENT_METHOD_SCAN_QR) {
- Step 6: 运行相关测试和编译
Run:
./gradlew :app:testDebugUnitTest \
--tests '*ScanPaymentTimeoutPolicyTest' \
--tests '*ScanPaymentHttpClientFactoryTest' \
--tests '*ScanPaymentRequestTimeoutTest' \
--tests '*ExceptionHandleTest'
./gradlew :app:compileDebugJavaWithJavac
Expected: 全部测试 PASS,编译 BUILD SUCCESSFUL。
- Step 7: 静态核对支付路由和硬编码
Run:
rg -n 'paymentType\s*==\s*2|METHOD_QR\s*=\s*2|callTimeout|SCAN_PAYMENT_TIMEOUT' app/src/main/java app/src/test/java
Expected:
- 不存在新的
paymentType == 2或METHOD_QR = 2魔法数字。 - 不存在 OkHttp 3.9.0 不支持的
callTimeout调用。 - 超时边界只从
Constants和ScanPaymentTimeoutPolicy获取。
Task 6: 完整验证与变更记录
Files:
Verify: all changed source and test files
Create:
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/AIDishRecognition/change_records/2026-09-10-scan-payment-timeout-settings.mdCreate:
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/AIDishRecognition/change_records/2026-09-10-scan-payment-timeout-settings.htmlStep 1: 运行全部单元测试
Run:
./gradlew :app:testDebugUnitTest
Expected: BUILD SUCCESSFUL,现有测试和新增测试全部通过。
- Step 2: 构建 Debug APK
Run:
./gradlew :app:assembleDebug
Expected: BUILD SUCCESSFUL,生成 app/build/outputs/apk/debug/app-debug.apk。
- Step 3: 执行代码差异检查
Run:
git diff --check
git status --short
Expected: git diff --check 无输出;git status 只包含本计划列出的业务源码、资源和测试文件,不包含新建业务仓库文档目录。
- Step 4: 手工验收设置页
按顺序验证:
- 未保存过该配置时显示“30 秒”。
- 点击设置行出现数字键盘和当前值。
- 空值、4、121 无法保存,弹窗保持打开并提示“请输入 5~120 之间的整数”。
- 5、30、120 可以保存;退出并重新进入设置页后仍显示保存值。
- 关闭再启用“扫码支付”后,超时值不被清除。
- Step 5: 手工验收支付超时
使用可延迟响应的测试接口或代理执行:
- 保存 5 秒,扫码支付接口延迟超过 5 秒,确认约 5 秒进入“请求超时”失败结果。
- 保存 30 秒,接口在 5~30 秒内响应,确认不会沿用全局 5 秒提前失败。
- 接口延迟超过 30 秒,确认约 30 秒结束等待且只回调一次失败。
- 超时后关闭结果弹窗,确认菜品识别和扫码监听恢复。
- 验证刷卡、刷脸和普通接口仍走原有客户端。
- Step 6: 检查日志和资源生命周期
Run:
rg -n 'payCode=.*|付款码|scan payment' app/src/main/java/com/cpt/aidishrecognition
Expected: 本次新增日志不输出完整付款码;连续扫码支付复用相同 baseUrl + timeoutSeconds 对应的 Retrofit 实例。
- Step 7: 创建提示词工程变更记录 Markdown
写入以下已确定内容。执行 Step 1~3 后,在“验证结果”中逐项写入命令、退出码和终端最终状态;成功示例为 ./gradlew :app:testDebugUnitTest — exit 0 — BUILD SUCCESSFUL,失败时原样记录失败任务和首个错误原因,不预填成功:
# 扫码支付超时时间设置变更记录
## 变更目标
在支付设置中增加扫码支付超时时间,默认 30 秒、范围 5~120 秒,并只约束扫码支付整笔 HTTP 请求。
## 实现内容
- 增加超时配置、默认值和边界策略。
- 支付设置页支持数字输入、校验、持久化和当前值展示。
- 扫码支付使用独立 Retrofit/OkHttp 客户端,连接、读取和写入超时采用设置值。
- RxJava `timeout` 控制整笔请求上限,超时统一显示“请求超时”。
- 普通网络请求、刷卡、刷脸、MQTT 倒计时和扫码枪收码延迟保持不变。
## 验证结果
- 扫码支付超时策略单元测试:写入对应 Gradle 命令、退出码和最终状态。
- 专用 HTTP 客户端单元测试:写入对应 Gradle 命令、退出码和最终状态。
- RxJava 整体超时和异常映射测试:写入对应 Gradle 命令、退出码和最终状态。
- `:app:testDebugUnitTest`:写入命令、退出码和最终状态。
- `:app:assembleDebug`:写入命令、退出码和最终状态。
- `git diff --check`:写入命令、退出码和是否存在格式错误。
## Git 操作
未执行暂存、提交、推送、分支切换或历史改写。
- Step 8: 生成同 basename HTML 并核对一致性
使用提示词工程现有 Markdown 转 HTML 方式生成:
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/AIDishRecognition/change_records/2026-09-10-scan-payment-timeout-settings.html
验证 Markdown 的每个标题、列表和真实测试结果均出现在 HTML 中。不得向业务仓库复制该记录。
- Step 9: 最终范围检查
Run:
find /Users/liang/AndroidStudioProjects/AIDishRecognition -maxdepth 2 \
-type d \( -name docs -o -name doc -o -name prompt \) -print
git -C /Users/liang/AndroidStudioProjects/zhct/zhctprompt status --short \
-- work_android/AIDishRecognition
Expected: 业务仓库没有本任务新建的文档目录;提示词工程存在设计稿、实施计划和实施完成后的 Markdown/HTML 变更记录配对。仍不执行 Git 提交。