Merge branch 'dev'
This commit is contained in:
@@ -537,6 +537,8 @@ export type WorkOrderCapabilities = {
|
||||
canPin: boolean
|
||||
canUnpublish: boolean
|
||||
canUpdateMaterial: boolean
|
||||
/** 待分配且未指派、无份额、未发布时,客服可以把补资料状态重置回待完善。 */
|
||||
canResetMaterial: boolean
|
||||
canAccept: boolean
|
||||
canMarkProblem: boolean
|
||||
canDelete: boolean
|
||||
|
||||
@@ -143,6 +143,7 @@ export async function assignWorkOrderToWorker(input: {
|
||||
order: WorkOrderRow | null
|
||||
failureReason:
|
||||
| 'work_order_not_assignable'
|
||||
| 'work_order_has_shares'
|
||||
| 'worker_active_order_limit'
|
||||
| 'worker_deposit_insufficient'
|
||||
| null
|
||||
@@ -167,6 +168,19 @@ export async function assignWorkOrderToWorker(input: {
|
||||
}
|
||||
const fromStatus = workOrder.status
|
||||
|
||||
// 已有拼单份额的订单不能按整单指派:否则会出现“整单接单人 + 其他人的份额”混在一起。
|
||||
const shareCountResult = await client.query<{ total: number }>(
|
||||
`
|
||||
SELECT COUNT(*)::int AS total
|
||||
FROM work_order_shares
|
||||
WHERE work_order_id = $1 AND status <> 'cancelled'
|
||||
`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
if (Number(shareCountResult.rows[0]?.total || 0) > 0) {
|
||||
return { order: null, failureReason: 'work_order_has_shares' }
|
||||
}
|
||||
|
||||
await ensureWorkerWalletWithClient(client, input.workerId, input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, input.workerId)
|
||||
const activeOrderCount = await countWorkerActiveOrdersWithClient(client, input.workerId)
|
||||
@@ -193,6 +207,9 @@ export async function assignWorkOrderToWorker(input: {
|
||||
friend_added_at = NULL,
|
||||
gift_available_at = NULL,
|
||||
${CLEAR_DELIVERY_RESIDUE_SQL},
|
||||
-- 管理端/寄卖履约都是整单指派,不会创建 work_order_shares;
|
||||
-- 必须同时清掉拼单标记,避免出现“已开拼单但没有份额”的脏状态。
|
||||
sharing_enabled = false,
|
||||
updated_at = $2
|
||||
WHERE id = $4
|
||||
`,
|
||||
|
||||
@@ -1,152 +1,32 @@
|
||||
import type { PoolClient } from 'pg'
|
||||
|
||||
import { query } from '../../db/client.js'
|
||||
import type { WorkOrderRow } from './types.js'
|
||||
|
||||
export type WorkOrderMaterialLinkStatus = 'active' | 'used' | 'expired' | 'revoked'
|
||||
|
||||
export type WorkOrderMaterialLinkRow = {
|
||||
id: number
|
||||
work_order_id: number
|
||||
token: string
|
||||
status: WorkOrderMaterialLinkStatus
|
||||
expires_at: string
|
||||
used_at: string | null
|
||||
created_by: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export async function findWorkOrderMaterialLinkByToken(
|
||||
token: string,
|
||||
): Promise<WorkOrderMaterialLinkRow | null> {
|
||||
const result = await query<WorkOrderMaterialLinkRow>(
|
||||
'SELECT * FROM work_order_material_links WHERE token = $1 LIMIT 1',
|
||||
[String(token || '').trim()],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function findActiveWorkOrderMaterialLink(
|
||||
workOrderId: number | string,
|
||||
now: string,
|
||||
): Promise<WorkOrderMaterialLinkRow | null> {
|
||||
const result = await query<WorkOrderMaterialLinkRow>(
|
||||
`SELECT * FROM work_order_material_links
|
||||
WHERE work_order_id = $1 AND status = 'active' AND expires_at > $2
|
||||
ORDER BY id DESC
|
||||
LIMIT 1`,
|
||||
[Number(workOrderId), now],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁工单并回读「有效拼单份额」。
|
||||
*
|
||||
* `SELECT * FROM work_orders` 本身没有聚合字段,直接看 `active_share_quantity`
|
||||
* 会永远是 0,导致补资料绕过「已有拼单份额不允许改资料」的守卫。
|
||||
*/
|
||||
export async function getWorkOrderForUpdateWithClient(
|
||||
client: PoolClient,
|
||||
workOrderId: number | string,
|
||||
): Promise<WorkOrderRow | null> {
|
||||
const result = await client.query<WorkOrderRow>(
|
||||
'SELECT * FROM work_orders WHERE id = $1 FOR UPDATE',
|
||||
`SELECT wo.*,
|
||||
COALESCE((
|
||||
SELECT SUM(wos.quantity)
|
||||
FROM work_order_shares wos
|
||||
WHERE wos.work_order_id = wo.id AND wos.status != 'cancelled'
|
||||
), 0)::int AS active_share_quantity
|
||||
FROM work_orders wo
|
||||
WHERE wo.id = $1
|
||||
FOR UPDATE`,
|
||||
[Number(workOrderId)],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function getWorkOrderMaterialLinkForUpdateWithClient(
|
||||
client: PoolClient,
|
||||
linkId: number | string,
|
||||
): Promise<WorkOrderMaterialLinkRow | null> {
|
||||
const result = await client.query<WorkOrderMaterialLinkRow>(
|
||||
'SELECT * FROM work_order_material_links WHERE id = $1 FOR UPDATE',
|
||||
[Number(linkId)],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function findActiveWorkOrderMaterialLinkWithClient(
|
||||
client: PoolClient,
|
||||
workOrderId: number | string,
|
||||
now: string,
|
||||
): Promise<WorkOrderMaterialLinkRow | null> {
|
||||
const result = await client.query<WorkOrderMaterialLinkRow>(
|
||||
`SELECT * FROM work_order_material_links
|
||||
WHERE work_order_id = $1 AND status = 'active' AND expires_at > $2
|
||||
ORDER BY id DESC
|
||||
LIMIT 1`,
|
||||
[Number(workOrderId), now],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
/** 重新生成前清掉旧 active:已过期标 expired,未过期标 revoked。 */
|
||||
export async function revokeActiveWorkOrderMaterialLinksWithClient(
|
||||
client: PoolClient,
|
||||
workOrderId: number | string,
|
||||
now: string,
|
||||
): Promise<number> {
|
||||
const result = await client.query(
|
||||
`UPDATE work_order_material_links
|
||||
SET status = CASE WHEN expires_at <= $2 THEN 'expired' ELSE 'revoked' END,
|
||||
updated_at = $2
|
||||
WHERE work_order_id = $1 AND status = 'active'`,
|
||||
[Number(workOrderId), now],
|
||||
)
|
||||
return result.rowCount || 0
|
||||
}
|
||||
|
||||
export async function createWorkOrderMaterialLinkWithClient(
|
||||
client: PoolClient,
|
||||
input: {
|
||||
workOrderId: number | string
|
||||
token: string
|
||||
expiresAt: string
|
||||
createdBy: string
|
||||
now: string
|
||||
},
|
||||
): Promise<WorkOrderMaterialLinkRow> {
|
||||
const result = await client.query<WorkOrderMaterialLinkRow>(
|
||||
`INSERT INTO work_order_material_links (
|
||||
work_order_id, token, status, expires_at, created_by, created_at, updated_at
|
||||
) VALUES ($1, $2, 'active', $3, $4, $5, $5)
|
||||
RETURNING *`,
|
||||
[
|
||||
Number(input.workOrderId),
|
||||
input.token,
|
||||
input.expiresAt,
|
||||
String(input.createdBy || '').trim(),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
return result.rows[0]!
|
||||
}
|
||||
|
||||
export async function markWorkOrderMaterialLinkUsedWithClient(
|
||||
client: PoolClient,
|
||||
linkId: number | string,
|
||||
now: string,
|
||||
): Promise<WorkOrderMaterialLinkRow | null> {
|
||||
const result = await client.query<WorkOrderMaterialLinkRow>(
|
||||
`UPDATE work_order_material_links
|
||||
SET status = 'used', used_at = $2, updated_at = $2
|
||||
WHERE id = $1 AND status = 'active' AND expires_at > $2
|
||||
RETURNING *`,
|
||||
[Number(linkId), now],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function markWorkOrderMaterialLinkExpired(
|
||||
linkId: number | string,
|
||||
now: string,
|
||||
): Promise<void> {
|
||||
await query(
|
||||
`UPDATE work_order_material_links
|
||||
SET status = 'expired', updated_at = $2
|
||||
WHERE id = $1 AND status = 'active'`,
|
||||
[Number(linkId), now],
|
||||
)
|
||||
}
|
||||
|
||||
export async function updateWorkOrderMaterialWithClient(
|
||||
client: PoolClient,
|
||||
input: {
|
||||
@@ -165,3 +45,32 @@ export async function updateWorkOrderMaterialWithClient(
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 客服重置补资料状态:只允许「待分配、未指派、未发布」的工单回到待完善。
|
||||
*
|
||||
* 服务层已经在事务里锁工单并校验过拼单份额;这里的 WHERE 条件再兜一层,
|
||||
* 防止并发或调用方漏校验。
|
||||
*/
|
||||
export async function resetWorkOrderMaterialStatusWithClient(
|
||||
client: PoolClient,
|
||||
input: {
|
||||
workOrderId: number | string
|
||||
fromStatus: string
|
||||
toStatus: string
|
||||
now: string
|
||||
},
|
||||
): Promise<WorkOrderRow | null> {
|
||||
const result = await client.query<WorkOrderRow>(
|
||||
`UPDATE work_orders
|
||||
SET status = $2, updated_at = $3
|
||||
WHERE id = $1
|
||||
AND status = $4
|
||||
AND assigned_worker_id IS NULL
|
||||
AND published_at IS NULL
|
||||
AND hall_queued_at IS NULL
|
||||
RETURNING *`,
|
||||
[Number(input.workOrderId), input.toStatus, input.now, input.fromStatus],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user