diff --git a/apps/backend/src/db/migrations/061_work_order_admin_note.sql b/apps/backend/src/db/migrations/061_work_order_admin_note.sql new file mode 100644 index 00000000..554a1a54 --- /dev/null +++ b/apps/backend/src/db/migrations/061_work_order_admin_note.sql @@ -0,0 +1,5 @@ +-- 061_work_order_admin_note.sql —— 后台客服工单备注。 +ALTER TABLE work_orders + ADD COLUMN IF NOT EXISTS admin_note TEXT NOT NULL DEFAULT ''; + +COMMENT ON COLUMN work_orders.admin_note IS '后台客服维护的工单备注,不展示给打手'; diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index b3c5d38c..b6f38d46 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -355,6 +355,7 @@ export type WorkOrderRow = { requirement_json: string | Record acceptance_json: string | Record draft_acceptance_json: string | Record + admin_note: string problem_note: string sharing_enabled: boolean sharing_total_quantity: number diff --git a/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts index 98043718..0e98f11a 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts @@ -167,6 +167,19 @@ export async function updateWorkOrderBasic( return getWorkOrderById(workOrderId) } +export async function updateWorkOrderAdminNote( + workOrderId: number | string, + note: string, + updatedAt: string, +): Promise { + const result = await query<{ id: number }>( + `UPDATE work_orders SET admin_note = $1, updated_at = $2 WHERE id = $3 RETURNING id`, + [String(note || '').trim(), updatedAt, Number(workOrderId)], + ) + if (!result.rows[0]) return null + return getWorkOrderById(result.rows[0].id) +} + 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 diff --git a/apps/backend/src/routes/admin/worker-platform/orders.ts b/apps/backend/src/routes/admin/worker-platform/orders.ts index 83c4de8d..ceffbbd6 100644 --- a/apps/backend/src/routes/admin/worker-platform/orders.ts +++ b/apps/backend/src/routes/admin/worker-platform/orders.ts @@ -23,6 +23,7 @@ import { unassignAdminWorkOrder, unpublishAdminWorkOrder, updateAdminWorkOrder, + updateAdminWorkOrderNote, updateAdminWorkOrderSharing, } from '../../../services/worker-platform/index.js' import { @@ -188,6 +189,30 @@ router.put( ), ) +router.put( + '/worker-platform/orders/:workOrderId/note', + requireAdminPermission('worker_order.edit'), + createJsonHandler( + (req) => + updateAdminWorkOrderNote( + String(req.params.workOrderId || ''), + req.body?.note, + req.adminSession?.username || '', + ), + { + successMessage: '工单备注已保存', + errorMessage: '保存工单备注失败', + scope: '[admin/worker-platform/orders/:workOrderId/note]', + audit: (req, data) => ({ + action: 'work_order_admin_note_updated', + targetType: 'work_order', + targetId: String(req.params.workOrderId || ''), + data: data && typeof data === 'object' ? (data as Record) : {}, + }), + }, + ), +) + router.post( '/worker-platform/orders/:workOrderId/assign', requireAdminPermission('worker_order.assign'), diff --git a/apps/backend/src/services/worker-platform/admin-work-order-management-service.ts b/apps/backend/src/services/worker-platform/admin-work-order-management-service.ts index 54a0b4ad..bd47017d 100644 --- a/apps/backend/src/services/worker-platform/admin-work-order-management-service.ts +++ b/apps/backend/src/services/worker-platform/admin-work-order-management-service.ts @@ -8,6 +8,7 @@ import { resolveProblemWorkOrder, updateWorkOrder, updateWorkOrderBasic, + updateWorkOrderAdminNote, } from '../../repositories/worker-platform/index.js' import type { JsonObject } from '../../types/json.js' import { createHttpError } from '../../utils/http.js' @@ -111,6 +112,40 @@ export async function updateAdminWorkOrder( return { order: mapWorkOrderAdmin(updated || workOrder) } } +export async function updateAdminWorkOrderNote( + workOrderId: number | string, + note: unknown, + actorName = '', +) { + const workOrder = await getRequiredWorkOrder(workOrderId) + const normalizedNote = String(note ?? '').trim() + if (normalizedNote.length > 2000) { + throw createHttpError('备注不能超过 2000 个字符', { + statusCode: 400, + errorCode: 'work_order_admin_note_too_long', + }) + } + const now = nowIso() + const updated = await updateWorkOrderAdminNote(workOrder.id, normalizedNote, now) + if (!updated) { + throw createHttpError('保存工单备注失败', { + statusCode: 409, + errorCode: 'work_order_admin_note_update_failed', + }) + } + await createWorkOrderEvent({ + workOrderId: workOrder.id, + actorType: 'admin', + actorId: actorName, + eventType: 'admin_note_updated', + fromStatus: workOrder.status, + toStatus: workOrder.status, + payloadJson: JSON.stringify({ note: normalizedNote }), + now, + }) + return { order: mapWorkOrderAdmin(updated) } +} + export async function deleteAdminWorkOrder(workOrderId: number | string) { const workOrder = await getRequiredWorkOrder(workOrderId) if (!mapWorkOrderAdmin(workOrder).capabilities.canDelete) { diff --git a/apps/backend/src/services/worker-platform/work-order-display-mappers.ts b/apps/backend/src/services/worker-platform/work-order-display-mappers.ts index f28bf473..0818d4c0 100644 --- a/apps/backend/src/services/worker-platform/work-order-display-mappers.ts +++ b/apps/backend/src/services/worker-platform/work-order-display-mappers.ts @@ -240,6 +240,7 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow, shares?: WorkOrderSha }, acceptance: mapAcceptanceForResponse(workOrder.acceptance_json), draftAcceptance: mapAcceptanceForResponse(workOrder.draft_acceptance_json), + adminNote: workOrder.admin_note || '', problemNote: workOrder.problem_note || '', cancel: workOrder.status === WORK_ORDER_STATUS.CANCELLED @@ -286,6 +287,7 @@ export function mapWorkOrderForWorker( rawStatus: _rawStatus, hallAvailable: _hallAvailable, capabilities: _capabilities, + adminNote: _adminNote, ...visibleOrder } = mapped const personalStatus = resolveWorkerShareStatus(workOrder.status, myShare?.status) diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index 6b0fe824..c8639622 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -67,6 +67,7 @@ import { submitAdminWorkOrderMaterial, unpublishAdminWorkOrder, updateAdminWorkOrder, + updateAdminWorkOrderNote, updateAdminWorkOrderSharing, } from '@/services/admin' import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform' @@ -223,6 +224,7 @@ export default function WorkOrdersPanel() { const [assignOrder, setAssignOrder] = useState(null) const [assignWorkerId, setAssignWorkerId] = useState(undefined) const [editOrder, setEditOrder] = useState(null) + const [noteOrder, setNoteOrder] = useState(null) const [deductOrder, setDeductOrder] = useState(null) const [cancelOrder, setCancelOrder] = useState(null) const [problemForm] = Form.useForm() @@ -241,6 +243,7 @@ export default function WorkOrdersPanel() { timeoutMinutes?: number timeoutPolicy?: string }>() + const [noteForm] = Form.useForm<{ note?: string }>() const [sharingForm] = Form.useForm<{ enabled?: boolean totalQuantity?: number @@ -710,6 +713,25 @@ export default function WorkOrdersPanel() { }) } + function openNoteModal(row: WorkOrder) { + setNoteOrder(row) + noteForm.setFieldsValue({ note: row.adminNote || '' }) + } + + async function submitNote(values: { note?: string }) { + if (!noteOrder) return + const succeeded = await runAction( + () => updateAdminWorkOrderNote(noteOrder.workOrderId, String(values.note || '')), + '工单备注已保存', + ) + if (!succeeded) return + if (detailOrder?.workOrderId === noteOrder.workOrderId) { + setDetailOrder({ ...detailOrder, adminNote: String(values.note || '').trim() }) + } + setNoteOrder(null) + noteForm.resetFields() + } + function openDeductModal(row: WorkOrder) { setDeductOrder(row) deductForm.setFieldsValue({ @@ -974,6 +996,15 @@ export default function WorkOrdersPanel() { {cancelText} ) : null} + {String(row.adminNote || '').trim() ? ( + + 备注:{row.adminNote} + + ) : null} ) }, @@ -1027,6 +1058,11 @@ export default function WorkOrdersPanel() { render: (_, row) => ( + {canEditOrder ? ( + + ) : null} {row.capabilities?.canEdit && canEditOrder ? ( + ) : null} + + {detailOrder.cancel ? ( @@ -1683,6 +1735,34 @@ export default function WorkOrdersPanel() { ) : null} + { + setNoteOrder(null) + noteForm.resetFields() + }} + onOk={() => noteForm.submit()} + destroyOnHidden + > + {noteOrder ? ( +
+ + + +
+ ) : null} +
(`/api/v1/admin/worker-platform/orders/${workOrderId}/note`, { + note, + }) +} + export function deleteAdminWorkOrder(workOrderId: number) { return apiDelete<{ deleted: boolean }>(`/api/v1/admin/worker-platform/orders/${workOrderId}`) } diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index 0d8d6f46..2e3ba93d 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -576,6 +576,8 @@ export type WorkOrder = { acceptance: WorkerAcceptance /** 暂存的验收草稿(未提交时保存于此,刷新不丢失;提交后清空) */ draftAcceptance: WorkerAcceptance + /** 后台客服维护的工单备注。 */ + adminNote?: string workerNote?: string /** 打手本人尚未点击查看或操作过该工单。 */ isUnread?: boolean