支持VIP5免审核后补图

This commit is contained in:
yml2213
2026-08-17 12:29:34 +08:00
parent 2881a99040
commit baf09dc533
13 changed files with 348 additions and 28 deletions
@@ -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,