merge: 同步 main 最新到 dev(接单平台店铺筛选)

This commit is contained in:
yml2213
2026-10-22 15:30:38 +08:00
9 changed files with 200 additions and 17 deletions
@@ -453,6 +453,12 @@ export type ListInput = {
/** 后台待办跳转时按工单主键精确定位 */
workOrderId?: number
keyword?: string
/**
* 后台按店铺筛选(精确匹配,可多选)。
* 来源是 `orders.shop_name`:工单只存 `order_id`,`material.source.shopName` 只是货源快照,
* 两者并不等价,所以这里统一按订单归属店铺过滤。
*/
shopNames?: string[]
/** 打手类型筛选:internal 内部打手 / external 外部打手 */
workerType?: string
/** 后台打手列表按主键精确定位。 */
@@ -347,6 +347,7 @@ export async function listWorkOrders({
status = '',
workOrderId = 0,
keyword = '',
shopNames = [],
workerId = 0,
workerLevelIds = [],
categoryId = 0,
@@ -370,6 +371,7 @@ export async function listWorkOrders({
statuses: effectiveStatuses,
workOrderId,
keyword,
shopNames,
workerId,
workerLevelIds,
...(resolvedWorkerIds ? { workerIds: resolvedWorkerIds } : {}),
@@ -455,6 +457,26 @@ export async function listWorkOrders({
}
}
/**
* 后台「店铺」筛选的候选值。
*
* 口径是「订单归属店铺」(orders.shop_name),只列真正挂在工单上的店铺——
* 订单里存在但没同步出工单的店铺如果出现在下拉里,选了必然是空列表。
* 固定全量去重、不随其它筛选变化,避免用户选完店铺后候选自己缩水。
*/
export async function listWorkOrderShopNames(): Promise<string[]> {
const result = await query<{ shop_name: string }>(
`SELECT DISTINCT BTRIM(o.shop_name) AS shop_name
FROM work_orders wo
INNER JOIN orders o ON o.id = wo.order_id
WHERE BTRIM(o.shop_name) <> ''
ORDER BY shop_name ASC`,
)
return result.rows
.map((row) => String(row.shop_name || '').trim())
.filter((shopName) => shopName.length > 0)
}
/** 只返回筛选范围总数,不加载工单 JSON、取消事件和当前页关联字段。 */
export async function countWorkOrders(input: ListInput = {}): Promise<number> {
const resolvedWorkerIds = await resolveWorkerLevelFilterIds(input.workerLevelIds)
@@ -1077,6 +1099,7 @@ export function buildWorkOrderWhere({
statuses = [],
workOrderId = 0,
keyword = '',
shopNames = [],
workerId = 0,
workerLevelIds = [],
workerIds,
@@ -1275,6 +1298,22 @@ export function buildWorkOrderWhere({
filters.push(`${workOrderSearchExpression()} LIKE LOWER($${params.length})`)
}
}
const normalizedShopNames = [
...new Set(shopNames.map((item) => String(item || '').trim()).filter(Boolean)),
]
if (normalizedShopNames.length > 0) {
// 店铺口径是「订单归属店铺」:工单只存 order_id,必须回查 orders.shop_name。
// 下拉选项本身就是真实值,这里做精确匹配,并用 BTRIM 兼容历史脏数据的前后空格
// (表达式索引见 103_work_order_shop_filter_index.sql)。
params.push(normalizedShopNames)
filters.push(`wo.order_id IN (
SELECT shop_order.id
FROM orders shop_order
WHERE BTRIM(shop_order.shop_name) = ANY($${params.length}::text[])
)`)
}
if (workerId) {
params.push(workerId)
filters.push(`(
@@ -14,6 +14,7 @@ import {
getAdminWorkOrderEvents,
getAdminWorkOrderSharing,
listAdminMockCustomShops,
listAdminWorkOrderShopNames,
listAdminWorkOrders,
pinAdminWorkOrder,
publishAdminWorkOrder,
@@ -47,6 +48,17 @@ router.get(
}),
)
// 「店铺」筛选下拉:固定全量候选,不随列表筛选变化(前端 Select 自带搜索过滤)。
router.get(
'/worker-platform/orders/shop-options',
requireAdminPermission('worker_order.view'),
createJsonHandler(() => listAdminWorkOrderShopNames(), {
successMessage: 'ok',
errorMessage: '读取店铺筛选选项失败',
scope: '[admin/worker-platform/orders/shop-options]',
}),
)
router.get(
'/worker-platform/orders/mock/custom-shops',
requireAdminRoles(['admin', 'operator', 'support']),
@@ -3,6 +3,7 @@ import {
listConsignmentInventoriesByIds,
listWorkOrderEventsByOrderId,
listWorkOrderSharesByOrderIds,
listWorkOrderShopNames,
listWorkOrders,
sumPendingUnfreezeByOrderIds,
type ConsignmentInventoryRow,
@@ -49,6 +50,7 @@ function buildWorkOrderStatisticsCacheKey(filters: JsonObject): string {
filters.status || '',
filters.workOrderId || 0,
filters.keyword || '',
filters.shopNames || [],
filters.workerSharingId || 0,
filters.workerId || 0,
filters.workerLevelIds || [],
@@ -92,6 +94,7 @@ export function resolveAdminWorkOrderListFilters(query: JsonObject = {}): ListIn
workOrderId: 0,
categoryId: 0,
keyword,
shopNames: [],
workerSharingId: 0,
workerLevelIds: [],
vipEvidencePending: false,
@@ -106,6 +109,8 @@ export function resolveAdminWorkOrderListFilters(query: JsonObject = {}): ListIn
.map(normalizeOptionalId)
.filter((levelId): levelId is number => levelId !== null)
const statuses = normalizeStatuses(query.status)
// 店铺来自 orders.shop_name,前端按逗号拼串传参,复用同一套数组解析(去重 + trim)。
const shopNames = normalizeStatuses(query.shopNames ?? query.shop_names)
const vipEvidencePending = normalizeBoolean(query.vipEvidencePending, false)
const createdFrom = normalizeDateString(query.createdFrom)
const createdTo = normalizeDateString(query.createdTo)
@@ -116,6 +121,7 @@ export function resolveAdminWorkOrderListFilters(query: JsonObject = {}): ListIn
workOrderId,
categoryId,
keyword,
shopNames,
workerSharingId: workerId,
workerLevelIds,
vipEvidencePending,
@@ -182,3 +188,13 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
statistics,
}
}
/**
* 后台工单「店铺」筛选的下拉候选。
*
* 固定全量去重(不随状态/日期等筛选变化),下拉里带搜索框由前端过滤即可;
* 数据口径见 repository 的 listWorkOrderShopNames(orders.shop_name,精确匹配)。
*/
export async function listAdminWorkOrderShopNames(): Promise<{ items: string[] }> {
return { items: await listWorkOrderShopNames() }
}