feat(worker-platform): 增加寄卖白名单总开关,关闭后全员可发布

资金配置与打手列表可一键切换白名单模式;发布校验、打手端入口按总开关放行。同时收紧未开通判定、关权限确认和开通弹窗文案。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-21 14:23:00 +08:00
co-authored by Cursor
parent 937e51db6b
commit 9298b31b9b
17 changed files with 342 additions and 56 deletions
@@ -10,8 +10,12 @@ export type WorkerContactModalProps = {
worker?: WorkerUser | null
financeConfig?: WorkerFinanceConfig | null
title?: string
alertMessage?: string
}
const DEFAULT_ALERT_MESSAGE =
'如需取消已接订单,请扫码联系客服处理。充值、提现与审核问题也由后台人工客服处理。'
export function WorkerContactModal({
open,
onClose,
@@ -19,6 +23,7 @@ export function WorkerContactModal({
worker,
financeConfig,
title = '联系客服',
alertMessage = DEFAULT_ALERT_MESSAGE,
}: WorkerContactModalProps) {
return (
<Modal
@@ -34,11 +39,7 @@ export function WorkerContactModal({
onCancel={onClose}
>
<Space direction="vertical" size={16} className="full-width">
<Alert
showIcon
type="info"
message="如需取消已接订单,请扫码联系客服处理。充值、提现与审核问题也由后台人工客服处理。"
/>
<Alert showIcon type="info" message={alertMessage} />
{financeConfig?.adminContact?.wechatQrCodeImage ? (
<div style={{ textAlign: 'center' }}>
<Image
@@ -44,6 +44,7 @@ type FinanceConfigFormValues = {
depositUnfreezeDays?: number
rewardUnfreezeEnabled?: boolean
consignmentPublishDepositYuan?: number
consignmentWhitelistEnabled?: boolean
recharge?: {
enabled?: boolean
channelName?: string
@@ -173,6 +174,7 @@ export default function FinancePanel() {
consignmentPublishDepositFen: Math.round(
Number(values.consignmentPublishDepositYuan ?? 20) * 100,
),
consignmentWhitelistEnabled: values.consignmentWhitelistEnabled !== false,
recharge: {
enabled: values.recharge?.enabled !== false,
channelName: String(values.recharge?.channelName || '').trim(),
@@ -426,6 +428,14 @@ export default function FinancePanel() {
>
<InputNumber min={0} max={10000} step={1} addonBefore="¥" />
</Form.Item>
<Form.Item
label="寄卖白名单"
name="consignmentWhitelistEnabled"
valuePropName="checked"
tooltip="开启后只有后台开通权限的打手能发布寄卖;关闭后所有打手均可发布。打手身上的白名单标记会保留,方便再次开启。"
>
<Switch checkedChildren="仅白名单" unCheckedChildren="全员可寄卖" />
</Form.Item>
<Form.Item
label="报酬待解冻"
name="rewardUnfreezeEnabled"
@@ -687,6 +697,7 @@ function mapFinanceConfigToFormValues(config?: WorkerFinanceConfig): FinanceConf
depositUnfreezeDays: Number(config?.depositUnfreezeDays ?? 3),
rewardUnfreezeEnabled: config?.rewardUnfreezeEnabled !== false,
consignmentPublishDepositYuan: Number(config?.consignmentPublishDepositFen ?? 2000) / 100,
consignmentWhitelistEnabled: config?.consignmentWhitelistEnabled !== false,
recharge: {
enabled: config?.recharge.enabled !== false,
channelName: config?.recharge.channelName || '',
@@ -177,7 +177,7 @@ function OverviewSection({ workerId }: { workerId: number }) {
<Descriptions.Item label="寄卖权限" span={2}>
<Space>
<Tag color={worker.canConsign ? 'green' : 'default'}>
{worker.canConsign ? '已开通寄卖' : '未开通(白名单限制)'}
{worker.canConsign ? '已加入白名单' : '未加入白名单'}
</Tag>
{worker.consignmentActivatedAt ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
@@ -23,11 +23,13 @@ import { useSearchParams } from 'react-router'
import {
creditAdminWorkerWallet,
fetchAdminWorkerFinanceConfig,
fetchAdminWorkerLevels,
fetchAdminWorkerUsers,
fetchAdminWorkerWithdrawalAccounts,
resetAdminWorkerPassword,
reviewAdminWorkerUser,
saveAdminWorkerFinanceConfig,
saveAdminWorkerPersonalDepositFreeAmount,
saveAdminWorkerWithdrawalAccount,
updateAdminWorkerWithdrawalBlacklist,
@@ -205,6 +207,13 @@ export default function WorkersPanel() {
queryFn: () => fetchAdminWorkerLevels(),
})
const workersPagination = workersQuery.data?.data.pagination
const financeConfigQuery = useQuery({
queryKey: ['admin-worker-platform-finance-config'],
queryFn: () => fetchAdminWorkerFinanceConfig(),
})
const consignmentWhitelistEnabled =
financeConfigQuery.data?.data.consignmentWhitelistEnabled !== false
const [updatingWhitelist, setUpdatingWhitelist] = useState(false)
useEffect(() => {
// 从工单流转记录等入口带 ?workerId= 定位到打手后,自动打开一次详情,关闭后不再重复弹。
@@ -225,6 +234,9 @@ export default function WorkersPanel() {
queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-summary'],
}),
queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-finance-config'],
}),
])
}
@@ -466,47 +478,117 @@ export default function WorkersPanel() {
}
}
const [updatingConsignmentWorkerId, setUpdatingConsignmentWorkerId] = useState<number | null>(
null,
const [updatingConsignmentWorkerIds, setUpdatingConsignmentWorkerIds] = useState<Set<number>>(
() => new Set(),
)
function markConsignmentWorkerUpdating(workerId: number, updating: boolean) {
setUpdatingConsignmentWorkerIds((prev) => {
const next = new Set(prev)
if (updating) next.add(workerId)
else next.delete(workerId)
return next
})
}
function applyUpdatedWorker(row: WorkerUser, updatedWorker: WorkerUser) {
queryClient.setQueriesData<{ data: { items: WorkerUser[]; pagination: any } }>(
{ queryKey: ['admin-worker-platform-workers'] },
(old) => {
if (!old?.data?.items) return old
return {
...old,
data: {
...old.data,
items: old.data.items.map((item) =>
item.workerId === row.workerId ? { ...item, ...updatedWorker } : item,
),
},
}
},
)
setDetailWorker((current) =>
current?.workerId === row.workerId ? { ...current, ...updatedWorker } : current,
)
}
async function handleToggleConsignmentPermission(row: WorkerUser, checked: boolean) {
setUpdatingConsignmentWorkerId(row.workerId)
markConsignmentWorkerUpdating(row.workerId, true)
try {
const res = await updateAdminWorkerConsignmentPermission(row.workerId, {
canConsign: checked,
})
const updatedWorker = res.data?.worker
if (updatedWorker) {
queryClient.setQueriesData<{ data: { items: WorkerUser[]; pagination: any } }>(
{ queryKey: ['admin-worker-platform-workers'] },
(old) => {
if (!old?.data?.items) return old
return {
...old,
data: {
...old.data,
items: old.data.items.map((item) =>
item.workerId === row.workerId ? { ...item, ...updatedWorker } : item,
),
},
}
},
)
applyUpdatedWorker(row, updatedWorker)
}
message.success(
checked
? `已为打手 ${row.displayName || row.username} 开通寄卖权限`
: `已关闭打手 ${row.displayName || row.username} 的寄卖权限`,
? `已为打手 ${row.displayName || row.username} 加入寄卖白名单`
: `已将打手 ${row.displayName || row.username} 移出寄卖白名单`,
)
await refreshWorkers()
} catch (err: any) {
} catch (err: unknown) {
message.error(err instanceof Error ? err.message : '更新寄卖权限失败')
} finally {
setUpdatingConsignmentWorkerId(null)
markConsignmentWorkerUpdating(row.workerId, false)
}
}
function requestToggleConsignmentPermission(row: WorkerUser, checked: boolean) {
if (checked) {
void handleToggleConsignmentPermission(row, true)
return
}
modal.confirm({
title: '移出寄卖白名单?',
content: `确认关闭打手“${row.displayName || row.username}”的寄卖权限?白名单开启时,该打手将无法发布新寄卖;已在途的寄卖不受影响。`,
okText: '确认关闭',
okButtonProps: { danger: true },
cancelText: '取消',
onOk: () => handleToggleConsignmentPermission(row, false),
})
}
async function handleToggleConsignmentWhitelist(checked: boolean) {
const current = financeConfigQuery.data?.data
if (!current) {
message.error('资金配置尚未加载完成,请稍后重试')
return
}
modal.confirm({
title: checked ? '开启寄卖白名单?' : '关闭寄卖白名单?',
content: checked
? '开启后只有白名单中的打手可以发布寄卖,未开通的打手会被拦截。'
: '关闭后所有打手都可以发布寄卖。打手身上的白名单标记会保留,方便再次开启。',
okText: checked ? '确认开启' : '确认关闭',
okButtonProps: checked ? undefined : { danger: true },
cancelText: '取消',
onOk: async () => {
setUpdatingWhitelist(true)
try {
await saveAdminWorkerFinanceConfig({
depositUnfreezeDays: current.depositUnfreezeDays,
rewardUnfreezeEnabled: current.rewardUnfreezeEnabled,
consignmentPublishDepositFen: current.consignmentPublishDepositFen,
consignmentWhitelistEnabled: checked,
recharge: current.recharge,
adminContact: current.adminContact,
withdraw: current.withdraw,
})
message.success(checked ? '已开启寄卖白名单' : '已关闭寄卖白名单,全部打手可发布')
await queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-finance-config'],
})
} catch (error) {
message.error(error instanceof Error ? error.message : '更新寄卖白名单失败')
} finally {
setUpdatingWhitelist(false)
}
},
})
}
const columns: TableColumnsType<WorkerUser> = [
{
title: '打手',
@@ -598,16 +680,24 @@ export default function WorkersPanel() {
),
},
{
title: '寄卖权限',
width: 120,
title: '寄卖白名单',
width: 130,
render: (_, row) => (
<Switch
checkedChildren="已开通"
unCheckedChildren="未开通"
checked={row.canConsign}
loading={updatingConsignmentWorkerId === row.workerId}
onChange={(checked) => handleToggleConsignmentPermission(row, checked)}
/>
<Tooltip
title={
consignmentWhitelistEnabled
? '白名单开启中:仅已开通的打手可发布寄卖'
: '白名单已关闭:开关暂不限制发布,再次开启后才会生效'
}
>
<Switch
checkedChildren="已开通"
unCheckedChildren="未开通"
checked={row.canConsign}
loading={updatingConsignmentWorkerIds.has(row.workerId)}
onChange={(checked) => requestToggleConsignmentPermission(row, checked)}
/>
</Tooltip>
),
},
{
@@ -778,7 +868,33 @@ export default function WorkersPanel() {
) : null}
</Card>
<WorkerStatisticsBar statistics={workersQuery.data?.data.statistics} />
<Card title="打手列表" bordered={false}>
<Card
title="打手列表"
bordered={false}
extra={
<Space>
<Typography.Text type="secondary">寄卖白名单</Typography.Text>
<Switch
checkedChildren="仅白名单"
unCheckedChildren="全员可寄卖"
checked={consignmentWhitelistEnabled}
loading={updatingWhitelist || financeConfigQuery.isLoading}
disabled={!financeConfigQuery.data?.data}
onChange={(checked) => void handleToggleConsignmentWhitelist(checked)}
/>
</Space>
}
>
<Alert
showIcon
type={consignmentWhitelistEnabled ? 'warning' : 'info'}
style={{ marginBottom: 12 }}
message={
consignmentWhitelistEnabled
? '寄卖白名单已开启:仅下方开关为「已开通」的打手可发布寄卖。客服无法开通,需运营或管理员操作。'
: '寄卖白名单已关闭:全部打手均可发布寄卖。下方开关仅在重新开启白名单后生效。'
}
/>
<Table<WorkerUser>
rowKey="workerId"
loading={workersQuery.isLoading}
@@ -33,6 +33,7 @@ import {
Tabs,
Tag,
Typography,
Spin,
} from 'antd'
import { useEffect, useMemo, useRef, useState } from 'react'
@@ -54,6 +55,7 @@ import {
SURNAN_PUBLISH_ZONE_OPTIONS,
} from '@/utils/consignment-spec'
import { formatGiftCooldownRemaining } from '@/utils/consignment-cooldown'
import { canWorkerPublishConsignment } from '@/utils/consignment-whitelist'
import { formatDateTime } from '@/utils/date-time'
import { useIsMobile } from '@/utils/use-is-mobile'
@@ -200,13 +202,13 @@ export default function WorkerConsignmentPage() {
const publishDepositYuan = publishDepositFen / 100
const availableAmount = Number(worker?.wallet?.availableAmount || 0)
const canPublish = availableAmount >= publishDepositFen
const canConsign = worker?.canConsign !== false
const canConsign = canWorkerPublishConsignment({
profileReady: !profileQuery.isLoading && Boolean(worker),
whitelistEnabled: financeConfig?.consignmentWhitelistEnabled !== false,
workerCanConsign: worker?.canConsign === true,
})
const [contactOpen, setContactOpen] = useState(false)
const reviewingCount = records.filter((item) => item.group === 'reviewing').length
const activeCount = records.filter((item) => item.group === 'active').length
const finishedCount = records.filter((item) => item.group === 'finished').length
// 阶段 → 分组由后端下发(item.group),这里只把它映射到筛选角标计数上
const groupCounts = useMemo(() => {
const stageGroups = new Map<string, string>()
@@ -527,7 +529,7 @@ export default function WorkerConsignmentPage() {
<div className="worker-consignment-kpi-main">
<span className="worker-consignment-kpi-title">待审核</span>
<span className="worker-consignment-kpi-val">
{reviewingCount}
{groupCounts.reviewing}
<span className="worker-consignment-kpi-unit">单</span>
</span>
</div>
@@ -546,8 +548,8 @@ export default function WorkerConsignmentPage() {
<div className="worker-consignment-kpi-main">
<span className="worker-consignment-kpi-title">履约中</span>
<span className="worker-consignment-kpi-val">
{activeCount}
<span className="worker-consignment-kpi-unit">把</span>
{groupCounts.active}
<span className="worker-consignment-kpi-unit">单</span>
</span>
</div>
<div className="worker-consignment-kpi-icon in-progress">
@@ -565,8 +567,8 @@ export default function WorkerConsignmentPage() {
<div className="worker-consignment-kpi-main">
<span className="worker-consignment-kpi-title">已完成</span>
<span className="worker-consignment-kpi-val">
{finishedCount}
<span className="worker-consignment-kpi-unit">把</span>
{groupCounts.finished}
<span className="worker-consignment-kpi-unit">单</span>
</span>
</div>
<div className="worker-consignment-kpi-icon accepted">
@@ -588,7 +590,11 @@ export default function WorkerConsignmentPage() {
<DollarCircleOutlined /> 发布寄卖
</span>
),
children: !canConsign ? (
children: profileQuery.isLoading ? (
<div style={{ padding: '48px 16px', textAlign: 'center' }}>
<Spin />
</div>
) : !canConsign ? (
<div style={{ padding: '36px 16px', textAlign: 'center' }}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
@@ -601,7 +607,7 @@ export default function WorkerConsignmentPage() {
type="secondary"
style={{ fontSize: 13, marginBottom: 20 }}
>
为保障寄卖道具交付质量与履约安全,平台当前实行白名单审核制。请联系在线客服申请开通寄卖权限。
平台当前开启寄卖白名单。请联系客服申请开通,由后台运营开通后即可发布苏尔南寄卖。
</Typography.Paragraph>
<Button
type="primary"
@@ -972,6 +978,7 @@ export default function WorkerConsignmentPage() {
worker={worker}
financeConfig={financeConfig}
title="联系客服开通寄卖"
alertMessage="请扫码联系客服申请开通寄卖。开通由后台运营处理,通过后即可发布苏尔南。"
/>
</section>
)
@@ -506,6 +506,7 @@ export function saveAdminWorkerFinanceConfig(payload: {
depositUnfreezeDays?: number
rewardUnfreezeEnabled?: boolean
consignmentPublishDepositFen?: number
consignmentWhitelistEnabled?: boolean
recharge?: {
enabled?: boolean
channelName?: string
@@ -265,6 +265,8 @@ export type WorkerFinanceConfig = {
rewardUnfreezeEnabled: boolean
/** 苏尔南寄卖发布履约押金(分),默认 2000 */
consignmentPublishDepositFen?: number
/** 寄卖白名单总开关。开启后仅白名单打手可发布;关闭后所有打手可发布。缺省视为开启。 */
consignmentWhitelistEnabled?: boolean
recharge: {
enabled: boolean
channelName: string
@@ -0,0 +1,12 @@
/**
* 打手端是否允许进入「发布寄卖」表单。
* 资料未就绪时一律不放行,避免白名单模式下先闪出表单。
*/
export function canWorkerPublishConsignment(input: {
profileReady: boolean
whitelistEnabled: boolean
workerCanConsign: boolean
}): boolean {
if (!input.profileReady) return false
return !input.whitelistEnabled || input.workerCanConsign
}