【寻迹校园 HarmonyOS NEXT 实战 04】ArkTS 统一结果模型:OperationResult 如何收敛错误

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

OperationResult 统一结果模型原创概念图

上图为本文原创生成的统一结果模型概念图:不同来源的数据先经过同一契约归一化,再明确分流为成功值或结构化错误。它展示的是状态语义,不是项目界面截图。

一、直接 throw 给页面会发生什么

在页面数量很少时,我们可能会这样写:

try {
  await repository.insert(record);
  this.message = '保存成功';
} catch (error) {
  this.message = `${error}`;
}

这种写法有三个风险:

  1. 页面知道了 Repository 的实现细节;
  2. 内部异常原文可能包含 SQL、路径或敏感字段;
  3. 每个页面会写出不同的错误文案、重试和 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 分层实战》。

Logo

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

更多推荐