feat(huya): 协作台账勾选导出 CK 到剪贴板(单点权限 + 逐行审计)+ 工具区固定两行

This commit is contained in:
yml2213
2026-10-16 22:24:08 +08:00
parent 6ef74c8e45
commit 82c59bb4af
9 changed files with 431 additions and 6 deletions
+5
View File
@@ -53,6 +53,8 @@ PERMISSIONS = {
"huya:collab:view": "查看外部 CK 协作台账",
"huya:collab:edit": "编辑外部 CK 协作台账",
"huya:collab:manage": "删除外部 CK 协作台账",
# CK 是凭据:导到剪贴板 = 把凭据带出系统,单独一个点(同 cookie:export 的口径),默认只给运营
"huya:collab:ck_export": "导出外部 CK 协作台账的 CK(写剪贴板)",
# 代理 & 白名单
"proxy:manage": "代理配置管理",
"whitelist:manage": "白名单配置管理",
@@ -105,6 +107,7 @@ ROLE_PERMISSIONS = {
"huya:collab:view",
"huya:collab:edit",
"huya:collab:manage",
"huya:collab:ck_export",
"proxy:manage",
"whitelist:manage",
"whitelist:test",
@@ -125,6 +128,8 @@ ROLE_PERMISSIONS = {
# 客服能看能改(共享表,靠审计留痕),但删除只给运营。
"huya:collab:view",
"huya:collab:edit",
# 注意:**不给** huya:collab:ck_export —— 贴 CK 是「录入」,导出是「把凭据带走」,
# 需要时在权限页单独勾(口径同 huya:cookie:export,见设计文档 §11.8)。
"file:view",
"file:upload",
],
+21
View File
@@ -34,6 +34,8 @@ from ...schemas import (
HuyaExternalOrderActivityQueryOut,
HuyaExternalOrderActivityQueryRequest,
HuyaExternalOrderAuditOut,
HuyaExternalOrderCkExportOut,
HuyaExternalOrderCkExportRequest,
HuyaExternalOrderCkRequest,
HuyaExternalOrderCkStatusRequest,
HuyaExternalOrderCreate,
@@ -375,6 +377,25 @@ def verify_external_orders_ck(
raise _http_error(exc) from exc
@router.post("/external-orders/ck/export", response_model=HuyaExternalOrderCkExportOut)
def export_external_orders_ck(
req: HuyaExternalOrderCkExportRequest,
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:collab:ck_export")),
):
"""勾选多行导出 **CK 明文**(前端写进剪贴板)。
单独一个权限点 ``huya:collab:ck_export``(默认只给运营):贴 CK 是「录入」,
导出是「把凭据带走」。逐行写审计(``huya:collab:ck_export``),但**只记单号**,
不把 CK 本体写进审计;没 CK 的行在 ``skipped`` 里显式回原因。
"""
try:
orders = collab.list_orders_by_ids(db, req.ids)
return collab.export_cks(db, current, orders)
except collab.CollabError as exc:
raise _http_error(exc) from exc
# --------------------------------------------------------------------------
# 单行操作
# --------------------------------------------------------------------------
+37
View File
@@ -1304,6 +1304,43 @@ class HuyaExternalOrderActivityQueryRequest(BaseModel):
ids: list[int] = Field(default_factory=list)
class HuyaExternalOrderCkExportRequest(BaseModel):
"""勾选多行,导出这些行的 CK 明文(前端写剪贴板;一次最多 50 条)。"""
ids: list[int] = Field(default_factory=list)
class HuyaExternalOrderCkExportItemOut(BaseModel):
"""导出结果里的一行:``ck_text`` 是**明文凭据**,只走这个接口、不留日志。"""
id: int
order_no: str = ""
uid: str = ""
nickname: str = ""
status: str = ""
status_label: str = ""
ck_status: str = ""
ck_status_label: str = ""
ck_text: str = ""
class HuyaExternalOrderCkExportSkippedOut(BaseModel):
"""没导出的行(例如这行还没贴 CK)——把原因显式回给前端,别静默丢。"""
id: int
order_no: str = ""
reason: str = ""
class HuyaExternalOrderCkExportOut(BaseModel):
"""导出 CK 的结果:``exported + len(skipped) = total``。"""
total: int
exported: int
skipped: list[HuyaExternalOrderCkExportSkippedOut] = Field(default_factory=list)
items: list[HuyaExternalOrderCkExportItemOut] = Field(default_factory=list)
class HuyaCollabMetricMatchedOut(BaseModel):
"""匹配到的那条兑换记录(只留界面要用的几个字段)。"""
@@ -1678,6 +1678,90 @@ def list_orders_by_ids(db: Session, ids: Sequence[int]) -> list[HuyaExternalOrde
return [by_id[value] for value in wanted if value in by_id]
# 导出 CK 的审计动作名(导出=把凭据带出系统,必须留痕)
CK_EXPORT_ACTION = "huya:collab:ck_export"
def export_cks(
db: Session,
current: User | None,
orders: Sequence[HuyaExternalOrder],
*,
commit: bool = True,
) -> dict:
"""把勾选行的 **CK 明文**取出来(前端写剪贴板用)。
这是本模块唯一一个把凭据**往外送**的读接口,所以三条约束写死在这里:
1. **权限单独一个点**(``huya:collab:ck_export``,默认只给运营)—— 贴 CK 是「录入」,
导出是「把凭据带走」,两件事不该共用一个权限(口径同 ``huya:cookie:export``);
2. **逐行写审计**(``huya:collab:ck_export``):只记「谁在什么时候导出了哪一单」,
**绝不把 CK 本体写进审计/日志**(列表接口也照旧不返回 cookie);
3. **没 CK 的行显式跳过并回原因**,不静默丢;也不替用户猜 ——
作废/停用的单只要还有 CK 照样导出,界面把状态标出来让人自己判断。
Returns:
``{total, exported, skipped: [{id, order_no, reason}], items: [{..., ck_text}]}``
——只有这个接口会返回 ``ck_text``。
"""
rows = list(orders)
if not rows:
raise CollabError("请先勾选要导出的行", code="ck_export_empty")
if len(rows) > PROBE_BATCH_MAX:
raise CollabError(
f"一次最多导出 {PROBE_BATCH_MAX} 行,请分批操作", code="ck_export_too_large"
)
items: list[dict] = []
skipped: list[dict] = []
for order in rows:
ck_text = str(order.cookie or "").strip()
if not ck_text:
skipped.append(
{
"id": int(order.id),
"order_no": order.order_no,
"reason": "这行还没贴 CK",
}
)
continue
items.append(
{
"id": int(order.id),
"order_no": order.order_no,
"uid": str(order.uid or ""),
"nickname": order.nickname or "",
"status": order.status,
"status_label": ORDER_STATUS_LABELS.get(order.status, order.status),
"ck_status": order.ck_status,
"ck_status_label": CK_STATUS_LABELS.get(
order.ck_status, order.ck_status
),
"ck_text": ck_text,
}
)
record_audit(
db,
current,
action=CK_EXPORT_ACTION,
target=_audit_target(order),
detail={
"order_no": order.order_no,
"uid": str(order.uid or ""),
"ck_status": order.ck_status,
"batch_size": len(rows),
},
)
if commit:
db.commit()
return {
"total": len(rows),
"exported": len(items),
"skipped": skipped,
"items": items,
}
# --------------------------------------------------------------------------
# 店铺字典(新建协作单时点一下选;也支持自定义输入)
# --------------------------------------------------------------------------