优化后端-接单平台部分逻辑
This commit is contained in:
@@ -2,6 +2,7 @@ export const APP_CONFIG_KEYS = {
|
||||
notification: 'notification',
|
||||
scheduledJobs: 'scheduled_jobs',
|
||||
fulfillmentRouting: 'fulfillment_routing',
|
||||
workerFinance: 'worker_finance',
|
||||
cloudtentaclesSources: 'cloudtentacles_sources',
|
||||
cloudtentaclesSession: 'cloudtentacles_session',
|
||||
cloudtentaclesOverrideRules: 'cloudtentacles_override_rules',
|
||||
|
||||
@@ -74,6 +74,9 @@ export type WorkerFinanceRequestRow = {
|
||||
created_at: string
|
||||
updated_at: string
|
||||
reviewed_at: string | null
|
||||
worker_username?: string
|
||||
worker_display_name?: string
|
||||
worker_phone?: string
|
||||
}
|
||||
|
||||
export type WorkCategoryRow = {
|
||||
@@ -179,6 +182,7 @@ type FinanceRequestListInput = {
|
||||
workerId?: number
|
||||
status?: string
|
||||
requestType?: string
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
type CreateWorkerInput = {
|
||||
@@ -242,6 +246,16 @@ const WORK_PRODUCT_RULE_SELECT = `
|
||||
LEFT JOIN work_categories wc ON wc.id = wpr.category_id
|
||||
`
|
||||
|
||||
const WORKER_FINANCE_REQUEST_SELECT = `
|
||||
SELECT
|
||||
wfr.*,
|
||||
wu.username AS worker_username,
|
||||
wu.display_name AS worker_display_name,
|
||||
wu.phone AS worker_phone
|
||||
FROM worker_finance_requests wfr
|
||||
LEFT JOIN worker_users wu ON wu.id = wfr.worker_id
|
||||
`
|
||||
|
||||
export async function getWorkerLevelByKey(levelKey: string): Promise<WorkerLevelRow | null> {
|
||||
const result = await query<WorkerLevelRow>(
|
||||
'SELECT * FROM worker_levels WHERE level_key = $1 LIMIT 1',
|
||||
@@ -580,32 +594,43 @@ export async function createWorkerFinanceRequest(input: {
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function getWorkerFinanceRequestById(
|
||||
requestId: number | string,
|
||||
): Promise<WorkerFinanceRequestRow | null> {
|
||||
const result = await query<WorkerFinanceRequestRow>(
|
||||
`${WORKER_FINANCE_REQUEST_SELECT} WHERE wfr.id = $1 LIMIT 1`,
|
||||
[Number(requestId)],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function listWorkerFinanceRequests({
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
workerId = 0,
|
||||
status = '',
|
||||
requestType = '',
|
||||
keyword = '',
|
||||
}: FinanceRequestListInput = {}): Promise<{ items: WorkerFinanceRequestRow[]; total: number }> {
|
||||
const { whereClause, params } = buildWorkerFinanceRequestWhere({
|
||||
workerId,
|
||||
status,
|
||||
requestType,
|
||||
keyword,
|
||||
})
|
||||
const totalResult = await query<{ total: number }>(
|
||||
`SELECT COUNT(*)::int AS total FROM worker_finance_requests wfr ${whereClause}`,
|
||||
`SELECT COUNT(*)::int AS total FROM worker_finance_requests wfr
|
||||
LEFT JOIN worker_users wu ON wu.id = wfr.worker_id
|
||||
${whereClause}`,
|
||||
params,
|
||||
)
|
||||
const offset = (page - 1) * pageSize
|
||||
params.push(pageSize, offset)
|
||||
const itemsResult = await query<WorkerFinanceRequestRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM worker_finance_requests wfr
|
||||
${whereClause}
|
||||
ORDER BY wfr.created_at DESC, wfr.id DESC
|
||||
LIMIT $${params.length - 1} OFFSET $${params.length}
|
||||
`,
|
||||
`${WORKER_FINANCE_REQUEST_SELECT}
|
||||
${whereClause}
|
||||
ORDER BY wfr.created_at DESC, wfr.id DESC
|
||||
LIMIT $${params.length - 1} OFFSET $${params.length}`,
|
||||
params,
|
||||
)
|
||||
return {
|
||||
@@ -614,6 +639,146 @@ export async function listWorkerFinanceRequests({
|
||||
}
|
||||
}
|
||||
|
||||
export async function reviewWorkerFinanceRequest(input: {
|
||||
requestId: number
|
||||
status: string
|
||||
reviewedNote: string
|
||||
now: string
|
||||
}): Promise<{
|
||||
request: WorkerFinanceRequestRow | null
|
||||
failureReason: 'request_not_pending' | 'withdraw_insufficient' | null
|
||||
}> {
|
||||
return withTransaction(async (client) => {
|
||||
const currentResult = await client.query<WorkerFinanceRequestRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM worker_finance_requests
|
||||
WHERE id = $1
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
`,
|
||||
[input.requestId],
|
||||
)
|
||||
const current = currentResult.rows[0] || null
|
||||
if (!current) {
|
||||
return {
|
||||
request: null,
|
||||
failureReason: null,
|
||||
}
|
||||
}
|
||||
|
||||
if (current.status !== 'pending') {
|
||||
return {
|
||||
request: await getWorkerFinanceRequestByIdWithClient(client, input.requestId),
|
||||
failureReason: 'request_not_pending',
|
||||
}
|
||||
}
|
||||
|
||||
if (input.status === 'approved') {
|
||||
await ensureWorkerWalletWithClient(client, Number(current.worker_id), input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, Number(current.worker_id))
|
||||
const availableAmount = Number(wallet?.available_amount || 0)
|
||||
const frozenAmount = Number(wallet?.frozen_deposit_amount || 0)
|
||||
const amount = Number(current.amount || 0)
|
||||
const note =
|
||||
input.reviewedNote ||
|
||||
(current.request_type === 'withdraw' ? '提现申请审核通过' : '充值申请审核通过')
|
||||
|
||||
if (current.request_type === 'withdraw') {
|
||||
if (availableAmount < amount) {
|
||||
return {
|
||||
request: await getWorkerFinanceRequestByIdWithClient(client, input.requestId),
|
||||
failureReason: 'withdraw_insufficient',
|
||||
}
|
||||
}
|
||||
|
||||
const nextAvailable = availableAmount - amount
|
||||
await client.query(
|
||||
`
|
||||
UPDATE worker_wallets
|
||||
SET available_amount = $1, updated_at = $2
|
||||
WHERE worker_id = $3
|
||||
`,
|
||||
[nextAvailable, input.now, Number(current.worker_id)],
|
||||
)
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
audit_status, note, payload_json, created_at
|
||||
) VALUES ($1, 'withdraw_paid', $2, $3, $4, 'approved', $5, $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
Number(current.worker_id),
|
||||
-amount,
|
||||
nextAvailable,
|
||||
frozenAmount,
|
||||
note,
|
||||
JSON.stringify({
|
||||
requestId: Number(current.id),
|
||||
requestType: current.request_type,
|
||||
source: 'admin_finance_review',
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
} else {
|
||||
const nextAvailable = availableAmount + amount
|
||||
await client.query(
|
||||
`
|
||||
UPDATE worker_wallets
|
||||
SET
|
||||
available_amount = $1,
|
||||
total_credited_amount = total_credited_amount + $2,
|
||||
updated_at = $3
|
||||
WHERE worker_id = $4
|
||||
`,
|
||||
[nextAvailable, amount, input.now, Number(current.worker_id)],
|
||||
)
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
audit_status, note, payload_json, created_at
|
||||
) VALUES ($1, 'manual_credit', $2, $3, $4, 'approved', $5, $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
Number(current.worker_id),
|
||||
amount,
|
||||
nextAvailable,
|
||||
frozenAmount,
|
||||
note,
|
||||
JSON.stringify({
|
||||
requestId: Number(current.id),
|
||||
requestType: current.request_type,
|
||||
source: 'admin_finance_review',
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE worker_finance_requests
|
||||
SET
|
||||
status = $1,
|
||||
reviewed_note = $2,
|
||||
reviewed_at = $3,
|
||||
updated_at = $3
|
||||
WHERE id = $4
|
||||
`,
|
||||
[input.status, input.reviewedNote, input.now, input.requestId],
|
||||
)
|
||||
|
||||
return {
|
||||
request: await getWorkerFinanceRequestByIdWithClient(client, input.requestId),
|
||||
failureReason: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function getWorkerFinanceRequestSummary(workerId: number | string) {
|
||||
const result = await query<{
|
||||
pending_withdraw_amount: number
|
||||
@@ -1421,6 +1586,17 @@ async function getWorkerUserByIdWithClient(
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
async function getWorkerFinanceRequestByIdWithClient(
|
||||
client: PoolClient,
|
||||
requestId: number | string,
|
||||
): Promise<WorkerFinanceRequestRow | null> {
|
||||
const result = await client.query<WorkerFinanceRequestRow>(
|
||||
`${WORKER_FINANCE_REQUEST_SELECT} WHERE wfr.id = $1 LIMIT 1`,
|
||||
[Number(requestId)],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
async function getWorkOrderByIdWithClient(
|
||||
client: PoolClient,
|
||||
workOrderId: number | string,
|
||||
@@ -1500,6 +1676,7 @@ function buildWorkerFinanceRequestWhere({
|
||||
workerId = 0,
|
||||
status = '',
|
||||
requestType = '',
|
||||
keyword = '',
|
||||
}: FinanceRequestListInput) {
|
||||
const filters: string[] = []
|
||||
const params: unknown[] = []
|
||||
@@ -1515,6 +1692,16 @@ function buildWorkerFinanceRequestWhere({
|
||||
params.push(requestType)
|
||||
filters.push(`wfr.request_type = $${params.length}`)
|
||||
}
|
||||
if (keyword) {
|
||||
params.push(`%${keyword}%`)
|
||||
filters.push(
|
||||
`(wu.username ILIKE $${params.length}
|
||||
OR wu.display_name ILIKE $${params.length}
|
||||
OR wu.phone ILIKE $${params.length}
|
||||
OR wfr.account_name ILIKE $${params.length}
|
||||
OR wfr.account_no ILIKE $${params.length})`,
|
||||
)
|
||||
}
|
||||
return {
|
||||
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
|
||||
params,
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
acceptAdminWorkOrder,
|
||||
creditAdminWorkerWallet,
|
||||
createAdminMockWorkOrder,
|
||||
getAdminWorkerFinanceConfig,
|
||||
getAdminWorkerPlatformSummary,
|
||||
listAdminWorkerFinanceRequests,
|
||||
listAdminWorkCategories,
|
||||
listAdminWorkProductRules,
|
||||
listAdminWorkerLevels,
|
||||
@@ -12,13 +14,16 @@ import {
|
||||
listAdminWorkOrders,
|
||||
markAdminWorkOrderProblem,
|
||||
publishAdminWorkOrder,
|
||||
reviewAdminWorkerFinanceRequest,
|
||||
resolveAdminProblemWorkOrder,
|
||||
reviewAdminWorkerUser,
|
||||
saveAdminWorkerFinanceConfig,
|
||||
saveAdminWorkCategory,
|
||||
saveAdminWorkProductRule,
|
||||
saveAdminWorkerLevel,
|
||||
submitAdminWorkOrderMaterial,
|
||||
syncAdminWorkerOrdersFromSource,
|
||||
unpublishAdminWorkOrder,
|
||||
} from '../../services/worker-platform/worker-platform-service.js'
|
||||
import { createJsonHandler, requireAdminRoles } from './session.js'
|
||||
|
||||
@@ -161,6 +166,62 @@ router.post(
|
||||
),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/worker-platform/finance-config',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(() => getAdminWorkerFinanceConfig(), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取资金配置失败',
|
||||
scope: '[admin/worker-platform/finance-config]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/finance-config',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler((req) => saveAdminWorkerFinanceConfig(req.body || {}), {
|
||||
successMessage: '资金配置已保存',
|
||||
errorMessage: '保存资金配置失败',
|
||||
scope: '[admin/worker-platform/finance-config]',
|
||||
audit: (_req, data) => ({
|
||||
action: 'worker_finance_config_saved',
|
||||
targetType: 'worker_finance_config',
|
||||
targetId: 'worker_finance',
|
||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/worker-platform/finance-requests',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler((req) => listAdminWorkerFinanceRequests(req.query), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取资金申请失败',
|
||||
scope: '[admin/worker-platform/finance-requests]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/finance-requests/:requestId/review',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
reviewAdminWorkerFinanceRequest(String(req.params.requestId || ''), req.body || {}),
|
||||
{
|
||||
successMessage: '资金申请已处理',
|
||||
errorMessage: '处理资金申请失败',
|
||||
scope: '[admin/worker-platform/finance-requests/:requestId/review]',
|
||||
audit: (req, data) => ({
|
||||
action: 'worker_finance_request_reviewed',
|
||||
targetType: 'worker_finance_request',
|
||||
targetId: String(req.params.requestId || ''),
|
||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/worker-platform/orders',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
@@ -238,6 +299,22 @@ router.post(
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/unpublish',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler((req) => unpublishAdminWorkOrder(String(req.params.workOrderId || '')), {
|
||||
successMessage: '订单已撤回到未分配',
|
||||
errorMessage: '下架接单订单失败',
|
||||
scope: '[admin/worker-platform/orders/:workOrderId/unpublish]',
|
||||
audit: (req, data) => ({
|
||||
action: 'work_order_unpublished',
|
||||
targetType: 'work_order',
|
||||
targetId: String(req.params.workOrderId || ''),
|
||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/problem',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user