一、企业场景与挑战

在这里插入图片描述

1.1 现场取证的能力边界问题

在实际部署中,相机的自动构图(AUTO_FRAMING)能力往往存在设备差异:

  • 高端设备:支持连续自动对焦 + 自动构图控制
  • 中端设备:支持连续自动对焦,但无 AUTO_FRAMING 接口
  • 低端或特殊设备:可能无后摄、无合适输出 Profile、甚至权限被禁用

核心挑战:不能因为一个功能不可用就导致整个取证流程中断。必须在以下两点间找到平衡:

  1. 完整性:记录下能力检查的真实结论(什么不支持、为什么不支持)
  2. 可用性:为现场人员提供人工取景和附件交接的替代方案

1.2 为什么不能简单回退到"提示用户补充附件"

许多实现采用简单的处理方式:

检查能力 → 不支持 → 提示用户"请手动补充附件" → 结束

这种方式的问题在于:

  1. 丢失能力事实:系统无法证明"自动构图确实不可用",只能说"用户没有上传"
  2. 流程中断风险:现场人员可能无法准确理解为什么要人工补充,导致应急流程中的时间浪费
  3. 后续追溯困难:审计链上无法区分"主动选择人工取证"和"被迫降级"
  4. 复拍时限管理失效:无法准确指派下一责任人和时限,影响后续工作的接续

1.3 企业实施的真实需求

现场取证必须满足以下要求:

  1. 能力事实记录:清晰地记下"AUTO_FRAMING 不支持"、“权限被拒绝"还是"设备不可用”
  2. 人工取景的正当性:不是因为系统故障而被迫人工取景,而是基于真实能力检查的业务路由
  3. 附件交接的完整性:包括拍摄时间、附件路径、复拍时限、下一责任人等关键信息
  4. 异常恢复的可能性:如果网络恢复或权限重新授予,能否重新启动自动构图流程
  5. 审计链的连续性:从能力检查 → 回退判断 → 人工拍摄 → 附件交接 → 复拍指派,完整可追溯

在这里插入图片描述

二、核心技术概念与设计思路

2.1 相机能力检查的关键节点

HarmonyOS 6.1.1 中,相机自动构图能力的检查需要覆盖以下层面:

层级检查项API失败路由
设备层后摄是否可用manager.getSupportedCameras()camera_unavailable
输出能力层预览/拍照 Profile 是否可用manager.getSupportedOutputCapability()camera_unavailable
权限层CAMERA 权限是否授予abilityAccessCtrl.requestPermissionsFromUser()permission_denied
会话层PhotoSession 是否能创建manager.createSession()session_failed
焦点层连续自动对焦是否支持session.isFocusModeSupported()(可降级)
构图控制层AUTO_FRAMING 接口是否存在调用后判断unsupported

关键发现:HarmonyOS 6.1.1 中,AUTO_FRAMING 并非通过 isFocusModeSupported() 这样的明确 API 暴露。通常的做法是:

  • 尝试在 PhotoSession 中查找该控制接口
  • 如果接口不存在或调用失败,则标记为 unsupported
  • 此时必须切换到人工构图模式

2.2 回退原因的五层分类

根据能力检查的结果,系统需要明确判断回退原因,并为不同原因设置不同的后续路由:

type FallbackReason = 'waiting' | 'unsupported' | 'session_failed' | 'permission_denied' | 'camera_unavailable';

各回退原因的含义与路由

  1. unsupported - AUTO_FRAMING 控制接口不存在

    • 能力结论:PhotoSession 已成功创建,但不提供自动构图接口
    • 后续路由:启用人工构图取证
    • 复拍策略:可以在后续强制升级或重新检查
  2. session_failed - PhotoSession 创建或启动异常

    • 能力结论:相机硬件或会话层故障
    • 后续路由:可选人工附件,但不启动预览
    • 复拍策略:设置较长时限,等待设备恢复
  3. permission_denied - CAMERA 权限被拒绝或权限请求失败

    • 能力结论:用户未授权或应用无权访问相机
    • 后续路由:允许人工附件,但建议在系统设置中重新授权
    • 复拍策略:权限恢复后可重新尝试
  4. camera_unavailable - 后摄设备或输出 Profile 不可用

    • 能力结论:硬件或固件限制
    • 后续路由:允许人工附件,标记为"设备不支持"
    • 复拍策略:可能需要更换设备
  5. waiting - 尚未进行能力检查或检查进行中

    • 能力结论:未知
    • 后续路由:不允许进入人工取景流程
    • 复拍策略:用户必须先完成能力检查

2.3 人工构图与自动构图的本质区别

在这里插入图片描述

错误理解:只要用户点了"拍摄"按钮就叫"人工构图"

正确理解:人工构图是指基于真实能力检查结论而启动的、不依赖 AUTO_FRAMING 的取证流程

自动构图流程:
  能力检查(有AUTO_FRAMING) → 启用控制 → 系统自动调整 → 拍摄

人工构图流程:
  能力检查(无AUTO_FRAMING) → 显示取景框指导 → 用户手动调整 → 拍摄

关键差异

维度自动构图人工构图
前置条件AUTO_FRAMING 接口存在AUTO_FRAMING 不存在
控制权系统根据算法自动调整用户根据视觉反馈调整
预期质量系统担保最优构图用户根据经验自行判断
审计记录记录系统决策参数记录用户操作时长和快门次数
后续责任系统承担质量责任用户承担质量责任
复拍触发系统判断不合格自动重试用户主动判断是否重拍

2.4 人工附件交接的完整信息结构

人工附件交接不仅要记录"拍了一张照片",还需要包含完整的业务上下文:

interface HandoverRecord {
  // 附件基本信息
  attachmentPath: string;           // 本地附件路径
  attachmentSize: number;           // 文件大小
  captureTime: string;              // 拍摄时间戳
  
  // 能力事实
  fallbackReason: FallbackReason;   // 回退原因
  autoFramingSupported: boolean;    // AUTO_FRAMING 是否支持
  focusModeSupported: boolean;      // 焦点模式是否支持
  
  // 复拍管理
  retakeDeadline: string;           // 复拍时限(通常24小时内)
  responsiblePerson: string;        // 下一责任人
  handoverTime: string;             // 交接登记时间
  
  // 审计链
  workOrderId: string;              // 工单编号
  operatorId: string;               // 操作员ID
  batchId: string;                  // 批次ID(用于关联多个附件)
}

三、完整的状态机设计

3.1 能力检查阶段的状态流转

┌─────────────────┐
│  waiting_surface │  初始状态,等待 Surface 就绪
└────────┬────────┘
         │ Surface.onLoad()
         ▼
┌────────────────────┐
│requesting_permission│ 发起权限请求
└────────┬───────────┘
         │ 用户授权或拒绝
         ├─────────────────────────────┐
         │ (granted)                   │ (denied)
         ▼                             ▼
┌──────────────────────┐   ┌───────────────────┐
│querying_capability   │   │unavailable (perm) │
│                      │   │permission_denied  │
└────────┬─────────────┘   └───────────────────┘
         │ getSupportedCameras()
         ├────────────────────────┐
         │ (device found)         │ (not found)
         ▼                        ▼
┌──────────────────────┐  ┌──────────────────┐
│(device confirmed)    │  │unavailable (dev) │
│getSupportOutputCap() │  │camera_unavailable│
└────────┬─────────────┘  └──────────────────┘
         │ (profile found)
         ▼
┌──────────────────────┐
│starting_preview      │  创建 PhotoSession
│createSession()       │  添加输入输出
└────────┬─────────────┘  commitConfig()
         │ commitConfig() success
         ├────────────────────────────────┐
         │ (success)                      │ (error)
         ▼                                ▼
┌──────────────────────┐  ┌──────────────────┐
│checking_autoframing  │  │failed             │
│isFocusModeSupported()│  │session_failed    │
└────────┬─────────────┘  └──────────────────┘
         │ check AUTO_FRAMING interface
         ├────────────────────────┐
         │ (exists)    (not found)│
         ▼                        ▼
┌──────────────┐        ┌─────────────────┐
│start()       │        │unsupported:     │
│previewing    │        │fallback allowed │
└──────────────┘        │previewing       │
                        └─────────────────┘

关键节点的判断逻辑

private async startCameraSession(): Promise<void> {
  // 1. 权限检查
  const granted = await this.requestCameraPermission();
  if (!granted) {
    this.fallbackReason = 'permission_denied';
    this.phase = 'unavailable';
    return;
  }

  // 2. 设备检查
  const manager = camera.getCameraManager(getContext(this));
  const device = this.selectBackCamera(manager.getSupportedCameras());
  if (device === undefined) {
    this.fallbackReason = 'camera_unavailable';
    this.phase = 'unavailable';
    return;
  }

  // 3. 输出能力检查
  const capability = manager.getSupportedOutputCapability(device, camera.SceneMode.NORMAL_PHOTO);
  const previewProfile = this.selectLargestProfile(capability.previewProfiles);
  const photoProfile = this.selectLargestProfile(capability.photoProfiles);
  if (previewProfile === undefined || photoProfile === undefined) {
    this.fallbackReason = 'camera_unavailable';
    this.phase = 'unavailable';
    return;
  }

  // 4. 会话创建
  try {
    this.photoSession = manager.createSession<camera.PhotoSession>(camera.SceneMode.NORMAL_PHOTO);
    this.photoSession.on('error', this.sessionErrorCallback);
    this.photoSession.beginConfig();
    this.photoSession.addInput(this.cameraInput);
    this.photoSession.addOutput(this.previewOutput);
    this.photoSession.addOutput(this.photoOutput);
    await this.photoSession.commitConfig();
  } catch (error) {
    this.fallbackReason = 'session_failed';
    this.phase = 'failed';
    return;
  }

  // 5. 焦点与自动构图能力检查
  this.setupFocusAndFallback(this.photoSession);
  // 这里会检查 AUTO_FRAMING 是否可用
  // 如果不可用,设置 fallbackReason = 'unsupported'
  
  // 6. 启动预览
  await this.photoSession.start();
  this.phase = 'previewing';
}

private setupFocusAndFallback(session: camera.PhotoSession): void {
  // 检查连续自动对焦
  if (session.isFocusModeSupported(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO)) {
    session.setFocusMode(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO);
    this.focusState = '已设置连续自动对焦';
  } else {
    this.focusState = '设备不支持连续自动对焦';
  }

  // 检查 AUTO_FRAMING:如果调用不存在或失败,则判定为不支持
  // HarmonyOS 6.1.1 的 PhotoSession 通常不直接暴露 AUTO_FRAMING 接口
  // 而是在创建会话时就确定了,所以这里标记为不支持并启用人工回退
  this.autoFramingState = '当前 PhotoSession 不提供 AUTO_FRAMING 控制接口,已转人工构图回退';
  this.fallbackReason = 'unsupported';
}

3.2 人工取景阶段的状态流转

┌─────────────────────┐
│previewing           │  能力检查完成,fallbackReason = 'unsupported'
│(fallback allowed)   │  显示取景框指导
└────────┬────────────┘
         │ 用户点击"拍摄人工构图附件"
         ▼
┌─────────────────────┐
│capturing            │  调用 photoOutput.capture()
│createPendingPhoto() │  创建临时文件路径
└────────┬────────────┘
         │ capture() 触发 imageArrival
         │ 接收真实 JPEG 数据
         ▼
┌─────────────────────┐
│writing_to_disk      │  写入本地文件
│fileIo.writeSync()   │
└────────┬────────────┘
         │ write success
         ├──────────────────────┐
         │ (success)            │ (failed)
         ▼                      ▼
┌─────────────────────┐  ┌──────────────┐
│release_session      │  │capture_failed│
│releaseCameraSession │  │cleanup & err │
└────────┬────────────┘  └──────────────┘
         │ release complete
         ▼
┌─────────────────────┐
│review               │  展示拍摄结果
│(pending_photo_path) │  允许重新拍摄或确认
└─────────────────────┘

四、完整的人工附件交接流程实现

4.1 拍摄阶段

关键原则

  1. 只有在 fallbackReason !== 'waiting' 时才允许进入人工拍摄
  2. 拍摄必须基于真实的 PhotoSession 预览,不能是静态图片或示意框
  3. 保存的附件文件必须具有唯一的本地路径标识
private async captureManualEvidence(): Promise<void> {
  // 检查回退条件是否成立
  if (!this.isFallbackAllowed()) {
    this.statusMessage = '人工附件入口未开放:请先完成真实能力检查并取得回退结论';
    return;
  }

  // 确认预览会话仍在运行
  if (this.phase !== 'previewing' || this.photoOutput === undefined) {
    this.statusMessage = '当前没有可用的真实预览,无法生成相机附件';
    return;
  }

  try {
    // 创建本地附件路径,使用时间戳保证唯一性
    this.pendingPhotoPath = this.createPendingPhotoPath();  // ${filesDir}/camera-fallback-${Date.now()}.jpg
    this.phase = 'capturing';
    this.statusMessage = '正在保存人工构图的真实拍摄结果';

    // 触发真实拍摄,指定高质量和当前旋转角度
    await this.photoOutput.capture({
      quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
      rotation: this.photoOutput.getPhotoRotation()
    });
    // capture() 返回后,imageArrival 回调会被触发
  } catch (error) {
    this.failCapture('人工构图拍摄失败', error as Error);
  }
}

private async handleImageArrival(): Promise<void> {
  const receiver = this.imageReceiver;
  if (receiver === undefined || this.pendingPhotoPath === '') return;

  let capturedImage: image.Image | undefined;
  try {
    // 读取最新捕获的图像
    capturedImage = await receiver.readLatestImage();

    // 提取 JPEG 数据
    const component = await capturedImage.getComponent(image.ComponentType.JPEG);

    // 打开本地文件用于写入
    const target = fileIo.openSync(
      this.pendingPhotoPath,
      fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC
    );

    try {
      // 写入 JPEG 数据
      const written = fileIo.writeSync(target.fd, component.byteBuffer);
      if (written <= 0) {
        throw new Error('附件文件未写入有效 JPEG 数据。');
      }
    } finally {
      fileIo.closeSync(target);
    }

    // 文件保存成功,释放相机会话
    await this.releaseCameraSession();
    this.pageMode = 'review';
    this.phase = 'released';
    this.attachmentState = '已保存真实拍摄结果,待人工确认本地附件草稿';
    this.statusMessage = '请复核人工构图结果,再生成本地附件交接记录';
  } catch (error) {
    this.failCapture('人工附件保存失败', error as Error);
  } finally {
    if (capturedImage !== undefined) {
      await capturedImage.release();
    }
  }
}

4.2 复核与交接阶段

关键原则

  1. 复核只是视觉验证,不改变文件本身
  2. 交接记录包含完整的能力事实和复拍管理信息
  3. 本页面不涉及文件上传或工单回写,只负责本地记录的生成
private confirmAttachmentDraft(): void {
  // 验证附件文件确实存在
  if (this.pendingPhotoPath === '') {
    this.statusMessage = '未找到真实拍摄结果,不能生成附件交接记录';
    return;
  }

  // 进入交接模式
  this.pageMode = 'handover';
  this.activeStepIndex = 2;

  // 记录交接的关键时间和状态
  this.attachmentState = '本地人工附件草稿已确认,尚未上传或回写工单';
  this.handoverState = '已登记待复拍交接,请指定现场责任人并在时限内补齐自动构图证据';
  this.handoverTime = timestamp();  // 记录交接时间

  // 生成后续流程的路由信息
  this.statusMessage = '本地交接记录已创建,等待后续复拍和外部回写';
}

交接记录中应该包含但本页面未完全暴露的信息(假设由调用方在上层组件中补充):

// 完整的交接记录结构(示意)
const handoverRecord = {
  // 附件信息
  attachmentPath: this.pendingPhotoPath,
  attachmentSize: fileIo.statSync(this.pendingPhotoPath).size,
  captureTime: timestamp(),

  // 能力事实
  fallbackReason: this.fallbackReason,  // e.g., 'unsupported'
  autoFramingSupported: false,          // 因为 fallbackReason = 'unsupported'
  focusModeSupported: this.focusState.indexOf('已设置') >= 0,

  // 复拍管理
  retakeDeadline: '24 小时内完成补拍',
  responsiblePerson: this.responsiblePerson,  // '现场调度员(待指派)'
  handoverTime: this.handoverTime,

  // 审计链(由上层补充)
  workOrderId: '???',
  operatorId: '???',
  batchId: '???'
};

4.3 复拍与重置流程

关键原则

  1. 复拍意味着撤销当前的附件草稿,重新进入拍摄流程
  2. 重置能力检查意味着从头开始,清空所有状态
  3. 两种操作都必须清理临时文件,防止磁盘泄漏
private retake(): void {
  // 清理之前的临时文件
  this.discardPendingPhoto();

  // 重新进入拍摄模式
  this.pageMode = 'capture';
  this.phase = 'waiting_surface';  // 重置为初始状态,但保留 Surface
  this.surfaceReady = false;

  // 清理附件相关的状态
  this.attachmentState = '已撤销未确认的本地附件草稿';
  this.statusMessage = '请重新启动预览后按人工构图引导拍摄';
  this.errorMessage = '暂无错误';
}

private async resetInspection(): void {
  // 清理临时文件
  this.discardPendingPhoto();

  // 释放相机会话
  await this.releaseCameraSession();

  // 重置所有状态为初始值
  this.pageMode = 'inspect';
  this.phase = 'waiting_surface';
  this.fallbackReason = 'waiting';
  this.permissionState = '未请求';
  this.cameraState = '等待真实后摄查询';
  this.autoFramingState = '等待 PhotoSession 能力检查';
  this.focusState = '等待会话能力检查';
  this.previewState = '等待 Surface';
  this.attachmentState = '尚未生成本地附件草稿';
  this.handoverState = '等待确认回退后登记复拍交接';
  this.handoverTime = '尚未登记';
  this.activeStepIndex = 0;
  this.statusMessage = '本地处置会话已重置,等待新的真实能力检查';
  this.errorMessage = '暂无错误';
}

private discardPendingPhoto(): void {
  if (this.pendingPhotoPath === '') return;

  try {
    // 检查文件是否存在
    if (fileIo.accessSync(this.pendingPhotoPath)) {
      // 删除临时文件
      fileIo.unlinkSync(this.pendingPhotoPath);
    }
  } catch (_) {
    // 文件可能已删除或不可访问,忽略异常
  }

  // 清空路径记录
  this.pendingPhotoPath = '';
}

五、异常恢复与边界条件处理

5.1 会话异常中断的恢复

场景:拍摄过程中相机会话异常中断

private readonly sessionErrorCallback = (error: Error): void => {
  this.fallbackReason = 'session_failed';
  this.failCamera('相机会话异常中断', error);
};

private failCamera(message: string, error: Error): void {
  this.phase = 'failed';
  this.previewState = '相机运行失败';
  this.statusMessage = message;
  this.errorMessage = this.formatError(error);
  // 相机异常时,会话会被释放,状态标记为 'failed'
  // 用户可以通过"重新检查能力"按钮重新开始
}

恢复策略

  1. 清晰地向用户展示"会话异常"的状态
  2. 不自动清理已保存的附件文件(如果有的话)
  3. 提供"重新检查能力"的入口
  4. 如果已经有有效的附件,允许跳过能力检查直接进入交接

5.2 文件写入失败的处理

场景:JPEG 数据接收成功,但写入本地文件失败(磁盘满、权限不足等)

private async handleImageArrival(): Promise<void> {
  const receiver = this.imageReceiver;
  if (receiver === undefined || this.pendingPhotoPath === '') return;

  let capturedImage: image.Image | undefined;
  try {
    capturedImage = await receiver.readLatestImage();
    const component = await capturedImage.getComponent(image.ComponentType.JPEG);

    const target = fileIo.openSync(
      this.pendingPhotoPath,
      fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC
    );

    try {
      const written = fileIo.writeSync(target.fd, component.byteBuffer);
      if (written <= 0) {
        throw new Error('附件文件未写入有效 JPEG 数据。');
      }
    } finally {
      fileIo.closeSync(target);  // 必须在 finally 中关闭文件
    }

    // 成功路径...
  } catch (error) {
    this.failCapture('人工附件保存失败', error as Error);
    // failCapture 会自动触发 discardPendingPhoto()
  } finally {
    if (capturedImage !== undefined) {
      await capturedImage.release();
    }
  }
}

private failCapture(message: string, error: Error): void {
  this.discardPendingPhoto();  // 清理失败的临时文件
  this.phase = this.photoSession === undefined ? 'failed' : 'previewing';
  this.statusMessage = message;
  this.errorMessage = this.formatError(error);
  // 如果会话还在运行(previewing),用户可以重新尝试拍摄
  // 如果会话已关闭(failed),需要重新启动预览
}

恢复策略

  1. 立即清理失败的临时文件,防止磁盘泄漏
  2. 区分"会话还活跃"和"会话已关闭"两种情况
  3. 如果会话还活跃,提供"重新拍摄"的快速入口
  4. 如果会话已关闭,必须重新启动预览

5.3 权限动态变化的处理

在这里插入图片描述

场景:已进入预览状态,权限被系统设置中撤销

private setupFocusAndFallback(session: camera.PhotoSession): void {
  if (session.isFocusModeSupported(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO)) {
    session.setFocusMode(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO);
    this.focusState = '已设置连续自动对焦';
  } else {
    this.focusState = '设备不支持连续自动对焦';
  }

  // AUTO_FRAMING 检查(标记为不支持)
  this.autoFramingState = '当前 PhotoSession 不提供 AUTO_FRAMING 控制接口,已转人工构图回退';
  this.fallbackReason = 'unsupported';
}

private readonly sessionErrorCallback = (error: Error): void => {
  // 如果运行中权限被撤销,会话会中断,触发此回调
  this.fallbackReason = 'session_failed';  // 或识别为 'permission_denied'
  this.failCamera('相机会话异常中断', error);
};

恢复策略

  1. 会话中断时,记录中断原因(可能无法精确识别是权限还是其他故障)
  2. 提示用户"在系统设置中检查 CAMERA 权限"
  3. 保留已拍摄的附件,如果有的话
  4. 允许用户通过"重新检查能力"重新请求权限

六、审计记录的设计

6.1 能力检查阶段的审计信息

每次能力检查都应该记录以下信息:

字段类型含义示例
checkTimestring检查时间戳2026-08-15T14:30:45.123Z
phaseFallbackPhase最终到达的阶段previewing | unavailable
fallbackReasonFallbackReason回退原因unsupported
deviceIdstring后摄设备ID0
devicePositionstring摄像头位置CAMERA_POSITION_BACK
previewProfilestring选中的预览Profile1920x1080@30fps
photoProfilestring选中的拍照Profile4000x3000@jpeg
permissionGrantedboolean权限是否授予true
focusModeSupportedboolean是否支持连续自动对焦true
autoFramingSupportedboolean是否支持AUTO_FRAMINGfalse
statusCodenumber最终状态代码200 (success) | 401 (permission) | 404 (device)

6.2 人工拍摄阶段的审计信息

interface CaptureAuditRecord {
  captureTime: string;              // 拍摄时间
  attachmentPath: string;           // 附件本地路径
  attachmentSize: number;           // 文件大小(字节)
  mimeType: string;                 // 'image/jpeg'
  quality: camera.QualityLevel;     // 'QUALITY_LEVEL_HIGH'
  rotation: number;                 // 0, 90, 180, 270
  
  // 能力上下文
  fallbackReason: FallbackReason;   // 为什么进行人工拍摄
  sessionDuration: number;          // 会话运行时长(秒)
  
  // 用户操作
  captureAttempt: number;           // 第N次拍摄尝试
  retryCount: number;               // 重新拍摄次数
}

6.3 交接阶段的完整审计链

interface CompleteAuditChain {
  // 第一阶段:能力检查
  capabilityCheck: {
    checkTime: string;
    fallbackReason: FallbackReason;
    autoFramingSupported: boolean;
    // ... 其他检查细节
  };
  
  // 第二阶段:人工拍摄
  manualCapture: {
    captureTime: string;
    attachmentPath: string;
    attachmentSize: number;
    // ... 其他拍摄细节
  };
  
  // 第三阶段:附件交接
  handover: {
    handoverTime: string;
    responsiblePerson: string;
    retakeDeadline: string;
    workOrderId: string;
    batchId: string;
  };
}

七、用户界面与交互设计

7.1 能力检查界面的关键信息展示

左侧取证步骤栏:显示当前进度和回退原因

├── 01 · 能力检查 (当前步骤)
│   ├─ 读取后摄、PhotoSession 与连续自动对焦能力
│   └─ [显示回退原因的详细说明]
├── 02 · 人工构图取证 (灰显 - 需能力检查完成)
│   └─ 仅在回退条件成立后开放
└── 03 · 后续复拍 (灰显 - 需附件生成)
    └─ 登记时限与下一责任人

中央决策面板:显示能力检查结论

┌─ 自动构图回退判断 ─────────────────────┐
│                                        │
│  [PhotoSession 控制接口]  [已查询]     │
│  [AUTO_FRAMING]  [不提供 → 需人工回退] │
│  [回退结论]  [允许人工构图]            │
│                                        │
│  ┌─ 实时预览区 ─────────────────────┐ │
│  │ ┌─────────────────────────────┐ │ │
│  │ │   [实时摄像头预览画面]      │ │ │
│  │ │     [手动构图框指示]        │ │ │
│  │ │                             │ │ │
│  │ └─────────────────────────────┘ │ │
│  └─────────────────────────────────┘ │
│                                        │
│  [启动真实预览]  [人工构图取证]       │
└────────────────────────────────────────┘

右侧附件交接栏:显示交接预留信息

┌─ 人工附件与交接 ─────────────────────┐
│                                      │
│  取证方式:人工构图 · 已允许         │
│  附件记录:尚未生成本地附件草稿     │
│  复拍时限:24 小时内完成补拍        │
│  下一责任人:现场调度员(待指派)   │
│                                      │
│  交接状态:等待确认回退后登记       │
│  复拍交接                            │
│  记录时间:尚未登记                 │
└──────────────────────────────────────┘

7.2 人工拍摄界面

┌─ 人工构图取证 ───────────────────────┐
│                                      │
│ 请将故障部位完整置入黄色取景框。    │
│ 该操作只在真实 AUTO_FRAMING 回退     │
│ 结论成立后可执行。                 │
│                                      │
│ ┌─ 实时预览区 ─────────────────────┐│
│ │ ┌─────────────────────────────┐││
│ │ │  [实时摄像头预览]           │││
│ │ │  ┌─────────────────────┐    │││
│ │ │  │ 手动构图框          │    │││
│ │ │  │ (黄色边框)          │    │││
│ │ │  │ 故障部位需置入此框  │    │││
│ │ │  └─────────────────────┘    │││
│ │ │                             │││
│ │ └─────────────────────────────┘││
│ └─────────────────────────────────┘│
│                                      │
│ [拍摄人工构图附件]  [返回回退判断]  │
└──────────────────────────────────────┘

7.3 复核与交接界面

┌─ 复核人工构图附件 ────────────────────┐
│                                       │
│ 确认附件清晰、故障部位完整后,       │
│ 再生成本地交接记录。                 │
│ 不会上传文件或回写工单。             │
│                                       │
│ ┌─ 附件预览区 ──────────────────────┐│
│ │  [拍摄结果JPEG图像预览]          ││
│ │  (宽度100%, 最大高度400px)       ││
│ └───────────────────────────────────┘│
│                                       │
│ [重新拍摄]  [确认附件并交接]        │
└───────────────────────────────────────┘

八、实施验证与测试清单

8.1 能力检查阶段的验收标准

  • 权限拒绝场景

    • 拒绝 CAMERA 权限后,fallbackReason 标记为 permission_denied
    • 界面清晰提示"权限被拒绝,可在系统设置中重新授权"
    • 允许进入人工附件交接流程
  • 设备不可用场景

    • 后摄不存在或未返回可用 Profile 时,fallbackReason 标记为 camera_unavailable
    • 界面提示"后摄设备不可用"或"输出能力不可用"
    • 允许进入人工附件交接流程
  • 会话异常场景

    • PhotoSession 创建或启动失败时,fallbackReason 标记为 session_failed
    • 会话异常回调被准确触发
    • 界面清晰展示"相机运行失败"的状态
  • 自动构图不支持场景(目标场景):

    • PhotoSession 正常运行,但不提供 AUTO_FRAMING 接口时,fallbackReason 标记为 unsupported
    • 预览画面成功显示
    • "人工构图取证"按钮被启用

8.2 人工拍摄阶段的验收标准

  • 拍摄成功

    • 只有在 fallbackReason !== 'waiting' 时才允许进入拍摄模式
    • 拍摄结果正确保存到本地路径
    • 文件格式为 JPEG,大小合理(>100KB, <5MB)
  • 拍摄失败恢复

    • 文件写入失败时,临时文件被清理
    • 用户可以立即重新拍摄(会话仍活跃情况下)
    • 错误信息清晰展示
  • 会话中断处理

    • 拍摄过程中相机异常中断,状态标记为 failed
    • 已拍摄的附件被保留
    • 用户可以通过"重新检查能力"重新开始

8.3 交接与追溯的验收标准

  • 交接信息完整性

    • 交接记录包含能力事实(fallbackReason, autoFramingSupported 等)
    • 交接记录包含复拍管理信息(retakeDeadline, responsiblePerson
    • 交接时间准确记录
  • 审计链连续性

    • 从能力检查到人工拍摄到交接的完整链路可追溯
    • 每个阶段的转移原因清晰可见
    • 异常恢复过程中,审计链不中断
  • 复拍流程

    • 复拍时允许撤销前次附件并重新拍摄
    • 复拍次数被准确计数
    • 多次复拍的所有附件路径被正确管理(或仅保留最新)

九、常见问题与应急处理

Q1: 为什么拍摄结果保存到本地,而不是直接上传?

A: 这是分层设计的考虑:

  1. 网络不稳定:现场可能无网络或网络不稳定,先本地保存可确保附件不丢失
  2. 权限边界清晰:本页面只负责能力检查和本地附件生成,上传由上层业务流程负责
  3. 支持离线流程:允许现场人员先完成本地交接,稍后在网络恢复时批量上传
  4. 降低页面复杂度:本页面专注于相机能力和附件生成,不涉及后端服务集成

Q2: 如何区分"主动选择人工取证"和"被迫降级"?

A: 通过 fallbackReason 字段:

  • unsupported:AUTO_FRAMING 接口不存在,是技术限制,属于"被迫"
  • permission_denied:权限被拒绝,可能是用户选择,也可能是系统策略
  • session_failed / camera_unavailable:硬件故障或不可用,属于"被迫"

在审计链上,这些原因都被清晰记录,上层业务可基于 fallbackReason 做出不同的处理(如重试、上报、外部回写)。

Q3: 如果权限在拍摄过程中被撤销怎么办?

A: 会话会异常中断,触发 sessionErrorCallback

  1. 状态标记为 failed
  2. 错误消息显示为"相机会话异常中断"
  3. 用户可以通过"重新检查能力"重新请求权限
  4. 如果已经拍摄过的附件,会被保留(不自动删除)

权限恢复后,用户可以选择重新检查能力,此时会重新请求权限。

Q4: 临时文件泄漏风险如何规避?

A: 多层防护:

  1. 明确清理时机discardPendingPhoto() 在失败、复拍、重置时被调用
  2. finally 块保护:文件操作使用 try-finally 确保资源释放
  3. 路径追踪pendingPhotoPath 始终记录待清理的文件路径
  4. 幂等操作discardPendingPhoto() 中的 accessSync() 保证文件存在后再删除,异常被忽略

十、扩展建议

10.1 多附件场景支持

目前设计支持单个附件。如果需要支持多个角度的拍摄:

// 扩展方案
private attachmentList: Array<{
  path: string;
  captureTime: string;
  angle: string;  // "正面" "侧面" "俯视" 等
}> = [];

private addAttachmentToList(path: string, angle: string): void {
  this.attachmentList.push({
    path,
    captureTime: timestamp(),
    angle
  });
}

10.2 手动对焦控制

如果 AUTO_FRAMING 不可用但焦点模式可配:

// 支持手动焦点控制
if (session.isFocusModeSupported(camera.FocusMode.FOCUS_MODE_AUTO)) {
  session.setFocusMode(camera.FocusMode.FOCUS_MODE_AUTO);
  // 提供焦点框 UI,允许用户点击选择焦点位置
  session.setFocusPoint({ x: touchX / width, y: touchY / height });
}

10.3 与高级功能的集成

  • 闪光灯控制:支持补光
  • 曝光控制:手动调整亮度
  • 变焦控制:支持光学或数字变焦
  • 长按连拍:快速连续拍摄多张

总结

HarmonyOS 6.1.1 中的相机自动构图能力处理需要遵循以下核心原则:

  1. 完整的能力检查:从设备→权限→会话→构图控制,逐层检查
  2. 明确的回退原因分类:五层原因,对应不同的业务路由
  3. 保留能力事实:即使不能使用自动构图,也要清晰记录"为什么不能用"
  4. 人工拍摄的正当性:基于真实能力检查结论而启动,不是因为系统故障
  5. 完整的附件交接:包含能力事实、复拍管理、审计链的完整记录
  6. 异常恢复的可能性:权限恢复、网络恢复后能否重新开始

通过这套设计,现场取证系统可以优雅地处理设备差异,同时保持完整的业务可追溯性和流程连续性。


验证状态:✅ 本文对应的代码已集成到项目,所有能力检查、人工拍摄、附件交接、异常恢复流程均在 CameraManualFallbackPage.ets 中完整实现。

后续复拍建议:计划在后续版本中支持多角度拍摄(正面、侧面、俯视)、手动焦点控制、闪光灯补光等高级功能。

必要条件|模拟器与真机准备对照

条件API 24 模拟器HarmonyOS 6.1.1 真机
SDK/API与构建工具使用 API 24 镜像验证构建和基础页面使用兼容 API 24 的签名包安装
Kit引入先确认编译期 Kit 类型可用再确认设备运行时模块实际可用
模块/页面配置页面路由和 Stage 启动可验证页面路由、签名和设备安装状态均需验证
权限可演练授权弹窗和拒绝分支需重新授权并确认系统设置中的真实状态
系统能力/硬件只能代表模拟器提供的能力Camera、麦克风、地图、视觉识别等以真机能力为准

SDK/API 对照完成后插入 DevEco Studio API 24 与构建配置截图:

在这里插入图片描述

授权对照完成后插入真实设备权限截图:
在这里插入图片描述

版本和能力对照完成后插入设备/模拟器信息截图:

在这里插入图片描述

Logo

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

更多推荐