fix(assignments): 未分配列表不再混入 CK 失效账号
分配池统一为:持 CK、CK 未判失效、未被停用/归档。新增 assignable_only 参数贯穿账号列表、批量分配与分配概览;前端列表与一键全部分配使用同一口径。
This commit is contained in:
@@ -14,7 +14,11 @@
|
||||
|
||||
from tests.support.db import get_test_engine, make_test_session
|
||||
from web.backend.models import Account, User
|
||||
from web.backend.routers.accounts import batch_assign_accounts, list_accounts
|
||||
from web.backend.routers.accounts import (
|
||||
assignments_summary,
|
||||
batch_assign_accounts,
|
||||
list_accounts,
|
||||
)
|
||||
from web.backend.schemas import BatchAssign
|
||||
|
||||
|
||||
@@ -97,6 +101,66 @@ class TestAssignFilter(_Base):
|
||||
assert self.names(assign_status="unassigned", tag="10.1-测试") == ["tag-a-free"]
|
||||
|
||||
|
||||
class TestAssignablePool(_Base):
|
||||
"""分配池口径:CK 失效的号必须先修复,不能混进「未分配」让人误分配。"""
|
||||
|
||||
def test_assignable_only_excludes_invalid_ck(self):
|
||||
self.make_account("ap-valid", ck_check_status="valid")
|
||||
self.make_account("ap-unchecked") # 还没检测过:按可用处理
|
||||
self.make_account("ap-error", ck_check_status="error") # 查不到 ≠ 失效
|
||||
self.make_account("ap-invalid", ck_check_status="invalid")
|
||||
self.make_account("ap-disabled", ck_check_status="valid", status="disabled")
|
||||
|
||||
assert self.names(assign_status="unassigned", assignable_only=True) == [
|
||||
"ap-valid",
|
||||
"ap-unchecked",
|
||||
"ap-error",
|
||||
]
|
||||
|
||||
def test_summary_counts_only_assignable(self):
|
||||
self.make_account("as-valid", ck_check_status="valid")
|
||||
self.make_account("as-invalid", ck_check_status="invalid")
|
||||
self.make_account(
|
||||
"as-assigned-valid", ck_check_status="valid", assigned_to=self.support.id
|
||||
)
|
||||
self.make_account(
|
||||
"as-assigned-invalid",
|
||||
ck_check_status="invalid",
|
||||
assigned_to=self.support.id,
|
||||
)
|
||||
|
||||
summary = assignments_summary(db=self.session, current=self.admin)
|
||||
|
||||
assert summary["unassigned_count"] == 1
|
||||
assert summary["assigned_count"] == 1
|
||||
assert summary["total_count"] == 2
|
||||
support = next(
|
||||
u for u in summary["support_users"] if u["id"] == self.support.id
|
||||
)
|
||||
assert support["assigned_count"] == 1
|
||||
|
||||
def test_all_matching_assignable_only_skips_invalid(self):
|
||||
valid = self.make_account("am-valid", ck_check_status="valid")
|
||||
invalid = self.make_account("am-invalid", ck_check_status="invalid")
|
||||
|
||||
result = batch_assign_accounts(
|
||||
BatchAssign(
|
||||
all_matching=True,
|
||||
assign_status="unassigned",
|
||||
assignable_only=True,
|
||||
assigned_to=self.support.id,
|
||||
),
|
||||
db=self.session,
|
||||
current=self.admin,
|
||||
)
|
||||
|
||||
assert result["count"] == 1
|
||||
self.session.refresh(valid)
|
||||
self.session.refresh(invalid)
|
||||
assert valid.assigned_to == self.support.id
|
||||
assert invalid.assigned_to is None
|
||||
|
||||
|
||||
class TestAllMatchingAssign(_Base):
|
||||
def test_assigns_every_matching_unassigned(self):
|
||||
free = [self.make_account(f"mf-{i}") for i in range(3)]
|
||||
|
||||
@@ -55,10 +55,7 @@ from ..services.account_lifecycle import (
|
||||
STATUS_MANUAL,
|
||||
set_manual_state,
|
||||
)
|
||||
from ..services.account_service import (
|
||||
cookie_account_ids_query,
|
||||
parse_and_build_accounts,
|
||||
)
|
||||
from ..services.account_service import parse_and_build_accounts
|
||||
from ..services.account_value import (
|
||||
SUIT_BAIRIMENG,
|
||||
SUIT_MENGTU,
|
||||
@@ -84,6 +81,23 @@ def _visible_accounts_query(db: Session, current: User):
|
||||
raise HTTPException(status_code=403, detail="无权查看账号")
|
||||
|
||||
|
||||
def _assignable_account_filter():
|
||||
"""分配池口径:有 CK 且 CK **还没被判失效**,并且没被人工停用/归档。
|
||||
|
||||
- `ck_check_status` 为空 = 还没检测过(`derive_status` 同样按可用处理);
|
||||
- `error` = 本轮查不到,不代表失效,照常留在池子里;
|
||||
- `invalid` = 巡检已判失效,必须先重登/修复,分配给它也跑不了任何任务。
|
||||
"""
|
||||
return and_(
|
||||
cookie_holding_filter(),
|
||||
or_(Account.ck_check_status.is_(None), Account.ck_check_status != "invalid"),
|
||||
or_(
|
||||
Account.status.is_(None),
|
||||
Account.status.notin_([STATUS_DISABLED, STATUS_ARCHIVED]),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _query_str(value) -> str:
|
||||
"""把查询参数安全地取成字符串。
|
||||
|
||||
@@ -120,6 +134,7 @@ def _filter_accounts_query(
|
||||
assigned_only: bool = False,
|
||||
assign_status: str = "",
|
||||
assigned_user_id: int | None = None,
|
||||
assignable_only: bool = False,
|
||||
tag: str | None = None,
|
||||
has_cookie: bool = False,
|
||||
search: str = "",
|
||||
@@ -140,6 +155,8 @@ def _filter_accounts_query(
|
||||
"""复用列表筛选条件,供分页列表和全量批量操作保持一致。"""
|
||||
if _query_bool(has_cookie):
|
||||
query = query.filter(Account.cookie.isnot(None), Account.cookie != "")
|
||||
if _query_bool(assignable_only):
|
||||
query = query.filter(_assignable_account_filter())
|
||||
if _query_bool(assigned_only) and user_has_permission(current, "account:view_all"):
|
||||
query = query.filter(Account.assigned_to.isnot(None))
|
||||
# 归属筛选:未分配 / 已分配,再叠加「指定客服」。与其它条件(标签、CK、
|
||||
@@ -263,6 +280,7 @@ def _selected_account_ids(
|
||||
assigned_only=req.assigned_only,
|
||||
assign_status=req.assign_status,
|
||||
assigned_user_id=req.assigned_user_id,
|
||||
assignable_only=req.assignable_only,
|
||||
tag=req.tag,
|
||||
has_cookie=req.has_cookie,
|
||||
search=req.search,
|
||||
@@ -416,6 +434,10 @@ def list_accounts(
|
||||
"", description="归属筛选:unassigned=未分配 / assigned=已分配;空=全部"
|
||||
),
|
||||
assigned_user_id: int | None = Query(None, description="只看某个客服名下的账号"),
|
||||
assignable_only: bool = Query(
|
||||
False,
|
||||
description="只看可分配账号:持 CK 且 CK 未失效(排除已停用/归档)",
|
||||
),
|
||||
tag: str = Query(None),
|
||||
account_names: str = Query(
|
||||
"", description="多行斗鱼账号名(每行一个),精确匹配并自动去重"
|
||||
@@ -465,6 +487,7 @@ def list_accounts(
|
||||
assigned_only=assigned_only,
|
||||
assign_status=assign_status,
|
||||
assigned_user_id=assigned_user_id,
|
||||
assignable_only=assignable_only,
|
||||
tag=tag,
|
||||
has_cookie=has_cookie,
|
||||
search=search,
|
||||
@@ -1113,15 +1136,18 @@ def assignments_summary(
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("account:assign")),
|
||||
):
|
||||
"""分配概览:每个客服分配了多少账号(仅统计当前仍持有 CK 的账号)。"""
|
||||
"""分配概览:每个客服分到了多少**可分配账号**(持 CK 且 CK 未失效)。
|
||||
|
||||
失效 CK 的账号必须先重登/修复,放进分配池只会让客服以为能跑任务,
|
||||
所以列表、全量分配与这里的统计统一用 `_assignable_account_filter()`。
|
||||
"""
|
||||
results = (
|
||||
db.query(User.id, User.username, func.count(Account.id).label("count"))
|
||||
.outerjoin(
|
||||
Account,
|
||||
and_(
|
||||
Account.assigned_to == User.id,
|
||||
Account.cookie.isnot(None),
|
||||
Account.cookie != "",
|
||||
_assignable_account_filter(),
|
||||
),
|
||||
)
|
||||
.filter(User.role == "support")
|
||||
@@ -1133,8 +1159,7 @@ def assignments_summary(
|
||||
db.query(func.count(Account.id))
|
||||
.filter(
|
||||
Account.assigned_to.is_(None),
|
||||
Account.cookie.isnot(None),
|
||||
Account.cookie != "",
|
||||
_assignable_account_filter(),
|
||||
)
|
||||
.scalar()
|
||||
) or 0
|
||||
|
||||
@@ -207,6 +207,8 @@ class AccountBulkSelection(BaseModel):
|
||||
assign_status: str = ""
|
||||
#: 只看某个客服名下的账号(与 assign_status 可叠加)
|
||||
assigned_user_id: int | None = None
|
||||
#: 分配池口径:持 CK 且 CK 未失效(排除停用/归档)
|
||||
assignable_only: bool = False
|
||||
has_cookie: bool = False
|
||||
status: str = ""
|
||||
ck_status: str = ""
|
||||
|
||||
@@ -25,6 +25,7 @@ export const accountApi = {
|
||||
assigned_only?: boolean;
|
||||
assign_status?: string;
|
||||
assigned_user_id?: number;
|
||||
assignable_only?: boolean;
|
||||
tag?: string;
|
||||
has_cookie?: boolean;
|
||||
include_sensitive?: boolean;
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface AccountBulkSelection {
|
||||
assign_status?: string;
|
||||
/** 只看某个客服名下的账号 */
|
||||
assigned_user_id?: number;
|
||||
/** 分配池口径:持 CK 且 CK 未失效(排除停用/归档) */
|
||||
assignable_only?: boolean;
|
||||
has_cookie?: boolean;
|
||||
status?: string;
|
||||
ck_status?: string;
|
||||
|
||||
@@ -72,7 +72,9 @@ export default function AssignmentsPage() {
|
||||
const params: Parameters<typeof accountApi.listPaged>[0] = {
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
has_cookie: true,
|
||||
// 分配池只收「有 CK 且 CK 未失效」的账号:失效号的下一步是重登/修复,
|
||||
// 不是分配给客服(分过去也跑不了任务)。
|
||||
assignable_only: true,
|
||||
include_sensitive: false,
|
||||
search: searchText.trim() || undefined,
|
||||
tag: filterTag || undefined,
|
||||
@@ -180,6 +182,7 @@ export default function AssignmentsPage() {
|
||||
const res = await accountApi.batchAssignSelection({
|
||||
all_matching: true,
|
||||
assign_status: 'unassigned',
|
||||
assignable_only: true,
|
||||
tag: filterTag || undefined,
|
||||
search: searchText.trim() || undefined,
|
||||
assigned_to: selectedUser.id,
|
||||
@@ -252,7 +255,7 @@ export default function AssignmentsPage() {
|
||||
<Row gutter={12} style={{ flexShrink: 0 }}>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="总账号 (持CK)" value={totalAccountCount} prefix={<TeamOutlined />} />
|
||||
<Statistic title="总账号 (CK 有效)" value={totalAccountCount} prefix={<TeamOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
|
||||
Reference in New Issue
Block a user