优化后端-接单平台部分逻辑
This commit is contained in:
@@ -2,6 +2,7 @@ export const APP_CONFIG_KEYS = {
|
|||||||
notification: 'notification',
|
notification: 'notification',
|
||||||
scheduledJobs: 'scheduled_jobs',
|
scheduledJobs: 'scheduled_jobs',
|
||||||
fulfillmentRouting: 'fulfillment_routing',
|
fulfillmentRouting: 'fulfillment_routing',
|
||||||
|
workerFinance: 'worker_finance',
|
||||||
cloudtentaclesSources: 'cloudtentacles_sources',
|
cloudtentaclesSources: 'cloudtentacles_sources',
|
||||||
cloudtentaclesSession: 'cloudtentacles_session',
|
cloudtentaclesSession: 'cloudtentacles_session',
|
||||||
cloudtentaclesOverrideRules: 'cloudtentacles_override_rules',
|
cloudtentaclesOverrideRules: 'cloudtentacles_override_rules',
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ export type WorkerFinanceRequestRow = {
|
|||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
reviewed_at: string | null
|
reviewed_at: string | null
|
||||||
|
worker_username?: string
|
||||||
|
worker_display_name?: string
|
||||||
|
worker_phone?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkCategoryRow = {
|
export type WorkCategoryRow = {
|
||||||
@@ -179,6 +182,7 @@ type FinanceRequestListInput = {
|
|||||||
workerId?: number
|
workerId?: number
|
||||||
status?: string
|
status?: string
|
||||||
requestType?: string
|
requestType?: string
|
||||||
|
keyword?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateWorkerInput = {
|
type CreateWorkerInput = {
|
||||||
@@ -242,6 +246,16 @@ const WORK_PRODUCT_RULE_SELECT = `
|
|||||||
LEFT JOIN work_categories wc ON wc.id = wpr.category_id
|
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> {
|
export async function getWorkerLevelByKey(levelKey: string): Promise<WorkerLevelRow | null> {
|
||||||
const result = await query<WorkerLevelRow>(
|
const result = await query<WorkerLevelRow>(
|
||||||
'SELECT * FROM worker_levels WHERE level_key = $1 LIMIT 1',
|
'SELECT * FROM worker_levels WHERE level_key = $1 LIMIT 1',
|
||||||
@@ -580,32 +594,43 @@ export async function createWorkerFinanceRequest(input: {
|
|||||||
return result.rows[0] || null
|
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({
|
export async function listWorkerFinanceRequests({
|
||||||
page = 1,
|
page = 1,
|
||||||
pageSize = 20,
|
pageSize = 20,
|
||||||
workerId = 0,
|
workerId = 0,
|
||||||
status = '',
|
status = '',
|
||||||
requestType = '',
|
requestType = '',
|
||||||
|
keyword = '',
|
||||||
}: FinanceRequestListInput = {}): Promise<{ items: WorkerFinanceRequestRow[]; total: number }> {
|
}: FinanceRequestListInput = {}): Promise<{ items: WorkerFinanceRequestRow[]; total: number }> {
|
||||||
const { whereClause, params } = buildWorkerFinanceRequestWhere({
|
const { whereClause, params } = buildWorkerFinanceRequestWhere({
|
||||||
workerId,
|
workerId,
|
||||||
status,
|
status,
|
||||||
requestType,
|
requestType,
|
||||||
|
keyword,
|
||||||
})
|
})
|
||||||
const totalResult = await query<{ total: number }>(
|
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,
|
params,
|
||||||
)
|
)
|
||||||
const offset = (page - 1) * pageSize
|
const offset = (page - 1) * pageSize
|
||||||
params.push(pageSize, offset)
|
params.push(pageSize, offset)
|
||||||
const itemsResult = await query<WorkerFinanceRequestRow>(
|
const itemsResult = await query<WorkerFinanceRequestRow>(
|
||||||
`
|
`${WORKER_FINANCE_REQUEST_SELECT}
|
||||||
SELECT *
|
|
||||||
FROM worker_finance_requests wfr
|
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY wfr.created_at DESC, wfr.id DESC
|
ORDER BY wfr.created_at DESC, wfr.id DESC
|
||||||
LIMIT $${params.length - 1} OFFSET $${params.length}
|
LIMIT $${params.length - 1} OFFSET $${params.length}`,
|
||||||
`,
|
|
||||||
params,
|
params,
|
||||||
)
|
)
|
||||||
return {
|
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) {
|
export async function getWorkerFinanceRequestSummary(workerId: number | string) {
|
||||||
const result = await query<{
|
const result = await query<{
|
||||||
pending_withdraw_amount: number
|
pending_withdraw_amount: number
|
||||||
@@ -1421,6 +1586,17 @@ async function getWorkerUserByIdWithClient(
|
|||||||
return result.rows[0] || null
|
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(
|
async function getWorkOrderByIdWithClient(
|
||||||
client: PoolClient,
|
client: PoolClient,
|
||||||
workOrderId: number | string,
|
workOrderId: number | string,
|
||||||
@@ -1500,6 +1676,7 @@ function buildWorkerFinanceRequestWhere({
|
|||||||
workerId = 0,
|
workerId = 0,
|
||||||
status = '',
|
status = '',
|
||||||
requestType = '',
|
requestType = '',
|
||||||
|
keyword = '',
|
||||||
}: FinanceRequestListInput) {
|
}: FinanceRequestListInput) {
|
||||||
const filters: string[] = []
|
const filters: string[] = []
|
||||||
const params: unknown[] = []
|
const params: unknown[] = []
|
||||||
@@ -1515,6 +1692,16 @@ function buildWorkerFinanceRequestWhere({
|
|||||||
params.push(requestType)
|
params.push(requestType)
|
||||||
filters.push(`wfr.request_type = $${params.length}`)
|
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 {
|
return {
|
||||||
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
|
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import {
|
|||||||
acceptAdminWorkOrder,
|
acceptAdminWorkOrder,
|
||||||
creditAdminWorkerWallet,
|
creditAdminWorkerWallet,
|
||||||
createAdminMockWorkOrder,
|
createAdminMockWorkOrder,
|
||||||
|
getAdminWorkerFinanceConfig,
|
||||||
getAdminWorkerPlatformSummary,
|
getAdminWorkerPlatformSummary,
|
||||||
|
listAdminWorkerFinanceRequests,
|
||||||
listAdminWorkCategories,
|
listAdminWorkCategories,
|
||||||
listAdminWorkProductRules,
|
listAdminWorkProductRules,
|
||||||
listAdminWorkerLevels,
|
listAdminWorkerLevels,
|
||||||
@@ -12,13 +14,16 @@ import {
|
|||||||
listAdminWorkOrders,
|
listAdminWorkOrders,
|
||||||
markAdminWorkOrderProblem,
|
markAdminWorkOrderProblem,
|
||||||
publishAdminWorkOrder,
|
publishAdminWorkOrder,
|
||||||
|
reviewAdminWorkerFinanceRequest,
|
||||||
resolveAdminProblemWorkOrder,
|
resolveAdminProblemWorkOrder,
|
||||||
reviewAdminWorkerUser,
|
reviewAdminWorkerUser,
|
||||||
|
saveAdminWorkerFinanceConfig,
|
||||||
saveAdminWorkCategory,
|
saveAdminWorkCategory,
|
||||||
saveAdminWorkProductRule,
|
saveAdminWorkProductRule,
|
||||||
saveAdminWorkerLevel,
|
saveAdminWorkerLevel,
|
||||||
submitAdminWorkOrderMaterial,
|
submitAdminWorkOrderMaterial,
|
||||||
syncAdminWorkerOrdersFromSource,
|
syncAdminWorkerOrdersFromSource,
|
||||||
|
unpublishAdminWorkOrder,
|
||||||
} from '../../services/worker-platform/worker-platform-service.js'
|
} from '../../services/worker-platform/worker-platform-service.js'
|
||||||
import { createJsonHandler, requireAdminRoles } from './session.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(
|
router.get(
|
||||||
'/worker-platform/orders',
|
'/worker-platform/orders',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
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(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/problem',
|
'/worker-platform/orders/:workOrderId/problem',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
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,
|
createWorkOrderEvent,
|
||||||
createWorkerFinanceRequest,
|
createWorkerFinanceRequest,
|
||||||
createWorkerUser,
|
createWorkerUser,
|
||||||
|
getWorkerFinanceRequestById,
|
||||||
getWorkerFinanceRequestSummary,
|
getWorkerFinanceRequestSummary,
|
||||||
|
getWorkerLevelById,
|
||||||
incrementWorkerSessionVersion,
|
incrementWorkerSessionVersion,
|
||||||
getWorkOrderById,
|
getWorkOrderById,
|
||||||
getWorkOrderByOrderItemId,
|
getWorkOrderByOrderItemId,
|
||||||
@@ -29,6 +31,7 @@ import {
|
|||||||
listWorkerLevels,
|
listWorkerLevels,
|
||||||
listWorkerUsers,
|
listWorkerUsers,
|
||||||
resolveProblemWorkOrder,
|
resolveProblemWorkOrder,
|
||||||
|
reviewWorkerFinanceRequest,
|
||||||
updateWorkOrder,
|
updateWorkOrder,
|
||||||
updateWorkerPassword,
|
updateWorkerPassword,
|
||||||
updateWorkerUser,
|
updateWorkerUser,
|
||||||
@@ -56,6 +59,10 @@ import {
|
|||||||
refreshStoredFileUrl,
|
refreshStoredFileUrl,
|
||||||
refreshUploadedFileUrls,
|
refreshUploadedFileUrls,
|
||||||
} from '../file-storage/file-storage-service.js'
|
} from '../file-storage/file-storage-service.js'
|
||||||
|
import {
|
||||||
|
getWorkerFinanceConfig,
|
||||||
|
saveWorkerFinanceConfig,
|
||||||
|
} from './worker-finance-config-service.js'
|
||||||
|
|
||||||
export type WorkerSession = {
|
export type WorkerSession = {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
@@ -286,9 +293,10 @@ export function requireActiveWorkerSession(session: WorkerSession | null | undef
|
|||||||
|
|
||||||
export async function getWorkerProfile(session: WorkerSession) {
|
export async function getWorkerProfile(session: WorkerSession) {
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
const [financeSummary, acceptedOrderCount] = await Promise.all([
|
const [financeSummary, acceptedOrderCount, financeConfig] = await Promise.all([
|
||||||
getWorkerFinanceRequestSummary(session.workerId),
|
getWorkerFinanceRequestSummary(session.workerId),
|
||||||
countWorkerAcceptedOrders(session.workerId),
|
countWorkerAcceptedOrders(session.workerId),
|
||||||
|
Promise.resolve(getWorkerFinanceConfig()),
|
||||||
])
|
])
|
||||||
return {
|
return {
|
||||||
worker: mapWorkerUser(worker),
|
worker: mapWorkerUser(worker),
|
||||||
@@ -299,6 +307,7 @@ export async function getWorkerProfile(session: WorkerSession) {
|
|||||||
approvedWithdrawAmount: financeSummary.approvedWithdrawAmount,
|
approvedWithdrawAmount: financeSummary.approvedWithdrawAmount,
|
||||||
pendingRechargeAmount: financeSummary.pendingRechargeAmount,
|
pendingRechargeAmount: financeSummary.pendingRechargeAmount,
|
||||||
},
|
},
|
||||||
|
financeConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,6 +357,13 @@ export async function createWorkerRechargeRequest(
|
|||||||
) {
|
) {
|
||||||
requireActiveWorkerSession(session)
|
requireActiveWorkerSession(session)
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
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)
|
const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0)
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
throw createHttpError('充值金额必须大于 0', {
|
throw createHttpError('充值金额必须大于 0', {
|
||||||
@@ -369,6 +385,12 @@ export async function createWorkerRechargeRequest(
|
|||||||
source: 'worker_profile_recharge',
|
source: 'worker_profile_recharge',
|
||||||
username: worker.username,
|
username: worker.username,
|
||||||
phone: worker.phone || '',
|
phone: worker.phone || '',
|
||||||
|
financeConfig: {
|
||||||
|
channelName: financeConfig.recharge.channelName,
|
||||||
|
accountName: financeConfig.recharge.accountName,
|
||||||
|
accountNo: financeConfig.recharge.accountNo,
|
||||||
|
qrCodeImage: financeConfig.recharge.qrCodeImage,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
now: nowIso(),
|
now: nowIso(),
|
||||||
})
|
})
|
||||||
@@ -389,6 +411,13 @@ export async function createWorkerWithdrawRequest(
|
|||||||
) {
|
) {
|
||||||
requireActiveWorkerSession(session)
|
requireActiveWorkerSession(session)
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
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)
|
const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0)
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
throw createHttpError('提现金额必须大于 0', {
|
throw createHttpError('提现金额必须大于 0', {
|
||||||
@@ -878,11 +907,21 @@ export async function listAdminWorkerUsers(query: JsonObject = {}) {
|
|||||||
export async function reviewAdminWorkerUser(workerId: number | string, payload: JsonObject = {}) {
|
export async function reviewAdminWorkerUser(workerId: number | string, payload: JsonObject = {}) {
|
||||||
const worker = await getRequiredWorker(workerId)
|
const worker = await getRequiredWorker(workerId)
|
||||||
const status = normalizeReviewStatus(payload.status)
|
const status = normalizeReviewStatus(payload.status)
|
||||||
const levelId =
|
const requestedLevelId = normalizeOptionalId(payload.levelId)
|
||||||
normalizeOptionalId(payload.levelId) ||
|
let levelId = worker.level_id || null
|
||||||
worker.level_id ||
|
if (requestedLevelId) {
|
||||||
(await ensureWorkerPlatformDefaults()).level?.id ||
|
const level = await getWorkerLevelById(requestedLevelId)
|
||||||
null
|
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 now = nowIso()
|
||||||
const updated = await updateWorkerUser(worker.id, {
|
const updated = await updateWorkerUser(worker.id, {
|
||||||
status,
|
status,
|
||||||
@@ -914,6 +953,81 @@ export async function creditAdminWorkerWallet(workerId: number | string, payload
|
|||||||
return { wallet: mapWallet(wallet) }
|
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 = {}) {
|
export async function listAdminWorkOrders(query: JsonObject = {}) {
|
||||||
const page = normalizePage(query.page)
|
const page = normalizePage(query.page)
|
||||||
const pageSize = normalizePageSize(query.pageSize)
|
const pageSize = normalizePageSize(query.pageSize)
|
||||||
@@ -1079,6 +1193,32 @@ export async function publishAdminWorkOrder(workOrderId: number | string) {
|
|||||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
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(
|
export async function markAdminWorkOrderProblem(
|
||||||
workOrderId: number | string,
|
workOrderId: number | string,
|
||||||
payload: JsonObject = {},
|
payload: JsonObject = {},
|
||||||
@@ -1559,6 +1699,14 @@ function mapFinanceRequest(request: WorkerFinanceRequestRow | null | undefined)
|
|||||||
createdAt: request.created_at,
|
createdAt: request.created_at,
|
||||||
updatedAt: request.updated_at,
|
updatedAt: request.updated_at,
|
||||||
reviewedAt: request.reviewed_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 (
|
if (
|
||||||
[
|
[
|
||||||
'manual_credit',
|
'manual_credit',
|
||||||
|
'withdraw_paid',
|
||||||
'deposit_freeze',
|
'deposit_freeze',
|
||||||
'deposit_release',
|
'deposit_release',
|
||||||
'deposit_deduction',
|
'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) {
|
function normalizeWithdrawChannel(value: unknown) {
|
||||||
const channel = String(value || '').trim()
|
const channel = String(value || '').trim()
|
||||||
if (['alipay', 'wechat', 'bank'].includes(channel)) {
|
if (['alipay', 'wechat', 'bank'].includes(channel)) {
|
||||||
|
|||||||
@@ -28,16 +28,19 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import type { TableColumnsType } from 'antd'
|
import type { TableColumnsType } from 'antd'
|
||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router'
|
||||||
|
|
||||||
import JsonPreview from '@/components/admin/JsonPreview'
|
import JsonPreview from '@/components/admin/JsonPreview'
|
||||||
|
import ImageUpload from '@/components/files/ImageUpload'
|
||||||
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||||
import PageHeader from '@/components/admin/PageHeader'
|
import PageHeader from '@/components/admin/PageHeader'
|
||||||
import {
|
import {
|
||||||
acceptAdminWorkOrder,
|
acceptAdminWorkOrder,
|
||||||
createAdminMockWorkOrder,
|
createAdminMockWorkOrder,
|
||||||
creditAdminWorkerWallet,
|
creditAdminWorkerWallet,
|
||||||
|
fetchAdminWorkerFinanceConfig,
|
||||||
|
fetchAdminWorkerFinanceRequests,
|
||||||
fetchAdminWorkCategories,
|
fetchAdminWorkCategories,
|
||||||
fetchAdminWorkProductRules,
|
fetchAdminWorkProductRules,
|
||||||
fetchAdminWorkerLevels,
|
fetchAdminWorkerLevels,
|
||||||
@@ -46,13 +49,16 @@ import {
|
|||||||
fetchAdminWorkOrders,
|
fetchAdminWorkOrders,
|
||||||
markAdminWorkOrderProblem,
|
markAdminWorkOrderProblem,
|
||||||
publishAdminWorkOrder,
|
publishAdminWorkOrder,
|
||||||
|
reviewAdminWorkerFinanceRequest,
|
||||||
resolveAdminProblemWorkOrder,
|
resolveAdminProblemWorkOrder,
|
||||||
reviewAdminWorkerUser,
|
reviewAdminWorkerUser,
|
||||||
|
saveAdminWorkerFinanceConfig,
|
||||||
saveAdminWorkCategory,
|
saveAdminWorkCategory,
|
||||||
saveAdminWorkProductRule,
|
saveAdminWorkProductRule,
|
||||||
saveAdminWorkerLevel,
|
saveAdminWorkerLevel,
|
||||||
submitAdminWorkOrderMaterial,
|
submitAdminWorkOrderMaterial,
|
||||||
syncAdminWorkerOrdersFromSource,
|
syncAdminWorkerOrdersFromSource,
|
||||||
|
unpublishAdminWorkOrder,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
import type {
|
import type {
|
||||||
CollectField,
|
CollectField,
|
||||||
@@ -60,6 +66,8 @@ import type {
|
|||||||
WorkCategory,
|
WorkCategory,
|
||||||
WorkOrder,
|
WorkOrder,
|
||||||
WorkProductRule,
|
WorkProductRule,
|
||||||
|
WorkerFinanceConfig,
|
||||||
|
WorkerFinanceRequest,
|
||||||
WorkerLevel,
|
WorkerLevel,
|
||||||
WorkerUser,
|
WorkerUser,
|
||||||
} from '@/types/worker-platform'
|
} from '@/types/worker-platform'
|
||||||
@@ -69,6 +77,23 @@ import {
|
|||||||
} from '@/utils/admin-pagination'
|
} from '@/utils/admin-pagination'
|
||||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
|
|
||||||
|
type FinanceConfigFormValues = {
|
||||||
|
recharge?: {
|
||||||
|
enabled?: boolean
|
||||||
|
channelName?: string
|
||||||
|
accountName?: string
|
||||||
|
accountNo?: string
|
||||||
|
qrCodeImageList?: UploadedFile[]
|
||||||
|
instructions?: string
|
||||||
|
}
|
||||||
|
withdraw?: {
|
||||||
|
enabled?: boolean
|
||||||
|
instructions?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FinanceReviewAction = 'approved' | 'rejected' | 'cancelled'
|
||||||
|
|
||||||
export default function AdminWorkerPlatformPage() {
|
export default function AdminWorkerPlatformPage() {
|
||||||
return (
|
return (
|
||||||
<section className="page-stack">
|
<section className="page-stack">
|
||||||
@@ -84,6 +109,7 @@ export default function AdminWorkerPlatformPage() {
|
|||||||
{ key: 'rules', label: '物品规则', children: <ProductRulesPanel /> },
|
{ key: 'rules', label: '物品规则', children: <ProductRulesPanel /> },
|
||||||
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
|
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
|
||||||
{ key: 'workers', label: '打手', children: <WorkersPanel /> },
|
{ key: 'workers', label: '打手', children: <WorkersPanel /> },
|
||||||
|
{ key: 'finance', label: '资金', children: <FinancePanel /> },
|
||||||
{ key: 'levels', label: '等级权限', children: <LevelsPanel /> },
|
{ key: 'levels', label: '等级权限', children: <LevelsPanel /> },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -123,7 +149,7 @@ function SummaryCards() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function WorkOrdersPanel() {
|
function WorkOrdersPanel() {
|
||||||
const { message } = App.useApp()
|
const { message, modal } = App.useApp()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [status, setStatus] = useState('')
|
const [status, setStatus] = useState('')
|
||||||
@@ -329,6 +355,26 @@ function WorkOrdersPanel() {
|
|||||||
发布
|
发布
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{row.status === 'open' ? (
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
onClick={() => {
|
||||||
|
modal.confirm({
|
||||||
|
title: '确认下架该订单?',
|
||||||
|
content: '下架后订单会回到未分配状态,不再在抢单大厅展示。',
|
||||||
|
okText: '确认下架',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () =>
|
||||||
|
runAction(
|
||||||
|
() => unpublishAdminWorkOrder(row.workOrderId),
|
||||||
|
'订单已撤回到未分配',
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
下架
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
{row.status === 'pending_acceptance' ? (
|
{row.status === 'pending_acceptance' ? (
|
||||||
<Button
|
<Button
|
||||||
icon={<CheckOutlined />}
|
icon={<CheckOutlined />}
|
||||||
@@ -1088,12 +1134,18 @@ function WorkersPanel() {
|
|||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||||
const [creditWorker, setCreditWorker] = useState<WorkerUser | null>(null)
|
const [creditWorker, setCreditWorker] = useState<WorkerUser | null>(null)
|
||||||
|
const [levelWorker, setLevelWorker] = useState<WorkerUser | null>(null)
|
||||||
const [creditForm] = Form.useForm()
|
const [creditForm] = Form.useForm()
|
||||||
|
const [levelForm] = Form.useForm<{ levelId?: number }>()
|
||||||
|
|
||||||
const workersQuery = useQuery({
|
const workersQuery = useQuery({
|
||||||
queryKey: ['admin-worker-platform-workers', status, page, pageSize],
|
queryKey: ['admin-worker-platform-workers', status, page, pageSize],
|
||||||
queryFn: () => fetchAdminWorkerUsers({ status, page, pageSize }),
|
queryFn: () => fetchAdminWorkerUsers({ status, page, pageSize }),
|
||||||
})
|
})
|
||||||
|
const levelsQuery = useQuery({
|
||||||
|
queryKey: ['admin-worker-platform-levels'],
|
||||||
|
queryFn: () => fetchAdminWorkerLevels(),
|
||||||
|
})
|
||||||
const workersPagination = workersQuery.data?.data.pagination
|
const workersPagination = workersQuery.data?.data.pagination
|
||||||
|
|
||||||
async function refreshWorkers() {
|
async function refreshWorkers() {
|
||||||
@@ -1133,6 +1185,23 @@ function WorkersPanel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitLevel(values: { levelId?: number }) {
|
||||||
|
if (!levelWorker) return
|
||||||
|
try {
|
||||||
|
await reviewAdminWorkerUser(levelWorker.workerId, {
|
||||||
|
status: levelWorker.status,
|
||||||
|
levelId: Number(values.levelId || 0),
|
||||||
|
reviewNote: levelWorker.reviewNote,
|
||||||
|
})
|
||||||
|
message.success('打手等级已更新')
|
||||||
|
setLevelWorker(null)
|
||||||
|
levelForm.resetFields()
|
||||||
|
await refreshWorkers()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '调整等级失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const columns: TableColumnsType<WorkerUser> = [
|
const columns: TableColumnsType<WorkerUser> = [
|
||||||
{
|
{
|
||||||
title: '打手',
|
title: '打手',
|
||||||
@@ -1168,7 +1237,7 @@ function WorkersPanel() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 260,
|
width: 340,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{row.status !== 'active' ? (
|
{row.status !== 'active' ? (
|
||||||
@@ -1177,6 +1246,16 @@ function WorkersPanel() {
|
|||||||
{row.status !== 'rejected' ? (
|
{row.status !== 'rejected' ? (
|
||||||
<Button onClick={() => review(row, 'rejected')}>拒绝</Button>
|
<Button onClick={() => review(row, 'rejected')}>拒绝</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setLevelWorker(row)
|
||||||
|
levelForm.setFieldsValue({
|
||||||
|
levelId: row.level?.levelId,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
调等级
|
||||||
|
</Button>
|
||||||
<Button onClick={() => setCreditWorker(row)}>充值</Button>
|
<Button onClick={() => setCreditWorker(row)}>充值</Button>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
@@ -1231,6 +1310,38 @@ function WorkersPanel() {
|
|||||||
scroll={{ x: 1000 }}
|
scroll={{ x: 1000 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="调整打手等级"
|
||||||
|
open={Boolean(levelWorker)}
|
||||||
|
onCancel={() => {
|
||||||
|
setLevelWorker(null)
|
||||||
|
levelForm.resetFields()
|
||||||
|
}}
|
||||||
|
onOk={() => levelForm.submit()}
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
<Form form={levelForm} layout="vertical" onFinish={submitLevel}>
|
||||||
|
<Form.Item label="打手">
|
||||||
|
<Typography.Text>
|
||||||
|
{levelWorker?.displayName || levelWorker?.username || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="目标等级"
|
||||||
|
name="levelId"
|
||||||
|
rules={[{ required: true, message: '请选择目标等级' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
loading={levelsQuery.isLoading}
|
||||||
|
options={(levelsQuery.data?.data.items || []).map((item) => ({
|
||||||
|
value: item.levelId,
|
||||||
|
label: `${item.name} / 最多同时 ${item.maxActiveOrders} 单`,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="人工增加余额"
|
title="人工增加余额"
|
||||||
open={Boolean(creditWorker)}
|
open={Boolean(creditWorker)}
|
||||||
@@ -1263,6 +1374,408 @@ function WorkersPanel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FinancePanel() {
|
||||||
|
const { message } = App.useApp()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [configForm] = Form.useForm<FinanceConfigFormValues>()
|
||||||
|
const [reviewForm] = Form.useForm<{ reviewedNote?: string }>()
|
||||||
|
const [requestStatus, setRequestStatus] = useState('')
|
||||||
|
const [requestType, setRequestType] = useState('')
|
||||||
|
const [keywordInput, setKeywordInput] = useState('')
|
||||||
|
const [keyword, setKeyword] = useState('')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||||
|
const [savingConfig, setSavingConfig] = useState(false)
|
||||||
|
const [reviewing, setReviewing] = useState(false)
|
||||||
|
const [reviewState, setReviewState] = useState<{
|
||||||
|
action: FinanceReviewAction
|
||||||
|
request: WorkerFinanceRequest
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
const financeConfigQuery = useQuery({
|
||||||
|
queryKey: ['admin-worker-platform-finance-config'],
|
||||||
|
queryFn: () => fetchAdminWorkerFinanceConfig(),
|
||||||
|
})
|
||||||
|
const financeRequestsQuery = useQuery({
|
||||||
|
queryKey: [
|
||||||
|
'admin-worker-platform-finance-requests',
|
||||||
|
requestStatus,
|
||||||
|
requestType,
|
||||||
|
keyword,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
fetchAdminWorkerFinanceRequests({
|
||||||
|
status: requestStatus,
|
||||||
|
requestType,
|
||||||
|
keyword,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const financeRequestsPagination = financeRequestsQuery.data?.data.pagination
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const config = financeConfigQuery.data?.data
|
||||||
|
if (!config) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configForm.setFieldsValue(mapFinanceConfigToFormValues(config))
|
||||||
|
}, [financeConfigQuery.data, configForm])
|
||||||
|
|
||||||
|
async function refreshAll() {
|
||||||
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['admin-worker-platform-finance-config'],
|
||||||
|
}),
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['admin-worker-platform-finance-requests'],
|
||||||
|
}),
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['admin-worker-platform-workers'],
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitFinanceConfig(values: FinanceConfigFormValues) {
|
||||||
|
setSavingConfig(true)
|
||||||
|
try {
|
||||||
|
await saveAdminWorkerFinanceConfig({
|
||||||
|
recharge: {
|
||||||
|
enabled: values.recharge?.enabled !== false,
|
||||||
|
channelName: String(values.recharge?.channelName || '').trim(),
|
||||||
|
accountName: String(values.recharge?.accountName || '').trim(),
|
||||||
|
accountNo: String(values.recharge?.accountNo || '').trim(),
|
||||||
|
qrCodeImage: values.recharge?.qrCodeImageList?.[0] || null,
|
||||||
|
instructions: String(values.recharge?.instructions || '').trim(),
|
||||||
|
},
|
||||||
|
withdraw: {
|
||||||
|
enabled: values.withdraw?.enabled !== false,
|
||||||
|
instructions: String(values.withdraw?.instructions || '').trim(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
message.success('资金配置已保存')
|
||||||
|
await refreshAll()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '保存资金配置失败')
|
||||||
|
} finally {
|
||||||
|
setSavingConfig(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openReviewModal(request: WorkerFinanceRequest, action: FinanceReviewAction) {
|
||||||
|
setReviewState({ request, action })
|
||||||
|
reviewForm.setFieldsValue({ reviewedNote: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitFinanceReview(values: { reviewedNote?: string }) {
|
||||||
|
if (!reviewState) return
|
||||||
|
setReviewing(true)
|
||||||
|
try {
|
||||||
|
await reviewAdminWorkerFinanceRequest(reviewState.request.requestId, {
|
||||||
|
status: reviewState.action,
|
||||||
|
reviewedNote: String(values.reviewedNote || '').trim(),
|
||||||
|
})
|
||||||
|
message.success(`${formatFinanceReviewAction(reviewState.action)}成功`)
|
||||||
|
setReviewState(null)
|
||||||
|
reviewForm.resetFields()
|
||||||
|
await refreshAll()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '处理资金申请失败')
|
||||||
|
} finally {
|
||||||
|
setReviewing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestColumns: TableColumnsType<WorkerFinanceRequest> = [
|
||||||
|
{
|
||||||
|
title: '打手',
|
||||||
|
minWidth: 180,
|
||||||
|
render: (_, row) => (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text strong>
|
||||||
|
{row.worker?.displayName || row.worker?.username || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{row.worker?.username || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '类型',
|
||||||
|
width: 120,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Tag color={row.requestType === 'withdraw' ? 'green' : 'blue'}>
|
||||||
|
{formatFinanceRequestType(row.requestType)}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '金额',
|
||||||
|
width: 120,
|
||||||
|
render: (_, row) => formatMoney(row.amount),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '收款信息',
|
||||||
|
minWidth: 240,
|
||||||
|
render: (_, row) => renderFinanceRequestAccount(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '备注',
|
||||||
|
minWidth: 260,
|
||||||
|
render: (_, row) => (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text>{row.note || '-'}</Typography.Text>
|
||||||
|
{row.reviewedNote ? (
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
审核备注:{row.reviewedNote}
|
||||||
|
</Typography.Text>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
width: 120,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Tag color={resolveFinanceRequestStatusColor(row.status)}>
|
||||||
|
{formatFinanceRequestStatus(row.status)}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
width: 180,
|
||||||
|
render: (_, row) => (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text>{formatAdminDateTime(row.createdAt)}</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{row.reviewedAt ? `审核:${formatAdminDateTime(row.reviewedAt)}` : '待审核'}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 240,
|
||||||
|
render: (_, row) =>
|
||||||
|
row.status === 'pending' ? (
|
||||||
|
<Space wrap>
|
||||||
|
<Button type="primary" onClick={() => openReviewModal(row, 'approved')}>
|
||||||
|
通过
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => openReviewModal(row, 'rejected')}>拒绝</Button>
|
||||||
|
<Button danger onClick={() => openReviewModal(row, 'cancelled')}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Typography.Text type="secondary">已处理</Typography.Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="platform-panel-stack">
|
||||||
|
<Card
|
||||||
|
title="资金配置"
|
||||||
|
bordered={false}
|
||||||
|
loading={financeConfigQuery.isLoading}
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
loading={financeConfigQuery.isFetching}
|
||||||
|
onClick={() => financeConfigQuery.refetch()}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{financeConfigQuery.error ? (
|
||||||
|
<Typography.Text type="danger">
|
||||||
|
{financeConfigQuery.error instanceof Error
|
||||||
|
? financeConfigQuery.error.message
|
||||||
|
: '读取资金配置失败'}
|
||||||
|
</Typography.Text>
|
||||||
|
) : null}
|
||||||
|
<Form
|
||||||
|
form={configForm}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={submitFinanceConfig}
|
||||||
|
initialValues={mapFinanceConfigToFormValues()}
|
||||||
|
>
|
||||||
|
<Card title="充值入口" size="small" style={{ marginBottom: 16 }}>
|
||||||
|
<Form.Item
|
||||||
|
label="开启充值申请"
|
||||||
|
name={['recharge', 'enabled']}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="收款通道名称" name={['recharge', 'channelName']}>
|
||||||
|
<Input placeholder="如:支付宝扫码、微信收款" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="收款人" name={['recharge', 'accountName']}>
|
||||||
|
<Input placeholder="请输入收款人姓名" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="收款账号" name={['recharge', 'accountNo']}>
|
||||||
|
<Input placeholder="请输入支付宝账号、微信号或银行卡号" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="收款二维码" name={['recharge', 'qrCodeImageList']}>
|
||||||
|
<ImageUpload scene="worker-finance" scope="admin" maxCount={1} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="充值说明" name={['recharge', 'instructions']}>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={4}
|
||||||
|
placeholder="例如:转账后请填写付款时间、付款尾号、截图说明。"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="提现入口" size="small" style={{ marginBottom: 16 }}>
|
||||||
|
<Form.Item
|
||||||
|
label="开启提现申请"
|
||||||
|
name={['withdraw', 'enabled']}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="提现说明" name={['withdraw', 'instructions']}>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={4}
|
||||||
|
placeholder="例如:每天 18:00 前审核,当日打款;银行卡请备注开户行。"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Button type="primary" htmlType="submit" loading={savingConfig}>
|
||||||
|
保存配置
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title="资金申请"
|
||||||
|
bordered={false}
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
<Input.Search
|
||||||
|
allowClear
|
||||||
|
value={keywordInput}
|
||||||
|
placeholder="搜索打手账号/昵称/手机号"
|
||||||
|
style={{ width: 240 }}
|
||||||
|
onChange={(event) => setKeywordInput(event.target.value)}
|
||||||
|
onSearch={(value) => {
|
||||||
|
setKeyword(String(value || '').trim())
|
||||||
|
setPage(1)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={requestType}
|
||||||
|
style={{ width: 140 }}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: '全部类型' },
|
||||||
|
{ value: 'recharge', label: '充值申请' },
|
||||||
|
{ value: 'withdraw', label: '提现申请' },
|
||||||
|
]}
|
||||||
|
onChange={(nextType) => {
|
||||||
|
setRequestType(nextType)
|
||||||
|
setPage(1)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={requestStatus}
|
||||||
|
style={{ width: 140 }}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: '全部状态' },
|
||||||
|
{ value: 'pending', label: '待处理' },
|
||||||
|
{ value: 'approved', label: '已通过' },
|
||||||
|
{ value: 'rejected', label: '已拒绝' },
|
||||||
|
{ value: 'cancelled', label: '已取消' },
|
||||||
|
]}
|
||||||
|
onChange={(nextStatus) => {
|
||||||
|
setRequestStatus(nextStatus)
|
||||||
|
setPage(1)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
loading={financeRequestsQuery.isFetching}
|
||||||
|
onClick={() => financeRequestsQuery.refetch()}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table<WorkerFinanceRequest>
|
||||||
|
rowKey="requestId"
|
||||||
|
loading={financeRequestsQuery.isLoading}
|
||||||
|
dataSource={financeRequestsQuery.data?.data.items || []}
|
||||||
|
columns={requestColumns}
|
||||||
|
pagination={buildAdminTablePagination({
|
||||||
|
current: financeRequestsPagination?.page || page,
|
||||||
|
pageSize: financeRequestsPagination?.pageSize || pageSize,
|
||||||
|
total: financeRequestsPagination?.total || 0,
|
||||||
|
onChange: (nextPage, nextPageSize) => {
|
||||||
|
setPage(nextPage)
|
||||||
|
setPageSize(nextPageSize)
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
scroll={{ x: 1380 }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={reviewState ? `${formatFinanceReviewAction(reviewState.action)}资金申请` : '处理资金申请'}
|
||||||
|
open={Boolean(reviewState)}
|
||||||
|
destroyOnHidden
|
||||||
|
confirmLoading={reviewing}
|
||||||
|
onCancel={() => {
|
||||||
|
setReviewState(null)
|
||||||
|
reviewForm.resetFields()
|
||||||
|
}}
|
||||||
|
onOk={() => reviewForm.submit()}
|
||||||
|
>
|
||||||
|
{reviewState ? (
|
||||||
|
<Space direction="vertical" size={16} className="full-width">
|
||||||
|
<Descriptions column={1} bordered size="small">
|
||||||
|
<Descriptions.Item label="打手">
|
||||||
|
{reviewState.request.worker?.displayName ||
|
||||||
|
reviewState.request.worker?.username ||
|
||||||
|
'-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="申请类型">
|
||||||
|
{formatFinanceRequestType(reviewState.request.requestType)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="金额">
|
||||||
|
{formatMoney(reviewState.request.amount)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="收款信息">
|
||||||
|
{renderFinanceRequestAccountText(reviewState.request)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="申请备注">
|
||||||
|
{reviewState.request.note || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
|
||||||
|
<Form form={reviewForm} layout="vertical" onFinish={submitFinanceReview}>
|
||||||
|
<Form.Item label="审核备注" name="reviewedNote">
|
||||||
|
<Input.TextArea
|
||||||
|
rows={4}
|
||||||
|
placeholder={`请输入${formatFinanceReviewAction(reviewState.action)}备注`}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Space>
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function LevelsPanel() {
|
function LevelsPanel() {
|
||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -1556,3 +2069,115 @@ function formatWorkerStatus(status: string) {
|
|||||||
}
|
}
|
||||||
return labels[status] || status
|
return labels[status] || status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapFinanceConfigToFormValues(
|
||||||
|
config?: WorkerFinanceConfig,
|
||||||
|
): FinanceConfigFormValues {
|
||||||
|
return {
|
||||||
|
recharge: {
|
||||||
|
enabled: config?.recharge.enabled !== false,
|
||||||
|
channelName: config?.recharge.channelName || '',
|
||||||
|
accountName: config?.recharge.accountName || '',
|
||||||
|
accountNo: config?.recharge.accountNo || '',
|
||||||
|
qrCodeImageList: config?.recharge.qrCodeImage ? [config.recharge.qrCodeImage] : [],
|
||||||
|
instructions: config?.recharge.instructions || '',
|
||||||
|
},
|
||||||
|
withdraw: {
|
||||||
|
enabled: config?.withdraw.enabled !== false,
|
||||||
|
instructions: config?.withdraw.instructions || '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFinanceReviewAction(action: FinanceReviewAction) {
|
||||||
|
if (action === 'approved') return '通过'
|
||||||
|
if (action === 'rejected') return '拒绝'
|
||||||
|
return '取消'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFinanceRequestType(requestType: string) {
|
||||||
|
if (requestType === 'recharge') return '充值申请'
|
||||||
|
if (requestType === 'withdraw') return '提现申请'
|
||||||
|
return requestType || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFinanceRequestStatus(status: string) {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
pending: '待处理',
|
||||||
|
approved: '已通过',
|
||||||
|
rejected: '已拒绝',
|
||||||
|
cancelled: '已取消',
|
||||||
|
}
|
||||||
|
return labels[status] || status || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFinanceRequestStatusColor(status: string) {
|
||||||
|
if (status === 'approved') return 'green'
|
||||||
|
if (status === 'pending') return 'gold'
|
||||||
|
if (status === 'rejected') return 'red'
|
||||||
|
if (status === 'cancelled') return 'default'
|
||||||
|
return 'blue'
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFinanceRequestAccount(request: WorkerFinanceRequest) {
|
||||||
|
return (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text>{renderFinanceRequestAccountText(request)}</Typography.Text>
|
||||||
|
{request.requestType === 'withdraw' && request.accountNo ? (
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{maskFinanceAccount(request.accountNo)}
|
||||||
|
</Typography.Text>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFinanceRequestAccountText(request: WorkerFinanceRequest) {
|
||||||
|
if (request.requestType === 'withdraw') {
|
||||||
|
const channel = formatFinanceChannel(request.accountChannel)
|
||||||
|
const accountName = request.accountName || '-'
|
||||||
|
return `${channel} / ${accountName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const rechargeSnapshot = getRechargeSnapshot(request)
|
||||||
|
if (
|
||||||
|
rechargeSnapshot.channelName ||
|
||||||
|
rechargeSnapshot.accountName ||
|
||||||
|
rechargeSnapshot.accountNo
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
rechargeSnapshot.channelName,
|
||||||
|
rechargeSnapshot.accountName,
|
||||||
|
rechargeSnapshot.accountNo ? maskFinanceAccount(rechargeSnapshot.accountNo) : '',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' / ')
|
||||||
|
}
|
||||||
|
|
||||||
|
return '线下充值,等待管理员核对'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRechargeSnapshot(request: WorkerFinanceRequest) {
|
||||||
|
const payload = asRecord(request.payload)
|
||||||
|
const financeConfig = asRecord(payload.financeConfig)
|
||||||
|
return {
|
||||||
|
channelName: String(financeConfig.channelName || '').trim(),
|
||||||
|
accountName: String(financeConfig.accountName || '').trim(),
|
||||||
|
accountNo: String(financeConfig.accountNo || '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFinanceChannel(channel: string) {
|
||||||
|
if (channel === 'alipay') return '支付宝'
|
||||||
|
if (channel === 'wechat') return '微信收款'
|
||||||
|
if (channel === 'bank') return '银行卡'
|
||||||
|
if (channel === 'manual') return '线下转账'
|
||||||
|
return channel || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskFinanceAccount(value: string) {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) return '-'
|
||||||
|
if (text.length <= 8) return text
|
||||||
|
return `${text.slice(0, 4)} **** ${text.slice(-4)}`
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Descriptions,
|
Descriptions,
|
||||||
Empty,
|
Empty,
|
||||||
Form,
|
Form,
|
||||||
|
Image,
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -113,6 +114,7 @@ export default function WorkerProfilePage() {
|
|||||||
|
|
||||||
const worker = profileQuery.data?.data.worker
|
const worker = profileQuery.data?.data.worker
|
||||||
const summary = profileQuery.data?.data.summary
|
const summary = profileQuery.data?.data.summary
|
||||||
|
const financeConfig = profileQuery.data?.data.financeConfig
|
||||||
const canUseFinanceActions = worker?.status === 'active'
|
const canUseFinanceActions = worker?.status === 'active'
|
||||||
|
|
||||||
const ledgersQuery = useQuery({
|
const ledgersQuery = useQuery({
|
||||||
@@ -179,6 +181,30 @@ export default function WorkerProfilePage() {
|
|||||||
setActiveTab('requests')
|
setActiveTab('requests')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRechargeModal() {
|
||||||
|
if (!canUseFinanceActions) {
|
||||||
|
message.warning('当前账号尚未通过审核,暂时不能发起充值申请')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (financeConfig?.recharge.enabled === false) {
|
||||||
|
message.warning('当前充值入口暂未开放,请联系管理员')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setRechargeOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWithdrawModal() {
|
||||||
|
if (!canUseFinanceActions) {
|
||||||
|
message.warning('当前账号尚未通过审核,暂时不能发起提现申请')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (financeConfig?.withdraw.enabled === false) {
|
||||||
|
message.warning('当前提现入口暂未开放,请联系管理员')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setWithdrawOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
async function submitRecharge(values: RechargeFormValues) {
|
async function submitRecharge(values: RechargeFormValues) {
|
||||||
setSubmittingRecharge(true)
|
setSubmittingRecharge(true)
|
||||||
try {
|
try {
|
||||||
@@ -327,7 +353,7 @@ export default function WorkerProfilePage() {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Typography.Text type="secondary">线下充值,等待管理员入账</Typography.Text>
|
renderRechargeRequestSummary(row)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -452,7 +478,7 @@ export default function WorkerProfilePage() {
|
|||||||
type="primary"
|
type="primary"
|
||||||
icon={<WalletOutlined />}
|
icon={<WalletOutlined />}
|
||||||
disabled={!canUseFinanceActions}
|
disabled={!canUseFinanceActions}
|
||||||
onClick={() => setRechargeOpen(true)}
|
onClick={openRechargeModal}
|
||||||
>
|
>
|
||||||
充值申请
|
充值申请
|
||||||
</Button>
|
</Button>
|
||||||
@@ -460,7 +486,7 @@ export default function WorkerProfilePage() {
|
|||||||
type="primary"
|
type="primary"
|
||||||
icon={<BankOutlined />}
|
icon={<BankOutlined />}
|
||||||
disabled={!canUseFinanceActions}
|
disabled={!canUseFinanceActions}
|
||||||
onClick={() => setWithdrawOpen(true)}
|
onClick={openWithdrawModal}
|
||||||
>
|
>
|
||||||
提现申请
|
提现申请
|
||||||
</Button>
|
</Button>
|
||||||
@@ -549,6 +575,7 @@ export default function WorkerProfilePage() {
|
|||||||
options={[
|
options={[
|
||||||
{ value: '', label: '全部流水' },
|
{ value: '', label: '全部流水' },
|
||||||
{ value: 'manual_credit', label: '人工充值' },
|
{ value: 'manual_credit', label: '人工充值' },
|
||||||
|
{ value: 'withdraw_paid', label: '提现打款' },
|
||||||
{ value: 'deposit_freeze', label: '冻结押金' },
|
{ value: 'deposit_freeze', label: '冻结押金' },
|
||||||
{ value: 'deposit_release', label: '释放押金' },
|
{ value: 'deposit_release', label: '释放押金' },
|
||||||
{ value: 'deposit_deduction', label: '扣除押金' },
|
{ value: 'deposit_deduction', label: '扣除押金' },
|
||||||
@@ -697,6 +724,51 @@ export default function WorkerProfilePage() {
|
|||||||
initialValues={{ amount: undefined, note: '' }}
|
initialValues={{ amount: undefined, note: '' }}
|
||||||
onFinish={submitRecharge}
|
onFinish={submitRecharge}
|
||||||
>
|
>
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type={hasRechargeConfig(financeConfig) ? 'info' : 'warning'}
|
||||||
|
message={
|
||||||
|
hasRechargeConfig(financeConfig)
|
||||||
|
? '请先按下方收款信息完成转账,再提交充值申请,管理员核对后会手动入账。'
|
||||||
|
: '管理员暂未配置完整收款信息,如无法转账请先联系管理员确认。'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size={12}
|
||||||
|
className="full-width"
|
||||||
|
style={{ marginTop: 16, marginBottom: 16 }}
|
||||||
|
>
|
||||||
|
{financeConfig?.recharge.qrCodeImage ? (
|
||||||
|
<Image
|
||||||
|
width={180}
|
||||||
|
src={
|
||||||
|
financeConfig.recharge.qrCodeImage.mediumUrl ||
|
||||||
|
financeConfig.recharge.qrCodeImage.url
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Descriptions column={1} bordered size="small">
|
||||||
|
<Descriptions.Item label="收款通道">
|
||||||
|
{financeConfig?.recharge.channelName || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="收款人">
|
||||||
|
{financeConfig?.recharge.accountName || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="收款账号">
|
||||||
|
{financeConfig?.recharge.accountNo ? (
|
||||||
|
<Typography.Text copyable>
|
||||||
|
{financeConfig.recharge.accountNo}
|
||||||
|
</Typography.Text>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="充值说明">
|
||||||
|
{financeConfig?.recharge.instructions || '请转账后填写备注说明,方便管理员核对。'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Space>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="充值金额"
|
label="充值金额"
|
||||||
name="amount"
|
name="amount"
|
||||||
@@ -730,6 +802,14 @@ export default function WorkerProfilePage() {
|
|||||||
initialValues={{ accountChannel: 'alipay' }}
|
initialValues={{ accountChannel: 'alipay' }}
|
||||||
onFinish={submitWithdraw}
|
onFinish={submitWithdraw}
|
||||||
>
|
>
|
||||||
|
{financeConfig?.withdraw.instructions ? (
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type="info"
|
||||||
|
message={financeConfig.withdraw.instructions}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<Form.Item label="可提现余额">
|
<Form.Item label="可提现余额">
|
||||||
<Typography.Text strong>
|
<Typography.Text strong>
|
||||||
{formatMoney(resolveAvailableForWithdraw(worker, summary))}
|
{formatMoney(resolveAvailableForWithdraw(worker, summary))}
|
||||||
@@ -962,6 +1042,7 @@ function resolveWorkerStatusColor(status: string) {
|
|||||||
function formatLedgerType(ledgerType: string) {
|
function formatLedgerType(ledgerType: string) {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
manual_credit: '人工充值',
|
manual_credit: '人工充值',
|
||||||
|
withdraw_paid: '提现打款',
|
||||||
deposit_freeze: '冻结押金',
|
deposit_freeze: '冻结押金',
|
||||||
deposit_release: '释放押金',
|
deposit_release: '释放押金',
|
||||||
deposit_deduction: '扣除押金',
|
deposit_deduction: '扣除押金',
|
||||||
@@ -972,6 +1053,7 @@ function formatLedgerType(ledgerType: string) {
|
|||||||
|
|
||||||
function resolveLedgerTagColor(ledgerType: string) {
|
function resolveLedgerTagColor(ledgerType: string) {
|
||||||
if (ledgerType === 'manual_credit') return 'blue'
|
if (ledgerType === 'manual_credit') return 'blue'
|
||||||
|
if (ledgerType === 'withdraw_paid') return 'purple'
|
||||||
if (ledgerType === 'deposit_freeze') return 'gold'
|
if (ledgerType === 'deposit_freeze') return 'gold'
|
||||||
if (ledgerType === 'deposit_release') return 'green'
|
if (ledgerType === 'deposit_release') return 'green'
|
||||||
if (ledgerType === 'deposit_deduction') return 'red'
|
if (ledgerType === 'deposit_deduction') return 'red'
|
||||||
@@ -995,6 +1077,46 @@ function formatRequestStatus(status: string) {
|
|||||||
return labels[status] || status || '-'
|
return labels[status] || status || '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasRechargeConfig(
|
||||||
|
financeConfig: {
|
||||||
|
recharge?: { accountName?: string; accountNo?: string; qrCodeImage?: unknown }
|
||||||
|
} | undefined,
|
||||||
|
) {
|
||||||
|
return Boolean(
|
||||||
|
financeConfig?.recharge?.accountName ||
|
||||||
|
financeConfig?.recharge?.accountNo ||
|
||||||
|
financeConfig?.recharge?.qrCodeImage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRechargeRequestSummary(row: WorkerFinanceRequest) {
|
||||||
|
const snapshot = getRechargeSnapshot(row)
|
||||||
|
if (!snapshot.channelName && !snapshot.accountName && !snapshot.accountNo) {
|
||||||
|
return <Typography.Text type="secondary">线下充值,等待管理员入账</Typography.Text>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text>
|
||||||
|
{[snapshot.channelName, snapshot.accountName].filter(Boolean).join(' / ') || '线下充值'}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{snapshot.accountNo ? maskAccountNo(snapshot.accountNo) : '等待管理员核对'}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRechargeSnapshot(row: WorkerFinanceRequest) {
|
||||||
|
const payload = asRecord(row.payload)
|
||||||
|
const financeConfig = asRecord(payload.financeConfig)
|
||||||
|
return {
|
||||||
|
channelName: String(financeConfig.channelName || '').trim(),
|
||||||
|
accountName: String(financeConfig.accountName || '').trim(),
|
||||||
|
accountNo: String(financeConfig.accountNo || '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resolveRequestStatusColor(status: string) {
|
function resolveRequestStatusColor(status: string) {
|
||||||
if (status === 'approved') return 'green'
|
if (status === 'approved') return 'green'
|
||||||
if (status === 'pending') return 'gold'
|
if (status === 'pending') return 'gold'
|
||||||
@@ -1016,3 +1138,9 @@ function maskAccountNo(value: string) {
|
|||||||
if (text.length <= 8) return text
|
if (text.length <= 8) return text
|
||||||
return `${text.slice(0, 4)} **** ${text.slice(-4)}`
|
return `${text.slice(0, 4)} **** ${text.slice(-4)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function asRecord(value: unknown): Record<string, unknown> {
|
||||||
|
return value && typeof value === 'object' && !Array.isArray(value)
|
||||||
|
? (value as Record<string, unknown>)
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type {
|
|||||||
WorkCategory,
|
WorkCategory,
|
||||||
WorkOrder,
|
WorkOrder,
|
||||||
WorkProductRule,
|
WorkProductRule,
|
||||||
|
WorkerFinanceConfig,
|
||||||
|
WorkerFinanceRequest,
|
||||||
WorkerLevel,
|
WorkerLevel,
|
||||||
WorkerListResponse,
|
WorkerListResponse,
|
||||||
WorkerUser,
|
WorkerUser,
|
||||||
@@ -110,6 +112,44 @@ export function creditAdminWorkerWallet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchAdminWorkerFinanceConfig() {
|
||||||
|
return apiGet<WorkerFinanceConfig>('/api/v1/admin/worker-platform/finance-config')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveAdminWorkerFinanceConfig(payload: {
|
||||||
|
recharge?: {
|
||||||
|
enabled?: boolean
|
||||||
|
channelName?: string
|
||||||
|
accountName?: string
|
||||||
|
accountNo?: string
|
||||||
|
qrCodeImage?: unknown
|
||||||
|
instructions?: string
|
||||||
|
}
|
||||||
|
withdraw?: {
|
||||||
|
enabled?: boolean
|
||||||
|
instructions?: string
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
return apiPost<WorkerFinanceConfig>('/api/v1/admin/worker-platform/finance-config', payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchAdminWorkerFinanceRequests(params?: Record<string, unknown>) {
|
||||||
|
return apiGet<WorkerListResponse<WorkerFinanceRequest>>(
|
||||||
|
'/api/v1/admin/worker-platform/finance-requests',
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reviewAdminWorkerFinanceRequest(
|
||||||
|
requestId: number,
|
||||||
|
payload: { status: 'approved' | 'rejected' | 'cancelled'; reviewedNote?: string; note?: string },
|
||||||
|
) {
|
||||||
|
return apiPost<{ request: WorkerFinanceRequest }>(
|
||||||
|
`/api/v1/admin/worker-platform/finance-requests/${requestId}/review`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchAdminWorkOrders(params?: Record<string, unknown>) {
|
export function fetchAdminWorkOrders(params?: Record<string, unknown>) {
|
||||||
return apiGet<WorkerListResponse<WorkOrder>>(
|
return apiGet<WorkerListResponse<WorkOrder>>(
|
||||||
'/api/v1/admin/worker-platform/orders',
|
'/api/v1/admin/worker-platform/orders',
|
||||||
@@ -157,6 +197,12 @@ export function publishAdminWorkOrder(workOrderId: number) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function unpublishAdminWorkOrder(workOrderId: number) {
|
||||||
|
return apiPost<{ order: WorkOrder }>(
|
||||||
|
`/api/v1/admin/worker-platform/orders/${workOrderId}/unpublish`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function markAdminWorkOrderProblem(workOrderId: number, note: string) {
|
export function markAdminWorkOrderProblem(workOrderId: number, note: string) {
|
||||||
return apiPost<{ order: WorkOrder }>(
|
return apiPost<{ order: WorkOrder }>(
|
||||||
`/api/v1/admin/worker-platform/orders/${workOrderId}/problem`,
|
`/api/v1/admin/worker-platform/orders/${workOrderId}/problem`,
|
||||||
|
|||||||
@@ -59,12 +59,34 @@ export type WorkerFinanceRequest = {
|
|||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
reviewedAt: string | null
|
reviewedAt: string | null
|
||||||
|
worker: null | {
|
||||||
|
workerId: number
|
||||||
|
username: string
|
||||||
|
displayName: string
|
||||||
|
phone: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkerFinanceConfig = {
|
||||||
|
recharge: {
|
||||||
|
enabled: boolean
|
||||||
|
channelName: string
|
||||||
|
accountName: string
|
||||||
|
accountNo: string
|
||||||
|
qrCodeImage: UploadedFile | null
|
||||||
|
instructions: string
|
||||||
|
}
|
||||||
|
withdraw: {
|
||||||
|
enabled: boolean
|
||||||
|
instructions: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkerProfileResponse = {
|
export type WorkerProfileResponse = {
|
||||||
worker: WorkerUser
|
worker: WorkerUser
|
||||||
permissions: Record<string, unknown>
|
permissions: Record<string, unknown>
|
||||||
summary: WorkerProfileSummary
|
summary: WorkerProfileSummary
|
||||||
|
financeConfig: WorkerFinanceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkerLevel = {
|
export type WorkerLevel = {
|
||||||
|
|||||||
Reference in New Issue
Block a user