From baf09dc533e9d8fe64daf4a8c5ae910d6d37c8ab Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 17 Aug 2026 12:29:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81VIP5=E5=85=8D=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E5=90=8E=E8=A1=A5=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../026_vip_auto_accept_evidence.sql | 16 ++ apps/backend/src/routes/worker.ts | 19 +++ .../services/worker-platform/admin-service.ts | 5 + .../src/services/worker-platform/mappers.ts | 2 + .../worker-platform-service.test.ts | 13 ++ .../worker-platform/worker-service.ts | 140 +++++++++++++++++- .../src/components/WorkOrderEventTimeline.tsx | 13 ++ .../src/pages/admin/panels/LevelsPanel.tsx | 19 +++ .../pages/admin/panels/WorkOrdersPanel.tsx | 34 ++++- .../src/pages/worker/WorkerOrdersPage.tsx | 98 ++++++++++-- .../src/services/admin/worker-platform.ts | 1 + apps/frontend/src/services/worker.ts | 10 ++ apps/frontend/src/types/worker-platform.ts | 6 + 13 files changed, 348 insertions(+), 28 deletions(-) create mode 100644 apps/backend/src/db/migrations/026_vip_auto_accept_evidence.sql diff --git a/apps/backend/src/db/migrations/026_vip_auto_accept_evidence.sql b/apps/backend/src/db/migrations/026_vip_auto_accept_evidence.sql new file mode 100644 index 00000000..fffd15c8 --- /dev/null +++ b/apps/backend/src/db/migrations/026_vip_auto_accept_evidence.sql @@ -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 免审核及其他等级能力配置'; diff --git a/apps/backend/src/routes/worker.ts b/apps/backend/src/routes/worker.ts index a8f03dc2..54821e6e 100644 --- a/apps/backend/src/routes/worker.ts +++ b/apps/backend/src/routes/worker.ts @@ -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)) }) diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index ada9f610..067f44e2 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -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({ diff --git a/apps/backend/src/services/worker-platform/mappers.ts b/apps/backend/src/services/worker-platform/mappers.ts index baf944f3..f84ce7ea 100644 --- a/apps/backend/src/services/worker-platform/mappers.ts +++ b/apps/backend/src/services/worker-platform/mappers.ts @@ -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, diff --git a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts index caaf8e45..7cd177f0 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts @@ -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') diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index bd3e657c..31263050 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -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, diff --git a/apps/frontend/src/components/WorkOrderEventTimeline.tsx b/apps/frontend/src/components/WorkOrderEventTimeline.tsx index 941f3c0e..59988e3b 100644 --- a/apps/frontend/src/components/WorkOrderEventTimeline.tsx +++ b/apps/frontend/src/components/WorkOrderEventTimeline.tsx @@ -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 { diff --git a/apps/frontend/src/pages/admin/panels/LevelsPanel.tsx b/apps/frontend/src/pages/admin/panels/LevelsPanel.tsx index 51d93b54..6fe53d54 100644 --- a/apps/frontend/src/pages/admin/panels/LevelsPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/LevelsPanel.tsx @@ -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 ? 开启 : 关闭, + }, { 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() { > + + + ( - - ), + render: (_, row) => { + const isVipAutoAcceptance = row.acceptance?.reviewMode === 'vip_auto' + const evidencePending = row.acceptance?.evidenceStatus === 'pending' + return ( + + + {isVipAutoAcceptance ? ( + + {evidencePending ? 'VIP 免审核,待补图' : 'VIP 免审核,已补图'} + + ) : null} + + ) + }, }, { title: '操作', @@ -1083,6 +1094,13 @@ export default function WorkOrdersPanel() { {getAcceptanceFiles(detailOrder).length + getAcceptanceImageUrls(detailOrder).length || '-'} + {detailOrder.acceptance?.reviewMode === 'vip_auto' ? ( + + {detailOrder.acceptance.evidenceStatus === 'pending' + ? `待补图,截止 ${formatAdminDateTime(detailOrder.acceptance.evidenceDueAt)}` + : `已补图 ${formatAdminDateTime(detailOrder.acceptance.evidenceSupplementedAt)}`} + + ) : null} {String(detailOrder.acceptance?.note || '').trim() || '-'} diff --git a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx index bc634b06..1430a62e 100644 --- a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx @@ -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 - if (acceptanceFiles.length === 0) { + 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)} ) : null} + {vipEvidencePending ? VIP 免审核,待补图 : null} {draftCount > 0 && !hasSubmittedAcceptance(row) ? (
}> @@ -794,6 +821,9 @@ export default function WorkerOrdersPage() {
) : null} + {isVipEvidencePending(order) ? ( + VIP 免审核,待补图 + ) : null} {draftCount > 0 && !hasSubmittedAcceptance(order) ? (
}> @@ -1160,7 +1190,7 @@ export default function WorkerOrdersPage() { 取消 - - {canSubmitAcceptance(editingOrder) ? ( + {canSupplementAcceptedEvidence(editingOrder) ? ( - ) : null} + ) : ( + <> + + {canSubmitAcceptance(editingOrder) ? ( + + ) : null} + + )} ) : null } @@ -1201,7 +1239,17 @@ export default function WorkerOrdersPage() { - + 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 | null | undefined): string { diff --git a/apps/frontend/src/services/admin/worker-platform.ts b/apps/frontend/src/services/admin/worker-platform.ts index 86b036a8..fac7deb3 100644 --- a/apps/frontend/src/services/admin/worker-platform.ts +++ b/apps/frontend/src/services/admin/worker-platform.ts @@ -64,6 +64,7 @@ export function saveAdminWorkerLevel(payload: { depositFreeAmount?: number maxActiveOrders?: number upgradeThreshold?: number + autoAcceptWithoutEvidence?: boolean visibleDelaySeconds?: number status?: string }) { diff --git a/apps/frontend/src/services/worker.ts b/apps/frontend/src/services/worker.ts index 237a48e7..a2a1274f 100644 --- a/apps/frontend/src/services/worker.ts +++ b/apps/frontend/src/services/worker.ts @@ -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('/api/v1/collect/lookup', { orderNo }) } diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index 6fe6fbd1..87cd64b0 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -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 }