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

扫码识别与业务准入封面

系统扫码负责识别,应用负责解释

本文用 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 加换行拒绝总长度
普通网址文本拒绝协议前缀与字符集合

有效编码、取消和格式拒绝的操作分流

相册入口也要单独验证。它使用用户选择的图片,不能因此推断应用已经获得整个相册的持续访问权限。相册中的多码图片与实时多码选择也不一定具有相同交互,参数说明和实际设备行为应分别检查,不能把一次成功识别推广成全部图片场景都支持。

不建议记录完整扫码原文。二维码可能包含登录信息、联系人或其他敏感文本,即使当前业务最后拒绝它,也不应先把全部结果写进日志。对定位问题真正有用的是码制、长度、解析阶段和经过筛选的错误信息,而不是一份可以重新利用的完整内容。

如果业务未来接收网址,应另行建立协议、主机和路径允许范围。不要因为扫描器返回一个看似合法的链接就自动打开,也不要把任意查询参数直接转成应用内动作。当前固定编码格式较窄,正好能够展示扫码接入最核心的职责划分:识别器还原信息,业务判断信息的含义。

无论界面多简单,都应给返回结果明确命名。状态叫“格式通过”时,测试人员会去检查编码;状态叫“提交成功”时,测试人员会寻找业务记录。名称与真实操作一致,可以避免开发、产品和测试对同一个绿色提示各自作出不同理解,也为之后增加确认步骤留出空间。

业务编码中的字符集也需要与生成端共同确认。有些系统为了避免误读,会排除字母与数字中外形相似的字符;本例没有作这种排除,所以这些字符仍然合法。客户端不能凭阅读习惯自行缩小范围,否则生成端发出的有效码会被拒绝。规则变更应该以协议版本和对应样本一起发布。

重复扫描同一编号是否应该提示重复,需要由后续场景决定。查询详情可以多次使用相同编号,执行一次性签收则需要业务幂等约束。当前页面只是显示解析结果,重复扫描可以得到相同文本,不人为添加“已处理”的标记;否则读者可能把本地展示历史误认为服务器业务记录。

测试码最好分成清晰打印、屏幕展示、轻微旋转和低照度等样本组,分别观察系统识别表现。业务解析的合法与非法样本则单独维护,不将图像质量因素混入字符串规则测试。这样识别失败时能判断是图案输入问题,还是系统已经识别了一个确实不符合业务要求的内容。

系统返回的码制信息可以用于诊断和产品限制,但不能单独证明内容来自可信发行方。一个攻击者同样可以制作符合码制的图案。需要真实性保证时,应由编码协议引入可验证凭证,并由业务服务处理有效期、发行者和撤销状态;仅在客户端增加固定前缀并不具备这样的保证。

业务页面的可访问性同样要保留。扫码不是用户唯一能够理解状态的方式,结果文本和按钮名称应能被系统正常读取,失败原因也不要只通过颜色传递。当前样本按钮用于开发验证,正式产品如果提供手工录入,应复用相同解析器,并清楚区分录入结果与扫码结果的来源。

这个页面最终只有两种输入来源,却有几种明确结果:尚未操作、等待扫码、用户取消、识别调用失败、业务格式拒绝,以及格式通过。让这些结果各有位置,接入就不会停留在“调用一次接口”的层面。后续无论接查询、核验还是录入,都应从已经通过解析的编号开始,继续完成自己的验证。

参考资料

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐