新增接单模板价格同步
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user