增加分组库存与客服
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
ALTER TABLE inventory_items
|
||||||
|
ADD COLUMN IF NOT EXISTS inventory_group_code TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_inventory_items_group_lookup
|
||||||
|
ON inventory_items(sku_code, credential_type, inventory_group_code, status, id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS admin_user_inventory_group_bindings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
admin_user_id BIGINT NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
|
||||||
|
inventory_group_code TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL,
|
||||||
|
UNIQUE(admin_user_id, inventory_group_code)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_admin_user_inventory_group_bindings_user_id
|
||||||
|
ON admin_user_inventory_group_bindings(admin_user_id, inventory_group_code);
|
||||||
@@ -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) {
|
export async function getAdminUserById(userId) {
|
||||||
const result = await query(
|
const result = await query(
|
||||||
'SELECT * FROM admin_users WHERE id = $1 LIMIT 1',
|
`${ADMIN_USER_SELECT} WHERE au.id = $1 LIMIT 1`,
|
||||||
[Number(userId)],
|
[Number(userId)],
|
||||||
)
|
)
|
||||||
return result.rows[0] || null
|
return result.rows[0] || null
|
||||||
@@ -10,7 +22,7 @@ export async function getAdminUserById(userId) {
|
|||||||
|
|
||||||
export async function getAdminUserByUsername(username) {
|
export async function getAdminUserByUsername(username) {
|
||||||
const result = await query(
|
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()],
|
[String(username || '').trim().toLowerCase()],
|
||||||
)
|
)
|
||||||
return result.rows[0] || null
|
return result.rows[0] || null
|
||||||
@@ -27,7 +39,7 @@ export async function createAdminUser(input) {
|
|||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6)
|
) VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
RETURNING *
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
input.username,
|
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) {
|
export async function updateAdminUser(userId, patch) {
|
||||||
@@ -59,7 +71,7 @@ export async function updateAdminUser(userId, patch) {
|
|||||||
status = $4,
|
status = $4,
|
||||||
updated_at = $5
|
updated_at = $5
|
||||||
WHERE id = $6
|
WHERE id = $6
|
||||||
RETURNING *
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
next.username,
|
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 = '' } = {}) {
|
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)
|
params.push(offset)
|
||||||
const itemsResult = await query(
|
const itemsResult = await query(
|
||||||
`
|
`
|
||||||
SELECT *
|
${ADMIN_USER_SELECT}
|
||||||
FROM admin_users
|
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY id DESC
|
ORDER BY au.id DESC
|
||||||
LIMIT $${params.length - 1} OFFSET $${params.length}
|
LIMIT $${params.length - 1} OFFSET $${params.length}
|
||||||
`,
|
`,
|
||||||
params,
|
params,
|
||||||
@@ -122,3 +133,35 @@ export async function countActiveAdminUsers() {
|
|||||||
)
|
)
|
||||||
return Number(result.rows[0]?.total || 0)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const INVENTORY_ITEM_SELECT = `
|
|||||||
ii.sku_code,
|
ii.sku_code,
|
||||||
ii.batch_no,
|
ii.batch_no,
|
||||||
ii.credential_type,
|
ii.credential_type,
|
||||||
|
ii.inventory_group_code,
|
||||||
ii.display_value,
|
ii.display_value,
|
||||||
ii.status,
|
ii.status,
|
||||||
ii.invalid_reason,
|
ii.invalid_reason,
|
||||||
@@ -36,13 +37,34 @@ const INVENTORY_ITEM_SELECT = `
|
|||||||
`
|
`
|
||||||
|
|
||||||
/** @returns {Promise<InventoryItemRow | null>} */
|
/** @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(
|
const result = await query(
|
||||||
`${INVENTORY_ITEM_SELECT}
|
`${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
|
ORDER BY ii.id ASC
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
[skuCode, credentialType],
|
params,
|
||||||
)
|
)
|
||||||
|
|
||||||
return /** @type {InventoryItemRow | null} */ (result.rows[0] || null)
|
return /** @type {InventoryItemRow | null} */ (result.rows[0] || null)
|
||||||
@@ -162,7 +184,17 @@ export async function listInventoryItems({
|
|||||||
credentialType = '',
|
credentialType = '',
|
||||||
status = '',
|
status = '',
|
||||||
batchNo = '',
|
batchNo = '',
|
||||||
|
inventoryGroupCode = '',
|
||||||
|
allowedInventoryGroupCodes = null,
|
||||||
} = /** @type {InventoryListQueryInput} */ ({})) {
|
} = /** @type {InventoryListQueryInput} */ ({})) {
|
||||||
|
const normalizedAllowedInventoryGroupCodes = normalizeInventoryGroupCodes(allowedInventoryGroupCodes)
|
||||||
|
if (normalizedAllowedInventoryGroupCodes && normalizedAllowedInventoryGroupCodes.length === 0) {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const offset = (page - 1) * pageSize
|
const offset = (page - 1) * pageSize
|
||||||
const filters = []
|
const filters = []
|
||||||
const params = []
|
const params = []
|
||||||
@@ -187,6 +219,16 @@ export async function listInventoryItems({
|
|||||||
filters.push(`ii.batch_no ILIKE $${params.length}`)
|
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 whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
|
||||||
const totalResult = await query(
|
const totalResult = await query(
|
||||||
`SELECT COUNT(*)::int AS total FROM inventory_items ii ${whereClause}`,
|
`SELECT COUNT(*)::int AS total FROM inventory_items ii ${whereClause}`,
|
||||||
@@ -215,7 +257,14 @@ export async function listInventorySkuSuggestions({
|
|||||||
credentialType = '',
|
credentialType = '',
|
||||||
keyword = '',
|
keyword = '',
|
||||||
limit = 50,
|
limit = 50,
|
||||||
|
inventoryGroupCode = '',
|
||||||
|
allowedInventoryGroupCodes = null,
|
||||||
} = /** @type {InventorySkuSuggestionQueryInput} */ ({})) {
|
} = /** @type {InventorySkuSuggestionQueryInput} */ ({})) {
|
||||||
|
const normalizedAllowedInventoryGroupCodes = normalizeInventoryGroupCodes(allowedInventoryGroupCodes)
|
||||||
|
if (normalizedAllowedInventoryGroupCodes && normalizedAllowedInventoryGroupCodes.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const filters = []
|
const filters = []
|
||||||
const params = []
|
const params = []
|
||||||
|
|
||||||
@@ -229,6 +278,16 @@ export async function listInventorySkuSuggestions({
|
|||||||
filters.push(`sku_code ILIKE $${params.length}`)
|
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))
|
const normalizedLimit = Number.isFinite(Number(limit))
|
||||||
? Math.max(1, Math.min(Number(limit), 100))
|
? Math.max(1, Math.min(Number(limit), 100))
|
||||||
: 50
|
: 50
|
||||||
@@ -240,17 +299,19 @@ export async function listInventorySkuSuggestions({
|
|||||||
SELECT
|
SELECT
|
||||||
sku_code,
|
sku_code,
|
||||||
credential_type,
|
credential_type,
|
||||||
|
inventory_group_code,
|
||||||
COUNT(*)::int AS total_count,
|
COUNT(*)::int AS total_count,
|
||||||
COUNT(*) FILTER (WHERE status = 'available')::int AS available_count,
|
COUNT(*) FILTER (WHERE status = 'available')::int AS available_count,
|
||||||
MAX(updated_at) AS latest_updated_at
|
MAX(updated_at) AS latest_updated_at
|
||||||
FROM inventory_items
|
FROM inventory_items
|
||||||
${whereClause}
|
${whereClause}
|
||||||
GROUP BY sku_code, credential_type
|
GROUP BY sku_code, credential_type, inventory_group_code
|
||||||
ORDER BY
|
ORDER BY
|
||||||
COUNT(*) FILTER (WHERE status = 'available') DESC,
|
COUNT(*) FILTER (WHERE status = 'available') DESC,
|
||||||
COUNT(*) DESC,
|
COUNT(*) DESC,
|
||||||
MAX(updated_at) DESC,
|
MAX(updated_at) DESC,
|
||||||
sku_code ASC
|
sku_code ASC,
|
||||||
|
inventory_group_code ASC
|
||||||
LIMIT $${params.length}
|
LIMIT $${params.length}
|
||||||
`,
|
`,
|
||||||
params,
|
params,
|
||||||
@@ -266,12 +327,14 @@ export async function createInventoryItems(rows) {
|
|||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const payloadJson = JSON.stringify(row.payload || { code: row.displayValue })
|
const payloadJson = JSON.stringify(row.payload || { code: row.displayValue })
|
||||||
const displayValue = String(row.displayValue || '').trim()
|
const displayValue = String(row.displayValue || '').trim()
|
||||||
|
const inventoryGroupCode = String(row.inventoryGroupCode || '').trim()
|
||||||
const result = await query(
|
const result = await query(
|
||||||
`
|
`
|
||||||
INSERT INTO inventory_items (
|
INSERT INTO inventory_items (
|
||||||
batch_no,
|
batch_no,
|
||||||
sku_code,
|
sku_code,
|
||||||
credential_type,
|
credential_type,
|
||||||
|
inventory_group_code,
|
||||||
display_value,
|
display_value,
|
||||||
payload_json,
|
payload_json,
|
||||||
source_type,
|
source_type,
|
||||||
@@ -280,7 +343,7 @@ export async function createInventoryItems(rows) {
|
|||||||
metadata_json,
|
metadata_json,
|
||||||
created_at,
|
created_at,
|
||||||
updated_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
|
ON CONFLICT DO NOTHING
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
@@ -288,6 +351,7 @@ export async function createInventoryItems(rows) {
|
|||||||
row.batchNo || '',
|
row.batchNo || '',
|
||||||
row.skuCode,
|
row.skuCode,
|
||||||
row.credentialType || 'tencent_code',
|
row.credentialType || 'tencent_code',
|
||||||
|
inventoryGroupCode,
|
||||||
displayValue,
|
displayValue,
|
||||||
payloadJson,
|
payloadJson,
|
||||||
row.createdAt,
|
row.createdAt,
|
||||||
@@ -303,6 +367,48 @@ export async function createInventoryItems(rows) {
|
|||||||
return created
|
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>} */
|
/** @returns {Promise<InventoryItemRow | null>} */
|
||||||
export async function releaseReservedInventoryItem(inventoryItemId, updatedAt) {
|
export async function releaseReservedInventoryItem(inventoryItemId, updatedAt) {
|
||||||
return withTransaction(async (client) => {
|
return withTransaction(async (client) => {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export async function listTaskInventoryBindingsByTaskId(taskId) {
|
|||||||
ii.sku_code,
|
ii.sku_code,
|
||||||
ii.batch_no,
|
ii.batch_no,
|
||||||
ii.credential_type,
|
ii.credential_type,
|
||||||
|
ii.inventory_group_code,
|
||||||
ii.display_value,
|
ii.display_value,
|
||||||
ii.status AS inventory_item_status,
|
ii.status AS inventory_item_status,
|
||||||
ii.invalid_reason
|
ii.invalid_reason
|
||||||
@@ -57,6 +58,7 @@ export async function getTaskInventoryBindingById(bindingId) {
|
|||||||
ii.sku_code,
|
ii.sku_code,
|
||||||
ii.batch_no,
|
ii.batch_no,
|
||||||
ii.credential_type,
|
ii.credential_type,
|
||||||
|
ii.inventory_group_code,
|
||||||
ii.display_value,
|
ii.display_value,
|
||||||
ii.status AS inventory_item_status,
|
ii.status AS inventory_item_status,
|
||||||
ii.invalid_reason
|
ii.invalid_reason
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ const router = Router()
|
|||||||
router.use('/inventory', requireAdminRoles(['admin', 'operator']))
|
router.use('/inventory', requireAdminRoles(['admin', 'operator']))
|
||||||
|
|
||||||
router.get('/inventory', createJsonHandler(
|
router.get('/inventory', createJsonHandler(
|
||||||
(req) => getAdminInventoryItems(/** @type {AdminInventoryRouteQuery} */ (req.query)),
|
(req) => getAdminInventoryItems(
|
||||||
|
/** @type {AdminInventoryRouteQuery} */ (req.query),
|
||||||
|
req.adminSession || null,
|
||||||
|
),
|
||||||
{
|
{
|
||||||
successMessage: 'ok',
|
successMessage: 'ok',
|
||||||
errorMessage: '读取库存列表失败',
|
errorMessage: '读取库存列表失败',
|
||||||
@@ -36,7 +39,10 @@ router.get('/inventory', createJsonHandler(
|
|||||||
))
|
))
|
||||||
|
|
||||||
router.get('/inventory/sku-suggestions', createJsonHandler(
|
router.get('/inventory/sku-suggestions', createJsonHandler(
|
||||||
(req) => getAdminInventorySkuSuggestions(/** @type {AdminInventorySkuSuggestionRouteQuery} */ (req.query)),
|
(req) => getAdminInventorySkuSuggestions(
|
||||||
|
/** @type {AdminInventorySkuSuggestionRouteQuery} */ (req.query),
|
||||||
|
req.adminSession || null,
|
||||||
|
),
|
||||||
{
|
{
|
||||||
successMessage: 'ok',
|
successMessage: 'ok',
|
||||||
errorMessage: '读取内部 SKU 建议失败',
|
errorMessage: '读取内部 SKU 建议失败',
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ const router = Router()
|
|||||||
router.use('/manual-redeem', requireAdminRoles(['admin', 'operator', 'support']))
|
router.use('/manual-redeem', requireAdminRoles(['admin', 'operator', 'support']))
|
||||||
|
|
||||||
router.get('/manual-redeem/sku-suggestions', createJsonHandler(
|
router.get('/manual-redeem/sku-suggestions', createJsonHandler(
|
||||||
(req) => getAdminInventorySkuSuggestions(/** @type {AdminInventorySkuSuggestionRouteQuery} */ (req.query)),
|
(req) => getAdminInventorySkuSuggestions(
|
||||||
|
/** @type {AdminInventorySkuSuggestionRouteQuery} */ (req.query),
|
||||||
|
req.adminSession || null,
|
||||||
|
),
|
||||||
{
|
{
|
||||||
successMessage: 'ok',
|
successMessage: 'ok',
|
||||||
errorMessage: '读取人工兑换 SKU 建议失败',
|
errorMessage: '读取人工兑换 SKU 建议失败',
|
||||||
@@ -60,7 +63,10 @@ router.post('/manual-redeem', createJsonHandler(
|
|||||||
))
|
))
|
||||||
|
|
||||||
router.get('/manual-redeem/:taskId', createJsonHandler(
|
router.get('/manual-redeem/:taskId', createJsonHandler(
|
||||||
(req) => getAdminManualRedeemDetail((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
|
(req) => getAdminManualRedeemDetail(
|
||||||
|
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
|
||||||
|
req.adminSession || null,
|
||||||
|
),
|
||||||
{
|
{
|
||||||
successMessage: 'ok',
|
successMessage: 'ok',
|
||||||
errorMessage: '读取人工兑换详情失败',
|
errorMessage: '读取人工兑换详情失败',
|
||||||
@@ -72,6 +78,7 @@ router.post('/manual-redeem/:taskId/session', createJsonHandler(
|
|||||||
(req) => createAdminManualRedeemSession(
|
(req) => createAdminManualRedeemSession(
|
||||||
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
|
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
|
||||||
/** @type {{ loginType?: string, forceRecreate?: boolean }} */ (req.body),
|
/** @type {{ loginType?: string, forceRecreate?: boolean }} */ (req.body),
|
||||||
|
req.adminSession || null,
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
successMessage: '人工兑换登录会话已创建',
|
successMessage: '人工兑换登录会话已创建',
|
||||||
@@ -81,7 +88,10 @@ router.post('/manual-redeem/:taskId/session', createJsonHandler(
|
|||||||
))
|
))
|
||||||
|
|
||||||
router.get('/manual-redeem/:taskId/session/summary', createJsonHandler(
|
router.get('/manual-redeem/:taskId/session/summary', createJsonHandler(
|
||||||
(req) => getAdminManualRedeemSessionSummary((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
|
(req) => getAdminManualRedeemSessionSummary(
|
||||||
|
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
|
||||||
|
req.adminSession || null,
|
||||||
|
),
|
||||||
{
|
{
|
||||||
successMessage: 'ok',
|
successMessage: 'ok',
|
||||||
errorMessage: '读取人工兑换会话摘要失败',
|
errorMessage: '读取人工兑换会话摘要失败',
|
||||||
@@ -90,7 +100,10 @@ router.get('/manual-redeem/:taskId/session/summary', createJsonHandler(
|
|||||||
))
|
))
|
||||||
|
|
||||||
router.post('/manual-redeem/:taskId/session/refresh', createJsonHandler(
|
router.post('/manual-redeem/:taskId/session/refresh', createJsonHandler(
|
||||||
(req) => reloadAdminManualRedeemSession((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
|
(req) => reloadAdminManualRedeemSession(
|
||||||
|
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
|
||||||
|
req.adminSession || null,
|
||||||
|
),
|
||||||
{
|
{
|
||||||
successMessage: '人工兑换会话已刷新',
|
successMessage: '人工兑换会话已刷新',
|
||||||
errorMessage: '刷新人工兑换会话失败',
|
errorMessage: '刷新人工兑换会话失败',
|
||||||
@@ -99,7 +112,10 @@ router.post('/manual-redeem/:taskId/session/refresh', createJsonHandler(
|
|||||||
))
|
))
|
||||||
|
|
||||||
router.delete('/manual-redeem/:taskId/session', createJsonHandler(
|
router.delete('/manual-redeem/:taskId/session', createJsonHandler(
|
||||||
(req) => closeAdminManualRedeemSession((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
|
(req) => closeAdminManualRedeemSession(
|
||||||
|
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
|
||||||
|
req.adminSession || null,
|
||||||
|
),
|
||||||
{
|
{
|
||||||
successMessage: '人工兑换会话已关闭',
|
successMessage: '人工兑换会话已关闭',
|
||||||
errorMessage: '关闭人工兑换会话失败',
|
errorMessage: '关闭人工兑换会话失败',
|
||||||
@@ -159,7 +175,10 @@ router.post('/manual-redeem/:taskId/redeem', createJsonHandler(
|
|||||||
))
|
))
|
||||||
|
|
||||||
router.post('/manual-redeem/:taskId/close', createJsonHandler(
|
router.post('/manual-redeem/:taskId/close', createJsonHandler(
|
||||||
(req) => closeAdminManualRedeemTask((/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId),
|
(req) => closeAdminManualRedeemTask(
|
||||||
|
(/** @type {AdminManualRedeemRouteParams} */ (req.params)).taskId,
|
||||||
|
req.adminSession || null,
|
||||||
|
),
|
||||||
{
|
{
|
||||||
successMessage: '人工兑换任务已关闭',
|
successMessage: '人工兑换任务已关闭',
|
||||||
errorMessage: '关闭人工兑换任务失败',
|
errorMessage: '关闭人工兑换任务失败',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
createManagedAdminUser,
|
createManagedAdminUser,
|
||||||
getAdminUserList,
|
getAdminUserList,
|
||||||
resetManagedAdminUserPassword,
|
resetManagedAdminUserPassword,
|
||||||
|
updateManagedAdminUserInventoryGroups,
|
||||||
updateManagedAdminUserRole,
|
updateManagedAdminUserRole,
|
||||||
updateManagedAdminUserStatus,
|
updateManagedAdminUserStatus,
|
||||||
} from '../../services/admin/admin-auth-service.js'
|
} from '../../services/admin/admin-auth-service.js'
|
||||||
@@ -77,6 +78,24 @@ router.post('/users/:userId/status', createJsonHandler(
|
|||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
|
||||||
|
router.post('/users/:userId/inventory-groups', createJsonHandler(
|
||||||
|
(req) => updateManagedAdminUserInventoryGroups(req.params.userId, req.body, req.adminSession),
|
||||||
|
{
|
||||||
|
successMessage: '用户库存组已更新',
|
||||||
|
errorMessage: '更新用户库存组失败',
|
||||||
|
scope: '[admin/users/:userId/inventory-groups]',
|
||||||
|
audit: (_req, data) => ({
|
||||||
|
action: 'admin_user_inventory_groups_updated',
|
||||||
|
targetType: 'admin_user',
|
||||||
|
targetId: String(data.user.userId),
|
||||||
|
data: {
|
||||||
|
username: data.user.username,
|
||||||
|
inventoryGroupCodes: data.user.inventoryGroupCodes,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
|
||||||
router.post('/users/:userId/reset-password', createJsonHandler(
|
router.post('/users/:userId/reset-password', createJsonHandler(
|
||||||
(req) => resetManagedAdminUserPassword(req.params.userId, req.body),
|
(req) => resetManagedAdminUserPassword(req.params.userId, req.body),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
getAdminUserById,
|
getAdminUserById,
|
||||||
getAdminUserByUsername,
|
getAdminUserByUsername,
|
||||||
listAdminUsers,
|
listAdminUsers,
|
||||||
|
replaceAdminUserInventoryGroupBindings,
|
||||||
updateAdminUser,
|
updateAdminUser,
|
||||||
} from '../../repositories/admin-user-repo.js'
|
} from '../../repositories/admin-user-repo.js'
|
||||||
import { addHours, nowIso } from '../../utils/time.js'
|
import { addHours, nowIso } from '../../utils/time.js'
|
||||||
@@ -127,6 +128,7 @@ export async function verifyAdminSessionToken(token) {
|
|||||||
userId: Number(user.id),
|
userId: Number(user.id),
|
||||||
username: String(user.username || ''),
|
username: String(user.username || ''),
|
||||||
role: normalizeAdminRole(user.role),
|
role: normalizeAdminRole(user.role),
|
||||||
|
allowedInventoryGroups: normalizeInventoryGroupCodes(user.inventory_group_codes),
|
||||||
expiresAt,
|
expiresAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,6 +143,7 @@ export async function getAdminSessionSummary(token) {
|
|||||||
userId: session.userId,
|
userId: session.userId,
|
||||||
username: session.username,
|
username: session.username,
|
||||||
role: session.role,
|
role: session.role,
|
||||||
|
inventoryGroupCodes: session.allowedInventoryGroups,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,6 +219,18 @@ export async function createManagedAdminUser(payload = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateManagedAdminUserInventoryGroups(userId, payload = {}, session) {
|
||||||
|
const user = await getRequiredAdminUser(userId)
|
||||||
|
const inventoryGroupCodes = normalizeInventoryGroupCodes(payload.inventoryGroupCodes)
|
||||||
|
|
||||||
|
await ensureAdminUserChangeAllowed(user, {}, session)
|
||||||
|
const updated = await replaceAdminUserInventoryGroupBindings(user.id, inventoryGroupCodes, nowIso())
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: mapAdminUser(updated),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateManagedAdminUserRole(userId, payload = {}, session) {
|
export async function updateManagedAdminUserRole(userId, payload = {}, session) {
|
||||||
const user = await getRequiredAdminUser(userId)
|
const user = await getRequiredAdminUser(userId)
|
||||||
const role = normalizeAdminRole(payload.role)
|
const role = normalizeAdminRole(payload.role)
|
||||||
@@ -315,6 +330,7 @@ function createAdminSession(user) {
|
|||||||
userId: Number(user.id),
|
userId: Number(user.id),
|
||||||
username: String(user.username || ''),
|
username: String(user.username || ''),
|
||||||
role: normalizeAdminRole(user.role),
|
role: normalizeAdminRole(user.role),
|
||||||
|
inventoryGroupCodes: normalizeInventoryGroupCodes(user.inventory_group_codes),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,7 +461,14 @@ function mapAdminUser(user) {
|
|||||||
username: String(user.username || ''),
|
username: String(user.username || ''),
|
||||||
role: normalizeAdminRole(user.role),
|
role: normalizeAdminRole(user.role),
|
||||||
status: normalizeAdminUserStatus(user.status),
|
status: normalizeAdminUserStatus(user.status),
|
||||||
|
inventoryGroupCodes: normalizeInventoryGroupCodes(user.inventory_group_codes),
|
||||||
createdAt: user.created_at,
|
createdAt: user.created_at,
|
||||||
updatedAt: user.updated_at,
|
updatedAt: user.updated_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeInventoryGroupCodes(values) {
|
||||||
|
return Array.from(new Set((Array.isArray(values) ? values : [])
|
||||||
|
.map((value) => String(value || '').trim())
|
||||||
|
.filter(Boolean)))
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export async function mapAdminInventoryListItem(item) {
|
|||||||
skuCode: item.sku_code,
|
skuCode: item.sku_code,
|
||||||
batchNo: item.batch_no,
|
batchNo: item.batch_no,
|
||||||
credentialType: item.credential_type || 'tencent_code',
|
credentialType: item.credential_type || 'tencent_code',
|
||||||
|
inventoryGroupCode: String(item.inventory_group_code || '').trim(),
|
||||||
displayValue: item.display_value,
|
displayValue: item.display_value,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
reservedByTaskId: item.reserved_by_task_id,
|
reservedByTaskId: item.reserved_by_task_id,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { createTaskEvent } from '../../repositories/task-event-repo.js'
|
|||||||
import { nowIso } from '../../utils/time.js'
|
import { nowIso } from '../../utils/time.js'
|
||||||
import { randomId } from '../../utils/random.js'
|
import { randomId } from '../../utils/random.js'
|
||||||
import { closeAdminTask, confirmAdminTaskAssistedRole, redeemAdminTaskAssisted } from './admin-write-service.js'
|
import { closeAdminTask, confirmAdminTaskAssistedRole, redeemAdminTaskAssisted } from './admin-write-service.js'
|
||||||
import { isAssistedClaimTask, parseTaskContext } from './admin-read-shared-helpers.js'
|
import { createAdminViewerContext, isAssistedClaimTask, parseTaskContext } from './admin-read-shared-helpers.js'
|
||||||
|
|
||||||
const MANUAL_PROVIDER = 'manual'
|
const MANUAL_PROVIDER = 'manual'
|
||||||
const MANUAL_PLATFORM = 'manual_redeem'
|
const MANUAL_PLATFORM = 'manual_redeem'
|
||||||
@@ -36,6 +36,7 @@ export async function createAdminManualRedeemTask(
|
|||||||
payload = /** @type {AdminManualRedeemCreateInput} */ ({}),
|
payload = /** @type {AdminManualRedeemCreateInput} */ ({}),
|
||||||
session = null,
|
session = null,
|
||||||
) {
|
) {
|
||||||
|
const viewerContext = createAdminViewerContext(session)
|
||||||
const proofValue = normalizeManualProofValue(payload.proofValue)
|
const proofValue = normalizeManualProofValue(payload.proofValue)
|
||||||
const skuCode = String(payload.skuCode || '').trim()
|
const skuCode = String(payload.skuCode || '').trim()
|
||||||
const skuName = String(payload.skuName || '').trim() || skuCode
|
const skuName = String(payload.skuName || '').trim() || skuCode
|
||||||
@@ -81,9 +82,20 @@ export async function createAdminManualRedeemTask(
|
|||||||
const primaryRequirement = requirements.find((item) => item.is_required !== false) || requirements[0] || null
|
const primaryRequirement = requirements.find((item) => item.is_required !== false) || requirements[0] || null
|
||||||
const credentialType = String(primaryRequirement?.credential_type || primaryRequirement?.credentialType || 'tencent_code').trim() || 'tencent_code'
|
const credentialType = String(primaryRequirement?.credential_type || primaryRequirement?.credentialType || 'tencent_code').trim() || 'tencent_code'
|
||||||
const roleKey = String(primaryRequirement?.role_key || primaryRequirement?.roleKey || 'primary_code').trim() || 'primary_code'
|
const roleKey = String(primaryRequirement?.role_key || primaryRequirement?.roleKey || 'primary_code').trim() || 'primary_code'
|
||||||
const availableInventory = await findFirstAvailableInventoryItemBySkuCode(skuCode, credentialType)
|
const availableInventory = await findFirstAvailableInventoryItemBySkuCode(
|
||||||
|
skuCode,
|
||||||
|
credentialType,
|
||||||
|
viewerContext.allowedInventoryGroupCodes,
|
||||||
|
)
|
||||||
|
|
||||||
if (!availableInventory) {
|
if (!availableInventory) {
|
||||||
|
if (viewerContext.restrictInventoryGroups && viewerContext.allowedInventoryGroupCodes?.length === 0) {
|
||||||
|
throw createHttpError('当前客服未绑定任何库存组,无法创建人工兑换任务', {
|
||||||
|
statusCode: 403,
|
||||||
|
errorCode: 'admin_manual_redeem_inventory_group_unbound',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
throw createHttpError('当前 SKU 没有可用库存,无法创建人工兑换任务', {
|
throw createHttpError('当前 SKU 没有可用库存,无法创建人工兑换任务', {
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
errorCode: 'admin_manual_redeem_inventory_unavailable',
|
errorCode: 'admin_manual_redeem_inventory_unavailable',
|
||||||
@@ -208,6 +220,7 @@ export async function createAdminManualRedeemTask(
|
|||||||
credentialType,
|
credentialType,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
inventoryGroupCode: String(availableInventory.inventory_group_code || '').trim(),
|
||||||
manualRedeem: manualContext,
|
manualRedeem: manualContext,
|
||||||
}),
|
}),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
@@ -226,6 +239,7 @@ export async function createAdminManualRedeemTask(
|
|||||||
taskId: createdTask.id,
|
taskId: createdTask.id,
|
||||||
credentialType,
|
credentialType,
|
||||||
roleKey,
|
roleKey,
|
||||||
|
inventoryGroupCodes: viewerContext.allowedInventoryGroupCodes,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!reservedInventory) {
|
if (!reservedInventory) {
|
||||||
@@ -250,6 +264,10 @@ export async function createAdminManualRedeemTask(
|
|||||||
claim_expires_at: claimToken.expired_at,
|
claim_expires_at: claimToken.expired_at,
|
||||||
user_action_status: 'pending_claim',
|
user_action_status: 'pending_claim',
|
||||||
last_error: '',
|
last_error: '',
|
||||||
|
context_json: JSON.stringify({
|
||||||
|
...parseTaskContext(createdTask),
|
||||||
|
inventoryGroupCode: String(reservedInventory.inventory_group_code || '').trim(),
|
||||||
|
}),
|
||||||
updated_at: nowIso(),
|
updated_at: nowIso(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -259,73 +277,80 @@ export async function createAdminManualRedeemTask(
|
|||||||
skuCode,
|
skuCode,
|
||||||
skuName,
|
skuName,
|
||||||
inventoryItemId: Number(reservedInventory.id || 0) || null,
|
inventoryItemId: Number(reservedInventory.id || 0) || null,
|
||||||
|
inventoryGroupCode: String(reservedInventory.inventory_group_code || '').trim(),
|
||||||
createdBy: manualContext.createdBy,
|
createdBy: manualContext.createdBy,
|
||||||
remark,
|
remark,
|
||||||
}, nowIso())
|
}, nowIso())
|
||||||
|
|
||||||
return getAdminManualRedeemDetail(createdTask.id)
|
return getAdminManualRedeemDetail(createdTask.id, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {AdminEntityIdInput} taskId */
|
/** @param {AdminEntityIdInput} taskId */
|
||||||
export async function getAdminManualRedeemDetail(taskId) {
|
/** @param {AdminViewerSessionInput | null} [session] */
|
||||||
const task = await getRequiredManualRedeemTask(taskId)
|
export async function getAdminManualRedeemDetail(taskId, session = null) {
|
||||||
|
const task = await getRequiredManualRedeemTask(taskId, session)
|
||||||
const detail = await getClaimDetailForAdminTask(task.id)
|
const detail = await getClaimDetailForAdminTask(task.id)
|
||||||
return decorateManualRedeemDetail(task, detail)
|
return decorateManualRedeemDetail(task, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {AdminEntityIdInput} taskId */
|
/** @param {AdminEntityIdInput} taskId */
|
||||||
/** @param {{ loginType?: string, forceRecreate?: boolean }} [payload] */
|
/** @param {{ loginType?: string, forceRecreate?: boolean }} [payload] */
|
||||||
export async function createAdminManualRedeemSession(taskId, payload = {}) {
|
/** @param {AdminViewerSessionInput | null} [session] */
|
||||||
const task = await getRequiredManualRedeemTask(taskId)
|
export async function createAdminManualRedeemSession(taskId, payload = {}, session = null) {
|
||||||
|
const task = await getRequiredManualRedeemTask(taskId, session)
|
||||||
const detail = await createClaimSessionForAdminTask(task.id, payload)
|
const detail = await createClaimSessionForAdminTask(task.id, payload)
|
||||||
return decorateManualRedeemDetail(task, detail)
|
return decorateManualRedeemDetail(task, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {AdminEntityIdInput} taskId */
|
/** @param {AdminEntityIdInput} taskId */
|
||||||
export async function getAdminManualRedeemSessionSummary(taskId) {
|
/** @param {AdminViewerSessionInput | null} [session] */
|
||||||
const task = await getRequiredManualRedeemTask(taskId)
|
export async function getAdminManualRedeemSessionSummary(taskId, session = null) {
|
||||||
|
const task = await getRequiredManualRedeemTask(taskId, session)
|
||||||
const detail = await getClaimSessionSummaryForAdminTask(task.id)
|
const detail = await getClaimSessionSummaryForAdminTask(task.id)
|
||||||
return decorateManualRedeemDetail(task, detail)
|
return decorateManualRedeemDetail(task, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {AdminEntityIdInput} taskId */
|
/** @param {AdminEntityIdInput} taskId */
|
||||||
export async function reloadAdminManualRedeemSession(taskId) {
|
/** @param {AdminViewerSessionInput | null} [session] */
|
||||||
const task = await getRequiredManualRedeemTask(taskId)
|
export async function reloadAdminManualRedeemSession(taskId, session = null) {
|
||||||
|
const task = await getRequiredManualRedeemTask(taskId, session)
|
||||||
const detail = await reloadClaimSessionForAdminTask(task.id)
|
const detail = await reloadClaimSessionForAdminTask(task.id)
|
||||||
return decorateManualRedeemDetail(task, detail)
|
return decorateManualRedeemDetail(task, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {AdminEntityIdInput} taskId */
|
/** @param {AdminEntityIdInput} taskId */
|
||||||
export async function closeAdminManualRedeemSession(taskId) {
|
/** @param {AdminViewerSessionInput | null} [session] */
|
||||||
const task = await getRequiredManualRedeemTask(taskId)
|
export async function closeAdminManualRedeemSession(taskId, session = null) {
|
||||||
|
const task = await getRequiredManualRedeemTask(taskId, session)
|
||||||
const detail = await closeClaimSessionForAdminTask(task.id)
|
const detail = await closeClaimSessionForAdminTask(task.id)
|
||||||
return decorateManualRedeemDetail(task, detail)
|
return decorateManualRedeemDetail(task, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {AdminEntityIdInput} taskId */
|
/** @param {AdminEntityIdInput} taskId */
|
||||||
export async function closeAdminManualRedeemTask(taskId) {
|
/** @param {AdminViewerSessionInput | null} [session] */
|
||||||
const task = await getRequiredManualRedeemTask(taskId)
|
export async function closeAdminManualRedeemTask(taskId, session = null) {
|
||||||
|
const task = await getRequiredManualRedeemTask(taskId, session)
|
||||||
await closeAdminTask(task.id)
|
await closeAdminTask(task.id)
|
||||||
return getAdminManualRedeemDetail(task.id)
|
return getAdminManualRedeemDetail(task.id, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {AdminEntityIdInput} taskId */
|
/** @param {AdminEntityIdInput} taskId */
|
||||||
/** @param {AdminViewerSessionInput | null} [session] */
|
/** @param {AdminViewerSessionInput | null} [session] */
|
||||||
export async function confirmAdminManualRedeemRole(taskId, session = null) {
|
export async function confirmAdminManualRedeemRole(taskId, session = null) {
|
||||||
const task = await getRequiredManualRedeemTask(taskId)
|
const task = await getRequiredManualRedeemTask(taskId, session)
|
||||||
await confirmAdminTaskAssistedRole(task.id, session)
|
await confirmAdminTaskAssistedRole(task.id, session)
|
||||||
return getAdminManualRedeemDetail(task.id)
|
return getAdminManualRedeemDetail(task.id, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {AdminEntityIdInput} taskId */
|
/** @param {AdminEntityIdInput} taskId */
|
||||||
/** @param {AdminViewerSessionInput | null} [session] */
|
/** @param {AdminViewerSessionInput | null} [session] */
|
||||||
export async function redeemAdminManualRedeemTask(taskId, session = null) {
|
export async function redeemAdminManualRedeemTask(taskId, session = null) {
|
||||||
const task = await getRequiredManualRedeemTask(taskId)
|
const task = await getRequiredManualRedeemTask(taskId, session)
|
||||||
await redeemAdminTaskAssisted(task.id, session)
|
await redeemAdminTaskAssisted(task.id, session)
|
||||||
return getAdminManualRedeemDetail(task.id)
|
return getAdminManualRedeemDetail(task.id, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRequiredManualRedeemTask(taskId) {
|
async function getRequiredManualRedeemTask(taskId, session = null) {
|
||||||
const task = await getTaskById(Number(taskId))
|
const task = await getTaskById(Number(taskId))
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
@@ -345,6 +370,25 @@ async function getRequiredManualRedeemTask(taskId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewerContext = createAdminViewerContext(session)
|
||||||
|
if (viewerContext.restrictInventoryGroups) {
|
||||||
|
if (!Array.isArray(viewerContext.allowedInventoryGroupCodes) || viewerContext.allowedInventoryGroupCodes.length === 0) {
|
||||||
|
throw createHttpError('当前客服未绑定任何库存组,无法操作人工兑换任务', {
|
||||||
|
statusCode: 403,
|
||||||
|
errorCode: 'admin_manual_redeem_inventory_group_unbound',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const inventoryGroupCode = String(taskContext?.inventoryGroupCode || '').trim()
|
||||||
|
|
||||||
|
if (inventoryGroupCode && !viewerContext.allowedInventoryGroupCodes.includes(inventoryGroupCode)) {
|
||||||
|
throw createHttpError('当前客服无权操作该库存组下的人工兑换任务', {
|
||||||
|
statusCode: 403,
|
||||||
|
errorCode: 'admin_manual_redeem_inventory_group_denied',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
import { buildClaimUrl } from '../claim/claim-service.js'
|
import { buildClaimUrl } from '../claim/claim-service.js'
|
||||||
import { getClaimTokenById } from '../../repositories/claim-token-repo.js'
|
import { getClaimTokenById } from '../../repositories/claim-token-repo.js'
|
||||||
import { getInventoryItemById, listInventoryItems, listInventorySkuSuggestions } from '../../repositories/inventory-repo.js'
|
import {
|
||||||
|
getInventoryItemById,
|
||||||
|
listInventoryItems,
|
||||||
|
listInventorySkuSuggestions,
|
||||||
|
} from '../../repositories/inventory-repo.js'
|
||||||
import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js'
|
import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js'
|
||||||
import { getOrderById, listOrders } from '../../repositories/order-repo.js'
|
import { getOrderById, listOrders } from '../../repositories/order-repo.js'
|
||||||
import { getTaskById, listTasks, listTasksByOrderId } from '../../repositories/task-repo.js'
|
import { getTaskById, listTasks, listTasksByOrderId } from '../../repositories/task-repo.js'
|
||||||
@@ -313,9 +317,11 @@ export async function getAdminTaskScreenshotPath(taskId, session = null) {
|
|||||||
|
|
||||||
/** @returns {Promise<AdminInventoryListResponse>} */
|
/** @returns {Promise<AdminInventoryListResponse>} */
|
||||||
/** @param {AdminInventoryListQueryInput} [query] */
|
/** @param {AdminInventoryListQueryInput} [query] */
|
||||||
export async function getAdminInventoryItems(query = /** @type {AdminInventoryListQueryInput} */ ({})) {
|
/** @param {AdminViewerSessionInput | null} [session] */
|
||||||
|
export async function getAdminInventoryItems(query = /** @type {AdminInventoryListQueryInput} */ ({}), session = null) {
|
||||||
const page = normalizePage(query.page)
|
const page = normalizePage(query.page)
|
||||||
const pageSize = normalizePageSize(query.pageSize)
|
const pageSize = normalizePageSize(query.pageSize)
|
||||||
|
const viewerContext = createAdminViewerContext(session)
|
||||||
const { items, total } = await listInventoryItems({
|
const { items, total } = await listInventoryItems({
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -323,6 +329,8 @@ export async function getAdminInventoryItems(query = /** @type {AdminInventoryLi
|
|||||||
credentialType: String(query.credentialType || '').trim(),
|
credentialType: String(query.credentialType || '').trim(),
|
||||||
status: String(query.status || '').trim(),
|
status: String(query.status || '').trim(),
|
||||||
batchNo: String(query.batchNo || '').trim(),
|
batchNo: String(query.batchNo || '').trim(),
|
||||||
|
inventoryGroupCode: String(query.inventoryGroupCode || '').trim(),
|
||||||
|
allowedInventoryGroupCodes: viewerContext.allowedInventoryGroupCodes,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -335,9 +343,12 @@ export async function getAdminInventoryItems(query = /** @type {AdminInventoryLi
|
|||||||
/** @param {AdminInventorySkuSuggestionQueryInput} [query] */
|
/** @param {AdminInventorySkuSuggestionQueryInput} [query] */
|
||||||
export async function getAdminInventorySkuSuggestions(
|
export async function getAdminInventorySkuSuggestions(
|
||||||
query = /** @type {AdminInventorySkuSuggestionQueryInput} */ ({}),
|
query = /** @type {AdminInventorySkuSuggestionQueryInput} */ ({}),
|
||||||
|
session = null,
|
||||||
) {
|
) {
|
||||||
|
const viewerContext = createAdminViewerContext(session)
|
||||||
const credentialType = String(query.credentialType || '').trim()
|
const credentialType = String(query.credentialType || '').trim()
|
||||||
const keyword = String(query.keyword || '').trim()
|
const keyword = String(query.keyword || '').trim()
|
||||||
|
const inventoryGroupCode = String(query.inventoryGroupCode || '').trim()
|
||||||
const requestedLimit = Number(query.limit)
|
const requestedLimit = Number(query.limit)
|
||||||
const limit = Number.isFinite(requestedLimit) && requestedLimit > 0
|
const limit = Number.isFinite(requestedLimit) && requestedLimit > 0
|
||||||
? Math.min(100, Math.floor(requestedLimit))
|
? Math.min(100, Math.floor(requestedLimit))
|
||||||
@@ -346,12 +357,15 @@ export async function getAdminInventorySkuSuggestions(
|
|||||||
credentialType,
|
credentialType,
|
||||||
keyword,
|
keyword,
|
||||||
limit,
|
limit,
|
||||||
|
inventoryGroupCode,
|
||||||
|
allowedInventoryGroupCodes: viewerContext.allowedInventoryGroupCodes,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: items.map((item) => ({
|
items: items.map((item) => ({
|
||||||
skuCode: String(item.sku_code || '').trim(),
|
skuCode: String(item.sku_code || '').trim(),
|
||||||
credentialType: String(item.credential_type || '').trim() || 'tencent_code',
|
credentialType: String(item.credential_type || '').trim() || 'tencent_code',
|
||||||
|
inventoryGroupCode: String(item.inventory_group_code || '').trim(),
|
||||||
totalCount: Number(item.total_count || 0),
|
totalCount: Number(item.total_count || 0),
|
||||||
availableCount: Number(item.available_count || 0),
|
availableCount: Number(item.available_count || 0),
|
||||||
latestUpdatedAt: item.latest_updated_at || null,
|
latestUpdatedAt: item.latest_updated_at || null,
|
||||||
|
|||||||
@@ -57,9 +57,14 @@ export function getTaskPrimaryClaimTokenId(task) {
|
|||||||
|
|
||||||
export function createAdminViewerContext(session = null) {
|
export function createAdminViewerContext(session = null) {
|
||||||
const role = normalizeAdminRole(session?.role)
|
const role = normalizeAdminRole(session?.role)
|
||||||
|
const allowedInventoryGroupCodes = role === 'support'
|
||||||
|
? normalizeInventoryGroupCodes(session?.allowedInventoryGroups)
|
||||||
|
: null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
role,
|
role,
|
||||||
|
allowedInventoryGroupCodes,
|
||||||
|
restrictInventoryGroups: role === 'support',
|
||||||
canViewSensitiveTaskData: role === 'admin' || role === 'operator',
|
canViewSensitiveTaskData: role === 'admin' || role === 'operator',
|
||||||
canManageSensitiveInventory: role === 'admin',
|
canManageSensitiveInventory: role === 'admin',
|
||||||
canManageTaskLifecycle: role === 'admin' || role === 'operator',
|
canManageTaskLifecycle: role === 'admin' || role === 'operator',
|
||||||
@@ -200,11 +205,13 @@ export function canRegenerateClaimLinkForViewer(task, viewerContext) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return viewerContext.canOperateAssistedTask && isAssistedClaimTask(task)
|
return viewerContext.canOperateAssistedTask
|
||||||
|
&& isAssistedClaimTask(task)
|
||||||
|
&& canViewerAccessTaskInventoryGroup(task, viewerContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canViewerConfirmAssistedRole(task, viewerContext) {
|
export function canViewerConfirmAssistedRole(task, viewerContext) {
|
||||||
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task)) {
|
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,13 +219,42 @@ export function canViewerConfirmAssistedRole(task, viewerContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function canViewerRedeemAssistedTask(task, viewerContext) {
|
export function canViewerRedeemAssistedTask(task, viewerContext) {
|
||||||
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task)) {
|
if (!viewerContext.canOperateAssistedTask || !isAssistedClaimTask(task) || !canViewerAccessTaskInventoryGroup(task, viewerContext)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['role_confirmed', 'redeeming'].includes(String(task?.task_status || '').trim())
|
return ['role_confirmed', 'redeeming'].includes(String(task?.task_status || '').trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canViewerAccessTaskInventoryGroup(task, viewerContext = createAdminViewerContext()) {
|
||||||
|
if (!viewerContext.restrictInventoryGroups) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedInventoryGroupCodes = Array.isArray(viewerContext.allowedInventoryGroupCodes)
|
||||||
|
? viewerContext.allowedInventoryGroupCodes
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (allowedInventoryGroupCodes.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskContext = parseTaskContext(task)
|
||||||
|
const inventoryGroupCode = String(taskContext?.inventoryGroupCode || '').trim()
|
||||||
|
|
||||||
|
if (!inventoryGroupCode) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowedInventoryGroupCodes.includes(inventoryGroupCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeInventoryGroupCodes(values) {
|
||||||
|
return Array.from(new Set((Array.isArray(values) ? values : [])
|
||||||
|
.map((value) => String(value || '').trim())
|
||||||
|
.filter(Boolean)))
|
||||||
|
}
|
||||||
|
|
||||||
function mapRedeemResolutionAttempt(value) {
|
function mapRedeemResolutionAttempt(value) {
|
||||||
if (!value || typeof value !== 'object') {
|
if (!value || typeof value !== 'object') {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import test from 'node:test'
|
|||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
canViewerConfirmAssistedRole,
|
||||||
canViewerCloseTask,
|
canViewerCloseTask,
|
||||||
createAdminViewerContext,
|
createAdminViewerContext,
|
||||||
resolveAdminTaskScreenshotUrl,
|
resolveAdminTaskScreenshotUrl,
|
||||||
@@ -37,3 +38,18 @@ test('canViewerCloseTask allows support to close active task but not redeemed ta
|
|||||||
assert.equal(canViewerCloseTask({ task_status: 'link_generated' }, viewerContext), true)
|
assert.equal(canViewerCloseTask({ task_status: 'link_generated' }, viewerContext), true)
|
||||||
assert.equal(canViewerCloseTask({ task_status: 'redeemed' }, viewerContext), false)
|
assert.equal(canViewerCloseTask({ task_status: 'redeemed' }, viewerContext), false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('canViewerConfirmAssistedRole blocks support when task inventory group is not bound', () => {
|
||||||
|
const viewerContext = createAdminViewerContext({
|
||||||
|
role: 'support',
|
||||||
|
allowedInventoryGroups: ['A组'],
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(canViewerConfirmAssistedRole({
|
||||||
|
task_status: 'claimed',
|
||||||
|
executor_key: 'tencent_claim_assisted',
|
||||||
|
context_json: {
|
||||||
|
inventoryGroupCode: 'B组',
|
||||||
|
},
|
||||||
|
}, viewerContext), false)
|
||||||
|
})
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export async function createAdminInventoryItem(payload = /** @type {AdminInvento
|
|||||||
const displayValue = String(payload.displayValue || '').trim()
|
const displayValue = String(payload.displayValue || '').trim()
|
||||||
const batchNo = String(payload.batchNo || '').trim()
|
const batchNo = String(payload.batchNo || '').trim()
|
||||||
const credentialType = String(payload.credentialType || 'tencent_code').trim() || 'tencent_code'
|
const credentialType = String(payload.credentialType || 'tencent_code').trim() || 'tencent_code'
|
||||||
|
const inventoryGroupCode = String(payload.inventoryGroupCode || '').trim()
|
||||||
|
|
||||||
if (!skuCode || !displayValue) {
|
if (!skuCode || !displayValue) {
|
||||||
throw createHttpError('缺少 skuCode 或 displayValue', {
|
throw createHttpError('缺少 skuCode 或 displayValue', {
|
||||||
@@ -79,6 +80,7 @@ export async function createAdminInventoryItem(payload = /** @type {AdminInvento
|
|||||||
displayValue,
|
displayValue,
|
||||||
batchNo,
|
batchNo,
|
||||||
credentialType,
|
credentialType,
|
||||||
|
inventoryGroupCode,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
},
|
},
|
||||||
@@ -93,10 +95,11 @@ export async function createAdminInventoryItem(payload = /** @type {AdminInvento
|
|||||||
|
|
||||||
const { items } = await listInventoryItems({
|
const { items } = await listInventoryItems({
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 1,
|
pageSize: 20,
|
||||||
skuCode,
|
skuCode,
|
||||||
|
inventoryGroupCode,
|
||||||
})
|
})
|
||||||
const createdItem = items.find((item) => item.display_value === displayValue) || null
|
const createdItem = items.find((item) => item.display_value === displayValue && item.inventory_group_code === inventoryGroupCode) || null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inventoryItem: createdItem ? await mapAdminInventoryListItem(createdItem) : null,
|
inventoryItem: createdItem ? await mapAdminInventoryListItem(createdItem) : null,
|
||||||
@@ -121,6 +124,7 @@ export async function importAdminInventoryItems(payload = /** @type {AdminInvent
|
|||||||
batchNo: row.batchNo,
|
batchNo: row.batchNo,
|
||||||
displayValue: row.displayValue,
|
displayValue: row.displayValue,
|
||||||
credentialType: row.credentialType,
|
credentialType: row.credentialType,
|
||||||
|
inventoryGroupCode: row.inventoryGroupCode,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
}))
|
}))
|
||||||
@@ -538,6 +542,7 @@ export async function retryAdminTask(taskId) {
|
|||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
credentialType: primaryRequirement?.credentialType || 'tencent_code',
|
credentialType: primaryRequirement?.credentialType || 'tencent_code',
|
||||||
roleKey: primaryRequirement?.roleKey || 'primary_code',
|
roleKey: primaryRequirement?.roleKey || 'primary_code',
|
||||||
|
inventoryGroupCodes: resolveTaskInventoryGroupCodes(task),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!reserved) {
|
if (!reserved) {
|
||||||
@@ -672,7 +677,7 @@ export async function completeAdminTaskManualDispatch(taskId, payload = {}, sess
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AdminInventoryImportInput} payload
|
* @param {AdminInventoryImportInput} payload
|
||||||
* @returns {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType'>>>}
|
* @returns {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType' | 'inventoryGroupCode'>>>}
|
||||||
*/
|
*/
|
||||||
function normalizeInventoryImportRows(payload) {
|
function normalizeInventoryImportRows(payload) {
|
||||||
const rows = []
|
const rows = []
|
||||||
@@ -685,12 +690,13 @@ function normalizeInventoryImportRows(payload) {
|
|||||||
const displayValue = String(row?.displayValue || '').trim()
|
const displayValue = String(row?.displayValue || '').trim()
|
||||||
const batchNo = String(row?.batchNo || '').trim()
|
const batchNo = String(row?.batchNo || '').trim()
|
||||||
const credentialType = String(row?.credentialType || payload.credentialType || 'tencent_code').trim() || 'tencent_code'
|
const credentialType = String(row?.credentialType || payload.credentialType || 'tencent_code').trim() || 'tencent_code'
|
||||||
|
const inventoryGroupCode = String(row?.inventoryGroupCode || payload.inventoryGroupCode || '').trim()
|
||||||
|
|
||||||
if (!skuCode || !displayValue) {
|
if (!skuCode || !displayValue) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.push({ skuCode, displayValue, batchNo, credentialType })
|
rows.push({ skuCode, displayValue, batchNo, credentialType, inventoryGroupCode })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,6 +704,7 @@ function normalizeInventoryImportRows(payload) {
|
|||||||
const skuCode = String(payload.skuCode || '').trim()
|
const skuCode = String(payload.skuCode || '').trim()
|
||||||
const batchNo = String(payload.batchNo || '').trim()
|
const batchNo = String(payload.batchNo || '').trim()
|
||||||
const credentialType = String(payload.credentialType || 'tencent_code').trim() || 'tencent_code'
|
const credentialType = String(payload.credentialType || 'tencent_code').trim() || 'tencent_code'
|
||||||
|
const inventoryGroupCode = String(payload.inventoryGroupCode || '').trim()
|
||||||
|
|
||||||
if (!skuCode) {
|
if (!skuCode) {
|
||||||
throw createHttpError('批量导入时缺少 skuCode', {
|
throw createHttpError('批量导入时缺少 skuCode', {
|
||||||
@@ -711,7 +718,7 @@ function normalizeInventoryImportRows(payload) {
|
|||||||
if (!displayValue) {
|
if (!displayValue) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rows.push({ skuCode, displayValue, batchNo, credentialType })
|
rows.push({ skuCode, displayValue, batchNo, credentialType, inventoryGroupCode })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,7 +726,7 @@ function normalizeInventoryImportRows(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType'>>>} rows
|
* @param {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType' | 'inventoryGroupCode'>>>} rows
|
||||||
*/
|
*/
|
||||||
function dedupeRows(rows) {
|
function dedupeRows(rows) {
|
||||||
const seen = new Set()
|
const seen = new Set()
|
||||||
@@ -737,6 +744,12 @@ function dedupeRows(rows) {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveTaskInventoryGroupCodes(task) {
|
||||||
|
const taskContext = parseTaskContext(task)
|
||||||
|
const inventoryGroupCode = String(taskContext?.inventoryGroupCode || '').trim()
|
||||||
|
return inventoryGroupCode ? [inventoryGroupCode] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Awaited<ReturnType<typeof getRequiredTask>>} task
|
* @param {Awaited<ReturnType<typeof getRequiredTask>>} task
|
||||||
|
|||||||
@@ -964,6 +964,9 @@ export async function redeemClaimTaskWithInventoryFallbackWithDeps(
|
|||||||
taskId: context.task.id,
|
taskId: context.task.id,
|
||||||
credentialType: currentInventoryItem.credential_type || 'tencent_code',
|
credentialType: currentInventoryItem.credential_type || 'tencent_code',
|
||||||
roleKey: 'primary_code',
|
roleKey: 'primary_code',
|
||||||
|
inventoryGroupCodes: currentInventoryItem.inventory_group_code
|
||||||
|
? [String(currentInventoryItem.inventory_group_code).trim()]
|
||||||
|
: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!replacement || !String(replacement.display_value || '').trim()) {
|
if (!replacement || !String(replacement.display_value || '').trim()) {
|
||||||
@@ -989,6 +992,7 @@ export async function redeemClaimTaskWithInventoryFallbackWithDeps(
|
|||||||
nextInventoryItemId: replacement.id,
|
nextInventoryItemId: replacement.id,
|
||||||
nextCodeMasked: maskCode(replacement.display_value),
|
nextCodeMasked: maskCode(replacement.display_value),
|
||||||
credentialType: String(replacement.credential_type || currentInventoryItem.credential_type || ''),
|
credentialType: String(replacement.credential_type || currentInventoryItem.credential_type || ''),
|
||||||
|
inventoryGroupCode: String(replacement.inventory_group_code || currentInventoryItem.inventory_group_code || '').trim(),
|
||||||
}, replacedAt)
|
}, replacedAt)
|
||||||
currentInventoryItem = replacement
|
currentInventoryItem = replacement
|
||||||
shouldReloadBeforeNextAttempt = true
|
shouldReloadBeforeNextAttempt = true
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ test('redeemClaimTaskWithInventoryFallbackWithDeps invalidates missing CDKEY and
|
|||||||
id: 1,
|
id: 1,
|
||||||
display_value: 'DJQFf7HgONCL4XD4EH',
|
display_value: 'DJQFf7HgONCL4XD4EH',
|
||||||
credential_type: 'tencent_code',
|
credential_type: 'tencent_code',
|
||||||
|
inventory_group_code: 'A组',
|
||||||
}
|
}
|
||||||
const replacementInventoryItem = {
|
const replacementInventoryItem = {
|
||||||
id: 2,
|
id: 2,
|
||||||
display_value: 'ABCD1234EFGH5678',
|
display_value: 'ABCD1234EFGH5678',
|
||||||
credential_type: 'tencent_code',
|
credential_type: 'tencent_code',
|
||||||
|
inventory_group_code: 'A组',
|
||||||
}
|
}
|
||||||
|
|
||||||
let redeemCount = 0
|
let redeemCount = 0
|
||||||
@@ -127,6 +129,7 @@ test('redeemClaimTaskWithInventoryFallbackWithDeps invalidates missing CDKEY and
|
|||||||
taskId: 42,
|
taskId: 42,
|
||||||
credentialType: 'tencent_code',
|
credentialType: 'tencent_code',
|
||||||
roleKey: 'primary_code',
|
roleKey: 'primary_code',
|
||||||
|
inventoryGroupCodes: ['A组'],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
assert.deepEqual(reloadCalls, ['txbs-test'])
|
assert.deepEqual(reloadCalls, ['txbs-test'])
|
||||||
@@ -160,6 +163,7 @@ test('redeemClaimTaskWithInventoryFallbackWithDeps invalidates missing CDKEY and
|
|||||||
nextInventoryItemId: 2,
|
nextInventoryItemId: 2,
|
||||||
nextCodeMasked: 'ABCD****5678',
|
nextCodeMasked: 'ABCD****5678',
|
||||||
credentialType: 'tencent_code',
|
credentialType: 'tencent_code',
|
||||||
|
inventoryGroupCode: 'A组',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,9 +5,15 @@ import {
|
|||||||
} from '../../repositories/inventory-repo.js'
|
} from '../../repositories/inventory-repo.js'
|
||||||
import { nowIso } from '../../utils/time.js'
|
import { nowIso } from '../../utils/time.js'
|
||||||
|
|
||||||
export async function reserveInventoryForTask({ skuCode, taskId, credentialType = 'tencent_code', roleKey = 'primary_code' }) {
|
export async function reserveInventoryForTask({
|
||||||
|
skuCode,
|
||||||
|
taskId,
|
||||||
|
credentialType = 'tencent_code',
|
||||||
|
roleKey = 'primary_code',
|
||||||
|
inventoryGroupCodes = null,
|
||||||
|
}) {
|
||||||
return reserveInventoryForTaskWithDeps(
|
return reserveInventoryForTaskWithDeps(
|
||||||
{ skuCode, taskId, credentialType, roleKey },
|
{ skuCode, taskId, credentialType, roleKey, inventoryGroupCodes },
|
||||||
{
|
{
|
||||||
assignReservedInventoryItem,
|
assignReservedInventoryItem,
|
||||||
findFirstAvailableInventoryItemBySkuCode,
|
findFirstAvailableInventoryItemBySkuCode,
|
||||||
@@ -17,7 +23,13 @@ export async function reserveInventoryForTask({ skuCode, taskId, credentialType
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function reserveInventoryForTaskWithDeps(
|
export async function reserveInventoryForTaskWithDeps(
|
||||||
{ skuCode, taskId, credentialType = 'tencent_code', roleKey = 'primary_code' },
|
{
|
||||||
|
skuCode,
|
||||||
|
taskId,
|
||||||
|
credentialType = 'tencent_code',
|
||||||
|
roleKey = 'primary_code',
|
||||||
|
inventoryGroupCodes = null,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
assignReservedInventoryItem: assignReserved = assignReservedInventoryItem,
|
assignReservedInventoryItem: assignReserved = assignReservedInventoryItem,
|
||||||
findFirstAvailableInventoryItemBySkuCode: findAvailable = findFirstAvailableInventoryItemBySkuCode,
|
findFirstAvailableInventoryItemBySkuCode: findAvailable = findFirstAvailableInventoryItemBySkuCode,
|
||||||
@@ -28,7 +40,11 @@ export async function reserveInventoryForTaskWithDeps(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const available = await findAvailable(skuCode, credentialType)
|
if (Array.isArray(inventoryGroupCodes) && inventoryGroupCodes.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const available = await findAvailable(skuCode, credentialType, inventoryGroupCodes)
|
||||||
|
|
||||||
if (!available) {
|
if (!available) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ test('reserveInventoryForTaskWithDeps reserves first available inventory item wi
|
|||||||
const result = await reserveInventoryForTaskWithDeps(
|
const result = await reserveInventoryForTaskWithDeps(
|
||||||
{ skuCode: 'dnf-cdk-a', taskId: 88 },
|
{ skuCode: 'dnf-cdk-a', taskId: 88 },
|
||||||
{
|
{
|
||||||
findFirstAvailableInventoryItemBySkuCode: async (skuCode, credentialType) => {
|
findFirstAvailableInventoryItemBySkuCode: async (skuCode, credentialType, inventoryGroupCodes) => {
|
||||||
calls.push(['find', skuCode, credentialType])
|
calls.push(['find', skuCode, credentialType, inventoryGroupCodes])
|
||||||
return { id: 321 }
|
return { id: 321 }
|
||||||
},
|
},
|
||||||
assignReservedInventoryItem: async (inventoryItemId, taskId, updatedAt, roleKey) => {
|
assignReservedInventoryItem: async (inventoryItemId, taskId, updatedAt, roleKey) => {
|
||||||
@@ -41,13 +41,50 @@ test('reserveInventoryForTaskWithDeps reserves first available inventory item wi
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [
|
||||||
['find', 'dnf-cdk-a', 'tencent_code'],
|
['find', 'dnf-cdk-a', 'tencent_code', null],
|
||||||
['assign', 321, 88, '2026-04-14T12:00:00.000Z', 'primary_code'],
|
['assign', 321, 88, '2026-04-14T12:00:00.000Z', 'primary_code'],
|
||||||
])
|
])
|
||||||
assert.equal(result?.id, 321)
|
assert.equal(result?.id, 321)
|
||||||
assert.equal(result?.reserved_by_task_id, 88)
|
assert.equal(result?.reserved_by_task_id, 88)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('reserveInventoryForTaskWithDeps forwards inventory group filters', async () => {
|
||||||
|
const calls = []
|
||||||
|
|
||||||
|
await reserveInventoryForTaskWithDeps(
|
||||||
|
{ skuCode: 'dnf-cdk-a', taskId: 88, inventoryGroupCodes: ['A组', 'B组'] },
|
||||||
|
{
|
||||||
|
findFirstAvailableInventoryItemBySkuCode: async (skuCode, credentialType, inventoryGroupCodes) => {
|
||||||
|
calls.push(['find', skuCode, credentialType, inventoryGroupCodes])
|
||||||
|
return { id: 321 }
|
||||||
|
},
|
||||||
|
assignReservedInventoryItem: async () => ({ id: 321 }),
|
||||||
|
nowIso: () => '2026-04-14T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
['find', 'dnf-cdk-a', 'tencent_code', ['A组', 'B组']],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reserveInventoryForTaskWithDeps returns null when restricted inventory groups are empty', async () => {
|
||||||
|
let findCalled = false
|
||||||
|
|
||||||
|
const result = await reserveInventoryForTaskWithDeps(
|
||||||
|
{ skuCode: 'dnf-cdk-a', taskId: 12, inventoryGroupCodes: [] },
|
||||||
|
{
|
||||||
|
findFirstAvailableInventoryItemBySkuCode: async () => {
|
||||||
|
findCalled = true
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(result, null)
|
||||||
|
assert.equal(findCalled, false)
|
||||||
|
})
|
||||||
|
|
||||||
test('releaseReservedInventoryItemsWithDeps skips empty ids and keeps successful releases only', async () => {
|
test('releaseReservedInventoryItemsWithDeps skips empty ids and keeps successful releases only', async () => {
|
||||||
const calls = []
|
const calls = []
|
||||||
const result = await releaseReservedInventoryItemsWithDeps(
|
const result = await releaseReservedInventoryItemsWithDeps(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export {}
|
|||||||
* userId?: number | string
|
* userId?: number | string
|
||||||
* username?: string
|
* username?: string
|
||||||
* role?: string
|
* role?: string
|
||||||
|
* allowedInventoryGroups?: string[]
|
||||||
* }} AdminViewerSessionInput
|
* }} AdminViewerSessionInput
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -41,9 +42,10 @@ export {}
|
|||||||
* page?: number | string
|
* page?: number | string
|
||||||
* pageSize?: number | string
|
* pageSize?: number | string
|
||||||
* skuCode?: string
|
* skuCode?: string
|
||||||
* credentialType?: string
|
* credentialType?: string
|
||||||
* status?: string
|
* status?: string
|
||||||
* batchNo?: string
|
* batchNo?: string
|
||||||
|
* inventoryGroupCode?: string
|
||||||
* }} AdminInventoryListQueryInput
|
* }} AdminInventoryListQueryInput
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -51,7 +53,8 @@ export {}
|
|||||||
* @typedef {{
|
* @typedef {{
|
||||||
* credentialType?: string
|
* credentialType?: string
|
||||||
* keyword?: string
|
* keyword?: string
|
||||||
* limit?: number | string
|
* limit?: number | string
|
||||||
|
* inventoryGroupCode?: string
|
||||||
* }} AdminInventorySkuSuggestionQueryInput
|
* }} AdminInventorySkuSuggestionQueryInput
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export {}
|
|||||||
* skuCode: string
|
* skuCode: string
|
||||||
* batchNo: string
|
* batchNo: string
|
||||||
* credentialType: string
|
* credentialType: string
|
||||||
|
* inventoryGroupCode: string
|
||||||
* displayValue: string
|
* displayValue: string
|
||||||
* status: string
|
* status: string
|
||||||
* reservedByTaskId: number | null
|
* reservedByTaskId: number | null
|
||||||
@@ -155,6 +156,7 @@ export {}
|
|||||||
* @typedef {{
|
* @typedef {{
|
||||||
* skuCode: string
|
* skuCode: string
|
||||||
* credentialType: string
|
* credentialType: string
|
||||||
|
* inventoryGroupCode: string
|
||||||
* totalCount: number
|
* totalCount: number
|
||||||
* availableCount: number
|
* availableCount: number
|
||||||
* latestUpdatedAt: string | null
|
* latestUpdatedAt: string | null
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ export {}
|
|||||||
* @typedef {import('./admin-write-inputs.js').AdminInventoryInvalidateInput} AdminInventoryInvalidateRouteBody
|
* @typedef {import('./admin-write-inputs.js').AdminInventoryInvalidateInput} AdminInventoryInvalidateRouteBody
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('./admin-write-inputs.js').AdminUserInventoryGroupsInput} AdminUserInventoryGroupsRouteBody
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('./admin-write-inputs.js').AdminTaskManualDispatchInput} AdminTaskManualDispatchRouteBody
|
* @typedef {import('./admin-write-inputs.js').AdminTaskManualDispatchInput} AdminTaskManualDispatchRouteBody
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export {}
|
|||||||
* displayValue?: string
|
* displayValue?: string
|
||||||
* batchNo?: string
|
* batchNo?: string
|
||||||
* credentialType?: string
|
* credentialType?: string
|
||||||
|
* inventoryGroupCode?: string
|
||||||
* }} AdminInventoryCreateInput
|
* }} AdminInventoryCreateInput
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ export {}
|
|||||||
* displayValue?: string
|
* displayValue?: string
|
||||||
* batchNo?: string
|
* batchNo?: string
|
||||||
* credentialType?: string
|
* credentialType?: string
|
||||||
|
* inventoryGroupCode?: string
|
||||||
* }} AdminInventoryImportRowInput
|
* }} AdminInventoryImportRowInput
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -27,9 +29,16 @@ export {}
|
|||||||
* skuCode?: string
|
* skuCode?: string
|
||||||
* batchNo?: string
|
* batchNo?: string
|
||||||
* credentialType?: string
|
* credentialType?: string
|
||||||
|
* inventoryGroupCode?: string
|
||||||
* }} AdminInventoryImportInput
|
* }} AdminInventoryImportInput
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* inventoryGroupCodes?: string[]
|
||||||
|
* }} AdminUserInventoryGroupsInput
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* messageTemplate?: string
|
* messageTemplate?: string
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ export {}
|
|||||||
* @typedef {import('./admin-read-models.js').AdminInventoryItemListItem} AdminInventoryItemListItem
|
* @typedef {import('./admin-read-models.js').AdminInventoryItemListItem} AdminInventoryItemListItem
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('./admin-read-models.js').AdminInventorySkuSuggestion} AdminInventorySkuSuggestion
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* inventoryItem: AdminInventoryItemListItem | null
|
* inventoryItem: AdminInventoryItemListItem | null
|
||||||
@@ -20,6 +24,12 @@ export {}
|
|||||||
* }} AdminInventoryImportResponse
|
* }} AdminInventoryImportResponse
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* items: AdminInventorySkuSuggestion[]
|
||||||
|
* }} AdminInventoryGroupListResponse
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* messageTemplate: string
|
* messageTemplate: string
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ export {}
|
|||||||
* credentialType?: string
|
* credentialType?: string
|
||||||
* status?: string
|
* status?: string
|
||||||
* batchNo?: string
|
* batchNo?: string
|
||||||
|
* inventoryGroupCode?: string
|
||||||
|
* allowedInventoryGroupCodes?: string[] | null
|
||||||
* }} InventoryListQueryInput
|
* }} InventoryListQueryInput
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -156,6 +158,8 @@ export {}
|
|||||||
* credentialType?: string
|
* credentialType?: string
|
||||||
* keyword?: string
|
* keyword?: string
|
||||||
* limit?: number
|
* limit?: number
|
||||||
|
* inventoryGroupCode?: string
|
||||||
|
* allowedInventoryGroupCodes?: string[] | null
|
||||||
* }} InventorySkuSuggestionQueryInput
|
* }} InventorySkuSuggestionQueryInput
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -164,6 +168,7 @@ export {}
|
|||||||
* batchNo?: string
|
* batchNo?: string
|
||||||
* skuCode: string
|
* skuCode: string
|
||||||
* credentialType?: string
|
* credentialType?: string
|
||||||
|
* inventoryGroupCode?: string
|
||||||
* displayValue: string
|
* displayValue: string
|
||||||
* payload?: Record<string, unknown>
|
* payload?: Record<string, unknown>
|
||||||
* createdAt: string
|
* createdAt: string
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export {}
|
|||||||
* sku_code: string
|
* sku_code: string
|
||||||
* batch_no: string
|
* batch_no: string
|
||||||
* credential_type: string
|
* credential_type: string
|
||||||
|
* inventory_group_code: string
|
||||||
* display_value: string
|
* display_value: string
|
||||||
* status: string
|
* status: string
|
||||||
* invalid_reason: string
|
* invalid_reason: string
|
||||||
@@ -109,6 +110,7 @@ export {}
|
|||||||
* @typedef {{
|
* @typedef {{
|
||||||
* sku_code: string
|
* sku_code: string
|
||||||
* credential_type: string
|
* credential_type: string
|
||||||
|
* inventory_group_code: string
|
||||||
* total_count: number
|
* total_count: number
|
||||||
* available_count: number
|
* available_count: number
|
||||||
* latest_updated_at: string | null
|
* latest_updated_at: string | null
|
||||||
@@ -152,6 +154,7 @@ export {}
|
|||||||
* sku_code: string
|
* sku_code: string
|
||||||
* batch_no: string
|
* batch_no: string
|
||||||
* credential_type: string
|
* credential_type: string
|
||||||
|
* inventory_group_code: string
|
||||||
* display_value: string
|
* display_value: string
|
||||||
* inventory_status: string
|
* inventory_status: string
|
||||||
* }} TaskInventoryBindingRow
|
* }} TaskInventoryBindingRow
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ export function updateAdminUserStatus(userId: number | string, payload: { status
|
|||||||
return apiPost<{ user: AdminUserListItem }>(`/api/v1/admin/users/${userId}/status`, payload)
|
return apiPost<{ user: AdminUserListItem }>(`/api/v1/admin/users/${userId}/status`, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateAdminUserInventoryGroups(userId: number | string, payload: { inventoryGroupCodes: string[] }) {
|
||||||
|
return apiPost<{ user: AdminUserListItem }>(`/api/v1/admin/users/${userId}/inventory-groups`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
export function resetAdminUserPassword(userId: number | string, payload: { password: string }) {
|
export function resetAdminUserPassword(userId: number | string, payload: { password: string }) {
|
||||||
return apiPost<{ user: AdminUserListItem }>(`/api/v1/admin/users/${userId}/reset-password`, payload)
|
return apiPost<{ user: AdminUserListItem }>(`/api/v1/admin/users/${userId}/reset-password`, payload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface AdminLoginResponse {
|
|||||||
userId: number
|
userId: number
|
||||||
username: string
|
username: string
|
||||||
role: AdminRole
|
role: AdminRole
|
||||||
|
inventoryGroupCodes?: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ export interface AdminSessionSummary {
|
|||||||
userId: number
|
userId: number
|
||||||
username: string
|
username: string
|
||||||
role: AdminRole
|
role: AdminRole
|
||||||
|
inventoryGroupCodes?: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ export interface AdminUserListItem {
|
|||||||
username: string
|
username: string
|
||||||
role: AdminRole
|
role: AdminRole
|
||||||
status: AdminUserStatus
|
status: AdminUserStatus
|
||||||
|
inventoryGroupCodes: string[]
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
@@ -436,6 +439,7 @@ export interface AdminTaskDetail {
|
|||||||
export interface AdminInventorySkuSuggestion {
|
export interface AdminInventorySkuSuggestion {
|
||||||
skuCode: string
|
skuCode: string
|
||||||
credentialType: string
|
credentialType: string
|
||||||
|
inventoryGroupCode: string
|
||||||
totalCount: number
|
totalCount: number
|
||||||
availableCount: number
|
availableCount: number
|
||||||
latestUpdatedAt: string | null
|
latestUpdatedAt: string | null
|
||||||
@@ -446,6 +450,7 @@ export interface AdminInventoryItemListItem {
|
|||||||
skuCode: string
|
skuCode: string
|
||||||
batchNo: string
|
batchNo: string
|
||||||
credentialType: string
|
credentialType: string
|
||||||
|
inventoryGroupCode: string
|
||||||
displayValue: string
|
displayValue: string
|
||||||
status: string
|
status: string
|
||||||
reservedByTaskId: number | null
|
reservedByTaskId: number | null
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export const adminAuditActionOptions = [
|
|||||||
{ label: '创建后台用户', value: 'admin_user_created' },
|
{ label: '创建后台用户', value: 'admin_user_created' },
|
||||||
{ label: '修改用户角色', value: 'admin_user_role_updated' },
|
{ label: '修改用户角色', value: 'admin_user_role_updated' },
|
||||||
{ label: '修改用户状态', value: 'admin_user_status_updated' },
|
{ label: '修改用户状态', value: 'admin_user_status_updated' },
|
||||||
|
{ label: '修改用户库存组', value: 'admin_user_inventory_groups_updated' },
|
||||||
{ label: '重置用户密码', value: 'admin_user_password_reset' },
|
{ label: '重置用户密码', value: 'admin_user_password_reset' },
|
||||||
{ label: '释放任务库存', value: 'task_release_inventory' },
|
{ label: '释放任务库存', value: 'task_release_inventory' },
|
||||||
{ label: '重发领取链接', value: 'task_regenerate_claim_link' },
|
{ label: '重发领取链接', value: 'task_regenerate_claim_link' },
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type InventoryImportRow = {
|
|||||||
skuCode: string
|
skuCode: string
|
||||||
credentialType: string
|
credentialType: string
|
||||||
batchNo: string
|
batchNo: string
|
||||||
|
inventoryGroupCode: string
|
||||||
displayValue: string
|
displayValue: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ type InventoryPoolGroup = {
|
|||||||
key: string
|
key: string
|
||||||
skuCode: string
|
skuCode: string
|
||||||
credentialType: string
|
credentialType: string
|
||||||
|
inventoryGroupCode: string
|
||||||
totalCount: number
|
totalCount: number
|
||||||
availableCount: number
|
availableCount: number
|
||||||
reservedCount: number
|
reservedCount: number
|
||||||
@@ -50,10 +52,12 @@ const filterSkuCode = ref('')
|
|||||||
const filterCredentialType = ref('')
|
const filterCredentialType = ref('')
|
||||||
const filterStatus = ref('')
|
const filterStatus = ref('')
|
||||||
const filterBatchNo = ref('')
|
const filterBatchNo = ref('')
|
||||||
|
const filterInventoryGroupCode = ref('')
|
||||||
|
|
||||||
const entrySkuCode = ref('')
|
const entrySkuCode = ref('')
|
||||||
const entryCredentialType = ref('tencent_code')
|
const entryCredentialType = ref('tencent_code')
|
||||||
const entryBatchNo = ref('')
|
const entryBatchNo = ref('')
|
||||||
|
const entryInventoryGroupCode = ref('')
|
||||||
const singleDisplayValue = ref('')
|
const singleDisplayValue = ref('')
|
||||||
const bulkText = ref('')
|
const bulkText = ref('')
|
||||||
const skuSuggestionsLoading = ref(false)
|
const skuSuggestionsLoading = ref(false)
|
||||||
@@ -84,6 +88,7 @@ const inventoryGroups = computed<InventoryPoolGroup[]>(() => {
|
|||||||
const groupMap = new Map<string, {
|
const groupMap = new Map<string, {
|
||||||
skuCode: string
|
skuCode: string
|
||||||
credentialType: string
|
credentialType: string
|
||||||
|
inventoryGroupCode: string
|
||||||
totalCount: number
|
totalCount: number
|
||||||
availableCount: number
|
availableCount: number
|
||||||
reservedCount: number
|
reservedCount: number
|
||||||
@@ -94,10 +99,11 @@ const inventoryGroups = computed<InventoryPoolGroup[]>(() => {
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
for (const item of items.value) {
|
for (const item of items.value) {
|
||||||
const key = `${item.skuCode}::${item.credentialType || 'tencent_code'}`
|
const key = `${item.skuCode}::${item.credentialType || 'tencent_code'}::${item.inventoryGroupCode || ''}`
|
||||||
const current = groupMap.get(key) || {
|
const current = groupMap.get(key) || {
|
||||||
skuCode: item.skuCode,
|
skuCode: item.skuCode,
|
||||||
credentialType: item.credentialType || 'tencent_code',
|
credentialType: item.credentialType || 'tencent_code',
|
||||||
|
inventoryGroupCode: item.inventoryGroupCode || '',
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
availableCount: 0,
|
availableCount: 0,
|
||||||
reservedCount: 0,
|
reservedCount: 0,
|
||||||
@@ -135,6 +141,7 @@ const inventoryGroups = computed<InventoryPoolGroup[]>(() => {
|
|||||||
key,
|
key,
|
||||||
skuCode: value.skuCode,
|
skuCode: value.skuCode,
|
||||||
credentialType: value.credentialType,
|
credentialType: value.credentialType,
|
||||||
|
inventoryGroupCode: value.inventoryGroupCode,
|
||||||
totalCount: value.totalCount,
|
totalCount: value.totalCount,
|
||||||
availableCount: value.availableCount,
|
availableCount: value.availableCount,
|
||||||
reservedCount: value.reservedCount,
|
reservedCount: value.reservedCount,
|
||||||
@@ -253,16 +260,18 @@ function getGroupSuggestion(group: InventoryPoolGroup) {
|
|||||||
function useGroupAsFilter(group: InventoryPoolGroup) {
|
function useGroupAsFilter(group: InventoryPoolGroup) {
|
||||||
filterSkuCode.value = group.skuCode
|
filterSkuCode.value = group.skuCode
|
||||||
filterCredentialType.value = group.credentialType
|
filterCredentialType.value = group.credentialType
|
||||||
|
filterInventoryGroupCode.value = group.inventoryGroupCode
|
||||||
void loadInventoryItems(1)
|
void loadInventoryItems(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function useGroupAsEntry(group: InventoryPoolGroup) {
|
function useGroupAsEntry(group: InventoryPoolGroup) {
|
||||||
entrySkuCode.value = group.skuCode
|
entrySkuCode.value = group.skuCode
|
||||||
entryCredentialType.value = group.credentialType
|
entryCredentialType.value = group.credentialType
|
||||||
|
entryInventoryGroupCode.value = group.inventoryGroupCode
|
||||||
if (group.batchSummary !== '未分批' && !group.batchSummary.includes(' / ')) {
|
if (group.batchSummary !== '未分批' && !group.batchSummary.includes(' / ')) {
|
||||||
entryBatchNo.value = group.batchSummary
|
entryBatchNo.value = group.batchSummary
|
||||||
}
|
}
|
||||||
showSuccess(`已把 ${group.skuCode} / ${group.credentialType} 填入入库表单`)
|
showSuccess(`已把 ${group.skuCode} / ${group.credentialType} / ${group.inventoryGroupCode || '未分组'} 填入入库表单`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySkuSuggestion(skuCode: string) {
|
function applySkuSuggestion(skuCode: string) {
|
||||||
@@ -285,11 +294,13 @@ function buildImportRows() {
|
|||||||
if (line.includes('|')) {
|
if (line.includes('|')) {
|
||||||
const parts = line.split('|').map((part) => part.trim())
|
const parts = line.split('|').map((part) => part.trim())
|
||||||
|
|
||||||
if (parts.length !== 4) {
|
if (parts.length !== 4 && parts.length !== 5) {
|
||||||
throw new Error(`第 ${index + 1} 行格式不正确,请使用 内部SKU|凭据类型|批次号|凭据内容`)
|
throw new Error(`第 ${index + 1} 行格式不正确,请使用 内部SKU|凭据类型|批次号|库存组|凭据内容`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [skuCode, credentialType, batchNo, displayValue] = parts
|
const [skuCode, credentialType, batchNo, inventoryGroupCode, displayValue] = parts.length === 5
|
||||||
|
? parts
|
||||||
|
: [parts[0], parts[1], parts[2], '', parts[3]]
|
||||||
if (!skuCode || !displayValue) {
|
if (!skuCode || !displayValue) {
|
||||||
throw new Error(`第 ${index + 1} 行缺少内部 SKU 或凭据内容`)
|
throw new Error(`第 ${index + 1} 行缺少内部 SKU 或凭据内容`)
|
||||||
}
|
}
|
||||||
@@ -298,6 +309,7 @@ function buildImportRows() {
|
|||||||
skuCode,
|
skuCode,
|
||||||
credentialType: credentialType || defaultCredentialType,
|
credentialType: credentialType || defaultCredentialType,
|
||||||
batchNo,
|
batchNo,
|
||||||
|
inventoryGroupCode: inventoryGroupCode || entryInventoryGroupCode.value.trim(),
|
||||||
displayValue,
|
displayValue,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
@@ -311,6 +323,7 @@ function buildImportRows() {
|
|||||||
skuCode: defaultSkuCode,
|
skuCode: defaultSkuCode,
|
||||||
credentialType: defaultCredentialType,
|
credentialType: defaultCredentialType,
|
||||||
batchNo: defaultBatchNo,
|
batchNo: defaultBatchNo,
|
||||||
|
inventoryGroupCode: entryInventoryGroupCode.value.trim(),
|
||||||
displayValue: line,
|
displayValue: line,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -330,6 +343,7 @@ async function loadInventoryItems(page = pagination.value.page) {
|
|||||||
credentialType: filterCredentialType.value.trim(),
|
credentialType: filterCredentialType.value.trim(),
|
||||||
status: filterStatus.value.trim(),
|
status: filterStatus.value.trim(),
|
||||||
batchNo: filterBatchNo.value.trim(),
|
batchNo: filterBatchNo.value.trim(),
|
||||||
|
inventoryGroupCode: filterInventoryGroupCode.value.trim(),
|
||||||
})
|
})
|
||||||
items.value = response.data.items
|
items.value = response.data.items
|
||||||
pagination.value = response.data.pagination
|
pagination.value = response.data.pagination
|
||||||
@@ -346,6 +360,7 @@ async function loadInventorySkuSuggestions() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetchAdminInventorySkuSuggestions({
|
const response = await fetchAdminInventorySkuSuggestions({
|
||||||
credentialType: entryCredentialType.value.trim() || 'tencent_code',
|
credentialType: entryCredentialType.value.trim() || 'tencent_code',
|
||||||
|
inventoryGroupCode: entryInventoryGroupCode.value.trim(),
|
||||||
limit: 50,
|
limit: 50,
|
||||||
})
|
})
|
||||||
inventorySkuSuggestions.value = response.data.items
|
inventorySkuSuggestions.value = response.data.items
|
||||||
@@ -361,6 +376,7 @@ function resetFilters() {
|
|||||||
filterCredentialType.value = ''
|
filterCredentialType.value = ''
|
||||||
filterStatus.value = ''
|
filterStatus.value = ''
|
||||||
filterBatchNo.value = ''
|
filterBatchNo.value = ''
|
||||||
|
filterInventoryGroupCode.value = ''
|
||||||
void loadInventoryItems(1)
|
void loadInventoryItems(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,6 +393,7 @@ async function submitCreate() {
|
|||||||
skuCode: entrySkuCode.value.trim(),
|
skuCode: entrySkuCode.value.trim(),
|
||||||
credentialType: entryCredentialType.value.trim() || 'tencent_code',
|
credentialType: entryCredentialType.value.trim() || 'tencent_code',
|
||||||
batchNo: entryBatchNo.value.trim(),
|
batchNo: entryBatchNo.value.trim(),
|
||||||
|
inventoryGroupCode: entryInventoryGroupCode.value.trim(),
|
||||||
displayValue: singleDisplayValue.value.trim(),
|
displayValue: singleDisplayValue.value.trim(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -450,6 +467,10 @@ watch(entryCredentialType, () => {
|
|||||||
void loadInventorySkuSuggestions()
|
void loadInventorySkuSuggestions()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(entryInventoryGroupCode, () => {
|
||||||
|
void loadInventorySkuSuggestions()
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadInventoryItems(),
|
loadInventoryItems(),
|
||||||
@@ -492,6 +513,7 @@ onMounted(async () => {
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<input v-model="filterBatchNo" class="text-input" placeholder="批次号" />
|
<input v-model="filterBatchNo" class="text-input" placeholder="批次号" />
|
||||||
|
<input v-model="filterInventoryGroupCode" class="text-input" placeholder="库存组,例如 A组" />
|
||||||
</div>
|
</div>
|
||||||
<div class="action-row">
|
<div class="action-row">
|
||||||
<el-button round type="primary" @click="() => loadInventoryItems(1)">查询列表</el-button>
|
<el-button round type="primary" @click="() => loadInventoryItems(1)">查询列表</el-button>
|
||||||
@@ -518,9 +540,9 @@ onMounted(async () => {
|
|||||||
<datalist id="inventory-sku-suggestions">
|
<datalist id="inventory-sku-suggestions">
|
||||||
<option
|
<option
|
||||||
v-for="suggestion in entrySkuSuggestions"
|
v-for="suggestion in entrySkuSuggestions"
|
||||||
:key="`${suggestion.credentialType}-${suggestion.skuCode}`"
|
:key="`${suggestion.credentialType}-${suggestion.inventoryGroupCode}-${suggestion.skuCode}`"
|
||||||
:value="suggestion.skuCode"
|
:value="suggestion.skuCode"
|
||||||
:label="`${suggestion.skuCode} · 可用 ${suggestion.availableCount} / 共 ${suggestion.totalCount}`"
|
:label="`${suggestion.skuCode} · ${suggestion.inventoryGroupCode || '未分组'} · 可用 ${suggestion.availableCount} / 共 ${suggestion.totalCount}`"
|
||||||
/>
|
/>
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
@@ -534,6 +556,7 @@ onMounted(async () => {
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<input v-model="entryBatchNo" class="text-input" placeholder="批次号,可选" />
|
<input v-model="entryBatchNo" class="text-input" placeholder="批次号,可选" />
|
||||||
|
<input v-model="entryInventoryGroupCode" class="text-input" placeholder="库存组,例如 A组 / B组" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sku-helper-bar">
|
<div class="sku-helper-bar">
|
||||||
@@ -544,13 +567,13 @@ onMounted(async () => {
|
|||||||
<div v-if="entrySkuSuggestions.length > 0" class="sku-chip-row">
|
<div v-if="entrySkuSuggestions.length > 0" class="sku-chip-row">
|
||||||
<button
|
<button
|
||||||
v-for="suggestion in entrySkuSuggestions"
|
v-for="suggestion in entrySkuSuggestions"
|
||||||
:key="`${suggestion.credentialType}-${suggestion.skuCode}-chip`"
|
:key="`${suggestion.credentialType}-${suggestion.inventoryGroupCode}-${suggestion.skuCode}-chip`"
|
||||||
type="button"
|
type="button"
|
||||||
class="sku-chip"
|
class="sku-chip"
|
||||||
@click="applySkuSuggestion(suggestion.skuCode)"
|
@click="applySkuSuggestion(suggestion.skuCode)"
|
||||||
>
|
>
|
||||||
<span class="sku-chip-title">{{ suggestion.skuCode }}</span>
|
<span class="sku-chip-title">{{ suggestion.skuCode }}</span>
|
||||||
<span class="sku-chip-meta">可用 {{ suggestion.availableCount }} / 共 {{ suggestion.totalCount }}</span>
|
<span class="sku-chip-meta">{{ suggestion.inventoryGroupCode || '未分组' }} · 可用 {{ suggestion.availableCount }} / 共 {{ suggestion.totalCount }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -559,6 +582,10 @@ onMounted(async () => {
|
|||||||
<span class="helper-title">当前默认匹配</span>
|
<span class="helper-title">当前默认匹配</span>
|
||||||
<span class="helper-copy">{{ getCredentialTypeLabel(entryCredentialType) }}</span>
|
<span class="helper-copy">{{ getCredentialTypeLabel(entryCredentialType) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="helper-item">
|
||||||
|
<span class="helper-title">库存组</span>
|
||||||
|
<span class="helper-copy">{{ entryInventoryGroupCode || '未分组,默认全局可用' }}</span>
|
||||||
|
</div>
|
||||||
<div class="helper-item helper-item-wide">
|
<div class="helper-item helper-item-wide">
|
||||||
<span class="helper-title">说明</span>
|
<span class="helper-title">说明</span>
|
||||||
<span class="helper-copy">{{ getCredentialTypeHint(entryCredentialType) }}</span>
|
<span class="helper-copy">{{ getCredentialTypeHint(entryCredentialType) }}</span>
|
||||||
@@ -573,9 +600,9 @@ onMounted(async () => {
|
|||||||
<textarea
|
<textarea
|
||||||
v-model="bulkText"
|
v-model="bulkText"
|
||||||
class="text-area"
|
class="text-area"
|
||||||
placeholder="批量导入支持两种格式: 1. 每行一个凭据内容(使用上方默认内部 SKU / 凭据类型 / 批次) 2. 每行一条:内部SKU|凭据类型|批次号|凭据内容"
|
placeholder="批量导入支持两种格式: 1. 每行一个凭据内容(使用上方默认内部 SKU / 凭据类型 / 批次 / 库存组) 2. 每行一条:内部SKU|凭据类型|批次号|库存组|凭据内容"
|
||||||
/>
|
/>
|
||||||
<p class="section-tip">示例:`sjz_hdl_test01|tencent_code|sjz-20260412|DJQFf7HgXXXXXXXXXX`</p>
|
<p class="section-tip">示例:`sjz_hdl_test01|tencent_code|sjz-20260412|A组|DJQFf7HgXXXXXXXXXX`</p>
|
||||||
|
|
||||||
<div class="action-row">
|
<div class="action-row">
|
||||||
<el-button :loading="importing" round type="primary" @click="submitImport">批量导入</el-button>
|
<el-button :loading="importing" round type="primary" @click="submitImport">批量导入</el-button>
|
||||||
@@ -625,7 +652,9 @@ onMounted(async () => {
|
|||||||
<div class="pool-topline">
|
<div class="pool-topline">
|
||||||
<div class="pool-title-block">
|
<div class="pool-title-block">
|
||||||
<h3 class="pool-title">{{ group.skuCode }}</h3>
|
<h3 class="pool-title">{{ group.skuCode }}</h3>
|
||||||
<span class="pool-subtitle">{{ getCredentialTypeLabel(group.credentialType) }} · {{ group.credentialType }}</span>
|
<span class="pool-subtitle">
|
||||||
|
{{ getCredentialTypeLabel(group.credentialType) }} · {{ group.credentialType }} · {{ group.inventoryGroupCode || '未分组' }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="pool-total">共 {{ group.totalCount }} 条</span>
|
<span class="pool-total">共 {{ group.totalCount }} 条</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -684,12 +713,14 @@ onMounted(async () => {
|
|||||||
<span class="item-title">#{{ item.inventoryItemId }}</span>
|
<span class="item-title">#{{ item.inventoryItemId }}</span>
|
||||||
<code class="item-value">{{ item.displayValue }}</code>
|
<code class="item-value">{{ item.displayValue }}</code>
|
||||||
<span class="item-subline">批次 {{ item.batchNo || '未分批' }}</span>
|
<span class="item-subline">批次 {{ item.batchNo || '未分批' }}</span>
|
||||||
|
<span class="item-subline">库存组 {{ item.inventoryGroupCode || '未分组' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="sku-cell">
|
<div class="sku-cell">
|
||||||
<span class="sku-title">{{ item.skuCode }}</span>
|
<span class="sku-title">{{ item.skuCode }}</span>
|
||||||
<span class="sku-subline">{{ getCredentialTypeLabel(item.credentialType) }}</span>
|
<span class="sku-subline">{{ getCredentialTypeLabel(item.credentialType) }}</span>
|
||||||
|
<span class="sku-subline">库存组 {{ item.inventoryGroupCode || '未分组' }}</span>
|
||||||
<span class="sku-subline sku-raw">{{ item.credentialType || '-' }}</span>
|
<span class="sku-subline sku-raw">{{ item.credentialType || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -828,7 +859,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,9 +135,9 @@ const taskDetailLink = computed(() => (task.value?.taskId ? `/admin/tasks/${task
|
|||||||
<datalist id="manual-redeem-sku-suggestions">
|
<datalist id="manual-redeem-sku-suggestions">
|
||||||
<option
|
<option
|
||||||
v-for="item in suggestions"
|
v-for="item in suggestions"
|
||||||
:key="`${item.credentialType}-${item.skuCode}`"
|
:key="`${item.credentialType}-${item.inventoryGroupCode}-${item.skuCode}`"
|
||||||
:value="item.skuCode"
|
:value="item.skuCode"
|
||||||
:label="`${item.skuCode} · 可用 ${item.availableCount} / 共 ${item.totalCount}`"
|
:label="`${item.skuCode} · ${item.inventoryGroupCode || '未分组'} · 可用 ${item.availableCount} / 共 ${item.totalCount}`"
|
||||||
/>
|
/>
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
createAdminUser,
|
createAdminUser,
|
||||||
fetchAdminUsers,
|
fetchAdminUsers,
|
||||||
resetAdminUserPassword,
|
resetAdminUserPassword,
|
||||||
|
updateAdminUserInventoryGroups,
|
||||||
updateAdminUserRole,
|
updateAdminUserRole,
|
||||||
updateAdminUserStatus,
|
updateAdminUserStatus,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
@@ -49,6 +50,18 @@ function formatRoleLabel(role: AdminRole) {
|
|||||||
return '普通运营'
|
return '普通运营'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatInventoryGroups(item: AdminUserListItem) {
|
||||||
|
if (item.role !== 'support') {
|
||||||
|
return ['不限库存组']
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.inventoryGroupCodes.length === 0) {
|
||||||
|
return ['未绑定']
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.inventoryGroupCodes
|
||||||
|
}
|
||||||
|
|
||||||
async function loadUsers(page = pagination.value.page) {
|
async function loadUsers(page = pagination.value.page) {
|
||||||
if (!hasAdminRole('admin')) {
|
if (!hasAdminRole('admin')) {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@@ -180,6 +193,46 @@ async function promptResetPassword(item: AdminUserListItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function promptUpdateInventoryGroups(item: AdminUserListItem) {
|
||||||
|
if (item.role !== 'support') {
|
||||||
|
showError('仅客服账号需要绑定库存组')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await showPrompt(
|
||||||
|
`请输入 ${item.username} 可用的库存组,多个组用英文逗号分隔`,
|
||||||
|
'配置库存组',
|
||||||
|
{
|
||||||
|
inputType: 'text',
|
||||||
|
inputValue: item.inventoryGroupCodes.join(','),
|
||||||
|
inputPlaceholder: '例如 A组,B组,售后组',
|
||||||
|
confirmButtonText: '保存',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const inventoryGroupCodes = String(result.value || '')
|
||||||
|
.split(',')
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
actionLoadingId.value = item.userId
|
||||||
|
await updateAdminUserInventoryGroups(item.userId, {
|
||||||
|
inventoryGroupCodes,
|
||||||
|
})
|
||||||
|
showSuccess('用户库存组已更新')
|
||||||
|
await loadUsers()
|
||||||
|
} catch (error) {
|
||||||
|
if (isFeedbackDismissed(error)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showError(error instanceof Error ? error.message : '更新库存组失败')
|
||||||
|
} finally {
|
||||||
|
actionLoadingId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(loadUsers)
|
onMounted(loadUsers)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -233,6 +286,7 @@ onMounted(loadUsers)
|
|||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>账号</th>
|
<th>账号</th>
|
||||||
<th>角色</th>
|
<th>角色</th>
|
||||||
|
<th>库存组</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>创建时间</th>
|
<th>创建时间</th>
|
||||||
<th>更新时间</th>
|
<th>更新时间</th>
|
||||||
@@ -244,6 +298,17 @@ onMounted(loadUsers)
|
|||||||
<td>{{ item.userId }}</td>
|
<td>{{ item.userId }}</td>
|
||||||
<td>{{ item.username }}</td>
|
<td>{{ item.username }}</td>
|
||||||
<td>{{ formatRoleLabel(item.role) }}</td>
|
<td>{{ formatRoleLabel(item.role) }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="group-chip-row">
|
||||||
|
<span
|
||||||
|
v-for="groupCode in formatInventoryGroups(item)"
|
||||||
|
:key="`${item.userId}-${groupCode}`"
|
||||||
|
:class="['group-chip', { 'group-chip--muted': groupCode === '未绑定' || groupCode === '不限库存组' }]"
|
||||||
|
>
|
||||||
|
{{ groupCode }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td><AdminStatusTag :status="item.status" /></td>
|
<td><AdminStatusTag :status="item.status" /></td>
|
||||||
<td>{{ formatAdminDateTime(item.createdAt) }}</td>
|
<td>{{ formatAdminDateTime(item.createdAt) }}</td>
|
||||||
<td>{{ formatAdminDateTime(item.updatedAt) }}</td>
|
<td>{{ formatAdminDateTime(item.updatedAt) }}</td>
|
||||||
@@ -281,6 +346,14 @@ onMounted(loadUsers)
|
|||||||
>
|
>
|
||||||
{{ item.status === 'active' ? '停用' : '启用' }}
|
{{ item.status === 'active' ? '停用' : '启用' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
:loading="actionLoadingId === item.userId"
|
||||||
|
link
|
||||||
|
:disabled="item.role !== 'support'"
|
||||||
|
@click="promptUpdateInventoryGroups(item)"
|
||||||
|
>
|
||||||
|
配库存组
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
:loading="actionLoadingId === item.userId"
|
:loading="actionLoadingId === item.userId"
|
||||||
link
|
link
|
||||||
@@ -385,6 +458,31 @@ onMounted(loadUsers)
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group-chip-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(37, 89, 167, 0.08);
|
||||||
|
border: 1px solid rgba(37, 89, 167, 0.12);
|
||||||
|
color: #2559a7;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-chip--muted {
|
||||||
|
background: rgba(148, 163, 184, 0.12);
|
||||||
|
border-color: rgba(148, 163, 184, 0.18);
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
.self-copy {
|
.self-copy {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user