完善客服操作台权限与绑定二维码
This commit is contained in:
@@ -79,7 +79,9 @@ ROLE_PERMISSIONS = {
|
||||
],
|
||||
"support": [
|
||||
"account:view_assigned",
|
||||
"douyu:task",
|
||||
"huya:view_assigned",
|
||||
"huya:task",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, unknown> | null | undefined, key: s
|
||||
}
|
||||
|
||||
export default function DouyuTasksPage() {
|
||||
const { can } = usePermissions();
|
||||
const [accounts, setAccounts] = useState<DouyuTaskAccountItem[]>([]);
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
const [goods, setGoods] = useState<DouyuGoodsItem[]>([]);
|
||||
@@ -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<number, DouyuTaskItem>();
|
||||
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() {
|
||||
</div>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadData} loading={loading}>刷新</Button>
|
||||
<Button icon={<SettingOutlined />} onClick={() => setConfigOpen(true)}>配置</Button>
|
||||
{canConfig && <Button icon={<SettingOutlined />} onClick={() => setConfigOpen(true)}>配置</Button>}
|
||||
{runningBatchId && <Button danger icon={<StopOutlined />} onClick={stopBatch}>停止</Button>}
|
||||
</Space>
|
||||
</Space>
|
||||
@@ -960,11 +963,10 @@ export default function DouyuTasksPage() {
|
||||
<Tag color={qrBindReady ? 'gold' : qrBindPhase === 'role_timeout' ? 'orange' : 'processing'}>
|
||||
{qrStatusText}
|
||||
</Tag>
|
||||
{/* 已有待确认角色时可能不生成二维码(cjm 已是最新换绑态) */}
|
||||
{qrUrl ? (
|
||||
<QRCode value={qrUrl} size={220} bordered={false} />
|
||||
) : qrBindReady ? (
|
||||
<Text type="secondary">已识别待确认角色,无需再次扫码</Text>
|
||||
<Text type="secondary">绑定二维码暂未返回</Text>
|
||||
) : qrTask?.status === 'running' ? (
|
||||
<Text type="secondary">二维码生成中...</Text>
|
||||
) : (
|
||||
|
||||
@@ -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:'] },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user