新增接单平台第一期闭环
This commit is contained in:
@@ -0,0 +1,838 @@
|
||||
import type { PoolClient } from 'pg'
|
||||
|
||||
import { query, withTransaction } from '../db/client.js'
|
||||
|
||||
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
|
||||
level_key?: string
|
||||
level_name?: string
|
||||
level_permission_json?: string | Record<string, unknown>
|
||||
available_amount?: number
|
||||
frozen_deposit_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 WorkCategoryRow = {
|
||||
id: number
|
||||
category_key: string
|
||||
name: string
|
||||
sort_order: number
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: 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
|
||||
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
|
||||
}
|
||||
|
||||
type ListInput = {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: string
|
||||
keyword?: string
|
||||
workerId?: number
|
||||
}
|
||||
|
||||
type CreateWorkerInput = {
|
||||
username: string
|
||||
passwordHash: string
|
||||
displayName: string
|
||||
phone: string
|
||||
levelId: number | null
|
||||
status: string
|
||||
now: string
|
||||
}
|
||||
|
||||
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
|
||||
materialJson: string
|
||||
requirementJson: string
|
||||
now: string
|
||||
}
|
||||
|
||||
const WORKER_USER_SELECT = `
|
||||
SELECT
|
||||
wu.*,
|
||||
wl.level_key,
|
||||
wl.name AS level_name,
|
||||
wl.permission_json AS level_permission_json,
|
||||
ww.available_amount,
|
||||
ww.frozen_deposit_amount
|
||||
FROM worker_users wu
|
||||
LEFT JOIN worker_levels wl ON wl.id = wu.level_id
|
||||
LEFT JOIN worker_wallets ww ON ww.worker_id = wu.id
|
||||
`
|
||||
|
||||
const WORK_ORDER_SELECT = `
|
||||
SELECT
|
||||
wo.*,
|
||||
wc.name AS category_name,
|
||||
wu.username AS worker_username,
|
||||
wu.display_name AS worker_display_name
|
||||
FROM work_orders wo
|
||||
LEFT JOIN work_categories wc ON wc.id = wo.category_id
|
||||
LEFT JOIN worker_users wu ON wu.id = wo.assigned_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 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 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,
|
||||
created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id
|
||||
`,
|
||||
[
|
||||
input.username,
|
||||
input.passwordHash,
|
||||
input.displayName,
|
||||
input.phone,
|
||||
input.levelId,
|
||||
input.status,
|
||||
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 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 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 getWorkCategoryByKey(categoryKey: string): Promise<WorkCategoryRow | null> {
|
||||
const result = await query<WorkCategoryRow>(
|
||||
'SELECT * FROM work_categories WHERE category_key = $1 LIMIT 1',
|
||||
[String(categoryKey || '').trim()],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function listWorkCategories(): Promise<WorkCategoryRow[]> {
|
||||
const result = await query<WorkCategoryRow>(
|
||||
`SELECT * FROM work_categories WHERE status = 'active' ORDER BY sort_order ASC, id ASC`,
|
||||
)
|
||||
return result.rows
|
||||
}
|
||||
|
||||
export async function upsertWorkCategory(input: {
|
||||
categoryKey: string
|
||||
name: string
|
||||
sortOrder: number
|
||||
status: string
|
||||
now: string
|
||||
}): Promise<WorkCategoryRow | null> {
|
||||
const result = await query<WorkCategoryRow>(
|
||||
`
|
||||
INSERT INTO work_categories (category_key, name, sort_order, status, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (category_key) DO UPDATE
|
||||
SET name = EXCLUDED.name, sort_order = EXCLUDED.sort_order, status = EXCLUDED.status, updated_at = EXCLUDED.updated_at
|
||||
RETURNING *
|
||||
`,
|
||||
[input.categoryKey, input.name, input.sortOrder, input.status, input.now, input.now],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function createWorkOrder(input: CreateWorkOrderInput): Promise<WorkOrderRow | null> {
|
||||
const result = await query<{ id: number }>(
|
||||
`
|
||||
INSERT INTO work_orders (
|
||||
work_order_no, order_id, order_item_id, task_id, platform_order_id,
|
||||
product_name, category_id, status, reward_amount, required_deposit_amount,
|
||||
deposit_threshold_amount, material_json, requirement_json, created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||
$11, $12::jsonb, $13::jsonb, $14, $15
|
||||
)
|
||||
RETURNING id
|
||||
`,
|
||||
[
|
||||
input.workOrderNo,
|
||||
input.orderId || null,
|
||||
input.orderItemId || null,
|
||||
input.taskId || null,
|
||||
input.platformOrderId,
|
||||
input.productName,
|
||||
input.categoryId || null,
|
||||
input.status,
|
||||
input.rewardAmount,
|
||||
input.requiredDepositAmount,
|
||||
input.depositThresholdAmount,
|
||||
input.materialJson,
|
||||
input.requirementJson,
|
||||
input.now,
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
return getWorkOrderById(result.rows[0]?.id || 0)
|
||||
}
|
||||
|
||||
export async function getWorkOrderById(workOrderId: number | string): Promise<WorkOrderRow | null> {
|
||||
const result = await query<WorkOrderRow>(
|
||||
`${WORK_ORDER_SELECT} WHERE wo.id = $1 LIMIT 1`,
|
||||
[Number(workOrderId)],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function findPendingMaterialWorkOrderByPlatformOrderId(
|
||||
platformOrderId: string,
|
||||
): Promise<WorkOrderRow | null> {
|
||||
const result = await query<WorkOrderRow>(
|
||||
`${WORK_ORDER_SELECT}
|
||||
WHERE wo.platform_order_id = $1 AND wo.status = 'pending_material'
|
||||
ORDER BY wo.id DESC
|
||||
LIMIT 1`,
|
||||
[String(platformOrderId || '').trim()],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function listWorkOrders({
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
status = '',
|
||||
keyword = '',
|
||||
workerId = 0,
|
||||
}: ListInput = {}): Promise<{ items: WorkOrderRow[]; total: number }> {
|
||||
const { whereClause, params } = buildWorkOrderWhere({ status, keyword, workerId })
|
||||
const totalResult = await query<{ total: number }>(
|
||||
`
|
||||
SELECT COUNT(*)::int AS total
|
||||
FROM work_orders wo
|
||||
${whereClause}
|
||||
`,
|
||||
params,
|
||||
)
|
||||
const offset = (page - 1) * pageSize
|
||||
params.push(pageSize, offset)
|
||||
const itemsResult = await query<WorkOrderRow>(
|
||||
`${WORK_ORDER_SELECT}
|
||||
${whereClause}
|
||||
ORDER BY wo.id DESC
|
||||
LIMIT $${params.length - 1} OFFSET $${params.length}`,
|
||||
params,
|
||||
)
|
||||
return { items: itemsResult.rows, total: Number(totalResult.rows[0]?.total || 0) }
|
||||
}
|
||||
|
||||
export async function updateWorkOrder(
|
||||
workOrderId: number | string,
|
||||
patch: Partial<Pick<
|
||||
WorkOrderRow,
|
||||
| 'status'
|
||||
| 'assigned_worker_id'
|
||||
| 'material_json'
|
||||
| 'acceptance_json'
|
||||
| 'problem_note'
|
||||
| 'published_at'
|
||||
| 'assigned_at'
|
||||
| 'submitted_at'
|
||||
| 'accepted_at'
|
||||
| 'updated_at'
|
||||
>>,
|
||||
): Promise<WorkOrderRow | null> {
|
||||
const current = await getWorkOrderById(workOrderId)
|
||||
if (!current) return null
|
||||
const next = { ...current, ...patch }
|
||||
await query(
|
||||
`
|
||||
UPDATE work_orders
|
||||
SET
|
||||
status = $1,
|
||||
assigned_worker_id = $2,
|
||||
material_json = $3::jsonb,
|
||||
acceptance_json = $4::jsonb,
|
||||
problem_note = $5,
|
||||
published_at = $6,
|
||||
assigned_at = $7,
|
||||
submitted_at = $8,
|
||||
accepted_at = $9,
|
||||
updated_at = $10
|
||||
WHERE id = $11
|
||||
`,
|
||||
[
|
||||
next.status,
|
||||
next.assigned_worker_id || null,
|
||||
toJsonString(next.material_json),
|
||||
toJsonString(next.acceptance_json),
|
||||
next.problem_note || '',
|
||||
next.published_at || null,
|
||||
next.assigned_at || null,
|
||||
next.submitted_at || null,
|
||||
next.accepted_at || null,
|
||||
next.updated_at,
|
||||
Number(workOrderId),
|
||||
],
|
||||
)
|
||||
return getWorkOrderById(workOrderId)
|
||||
}
|
||||
|
||||
export async function grabWorkOrder(input: {
|
||||
workOrderId: number
|
||||
workerId: number
|
||||
depositAmount: number
|
||||
now: string
|
||||
}): Promise<WorkOrderRow | null> {
|
||||
return withTransaction(async (client) => {
|
||||
await ensureWorkerWalletWithClient(client, input.workerId, input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, input.workerId)
|
||||
const available = Number(wallet?.available_amount || 0)
|
||||
if (available < input.depositAmount) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = await client.query<{ id: number }>(
|
||||
`
|
||||
UPDATE work_orders
|
||||
SET
|
||||
status = 'in_progress',
|
||||
assigned_worker_id = $1,
|
||||
assigned_at = $2,
|
||||
updated_at = $2
|
||||
WHERE id = $3
|
||||
AND status = 'open'
|
||||
AND assigned_worker_id IS NULL
|
||||
RETURNING id
|
||||
`,
|
||||
[input.workerId, input.now, input.workOrderId],
|
||||
)
|
||||
if (!result.rows[0]) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (input.depositAmount > 0) {
|
||||
const nextAvailable = available - input.depositAmount
|
||||
const nextFrozen = Number(wallet?.frozen_deposit_amount || 0) + input.depositAmount
|
||||
await client.query(
|
||||
`
|
||||
UPDATE worker_wallets
|
||||
SET available_amount = $1, frozen_deposit_amount = $2, updated_at = $3
|
||||
WHERE worker_id = $4
|
||||
`,
|
||||
[nextAvailable, nextFrozen, input.now, input.workerId],
|
||||
)
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'deposit_freeze', $2, $3, $4, $5, '抢单冻结押金', '{}'::jsonb, $6)
|
||||
`,
|
||||
[input.workerId, -input.depositAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
||||
)
|
||||
}
|
||||
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: 'worker',
|
||||
actorId: String(input.workerId),
|
||||
eventType: 'grabbed',
|
||||
fromStatus: 'open',
|
||||
toStatus: 'in_progress',
|
||||
payloadJson: '{}',
|
||||
now: input.now,
|
||||
})
|
||||
|
||||
return getWorkOrderByIdWithClient(client, input.workOrderId)
|
||||
})
|
||||
}
|
||||
|
||||
export async function acceptWorkOrderAndSettle(input: {
|
||||
workOrderId: number
|
||||
now: string
|
||||
}): Promise<WorkOrderRow | null> {
|
||||
return withTransaction(async (client) => {
|
||||
const currentResult = await client.query<WorkOrderRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM work_orders
|
||||
WHERE id = $1
|
||||
AND status = 'pending_acceptance'
|
||||
FOR UPDATE
|
||||
`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const workOrder = currentResult.rows[0] || null
|
||||
if (!workOrder) {
|
||||
return null
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE work_orders
|
||||
SET status = 'accepted', accepted_at = $1, updated_at = $1
|
||||
WHERE id = $2
|
||||
`,
|
||||
[input.now, input.workOrderId],
|
||||
)
|
||||
|
||||
const workerId = Number(workOrder.assigned_worker_id || 0)
|
||||
if (workerId > 0) {
|
||||
await ensureWorkerWalletWithClient(client, workerId, input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, workerId)
|
||||
const frozenLedgerResult = await client.query<{ amount: number }>(
|
||||
`
|
||||
SELECT COALESCE(ABS(SUM(amount)), 0)::int AS amount
|
||||
FROM worker_wallet_ledgers
|
||||
WHERE worker_id = $1
|
||||
AND related_work_order_id = $2
|
||||
AND ledger_type = 'deposit_freeze'
|
||||
`,
|
||||
[workerId, input.workOrderId],
|
||||
)
|
||||
const releaseAmount = Math.min(
|
||||
Number(frozenLedgerResult.rows[0]?.amount || 0),
|
||||
Number(wallet?.frozen_deposit_amount || 0),
|
||||
)
|
||||
const rewardAmount = Number(workOrder.reward_amount || 0)
|
||||
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount + rewardAmount
|
||||
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE worker_wallets
|
||||
SET
|
||||
available_amount = $1,
|
||||
frozen_deposit_amount = $2,
|
||||
total_settled_amount = total_settled_amount + $3,
|
||||
updated_at = $4
|
||||
WHERE worker_id = $5
|
||||
`,
|
||||
[nextAvailable, nextFrozen, rewardAmount, input.now, workerId],
|
||||
)
|
||||
|
||||
if (releaseAmount > 0) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'deposit_release', $2, $3, $4, $5, '验收通过释放押金', '{}'::jsonb, $6)
|
||||
`,
|
||||
[workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
||||
)
|
||||
}
|
||||
|
||||
if (rewardAmount > 0) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'reward_settlement', $2, $3, $4, $5, '验收通过结算报酬', '{}'::jsonb, $6)
|
||||
`,
|
||||
[workerId, rewardAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
eventType: 'accepted',
|
||||
fromStatus: 'pending_acceptance',
|
||||
toStatus: 'accepted',
|
||||
payloadJson: '{}',
|
||||
now: input.now,
|
||||
})
|
||||
|
||||
return getWorkOrderByIdWithClient(client, input.workOrderId)
|
||||
})
|
||||
}
|
||||
|
||||
export async function countWorkerActiveOrders(workerId: number | string): Promise<number> {
|
||||
const result = await query<{ total: number }>(
|
||||
`
|
||||
SELECT COUNT(*)::int AS total
|
||||
FROM work_orders
|
||||
WHERE assigned_worker_id = $1
|
||||
AND status IN ('in_progress', 'pending_acceptance', 'problem')
|
||||
`,
|
||||
[Number(workerId)],
|
||||
)
|
||||
return Number(result.rows[0]?.total || 0)
|
||||
}
|
||||
|
||||
export async function createWorkOrderEvent(input: {
|
||||
workOrderId: number
|
||||
actorType: string
|
||||
actorId: string
|
||||
eventType: string
|
||||
fromStatus?: string
|
||||
toStatus?: string
|
||||
payloadJson?: string
|
||||
now: string
|
||||
}): Promise<void> {
|
||||
await query(
|
||||
`
|
||||
INSERT INTO work_order_events (
|
||||
work_order_id, actor_type, actor_id, event_type, from_status, to_status, payload_json, created_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)
|
||||
`,
|
||||
[
|
||||
input.workOrderId,
|
||||
input.actorType,
|
||||
input.actorId,
|
||||
input.eventType,
|
||||
input.fromStatus || '',
|
||||
input.toStatus || '',
|
||||
input.payloadJson || '{}',
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
async function createWorkOrderEventWithClient(
|
||||
client: PoolClient,
|
||||
input: Parameters<typeof createWorkOrderEvent>[0],
|
||||
) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO work_order_events (
|
||||
work_order_id, actor_type, actor_id, event_type, from_status, to_status, payload_json, created_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)
|
||||
`,
|
||||
[
|
||||
input.workOrderId,
|
||||
input.actorType,
|
||||
input.actorId,
|
||||
input.eventType,
|
||||
input.fromStatus || '',
|
||||
input.toStatus || '',
|
||||
input.payloadJson || '{}',
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
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],
|
||||
)
|
||||
}
|
||||
|
||||
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',
|
||||
[workerId],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
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 getWorkOrderByIdWithClient(client: PoolClient, workOrderId: number | string): Promise<WorkOrderRow | null> {
|
||||
const result = await client.query<WorkOrderRow>(
|
||||
`${WORK_ORDER_SELECT} WHERE wo.id = $1 LIMIT 1`,
|
||||
[Number(workOrderId)],
|
||||
)
|
||||
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 buildWorkOrderWhere({ status = '', keyword = '', workerId = 0 }: ListInput) {
|
||||
const filters: string[] = []
|
||||
const params: unknown[] = []
|
||||
if (status) {
|
||||
params.push(status)
|
||||
filters.push(`wo.status = $${params.length}`)
|
||||
}
|
||||
if (keyword) {
|
||||
params.push(`%${keyword}%`)
|
||||
filters.push(`(wo.work_order_no ILIKE $${params.length} OR wo.platform_order_id ILIKE $${params.length} OR wo.product_name ILIKE $${params.length})`)
|
||||
}
|
||||
if (workerId) {
|
||||
params.push(workerId)
|
||||
filters.push(`wo.assigned_worker_id = $${params.length}`)
|
||||
}
|
||||
return {
|
||||
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
|
||||
params,
|
||||
}
|
||||
}
|
||||
|
||||
function toJsonString(value: unknown): string {
|
||||
if (typeof value === 'string') return value || '{}'
|
||||
return JSON.stringify(value || {})
|
||||
}
|
||||
Reference in New Issue
Block a user