From fe1f9596487bff46fa01a39f2271914e84a4cfca Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 17 Oct 2026 16:05:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(huya):=20=E5=8D=8F=E4=BD=9C=E5=8F=B0?= =?UTF-8?q?=E8=B4=A6=E5=9B=BA=E5=AE=9A=E5=BA=8F=E5=8F=B7=E5=B9=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=A1=A8=E5=A4=B4=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_huya_collab_orders.py | 31 ++++++++ web/backend/routers/huya/collab.py | 4 + web/backend/services/huya_collab_service.py | 47 ++++++++++- web/frontend/src/api/types.ts | 3 + web/frontend/src/pages/HuyaCollabPage.tsx | 86 +++++++++++++++++++-- 5 files changed, 161 insertions(+), 10 deletions(-) diff --git a/tests/test_huya_collab_orders.py b/tests/test_huya_collab_orders.py index 4101394..92c2f67 100644 --- a/tests/test_huya_collab_orders.py +++ b/tests/test_huya_collab_orders.py @@ -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): """列表搜索:一个输入框打穿订单号 / 店铺 / 绑定账号·角色 / 平台区服 / 虎牙号 / 礼包 / 备注。""" diff --git a/web/backend/routers/huya/collab.py b/web/backend/routers/huya/collab.py index c0bf462..4fbf11a 100644 --- a/web/backend/routers/huya/collab.py +++ b/web/backend/routers/huya/collab.py @@ -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, ) diff --git a/web/backend/services/huya_collab_service.py b/web/backend/services/huya_collab_service.py index cdc46a3..33beb3b 100644 --- a/web/backend/services/huya_collab_service.py +++ b/web/backend/services/huya_collab_service.py @@ -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() diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index 0708c2a..fed1e66 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -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; } diff --git a/web/frontend/src/pages/HuyaCollabPage.tsx b/web/frontend/src/pages/HuyaCollabPage.tsx index c2131df..29f640c 100644 --- a/web/frontend/src/pages/HuyaCollabPage.tsx +++ b/web/frontend/src/pages/HuyaCollabPage.tsx @@ -105,6 +105,32 @@ const CK_META: Record = { 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 = new Set([ + 'seq', + 'order_no', + 'shop', + 'owner', + 'bind', + 'gift_name', + 'status', + 'uid', + 'ck_status', + 'updated_at', +]); + const ACTION_LABELS: Record = { 'huya:collab:create': '新建', 'huya:collab:update': '编辑', @@ -397,6 +423,9 @@ export default function HuyaCollabPage() { const [giftNames, setGiftNames] = useState([]); const [shopModalOpen, setShopModalOpen] = useState(false); const [filters, setFilters] = useState({ include_void: false }); + // 默认按固定创建序号正序:老单在上、新单永远追加在最后。 + const [sortKey, setSortKey] = useState('seq'); + const [sortOrder, setSortOrder] = useState('ascend'); const [realtimePending, setRealtimePending] = useState(false); const [selectedIds, setSelectedIds] = useState([]); @@ -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['columns'] = [ { - title: '序号', width: 70, fixed: 'left', align: 'center', - render: (_, __, index) => ( - // 跨页连续:第 2 页第 1 行 = pageSize + 1 - {(page - 1) * pageSize + index + 1} + title: '序号', width: 76, fixed: 'left', align: 'center', + ...sortProps('seq'), + render: (_, row) => ( + // 序号 = 创建时分配的固定 id:不管切换排序、筛选还是翻页都不会重排。 + + {row.id} + ), }, { title: '订单编号', dataIndex: 'order_no', width: 170, fixed: 'left', + ...sortProps('order_no'), render: (value: string, row) => ( {value} @@ -1213,6 +1260,7 @@ export default function HuyaCollabPage() { }, { title: '店铺', dataIndex: 'shop', width: 120, + ...sortProps('shop'), render: (value: string, row) => ( (