押金解冻天数可配置与待解冻押金扣减,超时订单保留记录

This commit is contained in:
yml2213
2026-08-02 10:14:05 +08:00
parent 043dd3c128
commit 93df3d1c9b
20 changed files with 854 additions and 38 deletions
@@ -19,6 +19,7 @@ import {
createWorkOrderEvent,
createWorkerFinanceRequest,
createWorkerUser,
deductPendingDepositUnfreeze,
deleteWorkCategory,
deleteWorkOrder,
deleteWorkerLevel,
@@ -49,6 +50,7 @@ import {
listWorkerUsers,
listWorkOrderShares,
listWorkOrderSharesByOrderIds,
sumPendingUnfreezeByOrderIds,
resolveProblemWorkOrder,
reviewWorkerFinanceRequest,
submitWorkOrderShareAcceptance,
@@ -456,6 +458,9 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
bucket.push(share)
sharesByOrderId.set(orderId, bucket)
}
const pendingUnfreezeByOrderId = await sumPendingUnfreezeByOrderIds(
items.map((item) => Number(item.id)),
)
return {
items: items.map((item) => {
const orderShares = sharesByOrderId.get(Number(item.id)) || []
@@ -467,6 +472,7 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
).length
return {
...mapWorkOrderAdmin(item),
pendingUnfreezeAmount: pendingUnfreezeByOrderId.get(Number(item.id)) || 0,
sharingProgress: {
joinedQuantity,
pendingSubmissionCount,
@@ -477,6 +483,62 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
}
}
export async function deductAdminWorkOrderPendingDeposit(
workOrderId: number | string,
payload: JsonObject = {},
) {
const workOrder = await getRequiredWorkOrder(workOrderId)
if (workOrder.status !== WORK_ORDER_STATUS.ACCEPTED) {
throw createHttpError('只有已验收的工单可以扣减待解冻押金', {
statusCode: 409,
errorCode: 'work_order_deduct_pending_status_invalid',
})
}
const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0)
const note = String(payload.note || payload.reason || '').trim()
if (amount <= 0) {
throw createHttpError('扣减金额必须大于 0', {
statusCode: 400,
errorCode: 'work_order_deduct_pending_amount_invalid',
})
}
if (!note) {
throw createHttpError('请填写扣减原因', {
statusCode: 400,
errorCode: 'work_order_deduct_pending_note_required',
})
}
const result = await deductPendingDepositUnfreeze({
workOrderId: workOrder.id,
amount,
note,
now: nowIso(),
})
if (result.failureReason === 'no_pending_deposit') {
throw createHttpError('该工单没有待解冻的押金', {
statusCode: 409,
errorCode: 'work_order_deduct_pending_none',
})
}
if (result.failureReason === 'amount_exceeds_pending') {
throw createHttpError('扣减金额超过该工单待解冻押金总额', {
statusCode: 409,
errorCode: 'work_order_deduct_pending_amount_exceeds',
})
}
await createWorkOrderEvent({
workOrderId: workOrder.id,
actorType: 'admin',
actorId: '',
eventType: 'pending_deposit_deducted',
fromStatus: workOrder.status,
toStatus: workOrder.status,
payloadJson: JSON.stringify({ amount, note }),
now: nowIso(),
})
return { deductedAmount: result.deductedAmount }
}
export async function getAdminWorkOrderSharing(workOrderId: number | string) {
const workOrder = await getRequiredWorkOrder(workOrderId)
const shares = await listWorkOrderShares(workOrder.id)
@@ -937,6 +999,7 @@ export async function acceptAdminWorkOrder(workOrderId: number | string) {
const now = nowIso()
const { order: updated, failureReason } = await acceptWorkOrderAndSettle({
workOrderId: workOrder.id,
unfreezeDays: getWorkerFinanceConfig().depositUnfreezeDays,
now,
})
if (failureReason === 'sharing_pending_submissions') {
@@ -285,6 +285,7 @@ export function mapWorkerUser(worker: WorkerUserRow) {
wallet: {
availableAmount: Number(worker.available_amount || 0),
frozenDepositAmount: Number(worker.frozen_deposit_amount || 0),
pendingUnfreezeAmount: Number(worker.pending_unfreeze_amount || 0),
totalCreditedAmount: Number(worker.total_credited_amount || 0),
totalSettledAmount: Number(worker.total_settled_amount || 0),
},
@@ -452,6 +453,7 @@ export function mapWallet(wallet: Awaited<ReturnType<typeof addWorkerWalletCredi
workerId: Number(wallet?.worker_id || 0),
availableAmount: Number(wallet?.available_amount || 0),
frozenDepositAmount: Number(wallet?.frozen_deposit_amount || 0),
pendingUnfreezeAmount: Number(wallet?.pending_unfreeze_amount || 0),
totalCreditedAmount: Number(wallet?.total_credited_amount || 0),
totalSettledAmount: Number(wallet?.total_settled_amount || 0),
}
@@ -759,6 +761,8 @@ export function normalizeWalletLedgerType(value: unknown) {
'withdraw_paid',
'deposit_freeze',
'deposit_release',
'deposit_pending_unfreeze',
'deposit_unfreeze',
'deposit_deduction',
'reward_settlement',
].includes(ledgerType)
@@ -8,6 +8,7 @@ import {
} from '../file-storage/file-storage-service.js'
export type WorkerFinanceConfig = {
depositUnfreezeDays: number
recharge: {
enabled: boolean
channelName: string
@@ -46,6 +47,7 @@ export function normalizeWorkerFinanceConfig(rawValue: unknown): WorkerFinanceCo
const withdraw = isPlainObject(source.withdraw) ? source.withdraw : {}
return {
depositUnfreezeDays: normalizeRangeInteger(source.depositUnfreezeDays, 3, 0, 30),
recharge: {
enabled: typeof recharge.enabled === 'boolean' ? recharge.enabled : true,
channelName: String(recharge.channelName || recharge.channel_name || '').trim(),
@@ -65,6 +67,7 @@ export function normalizeWorkerFinanceConfig(rawValue: unknown): WorkerFinanceCo
export function createDefaultWorkerFinanceConfig(): WorkerFinanceConfig {
return {
depositUnfreezeDays: 3,
recharge: {
enabled: true,
channelName: '',
@@ -82,6 +85,7 @@ export function createDefaultWorkerFinanceConfig(): WorkerFinanceConfig {
function refreshWorkerFinanceConfig(config: WorkerFinanceConfig): WorkerFinanceConfig {
return {
depositUnfreezeDays: config.depositUnfreezeDays,
recharge: {
...config.recharge,
qrCodeImage: config.recharge.qrCodeImage
@@ -94,6 +98,14 @@ function refreshWorkerFinanceConfig(config: WorkerFinanceConfig): WorkerFinanceC
}
}
function normalizeRangeInteger(value: unknown, fallback: number, min: number, max: number) {
const parsed = Number(value)
if (!Number.isInteger(parsed)) {
return fallback
}
return Math.min(max, Math.max(min, parsed))
}
function normalizeUploadedFileForStorage(value: unknown): UploadedFileDto | null {
if (!isPlainObject(value)) {
return null
@@ -10,6 +10,7 @@ import {
countWorkerAcceptedOrders,
countWorkerActiveOrders,
countWorkerTimeoutEvents,
countWorkerWithdrawRequestsOnDay,
countWorkCategoryUsages,
countWorkerLevelUsages,
countWorkOrderPendingSharingSubmissions,
@@ -48,7 +49,9 @@ import {
listWorkerUsers,
listWorkOrderShares,
listWorkOrderSharesByOrderIds,
listDueDepositUnfreezes,
listOverdueWorkOrders,
releaseDepositUnfreeze,
resolveProblemWorkOrder,
reviewWorkerFinanceRequest,
settleOverdueWorkOrder,
@@ -521,6 +524,20 @@ export async function createWorkerWithdrawRequest(
errorCode: 'worker_withdraw_disabled',
})
}
await settleDueDepositUnfreezes({ workerId: worker.id, limit: 50 })
const withdrawCountToday = await countWorkerWithdrawRequestsOnDay(
worker.id,
resolveChinaDayRange().start,
resolveChinaDayRange().end,
)
if (withdrawCountToday > 0) {
throw createHttpError('每天限提现 1 次,今天已提交过提现申请', {
statusCode: 409,
errorCode: 'worker_withdraw_daily_limit_reached',
})
}
const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0)
if (amount <= 0) {
throw createHttpError('提现金额必须大于 0', {
@@ -743,6 +760,38 @@ export function normalizeWorkOrderTimeoutPolicy(value: unknown): WorkOrderTimeou
return isWorkOrderTimeoutPolicy(value) ? value : 'reopen'
}
export async function settleDueDepositUnfreezes(
options: { limit?: number; workerId?: number } = {},
) {
const due = await listDueDepositUnfreezes({
limit: Math.max(1, Number(options.limit || 100)),
workerId: Number(options.workerId || 0),
})
let processedCount = 0
for (const unfreeze of due) {
const released = await releaseDepositUnfreeze({
unfreezeId: unfreeze.id,
now: nowIso(),
})
if (released) processedCount += 1
}
return {
checkedCount: due.length,
processedCount,
}
}
function resolveChinaDayRange(): { start: string; end: string } {
const utcNow = new Date()
const utcDayStartMs =
Date.UTC(utcNow.getUTCFullYear(), utcNow.getUTCMonth(), utcNow.getUTCDate()) -
8 * 3_600_000
return {
start: new Date(utcDayStartMs).toISOString(),
end: new Date(utcDayStartMs + 86_400_000).toISOString(),
}
}
export async function settleOverdueWorkOrders(
options: { limit?: number; workerId?: number } = {},
) {