优化后端-接单平台部分逻辑

This commit is contained in:
yml2213
2026-07-28 14:06:31 +08:00
parent b505b6b85e
commit 6e635b2499
9 changed files with 1389 additions and 20 deletions
@@ -0,0 +1,123 @@
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
import type { JsonObject } from '../../types/json.js'
import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js'
import {
normalizeStoredFileUrl,
refreshUploadedFileUrls,
type UploadedFileDto,
} from '../file-storage/file-storage-service.js'
export type WorkerFinanceConfig = {
recharge: {
enabled: boolean
channelName: string
accountName: string
accountNo: string
qrCodeImage: UploadedFileDto | null
instructions: string
}
withdraw: {
enabled: boolean
instructions: string
}
}
export function getWorkerFinanceConfig(): WorkerFinanceConfig {
const config = readAppConfigEntry({
configKey: APP_CONFIG_KEYS.workerFinance,
fallback: createDefaultWorkerFinanceConfig,
normalize: normalizeWorkerFinanceConfig,
})
return refreshWorkerFinanceConfig(config)
}
export async function saveWorkerFinanceConfig(rawValue: unknown): Promise<WorkerFinanceConfig> {
const saved = await saveAppConfigEntry({
configKey: APP_CONFIG_KEYS.workerFinance,
value: rawValue,
normalize: normalizeWorkerFinanceConfig,
})
return refreshWorkerFinanceConfig(saved)
}
export function normalizeWorkerFinanceConfig(rawValue: unknown): WorkerFinanceConfig {
const source = isPlainObject(rawValue) ? rawValue : {}
const recharge = isPlainObject(source.recharge) ? source.recharge : {}
const withdraw = isPlainObject(source.withdraw) ? source.withdraw : {}
return {
recharge: {
enabled: typeof recharge.enabled === 'boolean' ? recharge.enabled : true,
channelName: String(recharge.channelName || recharge.channel_name || '').trim(),
accountName: String(recharge.accountName || recharge.account_name || '').trim(),
accountNo: String(recharge.accountNo || recharge.account_no || '').trim(),
qrCodeImage: normalizeUploadedFileForStorage(
recharge.qrCodeImage || recharge.qr_code_image,
),
instructions: String(recharge.instructions || '').trim(),
},
withdraw: {
enabled: typeof withdraw.enabled === 'boolean' ? withdraw.enabled : true,
instructions: String(withdraw.instructions || '').trim(),
},
}
}
export function createDefaultWorkerFinanceConfig(): WorkerFinanceConfig {
return {
recharge: {
enabled: true,
channelName: '',
accountName: '',
accountNo: '',
qrCodeImage: null,
instructions: '',
},
withdraw: {
enabled: true,
instructions: '',
},
}
}
function refreshWorkerFinanceConfig(config: WorkerFinanceConfig): WorkerFinanceConfig {
return {
recharge: {
...config.recharge,
qrCodeImage: config.recharge.qrCodeImage
? (refreshUploadedFileUrls(config.recharge.qrCodeImage) as UploadedFileDto)
: null,
},
withdraw: {
...config.withdraw,
},
}
}
function normalizeUploadedFileForStorage(value: unknown): UploadedFileDto | null {
if (!isPlainObject(value)) {
return null
}
const objectKey = String(value.objectKey || value.object_key || '').trim()
if (!objectKey) {
return null
}
return {
objectKey,
url: normalizeStoredFileUrl(value.url, objectKey),
thumbnailUrl: normalizeStoredFileUrl(
value.thumbnailUrl || value.thumbnail_url,
objectKey,
),
mediumUrl: normalizeStoredFileUrl(value.mediumUrl || value.medium_url, objectKey),
filename: String(value.filename || '').trim() || '收款二维码',
contentType: String(value.contentType || value.content_type || '').trim(),
size: Math.max(0, Number(value.size || 0) || 0),
}
}
function isPlainObject(value: unknown): value is JsonObject {
return Object.prototype.toString.call(value) === '[object Object]'
}
@@ -13,7 +13,9 @@ import {
createWorkOrderEvent,
createWorkerFinanceRequest,
createWorkerUser,
getWorkerFinanceRequestById,
getWorkerFinanceRequestSummary,
getWorkerLevelById,
incrementWorkerSessionVersion,
getWorkOrderById,
getWorkOrderByOrderItemId,
@@ -29,6 +31,7 @@ import {
listWorkerLevels,
listWorkerUsers,
resolveProblemWorkOrder,
reviewWorkerFinanceRequest,
updateWorkOrder,
updateWorkerPassword,
updateWorkerUser,
@@ -56,6 +59,10 @@ import {
refreshStoredFileUrl,
refreshUploadedFileUrls,
} from '../file-storage/file-storage-service.js'
import {
getWorkerFinanceConfig,
saveWorkerFinanceConfig,
} from './worker-finance-config-service.js'
export type WorkerSession = {
sessionId: string
@@ -286,9 +293,10 @@ export function requireActiveWorkerSession(session: WorkerSession | null | undef
export async function getWorkerProfile(session: WorkerSession) {
const worker = await getRequiredWorker(session.workerId)
const [financeSummary, acceptedOrderCount] = await Promise.all([
const [financeSummary, acceptedOrderCount, financeConfig] = await Promise.all([
getWorkerFinanceRequestSummary(session.workerId),
countWorkerAcceptedOrders(session.workerId),
Promise.resolve(getWorkerFinanceConfig()),
])
return {
worker: mapWorkerUser(worker),
@@ -299,6 +307,7 @@ export async function getWorkerProfile(session: WorkerSession) {
approvedWithdrawAmount: financeSummary.approvedWithdrawAmount,
pendingRechargeAmount: financeSummary.pendingRechargeAmount,
},
financeConfig,
}
}
@@ -348,6 +357,13 @@ export async function createWorkerRechargeRequest(
) {
requireActiveWorkerSession(session)
const worker = await getRequiredWorker(session.workerId)
const financeConfig = getWorkerFinanceConfig()
if (financeConfig.recharge.enabled === false) {
throw createHttpError('当前充值入口暂未开放,请联系管理员', {
statusCode: 409,
errorCode: 'worker_recharge_disabled',
})
}
const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0)
if (amount <= 0) {
throw createHttpError('充值金额必须大于 0', {
@@ -369,6 +385,12 @@ export async function createWorkerRechargeRequest(
source: 'worker_profile_recharge',
username: worker.username,
phone: worker.phone || '',
financeConfig: {
channelName: financeConfig.recharge.channelName,
accountName: financeConfig.recharge.accountName,
accountNo: financeConfig.recharge.accountNo,
qrCodeImage: financeConfig.recharge.qrCodeImage,
},
}),
now: nowIso(),
})
@@ -389,6 +411,13 @@ export async function createWorkerWithdrawRequest(
) {
requireActiveWorkerSession(session)
const worker = await getRequiredWorker(session.workerId)
const financeConfig = getWorkerFinanceConfig()
if (financeConfig.withdraw.enabled === false) {
throw createHttpError('当前提现入口暂未开放,请联系管理员', {
statusCode: 409,
errorCode: 'worker_withdraw_disabled',
})
}
const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0)
if (amount <= 0) {
throw createHttpError('提现金额必须大于 0', {
@@ -878,11 +907,21 @@ export async function listAdminWorkerUsers(query: JsonObject = {}) {
export async function reviewAdminWorkerUser(workerId: number | string, payload: JsonObject = {}) {
const worker = await getRequiredWorker(workerId)
const status = normalizeReviewStatus(payload.status)
const levelId =
normalizeOptionalId(payload.levelId) ||
worker.level_id ||
(await ensureWorkerPlatformDefaults()).level?.id ||
null
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,
@@ -914,6 +953,81 @@ export async function creditAdminWorkerWallet(workerId: number | string, payload
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)
@@ -1079,6 +1193,32 @@ export async function publishAdminWorkOrder(workOrderId: number | string) {
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 = {},
@@ -1559,6 +1699,14 @@ function mapFinanceRequest(request: WorkerFinanceRequestRow | null | undefined)
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,
}
}
@@ -1902,6 +2050,7 @@ function normalizeWalletLedgerType(value: unknown) {
if (
[
'manual_credit',
'withdraw_paid',
'deposit_freeze',
'deposit_release',
'deposit_deduction',
@@ -1940,6 +2089,17 @@ function normalizeFinanceRequestStatus(value: unknown) {
})
}
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',
})
}
function normalizeWithdrawChannel(value: unknown) {
const channel = String(value || '').trim()
if (['alipay', 'wechat', 'bank'].includes(channel)) {