完成 admin read helper 领域拆分
This commit is contained in:
@@ -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,
|
resolveOrderItemDeliveryMode,
|
||||||
resolveOrderItemTitle,
|
resolveOrderItemTitle,
|
||||||
} from './admin-read-shared-helpers.js'
|
} from './admin-read-shared-helpers.js'
|
||||||
import {
|
import { mapAdminInventoryListItem } from './admin-inventory-read-helpers.js'
|
||||||
mapAdminInventoryListItem,
|
import { mapAdminOrderListItem, summarizeOrderItems } from './admin-order-read-helpers.js'
|
||||||
mapAdminOrderListItem,
|
import { mapAdminWebhookEvent } from './admin-webhook-read-helpers.js'
|
||||||
mapAdminWebhookEvent,
|
|
||||||
summarizeOrderItems,
|
|
||||||
} from './admin-read-helpers.js'
|
|
||||||
import {
|
import {
|
||||||
buildOrderAgisoAutoDeliverySummary,
|
buildOrderAgisoAutoDeliverySummary,
|
||||||
buildOrderBindingSummary,
|
buildOrderBindingSummary,
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
export {
|
export {
|
||||||
getRequiredInventoryItem,
|
getRequiredInventoryItem,
|
||||||
mapAdminInventoryListItem,
|
mapAdminInventoryListItem,
|
||||||
|
} from './admin-inventory-read-helpers.js'
|
||||||
|
export {
|
||||||
mapAdminOrderListItem,
|
mapAdminOrderListItem,
|
||||||
mapAdminWebhookEvent,
|
summarizeOrderItems,
|
||||||
} from './admin-read-helpers.js'
|
} from './admin-order-read-helpers.js'
|
||||||
|
export { mapAdminWebhookEvent } from './admin-webhook-read-helpers.js'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
buildOrderAgisoAutoDeliverySummary,
|
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 {
|
import {
|
||||||
getRequiredInventoryItem,
|
getRequiredInventoryItem,
|
||||||
mapAdminInventoryListItem,
|
mapAdminInventoryListItem,
|
||||||
} from './admin-read-helpers.js'
|
} from './admin-inventory-read-helpers.js'
|
||||||
import {
|
import {
|
||||||
getRequiredTask,
|
getRequiredTask,
|
||||||
mapTaskActionPayload,
|
mapTaskActionPayload,
|
||||||
|
|||||||
@@ -184,6 +184,10 @@
|
|||||||
58. `admin-read-service / admin-write-service / admin-service` 已将 task 相关 helper 改为直接依赖独立 task helper 模块
|
58. `admin-read-service / admin-write-service / admin-service` 已将 task 相关 helper 改为直接依赖独立 task helper 模块
|
||||||
59. `admin-read-helpers.js` 已进一步收敛到 webhook / inventory / order-list 三类能力
|
59. `admin-read-helpers.js` 已进一步收敛到 webhook / inventory / order-list 三类能力
|
||||||
60. `npm --prefix apps/backend run typecheck` 已在 task helper 拆分后继续通过
|
60. `npm --prefix apps/backend run typecheck` 已在 task helper 拆分后继续通过
|
||||||
|
61. `admin-read-helpers.js` 已完成最终拆分,并删除过渡文件
|
||||||
|
62. 新增 `src/services/admin/admin-inventory-read-helpers.js`,库存读取映射与必需实体校验已独立
|
||||||
|
63. 新增 `src/services/admin/admin-order-read-helpers.js` 与 `src/services/admin/admin-webhook-read-helpers.js`,订单列表汇总与 webhook 事件映射已按领域独立
|
||||||
|
64. `admin-read-service / admin-write-service / admin-service` 已全部改为直接依赖 inventory / order / webhook / task / shared 五类 helper,读侧 helper 拆分阶段可以收口
|
||||||
|
|
||||||
这一步的设计原则是“只加静态约束,不动运行链路”,所以不会影响:
|
这一步的设计原则是“只加静态约束,不动运行链路”,所以不会影响:
|
||||||
|
|
||||||
@@ -195,11 +199,10 @@
|
|||||||
|
|
||||||
第一批继续推进时,建议按这个顺序:
|
第一批继续推进时,建议按这个顺序:
|
||||||
|
|
||||||
1. 为 `admin` 路由层补请求体 / 查询参数的共享输入类型
|
1. 为 `admin` 路由层继续补响应模型与审计 payload 的共享类型,减少 `unknown` 到处就地断言
|
||||||
2. 继续把 `admin-read-helpers.js` 细拆成按领域 helper 模块,下一步优先拆 `webhook` 或 `order-list` helper
|
2. 继续补 repository / service 的边界约束,但不急着改 `.ts` 扩展名
|
||||||
3. 为 `admin` 路由层继续补响应模型与审计 payload 的共享类型,减少 `unknown` 到处就地断言
|
3. 恢复并固定 `npm --prefix apps/backend run typecheck` 为日常校验步骤,确保本地与 CI 环境都能直接执行
|
||||||
4. 继续补 repository / service 的边界约束,但不急着改 `.ts` 扩展名
|
4. 为 webhook、库存换码、自动发货补测试
|
||||||
5. 为 webhook、库存换码、自动发货补测试
|
|
||||||
|
|
||||||
## 执行原则
|
## 执行原则
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user