项目:SchemaGate|页面:ImportReviewPage|任务:SG-2908
说明:以下导入内容、状态及 HiLog 均为离线人工演示数据,不代表真实企业数据、DevEco 编译结果或上架审核记录。
一、一个版本号,就能把“成功导入”变成错账
同事给我四条离线维保单,说只要能把 JSON 解析成功,就算导入完成。我没有立即做列表展示,而是先检查了四行数据里到底有哪些含义。第一条是旧版本,没有紧急程度字段;第二条使用当前版本;第三条却写着尚未支持的版本3;第四条版本正确,但等级写了根本不存在的extreme。这四行都能被JSON.parse读成对象,但只有两行符合业务约定。
因此这次把 Demo 命名为SchemaGate,入口页为ImportReviewPage,任务号SG-2908。演示输入固定为四条:R101的 v1“滤芯更换”,R102的 v2“阀门复检”,R103的 v3“密封检查”,R104的 v2“压力巡检”。目标并不是让解析器尽量宽容,而是明确允许哪些历史版本被降级处理,拒绝哪些未来结构和非法枚举值。
本例最后的结果应该非常具体:通过R101、R102两条,拒绝R103、R104两条,接受率为50%,状态为REVIEW_READY。拒绝原因分别为UNSUPPORTED_VERSION与INVALID_LEVEL。请注意,这些英文大写状态属于本文自定义的业务标识,不是 HarmonyOS SDK 的错误码,也没有向后台服务器上报任何内容。
二、先把导入契约写得比 UI 早一步
如果导入入口直接声明JSON.parse(text) as Ticket,后续组件很容易误以为拿到的已经是业务对象。实际上类型断言只影响开发期的类型认知,不会替不可信输入做运行时检查。我更愿意把它看成一道分界线:左侧是外来字符串,右侧才是经过验证和重建的规范化记录,中间不能省略字段判定。
我先定义输出对象和样例输入。第一条 v1 没有level字段,约定在接受时补成normal;第二条 v2 明确为high。第三条版本号3不做猜测性兼容,第四条即使写着v2也不能默认将extreme映射为high。展示字段只允许id、title、level,其它来源字段不能随手展平进页面状态。
type TicketLevel = 'normal' | 'high'; interface Ticket { id: string; title: string; level: TicketLevel; } interface RawTicket { version?: number; id?: string; title?: string; level?: string; } const IMPORT_LINES: string[] = [ '{"version":1,"id":"R101","title":"滤芯更换"}', '{"version":2,"id":"R102","title":"阀门复检","level":"high"}', '{"version":3,"id":"R103","title":"密封检查","level":"normal"}', '{"version":2,"id":"R104","title":"压力巡检","level":"extreme"}' ];这四个字符串是人工构造的最小复现实例,没有文件权限、网络传输或系统账号依赖。若换成真实导入文件,读取权限、输入大小和编码应由外层适配器处理,不能让单条 JSON 验证函数承担整个文件生命周期。当前所有字段都只存在于内存,并不写入偏好设置或关系型数据库。
三、版本支持不是看到整数就全部放行
接下来先处理单条记录:解析失败要单独返回原因,根值不是对象也要拒绝;版本只接受1和2;业务 ID 必须匹配R加三位数字,标题不能为空且长度不能超过20;v2 的等级严格在两值集合里选。这里选择重建一个新的Ticket对象,不直接返回解析所得的原始对象,避免随源数据附带不认识的字段混入后续渲染与持久化。
interface CheckResult { ok: boolean; row?: Ticket; reason: string; } function validateLine(text: string): CheckResult { let raw: RawTicket; try { raw = JSON.parse(text) as RawTicket; } catch (err) { return { ok: false, reason: 'BAD_JSON' }; } if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { return { ok: false, reason: 'BAD_SHAPE' }; } if (raw.version !== 1 && raw.version !== 2) { return { ok: false, reason: 'UNSUPPORTED_VERSION' }; } if (typeof raw.id !== 'string' || !/^R\d{3}$/.test(raw.id) || typeof raw.title !== 'string' || raw.title.trim().length === 0 || raw.title.trim().length > 20) { return { ok: false, reason: 'INVALID_FIELD' }; } const level: TicketLevel = raw.version === 1 ? 'normal' : (raw.level === 'high' ? 'high' : 'normal'); if (raw.version === 2 && raw.level !== 'high' && raw.level !== 'normal') { return { ok: false, reason: 'INVALID_LEVEL' }; } return { ok: true, reason: 'OK', row: { id: raw.id, title: raw.title.trim(), level: level }}; }这段代码有两处刻意保守:一是 v1 才允许使用normal作为缺省值,v2 缺字段也要判为非法;二是标题先做trim()再检查长度。未来如果有真实的版本迁移,需要明确迁移配置和变更记录,不能随意拿“向前兼容”四个字盖过去。异常路径只给出业务原因,不把解析源全文写到日志里,避免导入内容以后含有隐私信息时形成二次泄露。
从生命周期看,每次调用validateLine都只创建一份新对象,没有持久连接与线程实例。JSON.parse的异常在函数内收口,但内存分配失败、超长输入和系统资源紧张等情况并未通过这段短代码完全解决,真实产品必须在读取前限定总文件字节数与单行长度。
四、逐行接受与拒绝,比一个布尔值有用
单条合法并不等于整批导入已经可以提交。如果两条合法数据用了同一个 ID,直接写入可能覆盖已有工单。因此批处理阶段还要保持原始行顺序,禁止本批次重复 ID,分离可展示的通过清单与待人工检查的失败清单。这里的“通过”仅代表结构验证通过,不意味着工单真实性已被核验。
下面的汇总函数一次性处理四行,输出统一状态REVIEW_READY。它并不在循环中立即落库,原因是批量导入时最需要保存的是每行的诊断信息;待用户确认后,才应该单独设计数据库事务及幂等提交操作。若页面被关闭,本次内存报告可以直接丢弃,下次重新读取来源,而不是恢复一个可能已经过期的提交按钮状态。
interface ImportReport { accepted: Ticket[]; rejected: string[]; status: string; } function reviewBatch(lines: string[]): ImportReport { const accepted: Ticket[] = []; const rejected: string[] = []; const seen: Set<string> = new Set<string>(); for (let index = 0; index < lines.length; index++) { const result: CheckResult = validateLine(lines[index]); if (!result.ok || !result.row) { rejected.push(`row=${index + 1} reason=${result.reason}`); continue; } if (seen.has(result.row.id)) { rejected.push(`row=${index + 1} reason=DUPLICATE_ID`); continue; } seen.add(result.row.id); accepted.push(result.row); } console.info(`SG-2908 accepted=${accepted.length} rejected=${rejected.length} status=REVIEW_READY`); return { accepted: accepted, rejected: rejected, status: 'REVIEW_READY' }; }示意 DevEco 页面左侧是SchemaGate项目目录,中间放着validateLine与reviewBatch的业务关键段,右侧为ImportReviewPage的模拟布局,底部 HiLog 使用同一条演示日志:SG-2908 accepted=2 rejected=2 status=REVIEW_READY。这里没有“真实审核通过”这样的词,因为我们做的只是四段静态输入的业务检查,根本没有触达应用市场流程。
五、拒绝项不等于整个页面报错
以前做这类导入界面,容易把某一行 JSON 解析失败直接变成“整批失败”。在离线运维现场,这样会让维护人员看不出到底是第几行坏了,也无法确定前面已识别的记录是否安全。更实用的画面是上半部分汇总结果,下半部分把R103的未知版本和R104的非法等级并排列出,明确告诉操作者:这两行需要修复来源数据,本次不会被悄悄接受。
R101的normal是明确的旧版默认值,并不是任何版本遇到陌生字段都可以默默归一的借口。R102的high来自 v2 的显式输入。有效两项与拒绝两项合计四项,因此接受率固定为50%;如果页面把拒绝行也算进“已提交”,汇总文字就与真实业务语义冲突。REVIEW_READY表示可以进入人工复核页面,不表示已经保存、同步或审批。
手机演示界面保留完整状态栏,显示任务SG-2908、输入4条、通过2条、拒绝2条、接受率50%,下方逐项列出两条拒绝原因。此图是没有手机外壳的纯屏幕设计稿,目的是建立文章数据、示例日志和可视结果的共同基线;不能拿它证明真实手机完成了导入测试。
六、真正上线时还要加三道边界
第一道是资源预算。当前例子只处理四行短字符串,真实导入时应限制总大小、单行长度和最大记录数,并在解析前确定来源格式。一次读取巨大 JSON 字符串再交给JSON.parse,即使逻辑完全正确也可能造成内存峰值和界面停顿。大文件应在外层按格式设计分批读取与用户取消路径,不要把示例的小数组直接复制成生产处理器。
第二道是提交边界。本篇代码没有执行持久化,没有数据库事务,也没有跨设备同步;进入真实工作流时,必须把本次验证报告与提交时的源文件版本绑定。否则用户先看到“通过2条”,再替换来源文件,点击提交却写入了另一批数据。建议提交前重新校验当前源版本,并使用数据库唯一约束阻止跨批次重复 ID。
第三道是生命周期与隐私。页面返回时及时丢弃大体积原始文本和临时报告引用;若扩展为文件选择器,记得释放读取资源并遵守 URI 授权边界。日志只写总数、行号及拒绝原因,不记录维保内容本身。当前演示既没有真实句柄也没有后台任务,因此无需捏造 close、terminate 或系统权限调用;这恰恰是把示例能力与产品责任分开的做法。
本轮的结论只有一句:能被 JSON 解析,不代表能被业务接受。先建立显式版本白名单、字段重建和逐行拒绝清单,再考虑 UI 和落库,才能在处理来源复杂的离线单据时留下可追溯的判断链。
官方核对资料:ArkTS 语言介绍与 HarmonyOS 开发文档中心。文中JSON.parse、正则、数组与集合是语言层能力,字段契约、版本迁移和拒绝码完全属于业务自定义逻辑,不是操作系统提供的导入校验 SDK。
- https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-overview
- https://developer.huawei.com/consumer/cn/doc/