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
@@ -47,5 +47,6 @@ export {
listAdminWorkerUsers,
resetAdminWorkerPassword,
reviewAdminWorkerUser,
setAdminWorkerPersonalDepositFreeAmount,
} from './admin-worker-user-service.js'
export { syncWorkerOrdersForSourceOrder } from './sync-worker-orders-service.js'
@@ -13,6 +13,7 @@ import { normalizePage, normalizePageSize } from '../admin/admin-query-utils.js'
import {
hashWorkerPassword,
mapWorkerUser,
normalizeAmountFen,
normalizeOptionalId,
normalizeReviewStatus,
normalizeSessionVersion,
@@ -89,6 +90,30 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload:
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(
workerId: number | string,
payload: JsonObject = {},
@@ -315,6 +315,8 @@ export function mapWorkOrderForWorker(
freezeDepositAmount: myShare
? Number(myShare.share_deposit || 0)
: resolveFreezeDepositAmount(workOrder, permissions),
/** 用于大厅拼单按当前剩余份数准确计算个人免押后的押金。 */
depositFreeAmount: Number(permissions.depositFreeAmount || 0),
assignedAt: myShare?.created_at || visibleOrder.assignedAt,
submittedAt: myShare?.submitted_at || visibleOrder.submittedAt,
acceptedAt: myShare?.accepted_at || visibleOrder.acceptedAt,
@@ -62,6 +62,7 @@ export function normalizeWorkerType(value: unknown): 'internal' | 'external' {
}
export function mapWorkerUser(worker: WorkerUserRow) {
const permissions = resolveWorkerPermissions(worker)
return {
workerId: Number(worker.id),
username: worker.username,
@@ -85,9 +86,12 @@ export function mapWorkerUser(worker: WorkerUserRow) {
levelId: Number(worker.level_id),
levelKey: worker.level_key || '',
name: worker.level_name || '',
permissions: resolveWorkerPermissions(worker),
permissions,
}
: null,
personalDepositFreeAmount: permissions.personalDepositFreeAmount,
levelDepositFreeAmount: permissions.levelDepositFreeAmount,
effectiveDepositFreeAmount: permissions.depositFreeAmount,
wallet: {
availableAmount: Number(worker.available_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),
quantity: 0,
takeRemaining: true,
depositFreeAmount: permissions.depositFreeAmount,
maxActiveOrders: permissions.maxActiveOrders,
hallCandidateLimit: hallConfig.maxVisibleOrders,
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
@@ -143,6 +144,7 @@ export async function joinWorkerSharingOrder(
workOrderId: Number(workOrderId),
workerId: Number(worker.id),
quantity,
depositFreeAmount: permissions.depositFreeAmount,
maxActiveOrders: permissions.maxActiveOrders,
hallCandidateLimit: hallConfig.maxVisibleOrders,
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
@@ -8,8 +8,19 @@ import {
export function resolveWorkerPermissions(worker: WorkerUserRow) {
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 {
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
// 个人额度是在等级额度基础上的追加额度,两者叠加生效。
depositFreeAmount: levelDepositFreeAmount +
(hasPersonalDepositFreeAmount ? personalDepositFreeAmount : 0),
levelDepositFreeAmount,
personalDepositFreeAmount: hasPersonalDepositFreeAmount ? personalDepositFreeAmount : null,
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1),
upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0),
autoAcceptWithoutEvidence: normalizeBoolean(permission.autoAcceptWithoutEvidence, false),
@@ -13,6 +13,7 @@ import type {
import {
buildWorkOrderWhere,
resolveWorkOrderShareCancellationStatus,
resolveWorkOrderShareDepositAmount,
resolveWorkOrderSharingCancellationPlan,
resolveWorkOrderShareJoinQuantity,
resolveOpenSharingSyncReward,
@@ -36,6 +37,7 @@ import {
maskWorkerLeaderboardDisplayName,
resolveCollectSubmitTargetWorkOrder,
resolveAdminMaterialRewardAmount,
resolveFreezeDepositAmount,
resolveAdminMockWorkOrderInput,
resolveMatchingProductRuleDecision,
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 时不因母工单保留拼单配置而失去免审核资格', () => {
const vip5Worker = buildWorkerUserRow({
level_key: 'vip5',