快手发码数据接入发单平台工单侧
- 新增 sync-work-orders-from-send-code 服务:91 先到则回填 shopId/补建工单/凭证回填 material_json;91 未到则兜底建单建工单 - order-service 与 ninetyone order-service 改用 findLatestOrderByPlatformOrderId,放宽 shopId 查重避免重复建单 - work-order-repo 新增 listWorkOrdersByPlatformOrderId 供兜底查重 - send-code-service 挂接同步逻辑并吞错,不影响发码主流程 - ProductRulesPanel 新增店铺列 - .gitignore 忽略 send_code/ 与 .reasonix/
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
createOrder,
|
||||
findOrderByPlatformOrderId,
|
||||
findLatestOrderByPlatformOrderId,
|
||||
updateOrder,
|
||||
} from '../../repositories/order-repo.js'
|
||||
import { replaceOrderItems } from '../../repositories/order-item-repo.js'
|
||||
@@ -94,10 +94,9 @@ export async function upsertOrderFromSource(
|
||||
{ sourceLabel = 'source' }: UpsertOrderSourceOptions = {},
|
||||
): Promise<UpsertOrderResult> {
|
||||
const now = nowIso()
|
||||
const existing = await findOrderByPlatformOrderId({
|
||||
const existing = await findLatestOrderByPlatformOrderId({
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
platformOrderId: event.platformOrderId,
|
||||
})
|
||||
|
||||
@@ -146,8 +145,8 @@ export async function upsertOrderFromSource(
|
||||
const basePayload = {
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
shopName: event.shopName,
|
||||
shopId: String(existing?.shop_id || event.shopId || '').trim(),
|
||||
shopName: String(existing?.shop_name || event.shopName || '').trim(),
|
||||
platformOrderId: event.platformOrderId,
|
||||
orderStatus: event.orderStatus,
|
||||
payStatus: event.payStatus,
|
||||
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
} from '../../../repositories/kuaishou-industry-voucher-repo.js'
|
||||
import { OPEN_91_PLATFORM, OPEN_91_PROVIDER } from '../../open-91/config.js'
|
||||
import { retryOpen91Order } from '../ninetyone/order-service.js'
|
||||
import { syncWorkOrdersFromKuaishouSendCode } from '../../worker-platform/sync-work-orders-from-send-code.js'
|
||||
import { normalizeTimestampIso } from '../../../utils/time.js'
|
||||
import { logIntegration } from '../../../utils/logger.js'
|
||||
import { logWarn, logIntegration } from '../../../utils/logger.js'
|
||||
import {
|
||||
getKuaishouIndustryConfig,
|
||||
assertMatchingAppKey,
|
||||
@@ -119,6 +120,24 @@ export async function handleSendCode(rawBody: JsonObject = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
await syncWorkOrdersFromKuaishouSendCode({
|
||||
oid: normalizedOid,
|
||||
sellerId: params.sellerId,
|
||||
itemId: params.itemId,
|
||||
itemTitle: params.itemTitle,
|
||||
skuId: params.skuId,
|
||||
num: params.num,
|
||||
paymentFen: resolveSendCodePaymentFen(params.ext),
|
||||
rawParams: params,
|
||||
vouchers,
|
||||
now,
|
||||
}).catch((error) => {
|
||||
logWarn('[kuaishou-industry/send-code]', '发单平台工单同步失败,不影响发码主流程', {
|
||||
oid: normalizedOid,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
})
|
||||
|
||||
scheduleKuaishouIndustrySendCallbackRetry(normalizedOid, SEND_CALLBACK_INITIAL_DELAY_MS, {
|
||||
source: 'send_code_accepted',
|
||||
})
|
||||
@@ -671,6 +690,17 @@ function normalizePositiveInteger(value: unknown): number {
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : 0
|
||||
}
|
||||
|
||||
function resolveSendCodePaymentFen(ext: unknown): number {
|
||||
const parsed = parseJsonObject(ext)
|
||||
return normalizePositiveInteger(
|
||||
parsed.payment
|
||||
?? parsed.totalGoodsValue
|
||||
?? parsed.goodsValue
|
||||
?? parsed.payAmount
|
||||
?? parsed.amount,
|
||||
)
|
||||
}
|
||||
|
||||
function parseJsonObject(value: unknown): JsonObject {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value as JsonObject
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { query } from '../../../db/client.js'
|
||||
import {
|
||||
createOrder,
|
||||
findOrderByPlatformOrderId,
|
||||
findLatestOrderByPlatformOrderId,
|
||||
getOrderById,
|
||||
updateOrder,
|
||||
} from '../../../repositories/order-repo.js'
|
||||
@@ -122,10 +122,9 @@ export function parseOpen91ProductNo(value: unknown) {
|
||||
export async function upsertOpen91PendingOrder(payload: JsonObject = {}, config: JsonObject = {}) {
|
||||
const event = buildOpen91SourceEvent(payload, config)
|
||||
const now = nowIso()
|
||||
const existing = await findOrderByPlatformOrderId({
|
||||
const existing = await findLatestOrderByPlatformOrderId({
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
platformOrderId: event.platformOrderId,
|
||||
})
|
||||
|
||||
@@ -137,8 +136,8 @@ export async function upsertOpen91PendingOrder(payload: JsonObject = {}, config:
|
||||
const orderPayload = {
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
shopName: event.shopName,
|
||||
shopId: String(existing?.shop_id || event.shopId || '').trim(),
|
||||
shopName: String(existing?.shop_name || event.shopName || '').trim(),
|
||||
platformOrderId: event.platformOrderId,
|
||||
orderStatus: OPEN_91_PENDING_CONFIG_STATUS,
|
||||
payStatus: 'paid',
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
/**
|
||||
* 快手 send-code 数据接入发单平台(工单侧)。
|
||||
*
|
||||
* 定位:91 回调是订单骨架权威源,快手 send-code 是数据补全源与兜底源。
|
||||
* - 订单已存在(91 先到):回填 shop_id(91 的 productNo 无店铺后缀时)、
|
||||
* 补建缺失工单、把凭证信息回填已建工单 material_json
|
||||
* - 订单不存在(91 未到/缺失):用快手数据兜底建单 + 建工单 + 凭证回填
|
||||
*
|
||||
* 幂等依据:orders 唯一键 provider+platform+shop_id+platform_order_id
|
||||
* (由 91 侧查重逻辑保证同 oid 单条记录)、work_orders.order_item_id 唯一索引、
|
||||
* listWorkOrdersByPlatformOrderId 查重。
|
||||
*
|
||||
* 本服务只写 orders / order_items / work_orders,不触碰
|
||||
* upsertOrderFromSource / syncDeliveryTasksForOrder / readiness / 回调链路。
|
||||
*/
|
||||
|
||||
import {
|
||||
createOrder,
|
||||
findLatestOrderByPlatformOrderId,
|
||||
updateOrder,
|
||||
} from '../../repositories/order-repo.js'
|
||||
import { listOrderItemsByOrderId, replaceOrderItems } from '../../repositories/order-item-repo.js'
|
||||
import {
|
||||
listWorkOrdersByPlatformOrderId,
|
||||
updateWorkOrder,
|
||||
} from '../../repositories/worker-platform/work-order-repo.js'
|
||||
import { syncWorkerOrdersForSourceOrder } from './admin-service.js'
|
||||
import { createWorkOrderEvent } from '../../repositories/worker-platform/work-order-repo.js'
|
||||
import { OPEN_91_PLATFORM, OPEN_91_PROVIDER } from '../open-91/config.js'
|
||||
import { asJsonObject, type JsonObject } from '../../types/json.js'
|
||||
import type { OrderUpdateInput } from '../../types/repository/inputs.js'
|
||||
import type {
|
||||
KuaishouIndustryVoucherRow,
|
||||
OrderRow,
|
||||
} from '../../types/repository/rows.js'
|
||||
|
||||
const KUAISHOU_SEND_CODE_SOURCE = 'kuaishou_send_code'
|
||||
const KUAISHOU_SEND_CODE_ORDER_STATUS = 'paid'
|
||||
|
||||
export type SyncWorkOrdersFromSendCodeInput = {
|
||||
oid: string
|
||||
sellerId: string
|
||||
itemId: string
|
||||
itemTitle: string
|
||||
skuId: string
|
||||
num: number
|
||||
paymentFen: number
|
||||
rawParams: JsonObject
|
||||
vouchers: KuaishouIndustryVoucherRow[]
|
||||
now: string
|
||||
}
|
||||
|
||||
export type SyncWorkOrdersFromSendCodeResult = {
|
||||
order: OrderRow | null
|
||||
orderId: number | null
|
||||
workOrderIds: number[]
|
||||
createdWorkOrderCount: number
|
||||
backfilledVoucherCount: number
|
||||
skipped: string[]
|
||||
}
|
||||
|
||||
export async function syncWorkOrdersFromKuaishouSendCode(
|
||||
input: SyncWorkOrdersFromSendCodeInput,
|
||||
): Promise<SyncWorkOrdersFromSendCodeResult> {
|
||||
const normalizedOid = String(input.oid || '').trim()
|
||||
const skipped: string[] = []
|
||||
|
||||
if (!normalizedOid) {
|
||||
return {
|
||||
order: null,
|
||||
orderId: null,
|
||||
workOrderIds: [],
|
||||
createdWorkOrderCount: 0,
|
||||
backfilledVoucherCount: 0,
|
||||
skipped: ['missing_oid'],
|
||||
}
|
||||
}
|
||||
|
||||
let order = await findLatestOrderByPlatformOrderId({
|
||||
provider: OPEN_91_PROVIDER,
|
||||
platform: OPEN_91_PLATFORM,
|
||||
platformOrderId: normalizedOid,
|
||||
})
|
||||
|
||||
if (order) {
|
||||
const orderResult = await backfillOrderShopId(order, input)
|
||||
order = orderResult.order
|
||||
if (orderResult.skipped) {
|
||||
skipped.push(orderResult.skipped)
|
||||
}
|
||||
} else {
|
||||
const created = await createFallbackOrder(input)
|
||||
if (created.order) {
|
||||
order = created.order
|
||||
} else {
|
||||
skipped.push(created.reason || 'order_create_failed')
|
||||
}
|
||||
}
|
||||
|
||||
const workOrderIds: number[] = []
|
||||
let createdWorkOrderCount = 0
|
||||
|
||||
if (order) {
|
||||
const orderItems = await listOrderItemsByOrderId(order.id)
|
||||
|
||||
const syncResult = await syncWorkerOrdersForSourceOrder(order, orderItems, {
|
||||
source: KUAISHOU_SEND_CODE_SOURCE,
|
||||
autoOnly: true,
|
||||
})
|
||||
createdWorkOrderCount = syncResult.createdCount
|
||||
for (const item of syncResult.skipped) {
|
||||
skipped.push(`work_order:${item.reason}`)
|
||||
}
|
||||
}
|
||||
|
||||
const backfilled = await backfillVouchersToWorkOrders(normalizedOid, input.vouchers, input.now)
|
||||
for (const workOrderId of backfilled.workOrderIds) {
|
||||
workOrderIds.push(workOrderId)
|
||||
}
|
||||
if (backfilled.skipped) {
|
||||
skipped.push(backfilled.skipped)
|
||||
}
|
||||
|
||||
return {
|
||||
order,
|
||||
orderId: order?.id ?? null,
|
||||
workOrderIds,
|
||||
createdWorkOrderCount,
|
||||
backfilledVoucherCount: backfilled.count,
|
||||
skipped,
|
||||
}
|
||||
}
|
||||
|
||||
async function backfillOrderShopId(
|
||||
order: OrderRow,
|
||||
input: SyncWorkOrdersFromSendCodeInput,
|
||||
): Promise<{ order: OrderRow; skipped?: string }> {
|
||||
const sellerId = String(input.sellerId || '').trim()
|
||||
const currentShopId = String(order.shop_id || '').trim()
|
||||
|
||||
if (!sellerId || currentShopId === sellerId) {
|
||||
return { order }
|
||||
}
|
||||
|
||||
const isPlaceholderShopId = !currentShopId || currentShopId === OPEN_91_PROVIDER
|
||||
|
||||
if (!isPlaceholderShopId) {
|
||||
return { order, skipped: `shop_id_conflict:${currentShopId}` }
|
||||
}
|
||||
|
||||
const updated = await updateOrder(order.id, {
|
||||
...buildOrderUpdateBase(order),
|
||||
shopId: sellerId,
|
||||
updatedAt: input.now,
|
||||
})
|
||||
|
||||
return {
|
||||
order: updated || order,
|
||||
}
|
||||
}
|
||||
|
||||
function buildOrderUpdateBase(order: OrderRow): Omit<OrderUpdateInput, 'updatedAt'> {
|
||||
return {
|
||||
provider: String(order.provider || ''),
|
||||
platform: String(order.platform || ''),
|
||||
shopId: String(order.shop_id || ''),
|
||||
shopName: String(order.shop_name || ''),
|
||||
platformOrderId: String(order.platform_order_id || ''),
|
||||
orderStatus: String(order.order_status || ''),
|
||||
payStatus: String(order.pay_status || ''),
|
||||
buyerId: String(order.buyer_id || ''),
|
||||
buyerName: String(order.buyer_name || ''),
|
||||
receiverContact: String(order.receiver_contact || ''),
|
||||
totalAmount: Number(order.total_amount) || 0,
|
||||
currency: String(order.currency || 'CNY'),
|
||||
rawPayloadJson: order.raw_payload_json || '{}',
|
||||
paidAt: order.paid_at || null,
|
||||
}
|
||||
}
|
||||
|
||||
async function createFallbackOrder(input: SyncWorkOrdersFromSendCodeInput): Promise<{
|
||||
order: OrderRow | null
|
||||
reason?: string
|
||||
}> {
|
||||
const now = input.now
|
||||
const sellerId = String(input.sellerId || '').trim()
|
||||
const itemId = String(input.itemId || '').trim()
|
||||
const itemTitle = String(input.itemTitle || '').trim()
|
||||
const num = Math.max(1, Number(input.num) || 1)
|
||||
const paymentFen = Math.max(0, Number(input.paymentFen) || 0)
|
||||
|
||||
if (!itemId && !itemTitle) {
|
||||
return { order: null, reason: 'order_item_missing' }
|
||||
}
|
||||
|
||||
const order = await createOrder({
|
||||
provider: OPEN_91_PROVIDER,
|
||||
platform: OPEN_91_PLATFORM,
|
||||
shopId: sellerId,
|
||||
shopName: sellerId ? `快手店铺-${sellerId}` : '',
|
||||
platformOrderId: input.oid,
|
||||
orderStatus: KUAISHOU_SEND_CODE_ORDER_STATUS,
|
||||
payStatus: 'paid',
|
||||
buyerId: '',
|
||||
buyerName: '',
|
||||
receiverContact: '',
|
||||
totalAmount: paymentFen,
|
||||
currency: 'CNY',
|
||||
rawPayloadJson: {
|
||||
source: KUAISHOU_SEND_CODE_SOURCE,
|
||||
receivedAt: now,
|
||||
body: input.rawParams,
|
||||
},
|
||||
paidAt: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
return { order: null, reason: 'order_create_failed' }
|
||||
}
|
||||
|
||||
await replaceOrderItems(order.id, [
|
||||
{
|
||||
skuCode: itemId,
|
||||
skuName: itemTitle,
|
||||
quantity: num,
|
||||
specJson: JSON.stringify({
|
||||
sellerId,
|
||||
skuId: String(input.skuId || '').trim(),
|
||||
paymentFen,
|
||||
source: KUAISHOU_SEND_CODE_SOURCE,
|
||||
}),
|
||||
itemSnapshotJson: JSON.stringify({
|
||||
source: KUAISHOU_SEND_CODE_SOURCE,
|
||||
params: input.rawParams,
|
||||
}),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
])
|
||||
|
||||
return { order }
|
||||
}
|
||||
|
||||
async function backfillVouchersToWorkOrders(
|
||||
oid: string,
|
||||
vouchers: KuaishouIndustryVoucherRow[],
|
||||
now: string,
|
||||
): Promise<{ workOrderIds: number[]; count: number; skipped?: string }> {
|
||||
const workOrders = await listWorkOrdersByPlatformOrderId(oid)
|
||||
if (workOrders.length === 0) {
|
||||
return { workOrderIds: [], count: 0, skipped: 'work_order_not_found' }
|
||||
}
|
||||
|
||||
const voucherItems = vouchers.map((voucher) => ({
|
||||
voucherCode: String(voucher.voucher_code || ''),
|
||||
token: String(voucher.token || ''),
|
||||
eticketType: String(voucher.eticket_type || ''),
|
||||
status: String(voucher.status || ''),
|
||||
validStartTime: Number(voucher.valid_start_time) || 0,
|
||||
validEndTime: Number(voucher.valid_end_time) || 0,
|
||||
}))
|
||||
|
||||
const workOrderIds: number[] = []
|
||||
let count = 0
|
||||
|
||||
for (const workOrder of workOrders) {
|
||||
const material = asJsonObject(safeParseJson(workOrder.material_json))
|
||||
const existing = Array.isArray(material.kuaishouIndustryVoucher)
|
||||
? (material.kuaishouIndustryVoucher as unknown[])
|
||||
: []
|
||||
const existingCodes = new Set(
|
||||
existing.map((item) =>
|
||||
String((item as JsonObject)?.voucherCode || (item as JsonObject)?.code || ''),
|
||||
),
|
||||
)
|
||||
const merged = [
|
||||
...existing,
|
||||
...voucherItems.filter((item) => !existingCodes.has(item.voucherCode)),
|
||||
]
|
||||
|
||||
material.kuaishouIndustryVoucher = merged
|
||||
|
||||
const updated = await updateWorkOrder(workOrder.id, {
|
||||
material_json: JSON.stringify(material),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
if (!updated) {
|
||||
continue
|
||||
}
|
||||
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'system',
|
||||
actorId: KUAISHOU_SEND_CODE_SOURCE,
|
||||
eventType: 'voucher_backfilled',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: workOrder.status,
|
||||
payloadJson: JSON.stringify({
|
||||
oid,
|
||||
voucherCount: voucherItems.length,
|
||||
}),
|
||||
now,
|
||||
})
|
||||
|
||||
workOrderIds.push(Number(workOrder.id))
|
||||
count += 1
|
||||
}
|
||||
|
||||
return { workOrderIds, count }
|
||||
}
|
||||
|
||||
function safeParseJson(value: unknown): JsonObject {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value as JsonObject
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(String(value || '{}'))
|
||||
return asJsonObject(parsed)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user