增加管理员联系二维码

This commit is contained in:
yml2213
2026-08-15 16:24:36 +08:00
parent 521cbe9d38
commit 00491daf5b
9 changed files with 72 additions and 105 deletions
-14
View File
@@ -4,7 +4,6 @@ import { createRateLimitMiddleware, getBodyFieldRateLimitKey } from '../middlewa
import { uploadSingleFile } from './file-upload.js' import { uploadSingleFile } from './file-upload.js'
import { uploadFileAsset } from '../services/file-storage/file-storage-service.js' import { uploadFileAsset } from '../services/file-storage/file-storage-service.js'
import { import {
cancelWorkerOrder,
changeWorkerPassword, changeWorkerPassword,
createWorkerRechargeRequest, createWorkerRechargeRequest,
createWorkerWithdrawRequest, createWorkerWithdrawRequest,
@@ -284,19 +283,6 @@ router.post(
), ),
) )
router.post(
'/orders/:workOrderId/cancel',
requireActiveWorker,
createRouteHandler(
(req) => cancelWorkerOrder(String(req.params.workOrderId || ''), getRequiredWorkerSession(req)),
{
successMessage: '已取消接单',
errorMessage: '取消接单失败',
scope: '[worker/orders/:workOrderId/cancel]',
},
),
)
router.use((req, res) => { router.use((req, res) => {
res.status(404).json(buildNotFoundPayload(req)) res.status(404).json(buildNotFoundPayload(req))
}) })
@@ -17,6 +17,9 @@ export type WorkerFinanceConfig = {
qrCodeImage: UploadedFileDto | null qrCodeImage: UploadedFileDto | null
instructions: string instructions: string
} }
adminContact: {
wechatQrCodeImage: UploadedFileDto | null
}
withdraw: { withdraw: {
enabled: boolean enabled: boolean
instructions: string instructions: string
@@ -44,6 +47,11 @@ export async function saveWorkerFinanceConfig(rawValue: unknown): Promise<Worker
export function normalizeWorkerFinanceConfig(rawValue: unknown): WorkerFinanceConfig { export function normalizeWorkerFinanceConfig(rawValue: unknown): WorkerFinanceConfig {
const source = isPlainObject(rawValue) ? rawValue : {} const source = isPlainObject(rawValue) ? rawValue : {}
const recharge = isPlainObject(source.recharge) ? source.recharge : {} const recharge = isPlainObject(source.recharge) ? source.recharge : {}
const adminContact = isPlainObject(source.adminContact)
? source.adminContact
: isPlainObject(source.admin_contact)
? source.admin_contact
: {}
const withdraw = isPlainObject(source.withdraw) ? source.withdraw : {} const withdraw = isPlainObject(source.withdraw) ? source.withdraw : {}
return { return {
@@ -58,6 +66,12 @@ export function normalizeWorkerFinanceConfig(rawValue: unknown): WorkerFinanceCo
), ),
instructions: String(recharge.instructions || '').trim(), instructions: String(recharge.instructions || '').trim(),
}, },
adminContact: {
wechatQrCodeImage: normalizeUploadedFileForStorage(
adminContact.wechatQrCodeImage || adminContact.wechat_qr_code_image,
'管理员微信二维码',
),
},
withdraw: { withdraw: {
enabled: typeof withdraw.enabled === 'boolean' ? withdraw.enabled : true, enabled: typeof withdraw.enabled === 'boolean' ? withdraw.enabled : true,
instructions: String(withdraw.instructions || '').trim(), instructions: String(withdraw.instructions || '').trim(),
@@ -76,6 +90,9 @@ export function createDefaultWorkerFinanceConfig(): WorkerFinanceConfig {
qrCodeImage: null, qrCodeImage: null,
instructions: '', instructions: '',
}, },
adminContact: {
wechatQrCodeImage: null,
},
withdraw: { withdraw: {
enabled: true, enabled: true,
instructions: '', instructions: '',
@@ -92,6 +109,11 @@ function refreshWorkerFinanceConfig(config: WorkerFinanceConfig): WorkerFinanceC
? (refreshUploadedFileUrls(config.recharge.qrCodeImage) as UploadedFileDto) ? (refreshUploadedFileUrls(config.recharge.qrCodeImage) as UploadedFileDto)
: null, : null,
}, },
adminContact: {
wechatQrCodeImage: config.adminContact.wechatQrCodeImage
? (refreshUploadedFileUrls(config.adminContact.wechatQrCodeImage) as UploadedFileDto)
: null,
},
withdraw: { withdraw: {
...config.withdraw, ...config.withdraw,
}, },
@@ -106,7 +128,10 @@ function normalizeRangeInteger(value: unknown, fallback: number, min: number, ma
return Math.min(max, Math.max(min, parsed)) return Math.min(max, Math.max(min, parsed))
} }
function normalizeUploadedFileForStorage(value: unknown): UploadedFileDto | null { function normalizeUploadedFileForStorage(
value: unknown,
fallbackFilename = '收款二维码',
): UploadedFileDto | null {
if (!isPlainObject(value)) { if (!isPlainObject(value)) {
return null return null
} }
@@ -124,7 +149,7 @@ function normalizeUploadedFileForStorage(value: unknown): UploadedFileDto | null
objectKey, objectKey,
), ),
mediumUrl: normalizeStoredFileUrl(value.mediumUrl || value.medium_url, objectKey), mediumUrl: normalizeStoredFileUrl(value.mediumUrl || value.medium_url, objectKey),
filename: String(value.filename || '').trim() || '收款二维码', filename: String(value.filename || '').trim() || fallbackFilename,
contentType: String(value.contentType || value.content_type || '').trim(), contentType: String(value.contentType || value.content_type || '').trim(),
size: Math.max(0, Number(value.size || 0) || 0), size: Math.max(0, Number(value.size || 0) || 0),
} }
@@ -14,9 +14,7 @@ import {
countWorkCategoryUsages, countWorkCategoryUsages,
countWorkerLevelUsages, countWorkerLevelUsages,
countWorkOrderPendingSharingSubmissions, countWorkOrderPendingSharingSubmissions,
countWorkerCancellationsSince,
countWorkerSmsCodesOnDay, countWorkerSmsCodesOnDay,
cancelWorkerWorkOrder,
consumeWorkerSmsCode, consumeWorkerSmsCode,
createWorkerSmsCode, createWorkerSmsCode,
createWorkOrder, createWorkOrder,
@@ -102,7 +100,7 @@ import {
saveWorkerFinanceConfig, saveWorkerFinanceConfig,
} from './worker-finance-config-service.js' } from './worker-finance-config-service.js'
import { DEFAULT_CATEGORY_KEY, DEFAULT_LEVEL_KEY, DEFAULT_LEVEL_NAME, INVITE_CODE_LENGTH, INVITE_CODE_RETRY_LIMIT, WORKER_CANCEL_LIMIT_PER_WINDOW, WORKER_CANCEL_LIMIT_WINDOW_MS, createWorkerSession, ensureWorkerAuthConfigured, hashWorkerPassword, isIgnorableWorkerAuthError, mapCollectLookupOrder, mapFinanceRequest, mapWalletLedger, mapWorkCategory, mapWorkOrderForWorker, mapWorkOrderPublic, mapWorkOrderShare, mapWorkerUser, normalizeAmountFen, normalizeFinanceRequestStatus, normalizeFinanceRequestType, normalizeInteger, normalizeOptionalId, normalizePassword, normalizePositiveInteger, normalizeProofFiles, normalizeSessionVersion, normalizeStringArray, normalizeSubmittedFields, normalizeUploadedFiles, normalizeUsername, normalizeWalletLedgerType, normalizeWithdrawChannel, resolveFreezeDepositAmount, resolveRequirementFields, resolveWorkerPermissions, safeCompare, signWorkerPayload, throwGrabWorkOrderFailure, validateWorkerPassword, validateWorkerUsername, verifyWorkerPassword } from './mappers.js' import { DEFAULT_CATEGORY_KEY, DEFAULT_LEVEL_KEY, DEFAULT_LEVEL_NAME, INVITE_CODE_LENGTH, INVITE_CODE_RETRY_LIMIT, createWorkerSession, ensureWorkerAuthConfigured, hashWorkerPassword, isIgnorableWorkerAuthError, mapCollectLookupOrder, mapFinanceRequest, mapWalletLedger, mapWorkCategory, mapWorkOrderForWorker, mapWorkOrderPublic, mapWorkOrderShare, mapWorkerUser, normalizeAmountFen, normalizeFinanceRequestStatus, normalizeFinanceRequestType, normalizeInteger, normalizeOptionalId, normalizePassword, normalizePositiveInteger, normalizeProofFiles, normalizeSessionVersion, normalizeStringArray, normalizeSubmittedFields, normalizeUploadedFiles, normalizeUsername, normalizeWalletLedgerType, normalizeWithdrawChannel, resolveFreezeDepositAmount, resolveRequirementFields, resolveWorkerPermissions, safeCompare, signWorkerPayload, throwGrabWorkOrderFailure, validateWorkerPassword, validateWorkerUsername, verifyWorkerPassword } from './mappers.js'
export type WorkerSession = { export type WorkerSession = {
sessionId: string sessionId: string
@@ -1086,49 +1084,6 @@ function resolveWorkOrderShareJoinError(
}) })
} }
export async function cancelWorkerOrder(
workOrderId: number | string,
session: WorkerSession,
) {
requireActiveWorkerSession(session)
const worker = await getRequiredWorker(session.workerId)
const now = nowIso()
const since = new Date(Date.now() - WORKER_CANCEL_LIMIT_WINDOW_MS).toISOString()
const cancelCount = await countWorkerCancellationsSince(worker.id, since)
if (cancelCount >= WORKER_CANCEL_LIMIT_PER_WINDOW) {
throw createHttpError(
`30 天内取消接单已达 ${WORKER_CANCEL_LIMIT_PER_WINDOW} 次上限,请谨慎接单`,
{
statusCode: 409,
errorCode: 'worker_cancel_limit_reached',
},
)
}
const result = await cancelWorkerWorkOrder({
workOrderId: Number(workOrderId),
workerId: Number(worker.id),
now,
})
if (result.failureReason === 'work_order_owner_required') {
throw createHttpError('只能取消自己接的订单', {
statusCode: 403,
errorCode: 'work_order_owner_required',
})
}
if (!result.order) {
throw createHttpError('当前订单状态不能取消,仅进行中的订单可取消', {
statusCode: 409,
errorCode: 'work_order_cancel_status_invalid',
})
}
return {
order: mapWorkOrderForWorker(
result.order,
resolveWorkerPermissions(await getRequiredWorker(session.workerId)),
),
}
}
export async function submitWorkerOrderAcceptance( export async function submitWorkerOrderAcceptance(
workOrderId: number | string, workOrderId: number | string,
payload: JsonObject = {}, payload: JsonObject = {},
@@ -92,6 +92,9 @@ type FinanceConfigFormValues = {
qrCodeImageList?: UploadedFile[] qrCodeImageList?: UploadedFile[]
instructions?: string instructions?: string
} }
adminContact?: {
wechatQrCodeImageList?: UploadedFile[]
}
withdraw?: { withdraw?: {
enabled?: boolean enabled?: boolean
instructions?: string instructions?: string
@@ -177,6 +180,9 @@ export default function FinancePanel() {
qrCodeImage: values.recharge?.qrCodeImageList?.[0] || null, qrCodeImage: values.recharge?.qrCodeImageList?.[0] || null,
instructions: String(values.recharge?.instructions || '').trim(), instructions: String(values.recharge?.instructions || '').trim(),
}, },
adminContact: {
wechatQrCodeImage: values.adminContact?.wechatQrCodeImageList?.[0] || null,
},
withdraw: { withdraw: {
enabled: values.withdraw?.enabled !== false, enabled: values.withdraw?.enabled !== false,
instructions: String(values.withdraw?.instructions || '').trim(), instructions: String(values.withdraw?.instructions || '').trim(),
@@ -371,6 +377,15 @@ export default function FinancePanel() {
</Form.Item> </Form.Item>
</Card> </Card>
<Card title="管理员联系" size="small" style={{ marginBottom: 16 }}>
<Form.Item
label="管理员微信二维码"
name={['adminContact', 'wechatQrCodeImageList']}
>
<ImageUpload scene="worker-admin-contact" scope="admin" maxCount={1} />
</Form.Item>
</Card>
<Card title="押金与提现规则" size="small" style={{ marginBottom: 16 }}> <Card title="押金与提现规则" size="small" style={{ marginBottom: 16 }}>
<Form.Item <Form.Item
label="押金解冻天数" label="押金解冻天数"
@@ -551,6 +566,11 @@ function mapFinanceConfigToFormValues(
qrCodeImageList: config?.recharge.qrCodeImage ? [config.recharge.qrCodeImage] : [], qrCodeImageList: config?.recharge.qrCodeImage ? [config.recharge.qrCodeImage] : [],
instructions: config?.recharge.instructions || '', instructions: config?.recharge.instructions || '',
}, },
adminContact: {
wechatQrCodeImageList: config?.adminContact.wechatQrCodeImage
? [config.adminContact.wechatQrCodeImage]
: [],
},
withdraw: { withdraw: {
enabled: config?.withdraw.enabled !== false, enabled: config?.withdraw.enabled !== false,
instructions: config?.withdraw.instructions || '', instructions: config?.withdraw.instructions || '',
@@ -32,7 +32,6 @@ import { DeadlineCountdown } from '@/components/DeadlineCountdown'
import ImagePreviewList from '@/components/files/ImagePreviewList' import ImagePreviewList from '@/components/files/ImagePreviewList'
import ImageUpload from '@/components/files/ImageUpload' import ImageUpload from '@/components/files/ImageUpload'
import { import {
cancelWorkerOrder,
fetchWorkerMyOrders, fetchWorkerMyOrders,
saveWorkerAcceptanceDraft, saveWorkerAcceptanceDraft,
saveWorkerOrderNote, saveWorkerOrderNote,
@@ -73,7 +72,7 @@ type DetailFieldItem = {
} }
export default function WorkerOrdersPage() { export default function WorkerOrdersPage() {
const { message, modal } = App.useApp() const { message } = App.useApp()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [status, setStatus] = useState('') const [status, setStatus] = useState('')
const [keywordInput, setKeywordInput] = useState('') const [keywordInput, setKeywordInput] = useState('')
@@ -239,26 +238,6 @@ export default function WorkerOrdersPage() {
setPage(1) setPage(1)
} }
function openCancelModal(order: WorkOrder) {
modal.confirm({
title: '确认取消接单?',
content:
'取消后订单将重新回到抢单大厅,冻结押金全额退还。30 天内取消次数有限,请谨慎操作。',
okText: '确认取消',
cancelText: '再想想',
okButtonProps: { danger: true },
onOk: async () => {
try {
await cancelWorkerOrder(order.workOrderId)
message.success('已取消接单,押金已退还')
await refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '取消接单失败')
}
},
})
}
const columns: TableColumnsType<WorkOrder> = [ const columns: TableColumnsType<WorkOrder> = [
{ {
title: '订单信息', title: '订单信息',
@@ -464,11 +443,6 @@ export default function WorkerOrdersPage() {
</Button> </Button>
) : null} ) : null}
</Space> </Space>
{canCancelOrder(row) ? (
<Button type="link" danger size="small" onClick={() => openCancelModal(row)}>
</Button>
) : null}
</div> </div>
), ),
}, },
@@ -596,11 +570,6 @@ export default function WorkerOrdersPage() {
<Tag color={resolveStatusColor(detailOrder.status)}> <Tag color={resolveStatusColor(detailOrder.status)}>
{formatStatus(detailOrder.status)} {formatStatus(detailOrder.status)}
</Tag> </Tag>
{canCancelOrder(detailOrder) ? (
<Button danger onClick={() => openCancelModal(detailOrder)}>
</Button>
) : null}
{canEditAcceptanceImages(detailOrder) ? ( {canEditAcceptanceImages(detailOrder) ? (
<Button <Button
type={canSubmitAcceptance(detailOrder) ? 'primary' : 'default'} type={canSubmitAcceptance(detailOrder) ? 'primary' : 'default'}
@@ -851,10 +820,6 @@ function getDisplayOrderNo(order: Pick<WorkOrder, 'platformOrderId'> | null | un
return String(order?.platformOrderId || '').trim() || '-' return String(order?.platformOrderId || '').trim() || '-'
} }
function canCancelOrder(order: WorkOrder) {
return !order.myShare && order.status === 'in_progress'
}
function getRequirementFields(order: WorkOrder | null): CollectField[] { function getRequirementFields(order: WorkOrder | null): CollectField[] {
if (!order) return [] if (!order) return []
const rawFields = Array.isArray(order.requirement?.fields) const rawFields = Array.isArray(order.requirement?.fields)
@@ -1027,8 +1027,24 @@ export default function WorkerProfilePage() {
<Alert <Alert
showIcon showIcon
type="info" type="info"
message="充值、提现与审核问题目前都由后台管理员人工处理。提交申请后,如需加急,可把下方账号信息发给管理员。" message="如需取消已接订单,请扫码联系管理员处理。充值、提现与审核问题由后台管理员人工处理。"
/> />
{financeConfig?.adminContact?.wechatQrCodeImage ? (
<div style={{ textAlign: 'center' }}>
<Image
width={200}
src={
financeConfig.adminContact.wechatQrCodeImage.mediumUrl ||
financeConfig.adminContact.wechatQrCodeImage.url
}
alt="管理员微信二维码"
/>
</div>
) : (
<Typography.Text type="secondary">
</Typography.Text>
)}
<Descriptions column={1} bordered size="small"> <Descriptions column={1} bordered size="small">
<Descriptions.Item label="账号"> <Descriptions.Item label="账号">
<Typography.Text copyable>{worker?.username || '-'}</Typography.Text> <Typography.Text copyable>{worker?.username || '-'}</Typography.Text>
@@ -179,6 +179,9 @@ export function saveAdminWorkerFinanceConfig(payload: {
qrCodeImage?: unknown qrCodeImage?: unknown
instructions?: string instructions?: string
} }
adminContact?: {
wechatQrCodeImage?: unknown
}
withdraw?: { withdraw?: {
enabled?: boolean enabled?: boolean
instructions?: string instructions?: string
-6
View File
@@ -153,12 +153,6 @@ export function saveWorkerAcceptanceDraft(
) )
} }
export function cancelWorkerOrder(workOrderId: number) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/worker/orders/${workOrderId}/cancel`,
)
}
export function lookupCollectOrder(orderNo: string) { export function lookupCollectOrder(orderNo: string) {
return apiPost<CollectLookupResponse>('/api/v1/collect/lookup', { orderNo }) return apiPost<CollectLookupResponse>('/api/v1/collect/lookup', { orderNo })
} }
@@ -95,6 +95,9 @@ export type WorkerFinanceConfig = {
qrCodeImage: UploadedFile | null qrCodeImage: UploadedFile | null
instructions: string instructions: string
} }
adminContact: {
wechatQrCodeImage: UploadedFile | null
}
withdraw: { withdraw: {
enabled: boolean enabled: boolean
instructions: string instructions: string