# 手持核销车辆系统 — 开发方案 > 状态:**方案已按评审意见细化界面与流程,待最终确认后开发(暂不写代码)** > 日期:2026-09-25 > 目标设备:新大陆 NLS-MT95L 手持机(当前 ADB 已连接:MT95LTFFD00631),同时兼容通用安卓手机 > 识别引擎:百度车牌识别安卓离线 SDK V7.5 > 包名(已确认):com.ectrip.checkincar > 本期策略:**先模拟跑通全流程(识别 + 核销),百度 SDK 同步集成但等授权到位后启用** > 本次细化:第 5 章界面与交互(线框图 + 核销页 5 状态机 + 异常分支 + 硬件键/反馈 + 适配) --- ## 1. 需求概述 在手持机上开发一个竖屏安卓 App,供核销员工使用: 1. **主界面**:两个大按钮 —— 「车辆核销」「系统设置」。 2. **车辆核销流程**:调起后置摄像头拍摄/对准车牌 → 调用百度离线 SDK 识别车牌 → 展示识别结果并允许人工修正 → 调用后台预约平台接口完成核销 → 展示核销结果(预约信息)。 3. **系统设置页**:显示/配置 - 本机 IP 地址(自动获取、只读、可刷新) - 本机 MAC 地址(自动获取、只读) - 核销员工 ID(可编辑保存) - 后台核销地址(可编辑保存) - (附加)设备 SN、百度授权设备指纹、授权状态、识别引擎与模拟开关、版本号 4. 今天(第一期)后台与识别均先**模拟**,界面与流程按真实业务设计,后续只替换引擎与接口实现,不改界面。 --- ## 2. 实测环境 ### 2.1 目标手持机(ADB 实测) | 项目 | 实测值 | |---|---| | 厂商/型号 | Newland(新大陆)NLS-MT95L | | 序列号 SN | MT95LTFFD00631 | | 系统 | Android 11(API 30),固件 MT95L-X_V1.01.002_111006 | | CPU ABI | arm64-v8a(8 核) | | 内存/存储 | 2.8GB RAM;/data 20GB(仅用 593MB) | | 屏幕 | 720 × 1440,density 320(xhdpi),竖屏 | | 摄像头 | 后摄 + 前摄,支持自动对焦、闪光灯,camera level FULL | | 网络 | WLAN(wlan0),当前 IP `192.168.50.70/24`,SSID HUAWEI-ECTRIP-5G | | MAC | `a4:e8:a3:8d:82:56`(`/sys/class/net/wlan0/address`,权限 `-r--r--r--`,**App 无需权限可直接读取真实 MAC**,已实测节点存在且全局可读) | | 百度设备指纹(实测) | **`153DA2A38757D7D7016E76F8EE0AACE367`**(35 位大写。用官方 demo APK 7.5 在本机激活页实测取得,授权绑的就是它,**不是 SN**;正式 App 用同一套 jar 预期取值一致,以 App 设置页实显为准) | ### 2.2 开发机(macOS) - Android Studio 已安装;Android SDK 平台 19~36 齐全,build-tools 含 34.0.0 - JDK 8 / 11 / 17 / 19 均有(新工程用 **JDK 17**) - adb 路径 `/opt/homebrew/bin/adb`,设备连接正常 ### 2.3 百度 SDK 物料 路径:`/Volumes/USERDATA/anroidprj/Baidu_OCR_platenumber_OfflineSDK_Android_7.5-2/7.5车牌sdk/` | 文件 | 用途 | |---|---| | `sdk.zip` | 集成物料:3 个 jar + 两种 ABI 的 so + 模型资源 | | `ocr-sdk-offline-demo.zip` | 官方 demo 工程(Gradle,Java,已分析源码) | | `车牌识别安卓离线SDK_V7.5用户接入文档.pdf` | 官方接入文档(13 页,已逐页阅读) | | `车牌识别SDK_7.5.0.apk` | 官方演示 APK | | `readme.txt` | 模型与授权文件放 assets 的说明 | --- ## 3. 百度 SDK V7.5 技术分析(结论) ### 3.1 包内容(sdk.zip) **Java 依赖(3 个 jar)** - `libocrgveplate.jar` —— 车牌识别引擎(核心类 `com.baidu.vis.ocrgve.plate.OcrgveEnginePlate`) - `bd_unifylicense.jar` —— 统一授权组件(`AndroidLicenser` / `BDLicenseActivator`) - `liantian.jar` —— 安全模块 **Native 库(两种 ABI 都提供,本设备用 arm64-v8a)** - `libocrgveplate.so`(arm64 约 6.9MB)、`libpaddle_light_api_shared_visocr.so`(PaddleLite 推理,约 4.4MB)、`libbd_unifylicense.so`、`libliantian.so` **模型资源(assets,约 30MB,整目录拷入工程 assets)** - 目录名固定:`assets/ocrgve_resource_plate/` - 含 5 个加密模型(`.dpmodel.encrypted`:车牌检测、主分类、单行识别 a/b、车牌颜色)、`configure.ini`、字典与分类配置等 - `initFromAssets()` 内部按固定目录名加载,**目录名不可改** ### 3.2 支持范围 - Android 6.0+(文档声明),提供 armeabi-v7a / arm64-v8a;本设备 arm64-v8a 完全匹配 - 纯 Java 接口,离线推理(PaddleLite CPU),识别不依赖网络;**网络仅授权时需要** ### 3.3 核心 API(已通过 javap 反编译核对) ```java // 算法 ID(车牌识别固定值,demo 实测 = 7,以 getAlgorithmId() 返回为准) int algId = OcrgveEnginePlate.getAlgorithmId(); // ① 授权(必须在子线程调用;成功后才能初始化模型) // 方式A:本地授权文件(从控制台下载,放 assets) AndroidLicenser.ErrorCode ret = AndroidLicenser.getInstance() .authFromFile(context, licenseKey, licenseFileName, true/*isRemote*/, algId); // 方式B:在线(单台/批量)序列号激活,返回 0 成功 int ret = BDLicenseActivator.initLicenseOnLine(context, serialKey, "", algId); // 设备指纹(控制台绑定授权用) String deviceId = AndroidLicenser.getDeviceId(context); // 授权信息(有效期等) BDLicenseAuthInfo info = AndroidLicenser.getInstance().authGetAuthInfo(context, algId); // expireTime // ② 初始化模型(子线程;授权成功后调用;全局只初始化一次,重复初始化返回 MULTI_INIT=6) OcrgveEnginePlate engine = new OcrgveEnginePlate(); OcrgveEnginePlate.setPreFiltering(false); // 纯背景前置过滤,可选 int code = engine.initFromAssets(context); // 0=成功 // ③ 识别(子线程) ArrayList results = engine.processOcrgvesdk(rgbaBytes, 10/*PIX_RGBA*/, width, height, 0/*orientation*/); // ④ 释放(App 退出时) engine.uninit(); ``` **识别结果 `OcrgvesdkResponsePlate` 字段** | 字段 | 含义 | |---|---| | `name` | 类型,取 `"plate_number"` 为车牌结果 | | `mystring` | **车牌号字符串**(支持新能源 8 位等) | | `color` | 颜色:`blue/yellow/green/white/black`(蓝/黄/绿/白/黑) | | `detScore` | 检测置信度 | | `recgProb[]` | 每个字符的识别置信度(可用最小值做整体过滤,demo 默认阈值 0.8) | | `coords[8]` | 车牌四角坐标 | **初始化错误码**:0 成功、1 未知、2 非法参数、3 内部错误、4 鉴权错误、5 未初始化、6 重复初始化、7 图片处理错误、9 模型文件未找到。 **授权错误码(高频)**:7 设备 ID 校验错误、8 **包名(applicationId)校验错误**、9 过期时间不正确、11 授权已过期、12 本地文件读取失败、13 远程数据拉取失败。 ### 3.4 授权机制(关键约束,影响工程决策) > **重要澄清:百度授权绑定的是「设备指纹 Device-id」,不是设备硬件 SN。** > > | 标识 | 是什么 | 怎么取 | 用途 | > |---|---|---|---| > | **百度设备指纹 Device-id** | 百度安全模块(liantian)综合多因子硬件信息生成的一串大写 ID(带 `fingerVersion` 指纹版本) | 代码 `AndroidLicenser.getDeviceId(context)`;或 demo logcat 搜 `BDLicenseLocalInfo`(文档 2.5 节原文:「设备指纹即 deviceid」) | **百度离线授权的唯一设备标识,控制台必须绑它** | > | 设备硬件 SN | 厂商出厂序列号 `ro.serialno`(本机 = `MT95LTFFD00631`,与 ADB 设备号一致) | `Build.SERIAL` / 属性读取 / 设置页可手填兜底 | 仅我方资产管理、核销上报告备用,**百度授权不用它** | > > 注:demo 激活页把 Device-id 标注为「序列号」是口语化叫法,二者不是一回事。 > > **指纹构成(已反编译 liantian.jar 字节码核实)**:`com.baidu.liantian.ac.LH.getId()` 采集 `ANDROID_ID`、IMEI/IMSI/SIM 序列号、**直接读取 `/sys/class/net/wlan0/address` 与 `eth0/address` 的真实 MAC(键名 rpmacadd)**、WiFi BSSID、Build 硬件信息(MODEL/PRODUCT/BOARD/HARDWARE/MANUFACTURER/DEVICE/DISPLAY、CPU_ABI)等综合生成。 > > **稳定性(文档 6.9)**:刷机、更换硬件会导致指纹变化;安卓系统升级、App 卸载重装、恢复出厂值不会。 > > **本机实测(2026-09-25,官方 demo 7.5 激活页)**:设备指纹 = **`153DA2A38757D7D7016E76F8EE0AACE367`**,页面原文要求「点击复制下方本机硬件指纹 → 百度云控制台:离线 SDK 管理-单台设备授权-离线激活」。同时 logcat 显示 Android 11 已拒绝第三方读取 IMEI(`TelephonyPermissions ... getDeviceId:-1`),即本设备指纹实际由 ANDROID_ID + 真实 MAC + Build 硬件信息等因子生成,无电话权限也能稳定取到,**App 无需额外申请 READ_PHONE_STATE**。 1. 官方支持 4 种授权(V7.5 接入文档 2.1 节原文核实,jar 签名 `javap` 复核一致),**SN 和设备指纹都涉及,区别在激活方式**: | 方式 | 控制台操作 | 设备端调用(algId = `OcrgveEnginePlate.getAlgorithmId()` = 7) | 要不要手工提指纹 | |---|---|---|---| | ① 控制台离线授权文件 | 申请序列号 → 单台设备授权选「车牌识别」→ 点「离线激活」**绑定设备指纹** → 下载 `.ini` 授权文件 | 文件放 assets,`AndroidLicenser.getInstance().authFromFile(ctx, licenseKey, licenseFile, true, 7)`,文档示例 key/file = `AISEE_OCRGVE_7_ANDROID` / `AISEE_OCRGVE_7_ANDROID.ini`(key/file 同名时可直接照搬 demo) | **要**,先取指纹再去控制台绑定 | | ② 按 App 授权文件 | 申请按 app 授权的文件(绑包名+签名 MD5),licenseKey 随文件提供 | 同上 `authFromFile`,licenseKey/licenseName 填授权方给的值 | 否(绑包名+签名) | | ③ 在线单台设备序列号 | 控制台申请**单台序列号(SN)** | 设备联网调 `BDLicenseActivator.initLicenseOnLine(ctx, 序列号, "", 7)`,返回 0 成功;**首次联网激活时 SDK 自动把本机指纹绑定到该 SN** | 否(自动绑定,最省事) | | ④ 在线批量序列号 | 控制台申请**批量序列号(SN)** | 同③,同一个 SN 可激活多台设备 | 否 | 指纹获取 API(文档 2.5 节「获取本地鉴权设备指纹信息(离线激活需要)」,原文「设备指纹即 deviceid」): - 直接取:`AndroidLicenser.getDeviceId(context)`(本 App 设置页采用,实测 `153DA2A3…`) - 完整本地信息:`AndroidLicenser.getInstance().authGetLocalInfo(ctx, 7)` 返回 `BDLicenseLocalInfo`,字段含 `deviceId / packageName / md5(签名MD5) / licenseKey / fingerVersion`;demo 也可在 logcat 搜 `BDLicenseLocalInfo` 拿 Device-id。 2. **授权同时绑定「设备指纹 Device-id + 应用包名(applicationId)」**,一机一证;换设备、刷机、改包名均会失效;系统 OTA / 卸载重装不改变指纹。 3. **当前拿到的 SDK 包内没有任何授权文件**(assets 只有模型;demo 工程也没有 license),官方 demo 必须输入有效序列号/授权文件才能识别。 4. 因此:**应用包名必须先定下来再去申请授权,中途改包名授权作废**(包名已确认,见第 14 章;申请流程见第 11 章)。 5. 第一期 App 的设置页会同时展示「设备 SN(备查)」与「百度设备指纹(点按复制,授权必用)」,未购授权也能先取指纹去控制台申请。 6. **推荐落地路径**:手持机能联网时优先走方式③(设置页输入控制台申请的单台 SN,`initLicenseOnLine` 一键激活,无需手工收集指纹);设备现场无网络时走方式①(设置页读指纹 → 控制台离线激活下载 `.ini` → 随 App 打包进 assets,`LICENSE_KEY/LICENSE_FILE` 见 [LicenseManager.java](app/src/main/java/com/ectrip/checkincar/license/LicenseManager.java) 常量)。 7. 已实测坑:未授权状态下 `getAuthStatus(7)` 在该 SDK 内会抛 `ArrayIndexOutOfBoundsException: length=18; index=1000`(其内部用状态码索引 ErrorCode 枚举),必须 try/catch;`authGetAuthInfo()` 未授权时直接 native SIGSEGV 崩溃,**必须先确认 SUCCESS 再调**。 ### 3.5 demo 流程分析(可直接借鉴) `StartActivity`(检查授权)→ `ActivitionActivity`(三种激活方式 + 展示设备指纹)→ `HomeActivity`(子线程 `initFromAssets`)→ `ScaleActivity`(Camera1 连续取流 640×480 NV21 → 旋转为正向 Bitmap → RGBA → 子线程识别 → 置信度过滤 → 出结果)。 demo 相机部分较老旧(Camera1 + 离屏 SurfaceTexture + ImageView 显示画面),新工程不照搬,改用 CameraX;**授权、初始化、识别的调用时序与线程模型严格按 demo/文档实现**。 --- ## 4. 总体设计 ### 4.1 业务流程(详细设计见第 5 章) ``` ┌─────────┐ 车辆核销 ┌──────────────┐ 对准/拍照 ┌──────────────┐ │ 主界面 │ ─────────▶ │ 相机识别页 │ ──────────▶ │ 结果确认卡片 │ │ Main │ ①点击 │ VerifyActivity│ ②自动/拍照 │ (可人工改牌) │ └─────────┘ └──────────────┘ └──────┬───────┘ │ 系统设置 ▲ 重拍/继续识别 │ ③确认核销 ▼ └─────────────────────┤ ┌─────────┐ ▼ │ 设置页 │ ┌──────────────────┐ │ Settings│ │ 调用后台核销接口 │ └─────────┘ │ (本期 Mock) │ └──────┬───────────┘ ▼ ┌──────────────────┐ │ ④核销结果(成功/失败)│ │ 成功→2s 自动回取景 │ │ (连刷下一辆) │ └──────────────────┘ ``` **设计目标:每辆车常规操作只需 2 次点击** —— 主屏点「车辆核销」→ 对准车牌(自动识别)→ 点「确认核销」→ 成功后自动回到取景位继续下一辆,无需返回主屏。 识别策略(已确认:**自动 + 拍照**):连续自动识别为主(对准即出结果),手动拍照按钮为辅;取流帧节流约 400ms、单线程串行识别,命中且最小字符置信度 ≥ 阈值(默认 0.80,可设)后停止取流进入确认;支持「重拍」恢复。附闪光灯开关;预留音量下键/手持机实体扫描键作为拍照快捷键。 ### 4.2 技术栈 | 项 | 选型 | 理由 | |---|---|---| | 语言 | **Java 1.8** | 百度 SDK 与 demo 均为 Java,可最大量直接复用经过验证的调用代码 | | IDE/构建 | Android Studio + Gradle 8.x + AGP 8.x + **JDK 17** | 本机已具备;compileSdk/targetSdk 34 | | minSdk | **23(Android 6.0)** | 对齐百度声明的支持下限,兼容通用安卓手机 | | ABI | arm64-v8a + armeabi-v7a | 覆盖手持机与主流手机 | | 相机 | **CameraX 1.3.x**(camera-camera2 + lifecycle + view) | 生命周期安全、自动处理预览方向/分辨率;MT95L 为标准 Camera2 FULL 设备 | | UI | AndroidX AppCompat + Material + ViewBinding,传统 View/XML | 竖屏固定,720×1440 dp 适配(基准宽 360dp) | | 网络 | OkHttp 4 + org.json | 轻量;核销接口只有一个 | | 存储 | SharedPreferences(设置)+ 本地 JSON(核销记录,最近 100 条) | 不引入数据库,保持简单 | | 方向 | 全应用 `screenOrientation="portrait"` | 需求明确竖屏 | > 若 CameraX 在该工业设备上出现兼容性问题(概率低),回退方案:移植 demo 的 Camera1 取流代码(已验证可在百度引擎上跑通),识别/业务层不变。 ### 4.3 工程结构(拟新建于当前目录) ``` checkInCar/ ├── 手持核销车辆系统.md ← 本文档 ├── settings.gradle / build.gradle / gradle.properties / gradlew └── app/ ├── build.gradle └── src/main/ ├── AndroidManifest.xml ├── assets/ │ └── ocrgve_resource_plate/ ← 从 sdk.zip 整目录拷贝(约30MB) │ (百度授权 *.ini 待用户提供后放这里) ├── jniLibs/ │ ├── arm64-v8a/ ← 4 个 so(手持机用) │ └── armeabi-v7a/ ← 4 个 so(通用手机兼容) ├── libs/ ← libocrgveplate.jar / bd_unifylicense.jar / liantian.jar ├── res/ ← layout/drawable/values/mipmap(竖屏 UI) └── java/com/ectrip/checkincar/ ├── App.java ← Application,引擎单例初始化 ├── MainActivity.java ← 主界面(2 个按钮 + 今日核销数) ├── ui/settings/SettingsActivity.java ├── ui/verify/VerifyActivity.java ← 相机 + 识别 + 结果确认 + 核销 ├── core/prefs/AppPrefs.java ← 全部设置项的读写 ├── core/device/DeviceInfo.java ← IP / MAC / SN 获取(多策略) ├── plate/ │ ├── PlateInfo.java ← 车牌号/颜色/置信度/位图 │ ├── PlateRecognizer.java ← 识别引擎接口(可切换) │ ├── MockPlateRecognizer.java ← 模拟识别(本期默认) │ └── BaiduPlateRecognizer.java ← 百度 SDK 封装(授权+init+识别) ├── license/LicenseManager.java ← 百度授权状态/在线激活/设备指纹 ├── net/ │ ├── VerifyApi.java ← 核销接口(真实 HTTP) │ └── MockVerifyApi.java ← 模拟核销(本期默认) ├── model/VerifyRequest.java / VerifyResult.java └── history/VerifyHistory.java ← 本地核销记录(JSON) ``` > 包名 `com.ectrip.checkincar`(结合目录名 checkInCar 与现场 WiFi ECTRIP)**已评审确认采用**,App 显示名「车辆核销」;该包名将用于百度授权申请,之后不再变更。 --- ## 5. 界面与交互详细设计(本章为本次重点细化内容) ### 5.0 设计原则 1. **竖屏单手操作**:全部页面锁竖屏;核心按钮放在拇指热区(屏幕下半部),高度 ≥56dp。 2. **名称四字对齐**:主按钮统一 4 个字 —— 「车辆核销」「系统设置」「确认核销」「重拍」。 3. **步骤最少**:常规每车仅 2 次点击(进核销 → 确认核销),识别与连刷全部自动化。 4. **紧凑两列**:设置页信息项采用「左标签 : 右值」左右两列、分组卡片排列,减少纵向行数,把屏幕空间留给状态/记录区域。 5. **状态可见**:每个耗时动作都有明确提示(识别中、核销中),结果用大字 + 颜色 + 振动/音效三重反馈,户外嘈杂环境也能用。 ### 5.1 页面清单与跳转关系 | 页面 | Activity | 进入方式 | 返回行为 | |---|---|---|---| | 主界面 | MainActivity | 启动页 | — | | 系统设置 | SettingsActivity | 主界面「系统设置」 | 返回即回主界面,配置自动保存 | | 车辆核销 | VerifyActivity | 主界面「车辆核销」 | 返回主界面;核销成功不关闭本页,自动回取景态 | 页面间不产生堆叠(核销页只有一个实例),应用内不做登录页(员工 ID 设置页手填)。 ### 5.2 主界面 MainActivity(线框,基准 360×720dp) > **2026-09-25 更新:主界面按「鼎游 ECTRIP 科技风」改版(品牌 Logo + 模式状态 + 大按钮),最新设计以第 15.2 节为准,本节线框保留作历史对照。** ``` ┌──────────────────────────────┐ │ 车辆核销 v1.0.0 │ ← 标题栏 48dp,右侧版本号 │ │ │ │ │ ┌────────────────┐ │ │ │ │ │ │ │ 🚗 车辆核销 │ │ ← 主按钮:高 110dp,主色实心, │ │ │ │ 28sp 粗体,点击进核销页 │ └────────────────┘ │ │ │ │ ┌────────────────┐ │ │ │ ⚙ 系统设置 │ │ ← 次按钮:高 96dp,浅色描边 │ └────────────────┘ │ │ │ │ │ │ 引擎:模拟 网络:WLAN ✔ │ ← 状态行 14sp │ 今日已核销 12 台 │ └──────────────────────────────┘ ``` - 两按钮纵向居中、宽度占屏 80%;「车辆核销」视觉权重最高。 - 底部状态行实时显示:识别引擎(模拟/百度)、网络是否在线、**今日成功核销数**(从本地记录统计,进页面 onResume 刷新)。 - 连续点击「车辆核销」防抖;若选百度引擎但未授权/模型未就绪,按钮仍可点,进核销页时弹提示并引导去设置(不闪退)。 ### 5.3 车辆核销页 VerifyActivity —— 单页 5 个状态(核心) 该页只有一个 Activity,通过覆盖层切换 5 个状态,避免页面跳转打断节奏: ``` S1 取景待识别 ──自动帧/拍照──▶ S2 识别中 ──命中且置信度达标──▶ S3 结果确认 ▲ │ │ │ 未命中/低分 │ 确认核销 │ └──────────────▶ 回 S1 ▼ │ S4 核销提交中 │◀──────────── 成功:2s 后自动返回(连刷)──────────────────┤ │ │ └────────────────── 失败:停留,手动「重试/重拍」◀── S5 核销结果(成功/失败) ``` #### S1 取景待识别 ``` ┌──────────────────────────────┐ │ ✕ 车辆核销 💡 开 │ ← 顶栏:左=返回主界面,右=闪光灯开关 │ │ │ 后摄全屏预览 │ │ ┌────────────────────┐ │ │ │ ╲ ╱ │ │ ← 车牌对齐框(横向矩形,四角 L 形 │ │ │ │ 高亮线 + 半透明遮罩) │ │ │ │ │ │ ╱ ╲ │ │ │ └────────────────────┘ │ │ │ │ 将车牌移入框内,自动识别 │ ← 提示文字随状态变化 │ │ │ ⬤ │ ← 底部居中大快门(高 72dp), │ [拍照识别] │ 手动拍照;音量下键/扫描键等效 └──────────────────────────────┘ ``` - CameraX 全屏预览,画面按竖屏自动旋转校正;对齐框宽占屏 86%、高约宽的 1/3。 - 自动识别在后台持续进行(用户无感);快门是主动兜底。 - 连续 3 秒无任何检测时,提示文字轮播:「请靠近车牌 / 保持光线充足 / 点击下方按钮拍照」。 #### S2 识别中 - 命中帧后立即停止取流,画面定格;对齐框四角变为加载色,提示「识别中…」(正常 200~600ms,不做全屏遮罩,避免闪烁)。 - 结果不达标(无车牌或最低字符置信度 < 阈值):Toast 弱提示一次并自动回 S1 继续取流,不弹错误页。 #### S3 结果确认(底部抽屉式卡片,不挡全图) ``` ┌──────────────────────────────┐ │ (定格的车牌画面) │ │ │ ├──────────────────────────────┤ │ 车牌号 蓝牌 │ ← 颜色标签:蓝/黄/绿/白/黑 │ ┌──────────────────────────┐│ │ │ 京 A8D6F8 ✎ ││ ← 36sp 超大号;点 ✎ 弹键盘手改 │ └──────────────────────────┘│ │ 置信度 0.96 员工 10086 │ ← 次要信息 13sp 一行 │ │ │ ┌──────────┐ ┌────────────┐ │ │ │ 重拍 │ │ 确认核销 │ │ ← 左次右主,等高 56dp │ └──────────┘ └────────────┘ │ └──────────────────────────────┘ ``` - **车牌可人工修正**:点击号码区域弹数字+省份简称键盘(系统键盘即可,输入自动去空格/转大写);手改后置信度置灰显示「人工输入」。 - 员工 ID 取自设置页;若为空,点「确认核销」时提示「请先在设置中填写核销员工 ID」并给「去设置」入口。 - 后台地址为空或非法同理拦截,不发无效请求。 #### S4 核销提交中 - 卡片切换为 Loading:旋转指示 +「核销中,请勿移开…」;按钮全部禁用防重复提交;HTTP 超时 10s。 #### S5 核销结果 ``` 成功(绿底) 失败(红底) ┌──────────────────────┐ ┌──────────────────────┐ │ ✔ │ │ ✕ │ │ 核销成功 │ │ 核销失败 │ │ 京A8D6F8 │ │ 京A8D6F8 │ │ ─────────────────── │ │ 原因:该车牌无预约记录 │ ← 后台 msg 原文 │ 预约人:张三 │ │ │ │ 场 地:P1预约车位 │ │ │ │ 时 段:今日09-18点 │ │ │ │ 2 秒后自动继续… │ │ [重拍] [手改车牌重试] │ │ [立即继续] │ │ [返回] │ └──────────────────────┘ └──────────────────────┘ ``` - **成功:提示音 + 长振动 1 次**,展示后台返回的预约信息(无字段则只显车牌),2 秒倒计时后**自动回 S1 连刷**;点「立即继续」不等计时。 - **失败:短促两声振动 + 低音**,原因直接显示后台 `msg`(如:无预约 / 已核销 / 不在有效期 / 网络异常),不自动跳转,由员工选择重拍、手改车牌重试或返回。 - 无论成败都写本地记录(时间、车牌、颜色、员工、结果、原因),成功计入今日数。 ### 5.4 系统设置 SettingsActivity(紧凑两列分组卡片) ``` ┌──────────────────────────────┐ │ ‹ 系统设置 │ │ ── 设备信息(自动获取)─────────│ │ 本机 IP 192.168.50.70 ⟳ │ ← 左标签120dp 右值,⟳=刷新 │ MAC 地址 a4:e8:a3:8d:82:56 │ 值可点复制;IP/MAC 整行左右排 │ 设备 SN MT95LTFFD00631 📋 │ 硬件序列号,仅备查/上报 │ 授权指纹 86F3A2…D9C1 📋 │ ← 百度Device-id,授权必绑(详见3.4) │ 指纹版本 v3 / 算法ID 7 │ │ 授权状态 未授权 │ │ ── 核销配置 ─────────────────│ │ 员工 ID [ 10086 ] │ ← 标签左、输入框右,同一行 │ 核销地址 [ http://192.16… ] │ ← 一行,保存时校验 URL │ ── 识别配置 ─────────────────│ │ 识别引擎 (●模拟 ○百度离线) │ ← 单选 segmented │ 置信阈值 [────●────] 0.80 │ ← 滑块 0.50~0.99 │ ── 模拟配置(模拟引擎时显示)──│ │ 模拟车牌 [ 留空=随机 ] │ │ 识别耗时 [ 300 ] ms │ │ 核销结果 (●成功 ○失败 ○随机) │ │ ── 授权(百度引擎时显示)──────│ │ 序列号 [ ] │ │ [ 激活 ]│ │ │ │ 版本 v1.0.0 今日核销 12 台 │ └──────────────────────────────┘ ``` 交互规则: - **所有配置改完即存**(失焦/切换即写 SharedPreferences),不做「保存」按钮,减少一次点击。 - 「模拟配置」与「授权」分组按当前引擎**联动展开/收起**,不堆无关项。 - IP、MAC、授权指纹、SN 点击整行复制到剪贴板并 Toast「已复制」(申请授权、报备设备时高频使用);「授权指纹」行用浅色小字注明「百度授权绑定此 ID,非设备 SN」,防止混淆(见 3.4)。 - 授权状态:未授权 / 已授权(至 yyyy-MM-dd)/ 授权过期(红色)。 - 核销地址做格式校验(http(s):// 前缀 + host),不合法时行尾红字提示且不保存。 - 整页可滚动,但上述内容在 720×1440 一屏内基本可见,不做冗余留白。 ### 5.5 异常分支与处理方式一览 | 场景 | 触发时机 | 界面处理 | |---|---|---| | 相机权限未授予 | 进核销页 | 内嵌说明卡片 +「去授权」按钮跳系统设置;返回后自动重试 | | 设备无后摄/打开失败 | 绑定相机 | 结果位显示「相机不可用」+ 返回按钮(模拟模式下可用纯手动输牌继续演示) | | 百度未授权/授权过期 | 引擎=百度且校验失败 | 弹窗显示设备指纹与错误码中文说明 →「去设置激活」;可切回模拟模式 | | 模型初始化失败 | 进核销页前 | 提示错误码(4 鉴权/9 模型缺失等),禁止进入 S1,回设置处理 | | 一直识别不到车牌 | S1/S2 | 不打断,文字引导;员工可手动拍照 | | 识别到但置信度低 | S2 | Toast「识别不确定,请靠近或手改」后回 S1;若手改则直接进 S3 | | 员工 ID/核销地址为空 | S3 点确认 | 输入框抖动提示 +「去设置」,不发请求 | | 网络不通/超时(10s) | S4 | S5 失败态:原因「网络异常,请检查 WiFi 与核销地址」,可原样重试不重拍 | | 后台返回 code≠0 | S4→S5 | 失败态显示 msg(无预约/已核销/不在有效期…),支持改牌重试 | | HTTP 非 200/返回非法 JSON | S4 | 失败态「服务异常(HTTP 500)」并记录日志 | | 连续重复提交 | S4 | 按钮禁用,二次点击忽略 | | 识别中按返回 | S1~S3 | 直接回主界面,释放相机与分析器,引擎不销毁 | | 来电话/切后台 | 生命周期 onPause | 停取流停对焦;回前台自动恢复 S1 | ### 5.6 硬件键与反馈 - **物理键映射(手持机友好)**:音量下键 = 拍照(KeyEvent 监听);如后续确认 MT95L 实体扫描键可用,将其键值映射为同一动作(设置里加开关,默认开)。 - **音效**:成功/失败两种短音(assets 内置或系统提示音),跟随系统媒体音量;设置页暂不做静音开关(可后加)。 - **振动**:成功长振 1 次、失败短振 2 次,使用 VibrationEffect(API 26+),低版本降级。 - **闪光灯**:每次进页默认关闭,状态不记忆(避免白天误开)。 ### 5.7 屏幕适配 - 基准宽 360dp(720px@320dpi),用 ConstraintLayout + LinearLayout 组合;文字用 sp,触控目标 ≥48dp。 - 长屏手机(≥800dp 高)拉伸按钮间距与预览区,设置页两列比例固定;小屏(320dp 宽老机)允许设置页滚动,核销页对齐框按比例缩放。 - 颜色仅取功能语义(主色蓝、成功绿、失败红、标签灰),白天户外对比度优先;不使用横屏布局。 --- ## 6. 关键模块设计 ### 6.1 设备信息获取(IP / MAC / SN,已在设备上验证可行路径) - **IP**:`NetworkInterface.getNetworkInterfaces()` 枚举,取 `wlan0`(其次 eth0)上非 loopback、非 link-local 的 IPv4。实测当前 `192.168.50.70`。无需权限。 - **MAC(Android 6+ 官方 API 被屏蔽,返回 02:00:00:00:00:00)**,按顺序兜底: 1. 直接读 `/sys/class/net/wlan0/address` —— **本机实测权限 `-r--r--r--` 可读**,值 `a4:e8:a3:8d:82:56`,作为首选; 2. 枚举 `/sys/class/net/*/address` 找首个非 00/02 开头地址; 3. `NetworkInterface.getHardwareAddress()`; 4. `WifiInfo.getMacAddress()`(基本是兜底假值,并明确标注「系统限制」)。 - **SN**:`Build.getSerial()` 在 API 29+ 需要 READ_PRIVILEGED_PHONE_STATE(普通应用拿不到),改用 `Build.SERIAL`(28 以下)+ 读 `ro.serialno` 反射/备用;本设备 SN 与 ADB 一致(MT95LTFFD00631)。如系统接口受限,设置页允许**手工填写设备编号**并保存。 - 进入设置页与点击刷新时重新获取。 ### 6.2 相机与识别 - CameraX:`Preview` + `ImageAnalysis(STRATEGY_KEEP_ONLY_LATEST)`,目标 640×480 / 接近 4:3;输出 YUV_420_888 → 转 Bitmap(按 targetRotation 旋转为正向)→ 必要时缩放到宽 ≤1280 → `copyPixelsToBuffer` 得到 RGBA byte[]。 - 识别在单线程 Executor 串行执行(ReentrantLock 思路同 demo),帧间隔 ≥400ms;识别期间丢弃新帧。 - 结果过滤:仅取 `name=plate_number`;`min(recgProb) ≥ 阈值`(可配)才接受;记录颜色。 - 命中后 `analysis.clearAnalyzer()` 停止取流,显示结果卡片;重拍时重新绑定。 - 手电筒:`CameraControl.enableTorch`;权限:CAMERA 运行时申请。 - 引擎生命周期:`BaiduPlateRecognizer` 随 Application 持有单例,进入核销页前确保已授权 + `initFromAssets` 完成(只初始化一次);切换引擎/页面不重复初始化。 ### 6.3 模拟识别(MockPlateRecognizer,本期默认) - 实现同一个 `PlateRecognizer` 接口,延时 200~500ms 返回 `PlateInfo`: - 车牌 = 设置项固定值,或内置车牌库随机(如 京A·8D6F8 蓝牌、粤B·D12345 新能源绿牌、黄底牌等); - 置信度 0.90~0.99;颜色随牌型。 - 也支持在结果卡片直接手输车牌(真机演示时的保底手段)。 - **目的:不依赖百度授权即可完整演示「拍(模拟)→ 识 → 改 → 核销 → 记录」闭环。** ### 6.4 后台核销接口 - 统一封装 `VerifyApi` 接口,`MockVerifyApi` 与真实 `HttpVerifyApi` 可按设置切换,业务层无感。 - 请求在子线程,OkHttp 超时连接 5s / 读取 10s;支持 http 明文(现场内网地址),通过 `network_security_config` 放行内网明文,不全局放开。 - 接口契约为**草案(待与预约平台对齐)**,见第 7 章;Mock 按该契约返回。 - 每次成功/失败均写本地记录(时间、车牌、员工、结果、原因),主界面显示「今日成功核销数」。 ### 6.5 设置存储 AppPrefs - 单个 SharedPreferences(`car_verify_prefs`)封装全部键:staffId、baseUrl、engineType、scoreThreshold、mockPlate、mockResult、设备编号手填值等;改动即时生效。 ### 6.6 授权管理 LicenseManager - 封装:查询设备指纹、查询授权状态与到期时间、在线序列号激活、离线文件 authFromFile。 - 严格遵守官方时序:**授权(子线程)成功 → 再 initFromAssets(子线程)**,二者不并发、不重复初始化;所有错误码映射为中文提示(第 3.3 节)。 - App 启动时若引擎为「百度」:后台静默校验授权与模型状态,主界面/设置页展示;未授权则点击「车辆核销」时引导去授权页,不会闪退。 --- ## 7. 后台核销接口契约(草案,待平台方确认) **请求** ``` POST {后台核销地址}/api/checkin/carVerify Content-Type: application/json ``` ```json { "plateNo": "京A8D6F8", // 车牌号(必填) "plateColor": "blue", // blue/yellow/green/white/black "staffId": "10086", // 核销员工ID "deviceSn": "MT95LTFFD00631", // 设备SN "mac": "a4:e8:a3:8d:82:56", "ip": "192.168.50.70", "timestamp": 1727230000000 // 毫秒 } ``` **响应** ```json { "code": 0, // 0=核销成功,其他=失败 "msg": "核销成功", "data": { "reservationId": "YY20260925001", "visitorName": "张三", "lotName": "P1 预约车位", "visitTime": "2026-09-25 09:00-18:00", "plateNo": "京A8D6F8" } } ``` - 常见失败:无预约、已核销、不在有效期、车牌不匹配等,以 `code/msg` 展示。 - 真实路径、字段名、鉴权方式(是否加 token/签名/租户号)**拿到平台文档后只改 `HttpVerifyApi` 与 model**,界面不动。 --- ## 8. 第一期(今天,模拟版)交付内容与验收 **交付**:可在 MT95L 上安装运行的 debug APK + 完整 Android Studio 工程。 1. 新建工程、拷贝百度 jar/so/模型进工程(不启用不调用,仅保证能打包通过)。 2. 主界面两个按钮 + 今日核销数;全屏竖屏。 3. 设置页:IP/MAC/SN 自动显示并可刷新;员工 ID、核销地址可保存;引擎/阈值/模拟参数可配;显示百度设备指纹(即使未授权也能取到,供申请授权用)。 4. 核销页:CameraX 后摄预览、对焦、闪光灯、对齐框、手动拍照;**模拟识别**出车牌;结果卡片可改车牌;**模拟核销**返回成功/失败(可配);成功/失败结果页 + 振动;本地记录与今日计数。 5. 相机、网络等运行时权限处理。 6. `adb install -r` 安装到 MT95LTFFD00631 真机走查全部流程(竖屏、720×1440 适配)。 **验收标准** - 手持机上能连续演示:进入核销 → 对准/拍照 →(模拟)出号 → 改号 → 确认核销 → 成功/失败 → 继续下一辆。 - 设置页显示的 IP=`192.168.50.x`、MAC=`a4:e8:a3:8d:82:56`(以实际为准),员工 ID 与地址重启后保留。 - 切换「模拟核销结果=失败/随机」时错误流程与提示正确。 ## 9. 第二期(授权与真实接口到位后) 1. 你方在百度控制台完成授权(流程见第 11 章),或提供在线序列号。 2. 引擎切「百度离线识别」:真机初始化模型 → 真实车牌识别调试(光照、距离、新能源/黄牌、置信度阈值、识别耗时实测)。 3. 按预约平台真实文档实现 `HttpVerifyApi`、联调核销(含无预约/重复核销等异常分支),必要时加 token/签名。 4. release 签名、混淆(追加百度 keep 规则:`-keep class com.baidu.vis.**{*;}`、`-keep class com.baidu.liantian.**{*;}`、keep native 方法)、出正式包。 5. 如多台手持机推广:采用**在线批量序列号**,在设置页逐台激活,避免每台单独打包 license 文件。 --- ## 10. 构建、安装与验证命令 ```bash # 工程就绪后(本机 JDK17 + Android SDK 34) ./gradlew assembleDebug adb -s MT95LTFFD00631 install -r app/build/outputs/apk/debug/app-debug.apk adb logcat | grep -iE "Ocrgve|BDLicense|carverify" # 看授权/初始化/识别日志 ``` - 真机信息复核:`adb shell wm size` → 720x1440;`getprop ro.product.cpu.abi` → arm64-v8a。 - 授权排障时以设备指纹 + 授权错误码(第 3.3)为准,不凭猜测下结论。 --- ## 11. 百度授权办理流程(需要你方操作,开发侧配合) 1. **applicationId 已确认为 `com.ectrip.checkincar`,授权申请即按此包名,之后不再更改**。 2. 安装第一期 APK 后,在「系统设置」页复制**百度设备指纹(Device-id,授权必绑,注意不是硬件 SN)**——每台手持机不同;第一期 App 无需授权即可显示该值。当前这台 MT95L 用同版本 SDK 实测指纹为 `153DA2A38757D7D7016E76F8EE0AACE367`(可直接拿它去控制台申请,正式 App 装好后再复核一次)。 3. 登录百度智能云控制台 → 车牌识别离线 SDK 授权管理: - **单台离线授权**:新建单台授权,选择模型「车牌识别」、填设备指纹、绑定包名 → 离线激活 → 下载授权文件(`*.ini` + 对应 licenseKey)→ 发给开发放入 `assets/` 重新出包(一台一证,适合设备少)。 - **在线序列号(推荐多机)**:申请单台/批量序列号 → 在 App 设置页输入激活(需联网一次),激活信息本地持久化,之后离线可用。 4. 开发侧用 `authFromFile(key, 文件名, true, algId)` 或 `initLicenseOnLine(序列号)` 完成鉴权,成功后初始化模型。 5. 注意:App 签名、包名需与申请时一致;正式发布前用 release 签名重新核对授权策略。 --- ## 12. 风险与对策 | 风险 | 影响 | 对策 | |---|---|---| | 暂无百度授权文件/序列号 | 真机无法真实识别 | 第一期用模拟引擎跑通全部业务;SDK 物料与封装先行;设置页先取设备指纹便于立即申请授权 | | 授权绑定包名+设备指纹 | 包名定死、换机失效 | 包名先评审确认;多设备用在线批量序列号;换机要重新授权(官方规则) | | Android 11+ MAC 被系统屏蔽 | 设置页显示假 MAC | 首选读 sysfs(本机实测可读真实 MAC),多级兜底并标注来源 | | 工业设备 CameraX 兼容性 | 取流异常 | CameraX 先行;异常回退 demo 的 Camera1 方案,识别层不变 | | 现场内网 HTTP 明文 | 9.0+ 默认禁止明文 | networkSecurityConfig 仅放行配置的内网地址/端口 | | 模型/so 体积大(约 50MB) | APK 偏大 | arm64+v7a 都保留(通用手机兼容);后续可按渠道拆 ABI 包 | | 重复初始化/线程错误 | 模型初始化失败 | 严格单例、子线程、授权后初始化;错误码中文映射 | | 后台接口未确定 | 无法真实核销 | 接口层抽象 + Mock;契约草案先行,平台文档到位后仅替换实现 | --- ## 13. 配置/权限清单(开发时照此实施) - `AndroidManifest.xml`:`CAMERA`、`INTERNET`、`ACCESS_NETWORK_STATE`、`READ_EXTERNAL_STORAGE`(仅相册调试,API≤28);`largeHeap=true`;全部 Activity 竖屏;FileProvider(相册选图)。 - `build.gradle(app)`:minSdk 23 / target+compile 34;`ndk { abiFilters 'arm64-v8a','armeabi-v7a' }`;`jniLibs.srcDirs=['libs']` 或使用标准 jniLibs 目录;`implementation files('libs/*.jar')`;CameraX / OkHttp 依赖;`compileOptions 1.8`。 - 资源:`assets/ocrgve_resource_plate/` 整目录;so 与 jar 不重打包损坏(从 zip 解压后二进制原样拷贝)。 --- ## 14. 评审结论与遗留事项 ### 已确认(2026-09-25 评审) 1. ✅ 包名 applicationId:**com.ectrip.checkincar**,显示名「车辆核销」。 2. ✅ 第一期**默认模拟识别 + 模拟核销**,百度 SDK 同步完成集成与封装、暂不启用真实识别(当前无授权文件,这是今天跑通真机全流程的方式)。 3. ✅ 识别交互:**对准自动识别 + 手动拍照兜底 + 结果可人工修正**(详见 5.3 五状态设计)。 4. ✅ 核销员工 ID:**系统设置页手工填写并本地保存**。 5. ✅ 本次重点细化方向:**界面与流程**(第 5 章已补充设计原则、页面跳转、三屏线框图、核销页状态机、异常分支、硬件键与反馈、适配规则)。 ### 仍遗留(不阻塞第一期开发,可后续提供) 1. 预约平台核销接口是否已有正式文档?没有则先按第 7 章契约模拟,拿到后仅替换 `HttpVerifyApi` 与 model。 2. 百度授权由谁在控制台申请、预计何时提供序列号或授权文件(流程见第 11 章)。**本机指纹 `153DA2A38757D7D7016E76F8EE0AACE367` 已实测取得,现在就可以去控制台申请离线授权**;官方 demo APK(车牌识别7.5)已临时安装在手持机上用于取指纹,可保留用于授权后自测,也可卸载。 3. 是否需要核销记录的**查询/导出页面**?(第一期仅本地落盘 + 主界面今日计数;如需要可补一个记录列表页,支持按日期查看。) 4. MT95L 实体扫描键是否需要映射为拍照快捷键(第一期先适配音量下键,扫描键拿到真机键值后即可加开关)。 --- > **方案已按评审意见完成界面与流程细化。如无其他修改意见,请确认,我将新增 com.ectrip.checkincar 工程并在 MT95L 真机上开发、安装、验证第一期(模拟版)。** --- ## 15. 第二轮变更(2026-09-25):品牌图标、科技风主界面、在线/离线识别三路由 > 第一期模拟版真机走查通过后追加。**本章与前述章节冲突处,以本章为准。** ### 15.1 应用图标更换为 ECTRIP 鼎标(本轮已实施 ✅) - 源文件:工程根目录 [ectrip.ico](file:///Volumes/USERDATA/anroidprj/checkInCar/ectrip.ico)(Windows ICO,含 16/32/48/256 四档,256 主档透明底红色「鼎形 ECTRIP」标,与现场手持机上「智能移动验证系统」图标同源)。 - 已用 PIL 从 256px 主档生成全套 Android 启动图标(桌面图标 = 安装后显示图标,Android 不区分二者): - `mipmap-{mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi}/ic_launcher.png`(48/72/96/144/192px 方形,白底红鼎) - 同目录 `ic_launcher_round.png`(圆形裁剪版,供部分启动器) - 同目录 `ic_launcher_foreground.png`(108/162/216/324/432px,自适应图标前景,鼎标占画布 60%,保证落在 66dp 安全圈内) - [mipmap-anydpi-v26/ic_launcher.xml](file:///Volumes/USERDATA/anroidprj/checkInCar/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml) / `ic_launcher_round.xml`:白底 + 鼎标前景(API26+ 自动裁剪成圆形/圆角方形);旧蓝底车牌矢量前景已删除 - 背景取**白色**(红鼎在深蓝底上偏闷;参考 App 的品牌呈现也是红鼎 + 浅蓝徽标轮廓 + 白底)。 - 已 `assembleDebug` + 装机(2026-09-25),桌面/任务管理/安装确认界面均显示新图标。 - 主界面顶部要展示的**竖版品牌区**另用 256px 透明底 PNG(`drawable-nodpi/ic_logo_ectrip.png`),实施 15.2 时拷入,不做进 mipmap(mipmap 仅用于启动图标)。 ### 15.2 主界面科技风改版(参考现场鼎游「智能移动验证系统」界面)(本轮已实施 ✅) > 实施落地(2026-09-25 真机截图确认):品牌区 200dp 深蓝渐变 + 八边形描边徽标(`drawable/bg_octagon.xml`,内嵌 `drawable-nodpi/ic_logo_ectrip.png` 38dp 红鼎)+「鼎 游 信 息 / ECTRIP · SMART VERIFY」+ 25sp 宽字距标题 + 模式胶囊;底 2dp `#35C2FF` 高光线;内容区四角 L 形青色角标(`drawable/decor_corners.xml`);主按钮 96dp 蓝渐变 `bg_btn_tech_primary.xml`、车形图标在左;次按钮 88dp 白底蓝描边齿轮图标;状态行改为三张白底小卡片(引擎/网络/今日核销);左下角版本号 + 设备编号。核销页顶栏用半透明深蓝渐变(`bg_brand_header_overlay.xml`,压住相机预览),设置页顶栏用实色渐变。 参考照片(Newland MT95 上现有 App)关键元素:顶部红色 ECTRIP 鼎标 +「鼎游信息」、深色大标题、**绿色「当前:在线检票」模式状态**、整宽蓝色大按钮(字距拉开,硬朗、高对比)、底部「版本号 / 设备编号」。在此基础上融入科技感。 **竖屏线框(360×720dp,从上到下)** ``` ┌────────────────────────────────────┐ │ ▓▓▓▓▓ 深蓝→藏青渐变标题区 ▓▓▓▓▓▓▓ │ 高 200dp │ ╭──────────────╮ │ │ │ ◇ 红鼎ECTRIP ◇ │ │ ← 浅蓝八边形徽标(矢量描边) │ ╰──────────────╯ │ 内嵌鼎标 64dp │ 鼎 游 信 息 │ ← 12sp 浅蓝白,字距加宽 │ │ │ 车 辆 核 销 系 统 │ ← 24sp 白色粗体,字距 6sp │ ┌────────────────────┐ │ │ │ ● 当前:在线识别 │ │ ← 模式胶囊,在线=荧光绿 #39FF88 │ └────────────────────┘ │ 离线=亮蓝 #4FC3F7 / 模拟=暖橙 #FFB74D ├────────────────────────────────────┤ │ (浅灰底 #F4F6FA,微弱科技网格/角标) │ │ ┌──────────────────────────────┐ │ │ │ ▰▰ 车 辆 核 销 │ │ ← 主按钮 高 96dp,蓝色渐变 │ └──────────────────────────────┘ │ #1565C0→#0D47A1,白色车形图标 │ ┌──────────────────────────────┐ │ 圆角 12dp,按压 0.97 缩放 │ │ ⚙ 系 统 设 置 │ │ ← 次按钮 高 88dp,白底蓝描边 │ └──────────────────────────────┘ │ │ │ │ 引擎:在线 网络:在线 今日核销:1 │ ← 13sp 深灰状态行 ├────────────────────────────────────┤ │ 版本号 v1.0.0 │ │ 设备编号 MT95LTFFD00631 │ ← 12sp 浅灰,左对齐(同参考App) └────────────────────────────────────┘ ``` 设计要点: 1. **品牌区**:深蓝渐变(`#1565C0→#0D2B5C`)标题区,内含浅蓝八边形(双切角矩形)描边徽标 + 红鼎 +「鼎 游 信 息」;标题文字白色、宽字距,复刻现场 App 的硬朗工业感。 2. **科技感装饰**(克制、不影响信息):标题区底部一道 2dp 荧光青 `#35C2FF` 高光线;内容区四角放 L 形青色角标线;背景 `#F4F6FA` 叠极淡网格(可选,避免视觉噪点)。 3. **模式胶囊**随设置引擎实时变色(onResume 刷新),文字「当前:在线识别 / 离线识别 / 模拟识别」——直接对应现场 App「当前:在线检票」。 4. 按钮**整宽 88%**、大字距(4 字之间留空,与参考一致)、主按钮带车形图标;保持本期已验证的点击区域(底部手掌握持区)。 5. 底部双行信息保留参考 App 习惯:版本号 + 设备编号(SN);其上保留我们自有的「引擎/网络/今日核销」状态行。 6. 核销页不推翻:顶栏改为同样的深蓝渐变 + 青色高光,车牌框四角 L 线沿用(本来就是科技风),其余 5 状态机不动。 7. 设置页顶栏同步深蓝渐变;卡片式分组不变。 ### 15.3 识别引擎改三路由:模拟 / 在线 / 离线 设置页「识别引擎」由 2 段改 **3 段**:`模拟识别 | 在线识别 | 离线识别`。业务层(VerifyActivity 状态机、结果卡片、核销接口)**零改动**,只在识别实现层增加一个云端实现: | 引擎 | 实现类 | 依赖 | 要授权/密钥吗 | 识别位置 | |---|---|---|---|---| | 模拟识别 | `MockPlateRecognizer`(已有,默认) | 无 | 否 | 本地假数据 | | **在线识别(新增)** | `OnlinePlateRecognizer`(新增) | 外网 + 百度 AI 开放平台 | **AK/SK(已提供,预置默认值)** | 百度云 REST | | 离线识别 | `BaiduPlateRecognizer`(已有) | 离线 SDK 授权 | 设备指纹授权/序列号(第 11 章) | 端侧 so | 工厂 [PlateRecognizerFactory.java](file:///Volumes/USERDATA/anroidprj/checkInCar/app/src/main/java/com/ectrip/checkincar/plate/PlateRecognizerFactory.java) `create(Context)` 按 AppPrefs 当前引擎返回实例;主界面状态卡片/胶囊文案同步改为三态。旧引擎值迁移:AppPrefs 初始化时把旧 `engine=1`(百度离线)一次性迁到 `2`,标记位 `engineV2Migrated`。 #### 15.3.1 在线识别接口(依据示例 [rest2.0ocrv1license_plate.java](file:///Volumes/USERDATA/anroidprj/checkInCar/rest2.0ocrv1license_plate.java) 与百度 AI 开放平台文档) 1. **获取 access_token**(AK/SK 换 token,有效期 30 天,需缓存): ``` POST https://aip.baidubce.com/oauth/2.0/token Content-Type: application/x-www-form-urlencoded body: grant_type=client_credentials&client_id={APIKey}&client_secret={SecretKey} → JSON 取 access_token、expires_in(秒,通常 2592000) ``` 2. **车牌识别**: ``` POST https://aip.baidubce.com/rest/2.0/ocr/v1/license_plate?access_token=... Content-Type: application/x-www-form-urlencoded body: image={JPEG 的 Base64,URLEncode}&multi_detect=false&multi_scale=false 成功:{"words_result":{"color":"blue","number":"粤BD12345","probability":[...]}, "words_result_num":1,"log_id":...} 失败:{"error_code":110,"error_msg":"Access token invalid"} ``` 3. 已提供的凭证(控制台应用「闸机检票系统」,ak_sk 鉴权): | 项 | 值 | App 内存储 | |---|---|---| | client_id(API Key / AK) | `iKKGwJRwHxIe9CAAXgafSpyt` | AppPrefs,作默认值预填,可在设置改 | | client_secret(Secret Key / SK) | `FS0byCisfASTaqoaEcRDoye4wiPFuosq` | 同上,输入框默认密文显示(小眼睛切换) | #### 15.3.2 客户端实现要点(新增 `OnlinePlateRecognizer`) - **token 管理**(新增 `BaiduCloudToken` 小类):成功获取后把 token 与到期时间戳存 AppPrefs;剩余有效期 >24h 直接复用,否则重新获取;识别返回 `error_code=110/111`(token 失效)时**自动重取一次并重试一次**,仍失败才报错。 - **图像**:沿用现有 JPEG 通路(YUV→NV21→JPEG / 拍照 JPEG),宽压到 ≤1280、质量 80,Base64 后通常 100~300KB;OkHttp `FormBody` 自动 URLEncode;连接 5s / **读取 10s**(云端比本地慢)。 - **节流**:在线请求有 QPS 与计费,自动分析帧间隔由 400ms 放宽到 **≥1000ms**,识别中丢帧;命中即停(同离线)。S2 文案显示「云端识别中…」。 - **自动帧本地预筛(省次数)**:自动分析帧在上传前先缩到 160 宽灰度,统计平均亮度与梯度能量(`OnlinePlateRecognizer.frameWorthSending`):亮度 <25 或 >245、边缘能量 <5.5 直接丢弃不上传。真机标定(MT95L):暗噪空景边缘 4.3~4.6(跳过),合成车牌入框 0.75 倍引导框 6.6、满框 7.9(上传)。**手动拍照是有意触发,不做预筛**(再暗也只产生一次调用,避免"按了没反应")。 - **空结果静默**:`error_code=282102/282103`(未检测到车牌/目标识别错误,对空画面百度每次都回这个)与无 `words_result` 一样按"本帧无车牌"静默处理,不弹 Toast;其余错误(17/18/19 额度·QPS·并发、110/111 鉴权、网络)才经 `getLastError()` 在界面节流提示(3s 一次)。 - **结果映射**:`words_result.number`→车牌;`color`(blue/yellow/green/white/black/yellowgreen)→现有颜色标签(yellowgreen→green 按新能源处理);取 `probability` 数组最小值为置信度;云返回不带 probability 时 PlateInfo.confidence=-1,结果卡片显示「云识别」。在线引擎不过本地置信阈值。 - **错误中文映射**:网络不可达 →「无法连接百度云,请检查外网/WiFi」;token 获取失败 →「鉴权失败:…」;`error_code 17/18/19` →「云端识别今日次数已超限 / QPS 超限 / 并发超限」;216200/216201/216202 图片类错误;其余显示 `error_msg(code)`。失败后**不静默降级**,由员工决定切引擎。 - 在线引擎**不校验百度离线授权、不加载 so 模型**;LicenseManager 与百度 jar 不参与该路径。 - **核销接口不变**:`VerifyApiFactory` 仍按「仅模拟引擎 → MockVerifyApi,其余 → HttpVerifyApi」工作;在线识别 + 真实后台即生产组合,无后台时确认核销走失败页(「网络异常: …」,不闪退)。 #### 15.3.3 设置页联动(在现有「识别配置」分组上改) ``` ── 识别配置 ────────────────────── 识别引擎 [模拟识别|在线识别|离线识别] ← 3 段 置信阈值 [────●────] 0.80 ← 仅「离线识别」显示 ── 在线配置(引擎=在线时展开)──────── API Key [iKKGwJ****afSpyt 👁] ← 预填默认 AK,可改 Secret Key [******************** 👁] ← 预填默认 SK,密文 鉴权状态 未获取 Token / 有效(至 MM-dd HH:mm) ← 绿色,实测 30 天 [ 测试鉴权并获取 Token ] ← 立即调 oauth 验证 AK/SK ── 模拟配置(引擎=模拟时展开,已有)── ── 离线授权(引擎=离线时展开,已有)── ``` - AppPrefs 新增键:`engine`(0模拟/1在线/2离线,复用原键改值域并迁移)、`cloudAk`、`cloudSk`、`cloudToken`、`cloudTokenExpireAt`、`engineV2Migrated`;旧两段选择迁移(旧值 1 → 2)。AK/SK 输入框 TextWatcher 即时保存,**改动即 clearCloudToken()** 强制重鉴。 - 在线引擎立即可用的前提:手持机能访问**外网**(`aip.baidubce.com`);现场若只有内网,切离线或模拟。 - 主界面「网络:在线」检测需能区分**内网通但外网不通**:在线模式启动时可选做一次轻量探测(HEAD baidu.com / DNS),不通时胶囊仍可点但进核销页提示。 #### 15.3.4 安全与计费说明(须知) 1. AK/SK 内置在 APK 中可被逆向提取,仅适用于**专用手持机、受控分发**场景;控制台请勿开启无关付费接口,必要时在百度云设置用量告警。更稳妥的后续方案:我方业务后台代发 OCR 请求(App 调自己的核销地址,由后台持密钥),列为二期可选优化。 2. 在线识别**按次计费**(车牌识别有每日免费额度,超出按次计费,以百度价目表为准),故自动帧 1s 节流 + 暗/糊帧本地预筛(15.3.2),仅在 S1 自动识别;手动拍照为操作员有意触发,不节流、不预筛,每次只产生一次调用。 3. 在线引擎不需要第 11 章的设备指纹授权,**包名变更不影响在线识别**;离线引擎仍受包名+指纹绑定约束。 ### 15.4 实施任务与验收(2026-09-25 实施) | # | 任务 | 状态 | |---|---|---| | A | ECTRIP 图标全套 | ✅ 已装机(15.1) | | B | 主界面/顶栏科技风改版:品牌区、渐变、胶囊、角标高光、版本/SN | ✅ 真机截图与参考照一致 | | C | `OnlinePlateRecognizer` + token 缓存/重试 + 颜色/错误映射 + 1s 节流 + 帧预筛 | ✅ 新增 [OnlinePlateRecognizer.java](file:///Volumes/USERDATA/anroidprj/checkInCar/app/src/main/java/com/ectrip/checkincar/plate/OnlinePlateRecognizer.java)、[BaiduCloudToken.java](file:///Volumes/USERDATA/anroidprj/checkInCar/app/src/main/java/com/ectrip/checkincar/plate/BaiduCloudToken.java)、PlateRecognizerFactory | | D | 设置页 3 段引擎、在线配置分组(AK/SK 密文预填、测试鉴权)、联动、prefs 迁移 | ✅ | | E | MT95L 真机走查 | ✅ ②③④ 已验(详见下);①待对准真实车牌实测 | 已验记录(2026-09-25,设备 MT95LTFFD00631,WiFi HUAWEI-ECTRIP-5G 可出外网): 1. **鉴权**:设置页「测试鉴权并获取 Token」成功,状态「有效(至10-25 12:57)」;AK/SK 与 oauth/REST 通路在设备上打通。 2. **OCR 接口与凭证**:用合成蓝牌「京A12345」调 license_plate 成功返回 number=京A12345、color=blue、probability 数组(各字符 0.9998+),解析契约与 App 实现一致。 3. **在线取景**:核销页提示「请将车牌放入框内(在线识别)」、S2「云端识别中…」;无牌暗画面经帧预筛**零云端调用**(实测 10s 0 次请求);282103 空识别静默不弹窗。 4. **三切换联动**:模拟→模拟配置组;在线→在线配置组(AK 预填/SK 密文+小眼睛/鉴权状态/测试按钮);离线→阈值滑块+授权激活组;主界面胶囊橙/荧光绿/亮蓝三态与引擎卡片同步。 5. **不回归**:模拟引擎自动识别(粤JS77X4 蓝牌 94%)、结果卡片/重拍/确认流程正常;在线引擎下核销无后台时落失败页,不闪退。 待办(现场补测):引擎切「在线识别」对准**真实车牌**(蓝/黄/新能源绿牌)验证 1~2 秒回号并走通核销;改错误 SK 后中文失败提示;断网进入不闪退。 验收标准:引擎切「在线识别」、设备连外网时,对准真实车牌 1~2 秒内回号并走通核销(核销仍走 Mock/后台);AK/SK 错误时有明确中文提示;断网时不闪退;模拟与离线现有功能不回归。 ## 16. 第三轮变更(2026-09-27):现场演示打磨(已装机 ✅) > 目标:客户现场真机演示前的界面与流程微调。全部改动已构建装机(MT95LTFFD00631)并走查,当前引擎停留在「在线识别」(prefs engine=1)。 ### 16.1 变更清单 | # | 需求 | 实施 | |---|---|---| | 1 | 核销失败弹窗也显示车牌 | [VerifyActivity.java](file:///Volumes/USERDATA/anroidprj/checkInCar/app/src/main/java/com/ectrip/checkincar/ui/verify/VerifyActivity.java) 失败分支 `addInfoRow("车牌", currentPlate.number)`,位于失败原因下方 | | 2 | 识别结果卡增加「返回」按钮 | 底部由「重拍 / 确认核销」改为「重拍 / 确认核销 / 返回」三按钮;返回 = `finish()` 回主页([activity_verify.xml](file:///Volumes/USERDATA/anroidprj/checkInCar/app/src/main/res/layout/activity_verify.xml) `btnBackHome`) | | 3 | 车牌取景框加大 | 引导框 300×96dp → 340×116dp → **356×120dp**(屏宽 360dp,横向近满屏,竖向按车牌 440:140 比例)。纯视觉,识别仍送整帧 | | 4 | 首页引擎状态醒目 | 引擎卡片值 15sp → **17sp 加粗**并按引擎着色:在线=#00A854 绿、离线=#0288D1 蓝、模拟=#EF6C00 橙(与顶部胶囊同色);onResume 刷新 | | 5 | 核销后台固定走模拟、固定成功 | [VerifyApiFactory.java](file:///Volumes/USERDATA/anroidprj/checkInCar/app/src/main/java/com/ectrip/checkincar/net/VerifyApiFactory.java) 不再按识别引擎分流,恒返回 `MockVerifyApi`;Mock 去掉随机失败/失败原因分支,固定 600ms 返回成功。留 TODO:正式后台就绪后恢复 HttpVerifyApi 分流 | | 6 | 成功页预约信息补全 | 展示行:预约号 / **预约时间** / **预约人** / **证件号(脱敏 3502**********1234)** / **预约类型(老人/正常)** / **进入日期** / **核销流水(HX+时间戳)**;模型新增 `visitorIdCard / visitorType / verifySerialNo`,`fromJson` 同步解析同名字段 | | 7 | 「车位」改为「进入日期」 | 字段契约全局收口:`lotName` → **`enterDate`**(VerifyResult、VerifyRecord 本地历史键、MockVerifyApi、VerifyActivity)。取值规则:`data.enterDate` 优先,空则取 `data.enterTime` 兼容;Mock 演示值=当天 `yyyy-MM-dd`(实测显示 2026-09-27) | ### 16.2 真机走查记录(2026-09-27) - 模拟引擎全流程:自动识别 → 三按钮结果卡 → 确认 → 绿色成功页,7 行信息完整(含进入日期当日),2 秒自动连刷正常。 - 失败弹窗车牌行:Mock 失败路径造数验证过(本轮后失败路径在现场不再触发,因核销恒成功)。 - 在线引擎取景页文案、引导框尺寸、引擎卡片绿色加粗,截图确认。 - 百度云 access_token 性质澄清(已向客户说明):**应用级凭证,不绑设备**,同一 AK/SK 可多台 MT95L 共用,额度/QPS 按 AK 累计;与离线 License 的「指纹+包名一机一授权」不同。Token 约 30 天有效(2026-10-25 前后到期,App 自动刷新)。 ### 16.3 明天(及后续)待办 1. **现场真实车牌实测**:在线识别对准蓝/黄/新能源绿牌,确认 1~2s 回号(合成牌通路已验,真牌尚未现场拍)。 2. **正式核销接口对接**:后台 JSON 就绪后,① `VerifyApiFactory` 恢复/新增 HttpVerifyApi 分流;② 确认 `data.enterDate` 真实字段名(不一致则在 VerifyResult.fromJson 的兼容处加映射,**只改数据入口,不动界面**);③ 预约时间/预约人/证件号/预约类型/核销流水字段名一并对齐。 3. 非阻塞遗留:离线 License 激活(控制台 SN 流程见第 11 章)、MT95L 扫描键映射、核销记录查询页、断网/错误 SK 的现场演示话术。 ### 16.4 现场操作备忘 - 设备当前:引擎=在线识别,核销=模拟恒成功(任何引擎下点确认核销都成功),今日核销计数每天 0 点重置。 - 常用命令:构建 `./gradlew assembleDebug --no-daemon`;装机 `adb -s MT95LTFFD00631 install -r app/build/outputs/apk/debug/app-debug.apk`;启动 `adb shell am start -n com.ectrip.checkincar/.ui.main.MainActivity`(VerifyActivity/SettingsActivity 未导出,不能直接 am start,需从主页点入)。 - 坐标(720×1440):主页「车辆核销」约 (360,705)、「系统设置」约 (360,976);设置页引擎三段 y≈1237:模拟 x≈157 / 在线 x≈360 / 离线 x≈563。 ## 17. 第四轮:对接真实核销后台 mqttScan(2026-09-28) > 依据:`车牌订单核销模拟接口说明.md` + 后端源码(只读参考,**严禁修改**):`/Volumes/USERDATA/ideaProjects/wts_vehicle_reservation`(分支 tdos-dev)。 ### 17.1 后台事实(源码核实) - 入口 `ParkingController.mockMqttScan`(open-api/.../parking/controller/ParkingController.java)→ `CallbackService.processRecognizedPlate` → `ParkingService.queryVehicleInfo`;通道业务模式=3 时调 `ParkingOrderHandler.parkingVerify` 执行真实订单消费(消费畅游通订单 + 写 OrderCheckRecord 核销流水 + MQTT 开闸)。 - 请求:`GET {baseUrl}/parking/mock/mqttScan?deviceCode=&licensePlate=&colorValue=`;colorValue 0蓝/1绿/2黄(`LicensePlateColorEnum`);车牌明文需 URL 编码。 - 响应外壳 `AjaxData{success:boolean, info:string, data:VehicleInfoVO}`(**不是** code/msg)。 - VehicleInfoVO:orderId、orderCode、licencePlate(**Base64 明文车牌**)、createTime(首次入场时间)、seq、vehicleType(1本地/2工作/3预约/4非预约/5非本地/10免票)、licenseColor(蓝/绿/黄)、carType、pass(isPass→JSON 键 pass)、secondEntry、entryTimes。 - 判定规则(ParkingService 427-462 行):模式 1 只查询;模式 2 查询并放行;**模式 3 + vehicleType=3 + pass=true 才真实核销**。 - 已核销预约车再扫:status=1 → pass=false、secondEntry=true、entryTimes=N;免票车 vehicleType=10 且 pass=false;非预约 4/5 直接 return(pass=false,不开闸不核销,但仍写一条 VehicleChannelRecord 通道流水)。 - 3 秒同设备同车牌 Redis 去重:`success=false, info="3秒内重复车牌识别消息已忽略"`。 - 坑:`parkingVerify` 抛异常被 processBusinessMode catch 仅打印日志,HTTP 仍 success=true —— App 必须按 data 的 pass/vehicleType/secondEntry 组合判定,不能只看 success。 - 副作用警告:接口名为 mock 实为正式链路,**真实预约车牌测一次即消费一单**。 ### 17.2 实施方案(用户已确认三项决策) 1. **核销方式开关**:设置页「核销配置」组新增分段「模拟核销|后台核销」(AppPrefs `verifyMode` 0/1,默认 0);`VerifyApiFactory.create()` 按此分流,默认模拟防误核销。识别引擎三路由不受影响。 2. **设备编号**:AppPrefs 新增 `deviceCode`,默认 **DEVICE001**,设置页可改(即时保存);与手持机 SN 无关,须为后台已配置(device.status=1)且绑定模式 3 通道的编号。 3. **HttpVerifyApi 重写**: - GET,query 参数 deviceCode / licensePlate(URLEncoder UTF-8) / colorValue(blue→0、green→1、yellow→2,未知→0);连 5s 读 8s。 - 解析 AjaxData:success=false → `VerifyResult.fail(info)`(原样展示后台中文原因)。 - 解析 data:orderCode→预约号、orderId、Base64 解码 licencePlate、vehicleType→中文类型、licenseColor、createTime(兼容字符串/毫秒)、seq、pass、secondEntry、entryTimes。 - 成功/失败判定**只看 AjaxData.success**:false → 红色失败页,原样显示 info(设备未配置/已禁用/3秒重复等);true → 成功页,data 九项字段**全部显示**(null 显示“-”):订单号 orderCode、车牌号(licencePlate Base64 解码)、首次入场时间 createTime、入场序号 seq、车辆类型(编码+中文)、车牌颜色、是否允许通行 pass(是/否)、是否重复入场 secondEntry(是/否)、当前入场次数 entryTimes。标题:预约车(type=3)=核销成功,其余=处理成功,副标题显示 info 原文。 4. **成功页分行展示**:真实模式固定九行(见上,全部显示);模拟模式保持现有 7 行演示信息(Mock 继续填模拟值)。 5. 红色失败页保留后台 info 原文 + 车牌行。 ### 17.3 联调与测试 - 安全通路测试(不消费订单):用无预约的假车牌调后台 → 应得 success=true、vehicleType=4、pass=false,验证网络/解析/判定(副作用仅一条通道流水日志)。 - 真实核销测试前提:后台提供绑定模式 3 通道的有效 deviceCode + 当天未核销预约车牌(一次性);核销后第二次扫同牌应拦截为重复入场。 - 回归:核销方式切回模拟时,全流程与第 16 章一致。 **2026-09-28 现场网络勘察结果**:192.168.50.1:8080 当前返回 `404 / Server: Embedded Server/1.0.0`(网关或道闸设备自带服务,非本系统 Spring 后台);192.168.50.1-254 全段 8080 扫描无其他服务在线,手持机(.70)正常。**结论:后台服务今日未部署/未启动在本网段,真实链路待后台上线后联调**;App 侧代码已就绪,后台不通时红色失败页显示「后台返回 HTTP 404 / 网络异常」,不闪退。另注:open-api 模块 resources 无 application.yml(配置走外部配置中心),端口与 context-path 以部署环境为准,当前按文档直连 8080 根路径。 ### 17.4 待后台确认/遗留 - DEVICE001 是否真实存在且绑定模式 3 通道;不行则由后台管理员提供 deviceCode(设置页可直接改,无需发版)。 - 后台不返回预约人/证件/预约类型/核销流水,成功页暂不展示;后续后台加字段后仅在数据入口加映射。 - parkingVerify 失败对调用方不可见(HTTP 仍成功)——如需精确核销结果,需后台后续在 VehicleInfoVO 增加核销结果字段或提供核销流水查询接口。 ## 18. 第六轮:核销确认机制 + 预约查询接口(2026-10-27) > 背景:前几轮识别车牌后直接提交核销,无信息确认环节。用户要求增加确认弹窗,显示车牌号、驾驶员、预约日期、订单号等信息,确认后才执行核销。 > 核心问题:原核销接口 `/tdos/consumeOrder_/appedit.do` 只返回 `{success, info}` 两字段,不含任何预约详情,**无法在核销前展示**。 > 解决方案:后台新增免登录预约查询接口,App 侧改为两步流程(先查后核销)。 ### 18.1 后台新增查询接口(已上线) **请求地址**: ``` GET /tdos/consumeOrder_/appquery.do POST /tdos/consumeOrder_/appquery.do ``` **请求参数**: | 参数 | 必填 | 类型 | 说明 | |------|------|------|------| | licensePlate | 是 | String | Base64 编码后的车牌号(UTF-8),GET 时需再 URL 编码 | | callback | 否 | String | JSONP 回调函数名 | **Base64 编码规则**:与核销接口 `appedit.do` 完全一致——`Base64.encodeToString(plate.getBytes("UTF-8"), Base64.NO_WRAP)`,再用 `URLEncoder.encode(encoded, "UTF-8")` 处理 `+` `/` `=` 特殊字符。 **成功响应(查到有效预约)**: ```json { "success": true, "info": "查询成功", "data": { "licensePlate": "粤A12345", "orderId": 10001, "orderCode": "202610270001", "driverName": "张三", "driverPhone": "13800138000", "useStartDate": "2026-10-27", "parkingAddress": "停车场A区", "status": 0, "enterTime": null } } ``` **成功响应(无有效预约)**: ```json { "success": false, "info": "未查询到当天有效车牌预约信息", "data": { "licensePlate": "粤A12345" } } ``` **错误响应**: ```json {"success": false, "info": "车牌号Base64编码格式错误"} ``` **返回字段说明**: | 字段 | 类型 | 说明 | |------|------|------| | licensePlate | String | 解码后的车牌号 | | orderId | Integer | 订单 ID | | orderCode | String | 订单编号 | | driverName | String | 驾驶员姓名 | | driverPhone | String | 驾驶员手机号 | | useStartDate | String | 预约使用日期 | | parkingAddress | String | 停车地点 | | status | Integer | 0=待入场,1=已入场核销 | | enterTime | String/null | 入场核销时间,未核销时为 null | **接口特点**: - 免登录(与 `appedit.do` 同属 `/consumeOrder_/` 路径,Shiro 白名单已配置) - 只查不消费订单,不会核销 - Spring MVC `.do` 后缀拦截规则生效(路径必须带 `.do`) - 生产地址前缀:`https://wtspw.wtsykfwzx.com:8145/tdos` - 开发测试地址前缀:`https://wtstest.jingqu.cn:9143/tdos` ### 18.2 App 侧新增文件 | 文件 | 说明 | |------|------| | `model/QueryResult.java` | 查询结果模型:`ok`(网络层) + `success`(业务层) + `info` + `data` 各字段 | | `net/QueryApi.java` | 查询接口定义:`query(String plateNo)` 返回 `QueryResult` | | `net/HttpQueryApi.java` | 真实后台实现:GET `{baseUrl}/tdos/consumeOrder_/appquery.do`,5s 超时 | | `net/MockQueryApi.java` | 模拟实现:固定 300ms 耗时,80% 概率返回模拟预约详情 | | `res/layout/dialog_verify_confirm.xml` | 确认弹窗自定义布局(车牌号/状态/驾驶员/手机号/订单号/预约日期/停车地点 + 取消/确认核销按钮) | | `res/drawable/bg_btn_outline.xml` | 取消按钮背景(描边样式) | ### 18.3 已有文件改动 | 文件 | 改动 | |------|------| | `net/VerifyApiFactory.java` | 新增 `createQuery()` 方法,按 verifyMode 分流真实/模拟查询 | | `ui/verify/VerifyActivity.java` | ① `onRecognized()` 改为先调 `queryAndShowConfirm()` 再弹确认窗;② 取消自动核销(AUTO_VERIFY_ON 选项保留但不再自动提交);③ 新增 `showConfirmDialog()`(有预约)和 `showNoReservationDialog()`(无预约降级);④ 新增 `confirmDialog` 和 `queryPending` 状态变量;⑤ `backToScanning()` 和 `onDestroy()` 中清理弹窗状态 | | `core/prefs/AppPrefs.java` | ① 核销方式缺省值 `VERIFY_MOCK` → `VERIFY_SERVER`(新安装默认后台核销);② 识别引擎缺省值 `ENGINE_MOCK` → `ENGINE_ONLINE`(新安装默认在线识别);③ `migrateEngineV2()` 中旧默认值同步更新 | ### 18.4 核销流程(改造后) ``` 拍照/识别车牌 │ ▼ onRecognized(PlateInfo) │ ├─ 取消原 AUTO_VERIFY_ON 自动提交逻辑 │ ▼ workExecutor.execute: QueryApi.query(车牌) │ ┌──────────────────────────────────┐ │ │ 查询成功 + success=true 查询失败 或 success=false (有有效预约) (无预约 / 网络异常) │ │ ▼ ▼ 弹确认弹窗 Toast + 双按钮对话框 ┌─────────────────────┐ ┌─────────────────┐ │ 🚗 粤A12345 │ │ 未查到预约信息 │ │ 📌 待入场/已入场 │ │ 是否继续核销? │ │ 👤 张三 │ │ [继续核销][取消] │ │ 📱 138****8000 │ └─────────────────┘ │ 📋 202610270001 │ │ │ 📅 2026-10-27 │ ┌──────┴──────┐ │ 🅿️ A区停车场 │ │ │ ├─────────────────────┤ submitVerify backToScanning │ [取消] [确认核销] │ └─────────────────────┘ │ │ backToScanning submitVerify(currentPlate) │ ▼ 调 HttpVerifyApi.submit() POST /tdos/consumeOrder_/appedit.do │ ▼ 展示核销结果页 ``` ### 18.5 边界处理 | 场景 | 处理 | |------|------| | 查询成功 + status=0(待入场) | 正常弹窗,确认核销走原流程 | | 查询成功 + status=1(已入场) | 弹窗状态标签红色「已入场」,点「确认核销」后二次确认 | | 查询成功 + 无预约(success=false) | 提示"未查到预约信息",双按钮:继续核销 / 取消 | | 查询超时 / 网络异常 | 降级为"未查到预约信息"处理,不阻塞核销 | | 模拟核销模式 | MockQueryApi 返回模拟预约详情,流程与真实模式完全相同 | | 手动输入车牌 | 同样走查询+确认弹窗流程,与识别的车牌无区别 | | 查询中返回扫描态 | `queryPending` 标志位防止重复提交,用户返回后忽略查询结果 | | 确认弹窗中按返回键 | `setCancelable(true)`,取消弹窗回到扫描态 | ### 18.6 接口路径汇总 | 功能 | 方法 | 路径 | 说明 | |------|------|------|------| | 预约查询 | GET/POST | `/tdos/consumeOrder_/appquery.do` | 免登录,只查不核销,Base64+URL 编码车牌 | | 核销提交 | POST | `/tdos/consumeOrder_/appedit.do` | 免登录,真实消费订单,Base64 编码车牌 | | 生产地址前缀 | — | `https://wtspw.wtsykfwzx.com:8145/tdos` | .do 后缀由路径自行拼接 | | 开发测试地址前缀 | — | `https://wtstest.jingqu.cn:9143/tdos` | 同上 | ### 18.7 关键代码参考 ```java // HttpQueryApi.java — 查询接口实现 private static final String PATH = "/tdos/consumeOrder_/appquery.do"; public QueryResult query(String plateNo) throws Exception { String base = AppPrefs.getBaseUrl(); // 自动去除末尾 / String encodedPlate = Base64.encodeToString( plateNo.getBytes("UTF-8"), Base64.NO_WRAP); String url = base + PATH + "?licensePlate=" + URLEncoder.encode(encodedPlate, "UTF-8"); // GET 请求,5s 超时 // 解析 JSON → QueryResult.fromServerJson(body) } ``` ```java // VerifyActivity.java — 识别成功后先查询再弹窗 private void onRecognized(PlateInfo info) { currentPlate = info; queryAndShowConfirm(info); // 替代原 autoVerify 逻辑 } private void queryAndShowConfirm(PlateInfo plate) { workExecutor.execute(() -> { QueryResult result = VerifyApiFactory.createQuery().query(plate.number); runOnUiThread(() -> { if (result.ok && result.success) { showConfirmDialog(result); // 有预约 → 弹确认窗 } else { showNoReservationDialog(result.info); // 无预约 → 降级提示 } }); }); } ``` ### 18.8 2026-10-27 现场测试结果 - 缺省值调整:新安装 App 默认为「在线识别 + 后台核销 + 生产正式地址」 - 查询接口 `.do` 后缀修正(初版漏写导致 404) - Base64 编码 `NO_WRAP` + URL 编码处理 `+` `/` `=` 特殊字符 - 完整流程:拍照识别 → 查询 → 弹确认窗 → 确认核销 → 展示结果,测试通过