【寻迹校园 HarmonyOS NEXT 实战 04】ArkTS 统一结果模型:OperationResult<T> 如何收敛错误
【寻迹校园 HarmonyOS NEXT 实战 04】ArkTS 统一结果模型:OperationResult 如何收敛错误
这是“寻迹校园 HarmonyOS NEXT 实战”系列第 4 篇。本文讨论一个很小但非常关键的工程契约:Service 和能力适配器如何把校验、存储与平台异常转换为页面可以安全消费的统一结果。

上图为本文原创生成的统一结果模型概念图:不同来源的数据先经过同一契约归一化,再明确分流为成功值或结构化错误。它展示的是状态语义,不是项目界面截图。
一、直接 throw 给页面会发生什么
在页面数量很少时,我们可能会这样写:
try {
await repository.insert(record);
this.message = '保存成功';
} catch (error) {
this.message = `${error}`;
}
这种写法有三个风险:
- 页面知道了 Repository 的实现细节;
- 内部异常原文可能包含 SQL、路径或敏感字段;
- 每个页面会写出不同的错误文案、重试和 loading 逻辑。
更严重的是,校验失败、数据库失败和系统能力不支持会全部落入一个 catch。用户只能看到“操作失败”,开发者也难以判断该不该重试。
二、OperationResult 的最小结构
项目在 common-core 中定义统一结果:
export enum ErrorCategory {
VALIDATION = 'VALIDATION',
STORAGE = 'STORAGE',
PLATFORM = 'PLATFORM',
UNKNOWN = 'UNKNOWN'
}
export class OperationResult<T> {
success: boolean = false;
userMessage: string = '';
data?: T;
errorCategory?: ErrorCategory;
constructor(
success: boolean,
userMessage: string,
data?: T,
errorCategory?: ErrorCategory
) {
this.success = success;
this.userMessage = userMessage;
this.data = data;
this.errorCategory = errorCategory;
}
}
四个字段分别解决不同问题:
success:页面是否进入成功分支;userMessage:可以直接展示给用户的安全文案;data:成功结果或需要保留的部分结果;errorCategory:决定重试、定位和后续扩展,不直接暴露内部异常。
三、校验失败由 Service 转换
发布表单的字段校验集中在 ReportService。页面负责展示错误,Service 才是最终门禁。

左侧的校验、存储与平台能力故障形态各不相同,但进入映射层后会被收敛为少量稳定错误。页面因此不需要识别数据库异常、系统错误码或第三方返回结构,只消费统一结果契约。
async createReport(draft: ReportDraft): Promise<OperationResult<ItemReport>> {
const validation: PublishValidation = this.validateDraft(draft);
if (!validation.isValid()) {
return new OperationResult<ItemReport>(
false,
'请检查必填信息和公开内容',
undefined,
ErrorCategory.VALIDATION
);
}
try {
const report = await this.repository.insert(/* normalized data */);
return new OperationResult<ItemReport>(true, '发布成功', report);
} catch (error) {
return new OperationResult<ItemReport>(
false,
'发布失败,请稍后重试',
undefined,
ErrorCategory.STORAGE
);
}
}
页面不需要知道标题为什么不合格、哪个 SQL 失败。字段级错误由 PublishValidation 展示,操作级结果负责控制流程。
四、系统能力错误由 Adapter 转换
Photo Picker 是典型的平台能力。用户取消选择和系统调用失败是两种不同结果:
export class PhotoPickerService {
async selectImages(maxSelectNumber: number = 3): Promise<OperationResult<string[]>> {
const options = new photoAccessHelper.PhotoSelectOptions();
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
options.maxSelectNumber = Math.max(1, Math.min(maxSelectNumber, 3));
options.isPhotoTakingSupported = false;
try {
const picker = new photoAccessHelper.PhotoViewPicker();
const result = await picker.select(options);
const selected = result.photoUris.slice(0, 3);
if (selected.length === 0) {
return new OperationResult<string[]>(true, '未选择照片,可继续填写', []);
}
return new OperationResult<string[]>(true, `已选择 ${selected.length} 张照片`, selected);
} catch (error) {
return new OperationResult<string[]>(
false,
'系统图片选择暂不可用,请稍后重试',
undefined,
ErrorCategory.PLATFORM
);
}
}
}
用户主动取消不是错误。返回 success=true 和空数组后,表单可以保留原有图片并继续填写。真正的平台异常才返回 PLATFORM。
五、页面只消费 UI 所需信息
页面拿到结果后,处理逻辑非常稳定:
const result = await reportService.createReport(draft);
if (!result.success || !result.data) {
this.errorMessage = result.userMessage;
this.submitting = false;
return;
}
this.submitting = false;
this.pathStack.pushPathByName(
AppRoute.PUBLISH_SUCCESS,
new PublishSuccessRouteParam(result.data.id)
);
页面只做四件事:
- 维护 loading/submitting;
- 展示
userMessage; - 使用类型化
data; - 成功后导航或刷新。
页面不会拼接数据库错误,也不会根据 SQL 文本决定下一步。
六、部分成功应该怎么表达
跨 Repository 协调时,可能出现“主动作成功,副作用失败”。例如拒绝认领已保存,但物品状态恢复为 OPEN 失败。
项目不会吞掉这种情况,而是返回带数据的失败结果:
return new OperationResult<ClaimRecord>(
false,
'申请已拒绝,但物品状态恢复失败,请重新进入后检查',
result.data,
ErrorCategory.STORAGE
);
这比简单回滚 UI 更诚实。页面可以提示用户重新进入检查,同时保留已经发生的状态变更。正式联网系统还应配合事务、补偿任务和审计记录。
七、用户文案和内部日志必须分离
userMessage 只能包含用户可理解且安全的信息。内部异常应由安全日志记录,但日志同样不能包含:
- 私密核验答案;
- 手机号、证件号、精确位置;
- 签名口令、Token 和证书路径;
- 完整 Agent Prompt;
- 用户真实照片路径。
例如数据库抛出 duplicate column name,页面只需要显示“数据初始化失败,请稍后重试”。开发日志可以记录错误分类和安全摘要,但不应把完整记录内容序列化出来。
八、当前模型的不足与演进方向
当前枚举只有 VALIDATION、STORAGE、PLATFORM 和 UNKNOWN,足够覆盖单机版主要流程。未来引入网络与账号后,应扩展:
NETWORK 网络不可用、超时、服务端失败
AUTH 未登录、会话过期、无权限
UNSUPPORTED 设备、系统版本或地区不支持
CONFLICT 版本冲突、重复提交、状态已变化
还可以增加内部 errorCode,用于监控和测试,但不要把底层异常原文直接作为用户文案。
九、怎样验证统一结果模型
至少覆盖这些测试:
- 无效输入返回 VALIDATION 且不写入;
- Repository 异常返回 STORAGE;
- Photo Picker 取消返回成功空数组;
- 平台调用异常返回 PLATFORM;
- 错误结果不携带敏感字段;
- 页面在失败后恢复按钮状态;
- 重试不会重复创建记录;
- 部分成功时给出明确检查路径。
十、本文小结
OperationResult<T> 并不复杂,但它建立了 Page、Service、Repository 和系统能力之间的稳定错误契约。页面只处理用户可见状态,Service 负责业务语义,Adapter 负责平台错误映射,内部异常不会越层泄露。
下一篇将把这些层放到完整调用链中,具体说明为什么 ArkUI 页面不应该直接拥有持久化、匹配和状态机逻辑。
系列导航:第 4 篇 / 共 50 篇。上一篇:《NavPathStack 路由实战》;下一篇:《Page → Service → Repository 分层实战》。
更多推荐



所有评论(0)