feat(api): embed slots with ov replica and public feed
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,7 @@ describe('OutboxEvent', () => {
|
||||
});
|
||||
|
||||
describe('OUTBOX_EVENT_TYPES', () => {
|
||||
it('恰好包含全部 8 个事件类型字符串', () => {
|
||||
it('恰好包含全部 9 个事件类型字符串', () => {
|
||||
expect([...OUTBOX_EVENT_TYPES]).toEqual([
|
||||
'photo.published',
|
||||
'photo.hidden',
|
||||
@@ -70,6 +70,7 @@ describe('OUTBOX_EVENT_TYPES', () => {
|
||||
'category.updated',
|
||||
'category.deleted',
|
||||
'asset.updated',
|
||||
'slot.updated',
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -50,3 +50,7 @@ export const EMBED_POLL_INTERVAL_MS = 30000;
|
||||
export const EMBED_POLL_MAX_FAILURES = 5;
|
||||
/** 轮询失败退避倍数(连续失败达上限后拉长间隔,成功即恢复正常间隔)。 */
|
||||
export const EMBED_POLL_BACKOFF_MULTIPLIER = 5;
|
||||
/** 展示位公开端点基础限速:slotToken+IP 固定窗(EMBED_WINDOW_SECONDS)内请求数上限(含 304)。 */
|
||||
export const EMBED_WINDOW_REQUESTS = 120;
|
||||
/** 展示位限速固定窗秒数(轮询 30s/次 + 页面重载突发,窗口内余量充足)。 */
|
||||
export const EMBED_WINDOW_SECONDS = 60;
|
||||
|
||||
@@ -12,7 +12,8 @@ export interface OutboxEvent<T = unknown> {
|
||||
| 'album.updated'
|
||||
| 'category.updated'
|
||||
| 'category.deleted'
|
||||
| 'asset.updated';
|
||||
| 'asset.updated'
|
||||
| 'slot.updated';
|
||||
albumId: string;
|
||||
photoId?: string;
|
||||
entityVersion: number; // 单调递增,P2 inbox 消费依据
|
||||
@@ -122,6 +123,33 @@ export interface AssetUpdatedPayload {
|
||||
entityVersion: number;
|
||||
}
|
||||
|
||||
// —— 展示位(P4b Task 2)——
|
||||
// slot.updated 载荷:ov 副本(embed_slots_ov)重建展示位所需的**全量快照**——
|
||||
// 展示位无墓碑(无删除端点,enabled=false 即下线),每次管理面变更整行替换。
|
||||
// slotId 供发送器推导 entityId(同 categoryId/assetId 约定,outbox 表无此列);
|
||||
// albumId/albumPublicToken 为绑定相册双标识(未绑定时均 null):ov 副本外键
|
||||
// 落 albumPublicToken(公开端点以它关联 albums_ov),照片按副本实时查询、
|
||||
// 不随载荷携带(新照片发布走 photo.published,ETag 四元组含发布时间)。
|
||||
export interface SlotStateUpdatedPayload {
|
||||
slotId: string;
|
||||
publicToken: string;
|
||||
enabled: boolean;
|
||||
albumId: string | null;
|
||||
albumPublicToken: string | null;
|
||||
configVersion: number;
|
||||
settings: SlotSettingsPayload;
|
||||
entityVersion: number;
|
||||
}
|
||||
|
||||
/** 展示位设置载荷(契约 EmbedSlotSettings 的载荷形状;字段恒齐全)。 */
|
||||
export interface SlotSettingsPayload {
|
||||
position: string; // left|right
|
||||
size: string; // sm|md
|
||||
intervalMs: number;
|
||||
lang: string; // auto|zh-CN|en
|
||||
collapsedDefault: boolean;
|
||||
}
|
||||
|
||||
// 运行时校验用:全部合法事件类型字符串。satisfies 保证 数组 ⊆ 接口 union;
|
||||
// __tests__/events.test.ts 的 Equal 断言反向锁定 union ⊆ 数组,两侧永远 1:1。
|
||||
export const OUTBOX_EVENT_TYPES = [
|
||||
@@ -133,13 +161,14 @@ export const OUTBOX_EVENT_TYPES = [
|
||||
'category.updated',
|
||||
'category.deleted',
|
||||
'asset.updated',
|
||||
'slot.updated',
|
||||
] as const satisfies readonly OutboxEvent['type'][];
|
||||
|
||||
// ── outbox 类型 → 内部同步线格式(openapi SyncEvent.entityType)的映射 ──
|
||||
// 发送器(services/sync)与 cn reconcile 端点共用,单一实现防两侧漂移;
|
||||
// exhaustive switch:新增事件类型漏映射会在此编译报错。
|
||||
|
||||
/** 内部同步实体类型(对齐 openapi SyncEntityType;slot-state 为预留)。 */
|
||||
/** 内部同步实体类型(对齐 openapi SyncEntityType;slot-state 自 P4b Task 2 起 landing)。 */
|
||||
export type SyncEntityType =
|
||||
| 'album'
|
||||
| 'category'
|
||||
@@ -158,6 +187,8 @@ export function syncEntityTypeOfOutboxType(
|
||||
return 'category';
|
||||
case 'asset.updated':
|
||||
return 'asset';
|
||||
case 'slot.updated':
|
||||
return 'slot-state';
|
||||
case 'photo.published':
|
||||
case 'photo.hidden':
|
||||
case 'photo.deleted':
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
-- P4b Task 2:展示位域(embed_slots/embed_slot_members)。
|
||||
-- 校对手决:Prisma 生成 DDL 固定 utf8mb4_unicode_ci,逐表改写为库级约定的
|
||||
-- utf8mb4_0900_bin(publicToken 的大小写敏感唯一语义依赖它,同 albums 口径)。
|
||||
-- · embed_slots 双版本位:configVersion(嵌入端可见配置版本,任一管理面
|
||||
-- 变更递增)/entityVersion(slot.updated 同步事件版本);无删除端点,
|
||||
-- enabled=false 即下线。settings 为归一化 EmbedSlotSettings 全集 JSON。
|
||||
-- · embed_slot_members:授权成员表**建而未用**(P4b 权限简化 admin-only,
|
||||
-- 登记偏差;P5 授权面按需启用)。
|
||||
-- · 两表均扁平字符串外键(albumId/userId/slotId 无 FK 约束,同相册域约定)。
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `embed_slots` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`publicToken` VARCHAR(32) NOT NULL,
|
||||
`name` VARCHAR(64) NOT NULL,
|
||||
`enabled` BOOLEAN NOT NULL DEFAULT true,
|
||||
`albumId` VARCHAR(64) NULL,
|
||||
`configVersion` INTEGER NOT NULL DEFAULT 1,
|
||||
`settings` JSON NOT NULL,
|
||||
`entityVersion` INTEGER NOT NULL DEFAULT 1,
|
||||
`createdAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
`updatedAt` DATETIME(6) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `embed_slots_publicToken_key`(`publicToken`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `embed_slot_members` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`slotId` VARCHAR(64) NOT NULL,
|
||||
`userId` VARCHAR(64) NOT NULL,
|
||||
`permission` VARCHAR(32) NOT NULL,
|
||||
`createdAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
|
||||
INDEX `embed_slot_members_userId_idx`(`userId`),
|
||||
UNIQUE INDEX `embed_slot_members_slotId_userId_key`(`slotId`, `userId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
|
||||
@@ -332,3 +332,33 @@ model user_sync_state {
|
||||
runCount Int @default(0) // 已启动批次数(含失败),驱动周期全量对账
|
||||
updatedAt DateTime @updatedAt @db.DateTime(6)
|
||||
}
|
||||
// —— 展示位域(P4b Task 2,悬浮轮播组件挂载点)——
|
||||
// cn 权威实体:admin 建槽/绑定相册,经 outbox slot.updated 事件同步 ov 副本
|
||||
// (embed_slots_ov,载荷全量快照)。扁平字符串外键(albumId 无 relation/FK,
|
||||
// 同相册域约定)。双版本位:configVersion 是「嵌入端可见」的配置版本(任一
|
||||
// 管理面变更递增,公开端 ETag 组成部分);entityVersion 是同步版本(同相册
|
||||
// 模式,事务内自增)。无删除端点:enabled=false 即下线(软停用)。
|
||||
// embed_slot_members:授权成员表**建而未用**(P4b 权限简化为 admin-only——
|
||||
// 需求「创建者只操作被授权展示位」的授权面登记偏差,P5 按需出 API/UI)。
|
||||
model embed_slots {
|
||||
id String @id @default(cuid())
|
||||
publicToken String @unique @db.VarChar(32) // 嵌入代码 data-slot 引用(24 字符 [0-9a-z],同相册令牌口径)
|
||||
name String @db.VarChar(64)
|
||||
enabled Boolean @default(true)
|
||||
albumId String? @db.VarChar(64) // 绑定相册;null=未绑定(公开端 404)
|
||||
configVersion Int @default(1) // 任一管理面变更递增(嵌入端整体重置渲染依据)
|
||||
settings Json // EmbedSlotSettings 归一化全集(字段恒齐全)
|
||||
entityVersion Int @default(1) // slot.updated 事件版本(同 photos 模式)
|
||||
createdAt DateTime @default(now()) @db.DateTime(6)
|
||||
updatedAt DateTime @updatedAt @db.DateTime(6)
|
||||
}
|
||||
model embed_slot_members {
|
||||
id String @id @default(cuid())
|
||||
slotId String @db.VarChar(64)
|
||||
userId String @db.VarChar(64)
|
||||
permission String @db.VarChar(32) // manage|view(P4b 未用,P5 授权面按需启用)
|
||||
createdAt DateTime @default(now()) @db.DateTime(6)
|
||||
|
||||
@@unique([slotId, userId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
-- P4b Task 2:展示位副本表(cn embed_slots 的展示投影,slot-state 事件应用)。
|
||||
-- 校对手决:Prisma 生成 DDL 固定 utf8mb4_unicode_ci,改写为库级约定的
|
||||
-- utf8mb4_0900_bin(slotToken 主键/albumPublicToken 的大小写敏感寻址语义
|
||||
-- 依赖它,同 albums_ov.publicToken 口径)。无墓碑:下线是 enabled=false 的
|
||||
-- 全量快照替换(cn 无删除端点)。
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `embed_slots_ov` (
|
||||
`slotToken` VARCHAR(32) NOT NULL,
|
||||
`enabled` BOOLEAN NOT NULL DEFAULT true,
|
||||
`albumPublicToken` VARCHAR(32) NULL,
|
||||
`configVersion` INTEGER NOT NULL DEFAULT 1,
|
||||
`settings` JSON NOT NULL,
|
||||
`entityVersion` INTEGER NOT NULL,
|
||||
`updatedAt` DATETIME(6) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`slotToken`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
|
||||
@@ -162,6 +162,23 @@ model visit_requests {
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
// —— 副本域:embed_slots_ov(cn embed_slots 的展示投影,P4b Task 2)——
|
||||
// slotToken 即 cn embed_slots.publicToken(公开端点按它寻址,稳定不变);
|
||||
// albumPublicToken 为绑定相册的公开令牌(null=未绑定 → 公开端 404)——副本
|
||||
// 外键落公开令牌而非 cn id:嵌入侧只认 token(A22 换绑不认相册身份)。
|
||||
// entityVersion 是 cn 权威版本(应用写入,ov 不自增);settings 为载荷原文
|
||||
// (当前公开响应不含设置——loader 经 data-* 配置,副本位为对账/后续使用保留)。
|
||||
// 无墓碑:下线是 enabled=false 的全量快照替换(cn 无删除端点)。
|
||||
model embed_slots_ov {
|
||||
slotToken String @id @db.VarChar(32)
|
||||
enabled Boolean @default(true)
|
||||
albumPublicToken String? @db.VarChar(32)
|
||||
configVersion Int @default(1)
|
||||
settings Json
|
||||
entityVersion Int
|
||||
updatedAt DateTime @updatedAt @db.DateTime(6)
|
||||
}
|
||||
|
||||
// —— 统计回传队列:outbox_stat_events(ov→cn,Task 9 消费)——
|
||||
// 与 cn outbox_events 同构的发送位:sentAt 为空即重发(中断恢复),statVersion
|
||||
// 让 cn 侧「仅更高版本替换」幂等。total 恒为权威快照值(非增量)。
|
||||
|
||||
Reference in New Issue
Block a user