Merge branch 'dev'

This commit is contained in:
2026-11-02 12:04:32 +08:00
38 changed files with 1615 additions and 942 deletions
@@ -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
}