From fa0cc7772c9b325c5baf7465961bbaff29d6bc52 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 21 Aug 2026 17:51:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E6=89=93=E6=89=8B=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E5=8F=82=E6=95=B0=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/worker-platform/mappers.ts | 250 ++---------------- .../worker-platform-normalizers.ts | 219 +++++++++++++++ 2 files changed, 242 insertions(+), 227 deletions(-) create mode 100644 apps/backend/src/services/worker-platform/worker-platform-normalizers.ts diff --git a/apps/backend/src/services/worker-platform/mappers.ts b/apps/backend/src/services/worker-platform/mappers.ts index 48af63ba..80bf4017 100644 --- a/apps/backend/src/services/worker-platform/mappers.ts +++ b/apps/backend/src/services/worker-platform/mappers.ts @@ -12,12 +12,9 @@ import { type WorkOrderShareRow, type WorkerWalletLedgerRow, type WorkerUserRow, - type GrabWorkOrderFailureReason, - type ProblemWorkOrderResolutionAction, } from '../../repositories/worker-platform/index.js' import type { JsonObject } from '../../types/json.js' import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js' -import { createHttpError } from '../../utils/http.js' import { safeParseJson } from '../admin/admin-query-utils.js' import { refreshStoredFileUrl, @@ -46,6 +43,29 @@ import { normalizeStatuses, normalizeUploadedFiles, } from './worker-input-normalizers.js' +export { + assertWithdrawChannelAllowedForAmount, + isWorkerPasswordStrong, + normalizeAdminFinanceReviewStatus, + normalizeAmountFen, + normalizeEnabledStatus, + normalizeFinanceRequestStatus, + normalizeFinanceRequestType, + normalizeMatchType, + normalizeOptionalId, + normalizePassword, + normalizeProblemResolutionAction, + normalizeProofFiles, + normalizeReviewStatus, + normalizeSlugKey, + normalizeUsername, + normalizeWalletLedgerType, + normalizeWithdrawChannel, + normalizeWorkerFreezeReason, + throwGrabWorkOrderFailure, + validateWorkerPassword, + validateWorkerUsername, +} from './worker-platform-normalizers.js' export { normalizeRequirementFields, normalizeRequirementFieldsFromPayload, @@ -1123,230 +1143,6 @@ export function resolveCollectSubmittedFieldValues(workOrder: WorkOrderRow) { return normalizeSubmittedFields(collect.fields) } -export function normalizeReviewStatus(value: unknown) { - const status = String(value || '').trim() - if (['active', 'rejected', 'disabled', 'pending_review'].includes(status)) return status - return 'pending_review' -} - -/** 冻结原因必须可追溯,限制长度避免异常大载荷写入账号资料。 */ -export function normalizeWorkerFreezeReason(value: unknown): string { - const reason = String(value || '').trim() - if (!reason) { - throw createHttpError('请填写冻结原因或备注', { - statusCode: 400, - errorCode: 'worker_freeze_reason_required', - }) - } - if (reason.length > 500) { - throw createHttpError('冻结原因不能超过 500 个字符', { - statusCode: 400, - errorCode: 'worker_freeze_reason_too_long', - }) - } - return reason -} - -export function normalizeProblemResolutionAction(value: unknown): ProblemWorkOrderResolutionAction { - const action = String(value || '').trim() - if (['return_to_worker', 'reopen', 'cancel_release', 'cancel_deduct'].includes(action)) { - return action as ProblemWorkOrderResolutionAction - } - throw createHttpError('问题单处置方式不正确', { - statusCode: 400, - errorCode: 'work_order_problem_resolution_action_invalid', - }) -} - -export function throwGrabWorkOrderFailure(failureReason: GrabWorkOrderFailureReason | null): never { - if (failureReason === 'worker_active_order_limit') { - throw createHttpError('已达到当前等级最大同时接单量', { - statusCode: 409, - errorCode: 'worker_active_order_limit', - }) - } - if (failureReason === 'worker_deposit_insufficient') { - throw createHttpError('余额不足,无法冻结所需押金', { - statusCode: 409, - errorCode: 'worker_deposit_insufficient', - }) - } - throw createHttpError('抢单失败,订单可能已被抢走', { - statusCode: 409, - errorCode: 'work_order_grab_conflict', - }) -} - -export function normalizeEnabledStatus(value: unknown) { - return String(value || 'active').trim() === 'disabled' ? 'disabled' : 'active' -} - -export function normalizeWalletLedgerType(value: unknown) { - const ledgerType = String(value || '').trim() - if (!ledgerType) return '' - if ( - [ - 'manual_credit', - 'withdraw_paid', - 'deposit_freeze', - 'deposit_release', - 'deposit_pending_unfreeze', - 'deposit_unfreeze', - 'deposit_deduction', - 'reward_settlement', - 'after_sales_deposit_deduction', - 'after_sales_balance_deduction', - 'after_sales_debt_offset', - ].includes(ledgerType) - ) { - return ledgerType - } - throw createHttpError('钱包流水类型不正确', { - statusCode: 400, - errorCode: 'worker_wallet_ledger_type_invalid', - }) -} - -export function normalizeFinanceRequestType(value: unknown) { - const requestType = String(value || '').trim() - if (!requestType) return '' - if (['recharge', 'withdraw'].includes(requestType)) { - return requestType - } - throw createHttpError('资金申请类型不正确', { - statusCode: 400, - errorCode: 'worker_finance_request_type_invalid', - }) -} - -export function normalizeFinanceRequestStatus(value: unknown) { - const status = String(value || '').trim() - if (!status) return '' - if (['pending', 'approved', 'rejected', 'cancelled'].includes(status)) { - return status - } - throw createHttpError('资金申请状态不正确', { - statusCode: 400, - errorCode: 'worker_finance_request_status_invalid', - }) -} - -export function normalizeAdminFinanceReviewStatus(value: unknown) { - const status = String(value || '').trim() - if (['approved', 'rejected', 'cancelled'].includes(status)) { - return status - } - throw createHttpError('审核状态不正确', { - statusCode: 400, - errorCode: 'worker_finance_review_status_invalid', - }) -} - -export function normalizeWithdrawChannel(value: unknown) { - const channel = String(value || '').trim() - if (['alipay', 'wechat'].includes(channel)) { - return channel - } - throw createHttpError('提现方式不正确', { - statusCode: 400, - errorCode: 'worker_withdraw_channel_invalid', - }) -} - -/** 提现金额超过 100 元时,仅允许使用支付宝。 */ -export function assertWithdrawChannelAllowedForAmount(amount: number, channel: string) { - if (amount > 10_000 && channel !== 'alipay') { - throw createHttpError('提现金额超过 100 元时,仅支持支付宝提现', { - statusCode: 400, - errorCode: 'worker_withdraw_channel_amount_limited', - }) - } -} - -export function normalizeMatchType(value: unknown) { - const matchType = String(value || '').trim() - if (matchType === 'exact') return 'exact' - return 'contains' -} - -export function normalizeSlugKey(value: unknown, fallback: string) { - const key = String(value || '') - .trim() - .toLowerCase() - .replace(/[^a-z0-9._-]+/g, '-') - .replace(/^-+|-+$/g, '') - return key || fallback -} - -export function normalizeUsername(value: unknown): string { - return String(value || '') - .trim() - .toLowerCase() -} - -export function normalizePassword(value: unknown): string { - return String(value || '').trim() -} - -export function validateWorkerUsername(username: string) { - if (!/^[a-zA-Z0-9._-]{3,32}$/.test(username)) { - throw createHttpError('接单账号格式无效,需为 3-32 位字母数字或 ._-', { - statusCode: 400, - errorCode: 'worker_username_invalid', - }) - } -} - -export function validateWorkerPassword(password: string) { - if (!isWorkerPasswordStrong(password)) { - throw createHttpError('接单密码至少 8 位,且必须包含英文字母和数字', { - statusCode: 400, - errorCode: 'worker_password_invalid', - }) - } -} - -export function isWorkerPasswordStrong(password: string): boolean { - return password.length >= 8 && /[a-zA-Z]/.test(password) && /\d/.test(password) -} - -export function normalizeOptionalId(value: unknown): number | null { - const parsed = Number(value) - return Number.isInteger(parsed) && parsed > 0 ? parsed : null -} - -export function normalizeAmountFen(value: unknown, fallback: number): number { - const text = String(value ?? '').trim() - if (!text) return fallback - const parsed = Number(text) - if (!Number.isFinite(parsed) || parsed < 0) return fallback - return Math.round(parsed * 100) -} - -export function normalizeProofFiles(value: unknown): Array> { - if (!Array.isArray(value)) return [] - const files: Array> = [] - for (const item of value) { - if (!item || typeof item !== 'object') continue - const file = item as Record - const url = String(file.url || '').trim() - if (!url) continue - files.push({ - url, - thumbnailUrl: String(file.thumbnailUrl || file.thumbnail_url || '').trim(), - mediumUrl: String(file.mediumUrl || file.medium_url || '').trim(), - objectKey: String(file.objectKey || file.object_key || '').trim(), - originalFilename: String( - file.originalFilename || file.original_filename || file.name || '', - ).trim(), - contentType: String(file.contentType || file.content_type || '').trim(), - sizeBytes: Number(file.sizeBytes || file.size_bytes || 0), - }) - if (files.length >= 10) break - } - return files -} - export function isIgnorableWorkerAuthError(error: unknown) { return IGNORABLE_WORKER_AUTH_ERROR_CODES.has( String((error as { errorCode?: string })?.errorCode || '').trim(), diff --git a/apps/backend/src/services/worker-platform/worker-platform-normalizers.ts b/apps/backend/src/services/worker-platform/worker-platform-normalizers.ts new file mode 100644 index 00000000..22d128bd --- /dev/null +++ b/apps/backend/src/services/worker-platform/worker-platform-normalizers.ts @@ -0,0 +1,219 @@ +import type { + GrabWorkOrderFailureReason, + ProblemWorkOrderResolutionAction, +} from '../../repositories/worker-platform/index.js' +import { createHttpError } from '../../utils/http.js' + +export function normalizeReviewStatus(value: unknown) { + const status = String(value || '').trim() + if (['active', 'rejected', 'disabled', 'pending_review'].includes(status)) return status + return 'pending_review' +} + +/** 冻结原因必须可追溯,限制长度避免异常大载荷写入账号资料。 */ +export function normalizeWorkerFreezeReason(value: unknown): string { + const reason = String(value || '').trim() + if (!reason) { + throw createHttpError('请填写冻结原因或备注', { + statusCode: 400, + errorCode: 'worker_freeze_reason_required', + }) + } + if (reason.length > 500) { + throw createHttpError('冻结原因不能超过 500 个字符', { + statusCode: 400, + errorCode: 'worker_freeze_reason_too_long', + }) + } + return reason +} + +export function normalizeProblemResolutionAction(value: unknown): ProblemWorkOrderResolutionAction { + const action = String(value || '').trim() + if (['return_to_worker', 'reopen', 'cancel_release', 'cancel_deduct'].includes(action)) { + return action as ProblemWorkOrderResolutionAction + } + throw createHttpError('问题单处置方式不正确', { + statusCode: 400, + errorCode: 'work_order_problem_resolution_action_invalid', + }) +} + +export function throwGrabWorkOrderFailure(failureReason: GrabWorkOrderFailureReason | null): never { + if (failureReason === 'worker_active_order_limit') { + throw createHttpError('已达到当前等级最大同时接单量', { + statusCode: 409, + errorCode: 'worker_active_order_limit', + }) + } + if (failureReason === 'worker_deposit_insufficient') { + throw createHttpError('余额不足,无法冻结所需押金', { + statusCode: 409, + errorCode: 'worker_deposit_insufficient', + }) + } + throw createHttpError('抢单失败,订单可能已被抢走', { + statusCode: 409, + errorCode: 'work_order_grab_conflict', + }) +} + +export function normalizeEnabledStatus(value: unknown) { + return String(value || 'active').trim() === 'disabled' ? 'disabled' : 'active' +} + +export function normalizeWalletLedgerType(value: unknown) { + const ledgerType = String(value || '').trim() + if (!ledgerType) return '' + if ( + [ + 'manual_credit', + 'withdraw_paid', + 'deposit_freeze', + 'deposit_release', + 'deposit_pending_unfreeze', + 'deposit_unfreeze', + 'deposit_deduction', + 'reward_settlement', + 'after_sales_deposit_deduction', + 'after_sales_balance_deduction', + 'after_sales_debt_offset', + ].includes(ledgerType) + ) { + return ledgerType + } + throw createHttpError('钱包流水类型不正确', { + statusCode: 400, + errorCode: 'worker_wallet_ledger_type_invalid', + }) +} + +export function normalizeFinanceRequestType(value: unknown) { + const requestType = String(value || '').trim() + if (!requestType) return '' + if (['recharge', 'withdraw'].includes(requestType)) return requestType + throw createHttpError('资金申请类型不正确', { + statusCode: 400, + errorCode: 'worker_finance_request_type_invalid', + }) +} + +export function normalizeFinanceRequestStatus(value: unknown) { + const status = String(value || '').trim() + if (!status) return '' + if (['pending', 'approved', 'rejected', 'cancelled'].includes(status)) return status + throw createHttpError('资金申请状态不正确', { + statusCode: 400, + errorCode: 'worker_finance_request_status_invalid', + }) +} + +export function normalizeAdminFinanceReviewStatus(value: unknown) { + const status = String(value || '').trim() + if (['approved', 'rejected', 'cancelled'].includes(status)) return status + throw createHttpError('审核状态不正确', { + statusCode: 400, + errorCode: 'worker_finance_review_status_invalid', + }) +} + +export function normalizeWithdrawChannel(value: unknown) { + const channel = String(value || '').trim() + if (['alipay', 'wechat'].includes(channel)) return channel + throw createHttpError('提现方式不正确', { + statusCode: 400, + errorCode: 'worker_withdraw_channel_invalid', + }) +} + +/** 提现金额超过 100 元时,仅允许使用支付宝。 */ +export function assertWithdrawChannelAllowedForAmount(amount: number, channel: string) { + if (amount > 10_000 && channel !== 'alipay') { + throw createHttpError('提现金额超过 100 元时,仅支持支付宝提现', { + statusCode: 400, + errorCode: 'worker_withdraw_channel_amount_limited', + }) + } +} + +export function normalizeMatchType(value: unknown) { + return String(value || '').trim() === 'exact' ? 'exact' : 'contains' +} + +export function normalizeSlugKey(value: unknown, fallback: string) { + const key = String(value || '') + .trim() + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, '-') + .replace(/^-+|-+$/g, '') + return key || fallback +} + +export function normalizeUsername(value: unknown): string { + return String(value || '') + .trim() + .toLowerCase() +} + +export function normalizePassword(value: unknown): string { + return String(value || '').trim() +} + +export function validateWorkerUsername(username: string) { + if (!/^[a-zA-Z0-9._-]{3,32}$/.test(username)) { + throw createHttpError('接单账号格式无效,需为 3-32 位字母数字或 ._-', { + statusCode: 400, + errorCode: 'worker_username_invalid', + }) + } +} + +export function validateWorkerPassword(password: string) { + if (!isWorkerPasswordStrong(password)) { + throw createHttpError('接单密码至少 8 位,且必须包含英文字母和数字', { + statusCode: 400, + errorCode: 'worker_password_invalid', + }) + } +} + +export function isWorkerPasswordStrong(password: string): boolean { + return password.length >= 8 && /[a-zA-Z]/.test(password) && /\d/.test(password) +} + +export function normalizeOptionalId(value: unknown): number | null { + const parsed = Number(value) + return Number.isInteger(parsed) && parsed > 0 ? parsed : null +} + +export function normalizeAmountFen(value: unknown, fallback: number): number { + const text = String(value ?? '').trim() + if (!text) return fallback + const parsed = Number(text) + if (!Number.isFinite(parsed) || parsed < 0) return fallback + return Math.round(parsed * 100) +} + +export function normalizeProofFiles(value: unknown): Array> { + if (!Array.isArray(value)) return [] + const files: Array> = [] + for (const item of value) { + if (!item || typeof item !== 'object') continue + const file = item as Record + const url = String(file.url || '').trim() + if (!url) continue + files.push({ + url, + thumbnailUrl: String(file.thumbnailUrl || file.thumbnail_url || '').trim(), + mediumUrl: String(file.mediumUrl || file.medium_url || '').trim(), + objectKey: String(file.objectKey || file.object_key || '').trim(), + originalFilename: String( + file.originalFilename || file.original_filename || file.name || '', + ).trim(), + contentType: String(file.contentType || file.content_type || '').trim(), + sizeBytes: Number(file.sizeBytes || file.size_bytes || 0), + }) + if (files.length >= 10) break + } + return files +}