Skip to content

Latest commit

 

History

History
409 lines (329 loc) · 31 KB

File metadata and controls

409 lines (329 loc) · 31 KB

日序 V1 数据设计

1. 文档范围

本文定义逻辑数据模型、字段语义、索引意图、同步和安全约束,不是可直接部署的生产数据库代码。CloudBase 集合、索引和安全规则在对应 Stage 经真实环境验证后创建。

2. 通用约定

2.1 标识与作用域

  • 游客安装在 localUser.clientId 保存一个稳定 UUID;它只标识本地安装,不是登录或鉴权凭据。
  • 每个本地实体另有稳定 UUID 字段 clientId,创建后不可改变,并作为首次上云与重试的幂等业务键;云端正式记录 ID 使用 id,未同步时为 null。
  • 云端用户使用内部 userId,与微信可信身份映射;客户端提交的用户 ID 不作为鉴权依据。
  • 个人数据以所有者作用域隔离;共享数据以 listId 和成员关系隔离。

2.2 时间字段

  • date:用户语义日期,格式 YYYY-MM-DD,适用于全天、日历归属和倒数。
  • startAt / endAt:本地保存为规范化 UTC ISO 8601 字符串,输入必须带 Z 或明确偏移;上云时映射为数据库支持的时间类型。
  • timezone:创建/安排时使用的 IANA 时区标识;utcOffsetMinutes 保存该时间点实际使用的 UTC 偏移,支持夏令时与运行环境无法取得 IANA 标识时的日期一致性校验。
  • createdAt / updatedAt:个人离线记录保留客户端时间并用于 V1 冲突排序;用户活动、共享写入等服务端动作使用可信服务端时间。设备时钟偏差是已记录限制,不能把客户端时间当鉴权或审计依据。
  • deletedAt:空值表示有效,非空表示软删除墓碑。
  • completedAt:仅完成状态非空,取消完成时清空并更新版本。

2.3 并发与删除

  • version 为从 1 开始的整数,每次服务端有效写入递增。
  • 共享实体更新必须提交期望版本并由服务端条件校验。
  • 重要业务集合使用软删除,墓碑在所有客户端确认同步前不得清理。
  • 个人数据 V1 冲突可按 updatedAt 新者优先,但删除优先级、时钟相等和设备时钟偏差要有确定性规则。

2.4 枚举

建议枚举:

  • 事项类型:event、all_day、todo
  • 事项状态:pending、completed
  • 优先级:V1 为 normal、high
  • 清单类型:personal、shared;收集箱是系统视图,不创建伪共享权限
  • 成员角色:owner、member
  • 来源类型:native、imported、external;V1 只写入 native

枚举值最终在共享 TypeScript 类型中集中定义,禁止各页面自行发明字符串。

3. 实体关系概览

users 1 ── N lists
lists 1 ── N list_members ── N users
lists 1 ── N events
events 1 ── N subtasks
events N ── 0..1 repeat_rules
events 1 ── N reminders
events 1 ── N focus_sessions(可为空关联)
users 1 ── N notifications
users 1 ── N feedback
app_config(全局/环境配置,服务端管理)

关系图是逻辑关系,不要求文档数据库执行关系型联表。

4. 集合设计

字段后的“必需”指业务语义;游客本地数据在开启同步前可没有云端 ownerId,但必须有 clientId 和稳定实体 ID。

4.1 users

字段 类型 约束与说明
_id string 服务端根据可信 OPENID 映射的稳定内部 ID
openid string 仅服务端从微信上下文写入,不接受客户端自报
displayName string? 用户允许后保存的展示名,非登录前置
avatarUrl string? 可选,遵守微信授权与隐私规则
schemaVersion integer 用户记录结构版本,当前为 1
createdAt timestamp 服务端创建时间
updatedAt timestamp 服务端更新时间
lastActiveAt timestamp 最近调用云函数的可信时间
lastFocusRoomCreatedAt timestamp? 服务端限制邀请房创建频率,不接受客户端直接写入
deletedAt timestamp? 账号数据软删除状态;当前新建为空

安全:只能读取/修改自身允许字段;身份映射、角色和管理状态不能由普通客户端写。

4.2 lists

字段 类型 约束与说明
id string? 云端正式 ID;游客未同步时为空
clientId string 本地稳定实体 UUID,首次上云幂等键
ownerId string? 个人清单所有者或共享清单创建者;游客阶段为空
creatorId string? 创建者;游客阶段为空,云端由可信身份确定
type enum personal / shared
name string 去空白后必填,V1 本地实现最长 30 字符
icon string 受控图标键,不保存任意执行内容
color string 受控色值或 token,需通过校验
isDefault boolean 是否为本地默认清单;首次初始化必须且只能有一个有效默认清单
sortOrder number 用户清单排序
createdAt timestamp 创建时间
updatedAt timestamp 修改时间
version integer 乐观并发版本
deletedAt timestamp? 软删除/共享解散墓碑

说明:收集箱由“无日期事项”查询形成系统视图,不依赖一个可被用户删除的普通清单。事项仍可保留可选 listId 以表达所属自定义清单。默认清单不可删除;删除自定义个人清单时,先把引用该清单的事项迁移到默认清单,再写入清单软删除墓碑,不做级联删除。

4.3 list_members

字段 类型 约束与说明
id string 成员关系 UUID
listId string 仅可指向共享清单
userId string 成员用户 ID
role enum owner / member,只有两级
joinedAt timestamp 明确加入时间
createdAt timestamp 记录创建时间
updatedAt timestamp 状态更新时间
version integer 并发版本
deletedAt timestamp? 软删除墓碑

唯一性意图:同一 listId + userId 只能有一个有效成员关系。角色和状态只能由受信云函数按权限写入。

4.4 events

字段 类型 约束与说明
id string? 云端正式 ID;游客未同步时为空
clientId string 本地稳定实体 UUID;首次上云和重试的幂等键,不是鉴权凭据
creatorId string? 创建者;游客阶段为空,云端从可信身份确定
listId string? 云端清单 ID 或本地清单 clientId;创建时默认指向本地默认清单
title string 去空白后必填,V1 最长 100 字符
note string 默认空字符串,长度受限
type enum event / all_day / todo,与日期字段一致
startAt timestamp? event 必需,其他类型为空
endAt timestamp? 可选;存在时不得早于 startAt
date string? event / all_day 必需,todo 为空
timezone string? 有时间或重复规则时保存
utcOffsetMinutes integer? event 必需;安排时相对 UTC 的分钟偏移,范围 -840 到 840
status enum pending / completed
priority enum V1 为 normal / high
countdownEnabled boolean 是否展示基础倒数;需要有 date
repeatRuleId string? 指向重复规则;不重复时为空
seriesId string? 重复系列的稳定本地标识;普通事项为空
occurrenceDate string? 当前重复实例对应的 YYYY-MM-DD;普通事项为空
sourceType enum native / imported / external;V1 只写入 native
completedAt timestamp? 与 status 一致
createdAt timestamp 创建时间
updatedAt timestamp 最后修改时间
version integer 服务端写入递增
deletedAt timestamp? 软删除墓碑

一致性规则:

  • event:date、startAt、utcOffsetMinutes 非空;date 与 startAt 按安排时偏移还原的日历日期一致。
  • all_day:date 非空,startAt 为空。
  • todo:date、startAt、endAt 为空,出现在收集箱。
  • status=completed 时 completedAt 非空;pending 时为空。
  • countdownEnabled=true 时必须有 date。
  • 重复实例必须同时具有 repeatRuleId、seriesId 和 occurrenceDate;单次例外可脱离系列并保留自身事项记录。
  • 共享清单事项的调用者必须是有效成员,且更新需校验 version。

4.5 subtasks

字段 类型 约束与说明
id string? 云端 ID;本地阶段为空
clientId string 本地稳定 UUID
eventId string 父事项,必需
title string 去空白后必填
status enum pending / completed
sortOrder number 父事项内排序
completedAt timestamp? 与状态一致
createdAt timestamp 创建时间
updatedAt timestamp 修改时间
version integer 并发版本
deletedAt timestamp? 软删除墓碑

V1 子任务不含独立日期、时间或提醒字段。子任务全部完成后只询问是否完成主事项,不自动联动;主事项状态也不强制改写子任务。

4.6 repeat_rules

字段 类型 约束与说明
id string? 云端 ID;本地阶段为空
clientId string 本地稳定 UUID
seriesId string 关联重复系列的稳定本地标识
frequency enum daily / weekly / monthly / yearly
interval integer 大于等于 1
weekdays integer[] 自定义星期,统一使用 1–7 等约定后固定
startDate string 规则起始语义日期
endType enum never / date / count
endDate string? endType=date 时必需
count integer? endType=count 时必需且大于 0
createdAt timestamp 创建时间
updatedAt timestamp 修改时间
version integer 并发版本
deletedAt timestamp? 软删除墓碑

“不重复”通过事项无 repeatRuleId 表示。工作日使用 weekly + weekdays=[1,2,3,4,5],周一为 1、周日为 7;每月缺失日期和闰日均收束到当月二月/月份末日。首次生成取未来 12 个月或 200 个实例先到者,浏览更远日期时由仓库按需补充。

4.7 reminders

字段 类型 约束与说明
id string? 云端 ID;本地阶段为空
clientId string 本地稳定 UUID
eventId string 关联事项
offsetMinutes integer 相对开始时刻/全天默认时刻的提前分钟数
triggerAt timestamp 计划触发时间
status enum 本地为 scheduled / dismissed;Sprint 3 只同步本地提醒记录,正式订阅发送状态取得真实模板后另建可信记录
createdAt timestamp 创建时间
updatedAt timestamp 修改时间
version integer 并发版本
deletedAt timestamp? 软删除墓碑

微信订阅授权凭证/状态的具体存储以平台当时规范为准,不把 AppSecret 或访问令牌写入本集合或客户端。

4.8 focus_sessions

字段 类型 约束与说明
id string? 云端 ID;本地阶段为空
clientId string 本地稳定 UUID
eventId string? 可选关联事项
plannedMinutes integer 计划分钟数,1–240
actualSeconds integer 实际有效秒数,不小于 0
startedAt timestamp 开始时间戳
endedAt timestamp? 结束/中断时间
status enum running / paused / completed / cancelled
pausedAt timestamp? 当前暂停起点
pausedDurationSeconds integer 已累计暂停秒数
createdAt timestamp 创建时间
updatedAt timestamp 修改时间
version integer 并发版本
deletedAt timestamp? 软删除墓碑

计时恢复以 startedAt、pausedAt 和累计暂停时长为依据,不把 UI 内存倒计时当作事实来源。白噪音已从当前产品范围移除,不保存音频资源键或播放状态。

4.9 notifications

字段 类型 约束与说明
id string 通知 UUID
userId string 接收者
type enum upcoming / today / overdue / shared 等受控值
title string 安全展示标题
body string 简短正文,不包含不必要敏感数据
entityType string? 关联实体类型
entityId string? 关联实体 ID
readAt timestamp? 已读时间
createdAt timestamp 创建时间
deletedAt timestamp? 软删除墓碑

4.10 feedback

字段 类型 约束与说明
id string 反馈 UUID
userId string? 游客可为空
clientId string? 游客关联,展示时脱敏
category string 受控分类
content string 必填、限长、防注入输出
contact string? 用户主动提供,按隐私要求处理
status enum new / processing / resolved / closed
createdAt timestamp 创建时间
updatedAt timestamp 状态更新时间
deletedAt timestamp? 软删除墓碑

4.11 app_config

字段 类型 约束与说明
id string 配置键或 UUID
key string 唯一逻辑键,如公告配置
value object 通过服务端 schema 校验的公开配置值
scope enum public / server_only
enabled boolean 是否生效
updatedBy string 管理员内部 ID
createdAt timestamp 创建时间
updatedAt timestamp 修改时间
version integer 并发版本

严禁在 app_config 中存储 AppSecret、数据库管理密钥或访问令牌。敏感配置使用平台环境变量/秘密管理能力。

5. 本地同步元数据

5.1 Sprint 3 本地数据包

Stage 4 使用 Storage Adapter 在 rixu:local-data:v1 单 key 中保存一个版本化快照:

LocalData {
  schemaVersion: 3
  localUser: { clientId, createdAt }
  events: EventItem[]
  lists: ListItem[]
  repeatRules: RepeatRule[]
  subtasks: Subtask[]
  reminders: Reminder[]
  focusSessions: FocusSession[]
  listMembers: ListMember[]
  notifications: SharedNotification[]
  syncState: { enabled, userId, status, pending, lastSyncedAt, lastErrorCode }
  settings: { weekStartsOn, showCompletedEvents, updatedAt }
}
  • CURRENT_SCHEMA_VERSION = 3;schemaVersion=1 自动补齐效率与同步集合并为旧事项补齐重复字段,schemaVersion=2 自动补齐共享缓存、同步状态和设置。所有读取先经过 migrateLocalData(),未知版本或损坏结构返回明确迁移错误,且不覆盖原始数据。
  • 单次写入构造不可变快照,通过一次 wx.setStorageSync 替换同一 key;底层异常统一包装为稳定错误码。
  • 微信单 key 上限为 1MB,本项目在 900KiB 时提前拒绝写入并保留原快照,为序列化与后续字段预留空间。V1 数据增长超出该范围时必须通过新 schema 迁移为分片存储,不允许静默截断。
  • 默认查询排除 deletedAt 墓碑;getById 同时接受云端 id 与本地实体 clientId。

5.2 当前同步元数据

可采用独立本地元数据表/集合,避免污染领域实体:

字段 说明
enabled 用户是否主动开启同步
userId 云函数从可信 OPENID 映射的内部用户 ID
status local_only / pending / syncing / synced / failed
pending 本机是否存在待确认的写入
lastSyncedAt 最近一次成功同步时间
lastErrorCode 可诊断但不向普通用户暴露技术细节的错误码

首次开启同步按 用户作用域 + 集合 + clientId 生成稳定云端文档键并 upsert,不用“标题 + 日期”等不稳定组合去重。个人数据保留 deletedAt 墓碑;共享事项使用独立 expectedVersion 校验。

5.3 云端集合与协议

Sprint 3 云函数使用:users、lists、list_members、events、repeat_rules、subtasks、reminders、focus_sessions、notifications、sync_state、share_invites。共同专注扩展使用:focus_rooms、focus_room_members、focus_room_messages、focus_room_invites、focus_room_reports。本地 schemaVersion=3 与云同步协议 schemaVersion=1 分别演进,不能混为一个版本号;共同专注是云端临时协作数据,不写入游客本地快照,因此本轮不提升本地 schemaVersion。

  • users 文档 _id 由服务端根据 OPENID 生成,保存 openid、createdAt、updatedAt、lastActiveAt、schemaVersion;原始 OPENID 不作为客户端入参。
  • 邀请保存 SHA-256 token 哈希而不是明文 token;token 由 32 字节安全随机数生成,默认 7 天过期。
  • list_members 使用 listId + userId 的稳定文档键,重复加入为幂等读取;退出或移除写入软删除墓碑。
  • 所有小程序业务数据访问均经云函数;数据库客户端不授予通用集合读写能力。

5.4 共同专注云端模型

focus_rooms 保存固定公开房和邀请房:房间名、说明、kind、ownerId、容量、activeSlots、创建/更新时间和软删除状态。activeSlots 是服务端事务维护的最多 20 个在线占位(仅含 userId 与 lastSeenAt),所有加入、心跳、离线、退出和移除都通过同一事务规则预留、刷新或释放;超过 2 分钟的占位视为过期并在下一次事务中剔除。旧房间缺少该字段时按空数组迁移读取。公开房由服务端按固定 slug 幂等初始化,不接受客户端创建公开房;邀请房创建者成为 owner。

focus_room_members 以 roomId + userId 的稳定键保存成员关系和在线状态:

字段 说明
roomId / userId 房间与可信用户标识;用户标识由 OPENID 映射,不接收客户端冒充
nickname / avatarVariant / avatarUrl 默认使用服务端生成的日序昵称与几何头像;avatarUrl 仅在本人自愿上传且通过图片审核后保存,缺失时自然降级为几何头像
role owner / member;固定公开房只有普通成员
status idle / focusing / paused / completed
plannedMinutes / startedAt 成员主动公开的本次计时状态,不替代本地 FocusSession 事实来源
publicGoal 用户自愿公开的一句话目标,最多 60 字并经过内容安全检查
active / lastSeenAt 页面在线状态与心跳时间;过期在线状态不计入当前人数
mutedUntil 邀请房房主设置的有限禁言时间
lastMessageAt 服务端发送频率保护的最后消息时间,不由客户端直接写入
joinedAt / createdAt / updatedAt / deletedAt 成员生命周期与软删除墓碑
leftAt / removedAt 区分成员主动退出与被房主移除;前者可按加入规则恢复,后者拒绝重新进入

focus_room_messages 保存最多 200 字的纯文字消息、发送时的 senderNickname / senderAvatarVariant 展示快照、createdAt、7 天 expiresAt 和软删除状态;读取只返回最近 50 条有效消息。自愿头像文件位于 Cloud Storage,数据库不保存 base64,也不把头像 URL 固化进历史消息;资料恢复为几何身份后,历史消息不会继续展示旧头像。旧成员或旧消息缺少头像字段时直接使用几何头像,不需要清空或迁移历史数据。focus_room_reports 以“消息 + 举报人”稳定键记录举报,避免重复刷举报;普通成员举报后只对自己隐藏该消息,不允许一次举报直接让所有成员看不到内容,后续由管理端处理。focus_room_invites 只保存邀请 token 的 SHA-256 哈希、房间、邀请人、创建/过期时间和软删除状态,默认 7 天有效。

共同专注消息不进入个人同步快照,不提供图片、语音、关注、私聊或永久历史。服务端内容安全不可用或结果非通过时,消息和公开目标必须拒绝写入。

6. 索引意图

实际索引需依据 CloudBase 当前限制和查询计划验证。至少评估:

  • events:所有者/清单 + date + status + deletedAt
  • events:所有者 + updatedAt(增量同步)
  • events:repeatRuleId;若 Stage 8 最终采用实例键,再为当时确认的 recurrenceKey 建唯一性意图
  • list_members:listId + userId(有效关系唯一性意图)
  • lists:ownerId + type + deletedAt
  • reminders:status + triggerAt(待发送扫描)
  • notifications:userId + createdAt + readAt
  • focus_sessions:userId + startedAt
  • focus_room_members:roomId + deletedAt;userId + deletedAt
  • focus_room_messages:roomId + deletedAt + createdAt(倒序读取最近消息)
  • focus_room_invites:token 哈希文档键;并评估 roomId + expiresAt
  • focus_room_reports:roomId + reporterId + deletedAt
  • feedback:status + createdAt
  • app_config:key 唯一性意图

不要一次性加载用户所有历史记录;按日期范围、游标或分页查询。

7. 权限矩阵(数据层意图)

数据 游客 已同步本人 共享普通成员 共享创建者 管理员
本地个人事项 本地读写 本地读写 不适用 不适用 不适用
云端个人事项 无直接身份写 仅本人 不可访问他人 不可访问他人 仅受控支持场景
共享事项 需先开启身份 按成员关系 查看/新增/修改/完成 同左 仅异常处理受控操作
成员管理 不可 非创建者不可 不可 邀请/移除/设置/解散 受控异常处理
共同专注公开房 不可 加入后查看成员、独立计时、发送已审核文字 不适用 不适用 举报处理与异常治理
共同专注邀请房 不可 按房间成员关系 查看、独立计时、发送已审核文字 同左,并可邀请、禁言、移除 举报处理与异常治理
公告配置 只读公开项 只读公开项 只读公开项 只读公开项 受控写入

此矩阵必须在云函数测试中验证,不能只通过隐藏前端按钮实现。

8. 数据迁移与兼容

  • 本地数据库/序列化结构维护 schemaVersion。
  • 迁移应可重复执行,失败保留原数据并提供恢复路径。
  • 新字段先支持读取缺省值,再逐步写入;删除字段需经过兼容期。
  • 同步协议版本与应用版本解耦,旧客户端写入必须被校验或明确拒绝。
  • 生产迁移先备份/演练,脚本不默认连接生产环境。

9. 待决数据问题

以下在对应 Stage 前通过实验和 DECISIONS.md 定案:

  • 微信小程序本地结构化存储适配与容量策略(Stage 4)。
  • 重复事项采用预生成、按需投影或混合模型及例外记录(Stage 8)。
  • 墓碑目前无限期保留;自动清理、增量游标和设备时钟校正需在真实云环境数据量验证后另行定案。
  • 邀请当前 7 天有效并可多次转发给不同微信用户;邀请主动撤销 UI 尚未加入。
  • 统计以周一为周起始;完成率分母为截至今天有日期的应处理事项,零数据为 0%;仅统计 completed 专注会话。