拆分接单平台超长文件,按域组织代码
- worker-platform-service.ts (2733行) 拆为 worker-service/admin-service/mappers + index 聚合 - worker-platform-repo.ts (2456行) 拆为 repositories/worker-platform/ 目录(types/shared/worker-repo/work-order-repo) - AdminWorkerPlatformPage.tsx (2784行) 拆为主页面 + panels/ 目录(7 个面板文件) - 引用方统一改指向 index 聚合,行为不变
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { resolveOutstandingDepositAmount } from './worker-platform-repo.js'
|
||||
import { resolveOutstandingDepositAmount } from './worker-platform/index.js'
|
||||
|
||||
test('resolveOutstandingDepositAmount deducts released and deducted history', () => {
|
||||
assert.equal(
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './types.js'
|
||||
export * from './shared.js'
|
||||
export * from './worker-repo.js'
|
||||
export * from './work-order-repo.js'
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { PoolClient } from 'pg'
|
||||
|
||||
import type { WorkerWalletRow } from './types.js'
|
||||
|
||||
|
||||
export async function ensureWorkerWalletWithClient(
|
||||
client: PoolClient,
|
||||
workerId: number,
|
||||
now: string,
|
||||
): Promise<void> {
|
||||
if (!workerId) return
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallets (
|
||||
worker_id, available_amount, frozen_deposit_amount,
|
||||
total_credited_amount, total_settled_amount, created_at, updated_at
|
||||
) VALUES ($1, 0, 0, 0, 0, $2, $3)
|
||||
ON CONFLICT (worker_id) DO NOTHING
|
||||
`,
|
||||
[workerId, now, now],
|
||||
)
|
||||
}
|
||||
|
||||
export async function getWorkerWalletWithClient(
|
||||
client: PoolClient,
|
||||
workerId: number,
|
||||
): Promise<WorkerWalletRow | null> {
|
||||
const result = await client.query<WorkerWalletRow>(
|
||||
'SELECT * FROM worker_wallets WHERE worker_id = $1 LIMIT 1 FOR UPDATE',
|
||||
[workerId],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export function toJsonString(value: unknown): string {
|
||||
if (typeof value === 'string') return value
|
||||
return JSON.stringify(value ?? {})
|
||||
}
|
||||
|
||||
export function toPositiveInteger(value: unknown, fallback: number): number {
|
||||
const parsed = Number(value ?? '')
|
||||
if (!Number.isFinite(parsed)) return fallback
|
||||
return Math.max(0, Math.round(parsed))
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
|
||||
|
||||
export type WorkerLevelRow = {
|
||||
id: number
|
||||
level_key: string
|
||||
name: string
|
||||
sort_order: number
|
||||
status: string
|
||||
permission_json: string | Record<string, unknown>
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type WorkerUserRow = {
|
||||
id: number
|
||||
username: string
|
||||
password_hash: string
|
||||
display_name: string
|
||||
phone: string
|
||||
level_id: number | null
|
||||
status: string
|
||||
review_note: string
|
||||
session_version: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
reviewed_at: string | null
|
||||
inviter_id: number | null
|
||||
invite_code: string
|
||||
inviter_username?: string
|
||||
inviter_display_name?: string
|
||||
level_key?: string
|
||||
level_name?: string
|
||||
level_permission_json?: string | Record<string, unknown>
|
||||
available_amount?: number
|
||||
frozen_deposit_amount?: number
|
||||
total_credited_amount?: number
|
||||
total_settled_amount?: number
|
||||
}
|
||||
|
||||
export type WorkerWalletRow = {
|
||||
worker_id: number
|
||||
available_amount: number
|
||||
frozen_deposit_amount: number
|
||||
total_credited_amount: number
|
||||
total_settled_amount: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type WorkerWalletLedgerRow = {
|
||||
id: number
|
||||
worker_id: number
|
||||
ledger_type: string
|
||||
amount: number
|
||||
balance_after: number
|
||||
frozen_after: number
|
||||
related_work_order_id: number | null
|
||||
audit_status: string
|
||||
note: string
|
||||
payload_json: string | Record<string, unknown>
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type WorkerFinanceRequestRow = {
|
||||
id: number
|
||||
worker_id: number
|
||||
request_type: string
|
||||
status: string
|
||||
amount: number
|
||||
account_channel: string
|
||||
account_name: string
|
||||
account_no: string
|
||||
note: string
|
||||
reviewed_note: string
|
||||
payload_json: string | Record<string, unknown>
|
||||
created_at: string
|
||||
updated_at: string
|
||||
reviewed_at: string | null
|
||||
worker_username?: string
|
||||
worker_display_name?: string
|
||||
worker_phone?: string
|
||||
}
|
||||
|
||||
export type WorkCategoryRow = {
|
||||
id: number
|
||||
category_key: string
|
||||
name: string
|
||||
sort_order: number
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type WorkProductRuleRow = {
|
||||
id: number
|
||||
rule_key: string
|
||||
provider: string
|
||||
platform: string
|
||||
shop_id: string
|
||||
sku_code: string
|
||||
product_name: string
|
||||
match_type: string
|
||||
category_id: number | null
|
||||
enabled: boolean
|
||||
auto_create: boolean
|
||||
reward_amount: number
|
||||
required_deposit_amount: number
|
||||
deposit_threshold_amount: number
|
||||
sharing_enabled: boolean
|
||||
sharing_total_quantity: number
|
||||
sharing_unit_reward: number
|
||||
requirement_json: string | Record<string, unknown>
|
||||
sort_order: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
category_name?: string
|
||||
}
|
||||
|
||||
export type WorkOrderShareRow = {
|
||||
id: number
|
||||
work_order_id: number
|
||||
worker_id: number
|
||||
quantity: number
|
||||
unit_reward: number
|
||||
share_reward: number
|
||||
share_deposit: number
|
||||
status: string
|
||||
acceptance_json: string | Record<string, unknown>
|
||||
submitted_at: string | null
|
||||
accepted_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
worker_username?: string
|
||||
worker_display_name?: string
|
||||
}
|
||||
|
||||
export type WorkOrderRow = {
|
||||
id: number
|
||||
work_order_no: string
|
||||
order_id: number | null
|
||||
order_item_id: number | null
|
||||
task_id: number | null
|
||||
platform_order_id: string
|
||||
product_name: string
|
||||
category_id: number | null
|
||||
status: string
|
||||
reward_amount: number
|
||||
required_deposit_amount: number
|
||||
deposit_threshold_amount: number
|
||||
assigned_worker_id: number | null
|
||||
material_json: string | Record<string, unknown>
|
||||
requirement_json: string | Record<string, unknown>
|
||||
acceptance_json: string | Record<string, unknown>
|
||||
problem_note: string
|
||||
sharing_enabled: boolean
|
||||
sharing_total_quantity: number
|
||||
sharing_unit_reward: number
|
||||
published_at: string | null
|
||||
assigned_at: string | null
|
||||
submitted_at: string | null
|
||||
accepted_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
category_name?: string
|
||||
worker_username?: string
|
||||
worker_display_name?: string
|
||||
}
|
||||
|
||||
export type ProblemWorkOrderResolutionAction =
|
||||
| 'return_to_worker'
|
||||
| 'reopen'
|
||||
| 'cancel_release'
|
||||
| 'cancel_deduct'
|
||||
|
||||
export type GrabWorkOrderFailureReason =
|
||||
| 'worker_active_order_limit'
|
||||
| 'worker_deposit_insufficient'
|
||||
| 'work_order_not_open'
|
||||
|
||||
export type GrabWorkOrderResult = {
|
||||
order: WorkOrderRow | null
|
||||
failureReason: GrabWorkOrderFailureReason | null
|
||||
}
|
||||
|
||||
export type ListInput = {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: string
|
||||
keyword?: string
|
||||
workerId?: number
|
||||
categoryId?: number
|
||||
workerSharingId?: number
|
||||
}
|
||||
|
||||
export type ProductRuleListInput = {
|
||||
enabled?: boolean | null
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
export type WalletLedgerListInput = {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
workerId?: number
|
||||
ledgerType?: string
|
||||
}
|
||||
|
||||
export type FinanceRequestListInput = {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
workerId?: number
|
||||
status?: string
|
||||
requestType?: string
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
export type CreateWorkerInput = {
|
||||
username: string
|
||||
passwordHash: string
|
||||
displayName: string
|
||||
phone: string
|
||||
levelId: number | null
|
||||
status: string
|
||||
inviterId?: number | null
|
||||
inviteCode: string
|
||||
now: string
|
||||
}
|
||||
|
||||
export type CreateWorkOrderInput = {
|
||||
workOrderNo: string
|
||||
orderId?: number | null
|
||||
orderItemId?: number | null
|
||||
taskId?: number | null
|
||||
platformOrderId: string
|
||||
productName: string
|
||||
categoryId?: number | null
|
||||
status: string
|
||||
rewardAmount: number
|
||||
requiredDepositAmount: number
|
||||
depositThresholdAmount: number
|
||||
sharingEnabled?: boolean
|
||||
sharingTotalQuantity?: number
|
||||
sharingUnitReward?: number
|
||||
materialJson: string
|
||||
requirementJson: string
|
||||
now: string
|
||||
}
|
||||
+4
-1075
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,794 @@
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
|
||||
import type { CreateWorkerInput, FinanceRequestListInput, ListInput, WalletLedgerListInput, WorkerFinanceRequestRow, WorkerLevelRow, WorkerUserRow, WorkerWalletLedgerRow, WorkerWalletRow } from './types.js'
|
||||
import type { PoolClient } from 'pg'
|
||||
|
||||
|
||||
const WORKER_USER_SELECT = `
|
||||
SELECT
|
||||
wu.*,
|
||||
wl.level_key,
|
||||
wl.name AS level_name,
|
||||
wl.permission_json AS level_permission_json,
|
||||
inv.username AS inviter_username,
|
||||
inv.display_name AS inviter_display_name,
|
||||
ww.available_amount,
|
||||
ww.frozen_deposit_amount,
|
||||
ww.total_credited_amount,
|
||||
ww.total_settled_amount
|
||||
FROM worker_users wu
|
||||
LEFT JOIN worker_levels wl ON wl.id = wu.level_id
|
||||
LEFT JOIN worker_users inv ON inv.id = wu.inviter_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
|
||||
FROM worker_finance_requests wfr
|
||||
LEFT JOIN worker_users wu ON wu.id = wfr.worker_id
|
||||
`
|
||||
|
||||
export async function getWorkerLevelByKey(levelKey: string): Promise<WorkerLevelRow | null> {
|
||||
const result = await query<WorkerLevelRow>(
|
||||
'SELECT * FROM worker_levels WHERE level_key = $1 LIMIT 1',
|
||||
[String(levelKey || '').trim()],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function getWorkerLevelById(levelId: number | string): Promise<WorkerLevelRow | null> {
|
||||
const result = await query<WorkerLevelRow>('SELECT * FROM worker_levels WHERE id = $1 LIMIT 1', [
|
||||
Number(levelId),
|
||||
])
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function listWorkerLevels(): Promise<WorkerLevelRow[]> {
|
||||
const result = await query<WorkerLevelRow>(
|
||||
'SELECT * FROM worker_levels ORDER BY sort_order ASC, id ASC',
|
||||
)
|
||||
return result.rows
|
||||
}
|
||||
|
||||
export async function upsertWorkerLevel(input: {
|
||||
levelKey: string
|
||||
name: string
|
||||
sortOrder: number
|
||||
status: string
|
||||
permissionJson: string
|
||||
now: string
|
||||
}): Promise<WorkerLevelRow | null> {
|
||||
const result = await query<WorkerLevelRow>(
|
||||
`
|
||||
INSERT INTO worker_levels (
|
||||
level_key, name, sort_order, status, permission_json, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7)
|
||||
ON CONFLICT (level_key) DO UPDATE
|
||||
SET
|
||||
name = EXCLUDED.name,
|
||||
sort_order = EXCLUDED.sort_order,
|
||||
status = EXCLUDED.status,
|
||||
permission_json = EXCLUDED.permission_json,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING *
|
||||
`,
|
||||
[
|
||||
input.levelKey,
|
||||
input.name,
|
||||
input.sortOrder,
|
||||
input.status,
|
||||
input.permissionJson,
|
||||
input.now,
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function countWorkerLevelUsages(levelId: number | string): Promise<number> {
|
||||
const result = await query<{ total: number }>(
|
||||
'SELECT COUNT(*)::int AS total FROM worker_users WHERE level_id = $1',
|
||||
[Number(levelId)],
|
||||
)
|
||||
return Number(result.rows[0]?.total || 0)
|
||||
}
|
||||
|
||||
export async function deleteWorkerLevel(levelId: number | string): Promise<{ deleted: boolean }> {
|
||||
const result = await query<{ id: number }>(
|
||||
'DELETE FROM worker_levels WHERE id = $1 RETURNING id',
|
||||
[Number(levelId)],
|
||||
)
|
||||
return { deleted: Boolean(result.rows[0]) }
|
||||
}
|
||||
|
||||
export async function getWorkerUserByUsername(username: string): Promise<WorkerUserRow | null> {
|
||||
const result = await query<WorkerUserRow>(
|
||||
`${WORKER_USER_SELECT} WHERE wu.username = $1 LIMIT 1`,
|
||||
[
|
||||
String(username || '')
|
||||
.trim()
|
||||
.toLowerCase(),
|
||||
],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function getWorkerUserByInviteCode(
|
||||
inviteCode: string,
|
||||
): Promise<WorkerUserRow | null> {
|
||||
const result = await query<WorkerUserRow>(
|
||||
`${WORKER_USER_SELECT} WHERE wu.invite_code = $1 LIMIT 1`,
|
||||
[String(inviteCode || '').trim().toUpperCase()],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function getWorkerUserById(workerId: number | string): Promise<WorkerUserRow | null> {
|
||||
const result = await query<WorkerUserRow>(`${WORKER_USER_SELECT} WHERE wu.id = $1 LIMIT 1`, [
|
||||
Number(workerId),
|
||||
])
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function createWorkerUser(input: CreateWorkerInput): Promise<WorkerUserRow | null> {
|
||||
return withTransaction(async (client) => {
|
||||
const result = await client.query<{ id: number }>(
|
||||
`
|
||||
INSERT INTO worker_users (
|
||||
username, password_hash, display_name, phone, level_id, status,
|
||||
inviter_id, invite_code,
|
||||
created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id
|
||||
`,
|
||||
[
|
||||
input.username,
|
||||
input.passwordHash,
|
||||
input.displayName,
|
||||
input.phone,
|
||||
input.levelId,
|
||||
input.status,
|
||||
input.inviterId || null,
|
||||
input.inviteCode,
|
||||
input.now,
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
const workerId = Number(result.rows[0]?.id || 0)
|
||||
await ensureWorkerWalletWithClient(client, workerId, input.now)
|
||||
return getWorkerUserByIdWithClient(client, workerId)
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateWorkerUser(
|
||||
workerId: number | string,
|
||||
patch: Partial<
|
||||
Pick<
|
||||
WorkerUserRow,
|
||||
'status' | 'review_note' | 'level_id' | 'session_version' | 'updated_at' | 'reviewed_at'
|
||||
>
|
||||
>,
|
||||
): Promise<WorkerUserRow | null> {
|
||||
const current = await getWorkerUserById(workerId)
|
||||
if (!current) return null
|
||||
const next = { ...current, ...patch }
|
||||
|
||||
await query(
|
||||
`
|
||||
UPDATE worker_users
|
||||
SET
|
||||
status = $1,
|
||||
review_note = $2,
|
||||
level_id = $3,
|
||||
session_version = $4,
|
||||
reviewed_at = $5,
|
||||
updated_at = $6
|
||||
WHERE id = $7
|
||||
`,
|
||||
[
|
||||
next.status,
|
||||
next.review_note || '',
|
||||
next.level_id || null,
|
||||
Number(next.session_version || 1),
|
||||
next.reviewed_at || null,
|
||||
next.updated_at,
|
||||
Number(workerId),
|
||||
],
|
||||
)
|
||||
return getWorkerUserById(workerId)
|
||||
}
|
||||
|
||||
export async function updateWorkerPassword(input: {
|
||||
workerId: number
|
||||
passwordHash: string
|
||||
sessionVersion: number
|
||||
now: string
|
||||
}): Promise<WorkerUserRow | null> {
|
||||
const result = await query<{ id: number }>(
|
||||
`
|
||||
UPDATE worker_users
|
||||
SET
|
||||
password_hash = $1,
|
||||
session_version = $2,
|
||||
updated_at = $3
|
||||
WHERE id = $4
|
||||
RETURNING id
|
||||
`,
|
||||
[input.passwordHash, input.sessionVersion, input.now, input.workerId],
|
||||
)
|
||||
if (!result.rows[0]) {
|
||||
return null
|
||||
}
|
||||
return getWorkerUserById(input.workerId)
|
||||
}
|
||||
|
||||
export async function incrementWorkerSessionVersion(
|
||||
workerId: number | string,
|
||||
now: string,
|
||||
): Promise<WorkerUserRow | null> {
|
||||
const result = await query<{ id: number }>(
|
||||
`
|
||||
UPDATE worker_users
|
||||
SET session_version = session_version + 1, updated_at = $2
|
||||
WHERE id = $1
|
||||
RETURNING id
|
||||
`,
|
||||
[Number(workerId), now],
|
||||
)
|
||||
if (!result.rows[0]) {
|
||||
return null
|
||||
}
|
||||
return getWorkerUserById(workerId)
|
||||
}
|
||||
|
||||
export async function listWorkerUsers({
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
status = '',
|
||||
keyword = '',
|
||||
}: ListInput = {}): Promise<{ items: WorkerUserRow[]; total: number }> {
|
||||
const { whereClause, params } = buildWorkerUserWhere({ status, keyword })
|
||||
const totalResult = await query<{ total: number }>(
|
||||
`SELECT COUNT(*)::int AS total FROM worker_users wu ${whereClause}`,
|
||||
params,
|
||||
)
|
||||
|
||||
const offset = (page - 1) * pageSize
|
||||
params.push(pageSize, offset)
|
||||
const itemsResult = await query<WorkerUserRow>(
|
||||
`${WORKER_USER_SELECT}
|
||||
${whereClause}
|
||||
ORDER BY wu.id DESC
|
||||
LIMIT $${params.length - 1} OFFSET $${params.length}`,
|
||||
params,
|
||||
)
|
||||
return { items: itemsResult.rows, total: Number(totalResult.rows[0]?.total || 0) }
|
||||
}
|
||||
|
||||
export async function ensureWorkerWallet(
|
||||
workerId: number | string,
|
||||
now: string,
|
||||
): Promise<WorkerWalletRow | null> {
|
||||
return withTransaction(async (client) => {
|
||||
await ensureWorkerWalletWithClient(client, Number(workerId), now)
|
||||
return getWorkerWalletWithClient(client, Number(workerId))
|
||||
})
|
||||
}
|
||||
|
||||
export async function getWorkerWallet(workerId: number | string): Promise<WorkerWalletRow | null> {
|
||||
const result = await query<WorkerWalletRow>(
|
||||
'SELECT * FROM worker_wallets WHERE worker_id = $1 LIMIT 1',
|
||||
[Number(workerId)],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function listWorkerWalletLedgers({
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
workerId = 0,
|
||||
ledgerType = '',
|
||||
}: WalletLedgerListInput = {}): Promise<{ items: WorkerWalletLedgerRow[]; total: number }> {
|
||||
const { whereClause, params } = buildWorkerWalletLedgerWhere({
|
||||
workerId,
|
||||
ledgerType,
|
||||
})
|
||||
const totalResult = await query<{ total: number }>(
|
||||
`SELECT COUNT(*)::int AS total FROM worker_wallet_ledgers wwl ${whereClause}`,
|
||||
params,
|
||||
)
|
||||
const offset = (page - 1) * pageSize
|
||||
params.push(pageSize, offset)
|
||||
const itemsResult = await query<WorkerWalletLedgerRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM worker_wallet_ledgers wwl
|
||||
${whereClause}
|
||||
ORDER BY wwl.created_at DESC, wwl.id DESC
|
||||
LIMIT $${params.length - 1} OFFSET $${params.length}
|
||||
`,
|
||||
params,
|
||||
)
|
||||
return {
|
||||
items: itemsResult.rows,
|
||||
total: Number(totalResult.rows[0]?.total || 0),
|
||||
}
|
||||
}
|
||||
|
||||
export async function addWorkerWalletCredit(input: {
|
||||
workerId: number
|
||||
amount: number
|
||||
note: string
|
||||
payloadJson: string
|
||||
now: string
|
||||
}): Promise<WorkerWalletRow | null> {
|
||||
return withTransaction(async (client) => {
|
||||
await ensureWorkerWalletWithClient(client, input.workerId, input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, input.workerId)
|
||||
const nextAvailable = Number(wallet?.available_amount || 0) + input.amount
|
||||
const nextFrozen = Number(wallet?.frozen_deposit_amount || 0)
|
||||
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, input.amount, input.now, input.workerId],
|
||||
)
|
||||
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)
|
||||
`,
|
||||
[
|
||||
input.workerId,
|
||||
input.amount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.note,
|
||||
input.payloadJson,
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
return getWorkerWalletWithClient(client, input.workerId)
|
||||
})
|
||||
}
|
||||
|
||||
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 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 listWorkerFinanceRequests({
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
workerId = 0,
|
||||
status = '',
|
||||
requestType = '',
|
||||
keyword = '',
|
||||
}: FinanceRequestListInput = {}): Promise<{ items: WorkerFinanceRequestRow[]; total: number }> {
|
||||
const { whereClause, params } = buildWorkerFinanceRequestWhere({
|
||||
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> {
|
||||
const result = await query<{ total: number }>(
|
||||
`
|
||||
SELECT
|
||||
(SELECT COUNT(*)::int FROM work_orders WHERE assigned_worker_id = $1 AND status = 'accepted')
|
||||
+
|
||||
COALESCE(
|
||||
(SELECT SUM(quantity)::int FROM work_order_shares WHERE worker_id = $1 AND status = 'accepted'),
|
||||
0
|
||||
) AS total
|
||||
`,
|
||||
[Number(workerId)],
|
||||
)
|
||||
return Number(result.rows[0]?.total || 0)
|
||||
}
|
||||
|
||||
export async function maybeUpgradeWorkerLevelWithClient(
|
||||
client: PoolClient,
|
||||
workerId: number,
|
||||
now: string,
|
||||
): Promise<WorkerUserRow | null> {
|
||||
const acceptedResult = await client.query<{ total: number }>(
|
||||
`
|
||||
SELECT
|
||||
(SELECT COUNT(*)::int FROM work_orders WHERE assigned_worker_id = $1 AND status = 'accepted')
|
||||
+
|
||||
COALESCE(
|
||||
(SELECT SUM(quantity)::int FROM work_order_shares WHERE worker_id = $1 AND status = 'accepted'),
|
||||
0
|
||||
) AS total
|
||||
`,
|
||||
[workerId],
|
||||
)
|
||||
const acceptedCount = Number(acceptedResult.rows[0]?.total || 0)
|
||||
|
||||
const nextLevelResult = await client.query<{ id: number }>(
|
||||
`
|
||||
SELECT id
|
||||
FROM worker_levels
|
||||
WHERE status = 'active'
|
||||
AND COALESCE((permission_json->>'upgradeThreshold')::int, 0) <= $1
|
||||
ORDER BY COALESCE((permission_json->>'upgradeThreshold')::int, 0) DESC, sort_order ASC
|
||||
LIMIT 1
|
||||
`,
|
||||
[acceptedCount],
|
||||
)
|
||||
const nextLevelId = Number(nextLevelResult.rows[0]?.id || 0)
|
||||
if (!nextLevelId) return null
|
||||
|
||||
const current = await client.query<{ id: number; level_id: number | null }>(
|
||||
'SELECT id, level_id FROM worker_users WHERE id = $1 LIMIT 1',
|
||||
[workerId],
|
||||
)
|
||||
const currentRow = current.rows[0]
|
||||
if (!currentRow || Number(currentRow.level_id || 0) === nextLevelId) return null
|
||||
|
||||
await client.query(
|
||||
'UPDATE worker_users SET level_id = $1, updated_at = $2 WHERE id = $3',
|
||||
[nextLevelId, now, workerId],
|
||||
)
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_level_logs (
|
||||
worker_id, from_level_id, to_level_id, reason, payload_json, created_at
|
||||
) VALUES ($1, $2, $3, 'auto', $4::jsonb, $5)
|
||||
`,
|
||||
[
|
||||
workerId,
|
||||
currentRow.level_id || null,
|
||||
nextLevelId,
|
||||
JSON.stringify({ acceptedCount }),
|
||||
now,
|
||||
],
|
||||
)
|
||||
return getWorkerUserByIdWithClient(client, workerId)
|
||||
}
|
||||
|
||||
async function getWorkerUserByIdWithClient(
|
||||
client: PoolClient,
|
||||
workerId: number | string,
|
||||
): Promise<WorkerUserRow | null> {
|
||||
const result = await client.query<WorkerUserRow>(
|
||||
`${WORKER_USER_SELECT} WHERE wu.id = $1 LIMIT 1`,
|
||||
[Number(workerId)],
|
||||
)
|
||||
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({
|
||||
status = '',
|
||||
keyword = '',
|
||||
}: Pick<ListInput, 'status' | 'keyword'>) {
|
||||
const filters: string[] = []
|
||||
const params: unknown[] = []
|
||||
if (status) {
|
||||
params.push(status)
|
||||
filters.push(`wu.status = $${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})`,
|
||||
)
|
||||
}
|
||||
return {
|
||||
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
|
||||
params,
|
||||
}
|
||||
}
|
||||
|
||||
function buildWorkerWalletLedgerWhere({
|
||||
workerId = 0,
|
||||
ledgerType = '',
|
||||
}: WalletLedgerListInput) {
|
||||
const filters: string[] = []
|
||||
const params: unknown[] = []
|
||||
if (workerId) {
|
||||
params.push(workerId)
|
||||
filters.push(`wwl.worker_id = $${params.length}`)
|
||||
}
|
||||
if (ledgerType) {
|
||||
params.push(ledgerType)
|
||||
filters.push(`wwl.ledger_type = $${params.length}`)
|
||||
}
|
||||
return {
|
||||
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
|
||||
params,
|
||||
}
|
||||
}
|
||||
|
||||
function buildWorkerFinanceRequestWhere({
|
||||
workerId = 0,
|
||||
status = '',
|
||||
requestType = '',
|
||||
keyword = '',
|
||||
}: FinanceRequestListInput) {
|
||||
const filters: string[] = []
|
||||
const params: unknown[] = []
|
||||
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