+ 追加消息(后续处理器契约)
- 同 transactionSync 递增 channel_head.version 与 last_seq。
- 按新 head 插 messages,message_id 唯一;提交后写回 SOCIAL_DB 频道摘要。
每频道的消息正文、同步时钟、附件元数据、表情、提及与置顶。
用户资料归 USER_DB;频道目录、成员与未读游标归 SOCIAL_DB;金融状态归外部协议。
通用语言(术语定义)以 contexts/social/CONTEXT.md 为准,这里不重复。
channel_head + messages + channel_pins + message_attachments + message_mentions + message_reactionsChannelHead contexts/social/src/domain/social.types.ts| 消息序号为正且对应 head.last_seq,message_id 唯一 | 数据库 | messages CHECK / UNIQUE、messages_append_at_head |
| 消息身份不可变,版本不得超出 head | 数据库 | messages_stamp_on_change |
| 同步头只前进,reset_version 不超 version | 数据库 | channel_head CHECK / channel_head_forward_only |
| 消息禁止硬删,软删清正文与卡片快照 | 数据库 | messages_permanent / messages CHECK |
| 表情和置顶禁止 UPDATE,INSERT / DELETE 必须 stamp 消息 | 数据库 | message_reactions_* / channel_pins_* |
schema_migrations保证方:数据库 约束 / 索引 / 触发器 SQL 仓储里的条件写 / batch 领域代码 domain 层规则 外部 链上等外部权威
| 操作 | 原子性 | channel_head | channel_pins | message_attachments | message_mentions | message_reactions | messages | schema_migrations |
|---|---|---|---|---|---|---|---|---|
| migrateChannelSchema contexts/social/src/infrastructure/sqlite/migrate.ts DO transactionSync,每版迁移结构与版本行一起提交;v1 初始化同步头。业务消息处理器尚未实现。 | batch | ● | ● |
| 本库字段 | 对应 | 说明 |
|---|---|---|
messages.sender_id | USER_DB users.user_id | 公开资料只经 users /server 取 PublicUserCard。 |
message_attachments.object_key | R2 对象 key | 上传与清理流程后续交付。 |
messages.card_ref | 外部分享对象 / strategy 已发布快照 | 不可读私有 StrategyOwner DO。 |
| 表 | 规则 |
|---|---|
channel_head | 永久保留消息行,软删清 body / card_payload_json;其他频道附属记录按显式用户动作管理,本票未提供清理入口。 |
messages | 永久保留消息行,软删清 body / card_payload_json;其他频道附属记录按显式用户动作管理,本票未提供清理入口。 |
channel_pins | 永久保留消息行,软删清 body / card_payload_json;其他频道附属记录按显式用户动作管理,本票未提供清理入口。 |
message_attachments | 永久保留消息行,软删清 body / card_payload_json;其他频道附属记录按显式用户动作管理,本票未提供清理入口。 |
message_mentions | 永久保留消息行,软删清 body / card_payload_json;其他频道附属记录按显式用户动作管理,本票未提供清理入口。 |
message_reactions | 永久保留消息行,软删清 body / card_payload_json;其他频道附属记录按显式用户动作管理,本票未提供清理入口。 |
schema_migrations | 永久保留已应用版本,不删改历史。 |
| 字段 | 级别 | 说明 |
|---|---|---|
messages.body | 个人数据 | |
messages.card_payload_json | 个人数据 | |
message_attachments.object_key | 个人数据 |
0003 绿色迁移号表示该字段由后续迁移 ALTER TABLE 加入。点表名展开。
| 字段 | 类型 | 约束 | 含义 | 迁移 |
|---|---|---|---|---|
| PKid | INTEGER | PK · CHECK(id = 1) | 固定 1,保证单行。 | 1 |
| version | INTEGER | NOT NULL · DEFAULT 0 · CHECK(version >= 0) | 频道变更时钟,新增 / 编辑 / 软删 / 表情 / 置顶均递增。 | 1 |
| reset_version | INTEGER | NOT NULL · DEFAULT 0 · CHECK(reset_version >= 0 AND reset_version <= version) | 重置边界,0 表示从未重置;不得大于 version。 | 1 |
| last_seq | INTEGER | NOT NULL · DEFAULT 0 · CHECK(last_seq >= 0 AND last_seq <= version) | 最后插入消息序号,初始 0;仅新增消息时递增,未读不受编辑影响。 | 1 |
CREATE TRIGGER channel_head_forward_only BEFORE UPDATE ON channel_head WHEN NEW.version < OLD.version OR NEW.reset_version < OLD.reset_version OR NEW.last_seq < OLD.last_seq BEGIN SELECT RAISE(ABORT, 'channel_head only moves forward'); END
CREATE TRIGGER channel_head_permanent BEFORE DELETE ON channel_head BEGIN SELECT RAISE(ABORT, 'channel_head is permanent'); END种子行
| 字段 | 类型 | 约束 | 含义 | 迁移 |
|---|---|---|---|---|
| PKFKseq | INTEGER | PK · FK→messages.seq | 被置顶消息序号,FK 到 messages。 | 1 |
| pinned_by | TEXT | NOT NULL | 执行置顶的真人 user_id。 | 1 |
| pinned_at | TEXT | NOT NULL | 置顶时间,ISO TEXT。 | 1 |
CREATE TRIGGER channel_pins_delete_stamped BEFORE DELETE ON channel_pins WHEN (SELECT version FROM messages WHERE seq = OLD.seq) IS NOT (SELECT version FROM channel_head WHERE id = 1) BEGIN SELECT RAISE(ABORT, 'channel_pins: stamp the message with the bumped head version first'); END
CREATE TRIGGER channel_pins_insert_stamped BEFORE INSERT ON channel_pins WHEN (SELECT version FROM messages WHERE seq = NEW.seq) IS NOT (SELECT version FROM channel_head WHERE id = 1) BEGIN SELECT RAISE(ABORT, 'channel_pins: stamp the message with the bumped head version first'); END
CREATE TRIGGER channel_pins_no_update BEFORE UPDATE ON channel_pins BEGIN SELECT RAISE(ABORT, 'channel_pins: delete and insert instead of update'); END
| 字段 | 类型 | 约束 | 含义 | 迁移 |
|---|---|---|---|---|
| PKFKseq | INTEGER | PK · FK→messages.seq | 所属消息序号,FK 到 messages。 | 1 |
| PKposition | INTEGER | PK · CHECK(position >= 0) | 该消息附件的展示顺序,从 0 起。 | 1 |
| kind | TEXT | NOT NULL · CHECK(kind IN ('image','file')) | image / file。 | 1 |
| object_key | TEXT | NOT NULL · CHECK(length(object_key) BETWEEN 1 AND 512) | R2 object key,不是公开 URL。 | 1 |
| mime | TEXT | NOT NULL · CHECK(length(mime) BETWEEN 1 AND 127) | 文件 MIME 类型。 | 1 |
| size | INTEGER | NOT NULL · CHECK(size >= 0) | 文件字节数。 | 1 |
| width | INTEGER | CHECK(width IS NULL OR width > 0) | 图片宽度;不适用或未知为 NULL。 | 1 |
| height | INTEGER | CHECK(height IS NULL OR height > 0) | 图片高度;不适用或未知为 NULL。 | 1 |
| 字段 | 类型 | 约束 | 含义 | 迁移 |
|---|---|---|---|---|
| PKFKseq | INTEGER | PK · FK→messages.seq | 所属消息序号,FK 到 messages。 | 1 |
| PKuser_id | TEXT | PK | 被提及的真人用户 ID。 | 1 |
| 字段 | 类型 | 约束 | 含义 | 迁移 |
|---|---|---|---|---|
| PKFKseq | INTEGER | PK · FK→messages.seq | 被回应消息序号,FK 到 messages。 | 1 |
| PKuser_id | TEXT | PK | 作出回应的真人用户 ID。 | 1 |
| PKemoji | TEXT | PK · CHECK(length(emoji) BETWEEN 1 AND 64) | 回应表情文本。 | 1 |
| created_at | TEXT | NOT NULL | 回应时间,ISO TEXT。 | 1 |
CREATE TRIGGER message_reactions_delete_stamped BEFORE DELETE ON message_reactions WHEN (SELECT version FROM messages WHERE seq = OLD.seq) IS NOT (SELECT version FROM channel_head WHERE id = 1) BEGIN SELECT RAISE(ABORT, 'message_reactions: stamp the message with the bumped head version first'); END
CREATE TRIGGER message_reactions_insert_stamped BEFORE INSERT ON message_reactions WHEN (SELECT version FROM messages WHERE seq = NEW.seq) IS NOT (SELECT version FROM channel_head WHERE id = 1) BEGIN SELECT RAISE(ABORT, 'message_reactions: stamp the message with the bumped head version first'); END
CREATE TRIGGER message_reactions_no_update BEFORE UPDATE ON message_reactions BEGIN SELECT RAISE(ABORT, 'message_reactions: delete and insert instead of update'); END
| 字段 | 类型 | 约束 | 含义 | 迁移 |
|---|---|---|---|---|
| PKseq | INTEGER | PK · CHECK(seq >= 1) | 频道内插入序,从 1 起,身份列不可改;用于排序与未读。 | 1 |
| UQmessage_id | TEXT | UQ · NOT NULL | 客户端生成 ULID,消息重试的幂等键。 | 1 |
| version | INTEGER | NOT NULL · CHECK(version >= 1) | 本条最近变更的 head.version;索引供增量查询。 | 1 |
| sender_id | TEXT | NOT NULL | 发送者真人 user_id,Bot 只通过分享卡片出现。 | 1 |
| kind | TEXT | NOT NULL · CHECK(kind IN ('text','card','system')) | text / card / system,封闭集合。 | 1 |
| body | TEXT | CHECK(body IS NULL OR length(body) BETWEEN 1 AND 4000) | 文本正文;text 未删除时必填,软删时为 NULL。 | 1 |
| card_kind | TEXT | CHECK(card_kind IS NULL OR card_kind IN ('position','order','bot_run','strategy','market')) | 卡片种类,非卡片为 NULL。 | 1 |
| card_ref | TEXT | 不透明外部引用;strategy 必须是已发布快照 ID。 | 1 | |
| card_payload_json | TEXT | 展示快照,不查询 JSON;软删清 NULL。 | 1 | |
| FKreply_to_seq | INTEGER | FK→messages.seq · CHECK(reply_to_seq IS NULL OR reply_to_seq < seq) | 被回复的较早消息序号;无回复为 NULL,同实例自引用 FK。 | 1 |
| created_at | TEXT | NOT NULL | 创建时间,ISO TEXT。 | 1 |
| edited_at | TEXT | 最近编辑时间;未编辑时 NULL。 | 1 | |
| deleted_at | TEXT | 软删墓碑;未删除时 NULL。 | 1 |
CREATE TRIGGER messages_append_at_head BEFORE INSERT ON messages WHEN NEW.seq IS NOT (SELECT last_seq FROM channel_head WHERE id = 1) OR NEW.version IS NOT (SELECT version FROM channel_head WHERE id = 1) BEGIN SELECT RAISE(ABORT, 'messages: bump channel_head first, then insert seq = last_seq and version = head version'); END
CREATE TRIGGER messages_permanent BEFORE DELETE ON messages BEGIN SELECT RAISE(ABORT, 'messages are permanent; soft-delete clears the body'); END
CREATE TRIGGER messages_stamp_on_change BEFORE UPDATE ON messages WHEN NEW.seq IS NOT OLD.seq OR NEW.message_id IS NOT OLD.message_id OR NEW.sender_id IS NOT OLD.sender_id OR NEW.kind IS NOT OLD.kind OR NEW.created_at IS NOT OLD.created_at OR NEW.version < OLD.version OR NEW.version IS NOT (SELECT version FROM channel_head WHERE id = 1) BEGIN SELECT RAISE(ABORT, 'messages: identity columns are immutable and every change must carry the bumped head version'); END
| 字段 | 类型 | 约束 | 含义 | 迁移 |
|---|---|---|---|---|
| PKversion | INTEGER | PK | 已应用的 schema 版本号,从 1 起连续追加。 | 1 |
| applied_at | TEXT | NOT NULL | 应用迁移时间,ISO TEXT。 | 1 |