支持后台修改打手提现信息
This commit is contained in:
@@ -497,6 +497,38 @@ export async function createWorkerWithdrawalAccount(input: {
|
||||
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(
|
||||
requestId: number | string,
|
||||
): Promise<WorkerFinanceRequestRow | null> {
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
getAdminWorkOrderEvents,
|
||||
getAdminWorkOrderSharing,
|
||||
listAdminWorkerFinanceRequests,
|
||||
listAdminWorkerWithdrawalAccounts,
|
||||
listAdminWorkCategories,
|
||||
listAdminWorkProductRules,
|
||||
listAdminWorkerLevels,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
resolveAdminProblemWorkOrder,
|
||||
reviewAdminWorkerUser,
|
||||
saveAdminWorkerFinanceConfig,
|
||||
saveAdminWorkerWithdrawalAccount,
|
||||
saveAdminWorkCategory,
|
||||
saveAdminWorkProductRule,
|
||||
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(
|
||||
'/worker-platform/finance-config',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
listWorkProductRules,
|
||||
listWorkerLevels,
|
||||
listWorkerUsers,
|
||||
listWorkerWithdrawalAccounts,
|
||||
listWorkOrderShares,
|
||||
listWorkOrderSharesByOrderIds,
|
||||
sumPendingUnfreezeByOrderIds,
|
||||
@@ -40,11 +41,13 @@ import {
|
||||
updateWorkOrder,
|
||||
updateWorkOrderBasic,
|
||||
updateWorkerUser,
|
||||
upsertWorkerWithdrawalAccount,
|
||||
upsertWorkCategory,
|
||||
upsertWorkProductRule,
|
||||
upsertWorkerLevel,
|
||||
type WorkOrderRow,
|
||||
type WorkOrderShareRow,
|
||||
type WorkerWithdrawalAccountRow,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.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 { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import { getWorkerFinanceConfig, saveWorkerFinanceConfig } from './worker-finance-config-service.js'
|
||||
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
|
||||
import {
|
||||
consumeIndustryVouchersBeforeWorkOrderAssign,
|
||||
consumeIndustryVouchersBeforeWorkOrderPublish,
|
||||
@@ -81,6 +85,7 @@ import {
|
||||
normalizeMatchType,
|
||||
normalizeOptionalId,
|
||||
normalizePositiveInteger,
|
||||
normalizeProofFiles,
|
||||
normalizeProblemResolutionAction,
|
||||
normalizeRequirementFields,
|
||||
normalizeRequirementFieldsFromPayload,
|
||||
@@ -91,6 +96,7 @@ import {
|
||||
normalizeSubmittedFields,
|
||||
normalizeUploadedFiles,
|
||||
normalizeWorkerType,
|
||||
normalizeWithdrawChannel,
|
||||
resolveFreezeDepositAmount,
|
||||
mapWorkOrderEvents,
|
||||
resolveMatchingProductRule,
|
||||
@@ -524,6 +530,71 @@ export async function creditAdminWorkerWallet(workerId: number | string, payload
|
||||
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() {
|
||||
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) {
|
||||
await getRequiredWorkOrder(workOrderId)
|
||||
const events = await listWorkOrderEventsByOrderId(workOrderId)
|
||||
|
||||
@@ -21,12 +21,26 @@ import {
|
||||
creditAdminWorkerWallet,
|
||||
fetchAdminWorkerLevels,
|
||||
fetchAdminWorkerUsers,
|
||||
fetchAdminWorkerWithdrawalAccounts,
|
||||
reviewAdminWorkerUser,
|
||||
saveAdminWorkerWithdrawalAccount,
|
||||
} 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 { formatMoney, formatWorkerStatus } from './shared'
|
||||
|
||||
type WithdrawalAccountFormValues = {
|
||||
accountChannel?: string
|
||||
accountName?: string
|
||||
accountNo?: string
|
||||
wechatQrCodeImages?: UploadedFile[]
|
||||
}
|
||||
|
||||
export default function WorkersPanel() {
|
||||
const { message, modal } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -35,8 +49,14 @@ export default function WorkersPanel() {
|
||||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||
const [creditWorker, setCreditWorker] = 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 [levelForm] = Form.useForm<{ levelId?: number }>()
|
||||
const [withdrawalAccountForm] = Form.useForm<WithdrawalAccountFormValues>()
|
||||
const withdrawalAccountChannel = Form.useWatch('accountChannel', withdrawalAccountForm)
|
||||
|
||||
const workersQuery = useQuery({
|
||||
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> = [
|
||||
{
|
||||
title: '打手',
|
||||
@@ -214,6 +290,7 @@ export default function WorkersPanel() {
|
||||
调等级
|
||||
</Button>
|
||||
<Button onClick={() => setCreditWorker(row)}>充值</Button>
|
||||
<Button onClick={() => void openWithdrawalAccountModal(row)}>提现信息</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -267,6 +344,65 @@ export default function WorkersPanel() {
|
||||
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
|
||||
title="调整打手等级"
|
||||
open={Boolean(levelWorker)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { apiDelete, apiGet, apiPost, apiPut } from '@/lib/http'
|
||||
import type {
|
||||
UploadedFile,
|
||||
AdminWorkerWithdrawalAccount,
|
||||
WorkCategory,
|
||||
WorkOrder,
|
||||
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 }) {
|
||||
return apiPost<{
|
||||
order: WorkOrder
|
||||
|
||||
@@ -113,6 +113,14 @@ export type WorkerWithdrawalAccount = {
|
||||
createdAt: string | null
|
||||
}
|
||||
|
||||
export type AdminWorkerWithdrawalAccount = {
|
||||
accountChannel: string
|
||||
accountName: string
|
||||
accountNo: string
|
||||
wechatQrCodeImage: UploadedFile | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type WorkerProfileResponse = {
|
||||
worker: WorkerUser
|
||||
permissions: Record<string, unknown>
|
||||
|
||||
Reference in New Issue
Block a user