feat(subscriptions): overview groups, coverage matrix, multi-version auto-fill, form field normalization (W9-E)

- 0035: subscription.last_progress_at, acquisition_slot.position (primary version)
- GET /subscriptions: server-side groups + counts, card fields, sort/filter, offset cursor
- GET /subscriptions/{id}/coverage: season x episode (movie: edition) x slot matrix, 30s cache
- auto-fill for non-primary slots: inherit/on/off, per-subscription daily budget, upgrade priority
- params normalization with MP/legacy aliases; duplicate 409; pause/resume/resubscribe/batch/fill/slot-order

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 15:54:29 +08:00
1 parent e77fdc6549
commit 410d7fd636
12 files changed
+2158 -252

No files matched your search

+83
View File
@@ -629,6 +629,89 @@ export const Api = {
},
R,
),
/** 总览:在 crud 列表上加分组/排序/筛选,条目带卡片派生字段,另给各分组计数(docs/design/subscriptions.md §1) */
list: ep({
method: "get",
path: "/subscriptions",
scope: "read",
summary:
"订阅总览(分组、进度、各版本摘要、上次更新、下次搜索、站点、规则组;cancelled 只在显式 status=cancelled 时返回)",
query: listQuery({
status: policy.SubscriptionStatus.optional(),
mediaEntityId: Id.optional(),
mediaKind: policy.SubscriptionMediaKind.optional(),
/** 作品标题 / 原名 / 别名 / 未建档检索词,模糊匹配 */
q: z.string().max(200).optional(),
group: policy.SubscriptionGroup.optional(),
sort: policy.SubscriptionSort.optional(),
order: z.enum(["asc", "desc"]).optional(),
siteId: Id.optional(),
ruleGroupId: Id.optional(),
origin: policy.SubscriptionOrigin.optional(),
upgrading: z.enum(["true", "false"]).optional(),
}),
response: policy.SubscriptionOverviewPage,
}),
coverage: ep({
method: "get",
path: "/subscriptions/{id}/coverage",
scope: "read",
summary:
"覆盖矩阵:季 → 集 × 版本目标(电影:内容版本 × 版本目标),每格 have/inflight/missing/skipped;缓存 30s,refresh=true 强制重算",
params: IdParam,
query: z.object({ refresh: z.enum(["true", "false"]).optional() }),
response: policy.SubscriptionCoverage,
}),
pause: ep({
method: "post",
path: "/subscriptions/{id}/pause",
scope: "admin",
summary: "暂停订阅(不评估、不搜索)",
params: IdParam,
response: policy.Subscription,
}),
resume: ep({
method: "post",
path: "/subscriptions/{id}/resume",
scope: "admin",
summary: "恢复已暂停的订阅(TMDB 未就绪回到 pending),立即重评并排上搜索",
params: IdParam,
response: policy.Subscription,
}),
resubscribe: ep({
method: "post",
path: "/subscriptions/{id}/resubscribe",
scope: "admin",
summary: "重新订阅已完成/已取消的订阅(历史保留),立即重评并搜索",
params: IdParam,
response: policy.Subscription,
}),
batch: ep({
method: "post",
path: "/subscriptions/batch",
scope: "admin",
summary: "批量:立即搜索 / 暂停 / 恢复 / 删除(删除 = 取消,写审计)",
body: policy.SubscriptionBatch,
response: policy.SubscriptionBatchResult,
}),
fillSlot: ep({
method: "post",
path: "/subscriptions/{id}/slots/{slotId}/fill",
scope: "admin",
summary: "手动补全某版本目标(不受自动补全开关与补全预算限制)",
params: z.object({ id: Id, slotId: Id }),
body: policy.SlotFillRequest,
response: z.object({ runId: z.string(), deduplicated: z.boolean() }),
}),
slotOrder: ep({
method: "put",
path: "/subscriptions/{id}/slot-order",
scope: "admin",
summary: "版本目标排序(第一个为主版本)",
params: IdParam,
body: policy.SlotOrderSet,
response: z.array(policy.AcquisitionSlot),
}),
search: ep({
method: "post",
path: "/subscriptions/{id}/search",