Files
order_site/apps/backend/src/repositories/inventory-repo.js
T
2026-04-13 22:54:14 +08:00

304 lines
7.8 KiB
JavaScript

import { query, withTransaction } from '../db/client.js'
const INVENTORY_ITEM_SELECT = `
SELECT
ii.id,
ii.sku_code,
ii.batch_no,
ii.credential_type,
ii.display_value,
ii.status,
ii.invalid_reason,
ii.consumed_at AS delivered_at,
ii.created_at,
ii.updated_at,
tib.task_id AS reserved_by_task_id,
ft.task_no AS reserved_by_task_no,
ft.platform_order_id
FROM inventory_items ii
LEFT JOIN LATERAL (
SELECT *
FROM task_inventory_bindings
WHERE inventory_item_id = ii.id AND binding_status IN ('reserved', 'consumed')
ORDER BY id DESC
LIMIT 1
) tib ON TRUE
LEFT JOIN fulfillment_tasks ft ON ft.id = tib.task_id
`
export async function findFirstAvailableInventoryItemBySkuCode(skuCode, credentialType = 'tencent_code') {
const result = await query(
`${INVENTORY_ITEM_SELECT}
WHERE ii.sku_code = $1 AND ii.credential_type = $2 AND ii.status = 'available'
ORDER BY ii.id ASC
LIMIT 1`,
[skuCode, credentialType],
)
return result.rows[0] || null
}
export async function assignReservedInventoryItem(inventoryItemId, taskId, updatedAt, roleKey = 'primary_code') {
return withTransaction(async (client) => {
const inventoryResult = await client.query(
`
UPDATE inventory_items
SET status = 'reserved', updated_at = $1
WHERE id = $2 AND status = 'available'
RETURNING id
`,
[updatedAt, Number(inventoryItemId)],
)
if (!inventoryResult.rows[0]) {
return getInventoryItemById(inventoryItemId)
}
await client.query(
`
INSERT INTO task_inventory_bindings (
task_id,
inventory_item_id,
role_key,
quantity,
binding_status,
metadata_json,
created_at,
updated_at
) VALUES ($1, $2, $3, 1, 'reserved', '{}'::jsonb, $4, $4)
ON CONFLICT (task_id, role_key, inventory_item_id) DO UPDATE
SET binding_status = 'reserved', updated_at = EXCLUDED.updated_at, released_at = NULL
`,
[Number(taskId), Number(inventoryItemId), roleKey, updatedAt],
)
return getInventoryItemById(inventoryItemId)
})
}
export async function getInventoryItemById(inventoryItemId) {
const result = await query(
`${INVENTORY_ITEM_SELECT}
WHERE ii.id = $1
LIMIT 1`,
[Number(inventoryItemId)],
)
return result.rows[0] || null
}
export async function markInventoryItemDelivered(inventoryItemId, deliveredAt) {
return withTransaction(async (client) => {
await client.query(
`
UPDATE inventory_items
SET status = 'consumed', consumed_at = $1, updated_at = $1
WHERE id = $2
`,
[deliveredAt, Number(inventoryItemId)],
)
await client.query(
`
UPDATE task_inventory_bindings
SET binding_status = 'consumed', consumed_at = $1, updated_at = $1
WHERE inventory_item_id = $2 AND binding_status = 'reserved'
`,
[deliveredAt, Number(inventoryItemId)],
)
return getInventoryItemById(inventoryItemId)
})
}
export async function markInventoryItemConsumed(inventoryItemId, reason, consumedAt) {
return withTransaction(async (client) => {
await client.query(
`
UPDATE inventory_items
SET
status = 'consumed',
invalid_reason = $1,
consumed_at = $2,
updated_at = $2
WHERE id = $3
`,
[String(reason || '').trim(), consumedAt, Number(inventoryItemId)],
)
await client.query(
`
UPDATE task_inventory_bindings
SET binding_status = 'consumed', consumed_at = $1, updated_at = $1
WHERE inventory_item_id = $2 AND binding_status = 'reserved'
`,
[consumedAt, Number(inventoryItemId)],
)
return getInventoryItemById(inventoryItemId)
})
}
export async function listInventoryItems({
page = 1,
pageSize = 20,
skuCode = '',
credentialType = '',
status = '',
batchNo = '',
} = {}) {
const offset = (page - 1) * pageSize
const filters = []
const params = []
if (skuCode) {
params.push(skuCode)
filters.push(`ii.sku_code = $${params.length}`)
}
if (status) {
params.push(status === 'delivered' ? 'consumed' : status)
filters.push(`ii.status = $${params.length}`)
}
if (credentialType) {
params.push(credentialType)
filters.push(`ii.credential_type = $${params.length}`)
}
if (batchNo) {
params.push(`%${batchNo}%`)
filters.push(`ii.batch_no ILIKE $${params.length}`)
}
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
const totalResult = await query(
`SELECT COUNT(*)::int AS total FROM inventory_items ii ${whereClause}`,
params,
)
params.push(pageSize)
params.push(offset)
const itemsResult = await query(
`${INVENTORY_ITEM_SELECT}
${whereClause}
ORDER BY ii.id DESC
LIMIT $${params.length - 1} OFFSET $${params.length}`,
params,
)
return {
items: itemsResult.rows,
total: Number(totalResult.rows[0]?.total || 0),
}
}
export async function createInventoryItems(rows) {
let created = 0
for (const row of rows) {
const payloadJson = JSON.stringify(row.payload || { code: row.displayValue })
const displayValue = String(row.displayValue || '').trim()
const result = await query(
`
INSERT INTO inventory_items (
batch_no,
sku_code,
credential_type,
display_value,
payload_json,
source_type,
status,
invalid_reason,
metadata_json,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5::jsonb, 'static_import', 'available', '', '{}'::jsonb, $6, $7)
ON CONFLICT DO NOTHING
RETURNING id
`,
[
row.batchNo || '',
row.skuCode,
row.credentialType || 'tencent_code',
displayValue,
payloadJson,
row.createdAt,
row.updatedAt,
],
)
if (result.rows[0]?.id) {
created += 1
}
}
return created
}
export async function releaseReservedInventoryItem(inventoryItemId, updatedAt) {
return withTransaction(async (client) => {
await client.query(
`
UPDATE task_inventory_bindings
SET binding_status = 'released', released_at = $1, updated_at = $1
WHERE inventory_item_id = $2 AND binding_status = 'reserved'
`,
[updatedAt, Number(inventoryItemId)],
)
await client.query(
`
UPDATE inventory_items
SET status = 'available', updated_at = $1
WHERE id = $2 AND status = 'reserved'
`,
[updatedAt, Number(inventoryItemId)],
)
return getInventoryItemById(inventoryItemId)
})
}
export async function invalidateInventoryItem(inventoryItemId, invalidReason, updatedAt) {
const result = await query(
`
UPDATE inventory_items
SET status = 'invalid', invalid_reason = $1, updated_at = $2
WHERE id = $3 AND status = 'available'
RETURNING id
`,
[invalidReason, updatedAt, Number(inventoryItemId)],
)
if (!result.rows[0]) {
return getInventoryItemById(inventoryItemId)
}
return getInventoryItemById(inventoryItemId)
}
export async function invalidateReservedInventoryItem(inventoryItemId, invalidReason, updatedAt) {
return withTransaction(async (client) => {
await client.query(
`
UPDATE task_inventory_bindings
SET binding_status = 'released', released_at = $1, updated_at = $1
WHERE inventory_item_id = $2 AND binding_status = 'reserved'
`,
[updatedAt, Number(inventoryItemId)],
)
await client.query(
`
UPDATE inventory_items
SET status = 'invalid', invalid_reason = $1, updated_at = $2
WHERE id = $3
`,
[String(invalidReason || '').trim(), updatedAt, Number(inventoryItemId)],
)
return getInventoryItemById(inventoryItemId)
})
}