diff --git a/apps/backend/src/config/app-config-keys.ts b/apps/backend/src/config/app-config-keys.ts index 2b2e71af..9be4b937 100644 --- a/apps/backend/src/config/app-config-keys.ts +++ b/apps/backend/src/config/app-config-keys.ts @@ -9,8 +9,6 @@ export const APP_CONFIG_KEYS = { workerHall: 'worker_hall', workerRiskControl: 'worker_risk_control', workerMockShops: 'worker_mock_shops', - /** 接单工单:一次性补资料链接配置(有效期分钟数) */ - workOrderMaterialLink: 'work_order_material_link', kuaishouIndustrySource: 'kuaishou_industry_source', affiliateDash: 'affiliate_dash', pddSource: 'pdd_source', diff --git a/apps/backend/src/db/migrations/113_drop_work_order_material_links.sql b/apps/backend/src/db/migrations/113_drop_work_order_material_links.sql new file mode 100644 index 00000000..e389097b --- /dev/null +++ b/apps/backend/src/db/migrations/113_drop_work_order_material_links.sql @@ -0,0 +1,12 @@ +-- 113_drop_work_order_material_links.sql —— 下线旧的「一次性补资料链接 token」体系 +-- +-- 背景:补资料页已改为按平台订单号直连(/supplement/:订单号), +-- 不再需要客服先生成 token 链接: +-- - 后端:管理端 /orders/:id/material-link、链接仓储读写、material-link-config 配置接口 +-- - 前端:补资料链接有效期设置面板、生成链接请求 +-- - 数据:work_order_material_links 表、app_config_entries.work_order_material_link +-- 历史 token 记录已无任何读取入口,直接随表删除。 + +DROP TABLE IF EXISTS work_order_material_links; + +DELETE FROM app_config_entries WHERE config_key = 'work_order_material_link'; diff --git a/apps/backend/src/repositories/worker-platform/work-order-material-link-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-material-link-repo.ts index 0fdcc06b..f1e719b0 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-material-link-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-material-link-repo.ts @@ -2,20 +2,6 @@ import type { PoolClient } from 'pg' import type { WorkOrderRow } from './types.js' -export type WorkOrderMaterialLinkStatus = 'active' | 'used' | 'expired' | 'revoked' - -export type WorkOrderMaterialLinkRow = { - id: number - work_order_id: number - token: string - status: WorkOrderMaterialLinkStatus - expires_at: string - used_at: string | null - created_by: string - created_at: string - updated_at: string -} - /** * 锁工单并回读「有效拼单份额」。 * @@ -41,63 +27,6 @@ export async function getWorkOrderForUpdateWithClient( return result.rows[0] || null } -export async function findActiveWorkOrderMaterialLinkWithClient( - client: PoolClient, - workOrderId: number | string, - now: string, -): Promise { - const result = await client.query( - `SELECT * FROM work_order_material_links - WHERE work_order_id = $1 AND status = 'active' AND expires_at > $2 - ORDER BY id DESC - LIMIT 1`, - [Number(workOrderId), now], - ) - return result.rows[0] || null -} - -/** 重新生成前清掉旧 active:已过期标 expired,未过期标 revoked。 */ -export async function revokeActiveWorkOrderMaterialLinksWithClient( - client: PoolClient, - workOrderId: number | string, - now: string, -): Promise { - const result = await client.query( - `UPDATE work_order_material_links - SET status = CASE WHEN expires_at <= $2 THEN 'expired' ELSE 'revoked' END, - updated_at = $2 - WHERE work_order_id = $1 AND status = 'active'`, - [Number(workOrderId), now], - ) - return result.rowCount || 0 -} - -export async function createWorkOrderMaterialLinkWithClient( - client: PoolClient, - input: { - workOrderId: number | string - token: string - expiresAt: string - createdBy: string - now: string - }, -): Promise { - const result = await client.query( - `INSERT INTO work_order_material_links ( - work_order_id, token, status, expires_at, created_by, created_at, updated_at - ) VALUES ($1, $2, 'active', $3, $4, $5, $5) - RETURNING *`, - [ - Number(input.workOrderId), - input.token, - input.expiresAt, - String(input.createdBy || '').trim(), - input.now, - ], - ) - return result.rows[0]! -} - export async function updateWorkOrderMaterialWithClient( client: PoolClient, input: { diff --git a/apps/backend/src/routes/admin/worker-platform/config.ts b/apps/backend/src/routes/admin/worker-platform/config.ts index 901a88f5..fdacc2c2 100644 --- a/apps/backend/src/routes/admin/worker-platform/config.ts +++ b/apps/backend/src/routes/admin/worker-platform/config.ts @@ -13,7 +13,6 @@ import { getAdminWorkerPlatformNotificationConfig, getAdminWorkerPlatformSummary, getAdminWorkerProductMatchConfig, - getWorkerMaterialLinkConfig, listAdminKuaishouMatchSources, listAdminWorkCategories, listAdminWorkProductMatchLogs, @@ -30,7 +29,6 @@ import { saveAdminWorkerLevel, saveAdminWorkerPlatformNotificationConfig, saveAdminWorkerProductMatchConfig, - saveWorkerMaterialLinkConfig, syncAdminWorkProductRulePrice, testAdminKuaishouProductMatch, testAdminProductMatchByOrderItem, @@ -295,32 +293,6 @@ router.post( }), ) -router.get( - '/worker-platform/material-link-config', - requireAdminRoles(['admin', 'operator']), - createJsonHandler(() => getWorkerMaterialLinkConfig(), { - successMessage: 'ok', - errorMessage: '读取补资料链接配置失败', - scope: '[admin/worker-platform/material-link-config]', - }), -) - -router.post( - '/worker-platform/material-link-config', - requireAdminRoles(['admin', 'operator']), - createJsonHandler((req) => saveWorkerMaterialLinkConfig(req.body || {}), { - successMessage: '补资料链接配置已保存', - errorMessage: '保存补资料链接配置失败', - scope: '[admin/worker-platform/material-link-config]', - audit: (_req, data) => ({ - action: 'worker_material_link_config_saved', - targetType: 'work_order_material_link_config', - targetId: 'work_order_material_link', - data: data && typeof data === 'object' ? (data as Record) : {}, - }), - }), -) - router.post( '/worker-platform/announcement', requireAdminRoles(['admin']), diff --git a/apps/backend/src/routes/admin/worker-platform/orders.ts b/apps/backend/src/routes/admin/worker-platform/orders.ts index 3e08cbf2..19c49fb3 100644 --- a/apps/backend/src/routes/admin/worker-platform/orders.ts +++ b/apps/backend/src/routes/admin/worker-platform/orders.ts @@ -9,7 +9,6 @@ import { completeAdminWorkOrderGiftCooldown, confirmAdminWorkOrderFriendAdded, createAdminMockWorkOrder, - createAdminWorkOrderMaterialLink, deleteAdminMockCustomShop, deleteAdminWorkOrder, getAdminWorkOrderEvents, @@ -187,36 +186,6 @@ router.post( ), ) -router.post( - '/worker-platform/orders/:workOrderId/material-link', - requireAdminPermission('worker_order.update_material'), - createJsonHandler( - (req) => - createAdminWorkOrderMaterialLink( - String(req.params.workOrderId || ''), - req.adminSession?.displayName || req.adminSession?.username || '', - ), - { - successMessage: '补资料链接已生成', - errorMessage: '生成补资料链接失败', - scope: '[admin/worker-platform/orders/:workOrderId/material-link]', - audit: (req, data) => ({ - action: 'work_order_material_link_created', - targetType: 'work_order', - targetId: String(req.params.workOrderId || ''), - // 审计只记录有效期与是否复用,绝不落 token。 - data: - data && typeof data === 'object' - ? { - expiresAt: (data as Record).expiresAt, - reused: (data as Record).reused, - } - : {}, - }), - }, - ), -) - router.post( '/worker-platform/orders/:workOrderId/reset-material', requireAdminPermission('worker_order.update_material'), diff --git a/apps/backend/src/services/worker-platform/index.ts b/apps/backend/src/services/worker-platform/index.ts index 9b800c83..ca46aaa8 100644 --- a/apps/backend/src/services/worker-platform/index.ts +++ b/apps/backend/src/services/worker-platform/index.ts @@ -65,4 +65,3 @@ export * from './worker-acceptance-config-service.js' export * from './sync-worker-orders-service.js' export * from './consignment-contact-service.js' export * from './work-order-material-link-service.js' -export * from './worker-material-link-config-service.js' diff --git a/apps/backend/src/services/worker-platform/work-order-material-link-service.test.ts b/apps/backend/src/services/worker-platform/work-order-material-link-service.test.ts index 88ca0fad..b17da133 100644 --- a/apps/backend/src/services/worker-platform/work-order-material-link-service.test.ts +++ b/apps/backend/src/services/worker-platform/work-order-material-link-service.test.ts @@ -6,7 +6,6 @@ import { pickSubmittedMaterialFields, resolveMaterialResetBlockReason, resolvePublicMaterialOrderState, - resolveWorkOrderMaterialLinkOrderBlockReason, } from './work-order-material-link-service.js' test('公开补资料:只有待完善且未指派/无份额可提交', () => { @@ -129,50 +128,7 @@ test('客服重置补资料:只允许待分配、未指派、无份额、未 ) }) -test('管理端链接准入:待完善/待分配可用,已指派/有份额/其他状态拒绝', () => { - assert.equal( - resolveWorkOrderMaterialLinkOrderBlockReason({ - status: 'pending_material', - assigned_worker_id: null, - active_share_quantity: 0, - }), - '', - ) - assert.equal( - resolveWorkOrderMaterialLinkOrderBlockReason({ - status: 'unassigned', - assigned_worker_id: null, - active_share_quantity: 0, - }), - '', - ) - assert.equal( - resolveWorkOrderMaterialLinkOrderBlockReason({ - status: 'open', - assigned_worker_id: null, - active_share_quantity: 0, - }), - 'status', - ) - assert.equal( - resolveWorkOrderMaterialLinkOrderBlockReason({ - status: 'pending_material', - assigned_worker_id: 9, - active_share_quantity: 0, - }), - 'assigned', - ) - assert.equal( - resolveWorkOrderMaterialLinkOrderBlockReason({ - status: 'pending_material', - assigned_worker_id: null, - active_share_quantity: 2, - }), - 'shares', - ) -}) - -test('补资料链接:提交字段只保留 requirements 白名单,额外字段被丢弃', () => { +test('公开补资料:提交字段只保留 requirements 白名单,额外字段被丢弃', () => { const result = pickSubmittedMaterialFields([{ key: 'gameId' }, { key: 'gameNickname' }], { gameId: '123', gameNickname: '张三', diff --git a/apps/backend/src/services/worker-platform/work-order-material-link-service.ts b/apps/backend/src/services/worker-platform/work-order-material-link-service.ts index c449c3a1..6ae31497 100644 --- a/apps/backend/src/services/worker-platform/work-order-material-link-service.ts +++ b/apps/backend/src/services/worker-platform/work-order-material-link-service.ts @@ -1,14 +1,9 @@ -import { randomBytes } from 'node:crypto' - import { withTransaction } from '../../db/client.js' import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js' import { createWorkOrderEventWithClient, - createWorkOrderMaterialLinkWithClient, - findActiveWorkOrderMaterialLinkWithClient, getWorkOrderForUpdateWithClient, listWorkOrdersByPlatformOrderId, - revokeActiveWorkOrderMaterialLinksWithClient, updateWorkOrderMaterialWithClient, } from '../../repositories/worker-platform/index.js' import type { WorkOrderRow } from '../../repositories/worker-platform/types.js' @@ -21,20 +16,13 @@ import { resolveAdminNotificationEntity } from '../admin/admin-notification-serv import { publishWorkOrderRealtimeChange } from '../realtime/realtime-event-service.js' import { mapCollectLookupOrder, - mapWorkOrderAdmin, normalizeOptionalId, normalizeSubmittedFields, normalizeUploadedFiles, resolveRequirementFields, } from './mappers.js' -import { getWorkerMaterialLinkConfig } from './worker-material-link-config-service.js' import { getRequiredWorkOrder } from './worker-session-context-service.js' -const ALLOWED_MATERIAL_LINK_ORDER_STATUSES = new Set([ - WORK_ORDER_STATUS.PENDING_MATERIAL, - WORK_ORDER_STATUS.UNASSIGNED, -]) - const MAX_MATERIAL_SCREENSHOTS = 5 const PUBLIC_MATERIAL_STATUS_LABELS: Record = { @@ -49,8 +37,6 @@ const PUBLIC_MATERIAL_STATUS_LABELS: Record = { vip_evidence_pending: '待补图', } -export type WorkOrderMaterialLinkOrderBlockReason = 'status' | 'assigned' | 'shares' | '' - export type PublicMaterialOrderBlockReason = | 'already_submitted' | 'status' @@ -154,21 +140,6 @@ export function resolveMaterialResetBlockMessage(reason: MaterialResetBlockReaso return '只有待分配(未指派、无份额、未发布)的工单可以重置补资料状态' } -/** - * 管理端生成/复用一次性链接的准入判断。 - * - * 用户要求保留现有“复制补资料链接”按钮,所以这里仍按原口径允许 - * 待完善 / 待分配,并拒绝已指派/有份额的工单。 - */ -export function resolveWorkOrderMaterialLinkOrderBlockReason( - order: Pick, -): WorkOrderMaterialLinkOrderBlockReason { - if (!ALLOWED_MATERIAL_LINK_ORDER_STATUSES.has(order.status)) return 'status' - if (order.assigned_worker_id) return 'assigned' - if (Number(order.active_share_quantity || 0) > 0) return 'shares' - return '' -} - /** * 只保留工单 requirement 里定义过的字段。 * @@ -196,29 +167,6 @@ function throwPublicMaterialBlocked(state: PublicMaterialOrderState): never { }) } -/** 管理端生成补资料链接时仍沿用旧的准入文案。 */ -function assertWorkOrderAllowedForMaterialLink(order: WorkOrderRow): void { - const reason = resolveWorkOrderMaterialLinkOrderBlockReason(order) - if (reason === 'status') { - throw createHttpError('当前状态不能生成补资料链接', { - statusCode: 409, - errorCode: 'material_link_status_invalid', - }) - } - if (reason === 'assigned') { - throw createHttpError('订单已指派打手,不能生成补资料链接', { - statusCode: 409, - errorCode: 'material_link_status_invalid', - }) - } - if (reason === 'shares') { - throw createHttpError('订单已有拼单份额,不能生成补资料链接', { - statusCode: 409, - errorCode: 'material_link_status_invalid', - }) - } -} - function mapPublicOrder(order: WorkOrderRow) { return { workOrderNo: order.work_order_no, @@ -228,12 +176,6 @@ function mapPublicOrder(order: WorkOrderRow) { } } -/** PostgreSQL 返回的 timestamptz 可能是 "YYYY-MM-DD HH:mm:ss" 字符串,统一转 ISO。 */ -function toIsoTimestamp(value: string): string { - const date = new Date(value) - return Number.isNaN(date.getTime()) ? String(value || '') : date.toISOString() -} - function mapPublicMaterialOrder(order: WorkOrderRow) { const state = resolvePublicMaterialOrderState(order) const base = { @@ -290,76 +232,6 @@ async function resolvePublicMaterialTarget( return orders[0]! } -/** - * 管理端生成/复用一次性链接。 - * - * 用户要求保留现有“复制补资料链接”按钮,因此链接记录仍会生成; - * 但公开页已经不再依赖它,复制出的 `/supplement/<平台订单号>` 始终可用。 - */ -export async function createAdminWorkOrderMaterialLink( - workOrderId: number | string, - actorName = '', -) { - const { ttlMinutes } = getWorkerMaterialLinkConfig() - - return withTransaction(async (client) => { - const order = await getWorkOrderForUpdateWithClient(client, workOrderId) - if (!order) { - throw createHttpError('接单订单不存在', { - statusCode: 404, - errorCode: 'work_order_not_found', - }) - } - - if (!mapWorkOrderAdmin(order).capabilities.canUpdateMaterial) { - throw createHttpError('当前状态不能生成补资料链接', { - statusCode: 409, - errorCode: 'material_link_status_invalid', - }) - } - - const platformOrderId = String(order.platform_order_id || '').trim() - if (!platformOrderId) { - throw createHttpError('该工单没有平台订单号,不能生成补资料链接', { - statusCode: 409, - errorCode: 'material_link_platform_order_missing', - }) - } - - assertWorkOrderAllowedForMaterialLink(order) - - // 锁到手后再取时间,避免等锁期间旧链接过期却仍按旧时间复用。 - const now = nowIso() - const existing = await findActiveWorkOrderMaterialLinkWithClient(client, order.id, now) - if (existing) { - return { - token: existing.token, - platformOrderId, - expiresAt: toIsoTimestamp(existing.expires_at), - reused: true, - } - } - - await revokeActiveWorkOrderMaterialLinksWithClient(client, order.id, now) - const token = randomBytes(24).toString('base64url') - const expiresAt = new Date(Date.now() + ttlMinutes * 60 * 1000).toISOString() - const link = await createWorkOrderMaterialLinkWithClient(client, { - workOrderId: order.id, - token, - expiresAt, - createdBy: String(actorName || '').trim(), - now, - }) - - return { - token: link.token, - platformOrderId, - expiresAt: toIsoTimestamp(link.expires_at), - reused: false, - } - }) -} - /** * 公开页按平台订单号读取补资料内容。 * diff --git a/apps/backend/src/services/worker-platform/worker-material-link-config-service.test.ts b/apps/backend/src/services/worker-platform/worker-material-link-config-service.test.ts deleted file mode 100644 index 2c4d2ea8..00000000 --- a/apps/backend/src/services/worker-platform/worker-material-link-config-service.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' - -import { - createDefaultWorkerMaterialLinkConfig, - normalizeWorkerMaterialLinkConfig, - WORK_ORDER_MATERIAL_LINK_DEFAULT_TTL_MINUTES, - WORK_ORDER_MATERIAL_LINK_MAX_TTL_MINUTES, - WORK_ORDER_MATERIAL_LINK_MIN_TTL_MINUTES, -} from './worker-material-link-config-service.js' - -test('补资料链接配置:默认 30 分钟', () => { - assert.deepEqual(createDefaultWorkerMaterialLinkConfig(), { - ttlMinutes: WORK_ORDER_MATERIAL_LINK_DEFAULT_TTL_MINUTES, - }) - assert.equal(WORK_ORDER_MATERIAL_LINK_DEFAULT_TTL_MINUTES, 30) - assert.deepEqual(normalizeWorkerMaterialLinkConfig(undefined), { ttlMinutes: 30 }) -}) - -test('补资料链接配置:接受 ttlMinutes / ttl_minutes / 数字字符串', () => { - assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: 45 }), { ttlMinutes: 45 }) - assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttl_minutes: '10' }), { ttlMinutes: 10 }) - assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: 90.4 }), { ttlMinutes: 90 }) -}) - -test('补资料链接配置:越界值收敛到 1 - 10080 分钟', () => { - assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: 0 }), { - ttlMinutes: WORK_ORDER_MATERIAL_LINK_MIN_TTL_MINUTES, - }) - assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: -5 }), { ttlMinutes: 1 }) - assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: 999999 }), { - ttlMinutes: WORK_ORDER_MATERIAL_LINK_MAX_TTL_MINUTES, - }) - assert.equal(WORK_ORDER_MATERIAL_LINK_MAX_TTL_MINUTES, 10080) -}) - -test('补资料链接配置:非法输入回退默认值,不会写入 NaN', () => { - assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: 'abc' }), { ttlMinutes: 30 }) - assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: null }), { ttlMinutes: 30 }) - assert.deepEqual(normalizeWorkerMaterialLinkConfig('not-an-object'), { ttlMinutes: 30 }) -}) diff --git a/apps/backend/src/services/worker-platform/worker-material-link-config-service.ts b/apps/backend/src/services/worker-platform/worker-material-link-config-service.ts deleted file mode 100644 index f53d076a..00000000 --- a/apps/backend/src/services/worker-platform/worker-material-link-config-service.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js' -import type { JsonObject } from '../../types/json.js' -import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js' - -/** 补资料链接默认有效期(分钟):30 分钟。 */ -export const WORK_ORDER_MATERIAL_LINK_DEFAULT_TTL_MINUTES = 30 -/** 允许配置的最短有效期(分钟)。 */ -export const WORK_ORDER_MATERIAL_LINK_MIN_TTL_MINUTES = 1 -/** 允许配置的最长有效期(分钟):7 天。 */ -export const WORK_ORDER_MATERIAL_LINK_MAX_TTL_MINUTES = 7 * 24 * 60 - -export type WorkerMaterialLinkConfig = { - /** 新生成的补资料链接有效期,单位分钟。 */ - ttlMinutes: number -} - -/** 读取配置(内存缓存),未配置时返回默认 30 分钟。 */ -export function getWorkerMaterialLinkConfig(): WorkerMaterialLinkConfig { - return readAppConfigEntry({ - configKey: APP_CONFIG_KEYS.workOrderMaterialLink, - fallback: createDefaultWorkerMaterialLinkConfig, - normalize: normalizeWorkerMaterialLinkConfig, - }) -} - -export async function saveWorkerMaterialLinkConfig( - rawValue: unknown, -): Promise { - return saveAppConfigEntry({ - configKey: APP_CONFIG_KEYS.workOrderMaterialLink, - value: rawValue, - normalize: normalizeWorkerMaterialLinkConfig, - }) -} - -export function createDefaultWorkerMaterialLinkConfig(): WorkerMaterialLinkConfig { - return { ttlMinutes: WORK_ORDER_MATERIAL_LINK_DEFAULT_TTL_MINUTES } -} - -export function normalizeWorkerMaterialLinkConfig(rawValue: unknown): WorkerMaterialLinkConfig { - const source = isPlainObject(rawValue) ? rawValue : {} - const raw = source.ttlMinutes ?? source.ttl_minutes - if (raw === undefined || raw === null || raw === '') { - return createDefaultWorkerMaterialLinkConfig() - } - - const value = Number(raw) - if (!Number.isFinite(value)) { - return createDefaultWorkerMaterialLinkConfig() - } - - return { - ttlMinutes: Math.min( - WORK_ORDER_MATERIAL_LINK_MAX_TTL_MINUTES, - Math.max(WORK_ORDER_MATERIAL_LINK_MIN_TTL_MINUTES, Math.round(value)), - ), - } -} - -function isPlainObject(value: unknown): value is JsonObject { - return Object.prototype.toString.call(value) === '[object Object]' -} diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index 14b4f951..15749f23 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -127,7 +127,7 @@ export default function App() { return } - // 一次性补资料链接(`/supplement`):路径式公开页,先于站点判断, + // 补资料页(`/supplement`):路径式公开页,先于站点判断, // 保证在后台域名和打手端域名下都能打开。带订单号直接进补资料页,不带订单号进输入页。 if (isCollectMaterialLinkPath(window.location.pathname)) { return @@ -238,11 +238,11 @@ function ClaimApplication() { } /** - * 一次性补资料链接(`/supplement`)的独立路由树。 + * 补资料页(`/supplement`)的独立路由树。 * * - `/supplement`:输入平台订单号; * - `/supplement/<订单号>`:直接打开对应订单的补资料页; - * - 其余子路径:交给补资料页展示「链接无效」。 + * - 其余子路径:交给补资料页展示「订单号无效」。 * * 路径式公开页,不带 hash;不依赖后台会话。 */ diff --git a/apps/frontend/src/pages/admin/panels/AnnouncementPanel.tsx b/apps/frontend/src/pages/admin/panels/AnnouncementPanel.tsx index ccdb9915..9cb5a206 100644 --- a/apps/frontend/src/pages/admin/panels/AnnouncementPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/AnnouncementPanel.tsx @@ -32,7 +32,6 @@ import type { WorkerAnnouncementConfig } from '@/types/worker-platform' import { formatAdminDateTime } from '@/utils/admin-time' import HallConfigPanel from './HallConfigPanel' -import MaterialLinkConfigPanel from './MaterialLinkConfigPanel' function isDepositUnfreezeJob(job: AdminScheduledJobItem) { return job.type === 'deposit_unfreeze' || job.id === 'deposit-unfreeze' @@ -45,7 +44,6 @@ export default function AnnouncementPanel() { - ) } diff --git a/apps/frontend/src/pages/admin/panels/MaterialLinkConfigPanel.tsx b/apps/frontend/src/pages/admin/panels/MaterialLinkConfigPanel.tsx deleted file mode 100644 index 423596cd..00000000 --- a/apps/frontend/src/pages/admin/panels/MaterialLinkConfigPanel.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useQuery, useQueryClient } from '@tanstack/react-query' -import { App, Button, Card, Form, InputNumber, Space, Typography } from 'antd' -import { useEffect, useState } from 'react' - -import { - fetchAdminWorkerMaterialLinkConfig, - saveAdminWorkerMaterialLinkConfig, -} from '@/services/admin' -import type { WorkerMaterialLinkConfig } from '@/types/worker-platform' - -export default function MaterialLinkConfigPanel() { - const { message } = App.useApp() - const queryClient = useQueryClient() - const [form] = Form.useForm() - const [saving, setSaving] = useState(false) - - const configQuery = useQuery({ - queryKey: ['admin-worker-material-link-config'], - queryFn: fetchAdminWorkerMaterialLinkConfig, - }) - const config = configQuery.data?.data - - useEffect(() => { - if (config) { - form.setFieldsValue({ ttlMinutes: config.ttlMinutes }) - } - }, [config, form]) - - async function saveConfig(values: WorkerMaterialLinkConfig) { - setSaving(true) - try { - await saveAdminWorkerMaterialLinkConfig({ ttlMinutes: Number(values.ttlMinutes) }) - await queryClient.invalidateQueries({ queryKey: ['admin-worker-material-link-config'] }) - message.success('补资料链接配置已保存') - } catch (error) { - message.error(error instanceof Error ? error.message : '保存补资料链接配置失败') - } finally { - setSaving(false) - } - } - - return ( - - - - 客服在「接单工单」复制的一次性补资料链接,客户只能修改补资料内容;提交成功后工单转为「未分配」, - 链接立即失效,需要重新发送。修改有效期只影响新生成的链接。 - -
- - - - - - -
-
-
- ) -} diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index 39d0be61..c41b8855 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -60,7 +60,6 @@ import { cancelAdminWorkOrderSharingPart, completeAdminWorkOrderGiftCooldown, confirmAdminWorkOrderFriend, - createAdminWorkOrderMaterialLink, deductAdminWorkOrderPendingDeposit, deleteAdminWorkOrder, disposeAdminWorkOrderShareProblem, @@ -205,7 +204,7 @@ function getConsignmentFulfillment(row: WorkOrder): ConsignmentFulfillmentInfo | return Object.keys(consignment).length > 0 ? (consignment as ConsignmentFulfillmentInfo) : null } -/** 公开补资料链接只在「待完善 / 待分配」且有平台订单号时生成;已进大厅或已接单不允许客户改资料。 */ +/** 补资料链接按平台订单号打开;只有待完善 / 待分配且存在平台订单号时才展示复制入口。 */ function canCopyMaterialLink(order: WorkOrder): boolean { const status = String(order.rawStatus || order.status || '') const platformOrderId = String(order.platformOrderId || '').trim() @@ -354,11 +353,7 @@ export default function WorkOrdersPanel() { const [materialOrder, setMaterialOrder] = useState(null) const [materialScreenshots, setMaterialScreenshots] = useState([]) const [materialPasteText, setMaterialPasteText] = useState('') - const [materialLinkLoadingId, setMaterialLinkLoadingId] = useState(null) - const [materialLinkFallback, setMaterialLinkFallback] = useState<{ - url: string - expiresAt: string - } | null>(null) + const [materialLinkFallback, setMaterialLinkFallback] = useState<{ url: string } | null>(null) const [sharingOrder, setSharingOrder] = useState(null) const [cancelShare, setCancelShare] = useState(null) const [reduceShare, setReduceShare] = useState(null) @@ -1654,36 +1649,23 @@ export default function WorkOrdersPanel() { } async function copyMaterialLink(order: WorkOrder) { - setMaterialLinkLoadingId(order.workOrderId) - try { - const response = await createAdminWorkOrderMaterialLink(order.workOrderId) - // 新后端返回 platformOrderId;旧后端缺失时回退 token,仅用于前后端滚动发布。 - const platformOrderId = String( - response.data.platformOrderId || response.data.token || '', - ).trim() - if (!platformOrderId) { - message.error('生成补资料链接失败:缺少平台订单号') - return - } - const url = buildCollectMaterialLinkUrl(window.location.origin, platformOrderId) - if (!url) { - message.error('生成补资料链接失败:链接地址为空') - return - } - const copied = await copyTextToClipboard(url) - const expiresText = formatAdminDateTime(response.data.expiresAt) - if (copied) { - message.success( - `${response.data.reused ? '已复制现有链接' : '已生成并复制链接'}(订单号 ${platformOrderId}),${expiresText} 前有效;该订单号下已生成链接的待补商品会一起显示`, - ) - } else { - setMaterialLinkFallback({ url, expiresAt: response.data.expiresAt }) - } - } catch (error) { - message.error(error instanceof Error ? error.message : '生成补资料链接失败') - } finally { - setMaterialLinkLoadingId(null) + const platformOrderId = String(order.platformOrderId || '').trim() + if (!platformOrderId) { + message.error('复制补资料链接失败:缺少平台订单号') + return } + const url = buildCollectMaterialLinkUrl(window.location.origin, platformOrderId) + if (!url) { + message.error('复制补资料链接失败:链接地址为空') + return + } + const copied = await copyTextToClipboard(url) + if (copied) { + message.success(`补资料链接已复制(订单号 ${platformOrderId})`) + return + } + // 剪贴板 API 不可用时退回弹窗,让客服手动复制。 + setMaterialLinkFallback({ url }) } const columns: TableColumnsType = [ @@ -1998,9 +1980,8 @@ export default function WorkOrdersPanel() { {row.capabilities?.canUpdateMaterial && canUpdateMaterial && canCopyMaterialLink(row) ? ( @@ -3291,8 +3272,7 @@ export default function WorkOrdersPanel() { {materialLinkFallback ? ( - 同一订单号下已生成链接的多个待补工单会显示在同一个页面,每个工单各自仅可提交一次; - {formatAdminDateTime(materialLinkFallback.expiresAt)} 前有效。 + 该链接按平台订单号打开,可查看该订单号下全部工单的补资料状态;每个工单仅可提交一次。 ('/api/v1/admin/worker-platform/hall-config', payload) } -export function fetchAdminWorkerMaterialLinkConfig() { - return apiGet('/api/v1/admin/worker-platform/material-link-config') -} - -export function saveAdminWorkerMaterialLinkConfig(payload: WorkerMaterialLinkConfig) { - return apiPost( - '/api/v1/admin/worker-platform/material-link-config', - payload, - ) -} - export function fetchAdminWorkerAnnouncement() { return apiGet('/api/v1/admin/worker-platform/announcement') } @@ -769,16 +757,6 @@ export function submitAdminWorkOrderMaterial( ) } -export function createAdminWorkOrderMaterialLink(workOrderId: number) { - return apiPost<{ - token: string - /** 公开 URL 使用的平台订单号;旧后端可能没有该字段,前端仅用于滚动发布兜底。 */ - platformOrderId?: string - expiresAt: string - reused: boolean - }>(`/api/v1/admin/worker-platform/orders/${workOrderId}/material-link`) -} - /** 客服重置补资料:仅「待分配 + 未指派 + 无份额 + 未发布」可重置回待完善。 */ export function resetAdminWorkOrderMaterialStatus(workOrderId: number) { return apiPost<{ order: WorkOrder }>( diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index 1fa4c993..f7f8b30b 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -334,11 +334,6 @@ export type WorkerHallConfig = { queuedOrderCount: number } -/** 一次性补资料链接配置:有效期只影响新生成的链接。 */ -export type WorkerMaterialLinkConfig = { - ttlMinutes: number -} - export type WorkerWithdrawalAccount = { accountChannel: string accountName: string