第32篇|GalleryRecordService 新增记录:一张照片进入相册的真实路径
第 32 篇进入服务层。拍照页生成的是一次拍摄结果,真正能被相册、地图、隐私空间、同步模块复用的是GalleryMoment记录。GalleryRecordService负责模型、持久化、Uri 规范化和记录创建。它让项目不必在每个页面重复处理照片路径、同步脏标记和 AI 状态。
本文是 21 天「智能相机开发实战」训练营中的一篇实操记录。所有代码片段都来自当前项目,配图围绕运行页面和源码关键路径展开,读完以后可以直接回到工程里按函数名定位。
本篇目标
- 读懂
GalleryMoment的核心字段和它们服务的页面。 - 理解 Preferences 存储在本项目中的作用。
- 知道 createRecord 如何把拍摄结果变成统一记录。
- 区分页面状态更新和服务层持久化的职责。
代码位置
entry/src/main/ets/services/GalleryRecordService.etsentry/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,为后续端云同步保留依据。 - 隐私可见性应属于记录模型,而不是只靠页面过滤。
今日练习
- 打开
GalleryRecordService.ets,给每个字段标注它服务的页面或功能。 - 把
createRecord的返回对象和相册详情页展示字段对应起来。 - 尝试新增一个字段时,思考它应该由页面传入还是服务层默认生成。
下一篇会继续沿着同一条工程链路往下拆:先看用户能看到的效果,再回到源码确认状态、文件和服务边界是否闭合。