From de33bfa74cc220bd9e8143ca13287da6193f162b Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 1 Aug 2026 18:35:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=8D=95=E5=B7=A5=E5=8D=95=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=BC=96=E8=BE=91=E4=B8=8E=E5=88=A0=E9=99=A4=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BC=96=E8=BE=91=E6=8A=BC=E9=87=91=E4=B8=8D?= =?UTF-8?q?=E7=94=9F=E6=95=88=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../worker-platform/work-order-repo.ts | 61 +++++++ .../src/routes/admin/worker-platform.ts | 37 ++++ .../services/worker-platform/admin-service.ts | 113 ++++++++++++ .../src/services/worker-platform/mappers.ts | 1 + apps/frontend/src/lib/http.ts | 8 + .../pages/admin/panels/WorkOrdersPanel.tsx | 163 ++++++++++++++++++ .../src/services/admin/worker-platform.ts | 25 ++- apps/frontend/src/types/worker-platform.ts | 1 + 8 files changed, 408 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/repositories/worker-platform/work-order-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-repo.ts index 5086980f..f0353603 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -661,6 +661,67 @@ export async function updateWorkOrder( return getWorkOrderById(workOrderId) } +export async function updateWorkOrderBasic( + workOrderId: number | string, + patch: { + productName?: string + platformOrderId?: string + categoryId?: number | null + rewardAmount?: number + requiredDepositAmount?: number + depositThresholdAmount?: number + requirementJson?: string + updatedAt: string + }, +): Promise { + const current = await getWorkOrderById(workOrderId) + if (!current) return null + await query( + ` + UPDATE work_orders + SET + platform_order_id = $1, + product_name = $2, + category_id = $3, + reward_amount = $4, + required_deposit_amount = $5, + deposit_threshold_amount = $6, + requirement_json = $7::jsonb, + updated_at = $8 + WHERE id = $9 + `, + [ + String((patch.platformOrderId ?? current.platform_order_id) || '').trim(), + String((patch.productName ?? current.product_name) || '').trim(), + patch.categoryId === undefined ? current.category_id : patch.categoryId, + toPositiveInteger(patch.rewardAmount ?? current.reward_amount, 0), + toPositiveInteger( + patch.requiredDepositAmount ?? current.required_deposit_amount, + 0, + ), + toPositiveInteger( + patch.depositThresholdAmount ?? current.deposit_threshold_amount, + 0, + ), + toJsonString(patch.requirementJson ?? current.requirement_json), + patch.updatedAt, + Number(workOrderId), + ], + ) + return getWorkOrderById(workOrderId) +} + +export async function deleteWorkOrder(workOrderId: number | string): Promise { + const result = await query( + ` + DELETE FROM work_orders + WHERE id = $1 + `, + [Number(workOrderId)], + ) + return Number(result.rowCount || 0) > 0 +} + export async function grabWorkOrder(input: { workOrderId: number workerId: number diff --git a/apps/backend/src/routes/admin/worker-platform.ts b/apps/backend/src/routes/admin/worker-platform.ts index e4f2859f..1d64076d 100644 --- a/apps/backend/src/routes/admin/worker-platform.ts +++ b/apps/backend/src/routes/admin/worker-platform.ts @@ -5,6 +5,7 @@ import { creditAdminWorkerWallet, createAdminMockWorkOrder, deleteAdminWorkCategory, + deleteAdminWorkOrder, deleteAdminWorkerLevel, getAdminWorkerFinanceConfig, getAdminWorkerPlatformSummary, @@ -27,6 +28,7 @@ import { submitAdminWorkOrderMaterial, syncAdminWorkerOrdersFromSource, unpublishAdminWorkOrder, + updateAdminWorkOrder, updateAdminWorkOrderSharing, } from '../../services/worker-platform/index.js' import { createJsonHandler, requireAdminRoles } from './session.js' @@ -319,6 +321,41 @@ router.post( ), ) +router.put( + '/worker-platform/orders/:workOrderId', + requireAdminRoles(['admin', 'operator']), + createJsonHandler( + (req) => updateAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}), + { + successMessage: '工单信息已保存', + errorMessage: '保存工单信息失败', + scope: '[admin/worker-platform/orders/:workOrderId]', + audit: (req, data) => ({ + action: 'work_order_updated', + targetType: 'work_order', + targetId: String(req.params.workOrderId || ''), + data: data && typeof data === 'object' ? (data as Record) : {}, + }), + }, + ), +) + +router.delete( + '/worker-platform/orders/:workOrderId', + requireAdminRoles(['admin', 'operator']), + createJsonHandler((req) => deleteAdminWorkOrder(String(req.params.workOrderId || '')), { + successMessage: '工单已删除', + errorMessage: '删除工单失败', + scope: '[admin/worker-platform/orders/:workOrderId]', + audit: (req, data) => ({ + action: 'work_order_deleted', + targetType: 'work_order', + targetId: String(req.params.workOrderId || ''), + data: data && typeof data === 'object' ? (data as Record) : {}, + }), + }), +) + router.post( '/worker-platform/orders/:workOrderId/publish', requireAdminRoles(['admin', 'operator']), diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index a9a9c909..021404ad 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -19,6 +19,7 @@ import { createWorkerFinanceRequest, createWorkerUser, deleteWorkCategory, + deleteWorkOrder, deleteWorkerLevel, getWorkerFinanceRequestById, getWorkerFinanceRequestSummary, @@ -51,6 +52,7 @@ import { reviewWorkerFinanceRequest, submitWorkOrderShareAcceptance, updateWorkOrder, + updateWorkOrderBasic, updateWorkerPassword, updateWorkerUser, upsertWorkCategory, @@ -667,6 +669,117 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) { return { order: mapWorkOrderAdmin(workOrder) } } +export async function updateAdminWorkOrder( + workOrderId: number | string, + payload: JsonObject = {}, +) { + const workOrder = await getRequiredWorkOrder(workOrderId) + if ( + ![ + WORK_ORDER_STATUS.PENDING_MATERIAL, + WORK_ORDER_STATUS.UNASSIGNED, + WORK_ORDER_STATUS.OPEN, + ].includes(workOrder.status as never) + ) { + throw createHttpError('只有待完善、未分配或待抢单的工单可以编辑', { + statusCode: 409, + errorCode: 'work_order_edit_status_invalid', + }) + } + if (workOrder.assigned_worker_id) { + throw createHttpError('工单已被打手接走,不能编辑', { + statusCode: 409, + errorCode: 'work_order_edit_assigned_invalid', + }) + } + + const fields = normalizeRequirementFieldsFromPayload(payload) + const rewardAmount = + payload.rewardAmount === undefined + ? workOrder.reward_amount + : normalizeAmountFen(payload.rewardAmount, workOrder.reward_amount) + const requiredDepositAmount = + payload.requiredDepositAmount === undefined + ? workOrder.required_deposit_amount + : normalizeAmountFen(payload.requiredDepositAmount, workOrder.required_deposit_amount) + const depositThresholdAmount = + payload.depositThresholdAmount === undefined + ? workOrder.deposit_threshold_amount + : normalizeAmountFen(payload.depositThresholdAmount, workOrder.deposit_threshold_amount) + const categoryId = + payload.categoryId === undefined || payload.categoryId === null || payload.categoryId === '' + ? workOrder.category_id + : normalizeOptionalId(payload.categoryId) ?? workOrder.category_id + + const now = nowIso() + const updated = await updateWorkOrderBasic(workOrder.id, { + productName: String(payload.productName ?? workOrder.product_name).trim(), + platformOrderId: String( + payload.platformOrderId ?? workOrder.platform_order_id ?? '', + ).trim(), + categoryId, + rewardAmount, + requiredDepositAmount, + depositThresholdAmount, + requirementJson: JSON.stringify({ fields }), + updatedAt: now, + }) + await createWorkOrderEvent({ + workOrderId: workOrder.id, + actorType: 'admin', + actorId: '', + eventType: 'edited', + fromStatus: workOrder.status, + toStatus: workOrder.status, + payloadJson: JSON.stringify({ + productName: updated?.product_name, + rewardAmount, + requiredDepositAmount, + fields, + }), + now, + }) + return { order: mapWorkOrderAdmin(updated || workOrder) } +} + +export async function deleteAdminWorkOrder(workOrderId: number | string) { + const workOrder = await getRequiredWorkOrder(workOrderId) + if ( + ![ + WORK_ORDER_STATUS.PENDING_MATERIAL, + WORK_ORDER_STATUS.UNASSIGNED, + WORK_ORDER_STATUS.CANCELLED, + ].includes(workOrder.status as never) + ) { + throw createHttpError('只有待完善、未分配或已取消的工单可以删除', { + statusCode: 409, + errorCode: 'work_order_delete_status_invalid', + }) + } + if (workOrder.assigned_worker_id) { + throw createHttpError('工单已被打手接走,不能删除', { + statusCode: 409, + errorCode: 'work_order_delete_assigned_invalid', + }) + } + const shares = await listWorkOrderShares(workOrder.id) + const activeShares = shares.filter((share) => share.status !== 'cancelled') + if (activeShares.length > 0) { + throw createHttpError('工单存在参与中的拼单,不能删除', { + statusCode: 409, + errorCode: 'work_order_delete_has_shares', + }) + } + const deleted = await deleteWorkOrder(workOrder.id) + if (!deleted) { + throw createHttpError('工单删除失败', { + statusCode: 500, + errorCode: 'work_order_delete_failed', + }) + } + return { deleted: true } +} + export async function publishAdminWorkOrder(workOrderId: number | string) { const workOrder = await getRequiredWorkOrder(workOrderId) if (workOrder.status !== WORK_ORDER_STATUS.UNASSIGNED) { diff --git a/apps/backend/src/services/worker-platform/mappers.ts b/apps/backend/src/services/worker-platform/mappers.ts index 075f7101..b660c10c 100644 --- a/apps/backend/src/services/worker-platform/mappers.ts +++ b/apps/backend/src/services/worker-platform/mappers.ts @@ -345,6 +345,7 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow) { workOrderNo: workOrder.work_order_no, platformOrderId: workOrder.platform_order_id, productName: workOrder.product_name, + categoryId: workOrder.category_id ? Number(workOrder.category_id) : null, categoryName: workOrder.category_name || '', status: workOrder.status, rewardAmount: Number(workOrder.reward_amount || 0), diff --git a/apps/frontend/src/lib/http.ts b/apps/frontend/src/lib/http.ts index fc25c853..b9df455a 100644 --- a/apps/frontend/src/lib/http.ts +++ b/apps/frontend/src/lib/http.ts @@ -187,6 +187,14 @@ export function apiPost(url: string, data?: unknown) { }) } +export function apiPut(url: string, data?: unknown) { + return request({ + method: 'PUT', + url, + data: data as Record, + }) +} + export function apiPostForm(url: string, data: FormData) { return request({ method: 'POST', diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index d3ddb72b..f7516c51 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -1,5 +1,7 @@ import { CheckOutlined, + DeleteOutlined, + EditOutlined, FormOutlined, PlusOutlined, ReloadOutlined, @@ -41,6 +43,7 @@ import { createAdminMockWorkOrder, creditAdminWorkerWallet, deleteAdminWorkCategory, + deleteAdminWorkOrder, deleteAdminWorkerLevel, fetchAdminWorkerFinanceConfig, fetchAdminWorkerFinanceRequests, @@ -63,6 +66,7 @@ import { submitAdminWorkOrderMaterial, syncAdminWorkerOrdersFromSource, unpublishAdminWorkOrder, + updateAdminWorkOrder, updateAdminWorkOrderSharing, } from '@/services/admin' import type { @@ -114,9 +118,18 @@ export default function WorkOrdersPanel() { const [materialOrder, setMaterialOrder] = useState(null) const [materialScreenshots, setMaterialScreenshots] = useState([]) const [sharingOrder, setSharingOrder] = useState(null) + const [editOrder, setEditOrder] = useState(null) const [problemForm] = Form.useForm() const [resolutionForm] = Form.useForm() const [materialForm] = Form.useForm() + const [editForm] = Form.useForm<{ + productName?: string + platformOrderId?: string + categoryId?: number | null + rewardAmount?: number + requiredDepositAmount?: number + fieldsText?: string + }>() const [sharingForm] = Form.useForm<{ enabled?: boolean totalQuantity?: number @@ -129,6 +142,10 @@ export default function WorkOrdersPanel() { queryFn: () => fetchAdminWorkOrders({ status, page, pageSize }), }) const ordersPagination = ordersQuery.data?.data.pagination + const categoriesQuery = useQuery({ + queryKey: ['admin-worker-platform-categories'], + queryFn: () => fetchAdminWorkCategories(), + }) const sharingQuery = useQuery({ queryKey: ['admin-worker-platform-order-sharing', sharingOrder?.workOrderId], queryFn: () => fetchAdminWorkOrderSharing(Number(sharingOrder?.workOrderId)), @@ -293,6 +310,61 @@ export default function WorkOrdersPanel() { setDetailOrder(row) } + function openEditModal(row: WorkOrder) { + setEditOrder(row) + editForm.setFieldsValue({ + productName: row.productName, + platformOrderId: row.platformOrderId || '', + categoryId: row.categoryId ?? undefined, + rewardAmount: Math.round(Number(row.rewardAmount || 0)) / 100, + requiredDepositAmount: + Math.round(Number(row.requiredDepositAmount || 0)) / 100, + fieldsText: formatRequirementFieldsText(getRequirementFields(row)), + }) + } + + async function submitEdit(values: { + productName?: string + platformOrderId?: string + categoryId?: number | null + rewardAmount?: number + requiredDepositAmount?: number + fieldsText?: string + }) { + if (!editOrder) return + const succeeded = await runAction( + () => + updateAdminWorkOrder(editOrder.workOrderId, { + productName: String(values.productName || '').trim(), + platformOrderId: String(values.platformOrderId || '').trim(), + categoryId: values.categoryId || null, + rewardAmount: Number(values.rewardAmount || 0), + requiredDepositAmount: Number(values.requiredDepositAmount || 0), + fieldsText: String(values.fieldsText || ''), + }), + '工单信息已保存', + ) + if (!succeeded) return + setEditOrder(null) + editForm.resetFields() + } + + function requestDeleteWorkOrder(row: WorkOrder) { + modal.confirm({ + title: `确认删除工单 ${row.workOrderNo}?`, + content: + '删除后工单将从列表中移除,且不可恢复。仅未分配、待完善或已取消且无拼单参与的工单可删除。', + okText: '确认删除', + okButtonProps: { danger: true }, + cancelText: '取消', + onOk: () => + runAction( + () => deleteAdminWorkOrder(row.workOrderId), + '工单已删除', + ), + }) + } + const columns: TableColumnsType = [ { title: '订单', @@ -383,6 +455,23 @@ export default function WorkOrdersPanel() { render: (_, row) => ( + {['pending_material', 'unassigned', 'open'].includes(row.status) ? ( + + ) : null} + {['pending_material', 'unassigned', 'cancelled'].includes(row.status) ? ( + + ) : null} {['pending_material', 'unassigned', 'open'].includes(row.status) ? (