feat(db,contracts): W5-C subscription units migration 0025, contracts and mapping
- subscription: status pending (TMDB not ready: searchable/evaluable, not completable), last_searched_at / next_search_at - content_unit (season): episode_total ledger with source / lock / timestamp - acquisition_plan: unit_keys (Slot × unit reservation) and file_selection; drop the one-in-flight-per-slot unique index (checked under slot row lock per unit instead) - new tables slot_deferral, slot_unit (per-unit quality ledger), subscription_history - contracts: SubscriptionParams, SubscriptionHistory, SlotDeferral, SlotUnit, SubscriptionProgress, EpisodeTotalSet, CandidateEvaluation pri/trace/unitKeys and decision deferred; endpoints subscriptions.search/progress/setEpisodeTotal and subscription-history - mapping: evaluation messages via describeViolation, pri/trace passthrough Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d162d464c4
commit
1dd460d359
9 files changed
+405
-34
No files matched your search
@@ -0,0 +1,105 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts";
|
||||
|
||||
// W5-C 订阅增强(docs/plan/05-subscription-gap.md §3):
|
||||
// - subscription:pending 状态(TMDB 未就绪:可搜索/评估,不可完成);主动搜索的上次/下次时间;
|
||||
// - content_unit(季):总集数账本 episode_total(只升;回退不低于已覆盖最高集;可人工锁定);
|
||||
// - acquisition_plan:unit_keys 记录计划预留的内容单元(Slot×单元预留,等价 MP note);file_selection 记录季包选文件;
|
||||
// 去掉"同 Slot 只允许一个在途计划"的唯一索引,改为服务端在 Slot 行锁内按单元不相交校验;
|
||||
// - slot_deferral:Slot 忙(单元在途冲突 / 电影 Slot 在途)时到达的发布,计划结束后重评;
|
||||
// - slot_unit:分集质量账本(Slot × 内容单元 → 当前资产 + 质量摘要 + 阶梯 pri),升级按单元替换;
|
||||
// - subscription_history:订阅完成快照(完成后保留订阅行,可重新激活)。
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`
|
||||
alter table subscription drop constraint subscription_status_check;
|
||||
alter table subscription add constraint subscription_status_check ${oneOf("status", ["active", "paused", "pending", "completed", "cancelled"])};
|
||||
alter table subscription add column last_searched_at timestamptz;
|
||||
alter table subscription add column next_search_at timestamptz;
|
||||
create index subscription_next_search_idx on subscription (next_search_at) where next_search_at is not null;
|
||||
|
||||
alter table content_unit add column episode_total integer check (episode_total is null or episode_total >= 0);
|
||||
alter table content_unit add column episode_total_source text ${oneOf("episode_total_source", ["tmdb", "injected", "manual", "covered_floor", "previous"])};
|
||||
alter table content_unit add column episode_total_locked boolean not null default false;
|
||||
alter table content_unit add column episode_total_at timestamptz;
|
||||
|
||||
alter table acquisition_plan add column unit_keys text[] not null default '{}'::text[];
|
||||
alter table acquisition_plan add column file_selection jsonb;
|
||||
drop index if exists acquisition_plan_slot_active_key;
|
||||
create index acquisition_plan_slot_active_idx on acquisition_plan (slot_id) where status in ('reserved', 'submitted');
|
||||
|
||||
${entityTable(
|
||||
"slot_deferral",
|
||||
`
|
||||
slot_id uuid not null references acquisition_slot (id) on delete cascade,
|
||||
source_release_id uuid not null references source_release (id) on delete cascade,
|
||||
reason text not null,
|
||||
unit_keys text[] not null default '{}'::text[],
|
||||
attempts integer not null default 1,
|
||||
constraint slot_deferral_key unique (slot_id, source_release_id)`,
|
||||
)}
|
||||
|
||||
${entityTable(
|
||||
"slot_unit",
|
||||
`
|
||||
slot_id uuid not null references acquisition_slot (id) on delete cascade,
|
||||
content_unit_id uuid not null references content_unit (id) on delete cascade,
|
||||
media_asset_id uuid references media_asset (id) on delete set null,
|
||||
plan_id uuid references acquisition_plan (id) on delete set null,
|
||||
quality jsonb not null default '{}'::jsonb,
|
||||
pri integer,
|
||||
constraint slot_unit_key unique (slot_id, content_unit_id)`,
|
||||
)}
|
||||
create index slot_unit_asset_idx on slot_unit (media_asset_id) where media_asset_id is not null;
|
||||
|
||||
${entityTable(
|
||||
"subscription_history",
|
||||
`
|
||||
subscription_id uuid references subscription (id) on delete set null,
|
||||
media_entity_id uuid references media_entity (id) on delete set null,
|
||||
season_unit_id uuid references content_unit (id) on delete set null,
|
||||
media_kind text not null,
|
||||
mode text not null,
|
||||
reason text not null,
|
||||
snapshot jsonb not null default '{}'::jsonb,
|
||||
progress jsonb not null default '{}'::jsonb,
|
||||
completed_at timestamptz not null default now()`,
|
||||
)}
|
||||
create index subscription_history_entity_idx on subscription_history (media_entity_id, completed_at desc);
|
||||
create index subscription_history_subscription_idx on subscription_history (subscription_id);
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`
|
||||
${dropTables(["subscription_history", "slot_unit", "slot_deferral"])}
|
||||
drop index if exists acquisition_plan_slot_active_idx;
|
||||
-- 回到"每 Slot 一个在途":同 Slot 多个在途时只保留最早的一个,其余取消
|
||||
update acquisition_plan p set status = 'cancelled', status_reason = 'migration_down:multi_inflight'
|
||||
where p.status in ('reserved', 'submitted')
|
||||
and exists (select 1 from acquisition_plan q
|
||||
where q.slot_id = p.slot_id and q.status in ('reserved', 'submitted')
|
||||
and (q.created_at, q.id) < (p.created_at, p.id));
|
||||
create unique index acquisition_plan_slot_active_key on acquisition_plan (slot_id) where status in ('reserved', 'submitted');
|
||||
alter table acquisition_plan drop column if exists file_selection;
|
||||
alter table acquisition_plan drop column if exists unit_keys;
|
||||
alter table content_unit drop column if exists episode_total_at;
|
||||
alter table content_unit drop column if exists episode_total_locked;
|
||||
alter table content_unit drop column if exists episode_total_source;
|
||||
alter table content_unit drop column if exists episode_total;
|
||||
drop index if exists subscription_next_search_idx;
|
||||
alter table subscription drop column if exists next_search_at;
|
||||
alter table subscription drop column if exists last_searched_at;
|
||||
update subscription set status = 'active' where status = 'pending';
|
||||
alter table subscription drop constraint subscription_status_check;
|
||||
alter table subscription add constraint subscription_status_check ${oneOf("status", ["active", "paused", "completed", "cancelled"])};
|
||||
`,
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user