方案日期:2026-09-01
项目仓库:/Users/liang/AndroidStudioProjects/CusumptionMachine
当前分支:fix_pay_by_face
代码基线:804e617
文档状态:一期、二期代码已落地,真机待验
本次采用“兼容式内部重构”,在不改变现有业务和现有交互的前提下,解决人脸识别模型初始化失败后白屏、应用重新进入首页以及菜单消费订单丢失的问题。
第一阶段保持 7 个业务页面现有调用方式、FaceRecognitionPop 公开接口、识别成功确认流程、失败关闭时机和支付接口不变,只调整人脸识别弹框内部的模型、相机和生命周期职责:
现有业务页面 │ 原构造参数、原监听器、原业务回调保持不变 ▼ FaceRecognitionPop │ 保留现有 UI 和交互,内部委托识别工作 ▼ FaceRecognitionSession │ 管理本次弹框的相机、识别、倒计时和资源释放 ▼ FaceRecognitionRuntime 管理应用进程内的人脸模型、人脸库和初始化状态
第二阶段在一期稳定边界上增加统一入口,7 个页面只迁移弹框创建代码,原业务监听器和支付、核验方法继续留在页面:
7 个业务页面
│ 组装场景、金额、副屏和确认文案
▼
FaceRecognitionLauncher
│ 生命周期门禁、统一 Tag、防重复展示
▼
FaceRecognitionPop → FaceRecognitionSession → FaceRecognitionRuntime
必须删除 FaceRecognitionPop 中模型初始化失败后的 restartSelf()、应用入口重启和 Process.killProcess()。异常时改为记录错误、停止识别、释放相机、关闭弹框并留在原业务页面。
除上述异常路径从“白屏并重启”修正为“安全关闭弹框”以外,用户可见交互和业务结果不得改变。
人脸模型初始化失败后出现白屏。
人脸模型初始化失败后重新进入首页。
应用进程被结束导致菜单消费内存订单丢失。
LaunchActivity 和 FaceRecognitionPop 重复承担模型初始化职责。
弹框显示后模型初始化与相机启动并行,存在模型未就绪时开始识别的竞态。
弹框关闭或页面销毁后,识别异步回调仍可能继续到达的问题。
多次回调导致重复触发支付或重复释放资源的风险。
现有 7 个业务页面的人脸识别按钮、业务回调和支付或核验入口。
FaceRecognitionPop 的布局、尺寸、遮罩、按钮、文案和动画。
点击支付后打开当前人脸识别弹框的操作路径。
未识别人脸时的预览和倒计时交互。
识别到用户后展示用户信息,并由用户点击当前“重试”或“确认”按钮的交互。
用户点击“确认”后才回调识别成功并进入支付或核验。
识别超时后自动关闭弹框。
用户取消后自动关闭弹框。
单屏与双屏的现有显示效果。
各页面现有支付接口、订单核验接口、请求参数和结果处理。
支付成功、支付失败结果弹框及页面刷新规则。
菜单消费已选菜品和自定义金额的内存保存方式。
不统一改造所有支付接口。
不把国信人脸支付、普通消费支付和订单核验合并到一个支付管理类。
不修改服务端接口和数据协议。
不重做弹框 UI。
不改变识别阈值、活体检测和质量检测规则。
不增加弹框内持续自动重试模式。
不引入 STAY_AND_RETRY 交互。
不在第一阶段强制所有页面改用新的 Launcher。
不在第一阶段把 SDK User 替换成新的业务身份对象。
不处理系统杀进程、应用崩溃后的订单草稿持久化恢复。
当前共有 7 个业务页面直接使用 FaceRecognitionPop,共 8 个构造位置。MainActivity 因单屏和双屏分支存在两个构造位置。
MainActivity:菜单消费。
FreeActivity:自由消费。
FreeNoKeyboardActivity:无键盘自由消费。
QuotaActivity:定额消费。
QuotaNoKeyboardActivity:无键盘定额消费。
GuoxinFacePayActivity:国信人脸支付。
OrderVerifyActivity:订单核验。
这些页面通过同一个 FaceRecognitionResultListener 接收:
recognitionSuccess(User user):用户确认后执行现有支付或核验。
recognitionEnd():识别超时后执行当前提示及副屏收尾。
recognitionCancel():用户取消后执行当前提示及副屏收尾。
FaceRecognitionPop 当前同时承担:
弹框布局和用户交互。
模型初始化。
人脸数据库加载。
相机开启和关闭。
人脸检测。
识别动画。
倒计时。
识别结果展示。
重试和确认。
资源释放。
模型失败后的应用重启。
模型属于应用进程级资源,相机和识别属于单次弹框资源,UI 属于视图层。三类生命周期混在一个弹框中,使异常处理容易越过业务页面边界。
LaunchActivity 当前启动流程会:
执行人脸启动流程。
初始化自定义人脸模型。
初始化本地人脸数据库。
把本地人脸数据加载进内存。
完成后进入主页。
但 FaceRecognitionPop.onStart() 又调用 initModel(),onResume() 又调用 initDataBases() 并直接启动相机。模型初始化回调和相机启动没有形成严格的先后关系,存在重复初始化和就绪竞态。
FaceRecognitionPop.initModel() 初始化失败且错误码不为 -12 时,当前逻辑会:
提示“模型加载失败,即将重启应用”。
延迟调用 restartSelf()。
重新启动应用入口 Activity。
调用 Process.killProcess() 结束当前进程。
该流程不是普通的弹框关闭,而是整个应用重启。菜单消费订单保存在当前 MainActivity 内存中,进程结束后页面和订单对象都会被销毁,因此表现为白屏、回到首页以及订单丢失。
当前普通识别超时会停止预览、回调 recognitionEnd() 并关闭弹框;MainActivity.recognitionEnd() 只显示提示并隐藏副屏人脸区域,没有清空 mSelectDishList。
自定义金额确认后会生成使用 Constants.CUSTOM_DISH_ID 的 DishBean,并加入 mSelectDishList。因此在 Activity 和应用进程没有被销毁的情况下:
已选菜品仍然存在。
自定义金额对应的菜品仍然存在。
用户可以再次点击支付重新发起人脸识别。
本次修复不新增订单恢复逻辑,只需保证人脸识别异常不再主动销毁应用进程,即可保持这条现有业务链路。
采用以下三层结构:
FaceRecognitionRuntime:进程级模型与人脸库运行时。
FaceRecognitionSession:单次弹框识别会话。
FaceRecognitionPop:保持当前公开接口和 UI 交互,内部委托 Runtime 与 Session。
第一阶段不要求业务页面切换调用入口,避免一次性改动 7 个页面的业务代码。
只删除 restartSelf() 可以立即避免进程重启,但仍然保留:
每次打开弹框重复检查或初始化模型。
模型初始化与相机启动竞态。
UI、相机、模型职责混合。
异步回调越过弹框生命周期。
因此它只能作为紧急补丁,不能作为完整修复方案。
模型初始化耗时长、生命周期属于应用进程;相机属于一次展示会话。全部放在弹框会使每次进入都承担重资源初始化,并继续放大生命周期问题。
各页面的成功后业务不同:普通消费调用现有支付接口,国信页面调用独立人脸支付接口,订单核验不是支付。把识别和这些业务全部合并会造成高耦合,并增加业务回归范围。
FaceRecognitionRuntime 是应用进程级的具体封装类,不是 Activity、Fragment 或弹框生命周期组件。
由应用级单例或 CustomApplication 持有,仅保存 ApplicationContext,不得持有 Activity、Dialog、Fragment 或 PreviewView。初始化等待回调必须包装为可取消的一次性登记,页面或 Session 销毁后立即解除回调引用。
统一读取 SDK 激活和模型初始化状态。
统一执行自定义人脸模型初始化。
统一协调人脸数据库加载状态。
保证同一时刻只有一个初始化任务。
多个调用者并发请求时,共享同一次初始化结果。
为每个调用者返回可取消的 InitRequest,取消只解除当前调用者回调,不中断共享初始化。
在主线程分发最终状态回调。
记录初始化错误码、错误信息和运行时状态。
不打开相机。
不操作弹框 UI。
不执行识别倒计时。
不调用支付或核验接口。
不跳转页面。
不重启应用。
不结束进程。
UNINITIALIZED │ initialize/ensureReady ▼ INITIALIZING ├────────► READY └────────► FAILED
READY 状态下再次调用 ensureReady() 必须直接返回成功,不能重复加载模型。
INITIALIZING 状态下再次调用时,只登记等待回调,不能启动第二个初始化任务。
FAILED 状态下是否允许重试由明确的方法触发,不能在弹框内无限自动重试。
public final class FaceRecognitionRuntime {
public static FaceRecognitionRuntime getInstance();
public InitRequest initialize(Context context, InitCallback callback);
public InitRequest ensureReady(Context context, InitCallback callback);
public RuntimeState getState();
}
接口中的 Context 进入 Runtime 后必须转换为 getApplicationContext()。LaunchActivity.onDestroy() 和 FaceRecognitionSession.stop() 必须取消各自的 InitRequest;这只移除页面回调,不影响其他页面等待的同一模型初始化任务。
FaceRecognitionSession 表示一次弹框从显示到关闭的人脸识别会话。每次显示弹框创建一个 Session,弹框销毁后 Session 不得继续工作。
等待 Runtime 就绪后启动本次识别。
Session 停止时取消自己的 Runtime 等待请求。
根据当前设备类型配置摄像头方向。
绑定主屏预览和可选副屏预览。
开始及停止相机预览。
接收相机帧并调用现有 FaceSDKManager.onDetectCheck()。
管理识别倒计时。
管理识别动画开始和停止信号。
保存本次已识别用户。
过滤关闭后的旧异步回调。
防止同一次会话重复分发最终结果。
幂等释放相机、倒计时和异步任务。
IDLE │ start ▼ STARTING │ Runtime READY ▼ RECOGNIZING ├────────► MATCHED ├────────► TIMEOUT ├────────► ERROR └────────► STOPPED
MATCHED 只代表已经识别到用户,仍然保持当前弹框交互:展示用户信息,等待用户点击“重试”或“确认”。只有点击“确认”才调用原 recognitionSuccess(User user)。
public final class FaceRecognitionSession {
public void start(
Context context,
AutoTexturePreviewView primaryPreview,
AutoTexturePreviewView secondaryPreview,
SessionCallback callback
);
public void restart();
public void stop();
}
Session 可以持有本次弹框的预览 View,但不能跨弹框复用;FaceRecognitionPop.onDestroyView() 停止 Session 后必须释放对 Session 的引用。
使用原子状态或同步锁保证最终结果只分发一次。
使用会话序号识别旧回调;新会话开始后,旧会话回调直接丢弃。
stop() 必须幂等,多次调用只释放一次资源。
所有 UI 回调切换到主线程,并在回调前确认弹框仍然有效。
public FaceRecognitionPop(
Context context,
AutoTexturePreviewView previewView,
String orderAmount
);
public void setFaceRecognitionResultListener(
FaceRecognitionResultListener listener
);
public void setConfirmText(String confirmText);
FaceRecognitionResultListener 第一阶段继续保留:
void recognitionSuccess(User user); void recognitionEnd(); void recognitionCancel();
这样 7 个页面的现有调用代码和业务回调不需要同步改写。
创建现有布局。
展示主、副屏预览区域。
展示识别动画、用户头像、姓名、金额。
保留当前“重试”“确认”“取消”和返回按钮。
将弹框生命周期转发给 Session。
把 Session 结果映射到原有监听器。
移除直接模型初始化。
移除重复人脸数据库加载。
移除直接组织相机检测流程。
移除应用重启和进程结束。
不新增支付或订单逻辑。
统一 Launcher 不属于一期白屏修复的前置条件,现已作为二期兼容改造落地。
二期由 FaceRecognitionRequest 统一组装调用场景、金额、可选副屏、确认文案和弹框 Tag;由 FaceRecognitionLauncher 统一检查宿主生命周期、Fragment 状态保存和重复弹框,并创建唯一的 FaceRecognitionPop。
Launcher 不保存 Activity、页面监听器或请求对象,不调用支付、核验和订单接口。7 个页面的原 FaceRecognitionResultListener 及其业务实现保持原位。
展示结果分为:
SHOWN:已创建弹框。
ALREADY_SHOWN:相同 Tag 弹框已存在,不重复创建。
HOST_UNAVAILABLE:Activity 正在结束或已经销毁。
STATE_SAVED:Fragment 状态已经保存,安全拒绝展示。
INVALID_REQUEST:请求为空。
SHOWN 和 ALREADY_SHOWN 表示已有弹框负责后续收尾;其他结果由调用页面恢复自身支付门禁或副屏状态,不能造成按钮被永久锁定。
LaunchActivity 开始人脸启动流程 │ ▼ FaceRecognitionRuntime.initialize() │ ├─ 已 READY:直接返回成功 │ ├─ 正在 INITIALIZING:加入等待队列 │ └─ 未初始化:调用现有 InitFaceModelManager │ ├─ 成功:状态改为 READY └─ 失败:状态改为 FAILED
LaunchActivity 现有启动状态提示、许可证异常处理、本地人脸库流程和进入主页时机继续保留。Runtime 只接管模型初始化状态与并发控制,不接管页面跳转。
启动页自身原有的启动失败策略不在本次白屏修复范围内;本次必须移除的是业务弹框内部的重启和杀进程行为。
业务页面组装 FaceRecognitionRequest,继续传入原结果监听器。
Launcher 检查 Activity、Fragment 状态和相同 Tag 弹框。
满足展示条件时统一创建 FaceRecognitionPop;重复点击不重复创建。
弹框继续按当前方式显示布局并创建新的 FaceRecognitionSession。
Session 调用 Runtime 的 ensureReady()。
Runtime 已就绪时立即启动相机,不产生正常场景下的额外等待。
Runtime 尚未就绪时,弹框保持当前外观,等待初始化结果;不能提前送相机帧进入识别。
Runtime 失败时执行安全结束,不重启应用。
Session 接收到有效 User。
停止当前持续识别,防止重复匹配。
弹框按当前方式展示用户信息、金额、“重试”和“确认”。
用户点击“重试”,继续使用当前弹框重新开始识别。
用户点击“确认”,调用原 recognitionSuccess(User user)。
业务页面继续执行原支付或核验逻辑。
Session 停止并释放资源,弹框关闭。
保持现有流程:
停止相机预览。
停止识别动画和倒计时。
调用原 recognitionEnd()。
主页面执行当前提示和副屏隐藏逻辑。
自动关闭弹框。
不修改任何订单数据。
保持现有流程:
停止 Session。
调用原 recognitionCancel()。
主页面执行当前提示和副屏隐藏逻辑。
自动关闭弹框。
不修改任何订单数据。
替换当前重启行为:
记录错误阶段、错误码和错误信息。
停止倒计时、识别动画和相机预览。
通过现有结束通道完成页面及副屏收尾。
关闭弹框。
留在原业务页面。
不启动首页。
不结束进程。
不调用支付或核验接口。
不修改订单状态。
异常提示沿用项目当前提示方式,不新增新的确认步骤,不要求用户在弹框内处理异常。
MainActivity 继续调用现有菜单消费支付。
FreeActivity 继续调用现有自由消费支付。
FreeNoKeyboardActivity 继续调用现有无键盘自由消费支付。
QuotaActivity 继续调用现有定额消费支付。
QuotaNoKeyboardActivity 继续调用现有无键盘定额消费支付。
GuoxinFacePayActivity 继续调用现有国信人脸支付接口。
OrderVerifyActivity 继续调用现有订单核验接口。
Runtime、Session 和 Pop 均不得调用上述业务接口。
人脸识别流程不得直接调用:
mSelectDishList.clear()。
菜品重新加载。
MainActivity.finish()。
首页重启。
应用进程结束。
识别失败、超时、用户取消、模型异常或相机异常后,必须满足:
当前选中菜品不变。
自定义金额生成的 DishBean 不变。
当前总金额不变。
当前订单尚未提交。
用户可以再次点击支付。
只有现有支付成功分支可以继续执行当前订单清空逻辑。支付失败分支保持当前订单,不能由人脸封装代为清空。
MainActivity 仍然把副屏 AutoTexturePreviewView 通过 FaceRecognitionRequest 和 Launcher 传给 FaceRecognitionPop。Session 将它作为可选预览目标使用:
有副屏时,同时维护主屏与副屏预览。
无副屏时,只维护主屏预览。
成功、超时、取消和异常结束时都停止相机数据输出。
MainActivity 现有副屏显示和隐藏调用保持不变。
Runtime 初始化失败。
人脸数据库不可用。
相机打开失败。
相机数据异常。
人脸检测回调异常。
识别超时。
页面或弹框生命周期结束。
日志至少包含:
会话唯一标识。
所在业务页面。
单屏或双屏模式。
Runtime 当前状态。
模型初始化错误码及错误信息。
相机打开和关闭结果。
Session 状态变化。
最终结束原因。
是否已经分发成功结果。
日志不得记录完整人脸图像、特征值或不必要的敏感身份数据。
普通超时和取消提示保持现状。
模型或相机异常使用当前 Toast 风格提示识别暂不可用。
异常提示后直接关闭弹框,不增加确认按钮或页面跳转。
不再出现“即将重启应用”的提示。
app/src/main/java/com/cpt/cusumption/faceRecognition/
├── launcher/
│ ├── FaceRecognitionLauncher.java # 统一展示入口和生命周期门禁
│ ├── FaceRecognitionLaunchGate.java # 可测试的防重复展示规则
│ ├── FaceRecognitionRequest.java # 弹框参数对象
│ └── FaceRecognitionScene.java # 7 个调用场景枚举
├── model/
│ └── InitFaceModelManager.java # 保留现有底层初始化能力
├── runtime/
│ ├── FaceRecognitionRuntime.java # 进程级运行时
│ └── FaceRecognitionRuntimeState.java # 初始化状态
└── session/
├── FaceRecognitionSession.java # 单次识别会话
└── FaceRecognitionSessionCallback.java # Pop 内部回调
app/src/main/java/com/cpt/cusumption/view/
└── FaceRecognitionPop.java # 保持原公开接口和交互
第一阶段不为抽象而抽象。如果当前只有一种相机实现,不新增空泛的 FaceCameraAdapter;只有后续出现第二种真实相机实现时再抽取适配接口。
所有新增类和关键生命周期方法需要添加职责、边界和异常处理原因注释。超时时间、状态值、日志 Tag 等使用资源或命名常量,避免散落硬编码。
记录 7 个页面当前弹框入口、成功回调、结束回调和取消回调。
记录菜单消费的订单清空位置。
记录单屏、双屏的弹框与预览行为。
建立模型失败、超时、取消、成功确认的测试清单。
该阶段不修改业务代码。
包装现有 InitFaceModelManager,不重写底层 SDK 初始化算法。
增加 UNINITIALIZED、INITIALIZING、READY、FAILED 状态。
增加单次初始化和等待回调队列。
使用 ApplicationContext,不保存页面引用。
将 LaunchActivity 当前初始化结果映射到 Runtime,但保留原启动后续流程。
让弹框通过 ensureReady() 获取状态,不再直接重复初始化模型。
从 FaceRecognitionPop 抽出相机启动、帧回调和识别调用。
抽出倒计时、停止和重新识别逻辑。
增加会话序号和结果只分发一次保护。
统一停止异步任务、倒计时、动画和相机。
保持现有主、副屏预览参数。
保留构造方法和 FaceRecognitionResultListener。
保留现有布局、显示内容和按钮行为。
内部创建 Runtime 和 Session。
onDestroyView() 中停止 Session。
删除弹框内 initModel()、restartSelf() 和进程结束逻辑。
将 Session 结果映射回现有三个业务回调。
先验证 MainActivity 单屏。
再验证 MainActivity 双屏。
验证已选菜品和自定义金额在所有识别失败场景中保留。
验证关闭弹框后再次点击支付可以重新识别。
验证成功支付只清空一次订单。
逐个验证其余 6 个页面,不修改现有业务接口:
FreeActivity。
FreeNoKeyboardActivity。
QuotaActivity。
QuotaNoKeyboardActivity。
GuoxinFacePayActivity。
OrderVerifyActivity。
一期完成后实施:
增加 FaceRecognitionLauncher 统一弹框 Tag、生命周期门禁和防重复打开。
增加 FaceRecognitionRequest 统一金额、副屏、确认文案、Tag 和场景参数。
将 7 个页面的直接构造迁移为 Launcher 调用。
页面原业务监听器、支付和核验方法保持不变。
Launcher 拒绝展示时恢复页面支付门禁或菜单副屏状态。
仍作为后续可选项:
使用 newInstance(Bundle) 改进 Fragment 重建。
使用 FaceIdentity 隔离业务层对 SDK User 的依赖。
可选项不得与本次白屏修复或二期 Launcher 迁移强制绑定。
首次初始化成功后状态为 READY。
初始化失败后状态为 FAILED,不会重启应用。
READY 状态再次调用不会重复加载模型。
多个并发请求只执行一次真实初始化。
等待者全部收到同一次初始化结果。
Runtime 不保存 Activity 和 View 引用。
Runtime 未就绪前不提交相机帧进行检测。
Runtime 就绪后只启动一次相机。
多次识别回调只接受第一个有效结果。
弹框关闭后的旧回调不会更新 UI 或触发业务。
stop() 重复调用不会崩溃。
超时、取消、异常、确认均能释放相机和倒计时。
点击“重试”继续保持当前交互。
选择多个菜品后识别超时,菜品和总金额保持不变。
输入自定义金额后识别失败,自定义金额保持不变。
用户取消后订单保持不变。
模型初始化失败后不白屏、不返回首页、不结束进程。
相机失败后自动关闭弹框,订单保持不变。
关闭弹框后再次点击支付可以重新发起识别。
支付失败后订单保持不变。
支付成功后订单按现有逻辑清空一次。
双屏结束后副屏人脸区域按当前逻辑隐藏。
Activity 不可用时拒绝展示。
Fragment 状态已保存时拒绝展示。
相同 Tag 弹框存在时不重复创建。
正常状态允许展示。
SHOWN 和 ALREADY_SHOWN 被识别为已有活动弹框。
被拒绝的结果不被识别为活动弹框。
请求缺省金额和场景使用安全默认值。
7 个页面具有唯一且明确的调用场景映射。
每个页面都需要验证:
点击入口后弹框样式与当前版本一致。
倒计时和预览行为一致。
识别到用户后的信息、重试和确认交互一致。
点击确认后仍进入当前业务接口。
超时后自动关闭。
取消后自动关闭。
模型失败后安全关闭。
相机失败后安全关闭。
连续打开和关闭不会出现多个相机实例。
页面退出后不会收到旧识别回调。
使用项目兼容的 JDK 8 执行:
JAVA_HOME=/Users/liang/Library/Java/JavaVirtualMachines/corretto-1.8.0_482/Contents/Home \ sh ./gradlew :app:assembleDebug
如果项目已有相关单元测试,同时执行对应 Debug 单元测试任务。
风险:Runtime 接管初始化状态时改变 LaunchActivity 原有进入主页时机。
控制:保留 LaunchActivity 原回调顺序和后续数据库流程,Runtime 只统一状态与初始化并发,不接管导航。
风险:抽取 Session 后改变成功、结束或取消回调时机。
控制:以现有 FaceRecognitionResultListener 为兼容契约,逐条对照当前时机,不新增业务回调依赖。
风险:异常关闭时主屏弹框关闭,但副屏区域未隐藏。
控制:异常仍通过现有结束通道通知页面收尾,同时 Session 停止副屏相机数据输出。
风险:相机连续返回同一用户,导致多次执行成功回调。
控制:Session 使用最终结果只分发一次保护;确认按钮在提交后立即禁用,沿用当前关闭流程。
风险:Runtime 保存 Activity,或 Session 在弹框关闭后继续持有预览 View。
控制:Runtime 只使用 ApplicationContext;Session 在 stop() 中清理预览、回调和任务引用。
风险:Activity 状态已保存时安全拒绝展示,但页面支付门禁或菜单副屏没有恢复。
控制:Launcher 返回结构化 LaunchResult;自由和定额消费页在没有活动弹框时恢复 paymentIndex,菜单消费隐藏副屏人脸区域。
以下条件全部满足才允许判定实施完成:
模型初始化失败时不再执行弹框内的应用重启或进程结束。
人脸识别异常后不出现白屏,不返回首页。
菜单消费识别失败、超时、取消、模型异常、相机异常后,订单信息完整保留。
用户关闭弹框后可以再次点击支付重新识别。
识别弹框布局、文案、动画、按钮和操作步骤与当前版本一致。
识别到用户后仍由用户点击“确认”才进入业务。
7 个页面原支付或核验接口、参数和结果处理不变。
单屏和双屏行为一致且资源正确释放。
不产生重复识别成功回调和重复支付。
7 个页面统一使用 Launcher,页面内不再直接构造 FaceRecognitionPop。
Launcher 内只有一个弹框创建位置,且不包含支付、核验或订单接口调用。
Debug APK 构建通过,并完成 7 个页面的真机回归。
本文档是设计与实施约束;一期和二期代码已经修改,真机验收状态以对应变更记录为准。
实施应按阶段进行,每个阶段完成后先验证再进入下一阶段。
实际修改业务项目代码后,需要在 zhctprompt/work_android/CusumptionMachine/change_records/ 增加对应的 Markdown 与 HTML 变更记录。
未经明确要求,不执行 Git 暂存、提交、推送、分支切换或合并。
本次不通过修改业务流程规避问题,也不通过重做交互解决问题。最终方案是在现有 FaceRecognitionPop 外部契约不变的前提下:
使用 FaceRecognitionRuntime 承担应用进程级模型与人脸库状态。
使用 FaceRecognitionSession 承担单次弹框的相机和识别生命周期。
使用 FaceRecognitionLauncher 和 FaceRecognitionRequest 统一 7 个页面的弹框入口与参数。
保留 FaceRecognitionPop 当前 UI、按钮和业务监听器。
删除弹框初始化失败后的应用重启和进程结束。
所有异常安全关闭弹框并返回原业务页面。
菜单消费订单保持原样,用户重新点击支付即可再次识别。
最终验收基准为:除“异常时不再白屏、不再重启应用”这一修复结果外,当前业务、当前交互和当前页面状态规则均不改变。