茶器艺科智造HarmonyOS应用实战-61-打印机名称、IP和端口填什么都能显示连接成功:在创建订单前建立端点校验门
茶器艺科智造HarmonyOS应用实战-61-打印机名称、IP和端口填什么都能显示连接成功:在创建订单前建立端点校验门
打印机弹窗里有名称、IP 和端口三个输入框,但“开始连接”并没有消费这些输入来判断端点是否成立。按钮直接进入成功回调,随后创建订单并展示连接成功。于是页面上的“成功”只说明点击事件走到了回调,并不说明输入合法,更不说明打印机可达。

本文以 Gitee 仓库 aiding521/chaqi-app-gitee 的 master 分支、提交 f671fcd8de277973bd7518a3c462f68384575f1e 的静态源码为事实基线。本文没有修改项目源码,也没有执行 HAP 构建、模拟器或真机验证;建议代码属于演进方案,不能描述成已经落地的功能。
一、从“一按就成功”拆出真正的问题
静态源码能确认的调用关系非常直接:按钮没有先解析名称、IP 和端口,也没有等待任何网络结果,而是立刻调用成功函数。当前代码可以概括为:
Button('开始连接').onClick(() => this.onSlicePrinterConnectSuccess())
这里至少混在一起了三种不同语义:用户完成了输入、输入可被解析为端点、目标设备已经响应。现有按钮只证明第一件事发生过,却把第三件事的文案和后续动作一起触发。名称为空、IP 格式不成立、端口不是完整数字或超出范围时,页面仍可能沿着同一条成功路径创建订单。
这份结论只来自静态调用关系,不能反推真实设备一定连接失败,也不能证明网络栈的实际行为。它能证明的是:当前路径没有以输入校验结果或握手终态作为“成功”的前置条件,因此页面缺少一道可审计的端点校验门。
二、把“连接”拆成三道彼此独立的门
建议把一次操作按顺序拆成“端点解析、网络握手、订单创建”三道门。第一道门只回答字符串能否组成 PrinterEndpoint;第二道门才回答这个端点是否响应;第三道门在真实终态成功后提交订单。任意一道门失败,都应停在本门并给用户可修正的入口,不能借用下一道门的成功文案。

流程图的关键不是多画几个框,而是明确成功的来源:解析成功不等于连通成功,连通成功也不等于订单已经持久化。弹窗是否关闭、订单何时创建、Toast 显示什么,都应消费对应阶段的明确结果。这样即使后续替换真实连接能力,也不需要再次改写输入规则和订单提交规则。
三、先把三个字符串收口成端点值对象
export interface PrinterEndpoint { name: string; host: string; port: number; }
export function parsePrinterEndpoint(name: string, host: string, portText: string): PrinterEndpoint | null {
if (!name.trim() || !isValidIpv4(host.trim()) || !/^[0-9]+$/.test(portText.trim())) return null;
const port = Number(portText);
return port >= 1 && port <= 65535 ? { name: name.trim(), host: host.trim(), port } : null;
}
这个函数只负责语法边界:名称去除首尾空白后不能空,IP 必须通过 isValidIpv4,端口文本必须由数字完整组成,数值还要落在 1 到 65535 之间。成功时返回归一化后的结构,失败时返回 null,页面不再分别保存三份“看起来合法”的判断。
要特别注意,PrinterEndpoint 只代表“可用于发起连接的地址”,不代表打印机在线。把这个区别写进接口语义,可以阻止调用方看到非空对象就直接显示“连接成功”。isValidIpv4 的具体实现也应是全串校验,而不是只在字符串中找到四段类似数字的片段。
四、页面先消费解析结果,再决定是否发起握手
const endpoint = parsePrinterEndpoint(this.printerNameInput,
this.printerIpInput, this.printerPortInput);
if (endpoint === null) { this.showToast('请检查连接信息', 2400); return; }
这段接入代码做了两个重要动作:一次性读取三个输入,解析失败就原地返回。此时应保留用户已经输入的内容,让用户能够针对提示修改,而不是关闭弹窗或清空字段。若要进一步指出具体字段,可把 null 演进为带字段名和错误类型的结果,但仍应保持“解析函数给结论、页面决定怎么展示”的边界。
解析通过后才有资格进入异步握手。握手期间可以展示“连接中”,但“连接成功”、弹窗关闭和订单创建都必须等待服务终态。重复点击还需要复用、拒绝或取消前一个连接请求,不能让两个迟到回调分别创建订单。
五、用状态表约束按钮、弹窗和订单的先后关系
| 阶段 | 可以依赖的事实 | 页面允许做什么 | 失败时保留什么 |
|---|---|---|---|
| 编辑中 | 三个原始输入字符串 | 修改字段、取消弹窗 | 已输入内容 |
| 解析失败 | 字段不满足本地规则 | 标出问题、允许再次提交 | 输入值与错误位置 |
| 待握手 | 已得到规范化端点 | 展示连接中、阻止重复提交 | 端点与请求身份 |
| 握手失败 | 服务返回失败或请求结束 | 恢复提交入口、显示原因 | 端点,便于重试 |
| 握手成功 | 成功来自当前请求终态 | 创建订单、关闭弹窗 | 订单创建结果 |
| 页面离开 | 当前页面不再拥有展示状态 | 取消或失效化请求回调 | 不允许迟到结果改页面 |
这张表把“什么时候能创建订单”写成了唯一答案:只有当前连接请求的握手终态成功之后。解析阶段不能偷跑,旧请求的迟到成功也不能越过请求身份校验。若订单创建本身失败,连接结果与订单结果还应分别记录,不能用一个成功 Toast 覆盖第二次失败。
六、端点解析适合先做成可穷举的纯逻辑
interface BoundaryCase<T> {
name: string;
input: T;
expected: string;
}
const cases: BoundaryCase<string>[] = [
{ name: 'empty', input: '', expected: 'reject' },
{ name: 'normal', input: 'valid', expected: 'accept' },
{ name: 'repeat', input: 'valid', expected: 'idempotent' }
];
上面的通用用例骨架落到本文时,应把 input 换成名称、IP、端口三元组,把 expected 换成解析成功或具体错误。名称可覆盖空串、纯空白和首尾空白;IP 可覆盖段数不足、非数字段、越界段;端口要覆盖空串、夹杂字符、0、1、65535 和 65536。相同输入重复解析应产生相同结果,因为这个函数不应依赖页面状态或当前时间。
纯函数用例只能证明本地规则稳定,不能证明某个端点真的有打印机响应。握手超时、拒绝、快速重试以及页面离开后的迟到回调,仍要在具备相应能力的运行环境中补证据,并把未执行项保留为未执行。
七、握手请求的所有权必须跟弹窗生命周期对齐
private disposeOwnedResources(): void {
// timer/listener只由创建它的组件清理
// 异步结果写状态前核对taskId或pageGeneration
// 可恢复任务先保存业务事实,再释放进程句柄
}
一旦加入真实握手,页面就会拥有超时计时器、监听器或异步回调。创建这些资源的组件应保存其句柄,并在弹窗取消、页面离开或新请求替换旧请求时执行对应清理。仅把按钮禁用并不能阻止旧回调到达;回调写状态前还要核对请求身份或页面 generation。
连接请求和订单是两类不同对象。请求句柄属于当前进程,可以随页面销毁而释放;已经创建并需要展示的订单属于业务事实,不应跟着弹窗清理。把二者塞进同一个布尔字段,往往会造成“关弹窗就丢订单”或“订单还在就继续写已销毁页面”两种相反错误。
八、验收要把格式、连通和订单三层分开
- 名称为空、IP 不完整、端口含非数字时,提交被本地规则拦截,弹窗不关闭。
- 端口 1 和 65535 能进入下一阶段,0 和 65536 不能进入握手。
- 解析成功但端点无响应时,不创建订单,也不出现“连接成功”。
- 握手尚未结束时连续点击,只存在一个可解释的当前请求。
- 请求发出后立即关闭弹窗或离开页面,迟到回调不再写旧页面。
- 只有当前请求返回真实成功终态后,订单创建动作才执行一次。
- 订单创建失败时展示订单层失败,不把连接成功伪装成全流程成功。
- 静态检查、构建、模拟器、真机和真实端点结果分别记录;没有执行的项目明确写“未执行”。
九、五种看似改善体验、实际仍会假成功的改法
| 误修 | 为什么仍不成立 | 应收口到哪里 |
|---|---|---|
| 点击后固定等待一段时间 | 延迟不是设备响应 | 等待握手真实终态 |
| 只把文案改成“已提交” | 订单依旧可能被提前创建 | 拆开连接与订单状态 |
| 只校验输入非空 | 非空 IP、端口仍可能不可解析 | 统一端点解析函数 |
| 捕获异常后仍走成功回调 | 失败原因被吞掉 | 返回类型化失败结果 |
| 解析成功就关闭弹窗 | 格式正确不等于目标可达 | 握手成功后再关闭 |
颜色、加载动画和等待时间都可以改善反馈,却不能改变成功的证据来源。评审时应沿调用链反向追问:这个 Toast 依据哪个终态、这个终态属于哪个请求、这个请求消费了哪个规范化端点。三问有任意一问没有答案,就仍存在假成功窗口。
十、把规则、页面状态和网络能力放回各自模块

按本文已有模块关系,entry 负责产品入口,libraryhsp 承担页面与交互,libraryhar 放模型、算法、路由和通用服务。PrinterEndpoint、端口范围与纯解析规则不依赖 ArkUI,适合放在 HAR;输入文本、弹窗可见性、连接中状态以及页面拥有的请求句柄应留在 HSP/entry。真实网络能力可以通过明确接口被页面调用,但不应让解析函数偷偷发起网络请求。
entry / libraryhsp -> 页面、组件、生命周期
libraryhar -> 纯类型、纯函数、可持久化模型
tests -> 边界、幂等、恢复序列
runtime evidence -> 日志、设备行为、服务读回
十一、证据边界:本文只证明调用链缺门
baseline: f671fcd8de277973bd7518a3c462f68384575f1e
scope: source-level proposal
build: not run
simulator/device: not run
network/account: not run
result: static boundary identified; implementation pending
静态源码足以确认按钮直接调用成功函数,也足以确认这条路径中未出现空值、IPv4、端口校验或真实握手。但静态阅读不能回答目标打印机是否在线、某个网络接口如何返回、超时多长合适,也不能证明建议代码已经集成。所以上述记录继续把 build、设备和网络标为 not run,避免把设计推演写成运行结论。
十二、用五条操作序列复现端点校验门
建议把验收写成从输入到订单的完整序列,而不是只截一张成功 Toast。先用非法字段确认请求根本没有发出,再用格式正确但不可达的端点区分“解析成功”和“握手成功”,最后用连续提交与快速离开检查请求身份。每条序列都要记录订单数组是否变化,因为这正是现有成功回调带来的业务副作用。
| 场景 | 操作序列 | 必须观察的证据 | 不能作为结论的替代物 |
|---|---|---|---|
| 空名称 | 清空名称→填写其余字段→提交 | 原地提示、订单不增加 | 按钮有点击动画 |
| 端口边界 | 分别输入 0、1、65535、65536 | 只有合法边界进入握手 | 输入框不是红色 |
| 端点不可达 | 合法格式→发起连接→等待失败 | 无成功 Toast、无新订单 | 字段解析通过 |
| 连续连接 | A 未结束→修改端点→提交 B | A/B 结果与请求身份对应 | 最后只看到一个 Toast |
| 快速离开 | 发起握手→关闭弹窗或返回 | 旧回调不关闭新弹窗、不建订单 | 页面没有崩溃 |
日志不应只有 success 或 failed。至少需要当前请求身份、页面 generation、脱敏后的端点摘要、开始与结束时间、终态来源,以及订单创建是否发生。名称和完整地址是否可以记录要服从产品隐私规则;不确定时只记录摘要或状态码。
interface OperationTrace {
taskId: string;
generation: number;
startedAtMs: number;
finishedAtMs?: number;
terminalSource?: 'ui' | 'service' | 'restore' | 'cancel';
}
function canApplyResult(trace: OperationTrace, currentGeneration: number): boolean {
return trace.generation === currentGeneration &&
trace.finishedAtMs === undefined;
}
这段跟踪结构虽然是通用骨架,在本文中可以把 taskId 解释为连接请求身份。应用回调前既要核对 generation,也要确认当前请求尚未结束;若回调重复到达,第一次终态之后的结果必须被忽略。若 A、B 两次连接顺序颠倒,A 的迟到成功不能替 B 创建订单。
最终评审只需抓住一条因果链:原始输入先生成规范化端点,规范化端点再生成有身份的握手请求,当前请求的真实成功终态才允许创建一次订单。截图可以说明界面长什么样,却不能替代请求日志和订单读回。本文没有执行构建、模拟器、真机或网络步骤,因此这些运行结果仍保持“未执行”。
补充审查要求:端点示例、连接接口和订单副作用都要与后续源码及产品规则重新核对;若 SDK 或仓库版本变化,应重新建立基线,不沿用本文快照。对没有执行的构建、设备、网络和账号步骤继续标记为未执行。
更多推荐

所有评论(0)