fix(huya): 协作台账固定序号并支持表头排序
This commit is contained in:
@@ -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):
|
||||
"""列表搜索:一个输入框打穿订单号 / 店铺 / 绑定账号·角色 / 平台区服 / 虎牙号 / 礼包 / 备注。"""
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user