MultipurposeMachine · Implementation Plan

“我的营养”付款码扫码识别实施计划

按接口 v1.2 把人脸与付款码统一为人员身份,覆盖双设备扫码、设备编号复用、日志安全和真机验收。

10 个实施任务测试先行设备编号校验接口 v1.2
21.5 寸/dev/ttyS4 · 9600
27 寸HID 设备身份过滤
接口身份已配对设备编号
实施前置Gate 0 契约与样机

“我的营养”付款码扫码识别实施计划

For agentic workers: REQUIRED SUB-SKILL: Use subagent-driven-development (recommended) or executing-plans to execute this plan task by task.

接口契约基线:paycode-user-info-api.html v1.2(2026-09-17)。

Goal: 在不破坏现有人脸识别和餐单打印流程的前提下,为首页“我的营养”增加 21.5 寸串口扫码与 27 寸 HID 键盘扫码能力,通过付款码接口获得人员身份后进入现有营养日报、周报页面。

Architecture: 使用统一的 NutritionIdentity 隔离人脸 SDK 与营养页面;扫码层由统一控制器、串口实现、HID 实现和设备模式解析器组成;付款码接口复用配对后保存的设备编号,将 paycode 与 equipment_code 一并提交,并对请求、响应日志做专项脱敏。人脸与扫码并行运行,通过一次性会话状态保证“首次成功者进入页面,后续结果失效”。

Tech Stack: Android Java 8、AndroidX Fragment、Retrofit 2.1、OkHttp 3.9、Gson、JUnit 4、MMKV、com.kongqw.serialportlibrary.SerialPortManager。


1. 实施边界与当前结论

1.1 本计划覆盖

1.2 不在本计划内

1.3 已确认技术事实

事项 结论 实施影响
旧 ScanCodeUtils 底层 BarCodeScanner.openBarCodeScanner(String) 固定使用 115200 禁止用于本次 9600 串口扫码
可配置串口 项目已有 SerialPortManager.openSerialPort(path, baudRate) 21.5 寸复用该能力
当前营养入口 MainActivity.nutritionCheck() 打开 FaceRecognitionPop(type=1) 在现有弹框内并行增加扫码,不新增入口层级
当前身份传递 FACE_RECOGNITION_RESULT Sticky Event 承载百度 User 营养链路改为显式 Intent 参数;餐单打印继续兼容 Sticky Event
当前网络日志 NewLogInterceptor 记录请求头、请求体、响应体 接口上线前必须对付款码请求和人员响应完成专项脱敏
当前配对信息 GetPairInfoResponse.terminalNumber 在配对成功后保存到 Constants.SETTING_TERMINAL_CODE 直接作为 equipment_code,无需新增鉴权字段

2. 上线门禁

Gate 0:先冻结外部契约和硬件身份

目的: 禁止通过屏幕尺寸、输入速度或猜测型号选择扫码方式。

检查项:

  1. 冻结接口文档 v1.2 请求契约,确认不传 account/pwd/key,请求体固定包含:
{
  "paycode": "C68ca123456789",
  "equipment_code": "EQ-001"
}
  1. 目标环境已部署 MR #903 对应版本并可调用 POST /api/consume/paycodeUserInfo;成功响应满足 code == 0 且 data.staff_uuid、data.staff_name 非空,同时验证“缺少 equipment_code”和“设备不存在”失败分支。
  2. 在 21.5 寸样机记录 Build.MANUFACTURER、Build.MODEL、Android 版本,验证 /dev/ttyS4 可按 9600 打开并取得完整扫码帧。
  3. 27 寸代码参考 CusumptionMachine/QuotaNoKeyboardActivity 与 ScanManager:设备类型 5 的机型标识为 RK3568,输入模式放行,非输入模式消费外接键盘事件,Enter 结束组帧;真机到位后记录 Build.MANUFACTURER、Build.MODEL 和实际按键序列。
  4. 将设备信息以脱敏形式补入本计划第 13 节 Task 10 验收记录;不得写入真实付款码、人员信息或生产设备编号。

门禁结果(2026-09-17): 用户确认 MR #903 已合并并部署;21.5 寸样机已识别为 rockchip / YF_029E / Android 11,/dev/ttyS4 可按 9600 打开。用户随后指定 27 寸参考项目设备类型 5 的实现方式,客户端 HID 切片据此放行;RK3568 以外的未知设备仍解析为 DISABLED。27 寸真机无法连接,发布验收继续阻塞。

3. 目标文件清单

3.1 新增生产代码

app/src/main/java/com/zhct/multipurpose/nutrition/identity/NutritionIdentity.java
app/src/main/java/com/zhct/multipurpose/nutrition/identity/IdentityResolutionSession.java
app/src/main/java/com/zhct/multipurpose/scanner/PaycodeScannerController.java
app/src/main/java/com/zhct/multipurpose/scanner/ScanFrameAssembler.java
app/src/main/java/com/zhct/multipurpose/scanner/SerialPaycodeScannerController.java
app/src/main/java/com/zhct/multipurpose/scanner/KeyboardScanFrameAssembler.java
app/src/main/java/com/zhct/multipurpose/scanner/KeyboardInputPolicy.java
app/src/main/java/com/zhct/multipurpose/scanner/KeyboardWedgeScannerHub.java
app/src/main/java/com/zhct/multipurpose/scanner/KeyboardWedgeScannerController.java
app/src/main/java/com/zhct/multipurpose/scanner/ScannerMode.java
app/src/main/java/com/zhct/multipurpose/scanner/ScannerModeResolver.java
app/src/main/java/com/zhct/multipurpose/network/request/PaycodeUserInfoRequest.java
app/src/main/java/com/zhct/multipurpose/network/response/PaycodeUserInfoResponse.java
app/src/main/java/com/zhct/multipurpose/network/PaycodeUserInfoRepository.java
app/src/main/java/com/zhct/multipurpose/network/interceptor/SensitiveNetworkLogPolicy.java

3.2 修改生产代码和资源

app/src/main/java/com/zhct/multipurpose/application/BaseActivity.java
app/src/main/java/com/zhct/multipurpose/widget/FaceRecognitionPop.java
app/src/main/java/com/zhct/multipurpose/nutrition/NutritionReportActivity.java
app/src/main/java/com/zhct/multipurpose/network/CPTNetworkRequest.java
app/src/main/java/com/zhct/multipurpose/network/NetworkService.java
app/src/main/java/com/zhct/multipurpose/network/CPTService.java
app/src/main/java/com/zhct/multipurpose/network/interceptor/NewLogInterceptor.java
app/src/main/res/layout/face_recognition_pop_layout.xml
app/src/main/res/values/strings.xml

3.3 新增测试

app/src/test/java/com/zhct/multipurpose/nutrition/identity/IdentityResolutionSessionTest.java
app/src/test/java/com/zhct/multipurpose/scanner/ScanFrameAssemblerTest.java
app/src/test/java/com/zhct/multipurpose/scanner/KeyboardScanFrameAssemblerTest.java
app/src/test/java/com/zhct/multipurpose/scanner/ScannerModeResolverTest.java
app/src/test/java/com/zhct/multipurpose/scanner/KeyboardInputPolicyTest.java
app/src/test/java/com/zhct/multipurpose/network/PaycodeUserInfoContractTest.java
app/src/test/java/com/zhct/multipurpose/network/interceptor/SensitiveNetworkLogPolicyTest.java

4. Task 1:建立统一身份对象和一次性会话状态

Files:

Step 1:先写失败测试

测试必须覆盖:空 UUID/姓名拒绝、首次成功可提交、第二次成功被忽略、查询失败后可恢复监听、关闭后所有回调失效。

@Test
public void firstSuccessWinsAndLaterSuccessIsIgnored() {
    IdentityResolutionSession session = new IdentityResolutionSession();
    assertTrue(session.beginQuery());
    assertTrue(session.resolve(NutritionIdentity.paycode("staff-1", "张三")));
    assertFalse(session.resolve(NutritionIdentity.face("staff-2", "李四")));
    assertEquals(IdentityResolutionSession.State.RESOLVED, session.getState());
}

@Test
public void queryFailureReturnsToListening() {
    IdentityResolutionSession session = new IdentityResolutionSession();
    assertTrue(session.beginQuery());
    session.queryFailed();
    assertEquals(IdentityResolutionSession.State.LISTENING, session.getState());
}

运行:

JAVA_HOME=$(/usr/libexec/java_home -v 1.8) ./gradlew :app:testDebugUnitTest --tests '*IdentityResolutionSessionTest'

预期:测试因类不存在而失败。

Step 2:实现最小模型和状态机

public final class NutritionIdentity implements Serializable {
    public enum Source { FACE, PAYCODE }

    private final String staffUuid;
    private final String staffName;
    private final Source source;

    private NutritionIdentity(String staffUuid, String staffName, Source source) {
        if (isBlank(staffUuid) || isBlank(staffName) || source == null) {
            throw new IllegalArgumentException("人员身份字段不完整");
        }
        this.staffUuid = staffUuid.trim();
        this.staffName = staffName.trim();
        this.source = source;
    }

    public static NutritionIdentity face(String uuid, String name) {
        return new NutritionIdentity(uuid, name, Source.FACE);
    }

    public static NutritionIdentity paycode(String uuid, String name) {
        return new NutritionIdentity(uuid, name, Source.PAYCODE);
    }

    public String getStaffUuid() { return staffUuid; }
    public String getStaffName() { return staffName; }
    public Source getSource() { return source; }

    private static boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
}

IdentityResolutionSession 使用 LISTENING、QUERYING、RESOLVED、CLOSED 四态;所有状态变更集中在该类,不在 Dialog 中散落多个布尔值。

Step 3:跑测试并检查差异

JAVA_HOME=$(/usr/libexec/java_home -v 1.8) ./gradlew :app:testDebugUnitTest --tests '*IdentityResolutionSessionTest'
git diff --check

验收:测试通过;无敏感信息;不暂存、不提交。

5. Task 2:把营养页身份传递从 Sticky Event 改为显式参数

Files:

Step 1:为营养页增加唯一入口

private static final String EXTRA_NUTRITION_IDENTITY =
        "com.zhct.multipurpose.extra.NUTRITION_IDENTITY";

public static Intent createIntent(Context context, NutritionIdentity identity) {
    Intent intent = new Intent(context, NutritionReportActivity.class);
    intent.putExtra(EXTRA_NUTRITION_IDENTITY, identity);
    return intent;
}

在 NutritionReportActivity.onCreate() 中先校验 identity,再调用 initViews() 和首次营养请求;人员 UUID 统一改为 identity.getStaffUuid(),姓名统一改为 identity.getStaffName()。参数缺失时提示并 finish(),不得先发空 UUID 请求。

Step 2:仅改营养分支,保留餐单打印兼容

FaceRecognitionPop 人脸成功后按 mSkipType 分流:

if (mSkipType == 1) {
    NutritionIdentity identity = NutritionIdentity.face(user.getUserId(), user.getUserName());
    if (identitySession.resolve(identity)) {
        startActivity(NutritionReportActivity.createIntent(requireContext(), identity));
        dismissAllowingStateLoss();
    }
} else if (mSkipType == 2) {
    EventBus.getDefault().postSticky(
            MessageEvent.create(EventName.FACE_RECOGNITION_RESULT, user));
    startActivity(new Intent(requireContext(), MenuPrintActivity.class));
    dismissAllowingStateLoss();
}

删除 NutritionReportActivity 对 FACE_RECOGNITION_RESULT 的订阅,但不删除事件常量,也不改变 MenuPrintActivity 的订阅逻辑。

Step 3:验证回归

JAVA_HOME=$(/usr/libexec/java_home -v 1.8) ./gradlew :app:testDebugUnitTest
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) ./gradlew :app:assembleDebug
git diff --check

人工检查:人脸进入营养页姓名和日报正常;人脸进入餐单打印仍正常;营养页不再依赖 Sticky Event。

6. Task 3:复用并校验已配对设备编号

Files:

Step 1:确认现有数据来源,不改配对契约

现有链路已经满足接口 v1.2:

/p/api/matecode JSON 字段 code
  → GetPairInfoResponse.terminalNumber
  → MmkvUtils.put(Constants.SETTING_TERMINAL_CODE, terminalNumber)

不得新增 api_auth 字段、凭据存储或安全加密依赖,也不得使用 MQTT ClientId、设备型号或 Android 序列号代替服务端设备编号。

Step 2:建立可测试的设备编号提供者

public interface EquipmentCodeProvider {
    String getEquipmentCode();
}

public final class MmkvEquipmentCodeProvider implements EquipmentCodeProvider {
    @Override
    public String getEquipmentCode() {
        String value = MmkvUtils.getInstance().get(Constants.SETTING_TERMINAL_CODE, "");
        return value == null ? "" : value.trim();
    }
}

Repository 通过构造参数接收 EquipmentCodeProvider,单元测试使用内存 Fake;生产代码使用 MmkvEquipmentCodeProvider。这样既复用现有配对结果,又不让 Repository 直接依赖 MMKV 全局单例。

Step 3:验证现有清理边界

代码审查必须确认:

运行:

JAVA_HOME=$(/usr/libexec/java_home -v 1.8) ./gradlew :app:testDebugUnitTest --tests '*PaycodeUserInfoRepositoryTest'
git diff --check

7. Task 4:接入付款码人员接口并先封住日志泄露

Files:

Step 1:先写日志脱敏测试

@Test
public void paycodeEndpointNeverLogsRequestOrPersonPayload() {
    SensitiveNetworkLogPolicy policy = new SensitiveNetworkLogPolicy();
    String output = policy.sanitize(
            "/api/consume/paycodeUserInfo",
            "Content-Type: application/json",
            "{\"paycode\":\"raw-paycode-value\",\"equipment_code\":\"EQ-001\"}",
            "{\"staff_uuid\":\"staff-1\",\"staff_name\":\"张三\"}");

    assertFalse(output.contains("raw-paycode-value"));
    assertFalse(output.contains("EQ-001"));
    assertFalse(output.contains("staff-1"));
    assertFalse(output.contains("张三"));
}

策略要求:仅对精确路径 /api/consume/paycodeUserInfo 使用固定摘要,例如 requestBody=[PAYCODE_REQUEST_REDACTED]、responseBody=[PERSON_REDACTED];普通 Content-Type 请求头可以保留,其他接口维持现有日志行为。

Step 2:定义接口契约

public final class PaycodeUserInfoRequest {
    @SerializedName("paycode")
    private final String paycode;
    @SerializedName("equipment_code")
    private final String equipmentCode;

    public PaycodeUserInfoRequest(String paycode, String equipmentCode) {
        this.paycode = paycode;
        this.equipmentCode = equipmentCode;
    }
}

public final class PaycodeUserInfoResponse {
    @SerializedName("staff_uuid") private String staffUuid;
    @SerializedName("staff_name") private String staffName;

    public String getStaffUuid() { return staffUuid; }
    public String getStaffName() { return staffName; }

    @Override public String toString() {
        return "PaycodeUserInfoResponse{personPresent="
                + (staffUuid != null && staffName != null) + "}";
    }
}

Retrofit 方法只提交 JSON 请求体,不传 account、pwd、key:

@POST("/api/consume/paycodeUserInfo")
Call<NetworkResult<PaycodeUserInfoResponse>> getPaycodeUserInfo(
        @Body PaycodeUserInfoRequest request);

NetworkService 和 CPTService 增加同名方法,继续复用 ApiResultUtils.transferCallToServiceCall();不得为旧契约保留三项鉴权 Header 的兼容分支。

Step 3:实现 Repository 校验与错误分类

PaycodeUserInfoRepository 职责:

  1. 对扫码原文 trim(),空值不请求。
  2. 通过 EquipmentCodeProvider 读取并规范化已配对设备编号;为空时返回 DEVICE_CODE_MISSING,不请求。
  3. 使用 new PaycodeUserInfoRequest(paycode, equipmentCode) 组装请求体。
  4. 请求进行中拒绝重复提交。
  5. 成功数据转换为 NutritionIdentity.paycode(uuid, name)。
  6. 网络失败、付款码业务失败、设备不存在、协议字段缺失分别返回稳定错误类型;UI 文案由资源文件映射。
  7. Repository 不保存、打印或向异常消息拼接付款码原文、设备编号和人员结果。

Step 4:运行定向测试

JAVA_HOME=$(/usr/libexec/java_home -v 1.8) ./gradlew :app:testDebugUnitTest --tests '*SensitiveNetworkLogPolicyTest' --tests '*PaycodeUserInfoRepositoryTest'
git diff --check

验收:所有失败分支可恢复;请求体字段与 v1.2 一致;测试输出和本地日志不出现完整付款码、生产设备编号、人员 UUID 或姓名。

8. Task 5:建立扫码统一契约与分帧器

Files:

Step 1:定义生命周期契约

public interface PaycodeScannerController {
    void start(Listener listener);
    void stop();
}

Step 2:先测试分帧行为

覆盖以下场景:单块 PAYCODE\r\n、多块拼接、连续两帧、空帧、超长帧清空、停止后残留清空、HID Enter 结束。

@Test
public void fragmentedBytesAreJoinedUntilLineEnding() {
    ScanFrameAssembler assembler = new ScanFrameAssembler(1024);
    assertTrue(assembler.append("PAY".getBytes(UTF_8)).isEmpty());
    assertEquals(Collections.singletonList("PAYCODE"),
            assembler.append("CODE\r\n".getBytes(UTF_8)));
}

Step 3:实现共享分帧器

Step 4:运行测试

JAVA_HOME=$(/usr/libexec/java_home -v 1.8) ./gradlew :app:testDebugUnitTest --tests '*ScanFrameAssemblerTest'

9. Task 6:实现 21.5 寸串口扫码控制器

Files:

Step 1:集中协议常量并注明来源

public final class SerialPaycodeScannerController implements PaycodeScannerController {
    /** 21.5 寸样机硬件协议指定的扫码串口。 */
    static final String SCANNER_PORT = "/dev/ttyS4";
    /** 21.5 寸样机硬件协议指定的波特率。 */
    static final int SCANNER_BAUD_RATE = 9600;
    private static final int MAX_SCAN_LENGTH = 1024;
}

这些值属于外部硬件协议,允许作为有注释的集中常量存在,不得散落在 Dialog、Activity 或测试页。

Step 2:实现打开、接收和释放

serialPortManager.setOnOpenSerialPortListener(openListener);
serialPortManager.setOnSerialPortDataListener(dataListener);
boolean opened = serialPortManager.openSerialPort(
        SCANNER_PORT, SCANNER_BAUD_RATE);

Step 3:真机验证

在 21.5 寸样机执行:打开弹框连续扫码 20 次、拆包场景、快速重复扫码、关闭弹框后扫码、重新打开弹框扫码。要求 20 次均只产生一次完整回调,离开弹框后不请求接口,串口可再次打开。

10. Task 7:建立设备档案与管理员覆盖配置

Files:

Step 1:定义模式

public enum ScannerMode {
    SERIAL,
    KEYBOARD_WEDGE,
    DISABLED
}

Step 2:解析优先级

  1. 精确匹配 rockchip / YF_029E 时返回 SERIAL。
  2. 机型字符串包含参考设备类型 5 标识 RK3568 时返回 KEYBOARD_WEDGE。
  3. 未知型号返回 DISABLED,保留人脸识别并显示诊断提示。
  4. 禁止用 DisplayMetrics、屏幕分辨率、屏幕对角尺寸或输入速度推断设备类型。

测试覆盖两个设备档案、未知型号关闭、大小写和首尾空格规范化。

Step 3:未知设备失败关闭

当前版本不增加管理员手工覆盖项,避免错误配置导致串口冲突或全局键盘误拦截。新机型必须补充设备档案和测试后才能启用。

11. Task 8:实现 27 寸 HID 输入门和全局屏蔽

Files:

Step 1:按参考设备类型和输入场景建立输入门

ScannerModeResolver 在机型包含 RK3568 时返回 KEYBOARD_WEDGE。KeyboardInputPolicy 仅在该模式、无文本输入焦点、事件来源为外接键盘且不是音量键时返回消费;输入控件聚焦时交回系统分发。

Step 2:在 BaseActivity 最前面拦截

@Override
public boolean dispatchKeyEvent(KeyEvent event) {
    if (KeyboardInputPolicy.shouldConsume(
            mode, inputFocused, hardwareKeyboard, volumeKey)) {
        KeyboardWedgeScannerHub.getInstance().handleKeyEvent(event);
        return true;
    }
    return super.dispatchKeyEvent(event);
}

BaseActivity.dispatchKeyEvent() 覆盖普通 Activity;FaceRecognitionPop 因具有独立 Window,还需在 Dialog 的 OnKeyListener 中执行同一策略。KeyboardWedgeScannerHub 只在营养身份弹框激活监听时组帧,弹框外仅消费而不产生业务动作。

组帧规则:500ms 超时复位,Enter/NUMPAD_ENTER 完成一帧,支持数字、字母和常见符号,最大 1024 字符;超长帧整帧丢弃。ACTION_UP 被消费但不进入缓存,音量键放行。

Step 3:测试输入隔离

@Test
public void keyboardDeviceIsConsumedOutsideInputScene() {
    assertTrue(KeyboardInputPolicy.shouldConsume(
            ScannerMode.KEYBOARD_WEDGE, false, true, false));
}

@Test
public void inputSceneAndVolumeKeysRemainAvailable() {
    assertFalse(KeyboardInputPolicy.shouldConsume(
            ScannerMode.KEYBOARD_WEDGE, true, true, false));
    assertFalse(KeyboardInputPolicy.shouldConsume(
            ScannerMode.KEYBOARD_WEDGE, false, true, true));
}

真机验收必须包含:设置页密码框或其他输入控件聚焦时键盘输入正常;首页、营养页等非输入场景扫码不触发功能按钮;身份弹框内扫码可形成完整数据;音量键仍可用。

12. Task 9:把扫码链路接入人脸弹框

Files:

Step 1:只在营养类型启用扫码

Step 2:接入查询状态

private void onScanFrame(String paycode) {
    if (!identitySession.beginQuery()) {
        return;
    }
    renderIdentityStatus(R.string.paycode_identifying);
    paycodeRepository.resolve(paycode, new PaycodeUserInfoRepository.Callback() {
        @Override public void onSuccess(NutritionIdentity identity) {
            if (!isAdded() || !identitySession.resolve(identity)) {
                return;
            }
            stopAllIdentityProviders();
            startActivity(NutritionReportActivity.createIntent(requireContext(), identity));
            dismissAllowingStateLoss();
        }

        @Override public void onFailure(PaycodeFailure failure) {
            if (!isAdded()) {
                return;
            }
            identitySession.queryFailed();
            renderIdentityError(failure);
        }
    });
}

同一时刻若人脸先成功,扫码回调失效;若扫码先成功,人脸回调失效。查询失败恢复 LISTENING,用户可继续扫码或刷脸。

Step 3:补充状态 UI

在现有人脸弹框增加一个不遮挡摄像头的状态区,文案至少包含:

错误文案不得展示付款码、设备编号、人员 UUID、姓名或服务端堆栈。

13. Task 10:全量验证、双设备验收与文档闭环

Step 1:自动化验证

JAVA_HOME=$(/usr/libexec/java_home -v 1.8) ./gradlew :app:testDebugUnitTest
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) ./gradlew :app:assembleDebug
git diff --check

连接目标样机后执行:

adb devices -l
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) ./gradlew :app:connectedDebugAndroidTest

Step 2:接口安全核查

rg -n 'paycode|equipment_code|staff_uuid|staff_name' app/src/main/java/com/zhct/multipurpose

逐项确认:请求模型只有 paycode 和 equipment_code;不存在 account/pwd/key 兼容分支;日志不输出完整请求或人员结果;异常信息不拼接扫码原文和设备编号。

Step 3:21.5 寸验收

Step 4:27 寸验收

Step 5:业务回归

Step 6:文档与变更记录

实施完成后更新:

/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/MultipurposeMachine/requirements/2026-09-17-paycode-nutrition-identification-requirement-solution.md
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/MultipurposeMachine/requirements/2026-09-17-paycode-nutrition-identification-requirement-solution.html
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/MultipurposeMachine/implementation_plans/2026-09-17-paycode-nutrition-identification-implementation-plan.md
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/MultipurposeMachine/implementation_plans/2026-09-17-paycode-nutrition-identification-implementation-plan.html
/Users/liang/AndroidStudioProjects/zhct/zhctprompt/work_android/MultipurposeMachine/change_records/CHANGELOG.md

记录实际设备档案、接口环境、测试命令、构建结果、未通过项和回退方式;严禁写入真实付款码、生产设备编号和人员信息。

14. 验收完成定义

只有同时满足以下条件,任务才可标记完成:

  1. 配对成功后保存的 SETTING_TERMINAL_CODE 能作为 equipment_code 使用,切换 API 和清理配对后无旧值残留。
  2. 付款码接口请求体只包含正确的 paycode、equipment_code,不发送 account/pwd/key。
  3. 网络日志和业务日志均无完整付款码、生产设备编号、人员 UUID、姓名明文组合。
  4. 21.5 寸串口严格使用 /dev/ttyS4、9600,20 次扫码无丢帧、粘帧和重复请求。
  5. 27 寸 RK3568 机型中,输入控件聚焦时键盘输入正常,非输入场景外接键盘数据被消费,身份弹框能完成付款码组帧。
  6. 人脸与扫码同时成功时只跳转一次。
  7. 扫码和人脸均能将正确人员 UUID 交给现有营养日报、周报接口。
  8. 餐单打印人脸链路、设置页输入和其他首页功能回归通过。
  9. 单元测试、仪器测试、Debug 构建和 git diff --check 通过。
  10. Markdown、HTML 和变更记录已同步,未执行未经授权的 Git 提交。

15. 回退策略

16. 实施检查点

每个 Task 完成后执行一次自检:

git status --short
git diff --check

只审查本任务目标文件,不覆盖工作区内已有的无关修改。所有 Git 暂存、提交和推送动作都必须由用户另行明确授权。

17. 2026-09-17 首批执行记录

17.1 已完成实现

17.2 已完成验证

17.3 待完成验收