From df4297bee50bb25c37be582d6eaa9bec6c8392fa Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 25 Jul 2026 09:45:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=AE=A2=E6=9C=8D=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E5=8F=B0=E6=9D=83=E9=99=90=E4=B8=8E=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E4=BA=8C=E7=BB=B4=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/backend/permissions.py | 2 + web/backend/routers/douyu.py | 58 ++++++++++++++++++++--- web/backend/routers/huya.py | 58 ++++++++++++++++++++--- web/backend/routers/users.py | 2 +- web/backend/services/douyu_runner.py | 31 ------------ web/frontend/src/pages/DouyuTasksPage.tsx | 14 +++--- web/frontend/src/pages/UsersPage.tsx | 2 + 7 files changed, 115 insertions(+), 52 deletions(-) diff --git a/web/backend/permissions.py b/web/backend/permissions.py index 2ff47dc..3640f1f 100644 --- a/web/backend/permissions.py +++ b/web/backend/permissions.py @@ -79,7 +79,9 @@ ROLE_PERMISSIONS = { ], "support": [ "account:view_assigned", + "douyu:task", "huya:view_assigned", + "huya:task", ], } diff --git a/web/backend/routers/douyu.py b/web/backend/routers/douyu.py index ec38e45..fafa109 100644 --- a/web/backend/routers/douyu.py +++ b/web/backend/routers/douyu.py @@ -57,6 +57,42 @@ def _visible_task_accounts_query(db: Session, current: User): raise HTTPException(status_code=403, detail="无权查看斗鱼账号") +def _visible_tasks_query(db: Session, current: User): + """返回当前用户可查看的斗鱼任务查询。""" + query = db.query(DouyuTask).options(joinedload(DouyuTask.account)) + if _can_view_all(current): + return query + if user_has_permission(current, "account:view_assigned"): + return query.join(DouyuTask.account).filter(Account.assigned_to == current.id) + raise HTTPException(status_code=403, detail="无权查看斗鱼任务") + + +def _require_task_account_access(db: Session, current: User, account_ids: list[int]) -> None: + """确保任务只会提交到当前用户可操作的账号。""" + requested_ids = set(account_ids) + query = db.query(Account.id).filter(Account.id.in_(requested_ids)) + if not _can_view_all(current): + if not user_has_permission(current, "account:view_assigned"): + raise HTTPException(status_code=403, detail="无权操作斗鱼账号") + query = query.filter(Account.assigned_to == current.id) + allowed_ids = {account_id for account_id, in query.all()} + if allowed_ids != requested_ids: + raise HTTPException(status_code=403, detail="包含无权操作的斗鱼账号") + + +def _require_batch_owner(db: Session, current: User, batch_id: str) -> None: + """客服只能停止或订阅自己创建的任务批次。""" + if _can_view_all(current): + return + exists = ( + db.query(DouyuTask.id) + .filter(DouyuTask.batch_id == batch_id, DouyuTask.created_by == current.id) + .first() + ) + if not exists: + raise HTTPException(status_code=403, detail="无权操作该斗鱼任务批次") + + def _account_out(account: Account) -> DouyuTaskAccountOut: return DouyuTaskAccountOut( id=account.id, @@ -187,6 +223,7 @@ async def create_task_batch( """创建斗鱼任务记录并启动后台执行器。""" if not req.account_ids: raise HTTPException(status_code=400, detail="请选择斗鱼账号") + _require_task_account_access(db, current, req.account_ids) cleanup_orphan_douyu_tasks( db, @@ -241,7 +278,7 @@ def list_tasks( statuses=("pending", "running"), message="任务已中断(无执行器接管)", ) - query = db.query(DouyuTask).options(joinedload(DouyuTask.account)) + query = _visible_tasks_query(db, current) if batch_id: query = query.filter(DouyuTask.batch_id == batch_id) rows = query.order_by(DouyuTask.id.desc()).limit(300).all() @@ -255,12 +292,7 @@ def get_task( current: User = Depends(require_permission("douyu:task")), ): """获取单条斗鱼任务详情。""" - task = ( - db.query(DouyuTask) - .options(joinedload(DouyuTask.account)) - .filter(DouyuTask.id == task_id) - .first() - ) + task = _visible_tasks_query(db, current).filter(DouyuTask.id == task_id).first() if not task: raise HTTPException(status_code=404, detail="任务不存在") return _task_out(task) @@ -273,6 +305,7 @@ def stop_batch( current: User = Depends(require_permission("douyu:task")), ): """停止正在运行的斗鱼批次。""" + _require_batch_owner(db, current, batch_id) batch = douyu_batch_registry.get(batch_id) if batch: if batch.get("finished"): @@ -297,6 +330,17 @@ async def ws_douyu_logs(websocket: WebSocket, batch_id: str): if not user: await websocket.close(code=1008, reason="未授权") return + if not user_has_permission(user, "douyu:task"): + await websocket.close(code=1008, reason="无权限") + return + db = SessionLocal() + try: + _require_batch_owner(db, user, batch_id) + except HTTPException: + await websocket.close(code=1008, reason="无权访问该任务批次") + return + finally: + db.close() await websocket.accept() batch = douyu_batch_registry.get(batch_id) diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py index 52c79b4..b6bd272 100644 --- a/web/backend/routers/huya.py +++ b/web/backend/routers/huya.py @@ -103,6 +103,42 @@ def _visible_huya_accounts_query(db: Session, current: User): raise HTTPException(status_code=403, detail="无权查看虎牙账号") +def _visible_huya_tasks_query(db: Session, current: User): + """返回当前用户可查看的虎牙任务查询。""" + query = db.query(HuyaTask).options(joinedload(HuyaTask.account)) + if _can_view_huya_all(current): + return query + if _can_view_huya_assigned(current): + return query.join(HuyaTask.account).filter(HuyaAccount.assigned_to == current.id) + raise HTTPException(status_code=403, detail="无权查看虎牙任务") + + +def _require_huya_task_account_access(db: Session, current: User, account_ids: list[int]) -> None: + """确保任务只会提交到当前用户可操作的虎牙账号。""" + requested_ids = set(account_ids) + query = db.query(HuyaAccount.id).filter(HuyaAccount.id.in_(requested_ids)) + if not _can_view_huya_all(current): + if not _can_view_huya_assigned(current): + raise HTTPException(status_code=403, detail="无权操作虎牙账号") + query = query.filter(HuyaAccount.assigned_to == current.id) + allowed_ids = {account_id for account_id, in query.all()} + if allowed_ids != requested_ids: + raise HTTPException(status_code=403, detail="包含无权操作的虎牙账号") + + +def _require_huya_batch_owner(db: Session, current: User, batch_id: str) -> None: + """客服只能停止或订阅自己创建的任务批次。""" + if _can_view_huya_all(current): + return + exists = ( + db.query(HuyaTask.id) + .filter(HuyaTask.batch_id == batch_id, HuyaTask.created_by == current.id) + .first() + ) + if not exists: + raise HTTPException(status_code=403, detail="无权操作该虎牙任务批次") + + def _fmt_cookie_preview(cookie: str) -> str: if not cookie: return "" @@ -1148,6 +1184,7 @@ async def create_task_batch( """创建虎牙任务记录并启动后台执行器。""" if not req.account_ids: raise HTTPException(status_code=400, detail="请选择虎牙账号") + _require_huya_task_account_access(db, current, req.account_ids) # 先清掉没有执行器的历史 running/pending,避免前端被假活跃批次锁死。 # 不清理 planned:避免与刚创建的新任务产生竞态。 @@ -1205,7 +1242,7 @@ def list_tasks( statuses=("pending", "running"), message="任务已中断(无执行器接管)", ) - query = db.query(HuyaTask).options(joinedload(HuyaTask.account)) + query = _visible_huya_tasks_query(db, current) if batch_id: query = query.filter(HuyaTask.batch_id == batch_id) tasks = query.order_by(HuyaTask.id.desc()).limit(300).all() @@ -1219,12 +1256,7 @@ def get_task( current: User = Depends(require_permission("huya:task")), ): """获取单条虎牙任务详情(含二维码图片)。""" - task = ( - db.query(HuyaTask) - .options(joinedload(HuyaTask.account)) - .filter(HuyaTask.id == task_id) - .first() - ) + task = _visible_huya_tasks_query(db, current).filter(HuyaTask.id == task_id).first() if not task: raise HTTPException(status_code=404, detail="任务不存在") return _task_out(task, include_images=True) @@ -1237,6 +1269,7 @@ def stop_batch( current: User = Depends(require_permission("huya:task")), ): """停止正在运行的虎牙批次。""" + _require_huya_batch_owner(db, current, batch_id) batch = huya_batch_registry.get(batch_id) if batch: if batch.get("finished"): @@ -1269,6 +1302,17 @@ async def ws_huya_logs(websocket: WebSocket, batch_id: str): if not user: await websocket.close(code=1008, reason="未授权") return + if not _has_huya_perm(user, "huya:task"): + await websocket.close(code=1008, reason="无权限") + return + db = SessionLocal() + try: + _require_huya_batch_owner(db, user, batch_id) + except HTTPException: + await websocket.close(code=1008, reason="无权访问该任务批次") + return + finally: + db.close() await websocket.accept() batch = huya_batch_registry.get(batch_id) diff --git a/web/backend/routers/users.py b/web/backend/routers/users.py index 55f235e..a1a5ea3 100644 --- a/web/backend/routers/users.py +++ b/web/backend/routers/users.py @@ -85,7 +85,7 @@ def update_user( user.is_active = req.is_active if req.remark is not None: user.remark = req.remark - if req.custom_permissions is not None: + if "custom_permissions" in req.model_fields_set: user.custom_permissions = req.custom_permissions db.commit() diff --git a/web/backend/services/douyu_runner.py b/web/backend/services/douyu_runner.py index 8787e9d..5f7e69a 100644 --- a/web/backend/services/douyu_runner.py +++ b/web/backend/services/douyu_runner.py @@ -658,37 +658,6 @@ class DouyuBatchRunner: ) return - # 生成码前若 cjm 已有待确认新角色,直接返回,无需重复扫码。 - pending_snapshot = self._bind_snapshot(pending_before) - if self._is_pending_role( - pending_before, - baseline_role_name=current_role_name, - baseline_is_bound_act=before_snapshot["is_bound_act"], - ) and pending_snapshot["role_name"]: - result = { - "act_alias": qr_act_alias or pending_before.get("act_alias"), - "query_act_alias": pending_before.get("act_alias"), - "query_act_aliases": query_aliases, - "before_bind_info": before, - **pending_snapshot, - "current_role_name": current_role_name, - "current_area_name": before_snapshot["area_name"], - "current_plat_name": before_snapshot["plat_name"], - "bind_ready_for_confirm": True, - "bind_confirmed": False, - "bind_phase": "role_ready", - "bind_polling": False, - } - self._apply_bind_info_to_account(account, pending_before, "game_queried") - self._mark_task( - db, - task, - "success", - f"已识别角色: {pending_snapshot['role_name']},待确认绑定", - result, - ) - return - if not qr_act_alias: self._mark_task(db, task, "failed", "请先配置绑定二维码活动 actAlias") return diff --git a/web/frontend/src/pages/DouyuTasksPage.tsx b/web/frontend/src/pages/DouyuTasksPage.tsx index d29c130..b7968ca 100644 --- a/web/frontend/src/pages/DouyuTasksPage.tsx +++ b/web/frontend/src/pages/DouyuTasksPage.tsx @@ -16,6 +16,7 @@ import { type DouyuTaskItem, } from '../api/modules'; import RealtimeLogPanel from '../components/RealtimeLogPanel'; +import { usePermissions } from '../hooks/usePermissions'; import { useWebSocketLogs } from '../hooks/useWebSocketLogs'; import { formatTime } from '../utils/time'; import { getErrorMessage } from '../utils/error'; @@ -133,6 +134,7 @@ function resultNumber(result: Record | null | undefined, key: s } export default function DouyuTasksPage() { + const { can } = usePermissions(); const [accounts, setAccounts] = useState([]); const [selectedIds, setSelectedIds] = useState([]); const [goods, setGoods] = useState([]); @@ -163,6 +165,7 @@ export default function DouyuTasksPage() { const autoOpenPayReady = useRef(false); const logs = useWebSocketLogs(); + const canConfig = can('douyu:config'); const latestQueryGameTaskByAccount = useMemo(() => { const map = new Map(); for (const task of tasks) { @@ -197,7 +200,7 @@ export default function DouyuTasksPage() { douyuApi.listAccounts(), douyuApi.listGoods(), douyuApi.listTasks(), - douyuApi.getConfig(), + canConfig ? douyuApi.getConfig() : Promise.resolve(null), douyuApi.taskTypes(), ]); if (accResult.status === 'fulfilled') setAccounts(accResult.value); @@ -214,12 +217,12 @@ export default function DouyuTasksPage() { autoOpenPayReady.current = true; } } - if (cfgResult.status === 'fulfilled') setConfig(cfgResult.value); + if (cfgResult.status === 'fulfilled' && cfgResult.value) setConfig(cfgResult.value); if (typeResult.status === 'fulfilled') setTaskTypes(typeResult.value); } finally { setLoading(false); } - }, []); + }, [canConfig]); const loadTasks = useCallback(async () => { try { @@ -688,7 +691,7 @@ export default function DouyuTasksPage() { - + {canConfig && } {runningBatchId && } @@ -960,11 +963,10 @@ export default function DouyuTasksPage() { {qrStatusText} - {/* 已有待确认角色时可能不生成二维码(cjm 已是最新换绑态) */} {qrUrl ? ( ) : qrBindReady ? ( - 已识别待确认角色,无需再次扫码 + 绑定二维码暂未返回 ) : qrTask?.status === 'running' ? ( 二维码生成中... ) : ( diff --git a/web/frontend/src/pages/UsersPage.tsx b/web/frontend/src/pages/UsersPage.tsx index 2028361..f6eb266 100644 --- a/web/frontend/src/pages/UsersPage.tsx +++ b/web/frontend/src/pages/UsersPage.tsx @@ -27,6 +27,8 @@ const PERMISSION_GROUPS = [ { label: '账号管理', prefix: 'account:' }, { label: '登录任务', prefix: 'login:' }, { label: 'Cookie', prefix: 'cookie:' }, + { label: '斗鱼活动', prefix: 'douyu:' }, + { label: '虎牙', prefix: 'huya:' }, { label: '代理 & 白名单', prefix: ['proxy:', 'whitelist:'] }, { label: '系统', prefix: ['system:', 'audit:'] }, ];