news 2026/9/2 8:37:44

【时光清单|10】HarmonyOS ArkTS 情侣空间实战:设计双方纪念内容的数据模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【时光清单|10】HarmonyOS ArkTS 情侣空间实战:设计双方纪念内容的数据模型

【时光清单|10】HarmonyOS ArkTS 情侣空间实战:设计双方纪念内容的数据模型

“情侣空间”看起来只是两个头像、一块留言板和几条共同任务,真正落到数据层却很容易失控:双方资料放在哪里,任务与留言是否属于同一个聚合,纪念日要不要复制一份,页面更新数组后为什么必须重新赋值,未来做备份或多设备协同时又怎样避免旧结构把新数据覆盖掉?这些问题如果没有在模型阶段回答,页面越丰富,数据边界越模糊。

时光清单的真实源码给出了一个小而完整的起点。CoupleSpace.ets定义双方头像、双方称呼、关系起始日期、共同任务和留言;CoupleView.etsDataStore读取并保存该聚合,同时从AnniversaryRepository单独筛选恋爱与纪念日;BackupService.ets又把CoupleSpace | null纳入备份结构。它没有账号体系、远端同步、双向实时通信,也没有把“本地留言”包装成在线聊天。

本文从这些可复核代码出发,不把未运行的构建、设备验证或双用户同步写成当前结果,说明怎样用 ArkTS 明确聚合边界、子项身份、时间语义、不可变更新和持久化协议,并进一步讨论版本迁移、输入校验、多设备扩展与隐私审核。文中的“当前实现”和“演进建议”会分开标注,避免把设计方案误写成已经上线的能力。

本文将解决六个具体问题:

  1. 为什么CoupleSpace适合作为页面聚合,而不应吞并所有纪念日。
  2. CoupleTaskCoupleMessage为什么都需要稳定标识和创建时间。
  3. 当前fromSelf语义在哪些场景成立,何时必须升级为参与者标识。
  4. ArkUI 中为什么采用展开运算符生成新数组和新对象。
  5. Preferences JSON 持久化怎样补上校验、版本与迁移。
  6. 从单设备本地功能扩展到多设备协同时,模型还缺哪些协议。

本文唯一标记:CSDN-SERIES:ALL-163208083

一、先复核真实模型:一个聚合包含什么

源码中的核心接口只有三组:

export interface CoupleSpace { selfAvatar: string; loverAvatar: string; selfName: string; loverName: string; startDate: number; tasks: CoupleTask[]; messages: CoupleMessage[]; } export interface CoupleTask { id: string; title: string; completed: boolean; createdAt: number; } export interface CoupleMessage { id: string; content: string; fromSelf: boolean; createdAt: number; }

这里没有额外的基类,也没有把任务与留言塞进一个通用ContentItem。这是合理的:两类数据虽然都有idcreatedAt,但业务状态不同。任务需要completed,留言需要发送方语义fromSelf。为了少写两个字段而强行合并,会让页面到处判断type,反而削弱类型系统。

数据所属边界当前用途不应承担的职责
双方头像与称呼CoupleSpace顶部关系信息账号身份认证
startDateCoupleSpace计算相伴天数取代全部纪念日
tasksCoupleSpace页面任务列表全局待办系统
messagesCoupleSpace本地留言展示网络聊天记录
共同纪念日AnniversaryRepository按类型筛选组合复制进聚合形成双真源

这个边界最有价值的地方,是页面数据可以组合,数据真源却不必合并。情侣空间负责自身资料、任务与留言;纪念日仍由纪念日仓库维护。

二、默认对象不是示例数据,而是合法空状态

模型文件提供了默认工厂:

export function createDefaultCoupleSpace(): CoupleSpace { return { selfAvatar: 'avatar_default_male', loverAvatar: 'avatar_default_female', selfName: '我', loverName: 'TA', startDate: Date.now(), tasks: [], messages: [], }; }

它让页面在持久化读取完成前就拥有完整、非空、可渲染的状态。对 ArkUI 来说,这比把每个字段声明为可空更稳:构建阶段不需要不断写?.或占位分支,空任务与空留言也能自然映射为页面空状态。

不过,默认值包含明确的产品语义:

  • startDate: Date.now()表示首次进入时从当天开始计时。
  • “我”和“TA”是显示称呼,不是账号身份。
  • 头像字符串是资源键语义,实际页面当前直接使用固定媒体资源。
  • 空数组表示功能可用但暂无内容,而不是加载失败。

默认工厂需要满足“随时调用都得到全新对象”。当前实现每次都创建新数组,因此不会出现两个页面实例共享同一个可变数组的问题。若未来把默认数组提升为模块常量,再直接复用,就可能产生跨实例污染。

const first = createDefaultCoupleSpace(); const second = createDefaultCoupleSpace(); // 期望:两个数组不是同一引用 const isolated = first.tasks !== second.tasks;

默认状态还不等于持久化成功。页面必须把“没有历史数据”和“读取失败”区分开,否则损坏的 JSON 也会悄悄表现成首次使用。

三、读取流程:页面组合两个真源

CoupleView出现时执行loadData()

private async loadData(): Promise<void> { const saved = await this.store.getJson<CoupleSpace | null>( DataKeys.COUPLE, null ); if (saved) { this.couple = saved; } const all = await this.anniversaryRepo.getAll(); this.sharedAnniversaries = all.filter( (item: Anniversary) => item.type === 'love' || item.type === 'memorial' ); }

第一条数据路径读取DataKeys.COUPLE,第二条路径读取纪念日仓库。页面将它们放在couplesharedAnniversaries两个状态变量中。这样做避免了一个常见问题:把纪念日对象复制进CoupleSpace,之后编辑纪念日时只更新仓库,情侣空间仍展示旧副本。

当前串行读取便于理解,但两个数据源互不依赖,可以在后续优化时并发等待:

private async loadData(): Promise<void> { const couplePromise = this.store.getJson<CoupleSpace | null>( DataKeys.COUPLE, null ); const anniversariesPromise = this.anniversaryRepo.getAll(); const results = await Promise.all([ couplePromise, anniversariesPromise ]); const saved = results[0] as CoupleSpace | null; const all = results[1] as Anniversary[]; if (saved) { this.couple = saved; } this.sharedAnniversaries = all.filter( (item: Anniversary) => item.type === 'love' || item.type === 'memorial' ); }

并发只是性能演进,不是当前源码已有行为。更重要的是补齐页面状态:加载中、读取失败、无内容和正常内容不能都落成同一个默认界面。

当前源码边界:这是本地双人主题页,不是双方账号协作

字段名里的selflover容易让人误以为已经存在两个登录用户。本轮复核没有找到账号、成员绑定或网络同步逻辑,页面也没有编辑双方称呼、头像和关系起始日的交互入口。selfNameloverNamestartDate会参与展示,但两张头像并未读取模型中的selfAvatarloverAvatar,而是直接引用avatar_couple_maleavatar_couple_female固定媒体资源。因此,头像字符串目前只是模型中的预留数据,不能据此宣称用户已能换头像。

当前任务只支持新增与完成状态切换,没有编辑、删除;留言只支持新增,没有编辑、删除,也没有“代另一方留言”的入口。新留言固定保存fromSelf: true,所以左右气泡只是本机视角的展示规则,不是经过身份认证的发送者记录。这些限制属于当前事实;增加资料编辑、双成员身份与完整增删改,则是后续设计范围。

四、聚合边界:为什么纪念日不放进 CoupleSpace

共同纪念日看起来属于情侣空间,但它同时出现在全局纪念日列表、详情页、卡片服务和提醒服务中。若直接在CoupleSpace增加anniversaries: Anniversary[],就会出现两个可写副本:

DataKeys.ANNIVERSARIES -> love / memorial 条目 DataKeys.COUPLE -> 再复制一份相同纪念日

两个副本需要同步标题、日期、封面、置顶、重复规则和更新时间。任意一次保存漏掉一侧,就会产生不一致。真实实现选择在加载时过滤:

all.filter((item: Anniversary) => item.type === 'love' || item.type === 'memorial' );

这相当于建立一个页面投影,而不是创建第二个数据源。

方案一致性查询成本适合场景
复制完整纪念日对象几乎不建议
只保存纪念日 ID需要手工挑选共享项
按类型实时筛选当前恋爱/纪念日自动归集
独立关系表多空间、多成员、权限复杂

当前“按类型筛选”与产品规模匹配。如果未来允许用户决定某条纪念日是否出现在情侣空间,可在CoupleSpace中保存sharedAnniversaryIds: string[],但仍不复制完整对象。

当前投影还有四个需要如实标注的限制。第一,筛选条件只看type,会收集仓库中所有lovememorial条目,并没有coupleId或空间成员关联;当产品只有一个本地情侣空间时尚可理解,扩展多空间后就不够。第二,页面使用calcDaysPassed(item.startDate ?? item.targetDate)统一显示“经过天数”,若memorial只有一个未来targetDate,结果可能是负数,语义上应改为“还有多少天”或按日期方向选择计算函数。

第三,sharedAnniversaries只在aboutToAppear()触发的loadData()中读取;当前页面没有监听DATA_VERSION或仓库变更通知,所以纪念日在别处修改后,只有重新进入并完成加载才能确认展示最新值。第四,共同纪念日使用普通Column + ForEach,没有List或外层Scroll;条目很多时是否全部可达,本轮没有进行设备或窗口尺寸测试。以上是源码审阅结论,不是已复现的运行故障。

历史证据:共同纪念日同步问题曾有修复记录

项目级错误记录在2026-05-20留下过“恋爱/情侣共同纪念日不同步”的问题,记录的修复方向是让情侣页从AnniversaryRepository读取并筛选对应类型。当前源码确实存在该仓库读取与过滤代码,因此可以证明修复方案仍保留在代码中。另一方面,本轮对相关模型和页面执行了聚焦提交历史查询,没有得到可用于进一步归因的提交记录;也没有重新运行构建、安装或真机流程。历史记录只能说明当时记录过问题与修复,不能替代当前版本的构建和设备验证。

五、任务模型:稳定身份比数组下标更重要

任务创建代码使用当前时间生成字符串 ID:

const newTask: CoupleTask = { id: Date.now().toString(), title: this.newTaskText.trim(), completed: false, createdAt: Date.now() };

页面渲染时也把task.id作为ForEach键,切换完成状态时按 ID 查找。这样即使列表排序变化,操作仍指向同一个任务。若使用数组下标,删除、插入或重新排序后,UI 复用与业务定位都可能错位。

当前 ID 在单设备、低频点击下通常够用,但同一毫秒创建两项仍可能冲突。面向备份导入或多设备合并时,建议使用带随机片段的生成函数:

function createLocalId(prefix: string): string { const now = Date.now(); const random = Math.random().toString(36).slice(2, 10); return `${prefix}_${now}_${random}`; }

更完整的任务模型还可以加入:

export interface CoupleTaskV2 { id: string; title: string; completed: boolean; createdAt: number; updatedAt: number; completedAt?: number; deletedAt?: number; }

updatedAt支持新旧版本判断,completedAt支持完成记录,deletedAt则为多设备合并提供“墓碑”。这些字段属于演进设计,当前源码只有四个字段。

六、留言模型:fromSelf 是视角,不是永久身份

当前留言定义:

export interface CoupleMessage { id: string; content: string; fromSelf: boolean; createdAt: number; }

在单设备本地页面里,fromSelf很实用:它直接决定气泡方向与配色,不需要账号系统。创建留言时源码固定写入true,因此当前功能更接近“留给彼此看的本地便签”,不是两台设备实时互发消息。

当数据要跨设备共享时,布尔值会失去稳定意义。设备 A 的self可能是设备 B 的lover,导入后若不转换,气泡方向就会相反。跨设备模型应保存稳定参与者 ID:

export type CoupleMemberRole = 'owner' | 'partner'; export interface CoupleMessageV2 { id: string; content: string; authorRole: CoupleMemberRole; createdAt: number; updatedAt: number; }

如果未来接入账号,authorIdauthorRole更可靠;页面再根据当前登录身份计算fromSelf

const fromSelf = message.authorId === currentUserId;

模型应存事实,页面状态应存视角。authorId是事实,fromSelf是根据当前用户推导出的视角。当前实现没有账号与联网能力,因此布尔字段是符合现阶段边界的简化。

七、不可变更新:让 ArkUI 明确感知变化

添加任务时,源码没有直接执行this.couple.tasks.push(newTask),而是创建新数组和新对象:

this.couple = { selfAvatar: this.couple.selfAvatar, loverAvatar: this.couple.loverAvatar, selfName: this.couple.selfName, loverName: this.couple.loverName, startDate: this.couple.startDate, tasks: [...this.couple.tasks, newTask], messages: this.couple.messages, };

切换完成状态时同样先map(),再通过cloneCouple()替换聚合:

private async toggleTask(id: string): Promise<void> { const nextTasks = this.couple.tasks.map((task: CoupleTask) => { if (task.id !== id) return task; return { id: task.id, title: task.title, completed: !task.completed, createdAt: task.createdAt, }; }); this.couple = this.cloneCouple( nextTasks, this.couple.messages ); await this.saveData(); }

重新赋值能给 ArkUI 清晰的状态变化信号,也减少“内层数组已经变了,但依赖外层引用的逻辑没有更新”的隐患。

随着字段增加,手写复制容易漏字段。可以让克隆函数接收局部变更:

interface CoupleSpacePatch { tasks?: CoupleTask[]; messages?: CoupleMessage[]; selfName?: string; loverName?: string; } function patchCoupleSpace( current: CoupleSpace, patch: CoupleSpacePatch ): CoupleSpace { return { selfAvatar: current.selfAvatar, loverAvatar: current.loverAvatar, selfName: patch.selfName ?? current.selfName, loverName: patch.loverName ?? current.loverName, startDate: current.startDate, tasks: patch.tasks ?? current.tasks, messages: patch.messages ?? current.messages, }; }

ArkTS 项目中应保持显式类型,不用宽泛any或动态扩展对象。字段多到难以维护时,优先把更新逻辑放入 ViewModel 或服务层,而不是在每个点击事件里复制整段对象。

八、Preferences 持久化:JSON 是格式,不是协议

DataStore把复杂对象序列化为字符串:

async putJson<T>( key: string, value: T ): Promise<void> { await this.ensureReady(); if (!this.pref) return; try { const json = JSON.stringify(value); await this.pref.put(key, json); await this.pref.flush(); } catch (e) { hilog.error( DOMAIN, TAG, 'putJson [%{public}s] failed: %{public}s', key, JSON.stringify(e) ); } }

DataKeys.COUPLE的真实键值是ds_couple。JSON 便于保存小型本地对象,但JSON.parse()成功不代表对象结构合法。旧版本可能没有新字段,导入文件也可能把tasks写成字符串。

建议在模型边界增加正规化函数:

interface CoupleSpaceRecord { schemaVersion: number; data: CoupleSpace; } function normalizeCoupleSpace( value: CoupleSpace | null ): CoupleSpace { const fallback = createDefaultCoupleSpace(); if (!value) return fallback; return { selfAvatar: typeof value.selfAvatar === 'string' ? value.selfAvatar : fallback.selfAvatar, loverAvatar: typeof value.loverAvatar === 'string' ? value.loverAvatar : fallback.loverAvatar, selfName: typeof value.selfName === 'string' ? value.selfName : fallback.selfName, loverName: typeof value.loverName === 'string' ? value.loverName : fallback.loverName, startDate: Number.isFinite(value.startDate) ? value.startDate : fallback.startDate, tasks: Array.isArray(value.tasks) ? value.tasks : [], messages: Array.isArray(value.messages) ? value.messages : [], }; }

正规化至少防止缺字段导致页面构建异常。更严格的实现还应逐项验证任务和留言,限制文本长度,并拒绝无效时间戳。

九、模型版本:为字段演进留下入口

当前CoupleSpace本身没有schemaVersion,但BackupData已有version: number。备份版本负责整个备份包,聚合版本负责单个模型,两者作用不同。

export interface CoupleSpaceEnvelope { schemaVersion: number; updatedAt: number; data: CoupleSpace; }

读取时根据版本迁移:

function migrateCoupleSpace( envelope: CoupleSpaceEnvelope ): CoupleSpaceEnvelope { if (envelope.schemaVersion === 1) { return envelope; } throw new Error( `Unsupported couple schema: ${ envelope.schemaVersion }` ); }

首个版本看似不需要迁移,但只要未来把fromSelf改为authorRole、为任务增加删除标记、允许自定义头像,就会需要稳定入口。没有版本字段时,只能靠“字段是否存在”猜测来源。

变化是否需要迁移推荐处理
新增可选 UI 字段视情况读取时补默认值
字段重命名需要旧字段映射到新字段
布尔身份改为参与者 ID需要根据设备角色转换
时间单位改变必须显式换算并记录版本
删除字段建议读取兼容,写回新格式

迁移应保持幂等:同一份数据执行两次,结果不能继续变化。

十、时间字段:毫秒值必须约定日历语义

模型中的startDatecreatedAt都是number,实际写入Date.now()毫秒值。两者含义不同:

  • startDate是面向日历的关系起始日。
  • createdAt是事件发生时刻。

源码中的calcDaysPassed()会把当前时间与起始时间都归一到本地当天零点,再计算天数:

export function calcDaysPassed( startDate: number ): number { const now = new Date(); const today = new Date( now.getFullYear(), now.getMonth(), now.getDate() ).getTime(); const start = new Date(startDate); const startDay = new Date( start.getFullYear(), start.getMonth(), start.getDate() ).getTime(); return Math.floor( (today - startDay) / (1000 * 60 * 60 * 24) ); }

这避免了“今天 23:00 到明天 01:00 只过两小时却应显示跨一天”的部分问题,但跨夏令时地区可能出现一天不是固定 24 小时的情况。当前面向本地设备使用;若跨地区同步,应明确按哪个时区解释关系起始日。更稳的做法是存储日历日期字段,例如2026-07-26,显示时再按业务时区转换。

任务与留言的createdAt则应保留绝对时间戳,用于排序和冲突判断,不能按日历日期丢失时分秒。

十一、保存失败不能悄悄当作成功

当前saveData()只调用:

private async saveData(): Promise<void> { await this.store.putJson( DataKeys.COUPLE, this.couple ); }

DataStore.putJson()捕获异常后记录日志,不向上抛出,也不返回结果。页面会立即显示新任务或新留言,即使持久化失败。用户离开再回来时内容消失,容易被理解为数据丢失。

调用方式还存在一个可观察性差异:新增任务与新增留言的点击处理器会先清空输入框,再直接调用this.saveData(),既没有await,也没有根据结果恢复草稿;切换任务完成状态则会await this.saveData(),之后增加StateKeys.DATA_VERSION。但由于底层写入异常被吞掉,即使使用了await,页面仍无法获知持久化是否真正成功。新增任务和留言也没有递增DATA_VERSION,因此若其他页面依赖这个版本通知刷新,它们收不到这两类变化。这里讨论的是当前调用链的差异,不代表已经观察到数据丢失。

可以把写入结果显式化:

export interface SaveResult { success: boolean; reason?: string; } async putJsonSafely<T>( key: string, value: T ): Promise<SaveResult> { await this.ensureReady(); if (!this.pref) { return { success: false, reason: 'STORE_NOT_READY' }; } try { await this.pref.put( key, JSON.stringify(value) ); await this.pref.flush(); return { success: true }; } catch (e) { return { success: false, reason: 'WRITE_FAILED' }; } }

页面可以采用“先更新后回滚”或“保存成功后更新”两种策略。对于任务与留言这类用户明确输入的内容,至少要在失败时提示,并保留输入草稿,不能清空输入框后静默丢失。

十二、输入约束:在进入模型前做净化

真实页面已经对输入执行trim(),并拒绝空文本:

if (this.newTaskText.trim().length > 0) { // 创建任务 }

这是第一层保护,但还缺长度上限。无限长文本会放大 Preferences JSON、列表布局和备份文件的压力。可以定义领域约束:

export class CoupleLimits { static readonly NAME_MAX = 24; static readonly TASK_TITLE_MAX = 120; static readonly MESSAGE_MAX = 1000; static readonly TASK_COUNT_MAX = 500; static readonly MESSAGE_COUNT_MAX = 2000; } function normalizeText( raw: string, maxLength: number ): string { const text = raw.trim(); if (text.length > maxLength) { return text.slice(0, maxLength); } return text; }

约束不应只写在输入框的maxLength,因为备份导入、未来服务调用也可能绕过页面。页面负责即时反馈,模型服务负责最终校验。

十三、备份边界:结构合法与内容可信是两回事

真实BackupData包含:

export interface BackupData { version: number; timestamp: number; anniversaries: Anniversary[]; quotes: Quote[]; coupleSpace: CoupleSpace | null; themeId: string; }

BackupService.importBackup()当前检查versionanniversaries是否存在,但没有逐项校验coupleSpace。这意味着 JSON 语法正确的文件仍可能带来错误类型、超长文本或无效时间。

导入流程建议拆成四步:

  1. 限制文件大小,避免超大 JSON 占用内存。
  2. 解析后验证备份包版本。
  3. 分别校验纪念日、语录和情侣空间。
  4. 先生成迁移后的预览,用户确认后再覆盖本地数据。

情侣空间包含私人称呼、留言和共同事项。导出时应明确文件存放位置、分享风险与删除方式;不要默认上传云端,也不要在日志中输出正文内容。

十四、从本地双人页面到多设备协同,还缺什么

当前源码是单设备本地能力。真正的多设备情侣空间至少需要以下新增协议:

export interface SyncEntityMeta { entityId: string; spaceId: string; authorId: string; revision: number; createdAt: number; updatedAt: number; deletedAt?: number; }
能力当前模型多设备所需
成员身份self/lover显示语义稳定用户 ID 与空间成员关系
更新顺序本地数组顺序revision、updatedAt 或操作日志
删除直接从数组移除删除墓碑与传播确认
冲突不存在字段级或实体级合并策略
权限本机即可信成员校验、退出与撤销
传输官方网络能力与安全通道

不能只把 Preferences 文件复制到另一台设备就称为协同。双方可能同时完成同一任务、修改称呼或新增留言;没有空间 ID、作者 ID、版本号和冲突规则,最后写入者会覆盖另一端。

一种保守策略是任务和留言按实体合并,空间资料采用版本较新的整对象;删除使用deletedAt,等待两端确认后再物理清理。无论采用哪种方式,都需要先定义一致性承诺,再选 HarmonyOS 数据或网络能力。

十五、隐私与审核:亲密内容应默认留在本地

情侣空间天然可能包含私人称呼、计划与留言。当前情侣空间通过 Preferences 存储;项目另有向应用私有目录写 JSON 的BackupService,但本轮只证明BackupData结构可以容纳coupleSpace,没有证明页面已经把该字段接入完整的导出、选择文件、恢复与覆盖确认流程。源码中没有看到登录、云同步或实时通信,因此文章也不把它描述成在线社交功能。

发布前应核对:

  • 应用说明是否准确描述为本地记录。
  • 权限清单是否没有与功能无关的网络、通讯录、定位等权限。
  • 备份文件是否位于应用可控目录,并有清晰的导出操作。
  • 日志是否只记录键名和错误,不输出留言正文。
  • 删除情侣空间是否有确认,且能清除对应本地数据。
  • 若未来新增云同步,隐私政策、服务端位置、账号注销和数据删除是否同步更新。

敏感信息不应被用于崩溃上报或埋点标签。即使内容由用户主动输入,也不意味着应用可以在后台上传。

十六、测试矩阵:模型比页面截图更需要边界用例

模型测试应覆盖默认值、更新、持久化、迁移和异常:

用例输入期望
首次进入ds_couple不存在渲染全新默认对象
空任务tasks=[]显示明确空状态
空白任务仅空格不创建、不保存
切换完成已存在任务 ID只更新目标项
ID 不存在随机 ID聚合内容不变
损坏 JSON无法解析显示错误或安全默认值
旧版本缺字段messages迁移为空数组
超长留言超过领域上限阻止或截断并提示
保存失败Preferences 不可用提示失败,保留输入
纪念日更新仓库条目改变重新加载后展示新值

一个针对纯函数的最小验证可以这样写:

const original = createDefaultCoupleSpace(); const task: CoupleTask = { id: 'task_1', title: '一起看展', completed: false, createdAt: 100 }; const updated = patchCoupleSpace( original, { tasks: [task] } ); // original.tasks.length 应为 0 // updated.tasks.length 应为 1 // original 与 updated 不应是同一引用

把模型更新写成纯函数,可以在没有 ArkUI 运行环境时验证大部分行为;页面测试再关注点击、空状态、长文本、返回导航和安全区。

十七、性能与容量:Preferences 适合轻量聚合,不适合无限消息流

当前每次保存都会序列化整个CoupleSpace,任务或留言越多,单次写入成本越高。少量本地内容时这种设计简单可靠;当留言达到数千条,任何一次新增都要复制数组、序列化全部数据并flush()

可用以下指标判断是否需要迁移存储:

指标Preferences 仍合适应考虑关系型存储
数据量小型设置与有限列表大量、持续增长记录
查询整体读取分页、筛选、排序
更新整体替换可接受高频单条更新
迁移简单版本多表关系与索引
并发单页面低频写入多入口并发读写

如果产品坚持本地留言板,可设合理数量上限并归档旧内容。若演进为真正消息流,应把CoupleMessage放入关系型数据表,按spaceId + createdAt分页,不再把所有消息嵌入一个 JSON。

十八、常见问题与修复方向

现象可能原因修复
新任务当时可见,重进消失写入失败被静默吞掉返回保存结果并提示
修改纪念日后情侣页还是旧值页面未重新加载仓库页面恢复时刷新或订阅版本
导入后气泡方向反了fromSelf被跨设备直接复用保存作者身份,再计算视角
新字段上线后页面崩溃旧 JSON 没有该字段加 schemaVersion 与正规化
快速新增出现重复键仅用毫秒时间作 ID加随机片段或 UUID
长留言导致页面卡顿整个 JSON 高频重写限长、分页或迁移 RDB
清理数据误删纪念日聚合与仓库边界混乱分键存储并明确删除范围
两台设备互相覆盖没有 revision 与冲突规则先设计同步协议

排障时先判断问题属于页面状态、模型变换、持久化还是跨源组合。不要看到“显示不对”就直接修改 UI。

十九、发布前验证清单

  • [ ]CoupleSpace.ets的字段与文章描述一致。
  • [ ] 首次进入能渲染默认称呼、空任务和空留言。
  • [ ] 空白输入不会生成任务或留言。
  • [ ] 添加任务、切换完成状态、添加留言后能持久化。
  • [ ]DataKeys.COUPLE与纪念日数据键相互独立。
  • [ ] 共同纪念日来自仓库筛选,没有复制第二份真源。
  • [ ] 长文本在手机、小窗、平板布局中不遮挡操作。
  • [ ] 写入失败有用户可理解的状态,输入不会无提示丢失。
  • [ ] 备份导入对版本和coupleSpace结构做校验。
  • [ ] 日志、统计与崩溃信息不包含私人留言正文。
  • [ ] 应用说明、权限、隐私政策与“本地功能”事实一致。
  • [ ] 若新增同步,身份、版本、冲突、删除和注销协议已定义。

二十、总结:先稳住聚合,再扩展能力

时光清单的情侣空间模型没有追求复杂:CoupleSpace聚合双方资料、关系起始日、共同任务与留言;任务和留言各自保持独立业务字段;共同纪念日继续由AnniversaryRepository管理,页面只做筛选组合;更新时生成新数组和新对象,再通过DataStore序列化到ds_couple。这个结构与当前单设备、本地、轻量的真实能力匹配。

下一阶段最值得补的不是更多页面装饰,而是数据协议:保存结果、输入上限、结构校验、模型版本和迁移。只有当产品真的进入多设备协同时,才引入稳定成员身份、空间 ID、实体版本、删除墓碑和冲突合并。把“当前可用”与“未来可同步”分开设计,既能保持 HarmonyOS 本地体验简洁,也不会让后续演进被一个没有边界的 JSON 对象困住。

本轮没有执行当前源码的构建、安装、真机交互或性能测试,也没有据此推断发布状态、用户规模和平台评价结果。可确认的内容限于上述源码、项目错误记录以及文章更新后的平台回读;需要运行环境才能证明的结论,仍应通过当前版本的构建与设备测试补齐。


AI 辅助声明:本文由 AI 辅助整理,所有源码结构、接口字段与行为结论均依据文中列出的CoupleSpace.etsCoupleView.etsDataStore.etsAnniversary.etsBackupService.ets进行人工复核;演进代码为明确标注的设计建议,不代表当前版本已经实现。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 8:37:20

基于STM32的火灾报警系统开发全流程解析:从硬件设计到软件实现

简介&#xff1a;本资源是一套完整的基于STM32的火灾报警系统毕业设计/课程设计实战资料包&#xff0c;面向嵌入式初学者、电子信息类本科生及毕设开发者&#xff0c;聚焦环境参数实时监测、多传感器融合判断与声光联动报警等核心功能实现。压缩包含1445个文件&#xff0c;主体…

作者头像 李华
网站建设 2026/9/2 8:36:42

微信同城小程序开源项目实战:从源码修复到二次开发全流程解析

简介&#xff1a;这是一套基于UniApp与ThinkPHP&#xff08;FastAdmin框架&#xff09;开发的同城信息发布类微信小程序开源修复版源码&#xff0c;面向具备前后端基础的开发者&#xff0c;用于快速搭建本地生活服务类小程序平台。资源共2001个文件&#xff0c;涵盖1047个JS逻辑…

作者头像 李华
网站建设 2026/9/2 8:36:06

基于蚁群算法的直流电机PID参数优化:Simulink仿真实践

简介&#xff1a;本资源面向自动化、电机控制及智能优化算法领域的工程师与高校研究者&#xff0c;聚焦直流电机PID控制器参数整定这一典型工程难题&#xff0c;提供基于蚁群优化&#xff08;ACO&#xff09;的Simulink仿真完整实现方案。压缩包共4个文件&#xff08;14KB&…

作者头像 李华
网站建设 2026/9/2 8:34:42

论文速递 | TR-B 6月文章精选

编者按在本文章中&#xff0c;我们对交通运筹领域顶刊《Transportation Research Part B: Methodological》于2026年6月份在线发布的文章中进行了精选&#xff08;共8篇&#xff09;&#xff0c;并总结其基本信息&#xff0c;旨在帮助读者快速洞察行业最新动态。本月 TR-B 发文…

作者头像 李华
网站建设 2026/9/2 8:34:25

Vibe Coding不是提示词竞赛:工程规范才是关键

Vibe Coding 这个词最近在AI编程讨论里出现频率非常高。很多人的第一反应是“用自然语言指挥AI写代码”&#xff0c;于是把大量时间花在打磨提示词上&#xff1a;需求描述反复改、角色设定越写越长、语气和格式要求堆了一大段。我一开始也这么干&#xff0c;用了一段时间之后发…

作者头像 李华
网站建设 2026/9/2 8:33:57

TIA Portal中高效统计二进制位中1的个数:FC实现与工业应用

简介&#xff1a;本资源是面向西门子TIA博途V15平台开发者的实用功能库&#xff0c;专为解决工业自动化编程中频繁出现的二进制位统计需求——即快速计算整数&#xff08;INT&#xff09;或WORD类型变量中二进制表示下“1”的个数&#xff08;Hamming重量&#xff09;。该功能在…

作者头像 李华