茶器艺科智造HarmonyOS应用实战-21-缺失参数统一塞成-1,零值和非法值怎样区分:用可选字段重塑路由载荷
茶器艺科智造HarmonyOS应用实战-21-缺失参数统一塞成-1,零值和非法值怎样区分:用可选字段重塑路由载荷
小艺深链只想把杯体波纹清零,于是传来 generate=1&waveLevel=0。这条请求最需要保住的是数字 0;其他高度、口径、壁厚没有传,应沿用页面当前值。可当前路由载荷要求八个杯体字段全部存在,缺失或解析失败的数字统一写成 -1。于是“没有传”“写成 abc”“主动传 -1”在 HAR 边界上变成同一个值,而合法零值又很容易在后续代码中被真假判断漏掉。
指定工程目前没有用 if (value) 丢掉 waveLevel=0,HSP 的 clampRouteNumber 会让该值正常进入 0–10 范围;这点必须如实保留。但同一函数对 heightMm=0 会夹到下限 50,对缺失或非法的 -1 则沿用旧值。最终页面行为依赖字段范围,载荷本身却无法说明输入发生过什么。本文建议把路由杯体参数改成类型化 patch:缺失字段不出现,合法零明确出现,非法字段进入独立问题列表。

本文解决四个边界:
- 还原
EMPTY_CUP_PARAMS、ChaqiRouteCupParams与页面夹取的当前协作方式。 - 区分 missing、invalid、out-of-range 与合法 0,避免哨兵值碰撞。
- 比较
Partial<T>与显式可选接口在 HAR 公共 API 中的适用位置。 - 给出 typed decode、patch 应用、兼容迁移和验证矩阵。
一、当前公共接口强迫“没传的字段”也带一个数字
libraryhar/src/main/ets/routes/ChaqiRouteIntent.ets:24-45 定义的 ChaqiRouteCupParams 把所有属性声明为必填,数值注释约定 -1 表示未传或失败:
export interface ChaqiRouteCupParams {
presetKey: string;
heightMm: number;
diameterMm: number;
mouthMm: number;
bottomMm: number;
thicknessMm: number;
waistPct: number;
waveLevel: number;
}
const EMPTY_CUP_PARAMS: ChaqiRouteCupParams = {
presetKey: '', heightMm: -1, diameterMm: -1, mouthMm: -1,
bottomMm: -1, thicknessMm: -1, waistPct: -1, waveLevel: -1
};
parseCupParams 对七个数字分别调用 parseRouteNumber;空串与无法解析的文本都得到 -1。parseWant 在 generateCup 为 false 时把同一个 EMPTY_CUP_PARAMS 放入 payload,为 true 时则构造一份字段齐全的对象。即使某次路由根本不涉及杯体,cup 仍然存在。
这种形状让消费者写起来方便,却把来源信息压扁了。看到 heightMm === -1,无法判断键缺失、值为空、编码损坏、格式错误还是调用方真的传了负一。公开类型也没有阻止调用方把任意字符串当作 presetKey。
二、页面能保住部分零值,但行为取决于字段范围
HSP 的 ChaqiExperiencePage.ets:811-815 通过下面的函数应用路由数字:
private clampRouteNumber(
value: number,
min: number,
max: number,
fallback: number
): number {
if (value < 0 || Number.isNaN(value)) {
return fallback;
}
return Math.max(min, Math.min(max, value));
}
waveLevel=0 不小于 0,会被保留为合法 0;heightMm=0 同样不小于 0,却会被夹到 50。缺失 heightMm 和非法 abc 在 HAR 中都成为 -1,于是沿用当前高度。行为表如下:
| 外部输入 | HAR 当前值 | 页面当前结果 | 丢失的信息 |
|---|---|---|---|
| 未传 heightMm | -1 | 沿用当前高度 | 缺失 |
heightMm=abc | -1 | 沿用当前高度 | 格式错误 |
heightMm=-1 | -1 | 沿用当前高度 | 明确负数 |
heightMm=0 | 0 | 夹到 50 | 越界 |
waveLevel=0 | 0 | 应用 0 | 合法零值 |
页面最终没有崩溃,不代表协议清晰。对于外部请求,格式错误和越界值通常需要问题回执或脱敏日志;缺失字段则是正常情况,不应记录成错误。三者先在 HAR 中区分,页面才能采用不同策略。
三、patch 语义比“全量对象加哨兵”更符合深链
深链携带的是对当前杯体的局部修改,不是完整杯体快照。适合的语义是:属性出现表示调用方提供了一个已通过类型与范围校验的值;属性缺失表示保持当前状态。

建议先定义领域全集,再定义可选 patch。CupPresetKey 已在 ChaqiModels.ets 中声明,可以直接复用,避免继续暴露任意 string:
export interface ChaqiCupValues {
presetKey: CupPresetKey;
heightMm: number;
diameterMm: number;
mouthMm: number;
bottomMm: number;
thicknessMm: number;
waistPct: number;
waveLevel: number;
}
export interface ChaqiRouteCupPatch {
presetKey?: CupPresetKey;
heightMm?: number;
diameterMm?: number;
mouthMm?: number;
bottomMm?: number;
thicknessMm?: number;
waistPct?: number;
waveLevel?: number;
}
这样不再需要 EMPTY_CUP_PARAMS。generateCup 为 false 时,payload 可以不带 cup;generateCup 为 true 但只传 waveLevel 时,cup 只包含 { waveLevel: 0 }。类型形状本身就表达了请求意图。
四、Partial 适合内部组装,公共边界适合显式可选字段
从语义上看,type ChaqiRouteCupPatch = Partial<ChaqiCupValues> 很简洁:它把全集字段全部变为可选。内部解码器组装临时对象时,这种写法很合适。
type MutableCupPatch = Partial<ChaqiCupValues>;
function newCupPatch(): MutableCupPatch {
return {};
}
但 HAR 的公共 API 更推荐显式写出 ChaqiRouteCupPatch。原因不是 Partial 本身不安全,而是公共契约需要可读、可演进:某些字段未来可能变为必填、只读或不允许从深链修改;显式接口能逐字段写注释,也不会让新增到 ChaqiCupValues 的内部字段自动暴露给外部路由。
一个稳妥分工是:解码函数内部使用 Partial<ChaqiCupValues> 组装;返回前映射到明确的 ChaqiRouteCupPatch;Index.ets 只导出后者。ArkTS 在 TypeScript 风格基础上强化静态约束,实际使用 Partial 和可选属性时仍需以项目目标 SDK 编译为准,可参考华为的从 TypeScript 到 ArkTS 的适配指导。
五、先把 ParseResult 定义成真正的判别联合
为了让本篇代码可以独立阅读,先在 HAR 边界定义完整的 ParseResult<T>。成功分支独占 value;MISSING、格式错误、非有限数和越界四类失败分支都不带 value。这样调用方只要还没有把 kind 收窄到 OK,编译器就不会允许它把占位值当成业务数据。
export type ParseFailure =
| { kind: 'MISSING' }
| { kind: 'INVALID_FORMAT'; issueCode: string }
| { kind: 'NON_FINITE'; issueCode: string }
| { kind: 'OUT_OF_RANGE'; issueCode: string };
export type ParseResult<T> =
| { kind: 'OK'; value: T }
| ParseFailure;
export type RouteField =
| 'generate' | 'tab' | 'presetKey' | 'heightMm'
| 'diameterMm' | 'mouthMm'
| 'bottomMm' | 'thicknessMm' | 'waistPct' | 'waveLevel';
export interface RouteDecodeIssue {
field: RouteField;
code: string;
}
export interface CupPatchDecodeResult {
patch: ChaqiRouteCupPatch;
issues: RouteDecodeIssue[];
}
MISSING 没有 issueCode,因为杯体 patch 中“未提供字段”是正常语义;其余失败分支携带稳定问题码,供 issues、日志或错误页使用。问题对象不保存原始值,只保存受控字段名与问题码。若需要关联一次请求,可在更外层添加随机 requestId 或调用方提供的非敏感 id,不要把完整 URI 写进公开日志。
杯体解码不能只实现高度和波纹后就声称完成。下面的字段规范表列出本篇建议契约;所有数值都要先判格式与有限性,再判闭区间,任一步失败都不得进入 patch。
| 原始键 | patch 字段 | OK 条件 | 缺失时 | 非法时 |
|---|---|---|---|---|
preset | presetKey | 六个 CupPresetKey 白名单值之一 | 省略,不报 issue | 省略,PRESET_UNKNOWN |
heightMm | heightMm | 有限数,50–100 | 省略,不报 issue | 省略,HEIGHT_* |
diameterMm | diameterMm | 有限数,60–110 | 省略,不报 issue | 省略,DIAMETER_* |
mouthMm | mouthMm | 有限数,55–110 | 省略,不报 issue | 省略,MOUTH_* |
bottomMm | bottomMm | 有限数,25–60 | 省略,不报 issue | 省略,BOTTOM_* |
thicknessMm | thicknessMm | 有限数,3–15 | 省略,不报 issue | 省略,THICKNESS_* |
waistPct | waistPct | 有限数,30–70 | 省略,不报 issue | 省略,WAIST_* |
waveLevel | waveLevel | 有限数,0–10 | 省略,不报 issue | 省略,WAVE_* |
表中的 * 应展开成 FORMAT_INVALID、NON_FINITE 或 OUT_OF_RANGE,例如 HEIGHT_OUT_OF_RANGE。这些范围对应当前页面的杯体控制边界;如果领域模型以后调整范围,应先改一处字段规范,再让解码器和测试向量共同引用,避免 HAR 与页面再次分叉。
以下函数只节选高度和波纹两个差异最明显的字段,用来展示控制流,并不是完整的八字段实现。其余五个数值字段必须按表中范围走同一条分支,presetKey 则使用下一节的白名单解析器。
function decodeCupPatch(input: RouteTextMap): CupPatchDecodeResult {
const patch: ChaqiRouteCupPatch = {};
const issues: RouteDecodeIssue[] = [];
const height = parseRouteNumber(input.heightMm, 50, 100, 'HEIGHT');
if (height.kind === 'OK') {
patch.heightMm = height.value;
} else if (height.kind !== 'MISSING') {
issues.push({ field: 'heightMm', code: height.issueCode });
}
const wave = parseRouteNumber(input.waveLevel, 0, 10, 'WAVE');
if (wave.kind === 'OK') {
patch.waveLevel = wave.value;
} else if (wave.kind !== 'MISSING') {
issues.push({ field: 'waveLevel', code: wave.issueCode });
}
return { patch: patch, issues: issues };
}
判断条件是 kind === 'OK',不是 if (wave.value)。因此合法 0 会明确写入 patch;失败对象压根没有 value 可读。实现其余字段时可以复用字段规范,但不要为了七个数值引入动态反射或无类型对象,否则会削弱字段名和返回值之间的静态约束。
六、presetKey 也要做类型化白名单解码
当前 presetKey 是 string,页面再遍历 CUP_PRESET_KEYS 找索引。可以把这一步前移到 HAR 的 typed decode,让 patch 中出现的键一定属于联合类型。
function parsePresetKey(raw: string | undefined): ParseResult<CupPresetKey> {
if (raw === undefined || raw.length === 0) {
return { kind: 'MISSING' };
}
if (raw === 'luohan' || raw === 'wukong' || raw === 'fang' ||
raw === 'generic' || raw === 'spiral' || raw === 'zhujie') {
return { kind: 'OK', value: raw };
}
return {
kind: 'INVALID_FORMAT',
issueCode: 'PRESET_UNKNOWN'
};
}
解析器不再为失败结果伪造 luohan。只有 OK 分支才允许把 preset.value 写入 patch;MISSING 只省略字段,其他分支把 issueCode 加入 issues。这样 preset=hacker 不会被悄悄改成罗汉杯,也不会一路进入页面才被忽略。第 23 篇还会进一步把预设键、标签和参数合并为配置表,本篇只解决路由载荷类型。
const preset = parsePresetKey(input.preset);
if (preset.kind === 'OK') {
patch.presetKey = preset.value;
} else if (preset.kind !== 'MISSING') {
issues.push({ field: 'presetKey', code: preset.issueCode });
}
这段接线也展示了判别联合的价值:进入最后一个分支后,类型已收窄到三类带 issueCode 的失败结果;整个失败路径都不需要、也无法访问 value。
七、payload 不再携带无意义的空杯体对象
建议把公共载荷改为可选 cup 与独立 issues。tab 是否可选可以按现有路由协议另行决定,本篇保持其 number 形状以缩小改动:
export interface ChaqiRoutePayload {
tab: number;
generateCup: boolean;
cup?: ChaqiRouteCupPatch;
issues: RouteDecodeIssue[];
}
export type RoutePayloadDecodeResult =
| { kind: 'OK'; payload: ChaqiRoutePayload }
| { kind: 'INVALID_ROUTE'; issues: RouteDecodeIssue[] };
function generateIssue(result: ParseFailure): RouteDecodeIssue {
if (result.kind === 'MISSING') {
return { field: 'generate', code: 'GENERATE_MISSING' };
}
return { field: 'generate', code: result.issueCode };
}
function buildRoutePayload(want: Want): RoutePayloadDecodeResult {
const generate = decodeGenerate(want);
if (generate.kind !== 'OK') {
return {
kind: 'INVALID_ROUTE',
issues: [generateIssue(generate)]
};
}
if (!generate.value) {
return {
kind: 'OK',
payload: { tab: decodeTab(want), generateCup: false, issues: [] }
};
}
const decoded = decodeCupPatch(textMapFromWant(want));
return {
kind: 'OK',
payload: {
tab: decodeTab(want),
generateCup: true,
cup: decoded.patch,
issues: decoded.issues
}
};
}
代码先检查 generate.kind,随后才读取 generate.value。本文明确选择:generate 缺失或格式非法时直接返回 INVALID_ROUTE 和 issues,不构造普通 payload;generate=false 是合法 OK 值,返回不带 cup 的 payload;generate=true 允许 cup 为 {},语义是“进入生成页,但沿用当前杯体参数”。如果产品希望“generate 缺失”等价于 false,应由 decodeGenerate 明确归一化为 { kind: 'OK', value: false },不能让构造器读取失败结果。
示例沿用当前返回 number 的 decodeTab 以缩小讨论范围。如果 tab 也改成 ParseResult<number>,必须先做相同的 kind 收窄,再决定默认页签或路由失败,不能无条件读取 value。

八、页面应用 patch 时必须用 undefined 判定
HSP 不再需要通过负数哨兵决定是否沿用旧值,只需对出现的字段赋值。合法零必须用 !== undefined 判断:
private applyCupPatch(patch: ChaqiRouteCupPatch): void {
if (patch.presetKey !== undefined) {
this.applyPresetByKey(patch.presetKey);
}
if (patch.heightMm !== undefined) {
this.heightMm = patch.heightMm;
}
if (patch.diameterMm !== undefined) {
this.diameterMm = patch.diameterMm;
}
if (patch.mouthMm !== undefined) {
this.mouthMm = patch.mouthMm;
}
if (patch.bottomMm !== undefined) {
this.bottomMm = patch.bottomMm;
}
if (patch.thicknessMm !== undefined) {
this.thicknessMm = patch.thicknessMm;
}
if (patch.waistPct !== undefined) {
this.waistPct = patch.waistPct;
}
if (patch.waveLevel !== undefined) {
this.waveLevel = patch.waveLevel;
}
this.syncCupLatheFromUi();
}
这里列全了七个数值字段,不再用“其余字段同理”掩盖实现范围。不要写 if (patch.waveLevel),否则 0 会被当成 false 而跳过。也不要在页面再次把越界值夹到范围内并静默接受;HAR 边界已负责外部输入,页面可保留防御性断言,但遇到异常应暴露内部契约问题。
迁移时可先增加新类型与解码器,保留旧接口适配一版:把旧值 >=0 的字段转成 patch,把 -1 视为缺失;新调用链稳定后再移除哨兵。这个适配只能迁移旧载荷,无法恢复过去已经丢失的“非法还是缺失”信息。
九、验证矩阵要证明零、缺失和非法走三条路径
下面是建议加入 HAR 测试目录的用例草稿,用于说明应锁定哪些行为;本文整理时没有执行这些用例,不能把代码片段视为测试通过记录。
it('keeps explicit zero in patch', 0, () => {
const result = decodeCupPatch(routeText({ waveLevel: '0' }));
expect(result.patch.waveLevel).assertEqual(0);
expect(result.issues.length).assertEqual(0);
});
it('omits missing field without issue', 0, () => {
const result = decodeCupPatch(routeText({}));
expect(result.patch.heightMm === undefined).assertTrue();
expect(result.issues.length).assertEqual(0);
});
it('reports invalid field and omits it', 0, () => {
const result = decodeCupPatch(routeText({ heightMm: 'abc' }));
expect(result.patch.heightMm === undefined).assertTrue();
expect(result.issues[0].code).assertEqual('HEIGHT_FORMAT_INVALID');
});
it('does not replace unknown preset with luohan', 0, () => {
const result = decodeCupPatch(routeText({ preset: 'unknown' }));
expect(result.patch.presetKey === undefined).assertTrue();
expect(result.issues[0].code).assertEqual('PRESET_UNKNOWN');
});
it('returns route failure for invalid generate', 0, () => {
const result = buildRoutePayload(routeWant({ generate: 'maybe' }));
expect(result.kind).assertEqual('INVALID_ROUTE');
});
验证不能只覆盖一个零值和一个格式错误。下面的矩阵同时覆盖控制字段、边界值、非有限数、白名单和混合输入;“预期结果”是待验证的契约,不代表已经运行。
| 场景 | 预期解码结果 | 预期页面行为 |
|---|---|---|
| generate 缺失 | INVALID_ROUTE,GENERATE_MISSING | 不进入参数应用 |
generate=maybe | INVALID_ROUTE,GENERATE_FORMAT_INVALID | 不进入参数应用 |
generate=false | OK,payload 不带 cup | 不执行生成参数应用 |
generate=true,未传杯体字段 | OK,cup={},无 issue | 进入生成页,沿用全部当前值 |
waveLevel=0 | { waveLevel: 0 },无 issue | 清除波纹 |
waveLevel=10 | { waveLevel: 10 },无 issue | 应用上边界值 |
waveLevel=-0.1 | 不含 waveLevel,WAVE_OUT_OF_RANGE | 沿用当前波纹 |
heightMm=50 / 100 | 分别写入闭区间端点,无 issue | 高度应用 50 / 100 |
heightMm=0 | 不含 heightMm,HEIGHT_OUT_OF_RANGE | 沿用高度,不夹到 50 |
heightMm=abc | 不含 heightMm,HEIGHT_FORMAT_INVALID | 沿用高度并留问题码 |
heightMm=NaN / Infinity | 不含 heightMm,HEIGHT_NON_FINITE | 沿用高度并留问题码 |
preset=luohan | { presetKey: 'luohan' },无 issue | 先应用罗汉杯预设 |
preset=unknown | 不含 presetKey,PRESET_UNKNOWN | 不切预设,也不回退罗汉杯 |
preset=wukong&waveLevel=0 | 同时写入两个字段,无 issue | 先应用悟空杯,再把波纹覆盖为 0 |
heightMm=70&mouthMm=999 | 保留 heightMm,舍弃 mouthMm 并报 issue | 应用合法字段,非法字段沿用当前值 |
纯函数用例只能证明 decode 语义;页面集成还要观察 patch 应用顺序、3D 同步与持久化。两类证据不能互相替代。
待执行的验收清单
以下项目特意保持未勾选,只有在目标工程中拿到对应证据后才能逐项更新:
- 将
ParseResult<T>、字段规范和解码器接入 HAR,并通过目标 SDK 的 ArkTS 编译。 - 为七个数值字段分别覆盖缺失、格式错误、NaN、Infinity、上下边界和越界输入。
- 验证所有 ParseResult 失败对象都不含
value,业务代码只在kind === 'OK'后读取它。 - 验证未知 preset 不会生成
luohan,也不会改变页面当前预设。 - 验证 generate 缺失或非法返回路由失败,generate=false 返回不带 cup 的合法 payload。
- 验证 preset 与显式数值同时出现时,页面先应用预设,再应用字段覆盖,包括
waveLevel=0。 - 运行
hvigorw assembleHap --no-daemon,保存完整构建结果并处理编译错误。 - 在模拟器或真机从小艺入口验证冷启动与
onNewWant,观察页面、3D 同步、持久化和脱敏日志。 - 发布前在 CSDN 编辑器核对三张图片、代码块、表格和标题显示,再单独确认保存或发布状态。
十、故障排查
| 现象 | 首查 | 根因 | 修正 |
|---|---|---|---|
| waveLevel=0 没生效 | 页面是否用真假判断 | 0 被当 false | 改用 !== undefined |
| 非法值没有 issue | decode 是否仍返回 -1 | 旧哨兵链未移除 | 返回 ParseResult 并汇总 |
| 缺失字段产生大量错误 | 是否把 MISSING 加入 issues | 正常省略被当异常 | missing 只是不写 patch |
| 新领域字段意外可被深链修改 | 公共类型是否直接用 Partial | 全集新增自动暴露 | 公共接口显式列可选字段 |
| 未知 preset 变成罗汉杯 | 失败对象是否还有 luohan 占位 | 失败分支被误读 | 删除失败 value,只在 OK 后赋值 |
| generate 非法却按 false 处理 | 是否先读取 generate.value | 控制字段失败被吞掉 | 先收窄 kind,失败返回 INVALID_ROUTE |
| 页面仍把越界值夹到下限 | 是否复用旧 clamp | 输入错误被改写成合法值 | 越界字段不进入 patch |
十一、工程总结
路由参数的关键不是找一个更隐蔽的哨兵,而是让类型形状保留输入事实。ParseResult<T> 负责区分成功、缺失与三类非法输入;成功分支才拥有 value。ChaqiRouteCupPatch 负责表达“只修改出现过且已验证的字段”,所以 0 与 undefined 各有明确含义。
控制字段和业务字段还要分层处理。generate 决定整条路由是否成立,解析失败应在 payload 构造前结束;单个杯体字段非法则进入 issues,其他合法字段仍可组成 patch。页面只消费验证后的 patch,并按“先预设、后显式字段”的顺序应用,避免预设覆盖调用方明确给出的零值。
迁移可以保留一版旧载荷适配器,但它只能把 >=0 转成 patch、把 -1 当缺失,无法恢复已经被哨兵压扁的历史信息。稳定后应删除旧全量对象和页面 clamp 路由链,让 HAR 成为外部输入的唯一校验边界。
十二、证据边界
当前源码事实包括:ChaqiRouteCupParams 所有字段必填;EMPTY_CUP_PARAMS 用空字符串与 -1 填满;缺失和解析失败数字都成为 -1;generate=false 仍携带 cup;HSP 对负数沿用旧值,对非负越界值做夹取,且合法 waveLevel=0 当前能够生效。本文的可选 patch、Partial 分工、typed decode、issues 与迁移方案均为建议,没有修改只读项目。本文整理未运行单测、HAP 构建、模拟器、真机、小艺入口或 CSDN 发布,因此只能确认源码现状与静态设计,不能声称新载荷已经在运行环境生效。
更多推荐


所有评论(0)