拆分打手平台参数校验
This commit is contained in:
@@ -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<Record<string, unknown>> {
|
||||
if (!Array.isArray(value)) return []
|
||||
const files: Array<Record<string, unknown>> = []
|
||||
for (const item of value) {
|
||||
if (!item || typeof item !== 'object') continue
|
||||
const file = item as Record<string, unknown>
|
||||
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(),
|
||||
|
||||
@@ -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<Record<string, unknown>> {
|
||||
if (!Array.isArray(value)) return []
|
||||
const files: Array<Record<string, unknown>> = []
|
||||
for (const item of value) {
|
||||
if (!item || typeof item !== 'object') continue
|
||||
const file = item as Record<string, unknown>
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user