diff --git a/tests/test_account_sensitive_fields.py b/tests/test_account_sensitive_fields.py
index 0520c66..327408c 100644
--- a/tests/test_account_sensitive_fields.py
+++ b/tests/test_account_sensitive_fields.py
@@ -6,10 +6,12 @@ os.environ.setdefault("APP_ENCRYPTION_KEY", "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODl
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
+from fastapi import HTTPException
from web.backend.database import Base
from web.backend.models import Account, User
-from web.backend.routers.accounts import list_accounts
+from web.backend.routers.accounts import list_accounts, set_account_tag
+from web.backend.schemas import AccountTag
class AccountSensitiveFieldsTests(unittest.TestCase):
@@ -68,6 +70,30 @@ class AccountSensitiveFieldsTests(unittest.TestCase):
self.assertEqual(item.email, "account@example.com")
self.assertEqual(item.email_password, "email-password")
+ def test_support_can_only_change_tags_on_assigned_accounts(self):
+ support = User(username="support", password_hash="hash", role="support")
+ other_support = User(username="other-support", password_hash="hash", role="support")
+ self.session.add_all([support, other_support])
+ self.session.commit()
+ assigned = Account(
+ username="assigned", password="password", email="assigned@example.com",
+ email_password="mail-password", assigned_to=support.id,
+ )
+ other = Account(
+ username="other", password="password", email="other@example.com",
+ email_password="mail-password", assigned_to=other_support.id,
+ )
+ self.session.add_all([assigned, other])
+ self.session.commit()
+
+ set_account_tag(assigned.id, AccountTag(tag="客服组"), db=self.session, current=support)
+ self.session.refresh(assigned)
+ self.assertEqual(assigned.tag, "客服组")
+
+ with self.assertRaises(HTTPException) as context:
+ set_account_tag(other.id, AccountTag(tag="越权"), db=self.session, current=support)
+ self.assertEqual(context.exception.status_code, 404)
+
if __name__ == "__main__":
unittest.main()
diff --git a/web/backend/permissions.py b/web/backend/permissions.py
index 322afbc..6882c18 100644
--- a/web/backend/permissions.py
+++ b/web/backend/permissions.py
@@ -14,6 +14,7 @@ PERMISSIONS = {
"account:view_full": "查看账号完整字段(密码/邮箱,仅管理员)",
"account:view_assigned": "查看分配给自己的账号",
"account:import": "导入账号",
+ "account:tag": "修改账号标签",
"account:check": "账号检测",
"account:assign": "分配账号给客服",
"account:delete": "删除账号",
@@ -37,6 +38,7 @@ PERMISSIONS = {
"huya:view_all": "查看所有虎牙账号",
"huya:view_assigned": "查看分配给自己的虎牙账号",
"huya:import": "导入/登录虎牙账号",
+ "huya:tag": "修改虎牙账号标签",
"huya:assign": "分配虎牙账号",
"huya:delete": "删除虎牙账号",
"huya:cookie:view": "查看虎牙 Cookie",
@@ -60,6 +62,7 @@ ROLE_PERMISSIONS = {
"operation": [
"account:view_all",
"account:import",
+ "account:tag",
"account:check",
"account:assign",
"login:batch",
@@ -75,6 +78,7 @@ ROLE_PERMISSIONS = {
"huya:account",
"huya:view_all",
"huya:import",
+ "huya:tag",
"huya:assign",
"huya:delete",
"huya:cookie:view",
@@ -89,11 +93,13 @@ ROLE_PERMISSIONS = {
],
"support": [
"account:view_assigned",
+ "account:tag",
"cookie:operate",
"douyu:task",
"yyb:session",
"yyb:history",
"huya:view_assigned",
+ "huya:tag",
"huya:task",
],
}
diff --git a/web/backend/routers/accounts.py b/web/backend/routers/accounts.py
index 2bed7d2..d6a44a6 100644
--- a/web/backend/routers/accounts.py
+++ b/web/backend/routers/accounts.py
@@ -92,6 +92,15 @@ def _selected_account_ids(db: Session, current: User, req: AccountBulkSelection)
return [account_id for account_id in ids if account_id in allowed]
+def _require_account_tag_permission(current: User) -> None:
+ """标签权限兼容历史上使用账号导入权限的运营人员。"""
+ if not (
+ user_has_permission(current, "account:tag")
+ or user_has_permission(current, "account:import")
+ ):
+ raise HTTPException(status_code=403, detail="无权修改账号标签")
+
+
@router.get("")
def list_accounts(
assigned_only: bool = Query(False),
@@ -323,13 +332,16 @@ def set_account_tag(
account_id: int,
req: AccountTag,
db: Session = Depends(get_db),
- current: User = Depends(require_permission("account:import")),
+ current: User = Depends(get_current_user),
):
- """设置单个账号标签。"""
- acc = db.query(Account).filter(Account.id == account_id).first()
+ """设置单个可见账号的标签。"""
+ _require_account_tag_permission(current)
+ acc = _visible_accounts_query(db, current).filter(Account.id == account_id).first()
if not acc:
raise HTTPException(status_code=404, detail="账号不存在")
acc.tag = (req.tag or "").strip()
+ db.add(AuditLog(user_id=current.id, username=current.username,
+ action="account:tag", target=acc.username, detail=acc.tag))
db.commit()
return {"message": "标签已更新", "success": True}
@@ -338,13 +350,14 @@ def set_account_tag(
def batch_tag(
req: AccountTag,
db: Session = Depends(get_db),
- current: User = Depends(require_permission("account:import")),
+ current: User = Depends(get_current_user),
):
"""批量设置账号标签。"""
+ _require_account_tag_permission(current)
if not req.account_ids:
raise HTTPException(status_code=400, detail="请选择账号")
tag = (req.tag or "").strip()
- count = db.query(Account).filter(Account.id.in_(req.account_ids)).update(
+ count = _visible_accounts_query(db, current).filter(Account.id.in_(req.account_ids)).update(
{Account.tag: tag}, synchronize_session=False
)
db.commit()
@@ -355,9 +368,10 @@ def batch_tag(
def batch_tag_selection(
req: AccountBulkTag,
db: Session = Depends(get_db),
- current: User = Depends(require_permission("account:import")),
+ current: User = Depends(get_current_user),
):
"""按显式选择或当前筛选结果批量设置账号标签。"""
+ _require_account_tag_permission(current)
ids = _selected_account_ids(db, current, req)
if not ids:
raise HTTPException(status_code=400, detail="请选择账号")
diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py
index 25014cc..02133b1 100644
--- a/web/backend/routers/huya.py
+++ b/web/backend/routers/huya.py
@@ -94,6 +94,12 @@ def _require_huya_perm(user: User, permission: str) -> None:
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")):
+ raise HTTPException(status_code=403, detail="无权修改虎牙账号标签")
+
+
def _can_view_huya_all(user: User) -> bool:
return _has_huya_perm(user, "huya:view_all")
@@ -1064,8 +1070,8 @@ def set_account_tag(
current: User = Depends(get_current_user),
):
"""设置单个虎牙账号标签。"""
- _require_huya_perm(current, "huya:import")
- account = db.query(HuyaAccount).filter(HuyaAccount.id == account_id).first()
+ _require_huya_tag_permission(current)
+ account = _visible_huya_accounts_query(db, current).filter(HuyaAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
account.tag = (req.tag or "").strip()
@@ -1080,11 +1086,11 @@ def batch_tag(
current: User = Depends(get_current_user),
):
"""批量设置虎牙账号标签。"""
- _require_huya_perm(current, "huya:import")
+ _require_huya_tag_permission(current)
if not req.account_ids:
raise HTTPException(status_code=400, detail="请选择账号")
tag = (req.tag or "").strip()
- count = db.query(HuyaAccount).filter(HuyaAccount.id.in_(req.account_ids)).update(
+ count = _visible_huya_accounts_query(db, current).filter(HuyaAccount.id.in_(req.account_ids)).update(
{HuyaAccount.tag: tag},
synchronize_session=False,
)
@@ -1099,12 +1105,12 @@ def batch_tag_selection(
current: User = Depends(get_current_user),
):
"""按显式选择或当前筛选结果批量设置虎牙账号标签。"""
- _require_huya_perm(current, "huya:import")
+ _require_huya_tag_permission(current)
ids = _selected_huya_account_ids(db, current, req)
if not ids:
raise HTTPException(status_code=400, detail="请选择虎牙账号")
tag = (req.tag_value or "").strip()
- count = db.query(HuyaAccount).filter(HuyaAccount.id.in_(ids)).update(
+ count = _visible_huya_accounts_query(db, current).filter(HuyaAccount.id.in_(ids)).update(
{HuyaAccount.tag: tag},
synchronize_session=False,
)
diff --git a/web/frontend/src/pages/AccountsPage.tsx b/web/frontend/src/pages/AccountsPage.tsx
index a6c9182..37a84fa 100644
--- a/web/frontend/src/pages/AccountsPage.tsx
+++ b/web/frontend/src/pages/AccountsPage.tsx
@@ -45,9 +45,10 @@ export default function AccountsPage() {
const canViewFull = can('account:view_full');
const canImport = can('account:import');
+ const canTag = can('account:tag') || canImport;
const canAssign = can('account:assign');
const canDelete = can('account:delete');
- const canSelectRows = canImport || canDelete || canAssign;
+ const canSelectRows = canTag || canDelete || canAssign;
const loadAccounts = useCallback(async () => {
setLoading(true);
@@ -252,43 +253,29 @@ export default function AccountsPage() {
{
title: '标签',
dataIndex: 'tag',
- width: 120,
+ width: 170,
render: (tag: string, record: AccountItem) => {
- if (!tag) {
- if (canImport) {
- return (
- {
- const val = (e.target as HTMLInputElement).value.trim();
- if (val) handleSetTag(record.id, val);
- }}
- onBlur={(e) => {
- const val = e.target.value.trim();
- if (val) handleSetTag(record.id, val);
- }}
- />
- );
- }
- return -;
- }
- if (canImport) {
+ if (canTag) {
return (
- {
- e.preventDefault();
- handleSetTag(record.id, '');
+
+ options={tags.map((value) => ({ value, label: value }))}
+ />
);
}
- return {tag};
+ return tag ? {tag} : -;
},
},
{
@@ -346,6 +333,8 @@ export default function AccountsPage() {