完成 admin read helper 领域拆分

This commit is contained in:
yml
2026-04-14 10:41:06 +08:00
parent 9ca15fc503
commit d827eac855
8 changed files with 327 additions and 322 deletions
@@ -0,0 +1,51 @@
// @ts-check
import { getInventoryItemById } from '../../repositories/inventory-repo.js'
import { getOrderById } from '../../repositories/order-repo.js'
import { getTaskById } from '../../repositories/task-repo.js'
import { createHttpError } from '../../utils/http.js'
import { mapAdminTaskSummary } from './admin-task-read-helpers.js'
/** @typedef {import('../../types/admin-read-models.js').AdminInventoryItemListItem} AdminInventoryItemListItem */
/** @typedef {import('../../types/repository-rows.js').InventoryItemRow} InventoryItemRow */
/**
* @param {InventoryItemRow} item
* @returns {Promise<AdminInventoryItemListItem>}
*/
export async function mapAdminInventoryListItem(item) {
const task = item.reserved_by_task_id ? await getTaskById(item.reserved_by_task_id) : null
const order = task?.order_id ? await getOrderById(task.order_id) : null
const taskSummary = task ? mapAdminTaskSummary(task) : null
return {
inventoryItemId: item.id,
skuCode: item.sku_code,
batchNo: item.batch_no,
credentialType: item.credential_type || 'tencent_code',
displayValue: item.display_value,
status: item.status,
reservedByTaskId: item.reserved_by_task_id,
reservedByTaskNo: task?.task_no || '',
platformOrderId: order?.platform_order_id || '',
systemBindingStatus: item.status === 'consumed' ? 'system_bound' : (taskSummary?.systemBindingStatus || 'pending_binding'),
userBindingStatus: item.status === 'consumed' ? 'binding_completed' : (taskSummary?.userBindingStatus || 'not_started'),
invalidReason: item.invalid_reason || '',
deliveredAt: item.delivered_at,
createdAt: item.created_at,
updatedAt: item.updated_at,
}
}
export async function getRequiredInventoryItem(inventoryItemId) {
const inventoryItem = await getInventoryItemById(Number(inventoryItemId))
if (!inventoryItem) {
throw createHttpError('库存项不存在', {
statusCode: 404,
errorCode: 'admin_inventory_not_found',
})
}
return inventoryItem
}
@@ -0,0 +1,74 @@
// @ts-check
import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js'
import { listTasksByOrderId } from '../../repositories/task-repo.js'
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
import { resolveDisplayShopName, resolveOrderItemTitle } from './admin-read-shared-helpers.js'
import { buildOrderBindingSummary, getTaskBindingSummaryMap } from './admin-task-read-helpers.js'
/** @typedef {import('../../types/admin-read-models.js').AdminOrderListItem} AdminOrderListItem */
/** @typedef {import('../../types/repository-rows.js').OrderListRow} OrderListRow */
/**
* @param {OrderListRow} item
* @returns {Promise<AdminOrderListItem>}
*/
export async function mapAdminOrderListItem(item) {
const [tasks, orderItems] = await Promise.all([
listTasksByOrderId(item.id),
listOrderItemsByOrderId(item.id),
])
const taskBindingSummaryMap = await getTaskBindingSummaryMap(tasks.map((task) => task.id))
const bindingSummary = buildOrderBindingSummary(tasks, taskBindingSummaryMap)
const itemSummary = summarizeOrderItems(orderItems)
return {
orderId: item.id,
provider: item.provider || 'agiso',
platform: item.platform,
shopId: item.shop_id || '',
shopName: resolveDisplayShopName(item.provider, item.shop_id, item.shop_name),
platformOrderId: item.platform_order_id,
orderStatus: item.order_status,
payStatus: item.pay_status,
buyerId: item.buyer_id || '',
buyerName: item.buyer_name,
receiverContact: item.receiver_contact || '',
totalAmount: formatFenToAmount(item.total_amount),
totalAmountFen: normalizeFen(item.total_amount),
currency: item.currency,
paidAt: item.paid_at,
createdAt: item.created_at,
updatedAt: item.updated_at,
itemCount: orderItems.length,
totalQuantity: orderItems.reduce((sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)), 0),
itemSummary,
taskCount: Number(item.task_count || tasks.length || 0),
systemBindingStatus: bindingSummary.systemBindingStatus,
userBindingStatus: bindingSummary.userBindingStatus,
systemBoundTaskCount: bindingSummary.systemBoundTaskCount,
completedBindingTaskCount: bindingSummary.completedBindingTaskCount,
totalBindingCount: bindingSummary.totalBindingCount,
reservedBindingCount: bindingSummary.reservedBindingCount,
consumedBindingCount: bindingSummary.consumedBindingCount,
releasedBindingCount: bindingSummary.releasedBindingCount,
}
}
export function summarizeOrderItems(items) {
const normalizedItems = Array.isArray(items) ? items : []
if (normalizedItems.length === 0) {
return ''
}
const [firstItem] = normalizedItems
const firstLabel = resolveOrderItemTitle(firstItem)
|| String(firstItem?.sku_name || firstItem?.sku_code || '').trim()
if (normalizedItems.length === 1) {
return firstLabel
}
return `${firstLabel}${normalizedItems.length}`
}
@@ -1,308 +0,0 @@
// @ts-check
import { extractAgisoTradePayload, resolveAgisoTradePlatformOrderId } from '../order/agiso-trade-parsing.js'
import { getInventoryItemById } from '../../repositories/inventory-repo.js'
import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js'
import { getOrderById } from '../../repositories/order-repo.js'
import { getTaskById, listTasksByOrderId } from '../../repositories/task-repo.js'
import { createHttpError } from '../../utils/http.js'
import { formatFenToAmount, normalizeFen, parseAmountToFen } from '../../utils/money.js'
import { safeParseJson } from './admin-query-utils.js'
import { resolveDisplayShopName, resolveOrderItemTitle } from './admin-read-shared-helpers.js'
import { buildOrderBindingSummary, getTaskBindingSummaryMap, mapAdminTaskSummary } from './admin-task-read-helpers.js'
/** @typedef {import('../../types/admin-read-models.js').AdminInventoryItemListItem} AdminInventoryItemListItem */
/** @typedef {import('../../types/admin-read-models.js').AdminOrderListItem} AdminOrderListItem */
/** @typedef {import('../../types/admin-read-models.js').AdminWebhookEventListItem} AdminWebhookEventListItem */
/** @typedef {import('../../types/repository-rows.js').InventoryItemRow} InventoryItemRow */
/** @typedef {import('../../types/repository-rows.js').OrderListRow} OrderListRow */
/** @typedef {import('../../types/repository-rows.js').WebhookEventRow} WebhookEventRow */
/**
* @param {WebhookEventRow} item
* @returns {Promise<AdminWebhookEventListItem>}
*/
export async function mapAdminWebhookEvent(item, { includeRaw = false } = {}) {
const headers = normalizeRecord(safeParseJson(item.headers_json))
const query = normalizeRecord(safeParseJson(item.query_json))
const body = normalizeRecord(safeParseJson(item.body_json))
const payload = extractWebhookPayload(body)
const itemSources = extractWebhookItemSources(payload)
const rawAmount = pickFirstNonEmpty([
payload.total_fee,
payload.totalFee,
payload.TotalFee,
payload.pay_fee,
payload.payFee,
payload.Payment,
payload.payment,
payload.buy_amount,
payload.buyAmount,
])
const totalAmountFen = parseAmountToFen(rawAmount)
const taskCount = item.related_order_id ? (await listTasksByOrderId(item.related_order_id)).length : 0
const mapped = {
eventId: item.id,
provider: item.provider || 'agiso',
platform: item.platform,
platformRaw: pickFirstNonEmpty([
query.fromPlatform,
query.from_platform,
body.fromPlatform,
body.from_platform,
payload.fromPlatform,
payload.FromPlatform,
payload.platform,
payload.Platform,
]),
shopId: item.shop_id || pickFirstNonEmpty([
payload.PlatformUserId,
payload.platformUserId,
payload.platform_user_id,
payload.seller_id,
payload.sellerId,
payload.SellerId,
payload.seller_nick,
payload.sellerNick,
payload.SellerNick,
]),
shopName: resolveDisplayShopName(
item.provider,
item.shop_id || pickFirstNonEmpty([
payload.PlatformUserId,
payload.platformUserId,
payload.platform_user_id,
payload.seller_id,
payload.sellerId,
payload.SellerId,
payload.seller_nick,
payload.sellerNick,
payload.SellerNick,
]),
item.shop_name || pickFirstNonEmpty([
payload.shopName,
payload.shop_name,
payload.ShopName,
payload.seller_nick,
payload.sellerNick,
payload.SellerNick,
]),
),
eventType: item.event_type,
eventKey: item.event_key,
signatureValid: Boolean(item.signature_valid),
processed: Boolean(item.processed),
processError: item.process_error,
relatedOrderId: item.related_order_id,
createdAt: item.created_at,
platformOrderId: resolveAgisoTradePlatformOrderId(payload),
buyerId: pickFirstNonEmpty([
payload.buyer_id,
payload.buyerId,
payload.BuyerId,
payload.openid,
payload.BuyerOpenUid,
payload.buyer_open_uid,
payload.buyerOpenUid,
]),
buyerName: pickFirstNonEmpty([
payload.buyer_name,
payload.buyerName,
payload.BuyerName,
payload.nick,
payload.BuyerNick,
payload.buyer_nick,
payload.buyerNick,
]),
totalAmount: rawAmount ? formatFenToAmount(totalAmountFen) : '',
totalAmountFen,
currency: pickFirstNonEmpty([payload.currency, payload.Currency, 'CNY']) || 'CNY',
itemCount: itemSources.length,
taskCount,
messageText: pickFirstNonEmpty([
payload.MsgTypeDes,
payload.msgTypeDes,
payload.msg_type_desc,
payload.message,
payload.msg,
]),
requestTimestamp: pickFirstNonEmpty([query.timestamp]),
requestSign: pickFirstNonEmpty([query.sign]),
aopic: pickFirstNonEmpty([query.aopic]),
sourceHost: pickFirstNonEmpty([headers['x-forwarded-host'], headers.host]),
sourceIp: pickFirstNonEmpty([headers['x-forwarded-for'], headers['x-real-ip']]),
userAgent: pickFirstNonEmpty([headers['user-agent'], headers['User-Agent']]),
}
if (!includeRaw) {
return mapped
}
return {
...mapped,
headers,
query,
body,
payload,
}
}
/**
* @param {InventoryItemRow} item
* @returns {Promise<AdminInventoryItemListItem>}
*/
export async function mapAdminInventoryListItem(item) {
const task = item.reserved_by_task_id ? await getTaskById(item.reserved_by_task_id) : null
const order = task?.order_id ? await getOrderById(task.order_id) : null
const taskSummary = task ? mapAdminTaskSummary(task) : null
return {
inventoryItemId: item.id,
skuCode: item.sku_code,
batchNo: item.batch_no,
credentialType: item.credential_type || 'tencent_code',
displayValue: item.display_value,
status: item.status,
reservedByTaskId: item.reserved_by_task_id,
reservedByTaskNo: task?.task_no || '',
platformOrderId: order?.platform_order_id || '',
systemBindingStatus: item.status === 'consumed' ? 'system_bound' : (taskSummary?.systemBindingStatus || 'pending_binding'),
userBindingStatus: item.status === 'consumed' ? 'binding_completed' : (taskSummary?.userBindingStatus || 'not_started'),
invalidReason: item.invalid_reason || '',
deliveredAt: item.delivered_at,
createdAt: item.created_at,
updatedAt: item.updated_at,
}
}
export async function getRequiredInventoryItem(inventoryItemId) {
const inventoryItem = await getInventoryItemById(Number(inventoryItemId))
if (!inventoryItem) {
throw createHttpError('库存项不存在', {
statusCode: 404,
errorCode: 'admin_inventory_not_found',
})
}
return inventoryItem
}
/**
* @param {OrderListRow} item
* @returns {Promise<AdminOrderListItem>}
*/
export async function mapAdminOrderListItem(item) {
const [tasks, orderItems] = await Promise.all([
listTasksByOrderId(item.id),
listOrderItemsByOrderId(item.id),
])
const taskBindingSummaryMap = await getTaskBindingSummaryMap(tasks.map((task) => task.id))
const bindingSummary = buildOrderBindingSummary(tasks, taskBindingSummaryMap)
const itemSummary = summarizeOrderItems(orderItems)
return {
orderId: item.id,
provider: item.provider || 'agiso',
platform: item.platform,
shopId: item.shop_id || '',
shopName: resolveDisplayShopName(item.provider, item.shop_id, item.shop_name),
platformOrderId: item.platform_order_id,
orderStatus: item.order_status,
payStatus: item.pay_status,
buyerId: item.buyer_id || '',
buyerName: item.buyer_name,
receiverContact: item.receiver_contact || '',
totalAmount: formatFenToAmount(item.total_amount),
totalAmountFen: normalizeFen(item.total_amount),
currency: item.currency,
paidAt: item.paid_at,
createdAt: item.created_at,
updatedAt: item.updated_at,
itemCount: orderItems.length,
totalQuantity: orderItems.reduce((sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)), 0),
itemSummary,
taskCount: Number(item.task_count || tasks.length || 0),
systemBindingStatus: bindingSummary.systemBindingStatus,
userBindingStatus: bindingSummary.userBindingStatus,
systemBoundTaskCount: bindingSummary.systemBoundTaskCount,
completedBindingTaskCount: bindingSummary.completedBindingTaskCount,
totalBindingCount: bindingSummary.totalBindingCount,
reservedBindingCount: bindingSummary.reservedBindingCount,
consumedBindingCount: bindingSummary.consumedBindingCount,
releasedBindingCount: bindingSummary.releasedBindingCount,
}
}
export function summarizeOrderItems(items) {
const normalizedItems = Array.isArray(items) ? items : []
if (normalizedItems.length === 0) {
return ''
}
const [firstItem] = normalizedItems
const firstLabel = resolveOrderItemTitle(firstItem)
|| String(firstItem?.sku_name || firstItem?.sku_code || '').trim()
if (normalizedItems.length === 1) {
return firstLabel
}
return `${firstLabel}${normalizedItems.length}`
}
function extractWebhookPayload(body) {
return extractAgisoTradePayload(body)
}
function extractWebhookItemSources(payload) {
const normalizedPayload = normalizeRecord(payload)
const candidates = [
normalizedPayload.orders,
normalizedPayload.Orders,
normalizedPayload.items,
normalizedPayload.Items,
normalizedPayload.order_items,
]
for (const candidate of candidates) {
if (Array.isArray(candidate) && candidate.length > 0) {
return candidate.map((item) => normalizeRecord(item)).filter((item) => Object.keys(item).length > 0)
}
}
if (
pickFirstNonEmpty([
normalizedPayload.item_id,
normalizedPayload.itemId,
normalizedPayload.ItemId,
normalizedPayload.num_iid,
normalizedPayload.numIid,
normalizedPayload.NumIid,
])
) {
return [normalizedPayload]
}
return []
}
function normalizeRecord(value) {
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
}
function pickFirstNonEmpty(values) {
for (const value of values) {
if (typeof value === 'string' && value.trim()) {
return value.trim()
}
if (typeof value === 'number' && Number.isFinite(value)) {
return String(value)
}
}
return ''
}
@@ -31,12 +31,9 @@ import {
resolveOrderItemDeliveryMode,
resolveOrderItemTitle,
} from './admin-read-shared-helpers.js'
import {
mapAdminInventoryListItem,
mapAdminOrderListItem,
mapAdminWebhookEvent,
summarizeOrderItems,
} from './admin-read-helpers.js'
import { mapAdminInventoryListItem } from './admin-inventory-read-helpers.js'
import { mapAdminOrderListItem, summarizeOrderItems } from './admin-order-read-helpers.js'
import { mapAdminWebhookEvent } from './admin-webhook-read-helpers.js'
import {
buildOrderAgisoAutoDeliverySummary,
buildOrderBindingSummary,
@@ -3,9 +3,12 @@
export {
getRequiredInventoryItem,
mapAdminInventoryListItem,
} from './admin-inventory-read-helpers.js'
export {
mapAdminOrderListItem,
mapAdminWebhookEvent,
} from './admin-read-helpers.js'
summarizeOrderItems,
} from './admin-order-read-helpers.js'
export { mapAdminWebhookEvent } from './admin-webhook-read-helpers.js'
export {
buildOrderAgisoAutoDeliverySummary,
@@ -0,0 +1,185 @@
// @ts-check
import { listTasksByOrderId } from '../../repositories/task-repo.js'
import { formatFenToAmount, parseAmountToFen } from '../../utils/money.js'
import { extractAgisoTradePayload, resolveAgisoTradePlatformOrderId } from '../order/agiso-trade-parsing.js'
import { safeParseJson } from './admin-query-utils.js'
import { resolveDisplayShopName } from './admin-read-shared-helpers.js'
/** @typedef {import('../../types/admin-read-models.js').AdminWebhookEventListItem} AdminWebhookEventListItem */
/** @typedef {import('../../types/repository-rows.js').WebhookEventRow} WebhookEventRow */
/**
* @param {WebhookEventRow} item
* @returns {Promise<AdminWebhookEventListItem>}
*/
export async function mapAdminWebhookEvent(item, { includeRaw = false } = {}) {
const headers = normalizeRecord(safeParseJson(item.headers_json))
const query = normalizeRecord(safeParseJson(item.query_json))
const body = normalizeRecord(safeParseJson(item.body_json))
const payload = extractWebhookPayload(body)
const itemSources = extractWebhookItemSources(payload)
const rawAmount = pickFirstNonEmpty([
payload.total_fee,
payload.totalFee,
payload.TotalFee,
payload.pay_fee,
payload.payFee,
payload.Payment,
payload.payment,
payload.buy_amount,
payload.buyAmount,
])
const totalAmountFen = parseAmountToFen(rawAmount)
const taskCount = item.related_order_id ? (await listTasksByOrderId(item.related_order_id)).length : 0
const shopId = item.shop_id || pickFirstNonEmpty([
payload.PlatformUserId,
payload.platformUserId,
payload.platform_user_id,
payload.seller_id,
payload.sellerId,
payload.SellerId,
payload.seller_nick,
payload.sellerNick,
payload.SellerNick,
])
const mapped = {
eventId: item.id,
provider: item.provider || 'agiso',
platform: item.platform,
platformRaw: pickFirstNonEmpty([
query.fromPlatform,
query.from_platform,
body.fromPlatform,
body.from_platform,
payload.fromPlatform,
payload.FromPlatform,
payload.platform,
payload.Platform,
]),
shopId,
shopName: resolveDisplayShopName(
item.provider,
shopId,
item.shop_name || pickFirstNonEmpty([
payload.shopName,
payload.shop_name,
payload.ShopName,
payload.seller_nick,
payload.sellerNick,
payload.SellerNick,
]),
),
eventType: item.event_type,
eventKey: item.event_key,
signatureValid: Boolean(item.signature_valid),
processed: Boolean(item.processed),
processError: item.process_error,
relatedOrderId: item.related_order_id,
createdAt: item.created_at,
platformOrderId: resolveAgisoTradePlatformOrderId(payload),
buyerId: pickFirstNonEmpty([
payload.buyer_id,
payload.buyerId,
payload.BuyerId,
payload.openid,
payload.BuyerOpenUid,
payload.buyer_open_uid,
payload.buyerOpenUid,
]),
buyerName: pickFirstNonEmpty([
payload.buyer_name,
payload.buyerName,
payload.BuyerName,
payload.nick,
payload.BuyerNick,
payload.buyer_nick,
payload.buyerNick,
]),
totalAmount: rawAmount ? formatFenToAmount(totalAmountFen) : '',
totalAmountFen,
currency: pickFirstNonEmpty([payload.currency, payload.Currency, 'CNY']) || 'CNY',
itemCount: itemSources.length,
taskCount,
messageText: pickFirstNonEmpty([
payload.MsgTypeDes,
payload.msgTypeDes,
payload.msg_type_desc,
payload.message,
payload.msg,
]),
requestTimestamp: pickFirstNonEmpty([query.timestamp]),
requestSign: pickFirstNonEmpty([query.sign]),
aopic: pickFirstNonEmpty([query.aopic]),
sourceHost: pickFirstNonEmpty([headers['x-forwarded-host'], headers.host]),
sourceIp: pickFirstNonEmpty([headers['x-forwarded-for'], headers['x-real-ip']]),
userAgent: pickFirstNonEmpty([headers['user-agent'], headers['User-Agent']]),
}
if (!includeRaw) {
return mapped
}
return {
...mapped,
headers,
query,
body,
payload,
}
}
function extractWebhookPayload(body) {
return extractAgisoTradePayload(body)
}
function extractWebhookItemSources(payload) {
const normalizedPayload = normalizeRecord(payload)
const candidates = [
normalizedPayload.orders,
normalizedPayload.Orders,
normalizedPayload.items,
normalizedPayload.Items,
normalizedPayload.order_items,
]
for (const candidate of candidates) {
if (Array.isArray(candidate) && candidate.length > 0) {
return candidate.map((item) => normalizeRecord(item)).filter((item) => Object.keys(item).length > 0)
}
}
if (
pickFirstNonEmpty([
normalizedPayload.item_id,
normalizedPayload.itemId,
normalizedPayload.ItemId,
normalizedPayload.num_iid,
normalizedPayload.numIid,
normalizedPayload.NumIid,
])
) {
return [normalizedPayload]
}
return []
}
function normalizeRecord(value) {
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
}
function pickFirstNonEmpty(values) {
for (const value of values) {
if (typeof value === 'string' && value.trim()) {
return value.trim()
}
if (typeof value === 'number' && Number.isFinite(value)) {
return String(value)
}
}
return ''
}
@@ -37,7 +37,7 @@ import {
import {
getRequiredInventoryItem,
mapAdminInventoryListItem,
} from './admin-read-helpers.js'
} from './admin-inventory-read-helpers.js'
import {
getRequiredTask,
mapTaskActionPayload,