支持VIP5免审核后补图
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
-- VIP5 免审核验收:允许无验收图自动结算,并在验收后 24 小时内补图。
|
||||
-- 权限保存在等级 JSON 中,避免为单一等级能力新增固定表字段。
|
||||
|
||||
UPDATE worker_levels
|
||||
SET
|
||||
permission_json = jsonb_set(
|
||||
COALESCE(permission_json, '{}'::jsonb),
|
||||
'{autoAcceptWithoutEvidence}',
|
||||
CASE WHEN level_key = 'vip5' THEN 'true'::jsonb ELSE 'false'::jsonb END,
|
||||
true
|
||||
),
|
||||
updated_at = NOW()
|
||||
WHERE NOT (COALESCE(permission_json, '{}'::jsonb) ? 'autoAcceptWithoutEvidence');
|
||||
|
||||
COMMENT ON COLUMN worker_levels.permission_json IS
|
||||
'等级权限:免押额度、最大同时接单量、VIP 免审核及其他等级能力配置';
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
sendWorkerSmsCode,
|
||||
saveWorkerOrderAcceptanceDraft,
|
||||
saveWorkerOrderNote,
|
||||
supplementWorkerAcceptedOrderEvidence,
|
||||
submitWorkerOrderAcceptance,
|
||||
} from '../services/worker-platform/index.js'
|
||||
import { buildNotFoundPayload, createRouteHandler } from '../utils/http.js'
|
||||
@@ -335,6 +336,24 @@ router.post(
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/orders/:workOrderId/acceptance-evidence',
|
||||
requireActiveWorker,
|
||||
createRouteHandler(
|
||||
(req) =>
|
||||
supplementWorkerAcceptedOrderEvidence(
|
||||
String(req.params.workOrderId || ''),
|
||||
req.body || {},
|
||||
getRequiredWorkerSession(req),
|
||||
),
|
||||
{
|
||||
successMessage: '验收图片已补充',
|
||||
errorMessage: '补充验收图片失败',
|
||||
scope: '[worker/orders/:workOrderId/acceptance-evidence]',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.use((req, res) => {
|
||||
res.status(404).json(buildNotFoundPayload(req))
|
||||
})
|
||||
|
||||
@@ -164,6 +164,7 @@ export async function saveAdminWorkerLevel(payload: JsonObject = {}) {
|
||||
const now = nowIso()
|
||||
const levelKey = String(payload.levelKey || payload.level_key || '').trim() || DEFAULT_LEVEL_KEY
|
||||
const existingLevel = await getWorkerLevelByKey(levelKey)
|
||||
const existingPermissions = safeParseJson(existingLevel?.permission_json)
|
||||
const name = String(payload.name || '').trim() || DEFAULT_LEVEL_NAME
|
||||
const permissions = {
|
||||
depositFreeAmount: normalizeAmountFen(
|
||||
@@ -172,6 +173,10 @@ export async function saveAdminWorkerLevel(payload: JsonObject = {}) {
|
||||
),
|
||||
maxActiveOrders: normalizePositiveInteger(payload.maxActiveOrders, 1),
|
||||
upgradeThreshold: normalizeInteger(payload.upgradeThreshold, 0),
|
||||
autoAcceptWithoutEvidence: normalizeBoolean(
|
||||
payload.autoAcceptWithoutEvidence,
|
||||
normalizeBoolean(existingPermissions.autoAcceptWithoutEvidence, levelKey === 'vip5'),
|
||||
),
|
||||
visibleDelaySeconds: normalizeInteger(payload.visibleDelaySeconds, -1),
|
||||
}
|
||||
const level = await upsertWorkerLevel({
|
||||
|
||||
@@ -98,6 +98,7 @@ export function resolveWorkerPermissions(worker: WorkerUserRow) {
|
||||
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
|
||||
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1),
|
||||
upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0),
|
||||
autoAcceptWithoutEvidence: normalizeBoolean(permission.autoAcceptWithoutEvidence, false),
|
||||
visibleDelaySeconds: resolveVisibleDelaySeconds(
|
||||
worker.level_key || '',
|
||||
permission.visibleDelaySeconds,
|
||||
@@ -140,6 +141,7 @@ export function mapWorkerLevel(level: WorkerLevelRow | null | undefined) {
|
||||
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
|
||||
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1),
|
||||
upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0),
|
||||
autoAcceptWithoutEvidence: normalizeBoolean(permission.autoAcceptWithoutEvidence, false),
|
||||
visibleDelaySeconds: resolveVisibleDelaySeconds(
|
||||
level.level_key,
|
||||
permission.visibleDelaySeconds,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
normalizeWithdrawChannel,
|
||||
normalizeRequirementFields,
|
||||
resolveCollectSubmitTargetWorkOrder,
|
||||
resolveWorkerPermissions,
|
||||
resolveSkuNameQuantity,
|
||||
} from './index.js'
|
||||
|
||||
@@ -96,6 +97,18 @@ test('提现渠道仅支持支付宝和微信', () => {
|
||||
assert.throws(() => normalizeWithdrawChannel('bank'))
|
||||
})
|
||||
|
||||
test('等级免审核后补图权限默认关闭,仅显式开启时生效', () => {
|
||||
assert.equal(resolveWorkerPermissions(buildWorkerUserRow()).autoAcceptWithoutEvidence, false)
|
||||
assert.equal(
|
||||
resolveWorkerPermissions(
|
||||
buildWorkerUserRow({
|
||||
level_permission_json: { autoAcceptWithoutEvidence: true },
|
||||
}),
|
||||
).autoAcceptWithoutEvidence,
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('normalizeRequirementFields default template keeps gameId/gameNickname as text inputs', () => {
|
||||
const fields = normalizeRequirementFields([])
|
||||
const gameId = fields.find((field) => field.key === 'gameId')
|
||||
|
||||
@@ -62,7 +62,7 @@ import {
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { addHours, nowIso } from '../../utils/time.js'
|
||||
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import { getSmsProvider } from '../sms/index.js'
|
||||
import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
||||
@@ -115,6 +115,7 @@ import {
|
||||
} from './mappers.js'
|
||||
|
||||
const WORKER_DAILY_WITHDRAW_LIMIT = 3
|
||||
const VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS = 24
|
||||
|
||||
export type WorkerSession = {
|
||||
sessionId: string
|
||||
@@ -1266,10 +1267,13 @@ export async function submitWorkerOrderAcceptance(
|
||||
requireActiveWorkerSession(session)
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
const workerId = Number(session.workerId)
|
||||
const worker = await getRequiredWorker(session.workerId)
|
||||
const sharingShare = await getWorkOrderShare(workOrder.id, workerId)
|
||||
if (sharingShare) {
|
||||
return submitWorkerSharingAcceptance(workOrder, sharingShare, payload, session)
|
||||
}
|
||||
const canAutoAcceptWithoutEvidence =
|
||||
!workOrder.sharing_enabled && resolveWorkerPermissions(worker).autoAcceptWithoutEvidence
|
||||
const now = nowIso()
|
||||
if (
|
||||
workOrder.status === WORK_ORDER_STATUS.IN_PROGRESS &&
|
||||
@@ -1305,7 +1309,7 @@ export async function submitWorkerOrderAcceptance(
|
||||
payload,
|
||||
workOrder.draft_acceptance_json,
|
||||
)
|
||||
if (imageUrls.length === 0) {
|
||||
if (imageUrls.length === 0 && !canAutoAcceptWithoutEvidence) {
|
||||
throw createHttpError('请上传验收图片', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_order_acceptance_image_required',
|
||||
@@ -1316,6 +1320,14 @@ export async function submitWorkerOrderAcceptance(
|
||||
files,
|
||||
imageUrls: [...new Set(imageUrls)],
|
||||
submittedAt: now,
|
||||
...(canAutoAcceptWithoutEvidence
|
||||
? {
|
||||
reviewMode: 'vip_auto',
|
||||
evidenceStatus: imageUrls.length > 0 ? 'complete' : 'pending',
|
||||
evidenceDueAt:
|
||||
imageUrls.length > 0 ? null : addHours(now, VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
const updated = await updateWorkOrder(workOrder.id, {
|
||||
status: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
||||
@@ -1331,11 +1343,14 @@ export async function submitWorkerOrderAcceptance(
|
||||
eventType: 'acceptance_submitted',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
||||
payloadJson: JSON.stringify(acceptance),
|
||||
payloadJson: JSON.stringify({
|
||||
imageCount: imageUrls.length,
|
||||
autoAccepted: canAutoAcceptWithoutEvidence,
|
||||
evidenceDueAt: acceptance.evidenceDueAt || null,
|
||||
}),
|
||||
now,
|
||||
})
|
||||
const worker = await getRequiredWorker(session.workerId)
|
||||
if (worker.worker_type === 'internal') {
|
||||
if (worker.worker_type === 'internal' || canAutoAcceptWithoutEvidence) {
|
||||
const { order: acceptedOrder, failureReason } = await acceptWorkOrderAndSettle({
|
||||
workOrderId: workOrder.id,
|
||||
unfreezeDays: getWorkerFinanceConfig().depositUnfreezeDays,
|
||||
@@ -1350,7 +1365,7 @@ export async function submitWorkerOrderAcceptance(
|
||||
})
|
||||
}
|
||||
if (!acceptedOrder) {
|
||||
throw createHttpError('内部打手免审核验收失败,请稍后重试', {
|
||||
throw createHttpError('免审核验收失败,请稍后重试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_internal_auto_accept_failed',
|
||||
})
|
||||
@@ -1359,10 +1374,14 @@ export async function submitWorkerOrderAcceptance(
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'worker',
|
||||
actorId: String(session.workerId),
|
||||
eventType: 'accepted_by_internal',
|
||||
eventType: worker.worker_type === 'internal' ? 'accepted_by_internal' : 'accepted_by_vip',
|
||||
fromStatus: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
||||
toStatus: WORK_ORDER_STATUS.ACCEPTED,
|
||||
payloadJson: JSON.stringify({ autoAccepted: true }),
|
||||
payloadJson: JSON.stringify({
|
||||
autoAccepted: true,
|
||||
evidenceStatus: acceptance.evidenceStatus || 'complete',
|
||||
evidenceDueAt: acceptance.evidenceDueAt || null,
|
||||
}),
|
||||
now,
|
||||
})
|
||||
return {
|
||||
@@ -1374,6 +1393,111 @@ export async function submitWorkerOrderAcceptance(
|
||||
}
|
||||
}
|
||||
|
||||
/** VIP 自动验收订单在 24 小时内追加验收图片,不改变订单状态和结算结果。 */
|
||||
export async function supplementWorkerAcceptedOrderEvidence(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
session: WorkerSession,
|
||||
) {
|
||||
requireActiveWorkerSession(session)
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
const worker = await getRequiredWorker(session.workerId)
|
||||
if (workOrder.sharing_enabled) {
|
||||
throw createHttpError('拼单订单暂不支持免审核后补图', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_vip_evidence_sharing_not_supported',
|
||||
})
|
||||
}
|
||||
if (Number(workOrder.assigned_worker_id || 0) !== Number(session.workerId)) {
|
||||
throw createHttpError('只能补充自己的订单验收图片', {
|
||||
statusCode: 403,
|
||||
errorCode: 'work_order_owner_required',
|
||||
})
|
||||
}
|
||||
if (workOrder.status !== WORK_ORDER_STATUS.ACCEPTED) {
|
||||
throw createHttpError('只有已验收订单可以补充验收图片', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_evidence_supplement_status_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const previous = safeParseJson(workOrder.acceptance_json)
|
||||
if (String(previous.reviewMode || '').trim() !== 'vip_auto') {
|
||||
throw createHttpError('该订单不支持验收后补图', {
|
||||
statusCode: 403,
|
||||
errorCode: 'work_order_evidence_supplement_not_allowed',
|
||||
})
|
||||
}
|
||||
const evidenceDueAt = String(previous.evidenceDueAt || '').trim()
|
||||
if (!evidenceDueAt || Date.parse(evidenceDueAt) <= Date.now()) {
|
||||
throw createHttpError('补充验收图片已超过 24 小时时限', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_evidence_supplement_expired',
|
||||
})
|
||||
}
|
||||
|
||||
const existingFiles = normalizeUploadedFiles(previous.files)
|
||||
const uploadedFiles = normalizeUploadedFiles(payload.files)
|
||||
const mergedFiles = [...existingFiles]
|
||||
const fileIdentities = new Set(
|
||||
existingFiles.map((file) => file.objectKey || file.url || file.mediumUrl || file.thumbnailUrl),
|
||||
)
|
||||
for (const file of uploadedFiles) {
|
||||
const identity = file.objectKey || file.url || file.mediumUrl || file.thumbnailUrl
|
||||
if (!identity || fileIdentities.has(identity)) continue
|
||||
fileIdentities.add(identity)
|
||||
mergedFiles.push(file)
|
||||
}
|
||||
|
||||
const previousImageUrls = normalizeStringArray(previous.imageUrls)
|
||||
const imageUrls = [
|
||||
...new Set([
|
||||
...previousImageUrls,
|
||||
...mergedFiles.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean),
|
||||
]),
|
||||
]
|
||||
if (
|
||||
mergedFiles.length === existingFiles.length &&
|
||||
imageUrls.length === previousImageUrls.length
|
||||
) {
|
||||
throw createHttpError('请上传新的验收图片', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_order_evidence_supplement_image_required',
|
||||
})
|
||||
}
|
||||
|
||||
const acceptance = {
|
||||
...previous,
|
||||
files: mergedFiles,
|
||||
imageUrls,
|
||||
evidenceStatus: 'complete',
|
||||
evidenceSupplementedAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
const updated = await updateWorkOrderAcceptanceRecord({
|
||||
workOrderId: workOrder.id,
|
||||
acceptanceJson: JSON.stringify(acceptance),
|
||||
now,
|
||||
})
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'worker',
|
||||
actorId: String(session.workerId),
|
||||
eventType: 'acceptance_evidence_supplemented',
|
||||
fromStatus: WORK_ORDER_STATUS.ACCEPTED,
|
||||
toStatus: WORK_ORDER_STATUS.ACCEPTED,
|
||||
payloadJson: JSON.stringify({
|
||||
addedImageCount: mergedFiles.length - existingFiles.length,
|
||||
totalImageCount: imageUrls.length,
|
||||
}),
|
||||
now,
|
||||
})
|
||||
return {
|
||||
order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(worker)),
|
||||
}
|
||||
}
|
||||
|
||||
async function submitWorkerSharingAcceptance(
|
||||
workOrder: WorkOrderRow,
|
||||
sharingShare: WorkOrderShareRow,
|
||||
|
||||
@@ -129,6 +129,19 @@ export function formatWorkOrderEvent(event: WorkOrderEvent): EventNodeStyle {
|
||||
)}`,
|
||||
color: 'purple',
|
||||
}
|
||||
case 'accepted_by_vip':
|
||||
return {
|
||||
title: 'VIP 免审核验收通过,订单完成',
|
||||
description: readPayload(event, 'evidenceDueAt') ? '可在 24 小时内补充验收图片' : undefined,
|
||||
color: 'purple',
|
||||
}
|
||||
case 'acceptance_evidence_supplemented': {
|
||||
const addedImageCount = readPayload(event, 'addedImageCount')
|
||||
return {
|
||||
title: `已验收后补充验收图片${addedImageCount ? `(新增 ${addedImageCount} 张)` : ''}`,
|
||||
color: 'purple',
|
||||
}
|
||||
}
|
||||
case 'accepted':
|
||||
case 'accepted_by_admin':
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
InputNumber,
|
||||
Modal,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
@@ -46,6 +47,7 @@ export default function LevelsPanel() {
|
||||
depositFreeAmount: level.depositFreeAmount,
|
||||
maxActiveOrders: level.maxActiveOrders,
|
||||
upgradeThreshold: level.upgradeThreshold,
|
||||
autoAcceptWithoutEvidence: level.autoAcceptWithoutEvidence,
|
||||
visibleDelaySeconds: level.visibleDelaySeconds,
|
||||
})
|
||||
}
|
||||
@@ -61,6 +63,7 @@ export default function LevelsPanel() {
|
||||
depositFreeAmount?: number
|
||||
maxActiveOrders?: number
|
||||
upgradeThreshold?: number
|
||||
autoAcceptWithoutEvidence?: boolean
|
||||
visibleDelaySeconds?: number
|
||||
}) {
|
||||
try {
|
||||
@@ -81,6 +84,7 @@ export default function LevelsPanel() {
|
||||
depositFreeAmount: level.depositFreeAmount,
|
||||
maxActiveOrders: level.maxActiveOrders,
|
||||
upgradeThreshold: level.upgradeThreshold,
|
||||
autoAcceptWithoutEvidence: level.autoAcceptWithoutEvidence,
|
||||
visibleDelaySeconds: level.visibleDelaySeconds,
|
||||
status: level.status === 'active' ? 'disabled' : 'active',
|
||||
})
|
||||
@@ -116,6 +120,12 @@ export default function LevelsPanel() {
|
||||
},
|
||||
{ title: '最大同时接单', dataIndex: 'maxActiveOrders' },
|
||||
{ title: '升级所需累计完成', dataIndex: 'upgradeThreshold' },
|
||||
{
|
||||
title: '免审核后补图',
|
||||
width: 130,
|
||||
render: (_, row) =>
|
||||
row.autoAcceptWithoutEvidence ? <Tag color="purple">开启</Tag> : <Tag>关闭</Tag>,
|
||||
},
|
||||
{
|
||||
title: '可见延迟',
|
||||
width: 110,
|
||||
@@ -161,6 +171,7 @@ export default function LevelsPanel() {
|
||||
depositFreeAmount: 0,
|
||||
maxActiveOrders: 1,
|
||||
upgradeThreshold: 0,
|
||||
autoAcceptWithoutEvidence: false,
|
||||
visibleDelaySeconds: 20,
|
||||
}}
|
||||
onFinish={saveLevel}
|
||||
@@ -188,6 +199,14 @@ export default function LevelsPanel() {
|
||||
>
|
||||
<InputNumber min={0} max={100000} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="免审核后补图"
|
||||
name="autoAcceptWithoutEvidence"
|
||||
valuePropName="checked"
|
||||
tooltip="开启后,打手可无验收图提交并自动验收;验收后仅可在 24 小时内补图。"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="大厅可见延迟(秒)"
|
||||
name="visibleDelaySeconds"
|
||||
|
||||
@@ -727,14 +727,25 @@ export default function WorkOrdersPanel() {
|
||||
{
|
||||
title: '验收图',
|
||||
width: 170,
|
||||
render: (_, row) => (
|
||||
render: (_, row) => {
|
||||
const isVipAutoAcceptance = row.acceptance?.reviewMode === 'vip_auto'
|
||||
const evidencePending = row.acceptance?.evidenceStatus === 'pending'
|
||||
return (
|
||||
<Space direction="vertical" size={4}>
|
||||
<ImagePreviewList
|
||||
files={getAcceptanceFiles(row)}
|
||||
imageUrls={getAcceptanceImageUrls(row)}
|
||||
size={44}
|
||||
maxVisible={2}
|
||||
/>
|
||||
),
|
||||
{isVipAutoAcceptance ? (
|
||||
<Tag color="purple">
|
||||
{evidencePending ? 'VIP 免审核,待补图' : 'VIP 免审核,已补图'}
|
||||
</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -1083,6 +1094,13 @@ export default function WorkOrdersPanel() {
|
||||
{getAcceptanceFiles(detailOrder).length +
|
||||
getAcceptanceImageUrls(detailOrder).length || '-'}
|
||||
</Descriptions.Item>
|
||||
{detailOrder.acceptance?.reviewMode === 'vip_auto' ? (
|
||||
<Descriptions.Item label="VIP 补图">
|
||||
{detailOrder.acceptance.evidenceStatus === 'pending'
|
||||
? `待补图,截止 ${formatAdminDateTime(detailOrder.acceptance.evidenceDueAt)}`
|
||||
: `已补图 ${formatAdminDateTime(detailOrder.acceptance.evidenceSupplementedAt)}`}
|
||||
</Descriptions.Item>
|
||||
) : null}
|
||||
<Descriptions.Item label="备注" span={2}>
|
||||
<span className="worker-order-detail-value">
|
||||
{String(detailOrder.acceptance?.note || '').trim() || '-'}
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
fetchWorkerProfile,
|
||||
saveWorkerAcceptanceDraft,
|
||||
saveWorkerOrderNote,
|
||||
supplementWorkerAcceptedOrderEvidence,
|
||||
submitWorkerAcceptance,
|
||||
} from '@/services/worker'
|
||||
import type {
|
||||
@@ -199,6 +200,8 @@ export default function WorkerOrdersPage() {
|
||||
retry: false,
|
||||
})
|
||||
const orderTimelineVisible = profileQuery.data?.data.orderTimelineVisible === true
|
||||
const workerCanAutoAcceptWithoutEvidence =
|
||||
profileQuery.data?.data.worker.level?.permissions.autoAcceptWithoutEvidence === true
|
||||
|
||||
const overviewQuery = useQuery({
|
||||
queryKey: ['worker-my-orders-overview'],
|
||||
@@ -297,7 +300,29 @@ export default function WorkerOrdersPage() {
|
||||
|
||||
async function submitAcceptance(values: AcceptanceFormValues) {
|
||||
if (!editingOrder) return
|
||||
const isAcceptedEvidenceSupplement = canSupplementAcceptedEvidence(editingOrder)
|
||||
if (isAcceptedEvidenceSupplement) {
|
||||
if (acceptanceFiles.length === 0) {
|
||||
message.error('请上传新的验收图片')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await supplementWorkerAcceptedOrderEvidence(editingOrder.workOrderId, {
|
||||
files: acceptanceFiles,
|
||||
})
|
||||
message.success('验收图片已补充')
|
||||
setEditingOrder(null)
|
||||
setAcceptanceFiles([])
|
||||
form.resetFields()
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '补充验收图片失败')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const canAutoAccept = canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence)
|
||||
if (acceptanceFiles.length === 0 && !canAutoAccept) {
|
||||
message.error('请上传验收图片')
|
||||
return
|
||||
}
|
||||
@@ -306,7 +331,7 @@ export default function WorkerOrdersPage() {
|
||||
note: values.note,
|
||||
files: acceptanceFiles,
|
||||
})
|
||||
message.success('已提交验收')
|
||||
message.success(canAutoAccept ? '已自动验收,可在 24 小时内补充图片' : '已提交验收')
|
||||
setEditingOrder(null)
|
||||
setAcceptanceFiles([])
|
||||
form.resetFields()
|
||||
@@ -426,6 +451,7 @@ export default function WorkerOrdersPage() {
|
||||
const hasAcceptance = acceptanceFiles.length > 0
|
||||
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(row))
|
||||
const draftCount = draftFiles.length
|
||||
const vipEvidencePending = isVipEvidencePending(row)
|
||||
|
||||
if (!hasAcceptance && !submittedAt && draftCount === 0) {
|
||||
return (
|
||||
@@ -444,6 +470,7 @@ export default function WorkerOrdersPage() {
|
||||
提交:{formatDateTime(submittedAt)}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
{vipEvidencePending ? <Tag color="purple">VIP 免审核,待补图</Tag> : null}
|
||||
{draftCount > 0 && !hasSubmittedAcceptance(row) ? (
|
||||
<div className="acceptance-draft-badge-wrap">
|
||||
<Tag color="processing" icon={<PictureOutlined />}>
|
||||
@@ -794,6 +821,9 @@ export default function WorkerOrdersPage() {
|
||||
<ImagePreviewList files={acceptanceFiles} size={32} maxVisible={3} />
|
||||
</div>
|
||||
) : null}
|
||||
{isVipEvidencePending(order) ? (
|
||||
<Tag color="purple">VIP 免审核,待补图</Tag>
|
||||
) : null}
|
||||
{draftCount > 0 && !hasSubmittedAcceptance(order) ? (
|
||||
<div className="worker-order-mobile-draft-files">
|
||||
<Tag color="processing" icon={<PictureOutlined />}>
|
||||
@@ -1160,7 +1190,7 @@ export default function WorkerOrdersPage() {
|
||||
<Modal
|
||||
title={
|
||||
editingOrder
|
||||
? hasSubmittedAcceptance(editingOrder)
|
||||
? canSupplementAcceptedEvidence(editingOrder) || hasSubmittedAcceptance(editingOrder)
|
||||
? '补充验收图片'
|
||||
: '验收图片'
|
||||
: '验收图片'
|
||||
@@ -1185,6 +1215,12 @@ export default function WorkerOrdersPage() {
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
{canSupplementAcceptedEvidence(editingOrder) ? (
|
||||
<Button type="primary" onClick={() => form.submit()}>
|
||||
补充图片
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button onClick={() => void saveAcceptanceImages(form.getFieldsValue())}>
|
||||
{hasSubmittedAcceptance(editingOrder) ? '保存修改' : '保存暂存'}
|
||||
</Button>
|
||||
@@ -1193,6 +1229,8 @@ export default function WorkerOrdersPage() {
|
||||
提交验收
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
) : null
|
||||
}
|
||||
@@ -1201,7 +1239,17 @@ export default function WorkerOrdersPage() {
|
||||
<Form.Item label="完成说明" name="note">
|
||||
<Input.TextArea rows={3} placeholder="可补充代练结果、注意事项或截图说明。" />
|
||||
</Form.Item>
|
||||
<Form.Item label="验收图片" required>
|
||||
<Form.Item
|
||||
label="验收图片"
|
||||
required={
|
||||
!editingOrder || !canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence)
|
||||
}
|
||||
extra={
|
||||
editingOrder && canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence)
|
||||
? 'VIP 免审核可先提交,验收后需在 24 小时内补充图片。'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<ImageUpload
|
||||
scene="worker-acceptance"
|
||||
scope="worker"
|
||||
@@ -1224,12 +1272,38 @@ function canSubmitAcceptance(order: WorkOrder) {
|
||||
return ['in_progress', 'problem'].includes(order.status)
|
||||
}
|
||||
|
||||
function canAutoAcceptOrder(order: WorkOrder, workerCanAutoAcceptWithoutEvidence: boolean) {
|
||||
return (
|
||||
workerCanAutoAcceptWithoutEvidence &&
|
||||
!order.myShare &&
|
||||
order.sharing?.enabled !== true &&
|
||||
['in_progress', 'problem'].includes(order.status)
|
||||
)
|
||||
}
|
||||
|
||||
function canSupplementAcceptedEvidence(order: WorkOrder) {
|
||||
if (order.myShare || order.status !== 'accepted' || order.acceptance?.reviewMode !== 'vip_auto') {
|
||||
return false
|
||||
}
|
||||
const dueAt = Date.parse(String(order.acceptance.evidenceDueAt || ''))
|
||||
return Number.isFinite(dueAt) && dueAt > Date.now()
|
||||
}
|
||||
|
||||
function isVipEvidencePending(order: WorkOrder) {
|
||||
return (
|
||||
order.acceptance?.reviewMode === 'vip_auto' && order.acceptance?.evidenceStatus === 'pending'
|
||||
)
|
||||
}
|
||||
|
||||
/** 是否可编辑验收图片:未提交时可暂存,已提交(待验收)时可补充/修改 */
|
||||
function canEditAcceptanceImages(order: WorkOrder) {
|
||||
if (order.myShare) {
|
||||
return ['joined', 'submitted'].includes(order.myShare.status) && order.status !== 'accepted'
|
||||
}
|
||||
return ['in_progress', 'problem', 'pending_acceptance'].includes(order.status)
|
||||
return (
|
||||
['in_progress', 'problem', 'pending_acceptance'].includes(order.status) ||
|
||||
canSupplementAcceptedEvidence(order)
|
||||
)
|
||||
}
|
||||
|
||||
/** 是否已提交验收(整单 pending_acceptance / 拼单份额 submitted) */
|
||||
@@ -1237,7 +1311,7 @@ function hasSubmittedAcceptance(order: WorkOrder) {
|
||||
if (order.myShare) {
|
||||
return order.myShare.status === 'submitted'
|
||||
}
|
||||
return order.status === 'pending_acceptance'
|
||||
return order.status === 'pending_acceptance' || order.acceptance?.reviewMode === 'vip_auto'
|
||||
}
|
||||
|
||||
function getDisplayOrderNo(order: Pick<WorkOrder, 'platformOrderId'> | null | undefined): string {
|
||||
|
||||
@@ -64,6 +64,7 @@ export function saveAdminWorkerLevel(payload: {
|
||||
depositFreeAmount?: number
|
||||
maxActiveOrders?: number
|
||||
upgradeThreshold?: number
|
||||
autoAcceptWithoutEvidence?: boolean
|
||||
visibleDelaySeconds?: number
|
||||
status?: string
|
||||
}) {
|
||||
|
||||
@@ -159,6 +159,16 @@ export function saveWorkerAcceptanceDraft(
|
||||
)
|
||||
}
|
||||
|
||||
export function supplementWorkerAcceptedOrderEvidence(
|
||||
workOrderId: number,
|
||||
payload: { files: UploadedFile[] },
|
||||
) {
|
||||
return apiPost<{ order: WorkOrder }>(
|
||||
`/api/v1/worker/orders/${workOrderId}/acceptance-evidence`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function lookupCollectOrder(orderNo: string) {
|
||||
return apiPost<CollectLookupResponse>('/api/v1/collect/lookup', { orderNo })
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export type WorkerUser = {
|
||||
depositFreeAmount: number
|
||||
maxActiveOrders: number
|
||||
upgradeThreshold: number
|
||||
autoAcceptWithoutEvidence: boolean
|
||||
visibleDelaySeconds: number
|
||||
}
|
||||
}
|
||||
@@ -141,6 +142,7 @@ export type WorkerLevel = {
|
||||
depositFreeAmount: number
|
||||
maxActiveOrders: number
|
||||
upgradeThreshold: number
|
||||
autoAcceptWithoutEvidence: boolean
|
||||
visibleDelaySeconds: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
@@ -217,6 +219,10 @@ export type WorkerAcceptance = {
|
||||
imageUrls?: string[]
|
||||
submittedAt?: string
|
||||
updatedAt?: string
|
||||
reviewMode?: 'vip_auto' | string
|
||||
evidenceStatus?: 'pending' | 'complete' | string
|
||||
evidenceDueAt?: string | null
|
||||
evidenceSupplementedAt?: string | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user