0fd093a2
李进
初始提交:手持车牌核销应用
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
|
# 手持核销车辆系统 — 开发方案
> 状态:**方案已按评审意见细化界面与流程,待最终确认后开发(暂不写代码)**
> 日期: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<OcrgvesdkResponsePlate> 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 编码处理 `+` `/` `=` 特殊字符
- 完整流程:拍照识别 → 查询 → 弹确认窗 → 确认核销 → 展示结果,测试通过
|