diff --git a/README.md b/README.md index eb6ca9f..b027cb5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ - **斗鱼**:极验滑块自动过验、邮箱验证码、批量登录与 CK 检测/重登;精英宝典 / 电竞手册 / 和平小店三套工作台(绑定、开通、充值、兑换);鱼翅可由微信二维码或供应商直充。 - **虎牙**:Cookie / 密码 / App / 短信登录、自动注册、设备绑定、精英宝典、浏览器人工兑换;网页运行 Cookie 与 App 原始凭据(`raw_cookie`)分离,可从原始凭据重建网页 Cookie。 - **应用宝**:和平精英点券扫码登录、选区服角色、支付二维码。Worker 合在 Web 容器内,只监听 `127.0.0.1:8810`。 -- **权限**:超级管理员 / 运营 / 客服;可按权限点覆盖。客服只看分配给自己的账号,虎牙侧默认只开放浏览器兑换。 +- **权限**:超级管理员 / 运营 / 客服;可按权限点覆盖。客服只看分配给自己的账号,虎牙侧默认开放精英宝典(含外部 CK 导入)与浏览器兑换。 - **安全**:密码、邮箱、Cookie、代理密钥等敏感字段落盘加密;操作写审计日志。 ## 项目结构 @@ -120,7 +120,7 @@ python3 -c 'import secrets; print(secrets.token_urlsafe(32))' |---|---| | 超级管理员 | 全部功能 + 用户管理 + 权限分配 + 审计日志 | | 运营 | 账号管理、批量登录、代理、Cookie、双平台任务、应用宝 | -| 客服 | 仅查看分配给自己的账号;虎牙只保留浏览器兑换 | +| 客服 | 仅查看分配给自己的账号;虎牙保留精英宝典(含外部 CK 导入)与浏览器兑换 | ## 登录与任务流 diff --git a/docs/虎牙/HUYA_BROWSER_EXCHANGE.md b/docs/虎牙/HUYA_BROWSER_EXCHANGE.md index 264303c..64cf253 100644 --- a/docs/虎牙/HUYA_BROWSER_EXCHANGE.md +++ b/docs/虎牙/HUYA_BROWSER_EXCHANGE.md @@ -4,7 +4,7 @@ ## 入口与账号范围 -独立页面:虎牙 → 浏览器兑换。导航按角色区分:客服只看到浏览器兑换一个入口;管理员/运营仍显示完整的虎牙菜单(账号管理、精英宝典等)。 +独立页面:虎牙 → 浏览器兑换。导航按角色区分:客服默认看到精英宝典与浏览器兑换;管理员/运营仍显示完整的虎牙菜单(账号管理、精英宝典等)。 - 页面维护一份「兑换账号列表」,通过「导入账号」从自己可见的账号池勾选加入;客服只能看到分配给自己的账号。 - 列表按用户隔离保存(工作台 scope `browser_exchange`),与精英宝典的账号集合互不影响。 diff --git a/tests/test_huya_workbench_import_ck.py b/tests/test_huya_workbench_import_ck.py index 2c6f2b7..67cb02d 100644 --- a/tests/test_huya_workbench_import_ck.py +++ b/tests/test_huya_workbench_import_ck.py @@ -1,8 +1,13 @@ """工作台「导入外部 CK」:入库 + 自动加入工作台 + 归属导入者。""" +import pytest +from fastapi import HTTPException + from tests.support.db import get_test_engine, make_test_session from web.backend.models import HuyaAccount, HuyaWorkbench, HuyaWorkbenchAccount, User +from web.backend.permissions import ROLE_PERMISSIONS from web.backend.routers.huya import import_workbench_cookies +from web.backend.routers.huya._common import _require_huya_workbench_import_ck from web.backend.schemas import HuyaWorkbenchCookieImport COOKIE_1 = ( @@ -79,3 +84,18 @@ class TestImportExternalCkToWorkbench: assert result["account_ids"] == [] assert self.session.query(HuyaAccount).count() == 0 assert self.session.query(HuyaWorkbench).count() == 0 + + def test_support_can_import_workbench_ck_by_default(self): + """客服默认允许工作台导入外部 CK,但旧 huya:import 仍只属于运营。""" + assert "huya:elite:import_ck" in ROLE_PERMISSIONS["support"] + assert "huya:elite:import_ck" not in ROLE_PERMISSIONS["operation"] + + support = User(username="cs", password_hash="hash", role="support") + operation = User(username="op", password_hash="hash", role="operation") + guest = User(username="guest", password_hash="hash", role="guest") + assert _require_huya_workbench_import_ck(support) is None + assert _require_huya_workbench_import_ck(operation) is None + + with pytest.raises(HTTPException) as excinfo: + _require_huya_workbench_import_ck(guest) + assert excinfo.value.status_code == 403 diff --git a/web/backend/permissions.py b/web/backend/permissions.py index c8b3962..72f99be 100644 --- a/web/backend/permissions.py +++ b/web/backend/permissions.py @@ -66,6 +66,7 @@ PERMISSIONS = { "huya:cookie:view": "查看虎牙 Cookie", "huya:cookie:export": "导出/清除虎牙 Cookie", "huya:task": "虎牙任务管理", + "huya:elite:import_ck": "精英宝典工作台导入外部 CK", "huya:bind": "虎牙绑定操作", "huya:recharge": "虎牙充值操作", "huya:config": "虎牙配置管理", @@ -176,6 +177,7 @@ ROLE_PERMISSIONS = { "huya:view_assigned", "huya:tag", "huya:task", + "huya:elite:import_ck", # 客服能看能改(共享表,靠审计留痕),但删除只给运营。 "huya:collab:view", "huya:collab:edit", diff --git a/web/backend/routers/huya/_common.py b/web/backend/routers/huya/_common.py index 7f3dc19..51bf0bc 100644 --- a/web/backend/routers/huya/_common.py +++ b/web/backend/routers/huya/_common.py @@ -132,6 +132,15 @@ def _require_huya_perm(user: User, permission: str) -> None: raise HTTPException(status_code=403, detail="权限不足") +def _require_huya_workbench_import_ck(user: User) -> None: + """工作台导入外部 CK 的专用权限;运营仍走历史 huya:import。""" + if not ( + _has_huya_perm(user, "huya:import") + or _has_huya_perm(user, "huya:elite:import_ck") + ): + raise HTTPException(status_code=403, detail="权限不足") + + def _require_huya_tag_permission(user: User) -> None: """标签权限兼容历史上使用导入权限的运营人员。""" if not (_has_huya_perm(user, "huya:tag") or _has_huya_perm(user, "huya:import")): diff --git a/web/backend/routers/huya/workbench.py b/web/backend/routers/huya/workbench.py index f761081..cb06072 100644 --- a/web/backend/routers/huya/workbench.py +++ b/web/backend/routers/huya/workbench.py @@ -414,7 +414,7 @@ def import_workbench_cookies( 导入后的账号会出现在工作台账号列表里,可照常执行「生成开通支付码 / 绑定二维码 / 查询角色 / 刷新积分」等操作。没有归属的新账号自动归属到导入者,否则客服看不到。 """ - _require_huya_perm(current, "huya:import") + _require_huya_workbench_import_ck(current) accounts, skipped = import_huya_cookies_detailed( db, req.text, req.tag, assign_to=current.id ) diff --git a/web/frontend/src/layouts/MainLayout.tsx b/web/frontend/src/layouts/MainLayout.tsx index b1f70c6..b561b7c 100644 --- a/web/frontend/src/layouts/MainLayout.tsx +++ b/web/frontend/src/layouts/MainLayout.tsx @@ -91,9 +91,10 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) { douyuItems.push({ key: '/douyu/collab', label: '外部单协作', icon: }); } - // 虎牙:客服只保留浏览器兑换;管理员/运营显示完整入口 + // 虎牙:客服用浏览器兑换和精英宝典;管理员/运营显示完整入口 if (can('huya:task')) { if (user?.role === 'support') { + huyaItems.push({ key: '/huya/elite', label: '精英宝典', icon: }); huyaItems.push({ key: '/huya/browser-exchange', label: '浏览器兑换', icon: }); } else { if (canAny(['huya:account', 'huya:view_all', 'huya:view_assigned'])) { diff --git a/web/frontend/src/pages/HuyaElitePage.tsx b/web/frontend/src/pages/HuyaElitePage.tsx index 79b07c0..df83e30 100644 --- a/web/frontend/src/pages/HuyaElitePage.tsx +++ b/web/frontend/src/pages/HuyaElitePage.tsx @@ -164,7 +164,7 @@ function taskQrUrl(task: HuyaTaskItem | undefined): string { export default function HuyaElitePage() { const { can } = usePermissions(); const canConfig = can('huya:config'); - const canImport = can('huya:import'); + const canImport = can('huya:import') || can('huya:elite:import_ck'); const [pool, setPool] = useState([]); const [accounts, setAccounts] = useState([]); const [selectedIds, setSelectedIds] = useState([]);