第32篇|GalleryRecordService 新增记录:一张照片进入相册的真实路径
2026/5/31 14:53:40 网站建设 项目流程

第32篇|GalleryRecordService 新增记录:一张照片进入相册的真实路径

第 32 篇进入服务层。拍照页生成的是一次拍摄结果,真正能被相册、地图、隐私空间、同步模块复用的是GalleryMoment记录。GalleryRecordService负责模型、持久化、Uri 规范化和记录创建。它让项目不必在每个页面重复处理照片路径、同步脏标记和 AI 状态。

本文是 21 天「智能相机开发实战」训练营中的一篇实操记录。所有代码片段都来自当前项目,配图围绕运行页面和源码关键路径展开,读完以后可以直接回到工程里按函数名定位。

本篇目标

  • 读懂GalleryMoment的核心字段和它们服务的页面。
  • 理解 Preferences 存储在本项目中的作用。
  • 知道 createRecord 如何把拍摄结果变成统一记录。
  • 区分页面状态更新和服务层持久化的职责。

代码位置

  • entry/src/main/ets/services/GalleryRecordService.ets
  • entry/src/main/ets/pages/Index.ets

一、相册看到的不是图片数组,而是记录模型

相册页按照片、视频、分组、详情等方式组织内容,底层需要的不只是图片 Uri。记录里要有 id、创建时间、地点、经纬度、前后图路径、AI 文案、同步状态、可见性和隐私标记。只有字段足够完整,后续功能才不会靠临时变量硬拼。

图1 GalleryRecordService 支撑相册、地图、同步和隐私空间

二、GalleryMoment:把展示、同步和隐私状态放在一条记录里

GalleryMoment是相册的中心模型。backPath/frontPath保留文件路径,backUri/frontUri负责展示;syncDirty/cloudRevision给端云同步预留状态;visibility支持公开相册和隐私空间分流。模型不是越小越好,而是要刚好覆盖产品里会被多处引用的状态。

图2 GalleryMoment 模型字段覆盖图片、地点、同步和隐私状态

export interface GalleryMoment { id: string; createdAt: number; updatedAt?: number; createdLabel: string; pairIndex: number; place: string; memoryTitle: string; latitude: number; longitude: number; backPath: string; frontPath: string; backUri: string; frontUri: string; aiStatus: GalleryMomentStatus; visibility: GalleryMomentVisibility; aiCaption: string; videoPrompt: string; watermarkStyle?: GalleryWatermarkStyle; watermarkText?: string; userNote?: string; aiPoem?: string; ownerKey?: string; syncDirty?: boolean; cloudRevision?: number; cloudBackAssetDataUrl?: string; cloudFrontAssetDataUrl?: string; }

如果把这些字段散落在页面里,后面做分组、云同步或隐私空间时都会遇到“同一张照片在不同页面长得不一样”的问题。

三、loadRecords/saveRecords:本地持久化先闭合

服务层使用 Preferences 保存记录数组。loadRecords读取 JSON 后做 parse 和 normalize,saveRecords写入字符串并 flush。这样 App 重启后,相册可以从本地恢复;即使云同步暂时不可用,用户刚拍的作品也不会只停留在内存里。

图3 loadRecords/saveRecords 使用 Preferences 保存 GalleryMoment 数组

export class GalleryRecordService { private static readonly STORE_NAME: string = 'super_image_gallery'; private static readonly STORE_KEY: string = 'gallery_records'; private static readonly DEFAULT_USER_NOTE: string = ''; private static readonly DEFAULT_AI_POEM: string = ''; private static readonly DEFAULT_AI_CAPTION: string = '这份照片会保留拍摄地点、时间和画面氛围,你可以继续补充备注。'; private static readonly DEFAULT_VIDEO_PROMPT: string = '选择多张照片后,可以整理成一条回忆短片。'; static async loadRecords(context: common.UIAbilityContext): Promise<Array<GalleryMoment>> { try { const store = await preferences.getPreferences(context, GalleryRecordService.STORE_NAME); const rawValue = store.getSync(GalleryRecordService.STORE_KEY, '[]') as string; return GalleryRecordService.parseRecords(rawValue); } catch (error) { console.error(`Failed to load gallery records: ${JSON.stringify(error)}`); return []; } } static async saveRecords(context: common.UIAbilityContext, records: Array<GalleryMoment>): Promise<void> { try { const store = await preferences.getPreferences(context, GalleryRecordService.STORE_NAME); store.putSync(GalleryRecordService.STORE_KEY, JSON.stringify(records)); await store.flush(); } catch (error) { console.error(`Failed to save gallery records: ${JSON.stringify(error)}`); }

这里的存储粒度是记录数组,适合训练营阶段的本地闭环。后续如果接入更复杂的数据库,也可以保持GalleryRecordService的外部接口不变。

四、createRecord:统一生成可展示、可同步的记录

createRecord把拍摄入口传来的参数收口成完整记录。它会生成展示 Uri、设置同步脏标记、初始化云端修订号,并将 AI 状态设置为 pending。页面不需要知道这些默认值,页面只负责提供本次拍摄真实产生的路径和地点。

图4 createRecord 统一生成 GalleryMoment 的默认字段

static createRecord(options: CreateGalleryMomentOptions): GalleryMoment { const record: GalleryMoment = { id: options.id, createdAt: options.createdAt, updatedAt: options.createdAt, createdLabel: GalleryRecordService.formatTimestamp(options.createdAt), pairIndex: options.pairIndex, place: options.place, memoryTitle: options.memoryTitle, latitude: GalleryRecordService.normalizeCoordinate(options.latitude), longitude: GalleryRecordService.normalizeCoordinate(options.longitude), backPath: options.backPath, frontPath: options.frontPath, backUri: GalleryRecordService.toFileUri(options.backPath), frontUri: GalleryRecordService.toFileUri(options.frontPath), aiStatus: 'pending', visibility: 'public', aiCaption: GalleryRecordService.DEFAULT_AI_CAPTION, videoPrompt: GalleryRecordService.DEFAULT_VIDEO_PROMPT, watermarkStyle: GalleryRecordService.normalizeWatermarkStyle(options.watermarkStyle), watermarkText: options.watermarkText && options.watermarkText.trim().length > 0 ? options.watermarkText.trim() : '', userNote: GalleryRecordService.DEFAULT_USER_NOTE, aiPoem: GalleryRecordService.DEFAULT_AI_POEM, syncDirty: true, cloudRevision: 0 }; return record;

这个服务边界很适合训练营学习:页面层负责用户动作和状态反馈,服务层负责数据形状和持久化默认规则。

工程检查清单

  • 相册记录必须有稳定 id,不能靠文件名或展示标题当唯一标识。
  • 路径和 Uri 同时存在,但职责不同:path 用于文件操作,uri 用于展示。
  • 保存记录后要 flush,避免 App 退出时丢失。
  • 新增记录默认syncDirty: true,为后续端云同步保留依据。
  • 隐私可见性应属于记录模型,而不是只靠页面过滤。

今日练习

  1. 打开GalleryRecordService.ets,给每个字段标注它服务的页面或功能。
  2. createRecord的返回对象和相册详情页展示字段对应起来。
  3. 尝试新增一个字段时,思考它应该由页面传入还是服务层默认生成。

下一篇会继续沿着同一条工程链路往下拆:先看用户能看到的效果,再回到源码确认状态、文件和服务边界是否闭合。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询