第3.7篇:网络请求封装——Service 层架构设计

系列:HarmonyOS 从入门到实践 · 画伴梦工厂实战
难度:⭐⭐ 进阶
前置知识:第 3.1 篇 ~ 第 3.6 篇(HTTP 网络请求、文生图、图生视频、图片压缩、图像识别、JSON 序列化)
涉及源文件products/default/src/main/ets/services/AIGenerationService.etsproducts/default/src/main/ets/services/ImageRecognitionService.ets


概述

在"画伴梦工厂"中,我们对接了多个 AI 服务:火山引擎 Seedream 文生图、Seedance 图生视频、GPT-4o-mini 图像识别等。每个服务都有独立的 API 地址、鉴权方式、请求/响应结构和超时要求。如果将网络请求逻辑散落在页面组件中,代码将迅速膨胀、难以维护。

项目的解决方案是 Service 层抽象——将所有网络请求封装在独立的 Service 类中,页面层只关心"调用哪个方法、传入什么参数、拿到什么结果",完全不需要了解 HTTP 细节。

本文将深入分析项目中三个 Service 类的架构设计,涵盖静态方法模式、统一错误处理、超时与重试策略、日志埋点、Context 管理等核心设计模式。


一、Service 架构全景

项目中设计了三个典型的 Service 类,分别应对不同复杂度的网络请求场景:

Service 类 职责 复杂度 源码行数
AIGenerationService 文生图 + 图生视频全流程 ⭐⭐⭐ 高 ~900 行
ImageRecognitionService 图片识别(单次请求) ⭐⭐ 中 ~430 行
ImageUploadService 图片上传(存根模式) ⭐ 低 ~10 行

三个 Service 遵循统一的设计范式:

┌─────────────────────────────────────────┐
│             页面层(Page/Component)       │
│  await AIGenerationService.generateImage() │
│  await ImageRecognitionService.recognize() │
└─────────────────┬───────────────────────┘
                  │ 调用静态方法
                  ▼
┌─────────────────────────────────────────┐
│            Service 层                     │
│  ┌─────────────────────────────────────┐ │
│  │  静态方法入口(public static)        │ │
│  │  ↓                                  │ │
│  │  私有辅助方法(private static)       │ │
│  │  ↓                                  │ │
│  │  HTTP 请求(http.createHttp)        │ │
│  │  ↓                                  │ │
│  │  响应解析与规范化                     │ │
│  │  ↓                                  │ │
│  │  统一错误处理 + 日志                  │ │
│  └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘

所有方法均为 static,Service 类无实例、无状态,本质上是纯函数集合。


二、静态方法模式:无状态纯函数设计

2.1 为什么全部用静态方法?

项目中所有 Service 类的方法都是 static 的,这不是偶然,而是一种深思熟虑的设计选择:

export class AIGenerationService {
  // 所有公开方法均为静态
  static async generateImage(prompt: string): Promise<GeneratedImage> { ... }
  static async generateVideo(imageUrl: string, prompt: string, onStatus?: (message: string) => void): Promise<GeneratedVideo> { ... }
  static async transcribeAudio(filePath: string): Promise<string> { ... }

  // 私有辅助方法同样为静态
  private static async createSeedreamImage(prompt: string): Promise<string> { ... }
  private static async pollImg2VideoTask(taskId: string, onStatus?): Promise<string> { ... }
  private static async downloadVideo(remoteUrl: string, taskId: string): Promise<string> { ... }
}

静态方法模式的优势:

优势 说明
无须实例化 调用方不需要 new Service(),直接 Service.method()
无副作用 所有输入都通过参数传递,没有成员变量污染
天然线程安全 没有共享的可变状态,多线程调用无竞态风险
树摇友好 编译时可以安全地移除未使用的静态方法
调用简洁 页面代码更加干净,减少 import 负担

2.2 与实例化模式对比

如果采用实例化模式,代码会变成:

// ❌ 实例化模式——不必要地增加了复杂度
const service = new AIGenerationService();
service.setApiKey(ARK_API_KEY);
const result = await service.generateImage(prompt);

// ✅ 静态方法模式——简洁、确定性强
const result = await AIGenerationService.generateImage(prompt);

当 Service 不持有任何运行时状态时(所有配置都是编译时常量),实例化只会增加心智负担。

2.3 纯函数方法链

在 AIGenerationService 中,整个图生视频流程通过方法链串联:

static async generateVideo(imageUrl: string, prompt: string, onStatus?): Promise<GeneratedVideo> {
  // 1. 准备上传用的 Base64
  const uploadBase64 = await AIGenerationService.prepareUploadBase64(imageUrl, onStatus);
  // 2. 创建视频生成任务
  const task = await AIGenerationService.createImg2VideoTask(uploadBase64);
  // 3. 轮询任务状态
  const remoteVideoUrl = await AIGenerationService.pollImg2VideoTask(task.taskId, onStatus);
  // 4. 下载到本地
  const localUri = await AIGenerationService.downloadVideo(remoteVideoUrl, task.taskId);
  // 5. 组装结果
  return { prompt, videoUri: localUri, taskId: task.taskId, remoteVideoUrl };
}

每一步都是独立的纯函数,输入输出明确,可以单独测试、替换或添加中间处理环节。


三、统一错误处理设计

3.1 getErrorMessage 工具方法

项目中两个主要 Service 都定义了 getErrorMessage 方法,用于将 Error 对象安全地转换为可读字符串:

// AIGenerationService
static getErrorMessage(error: Error): string {
  if (error && error.message && error.message !== '') {
    return error.message;
  }
  return JSON.stringify(error);
}

// ImageRecognitionService(同样的实现)
static getErrorMessage(error: Error): string {
  if (error && error.message && error.message !== '') {
    return error.message;
  }
  return JSON.stringify(error);
}

这个方法的必要性在于:ArkTS 中 catch 到的 error 可能是任意类型(字符串、对象、或者 Error 实例),直接访问 .message 可能为 undefined。统一方法保证了所有错误信息都能被安全序列化。

3.2 多层 try-catch 边界

项目中采用了防御性 try-catch 策略,在关键边界处捕获异常:

// 外层边界——调用方捕获
static async generateImage(prompt: string): Promise<GeneratedImage> {
  const remoteUrl = await AIGenerationService.createSeedreamImage(enrichedPrompt);
  // 没有在此层 try-catch,让异常透传
}

// 中层边界——内部操作失败后的降级
private static async prepareUploadBase64(imageUri: string, onStatus?): Promise<string> {
  try {
    const compressedBuffer = await AIGenerationService.compressImageBuffer(sourceBuffer);
    // 压缩成功,返回压缩后的 Base64
    return compressedBase64;
  } catch (error) {
    // 压缩失败,降级为原图
    onStatus?.('图片压缩未完成,使用原图继续上传');
    const sourceBase64 = AIGenerationService.arrayBufferToBase64(sourceBuffer);
    return sourceBase64; // 降级返回
  }
}

// 内层边界——记录日志后重新抛出
private static async createImg2VideoTask(base64: string): Promise<GeneratedVideoTask> {
  try {
    // ... 执行网络请求
  } catch (error) {
    const safeError = error as Error;
    AIGenerationService.logError('createImg2VideoTask failed: ' +
      AIGenerationService.getErrorMessage(safeError));
    throw safeError; // 重新抛出,让上层处理
  } finally {
    request.destroy();
  }
}

三层边界的职责:

层次 职责 示例
外层 透传异常,让调用方决定如何处理 generateImage 不 catch
中层 降级处理,提供备用方案 prepareUploadBase64 压缩失败用原图
内层 日志记录 + 资源清理 createImg2VideoTask catch + finally

3.3 服务降级:getFallbackResult

ImageRecognitionService 还实现了降级响应机制:

static getFallbackResult(): DrawingRecognitionResult {
  return {
    protagonist: '小恐龙',
    scene: '草地和太阳',
    emotion: '快乐探险',
    animationSuggestion: '跳跃、摇尾、看向太阳',
    summary: '识别到儿童手绘中的主角、自然场景和明亮情绪...',
    elements: [
      { name: '小恐龙', confidence: 96, color: '#D8F7EA' },
      { name: '太阳', confidence: 91, color: '#FFF0DD' }
    ]
  };
}

当网络不可用或模型返回异常时,调用方可以选择使用降级结果,确保用户的创作流程不被中断——即使识别不是 100% 准确,也比直接崩掉要好。


四、超时配置策略

4.1 connectTimeout vs readTimeout

项目中每一个 HTTP 请求都显式配置了超时参数,这是网络请求中最容易被忽视但又最关键的安全机制:

// AIGenerationService 中的典型配置
const response = await request.request(url, {
  method: http.RequestMethod.POST,
  expectDataType: http.HttpDataType.STRING,
  connectTimeout: 30000,    // 30 秒连接超时
  readTimeout: 90000        // 90 秒读取超时
});
参数 含义 防护场景
connectTimeout 建立 TCP 连接的最大等待时间 DNS 解析失败、服务器不可达、防火墙拦截
readTimeout 从连接建立到接收到完整响应的最大等待时间 服务器处理慢、响应体过大、网络抖动

4.2 按服务精细调优

项目中针对不同 API 的特性,配置了差异化的读取超时:

// 常量定义(AIGenerationService)
const SEEDANCE_CREATE_TIMEOUT_MS: number = 180000;   // 3 分钟——任务创建
const SEEDANCE_QUERY_TIMEOUT_MS: number = 240000;    // 4 分钟——状态查询
const SEEDANCE_DOWNLOAD_TIMEOUT_MS: number = 240000; // 4 分钟——视频下载
const POLL_INTERVAL_MS: number = 5000;                // 5 秒——轮询间隔
服务 connectTimeout readTimeout 原因
Seedream 文生图 30s 90s 图片生成通常较快
Seedance 任务创建 30s 180s 大文件上传耗时较长
Seedance 状态查询 30s 240s 轮询等待时间长
Seedance 视频下载 30s 240s 视频文件体积大
GPT-4o-mini 识别 30s 60s 小图片识别响应快

ImageRecognitionService 的配置更激进:

connectTimeout: 30000,  // 30 秒
readTimeout: 60000,     // 60 秒

因为图片识别请求体小(压缩后约 500KB),模型响应通常在十几秒内返回,60 秒的超时已经足够。

核心原则:超时不是越长越好。过短的超时会导致正常请求失败,过长的超时会让用户等待太久。最佳实践是根据每个 API 的 P99 响应时间 + 安全余量来配置。


五、重试策略设计

5.1 canRetryTaskQuery:区分可重试与不可重试错误

在轮询任务状态的场景中,AIGenerationService 实现了一套精细的重试逻辑。其核心是 canRetryTaskQuery 方法:

static canRetryTaskQuery(message: string): boolean {
  return message.indexOf('400') < 0 && message.indexOf('401') < 0 &&
    message.indexOf('403') < 0 && message.indexOf('404') < 0;
}

这个方法的判断逻辑基于 HTTP 状态码语义:

状态码 含义 可重试? 原因
400 Bad Request ❌ 否 请求参数有误,重试同样会失败
401 Unauthorized ❌ 否 鉴权失败,需要更新密钥
403 Forbidden ❌ 否 权限不足,重试无意义
404 Not Found ❌ 否 资源不存在,重试也是徒劳
其他 网络异常/5xx ✅ 是 可能是临时性故障

5.2 轮询中的重试循环

pollImg2VideoTask 方法中,重试逻辑与轮询机制深度集成:

static async pollImg2VideoTask(taskId: string, onStatus?: (message: string) => void): Promise<string> {
  const startedAt = Date.now();
  let attempt = 0;
  let lastErrorMessage = '';

  while (Date.now() - startedAt < MAX_SEEDANCE_WAIT_MS) {
    attempt++;
    onStatus?.('正在等待动画生成,第 ' + attempt.toString() + ' 次检查');

    try {
      responseBody = await AIGenerationService.queryImg2VideoTask(taskId);
    } catch (error) {
      lastErrorMessage = AIGenerationService.getErrorMessage(error as Error);
      // 检查错误是否可重试
      if (!AIGenerationService.canRetryTaskQuery(lastErrorMessage)) {
        throw new Error(lastErrorMessage); // 不可重试,立即终止
      }
      onStatus?.('网络有点慢,继续等待动画完成');
      await AIGenerationService.sleep(POLL_INTERVAL_MS); // 5秒后重试
      continue;
    }

    // 检查任务是否完成
    if (responseBody.status === 1 && videoUrl !== '') {
      return videoUrl;
    }

    await AIGenerationService.sleep(POLL_INTERVAL_MS);
  }
  throw new Error('视频生成超时');
}

重试策略要点:

  1. 超时兜底MAX_SEEDANCE_WAIT_MS = 6 * 60 * 1000(6 分钟),超过总时长直接抛出超时异常
  2. 指数退避的替代方案:固定 5 秒间隔(POLL_INTERVAL_MS)——对于 AI 任务来说,5 秒的轮询间隔已经足够,不需要指数退避
  3. 累计重试:只在请求抛出异常时消耗重试次数,正常返回(无论任务是否完成)不计入重试
  4. 用户进度感知:每次重试都通过 onStatus 回调通知用户,让用户知道"还在进行中"

5.3 另一种重试思路

在 ImageRecognitionService 中,没有实现显式的重试循环,而是采用了一层降级容错

try {
  return ImageRecognitionService.normalizeModelResult(responseBody.choices[0].message.content);
} catch (error) {
  throw new Error('模型结果不是约定 JSON:' + ImageRecognitionService.getErrorMessage(error as Error));
}

这里的选择是"快速失败"——因为 AI 模型的结果不符合预期 JSON 格式,重试很可能得到同样的结果。此时应该通知用户或展示降级结果,而不是盲目重试。


六、日志埋点基础设施

6.1 统一的日志常量

AIGenerationService 中定义了日志基础设施常量:

const LOG_DOMAIN: number = 0x0000;   // 日志域——0x0000 是系统保留域,生产项目应使用自定义域
const LOG_TAG: string = 'AIGenerationService';  // 日志标签,用于分类过滤
const LOG_CHUNK_SIZE: number = 3000; // 单条日志最大字符数

6.2 日志分级方法

项目封装了三个层级的日志方法:

private static logInfo(message: string): void {
  hilog.info(LOG_DOMAIN, LOG_TAG, '%{public}s', message);
}

private static logError(message: string): void {
  hilog.error(LOG_DOMAIN, LOG_TAG, '%{public}s', message);
}

%{public}s 是 hilog 的隐私标记语法。在 HarmonyOS 中,日志默认会过滤掉隐私数据,使用 %{public}s 明确标记此参数为公开可记录。

方法 日志级别 使用场景
logInfo info 正常流程的关键节点(请求开始、任务创建、文件保存)
logError error catch 块中记录错误详情

6.3 大负载日志分段:logLong

网络请求的日志往往包含超大的请求/响应体(如 Base64 编码的图片数据),直接输出到日志系统会被截断或导致性能问题。logLong 方法将大字符串分段输出:

private static logLong(label: string, value: string): void {
  if (value.length <= LOG_CHUNK_SIZE) {
    AIGenerationService.logInfo(label + '=' + value);
    return;
  }
  const total = Math.ceil(value.length / LOG_CHUNK_SIZE);
  AIGenerationService.logInfo(label + ' length=' + value.length.toString() +
    ', chunks=' + total.toString());
  for (let i = 0; i < total; i++) {
    const start = i * LOG_CHUNK_SIZE;
    const end = Math.min(start + LOG_CHUNK_SIZE, value.length);
    AIGenerationService.logInfo(label + ' part ' + (i + 1).toString() +
      '/' + total.toString() + ': ' + value.substring(start, end));
  }
}

6.4 敏感数据截断:summarizeValue

在记录图片 URI 或文件路径时,直接输出完整路径可能导致日志过于冗长或泄漏信息。summarizeValue 方法提供了安全的截断显示:

private static summarizeValue(value: string): string {
  if (value.length <= 160) {
    return value;
  }
  return value.substring(0, 80) + '...' + value.substring(value.length - 40);
}

输出效果示例:

// 原始路径(很长)
file://data/storage/el2/base/haps/entry/files/seedance_abc123_very_long_image_name.jpg
// 截断后
file://data/storage/el2/base/haps/entry/files/seedance_abc123_..._long_image_name.jpg

6.5 全链路日志示例

在实际运行中,一次完整的 generateVideo 调用会产生如下的日志序列:

[AIGenerationService] generateVideo start imageUri=file://.../photo.jpg, promptLength=12
[AIGenerationService] prepareUploadBase64 start imageUri=file://.../photo.jpg
[AIGenerationService] readImageAsArrayBuffer source=local uri=file://.../photo.jpg
[AIGenerationService] prepareUploadBase64 sourceBytes=3485722
[AIGenerationService] prepareUploadBase64 compressedBytes=892513
[AIGenerationService] createImg2VideoTask request url=http://..., base64Length=1189809
[AIGenerationService] createImg2VideoTask response: 200, taskId=task_12345
[AIGenerationService] generateVideo task created taskId=task_12345
[AIGenerationService] pollImg2VideoTask attempt 1 status=0
[AIGenerationService] pollImg2VideoTask attempt 2 status=1, videoUrl=http://...
[AIGenerationService] generateVideo remote video ready taskId=task_12345
[AIGenerationService] downloadVideo bytes=4567890
[AIGenerationService] downloadVideo saved path=file://.../seedance_task_12345.mp4
[AIGenerationService] generateVideo success taskId=task_12345

这样的日志埋点让开发者和运维人员可以精确追踪每个请求的全生命周期,快速定位问题环节。


七、Context 获取与管理

7.1 getContext() 的妙用

在 Service 层中,有时需要获取应用上下文来访问文件系统。ArkTS 提供了全局 getContext() 方法:

private static async downloadVideo(remoteUrl: string, taskId: string): Promise<string> {
  // ...
  const context = getContext() as common.UIAbilityContext;
  const path = context.filesDir + '/seedance_' + taskId + '.mp4';
  // ...
}

getContext() 返回的是 common.UIAbilityContext,它提供了:

  • filesDir:应用沙箱文件目录路径
  • cacheDir:缓存目录路径
  • tempDir:临时目录路径
  • bundleCodeDir:应用代码目录路径

7.2 为什么在静态方法中也能获取 Context?

在 HarmonyOS 中,getContext() 是一个全局函数,不依赖于组件实例。这意味着即使是在静态方法中,也能获取到正确的 UIAbilityContext。

// 在 @Component 中
const ctx = getContext(this);  // 传入组件实例

// 在 Service 静态方法中
const ctx = getContext();      // 不需要参数——自动获取当前 AbilityContext

7.3 沙箱文件操作模式

获得 Context 后,Service 层通过 filesDir 构建文件路径,然后在沙箱内完成文件读写:

// 1. 构建沙箱路径
const path = context.filesDir + '/seedance_' + taskId + '.mp4';

// 2. 创建文件并写入
const file = fileIo.openSync(path, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
try {
  fileIo.writeSync(file.fd, videoBuffer);
} finally {
  fileIo.closeSync(file);
}

// 3. 返回可访问的 file:// URI
return 'file://' + path;

这种模式下,Service 层不关心文件最终去哪里,只负责将数据可靠地写入沙箱。外部调用方可以通过 file:// URI 在 UI 层展示或导出。


八、Callback 模式:onStatus 进度通知

8.1 设计动机

网络请求是异步的,尤其是在图生视频这种耗时数分钟的任务中,用户需要知道当前进度。项目中定义了一个可选的 onStatus 回调参数:

static async generateVideo(
  imageUrl: string,
  prompt: string,
  onStatus?: (message: string) => void  // 可选的回调
): Promise<GeneratedVideo>

8.2 回调驱动的进度传达

generateVideo 方法中,onStatus 回调在多个关键节点被调用:

阶段 回调消息 位置
图片压缩 '正在压缩图片' prepareUploadBase64
图片上传 '图片已压缩,正在上传' prepareUploadBase64
任务提交 '任务已提交,正在生成动画' generateVideo
轮询中 '正在等待动画生成,第 N 次检查' pollImg2VideoTask
网络抖动 '网络有点慢,继续等待动画完成' pollImg2VideoTask(catch 中)
视频就绪 '动画已生成,正在保存到本地' generateVideo

8.3 类型安全调用

ArkTS 中调用可选回调的最佳实践是使用可选链操作符 ?.

// ✅ 推荐——简洁且类型安全
onStatus?.('任务已提交,正在生成动画');

// ❌ 不推荐——需要类型守卫
if (onStatus) {
  onStatus('任务已提交,正在生成动画');
}

8.4 页面层的消费模式

在页面层,回调被绑定到 @State 状态变量上,驱动 UI 更新:

// RecognitionWaitingPage.ets
@State private statusText: string = '正在准备生成任务';

private async startGeneration() {
  const result = await AIGenerationService.generateVideo(
    this.imageUri,
    this.prompt,
    (message: string) => {
      this.statusText = message;  // 每步更新状态文本
    }
  );
  // 生成完成后跳转到结果页
}

这种方式实现了 Service 层与 UI 层的解耦——Service 层不需要知道任何 UI 细节,只需要发送字符串消息;页面层决定如何展示这些消息。


九、三个 Service 的架构对比

维度 AIGenerationService ImageRecognitionService ImageUploadService
复杂度 高(~900 行) 中(~430 行) 低(~13 行)
HTTP 请求数 多(创建/查询/下载) 1 次 0 次(存根)
图像处理 压缩 + Base64 压缩 + Base64
轮询机制 ✅ 有(setInterval 模拟) ❌ 无 ❌ 无
降级策略 Mock 数据 + fallback getFallbackResult 本地 URI 回退
错误重试 canRetryTaskQuery 快速失败 N/A
进度通知 onStatus 回调
日志埋点 详细(info + error + logLong)
Mock 模式 USE_MOCK_GENERATION 开关

9.1 模式演进的启示

从三个 Service 可以看到 Service 层架构的演进路径:

ImageUploadService(存根)→ ImageRecognitionService(单次请求)
→ AIGenerationService(多步骤编排 + 轮询 + 重试 + 日志)
  1. 存根模式:先定义接口签名,返回模拟数据,让前端开发不 blockade
  2. 单次请求:加入真实的 HTTP 调用、请求体构建、响应解析
  3. 多步骤编排:将多个 API 组合成完整的业务流,加入重试、超时、日志等企业级能力

十、ArkTS Service 层设计最佳实践

10.1 设计原则速查

┌─────────────────────────────────────────────────────┐
│          ArkTS Service 层设计清单                     │
├─────────────────────────────────────────────────────┤
│ ☑ 所有方法声明为 static——无状态、无实例化             │
│ ☑ 每个方法职责单一——一个方法只做一件事                │
│ ☑ 输入输出都用 interface 定义——类型安全               │
│ ☑ 每个 HTTP 请求显式配置 connectTimeout + readTimeout │
│ ☑ 关键边界加 try-catch——防止未捕获异常向上传播        │
│ ☑ 异常信息用 getErrorMessage 安全序列化               │
│ ☑ finally 中调用 request.destroy()——资源不会泄漏      │
│ ☑ 长时间任务提供 onStatus 回调——让 UI 层感知进度       │
│ ☑ 关键节点打日志——便于调试和运维                      │
│ ☑ 大负载日志分段输出——避免日志截断或性能问题           │
│ ☑ 可重试 / 不可重试错误区分——避免无效重试              │
│ ☑ 提供降级方案——网络失败时不阻断用户流程               │
└─────────────────────────────────────────────────────┘

10.2 request.destroy() 的重要性

每个使用 http.createHttp() 的请求,都必须确保在 finally 块中调用 request.destroy()

const request = http.createHttp();
try {
  // ... 执行请求
  const response = await request.request(url, options);
  // ... 处理响应
} finally {
  request.destroy(); // 必须——释放 HTTP 会话资源
}

如果不调用 destroy(),HTTP 会话会持续占用内存和 socket 资源,极端情况下可能导致应用层 OOM 或达到系统连接数上限。

10.3 类型安全:用 interface 定义每一个数据模型

Service 层中所有的请求体和响应体都应通过 interface 明确声明,这不仅是 ArkTS 编译器的要求,也是工程质量的保障:

// AIGenerationService 中的请求/响应接口
interface ImageGenerationRequest {
  model: string;
  prompt: string;
  response_format: string;
  size: string;
  guidance_scale: number;
  watermark: boolean;
}

interface ImageGenerationResponse {
  data?: ImageGenerationData[];
  error?: SeedanceTaskError;
}

interface Img2VideoStatusResponse {
  sdUrl?: string;
  stat?: string;
  status?: number;
  url?: string;
  error?: SeedanceTaskError;
  message?: string;
}

值得注意的设计细节:

  • 可选属性用 ? 标记:API 响应的字段可能缺失,可选属性让类型定义更准确
  • 嵌套 interface 结构化ImageGenerationResponse.data[].url 通过两层接口精确定位
  • 联合类型处理多态响应(SeedanceContentText | SeedanceContentImage)[] 精确描述数组内容

10.4 常量管理

网络请求相关的配置(URL、Key、超时阈值)集中定义为模块级常量,便于维护和修改:

const ARK_API_URL: string = 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks';
const ARK_IMAGE_API_URL: string = 'https://ark.cn-beijing.volces.com/api/v3/images/generations';
const MAX_IMAGE_BYTES: number = 12 * 1024 * 1024;         // 12MB
const TARGET_UPLOAD_IMAGE_BYTES: number = 900 * 1024;     // 900KB
const COMPRESSED_IMAGE_QUALITY: number = 78;
const POLL_INTERVAL_MS: number = 5000;                     // 5s
const MAX_SEEDANCE_WAIT_MS: number = 6 * 60 * 1000;       // 6min

这样设计的好处是:

  • 一处修改,全局生效:切换 API 版本只需改一行
  • 语义化命名MAX_IMAGE_BYTES 比魔法数字 12582912 直观得多
  • 方便 Mock 测试:通过 USE_MOCK_GENERATION 常量一键切换模式

总结

本文从"画伴梦工厂"三个真实的 Service 实现出发,深入讲解了网络请求封装的核心设计模式:

设计模式 实现方式 对应代码
静态方法模式 全部方法声明为 static,无实例化 AIGenerationService.generateImage()
统一错误处理 getErrorMessage 安全序列化 + try-catch 三层边界 getErrorMessageprepareUploadBase64
超时策略 按服务精细调优 connectTimeout / readTimeout 30s~240s 分级配置
重试策略 canRetryTaskQuery 区分 4xx 与其他错误 pollImg2VideoTask 轮询循环
日志埋点 hilog 分级 + summarizeValue 截断 + logLong 分段 logInfologErrorlogLong
Context 管理 getContext() 获取 filesDir 进行沙箱文件操作 downloadVideo
进度通知 可选 onStatus 回调 + 可选链调用 generateVideo(onStatus?)
降级处理 Mock 数据 / Fallback 结果,保证流程不中断 USE_MOCK_GENERATIONgetFallbackResult

Service 层的核心设计哲学可以概括为三句话:

无状态是王道——所有方法都是静态的,没有成员变量,没有实例化开销。

边界清晰是基石——错误处理、资源清理、日志记录都在正确的位置。

可观测性是保障——每一步都有日志、每个错误都有兜底、每个长时间操作都有进度反馈。

下一篇: 第 3.8 篇将介绍图片上传服务设计——使用存根模式(Stub Pattern)实现可替换的服务架构,让前端开发不依赖后端即可完成全流程联调。


参考源码

本文所有代码均来自项目文件:

  • products/default/src/main/ets/services/AIGenerationService.ets — AI 生成服务,涵盖文生图、图生视频、语音转写,约 900 行
  • products/default/src/main/ets/services/ImageRecognitionService.ets — 图片识别服务,对接 GPT-4o-mini,约 430 行
  • products/default/src/main/ets/services/ImageUploadService.ets — 图片上传服务(存根模式),约 13 行
Logo

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

更多推荐