新增接单模板价格同步

This commit is contained in:
yml2213
2026-08-20 20:24:00 +08:00
parent 6539e0fb21
commit e05a6797a3
9 changed files with 437 additions and 3 deletions
@@ -260,6 +260,8 @@ export type WorkOrderShareRow = {
export type WorkOrderRow = {
id: number
work_order_no: string
/** 自动建单命中的接单模板;历史或人工工单可能为空。 */
product_rule_id?: number | null
order_id: number | null
order_item_id: number | null
task_id: number | null
@@ -411,6 +413,7 @@ export type CreateWorkerInput = {
export type CreateWorkOrderInput = {
workOrderNo: string
productRuleId?: number | null
orderId?: number | null
orderItemId?: number | null
taskId?: number | null
@@ -867,6 +867,183 @@ export async function getWorkProductRuleById(
return result.rows[0] || null
}
/** 查询仍在接单大厅、且明确来源于指定模板的工单。 */
export async function listOpenWorkOrdersByProductRuleId(
ruleId: number | string,
): Promise<WorkOrderRow[]> {
const result = await query<WorkOrderRow>(
`${WORK_ORDER_SELECT}
WHERE wo.product_rule_id = $1
AND wo.status = 'open'
ORDER BY wo.id ASC`,
[Number(ruleId)],
)
return result.rows
}
export type SyncOpenWorkOrderPriceResult = {
updated: boolean
reason: 'updated' | 'price_unchanged' | 'not_open' | 'assigned' | 'sharing_filled'
workOrderId: number
sharingPartial: boolean
workerIds: number[]
}
/** 拼单改价时保留已加入份额的报酬,只按新价计算剩余份额。 */
export function resolveOpenSharingSyncReward(input: {
totalQuantity: number
joinedQuantity: number
lockedShareReward: number
nextUnitReward: number
}) {
const totalQuantity = toPositiveInteger(input.totalQuantity, 1)
const joinedQuantity = Math.min(totalQuantity, Math.max(0, Number(input.joinedQuantity || 0)))
return (
Math.max(0, Number(input.lockedShareReward || 0)) +
(totalQuantity - joinedQuantity) * toPositiveInteger(input.nextUnitReward, 0)
)
}
/**
* 同步大厅工单的模板价格。
* 已加入的拼单份额报酬是合同快照,只更新尚未加入的剩余份额价格。
*/
export async function syncOpenWorkOrderPriceFromRule(input: {
workOrderId: number
productRuleId: number
rewardAmount: number
sharingUnitReward?: number
actorName: string
now: string
}): Promise<SyncOpenWorkOrderPriceResult> {
return withTransaction(async (client) => {
const orderResult = await client.query<WorkOrderRow>(
`SELECT *
FROM work_orders
WHERE id = $1 AND product_rule_id = $2
FOR UPDATE`,
[input.workOrderId, input.productRuleId],
)
const workOrder = orderResult.rows[0] || null
if (!workOrder || workOrder.status !== 'open') {
return {
updated: false,
reason: 'not_open',
workOrderId: input.workOrderId,
sharingPartial: false,
workerIds: [],
}
}
if (workOrder.assigned_worker_id) {
return {
updated: false,
reason: 'assigned',
workOrderId: input.workOrderId,
sharingPartial: false,
workerIds: [],
}
}
const sharesResult = await client.query<WorkOrderShareRow>(
`SELECT *
FROM work_order_shares
WHERE work_order_id = $1
FOR UPDATE`,
[workOrder.id],
)
const activeShares = sharesResult.rows.filter((share) => share.status !== 'cancelled')
const joinedQuantity = activeShares.reduce(
(sum, share) => sum + Math.max(0, Number(share.quantity || 0)),
0,
)
const workerIds = [
...new Set(
activeShares.map((share) => Number(share.worker_id)).filter((workerId) => workerId > 0),
),
]
const sharingPartial = workOrder.sharing_enabled === true && joinedQuantity > 0
let nextRewardAmount = toPositiveInteger(input.rewardAmount, 0)
let nextSharingUnitReward = Number(workOrder.sharing_unit_reward || 0)
if (workOrder.sharing_enabled === true) {
const totalQuantity = toPositiveInteger(workOrder.sharing_total_quantity, 1)
if (joinedQuantity >= totalQuantity) {
return {
updated: false,
reason: 'sharing_filled',
workOrderId: Number(workOrder.id),
sharingPartial,
workerIds,
}
}
nextSharingUnitReward = toPositiveInteger(input.sharingUnitReward, 0)
const lockedShareReward = activeShares.reduce(
(sum, share) => sum + Math.max(0, Number(share.share_reward || 0)),
0,
)
nextRewardAmount = resolveOpenSharingSyncReward({
totalQuantity,
joinedQuantity,
lockedShareReward,
nextUnitReward: nextSharingUnitReward,
})
}
const unchanged =
Number(workOrder.reward_amount || 0) === nextRewardAmount &&
(!workOrder.sharing_enabled ||
Number(workOrder.sharing_unit_reward || 0) === nextSharingUnitReward)
if (unchanged) {
return {
updated: false,
reason: 'price_unchanged',
workOrderId: Number(workOrder.id),
sharingPartial,
workerIds,
}
}
await client.query(
`UPDATE work_orders
SET reward_amount = $1,
sharing_unit_reward = $2,
updated_at = $3
WHERE id = $4`,
[nextRewardAmount, nextSharingUnitReward, input.now, workOrder.id],
)
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, 'admin', $2, 'price_synced_from_rule', $3, $3, $4::jsonb, $5)`,
[
workOrder.id,
input.actorName,
workOrder.status,
JSON.stringify({
productRuleId: input.productRuleId,
previousRewardAmount: Number(workOrder.reward_amount || 0),
rewardAmount: nextRewardAmount,
previousSharingUnitReward: Number(workOrder.sharing_unit_reward || 0),
sharingUnitReward: nextSharingUnitReward,
joinedQuantity,
lockedShareReward: activeShares.reduce(
(sum, share) => sum + Math.max(0, Number(share.share_reward || 0)),
0,
),
}),
input.now,
],
)
return {
updated: true,
reason: 'updated',
workOrderId: Number(workOrder.id),
sharingPartial,
workerIds,
}
})
}
export async function deleteWorkProductRule(
ruleId: number | string,
): Promise<{ deleted: boolean }> {
@@ -986,19 +1163,20 @@ export async function createWorkOrder(input: CreateWorkOrderInput): Promise<Work
const result = await query<{ id: number }>(
`
INSERT INTO work_orders (
work_order_no, order_id, order_item_id, task_id, platform_order_id,
work_order_no, product_rule_id, order_id, order_item_id, task_id, platform_order_id,
product_name, category_id, status, reward_amount, required_deposit_amount,
deposit_threshold_amount, sharing_enabled, sharing_total_quantity,
sharing_unit_reward, timeout_minutes, timeout_policy,
material_json, requirement_json, created_at, updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17::jsonb, $18::jsonb, $19, $20
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,
$12, $13, $14, $15, $16, $17, $18::jsonb, $19::jsonb, $20, $21
)
RETURNING id
`,
[
input.workOrderNo,
input.productRuleId || null,
input.orderId || null,
input.orderItemId || null,
input.taskId || null,