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
@@ -0,0 +1,6 @@
-- 065_worker_personal_deposit_free_amount.sql —— 单个打手的免押额度覆盖等级配置。
ALTER TABLE worker_users
ADD COLUMN IF NOT EXISTS personal_deposit_free_amount INTEGER;
COMMENT ON COLUMN worker_users.personal_deposit_free_amount IS '个人追加免押额度,单位分;与等级免押额度叠加,NULL/0 表示未追加';
@@ -16,6 +16,8 @@ export type WorkerUserRow = {
display_name: string display_name: string
phone: string phone: string
level_id: number | null level_id: number | null
/** 个人追加免押额度(分),与等级额度叠加。 */
personal_deposit_free_amount?: number | null
status: string status: string
worker_type: string worker_type: string
review_note: string review_note: string
@@ -95,12 +95,37 @@ export function resolveWorkOrderShareJoinQuantity(input: {
return input.takeRemaining ? Math.max(0, input.remaining) : input.quantity return input.takeRemaining ? Math.max(0, input.remaining) : input.quantity
} }
/**
* 计算打手当前累计拼单份额实际需要冻结的押金。
* 等级与个人叠加后的免押额度针对该打手在本工单的累计份额生效;已经冻结的金额不自动回退。
*/
export function resolveWorkOrderShareDepositAmount(input: {
requiredDepositAmount: number
totalQuantity: number
nextQuantity: number
depositFreeAmount?: number
currentFrozenDeposit?: number
}) {
const totalQuantity = Math.max(1, Number(input.totalQuantity || 0))
const nextRequiredDeposit = Math.round(
(Math.max(0, Number(input.requiredDepositAmount || 0)) * Math.max(0, Number(input.nextQuantity || 0))) /
totalQuantity,
)
const requiredShareDeposit = Math.max(
0,
nextRequiredDeposit - Math.max(0, Number(input.depositFreeAmount || 0)),
)
return Math.max(Math.max(0, Number(input.currentFrozenDeposit || 0)), requiredShareDeposit)
}
export async function joinWorkOrderShare(input: { export async function joinWorkOrderShare(input: {
workOrderId: number workOrderId: number
workerId: number workerId: number
quantity: number quantity: number
/** 立即抢单时在事务锁内承接全部剩余份数。 */ /** 立即抢单时在事务锁内承接全部剩余份数。 */
takeRemaining?: boolean takeRemaining?: boolean
/** 当前打手的有效免押额度(分),按其本次累计拼单份额抵扣。 */
depositFreeAmount?: number
maxActiveOrders: number maxActiveOrders: number
hallCandidateLimit?: number hallCandidateLimit?: number
hallCategoryLimits?: Record<string, number> hallCategoryLimits?: Record<string, number>
@@ -192,9 +217,15 @@ export async function joinWorkOrderShare(input: {
const unitReward = toPositiveInteger(workOrder.sharing_unit_reward, 0) const unitReward = toPositiveInteger(workOrder.sharing_unit_reward, 0)
const nextQuantity = Number(existingShare?.quantity || 0) + requestedQuantity const nextQuantity = Number(existingShare?.quantity || 0) + requestedQuantity
const shareReward = nextQuantity * unitReward const shareReward = nextQuantity * unitReward
const nextShareDeposit = Math.round( const nextShareDeposit = resolveWorkOrderShareDepositAmount({
(Number(workOrder.required_deposit_amount || 0) * nextQuantity) / totalQuantity, requiredDepositAmount: Number(workOrder.required_deposit_amount || 0),
) totalQuantity,
nextQuantity,
...(input.depositFreeAmount === undefined
? {}
: { depositFreeAmount: input.depositFreeAmount }),
currentFrozenDeposit: Number(existingShare?.share_deposit || 0),
})
const depositToFreeze = Math.max( const depositToFreeze = Math.max(
0, 0,
nextShareDeposit - Number(existingShare?.share_deposit || 0), nextShareDeposit - Number(existingShare?.share_deposit || 0),
@@ -145,6 +145,7 @@ export async function updateWorkerUser(
| 'frozen_reason' | 'frozen_reason'
| 'frozen_at' | 'frozen_at'
| 'level_id' | 'level_id'
| 'personal_deposit_free_amount'
| 'session_version' | 'session_version'
| 'updated_at' | 'updated_at'
| 'reviewed_at' | 'reviewed_at'
@@ -165,10 +166,11 @@ export async function updateWorkerUser(
frozen_reason = $4, frozen_reason = $4,
frozen_at = $5, frozen_at = $5,
level_id = $6, level_id = $6,
session_version = $7, personal_deposit_free_amount = $7,
reviewed_at = $8, session_version = $8,
updated_at = $9 reviewed_at = $9,
WHERE id = $10 updated_at = $10
WHERE id = $11
`, `,
[ [
next.status, next.status,
@@ -177,6 +179,7 @@ export async function updateWorkerUser(
next.frozen_reason || '', next.frozen_reason || '',
next.frozen_at || null, next.frozen_at || null,
next.level_id || null, next.level_id || null,
next.personal_deposit_free_amount ?? null,
Number(next.session_version || 1), Number(next.session_version || 1),
next.reviewed_at || null, next.reviewed_at || null,
next.updated_at, next.updated_at,
@@ -10,6 +10,7 @@ import {
resetAdminWorkerPassword, resetAdminWorkerPassword,
reviewAdminWorkerUser, reviewAdminWorkerUser,
saveAdminWorkerWithdrawalAccount, saveAdminWorkerWithdrawalAccount,
setAdminWorkerPersonalDepositFreeAmount,
} from '../../../services/worker-platform/index.js' } from '../../../services/worker-platform/index.js'
import { createJsonHandler, requireAdminRoles } from '../session.js' import { createJsonHandler, requireAdminRoles } from '../session.js'
@@ -99,6 +100,26 @@ router.post(
), ),
) )
router.post(
'/worker-platform/workers/:workerId/personal-deposit-free-amount',
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) =>
setAdminWorkerPersonalDepositFreeAmount(String(req.params.workerId || ''), req.body || {}),
{
successMessage: '打手个人免押额度已更新',
errorMessage: '更新个人免押额度失败',
scope: '[admin/worker-platform/workers/:workerId/personal-deposit-free-amount]',
audit: (req, data) => ({
action: 'worker_personal_deposit_free_amount_updated',
targetType: 'worker',
targetId: String(req.params.workerId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
},
),
)
router.post( router.post(
'/worker-platform/workers/:workerId/wallet-credit', '/worker-platform/workers/:workerId/wallet-credit',
requireAdminRoles(['admin', 'operator']), requireAdminRoles(['admin', 'operator']),
@@ -47,5 +47,6 @@ export {
listAdminWorkerUsers, listAdminWorkerUsers,
resetAdminWorkerPassword, resetAdminWorkerPassword,
reviewAdminWorkerUser, reviewAdminWorkerUser,
setAdminWorkerPersonalDepositFreeAmount,
} from './admin-worker-user-service.js' } from './admin-worker-user-service.js'
export { syncWorkerOrdersForSourceOrder } from './sync-worker-orders-service.js' export { syncWorkerOrdersForSourceOrder } from './sync-worker-orders-service.js'
@@ -13,6 +13,7 @@ import { normalizePage, normalizePageSize } from '../admin/admin-query-utils.js'
import { import {
hashWorkerPassword, hashWorkerPassword,
mapWorkerUser, mapWorkerUser,
normalizeAmountFen,
normalizeOptionalId, normalizeOptionalId,
normalizeReviewStatus, normalizeReviewStatus,
normalizeSessionVersion, normalizeSessionVersion,
@@ -89,6 +90,30 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload:
return { worker: mapWorkerUser(updated || worker) } return { worker: mapWorkerUser(updated || worker) }
} }
/** 设置单个打手的追加免押额度,会与该打手等级的免押额度叠加。 */
export async function setAdminWorkerPersonalDepositFreeAmount(
workerId: number | string,
payload: JsonObject = {},
) {
const worker = await getRequiredWorker(workerId)
const rawAmount = payload.depositFreeAmount ?? payload.deposit_free_amount
const clearPersonalAmount = rawAmount === null || rawAmount === undefined || String(rawAmount).trim() === ''
if (!clearPersonalAmount) {
const amount = Number(rawAmount)
if (!Number.isFinite(amount) || amount < 0) {
throw createHttpError('个人免押额度必须是不小于 0 的金额', {
statusCode: 400,
errorCode: 'worker_personal_deposit_free_amount_invalid',
})
}
}
const updated = await updateWorkerUser(worker.id, {
personal_deposit_free_amount: clearPersonalAmount ? null : normalizeAmountFen(rawAmount, 0),
updated_at: nowIso(),
})
return { worker: mapWorkerUser(updated || worker) }
}
export async function resetAdminWorkerPassword( export async function resetAdminWorkerPassword(
workerId: number | string, workerId: number | string,
payload: JsonObject = {}, payload: JsonObject = {},
@@ -315,6 +315,8 @@ export function mapWorkOrderForWorker(
freezeDepositAmount: myShare freezeDepositAmount: myShare
? Number(myShare.share_deposit || 0) ? Number(myShare.share_deposit || 0)
: resolveFreezeDepositAmount(workOrder, permissions), : resolveFreezeDepositAmount(workOrder, permissions),
/** 用于大厅拼单按当前剩余份数准确计算个人免押后的押金。 */
depositFreeAmount: Number(permissions.depositFreeAmount || 0),
assignedAt: myShare?.created_at || visibleOrder.assignedAt, assignedAt: myShare?.created_at || visibleOrder.assignedAt,
submittedAt: myShare?.submitted_at || visibleOrder.submittedAt, submittedAt: myShare?.submitted_at || visibleOrder.submittedAt,
acceptedAt: myShare?.accepted_at || visibleOrder.acceptedAt, acceptedAt: myShare?.accepted_at || visibleOrder.acceptedAt,
@@ -62,6 +62,7 @@ export function normalizeWorkerType(value: unknown): 'internal' | 'external' {
} }
export function mapWorkerUser(worker: WorkerUserRow) { export function mapWorkerUser(worker: WorkerUserRow) {
const permissions = resolveWorkerPermissions(worker)
return { return {
workerId: Number(worker.id), workerId: Number(worker.id),
username: worker.username, username: worker.username,
@@ -85,9 +86,12 @@ export function mapWorkerUser(worker: WorkerUserRow) {
levelId: Number(worker.level_id), levelId: Number(worker.level_id),
levelKey: worker.level_key || '', levelKey: worker.level_key || '',
name: worker.level_name || '', name: worker.level_name || '',
permissions: resolveWorkerPermissions(worker), permissions,
} }
: null, : null,
personalDepositFreeAmount: permissions.personalDepositFreeAmount,
levelDepositFreeAmount: permissions.levelDepositFreeAmount,
effectiveDepositFreeAmount: permissions.depositFreeAmount,
wallet: { wallet: {
availableAmount: Number(worker.available_amount || 0), availableAmount: Number(worker.available_amount || 0),
frozenDepositAmount: Number(worker.frozen_deposit_amount || 0), frozenDepositAmount: Number(worker.frozen_deposit_amount || 0),
@@ -53,6 +53,7 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session:
workerId: Number(worker.id), workerId: Number(worker.id),
quantity: 0, quantity: 0,
takeRemaining: true, takeRemaining: true,
depositFreeAmount: permissions.depositFreeAmount,
maxActiveOrders: permissions.maxActiveOrders, maxActiveOrders: permissions.maxActiveOrders,
hallCandidateLimit: hallConfig.maxVisibleOrders, hallCandidateLimit: hallConfig.maxVisibleOrders,
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders, hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
@@ -143,6 +144,7 @@ export async function joinWorkerSharingOrder(
workOrderId: Number(workOrderId), workOrderId: Number(workOrderId),
workerId: Number(worker.id), workerId: Number(worker.id),
quantity, quantity,
depositFreeAmount: permissions.depositFreeAmount,
maxActiveOrders: permissions.maxActiveOrders, maxActiveOrders: permissions.maxActiveOrders,
hallCandidateLimit: hallConfig.maxVisibleOrders, hallCandidateLimit: hallConfig.maxVisibleOrders,
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders, hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
@@ -8,8 +8,19 @@ import {
export function resolveWorkerPermissions(worker: WorkerUserRow) { export function resolveWorkerPermissions(worker: WorkerUserRow) {
const permission = safeParseJson(worker.level_permission_json) const permission = safeParseJson(worker.level_permission_json)
const levelDepositFreeAmount = normalizeInteger(permission.depositFreeAmount, 0)
const personalDepositFreeAmount = Number(worker.personal_deposit_free_amount)
const hasPersonalDepositFreeAmount =
worker.personal_deposit_free_amount !== null &&
worker.personal_deposit_free_amount !== undefined &&
Number.isInteger(personalDepositFreeAmount) &&
personalDepositFreeAmount >= 0
return { return {
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0), // 个人额度是在等级额度基础上的追加额度,两者叠加生效。
depositFreeAmount: levelDepositFreeAmount +
(hasPersonalDepositFreeAmount ? personalDepositFreeAmount : 0),
levelDepositFreeAmount,
personalDepositFreeAmount: hasPersonalDepositFreeAmount ? personalDepositFreeAmount : null,
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1), maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1),
upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0), upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0),
autoAcceptWithoutEvidence: normalizeBoolean(permission.autoAcceptWithoutEvidence, false), autoAcceptWithoutEvidence: normalizeBoolean(permission.autoAcceptWithoutEvidence, false),
@@ -13,6 +13,7 @@ import type {
import { import {
buildWorkOrderWhere, buildWorkOrderWhere,
resolveWorkOrderShareCancellationStatus, resolveWorkOrderShareCancellationStatus,
resolveWorkOrderShareDepositAmount,
resolveWorkOrderSharingCancellationPlan, resolveWorkOrderSharingCancellationPlan,
resolveWorkOrderShareJoinQuantity, resolveWorkOrderShareJoinQuantity,
resolveOpenSharingSyncReward, resolveOpenSharingSyncReward,
@@ -36,6 +37,7 @@ import {
maskWorkerLeaderboardDisplayName, maskWorkerLeaderboardDisplayName,
resolveCollectSubmitTargetWorkOrder, resolveCollectSubmitTargetWorkOrder,
resolveAdminMaterialRewardAmount, resolveAdminMaterialRewardAmount,
resolveFreezeDepositAmount,
resolveAdminMockWorkOrderInput, resolveAdminMockWorkOrderInput,
resolveMatchingProductRuleDecision, resolveMatchingProductRuleDecision,
resolveWorkerPermissions, resolveWorkerPermissions,
@@ -698,6 +700,43 @@ test('等级免审核后补图权限默认关闭,仅显式开启时生效', ()
) )
}) })
test('个人免押额度与等级免押额度叠加', () => {
const worker = buildWorkerUserRow({
level_permission_json: { depositFreeAmount: 5_000 },
personal_deposit_free_amount: 12_000,
})
const permissions = resolveWorkerPermissions(worker)
assert.equal(permissions.depositFreeAmount, 17_000)
assert.equal(permissions.levelDepositFreeAmount, 5_000)
assert.equal(permissions.personalDepositFreeAmount, 12_000)
assert.equal(
resolveFreezeDepositAmount(buildWorkOrderRow({ required_deposit_amount: 20_000 }), permissions),
3_000,
)
})
test('拼单按打手累计份额扣除个人免押额度', () => {
assert.equal(
resolveWorkOrderShareDepositAmount({
requiredDepositAmount: 10_000,
totalQuantity: 10,
nextQuantity: 5,
depositFreeAmount: 3_000,
}),
2_000,
)
assert.equal(
resolveWorkOrderShareDepositAmount({
requiredDepositAmount: 10_000,
totalQuantity: 10,
nextQuantity: 5,
depositFreeAmount: 8_000,
currentFrozenDeposit: 1_500,
}),
1_500,
)
})
test('后台整单指派给 VIP5 时不因母工单保留拼单配置而失去免审核资格', () => { test('后台整单指派给 VIP5 时不因母工单保留拼单配置而失去免审核资格', () => {
const vip5Worker = buildWorkerUserRow({ const vip5Worker = buildWorkerUserRow({
level_key: 'vip5', level_key: 'vip5',
@@ -355,6 +355,7 @@ function applyWorkerDepositRule(queryClient: ReturnType<typeof useQueryClient>,
const requiredDepositAmount = Number(data.requiredDepositAmount || 0) const requiredDepositAmount = Number(data.requiredDepositAmount || 0)
return { return {
...data, ...data,
depositFreeAmount,
freezeDepositAmount: Math.max(0, requiredDepositAmount - depositFreeAmount), freezeDepositAmount: Math.max(0, requiredDepositAmount - depositFreeAmount),
} }
} }
@@ -25,6 +25,7 @@ import {
fetchAdminWorkerWithdrawalAccounts, fetchAdminWorkerWithdrawalAccounts,
resetAdminWorkerPassword, resetAdminWorkerPassword,
reviewAdminWorkerUser, reviewAdminWorkerUser,
saveAdminWorkerPersonalDepositFreeAmount,
saveAdminWorkerWithdrawalAccount, saveAdminWorkerWithdrawalAccount,
} from '@/services/admin' } from '@/services/admin'
import ImageUpload from '@/components/files/ImageUpload' import ImageUpload from '@/components/files/ImageUpload'
@@ -49,6 +50,10 @@ type FreezeWorkerFormValues = {
freezeReason: string freezeReason: string
} }
type PersonalDepositFreeAmountFormValues = {
depositFreeAmount?: number
}
export default function WorkersPanel() { export default function WorkersPanel() {
const { message, modal } = App.useApp() const { message, modal } = App.useApp()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -66,6 +71,9 @@ export default function WorkersPanel() {
const [withdrawalAccountWorker, setWithdrawalAccountWorker] = useState<WorkerUser | null>(null) const [withdrawalAccountWorker, setWithdrawalAccountWorker] = useState<WorkerUser | null>(null)
const [resetPasswordWorker, setResetPasswordWorker] = useState<WorkerUser | null>(null) const [resetPasswordWorker, setResetPasswordWorker] = useState<WorkerUser | null>(null)
const [freezeWorker, setFreezeWorker] = 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 [detailWorker, setDetailWorker] = useState<WorkerUser | null>(null)
const autoOpenedDetailRef = useRef(0) const autoOpenedDetailRef = useRef(0)
const [withdrawalAccounts, setWithdrawalAccounts] = useState<AdminWorkerWithdrawalAccount[]>([]) const [withdrawalAccounts, setWithdrawalAccounts] = useState<AdminWorkerWithdrawalAccount[]>([])
@@ -76,6 +84,7 @@ export default function WorkersPanel() {
const [withdrawalAccountForm] = Form.useForm<WithdrawalAccountFormValues>() const [withdrawalAccountForm] = Form.useForm<WithdrawalAccountFormValues>()
const [resetPasswordForm] = Form.useForm<{ password: string }>() const [resetPasswordForm] = Form.useForm<{ password: string }>()
const [freezeForm] = Form.useForm<FreezeWorkerFormValues>() const [freezeForm] = Form.useForm<FreezeWorkerFormValues>()
const [personalDepositFreeForm] = Form.useForm<PersonalDepositFreeAmountFormValues>()
const withdrawalAccountChannel = Form.useWatch('accountChannel', withdrawalAccountForm) const withdrawalAccountChannel = Form.useWatch('accountChannel', withdrawalAccountForm)
const editingAlipayAccount = withdrawalAccounts.find( const editingAlipayAccount = withdrawalAccounts.find(
(account) => account.accountChannel === 'alipay', (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) { async function openWithdrawalAccountModal(worker: WorkerUser) {
setWithdrawalAccountWorker(worker) setWithdrawalAccountWorker(worker)
setWithdrawalAccounts([]) setWithdrawalAccounts([])
@@ -341,6 +365,22 @@ export default function WorkersPanel() {
), ),
}, },
{ title: '等级', width: 140, render: (_, row) => row.level?.name || '-' }, { 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: '邀请人', title: '邀请人',
width: 160, width: 160,
@@ -428,6 +468,20 @@ export default function WorkersPanel() {
> >
</Button> </Button>
<Button
onClick={() => {
setPersonalDepositFreeWorker(row)
personalDepositFreeForm.setFieldsValue({
// 接口金额使用“分”,输入框按“元”编辑。
depositFreeAmount:
row.personalDepositFreeAmount === null
? undefined
: Number(row.personalDepositFreeAmount) / 100,
})
}}
>
</Button>
<Button onClick={() => setCreditWorker(row)}></Button> <Button onClick={() => setCreditWorker(row)}></Button>
<Button onClick={() => setResetPasswordWorker(row)}></Button> <Button onClick={() => setResetPasswordWorker(row)}></Button>
<Button onClick={() => void openWithdrawalAccountModal(row)}></Button> <Button onClick={() => void openWithdrawalAccountModal(row)}></Button>
@@ -563,6 +617,39 @@ export default function WorkersPanel() {
</Form> </Form>
</Modal> </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 <Modal
title="重置打手密码" title="重置打手密码"
open={Boolean(resetPasswordWorker)} 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 }) { export function resetAdminWorkerPassword(workerId: number, payload: { password: string }) {
return apiPost<{ worker: WorkerUser; reloginRequired: boolean }>( return apiPost<{ worker: WorkerUser; reloginRequired: boolean }>(
`/api/v1/admin/worker-platform/workers/${workerId}/reset-password`, `/api/v1/admin/worker-platform/workers/${workerId}/reset-password`,
@@ -9,6 +9,12 @@ export type WorkerUser = {
freezeReason: string freezeReason: string
frozenAt: string | null frozenAt: string | null
inviteCode: string inviteCode: string
/** 个人追加免押额度;null 表示未追加。 */
personalDepositFreeAmount: number | null
/** 打手等级带来的免押额度。 */
levelDepositFreeAmount: number
/** 当前实际生效的免押额度。 */
effectiveDepositFreeAmount: number
inviter: null | { inviter: null | {
workerId: number workerId: number
username: string username: string
@@ -561,6 +567,8 @@ export type WorkOrder = {
canResetGiftFlow?: boolean canResetGiftFlow?: boolean
} }
rewardAmount: number rewardAmount: number
/** 当前打手生效的免押额度,仅打手端订单数据返回。 */
depositFreeAmount?: number
requiredDepositAmount?: number requiredDepositAmount?: number
depositThresholdAmount?: number depositThresholdAmount?: number
freezeDepositAmount?: number freezeDepositAmount?: number
@@ -2,6 +2,7 @@ type HallProduct = {
productName?: string productName?: string
rewardAmount?: number rewardAmount?: number
freezeDepositAmount?: number freezeDepositAmount?: number
depositFreeAmount?: number
sharing?: { sharing?: {
enabled: boolean enabled: boolean
totalQuantity: number totalQuantity: number
@@ -41,11 +42,15 @@ export function resolveHallRewardAmount(order: HallProduct): number {
return resolveHallRemainingQuantity(order) * Number(order.sharing.unitReward || 0) return resolveHallRemainingQuantity(order) * Number(order.sharing.unitReward || 0)
} }
/** 拼单卡片保证金按剩余份数占总份数的比例展示。 */ /** 拼单卡片按当前剩余份数计算个人免押后的实际保证金。 */
export function resolveHallDepositAmount(order: HallProduct): number { export function resolveHallDepositAmount(order: HallProduct): number {
if (!order.sharing?.enabled) return Number(order.freezeDepositAmount || 0) if (!order.sharing?.enabled) return Number(order.freezeDepositAmount || 0)
const totalQuantity = Math.max(1, Number(order.sharing.totalQuantity) || 1) const totalQuantity = Math.max(1, Number(order.sharing.totalQuantity) || 1)
return Math.round( const depositFreeAmount = Math.max(0, Number(order.depositFreeAmount || 0))
(Number(order.freezeDepositAmount || 0) * resolveHallRemainingQuantity(order)) / totalQuantity, 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) 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('拼单份数用完后剩余报酬和保证金归零', () => { test('拼单份数用完后剩余报酬和保证金归零', () => {
const order = { const order = {
rewardAmount: 10_500, rewardAmount: 10_500,