押金解冻天数可配置与待解冻押金扣减,超时订单保留记录
This commit is contained in:
@@ -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 } = {},
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user