diff --git a/apps/backend/src/repositories/order-repo.test.ts b/apps/backend/src/repositories/order-repo.test.ts index 043593ee..27e94cbe 100644 --- a/apps/backend/src/repositories/order-repo.test.ts +++ b/apps/backend/src/repositories/order-repo.test.ts @@ -100,6 +100,44 @@ test('订单列表 platformOrderId 为拼多多 order_sn 时走索引快路径', assert.deepEqual(calls[0]?.params, [orderSn]) }) +test('订单列表按当前履约模式筛选,不再依赖任务执行器', async () => { + const calls: Array<{ sql: string; params: unknown[] }> = [] + + await listOrdersWithExecutor(createExecutor(calls, 0), { + fulfillmentMode: 'manual', + }) + + assert.match(calls[0]?.sql || '', /oi\.fulfillment_mode = \$1/) + assert.match(calls[0]?.sql || '', /oi\.fulfillment_mode <> \$1/) + assert.doesNotMatch(calls[0]?.sql || '', /COUNT\(DISTINCT/) + assert.doesNotMatch(calls[0]?.sql || '', /ft\.executor_key/) + assert.deepEqual(calls[0]?.params, ['manual']) +}) + +test('订单列表待判定筛选不扫描任务执行器', async () => { + const calls: Array<{ sql: string; params: unknown[] }> = [] + + await listOrdersWithExecutor(createExecutor(calls, 0), { + fulfillmentMode: 'pending', + }) + + assert.match(calls[0]?.sql || '', /NOT EXISTS/) + assert.match(calls[0]?.sql || '', /oi\.fulfillment_mode IN/) + assert.doesNotMatch(calls[0]?.sql || '', /COUNT\(DISTINCT/) + assert.deepEqual(calls[0]?.params, []) +}) + +test('订单列表混合履约筛选按订单项聚合判定', async () => { + const calls: Array<{ sql: string; params: unknown[] }> = [] + + await listOrdersWithExecutor(createExecutor(calls, 0), { + fulfillmentMode: 'mixed', + }) + + assert.match(calls[0]?.sql || '', /HAVING COUNT\(DISTINCT oi\.fulfillment_mode\) > 1/) + assert.deepEqual(calls[0]?.params, []) +}) + function createExecutor( calls: Array<{ sql: string; params: unknown[] }>, total: number, diff --git a/apps/backend/src/repositories/order-repo.ts b/apps/backend/src/repositories/order-repo.ts index e9b79b6f..90b445ea 100644 --- a/apps/backend/src/repositories/order-repo.ts +++ b/apps/backend/src/repositories/order-repo.ts @@ -240,6 +240,7 @@ export async function listOrdersWithExecutor( keyword = '', provider = '', executorKey = '', + fulfillmentMode = '', platformOrderId = '', payStatus = '', taskStatus = '', @@ -333,6 +334,38 @@ export async function listOrdersWithExecutor( ) } + if (fulfillmentMode) { + const knownModes = `('work_order', 'affiliate_dash', 'legacy_platform', 'manual')` + if (fulfillmentMode === 'pending') { + filters.push(`NOT EXISTS ( + SELECT 1 FROM order_items oi + WHERE oi.order_id = o.id AND oi.fulfillment_mode IN ${knownModes} + )`) + } else if (fulfillmentMode === 'mixed') { + filters.push(`o.id IN ( + SELECT oi.order_id FROM order_items oi + WHERE oi.fulfillment_mode IN ${knownModes} + GROUP BY oi.order_id + HAVING COUNT(DISTINCT oi.fulfillment_mode) > 1 + )`) + } else { + params.push(fulfillmentMode) + const modeParam = `$${params.length}` + filters.push(`( + EXISTS ( + SELECT 1 FROM order_items oi + WHERE oi.order_id = o.id AND oi.fulfillment_mode = ${modeParam} + ) + AND NOT EXISTS ( + SELECT 1 FROM order_items oi + WHERE oi.order_id = o.id + AND oi.fulfillment_mode IN ${knownModes} + AND oi.fulfillment_mode <> ${modeParam} + ) + )`) + } + } + if (skuCode) { params.push(skuCode) filters.push( diff --git a/apps/backend/src/routes/admin/finance.ts b/apps/backend/src/routes/admin/finance.ts index 7c80bf3c..32976fe1 100644 --- a/apps/backend/src/routes/admin/finance.ts +++ b/apps/backend/src/routes/admin/finance.ts @@ -11,10 +11,6 @@ import { listSalesDailyFacts, rebuildSalesDailyFacts, } from '../../services/finance/sales-facts-service.js' -import { - createManualOrderCost, - listManualOrderCostItems, -} from '../../services/finance/manual-order-cost-service.js' import { confirmManualFulfillment, listManualFulfillmentPending, @@ -118,45 +114,6 @@ router.post( ), ) -router.get( - '/finance/manual-order-cost-items', - requireAnyAdminPermission(['finance.view', 'fund.view']), - createJsonHandler(() => listManualOrderCostItems(), { - successMessage: 'ok', - errorMessage: '读取人工录单成本项失败', - scope: '[admin/finance/manual-order-cost-items]', - }), -) - -router.post( - '/finance/manual-order-costs', - requireAnyAdminPermission(['fund.expense', 'finance.config']), - createJsonHandler((req) => createManualOrderCost(bodyOf(req), sessionOf(req)), { - successMessage: '订单人工成本已录入', - errorMessage: '录入订单人工成本失败', - scope: '[admin/finance/manual-order-costs]', - audit: (req, data) => { - const result = data as { - expenseNo?: string - amountFen?: number - orderId?: number - costSnapshotId?: number | null - } - return { - action: 'manual_order_cost_created', - targetType: 'fund_expense', - targetId: result.expenseNo || '', - data: { - expenseNo: result.expenseNo || '', - amountFen: result.amountFen || 0, - orderId: result.orderId || req.body?.orderId || '', - costSnapshotId: result.costSnapshotId || null, - }, - } - }, - }), -) - // --------------------------------------------------------------------------- // 人工履约待办与确认履约 // --------------------------------------------------------------------------- diff --git a/apps/backend/src/services/admin/admin-order-read-helpers.ts b/apps/backend/src/services/admin/admin-order-read-helpers.ts index b2706bb1..233971e1 100644 --- a/apps/backend/src/services/admin/admin-order-read-helpers.ts +++ b/apps/backend/src/services/admin/admin-order-read-helpers.ts @@ -28,11 +28,13 @@ export function mapAdminOrderListItem( orderItems = [], vouchers = [], workOrderCount = 0, + manualConfig = { pending: 0, unconfigured: 0 }, }: { tasks?: TaskRow[] orderItems?: OrderItemRow[] vouchers?: KuaishouIndustryVoucherRow[] workOrderCount?: number + manualConfig?: { pending: number; unconfigured: number } } = {}, ): AdminOrderListItem { const fulfillmentProgress = buildOrderFulfillmentProgress(tasks) @@ -121,6 +123,8 @@ export function mapAdminOrderListItem( fulfillmentMode: fulfillment.fulfillmentMode, manualItemCount: fulfillment.manualItemCount, pendingItemCount: fulfillment.pendingItemCount, + manualPendingCount: manualConfig.pending, + manualUnconfiguredCount: manualConfig.unconfigured, hasVoucher: fulfillment.hasVoucher, voucherState: fulfillment.voucherState, canClaimH5: fulfillment.canClaimH5, diff --git a/apps/backend/src/services/admin/admin-read-service.ts b/apps/backend/src/services/admin/admin-read-service.ts index e46d4171..5e2da147 100644 --- a/apps/backend/src/services/admin/admin-read-service.ts +++ b/apps/backend/src/services/admin/admin-read-service.ts @@ -7,6 +7,7 @@ import { } from '../../repositories/order-item-repo.js' import { getOrderById, listOrders } from '../../repositories/order-repo.js' import { listOrderRefunds } from '../finance/order-refund-service.js' +import { getManualFulfillmentCountsByOrderIds } from '../finance/manual-fulfillment-service.js' import { getTaskById, listTasks, @@ -104,6 +105,7 @@ export async function getAdminOrders( keyword: String(query.keyword || '').trim(), provider: String(query.provider || '').trim(), executorKey: String(query.executorKey || '').trim(), + fulfillmentMode: normalizeOrderFulfillmentModeFilter(query.fulfillmentMode), platformOrderId: String(query.platformOrderId || '').trim(), payStatus: String(query.payStatus || '').trim(), taskStatus: String(query.taskStatus || '').trim(), @@ -114,11 +116,12 @@ export async function getAdminOrders( const orderIds = items.map((item) => Number(item.id)).filter((id) => id > 0) const oids = items.map((item) => item.platform_order_id).filter(Boolean) - const [orderItems, tasks, vouchers, workOrders] = await Promise.all([ + const [orderItems, tasks, vouchers, workOrders, manualCounts] = await Promise.all([ listOrderItemsByOrderIds(orderIds), listTasksByOrderIds(orderIds), listKuaishouIndustryVouchersByOids(oids), listWorkOrdersByOrderIds(orderIds), + getManualFulfillmentCountsByOrderIds(orderIds), ]) const orderItemsByOrderId = groupRowsByOrderId(orderItems) const tasksByOrderId = groupRowsByOrderId(tasks) @@ -136,12 +139,29 @@ export async function getAdminOrders( tasks: orderTasks, vouchers: orderVouchers, workOrderCount: orderWorkOrders.length, + manualConfig: manualCounts.get(orderId) || { pending: 0, unconfigured: 0 }, }) }), pagination: { page, pageSize, total }, } } +const ADMIN_ORDER_FULFILLMENT_MODES = new Set([ + 'work_order', + 'affiliate_dash', + 'legacy_platform', + 'manual', + 'mixed', + 'pending', +]) + +function normalizeOrderFulfillmentModeFilter(value: unknown): string { + const mode = String(value || '') + .trim() + .toLowerCase() + return ADMIN_ORDER_FULFILLMENT_MODES.has(mode) ? mode : '' +} + function groupRowsByOrderId( rows: T[], ): Map { @@ -182,12 +202,14 @@ export async function getAdminOrderDetail( }) } - const [items, tasks, workOrders, refunds] = await Promise.all([ + const [items, tasks, workOrders, refunds, manualCounts] = await Promise.all([ listOrderItemsByOrderId(order.id), listTasksByOrderId(order.id), listWorkOrdersByOrderId(order.id), listOrderRefunds(order.id), + getManualFulfillmentCountsByOrderIds([order.id]), ]) + const manualConfig = manualCounts.get(Number(order.id)) || { pending: 0, unconfigured: 0 } const kuaishouIndustryVouchers = await listKuaishouIndustryVouchersByOid(order.platform_order_id) const itemSummary = summarizeOrderItems(items, order, kuaishouIndustryVouchers) const orderItemTitle = resolveOrderItemTitle(items[0], order, kuaishouIndustryVouchers) @@ -233,6 +255,8 @@ export async function getAdminOrderDetail( fulfillmentMode: fulfillment.fulfillmentMode, manualItemCount: fulfillment.manualItemCount, pendingItemCount: fulfillment.pendingItemCount, + manualPendingCount: manualConfig.pending, + manualUnconfiguredCount: manualConfig.unconfigured, hasVoucher: fulfillment.hasVoucher, voucherState: fulfillment.voucherState, workOrderCount: workOrders.length, diff --git a/apps/backend/src/services/finance/manual-fulfillment-service.ts b/apps/backend/src/services/finance/manual-fulfillment-service.ts index 4a7d5896..eb015ebf 100644 --- a/apps/backend/src/services/finance/manual-fulfillment-service.ts +++ b/apps/backend/src/services/finance/manual-fulfillment-service.ts @@ -78,6 +78,12 @@ export async function listManualFulfillmentPending( } else if (status === 'fulfilled') { conditions.push('fulfilled.fulfilled_at IS NOT NULL') } + // 订单页按单打开确认框:只取该订单的待办项。 + const orderId = Number(input.orderId ?? input.order_id ?? 0) + if (Number.isFinite(orderId) && orderId > 0) { + params.push(Math.trunc(orderId)) + conditions.push(`oi.order_id = $${params.length}`) + } if (keyword) { params.push(`%${escapeLike(keyword)}%`) const placeholder = `$${params.length}` @@ -565,3 +571,52 @@ function optionalText(value: unknown, maxLength: number) { function escapeLike(value: string) { return value.replace(/[\\%_]/g, (char) => `\\${char}`) } + +/** + * 订单列表/详情用:按订单统计「可确认履约」与「未配置履约渠道」的数量。 + * + * - pending:有人工履约配置且还没有 manual_case 快照; + * - unconfigured:人工单但没有绑定履约配置(需要先到成本中心配 SKU)。 + */ +export async function getManualFulfillmentCountsByOrderIds( + orderIds: Array, +): Promise> { + const ids = orderIds.map(Number).filter((id) => Number.isFinite(id) && id > 0) + if (ids.length === 0) return new Map() + const result = await query<{ + order_id: number + pending_count: number + unconfigured_count: number + }>( + ` + WITH fulfilled AS ( + SELECT order_item_id, MAX(created_at) AS fulfilled_at + FROM cost_snapshots + WHERE subject_type = 'manual_case' AND superseded_at IS NULL + GROUP BY order_item_id + ) + SELECT + oi.order_id, + COUNT(*) FILTER ( + WHERE cfg.id IS NOT NULL AND fulfilled.fulfilled_at IS NULL + )::int AS pending_count, + COUNT(*) FILTER (WHERE cfg.id IS NULL)::int AS unconfigured_count + FROM order_items oi + LEFT JOIN manual_fulfillment_configs cfg ON cfg.id = oi.fulfillment_config_id + LEFT JOIN fulfilled ON fulfilled.order_item_id = oi.id + WHERE oi.order_id = ANY($1::bigint[]) + AND oi.fulfillment_mode = 'manual' + GROUP BY oi.order_id + `, + [ids], + ) + return new Map( + result.rows.map((row) => [ + Number(row.order_id), + { + pending: Number(row.pending_count || 0), + unconfigured: Number(row.unconfigured_count || 0), + }, + ]), + ) +} diff --git a/apps/backend/src/services/finance/manual-order-cost-service.ts b/apps/backend/src/services/finance/manual-order-cost-service.ts deleted file mode 100644 index 08fe9d49..00000000 --- a/apps/backend/src/services/finance/manual-order-cost-service.ts +++ /dev/null @@ -1,547 +0,0 @@ -import type { PoolClient } from 'pg' - -import { query, withTransaction } from '../../db/client.js' -import type { JsonObject } from '../../types/json.js' -import { createHttpError } from '../../utils/http.js' -import { nowIso } from '../../utils/time.js' -import type { AdminSession } from '../admin/admin-auth-service.js' -import { getOwnAgentAccountId, writeFundExpense } from '../admin/admin-fund-service.js' - -/** - * 订单页「人工录单」服务。 - * - * 口径(2026-10-23 确认): - * * 人工成本逐单、按实际金额,不按工时、不按固定单量/固定单价; - * * 成本从客服备用金账户直接扣款,客服角色只能用本人账户,管理员/财务可代录; - * * **没有登记单、没有审批、没有报销**:录入即写 actual 成本快照; - * * 资金台账唯一真源是 manual_expenses(客服备用金支出明细),本服务不另存支出表。 - * - * 成本快照主体:subject_type='manual_case',subject_id=manual_expenses.id。 - */ - -const PAYEE_TYPES = new Set(['worker', 'upstream', 'customer', 'platform', 'other']) -const PAYMENT_METHODS = new Set(['wechat', 'alipay', 'bank', 'cash', 'platform_balance', 'other']) -/** 人工录单的费用类型;与备用金分类的映射见 mapFundExpenseCategory()。 */ -const EXPENSE_CATEGORIES = new Set([ - 'goods_purchase', - 'compensation', - 'refund_advance', - 'service_fee', - 'freight', - 'recharge', - 'other', -]) - -type ManualOrderCostResult = { - expenseNo: string - fundExpenseId: number - fundAccountId: number - fundAccountNo: string - amountFen: number - orderId: number - orderItemId: number - costSnapshotId: number | null - fulfillmentMode: string - warning: string -} - -/** - * 录入一笔人工成本:扣客服备用金 + 写 actual 成本快照。 - * - * 全流程在同一事务内:余额不足直接拒绝,不允许负余额。 - */ -export async function createManualOrderCost( - payload: JsonObject, - session: AdminSession, -): Promise { - const orderId = normalizeId(payload.orderId ?? payload.order_id, '订单号无效') - const amountFen = normalizeAmountFen(payload.amountFen ?? payload.amount_fen) - const note = optionalText(payload.note, 500) || '订单页人工录单' - const expenseCategory = normalizeStatus( - payload.expenseCategory ?? payload.expense_category, - EXPENSE_CATEGORIES, - 'service_fee', - '费用类型无效', - ) - const payeeName = optionalText(payload.payeeName ?? payload.payee_name, 120) || '内部人工' - const payeeType = normalizeStatus( - payload.payeeType ?? payload.payee_type, - PAYEE_TYPES, - 'other', - '收款方类型无效', - ) - const paymentMethod = normalizeStatus( - payload.paymentMethod ?? payload.payment_method, - PAYMENT_METHODS, - 'other', - '付款方式无效', - ) - const evidenceJson = normalizeEvidence(payload.evidenceJson ?? payload.evidence_json) - if (!hasEvidence(evidenceJson)) { - evidenceJson.note = note - } - - const orderItemId = await resolveOrderItemId( - orderId, - normalizeOptionalId(payload.orderItemId ?? payload.order_item_id), - ) - const fundAccountId = await resolveFundAccountId( - normalizeOptionalId(payload.fundAccountId ?? payload.fund_account_id), - session, - ) - const costItemId = await resolveManualCostItemId() - const paidAt = nowIso() - - return withTransaction(async (client) => { - const fundWritten = await writeFundExpense(client, { - agentAccountId: fundAccountId, - amountFen, - category: mapFundExpenseCategory(expenseCategory), - payeeType: mapFundPayeeType(payeeType), - payeeName, - payeeAccount: optionalText(payload.payeeAccount ?? payload.payee_account, 120), - orderId, - orderItemId, - paidAt, - evidence: toFundEvidence(evidenceJson), - note: buildFundExpenseNote(note, optionalText(evidenceJson.reference, 200)), - actorUserId: session.userId, - }) - const fundExpenseId = Number(fundWritten.expense.id) - const fundAccountNo = await loadFundAccountNo(client, fundAccountId) - - const context = await loadOrderItemContext(client, orderItemId) - const versionId = await resolveCostVersionId(client, costItemId, context, paidAt) - let costSnapshotId: number | null = null - let warning = '' - - if (!versionId) { - // 匹配不到有效成本版本不允许静默记 0,必须进异常队列。 - await client.query( - ` - INSERT INTO cost_exceptions ( - subject_type, subject_id, order_item_id, work_order_id, - reason, status, detail_json, created_at - ) VALUES ( - 'order_item', $1, $1, $2, 'manual_expense_cost_missing', 'open', $3::jsonb, NOW() - ) - ON CONFLICT (subject_type, subject_id, reason) DO NOTHING - `, - [ - orderItemId, - context.workOrderId, - JSON.stringify({ - fundExpenseId, - fundExpenseNo: fundWritten.expense.expense_no, - amountFen, - message: '人工录单已扣备用金,但该成本项没有匹配到有效成本版本', - }), - ], - ) - warning = '人工录单已扣备用金,但未匹配到有效成本版本,已进入异常队列' - } else { - const snapshotResult = await client.query<{ id: number }>( - ` - INSERT INTO cost_snapshots ( - subject_type, subject_id, order_id, order_item_id, work_order_id, - cost_item_id, cost_version_id, canonical_product_id, - quantity, unit_amount_fen, total_amount_fen, snapshot_stage, source, - metadata_json, cost_basis_at, resolved_at, created_at - ) VALUES ( - 'manual_case', $1, $2, $3, $4, - $5, $6, $7, - 1, $8, $8, 'actual', 'manual_input', - $9::jsonb, $10, $11, $11 - ) - ON CONFLICT (subject_type, subject_id, cost_item_id) - WHERE superseded_at IS NULL - DO NOTHING - RETURNING id - `, - [ - fundExpenseId, - orderId, - orderItemId, - context.workOrderId, - costItemId, - versionId, - context.canonicalProductId, - amountFen, - JSON.stringify({ - source: 'manual_order_cost', - fundExpenseId, - fundExpenseNo: fundWritten.expense.expense_no, - fundAccountNo, - paidByName: fundWritten.ownerName, - payeeName, - payeeType, - paymentMethod, - expenseCategory, - evidence: evidenceJson, - fulfillmentMode: context.fulfillmentMode, - }), - paidAt, - nowIso(), - ], - ) - costSnapshotId = snapshotResult.rows[0]?.id ?? null - if (!costSnapshotId) { - // 唯一索引冲突意味着同一主体+成本项已有有效快照。钱已经扣了,不能静默吞掉, - // 否则会出现「钱扣了、成本没记」的账实不符。 - throw createHttpError( - '该订单项已有同成本项的人工成本快照,本次未写入成本;备用金扣款已回滚,请改用新的支出单或先处理已有快照', - { statusCode: 409, errorCode: 'manual_order_cost_snapshot_conflict' }, - ) - } - } - - return { - expenseNo: fundWritten.expense.expense_no, - fundExpenseId, - fundAccountId, - fundAccountNo, - amountFen, - orderId, - orderItemId, - costSnapshotId, - fulfillmentMode: context.fulfillmentMode, - warning, - } - }) -} - -/** 人工录单可用的成本项:人工客服履约成本。 */ -export async function listManualOrderCostItems() { - const result = await query<{ - id: number - code: string - name: string - cost_model: string - pricing_mode: string - }>( - ` - SELECT id, code, name, cost_model, pricing_mode - FROM cost_items - WHERE status = 'active' - AND cost_model = 'manual_actual' - ORDER BY id - `, - ) - return { - items: result.rows.map((row) => ({ - id: Number(row.id), - code: row.code, - name: row.name, - costModel: row.cost_model, - pricingMode: row.pricing_mode, - })), - } -} - -async function resolveOrderItemId(orderId: number, requestedItemId: number | null) { - const itemsResult = await query<{ id: number }>( - 'SELECT id FROM order_items WHERE order_id = $1 ORDER BY id ASC', - [orderId], - ) - if (itemsResult.rows.length === 0) { - throw createHttpError('订单没有订单项,无法录入人工成本', { - statusCode: 400, - errorCode: 'manual_order_cost_items_missing', - }) - } - if (requestedItemId) { - if (!itemsResult.rows.some((row) => Number(row.id) === requestedItemId)) { - throw createHttpError('订单项不属于该订单', { - statusCode: 400, - errorCode: 'manual_order_cost_item_mismatch', - }) - } - return requestedItemId - } - if (itemsResult.rows.length === 1) { - return Number(itemsResult.rows[0]?.id || 0) - } - throw createHttpError('订单有多个订单项,请选择要计入的订单项', { - statusCode: 400, - errorCode: 'manual_order_cost_item_required', - }) -} - -/** - * 备用金账户归属校验:客服只能用本人账户;管理员/财务可代录到指定客服账户。 - */ -async function resolveFundAccountId(requestedFundAccountId: number | null, session: AdminSession) { - if (session.role === 'support') { - const ownAccountId = await getOwnAgentAccountId(session.userId) - if (!ownAccountId) { - throw createHttpError('当前账号还没有备用金账户', { - statusCode: 404, - errorCode: 'fund_agent_account_not_found', - }) - } - if (requestedFundAccountId && requestedFundAccountId !== ownAccountId) { - throw createHttpError('只能使用自己的备用金账户', { - statusCode: 403, - errorCode: 'manual_order_cost_fund_account_forbidden', - }) - } - return ownAccountId - } - if (!requestedFundAccountId) { - throw createHttpError('请选择备用金账户,人工录单必须从备用金扣款', { - statusCode: 400, - errorCode: 'manual_order_cost_fund_account_required', - }) - } - return requestedFundAccountId -} - -async function resolveManualCostItemId() { - const result = await query<{ id: number }>( - `SELECT id FROM cost_items - WHERE code = 'sys_manual_service' AND status = 'active' - LIMIT 1`, - ) - const costItemId = Number(result.rows[0]?.id || 0) - if (!costItemId) { - throw createHttpError('人工客服履约成本项未启用', { - statusCode: 409, - errorCode: 'manual_order_cost_item_unavailable', - }) - } - return costItemId -} - -type OrderItemContext = { - orderId: number - canonicalProductId: number | null - provider: string - shopId: string - workOrderId: number | null - workProductRuleId: number | null - fulfillmentMode: string -} - -async function loadFundAccountNo(client: PoolClient, fundAccountId: number) { - const result = await client.query<{ account_no: string }>( - 'SELECT account_no FROM fund_accounts WHERE id = $1 LIMIT 1', - [fundAccountId], - ) - return result.rows[0]?.account_no || '' -} - -async function loadOrderItemContext( - client: PoolClient, - orderItemId: number, -): Promise { - const result = await client.query( - ` - SELECT - oi.order_id, - oi.canonical_product_id, - o.provider, - o.shop_id, - wo.id AS work_order_id, - wo.product_rule_id AS work_product_rule_id, - COALESCE(oi.fulfillment_mode, 'manual') AS fulfillment_mode - FROM order_items oi - JOIN orders o ON o.id = oi.order_id - LEFT JOIN LATERAL ( - SELECT id, product_rule_id - FROM work_orders - WHERE order_item_id = oi.id - ORDER BY id DESC - LIMIT 1 - ) wo ON TRUE - WHERE oi.id = $1 - LIMIT 1 - `, - [orderItemId], - ) - const row = result.rows[0] - if (!row) { - throw createHttpError('订单项不存在', { - statusCode: 404, - errorCode: 'manual_order_cost_order_item_not_found', - }) - } - return row -} - -/** - * 解析成本版本:优先级 rule > product > shop > provider > global,按业务发生时间匹配。 - * 人工成本项的 draft 占位版本也接受,避免未启用正式版本时人工单无法入账。 - */ -async function resolveCostVersionId( - client: PoolClient, - costItemId: number, - context: OrderItemContext, - basisAt: string, -) { - const result = await client.query<{ id: number }>( - ` - SELECT v.id - FROM cost_versions v - WHERE v.cost_item_id = $1 - AND ( - v.status = 'active' - OR ( - v.status = 'draft' - AND EXISTS ( - SELECT 1 FROM cost_items ci - WHERE ci.id = v.cost_item_id AND ci.code = 'sys_manual_service' - ) - ) - ) - AND v.effective_from <= $2 - AND (v.effective_to IS NULL OR v.effective_to > $2) - AND ( - (v.scope_type = 'rule' AND $6::bigint IS NOT NULL AND v.work_product_rule_id = $6) - OR (v.scope_type = 'product' AND $5::bigint IS NOT NULL AND v.canonical_product_id = $5) - OR (v.scope_type = 'shop' AND v.provider = $3 AND v.shop_id = $4) - OR (v.scope_type = 'provider' AND v.provider = $3) - OR (v.scope_type = 'global') - ) - ORDER BY - CASE v.scope_type - WHEN 'rule' THEN 1 - WHEN 'product' THEN 2 - WHEN 'shop' THEN 3 - WHEN 'provider' THEN 4 - ELSE 5 - END, - v.effective_from DESC, - v.id DESC - LIMIT 1 - `, - [ - costItemId, - basisAt, - context.provider, - context.shopId, - context.canonicalProductId, - context.workProductRuleId, - ], - ) - return result.rows[0]?.id ?? null -} - -/** 人工录单的费用类型映射到备用金支出分类。 */ -function mapFundExpenseCategory(category: string): string { - switch (category) { - case 'goods_purchase': - return 'purchase' - case 'compensation': - return 'compensation' - case 'freight': - return 'freight' - case 'recharge': - return 'platform_recharge' - default: - return 'other' - } -} - -function mapFundPayeeType(payeeType: string): string { - switch (payeeType) { - case 'worker': - case 'upstream': - return 'supplier' - case 'customer': - return 'customer' - case 'platform': - return 'platform' - default: - return 'other' - } -} - -function toFundEvidence(evidence: JsonObject): JsonObject[] { - const items: JsonObject[] = [] - const urls = Array.isArray(evidence.urls) ? evidence.urls : [] - for (const value of urls) { - const url = String(value || '').trim() - if (url) { - items.push({ url, filename: '人工录单凭证', contentType: 'image/*' }) - } - } - const reference = String(evidence.reference || '').trim() - if (/^https?:\/\//i.test(reference) && !items.some((item) => item.url === reference)) { - items.push({ url: reference, filename: '人工录单凭证', contentType: 'image/*' }) - } - return items.slice(0, 6) -} - -function buildFundExpenseNote(note: string, reference: string): string { - const parts = ['人工录单'] - if (reference) parts.push(`凭证:${reference}`) - if (note) parts.push(note) - return parts.join(' · ').slice(0, 500) -} - -function normalizeEvidence(value: unknown): JsonObject { - if (value && typeof value === 'object' && !Array.isArray(value)) { - return value as JsonObject - } - const text = String(value || '').trim() - return text ? { reference: text } : {} -} - -function hasEvidence(evidence: JsonObject) { - const urls = Array.isArray(evidence.urls) ? evidence.urls : [] - return ( - urls.length > 0 || - Boolean(String(evidence.reference || '').trim()) || - Boolean(String(evidence.note || '').trim()) - ) -} - -function normalizeAmountFen(value: unknown) { - const amount = Math.trunc(Number(value) || 0) - if (amount <= 0) { - throw createHttpError('支出金额必须大于 0', { - statusCode: 400, - errorCode: 'manual_order_cost_amount_invalid', - }) - } - if (amount > 100_000_000_00) { - throw createHttpError('支出金额超出允许范围', { - statusCode: 400, - errorCode: 'manual_order_cost_amount_too_large', - }) - } - return amount -} - -function normalizeOptionalId(value: unknown) { - const parsed = Number(value) - return Number.isInteger(parsed) && parsed > 0 ? parsed : null -} - -function normalizeId(value: unknown, message: string) { - const parsed = Number(value) - if (!Number.isInteger(parsed) || parsed <= 0) { - throw createHttpError(message, { - statusCode: 400, - errorCode: 'manual_order_cost_id_invalid', - }) - } - return parsed -} - -function normalizeStatus(value: unknown, allowed: Set, fallback: string, message: string) { - const text = String(value || '').trim() || fallback - if (!allowed.has(text)) { - throw createHttpError(message, { - statusCode: 400, - errorCode: 'manual_order_cost_value_invalid', - }) - } - return text -} - -function optionalText(value: unknown, maxLength: number) { - return String(value || '') - .trim() - .slice(0, maxLength) -} diff --git a/apps/backend/src/types/admin/read-inputs.ts b/apps/backend/src/types/admin/read-inputs.ts index aca413f0..83c5296b 100644 --- a/apps/backend/src/types/admin/read-inputs.ts +++ b/apps/backend/src/types/admin/read-inputs.ts @@ -11,6 +11,7 @@ export type AdminOrderListQueryInput = { keyword?: string provider?: string executorKey?: string + fulfillmentMode?: string platformOrderId?: string payStatus?: string taskStatus?: string diff --git a/apps/backend/src/types/admin/read-models.ts b/apps/backend/src/types/admin/read-models.ts index 6767188a..4bd01e3a 100644 --- a/apps/backend/src/types/admin/read-models.ts +++ b/apps/backend/src/types/admin/read-models.ts @@ -61,6 +61,8 @@ export type AdminOrderListItem = { fulfillmentMode: AdminOrderFulfillmentMode manualItemCount: number pendingItemCount: number + manualPendingCount: number + manualUnconfiguredCount: number hasVoucher: boolean voucherState: AdminVoucherState workOrderCount?: number diff --git a/apps/backend/src/types/repository/inputs.ts b/apps/backend/src/types/repository/inputs.ts index 73947ff4..90825db9 100644 --- a/apps/backend/src/types/repository/inputs.ts +++ b/apps/backend/src/types/repository/inputs.ts @@ -4,6 +4,7 @@ export type OrderListQueryInput = { keyword?: string provider?: string executorKey?: string + fulfillmentMode?: string platformOrderId?: string payStatus?: string taskStatus?: string diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index e3c85226..2596bd49 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -23,7 +23,6 @@ const AdminDashboardPage = lazy(() => import('@/pages/admin/AdminDashboardPage') const AdminFinancePage = lazy(() => import('@/pages/admin/AdminFinancePage')) const AdminSalesProfitPage = lazy(() => import('@/pages/admin/AdminSalesProfitPage')) const AdminCostCenterPage = lazy(() => import('@/pages/admin/AdminCostCenterPage')) -const AdminManualFulfillmentPage = lazy(() => import('@/pages/admin/AdminManualFulfillmentPage')) const AdminFundPage = lazy(() => import('@/pages/admin/AdminFundPage')) const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage')) const AdminKuaishouIndustryPage = lazy(() => import('@/pages/admin/AdminKuaishouIndustryPage')) @@ -145,7 +144,6 @@ function AdminApplication() { } /> } /> } /> - } /> }> } /> diff --git a/apps/frontend/src/components/admin/ManualFulfillmentConfirmModal.tsx b/apps/frontend/src/components/admin/ManualFulfillmentConfirmModal.tsx new file mode 100644 index 00000000..5dfdf92c --- /dev/null +++ b/apps/frontend/src/components/admin/ManualFulfillmentConfirmModal.tsx @@ -0,0 +1,222 @@ +/** + * 确认人工履约弹窗(订单列表 / 订单详情共用)。 + * + * 口径(2026-10-24): + * * 渠道与消耗积分在成本中心提前配好,客服不选、不填; + * * 固定成本 → 直接确认; + * * 成本不固定(消耗积分 = 0)→ 只填本次实际成本; + * * 确认后扣对应平台积分/备用金并写成本快照。 + * + * 原「人工履约待办」页面已下线,入口收敛到订单界面。 + */ +import { CheckOutlined } from '@ant-design/icons' +import { + Alert, + App, + Form, + Input, + InputNumber, + Modal, + Select, + Space, + Statistic, + Tag, + Typography, +} from 'antd' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useEffect, useState } from 'react' + +import { confirmAdminManualFulfillment, fetchAdminManualFulfillmentPending } from '@/services/admin' +import type { ManualFulfillmentPendingItem } from '@/types/admin' + +type ConfirmFormValues = { + costYuan?: number + payeeName?: string + note?: string +} + +const PLATFORM_TAG_COLORS: Record = { + nanhang: 'blue', + lewan: 'purple', + rock: 'cyan', + fund_cash: 'orange', +} + +export type ManualFulfillmentConfirmModalProps = { + open: boolean + orderId: number | null + initialOrderItemId?: number | null + onClose: () => void + onConfirmed?: () => void +} + +export function ManualFulfillmentConfirmModal({ + open, + orderId, + initialOrderItemId = null, + onClose, + onConfirmed, +}: ManualFulfillmentConfirmModalProps) { + const { message } = App.useApp() + const queryClient = useQueryClient() + const [form] = Form.useForm() + const [selectedId, setSelectedId] = useState(null) + + const pendingQuery = useQuery({ + queryKey: ['admin-manual-fulfillment-pending', 'order', orderId], + queryFn: () => + fetchAdminManualFulfillmentPending({ + orderId: orderId || undefined, + status: 'pending', + page: 1, + pageSize: 20, + }), + enabled: open && Boolean(orderId), + }) + + const items = pendingQuery.data?.data.items || [] + const selected = + items.find((item) => item.orderItemId === selectedId) || + items.find((item) => item.orderItemId === initialOrderItemId) || + items[0] || + null + + useEffect(() => { + if (!open) return + setSelectedId(initialOrderItemId || null) + form.resetFields() + }, [open, initialOrderItemId, form]) + + const confirmMutation = useMutation({ + mutationFn: (values: ConfirmFormValues) => + confirmAdminManualFulfillment({ + orderItemId: selected?.orderItemId || 0, + costFen: values.costYuan === undefined ? undefined : Math.round(values.costYuan * 100), + payeeName: values.payeeName || '', + note: values.note || '', + }), + onSuccess: (response) => { + const result = response.data + message.success( + `履约已确认,扣 ${result.creditQuantity} 积分,成本 ¥${(result.costFen / 100).toFixed(2)}`, + ) + void queryClient.invalidateQueries({ queryKey: ['admin-manual-fulfillment-pending'] }) + void queryClient.invalidateQueries({ queryKey: ['admin-order-detail'] }) + void queryClient.invalidateQueries({ queryKey: ['admin-orders'] }) + void queryClient.invalidateQueries({ queryKey: ['admin-fund-my'] }) + void queryClient.invalidateQueries({ queryKey: ['admin-fund-accounts'] }) + void queryClient.invalidateQueries({ queryKey: ['admin-platform-credit-accounts'] }) + void queryClient.invalidateQueries({ queryKey: ['admin-platform-credit-entries'] }) + onConfirmed?.() + onClose() + }, + onError: (error) => { + message.error(error instanceof Error ? error.message : '确认履约失败') + }, + }) + + const showCostInput = selected?.costFen === null + + return ( + , disabled: !selected }} + cancelText="取消" + confirmLoading={confirmMutation.isPending} + onOk={() => { + void form.validateFields().then((values) => confirmMutation.mutate(values)) + }} + onCancel={onClose} + destroyOnHidden + width={560} + > + {pendingQuery.isLoading ? ( + 正在读取待履约商品… + ) : items.length === 0 ? ( + + ) : ( + + {items.length > 1 ? ( + + + + + + + + )} + + ) +} diff --git a/apps/frontend/src/components/admin/ManualOrderCostModal.tsx b/apps/frontend/src/components/admin/ManualOrderCostModal.tsx deleted file mode 100644 index 076e3557..00000000 --- a/apps/frontend/src/components/admin/ManualOrderCostModal.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import { App, Alert, Form, Input, InputNumber, Modal, Select } from 'antd' -import type { FormInstance } from 'antd' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { useEffect, useState } from 'react' - -import { createAdminManualOrderCost, fetchAdminFundAccounts } from '@/services/admin' -import { getAdminRole } from '@/utils/admin-auth' - -export type ManualOrderCostItem = { - orderItemId: number - matchSku?: string - skuName?: string - skuCode?: string - quantity?: number -} - -type ManualOrderCostModalProps = { - open: boolean - orderId: number - orderNo: string - items: ManualOrderCostItem[] - onClose: () => void -} - -type ManualOrderCostFormValues = { - orderItemId: number - fundAccountId: number - amountYuan: number - expenseCategory: string - payeeName?: string - paymentMethod?: string - evidence?: string - note?: string -} - -const CATEGORY_OPTIONS = [ - { value: 'service_fee', label: '人工服务费' }, - { value: 'goods_purchase', label: '商品采购' }, - { value: 'compensation', label: '赔付 / 补偿' }, - { value: 'refund_advance', label: '垫付退款' }, - { value: 'freight', label: '运费' }, - { value: 'other', label: '其他' }, -] - -const PAYMENT_METHOD_OPTIONS = [ - { value: 'wechat', label: '微信' }, - { value: 'alipay', label: '支付宝' }, - { value: 'bank', label: '银行卡' }, - { value: 'cash', label: '现金' }, - { value: 'platform_balance', label: '平台余额' }, - { value: 'other', label: '其他' }, -] - -function itemLabel(item: ManualOrderCostItem) { - // 只展示平台判断用的真实 SKU,引流长标题不进录单选项。 - const sku = item.matchSku || item.skuName || item.skuCode || `订单项 #${item.orderItemId}` - return item.quantity && item.quantity > 1 ? `${sku} ×${item.quantity}` : sku -} - -export default function ManualOrderCostModal({ - open, - orderId, - orderNo, - items, - onClose, -}: ManualOrderCostModalProps) { - const { message } = App.useApp() - const queryClient = useQueryClient() - const [form] = Form.useForm() - const fundAccountsQuery = useQuery({ - queryKey: ['admin-fund-accounts', 'agent'], - queryFn: () => - fetchAdminFundAccounts({ accountType: 'agent', status: 'active', pageSize: 200 }), - enabled: open, - }) - const fundAccounts = fundAccountsQuery.data?.data.items || [] - const soleFundAccountId = fundAccounts.length === 1 ? fundAccounts[0]?.id : undefined - const isSupport = getAdminRole() === 'support' - const ownFundAccount = isSupport ? fundAccounts[0] : undefined - - useEffect(() => { - if (!open) return - form.resetFields() - form.setFieldsValue({ - orderItemId: items.length === 1 ? items[0].orderItemId : undefined, - fundAccountId: soleFundAccountId, - amountYuan: undefined, - expenseCategory: 'service_fee', - paymentMethod: 'other', - }) - }, [form, items, open, soleFundAccountId]) - - const mutation = useMutation({ - mutationFn: (values: ManualOrderCostFormValues) => - createAdminManualOrderCost({ - orderId, - orderItemId: values.orderItemId, - fundAccountId: values.fundAccountId, - amountFen: Math.max(1, Math.round(Number(values.amountYuan || 0) * 100)), - expenseCategory: values.expenseCategory, - payeeName: values.payeeName || '内部人工', - paymentMethod: values.paymentMethod || 'other', - note: values.note || '', - evidenceJson: values.evidence - ? { reference: values.evidence } - : { note: values.note || '订单页人工录单' }, - }), - onSuccess: (response) => { - const result = response.data - if (result.warning) { - message.warning(result.warning) - } else { - message.success(`已计入订单成本,支出单号 ${result.expenseNo}`) - } - void queryClient.invalidateQueries({ queryKey: ['admin-fund-accounts'] }) - void queryClient.invalidateQueries({ queryKey: ['admin-fund-expenses'] }) - void queryClient.invalidateQueries({ queryKey: ['admin-sales-profit-overview'] }) - void queryClient.invalidateQueries({ queryKey: ['admin-sales-facts'] }) - void queryClient.invalidateQueries({ queryKey: ['admin-order-detail', String(orderId)] }) - onClose() - }, - onError: (error) => { - message.error(error instanceof Error ? error.message : '录入人工成本失败') - }, - }) - - async function submit() { - const values = await form.validateFields() - mutation.mutate(values) - } - - return ( - void submit()} - onCancel={onClose} - destroyOnHidden - width={620} - > - - form={form as FormInstance} - layout="vertical" - initialValues={{ expenseCategory: 'service_fee', paymentMethod: 'other' }} - > - - - - - {isSupport ? ( - <> - - - - - - ) : ( - - - - - - ) : ( - - - - - - - - - - - - - - - ) -} diff --git a/apps/frontend/src/layouts/AdminLayout.tsx b/apps/frontend/src/layouts/AdminLayout.tsx index 7accd7a8..4e435543 100644 --- a/apps/frontend/src/layouts/AdminLayout.tsx +++ b/apps/frontend/src/layouts/AdminLayout.tsx @@ -1,5 +1,4 @@ import { - CheckCircleOutlined, AuditOutlined, AccountBookOutlined, ApartmentOutlined, @@ -163,11 +162,6 @@ export default function AdminLayout() { }, ]), { key: '/admin/tasks', icon: , label: '任务' }, - { - key: '/admin/manual-fulfillment', - icon: , - label: '人工履约', - }, { key: '/admin/worker-platform', icon: , label: '接单平台' }, { key: '/admin/kuaishou-industry', diff --git a/apps/frontend/src/pages/admin/AdminManualFulfillmentPage.tsx b/apps/frontend/src/pages/admin/AdminManualFulfillmentPage.tsx deleted file mode 100644 index 7ecef63f..00000000 --- a/apps/frontend/src/pages/admin/AdminManualFulfillmentPage.tsx +++ /dev/null @@ -1,343 +0,0 @@ -/** - * 人工履约待办页:客服确认履约。 - * - * 口径(2026-10-24 确认): - * * 渠道与消耗积分在成本中心提前配好,客服不选、不填; - * * 固定成本 → 点「确认履约」零输入; - * * 成本不固定(消耗积分 = 0)→ 只填本次实际成本; - * * 确认后自动扣对应平台的积分并写成本快照。 - */ -import { CheckOutlined, ReloadOutlined } from '@ant-design/icons' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { - Alert, - App, - Button, - Card, - Form, - Input, - InputNumber, - Modal, - Space, - Statistic, - Tag, - Typography, -} from 'antd' -import PercentTable from '@/components/common/PercentTable' -import type { TableColumnsType } from 'antd' -import { useState } from 'react' - -import PageHeader from '@/components/admin/PageHeader' -import { confirmAdminManualFulfillment, fetchAdminManualFulfillmentPending } from '@/services/admin' -import type { ManualFulfillmentPendingItem } from '@/types/admin' -import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' -import { formatAdminDateTime } from '@/utils/admin-time' - -type ConfirmFormValues = { - costYuan?: number - payeeName?: string - note?: string -} - -const PLATFORM_TAG_COLORS: Record = { - nanhang: 'blue', - lewan: 'purple', - rock: 'cyan', - fund_cash: 'orange', -} - -export default function AdminManualFulfillmentPage() { - const { message } = App.useApp() - const queryClient = useQueryClient() - const [form] = Form.useForm() - - const [status, setStatus] = useState<'pending' | 'fulfilled' | ''>('pending') - const [keyword, setKeyword] = useState('') - const [page, setPage] = useState(1) - const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE) - const [confirming, setConfirming] = useState(null) - const [showCostInput, setShowCostInput] = useState(false) - - const listQuery = useQuery({ - queryKey: ['admin-manual-fulfillment-pending', status, keyword, page, pageSize], - queryFn: () => - fetchAdminManualFulfillmentPending({ - status: status || undefined, - keyword: keyword || undefined, - page, - pageSize, - }), - }) - - const confirmMutation = useMutation({ - mutationFn: (values: ConfirmFormValues) => - confirmAdminManualFulfillment({ - orderItemId: confirming?.orderItemId || 0, - costFen: values.costYuan === undefined ? undefined : Math.round(values.costYuan * 100), - payeeName: values.payeeName || '', - note: values.note || '', - }), - onSuccess: (response) => { - const result = response.data - message.success( - `履约已确认,扣 ${result.creditQuantity} 积分,成本 ¥${(result.costFen / 100).toFixed(2)}`, - ) - setConfirming(null) - setShowCostInput(false) - void queryClient.invalidateQueries({ - queryKey: ['admin-manual-fulfillment-pending'], - }) - void queryClient.invalidateQueries({ queryKey: ['admin-fund-my'] }) - void queryClient.invalidateQueries({ queryKey: ['admin-fund-accounts'] }) - void queryClient.invalidateQueries({ queryKey: ['admin-platform-credit-accounts'] }) - void queryClient.invalidateQueries({ queryKey: ['admin-platform-credit-entries'] }) - }, - onError: (error) => { - message.error(error instanceof Error ? error.message : '确认履约失败') - }, - }) - - function openConfirm(item: ManualFulfillmentPendingItem) { - setConfirming(item) - setShowCostInput(item.costFen === null) - form.resetFields() - } - - const columns: TableColumnsType = [ - { - title: '商品', - key: 'sku', - width: 260, - render: (_, row) => ( -
- - {row.skuName || row.skuCode} - - - {row.internalSku || row.skuCode} - -
- ), - }, - { - title: '平台单号', - dataIndex: 'platformOrderId', - width: 170, - render: (value: string) => ( - - {value} - - ), - }, - { - title: '履约渠道', - key: 'platform', - width: 110, - render: (_, row) => ( - {row.platformLabel} - ), - }, - { - title: '消耗', - key: 'credit', - width: 120, - render: (_, row) => - row.isFundCash ? ( - — - ) : ( - {row.creditQuantity} 积分 - ), - }, - { - title: '成本', - key: 'cost', - width: 120, - align: 'right', - render: (_, row) => - row.costFen === null ? ( - 待填写 - ) : ( - ¥{(row.costFen / 100).toFixed(2)} - ), - }, - { - title: '订单金额', - key: 'revenue', - width: 100, - align: 'right', - render: (_, row) => `¥${(row.revenueFen / 100).toFixed(2)}`, - }, - { - title: '支付时间', - dataIndex: 'paidAt', - width: 150, - render: (value: string) => formatAdminDateTime(value), - }, - { - title: '操作', - key: 'actions', - width: 110, - fixed: 'right', - render: (_, row) => - row.fulfilled ? ( - 已履约 - ) : ( - - ), - }, - ] - - const total = listQuery.data?.data.pagination.total ?? 0 - - return ( -
- - - - - - - - - - - - - { - setPage(nextPage) - if (nextPageSize !== pageSize) setPageSize(nextPageSize) - }, - })} - /> - - - { - void form.validateFields().then((values) => confirmMutation.mutate(values)) - }} - onCancel={() => { - setConfirming(null) - setShowCostInput(false) - }} - destroyOnHidden - width={520} - > - {confirming ? ( - -
- 商品: - {confirming.skuName || confirming.skuCode} -
-
- 内部 SKU: - - {confirming.internalSku} - -
-
- 履约渠道: - - {confirming.platformLabel} - - {!confirming.isFundCash ? ( - 消耗 {confirming.creditQuantity} 积分 - ) : null} -
- {showCostInput ? ( - - ) : ( - - )} -
- ) : null} - - form={form} layout="vertical"> - {showCostInput ? ( - - - - ) : null} - - - - - - - -
-
- ) -} diff --git a/apps/frontend/src/pages/admin/AdminOrderDetailPage.tsx b/apps/frontend/src/pages/admin/AdminOrderDetailPage.tsx index c1ef8848..adfe5b76 100644 --- a/apps/frontend/src/pages/admin/AdminOrderDetailPage.tsx +++ b/apps/frontend/src/pages/admin/AdminOrderDetailPage.tsx @@ -1,7 +1,7 @@ import { ArrowLeftOutlined, + CheckOutlined, CopyOutlined, - FileTextOutlined, PauseCircleOutlined, PlayCircleOutlined, RollbackOutlined, @@ -23,19 +23,20 @@ import { Skeleton, Space, Tag, + Tooltip, Typography, } from 'antd' import PercentTable from '@/components/common/PercentTable' import type { TableColumnsType } from 'antd' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import type { Dayjs } from 'dayjs' -import { useEffect, useState } from 'react' -import { useNavigate, useParams, useSearchParams } from 'react-router' +import { useState } from 'react' +import { useNavigate, useParams } from 'react-router' import dayjs from '@/lib/dayjs' import JsonPreview from '@/components/admin/JsonPreview' -import ManualOrderCostModal from '@/components/admin/ManualOrderCostModal' +import { ManualFulfillmentConfirmModal } from '@/components/admin/ManualFulfillmentConfirmModal' import PageHeader from '@/components/admin/PageHeader' import StatusTag from '@/components/admin/StatusTag' import { @@ -57,9 +58,8 @@ export default function AdminOrderDetailPage() { const queryClient = useQueryClient() const { message, modal } = App.useApp() const { orderId = '' } = useParams() - const [searchParams, setSearchParams] = useSearchParams() const [statusModalVisible, setStatusModalVisible] = useState(false) - const [manualCostOpen, setManualCostOpen] = useState(false) + const [fulfillOpen, setFulfillOpen] = useState(false) const [costPreviewItemId, setCostPreviewItemId] = useState(null) const [refundModalOpen, setRefundModalOpen] = useState(false) const [refundScope, setRefundScope] = useState<'order' | 'item'>('order') @@ -75,21 +75,6 @@ export default function AdminOrderDetailPage() { const [targetStatus, setTargetStatus] = useState<'active' | 'paused' | 'closed'>('active') const [statusReason, setStatusReason] = useState('') - useEffect(() => { - if (searchParams.get('manualCost') === '1') { - setManualCostOpen(true) - } - }, [searchParams]) - - function closeManualCost() { - setManualCostOpen(false) - if (searchParams.get('manualCost')) { - const next = new URLSearchParams(searchParams) - next.delete('manualCost') - setSearchParams(next, { replace: true }) - } - } - const query = useQuery({ queryKey: ['admin-order-detail', orderId], queryFn: () => fetchAdminOrderDetail(orderId), @@ -314,9 +299,19 @@ export default function AdminOrderDetailPage() { 退款登记 ) : null} - + {Number(detail.order.manualPendingCount || 0) > 0 ? ( + + ) : Number(detail.order.manualUnconfiguredCount || 0) > 0 ? ( + + + 待补全履约 + + + ) : null} {isAutoClaimOrder ? ( <> - {isManualOrder ? ( - + {manualPending > 0 ? ( + + ) : manualUnconfigured > 0 ? ( + + + 待补全履约 + + ) : null} {row.canClaimH5 ? ( @@ -398,7 +413,7 @@ export default function AdminOrdersPage() { form.setFieldsValue({ keyword: undefined, provider: undefined, - executorKey: undefined, + fulfillmentMode: undefined, payStatus: undefined, taskStatus: undefined, dateRange: undefined, @@ -459,11 +474,11 @@ export default function AdminOrdersPage() { }} /> - +