feat(contracts): ad.updated/ad.deleted 事件类型与 ad 实体映射
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,11 @@
|
|||||||
import { ErrorCodes, type ErrorCode } from '../errors.js';
|
import { ErrorCodes, type ErrorCode } from '../errors.js';
|
||||||
import type { components } from '../generated/openapi.js';
|
import type { components } from '../generated/openapi.js';
|
||||||
import {
|
import {
|
||||||
|
isTombstoneOutboxType,
|
||||||
OUTBOX_EVENT_TYPES,
|
OUTBOX_EVENT_TYPES,
|
||||||
type OutboxEvent,
|
type OutboxEvent,
|
||||||
type PhotoPublishedPayload,
|
type PhotoPublishedPayload,
|
||||||
|
syncEntityTypeOfOutboxType,
|
||||||
} from '../events.js';
|
} from '../events.js';
|
||||||
|
|
||||||
// 类型恒等辅助:仅当 X 与 Y 是完全相同的类型(双向兼容,多/少一个成员都为 false)。
|
// 类型恒等辅助:仅当 X 与 Y 是完全相同的类型(双向兼容,多/少一个成员都为 false)。
|
||||||
@@ -60,7 +62,7 @@ describe('OutboxEvent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('OUTBOX_EVENT_TYPES', () => {
|
describe('OUTBOX_EVENT_TYPES', () => {
|
||||||
it('恰好包含全部 9 个事件类型字符串', () => {
|
it('恰好包含全部 11 个事件类型字符串', () => {
|
||||||
expect([...OUTBOX_EVENT_TYPES]).toEqual([
|
expect([...OUTBOX_EVENT_TYPES]).toEqual([
|
||||||
'photo.published',
|
'photo.published',
|
||||||
'photo.hidden',
|
'photo.hidden',
|
||||||
@@ -71,6 +73,8 @@ describe('OUTBOX_EVENT_TYPES', () => {
|
|||||||
'category.deleted',
|
'category.deleted',
|
||||||
'asset.updated',
|
'asset.updated',
|
||||||
'slot.updated',
|
'slot.updated',
|
||||||
|
'ad.updated',
|
||||||
|
'ad.deleted',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,6 +87,16 @@ describe('OUTBOX_EVENT_TYPES', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('syncEntityTypeOfOutboxType / isTombstoneOutboxType', () => {
|
||||||
|
// ad.* 映射与墓碑(广告位轮播 2026-09-28)
|
||||||
|
it('ad.updated/ad.deleted → entityType ad;ad.deleted 为墓碑', () => {
|
||||||
|
expect(syncEntityTypeOfOutboxType('ad.updated')).toBe('ad');
|
||||||
|
expect(syncEntityTypeOfOutboxType('ad.deleted')).toBe('ad');
|
||||||
|
expect(isTombstoneOutboxType('ad.deleted')).toBe(true);
|
||||||
|
expect(isTombstoneOutboxType('ad.updated')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('openapi Error.code enum ↔ errors.ts ErrorCodes 同步', () => {
|
describe('openapi Error.code enum ↔ errors.ts ErrorCodes 同步', () => {
|
||||||
it('两侧 code 联合类型双向恒等(1:1)', () => {
|
it('两侧 code 联合类型双向恒等(1:1)', () => {
|
||||||
const apiEqualsErrors: Equal<ApiErrorCode, ErrorCode> = true;
|
const apiEqualsErrors: Equal<ApiErrorCode, ErrorCode> = true;
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ export interface OutboxEvent<T = unknown> {
|
|||||||
| 'category.updated'
|
| 'category.updated'
|
||||||
| 'category.deleted'
|
| 'category.deleted'
|
||||||
| 'asset.updated'
|
| 'asset.updated'
|
||||||
| 'slot.updated';
|
| 'slot.updated'
|
||||||
|
| 'ad.updated'
|
||||||
|
| 'ad.deleted';
|
||||||
albumId: string;
|
albumId: string;
|
||||||
photoId?: string;
|
photoId?: string;
|
||||||
entityVersion: number; // 单调递增,P2 inbox 消费依据
|
entityVersion: number; // 单调递增,P2 inbox 消费依据
|
||||||
@@ -162,6 +164,27 @@ export interface SlotSettingsPayload {
|
|||||||
reactionsEnabled?: boolean; // 默认 true(UI-04;false 仅隐藏气泡)
|
reactionsEnabled?: boolean; // 默认 true(UI-04;false 仅隐藏气泡)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// —— 广告位(2026-09-28 设计 §4)——
|
||||||
|
// ad.updated 载荷:ads_ov 投影重建所需的**全量快照**——每次管理面变更整行替换
|
||||||
|
// (slot.updated 同款);adId 供发送器推导 entityId(outbox 表无此列)。
|
||||||
|
// albumIds null=全局投放;enabled=false 消费侧按「删投影行」处理(无墓碑下线)。
|
||||||
|
export interface AdUpdatedPayload {
|
||||||
|
adId: string;
|
||||||
|
imageObjectKey: string;
|
||||||
|
linkUrl: string;
|
||||||
|
enabled: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
albumIds: string[] | null;
|
||||||
|
createdAt: string; // ISO UTC;ov 排序 tie-break(设计 §5.2)
|
||||||
|
entityVersion: number;
|
||||||
|
}
|
||||||
|
// ad.deleted 载荷:墓碑(ov 删 ads_ov 行且不被旧事件复活);cn 行已删,
|
||||||
|
// entityVersion 为删除时刻终值(末次版本+1)。
|
||||||
|
export interface AdDeletedPayload {
|
||||||
|
adId: string;
|
||||||
|
entityVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
// 运行时校验用:全部合法事件类型字符串。satisfies 保证 数组 ⊆ 接口 union;
|
// 运行时校验用:全部合法事件类型字符串。satisfies 保证 数组 ⊆ 接口 union;
|
||||||
// __tests__/events.test.ts 的 Equal 断言反向锁定 union ⊆ 数组,两侧永远 1:1。
|
// __tests__/events.test.ts 的 Equal 断言反向锁定 union ⊆ 数组,两侧永远 1:1。
|
||||||
export const OUTBOX_EVENT_TYPES = [
|
export const OUTBOX_EVENT_TYPES = [
|
||||||
@@ -174,6 +197,8 @@ export const OUTBOX_EVENT_TYPES = [
|
|||||||
'category.deleted',
|
'category.deleted',
|
||||||
'asset.updated',
|
'asset.updated',
|
||||||
'slot.updated',
|
'slot.updated',
|
||||||
|
'ad.updated',
|
||||||
|
'ad.deleted',
|
||||||
] as const satisfies readonly OutboxEvent['type'][];
|
] as const satisfies readonly OutboxEvent['type'][];
|
||||||
|
|
||||||
// ── outbox 类型 → 内部同步线格式(openapi SyncEvent.entityType)的映射 ──
|
// ── outbox 类型 → 内部同步线格式(openapi SyncEvent.entityType)的映射 ──
|
||||||
@@ -186,7 +211,8 @@ export type SyncEntityType =
|
|||||||
| 'category'
|
| 'category'
|
||||||
| 'photo'
|
| 'photo'
|
||||||
| 'asset'
|
| 'asset'
|
||||||
| 'slot-state';
|
| 'slot-state'
|
||||||
|
| 'ad';
|
||||||
|
|
||||||
export function syncEntityTypeOfOutboxType(
|
export function syncEntityTypeOfOutboxType(
|
||||||
type: OutboxEvent['type'],
|
type: OutboxEvent['type'],
|
||||||
@@ -201,6 +227,9 @@ export function syncEntityTypeOfOutboxType(
|
|||||||
return 'asset';
|
return 'asset';
|
||||||
case 'slot.updated':
|
case 'slot.updated':
|
||||||
return 'slot-state';
|
return 'slot-state';
|
||||||
|
case 'ad.updated':
|
||||||
|
case 'ad.deleted':
|
||||||
|
return 'ad';
|
||||||
case 'photo.published':
|
case 'photo.published':
|
||||||
case 'photo.hidden':
|
case 'photo.hidden':
|
||||||
case 'photo.deleted':
|
case 'photo.deleted':
|
||||||
@@ -211,5 +240,9 @@ export function syncEntityTypeOfOutboxType(
|
|||||||
|
|
||||||
/** 墓碑类事件(副本删除且不被旧事件复活,A26)。 */
|
/** 墓碑类事件(副本删除且不被旧事件复活,A26)。 */
|
||||||
export function isTombstoneOutboxType(type: OutboxEvent['type']): boolean {
|
export function isTombstoneOutboxType(type: OutboxEvent['type']): boolean {
|
||||||
return type === 'category.deleted' || type === 'photo.deleted';
|
return (
|
||||||
|
type === 'category.deleted' ||
|
||||||
|
type === 'photo.deleted' ||
|
||||||
|
type === 'ad.deleted'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user