接单工单支持编辑与删除,修复编辑押金不生效问题

This commit is contained in:
yml2213
2026-08-01 18:35:12 +08:00
parent fddb757078
commit de33bfa74c
8 changed files with 408 additions and 1 deletions
@@ -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<WorkOrderRow | null> {
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<boolean> {
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
@@ -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<string, unknown>) : {},
}),
},
),
)
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<string, unknown>) : {},
}),
}),
)
router.post(
'/worker-platform/orders/:workOrderId/publish',
requireAdminRoles(['admin', 'operator']),
@@ -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) {
@@ -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),
+8
View File
@@ -187,6 +187,14 @@ export function apiPost<T>(url: string, data?: unknown) {
})
}
export function apiPut<T>(url: string, data?: unknown) {
return request<T>({
method: 'PUT',
url,
data: data as Record<string, unknown>,
})
}
export function apiPostForm<T>(url: string, data: FormData) {
return request<T>({
method: 'POST',
@@ -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<WorkOrder | null>(null)
const [materialScreenshots, setMaterialScreenshots] = useState<UploadedFile[]>([])
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
const [editOrder, setEditOrder] = useState<WorkOrder | null>(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<WorkOrder> = [
{
title: '订单',
@@ -383,6 +455,23 @@ export default function WorkOrdersPanel() {
render: (_, row) => (
<Space wrap>
<Button onClick={() => openDetailDrawer(row)}></Button>
{['pending_material', 'unassigned', 'open'].includes(row.status) ? (
<Button
icon={<EditOutlined />}
onClick={() => openEditModal(row)}
>
</Button>
) : null}
{['pending_material', 'unassigned', 'cancelled'].includes(row.status) ? (
<Button
danger
icon={<DeleteOutlined />}
onClick={() => requestDeleteWorkOrder(row)}
>
</Button>
) : null}
{['pending_material', 'unassigned', 'open'].includes(row.status) ? (
<Button
icon={<TeamOutlined />}
@@ -710,6 +799,68 @@ export default function WorkOrdersPanel() {
) : null}
</Drawer>
<Modal
title={`编辑工单 · ${editOrder?.workOrderNo || ''}`}
open={Boolean(editOrder)}
onCancel={() => {
setEditOrder(null)
editForm.resetFields()
}}
onOk={() => editForm.submit()}
destroyOnHidden
width={720}
>
{editOrder ? (
<Form form={editForm} layout="vertical" onFinish={submitEdit}>
<Form.Item
label="商品"
name="productName"
rules={[{ required: true, message: '请填写商品名称' }]}
>
<Input />
</Form.Item>
<Form.Item label="平台订单号" name="platformOrderId">
<Input placeholder="留空表示无平台订单号" />
</Form.Item>
<Form.Item label="分类" name="categoryId">
<Select
allowClear
placeholder="选择接单分类"
options={(categoriesQuery.data?.data.items || [])
.filter((category) => category.status === 'active')
.map((category) => ({
label: category.name,
value: category.categoryId,
}))}
/>
</Form.Item>
<Form.Item
label="接单金额"
name="rewardAmount"
rules={[{ required: true, message: '请填写接单金额' }]}
tooltip="打手看到的接单报酬"
>
<InputNumber min={0.01} step={1} addonAfter="元" />
</Form.Item>
<Form.Item
label="冻结押金"
name="requiredDepositAmount"
rules={[{ required: true, message: '请填写冻结押金' }]}
tooltip="抢单时需要冻结的押金,0 表示无需押金"
>
<InputNumber min={0} step={1} addonAfter="元" />
</Form.Item>
<Form.Item
label="资料字段"
name="fieldsText"
extra="每行一个字段:key:名称,下拉项用 # 分隔(如 system:系统#安卓,苹果)"
>
<Input.TextArea rows={5} />
</Form.Item>
</Form>
) : null}
</Modal>
<Modal
title="标记问题单"
open={Boolean(problemOrder)}
@@ -952,6 +1103,18 @@ export default function WorkOrdersPanel() {
)
}
function formatRequirementFieldsText(fields: CollectField[]): string {
return fields
.map((field) => {
const labelPart = `${field.key}:${field.label}`
const options = (field.options || []).filter(Boolean)
return options.length > 0
? `${labelPart}#${options.join(',')}`
: labelPart
})
.join('\n')
}
function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void> }) {
const { message } = App.useApp()
const [loading, setLoading] = useState(false)
@@ -1,4 +1,4 @@
import { apiDelete, apiGet, apiPost } from '@/lib/http'
import { apiDelete, apiGet, apiPost, apiPut } from '@/lib/http'
import type {
UploadedFile,
WorkCategory,
@@ -219,6 +219,29 @@ export function publishAdminWorkOrder(workOrderId: number) {
)
}
export function updateAdminWorkOrder(
workOrderId: number,
payload: {
productName?: string
platformOrderId?: string
categoryId?: number | null
rewardAmount?: number
requiredDepositAmount?: number
fieldsText?: string
},
) {
return apiPut<{ order: WorkOrder }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}`,
payload,
)
}
export function deleteAdminWorkOrder(workOrderId: number) {
return apiDelete<{ deleted: boolean }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}`,
)
}
export function unpublishAdminWorkOrder(workOrderId: number) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/unpublish`,
@@ -208,6 +208,7 @@ export type WorkOrder = {
workOrderNo: string
platformOrderId: string
productName: string
categoryId?: number | null
categoryName: string
status: WorkOrderStatus
rewardAmount: number