增加分组库存与客服

This commit is contained in:
yml2213
2026-04-23 13:38:21 +08:00
parent 2a05a68959
commit f0b31121d7
31 changed files with 675 additions and 80 deletions
@@ -1,8 +1,20 @@
import { query } from '../db/client.js'
import { query, withTransaction } from '../db/client.js'
const ADMIN_USER_SELECT = `
SELECT
au.*,
COALESCE(bindings.inventory_group_codes, ARRAY[]::text[]) AS inventory_group_codes
FROM admin_users au
LEFT JOIN LATERAL (
SELECT ARRAY_AGG(auigb.inventory_group_code ORDER BY auigb.inventory_group_code) AS inventory_group_codes
FROM admin_user_inventory_group_bindings auigb
WHERE auigb.admin_user_id = au.id
) bindings ON TRUE
`
export async function getAdminUserById(userId) {
const result = await query(
'SELECT * FROM admin_users WHERE id = $1 LIMIT 1',
`${ADMIN_USER_SELECT} WHERE au.id = $1 LIMIT 1`,
[Number(userId)],
)
return result.rows[0] || null
@@ -10,7 +22,7 @@ export async function getAdminUserById(userId) {
export async function getAdminUserByUsername(username) {
const result = await query(
'SELECT * FROM admin_users WHERE username = $1 LIMIT 1',
`${ADMIN_USER_SELECT} WHERE au.username = $1 LIMIT 1`,
[String(username || '').trim().toLowerCase()],
)
return result.rows[0] || null
@@ -27,7 +39,7 @@ export async function createAdminUser(input) {
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *
RETURNING id
`,
[
input.username,
@@ -39,7 +51,7 @@ export async function createAdminUser(input) {
],
)
return result.rows[0] || null
return getAdminUserById(result.rows[0]?.id || 0)
}
export async function updateAdminUser(userId, patch) {
@@ -59,7 +71,7 @@ export async function updateAdminUser(userId, patch) {
status = $4,
updated_at = $5
WHERE id = $6
RETURNING *
RETURNING id
`,
[
next.username,
@@ -71,7 +83,7 @@ export async function updateAdminUser(userId, patch) {
],
)
return result.rows[0] || null
return getAdminUserById(result.rows[0]?.id || 0)
}
export async function listAdminUsers({ page = 1, pageSize = 20, username = '', role = '', status = '' } = {}) {
@@ -101,10 +113,9 @@ export async function listAdminUsers({ page = 1, pageSize = 20, username = '', r
params.push(offset)
const itemsResult = await query(
`
SELECT *
FROM admin_users
${ADMIN_USER_SELECT}
${whereClause}
ORDER BY id DESC
ORDER BY au.id DESC
LIMIT $${params.length - 1} OFFSET $${params.length}
`,
params,
@@ -122,3 +133,35 @@ export async function countActiveAdminUsers() {
)
return Number(result.rows[0]?.total || 0)
}
export async function replaceAdminUserInventoryGroupBindings(userId, inventoryGroupCodes = [], timestamp) {
const normalizedUserId = Number(userId)
const normalizedCodes = Array.from(new Set((Array.isArray(inventoryGroupCodes) ? inventoryGroupCodes : [])
.map((value) => String(value || '').trim())
.filter(Boolean)))
await withTransaction(async (client) => {
await client.query(
'DELETE FROM admin_user_inventory_group_bindings WHERE admin_user_id = $1',
[normalizedUserId],
)
for (const inventoryGroupCode of normalizedCodes) {
await client.query(
`
INSERT INTO admin_user_inventory_group_bindings (
admin_user_id,
inventory_group_code,
created_at,
updated_at
) VALUES ($1, $2, $3, $4)
ON CONFLICT (admin_user_id, inventory_group_code) DO UPDATE
SET updated_at = EXCLUDED.updated_at
`,
[normalizedUserId, inventoryGroupCode, timestamp, timestamp],
)
}
})
return getAdminUserById(normalizedUserId)
}
+112 -6
View File
@@ -15,6 +15,7 @@ const INVENTORY_ITEM_SELECT = `
ii.sku_code,
ii.batch_no,
ii.credential_type,
ii.inventory_group_code,
ii.display_value,
ii.status,
ii.invalid_reason,
@@ -36,13 +37,34 @@ const INVENTORY_ITEM_SELECT = `
`
/** @returns {Promise<InventoryItemRow | null>} */
export async function findFirstAvailableInventoryItemBySkuCode(skuCode, credentialType = 'tencent_code') {
export async function findFirstAvailableInventoryItemBySkuCode(
skuCode,
credentialType = 'tencent_code',
inventoryGroupCodes = null,
) {
const normalizedInventoryGroupCodes = normalizeInventoryGroupCodes(inventoryGroupCodes)
if (normalizedInventoryGroupCodes && normalizedInventoryGroupCodes.length === 0) {
return null
}
const params = [skuCode, credentialType]
const filters = [
'ii.sku_code = $1',
'ii.credential_type = $2',
`ii.status = 'available'`,
]
if (normalizedInventoryGroupCodes) {
params.push(normalizedInventoryGroupCodes)
filters.push(`ii.inventory_group_code = ANY($${params.length}::text[])`)
}
const result = await query(
`${INVENTORY_ITEM_SELECT}
WHERE ii.sku_code = $1 AND ii.credential_type = $2 AND ii.status = 'available'
WHERE ${filters.join(' AND ')}
ORDER BY ii.id ASC
LIMIT 1`,
[skuCode, credentialType],
params,
)
return /** @type {InventoryItemRow | null} */ (result.rows[0] || null)
@@ -162,7 +184,17 @@ export async function listInventoryItems({
credentialType = '',
status = '',
batchNo = '',
inventoryGroupCode = '',
allowedInventoryGroupCodes = null,
} = /** @type {InventoryListQueryInput} */ ({})) {
const normalizedAllowedInventoryGroupCodes = normalizeInventoryGroupCodes(allowedInventoryGroupCodes)
if (normalizedAllowedInventoryGroupCodes && normalizedAllowedInventoryGroupCodes.length === 0) {
return {
items: [],
total: 0,
}
}
const offset = (page - 1) * pageSize
const filters = []
const params = []
@@ -187,6 +219,16 @@ export async function listInventoryItems({
filters.push(`ii.batch_no ILIKE $${params.length}`)
}
if (inventoryGroupCode) {
params.push(inventoryGroupCode)
filters.push(`ii.inventory_group_code = $${params.length}`)
}
if (normalizedAllowedInventoryGroupCodes) {
params.push(normalizedAllowedInventoryGroupCodes)
filters.push(`ii.inventory_group_code = ANY($${params.length}::text[])`)
}
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
const totalResult = await query(
`SELECT COUNT(*)::int AS total FROM inventory_items ii ${whereClause}`,
@@ -215,7 +257,14 @@ export async function listInventorySkuSuggestions({
credentialType = '',
keyword = '',
limit = 50,
inventoryGroupCode = '',
allowedInventoryGroupCodes = null,
} = /** @type {InventorySkuSuggestionQueryInput} */ ({})) {
const normalizedAllowedInventoryGroupCodes = normalizeInventoryGroupCodes(allowedInventoryGroupCodes)
if (normalizedAllowedInventoryGroupCodes && normalizedAllowedInventoryGroupCodes.length === 0) {
return []
}
const filters = []
const params = []
@@ -229,6 +278,16 @@ export async function listInventorySkuSuggestions({
filters.push(`sku_code ILIKE $${params.length}`)
}
if (inventoryGroupCode) {
params.push(inventoryGroupCode)
filters.push(`inventory_group_code = $${params.length}`)
}
if (normalizedAllowedInventoryGroupCodes) {
params.push(normalizedAllowedInventoryGroupCodes)
filters.push(`inventory_group_code = ANY($${params.length}::text[])`)
}
const normalizedLimit = Number.isFinite(Number(limit))
? Math.max(1, Math.min(Number(limit), 100))
: 50
@@ -240,17 +299,19 @@ export async function listInventorySkuSuggestions({
SELECT
sku_code,
credential_type,
inventory_group_code,
COUNT(*)::int AS total_count,
COUNT(*) FILTER (WHERE status = 'available')::int AS available_count,
MAX(updated_at) AS latest_updated_at
FROM inventory_items
${whereClause}
GROUP BY sku_code, credential_type
GROUP BY sku_code, credential_type, inventory_group_code
ORDER BY
COUNT(*) FILTER (WHERE status = 'available') DESC,
COUNT(*) DESC,
MAX(updated_at) DESC,
sku_code ASC
sku_code ASC,
inventory_group_code ASC
LIMIT $${params.length}
`,
params,
@@ -266,12 +327,14 @@ export async function createInventoryItems(rows) {
for (const row of rows) {
const payloadJson = JSON.stringify(row.payload || { code: row.displayValue })
const displayValue = String(row.displayValue || '').trim()
const inventoryGroupCode = String(row.inventoryGroupCode || '').trim()
const result = await query(
`
INSERT INTO inventory_items (
batch_no,
sku_code,
credential_type,
inventory_group_code,
display_value,
payload_json,
source_type,
@@ -280,7 +343,7 @@ export async function createInventoryItems(rows) {
metadata_json,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5::jsonb, 'static_import', 'available', '', '{}'::jsonb, $6, $7)
) VALUES ($1, $2, $3, $4, $5, $6::jsonb, 'static_import', 'available', '', '{}'::jsonb, $7, $8)
ON CONFLICT DO NOTHING
RETURNING id
`,
@@ -288,6 +351,7 @@ export async function createInventoryItems(rows) {
row.batchNo || '',
row.skuCode,
row.credentialType || 'tencent_code',
inventoryGroupCode,
displayValue,
payloadJson,
row.createdAt,
@@ -303,6 +367,48 @@ export async function createInventoryItems(rows) {
return created
}
export async function listInventoryGroupCodes({
keyword = '',
limit = 100,
} = {}) {
const params = []
const filters = [`inventory_group_code <> ''`]
if (keyword) {
params.push(`%${String(keyword || '').trim()}%`)
filters.push(`inventory_group_code ILIKE $${params.length}`)
}
const normalizedLimit = Number.isFinite(Number(limit))
? Math.max(1, Math.min(Number(limit), 200))
: 100
params.push(normalizedLimit)
const result = await query(
`
SELECT inventory_group_code
FROM inventory_items
WHERE ${filters.join(' AND ')}
GROUP BY inventory_group_code
ORDER BY inventory_group_code ASC
LIMIT $${params.length}
`,
params,
)
return result.rows.map((row) => String(row.inventory_group_code || '').trim()).filter(Boolean)
}
function normalizeInventoryGroupCodes(inventoryGroupCodes) {
if (inventoryGroupCodes == null) {
return null
}
return Array.from(new Set((Array.isArray(inventoryGroupCodes) ? inventoryGroupCodes : [inventoryGroupCodes])
.map((value) => String(value || '').trim())
.filter(Boolean)))
}
/** @returns {Promise<InventoryItemRow | null>} */
export async function releaseReservedInventoryItem(inventoryItemId, updatedAt) {
return withTransaction(async (client) => {
@@ -24,6 +24,7 @@ export async function listTaskInventoryBindingsByTaskId(taskId) {
ii.sku_code,
ii.batch_no,
ii.credential_type,
ii.inventory_group_code,
ii.display_value,
ii.status AS inventory_item_status,
ii.invalid_reason
@@ -57,6 +58,7 @@ export async function getTaskInventoryBindingById(bindingId) {
ii.sku_code,
ii.batch_no,
ii.credential_type,
ii.inventory_group_code,
ii.display_value,
ii.status AS inventory_item_status,
ii.invalid_reason