From 92bce3b6012e7b7d12eb8b4898809899acfdf3ec Mon Sep 17 00:00:00 2001 From: root Date: Sat, 3 Oct 2026 23:31:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(identity):=20=E6=94=B9=20TMDB=20=E6=98=A0?= =?UTF-8?q?=E5=B0=84=E5=88=A0=E6=97=A7=E4=BD=9C=E5=93=81=E7=9A=84=20tmdb?= =?UTF-8?q?=20=E5=88=AB=E5=90=8D;=E5=90=88=E5=B9=B6=E4=BD=9C=E5=93=81?= =?UTF-8?q?=E6=97=B6=E5=86=85=E5=AE=B9=E5=8D=95=E5=85=83=E8=BF=81=E5=88=B0?= =?UTF-8?q?=E7=9B=AE=E6=A0=87=E4=BD=9C=E5=93=81(M3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改映射:没锁的 tmdb 来源作品别名删除,人工 / 站点别名保留,刷新后按新详情重新登记。 合并:被合并作品的单元改归目标作品(单元级别名一并改),与目标同季同集的按重复单元合并、 所有引用改指向保留的那条;目标是电影时源单元退役。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei --- .../service/identity-admin.ts | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/sources-identity/service/identity-admin.ts b/apps/server/src/modules/sources-identity/service/identity-admin.ts index 441d8f6a..7b02c6ff 100644 --- a/apps/server/src/modules/sources-identity/service/identity-admin.ts +++ b/apps/server/src/modules/sources-identity/service/identity-admin.ts @@ -33,6 +33,7 @@ import { clearTmdbMetadata } from "./metadata.ts"; import { metadataLanguages, metadataTextOptions } from "./metadata-rich.ts"; import { recordAttempt } from "./resolve.ts"; import { backfillRecognizeCache } from "./tmdb-recognize-cache.ts"; +import { mergeDuplicateUnits } from "./unit-merge.ts"; // 人工纠错:指定来源资源身份(加锁)、改作品外部映射(新增修订、旧映射退役)、合并作品(旧 ID 跳转)。 // 所有变更留 identity_revision / resolution_attempt / audit_event,不直接覆盖历史。 @@ -314,6 +315,15 @@ export async function setEntityIdentity(rt: SourcesRuntime, entityId: string, in .execute(); for (const o of derived) await retireIdentity(trx, o, actor, "tmdb_mapping_changed"); await clearTmdbMetadata(trx, id, rt.now()); + // 旧 TMDB 条目带来的别名(没锁的)删除:留着会让旧作品的同名资源继续本地召回到这里;人工 / 站点别名保留, + // 刷新后按新详情重新登记 tmdb 别名 + await trx + .deleteFrom("title_alias") + .where("media_entity_id", "=", id) + .where("content_unit_id", "is", null) + .where("source", "=", "tmdb") + .where("locked", "=", false) + .execute(); } await audit( trx, @@ -414,7 +424,7 @@ export async function updateExternalIdentity( /** * 合并重复作品:from → into。旧 ID 保留(status=merged + merged_into_id)并写 identity_redirect; - * 本模块的引用(来源资源、别名、待识别池、外部身份)迁到目标;其它模块的表按 merged_into_id 跳转解析。 + * 本模块的引用(来源资源、别名、待识别池、外部身份、内容单元)迁到目标;其它模块的表按 merged_into_id 跳转解析。 */ export async function mergeEntities( rt: SourcesRuntime, @@ -483,6 +493,35 @@ export async function mergeEntities( select ${intoResolved}, null, alias, normalized, language, kind, source, locked from title_alias where media_entity_id = ${fromId} and content_unit_id is null on conflict (media_entity_id, content_unit_id, normalized, kind) do nothing`.execute(trx); + // 内容单元随作品迁到目标(M3):单元改归目标作品;与目标已有单元同季同集的 + // 按重复单元合并(所有引用改指向保留的那条,见 unit-merge.ts)。目标是电影时源作品的单元退役 + const into = await trx + .selectFrom("media_entity") + .select(["kind"]) + .where("id", "=", intoResolved) + .executeTakeFirstOrThrow(); + if (into.kind === "movie") { + await trx.updateTable("content_unit").set({ status: "retired" }).where("media_entity_id", "=", fromId).execute(); + } else { + const moved = ( + await trx + .updateTable("content_unit") + .set({ media_entity_id: intoResolved }) + .where("media_entity_id", "=", fromId) + .returning(["id"]) + .execute() + ).map((u) => u.id); + if (moved.length) { + // 单元级元数据 / 覆盖只挂 content_unit_id,随单元走;单元级别名还带作品 ID,一并改 + await trx + .updateTable("title_alias") + .set({ media_entity_id: intoResolved }) + .where("media_entity_id", "=", fromId) + .where("content_unit_id", "in", moved) + .execute(); + await mergeDuplicateUnits(trx, intoResolved); + } + } await trx .updateTable("source_release") .set({ media_entity_id: intoResolved })