feat(打手): 支持个人追加免押额度

This commit is contained in:
yml2213
2026-08-28 21:36:54 +08:00
parent 5d5d63bb59
commit f20ff3018f
18 changed files with 282 additions and 12 deletions
@@ -355,6 +355,7 @@ function applyWorkerDepositRule(queryClient: ReturnType<typeof useQueryClient>,
const requiredDepositAmount = Number(data.requiredDepositAmount || 0)
return {
...data,
depositFreeAmount,
freezeDepositAmount: Math.max(0, requiredDepositAmount - depositFreeAmount),
}
}
@@ -25,6 +25,7 @@ import {
fetchAdminWorkerWithdrawalAccounts,
resetAdminWorkerPassword,
reviewAdminWorkerUser,
saveAdminWorkerPersonalDepositFreeAmount,
saveAdminWorkerWithdrawalAccount,
} from '@/services/admin'
import ImageUpload from '@/components/files/ImageUpload'
@@ -49,6 +50,10 @@ type FreezeWorkerFormValues = {
freezeReason: string
}
type PersonalDepositFreeAmountFormValues = {
depositFreeAmount?: number
}
export default function WorkersPanel() {
const { message, modal } = App.useApp()
const queryClient = useQueryClient()
@@ -66,6 +71,9 @@ export default function WorkersPanel() {
const [withdrawalAccountWorker, setWithdrawalAccountWorker] = useState<WorkerUser | null>(null)
const [resetPasswordWorker, setResetPasswordWorker] = useState<WorkerUser | null>(null)
const [freezeWorker, setFreezeWorker] = useState<WorkerUser | null>(null)
const [personalDepositFreeWorker, setPersonalDepositFreeWorker] = useState<WorkerUser | null>(
null,
)
const [detailWorker, setDetailWorker] = useState<WorkerUser | null>(null)
const autoOpenedDetailRef = useRef(0)
const [withdrawalAccounts, setWithdrawalAccounts] = useState<AdminWorkerWithdrawalAccount[]>([])
@@ -76,6 +84,7 @@ export default function WorkersPanel() {
const [withdrawalAccountForm] = Form.useForm<WithdrawalAccountFormValues>()
const [resetPasswordForm] = Form.useForm<{ password: string }>()
const [freezeForm] = Form.useForm<FreezeWorkerFormValues>()
const [personalDepositFreeForm] = Form.useForm<PersonalDepositFreeAmountFormValues>()
const withdrawalAccountChannel = Form.useWatch('accountChannel', withdrawalAccountForm)
const editingAlipayAccount = withdrawalAccounts.find(
(account) => account.accountChannel === 'alipay',
@@ -266,6 +275,21 @@ export default function WorkersPanel() {
}
}
async function submitPersonalDepositFreeAmount(values: PersonalDepositFreeAmountFormValues) {
if (!personalDepositFreeWorker) return
try {
await saveAdminWorkerPersonalDepositFreeAmount(personalDepositFreeWorker.workerId, {
depositFreeAmount: values.depositFreeAmount ?? null,
})
message.success('个人免押额度已更新')
setPersonalDepositFreeWorker(null)
personalDepositFreeForm.resetFields()
await refreshWorkers()
} catch (error) {
message.error(error instanceof Error ? error.message : '更新个人免押额度失败')
}
}
async function openWithdrawalAccountModal(worker: WorkerUser) {
setWithdrawalAccountWorker(worker)
setWithdrawalAccounts([])
@@ -341,6 +365,22 @@ export default function WorkersPanel() {
),
},
{ title: '等级', width: 140, render: (_, row) => row.level?.name || '-' },
{
title: '免押额度',
width: 280,
render: (_, row) =>
row.personalDepositFreeAmount !== null ? (
<Typography.Text>
{formatMoney(row.effectiveDepositFreeAmount)} {formatMoney(
row.levelDepositFreeAmount,
)} + {formatMoney(row.personalDepositFreeAmount)}
</Typography.Text>
) : (
<Typography.Text type="secondary">
{formatMoney(row.effectiveDepositFreeAmount)}
</Typography.Text>
),
},
{
title: '邀请人',
width: 160,
@@ -428,6 +468,20 @@ export default function WorkersPanel() {
>
</Button>
<Button
onClick={() => {
setPersonalDepositFreeWorker(row)
personalDepositFreeForm.setFieldsValue({
// 接口金额使用“分”,输入框按“元”编辑。
depositFreeAmount:
row.personalDepositFreeAmount === null
? undefined
: Number(row.personalDepositFreeAmount) / 100,
})
}}
>
</Button>
<Button onClick={() => setCreditWorker(row)}></Button>
<Button onClick={() => setResetPasswordWorker(row)}></Button>
<Button onClick={() => void openWithdrawalAccountModal(row)}></Button>
@@ -563,6 +617,39 @@ export default function WorkersPanel() {
</Form>
</Modal>
<Modal
title="设置个人免押额度"
open={Boolean(personalDepositFreeWorker)}
onCancel={() => {
setPersonalDepositFreeWorker(null)
personalDepositFreeForm.resetFields()
}}
onOk={() => personalDepositFreeForm.submit()}
destroyOnHidden
>
<Form
form={personalDepositFreeForm}
layout="vertical"
onFinish={submitPersonalDepositFreeAmount}
>
<Form.Item label="打手">
<Typography.Text>
{personalDepositFreeWorker?.displayName || personalDepositFreeWorker?.username || '-'}
</Typography.Text>
</Form.Item>
<Form.Item
label="个人免押额度"
name="depositFreeAmount"
tooltip="该额度会与等级免押额度相加;留空或填 0 表示不追加个人额度。"
>
<InputNumber min={0} step={10} addonAfter="元" className="full-width" />
</Form.Item>
<Typography.Text type="secondary">
</Typography.Text>
</Form>
</Modal>
<Modal
title="重置打手密码"
open={Boolean(resetPasswordWorker)}
@@ -365,6 +365,16 @@ export function reviewAdminWorkerUser(
)
}
export function saveAdminWorkerPersonalDepositFreeAmount(
workerId: number,
payload: { depositFreeAmount: number | null },
) {
return apiPost<{ worker: WorkerUser }>(
`/api/v1/admin/worker-platform/workers/${workerId}/personal-deposit-free-amount`,
payload,
)
}
export function resetAdminWorkerPassword(workerId: number, payload: { password: string }) {
return apiPost<{ worker: WorkerUser; reloginRequired: boolean }>(
`/api/v1/admin/worker-platform/workers/${workerId}/reset-password`,
@@ -9,6 +9,12 @@ export type WorkerUser = {
freezeReason: string
frozenAt: string | null
inviteCode: string
/** 个人追加免押额度;null 表示未追加。 */
personalDepositFreeAmount: number | null
/** 打手等级带来的免押额度。 */
levelDepositFreeAmount: number
/** 当前实际生效的免押额度。 */
effectiveDepositFreeAmount: number
inviter: null | {
workerId: number
username: string
@@ -561,6 +567,8 @@ export type WorkOrder = {
canResetGiftFlow?: boolean
}
rewardAmount: number
/** 当前打手生效的免押额度,仅打手端订单数据返回。 */
depositFreeAmount?: number
requiredDepositAmount?: number
depositThresholdAmount?: number
freezeDepositAmount?: number
@@ -2,6 +2,7 @@ type HallProduct = {
productName?: string
rewardAmount?: number
freezeDepositAmount?: number
depositFreeAmount?: number
sharing?: {
enabled: boolean
totalQuantity: number
@@ -41,11 +42,15 @@ export function resolveHallRewardAmount(order: HallProduct): number {
return resolveHallRemainingQuantity(order) * Number(order.sharing.unitReward || 0)
}
/** 拼单卡片保证金按剩余份数占总份数的比例展示。 */
/** 拼单卡片按当前剩余份数计算个人免押后的实际保证金。 */
export function resolveHallDepositAmount(order: HallProduct): number {
if (!order.sharing?.enabled) return Number(order.freezeDepositAmount || 0)
const totalQuantity = Math.max(1, Number(order.sharing.totalQuantity) || 1)
return Math.round(
(Number(order.freezeDepositAmount || 0) * resolveHallRemainingQuantity(order)) / totalQuantity,
const depositFreeAmount = Math.max(0, Number(order.depositFreeAmount || 0))
const requiredDepositAmount = Number(order.freezeDepositAmount || 0) + depositFreeAmount
return Math.max(
0,
Math.round((requiredDepositAmount * resolveHallRemainingQuantity(order)) / totalQuantity) -
depositFreeAmount,
)
}
@@ -55,6 +55,18 @@ test('立即抢单的份数、报酬和保证金都按剩余数量计算', () =>
assert.equal(resolveHallDepositAmount(order), 1_200)
})
test('拼单大厅保证金先按个人份额计算再抵扣免押额度', () => {
assert.equal(
resolveHallDepositAmount({
freezeDepositAmount: 7_000,
depositFreeAmount: 3_000,
sharing: { enabled: true, totalQuantity: 10 },
sharingProgress: { joinedQuantity: 5 },
}),
2_000,
)
})
test('拼单份数用完后剩余报酬和保证金归零', () => {
const order = {
rewardAmount: 10_500,