第32篇|GalleryRecordService 新增记录:一张照片进入相册的真实路径 第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 和 normalizesaveRecords写入字符串并 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): PromiseArrayGalleryMoment { 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: ArrayGalleryMoment): Promisevoid { 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的返回对象和相册详情页展示字段对应起来。尝试新增一个字段时思考它应该由页面传入还是服务层默认生成。下一篇会继续沿着同一条工程链路往下拆先看用户能看到的效果再回到源码确认状态、文件和服务边界是否闭合。