import { z } from "zod"; import { ActorType, EntityBase, Id, JsonObject, Timestamp } from "./common"; // 身份域:外部身份映射与修订(REQ §7.2) // provider + entityType + externalId 对 accepted 唯一;TMDB movie:123 与 tv:123 是两个身份。 export const IdentityStatus = z.enum(["accepted", "proposed", "conflicted", "retired"]); export type IdentityStatus = z.infer; export const ExternalEntityType = z.enum(["movie", "tv", "season", "episode", "collection", "person"]); export type ExternalEntityType = z.infer; /** 证据等级:未经标注集校准前不展示概率(REQ §5.3) */ export const EvidenceLevel = z.enum(["manual", "rule_confirmed", "multi_evidence", "needs_review"]); export type EvidenceLevel = z.infer; export const IdentityVerifyStatus = z.enum(["unverified", "ok", "not_found", "type_mismatch", "error"]); export type IdentityVerifyStatus = z.infer; export const IdentityProvider = z.string().regex(/^[a-z][a-z0-9_]*$/); export const ExternalIdentity = EntityBase.extend({ mediaEntityId: Id.nullable(), contentUnitId: Id.nullable(), provider: IdentityProvider, entityType: ExternalEntityType, externalId: z.string().min(1), status: IdentityStatus, evidenceLevel: EvidenceLevel, evidence: JsonObject, /** 人工锁:自动匹配/刷新不得改动 */ locked: z.boolean(), revision: z.number().int().min(1), validFrom: Timestamp.nullable(), validUntil: Timestamp.nullable(), verifiedAt: Timestamp.nullable(), /** 404/超时/鉴权失败只记录到这里,不是删除映射的依据 */ verifyStatus: IdentityVerifyStatus, /** 附属字段,如 TMDB episode_group */ extra: JsonObject, }); export type ExternalIdentity = z.infer; const oneTarget = (v: { mediaEntityId?: string | null; contentUnitId?: string | null }) => Number(!!v.mediaEntityId) + Number(!!v.contentUnitId) === 1; export const ExternalIdentityCreate = ExternalIdentity.pick({ mediaEntityId: true, contentUnitId: true, provider: true, entityType: true, externalId: true, status: true, evidenceLevel: true, evidence: true, locked: true, validFrom: true, validUntil: true, extra: true, }) .partial({ mediaEntityId: true, contentUnitId: true, status: true, evidenceLevel: true, evidence: true, locked: true, validFrom: true, validUntil: true, extra: true, }) .refine(oneTarget, { message: "mediaEntityId 与 contentUnitId 必须且只能给一个" }); export type ExternalIdentityCreate = z.infer; /** 变更一律生成 identity_revision;expectedRevision 用于乐观并发 */ export const ExternalIdentityUpdate = z.object({ expectedRevision: z.number().int().min(1), status: IdentityStatus.optional(), evidenceLevel: EvidenceLevel.optional(), evidence: JsonObject.optional(), locked: z.boolean().optional(), validUntil: Timestamp.nullable().optional(), extra: JsonObject.optional(), reason: z.string().max(2000).optional(), }); export type ExternalIdentityUpdate = z.infer; export const IdentityRevisionAction = z.enum([ "created", "status_changed", "relinked", "evidence_updated", "locked", "unlocked", ]); export type IdentityRevisionAction = z.infer; /** 身份修订历史:只追加,不修改 */ export const IdentityRevision = EntityBase.extend({ externalIdentityId: Id, revision: z.number().int().min(1), action: IdentityRevisionAction, fromStatus: IdentityStatus.nullable(), toStatus: IdentityStatus.nullable(), before: JsonObject.nullable(), after: JsonObject, actorType: ActorType, actorId: z.string().nullable(), reason: z.string().nullable(), }); export type IdentityRevision = z.infer; /** 由服务端在身份变更事务内写入;此输入仅供内部仓储使用 */ export const IdentityRevisionCreate = IdentityRevision.pick({ externalIdentityId: true, revision: true, action: true, fromStatus: true, toStatus: true, before: true, after: true, actorType: true, actorId: true, reason: true, }).partial({ fromStatus: true, toStatus: true, before: true, actorId: true, reason: true }); export type IdentityRevisionCreate = z.infer;