fix(huya): 协作台账固定序号并支持表头排序

This commit is contained in:
yml2213
2026-10-17 16:05:11 +08:00
parent fcc2fbdc09
commit fe1f959648
5 changed files with 161 additions and 10 deletions
+31
View File
@@ -1444,6 +1444,37 @@ class TestCollabCreateStrict(TestCollabBase):
assert order.void_reason == ""
class TestCollabOrderSorting(TestCollabBase):
"""列表默认按创建顺序;表头排序走白名单,升/降序都稳定。"""
def test_default_order_is_creation_order(self):
first = self._create(order_no="SORT-1", shop="乙")
second = self._create(order_no="SORT-2", shop="甲")
rows, total = collab.list_orders(self.db)
assert total == 2
assert [row.id for row in rows] == [first.id, second.id]
def test_sort_by_order_no_ascending_and_descending(self):
self._create(order_no="SORT-B")
self._create(order_no="SORT-A")
rows, _ = collab.list_orders(self.db, sort_by="order_no", sort_order="asc")
assert [row.order_no for row in rows] == ["SORT-A", "SORT-B"]
rows, _ = collab.list_orders(self.db, sort_by="order_no", sort_order="desc")
assert [row.order_no for row in rows] == ["SORT-B", "SORT-A"]
def test_sort_by_owner_username(self):
self._create(order_no="SORT-OWNER-1", owner_id=self.user.id)
self._create(order_no="SORT-OWNER-2", owner_id=self.other.id)
rows, _ = collab.list_orders(self.db, sort_by="owner", sort_order="asc")
assert [row.owner_id for row in rows] == [self.user.id, self.other.id]
class TestCollabKeywordSearch(TestCollabBase):
"""列表搜索:一个输入框打穿订单号 / 店铺 / 绑定账号·角色 / 平台区服 / 虎牙号 / 礼包 / 备注。"""
+4
View File
@@ -221,6 +221,8 @@ def list_external_orders(
gift_name: str = Query("", max_length=128),
keyword: str = Query("", max_length=128),
include_void: bool = Query(False, description="是否包含已作废的行(默认隐藏)"),
sort_by: str = Query("seq", max_length=32, description="排序字段(白名单)"),
sort_order: str = Query("asc", max_length=8, description="asc / desc"),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
@@ -236,6 +238,8 @@ def list_external_orders(
gift_name=gift_name,
keyword=keyword,
include_void=include_void,
sort_by=sort_by,
sort_order=sort_order,
page=page,
page_size=page_size,
)
+43 -4
View File
@@ -2308,6 +2308,21 @@ def get_order(db: Session, order_id: int) -> HuyaExternalOrder:
return order
ORDER_SORT_KEYS: tuple[str, ...] = (
"seq",
"order_no",
"shop",
"owner",
"bind",
"gift_name",
"status",
"uid",
"ck_status",
"updated_at",
"created_at",
)
def list_orders(
db: Session,
*,
@@ -2318,10 +2333,15 @@ def list_orders(
gift_name: str = "",
keyword: str = "",
include_void: bool = False,
sort_by: str = "seq",
sort_order: str = "asc",
page: int = 1,
page_size: int = 50,
) -> tuple[list[HuyaExternalOrder], int]:
"""共享视图:默认**不按人过滤**(负责人只是筛选维度,不是权限边界)。"""
"""共享视图:默认**不按人过滤**(负责人只是筛选维度,不是权限边界)。
默认按 ``id`` 升序(即创建顺序),新建的行永远追加在最后;表头排序走白名单,
不允许前端把任意列名拼进 SQL。"""
query = db.query(HuyaExternalOrder)
if shop:
query = query.filter(HuyaExternalOrder.shop == shop)
@@ -2358,10 +2378,29 @@ def list_orders(
total = query.count()
page = max(1, int(page or 1))
page_size = min(200, max(1, int(page_size or 50)))
# 排序:业务状态(作废排最后)+ 最近改动在前,让「刚被谁动过」的浮上来。
stage = func.field(HuyaExternalOrder.status, *ORDER_STATUSES)
key = (sort_by or "seq").strip().lower()
if key not in ORDER_SORT_KEYS:
key = "seq"
descending = (sort_order or "asc").strip().lower() == "desc"
# 负责人按显示的用户名排序(不是 id),需要一次 outerjoin;其它列都直接落在订单表上。
if key == "owner":
query = query.outerjoin(User, HuyaExternalOrder.owner_id == User.id)
sort_column = User.username
elif key == "status":
# 与页头状态顺序保持一致,避免状态列按字母序排得莫名其妙。
sort_column = func.field(HuyaExternalOrder.status, *ORDER_STATUSES)
elif key == "bind":
sort_column = HuyaExternalOrder.bind_game_account
elif key == "seq":
sort_column = HuyaExternalOrder.id
else:
sort_column = getattr(HuyaExternalOrder, key)
ordered = sort_column.desc() if descending else sort_column.asc()
rows = (
query.order_by(stage.asc(), HuyaExternalOrder.updated_at.desc())
query.order_by(ordered, HuyaExternalOrder.id.asc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
+3
View File
@@ -1459,6 +1459,9 @@ export interface HuyaCollabOrderQuery {
gift_name?: string;
keyword?: string;
include_void?: boolean;
/** 服务端排序字段(后端白名单) */
sort_by?: string;
sort_order?: 'asc' | 'desc';
page?: number;
page_size?: number;
}
+80 -6
View File
@@ -105,6 +105,32 @@ const CK_META: Record<string, { label: string; color: string }> = {
disabled: { label: '已停用', color: 'warning' },
};
/** 表头排序字段(后端白名单):seq = 固定创建序号;点击一次正序,再点一次反序。 */
type CollabSortKey =
| 'seq'
| 'order_no'
| 'shop'
| 'owner'
| 'bind'
| 'gift_name'
| 'status'
| 'uid'
| 'ck_status'
| 'updated_at';
type CollabSortOrder = 'ascend' | 'descend';
const COLLAB_SORT_KEYS: ReadonlySet<string> = new Set<CollabSortKey>([
'seq',
'order_no',
'shop',
'owner',
'bind',
'gift_name',
'status',
'uid',
'ck_status',
'updated_at',
]);
const ACTION_LABELS: Record<string, string> = {
'huya:collab:create': '新建',
'huya:collab:update': '编辑',
@@ -397,6 +423,9 @@ export default function HuyaCollabPage() {
const [giftNames, setGiftNames] = useState<string[]>([]);
const [shopModalOpen, setShopModalOpen] = useState(false);
const [filters, setFilters] = useState<HuyaCollabOrderQuery>({ include_void: false });
// 默认按固定创建序号正序:老单在上、新单永远追加在最后。
const [sortKey, setSortKey] = useState<CollabSortKey>('seq');
const [sortOrder, setSortOrder] = useState<CollabSortOrder>('ascend');
const [realtimePending, setRealtimePending] = useState(false);
const [selectedIds, setSelectedIds] = useState<number[]>([]);
@@ -461,7 +490,13 @@ export default function HuyaCollabPage() {
if (!silent) setLoading(true);
try {
const [list, nextSummary, nextOwners, nextShops, nextGiftNames] = await Promise.all([
huyaApi.listCollabOrders({ ...filters, page, page_size: pageSize }),
huyaApi.listCollabOrders({
...filters,
sort_by: sortKey,
sort_order: sortOrder === 'descend' ? 'desc' : 'asc',
page,
page_size: pageSize,
}),
huyaApi.collabOrdersSummary(),
withMeta ? huyaApi.collabOrderOwners() : Promise.resolve(null),
withMeta ? huyaApi.listCollabShops() : Promise.resolve(null),
@@ -480,7 +515,7 @@ export default function HuyaCollabPage() {
} finally {
setLoading(false);
}
}, [filters, page, pageSize]);
}, [filters, page, pageSize, sortKey, sortOrder]);
useEffect(() => { void load(); }, [load]);
@@ -1194,16 +1229,28 @@ export default function HuyaCollabPage() {
? `礼包用本地缓存(${giftOptions.length} 个${giftCacheLabel ? ` · ${giftCacheLabel}` : ''}),要更新点「强制刷新礼包」`
: (ckPreview?.gifts_message || '');
// 表头排序:一次正序、再点反序(Table 上限制 sortDirections,不会出现「第三次取消」)。
const sortProps = (key: CollabSortKey) => ({
key,
sorter: true,
sortOrder: sortKey === key ? sortOrder : null,
showSorterTooltip: { title: '点击正序,再点一次反序' },
});
const columns: TableProps<HuyaCollabOrder>['columns'] = [
{
title: '序号', width: 70, fixed: 'left', align: 'center',
render: (_, __, index) => (
// 跨页连续:第 2 页第 1 行 = pageSize + 1
<Text type="secondary">{(page - 1) * pageSize + index + 1}</Text>
title: '序号', width: 76, fixed: 'left', align: 'center',
...sortProps('seq'),
render: (_, row) => (
// 序号 = 创建时分配的固定 id:不管切换排序、筛选还是翻页都不会重排。
<Tooltip title="创建序号,固定不变;新单会追加在最后">
<Text type="secondary">{row.id}</Text>
</Tooltip>
),
},
{
title: '订单编号', dataIndex: 'order_no', width: 170, fixed: 'left',
...sortProps('order_no'),
render: (value: string, row) => (
<Space direction="vertical" size={0}>
<Text copyable={{ text: value }} strong>{value}</Text>
@@ -1213,6 +1260,7 @@ export default function HuyaCollabPage() {
},
{
title: '店铺', dataIndex: 'shop', width: 120,
...sortProps('shop'),
render: (value: string, row) => (
<Popover
trigger="click"
@@ -1236,6 +1284,7 @@ export default function HuyaCollabPage() {
},
{
title: '负责人', dataIndex: 'owner_id', width: 130,
...sortProps('owner'),
render: (value: number | null, row) => (
<Select
size="small" variant="borderless" style={{ width: '100%' }}
@@ -1247,6 +1296,7 @@ export default function HuyaCollabPage() {
},
{
title: '绑定账号 / 角色', width: 210,
...sortProps('bind'),
render: (_, row) => (
<Space direction="vertical" size={0}>
<Text strong>{row.bind_game_account || <Text type="secondary">未读到</Text>}</Text>
@@ -1339,6 +1389,7 @@ export default function HuyaCollabPage() {
},
{
title: '礼包 / 备注', width: 160,
...sortProps('gift_name'),
render: (_, row) => (
<Space direction="vertical" size={0} style={{ width: '100%' }}>
<EditableText value={row.gift_name} disabled={!canEdit} placeholder="+ 礼包名"
@@ -1350,6 +1401,7 @@ export default function HuyaCollabPage() {
},
{
title: '状态', dataIndex: 'status', width: 130,
...sortProps('status'),
render: (value: HuyaCollabStatus, row) => (
<Space direction="vertical" size={2}>
<Tag color={STATUS_META[value]?.color}>{row.status_label || STATUS_META[value]?.label || value}</Tag>
@@ -1366,6 +1418,7 @@ export default function HuyaCollabPage() {
},
{
title: '虎牙号', width: 160,
...sortProps('uid'),
render: (_, row) => (
<Space direction="vertical" size={0}>
<Text>{row.uid || <Text type="secondary">未贴 CK</Text>}</Text>
@@ -1375,6 +1428,7 @@ export default function HuyaCollabPage() {
},
{
title: 'CK 状态', dataIndex: 'ck_status', width: 170,
...sortProps('ck_status'),
render: (value: string, row) => (
<Space direction="vertical" size={2} style={{ width: '100%' }}>
<Tag color={CK_META[value]?.color}>{row.ck_status_label || CK_META[value]?.label || value}</Tag>
@@ -1420,6 +1474,7 @@ export default function HuyaCollabPage() {
},
{
title: '最后改动', width: 160,
...sortProps('updated_at'),
render: (_, row) => (
<Space direction="vertical" size={0}>
<Text>{row.updated_by_name || '-'}</Text>
@@ -1682,6 +1737,25 @@ export default function HuyaCollabPage() {
columns={columns}
dataSource={rows}
scroll={{ x: 1800, y: 'calc(100vh - 460px)' }}
sortDirections={['ascend', 'descend']}
onChange={(_pagination, _filters, sorter, extra) => {
if (extra.action !== 'sort') return;
const current = Array.isArray(sorter) ? sorter[0] : sorter;
const nextOrder = current?.order;
const nextKey = String(current?.columnKey ?? '');
// 只在白名单命中时切换;排序状态变了必须回第一页,否则会停在一个空页。
if (
(nextOrder === 'ascend' || nextOrder === 'descend')
&& COLLAB_SORT_KEYS.has(nextKey)
) {
setSortKey(nextKey as CollabSortKey);
setSortOrder(nextOrder);
} else {
setSortKey('seq');
setSortOrder('ascend');
}
setPage(1);
}}
rowSelection={canEdit ? {
selectedRowKeys: selectedIds,
onChange: (keys) => setSelectedIds(keys.map(Number)),