feat(ov): emergency disable channel independent of sync

P2 Task 5(海外紧急停用入口):
- ov 新增 POST /v1/internal/emergency/disable|enable(InternalSyncGuard
  HMAC,albumToken/albumId 恰好其一、都缺省 400;副本不存在 404
  ALBUM_NOT_FOUND):直写 albums_ov.emergencyDisabled(唯一 ov 本地可写
  位),独立于同步版本链——更高版本 album.updated 重放不解除(e2e 断言
  entityVersion 推进而标志保持),恢复仅经显式 enable;幂等语义
  (alreadyDisabled/wasDisabled 入审计行)
- cn 侧不感知:无对应 cn 状态位、无反向同步(契约描述文档化)
- 审计:pino child(audit=emergency 字段常驻每行)结构化日志——偏差
  登记:计划原文「写 ov 侧日志文件」落地为字段标记而非独立文件通道
  (免新增文件 transport 依赖,P5 运维按字段路由聚合);注入令牌
  EMERGENCY_AUDIT_LOGGER,测试覆写为内存目的地断言字段
- isAlbumVisibleOv 共享判定(status=active && publicEnabled &&
  !emergencyDisabled——镜像 cn 公开语义叠加本地停用位):Task 6 公开
  API / Task 7 WS subscribe 的 404 ALBUM_NOT_PUBLIC 语义单一实现,
  判定矩阵单测锁定
- 契约 [internal] 增 emergency disable/enable 操作与 schema(gen 已跑、
  redocly 通过);e2e:token/id 定位、幂等、enable 恢复、未签名 401、
  均缺省 400、未知相册 404、同步事件不解除

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-18 16:19:02 +08:00
parent 36082c5a74
commit eac8da6d6c
11 changed files with 813 additions and 3 deletions
+122 -1
View File
@@ -51,7 +51,8 @@ tags:
description: 观众匿名互动(点赞批次与浏览上报;ov 域部署,路径前缀不变、域名区分)
- name: internal
description: |
服务间同步接口,HMAC 鉴权,不对匿名开放。共用签名规则:
服务间内部接口(cn↔ov 同步 + ov 紧急停用通道),HMAC 鉴权,不对匿名
开放。共用签名规则:
`X-Sync-Signature` = hex(HMAC-SHA256(原始请求体, SYNC_INTERNAL_KEY)),
`X-Sync-Timestamp` = Unix 秒(与服务端时差 ±300s 内有效,防重放);
签名缺失/不符或时间戳超窗一律拒绝。OpenAPI 无 HMAC 标准 scheme,
@@ -1403,6 +1404,86 @@ paths:
'500':
$ref: '#/components/responses/InternalError'
/internal/emergency/disable:
post:
operationId: emergencyDisable
tags: [internal]
summary: 海外紧急停用相册(ov 域)
description: |
ov 侧独立于同步链路的紧急停用入口:直接置 albums_ov.emergency_disabled
=true(ov 本地写,不经同步版本链——后续任何同步事件不得解除,恢复仅经
/internal/emergency/enable 显式操作)。停用相册在 ov 公开 API/WS 侧按
404 ALBUM_NOT_PUBLIC 语义处理;cn 侧不感知(同步照常)。albumToken 与
albumId 二选一(都缺省 400)。审计:ov 侧结构化日志以 audit=emergency
字段标记(P2 决策:不做独立文件通道,P5 运维按字段路由聚合)。
鉴权见 [internal] tag 描述。
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EmergencyDisableRequest'
responses:
'200':
description: 已停用(幂等:重复停用返回同一结果)。
content:
application/json:
schema:
$ref: '#/components/schemas/EmergencyStatus'
'400':
description: 请求体校验失败(VALIDATION_FAILED;含 albumToken/albumId 均缺省)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册副本不存在(ALBUM_NOT_FOUND——ov 尚未同步该相册)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/internal/emergency/enable:
post:
operationId: emergencyEnable
tags: [internal]
summary: 解除海外紧急停用(ov 域,显式唯一恢复通道)
description: |
唯一能清除 albums_ov.emergency_disabled 的入口(显式操作,任何同步
事件都不会代为解除)。albumToken 与 albumId 二选一(都缺省 400)。
审计同 disable(audit=emergency 字段)。鉴权见 [internal] tag 描述。
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EmergencyEnableRequest'
responses:
'200':
description: 已恢复(幂等:重复启用返回同一结果)。
content:
application/json:
schema:
$ref: '#/components/schemas/EmergencyStatus'
'400':
description: 请求体校验失败(VALIDATION_FAILED;含 albumToken/albumId 均缺省)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册副本不存在(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
components:
securitySchemes:
bearerAuth:
@@ -2842,3 +2923,43 @@ components:
description: 相册摘要清单;空库为空数组。
items:
$ref: '#/components/schemas/AlbumDigest'
EmergencyDisableRequest:
type: object
description: 紧急停用请求(albumToken 与 albumId 恰好提供其一)。
required: [reason]
properties:
albumToken:
type: string
description: 公开令牌定位相册(与 albumId 二选一)。
albumId:
type: string
description: 相册 ID 定位(与 albumToken 二选一)。
reason:
type: string
minLength: 1
maxLength: 500
description: 停用原因(审计留痕)。
EmergencyEnableRequest:
type: object
description: 解除停用请求(albumToken 与 albumId 恰好提供其一)。
properties:
albumToken:
type: string
description: 公开令牌定位相册(与 albumId 二选一)。
albumId:
type: string
description: 相册 ID 定位(与 albumToken 二选一)。
EmergencyStatus:
type: object
description: 紧急停用通道操作结果。
required: [disabled, albumId]
properties:
disabled:
type: boolean
description: 操作后的停用状态(disable 恒 true / enable 恒 false)。
albumId:
type: string
description: 相册 ID。