feat: 打手分内部/外部,内部免审核;支持后台指派打手

- worker_users 新增 worker_type(internal/external,迁移 017)
- 内部打手提交验收后自动通过进入待打款(accepted+结算),外部打手保持后台审核
- 打手管理页支持设为内部/外部并显示类型标签
- 新增后台指派打手:未分配/待抢单工单可指定已注册打手,进入其我的订单并冻结押金
- 指派带审计与事件记录,校验打手状态/接单上限/余额押金
This commit is contained in:
yml2213
2026-08-14 16:35:15 +08:00
parent 6a17113374
commit 7bb3142958
13 changed files with 403 additions and 16 deletions
@@ -8,6 +8,7 @@ import {
SearchOutlined,
SendOutlined,
TeamOutlined,
UserAddOutlined,
WarningOutlined,
} from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query'
@@ -42,6 +43,7 @@ import ImagePreviewList from '@/components/files/ImagePreviewList'
import PageHeader from '@/components/admin/PageHeader'
import {
acceptAdminWorkOrder,
assignAdminWorkOrderToWorker,
createAdminMockWorkOrder,
creditAdminWorkerWallet,
deductAdminWorkOrderPendingDeposit,
@@ -125,6 +127,8 @@ export default function WorkOrdersPanel() {
const [materialOrder, setMaterialOrder] = useState<WorkOrder | null>(null)
const [materialScreenshots, setMaterialScreenshots] = useState<UploadedFile[]>([])
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
const [assignOrder, setAssignOrder] = useState<WorkOrder | null>(null)
const [assignWorkerId, setAssignWorkerId] = useState<number | undefined>(undefined)
const [editOrder, setEditOrder] = useState<WorkOrder | null>(null)
const [deductOrder, setDeductOrder] = useState<WorkOrder | null>(null)
const [problemForm] = Form.useForm()
@@ -162,6 +166,30 @@ export default function WorkOrdersPanel() {
queryFn: () => fetchAdminWorkOrderSharing(Number(sharingOrder?.workOrderId)),
enabled: Boolean(sharingOrder),
})
const assignWorkersQuery = useQuery({
queryKey: ['admin-worker-platform-workers', 'active', 1, 200],
queryFn: () => fetchAdminWorkerUsers({ status: 'active', page: 1, pageSize: 200 }),
enabled: Boolean(assignOrder),
})
function openAssignModal(row: WorkOrder) {
setAssignOrder(row)
setAssignWorkerId(undefined)
}
async function submitAssign() {
if (!assignOrder || !assignWorkerId) return
const succeeded = await runAction(
() =>
assignAdminWorkOrderToWorker(assignOrder.workOrderId, {
workerId: assignWorkerId,
}),
'已指派打手',
)
if (!succeeded) return
setAssignOrder(null)
setAssignWorkerId(undefined)
}
function openSharingConfigModal(row: WorkOrder) {
setSharingOrder(row)
@@ -551,6 +579,11 @@ export default function WorkOrdersPanel() {
</Button>
) : null}
{['unassigned', 'open'].includes(row.status) ? (
<Button icon={<UserAddOutlined />} onClick={() => openAssignModal(row)}>
</Button>
) : null}
{row.status === 'pending_material' ? (
<Button
icon={<FormOutlined />}
@@ -1142,6 +1175,41 @@ export default function WorkOrdersPanel() {
</Form>
</Modal>
<Modal
title={`指派打手 · ${assignOrder?.productName || ''}`}
open={Boolean(assignOrder)}
onCancel={() => {
setAssignOrder(null)
setAssignWorkerId(undefined)
}}
onOk={() => submitAssign()}
okText="确认指派"
okButtonProps={{ disabled: !assignWorkerId }}
destroyOnHidden
width={520}
>
<Space direction="vertical" size={12} className="full-width">
<Typography.Paragraph type="secondary">
</Typography.Paragraph>
<Select
style={{ width: '100%' }}
placeholder="选择要指派的打手"
loading={assignWorkersQuery.isLoading}
showSearch
optionFilterProp="label"
value={assignWorkerId}
onChange={setAssignWorkerId}
options={(assignWorkersQuery.data?.data.items || []).map((worker) => ({
value: worker.workerId,
label: `${worker.displayName || worker.username}${worker.username}${
worker.workerType === 'internal' ? '· 内部' : ''
}`,
}))}
/>
</Space>
</Modal>
<Modal
title={`拼单配置 · ${sharingOrder?.productName || ''}`}
open={Boolean(sharingOrder)}
@@ -84,7 +84,7 @@ import { formatAdminDateTime } from '@/utils/admin-time'
import { formatMoney, formatWorkerStatus } from './shared'
export default function WorkersPanel() {
const { message } = App.useApp()
const { message, modal } = App.useApp()
const queryClient = useQueryClient()
const [status, setStatus] = useState('')
const [page, setPage] = useState(1)
@@ -125,6 +125,30 @@ export default function WorkersPanel() {
}
}
function toggleWorkerType(worker: WorkerUser, workerType: 'internal' | 'external') {
const isInternal = workerType === 'internal'
modal.confirm({
title: isInternal ? '设为内部打手?' : '设为外部打手?',
content: isInternal
? '内部打手提交验收后将免审核,直接进入待打款;请确认该打手可信。'
: '外部打手提交验收后需后台审核通过,才进入待打款。',
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
await reviewAdminWorkerUser(worker.workerId, {
status: worker.status,
workerType,
})
message.success('打手类型已更新')
await refreshWorkers()
} catch (error) {
message.error(error instanceof Error ? error.message : '操作失败')
}
},
})
}
async function submitCredit(values: { amount?: number; note?: string }) {
if (!creditWorker) return
try {
@@ -164,9 +188,14 @@ export default function WorkersPanel() {
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong>
{row.displayName || row.username}
</Typography.Text>
<Space size={6} wrap>
<Typography.Text strong>
{row.displayName || row.username}
</Typography.Text>
<Tag color={row.workerType === 'internal' ? 'blue' : 'default'}>
{row.workerType === 'internal' ? '内部' : '外部'}
</Tag>
</Space>
<Typography.Text type="secondary">{row.username}</Typography.Text>
</div>
),
@@ -227,6 +256,11 @@ export default function WorkersPanel() {
{row.status !== 'rejected' ? (
<Button onClick={() => review(row, 'rejected')}></Button>
) : null}
{row.workerType !== 'internal' ? (
<Button onClick={() => toggleWorkerType(row, 'internal')}></Button>
) : (
<Button onClick={() => toggleWorkerType(row, 'external')}></Button>
)}
<Button
onClick={() => {
setLevelWorker(row)
@@ -456,6 +456,9 @@ export default function WorkerProfilePage() {
<Tag color={resolveWorkerStatusColor(worker.status)}>
{formatWorkerStatus(worker.status)}
</Tag>
<Tag color={worker.workerType === 'internal' ? 'blue' : 'default'}>
{worker.workerType === 'internal' ? '内部打手' : '外部打手'}
</Tag>
<Tag>{worker.level?.name || '未分级'}</Tag>
</Space>
</Space>
@@ -124,7 +124,7 @@ export function deleteAdminWorkProductRule(ruleId: number) {
export function reviewAdminWorkerUser(
workerId: number,
payload: { status: string; reviewNote?: string; levelId?: number },
payload: { status: string; reviewNote?: string; levelId?: number; workerType?: string },
) {
return apiPost<{ worker: WorkerUser }>(
`/api/v1/admin/worker-platform/workers/${workerId}/review`,
@@ -132,6 +132,16 @@ export function reviewAdminWorkerUser(
)
}
export function assignAdminWorkOrderToWorker(
workOrderId: number,
payload: { workerId: number },
) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/assign`,
payload,
)
}
export function creditAdminWorkerWallet(
workerId: number,
payload: { amount: number; note?: string },
@@ -4,6 +4,7 @@ export type WorkerUser = {
displayName: string
phone: string
status: string
workerType: string
reviewNote: string
inviteCode: string
inviter: null | {