HarmonyOS ScanKit:扫码结果进入业务前,还要经过哪道门【共创稿事节】
“扫描器已经返回成功,为什么页面还提示编码不符合规则?”这是接入扫码功能时很容易混淆的两个结果。扫描器回答图案里写了什么,业务代码回答这段内容能否用于当前操作。一个清晰可识别的二维码,完全可能装着网址、联系人或另一家系统的编号。把这两道判断分开,才不会把每次识别成功都误当成业务提交成功。

系统扫码负责识别,应用负责解释
本文用 ParcelScan 工程建立一个“包裹识别”页面,目标是接收固定格式的业务码。编码以约定前缀开始,后面是八位大写英文字母或数字。界面只展示通过规则的编号,不执行开门、付款、签收或联网查询,因此识别结果不会直接产生外部业务动作。
工程使用 API 12、Stage 模型和 ScanKit 的统一扫码界面。这里采用从 API 11 提供的带上下文结果接口,不继续使用已经废弃的旧启动方法。官方接入说明说明统一扫码已经处理相机预授权,调用这一路径不需要应用再次申请相机权限。自行开发相机预览属于另一条接入路线,不能混用配置结论。
统一界面能省去相机预览和识别交互的开发,但它不是可以任意替换布局的自定义组件。因此本文应用图展示的是调用前的业务页面,系统扫码页由设备提供;不要把自己画的取景框当成已经覆盖系统界面的证据,也不要按概念稿去寻找不存在的换肤属性。
设备和系统能力也是前提。使用提供相应扫码能力的手机或平板进行接入验证,项目编译版本与所用接口保持一致。若目标设备不满足能力条件,应给出不可用路径,而不是因为导入模块成功就推断摄像头、图库识别和所有码制都能工作。
有读者会问,限制扫描类型是否就等于限制业务格式。答案藏在两层规则里:码制描述二维码或条码怎样编码图案,业务格式描述图案承载的字符应长什么样。同一个业务编号可以被装进不同码制,反过来,同一种二维码也可以存放各种文本,因此不能用其中一层替代另一层。
本例显式允许系统支持的全部码制,关闭多码模式并保留相册入口。这样一次返回仍然表示用户选择的一个识别结果,不构建自动批量采集行为。若后续启用多码模式,也应重新理解系统的选择流程,不能直接把单个返回对象强制当成数组使用。
先写独立解析器,才能准确说明拒绝原因
新建 common/ParcelCode.ets,让它只处理字符串。解析器不接收页面对象,不访问扫描器,也不弹提示。这样本地字符串样本可以先验证业务规则;实际扫描只需要把结果中的原始文本交给同一个入口,而不再复制一份稍有差别的正则。
// entry/src/main/ets/common/ParcelCode.ets
export function parseParcel(value: string): string {
if (value.length !== 12) {
throw new Error('编码长度不符合本应用规则');
}
if (!/^PKG-[A-Z0-9]{8}$/.test(value)) {
throw new Error('需要 PKG- 开头的八位大写字母或数字');
}
return value.substring(4);
}
export function fixtureResult(): string {
const sample = 'PKG-A1B2C3D4';
return parseParcel(sample);
}
长度先判断,再检查字符集合,这两步有意保留。前缀四个字符加八位内容,总长应为十二。这个长度约束也能拒绝末尾换行,避免仅凭正则结束锚点的语言行为误收带额外字符的输入。返回值去掉协议前缀,留下页面真正需要展示的业务编号。
解析前不自动去空格、不转换大小写,也不把全角字符替换成半角。对于人工输入表单,友好的规范化可能合理;对于机器编码,悄悄修正会改变原始协议含义。本例选择严格接收,若未来业务允许规范化,应把规则写进编码规范,并在测试中分别保留原始值与规范值。
输入已经是字符串,仍然需要限制长度。识别结果可能远长于正常编号,业务不应把它直接塞入状态文本、日志或网络地址。当前解析器在任何复杂操作前拒绝不符长度的数据,避免页面为了显示“识别到了什么”而接收一大段完全无关的内容。
解析成功只表示结构符合约定,不证明包裹存在,也不证明当前账号有权操作它。真实签收系统还需要到业务服务验证记录、操作权限和当前状态,重复提交也应由服务端约束。本文把这些职责留在后续业务层,页面使用“识别结果”而不是“签收成功”。
一次点击只对应一次扫码会话
入口页保留模板的窗口加载配置,替换 pages/Index 即可。点击时获取当前界面的宿主上下文,并在调用之前检查它是否存在。上下文必须对应仍然有效的页面,不能在模块加载时缓存一个未经生命周期约束的全局对象,等任意后台任务需要时再发起扫码。
// entry/src/main/ets/pages/Index.ets
import { scanBarcode, scanCore } from '@kit.ScanKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { parseParcel, fixtureResult } from '../common/ParcelCode';
@Entry
@Component
struct Index {
private alive: boolean = true;
@State busy: boolean = false;
@State parcel: string = '尚无结果';
@State status: string = '点击按钮打开系统扫码';
aboutToDisappear(): void { this.alive = false; }
private async scan(): Promise<void> {
if (this.busy) { return; }
const context = this.getUIContext().getHostContext();
if (!context) { this.status = '当前页面上下文不可用'; return; }
this.busy = true;
this.status = '等待扫码返回';
const options: scanBarcode.ScanOptions = {
scanTypes: [scanCore.ScanType.ALL],
enableMultiMode: false,
enableAlbum: true
};
try {
const data = await scanBarcode.startScanForResult(context, options);
const accepted = parseParcel(data.originalValue);
if (this.alive) {
this.parcel = accepted;
this.status = '格式通过,尚未提交业务操作';
}
} catch (error) {
if (this.alive) {
const e = error as BusinessError;
this.status = e.code === 1000500002 ? '已取消,保留原结果'
: (e.code ? `扫码未完成:${e.code}` : e.message);
}
} finally {
if (this.alive) { this.busy = false; }
}
}
private testParser(): void {
this.parcel = fixtureResult();
this.status = '本地样本通过,仅验证解析规则';
}
build() {
Column({ space: 18 }) {
Text('包裹识别').fontSize(28).fontWeight(FontWeight.Bold)
Text('识别图案,再核对业务格式').fontSize(14).fontColor('#64748B')
Column({ space: 12 }) {
Text('当前业务编号').fontSize(14).fontColor('#64748B')
Text(this.parcel).fontSize(26).fontWeight(FontWeight.Bold)
}.width('100%').padding(24).backgroundColor('#FFFFFF').borderRadius(20)
Text('编码格式:PKG- + 八位大写字母或数字').fontSize(14)
Button('打开系统扫码').width('100%').height(44).enabled(!this.busy)
.backgroundColor('#0369A1').onClick(() => { this.scan(); })
Button('验证本地样本').width('100%').height(44).enabled(!this.busy)
.backgroundColor('#64748B').onClick(() => { this.testParser(); })
Text(this.status).fontSize(16).fontColor('#0369A1')
}.width('100%').height('100%').padding(24).backgroundColor('#F2F9FF')
}
}

忙碌期间两个按钮都禁用。否则系统扫码尚未返回时,用户可能点击本地样本,界面先出现测试编号,随后又被真实结果覆盖。防止多个输入来源同时写同一个结果,比仅防止重复扫描更完整;状态文字也因此能准确解释当前编号来自哪条路径。
外部扫码界面出现时,当前页面可能暂时不在前台,这不等价于当前组件已经销毁。代码不在页面隐藏时把这次结果作废;真正销毁后才关闭回写。若把所有隐藏都视为取消,会导致系统正常返回时结果永远被丢弃,形成“能扫到,但页面没有变化”的假故障。
取消是一条用户主动退出路径,API 12 中按官方错误码单独处理。取消之后保留原结果,提示也明确说明这一点。格式错误同样不覆盖上一份有效编号;如果产品希望每次启动就清空,需要同时修改界面语义和验证样本,不能只在异常分支里随意清空。
分开验证识别能力与业务解析
先点击本地样本按钮,应该得到固定测试编号和明确的样本提示。这个操作证明解析函数能处理给定字符串,不证明摄像头识别成功。随后用符合约定的二维码或条码验证系统路径,观察原始值通过解析后是否得到相同编号,两条验证证据分别记录。

故障排查时,先看是否打开系统扫码页,再看是否返回结果,最后看是否通过解析。打不开页面时查上下文、设备能力和接口调用;有结果却拒绝时查编码规则;已经展示编号却没有完成业务时,应进入业务层诊断,而不是继续修改扫描选项。
| 样本内容 | 解析结果 | 关注点 |
|---|---|---|
| PKG-A1B2C3D4 | 接收为 A1B2C3D4 | 合法边界 |
| PKG-a1b2c3d4 | 拒绝 | 大小写约定 |
| PKG-A1B2C3D4 加换行 | 拒绝 | 总长度 |
| 普通网址文本 | 拒绝 | 协议前缀与字符集合 |

相册入口也要单独验证。它使用用户选择的图片,不能因此推断应用已经获得整个相册的持续访问权限。相册中的多码图片与实时多码选择也不一定具有相同交互,参数说明和实际设备行为应分别检查,不能把一次成功识别推广成全部图片场景都支持。
不建议记录完整扫码原文。二维码可能包含登录信息、联系人或其他敏感文本,即使当前业务最后拒绝它,也不应先把全部结果写进日志。对定位问题真正有用的是码制、长度、解析阶段和经过筛选的错误信息,而不是一份可以重新利用的完整内容。
如果业务未来接收网址,应另行建立协议、主机和路径允许范围。不要因为扫描器返回一个看似合法的链接就自动打开,也不要把任意查询参数直接转成应用内动作。当前固定编码格式较窄,正好能够展示扫码接入最核心的职责划分:识别器还原信息,业务判断信息的含义。
无论界面多简单,都应给返回结果明确命名。状态叫“格式通过”时,测试人员会去检查编码;状态叫“提交成功”时,测试人员会寻找业务记录。名称与真实操作一致,可以避免开发、产品和测试对同一个绿色提示各自作出不同理解,也为之后增加确认步骤留出空间。
业务编码中的字符集也需要与生成端共同确认。有些系统为了避免误读,会排除字母与数字中外形相似的字符;本例没有作这种排除,所以这些字符仍然合法。客户端不能凭阅读习惯自行缩小范围,否则生成端发出的有效码会被拒绝。规则变更应该以协议版本和对应样本一起发布。
重复扫描同一编号是否应该提示重复,需要由后续场景决定。查询详情可以多次使用相同编号,执行一次性签收则需要业务幂等约束。当前页面只是显示解析结果,重复扫描可以得到相同文本,不人为添加“已处理”的标记;否则读者可能把本地展示历史误认为服务器业务记录。
测试码最好分成清晰打印、屏幕展示、轻微旋转和低照度等样本组,分别观察系统识别表现。业务解析的合法与非法样本则单独维护,不将图像质量因素混入字符串规则测试。这样识别失败时能判断是图案输入问题,还是系统已经识别了一个确实不符合业务要求的内容。
系统返回的码制信息可以用于诊断和产品限制,但不能单独证明内容来自可信发行方。一个攻击者同样可以制作符合码制的图案。需要真实性保证时,应由编码协议引入可验证凭证,并由业务服务处理有效期、发行者和撤销状态;仅在客户端增加固定前缀并不具备这样的保证。
业务页面的可访问性同样要保留。扫码不是用户唯一能够理解状态的方式,结果文本和按钮名称应能被系统正常读取,失败原因也不要只通过颜色传递。当前样本按钮用于开发验证,正式产品如果提供手工录入,应复用相同解析器,并清楚区分录入结果与扫码结果的来源。
这个页面最终只有两种输入来源,却有几种明确结果:尚未操作、等待扫码、用户取消、识别调用失败、业务格式拒绝,以及格式通过。让这些结果各有位置,接入就不会停留在“调用一次接口”的层面。后续无论接查询、核验还是录入,都应从已经通过解析的编号开始,继续完成自己的验证。
参考资料
更多推荐

所有评论(0)