For agentic workers: REQUIRED SUB-SKILL: Use
executing-plansto implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: 将基础设置中的人脸识别阈值滑块改为点击后输入,并支持 50~100 的整数范围、当前值回显、输入框下方提示和即时生效。
Architecture: 保留现有 FaceRecognitionThresholdConfig 作为阈值持久化和 SDK 分值转换边界,将范围调整为 50~100且默认值独立保持 70。新增职责单一的 InputFaceRecognitionThresholdPop 处理当前值回显、输入校验和结果回调;FragmentBasic 只负责打开弹窗、保存有效结果及刷新设置行,FaceSDKManager 继续在每次检索时读取最新配置。
Tech Stack: Android Java 8、AndroidX DialogFragment、SharedPreferences、XML Layout、JUnit 4、Gradle Android Plugin 4.2.1。
Git Boundary: 当前工作区包含用户的其他未提交修改。实施时只修改本计划列出的阈值相关文件,不暂存、不提交、不推送,也不覆盖 AlarmReceiver、OfflineOrderUploadPolicy、UploadOrderWorker 及其测试。
app/src/test/java/com/cpt/cusumption/utils/FaceRecognitionThresholdConfigTest.java:覆盖 50~100 边界和 SDK 转换。app/src/main/java/com/cpt/cusumption/utils/FaceRecognitionThresholdConfig.java:最小值改为 50、最大值改为 100、默认值保持 70,并提供输入范围判断。app/src/main/java/com/cpt/cusumption/view/InputFaceRecognitionThresholdPop.java:阈值输入弹窗,只负责回显、校验与回调。app/src/main/res/layout/input_face_recognition_threshold_pop_layout.xml:弹窗标题、数字输入框、输入框下方提示和操作按钮。app/src/main/res/layout/fragment_basic_layout.xml:将滑块行替换为可点击设置行。app/src/main/java/com/cpt/cusumption/fragment/FragmentBasic.java:移除 SeekBar 交互,绑定当前值并打开输入弹窗。app/src/main/res/values/strings.xml:集中维护标题、当前值、输入提示和错误文案。app/src/main/res/values/dimens.xml:集中维护新弹窗专用尺寸。app/src/main/java/com/cpt/cusumption/manager/FaceSDKManager.java:保留现有每次检索读取配置快照的实现。/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/CusumptionMachine/change_records/2026-08-31-face-recognition-threshold-input-config.md 与同名 .html。Files:
Modify: app/src/test/java/com/cpt/cusumption/utils/FaceRecognitionThresholdConfigTest.java
Modify: app/src/main/java/com/cpt/cusumption/utils/FaceRecognitionThresholdConfig.java
Step 1:先把测试改为 50~100,并增加有效范围判断
@Test
public void clampThresholdLimitsValuesToSupportedRange() {
assertEquals(50, FaceRecognitionThresholdConfig.clampThreshold(49));
assertEquals(50, FaceRecognitionThresholdConfig.clampThreshold(50));
assertEquals(70, FaceRecognitionThresholdConfig.clampThreshold(70));
assertEquals(100, FaceRecognitionThresholdConfig.clampThreshold(100));
assertEquals(100, FaceRecognitionThresholdConfig.clampThreshold(101));
}
@Test
public void supportedThresholdIncludesBothBoundaries() {
assertFalse(FaceRecognitionThresholdConfig.isThresholdSupported(49));
assertTrue(FaceRecognitionThresholdConfig.isThresholdSupported(50));
assertTrue(FaceRecognitionThresholdConfig.isThresholdSupported(70));
assertTrue(FaceRecognitionThresholdConfig.isThresholdSupported(100));
assertFalse(FaceRecognitionThresholdConfig.isThresholdSupported(101));
}
@Test
public void toSdkThresholdConvertsIntegerScoreToDecimal() {
assertEquals(0.50f, FaceRecognitionThresholdConfig.toSdkThreshold(50), 0.0001f);
assertEquals(0.70f, FaceRecognitionThresholdConfig.toSdkThreshold(70), 0.0001f);
assertEquals(1.00f, FaceRecognitionThresholdConfig.toSdkThreshold(100), 0.0001f);
}
同时增加:
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
JAVA_HOME=/Users/liang/Library/Java/JavaVirtualMachines/corretto-1.8.0_482/Contents/Home bash gradlew :app:testDebugUnitTest --tests com.cpt.cusumption.utils.FaceRecognitionThresholdConfigTest
Expected: 测试编译或断言失败;当前实现仍限制为 70~99,且尚无 isThresholdSupported(int)。
/**
* 管理消费人脸识别匹配阈值的持久化、边界限制和 SDK 分值转换。
*
* <p>设置页使用 50~100 的整数分值,识别 SDK 使用 0.50~1.00 的浮点分值。
* 默认值保持 70,以延续未配置设备的原识别行为。</p>
*/
public final class FaceRecognitionThresholdConfig {
public static final int MIN_THRESHOLD = 50;
public static final int MAX_THRESHOLD = 100;
public static final int DEFAULT_THRESHOLD = 70;
private static final float SDK_SCORE_SCALE = 100f;
/**
* 判断用户输入是否位于可保存范围内。
*/
public static boolean isThresholdSupported(int thresholdLevel) {
return thresholdLevel >= MIN_THRESHOLD && thresholdLevel <= MAX_THRESHOLD;
}
}
现有读取、保存、clampThreshold(int) 和 toSdkThreshold(int) 继续保留。clampThreshold(int) 用于兼容历史存储值;弹窗确认必须调用 isThresholdSupported(int),不能把越界输入静默截断后保存。
Run: 与 Step 2 相同。
Expected: FaceRecognitionThresholdConfigTest 共 4 个测试,失败 0、错误 0,BUILD SUCCESSFUL。
Files:
Create: app/src/main/java/com/cpt/cusumption/view/InputFaceRecognitionThresholdPop.java
Create: app/src/main/res/layout/input_face_recognition_threshold_pop_layout.xml
Modify: app/src/main/res/values/strings.xml
Modify: app/src/main/res/values/dimens.xml
Step 1:增加集中管理的文案和尺寸资源
strings.xml:
<string name="basic_set_face_recognition_threshold_value">%1$d</string>
<string name="face_recognition_threshold_input_title">人脸识别阈值</string>
<string name="face_recognition_threshold_input_hint">请输入阈值</string>
<string name="face_recognition_threshold_input_tip">输入范围是 50~100,推荐 70 以上,80 最优。</string>
<string name="face_recognition_threshold_input_error">请输入 50~100 之间的整数</string>
dimens.xml:
<dimen name="face_threshold_pop_title_margin_top">32dp</dimen>
<dimen name="face_threshold_pop_content_margin">40dp</dimen>
<dimen name="face_threshold_pop_input_height">70dp</dimen>
<dimen name="face_threshold_pop_action_height">80dp</dimen>
<dimen name="face_threshold_pop_title_text_size">20sp</dimen>
<dimen name="face_threshold_pop_input_text_size">20sp</dimen>
创建 input_face_recognition_threshold_pop_layout.xml。输入提示必须位于输入框下方:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<LinearLayout
android:layout_width="@dimen/common_pop_width"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@color/white"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/face_threshold_pop_title_margin_top"
android:gravity="center"
android:text="@string/face_recognition_threshold_input_title"
android:textColor="@color/text_color_show"
android:textSize="@dimen/face_threshold_pop_title_text_size"
android:textStyle="bold" />
<EditText
android:id="@+id/input_face_recognition_threshold_view"
android:layout_width="match_parent"
android:layout_height="@dimen/face_threshold_pop_input_height"
android:layout_marginStart="@dimen/face_threshold_pop_content_margin"
android:layout_marginTop="@dimen/face_threshold_pop_content_margin"
android:layout_marginEnd="@dimen/face_threshold_pop_content_margin"
android:background="@drawable/input_price_input_view_bac"
android:gravity="center"
android:hint="@string/face_recognition_threshold_input_hint"
android:imeOptions="flagNoExtractUi"
android:inputType="number"
android:maxLength="3"
android:selectAllOnFocus="true"
android:textColor="@color/text_color_input"
android:textColorHint="@color/text_color_hint"
android:textSize="@dimen/face_threshold_pop_input_text_size" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/face_threshold_pop_content_margin"
android:layout_marginTop="@dimen/common_interval"
android:layout_marginEnd="@dimen/face_threshold_pop_content_margin"
android:layout_marginBottom="@dimen/face_threshold_pop_content_margin"
android:text="@string/face_recognition_threshold_input_tip"
android:textColor="@color/text_color_hint"
android:textSize="@dimen/setting_item_text_size" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/face_threshold_pop_action_height"
android:orientation="horizontal">
<TextView
android:id="@+id/input_face_recognition_threshold_cancel_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/input_price_pop_cancel_bac"
android:gravity="center"
android:text="@string/home_input_price_cancel"
android:textColor="@color/text_color_input"
android:textSize="@dimen/face_threshold_pop_input_text_size" />
<TextView
android:id="@+id/input_face_recognition_threshold_confirm_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/input_price_pop_confirm_bac"
android:gravity="center"
android:text="@string/home_input_price_confirm"
android:textColor="@color/white"
android:textSize="@dimen/face_threshold_pop_input_text_size" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
弹窗使用无参构造和 arguments 保存当前阈值,完整实现如下:
package com.cpt.cusumption.view;
import android.app.Dialog;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.cpt.cusumption.R;
import com.cpt.cusumption.utils.FaceRecognitionThresholdConfig;
import com.cpt.cusumption.utils.ResourcesUtils;
import com.cpt.cusumption.utils.ToastUtils;
/**
* 消费人脸识别阈值输入弹窗。
*
* <p>只负责展示当前值、校验输入并返回有效阈值,不直接持久化配置。</p>
*/
public class InputFaceRecognitionThresholdPop extends DialogFragment
implements View.OnClickListener {
public static final String TAG =
InputFaceRecognitionThresholdPop.class.getSimpleName();
private static final String ARG_CURRENT_THRESHOLD = "arg_current_threshold";
private EditText mInputView;
private OnThresholdInputListener mListener;
public static InputFaceRecognitionThresholdPop newInstance(int currentThreshold) {
InputFaceRecognitionThresholdPop dialog =
new InputFaceRecognitionThresholdPop();
Bundle arguments = new Bundle();
arguments.putInt(ARG_CURRENT_THRESHOLD, currentThreshold);
dialog.setArguments(arguments);
return dialog;
}
@Nullable
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState
) {
Dialog dialog = getDialog();
if (dialog != null && dialog.getWindow() != null) {
dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE);
}
return inflater.inflate(
R.layout.input_face_recognition_threshold_pop_layout,
container,
false
);
}
@Override
public void onViewCreated(
@NonNull View view,
@Nullable Bundle savedInstanceState
) {
super.onViewCreated(view, savedInstanceState);
mInputView = view.findViewById(
R.id.input_face_recognition_threshold_view
);
view.findViewById(
R.id.input_face_recognition_threshold_cancel_view
).setOnClickListener(this);
view.findViewById(
R.id.input_face_recognition_threshold_confirm_view
).setOnClickListener(this);
int currentThreshold = FaceRecognitionThresholdConfig.DEFAULT_THRESHOLD;
Bundle arguments = getArguments();
if (arguments != null) {
int argumentValue = arguments.getInt(
ARG_CURRENT_THRESHOLD,
FaceRecognitionThresholdConfig.DEFAULT_THRESHOLD
);
if (FaceRecognitionThresholdConfig.isThresholdSupported(argumentValue)) {
currentThreshold = argumentValue;
}
}
mInputView.setText(String.valueOf(currentThreshold));
mInputView.requestFocus();
mInputView.selectAll();
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
dialog.setCanceledOnTouchOutside(false);
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(
new ColorDrawable(Color.WHITE)
);
}
return dialog;
}
@Override
public void onStart() {
super.onStart();
Dialog dialog = getDialog();
if (dialog == null || dialog.getWindow() == null) {
return;
}
int width = (int) ResourcesUtils.getDimension(R.dimen.common_pop_width);
dialog.getWindow().setLayout(
width,
ViewGroup.LayoutParams.WRAP_CONTENT
);
dialog.getWindow().setGravity(Gravity.CENTER);
}
@Override
public void onClick(View view) {
if (view.getId()
== R.id.input_face_recognition_threshold_cancel_view) {
if (mListener != null) {
mListener.onCancel();
}
dismiss();
return;
}
if (view.getId()
== R.id.input_face_recognition_threshold_confirm_view) {
confirmInput();
}
}
private void confirmInput() {
String input = mInputView.getText().toString().trim();
int threshold;
try {
threshold = Integer.parseInt(input);
} catch (NumberFormatException error) {
showInputError();
return;
}
if (!FaceRecognitionThresholdConfig.isThresholdSupported(threshold)) {
showInputError();
return;
}
if (mListener != null) {
mListener.onConfirm(threshold);
}
dismiss();
}
private void showInputError() {
ToastUtils.toast(
requireContext(),
getString(R.string.face_recognition_threshold_input_error)
);
}
public void setOnThresholdInputListener(OnThresholdInputListener listener) {
mListener = listener;
}
public interface OnThresholdInputListener {
void onConfirm(int threshold);
void onCancel();
}
}
空字符串和非整数均进入 NumberFormatException 分支;越界值由 isThresholdSupported(int) 拒绝。三种情况都保持弹窗打开且不保存。
JAVA_HOME=/Users/liang/Library/Java/JavaVirtualMachines/corretto-1.8.0_482/Contents/Home bash gradlew :app:compileDebugJavaWithJavac
Expected: BUILD SUCCESSFUL;没有资源 ID、Java 8 或包可见性错误。
Files:
Modify: app/src/main/res/layout/fragment_basic_layout.xml
Modify: app/src/main/java/com/cpt/cusumption/fragment/FragmentBasic.java
Step 1:将滑块行替换为可点击设置行
<LinearLayout
android:id="@+id/set_basic_face_recognition_threshold_layout"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_item_height_size"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/basic_set_face_recognition_threshold"
android:textColor="@color/text_color_show"
android:textSize="@dimen/setting_item_text_size"
android:textStyle="bold" />
<TextView
android:id="@+id/set_basic_face_recognition_threshold_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:textColor="@color/text_color_input"
android:textSize="@dimen/setting_item_text_size" />
<ImageView
android:layout_width="@dimen/setting_item_arrow_width"
android:layout_height="@dimen/setting_item_arrow_height"
android:layout_marginStart="@dimen/common_interval"
android:src="@drawable/icon_arrow_right" />
</LinearLayout>
在 FragmentBasic 删除 SeekBar 导入,增加 InputFaceRecognitionThresholdPop 导入和字段:
private TextView mFaceRecognitionThresholdValueView;
用以下逻辑替换 bindFaceRecognitionThreshold(layout):
mFaceRecognitionThresholdValueView = layout.findViewById(
R.id.set_basic_face_recognition_threshold_value
);
refreshFaceRecognitionThresholdValue();
layout.findViewById(
R.id.set_basic_face_recognition_threshold_layout
).setOnClickListener(this);
删除原 bindFaceRecognitionThreshold(...) 方法,并改为:
private void refreshFaceRecognitionThresholdValue() {
int threshold = FaceRecognitionThresholdConfig.getThresholdLevel();
mFaceRecognitionThresholdValueView.setText(getString(
R.string.basic_set_face_recognition_threshold_value,
threshold
));
}
onClick 增加:
case R.id.set_basic_face_recognition_threshold_layout:
showFaceRecognitionThresholdInputPop();
break;
增加:
/**
* 打开阈值输入弹窗;有效值由设置页统一保存并刷新显示。
*/
private void showFaceRecognitionThresholdInputPop() {
Fragment existing = requireActivity()
.getSupportFragmentManager()
.findFragmentByTag(InputFaceRecognitionThresholdPop.TAG);
if (existing != null) {
Log.e(TAG, "人脸识别阈值输入弹窗已经显示");
return;
}
InputFaceRecognitionThresholdPop dialog =
InputFaceRecognitionThresholdPop.newInstance(
FaceRecognitionThresholdConfig.getThresholdLevel()
);
dialog.setOnThresholdInputListener(
new InputFaceRecognitionThresholdPop.OnThresholdInputListener() {
@Override
public void onConfirm(int threshold) {
FaceRecognitionThresholdConfig.saveThresholdLevel(threshold);
refreshFaceRecognitionThresholdValue();
}
@Override
public void onCancel() {
// 取消时保持当前阈值和页面显示不变。
}
}
);
dialog.showNow(
requireActivity().getSupportFragmentManager(),
InputFaceRecognitionThresholdPop.TAG
);
}
JAVA_HOME=/Users/liang/Library/Java/JavaVirtualMachines/corretto-1.8.0_482/Contents/Home bash gradlew :app:compileDebugJavaWithJavac
Expected: BUILD SUCCESSFUL;FragmentBasic 不再导入或引用 SeekBar,布局中不再存在旧滑块 ID。
Files:
Verify: app/src/main/java/com/cpt/cusumption/manager/FaceSDKManager.java
Verify: app/src/main/java/com/cpt/cusumption/utils/Constants.java
Step 1:静态确认动态配置读取链
rg -n "SETTING_FACE_RECOGNITION_THRESHOLD|getSdkThreshold|thresholdScore|set_basic_face_recognition_threshold_seek_bar|SeekBar" app/src/main/java app/src/main/res app/src/test
Expected: Constants 只有一个配置键;FaceSDKManager 每次检索调用 getSdkThreshold();判断仍为 topFeature.getScore() > thresholdScore;新增阈值功能不再包含 SeekBar 或旧滑块 ID。
JAVA_HOME=/Users/liang/Library/Java/JavaVirtualMachines/corretto-1.8.0_482/Contents/Home bash gradlew :app:testDebugUnitTest --tests com.cpt.cusumption.utils.FaceRecognitionThresholdConfigTest
JAVA_HOME=/Users/liang/Library/Java/JavaVirtualMachines/corretto-1.8.0_482/Contents/Home bash gradlew :app:testDebugUnitTest
Expected: 阈值测试 4 个全部通过;全部 Debug 单元测试失败 0、错误 0。
JAVA_HOME=/Users/liang/Library/Java/JavaVirtualMachines/corretto-1.8.0_482/Contents/Home bash gradlew :app:assembleDebug
Expected: BUILD SUCCESSFUL,生成 app/build/outputs/apk/debug/app-debug.apk。
git diff --check
git status --short --branch
file -I app/src/main/java/com/cpt/cusumption/view/InputFaceRecognitionThresholdPop.java \
app/src/main/res/layout/input_face_recognition_threshold_pop_layout.xml
xxd -l 3 app/src/main/java/com/cpt/cusumption/view/InputFaceRecognitionThresholdPop.java
xxd -l 3 app/src/main/res/layout/input_face_recognition_threshold_pop_layout.xml
Expected: git diff --check 无输出;分支仍为 fix-menu-dish-list-refresh-crash;新文件为 UTF-8 无 BOM;用户已有的离线订单相关修改仍存在且未被改写。
Files:
Create: /Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/CusumptionMachine/change_records/2026-08-31-face-recognition-threshold-input-config.md
Create: /Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/CusumptionMachine/change_records/2026-08-31-face-recognition-threshold-input-config.html
Step 1:生成成对变更记录
记录必须包含:
需求变化:阈值设置从 70~99 滑块改为 50~100 整数输入。
交互结果:点击设置行打开弹窗,输入框回显当前值并默认全选,提示词显示在输入框下方。
校验结果:空值、非整数、小于 50 或大于 100 均不保存且弹窗不关闭。
运行结果:定向测试数、全量测试数、Debug APK 路径与 SHA-256。
交付边界:未修改注册、活体、质量检测和服务端接口;未执行 Git 暂存、提交或推送。
Step 2:核对 Markdown 与 HTML 内容和编码
rg -n "50~100|回显当前值|输入框下方|默认值 70|BUILD SUCCESSFUL|SHA-256" \
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/CusumptionMachine/change_records/2026-08-31-face-recognition-threshold-input-config.md \
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/CusumptionMachine/change_records/2026-08-31-face-recognition-threshold-input-config.html
file -I \
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/CusumptionMachine/change_records/2026-08-31-face-recognition-threshold-input-config.md \
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/CusumptionMachine/change_records/2026-08-31-face-recognition-threshold-input-config.html
Expected: 两个文件关键事实一致,均为 UTF-8,无占位内容。
报告源码仓库和提示词仓库的工作区状态,列明本次修改文件、测试结果、APK 路径与哈希。除非用户明确要求,不运行 git add、git commit、git push 或其他会改写 Git 状态的命令。