feat(打手): 支持个人追加免押额度
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user