From 77ed97fe1f82c69b57ba4c568becef0f43d9167b Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 17 Aug 2026 11:28:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=90=8E=E5=8F=B0=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E6=89=93=E6=89=8B=E6=8F=90=E7=8E=B0=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../worker-platform/worker-repo.ts | 32 ++++ .../src/routes/admin/worker-platform.ts | 36 +++++ .../services/worker-platform/admin-service.ts | 82 +++++++++++ .../src/pages/admin/panels/WorkersPanel.tsx | 138 +++++++++++++++++- .../src/services/admin/worker-platform.ts | 22 +++ apps/frontend/src/types/worker-platform.ts | 8 + 6 files changed, 317 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/repositories/worker-platform/worker-repo.ts b/apps/backend/src/repositories/worker-platform/worker-repo.ts index 23ef9371..c8d08c46 100644 --- a/apps/backend/src/repositories/worker-platform/worker-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-repo.ts @@ -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 { + const result = await query( + ` + 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 { diff --git a/apps/backend/src/routes/admin/worker-platform.ts b/apps/backend/src/routes/admin/worker-platform.ts index a8c66d72..d7dcb68d 100644 --- a/apps/backend/src/routes/admin/worker-platform.ts +++ b/apps/backend/src/routes/admin/worker-platform.ts @@ -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']), diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index f059f41c..ada9f610 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -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) diff --git a/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx index 7e909640..eeb3a03a 100644 --- a/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx @@ -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(null) const [levelWorker, setLevelWorker] = useState(null) + const [withdrawalAccountWorker, setWithdrawalAccountWorker] = useState(null) + const [withdrawalAccounts, setWithdrawalAccounts] = useState([]) + const [loadingWithdrawalAccounts, setLoadingWithdrawalAccounts] = useState(false) + const [savingWithdrawalAccount, setSavingWithdrawalAccount] = useState(false) const [creditForm] = Form.useForm() const [levelForm] = Form.useForm<{ levelId?: number }>() + const [withdrawalAccountForm] = Form.useForm() + 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 = [ { title: '打手', @@ -214,6 +290,7 @@ export default function WorkersPanel() { 调等级 + ), }, @@ -267,6 +344,65 @@ export default function WorkersPanel() { scroll={{ x: 1000 }} /> + { + setWithdrawalAccountWorker(null) + setWithdrawalAccounts([]) + withdrawalAccountForm.resetFields() + }} + onOk={() => withdrawalAccountForm.submit()} + destroyOnHidden + > +
+ + + {withdrawalAccountWorker?.displayName || withdrawalAccountWorker?.username || '-'} + + + + + + {withdrawalAccountChannel === 'wechat' ? ( + + + + ) : ( + + + + )} +
+
+ ( + `/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 diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index e16b3ef7..6fe6fbd1 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -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