From f20ff3018fdcad022856f8c8eb204a86edb0e4e0 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 28 Aug 2026 21:36:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=89=93=E6=89=8B):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E8=BF=BD=E5=8A=A0=E5=85=8D=E6=8A=BC=E9=A2=9D?= =?UTF-8?q?=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...65_worker_personal_deposit_free_amount.sql | 6 ++ .../src/repositories/worker-platform/types.ts | 2 + .../worker-platform/work-order-share-repo.ts | 37 +++++++- .../worker-platform/worker-repo.ts | 11 ++- .../routes/admin/worker-platform/workers.ts | 21 +++++ .../services/worker-platform/admin-service.ts | 1 + .../admin-worker-user-service.ts | 25 ++++++ .../work-order-display-mappers.ts | 2 + .../worker-platform/worker-finance-mappers.ts | 6 +- .../worker-hall-action-service.ts | 2 + .../worker-permission-utils.ts | 13 ++- .../worker-platform-service.test.ts | 39 +++++++++ apps/frontend/src/layouts/WorkerLayout.tsx | 1 + .../src/pages/admin/panels/WorkersPanel.tsx | 87 +++++++++++++++++++ .../src/services/admin/worker-platform.ts | 10 +++ apps/frontend/src/types/worker-platform.ts | 8 ++ .../frontend/src/utils/worker-product-name.ts | 11 ++- .../tests/worker-product-name.test.ts | 12 +++ 18 files changed, 282 insertions(+), 12 deletions(-) create mode 100644 apps/backend/src/db/migrations/065_worker_personal_deposit_free_amount.sql diff --git a/apps/backend/src/db/migrations/065_worker_personal_deposit_free_amount.sql b/apps/backend/src/db/migrations/065_worker_personal_deposit_free_amount.sql new file mode 100644 index 00000000..4db149cd --- /dev/null +++ b/apps/backend/src/db/migrations/065_worker_personal_deposit_free_amount.sql @@ -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 表示未追加'; diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index a7a2d866..2a482967 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -16,6 +16,8 @@ export type WorkerUserRow = { display_name: string phone: string level_id: number | null + /** 个人追加免押额度(分),与等级额度叠加。 */ + personal_deposit_free_amount?: number | null status: string worker_type: string review_note: string diff --git a/apps/backend/src/repositories/worker-platform/work-order-share-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-share-repo.ts index b1ae56fb..7764a780 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-share-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-share-repo.ts @@ -95,12 +95,37 @@ export function resolveWorkOrderShareJoinQuantity(input: { 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: { workOrderId: number workerId: number quantity: number /** 立即抢单时在事务锁内承接全部剩余份数。 */ takeRemaining?: boolean + /** 当前打手的有效免押额度(分),按其本次累计拼单份额抵扣。 */ + depositFreeAmount?: number maxActiveOrders: number hallCandidateLimit?: number hallCategoryLimits?: Record @@ -192,9 +217,15 @@ export async function joinWorkOrderShare(input: { const unitReward = toPositiveInteger(workOrder.sharing_unit_reward, 0) const nextQuantity = Number(existingShare?.quantity || 0) + requestedQuantity const shareReward = nextQuantity * unitReward - const nextShareDeposit = Math.round( - (Number(workOrder.required_deposit_amount || 0) * nextQuantity) / totalQuantity, - ) + const nextShareDeposit = resolveWorkOrderShareDepositAmount({ + 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( 0, nextShareDeposit - Number(existingShare?.share_deposit || 0), diff --git a/apps/backend/src/repositories/worker-platform/worker-repo.ts b/apps/backend/src/repositories/worker-platform/worker-repo.ts index 47d5be30..35b7b7f7 100644 --- a/apps/backend/src/repositories/worker-platform/worker-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-repo.ts @@ -145,6 +145,7 @@ export async function updateWorkerUser( | 'frozen_reason' | 'frozen_at' | 'level_id' + | 'personal_deposit_free_amount' | 'session_version' | 'updated_at' | 'reviewed_at' @@ -165,10 +166,11 @@ export async function updateWorkerUser( frozen_reason = $4, frozen_at = $5, level_id = $6, - session_version = $7, - reviewed_at = $8, - updated_at = $9 - WHERE id = $10 + personal_deposit_free_amount = $7, + session_version = $8, + reviewed_at = $9, + updated_at = $10 + WHERE id = $11 `, [ next.status, @@ -177,6 +179,7 @@ export async function updateWorkerUser( next.frozen_reason || '', next.frozen_at || null, next.level_id || null, + next.personal_deposit_free_amount ?? null, Number(next.session_version || 1), next.reviewed_at || null, next.updated_at, diff --git a/apps/backend/src/routes/admin/worker-platform/workers.ts b/apps/backend/src/routes/admin/worker-platform/workers.ts index 3820330b..38fe00ca 100644 --- a/apps/backend/src/routes/admin/worker-platform/workers.ts +++ b/apps/backend/src/routes/admin/worker-platform/workers.ts @@ -10,6 +10,7 @@ import { resetAdminWorkerPassword, reviewAdminWorkerUser, saveAdminWorkerWithdrawalAccount, + setAdminWorkerPersonalDepositFreeAmount, } from '../../../services/worker-platform/index.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) : {}, + }), + }, + ), +) + router.post( '/worker-platform/workers/:workerId/wallet-credit', requireAdminRoles(['admin', 'operator']), diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index 1e0f56c1..6bf89105 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -47,5 +47,6 @@ export { listAdminWorkerUsers, resetAdminWorkerPassword, reviewAdminWorkerUser, + setAdminWorkerPersonalDepositFreeAmount, } from './admin-worker-user-service.js' export { syncWorkerOrdersForSourceOrder } from './sync-worker-orders-service.js' diff --git a/apps/backend/src/services/worker-platform/admin-worker-user-service.ts b/apps/backend/src/services/worker-platform/admin-worker-user-service.ts index 73985cac..251b38a3 100644 --- a/apps/backend/src/services/worker-platform/admin-worker-user-service.ts +++ b/apps/backend/src/services/worker-platform/admin-worker-user-service.ts @@ -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 = {}, diff --git a/apps/backend/src/services/worker-platform/work-order-display-mappers.ts b/apps/backend/src/services/worker-platform/work-order-display-mappers.ts index 3b1635c3..37cb2ad1 100644 --- a/apps/backend/src/services/worker-platform/work-order-display-mappers.ts +++ b/apps/backend/src/services/worker-platform/work-order-display-mappers.ts @@ -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, diff --git a/apps/backend/src/services/worker-platform/worker-finance-mappers.ts b/apps/backend/src/services/worker-platform/worker-finance-mappers.ts index 7dab816e..e560828a 100644 --- a/apps/backend/src/services/worker-platform/worker-finance-mappers.ts +++ b/apps/backend/src/services/worker-platform/worker-finance-mappers.ts @@ -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), diff --git a/apps/backend/src/services/worker-platform/worker-hall-action-service.ts b/apps/backend/src/services/worker-platform/worker-hall-action-service.ts index cc93db4b..4c59b43f 100644 --- a/apps/backend/src/services/worker-platform/worker-hall-action-service.ts +++ b/apps/backend/src/services/worker-platform/worker-hall-action-service.ts @@ -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, diff --git a/apps/backend/src/services/worker-platform/worker-permission-utils.ts b/apps/backend/src/services/worker-platform/worker-permission-utils.ts index e0e201c1..95b6707e 100644 --- a/apps/backend/src/services/worker-platform/worker-permission-utils.ts +++ b/apps/backend/src/services/worker-platform/worker-permission-utils.ts @@ -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), diff --git a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts index ecb47995..06389534 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts @@ -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', diff --git a/apps/frontend/src/layouts/WorkerLayout.tsx b/apps/frontend/src/layouts/WorkerLayout.tsx index dea8bedf..31000706 100644 --- a/apps/frontend/src/layouts/WorkerLayout.tsx +++ b/apps/frontend/src/layouts/WorkerLayout.tsx @@ -355,6 +355,7 @@ function applyWorkerDepositRule(queryClient: ReturnType, const requiredDepositAmount = Number(data.requiredDepositAmount || 0) return { ...data, + depositFreeAmount, freezeDepositAmount: Math.max(0, requiredDepositAmount - depositFreeAmount), } } diff --git a/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx index a6e051b3..8ea43cb1 100644 --- a/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx @@ -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(null) const [resetPasswordWorker, setResetPasswordWorker] = useState(null) const [freezeWorker, setFreezeWorker] = useState(null) + const [personalDepositFreeWorker, setPersonalDepositFreeWorker] = useState( + null, + ) const [detailWorker, setDetailWorker] = useState(null) const autoOpenedDetailRef = useRef(0) const [withdrawalAccounts, setWithdrawalAccounts] = useState([]) @@ -76,6 +84,7 @@ export default function WorkersPanel() { const [withdrawalAccountForm] = Form.useForm() const [resetPasswordForm] = Form.useForm<{ password: string }>() const [freezeForm] = Form.useForm() + const [personalDepositFreeForm] = Form.useForm() 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 ? ( + + {formatMoney(row.effectiveDepositFreeAmount)}(等级 {formatMoney( + row.levelDepositFreeAmount, + )} + 个人 {formatMoney(row.personalDepositFreeAmount)}) + + ) : ( + + {formatMoney(row.effectiveDepositFreeAmount)}(等级) + + ), + }, { title: '邀请人', width: 160, @@ -428,6 +468,20 @@ export default function WorkersPanel() { > 调等级 + @@ -563,6 +617,39 @@ export default function WorkersPanel() { + { + setPersonalDepositFreeWorker(null) + personalDepositFreeForm.resetFields() + }} + onOk={() => personalDepositFreeForm.submit()} + destroyOnHidden + > +
+ + + {personalDepositFreeWorker?.displayName || personalDepositFreeWorker?.username || '-'} + + + + + + + 个人额度会叠加等级额度,合计额度以内的订单或拼单份额无需冻结押金;超过部分才会冻结。 + +
+
+ ( + `/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`, diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index b7ed48bc..8971e76d 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -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 diff --git a/apps/frontend/src/utils/worker-product-name.ts b/apps/frontend/src/utils/worker-product-name.ts index 51f9ccc6..be4e147f 100644 --- a/apps/frontend/src/utils/worker-product-name.ts +++ b/apps/frontend/src/utils/worker-product-name.ts @@ -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, ) } diff --git a/apps/frontend/tests/worker-product-name.test.ts b/apps/frontend/tests/worker-product-name.test.ts index 1e1937c7..98f58c8d 100644 --- a/apps/frontend/tests/worker-product-name.test.ts +++ b/apps/frontend/tests/worker-product-name.test.ts @@ -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,