支持后台修改打手提现信息

This commit is contained in:
yml2213
2026-08-17 11:28:23 +08:00
parent ea5c2a4105
commit 77ed97fe1f
6 changed files with 317 additions and 1 deletions
@@ -497,6 +497,38 @@ export async function createWorkerWithdrawalAccount(input: {
return result.rows[0] || null return result.rows[0] || null
} }
export async function upsertWorkerWithdrawalAccount(input: {
workerId: number
accountChannel: string
accountName: string
accountNo: string
wechatQrCodeJson: string
now: string
}): Promise<WorkerWithdrawalAccountRow | null> {
const result = await query<WorkerWithdrawalAccountRow>(
`
INSERT INTO worker_withdrawal_accounts (
worker_id, account_channel, account_name, account_no, wechat_qr_code_json, created_at
) VALUES ($1, $2, $3, $4, $5::jsonb, $6)
ON CONFLICT (worker_id, account_channel) DO UPDATE
SET
account_name = EXCLUDED.account_name,
account_no = EXCLUDED.account_no,
wechat_qr_code_json = EXCLUDED.wechat_qr_code_json
RETURNING *
`,
[
input.workerId,
input.accountChannel,
input.accountName,
input.accountNo,
input.wechatQrCodeJson,
input.now,
],
)
return result.rows[0] || null
}
export async function getWorkerFinanceRequestById( export async function getWorkerFinanceRequestById(
requestId: number | string, requestId: number | string,
): Promise<WorkerFinanceRequestRow | null> { ): Promise<WorkerFinanceRequestRow | null> {
@@ -18,6 +18,7 @@ import {
getAdminWorkOrderEvents, getAdminWorkOrderEvents,
getAdminWorkOrderSharing, getAdminWorkOrderSharing,
listAdminWorkerFinanceRequests, listAdminWorkerFinanceRequests,
listAdminWorkerWithdrawalAccounts,
listAdminWorkCategories, listAdminWorkCategories,
listAdminWorkProductRules, listAdminWorkProductRules,
listAdminWorkerLevels, listAdminWorkerLevels,
@@ -30,6 +31,7 @@ import {
resolveAdminProblemWorkOrder, resolveAdminProblemWorkOrder,
reviewAdminWorkerUser, reviewAdminWorkerUser,
saveAdminWorkerFinanceConfig, saveAdminWorkerFinanceConfig,
saveAdminWorkerWithdrawalAccount,
saveAdminWorkCategory, saveAdminWorkCategory,
saveAdminWorkProductRule, saveAdminWorkProductRule,
saveAdminWorkerLevel, saveAdminWorkerLevel,
@@ -227,6 +229,40 @@ router.post(
), ),
) )
router.get(
'/worker-platform/workers/:workerId/withdrawal-accounts',
requireAdminRoles(['admin', 'operator']),
createJsonHandler((req) => listAdminWorkerWithdrawalAccounts(String(req.params.workerId || '')), {
successMessage: 'ok',
errorMessage: '读取打手提现信息失败',
scope: '[admin/worker-platform/workers/:workerId/withdrawal-accounts]',
}),
)
router.post(
'/worker-platform/workers/:workerId/withdrawal-accounts/:accountChannel',
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) =>
saveAdminWorkerWithdrawalAccount(
String(req.params.workerId || ''),
String(req.params.accountChannel || ''),
req.body || {},
),
{
successMessage: '打手提现信息已保存',
errorMessage: '保存打手提现信息失败',
scope: '[admin/worker-platform/workers/:workerId/withdrawal-accounts/:accountChannel]',
audit: (req) => ({
action: 'worker_withdrawal_account_saved',
targetType: 'worker',
targetId: String(req.params.workerId || ''),
data: { accountChannel: String(req.params.accountChannel || '') },
}),
},
),
)
router.get( router.get(
'/worker-platform/finance-config', '/worker-platform/finance-config',
requireAdminRoles(['admin', 'operator', 'support']), requireAdminRoles(['admin', 'operator', 'support']),
@@ -32,6 +32,7 @@ import {
listWorkProductRules, listWorkProductRules,
listWorkerLevels, listWorkerLevels,
listWorkerUsers, listWorkerUsers,
listWorkerWithdrawalAccounts,
listWorkOrderShares, listWorkOrderShares,
listWorkOrderSharesByOrderIds, listWorkOrderSharesByOrderIds,
sumPendingUnfreezeByOrderIds, sumPendingUnfreezeByOrderIds,
@@ -40,11 +41,13 @@ import {
updateWorkOrder, updateWorkOrder,
updateWorkOrderBasic, updateWorkOrderBasic,
updateWorkerUser, updateWorkerUser,
upsertWorkerWithdrawalAccount,
upsertWorkCategory, upsertWorkCategory,
upsertWorkProductRule, upsertWorkProductRule,
upsertWorkerLevel, upsertWorkerLevel,
type WorkOrderRow, type WorkOrderRow,
type WorkOrderShareRow, type WorkOrderShareRow,
type WorkerWithdrawalAccountRow,
} from '../../repositories/worker-platform/index.js' } from '../../repositories/worker-platform/index.js'
import type { JsonObject } from '../../types/json.js' import type { JsonObject } from '../../types/json.js'
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js' import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
@@ -53,6 +56,7 @@ import { randomId } from '../../utils/random.js'
import { nowIso } from '../../utils/time.js' import { nowIso } from '../../utils/time.js'
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js' import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
import { getWorkerFinanceConfig, saveWorkerFinanceConfig } from './worker-finance-config-service.js' import { getWorkerFinanceConfig, saveWorkerFinanceConfig } from './worker-finance-config-service.js'
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
import { import {
consumeIndustryVouchersBeforeWorkOrderAssign, consumeIndustryVouchersBeforeWorkOrderAssign,
consumeIndustryVouchersBeforeWorkOrderPublish, consumeIndustryVouchersBeforeWorkOrderPublish,
@@ -81,6 +85,7 @@ import {
normalizeMatchType, normalizeMatchType,
normalizeOptionalId, normalizeOptionalId,
normalizePositiveInteger, normalizePositiveInteger,
normalizeProofFiles,
normalizeProblemResolutionAction, normalizeProblemResolutionAction,
normalizeRequirementFields, normalizeRequirementFields,
normalizeRequirementFieldsFromPayload, normalizeRequirementFieldsFromPayload,
@@ -91,6 +96,7 @@ import {
normalizeSubmittedFields, normalizeSubmittedFields,
normalizeUploadedFiles, normalizeUploadedFiles,
normalizeWorkerType, normalizeWorkerType,
normalizeWithdrawChannel,
resolveFreezeDepositAmount, resolveFreezeDepositAmount,
mapWorkOrderEvents, mapWorkOrderEvents,
resolveMatchingProductRule, resolveMatchingProductRule,
@@ -524,6 +530,71 @@ export async function creditAdminWorkerWallet(workerId: number | string, payload
return { wallet: mapWallet(wallet) } return { wallet: mapWallet(wallet) }
} }
export async function listAdminWorkerWithdrawalAccounts(workerId: number | string) {
const worker = await getRequiredWorker(workerId)
const items = await listWorkerWithdrawalAccounts(worker.id)
return {
worker: mapWorkerUser(worker),
items: items.map(mapAdminWorkerWithdrawalAccount),
}
}
export async function saveAdminWorkerWithdrawalAccount(
workerId: number | string,
accountChannelValue: unknown,
payload: JsonObject = {},
) {
const worker = await getRequiredWorker(workerId)
const accountChannel = normalizeWithdrawChannel(accountChannelValue)
const currentAccounts = await listWorkerWithdrawalAccounts(worker.id)
const current = currentAccounts.find((item) => item.account_channel === accountChannel)
const accountName = String(payload.accountName || payload.realName || '').trim()
const accountNo =
accountChannel === 'alipay' ? String(payload.accountNo || payload.account || '').trim() : ''
const uploadedWechatQrCode = normalizeProofFiles(
payload.wechatQrCodeImages || payload.wechatQrCodes,
)[0]
const wechatQrCodeImage =
accountChannel === 'wechat'
? uploadedWechatQrCode || safeParseJson(current?.wechat_qr_code_json)
: {}
if (!accountName) {
throw createHttpError('请填写收款人姓名', {
statusCode: 400,
errorCode: 'admin_worker_withdraw_account_name_required',
})
}
if (accountChannel === 'alipay' && !accountNo) {
throw createHttpError('请填写支付宝账号', {
statusCode: 400,
errorCode: 'admin_worker_withdraw_alipay_account_required',
})
}
if (accountChannel === 'wechat' && !String(wechatQrCodeImage.url || '').trim()) {
throw createHttpError('请上传微信收款二维码', {
statusCode: 400,
errorCode: 'admin_worker_withdraw_wechat_qr_required',
})
}
const saved = await upsertWorkerWithdrawalAccount({
workerId: worker.id,
accountChannel,
accountName,
accountNo,
wechatQrCodeJson: JSON.stringify(wechatQrCodeImage),
now: nowIso(),
})
if (!saved) {
throw createHttpError('保存打手提现信息失败', {
statusCode: 500,
errorCode: 'admin_worker_withdraw_account_save_failed',
})
}
return { account: mapAdminWorkerWithdrawalAccount(saved) }
}
export async function getAdminWorkerFinanceConfig() { export async function getAdminWorkerFinanceConfig() {
return getWorkerFinanceConfig() return getWorkerFinanceConfig()
} }
@@ -599,6 +670,17 @@ export async function reviewAdminWorkerFinanceRequest(
} }
} }
function mapAdminWorkerWithdrawalAccount(account: WorkerWithdrawalAccountRow) {
const wechatQrCodeImage = refreshUploadedFileUrls(safeParseJson(account.wechat_qr_code_json))
return {
accountChannel: account.account_channel,
accountName: account.account_name,
accountNo: account.account_channel === 'alipay' ? account.account_no : '',
wechatQrCodeImage: String(wechatQrCodeImage.url || '').trim() ? wechatQrCodeImage : null,
createdAt: account.created_at,
}
}
export async function getAdminWorkOrderEvents(workOrderId: number | string) { export async function getAdminWorkOrderEvents(workOrderId: number | string) {
await getRequiredWorkOrder(workOrderId) await getRequiredWorkOrder(workOrderId)
const events = await listWorkOrderEventsByOrderId(workOrderId) const events = await listWorkOrderEventsByOrderId(workOrderId)
@@ -21,12 +21,26 @@ import {
creditAdminWorkerWallet, creditAdminWorkerWallet,
fetchAdminWorkerLevels, fetchAdminWorkerLevels,
fetchAdminWorkerUsers, fetchAdminWorkerUsers,
fetchAdminWorkerWithdrawalAccounts,
reviewAdminWorkerUser, reviewAdminWorkerUser,
saveAdminWorkerWithdrawalAccount,
} from '@/services/admin' } from '@/services/admin'
import type { WorkerUser } from '@/types/worker-platform' import ImageUpload from '@/components/files/ImageUpload'
import type {
AdminWorkerWithdrawalAccount,
UploadedFile,
WorkerUser,
} from '@/types/worker-platform'
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
import { formatMoney, formatWorkerStatus } from './shared' import { formatMoney, formatWorkerStatus } from './shared'
type WithdrawalAccountFormValues = {
accountChannel?: string
accountName?: string
accountNo?: string
wechatQrCodeImages?: UploadedFile[]
}
export default function WorkersPanel() { export default function WorkersPanel() {
const { message, modal } = App.useApp() const { message, modal } = App.useApp()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -35,8 +49,14 @@ export default function WorkersPanel() {
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 [levelWorker, setLevelWorker] = useState<WorkerUser | null>(null)
const [withdrawalAccountWorker, setWithdrawalAccountWorker] = useState<WorkerUser | null>(null)
const [withdrawalAccounts, setWithdrawalAccounts] = useState<AdminWorkerWithdrawalAccount[]>([])
const [loadingWithdrawalAccounts, setLoadingWithdrawalAccounts] = useState(false)
const [savingWithdrawalAccount, setSavingWithdrawalAccount] = useState(false)
const [creditForm] = Form.useForm() const [creditForm] = Form.useForm()
const [levelForm] = Form.useForm<{ levelId?: number }>() const [levelForm] = Form.useForm<{ levelId?: number }>()
const [withdrawalAccountForm] = Form.useForm<WithdrawalAccountFormValues>()
const withdrawalAccountChannel = Form.useWatch('accountChannel', withdrawalAccountForm)
const workersQuery = useQuery({ const workersQuery = useQuery({
queryKey: ['admin-worker-platform-workers', status, page, pageSize], queryKey: ['admin-worker-platform-workers', status, page, pageSize],
@@ -126,6 +146,62 @@ export default function WorkersPanel() {
} }
} }
async function openWithdrawalAccountModal(worker: WorkerUser) {
setWithdrawalAccountWorker(worker)
setWithdrawalAccounts([])
setLoadingWithdrawalAccounts(true)
try {
const response = await fetchAdminWorkerWithdrawalAccounts(worker.workerId)
const accounts = response.data.items
setWithdrawalAccounts(accounts)
setWithdrawalAccountFormValues('alipay', accounts)
} catch (error) {
message.error(error instanceof Error ? error.message : '读取提现信息失败')
setWithdrawalAccountWorker(null)
} finally {
setLoadingWithdrawalAccounts(false)
}
}
function setWithdrawalAccountFormValues(accountChannel: string, accounts = withdrawalAccounts) {
const account = accounts.find((item) => item.accountChannel === accountChannel)
withdrawalAccountForm.setFieldsValue({
accountChannel,
accountName: account?.accountName || '',
accountNo: account?.accountNo || '',
wechatQrCodeImages: account?.wechatQrCodeImage ? [account.wechatQrCodeImage] : [],
})
}
async function submitWithdrawalAccount(values: WithdrawalAccountFormValues) {
if (!withdrawalAccountWorker) return
const accountChannel = String(values.accountChannel || '')
setSavingWithdrawalAccount(true)
try {
const response = await saveAdminWorkerWithdrawalAccount(
withdrawalAccountWorker.workerId,
accountChannel,
{
accountName: String(values.accountName || '').trim(),
accountNo: String(values.accountNo || '').trim(),
wechatQrCodeImages: values.wechatQrCodeImages || [],
},
)
const account = response.data.account
const nextAccounts = [
...withdrawalAccounts.filter((item) => item.accountChannel !== account.accountChannel),
account,
]
setWithdrawalAccounts(nextAccounts)
setWithdrawalAccountFormValues(account.accountChannel, nextAccounts)
message.success('提现信息已保存')
} catch (error) {
message.error(error instanceof Error ? error.message : '保存提现信息失败')
} finally {
setSavingWithdrawalAccount(false)
}
}
const columns: TableColumnsType<WorkerUser> = [ const columns: TableColumnsType<WorkerUser> = [
{ {
title: '打手', title: '打手',
@@ -214,6 +290,7 @@ export default function WorkersPanel() {
</Button> </Button>
<Button onClick={() => setCreditWorker(row)}></Button> <Button onClick={() => setCreditWorker(row)}></Button>
<Button onClick={() => void openWithdrawalAccountModal(row)}></Button>
</Space> </Space>
), ),
}, },
@@ -267,6 +344,65 @@ export default function WorkersPanel() {
scroll={{ x: 1000 }} scroll={{ x: 1000 }}
/> />
<Modal
title="编辑打手提现信息"
open={Boolean(withdrawalAccountWorker)}
confirmLoading={savingWithdrawalAccount}
onCancel={() => {
setWithdrawalAccountWorker(null)
setWithdrawalAccounts([])
withdrawalAccountForm.resetFields()
}}
onOk={() => withdrawalAccountForm.submit()}
destroyOnHidden
>
<Form form={withdrawalAccountForm} layout="vertical" onFinish={submitWithdrawalAccount}>
<Form.Item label="打手">
<Typography.Text>
{withdrawalAccountWorker?.displayName || withdrawalAccountWorker?.username || '-'}
</Typography.Text>
</Form.Item>
<Form.Item
label="提现方式"
name="accountChannel"
rules={[{ required: true, message: '请选择提现方式' }]}
>
<Select
loading={loadingWithdrawalAccounts}
options={[
{ value: 'alipay', label: '支付宝' },
{ value: 'wechat', label: '微信收款' },
]}
onChange={(accountChannel) => setWithdrawalAccountFormValues(accountChannel)}
/>
</Form.Item>
<Form.Item
label="收款人姓名"
name="accountName"
rules={[{ required: true, message: '请输入收款人姓名' }]}
>
<Input placeholder="请输入真实姓名" />
</Form.Item>
{withdrawalAccountChannel === 'wechat' ? (
<Form.Item
label="微信收款二维码"
name="wechatQrCodeImages"
rules={[{ required: true, message: '请上传微信收款二维码' }]}
>
<ImageUpload scene="admin-worker-withdrawal-wechat-qr" scope="admin" maxCount={1} />
</Form.Item>
) : (
<Form.Item
label="支付宝账号"
name="accountNo"
rules={[{ required: true, message: '请输入支付宝账号' }]}
>
<Input placeholder="请输入支付宝账号" />
</Form.Item>
)}
</Form>
</Modal>
<Modal <Modal
title="调整打手等级" title="调整打手等级"
open={Boolean(levelWorker)} open={Boolean(levelWorker)}
@@ -1,6 +1,7 @@
import { apiDelete, apiGet, apiPost, apiPut } from '@/lib/http' import { apiDelete, apiGet, apiPost, apiPut } from '@/lib/http'
import type { import type {
UploadedFile, UploadedFile,
AdminWorkerWithdrawalAccount,
WorkCategory, WorkCategory,
WorkOrder, WorkOrder,
WorkOrderEvent, WorkOrderEvent,
@@ -119,6 +120,27 @@ export function reviewAdminWorkerUser(
) )
} }
export function fetchAdminWorkerWithdrawalAccounts(workerId: number) {
return apiGet<{ worker: WorkerUser; items: AdminWorkerWithdrawalAccount[] }>(
`/api/v1/admin/worker-platform/workers/${workerId}/withdrawal-accounts`,
)
}
export function saveAdminWorkerWithdrawalAccount(
workerId: number,
accountChannel: string,
payload: {
accountName: string
accountNo?: string
wechatQrCodeImages?: UploadedFile[]
},
) {
return apiPost<{ account: AdminWorkerWithdrawalAccount }>(
`/api/v1/admin/worker-platform/workers/${workerId}/withdrawal-accounts/${accountChannel}`,
payload,
)
}
export function assignAdminWorkOrderToWorker(workOrderId: number, payload: { workerId: number }) { export function assignAdminWorkOrderToWorker(workOrderId: number, payload: { workerId: number }) {
return apiPost<{ return apiPost<{
order: WorkOrder order: WorkOrder
@@ -113,6 +113,14 @@ export type WorkerWithdrawalAccount = {
createdAt: string | null createdAt: string | null
} }
export type AdminWorkerWithdrawalAccount = {
accountChannel: string
accountName: string
accountNo: string
wechatQrCodeImage: UploadedFile | null
createdAt: string
}
export type WorkerProfileResponse = { export type WorkerProfileResponse = {
worker: WorkerUser worker: WorkerUser
permissions: Record<string, unknown> permissions: Record<string, unknown>