拆分打手资金仓储
This commit is contained in:
@@ -4,6 +4,7 @@ export * from './worker-repo.js'
|
|||||||
export * from './worker-session-repo.js'
|
export * from './worker-session-repo.js'
|
||||||
export * from './worker-level-repo.js'
|
export * from './worker-level-repo.js'
|
||||||
export * from './worker-wallet-repo.js'
|
export * from './worker-wallet-repo.js'
|
||||||
|
export * from './worker-finance-repo.js'
|
||||||
export * from './work-order-repo.js'
|
export * from './work-order-repo.js'
|
||||||
export * from './work-order-event-repo.js'
|
export * from './work-order-event-repo.js'
|
||||||
export * from './work-order-share-repo.js'
|
export * from './work-order-share-repo.js'
|
||||||
|
|||||||
@@ -0,0 +1,451 @@
|
|||||||
|
import { query, withTransaction } from '../../db/client.js'
|
||||||
|
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
|
||||||
|
import type {
|
||||||
|
FinanceRequestListInput,
|
||||||
|
WorkerFinanceRequestRow,
|
||||||
|
WorkerWithdrawalAccountRow,
|
||||||
|
} from './types.js'
|
||||||
|
import type { PoolClient } from 'pg'
|
||||||
|
|
||||||
|
const WORKER_FINANCE_REQUEST_SELECT = `
|
||||||
|
SELECT
|
||||||
|
wfr.*,
|
||||||
|
wu.username AS worker_username,
|
||||||
|
wu.display_name AS worker_display_name,
|
||||||
|
wu.phone AS worker_phone,
|
||||||
|
wu.worker_type,
|
||||||
|
wl.level_key AS worker_level_key,
|
||||||
|
wl.name AS worker_level_name,
|
||||||
|
withdraw_stats.withdraw_request_count AS worker_withdraw_request_count,
|
||||||
|
withdraw_stats.total_withdraw_amount AS worker_total_withdraw_amount
|
||||||
|
FROM worker_finance_requests wfr
|
||||||
|
LEFT JOIN worker_users wu ON wu.id = wfr.worker_id
|
||||||
|
LEFT JOIN worker_levels wl ON wl.id = wu.level_id
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::int AS withdraw_request_count,
|
||||||
|
COALESCE(SUM(amount) FILTER (WHERE status = 'approved'), 0)::bigint AS total_withdraw_amount
|
||||||
|
FROM worker_finance_requests history_request
|
||||||
|
WHERE history_request.worker_id = wfr.worker_id
|
||||||
|
AND history_request.request_type = 'withdraw'
|
||||||
|
) withdraw_stats ON TRUE
|
||||||
|
`
|
||||||
|
|
||||||
|
export async function createWorkerFinanceRequest(input: {
|
||||||
|
workerId: number
|
||||||
|
requestType: string
|
||||||
|
amount: number
|
||||||
|
accountChannel: string
|
||||||
|
accountName: string
|
||||||
|
accountNo: string
|
||||||
|
note: string
|
||||||
|
payloadJson: string
|
||||||
|
now: string
|
||||||
|
}): Promise<WorkerFinanceRequestRow | null> {
|
||||||
|
const result = await query<WorkerFinanceRequestRow>(
|
||||||
|
`
|
||||||
|
INSERT INTO worker_finance_requests (
|
||||||
|
worker_id, request_type, status, amount,
|
||||||
|
account_channel, account_name, account_no,
|
||||||
|
note, reviewed_note, payload_json, created_at, updated_at
|
||||||
|
) VALUES ($1, $2, 'pending', $3, $4, $5, $6, $7, '', $8::jsonb, $9, $10)
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
input.workerId,
|
||||||
|
input.requestType,
|
||||||
|
input.amount,
|
||||||
|
input.accountChannel,
|
||||||
|
input.accountName,
|
||||||
|
input.accountNo,
|
||||||
|
input.note,
|
||||||
|
input.payloadJson,
|
||||||
|
input.now,
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return result.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWorkerWithdrawalAccount(
|
||||||
|
workerId: number | string,
|
||||||
|
accountChannel: string,
|
||||||
|
): Promise<WorkerWithdrawalAccountRow | null> {
|
||||||
|
const result = await query<WorkerWithdrawalAccountRow>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM worker_withdrawal_accounts
|
||||||
|
WHERE worker_id = $1 AND account_channel = $2
|
||||||
|
LIMIT 1
|
||||||
|
`,
|
||||||
|
[Number(workerId), accountChannel],
|
||||||
|
)
|
||||||
|
return result.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listWorkerWithdrawalAccounts(
|
||||||
|
workerId: number | string,
|
||||||
|
): Promise<WorkerWithdrawalAccountRow[]> {
|
||||||
|
const result = await query<WorkerWithdrawalAccountRow>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM worker_withdrawal_accounts
|
||||||
|
WHERE worker_id = $1
|
||||||
|
ORDER BY account_channel ASC
|
||||||
|
`,
|
||||||
|
[Number(workerId)],
|
||||||
|
)
|
||||||
|
return result.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createWorkerWithdrawalAccount(input: {
|
||||||
|
workerId: number
|
||||||
|
accountChannel: string
|
||||||
|
accountName: string
|
||||||
|
accountNo: string
|
||||||
|
alipayQrCodeJson: string
|
||||||
|
wechatQrCodeJson: string
|
||||||
|
now: string
|
||||||
|
}): Promise<WorkerWithdrawalAccountRow | null> {
|
||||||
|
const result = await query<WorkerWithdrawalAccountRow>(
|
||||||
|
`
|
||||||
|
INSERT INTO worker_withdrawal_accounts (
|
||||||
|
worker_id, account_channel, account_name, account_no,
|
||||||
|
alipay_qr_code_json, wechat_qr_code_json, created_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7)
|
||||||
|
ON CONFLICT (worker_id, account_channel) DO NOTHING
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
input.workerId,
|
||||||
|
input.accountChannel,
|
||||||
|
input.accountName,
|
||||||
|
input.accountNo,
|
||||||
|
input.alipayQrCodeJson,
|
||||||
|
input.wechatQrCodeJson,
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return result.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertWorkerWithdrawalAccount(input: {
|
||||||
|
workerId: number
|
||||||
|
accountChannel: string
|
||||||
|
accountName: string
|
||||||
|
accountNo: string
|
||||||
|
alipayQrCodeJson: string
|
||||||
|
wechatQrCodeJson: string
|
||||||
|
now: string
|
||||||
|
}): Promise<WorkerWithdrawalAccountRow | null> {
|
||||||
|
const result = await query<WorkerWithdrawalAccountRow>(
|
||||||
|
`
|
||||||
|
INSERT INTO worker_withdrawal_accounts (
|
||||||
|
worker_id, account_channel, account_name, account_no,
|
||||||
|
alipay_qr_code_json, wechat_qr_code_json, created_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7)
|
||||||
|
ON CONFLICT (worker_id, account_channel) DO UPDATE
|
||||||
|
SET
|
||||||
|
account_name = EXCLUDED.account_name,
|
||||||
|
account_no = EXCLUDED.account_no,
|
||||||
|
alipay_qr_code_json = EXCLUDED.alipay_qr_code_json,
|
||||||
|
wechat_qr_code_json = EXCLUDED.wechat_qr_code_json
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
input.workerId,
|
||||||
|
input.accountChannel,
|
||||||
|
input.accountName,
|
||||||
|
input.accountNo,
|
||||||
|
input.alipayQrCodeJson,
|
||||||
|
input.wechatQrCodeJson,
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return result.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWorkerFinanceRequestById(
|
||||||
|
requestId: number | string,
|
||||||
|
): Promise<WorkerFinanceRequestRow | null> {
|
||||||
|
const result = await query<WorkerFinanceRequestRow>(
|
||||||
|
`${WORKER_FINANCE_REQUEST_SELECT} WHERE wfr.id = $1 LIMIT 1`,
|
||||||
|
[Number(requestId)],
|
||||||
|
)
|
||||||
|
return result.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function countWorkerWithdrawRequestsOnDay(
|
||||||
|
workerId: number | string,
|
||||||
|
dayStartIso: string,
|
||||||
|
dayEndIso: string,
|
||||||
|
): Promise<number> {
|
||||||
|
const result = await query<{ total: number }>(
|
||||||
|
`
|
||||||
|
SELECT COUNT(*)::int AS total
|
||||||
|
FROM worker_finance_requests
|
||||||
|
WHERE worker_id = $1
|
||||||
|
AND request_type = 'withdraw'
|
||||||
|
AND status != 'cancelled'
|
||||||
|
AND created_at >= $2
|
||||||
|
AND created_at < $3
|
||||||
|
`,
|
||||||
|
[Number(workerId), dayStartIso, dayEndIso],
|
||||||
|
)
|
||||||
|
return Number(result.rows[0]?.total || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listWorkerFinanceRequests({
|
||||||
|
page = 1,
|
||||||
|
pageSize = 20,
|
||||||
|
requestId = 0,
|
||||||
|
workerId = 0,
|
||||||
|
status = '',
|
||||||
|
requestType = '',
|
||||||
|
keyword = '',
|
||||||
|
}: FinanceRequestListInput = {}): Promise<{ items: WorkerFinanceRequestRow[]; total: number }> {
|
||||||
|
const { whereClause, params } = buildWorkerFinanceRequestWhere({
|
||||||
|
requestId,
|
||||||
|
workerId,
|
||||||
|
status,
|
||||||
|
requestType,
|
||||||
|
keyword,
|
||||||
|
})
|
||||||
|
const totalResult = await query<{ total: number }>(
|
||||||
|
`SELECT COUNT(*)::int AS total FROM worker_finance_requests wfr
|
||||||
|
LEFT JOIN worker_users wu ON wu.id = wfr.worker_id
|
||||||
|
${whereClause}`,
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
const offset = (page - 1) * pageSize
|
||||||
|
params.push(pageSize, offset)
|
||||||
|
const itemsResult = await query<WorkerFinanceRequestRow>(
|
||||||
|
`${WORKER_FINANCE_REQUEST_SELECT}
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY wfr.created_at DESC, wfr.id DESC
|
||||||
|
LIMIT $${params.length - 1} OFFSET $${params.length}`,
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
items: itemsResult.rows,
|
||||||
|
total: Number(totalResult.rows[0]?.total || 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reviewWorkerFinanceRequest(input: {
|
||||||
|
requestId: number
|
||||||
|
status: string
|
||||||
|
reviewedNote: string
|
||||||
|
now: string
|
||||||
|
}): Promise<{
|
||||||
|
request: WorkerFinanceRequestRow | null
|
||||||
|
failureReason: 'request_not_pending' | 'withdraw_insufficient' | null
|
||||||
|
}> {
|
||||||
|
return withTransaction(async (client) => {
|
||||||
|
const currentResult = await client.query<WorkerFinanceRequestRow>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM worker_finance_requests
|
||||||
|
WHERE id = $1
|
||||||
|
LIMIT 1
|
||||||
|
FOR UPDATE
|
||||||
|
`,
|
||||||
|
[input.requestId],
|
||||||
|
)
|
||||||
|
const current = currentResult.rows[0] || null
|
||||||
|
if (!current) return { request: null, failureReason: null }
|
||||||
|
|
||||||
|
if (current.status !== 'pending') {
|
||||||
|
return {
|
||||||
|
request: await getWorkerFinanceRequestByIdWithClient(client, input.requestId),
|
||||||
|
failureReason: 'request_not_pending',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.status === 'approved') {
|
||||||
|
await ensureWorkerWalletWithClient(client, Number(current.worker_id), input.now)
|
||||||
|
const wallet = await getWorkerWalletWithClient(client, Number(current.worker_id))
|
||||||
|
const availableAmount = Number(wallet?.available_amount || 0)
|
||||||
|
const frozenAmount = Number(wallet?.frozen_deposit_amount || 0)
|
||||||
|
const amount = Number(current.amount || 0)
|
||||||
|
const note =
|
||||||
|
input.reviewedNote ||
|
||||||
|
(current.request_type === 'withdraw' ? '提现申请审核通过' : '充值申请审核通过')
|
||||||
|
|
||||||
|
if (current.request_type === 'withdraw') {
|
||||||
|
if (availableAmount < amount) {
|
||||||
|
return {
|
||||||
|
request: await getWorkerFinanceRequestByIdWithClient(client, input.requestId),
|
||||||
|
failureReason: 'withdraw_insufficient',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nextAvailable = availableAmount - amount
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE worker_wallets
|
||||||
|
SET available_amount = $1, updated_at = $2
|
||||||
|
WHERE worker_id = $3
|
||||||
|
`,
|
||||||
|
[nextAvailable, input.now, Number(current.worker_id)],
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO worker_wallet_ledgers (
|
||||||
|
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||||
|
audit_status, note, payload_json, created_at
|
||||||
|
) VALUES ($1, 'withdraw_paid', $2, $3, $4, 'approved', $5, $6::jsonb, $7)
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
Number(current.worker_id),
|
||||||
|
-amount,
|
||||||
|
nextAvailable,
|
||||||
|
frozenAmount,
|
||||||
|
note,
|
||||||
|
JSON.stringify({
|
||||||
|
requestId: Number(current.id),
|
||||||
|
requestType: current.request_type,
|
||||||
|
source: 'admin_finance_review',
|
||||||
|
}),
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const nextAvailable = availableAmount + amount
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE worker_wallets
|
||||||
|
SET
|
||||||
|
available_amount = $1,
|
||||||
|
total_credited_amount = total_credited_amount + $2,
|
||||||
|
updated_at = $3
|
||||||
|
WHERE worker_id = $4
|
||||||
|
`,
|
||||||
|
[nextAvailable, amount, input.now, Number(current.worker_id)],
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO worker_wallet_ledgers (
|
||||||
|
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||||
|
audit_status, note, payload_json, created_at
|
||||||
|
) VALUES ($1, 'manual_credit', $2, $3, $4, 'approved', $5, $6::jsonb, $7)
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
Number(current.worker_id),
|
||||||
|
amount,
|
||||||
|
nextAvailable,
|
||||||
|
frozenAmount,
|
||||||
|
note,
|
||||||
|
JSON.stringify({
|
||||||
|
requestId: Number(current.id),
|
||||||
|
requestType: current.request_type,
|
||||||
|
source: 'admin_finance_review',
|
||||||
|
}),
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE worker_finance_requests
|
||||||
|
SET
|
||||||
|
status = $1,
|
||||||
|
reviewed_note = $2,
|
||||||
|
reviewed_at = $3,
|
||||||
|
updated_at = $3
|
||||||
|
WHERE id = $4
|
||||||
|
`,
|
||||||
|
[input.status, input.reviewedNote, input.now, input.requestId],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
request: await getWorkerFinanceRequestByIdWithClient(client, input.requestId),
|
||||||
|
failureReason: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWorkerFinanceRequestSummary(workerId: number | string) {
|
||||||
|
const result = await query<{
|
||||||
|
pending_withdraw_amount: number
|
||||||
|
approved_withdraw_amount: number
|
||||||
|
pending_recharge_amount: number
|
||||||
|
}>(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
COALESCE(
|
||||||
|
SUM(CASE WHEN request_type = 'withdraw' AND status = 'pending' THEN amount ELSE 0 END),
|
||||||
|
0
|
||||||
|
)::int AS pending_withdraw_amount,
|
||||||
|
COALESCE(
|
||||||
|
SUM(CASE WHEN request_type = 'withdraw' AND status = 'approved' THEN amount ELSE 0 END),
|
||||||
|
0
|
||||||
|
)::int AS approved_withdraw_amount,
|
||||||
|
COALESCE(
|
||||||
|
SUM(CASE WHEN request_type = 'recharge' AND status = 'pending' THEN amount ELSE 0 END),
|
||||||
|
0
|
||||||
|
)::int AS pending_recharge_amount
|
||||||
|
FROM worker_finance_requests
|
||||||
|
WHERE worker_id = $1
|
||||||
|
`,
|
||||||
|
[Number(workerId)],
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
pendingWithdrawAmount: Number(result.rows[0]?.pending_withdraw_amount || 0),
|
||||||
|
approvedWithdrawAmount: Number(result.rows[0]?.approved_withdraw_amount || 0),
|
||||||
|
pendingRechargeAmount: Number(result.rows[0]?.pending_recharge_amount || 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWorkerFinanceRequestByIdWithClient(
|
||||||
|
client: PoolClient,
|
||||||
|
requestId: number | string,
|
||||||
|
): Promise<WorkerFinanceRequestRow | null> {
|
||||||
|
const result = await client.query<WorkerFinanceRequestRow>(
|
||||||
|
`${WORKER_FINANCE_REQUEST_SELECT} WHERE wfr.id = $1 LIMIT 1`,
|
||||||
|
[Number(requestId)],
|
||||||
|
)
|
||||||
|
return result.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWorkerFinanceRequestWhere({
|
||||||
|
requestId = 0,
|
||||||
|
workerId = 0,
|
||||||
|
status = '',
|
||||||
|
requestType = '',
|
||||||
|
keyword = '',
|
||||||
|
}: FinanceRequestListInput) {
|
||||||
|
const filters: string[] = []
|
||||||
|
const params: unknown[] = []
|
||||||
|
if (requestId) {
|
||||||
|
params.push(requestId)
|
||||||
|
filters.push(`wfr.id = $${params.length}`)
|
||||||
|
}
|
||||||
|
if (workerId) {
|
||||||
|
params.push(workerId)
|
||||||
|
filters.push(`wfr.worker_id = $${params.length}`)
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
params.push(status)
|
||||||
|
filters.push(`wfr.status = $${params.length}`)
|
||||||
|
}
|
||||||
|
if (requestType) {
|
||||||
|
params.push(requestType)
|
||||||
|
filters.push(`wfr.request_type = $${params.length}`)
|
||||||
|
}
|
||||||
|
if (keyword) {
|
||||||
|
params.push(`%${keyword}%`)
|
||||||
|
filters.push(
|
||||||
|
`(wu.username ILIKE $${params.length}
|
||||||
|
OR wu.display_name ILIKE $${params.length}
|
||||||
|
OR wu.phone ILIKE $${params.length}
|
||||||
|
OR wfr.account_name ILIKE $${params.length}
|
||||||
|
OR wfr.account_no ILIKE $${params.length})`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
import { query, withTransaction } from '../../db/client.js'
|
import { query, withTransaction } from '../../db/client.js'
|
||||||
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
|
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
|
||||||
import type {
|
import type { CreateWorkerInput, ListInput, WorkerUserRow } from './types.js'
|
||||||
CreateWorkerInput,
|
|
||||||
FinanceRequestListInput,
|
|
||||||
ListInput,
|
|
||||||
WorkerFinanceRequestRow,
|
|
||||||
WorkerUserRow,
|
|
||||||
WorkerWithdrawalAccountRow,
|
|
||||||
} from './types.js'
|
|
||||||
import type { PoolClient } from 'pg'
|
import type { PoolClient } from 'pg'
|
||||||
|
|
||||||
const WORKER_USER_SELECT = `
|
const WORKER_USER_SELECT = `
|
||||||
@@ -29,30 +22,6 @@ const WORKER_USER_SELECT = `
|
|||||||
LEFT JOIN worker_wallets ww ON ww.worker_id = wu.id
|
LEFT JOIN worker_wallets ww ON ww.worker_id = wu.id
|
||||||
`
|
`
|
||||||
|
|
||||||
const WORKER_FINANCE_REQUEST_SELECT = `
|
|
||||||
SELECT
|
|
||||||
wfr.*,
|
|
||||||
wu.username AS worker_username,
|
|
||||||
wu.display_name AS worker_display_name,
|
|
||||||
wu.phone AS worker_phone,
|
|
||||||
wu.worker_type,
|
|
||||||
wl.level_key AS worker_level_key,
|
|
||||||
wl.name AS worker_level_name,
|
|
||||||
withdraw_stats.withdraw_request_count AS worker_withdraw_request_count,
|
|
||||||
withdraw_stats.total_withdraw_amount AS worker_total_withdraw_amount
|
|
||||||
FROM worker_finance_requests wfr
|
|
||||||
LEFT JOIN worker_users wu ON wu.id = wfr.worker_id
|
|
||||||
LEFT JOIN worker_levels wl ON wl.id = wu.level_id
|
|
||||||
LEFT JOIN LATERAL (
|
|
||||||
SELECT
|
|
||||||
COUNT(*)::int AS withdraw_request_count,
|
|
||||||
COALESCE(SUM(amount) FILTER (WHERE status = 'approved'), 0)::bigint AS total_withdraw_amount
|
|
||||||
FROM worker_finance_requests history_request
|
|
||||||
WHERE history_request.worker_id = wfr.worker_id
|
|
||||||
AND history_request.request_type = 'withdraw'
|
|
||||||
) withdraw_stats ON TRUE
|
|
||||||
`
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createWorkerSessionRecord,
|
createWorkerSessionRecord,
|
||||||
getWorkerSessionBySessionId,
|
getWorkerSessionBySessionId,
|
||||||
@@ -293,378 +262,18 @@ export {
|
|||||||
listWorkerWalletLedgers,
|
listWorkerWalletLedgers,
|
||||||
} from './worker-wallet-repo.js'
|
} from './worker-wallet-repo.js'
|
||||||
|
|
||||||
export async function createWorkerFinanceRequest(input: {
|
export {
|
||||||
workerId: number
|
countWorkerWithdrawRequestsOnDay,
|
||||||
requestType: string
|
createWorkerFinanceRequest,
|
||||||
amount: number
|
createWorkerWithdrawalAccount,
|
||||||
accountChannel: string
|
getWorkerFinanceRequestById,
|
||||||
accountName: string
|
getWorkerFinanceRequestSummary,
|
||||||
accountNo: string
|
getWorkerWithdrawalAccount,
|
||||||
note: string
|
listWorkerFinanceRequests,
|
||||||
payloadJson: string
|
listWorkerWithdrawalAccounts,
|
||||||
now: string
|
reviewWorkerFinanceRequest,
|
||||||
}): Promise<WorkerFinanceRequestRow | null> {
|
upsertWorkerWithdrawalAccount,
|
||||||
const result = await query<WorkerFinanceRequestRow>(
|
} from './worker-finance-repo.js'
|
||||||
`
|
|
||||||
INSERT INTO worker_finance_requests (
|
|
||||||
worker_id, request_type, status, amount,
|
|
||||||
account_channel, account_name, account_no,
|
|
||||||
note, reviewed_note, payload_json, created_at, updated_at
|
|
||||||
) VALUES ($1, $2, 'pending', $3, $4, $5, $6, $7, '', $8::jsonb, $9, $10)
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[
|
|
||||||
input.workerId,
|
|
||||||
input.requestType,
|
|
||||||
input.amount,
|
|
||||||
input.accountChannel,
|
|
||||||
input.accountName,
|
|
||||||
input.accountNo,
|
|
||||||
input.note,
|
|
||||||
input.payloadJson,
|
|
||||||
input.now,
|
|
||||||
input.now,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
return result.rows[0] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWorkerWithdrawalAccount(
|
|
||||||
workerId: number | string,
|
|
||||||
accountChannel: string,
|
|
||||||
): Promise<WorkerWithdrawalAccountRow | null> {
|
|
||||||
const result = await query<WorkerWithdrawalAccountRow>(
|
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM worker_withdrawal_accounts
|
|
||||||
WHERE worker_id = $1 AND account_channel = $2
|
|
||||||
LIMIT 1
|
|
||||||
`,
|
|
||||||
[Number(workerId), accountChannel],
|
|
||||||
)
|
|
||||||
return result.rows[0] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listWorkerWithdrawalAccounts(
|
|
||||||
workerId: number | string,
|
|
||||||
): Promise<WorkerWithdrawalAccountRow[]> {
|
|
||||||
const result = await query<WorkerWithdrawalAccountRow>(
|
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM worker_withdrawal_accounts
|
|
||||||
WHERE worker_id = $1
|
|
||||||
ORDER BY account_channel ASC
|
|
||||||
`,
|
|
||||||
[Number(workerId)],
|
|
||||||
)
|
|
||||||
return result.rows
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createWorkerWithdrawalAccount(input: {
|
|
||||||
workerId: number
|
|
||||||
accountChannel: string
|
|
||||||
accountName: string
|
|
||||||
accountNo: string
|
|
||||||
alipayQrCodeJson: string
|
|
||||||
wechatQrCodeJson: string
|
|
||||||
now: string
|
|
||||||
}): Promise<WorkerWithdrawalAccountRow | null> {
|
|
||||||
const result = await query<WorkerWithdrawalAccountRow>(
|
|
||||||
`
|
|
||||||
INSERT INTO worker_withdrawal_accounts (
|
|
||||||
worker_id, account_channel, account_name, account_no,
|
|
||||||
alipay_qr_code_json, wechat_qr_code_json, created_at
|
|
||||||
) VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7)
|
|
||||||
ON CONFLICT (worker_id, account_channel) DO NOTHING
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[
|
|
||||||
input.workerId,
|
|
||||||
input.accountChannel,
|
|
||||||
input.accountName,
|
|
||||||
input.accountNo,
|
|
||||||
input.alipayQrCodeJson,
|
|
||||||
input.wechatQrCodeJson,
|
|
||||||
input.now,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
return result.rows[0] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function upsertWorkerWithdrawalAccount(input: {
|
|
||||||
workerId: number
|
|
||||||
accountChannel: string
|
|
||||||
accountName: string
|
|
||||||
accountNo: string
|
|
||||||
alipayQrCodeJson: string
|
|
||||||
wechatQrCodeJson: string
|
|
||||||
now: string
|
|
||||||
}): Promise<WorkerWithdrawalAccountRow | null> {
|
|
||||||
const result = await query<WorkerWithdrawalAccountRow>(
|
|
||||||
`
|
|
||||||
INSERT INTO worker_withdrawal_accounts (
|
|
||||||
worker_id, account_channel, account_name, account_no,
|
|
||||||
alipay_qr_code_json, wechat_qr_code_json, created_at
|
|
||||||
) VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7)
|
|
||||||
ON CONFLICT (worker_id, account_channel) DO UPDATE
|
|
||||||
SET
|
|
||||||
account_name = EXCLUDED.account_name,
|
|
||||||
account_no = EXCLUDED.account_no,
|
|
||||||
alipay_qr_code_json = EXCLUDED.alipay_qr_code_json,
|
|
||||||
wechat_qr_code_json = EXCLUDED.wechat_qr_code_json
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[
|
|
||||||
input.workerId,
|
|
||||||
input.accountChannel,
|
|
||||||
input.accountName,
|
|
||||||
input.accountNo,
|
|
||||||
input.alipayQrCodeJson,
|
|
||||||
input.wechatQrCodeJson,
|
|
||||||
input.now,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
return result.rows[0] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWorkerFinanceRequestById(
|
|
||||||
requestId: number | string,
|
|
||||||
): Promise<WorkerFinanceRequestRow | null> {
|
|
||||||
const result = await query<WorkerFinanceRequestRow>(
|
|
||||||
`${WORKER_FINANCE_REQUEST_SELECT} WHERE wfr.id = $1 LIMIT 1`,
|
|
||||||
[Number(requestId)],
|
|
||||||
)
|
|
||||||
return result.rows[0] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function countWorkerWithdrawRequestsOnDay(
|
|
||||||
workerId: number | string,
|
|
||||||
dayStartIso: string,
|
|
||||||
dayEndIso: string,
|
|
||||||
): Promise<number> {
|
|
||||||
const result = await query<{ total: number }>(
|
|
||||||
`
|
|
||||||
SELECT COUNT(*)::int AS total
|
|
||||||
FROM worker_finance_requests
|
|
||||||
WHERE worker_id = $1
|
|
||||||
AND request_type = 'withdraw'
|
|
||||||
AND status != 'cancelled'
|
|
||||||
AND created_at >= $2
|
|
||||||
AND created_at < $3
|
|
||||||
`,
|
|
||||||
[Number(workerId), dayStartIso, dayEndIso],
|
|
||||||
)
|
|
||||||
return Number(result.rows[0]?.total || 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listWorkerFinanceRequests({
|
|
||||||
page = 1,
|
|
||||||
pageSize = 20,
|
|
||||||
requestId = 0,
|
|
||||||
workerId = 0,
|
|
||||||
status = '',
|
|
||||||
requestType = '',
|
|
||||||
keyword = '',
|
|
||||||
}: FinanceRequestListInput = {}): Promise<{ items: WorkerFinanceRequestRow[]; total: number }> {
|
|
||||||
const { whereClause, params } = buildWorkerFinanceRequestWhere({
|
|
||||||
requestId,
|
|
||||||
workerId,
|
|
||||||
status,
|
|
||||||
requestType,
|
|
||||||
keyword,
|
|
||||||
})
|
|
||||||
const totalResult = await query<{ total: number }>(
|
|
||||||
`SELECT COUNT(*)::int AS total FROM worker_finance_requests wfr
|
|
||||||
LEFT JOIN worker_users wu ON wu.id = wfr.worker_id
|
|
||||||
${whereClause}`,
|
|
||||||
params,
|
|
||||||
)
|
|
||||||
const offset = (page - 1) * pageSize
|
|
||||||
params.push(pageSize, offset)
|
|
||||||
const itemsResult = await query<WorkerFinanceRequestRow>(
|
|
||||||
`${WORKER_FINANCE_REQUEST_SELECT}
|
|
||||||
${whereClause}
|
|
||||||
ORDER BY wfr.created_at DESC, wfr.id DESC
|
|
||||||
LIMIT $${params.length - 1} OFFSET $${params.length}`,
|
|
||||||
params,
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
items: itemsResult.rows,
|
|
||||||
total: Number(totalResult.rows[0]?.total || 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function reviewWorkerFinanceRequest(input: {
|
|
||||||
requestId: number
|
|
||||||
status: string
|
|
||||||
reviewedNote: string
|
|
||||||
now: string
|
|
||||||
}): Promise<{
|
|
||||||
request: WorkerFinanceRequestRow | null
|
|
||||||
failureReason: 'request_not_pending' | 'withdraw_insufficient' | null
|
|
||||||
}> {
|
|
||||||
return withTransaction(async (client) => {
|
|
||||||
const currentResult = await client.query<WorkerFinanceRequestRow>(
|
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM worker_finance_requests
|
|
||||||
WHERE id = $1
|
|
||||||
LIMIT 1
|
|
||||||
FOR UPDATE
|
|
||||||
`,
|
|
||||||
[input.requestId],
|
|
||||||
)
|
|
||||||
const current = currentResult.rows[0] || null
|
|
||||||
if (!current) {
|
|
||||||
return {
|
|
||||||
request: null,
|
|
||||||
failureReason: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current.status !== 'pending') {
|
|
||||||
return {
|
|
||||||
request: await getWorkerFinanceRequestByIdWithClient(client, input.requestId),
|
|
||||||
failureReason: 'request_not_pending',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.status === 'approved') {
|
|
||||||
await ensureWorkerWalletWithClient(client, Number(current.worker_id), input.now)
|
|
||||||
const wallet = await getWorkerWalletWithClient(client, Number(current.worker_id))
|
|
||||||
const availableAmount = Number(wallet?.available_amount || 0)
|
|
||||||
const frozenAmount = Number(wallet?.frozen_deposit_amount || 0)
|
|
||||||
const amount = Number(current.amount || 0)
|
|
||||||
const note =
|
|
||||||
input.reviewedNote ||
|
|
||||||
(current.request_type === 'withdraw' ? '提现申请审核通过' : '充值申请审核通过')
|
|
||||||
|
|
||||||
if (current.request_type === 'withdraw') {
|
|
||||||
if (availableAmount < amount) {
|
|
||||||
return {
|
|
||||||
request: await getWorkerFinanceRequestByIdWithClient(client, input.requestId),
|
|
||||||
failureReason: 'withdraw_insufficient',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextAvailable = availableAmount - amount
|
|
||||||
await client.query(
|
|
||||||
`
|
|
||||||
UPDATE worker_wallets
|
|
||||||
SET available_amount = $1, updated_at = $2
|
|
||||||
WHERE worker_id = $3
|
|
||||||
`,
|
|
||||||
[nextAvailable, input.now, Number(current.worker_id)],
|
|
||||||
)
|
|
||||||
await client.query(
|
|
||||||
`
|
|
||||||
INSERT INTO worker_wallet_ledgers (
|
|
||||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
|
||||||
audit_status, note, payload_json, created_at
|
|
||||||
) VALUES ($1, 'withdraw_paid', $2, $3, $4, 'approved', $5, $6::jsonb, $7)
|
|
||||||
`,
|
|
||||||
[
|
|
||||||
Number(current.worker_id),
|
|
||||||
-amount,
|
|
||||||
nextAvailable,
|
|
||||||
frozenAmount,
|
|
||||||
note,
|
|
||||||
JSON.stringify({
|
|
||||||
requestId: Number(current.id),
|
|
||||||
requestType: current.request_type,
|
|
||||||
source: 'admin_finance_review',
|
|
||||||
}),
|
|
||||||
input.now,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
const nextAvailable = availableAmount + amount
|
|
||||||
await client.query(
|
|
||||||
`
|
|
||||||
UPDATE worker_wallets
|
|
||||||
SET
|
|
||||||
available_amount = $1,
|
|
||||||
total_credited_amount = total_credited_amount + $2,
|
|
||||||
updated_at = $3
|
|
||||||
WHERE worker_id = $4
|
|
||||||
`,
|
|
||||||
[nextAvailable, amount, input.now, Number(current.worker_id)],
|
|
||||||
)
|
|
||||||
await client.query(
|
|
||||||
`
|
|
||||||
INSERT INTO worker_wallet_ledgers (
|
|
||||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
|
||||||
audit_status, note, payload_json, created_at
|
|
||||||
) VALUES ($1, 'manual_credit', $2, $3, $4, 'approved', $5, $6::jsonb, $7)
|
|
||||||
`,
|
|
||||||
[
|
|
||||||
Number(current.worker_id),
|
|
||||||
amount,
|
|
||||||
nextAvailable,
|
|
||||||
frozenAmount,
|
|
||||||
note,
|
|
||||||
JSON.stringify({
|
|
||||||
requestId: Number(current.id),
|
|
||||||
requestType: current.request_type,
|
|
||||||
source: 'admin_finance_review',
|
|
||||||
}),
|
|
||||||
input.now,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.query(
|
|
||||||
`
|
|
||||||
UPDATE worker_finance_requests
|
|
||||||
SET
|
|
||||||
status = $1,
|
|
||||||
reviewed_note = $2,
|
|
||||||
reviewed_at = $3,
|
|
||||||
updated_at = $3
|
|
||||||
WHERE id = $4
|
|
||||||
`,
|
|
||||||
[input.status, input.reviewedNote, input.now, input.requestId],
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
request: await getWorkerFinanceRequestByIdWithClient(client, input.requestId),
|
|
||||||
failureReason: null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWorkerFinanceRequestSummary(workerId: number | string) {
|
|
||||||
const result = await query<{
|
|
||||||
pending_withdraw_amount: number
|
|
||||||
approved_withdraw_amount: number
|
|
||||||
pending_recharge_amount: number
|
|
||||||
}>(
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
COALESCE(
|
|
||||||
SUM(CASE WHEN request_type = 'withdraw' AND status = 'pending' THEN amount ELSE 0 END),
|
|
||||||
0
|
|
||||||
)::int AS pending_withdraw_amount,
|
|
||||||
COALESCE(
|
|
||||||
SUM(CASE WHEN request_type = 'withdraw' AND status = 'approved' THEN amount ELSE 0 END),
|
|
||||||
0
|
|
||||||
)::int AS approved_withdraw_amount,
|
|
||||||
COALESCE(
|
|
||||||
SUM(CASE WHEN request_type = 'recharge' AND status = 'pending' THEN amount ELSE 0 END),
|
|
||||||
0
|
|
||||||
)::int AS pending_recharge_amount
|
|
||||||
FROM worker_finance_requests
|
|
||||||
WHERE worker_id = $1
|
|
||||||
`,
|
|
||||||
[Number(workerId)],
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
pendingWithdrawAmount: Number(result.rows[0]?.pending_withdraw_amount || 0),
|
|
||||||
approvedWithdrawAmount: Number(result.rows[0]?.approved_withdraw_amount || 0),
|
|
||||||
pendingRechargeAmount: Number(result.rows[0]?.pending_recharge_amount || 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 完成单数按源订单去重,拼单份数只影响收益,不增加完成单数。 */
|
/** 完成单数按源订单去重,拼单份数只影响收益,不增加完成单数。 */
|
||||||
export async function countWorkerAcceptedOrders(workerId: number | string): Promise<number> {
|
export async function countWorkerAcceptedOrders(workerId: number | string): Promise<number> {
|
||||||
@@ -939,17 +548,6 @@ async function getWorkerUserByIdWithClient(
|
|||||||
return result.rows[0] || null
|
return result.rows[0] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWorkerFinanceRequestByIdWithClient(
|
|
||||||
client: PoolClient,
|
|
||||||
requestId: number | string,
|
|
||||||
): Promise<WorkerFinanceRequestRow | null> {
|
|
||||||
const result = await client.query<WorkerFinanceRequestRow>(
|
|
||||||
`${WORKER_FINANCE_REQUEST_SELECT} WHERE wfr.id = $1 LIMIT 1`,
|
|
||||||
[Number(requestId)],
|
|
||||||
)
|
|
||||||
return result.rows[0] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildWorkerUserWhere({
|
function buildWorkerUserWhere({
|
||||||
status = '',
|
status = '',
|
||||||
keyword = '',
|
keyword = '',
|
||||||
@@ -981,44 +579,3 @@ function buildWorkerUserWhere({
|
|||||||
params,
|
params,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWorkerFinanceRequestWhere({
|
|
||||||
requestId = 0,
|
|
||||||
workerId = 0,
|
|
||||||
status = '',
|
|
||||||
requestType = '',
|
|
||||||
keyword = '',
|
|
||||||
}: FinanceRequestListInput) {
|
|
||||||
const filters: string[] = []
|
|
||||||
const params: unknown[] = []
|
|
||||||
if (requestId) {
|
|
||||||
params.push(requestId)
|
|
||||||
filters.push(`wfr.id = $${params.length}`)
|
|
||||||
}
|
|
||||||
if (workerId) {
|
|
||||||
params.push(workerId)
|
|
||||||
filters.push(`wfr.worker_id = $${params.length}`)
|
|
||||||
}
|
|
||||||
if (status) {
|
|
||||||
params.push(status)
|
|
||||||
filters.push(`wfr.status = $${params.length}`)
|
|
||||||
}
|
|
||||||
if (requestType) {
|
|
||||||
params.push(requestType)
|
|
||||||
filters.push(`wfr.request_type = $${params.length}`)
|
|
||||||
}
|
|
||||||
if (keyword) {
|
|
||||||
params.push(`%${keyword}%`)
|
|
||||||
filters.push(
|
|
||||||
`(wu.username ILIKE $${params.length}
|
|
||||||
OR wu.display_name ILIKE $${params.length}
|
|
||||||
OR wu.phone ILIKE $${params.length}
|
|
||||||
OR wfr.account_name ILIKE $${params.length}
|
|
||||||
OR wfr.account_no ILIKE $${params.length})`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
|
|
||||||
params,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user