本文定义逻辑数据模型、字段语义、索引意图、同步和安全约束,不是可直接部署的生产数据库代码。CloudBase 集合、索引和安全规则在对应 Stage 经真实环境验证后创建。
- 游客安装在
localUser.clientId保存一个稳定 UUID;它只标识本地安装,不是登录或鉴权凭据。 - 每个本地实体另有稳定 UUID 字段
clientId,创建后不可改变,并作为首次上云与重试的幂等业务键;云端正式记录 ID 使用id,未同步时为null。 - 云端用户使用内部
userId,与微信可信身份映射;客户端提交的用户 ID 不作为鉴权依据。 - 个人数据以所有者作用域隔离;共享数据以
listId和成员关系隔离。
date:用户语义日期,格式YYYY-MM-DD,适用于全天、日历归属和倒数。startAt/endAt:本地保存为规范化 UTC ISO 8601 字符串,输入必须带Z或明确偏移;上云时映射为数据库支持的时间类型。timezone:创建/安排时使用的 IANA 时区标识;utcOffsetMinutes保存该时间点实际使用的 UTC 偏移,支持夏令时与运行环境无法取得 IANA 标识时的日期一致性校验。createdAt/updatedAt:个人离线记录保留客户端时间并用于 V1 冲突排序;用户活动、共享写入等服务端动作使用可信服务端时间。设备时钟偏差是已记录限制,不能把客户端时间当鉴权或审计依据。deletedAt:空值表示有效,非空表示软删除墓碑。completedAt:仅完成状态非空,取消完成时清空并更新版本。
version为从 1 开始的整数,每次服务端有效写入递增。- 共享实体更新必须提交期望版本并由服务端条件校验。
- 重要业务集合使用软删除,墓碑在所有客户端确认同步前不得清理。
- 个人数据 V1 冲突可按
updatedAt新者优先,但删除优先级、时钟相等和设备时钟偏差要有确定性规则。
建议枚举:
- 事项类型:
event、all_day、todo - 事项状态:
pending、completed - 优先级:V1 为
normal、high - 清单类型:
personal、shared;收集箱是系统视图,不创建伪共享权限 - 成员角色:
owner、member - 来源类型:
native、imported、external;V1 只写入native
枚举值最终在共享 TypeScript 类型中集中定义,禁止各页面自行发明字符串。
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(全局/环境配置,服务端管理)
关系图是逻辑关系,不要求文档数据库执行关系型联表。
字段后的“必需”指业务语义;游客本地数据在开启同步前可没有云端 ownerId,但必须有 clientId 和稳定实体 ID。
| 字段 | 类型 | 约束与说明 |
|---|---|---|
_id |
string | 服务端根据可信 OPENID 映射的稳定内部 ID |
openid |
string | 仅服务端从微信上下文写入,不接受客户端自报 |
displayName |
string? | 用户允许后保存的展示名,非登录前置 |
avatarUrl |
string? | 可选,遵守微信授权与隐私规则 |
schemaVersion |
integer | 用户记录结构版本,当前为 1 |
createdAt |
timestamp | 服务端创建时间 |
updatedAt |
timestamp | 服务端更新时间 |
lastActiveAt |
timestamp | 最近调用云函数的可信时间 |
lastFocusRoomCreatedAt |
timestamp? | 服务端限制邀请房创建频率,不接受客户端直接写入 |
deletedAt |
timestamp? | 账号数据软删除状态;当前新建为空 |
安全:只能读取/修改自身允许字段;身份映射、角色和管理状态不能由普通客户端写。
| 字段 | 类型 | 约束与说明 |
|---|---|---|
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 以表达所属自定义清单。默认清单不可删除;删除自定义个人清单时,先把引用该清单的事项迁移到默认清单,再写入清单软删除墓碑,不做级联删除。
| 字段 | 类型 | 约束与说明 |
|---|---|---|
id |
string | 成员关系 UUID |
listId |
string | 仅可指向共享清单 |
userId |
string | 成员用户 ID |
role |
enum | owner / member,只有两级 |
joinedAt |
timestamp | 明确加入时间 |
createdAt |
timestamp | 记录创建时间 |
updatedAt |
timestamp | 状态更新时间 |
version |
integer | 并发版本 |
deletedAt |
timestamp? | 软删除墓碑 |
唯一性意图:同一 listId + userId 只能有一个有效成员关系。角色和状态只能由受信云函数按权限写入。
| 字段 | 类型 | 约束与说明 |
|---|---|---|
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。
| 字段 | 类型 | 约束与说明 |
|---|---|---|
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 子任务不含独立日期、时间或提醒字段。子任务全部完成后只询问是否完成主事项,不自动联动;主事项状态也不强制改写子任务。
| 字段 | 类型 | 约束与说明 |
|---|---|---|
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 个实例先到者,浏览更远日期时由仓库按需补充。
| 字段 | 类型 | 约束与说明 |
|---|---|---|
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 或访问令牌写入本集合或客户端。
| 字段 | 类型 | 约束与说明 |
|---|---|---|
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 内存倒计时当作事实来源。白噪音已从当前产品范围移除,不保存音频资源键或播放状态。
| 字段 | 类型 | 约束与说明 |
|---|---|---|
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? | 软删除墓碑 |
| 字段 | 类型 | 约束与说明 |
|---|---|---|
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? | 软删除墓碑 |
| 字段 | 类型 | 约束与说明 |
|---|---|---|
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、数据库管理密钥或访问令牌。敏感配置使用平台环境变量/秘密管理能力。
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。
可采用独立本地元数据表/集合,避免污染领域实体:
| 字段 | 说明 |
|---|---|
enabled |
用户是否主动开启同步 |
userId |
云函数从可信 OPENID 映射的内部用户 ID |
status |
local_only / pending / syncing / synced / failed |
pending |
本机是否存在待确认的写入 |
lastSyncedAt |
最近一次成功同步时间 |
lastErrorCode |
可诊断但不向普通用户暴露技术细节的错误码 |
首次开启同步按 用户作用域 + 集合 + clientId 生成稳定云端文档键并 upsert,不用“标题 + 日期”等不稳定组合去重。个人数据保留 deletedAt 墓碑;共享事项使用独立 expectedVersion 校验。
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的稳定文档键,重复加入为幂等读取;退出或移除写入软删除墓碑。- 所有小程序业务数据访问均经云函数;数据库客户端不授予通用集合读写能力。
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 天有效。
共同专注消息不进入个人同步快照,不提供图片、语音、关注、私聊或永久历史。服务端内容安全不可用或结果非通过时,消息和公开目标必须拒绝写入。
实际索引需依据 CloudBase 当前限制和查询计划验证。至少评估:
events:所有者/清单 +date+status+deletedAtevents:所有者 +updatedAt(增量同步)events:repeatRuleId;若 Stage 8 最终采用实例键,再为当时确认的recurrenceKey建唯一性意图list_members:listId+userId(有效关系唯一性意图)lists:ownerId+type+deletedAtreminders:status+triggerAt(待发送扫描)notifications:userId+createdAt+readAtfocus_sessions:userId+startedAtfocus_room_members:roomId+deletedAt;userId+deletedAtfocus_room_messages:roomId+deletedAt+createdAt(倒序读取最近消息)focus_room_invites:token 哈希文档键;并评估roomId+expiresAtfocus_room_reports:roomId+reporterId+deletedAtfeedback:status+createdAtapp_config:key唯一性意图
不要一次性加载用户所有历史记录;按日期范围、游标或分页查询。
| 数据 | 游客 | 已同步本人 | 共享普通成员 | 共享创建者 | 管理员 |
|---|---|---|---|---|---|
| 本地个人事项 | 本地读写 | 本地读写 | 不适用 | 不适用 | 不适用 |
| 云端个人事项 | 无直接身份写 | 仅本人 | 不可访问他人 | 不可访问他人 | 仅受控支持场景 |
| 共享事项 | 需先开启身份 | 按成员关系 | 查看/新增/修改/完成 | 同左 | 仅异常处理受控操作 |
| 成员管理 | 不可 | 非创建者不可 | 不可 | 邀请/移除/设置/解散 | 受控异常处理 |
| 共同专注公开房 | 不可 | 加入后查看成员、独立计时、发送已审核文字 | 不适用 | 不适用 | 举报处理与异常治理 |
| 共同专注邀请房 | 不可 | 按房间成员关系 | 查看、独立计时、发送已审核文字 | 同左,并可邀请、禁言、移除 | 举报处理与异常治理 |
| 公告配置 | 只读公开项 | 只读公开项 | 只读公开项 | 只读公开项 | 受控写入 |
此矩阵必须在云函数测试中验证,不能只通过隐藏前端按钮实现。
- 本地数据库/序列化结构维护
schemaVersion。 - 迁移应可重复执行,失败保留原数据并提供恢复路径。
- 新字段先支持读取缺省值,再逐步写入;删除字段需经过兼容期。
- 同步协议版本与应用版本解耦,旧客户端写入必须被校验或明确拒绝。
- 生产迁移先备份/演练,脚本不默认连接生产环境。
以下在对应 Stage 前通过实验和 DECISIONS.md 定案:
- 微信小程序本地结构化存储适配与容量策略(Stage 4)。
- 重复事项采用预生成、按需投影或混合模型及例外记录(Stage 8)。
- 墓碑目前无限期保留;自动清理、增量游标和设备时钟校正需在真实云环境数据量验证后另行定案。
- 邀请当前 7 天有效并可多次转发给不同微信用户;邀请主动撤销 UI 尚未加入。
- 统计以周一为周起始;完成率分母为截至今天有日期的应处理事项,零数据为 0%;仅统计
completed专注会话。