拆分接单平台超长文件,按域组织代码
- worker-platform-service.ts (2733行) 拆为 worker-service/admin-service/mappers + index 聚合 - worker-platform-repo.ts (2456行) 拆为 repositories/worker-platform/ 目录(types/shared/worker-repo/work-order-repo) - AdminWorkerPlatformPage.tsx (2784行) 拆为主页面 + panels/ 目录(7 个面板文件) - 引用方统一改指向 index 聚合,行为不变
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
resolveOrderFulfillmentReadiness,
|
||||
syncOrderFulfillmentAttachments,
|
||||
} from '../fulfillment/order-fulfillment-readiness-service.js'
|
||||
import { syncWorkerOrdersForSourceOrder } from '../worker-platform/worker-platform-service.js'
|
||||
import { syncWorkerOrdersForSourceOrder } from '../worker-platform/index.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { logIntegration } from '../../utils/logger.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '../../../repositories/order-item-repo.js'
|
||||
import { listTasksByOrderId } from '../../../repositories/task-repo.js'
|
||||
import { upsertOrderFromSource } from '../../order/order-service.js'
|
||||
import { syncWorkerOrdersForSourceOrder } from '../../worker-platform/worker-platform-service.js'
|
||||
import { syncWorkerOrdersForSourceOrder } from '../../worker-platform/index.js'
|
||||
import { bindKuaishouIndustryVouchersToOrderTasks } from '../kuaishou-industry/voucher-binding-service.js'
|
||||
import { asJsonObject, type JsonObject } from '../../../types/json.js'
|
||||
import { OPEN_91_PLATFORM, OPEN_91_PROVIDER } from '../../open-91/config.js'
|
||||
|
||||
@@ -0,0 +1,955 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||
import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js'
|
||||
import { findLatestOrderByAnyPlatformOrderId, getOrderById } from '../../repositories/order-repo.js'
|
||||
import {
|
||||
acceptWorkOrderAndSettle,
|
||||
addWorkerWalletCredit,
|
||||
countWorkerAcceptedOrders,
|
||||
countWorkerActiveOrders,
|
||||
countWorkCategoryUsages,
|
||||
countWorkerLevelUsages,
|
||||
countWorkOrderPendingSharingSubmissions,
|
||||
cancelWorkerWorkOrder,
|
||||
countWorkerCancellationsSince,
|
||||
createWorkOrder,
|
||||
createWorkOrderEvent,
|
||||
createWorkerFinanceRequest,
|
||||
createWorkerUser,
|
||||
deleteWorkCategory,
|
||||
deleteWorkerLevel,
|
||||
getWorkerFinanceRequestById,
|
||||
getWorkerFinanceRequestSummary,
|
||||
getWorkCategoryById,
|
||||
getWorkCategoryByKey,
|
||||
getWorkerLevelById,
|
||||
getWorkerLevelByKey,
|
||||
getWorkerUserByInviteCode,
|
||||
getWorkerUserById,
|
||||
getWorkerUserByUsername,
|
||||
getWorkOrderById,
|
||||
getWorkOrderByOrderItemId,
|
||||
getWorkOrderShare,
|
||||
grabWorkOrder,
|
||||
incrementWorkerSessionVersion,
|
||||
joinWorkOrderShare,
|
||||
listAllWorkCategories,
|
||||
listPendingMaterialWorkOrdersByPlatformOrderId,
|
||||
listWorkerFinanceRequests,
|
||||
listWorkerSharesByWorker,
|
||||
listWorkerWalletLedgers,
|
||||
listWorkCategories,
|
||||
listWorkOrders,
|
||||
listWorkProductRules,
|
||||
listWorkerLevels,
|
||||
listWorkerUsers,
|
||||
listWorkOrderShares,
|
||||
listWorkOrderSharesByOrderIds,
|
||||
resolveProblemWorkOrder,
|
||||
reviewWorkerFinanceRequest,
|
||||
submitWorkOrderShareAcceptance,
|
||||
updateWorkOrder,
|
||||
updateWorkerPassword,
|
||||
updateWorkerUser,
|
||||
upsertWorkCategory,
|
||||
upsertWorkProductRule,
|
||||
upsertWorkerLevel,
|
||||
type WorkCategoryRow,
|
||||
type WorkerFinanceRequestRow,
|
||||
type WorkOrderRow,
|
||||
type WorkProductRuleRow,
|
||||
type WorkerLevelRow,
|
||||
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 { randomId } from '../../utils/random.js'
|
||||
import { nowIso, addHours } from '../../utils/time.js'
|
||||
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import {
|
||||
normalizeStoredFileUrl,
|
||||
refreshStoredFileUrl,
|
||||
refreshUploadedFileUrls,
|
||||
} from '../file-storage/file-storage-service.js'
|
||||
import {
|
||||
getWorkerFinanceConfig,
|
||||
saveWorkerFinanceConfig,
|
||||
} from './worker-finance-config-service.js'
|
||||
|
||||
import { DEFAULT_CATEGORY_KEY, DEFAULT_DEPOSIT_THRESHOLD_AMOUNT, DEFAULT_LEVEL_KEY, DEFAULT_LEVEL_NAME, mapFinanceRequest, mapWallet, mapWorkCategory, mapWorkOrderAdmin, mapWorkOrderShare, mapWorkProductRule, mapWorkerLevel, mapWorkerUser, normalizeAdminFinanceReviewStatus, normalizeAmountFen, normalizeBoolean, normalizeEnabledStatus, normalizeFinanceRequestStatus, normalizeFinanceRequestType, normalizeInteger, normalizeMatchType, normalizeOptionalId, normalizePositiveInteger, normalizeProblemResolutionAction, normalizeRequirementFields, normalizeRequirementFieldsFromPayload, normalizeReviewStatus, normalizeSessionVersion, normalizeSlugKey, normalizeSubmittedFields, resolveMatchingProductRule, resolveRequirementFields } from './mappers.js'
|
||||
import { ensureWorkerPlatformDefaults, getRequiredWorkOrder, getRequiredWorker } from './worker-service.js'
|
||||
|
||||
export async function listAdminWorkerLevels() {
|
||||
await ensureWorkerPlatformDefaults()
|
||||
return { items: (await listWorkerLevels()).map(mapWorkerLevel) }
|
||||
}
|
||||
|
||||
export async function listAdminWorkCategories() {
|
||||
await ensureWorkerPlatformDefaults()
|
||||
return { items: (await listAllWorkCategories()).map(mapWorkCategory) }
|
||||
}
|
||||
|
||||
export async function saveAdminWorkCategory(payload: JsonObject = {}) {
|
||||
const now = nowIso()
|
||||
const categoryKey = normalizeSlugKey(
|
||||
payload.categoryKey || payload.category_key || payload.name,
|
||||
DEFAULT_CATEGORY_KEY,
|
||||
)
|
||||
const name = String(payload.name || '').trim() || '默认分类'
|
||||
const category = await upsertWorkCategory({
|
||||
categoryKey,
|
||||
name,
|
||||
sortOrder: normalizeInteger(payload.sortOrder, 100),
|
||||
status: normalizeEnabledStatus(payload.status),
|
||||
now,
|
||||
})
|
||||
return { category: mapWorkCategory(category) }
|
||||
}
|
||||
|
||||
export async function deleteAdminWorkCategory(categoryId: number | string) {
|
||||
const category = await getWorkCategoryById(Number(categoryId))
|
||||
if (!category) {
|
||||
throw createHttpError('分类不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'work_category_not_found',
|
||||
})
|
||||
}
|
||||
const usages = await countWorkCategoryUsages(Number(categoryId))
|
||||
if (usages.workOrderCount > 0 || usages.productRuleCount > 0) {
|
||||
throw createHttpError(
|
||||
`该分类已被 ${usages.workOrderCount} 个工单和 ${usages.productRuleCount} 条物品规则使用,无法删除,请先停用`,
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: 'work_category_in_use',
|
||||
},
|
||||
)
|
||||
}
|
||||
const { deleted } = await deleteWorkCategory(Number(categoryId))
|
||||
return { deleted }
|
||||
}
|
||||
|
||||
export async function saveAdminWorkerLevel(payload: JsonObject = {}) {
|
||||
const now = nowIso()
|
||||
const levelKey = String(payload.levelKey || payload.level_key || '').trim() || DEFAULT_LEVEL_KEY
|
||||
const name = String(payload.name || '').trim() || DEFAULT_LEVEL_NAME
|
||||
const permissions = {
|
||||
depositFreeAmount: normalizeAmountFen(
|
||||
payload.depositFreeAmount ?? payload.depositFreeAmountYuan,
|
||||
0,
|
||||
),
|
||||
maxActiveOrders: normalizePositiveInteger(payload.maxActiveOrders, 1),
|
||||
upgradeThreshold: normalizeInteger(payload.upgradeThreshold, 0),
|
||||
}
|
||||
const level = await upsertWorkerLevel({
|
||||
levelKey,
|
||||
name,
|
||||
sortOrder: normalizeInteger(payload.sortOrder, 100),
|
||||
status: String(payload.status || 'active').trim() === 'disabled' ? 'disabled' : 'active',
|
||||
permissionJson: JSON.stringify(permissions),
|
||||
now,
|
||||
})
|
||||
return { level: mapWorkerLevel(level) }
|
||||
}
|
||||
|
||||
export async function deleteAdminWorkerLevel(levelId: number | string) {
|
||||
const level = await getWorkerLevelById(Number(levelId))
|
||||
if (!level) {
|
||||
throw createHttpError('等级不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'worker_level_not_found',
|
||||
})
|
||||
}
|
||||
if (level.level_key === DEFAULT_LEVEL_KEY) {
|
||||
throw createHttpError('默认等级(VIP1)不可删除', {
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_level_default_protected',
|
||||
})
|
||||
}
|
||||
const usageCount = await countWorkerLevelUsages(Number(levelId))
|
||||
if (usageCount > 0) {
|
||||
throw createHttpError(`该等级正在被 ${usageCount} 名打手使用,请先调整打手等级后再删除`, {
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_level_in_use',
|
||||
})
|
||||
}
|
||||
const { deleted } = await deleteWorkerLevel(Number(levelId))
|
||||
return { deleted }
|
||||
}
|
||||
|
||||
export async function listAdminWorkProductRules(query: JsonObject = {}) {
|
||||
await ensureWorkerPlatformDefaults()
|
||||
const enabledValue = String(query.enabled ?? '').trim()
|
||||
const enabled = enabledValue
|
||||
? ['true', '1', 'enabled', 'active'].includes(enabledValue.toLowerCase())
|
||||
: null
|
||||
const items = await listWorkProductRules({
|
||||
enabled,
|
||||
keyword: String(query.keyword || '').trim(),
|
||||
})
|
||||
return { items: items.map(mapWorkProductRule) }
|
||||
}
|
||||
|
||||
export async function saveAdminWorkProductRule(payload: JsonObject = {}) {
|
||||
const defaults = await ensureWorkerPlatformDefaults()
|
||||
const productName = String(
|
||||
payload.productName || payload.product_name || payload.skuName || '',
|
||||
).trim()
|
||||
const skuCode = String(payload.skuCode || payload.sku_code || '').trim()
|
||||
if (!productName && !skuCode) {
|
||||
throw createHttpError('请填写商品名或 SKU', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_product_rule_target_required',
|
||||
})
|
||||
}
|
||||
|
||||
const rewardAmount = normalizeAmountFen(payload.rewardAmount ?? payload.rewardAmountYuan, 0)
|
||||
const sharingEnabled = normalizeBoolean(
|
||||
payload.sharingEnabled ?? payload.sharing_enabled,
|
||||
false,
|
||||
)
|
||||
const sharingUnitReward = normalizeAmountFen(
|
||||
payload.sharingUnitReward ?? payload.sharingUnitRewardYuan,
|
||||
0,
|
||||
)
|
||||
const sharingTotalAmount = normalizeAmountFen(
|
||||
payload.sharingTotalAmount ?? payload.sharingTotalAmountYuan,
|
||||
0,
|
||||
)
|
||||
const sharingTotalQuantity = resolveSharingQuantity(
|
||||
sharingTotalAmount,
|
||||
normalizePositiveInteger(payload.sharingTotalQuantity ?? payload.sharing_total_quantity, 1),
|
||||
sharingUnitReward,
|
||||
)
|
||||
const resolvedSharingTotalAmount =
|
||||
sharingTotalAmount > 0 ? sharingTotalAmount : sharingUnitReward * sharingTotalQuantity
|
||||
if (sharingEnabled) {
|
||||
if (resolvedSharingTotalAmount <= 0) {
|
||||
throw createHttpError('启用拼单时请填写拼单总价或单价', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_product_rule_sharing_amount_invalid',
|
||||
})
|
||||
}
|
||||
} else if (rewardAmount <= 0) {
|
||||
throw createHttpError('接单金额必须大于 0', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_product_rule_reward_invalid',
|
||||
})
|
||||
}
|
||||
const finalRewardAmount = sharingEnabled ? resolvedSharingTotalAmount : rewardAmount
|
||||
|
||||
const depositThresholdAmount = normalizeAmountFen(
|
||||
payload.depositThresholdAmount ?? payload.depositThresholdAmountYuan,
|
||||
DEFAULT_DEPOSIT_THRESHOLD_AMOUNT,
|
||||
)
|
||||
const requiredDepositAmount =
|
||||
payload.requiredDepositAmount === undefined && payload.requiredDepositAmountYuan === undefined
|
||||
? finalRewardAmount > depositThresholdAmount
|
||||
? finalRewardAmount
|
||||
: 0
|
||||
: normalizeAmountFen(payload.requiredDepositAmount ?? payload.requiredDepositAmountYuan, 0)
|
||||
const fields = normalizeRequirementFieldsFromPayload(payload)
|
||||
const rule = await upsertWorkProductRule({
|
||||
ruleKey: normalizeSlugKey(
|
||||
payload.ruleKey || payload.rule_key || skuCode || productName,
|
||||
`rule-${Date.now()}`,
|
||||
),
|
||||
provider: String(payload.provider || '').trim(),
|
||||
platform: String(payload.platform || '').trim(),
|
||||
shopId: String(payload.shopId || payload.shop_id || '').trim(),
|
||||
skuCode,
|
||||
productName,
|
||||
matchType: normalizeMatchType(payload.matchType || payload.match_type),
|
||||
categoryId:
|
||||
normalizeOptionalId(payload.categoryId || payload.category_id) ||
|
||||
defaults.category?.id ||
|
||||
null,
|
||||
enabled: normalizeBoolean(payload.enabled, true),
|
||||
autoCreate: normalizeBoolean(payload.autoCreate ?? payload.auto_create, false),
|
||||
rewardAmount: finalRewardAmount,
|
||||
requiredDepositAmount,
|
||||
depositThresholdAmount,
|
||||
sharingEnabled,
|
||||
sharingTotalQuantity,
|
||||
sharingUnitReward,
|
||||
requirementJson: JSON.stringify({ fields }),
|
||||
sortOrder: normalizeInteger(payload.sortOrder, 100),
|
||||
now: nowIso(),
|
||||
})
|
||||
return { rule: mapWorkProductRule(rule) }
|
||||
}
|
||||
|
||||
export async function listAdminWorkerUsers(query: JsonObject = {}) {
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
const { items, total } = await listWorkerUsers({
|
||||
page,
|
||||
pageSize,
|
||||
status: String(query.status || '').trim(),
|
||||
keyword: String(query.keyword || '').trim(),
|
||||
})
|
||||
return {
|
||||
items: items.map(mapWorkerUser),
|
||||
pagination: { page, pageSize, total },
|
||||
}
|
||||
}
|
||||
|
||||
export async function reviewAdminWorkerUser(workerId: number | string, payload: JsonObject = {}) {
|
||||
const worker = await getRequiredWorker(workerId)
|
||||
const status = normalizeReviewStatus(payload.status)
|
||||
const requestedLevelId = normalizeOptionalId(payload.levelId)
|
||||
let levelId = worker.level_id || null
|
||||
if (requestedLevelId) {
|
||||
const level = await getWorkerLevelById(requestedLevelId)
|
||||
if (!level) {
|
||||
throw createHttpError('接单等级不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'worker_level_not_found',
|
||||
})
|
||||
}
|
||||
levelId = Number(level.id)
|
||||
}
|
||||
if (!levelId) {
|
||||
levelId = (await ensureWorkerPlatformDefaults()).level?.id || null
|
||||
}
|
||||
const now = nowIso()
|
||||
const updated = await updateWorkerUser(worker.id, {
|
||||
status,
|
||||
level_id: levelId,
|
||||
review_note: String(payload.reviewNote || payload.note || '').trim(),
|
||||
session_version: normalizeSessionVersion(worker.session_version) + 1,
|
||||
reviewed_at: now,
|
||||
updated_at: now,
|
||||
})
|
||||
return { worker: mapWorkerUser(updated || worker) }
|
||||
}
|
||||
|
||||
export async function creditAdminWorkerWallet(workerId: number | string, payload: JsonObject = {}) {
|
||||
await getRequiredWorker(workerId)
|
||||
const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0)
|
||||
if (amount <= 0) {
|
||||
throw createHttpError('充值金额必须大于 0', {
|
||||
statusCode: 400,
|
||||
errorCode: 'worker_credit_amount_invalid',
|
||||
})
|
||||
}
|
||||
const wallet = await addWorkerWalletCredit({
|
||||
workerId: Number(workerId),
|
||||
amount,
|
||||
note: String(payload.note || '后台人工充值').trim(),
|
||||
payloadJson: JSON.stringify({ source: 'admin_manual_credit' }),
|
||||
now: nowIso(),
|
||||
})
|
||||
return { wallet: mapWallet(wallet) }
|
||||
}
|
||||
|
||||
export async function getAdminWorkerFinanceConfig() {
|
||||
return getWorkerFinanceConfig()
|
||||
}
|
||||
|
||||
export async function saveAdminWorkerFinanceConfig(payload: JsonObject = {}) {
|
||||
const config = await saveWorkerFinanceConfig(payload)
|
||||
return config
|
||||
}
|
||||
|
||||
export async function listAdminWorkerFinanceRequests(query: JsonObject = {}) {
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
const status = normalizeFinanceRequestStatus(query.status)
|
||||
const requestType = normalizeFinanceRequestType(query.requestType)
|
||||
const keyword = String(query.keyword || '').trim()
|
||||
const { items, total } = await listWorkerFinanceRequests({
|
||||
page,
|
||||
pageSize,
|
||||
status,
|
||||
requestType,
|
||||
keyword,
|
||||
})
|
||||
return {
|
||||
items: items.map(mapFinanceRequest),
|
||||
pagination: { page, pageSize, total },
|
||||
}
|
||||
}
|
||||
|
||||
export async function reviewAdminWorkerFinanceRequest(
|
||||
requestId: number | string,
|
||||
payload: JsonObject = {},
|
||||
) {
|
||||
const current = await getWorkerFinanceRequestById(requestId)
|
||||
if (!current) {
|
||||
throw createHttpError('资金申请不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'worker_finance_request_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
const status = normalizeAdminFinanceReviewStatus(payload.status)
|
||||
const reviewedNote = String(payload.reviewedNote || payload.note || '').trim()
|
||||
const reviewed = await reviewWorkerFinanceRequest({
|
||||
requestId: Number(current.id),
|
||||
status,
|
||||
reviewedNote,
|
||||
now: nowIso(),
|
||||
})
|
||||
|
||||
if (reviewed.failureReason === 'request_not_pending') {
|
||||
throw createHttpError('该申请已处理,请刷新后重试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_finance_request_reviewed',
|
||||
})
|
||||
}
|
||||
|
||||
if (reviewed.failureReason === 'withdraw_insufficient') {
|
||||
throw createHttpError('打手余额不足,暂时无法通过该提现申请', {
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_finance_request_withdraw_insufficient',
|
||||
})
|
||||
}
|
||||
|
||||
if (!reviewed.request) {
|
||||
throw createHttpError('资金申请不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'worker_finance_request_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
request: mapFinanceRequest(reviewed.request),
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAdminWorkOrders(query: JsonObject = {}) {
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
const { items, total } = await listWorkOrders({
|
||||
page,
|
||||
pageSize,
|
||||
status: String(query.status || '').trim(),
|
||||
keyword: String(query.keyword || '').trim(),
|
||||
})
|
||||
const shares = await listWorkOrderSharesByOrderIds(items.map((item) => Number(item.id)))
|
||||
const sharesByOrderId = new Map<number, WorkOrderShareRow[]>()
|
||||
for (const share of shares) {
|
||||
const orderId = Number(share.work_order_id)
|
||||
const bucket = sharesByOrderId.get(orderId) || []
|
||||
bucket.push(share)
|
||||
sharesByOrderId.set(orderId, bucket)
|
||||
}
|
||||
return {
|
||||
items: items.map((item) => {
|
||||
const orderShares = sharesByOrderId.get(Number(item.id)) || []
|
||||
const joinedQuantity = orderShares
|
||||
.filter((share) => share.status !== 'cancelled')
|
||||
.reduce((sum, share) => sum + Number(share.quantity || 0), 0)
|
||||
const pendingSubmissionCount = orderShares.filter(
|
||||
(share) => share.status === 'joined',
|
||||
).length
|
||||
return {
|
||||
...mapWorkOrderAdmin(item),
|
||||
sharingProgress: {
|
||||
joinedQuantity,
|
||||
pendingSubmissionCount,
|
||||
},
|
||||
}
|
||||
}),
|
||||
pagination: { page, pageSize, total },
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAdminWorkOrderSharing(workOrderId: number | string) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
const shares = await listWorkOrderShares(workOrder.id)
|
||||
return {
|
||||
order: mapWorkOrderAdmin(workOrder),
|
||||
shares: shares.map(mapWorkOrderShare),
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAdminWorkOrderSharing(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (![WORK_ORDER_STATUS.PENDING_MATERIAL, WORK_ORDER_STATUS.UNASSIGNED, WORK_ORDER_STATUS.OPEN].includes(workOrder.status as never)) {
|
||||
throw createHttpError('只有待完善、未分配或待抢单的工单可以配置拼单', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_sharing_config_status_invalid',
|
||||
})
|
||||
}
|
||||
const enabled = normalizeBoolean(
|
||||
payload.enabled ?? payload.sharingEnabled ?? payload.sharing_enabled,
|
||||
false,
|
||||
)
|
||||
const unitReward = normalizeAmountFen(
|
||||
payload.unitReward ?? payload.sharingUnitReward ?? payload.sharingUnitRewardYuan,
|
||||
0,
|
||||
)
|
||||
const totalAmount = normalizeAmountFen(
|
||||
payload.totalAmount ?? payload.sharingTotalAmount ?? payload.sharingTotalAmountYuan,
|
||||
0,
|
||||
)
|
||||
const totalQuantity = resolveSharingQuantity(
|
||||
totalAmount,
|
||||
normalizePositiveInteger(
|
||||
payload.totalQuantity ?? payload.sharingTotalQuantity ?? payload.sharing_total_quantity,
|
||||
1,
|
||||
),
|
||||
unitReward,
|
||||
)
|
||||
const resolvedTotalAmount = totalAmount > 0 ? totalAmount : unitReward * totalQuantity
|
||||
const now = nowIso()
|
||||
const updated = await updateWorkOrder(workOrder.id, {
|
||||
sharing_enabled: enabled,
|
||||
sharing_total_quantity: totalQuantity,
|
||||
sharing_unit_reward: unitReward,
|
||||
reward_amount: enabled ? resolvedTotalAmount : workOrder.reward_amount,
|
||||
updated_at: now,
|
||||
})
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
eventType: 'sharing_config_updated',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: updated?.status || workOrder.status,
|
||||
payloadJson: JSON.stringify({
|
||||
enabled,
|
||||
totalQuantity,
|
||||
unitReward,
|
||||
totalAmount: resolvedTotalAmount,
|
||||
}),
|
||||
now,
|
||||
})
|
||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||
}
|
||||
|
||||
function resolveSharingQuantity(
|
||||
totalAmount: number,
|
||||
fallbackQuantity: number,
|
||||
unitReward: number,
|
||||
): number {
|
||||
if (totalAmount > 0 && unitReward > 0) {
|
||||
return Math.max(1, Math.round(totalAmount / unitReward))
|
||||
}
|
||||
return Math.max(1, fallbackQuantity)
|
||||
}
|
||||
|
||||
export async function submitAdminWorkOrderMaterial(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (
|
||||
![WORK_ORDER_STATUS.PENDING_MATERIAL, WORK_ORDER_STATUS.UNASSIGNED].includes(
|
||||
workOrder.status as never,
|
||||
)
|
||||
) {
|
||||
throw createHttpError('当前状态不能补充资料', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_material_status_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const fields = resolveRequirementFields(workOrder)
|
||||
const submittedFields = normalizeSubmittedFields(payload.fields || payload.material || payload)
|
||||
const complete = fields.every(
|
||||
(field) => !field.required || String(submittedFields[field.key] || '').trim(),
|
||||
)
|
||||
const now = nowIso()
|
||||
const material = {
|
||||
...safeParseJson(workOrder.material_json),
|
||||
collect: {
|
||||
fields: submittedFields,
|
||||
submittedAt: now,
|
||||
complete,
|
||||
source: 'admin',
|
||||
},
|
||||
}
|
||||
const nextStatus = complete ? WORK_ORDER_STATUS.UNASSIGNED : WORK_ORDER_STATUS.PENDING_MATERIAL
|
||||
const updated = await updateWorkOrder(workOrder.id, {
|
||||
status: nextStatus,
|
||||
material_json: material,
|
||||
updated_at: now,
|
||||
})
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
eventType: 'material_completed',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: nextStatus,
|
||||
payloadJson: JSON.stringify({ fields: submittedFields, complete }),
|
||||
now,
|
||||
})
|
||||
return { order: mapWorkOrderAdmin(updated || workOrder), complete }
|
||||
}
|
||||
|
||||
export async function syncAdminWorkerOrdersFromSource(payload: JsonObject = {}) {
|
||||
const order = await resolveSourceOrder(payload)
|
||||
const orderItems = await listOrderItemsByOrderId(order.id)
|
||||
const result = await syncWorkerOrdersForSourceOrder(order, orderItems, {
|
||||
source: 'admin_manual_sync',
|
||||
autoOnly: false,
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export async function createAdminMockWorkOrder(payload: JsonObject = {}) {
|
||||
const defaults = await ensureWorkerPlatformDefaults()
|
||||
const now = nowIso()
|
||||
const rewardAmount = normalizeAmountFen(payload.rewardAmount ?? payload.rewardAmountYuan, 2500)
|
||||
const depositThresholdAmount = normalizeAmountFen(
|
||||
payload.depositThresholdAmount ?? payload.depositThresholdAmountYuan,
|
||||
DEFAULT_DEPOSIT_THRESHOLD_AMOUNT,
|
||||
)
|
||||
const requiredDepositAmount =
|
||||
payload.requiredDepositAmount === undefined
|
||||
? rewardAmount > depositThresholdAmount
|
||||
? rewardAmount
|
||||
: 0
|
||||
: normalizeAmountFen(payload.requiredDepositAmount, 0)
|
||||
const materialComplete = payload.materialComplete === true || payload.materialComplete === 'true'
|
||||
const fields = normalizeRequirementFields(payload.fields)
|
||||
const platformOrderId = String(
|
||||
payload.platformOrderId || payload.orderNo || `MOCKWORK${Date.now()}`,
|
||||
).trim()
|
||||
const productName = String(payload.productName || '指挥官秘钥1个').trim()
|
||||
const material = materialComplete
|
||||
? {
|
||||
collect: {
|
||||
complete: true,
|
||||
submittedAt: now,
|
||||
fields: fields.reduce<Record<string, string>>((result, field) => {
|
||||
result[field.key] = String(field.mockValue || `${field.label}测试值`)
|
||||
return result
|
||||
}, {}),
|
||||
},
|
||||
}
|
||||
: {}
|
||||
const workOrder = await createWorkOrder({
|
||||
workOrderNo: randomId('WO'),
|
||||
platformOrderId,
|
||||
productName,
|
||||
categoryId: defaults.category?.id || null,
|
||||
status: materialComplete ? WORK_ORDER_STATUS.UNASSIGNED : WORK_ORDER_STATUS.PENDING_MATERIAL,
|
||||
rewardAmount,
|
||||
requiredDepositAmount,
|
||||
depositThresholdAmount,
|
||||
materialJson: JSON.stringify(material),
|
||||
requirementJson: JSON.stringify({ fields }),
|
||||
now,
|
||||
})
|
||||
if (!workOrder) {
|
||||
throw createHttpError('接单工单创建失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'work_order_create_failed',
|
||||
})
|
||||
}
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: 'dev_mock',
|
||||
eventType: 'mock_created',
|
||||
fromStatus: '',
|
||||
toStatus: workOrder.status,
|
||||
payloadJson: JSON.stringify({ platformOrderId, productName, rewardAmount }),
|
||||
now,
|
||||
})
|
||||
return { order: mapWorkOrderAdmin(workOrder) }
|
||||
}
|
||||
|
||||
export async function publishAdminWorkOrder(workOrderId: number | string) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (workOrder.status !== WORK_ORDER_STATUS.UNASSIGNED) {
|
||||
throw createHttpError('只有未分配订单可以发布到大厅', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_publish_status_invalid',
|
||||
})
|
||||
}
|
||||
const now = nowIso()
|
||||
const updated = await updateWorkOrder(workOrder.id, {
|
||||
status: WORK_ORDER_STATUS.OPEN,
|
||||
published_at: now,
|
||||
updated_at: now,
|
||||
})
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
eventType: 'published',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: WORK_ORDER_STATUS.OPEN,
|
||||
now,
|
||||
})
|
||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||
}
|
||||
|
||||
export async function unpublishAdminWorkOrder(workOrderId: number | string) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (workOrder.status !== WORK_ORDER_STATUS.OPEN || workOrder.assigned_worker_id) {
|
||||
throw createHttpError('只有待抢单且未被接走的订单可以下架', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_unpublish_status_invalid',
|
||||
})
|
||||
}
|
||||
const now = nowIso()
|
||||
const updated = await updateWorkOrder(workOrder.id, {
|
||||
status: WORK_ORDER_STATUS.UNASSIGNED,
|
||||
published_at: null,
|
||||
updated_at: now,
|
||||
})
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
eventType: 'unpublished',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: WORK_ORDER_STATUS.UNASSIGNED,
|
||||
now,
|
||||
})
|
||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||
}
|
||||
|
||||
export async function markAdminWorkOrderProblem(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (
|
||||
![WORK_ORDER_STATUS.IN_PROGRESS, WORK_ORDER_STATUS.PENDING_ACCEPTANCE].includes(
|
||||
workOrder.status as never,
|
||||
)
|
||||
) {
|
||||
throw createHttpError('当前状态不能标记问题单', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_problem_status_invalid',
|
||||
})
|
||||
}
|
||||
const now = nowIso()
|
||||
const note = String(payload.note || payload.problemNote || '').trim()
|
||||
const updated = await updateWorkOrder(workOrder.id, {
|
||||
status: WORK_ORDER_STATUS.PROBLEM,
|
||||
problem_note: note,
|
||||
updated_at: now,
|
||||
})
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
eventType: 'marked_problem',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: WORK_ORDER_STATUS.PROBLEM,
|
||||
payloadJson: JSON.stringify({ note }),
|
||||
now,
|
||||
})
|
||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||
}
|
||||
|
||||
export async function resolveAdminProblemWorkOrder(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (workOrder.status !== WORK_ORDER_STATUS.PROBLEM) {
|
||||
throw createHttpError('只有问题单可以执行处置', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_problem_resolution_status_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const action = normalizeProblemResolutionAction(payload.action)
|
||||
const note = String(payload.note || '').trim()
|
||||
if (!note) {
|
||||
throw createHttpError('请填写处置备注', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_order_problem_resolution_note_required',
|
||||
})
|
||||
}
|
||||
|
||||
const updated = await resolveProblemWorkOrder({
|
||||
workOrderId: workOrder.id,
|
||||
action,
|
||||
note,
|
||||
now: nowIso(),
|
||||
})
|
||||
if (!updated) {
|
||||
throw createHttpError('问题单处置失败,订单状态可能已变化', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_problem_resolution_conflict',
|
||||
})
|
||||
}
|
||||
return { order: mapWorkOrderAdmin(updated) }
|
||||
}
|
||||
|
||||
export async function acceptAdminWorkOrder(workOrderId: number | string) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (workOrder.status !== WORK_ORDER_STATUS.PENDING_ACCEPTANCE) {
|
||||
throw createHttpError('只有待验收订单可以通过验收', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_accept_status_invalid',
|
||||
})
|
||||
}
|
||||
const now = nowIso()
|
||||
const { order: updated, failureReason } = await acceptWorkOrderAndSettle({
|
||||
workOrderId: workOrder.id,
|
||||
now,
|
||||
})
|
||||
if (failureReason === 'sharing_pending_submissions') {
|
||||
throw createHttpError('还有拼单打手未提交验收资料,暂不能统一验收', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_sharing_pending_submissions',
|
||||
})
|
||||
}
|
||||
if (!updated) {
|
||||
throw createHttpError('验收失败,订单状态可能已变化', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_accept_conflict',
|
||||
})
|
||||
}
|
||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||
}
|
||||
|
||||
export async function getAdminWorkerPlatformSummary() {
|
||||
const [pendingWorkers, pendingMaterial, openOrders, inProgressOrders] = await Promise.all([
|
||||
listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }),
|
||||
listWorkOrders({ page: 1, pageSize: 1, status: WORK_ORDER_STATUS.PENDING_MATERIAL }),
|
||||
listWorkOrders({ page: 1, pageSize: 1, status: WORK_ORDER_STATUS.OPEN }),
|
||||
listWorkOrders({ page: 1, pageSize: 1, status: WORK_ORDER_STATUS.IN_PROGRESS }),
|
||||
])
|
||||
return {
|
||||
pendingWorkerCount: pendingWorkers.total,
|
||||
pendingMaterialCount: pendingMaterial.total,
|
||||
openOrderCount: openOrders.total,
|
||||
inProgressOrderCount: inProgressOrders.total,
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncWorkerOrdersForSourceOrder(
|
||||
order: OrderRow,
|
||||
orderItems: OrderItemRow[],
|
||||
options: { source?: string; autoOnly?: boolean } = {},
|
||||
) {
|
||||
const rules = await listWorkProductRules({ enabled: true })
|
||||
const created: WorkOrderRow[] = []
|
||||
const skipped: Array<{ orderItemId: number; reason: string }> = []
|
||||
|
||||
for (const item of orderItems) {
|
||||
const rule = resolveMatchingProductRule(order, item, rules)
|
||||
if (!rule) {
|
||||
skipped.push({ orderItemId: Number(item.id), reason: 'rule_not_matched' })
|
||||
continue
|
||||
}
|
||||
if (options.autoOnly && !rule.auto_create) {
|
||||
skipped.push({ orderItemId: Number(item.id), reason: 'auto_create_disabled' })
|
||||
continue
|
||||
}
|
||||
if (await getWorkOrderByOrderItemId(item.id)) {
|
||||
skipped.push({ orderItemId: Number(item.id), reason: 'already_exists' })
|
||||
continue
|
||||
}
|
||||
|
||||
const fields = normalizeRequirementFields(safeParseJson(rule.requirement_json).fields)
|
||||
const rewardAmount = Number(rule.reward_amount || 0)
|
||||
const depositThresholdAmount = Number(
|
||||
rule.deposit_threshold_amount || DEFAULT_DEPOSIT_THRESHOLD_AMOUNT,
|
||||
)
|
||||
const requiredDepositAmount =
|
||||
Number(rule.required_deposit_amount || 0) ||
|
||||
(rewardAmount > depositThresholdAmount ? rewardAmount : 0)
|
||||
const now = nowIso()
|
||||
const materialComplete = fields.every((field) => !field.required)
|
||||
const workOrder = await createWorkOrder({
|
||||
workOrderNo: randomId('WO'),
|
||||
orderId: Number(order.id),
|
||||
orderItemId: Number(item.id),
|
||||
taskId: null,
|
||||
platformOrderId: order.platform_order_id,
|
||||
productName: item.sku_name || item.sku_code,
|
||||
categoryId: rule.category_id,
|
||||
status: materialComplete ? WORK_ORDER_STATUS.UNASSIGNED : WORK_ORDER_STATUS.PENDING_MATERIAL,
|
||||
rewardAmount,
|
||||
requiredDepositAmount,
|
||||
depositThresholdAmount,
|
||||
sharingEnabled: rule.sharing_enabled === true,
|
||||
sharingTotalQuantity: Number(rule.sharing_total_quantity || 1),
|
||||
sharingUnitReward: Number(rule.sharing_unit_reward || 0),
|
||||
materialJson: JSON.stringify({
|
||||
source: {
|
||||
orderId: Number(order.id),
|
||||
orderItemId: Number(item.id),
|
||||
provider: order.provider,
|
||||
platform: order.platform,
|
||||
shopId: order.shop_id,
|
||||
skuCode: item.sku_code,
|
||||
skuName: item.sku_name,
|
||||
quantity: Number(item.quantity || 1),
|
||||
syncSource: options.source || 'source_order',
|
||||
},
|
||||
}),
|
||||
requirementJson: JSON.stringify({ fields }),
|
||||
now,
|
||||
})
|
||||
|
||||
if (workOrder) {
|
||||
created.push(workOrder)
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'system',
|
||||
actorId: options.source || 'source_order',
|
||||
eventType: 'source_synced',
|
||||
fromStatus: '',
|
||||
toStatus: workOrder.status,
|
||||
payloadJson: JSON.stringify({
|
||||
ruleKey: rule.rule_key,
|
||||
orderId: order.id,
|
||||
orderItemId: item.id,
|
||||
}),
|
||||
now,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
created: created.map(mapWorkOrderAdmin),
|
||||
skipped,
|
||||
createdCount: created.length,
|
||||
skippedCount: skipped.length,
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveSourceOrder(payload: JsonObject): Promise<OrderRow> {
|
||||
const orderId = normalizeOptionalId(payload.orderId || payload.sourceOrderId)
|
||||
if (orderId) {
|
||||
const order = await getOrderById(orderId)
|
||||
if (order) {
|
||||
return order
|
||||
}
|
||||
throw createHttpError('源订单不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'source_order_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
const platformOrderId = String(payload.platformOrderId || payload.orderNo || '').trim()
|
||||
if (!platformOrderId) {
|
||||
throw createHttpError('请填写源订单号', {
|
||||
statusCode: 400,
|
||||
errorCode: 'source_order_no_required',
|
||||
})
|
||||
}
|
||||
|
||||
const order = await findLatestOrderByAnyPlatformOrderId(platformOrderId)
|
||||
if (!order) {
|
||||
throw createHttpError('未找到源订单,请确认订单号是否正确', {
|
||||
statusCode: 404,
|
||||
errorCode: 'source_order_not_found',
|
||||
})
|
||||
}
|
||||
return order
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './mappers.js'
|
||||
export * from './worker-service.js'
|
||||
export * from './admin-service.js'
|
||||
@@ -0,0 +1,870 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||
import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js'
|
||||
import { findLatestOrderByAnyPlatformOrderId, getOrderById } from '../../repositories/order-repo.js'
|
||||
import {
|
||||
acceptWorkOrderAndSettle,
|
||||
addWorkerWalletCredit,
|
||||
countWorkerAcceptedOrders,
|
||||
countWorkerActiveOrders,
|
||||
countWorkCategoryUsages,
|
||||
countWorkerLevelUsages,
|
||||
countWorkOrderPendingSharingSubmissions,
|
||||
cancelWorkerWorkOrder,
|
||||
countWorkerCancellationsSince,
|
||||
createWorkOrder,
|
||||
createWorkOrderEvent,
|
||||
createWorkerFinanceRequest,
|
||||
createWorkerUser,
|
||||
deleteWorkCategory,
|
||||
deleteWorkerLevel,
|
||||
getWorkerFinanceRequestById,
|
||||
getWorkerFinanceRequestSummary,
|
||||
getWorkCategoryById,
|
||||
getWorkCategoryByKey,
|
||||
getWorkerLevelById,
|
||||
getWorkerLevelByKey,
|
||||
getWorkerUserByInviteCode,
|
||||
getWorkerUserById,
|
||||
getWorkerUserByUsername,
|
||||
getWorkOrderById,
|
||||
getWorkOrderByOrderItemId,
|
||||
getWorkOrderShare,
|
||||
grabWorkOrder,
|
||||
incrementWorkerSessionVersion,
|
||||
joinWorkOrderShare,
|
||||
listAllWorkCategories,
|
||||
listPendingMaterialWorkOrdersByPlatformOrderId,
|
||||
listWorkerFinanceRequests,
|
||||
listWorkerSharesByWorker,
|
||||
listWorkerWalletLedgers,
|
||||
listWorkCategories,
|
||||
listWorkOrders,
|
||||
listWorkProductRules,
|
||||
listWorkerLevels,
|
||||
listWorkerUsers,
|
||||
listWorkOrderShares,
|
||||
listWorkOrderSharesByOrderIds,
|
||||
resolveProblemWorkOrder,
|
||||
reviewWorkerFinanceRequest,
|
||||
submitWorkOrderShareAcceptance,
|
||||
updateWorkOrder,
|
||||
updateWorkerPassword,
|
||||
updateWorkerUser,
|
||||
upsertWorkCategory,
|
||||
upsertWorkProductRule,
|
||||
upsertWorkerLevel,
|
||||
type WorkCategoryRow,
|
||||
type WorkerFinanceRequestRow,
|
||||
type WorkOrderRow,
|
||||
type WorkProductRuleRow,
|
||||
type WorkerLevelRow,
|
||||
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 { randomId } from '../../utils/random.js'
|
||||
import { nowIso, addHours } from '../../utils/time.js'
|
||||
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import {
|
||||
normalizeStoredFileUrl,
|
||||
refreshStoredFileUrl,
|
||||
refreshUploadedFileUrls,
|
||||
} from '../file-storage/file-storage-service.js'
|
||||
import {
|
||||
getWorkerFinanceConfig,
|
||||
saveWorkerFinanceConfig,
|
||||
} from './worker-finance-config-service.js'
|
||||
|
||||
|
||||
|
||||
export const DEFAULT_LEVEL_KEY = 'vip1'
|
||||
export const DEFAULT_LEVEL_NAME = 'VIP1'
|
||||
export const DEFAULT_CATEGORY_KEY = 'default'
|
||||
export const DEFAULT_DEPOSIT_THRESHOLD_AMOUNT = 20_000
|
||||
export const INVITE_CODE_LENGTH = 8
|
||||
export const INVITE_CODE_RETRY_LIMIT = 5
|
||||
export const WORKER_CANCEL_LIMIT_PER_WINDOW = 5
|
||||
export const WORKER_CANCEL_LIMIT_WINDOW_MS = 30 * 24 * 60 * 60 * 1000
|
||||
export function createWorkerSession(worker: WorkerUserRow) {
|
||||
const issuedAt = nowIso()
|
||||
const expiresAt = addHours(issuedAt, Number(runtimeConfig.admin?.sessionTtlHours || 12))
|
||||
const payload = {
|
||||
typ: 'worker',
|
||||
sid: crypto.randomBytes(12).toString('hex'),
|
||||
uid: Number(worker.id),
|
||||
usr: worker.username,
|
||||
status: worker.status,
|
||||
ver: normalizeSessionVersion(worker.session_version),
|
||||
iat: issuedAt,
|
||||
exp: expiresAt,
|
||||
}
|
||||
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
||||
return {
|
||||
token: `${encodedPayload}.${signWorkerPayload(encodedPayload)}`,
|
||||
expiresAt,
|
||||
worker: mapWorkerUser(worker),
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureWorkerAuthConfigured() {
|
||||
if (String(runtimeConfig.admin?.sessionSecret || '').trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
throw createHttpError('接单登录态签名密钥未配置,请设置 ADMIN_SESSION_SECRET', {
|
||||
statusCode: 503,
|
||||
errorCode: 'worker_auth_not_configured',
|
||||
})
|
||||
}
|
||||
|
||||
export function hashWorkerPassword(password: string): string {
|
||||
const salt = crypto.randomBytes(16).toString('hex')
|
||||
const derived = crypto.scryptSync(password, salt, 64).toString('hex')
|
||||
return `scrypt$${salt}$${derived}`
|
||||
}
|
||||
|
||||
export function verifyWorkerPassword(password: string, storedHash: string): boolean {
|
||||
const [algorithm, salt, expectedHash] = String(storedHash || '').split('$')
|
||||
if (algorithm !== 'scrypt' || !salt || !expectedHash) return false
|
||||
return safeCompare(crypto.scryptSync(password, salt, 64).toString('hex'), expectedHash)
|
||||
}
|
||||
|
||||
export function signWorkerPayload(encodedPayload: string): string {
|
||||
return crypto
|
||||
.createHmac('sha256', String(runtimeConfig.admin?.sessionSecret || ''))
|
||||
.update(`worker:${encodedPayload}`)
|
||||
.digest('base64url')
|
||||
}
|
||||
|
||||
export function safeCompare(input: unknown, expected: unknown): boolean {
|
||||
const left = Buffer.from(String(input || ''), 'utf8')
|
||||
const right = Buffer.from(String(expected || ''), 'utf8')
|
||||
if (left.length !== right.length) return false
|
||||
return crypto.timingSafeEqual(left, right)
|
||||
}
|
||||
|
||||
export function resolveWorkerPermissions(worker: WorkerUserRow) {
|
||||
const permission = safeParseJson(worker.level_permission_json)
|
||||
return {
|
||||
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
|
||||
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1),
|
||||
upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0),
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveFreezeDepositAmount(
|
||||
workOrder: WorkOrderRow,
|
||||
permissions: { depositFreeAmount: number },
|
||||
) {
|
||||
return Math.max(
|
||||
0,
|
||||
Number(workOrder.required_deposit_amount || 0) - Number(permissions.depositFreeAmount || 0),
|
||||
)
|
||||
}
|
||||
|
||||
export function mapWorkerLevel(level: WorkerLevelRow | null | undefined) {
|
||||
if (!level) return null
|
||||
const permission = safeParseJson(level.permission_json)
|
||||
return {
|
||||
levelId: Number(level.id),
|
||||
levelKey: level.level_key,
|
||||
name: level.name,
|
||||
sortOrder: Number(level.sort_order || 100),
|
||||
status: level.status,
|
||||
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
|
||||
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1),
|
||||
upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0),
|
||||
createdAt: level.created_at,
|
||||
updatedAt: level.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapWorkCategory(category: WorkCategoryRow | null | undefined) {
|
||||
if (!category) return null
|
||||
return {
|
||||
categoryId: Number(category.id),
|
||||
categoryKey: category.category_key,
|
||||
name: category.name,
|
||||
sortOrder: Number(category.sort_order || 100),
|
||||
status: category.status,
|
||||
createdAt: category.created_at,
|
||||
updatedAt: category.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapWorkProductRule(rule: WorkProductRuleRow | null | undefined) {
|
||||
if (!rule) return null
|
||||
const requirement = safeParseJson(rule.requirement_json)
|
||||
return {
|
||||
ruleId: Number(rule.id),
|
||||
ruleKey: rule.rule_key,
|
||||
provider: rule.provider || '',
|
||||
platform: rule.platform || '',
|
||||
shopId: rule.shop_id || '',
|
||||
skuCode: rule.sku_code || '',
|
||||
productName: rule.product_name || '',
|
||||
matchType: rule.match_type || 'contains',
|
||||
categoryId: rule.category_id ? Number(rule.category_id) : null,
|
||||
categoryName: rule.category_name || '',
|
||||
enabled: Boolean(rule.enabled),
|
||||
autoCreate: Boolean(rule.auto_create),
|
||||
rewardAmount: Number(rule.reward_amount || 0),
|
||||
requiredDepositAmount: Number(rule.required_deposit_amount || 0),
|
||||
depositThresholdAmount: Number(
|
||||
rule.deposit_threshold_amount || DEFAULT_DEPOSIT_THRESHOLD_AMOUNT,
|
||||
),
|
||||
sharing: {
|
||||
enabled: rule.sharing_enabled === true,
|
||||
totalQuantity: Number(rule.sharing_total_quantity || 1),
|
||||
unitReward: Number(rule.sharing_unit_reward || 0),
|
||||
},
|
||||
requirement: {
|
||||
fields: normalizeRequirementFields(requirement.fields),
|
||||
},
|
||||
sortOrder: Number(rule.sort_order || 100),
|
||||
createdAt: rule.created_at,
|
||||
updatedAt: rule.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapWorkerUser(worker: WorkerUserRow) {
|
||||
return {
|
||||
workerId: Number(worker.id),
|
||||
username: worker.username,
|
||||
displayName: worker.display_name || worker.username,
|
||||
phone: worker.phone,
|
||||
status: worker.status,
|
||||
reviewNote: worker.review_note || '',
|
||||
inviteCode: worker.invite_code || '',
|
||||
inviter: worker.inviter_id
|
||||
? {
|
||||
workerId: Number(worker.inviter_id),
|
||||
username: worker.inviter_username || '',
|
||||
displayName: worker.inviter_display_name || worker.inviter_username || '',
|
||||
}
|
||||
: null,
|
||||
level: worker.level_id
|
||||
? {
|
||||
levelId: Number(worker.level_id),
|
||||
levelKey: worker.level_key || '',
|
||||
name: worker.level_name || '',
|
||||
permissions: resolveWorkerPermissions(worker),
|
||||
}
|
||||
: null,
|
||||
wallet: {
|
||||
availableAmount: Number(worker.available_amount || 0),
|
||||
frozenDepositAmount: Number(worker.frozen_deposit_amount || 0),
|
||||
totalCreditedAmount: Number(worker.total_credited_amount || 0),
|
||||
totalSettledAmount: Number(worker.total_settled_amount || 0),
|
||||
},
|
||||
createdAt: worker.created_at,
|
||||
updatedAt: worker.updated_at,
|
||||
reviewedAt: worker.reviewed_at,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapWalletLedger(ledger: WorkerWalletLedgerRow | null | undefined) {
|
||||
if (!ledger) return null
|
||||
return {
|
||||
ledgerId: Number(ledger.id),
|
||||
ledgerType: ledger.ledger_type,
|
||||
amount: Number(ledger.amount || 0),
|
||||
balanceAfter: Number(ledger.balance_after || 0),
|
||||
frozenAfter: Number(ledger.frozen_after || 0),
|
||||
relatedWorkOrderId: ledger.related_work_order_id
|
||||
? Number(ledger.related_work_order_id)
|
||||
: null,
|
||||
auditStatus: ledger.audit_status || '',
|
||||
note: ledger.note || '',
|
||||
payload: safeParseJson(ledger.payload_json),
|
||||
createdAt: ledger.created_at,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapFinanceRequest(request: WorkerFinanceRequestRow | null | undefined) {
|
||||
if (!request) return null
|
||||
return {
|
||||
requestId: Number(request.id),
|
||||
requestType: request.request_type || '',
|
||||
status: request.status || '',
|
||||
amount: Number(request.amount || 0),
|
||||
accountChannel: request.account_channel || '',
|
||||
accountName: request.account_name || '',
|
||||
accountNo: request.account_no || '',
|
||||
note: request.note || '',
|
||||
reviewedNote: request.reviewed_note || '',
|
||||
payload: safeParseJson(request.payload_json),
|
||||
createdAt: request.created_at,
|
||||
updatedAt: request.updated_at,
|
||||
reviewedAt: request.reviewed_at,
|
||||
worker: request.worker_id
|
||||
? {
|
||||
workerId: Number(request.worker_id),
|
||||
username: request.worker_username || '',
|
||||
displayName: request.worker_display_name || request.worker_username || '',
|
||||
phone: request.worker_phone || '',
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapWorkOrderAdmin(workOrder: WorkOrderRow) {
|
||||
return {
|
||||
workOrderId: Number(workOrder.id),
|
||||
orderId: workOrder.order_id ? Number(workOrder.order_id) : null,
|
||||
orderItemId: workOrder.order_item_id ? Number(workOrder.order_item_id) : null,
|
||||
workOrderNo: workOrder.work_order_no,
|
||||
platformOrderId: workOrder.platform_order_id,
|
||||
productName: workOrder.product_name,
|
||||
categoryName: workOrder.category_name || '',
|
||||
status: workOrder.status,
|
||||
rewardAmount: Number(workOrder.reward_amount || 0),
|
||||
requiredDepositAmount: Number(workOrder.required_deposit_amount || 0),
|
||||
sharing: {
|
||||
enabled: workOrder.sharing_enabled === true,
|
||||
totalQuantity: Number(workOrder.sharing_total_quantity || 1),
|
||||
unitReward: Number(workOrder.sharing_unit_reward || 0),
|
||||
},
|
||||
depositThresholdAmount: Number(
|
||||
workOrder.deposit_threshold_amount || DEFAULT_DEPOSIT_THRESHOLD_AMOUNT,
|
||||
),
|
||||
material: safeParseJson(workOrder.material_json),
|
||||
requirement: safeParseJson(workOrder.requirement_json),
|
||||
acceptance: mapAcceptanceForResponse(workOrder.acceptance_json),
|
||||
problemNote: workOrder.problem_note || '',
|
||||
worker: workOrder.assigned_worker_id
|
||||
? {
|
||||
workerId: Number(workOrder.assigned_worker_id),
|
||||
username: workOrder.worker_username || '',
|
||||
displayName: workOrder.worker_display_name || '',
|
||||
}
|
||||
: null,
|
||||
publishedAt: workOrder.published_at,
|
||||
assignedAt: workOrder.assigned_at,
|
||||
submittedAt: workOrder.submitted_at,
|
||||
acceptedAt: workOrder.accepted_at,
|
||||
createdAt: workOrder.created_at,
|
||||
updatedAt: workOrder.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapWorkOrderForWorker(
|
||||
workOrder: WorkOrderRow,
|
||||
permissions: { depositFreeAmount: number },
|
||||
myShare?: WorkOrderShareRow | null,
|
||||
) {
|
||||
const mapped = mapWorkOrderAdmin(workOrder)
|
||||
const {
|
||||
requiredDepositAmount,
|
||||
depositThresholdAmount,
|
||||
worker: _worker,
|
||||
orderId: _orderId,
|
||||
orderItemId: _orderItemId,
|
||||
...visibleOrder
|
||||
} = mapped
|
||||
return {
|
||||
...visibleOrder,
|
||||
freezeDepositAmount: resolveFreezeDepositAmount(workOrder, permissions),
|
||||
myShare: myShare ? mapWorkOrderShare(myShare) : null,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapWorkOrderShare(share: WorkOrderShareRow) {
|
||||
return {
|
||||
shareId: Number(share.id),
|
||||
workOrderId: Number(share.work_order_id),
|
||||
workerId: Number(share.worker_id),
|
||||
quantity: Number(share.quantity || 0),
|
||||
unitReward: Number(share.unit_reward || 0),
|
||||
shareReward: Number(share.share_reward || 0),
|
||||
shareDeposit: Number(share.share_deposit || 0),
|
||||
status: share.status,
|
||||
acceptance: safeParseJson(share.acceptance_json),
|
||||
submittedAt: share.submitted_at,
|
||||
acceptedAt: share.accepted_at,
|
||||
createdAt: share.created_at,
|
||||
updatedAt: share.updated_at,
|
||||
worker: share.worker_id
|
||||
? {
|
||||
workerId: Number(share.worker_id),
|
||||
username: share.worker_username || '',
|
||||
displayName: share.worker_display_name || share.worker_username || '',
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapWorkOrderPublic(workOrder: WorkOrderRow) {
|
||||
return {
|
||||
workOrderId: Number(workOrder.id),
|
||||
workOrderNo: workOrder.work_order_no,
|
||||
platformOrderId: workOrder.platform_order_id,
|
||||
productName: workOrder.product_name,
|
||||
status: workOrder.status,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapCollectLookupOrder(workOrder: WorkOrderRow) {
|
||||
return {
|
||||
...mapWorkOrderPublic(workOrder),
|
||||
fields: resolveRequirementFields(workOrder),
|
||||
values: resolveCollectSubmittedFieldValues(workOrder),
|
||||
}
|
||||
}
|
||||
|
||||
export function mapWallet(wallet: Awaited<ReturnType<typeof addWorkerWalletCredit>>) {
|
||||
return {
|
||||
workerId: Number(wallet?.worker_id || 0),
|
||||
availableAmount: Number(wallet?.available_amount || 0),
|
||||
frozenDepositAmount: Number(wallet?.frozen_deposit_amount || 0),
|
||||
totalCreditedAmount: Number(wallet?.total_credited_amount || 0),
|
||||
totalSettledAmount: Number(wallet?.total_settled_amount || 0),
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveRequirementFields(workOrder: WorkOrderRow) {
|
||||
return normalizeRequirementFields(safeParseJson(workOrder.requirement_json).fields)
|
||||
}
|
||||
|
||||
export function normalizeRequirementFields(value: unknown) {
|
||||
const rawFields = Array.isArray(value) ? value : []
|
||||
const fields = rawFields
|
||||
.map((item) => {
|
||||
const source =
|
||||
item && typeof item === 'object' && !Array.isArray(item) ? (item as JsonObject) : {}
|
||||
const key = String(source.key || '').trim()
|
||||
if (!key) return null
|
||||
return {
|
||||
key,
|
||||
label: String(source.label || key).trim(),
|
||||
required: source.required !== false,
|
||||
mockValue: String(source.mockValue || '').trim(),
|
||||
}
|
||||
})
|
||||
.filter((item): item is { key: string; label: string; required: boolean; mockValue: string } =>
|
||||
Boolean(item),
|
||||
)
|
||||
|
||||
return fields.length > 0
|
||||
? fields
|
||||
: [
|
||||
{ key: 'gameAccount', label: '游戏账号', required: true, mockValue: 'test_account' },
|
||||
{ key: 'serverName', label: '区服', required: true, mockValue: '安卓一区' },
|
||||
{ key: 'roleName', label: '角色名', required: true, mockValue: '测试角色' },
|
||||
]
|
||||
}
|
||||
|
||||
export function normalizeRequirementFieldsFromPayload(payload: JsonObject) {
|
||||
const directFields = Array.isArray(payload.fields) ? payload.fields : null
|
||||
const requirement =
|
||||
payload.requirement &&
|
||||
typeof payload.requirement === 'object' &&
|
||||
!Array.isArray(payload.requirement)
|
||||
? (payload.requirement as JsonObject)
|
||||
: {}
|
||||
if (directFields) {
|
||||
return normalizeRequirementFields(directFields)
|
||||
}
|
||||
if (Array.isArray(requirement.fields)) {
|
||||
return normalizeRequirementFields(requirement.fields)
|
||||
}
|
||||
|
||||
const textFields = normalizeRequirementFieldsText(
|
||||
payload.fieldsText || payload.requiredFieldsText,
|
||||
)
|
||||
return textFields.length > 0 ? textFields : normalizeRequirementFields([])
|
||||
}
|
||||
|
||||
export function normalizeRequirementFieldsText(value: unknown) {
|
||||
return String(value || '')
|
||||
.split(/\r?\n/)
|
||||
.map((line, index) => {
|
||||
const text = line.trim()
|
||||
if (!text) return null
|
||||
const delimiter = text.includes(':') ? ':' : ':'
|
||||
if (text.includes(delimiter)) {
|
||||
const [rawKey, ...labelParts] = text.split(delimiter)
|
||||
const key = String(rawKey || '').trim()
|
||||
if (!key) return null
|
||||
return {
|
||||
key,
|
||||
label: labelParts.join(delimiter).trim() || key,
|
||||
required: true,
|
||||
mockValue: '',
|
||||
}
|
||||
}
|
||||
return {
|
||||
key: `field${index + 1}`,
|
||||
label: text,
|
||||
required: true,
|
||||
mockValue: '',
|
||||
}
|
||||
})
|
||||
.filter((item): item is { key: string; label: string; required: boolean; mockValue: string } =>
|
||||
Boolean(item),
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveMatchingProductRule(
|
||||
order: OrderRow,
|
||||
item: OrderItemRow,
|
||||
rules: WorkProductRuleRow[],
|
||||
) {
|
||||
return (
|
||||
rules.find((rule) => {
|
||||
if (!matchesOptionalText(rule.provider, order.provider)) return false
|
||||
if (!matchesOptionalText(rule.platform, order.platform)) return false
|
||||
if (!matchesOptionalText(rule.shop_id, order.shop_id)) return false
|
||||
|
||||
const skuCode = String(item.sku_code || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
const skuName = String(item.sku_name || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
const ruleSkuCode = String(rule.sku_code || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
const ruleProductName = String(rule.product_name || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
|
||||
if (ruleSkuCode && ruleSkuCode !== skuCode) {
|
||||
return false
|
||||
}
|
||||
if (!ruleProductName) {
|
||||
return Boolean(ruleSkuCode)
|
||||
}
|
||||
if (rule.match_type === 'exact') {
|
||||
return ruleProductName === skuName || ruleProductName === skuCode
|
||||
}
|
||||
return skuName.includes(ruleProductName) || skuCode.includes(ruleProductName)
|
||||
}) || null
|
||||
)
|
||||
}
|
||||
|
||||
export function matchesOptionalText(ruleValue: unknown, sourceValue: unknown) {
|
||||
const ruleText = String(ruleValue || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (!ruleText) {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
ruleText ===
|
||||
String(sourceValue || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeSubmittedFields(value: unknown) {
|
||||
const source =
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
|
||||
return Object.fromEntries(
|
||||
Object.entries(source)
|
||||
.filter(([key]) => !['orderNo', 'platformOrderId', 'fields', 'material'].includes(key))
|
||||
.map(([key, currentValue]) => [key, String(currentValue || '').trim()]),
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeStringArray(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.map((item) => String(item || '').trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
export function normalizeUploadedFiles(value: unknown) {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value
|
||||
.map((item) => {
|
||||
const source =
|
||||
item && typeof item === 'object' && !Array.isArray(item) ? (item as JsonObject) : {}
|
||||
const objectKey = String(source.objectKey || source.object_key || '').trim()
|
||||
const url = String(source.url || '').trim()
|
||||
if (!objectKey && !url) return null
|
||||
return {
|
||||
objectKey,
|
||||
url: normalizeStoredFileUrl(url, objectKey),
|
||||
thumbnailUrl: normalizeStoredFileUrl(
|
||||
source.thumbnailUrl || source.thumbnail_url || url,
|
||||
objectKey,
|
||||
),
|
||||
mediumUrl: normalizeStoredFileUrl(source.mediumUrl || source.medium_url || url, objectKey),
|
||||
filename: String(source.filename || '').trim(),
|
||||
contentType: String(source.contentType || source.content_type || '').trim(),
|
||||
size: normalizeInteger(source.size, 0),
|
||||
}
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
item,
|
||||
): item is {
|
||||
objectKey: string
|
||||
url: string
|
||||
thumbnailUrl: string
|
||||
mediumUrl: string
|
||||
filename: string
|
||||
contentType: string
|
||||
size: number
|
||||
} => Boolean(item),
|
||||
)
|
||||
}
|
||||
|
||||
export function mapAcceptanceForResponse(value: unknown) {
|
||||
const acceptance = safeParseJson(value)
|
||||
const files = normalizeUploadedFiles(acceptance.files).map((file) =>
|
||||
refreshUploadedFileUrls(file),
|
||||
)
|
||||
const imageUrls = normalizeStringArray(acceptance.imageUrls).map((url) =>
|
||||
refreshStoredFileUrl(url),
|
||||
)
|
||||
return {
|
||||
...acceptance,
|
||||
files,
|
||||
imageUrls,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveCollectSubmittedFieldValues(workOrder: WorkOrderRow) {
|
||||
const material = safeParseJson(workOrder.material_json)
|
||||
const collect =
|
||||
material.collect && typeof material.collect === 'object' && !Array.isArray(material.collect)
|
||||
? safeParseJson(material.collect)
|
||||
: {}
|
||||
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 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_deduction',
|
||||
'reward_settlement',
|
||||
].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', 'bank'].includes(channel)) {
|
||||
return channel
|
||||
}
|
||||
throw createHttpError('提现方式不正确', {
|
||||
statusCode: 400,
|
||||
errorCode: 'worker_withdraw_channel_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
export function normalizeMatchType(value: unknown) {
|
||||
const matchType = String(value || '').trim()
|
||||
if (matchType === 'exact') return 'exact'
|
||||
return 'contains'
|
||||
}
|
||||
|
||||
export function normalizeBoolean(value: unknown, fallback: boolean) {
|
||||
if (typeof value === 'boolean') return value
|
||||
const text = String(value ?? '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (!text) return fallback
|
||||
if (['true', '1', 'yes', 'enabled', 'active', 'on'].includes(text)) return true
|
||||
if (['false', '0', 'no', 'disabled', 'off'].includes(text)) return false
|
||||
return fallback
|
||||
}
|
||||
|
||||
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 (password.length < 6) {
|
||||
throw createHttpError('接单密码至少 6 位', {
|
||||
statusCode: 400,
|
||||
errorCode: 'worker_password_invalid',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeSessionVersion(value: unknown): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1
|
||||
}
|
||||
|
||||
export function normalizeOptionalId(value: unknown): number | null {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : null
|
||||
}
|
||||
|
||||
export function normalizeInteger(value: unknown, fallback: number): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) ? parsed : fallback
|
||||
}
|
||||
|
||||
export function normalizePositiveInteger(value: unknown, fallback: number): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
||||
}
|
||||
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
export const IGNORABLE_WORKER_AUTH_ERROR_CODES = new Set([
|
||||
'worker_auth_required',
|
||||
'worker_auth_invalid',
|
||||
'worker_auth_expired',
|
||||
'worker_auth_user_invalid',
|
||||
'worker_auth_stale',
|
||||
])
|
||||
@@ -1,8 +1,8 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { WorkOrderRow } from '../../repositories/worker-platform-repo.js'
|
||||
import { resolveCollectSubmitTargetWorkOrder } from './worker-platform-service.js'
|
||||
import type { WorkOrderRow } from '../../repositories/worker-platform/index.js'
|
||||
import { resolveCollectSubmitTargetWorkOrder } from './index.js'
|
||||
|
||||
function buildWorkOrderRow(
|
||||
overrides: Partial<WorkOrderRow> = {},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user