前面的ok了, 绑定与角色
This commit is contained in:
@@ -23,6 +23,35 @@ export async function findOrderByPlatformOrderId({ provider = 'agiso', platform,
|
||||
return /** @type {OrderRow | null} */ (result.rows[0] || null)
|
||||
}
|
||||
|
||||
/** @returns {Promise<OrderRow | null>} */
|
||||
export async function findOrderByPlatformOrderIdCandidates({
|
||||
provider = 'agiso',
|
||||
platform,
|
||||
shopIds = [],
|
||||
platformOrderId,
|
||||
}) {
|
||||
const normalizedShopIds = [...new Set((Array.isArray(shopIds) ? shopIds : [])
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean))]
|
||||
|
||||
if (!platform || !platformOrderId || normalizedShopIds.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
`
|
||||
SELECT *
|
||||
FROM orders
|
||||
WHERE provider = $1 AND platform = $2 AND shop_id = ANY($3::text[]) AND platform_order_id = $4
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
[provider, platform, normalizedShopIds, platformOrderId],
|
||||
)
|
||||
|
||||
return /** @type {OrderRow | null} */ (result.rows[0] || null)
|
||||
}
|
||||
|
||||
/** @returns {Promise<OrderRow | null>} */
|
||||
/** @param {OrderUpsertInput} input */
|
||||
export async function createOrder(input) {
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
reloadClaimSession,
|
||||
redeemClaimTask,
|
||||
} from '../services/claim/claim-session-service.js'
|
||||
import {
|
||||
getKuaishouCloudClaimGuideAssetPath,
|
||||
verifyKuaishouCloudClaimTicket,
|
||||
} from '../services/claim/kuaishou-cloud-claim-service.js'
|
||||
import { buildNotFoundPayload, buildSuccessPayload, sendRouteError } from '../utils/http.js'
|
||||
|
||||
const router = Router()
|
||||
@@ -77,6 +81,15 @@ router.post('/:token/redeem', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:token/kuaishou-cloud/verify-ticket', async (req, res) => {
|
||||
try {
|
||||
const data = await verifyKuaishouCloudClaimTicket(req.params.token, req.body)
|
||||
res.json(buildSuccessPayload(data, '核销码校验成功'))
|
||||
} catch (error) {
|
||||
sendRouteError(res, error, '校验核销码失败', '[claims/:token/kuaishou-cloud/verify-ticket]')
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:token/screenshot', async (req, res) => {
|
||||
try {
|
||||
const screenshotPath = await getClaimScreenshotPath(req.params.token)
|
||||
@@ -86,6 +99,15 @@ router.get('/:token/screenshot', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/assets/kuaishou-cloud/:filename', async (req, res) => {
|
||||
try {
|
||||
const filePath = await getKuaishouCloudClaimGuideAssetPath(req.params.filename)
|
||||
res.sendFile(filePath)
|
||||
} catch (error) {
|
||||
sendRouteError(res, error, '读取指引图片失败', '[claims/assets/kuaishou-cloud/:filename]')
|
||||
}
|
||||
})
|
||||
|
||||
router.use((req, res) => {
|
||||
res.status(404).json(buildNotFoundPayload(req))
|
||||
})
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
getKuaishouEticketSourceConfig,
|
||||
getKuaishouEticketSourceFilePath,
|
||||
listKuaishouEticketShopConfigs,
|
||||
resolveKuaishouEticketShopConfig,
|
||||
saveKuaishouEticketSourceConfig,
|
||||
} from '../platforms/kuaishou-eticket/source-config-service.js'
|
||||
import { queryKuaishouEticketStableInfo } from '../platforms/kuaishou-eticket/info-service.js'
|
||||
@@ -991,8 +992,8 @@ export async function updateAdminFulfillmentBindingConfigs(
|
||||
payload = /** @type {AdminFulfillmentBindingConfigSaveInput} */ ({}),
|
||||
) {
|
||||
const bindingsInput = Array.isArray(payload.bindings) ? payload.bindings : []
|
||||
await validateAdminFulfillmentBindingConfigs(bindingsInput)
|
||||
const saved = saveOrderFulfillmentBindingConfigs(bindingsInput)
|
||||
const normalizedBindings = await validateAdminFulfillmentBindingConfigs(bindingsInput)
|
||||
const saved = saveOrderFulfillmentBindingConfigs(normalizedBindings)
|
||||
await syncConfiguredFulfillmentBindings()
|
||||
|
||||
return {
|
||||
@@ -1010,6 +1011,8 @@ async function validateAdminFulfillmentBindingConfigs(bindings = []) {
|
||||
}
|
||||
|
||||
const seenKeys = new Set()
|
||||
const kuaishouEticketSource = getKuaishouEticketSourceConfig()
|
||||
const normalizedBindings = []
|
||||
|
||||
for (const [index, rawBinding] of bindings.entries()) {
|
||||
if (!isPlainObject(rawBinding)) {
|
||||
@@ -1021,13 +1024,53 @@ async function validateAdminFulfillmentBindingConfigs(bindings = []) {
|
||||
|
||||
const provider = String(rawBinding.provider || 'agiso').trim() || 'agiso'
|
||||
const platform = String(rawBinding.platform || '').trim()
|
||||
const shopId = String(rawBinding.shopId || '').trim()
|
||||
let shopId = String(rawBinding.shopId || '').trim()
|
||||
let shopName = String(rawBinding.shopName || '').trim()
|
||||
const khhaoShopId = String(rawBinding.khhaoShopId || '').trim()
|
||||
const skuCode = String(rawBinding.skuCode || '').trim()
|
||||
const skuName = String(rawBinding.skuName || '').trim()
|
||||
const profileKey = String(rawBinding.profileKey || '').trim() || 'manual_review'
|
||||
const match = isPlainObject(rawBinding.match) ? rawBinding.match : {}
|
||||
const externalItemId = String(match.externalItemId || '').trim()
|
||||
const externalSkuCode = String(match.externalSkuCode || '').trim()
|
||||
const externalSkuName = String(match.externalSkuName || '').trim()
|
||||
const config = isPlainObject(rawBinding.config) ? { ...rawBinding.config } : {}
|
||||
|
||||
if (provider === 'khhao' && platform === 'kuaishou') {
|
||||
if (!khhaoShopId) {
|
||||
throw createHttpError(`第 ${index + 1} 条快手规则缺少 khhao 店铺 ID`, {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_fulfillment_bindings_missing_khhao_shop_id',
|
||||
})
|
||||
}
|
||||
|
||||
const matchedShop = resolveKuaishouEticketShopConfig(
|
||||
{
|
||||
shopId,
|
||||
shopName,
|
||||
},
|
||||
kuaishouEticketSource,
|
||||
)
|
||||
|
||||
if (!matchedShop?.shopId || !matchedShop?.kshopName) {
|
||||
throw createHttpError(
|
||||
`第 ${index + 1} 条快手规则的店铺未匹配到已配置的快手官方店铺,请先到“平台配置”补齐 Cookie 后再选择`,
|
||||
{
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_fulfillment_bindings_kuaishou_shop_not_configured',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
shopId = String(matchedShop.shopId || '').trim()
|
||||
shopName = String(matchedShop.kshopName || '').trim()
|
||||
config.kuaishouShop = {
|
||||
...(isPlainObject(config.kuaishouShop) ? config.kuaishouShop : {}),
|
||||
shopId,
|
||||
shopName,
|
||||
khhaoShopId,
|
||||
}
|
||||
}
|
||||
|
||||
if (!skuCode) {
|
||||
throw createHttpError(`第 ${index + 1} 条规则缺少内部履约 SKU`, {
|
||||
@@ -1054,7 +1097,12 @@ async function validateAdminFulfillmentBindingConfigs(bindings = []) {
|
||||
const uniqueKey = [
|
||||
provider,
|
||||
platform,
|
||||
shopId,
|
||||
resolveFulfillmentBindingMatchShopId({
|
||||
provider,
|
||||
platform,
|
||||
shopId,
|
||||
khhaoShopId,
|
||||
}),
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
normalizeProductName(externalSkuName),
|
||||
@@ -1069,7 +1117,29 @@ async function validateAdminFulfillmentBindingConfigs(bindings = []) {
|
||||
}
|
||||
|
||||
seenKeys.add(uniqueKey)
|
||||
|
||||
normalizedBindings.push({
|
||||
provider,
|
||||
platform,
|
||||
shopId,
|
||||
shopName,
|
||||
khhaoShopId,
|
||||
skuCode,
|
||||
skuName,
|
||||
profileKey,
|
||||
enabled: rawBinding.enabled !== false,
|
||||
priority: rawBinding.priority,
|
||||
config,
|
||||
match: {
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
externalSkuName,
|
||||
config: isPlainObject(match.config) ? match.config : {},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return normalizedBindings
|
||||
}
|
||||
|
||||
function mapAdminFulfillmentBindingConfigItem(item) {
|
||||
@@ -1078,6 +1148,8 @@ function mapAdminFulfillmentBindingConfigItem(item) {
|
||||
provider: String(item?.provider || '').trim(),
|
||||
platform: String(item?.platform || '').trim(),
|
||||
shopId: String(item?.shopId || '').trim(),
|
||||
shopName: String(item?.shopName || '').trim(),
|
||||
khhaoShopId: String(item?.khhaoShopId || '').trim(),
|
||||
skuCode: String(item?.skuCode || '').trim(),
|
||||
skuName: String(item?.skuName || '').trim(),
|
||||
profileKey: String(item?.profileKey || '').trim(),
|
||||
@@ -1093,6 +1165,19 @@ function mapAdminFulfillmentBindingConfigItem(item) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveFulfillmentBindingMatchShopId({
|
||||
provider = '',
|
||||
platform = '',
|
||||
shopId = '',
|
||||
khhaoShopId = '',
|
||||
} = {}) {
|
||||
if (String(provider || '').trim() === 'khhao' && String(platform || '').trim() === 'kuaishou') {
|
||||
return String(khhaoShopId || shopId || '').trim()
|
||||
}
|
||||
|
||||
return String(shopId || '').trim()
|
||||
}
|
||||
|
||||
function matchesObservedProduct(binding, observed) {
|
||||
const provider = String(binding?.provider || '').trim()
|
||||
const platform = String(binding?.platform || '').trim()
|
||||
|
||||
@@ -221,6 +221,7 @@ export async function getAdminTaskDetail(taskId, session = null) {
|
||||
const mappedInventoryBindings = inventoryBindings.map((binding) => mapAdminTaskInventoryBinding(binding, task, viewerContext))
|
||||
const claimUrl = claimToken ? buildClaimUrl(claimToken.token) : ''
|
||||
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, viewerContext)
|
||||
const kuaishouCloudFulfillment = mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment)
|
||||
|
||||
return {
|
||||
task: mapAdminTaskListItem({
|
||||
@@ -279,7 +280,7 @@ export async function getAdminTaskDetail(taskId, session = null) {
|
||||
roleName: String(taskState.reviewRoleName || '').trim() || '',
|
||||
},
|
||||
redeemResolution: mapRedeemResolutionContext(taskContext.redeemResolution),
|
||||
kuaishouCloudFulfillment: mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment),
|
||||
kuaishouCloudFulfillment,
|
||||
manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext),
|
||||
events: taskEvents.map(mapAdminTaskEvent),
|
||||
operations: {
|
||||
@@ -293,6 +294,8 @@ export async function getAdminTaskDetail(taskId, session = null) {
|
||||
canCompleteManualDispatch: viewerContext.canManageTaskLifecycle && isManualDispatchTask(task) && !['redeemed', 'closed'].includes(task.task_status),
|
||||
canPrepareKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
|
||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
&& String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() === 'pending'
|
||||
&& String(kuaishouCloudFulfillment?.returnNumber.status || 'pending').trim() === 'pending'
|
||||
&& ['pending_binding_prepare', 'manual_review', 'failed'].includes(String(task.task_status || '').trim()),
|
||||
canDispatchKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
|
||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
|
||||
@@ -45,6 +45,10 @@ export function mapKuaishouCloudFulfillmentContext(value) {
|
||||
ticket: {
|
||||
code: String(ticket.code || '').trim(),
|
||||
capturedAt: ticket.capturedAt || null,
|
||||
status: String(ticket.status || 'pending').trim() || 'pending',
|
||||
verifiedAt: ticket.verifiedAt || null,
|
||||
goodsTitle: String(ticket.goodsTitle || '').trim(),
|
||||
leftCount: Number(ticket.leftCount || 0) || 0,
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
||||
@@ -81,6 +85,8 @@ export function mapKuaishouCloudFulfillmentContext(value) {
|
||||
status: String(consume.status || 'pending').trim() || 'pending',
|
||||
shopId: String(consume.shopId || '').trim(),
|
||||
autoConsumeEnabled: consume.autoConsumeEnabled === true,
|
||||
consumedAt: consume.consumedAt || null,
|
||||
errorMessage: String(consume.errorMessage || '').trim(),
|
||||
},
|
||||
notes: String(value.notes || '').trim(),
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '../../repositories/task-inventory-binding-repo.js'
|
||||
import { createTaskEvent } from '../../repositories/task-event-repo.js'
|
||||
import { getWebhookEventById } from '../../repositories/webhook-event-repo.js'
|
||||
import { createTaskClaimToken } from '../claim/claim-service.js'
|
||||
import { buildClaimUrl, createTaskClaimToken } from '../claim/claim-service.js'
|
||||
import { confirmClaimRoleForAdminTask, redeemClaimTaskForAdminTask } from '../claim/claim-session-service.js'
|
||||
import { reserveInventoryForTask } from '../order/inventory-service.js'
|
||||
import { normalizeProductName } from '../order/product-match-service.js'
|
||||
@@ -32,6 +32,11 @@ import {
|
||||
useCloudtentaclesSku,
|
||||
} from '../platforms/cloudtentacles/catalog-service.js'
|
||||
import { getCloudtentaclesKnapsack } from '../platforms/cloudtentacles/knapsack-service.js'
|
||||
import { consumeKuaishouEticket } from '../platforms/kuaishou-eticket/consume-service.js'
|
||||
import {
|
||||
getKuaishouEticketSourceConfig,
|
||||
resolveKuaishouEticketShopConfig,
|
||||
} from '../platforms/kuaishou-eticket/source-config-service.js'
|
||||
import {
|
||||
appointCloudtentaclesVirtualNumber,
|
||||
backCloudtentaclesVirtualNumber,
|
||||
@@ -755,6 +760,7 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session =
|
||||
let purchaseTriggered = false
|
||||
let assetBefore = 0
|
||||
let assetAfter = 0
|
||||
const claimLinkState = await ensureTaskClaimLink(task)
|
||||
|
||||
if (!usedKnapsack) {
|
||||
if (!flowWithResolvedBinding.purchase.autoBuyEnabled) {
|
||||
@@ -833,6 +839,8 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session =
|
||||
task_status: 'waiting_binding',
|
||||
inventory_status: 'not_required',
|
||||
user_action_status: 'pending',
|
||||
claim_token: claimLinkState.token || task.claim_token || '',
|
||||
claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
|
||||
last_error: '',
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
@@ -852,6 +860,8 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session =
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
claimUrl: claimLinkState.claimUrl,
|
||||
token: claimLinkState.token,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -890,6 +900,15 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(taskId, payload
|
||||
}
|
||||
|
||||
const ticketCode = String(payload.ticketCode || '').trim()
|
||||
const persistedTicketCode = String(flow.ticket.code || '').trim()
|
||||
|
||||
if (!persistedTicketCode && !ticketCode) {
|
||||
throw createHttpError('客户还没有在领取页提交核销码,暂时不能直接发货', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_kuaishou_cloud_missing_ticket_code',
|
||||
})
|
||||
}
|
||||
|
||||
const dispatchResult = await useCloudtentaclesSku({
|
||||
...cloudContext,
|
||||
id: flow.binding.skuId,
|
||||
@@ -903,7 +922,7 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(taskId, payload
|
||||
...flow,
|
||||
ticket: {
|
||||
...flow.ticket,
|
||||
code: ticketCode || flow.ticket.code,
|
||||
code: ticketCode || persistedTicketCode,
|
||||
capturedAt: ticketCode ? now : flow.ticket.capturedAt,
|
||||
capturedBy: ticketCode && session
|
||||
? {
|
||||
@@ -941,7 +960,7 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(taskId, payload
|
||||
})
|
||||
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_dispatched', {
|
||||
ticketCodeMasked: maskCode(ticketCode),
|
||||
ticketCodeMasked: maskCode(ticketCode || persistedTicketCode),
|
||||
skuId: flow.binding.skuId,
|
||||
vnId: flow.binding.vnId,
|
||||
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
||||
@@ -993,6 +1012,59 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(taskId, sess
|
||||
id: flow.binding.vnId,
|
||||
})
|
||||
|
||||
const order = await getOrderById(task.order_id)
|
||||
const ticketCode = String(flow.ticket.code || '').trim()
|
||||
const shopId = String(order?.shop_id || '').trim()
|
||||
const eticketSource = getKuaishouEticketSourceConfig()
|
||||
const shopConfig = resolveKuaishouEticketShopConfig({
|
||||
shopId,
|
||||
shopName: String(order?.shop_name || '').trim(),
|
||||
})
|
||||
let consumeStatus = 'pending'
|
||||
let consumeErrorMessage = ''
|
||||
let consumedAt = null
|
||||
let nextTaskStatus = 'completed'
|
||||
let nextResultCode = 'kuaishou_cloud_completed'
|
||||
let nextResultMessage = 'cloudtentacles 发货、退号并完成快手核销'
|
||||
|
||||
if (!order) {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = '任务关联订单不存在,无法执行快手核销'
|
||||
} else if (!ticketCode) {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = '客户未提交有效核销码,无法执行快手核销'
|
||||
} else if (!shopConfig || shopConfig.enabled === false || !String(shopConfig.cookie || '').trim()) {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = '订单对应快手小店缺少可用 Cookie,无法执行快手核销'
|
||||
} else {
|
||||
try {
|
||||
const consumeResult = await consumeKuaishouEticket({
|
||||
baseUrl: eticketSource.baseUrl,
|
||||
cookie: shopConfig.cookie,
|
||||
eTicketId: ticketCode,
|
||||
oid: String(flow.ticket.oid || '').trim(),
|
||||
formToken: String(flow.ticket.formToken || '').trim(),
|
||||
})
|
||||
|
||||
if (consumeResult.consumed) {
|
||||
consumeStatus = 'success'
|
||||
consumedAt = now
|
||||
} else {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = String(consumeResult.errorMessage || '快手核销失败').trim()
|
||||
}
|
||||
} catch (error) {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = error instanceof Error ? error.message : '快手核销失败'
|
||||
}
|
||||
}
|
||||
|
||||
if (consumeStatus !== 'success') {
|
||||
nextTaskStatus = 'manual_review'
|
||||
nextResultCode = 'kuaishou_cloud_consume_failed'
|
||||
nextResultMessage = consumeErrorMessage || '号码已退还,但快手核销未完成,请人工处理'
|
||||
}
|
||||
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
kuaishouCloudFulfillment: {
|
||||
@@ -1009,16 +1081,24 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(taskId, sess
|
||||
}
|
||||
: null,
|
||||
},
|
||||
consume: {
|
||||
...flow.consume,
|
||||
status: consumeStatus,
|
||||
shopId: shopId || flow.consume.shopId,
|
||||
autoConsumeEnabled: flow.consume.autoConsumeEnabled === true,
|
||||
consumedAt,
|
||||
errorMessage: consumeErrorMessage,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: 'completed',
|
||||
task_status: nextTaskStatus,
|
||||
delivery_status: 'delivered',
|
||||
result_code: 'kuaishou_cloud_completed',
|
||||
result_message: 'cloudtentacles 发货并退号完成',
|
||||
redeemed_at: now,
|
||||
last_error: '',
|
||||
result_code: nextResultCode,
|
||||
result_message: nextResultMessage,
|
||||
redeemed_at: consumeStatus === 'success' ? now : task.redeemed_at,
|
||||
last_error: consumeErrorMessage,
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
})
|
||||
@@ -1028,6 +1108,18 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(taskId, sess
|
||||
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
||||
}, now)
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
consumeStatus === 'success' ? 'kuaishou_cloud_consumed' : 'kuaishou_cloud_consume_failed',
|
||||
{
|
||||
ticketCodeMasked: maskCode(ticketCode),
|
||||
shopId,
|
||||
consumeStatus,
|
||||
errorMessage: consumeErrorMessage,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
}
|
||||
@@ -1123,8 +1215,14 @@ function normalizeKuaishouCloudFlow(value) {
|
||||
internalSkuName: String(source.internalSkuName || '').trim(),
|
||||
ticket: {
|
||||
code: String(ticket.code || '').trim(),
|
||||
status: String(ticket.status || 'pending').trim() || 'pending',
|
||||
capturedAt: ticket.capturedAt || null,
|
||||
capturedBy: ticket.capturedBy || null,
|
||||
verifiedAt: ticket.verifiedAt || null,
|
||||
oid: String(ticket.oid || '').trim(),
|
||||
formToken: String(ticket.formToken || '').trim(),
|
||||
leftCount: Number(ticket.leftCount || 0) || 0,
|
||||
goodsTitle: String(ticket.goodsTitle || '').trim(),
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
||||
@@ -1163,6 +1261,8 @@ function normalizeKuaishouCloudFlow(value) {
|
||||
status: String(consume.status || 'pending').trim() || 'pending',
|
||||
shopId: String(consume.shopId || '').trim(),
|
||||
autoConsumeEnabled: consume.autoConsumeEnabled === true,
|
||||
consumedAt: consume.consumedAt || null,
|
||||
errorMessage: String(consume.errorMessage || '').trim(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1462,3 +1562,33 @@ function normalizeManualDispatchOutcome(value) {
|
||||
function getTaskClaimExpiresAt(task) {
|
||||
return task?.claim_expires_at || task?.primary_claim_expires_at || null
|
||||
}
|
||||
|
||||
async function ensureTaskClaimLink(task) {
|
||||
const tokenStatus = String(task?.primary_claim_token_status || '').trim()
|
||||
const token = String(task?.primary_claim_token || task?.claim_token || '').trim()
|
||||
const expiredAt = getTaskClaimExpiresAt(task)
|
||||
|
||||
if (tokenStatus === 'active' && token && !isClaimExpired(expiredAt)) {
|
||||
return {
|
||||
token,
|
||||
expiredAt,
|
||||
claimUrl: buildClaimUrl(token),
|
||||
}
|
||||
}
|
||||
|
||||
const claimToken = await createTaskClaimToken(task.id)
|
||||
return {
|
||||
token: claimToken.token,
|
||||
expiredAt: claimToken.expired_at,
|
||||
claimUrl: claimToken.claimUrl,
|
||||
}
|
||||
}
|
||||
|
||||
function isClaimExpired(expiredAt) {
|
||||
if (!expiredAt) {
|
||||
return false
|
||||
}
|
||||
|
||||
const timestamp = new Date(expiredAt).getTime()
|
||||
return Number.isFinite(timestamp) && timestamp <= Date.now()
|
||||
}
|
||||
|
||||
@@ -120,15 +120,18 @@ export async function syncConfiguredFulfillmentBindings(profileMap = {}) {
|
||||
continue
|
||||
}
|
||||
|
||||
const matchShopId = resolveBindingMatchShopId(binding)
|
||||
const bindingConfig = resolveBindingRuntimeConfig(binding)
|
||||
|
||||
await upsertSkuFulfillmentBinding({
|
||||
skuCode: binding.skuCode,
|
||||
provider: binding.provider,
|
||||
platform: binding.platform,
|
||||
shopId: binding.shopId,
|
||||
shopId: matchShopId,
|
||||
profileId: profile.id,
|
||||
enabled: binding.enabled,
|
||||
priority: binding.priority,
|
||||
configJson: JSON.stringify(binding.config || {}),
|
||||
configJson: JSON.stringify(bindingConfig),
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
})
|
||||
@@ -145,7 +148,7 @@ export async function syncConfiguredFulfillmentBindings(profileMap = {}) {
|
||||
await upsertProductMatchRule({
|
||||
provider: binding.provider,
|
||||
platform: binding.platform,
|
||||
shopId: binding.shopId,
|
||||
shopId: matchShopId,
|
||||
externalItemId,
|
||||
externalSkuCode,
|
||||
externalSkuName,
|
||||
@@ -159,3 +162,30 @@ export async function syncConfiguredFulfillmentBindings(profileMap = {}) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function resolveBindingMatchShopId(binding = {}) {
|
||||
if (String(binding.provider || '').trim() === 'khhao' && String(binding.platform || '').trim() === 'kuaishou') {
|
||||
return String(binding.khhaoShopId || binding.shopId || '').trim()
|
||||
}
|
||||
|
||||
return String(binding.shopId || '').trim()
|
||||
}
|
||||
|
||||
function resolveBindingRuntimeConfig(binding = {}) {
|
||||
const baseConfig = isPlainObject(binding.config) ? { ...binding.config } : {}
|
||||
|
||||
if (String(binding.provider || '').trim() === 'khhao' && String(binding.platform || '').trim() === 'kuaishou') {
|
||||
baseConfig.kuaishouShop = {
|
||||
...(isPlainObject(baseConfig.kuaishouShop) ? baseConfig.kuaishouShop : {}),
|
||||
shopId: String(binding.shopId || '').trim(),
|
||||
shopName: String(binding.shopName || '').trim(),
|
||||
khhaoShopId: String(binding.khhaoShopId || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
return baseConfig
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { nowIso } from '../../utils/time.js'
|
||||
|
||||
const CLAIM_TERMINAL_STATUSES = new Set(['expired', 'closed'])
|
||||
const REDEEM_REPLACEMENT_LIMIT = 10
|
||||
const KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH = '/kuaishou-cloud-guide'
|
||||
|
||||
export async function getClaimDetail(token, { includeQrImage = true } = {}) {
|
||||
const context = await getClaimContext(token)
|
||||
@@ -574,7 +575,7 @@ export async function getClaimScreenshotPath(token) {
|
||||
return getTencentBrowserSessionScreenshotPath(context.task.browser_session_id)
|
||||
}
|
||||
|
||||
async function getClaimContext(token) {
|
||||
export async function getClaimContext(token) {
|
||||
const normalized = String(token || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
@@ -818,10 +819,12 @@ function buildClaimDetailPayload({ claimToken, task, order, orderItem, session }
|
||||
const screenshotReady = Boolean(task.screenshot_path) || Boolean(session?.artifacts?.hasScreenshot)
|
||||
const screenshotUrl = screenshotReady ? `/api/v1/claim/${claimToken.token}/screenshot` : ''
|
||||
const finalRedeem = session?.redeem?.final?.redeem || null
|
||||
const kuaishouCloudFulfillment = mapClaimKuaishouCloudFulfillment(task, order)
|
||||
|
||||
return {
|
||||
tokenStatus: claimToken.status,
|
||||
claimUrl: buildClaimUrl(claimToken.token),
|
||||
flowType: kuaishouCloudFulfillment ? 'kuaishou_cloud' : 'tencent_claim',
|
||||
task: {
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
@@ -853,6 +856,7 @@ function buildClaimDetailPayload({ claimToken, task, order, orderItem, session }
|
||||
quantity: orderItem.quantity,
|
||||
},
|
||||
session,
|
||||
kuaishouCloudFulfillment,
|
||||
result: task.redeemed_at || session?.status === 'redeemed'
|
||||
? {
|
||||
resultCode: String(task.result_code || finalRedeem?.iRet || finalRedeem?.ret || ''),
|
||||
@@ -864,6 +868,62 @@ function buildClaimDetailPayload({ claimToken, task, order, orderItem, session }
|
||||
}
|
||||
}
|
||||
|
||||
function mapClaimKuaishouCloudFulfillment(task, order) {
|
||||
if (String(task?.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
||||
return null
|
||||
}
|
||||
|
||||
const source = parseTaskContext(task).kuaishouCloudFulfillment
|
||||
if (!source || typeof source !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
|
||||
const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {}
|
||||
const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {}
|
||||
const returnNumber = source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {}
|
||||
const consume = source.consume && typeof source.consume === 'object' ? source.consume : {}
|
||||
|
||||
return {
|
||||
flowType: 'kuaishou_cloud',
|
||||
shopId: String(order?.shop_id || '').trim(),
|
||||
shopName: String(order?.shop_name || '').trim(),
|
||||
guideImages: [
|
||||
`${KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH}/1.png`,
|
||||
`${KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH}/2.png`,
|
||||
`${KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH}/3.png`,
|
||||
],
|
||||
ticket: {
|
||||
code: String(ticket.code || '').trim(),
|
||||
status: String(ticket.status || 'pending').trim() || 'pending',
|
||||
capturedAt: ticket.capturedAt || null,
|
||||
verifiedAt: ticket.verifiedAt || null,
|
||||
goodsTitle: String(ticket.goodsTitle || '').trim(),
|
||||
leftCount: Number(ticket.leftCount || 0) || 0,
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
||||
bindUrl: String(binding.bindUrl || '').trim(),
|
||||
bindPreparedAt: binding.bindPreparedAt || null,
|
||||
vnPhone: String(binding.vnPhone || '').trim(),
|
||||
},
|
||||
dispatch: {
|
||||
status: String(dispatch.status || 'pending').trim() || 'pending',
|
||||
dispatchAt: dispatch.dispatchAt || null,
|
||||
note: String(dispatch.note || '').trim(),
|
||||
},
|
||||
returnNumber: {
|
||||
status: String(returnNumber.status || 'pending').trim() || 'pending',
|
||||
returnedAt: returnNumber.returnedAt || null,
|
||||
},
|
||||
consume: {
|
||||
status: String(consume.status || 'pending').trim() || 'pending',
|
||||
consumedAt: consume.consumedAt || null,
|
||||
errorMessage: String(consume.errorMessage || '').trim(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function redeemClaimTaskWithInventoryFallback(context, initialInventoryItem) {
|
||||
return redeemClaimTaskWithInventoryFallbackWithDeps(context, initialInventoryItem)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { PROJECT_ROOT } from '../../config/runtime.js'
|
||||
import { createTaskEvent } from '../../repositories/task-event-repo.js'
|
||||
import { updateTask } from '../../repositories/task-repo.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import {
|
||||
getKuaishouEticketSourceConfig,
|
||||
resolveKuaishouEticketShopConfig,
|
||||
} from '../platforms/kuaishou-eticket/source-config-service.js'
|
||||
import { queryKuaishouEticketConsumeDetail } from '../platforms/kuaishou-eticket/consume-service.js'
|
||||
import { getClaimContext, getClaimDetail } from './claim-session-service.js'
|
||||
|
||||
const KUAISHOU_CLOUD_GUIDE_DIR = path.resolve(PROJECT_ROOT, '../../tems/imgs')
|
||||
const ALLOWED_GUIDE_FILES = new Set(['1.png', '2.png', '3.png'])
|
||||
|
||||
export async function verifyKuaishouCloudClaimTicket(token, payload = {}) {
|
||||
const context = await getClaimContext(token)
|
||||
const now = nowIso()
|
||||
|
||||
if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
||||
throw createHttpError('当前领取链接不是快手 Cloud 客户领取流程', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_not_kuaishou_cloud',
|
||||
})
|
||||
}
|
||||
|
||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(context.task).kuaishouCloudFulfillment)
|
||||
if (!flow.binding.bindUrl || flow.binding.prepareStatus !== 'ready') {
|
||||
throw createHttpError('当前任务还没有准备好绑定资源,请联系客服稍后重试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_kuaishou_cloud_binding_not_ready',
|
||||
})
|
||||
}
|
||||
|
||||
const ticketCode = String(payload.ticketCode || payload.eTicketId || '').trim()
|
||||
if (!ticketCode) {
|
||||
throw createHttpError('请先粘贴快手小店核销码', {
|
||||
statusCode: 400,
|
||||
errorCode: 'claim_kuaishou_cloud_missing_ticket_code',
|
||||
})
|
||||
}
|
||||
|
||||
const shopId = String(flow.consume.shopId || context.order.shop_id || '').trim()
|
||||
const shopConfig = resolveKuaishouEticketShopConfig({
|
||||
shopId,
|
||||
shopName: String(context.order.shop_name || '').trim(),
|
||||
})
|
||||
if (!shopConfig || shopConfig.enabled === false || !String(shopConfig.cookie || '').trim()) {
|
||||
throw createHttpError('这笔订单对应的快手小店还没有配置可用 Cookie,请联系客服处理', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_kuaishou_cloud_shop_cookie_missing',
|
||||
})
|
||||
}
|
||||
|
||||
const eticketSource = getKuaishouEticketSourceConfig()
|
||||
const detailResult = await queryKuaishouEticketConsumeDetail({
|
||||
baseUrl: eticketSource.baseUrl,
|
||||
cookie: shopConfig.cookie,
|
||||
eTicketId: ticketCode,
|
||||
})
|
||||
|
||||
if (!detailResult.ok || detailResult.alreadyConsumed || !detailResult.detail) {
|
||||
throw createHttpError(detailResult.errorMessage || '核销码校验失败,请确认是否复制完整', {
|
||||
statusCode: 409,
|
||||
errorCode: detailResult.alreadyConsumed
|
||||
? 'claim_kuaishou_cloud_ticket_already_consumed'
|
||||
: 'claim_kuaishou_cloud_ticket_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const nextContext = {
|
||||
...parseTaskContext(context.task),
|
||||
kuaishouCloudFulfillment: {
|
||||
...flow,
|
||||
ticket: {
|
||||
...flow.ticket,
|
||||
code: detailResult.eTicketId || ticketCode,
|
||||
status: 'verified',
|
||||
capturedAt: flow.ticket.capturedAt || now,
|
||||
capturedBy: flow.ticket.capturedBy || {
|
||||
source: 'claim_page',
|
||||
},
|
||||
verifiedAt: now,
|
||||
oid: String(detailResult.detail.oid || '').trim(),
|
||||
formToken: String(detailResult.detail.formToken || '').trim(),
|
||||
leftCount: Number(detailResult.detail.leftCount || 0) || 0,
|
||||
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
|
||||
},
|
||||
consume: {
|
||||
...flow.consume,
|
||||
shopId,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
await updateTask(context.task.id, {
|
||||
claim_token: context.claimToken.token,
|
||||
claim_expires_at: context.claimToken.expired_at,
|
||||
claimed_at: context.task.claimed_at || now,
|
||||
context_json: JSON.stringify(nextContext),
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(context.task.id, 'kuaishou_cloud_ticket_verified', {
|
||||
ticketCodeMasked: maskCode(detailResult.eTicketId || ticketCode),
|
||||
shopId,
|
||||
shopName: String(context.order.shop_name || '').trim(),
|
||||
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
|
||||
}, now)
|
||||
|
||||
return getClaimDetail(token, { includeQrImage: false })
|
||||
}
|
||||
|
||||
export async function getKuaishouCloudClaimGuideAssetPath(filename) {
|
||||
const normalized = String(filename || '').trim()
|
||||
if (!ALLOWED_GUIDE_FILES.has(normalized)) {
|
||||
throw createHttpError('指引图片不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'claim_kuaishou_cloud_asset_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
const filePath = path.resolve(KUAISHOU_CLOUD_GUIDE_DIR, normalized)
|
||||
if (!filePath.startsWith(KUAISHOU_CLOUD_GUIDE_DIR) || !fs.existsSync(filePath)) {
|
||||
throw createHttpError('指引图片不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'claim_kuaishou_cloud_asset_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
return filePath
|
||||
}
|
||||
|
||||
function normalizeKuaishouCloudFlow(value) {
|
||||
const source = value && typeof value === 'object' ? value : {}
|
||||
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
|
||||
const consume = source.consume && typeof source.consume === 'object' ? source.consume : {}
|
||||
const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {}
|
||||
|
||||
return {
|
||||
...source,
|
||||
ticket: {
|
||||
code: String(ticket.code || '').trim(),
|
||||
status: String(ticket.status || 'pending').trim() || 'pending',
|
||||
capturedAt: ticket.capturedAt || null,
|
||||
capturedBy: ticket.capturedBy || null,
|
||||
verifiedAt: ticket.verifiedAt || null,
|
||||
oid: String(ticket.oid || '').trim(),
|
||||
formToken: String(ticket.formToken || '').trim(),
|
||||
leftCount: Number(ticket.leftCount || 0) || 0,
|
||||
goodsTitle: String(ticket.goodsTitle || '').trim(),
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
||||
bindUrl: String(binding.bindUrl || '').trim(),
|
||||
},
|
||||
consume: {
|
||||
status: String(consume.status || 'pending').trim() || 'pending',
|
||||
shopId: String(consume.shopId || '').trim(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function parseTaskContext(task) {
|
||||
const rawValue = task?.context_json
|
||||
if (!rawValue) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (typeof rawValue === 'object') {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(String(rawValue || '{}'))
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function maskCode(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized.length <= 6) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
return `${normalized.slice(0, 3)}***${normalized.slice(-3)}`
|
||||
}
|
||||
@@ -42,6 +42,9 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
const primaryRequirement = requirements.find((requirement) => requirement.is_required !== false) || requirements[0] || null
|
||||
const quantity = Math.max(1, Number(item.quantity || 1))
|
||||
const fulfillmentConfig = parseJsonObject(profile.config_json)
|
||||
const kuaishouShopConfig = fulfillmentConfig.kuaishouShop && typeof fulfillmentConfig.kuaishouShop === 'object'
|
||||
? fulfillmentConfig.kuaishouShop
|
||||
: {}
|
||||
|
||||
for (let index = 0; index < quantity; index += 1) {
|
||||
const createdAt = nowIso()
|
||||
@@ -93,8 +96,14 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
internalSkuName: item.sku_name,
|
||||
ticket: {
|
||||
code: '',
|
||||
status: 'pending',
|
||||
capturedAt: null,
|
||||
capturedBy: null,
|
||||
verifiedAt: null,
|
||||
oid: '',
|
||||
formToken: '',
|
||||
leftCount: 0,
|
||||
goodsTitle: '',
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: 'pending',
|
||||
@@ -131,8 +140,10 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
},
|
||||
consume: {
|
||||
status: 'pending',
|
||||
shopId: String(fulfillmentConfig.kuaishouConsume?.shopId || '').trim(),
|
||||
shopId: String(fulfillmentConfig.kuaishouConsume?.shopId || kuaishouShopConfig.shopId || '').trim(),
|
||||
autoConsumeEnabled: fulfillmentConfig.kuaishouConsume?.autoConsumeAfterDispatch === true,
|
||||
consumedAt: null,
|
||||
errorMessage: '',
|
||||
},
|
||||
notes: String(fulfillmentConfig.notes || '').trim(),
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ function normalizeOrderFulfillmentBinding(rawValue) {
|
||||
const provider = String(rawValue.provider || 'agiso').trim() || 'agiso'
|
||||
const platform = String(rawValue.platform || '').trim()
|
||||
const shopId = String(rawValue.shopId || '').trim()
|
||||
const shopName = String(rawValue.shopName || '').trim()
|
||||
const khhaoShopId = String(rawValue.khhaoShopId || '').trim()
|
||||
const skuCode = String(rawValue.skuCode || '').trim()
|
||||
const skuName = String(rawValue.skuName || '').trim()
|
||||
const profileKey = String(rawValue.profileKey || '').trim() || 'manual_review'
|
||||
@@ -72,6 +74,8 @@ function normalizeOrderFulfillmentBinding(rawValue) {
|
||||
provider,
|
||||
platform,
|
||||
shopId,
|
||||
shopName,
|
||||
khhaoShopId,
|
||||
skuCode,
|
||||
skuName,
|
||||
profileKey,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { createOrder, findOrderByPlatformOrderId, updateOrder } from '../../repositories/order-repo.js'
|
||||
import {
|
||||
createOrder,
|
||||
findOrderByPlatformOrderId,
|
||||
findOrderByPlatformOrderIdCandidates,
|
||||
updateOrder,
|
||||
} from '../../repositories/order-repo.js'
|
||||
import { replaceOrderItems } from '../../repositories/order-item-repo.js'
|
||||
import { getClaimTokenById } from '../../repositories/claim-token-repo.js'
|
||||
import { buildClaimUrl } from '../claim/claim-service.js'
|
||||
@@ -14,12 +19,18 @@ export async function upsertOrderFromWebhook(event) {
|
||||
|
||||
export async function upsertOrderFromSource(event, { sourceLabel = 'source' } = {}) {
|
||||
const now = nowIso()
|
||||
const existing = await findOrderByPlatformOrderId({
|
||||
const exactExisting = await findOrderByPlatformOrderId({
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
platformOrderId: event.platformOrderId,
|
||||
})
|
||||
const existing = exactExisting || await findOrderByPlatformOrderIdCandidates({
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopIds: resolveEventShopIdCandidates(event),
|
||||
platformOrderId: event.platformOrderId,
|
||||
})
|
||||
|
||||
logWebhook('[order-service]', `开始处理 ${sourceLabel} 订单 upsert`, {
|
||||
provider: event.provider,
|
||||
@@ -155,6 +166,13 @@ export async function upsertOrderFromSource(event, { sourceLabel = 'source' } =
|
||||
}
|
||||
}
|
||||
|
||||
function resolveEventShopIdCandidates(event) {
|
||||
return [...new Set([
|
||||
String(event?.shopId || '').trim(),
|
||||
...(Array.isArray(event?.shopIdAliases) ? event.shopIdAliases : []).map((item) => String(item || '').trim()),
|
||||
].filter(Boolean))]
|
||||
}
|
||||
|
||||
const ORDER_STATUS_PRIORITY = {
|
||||
created: 0,
|
||||
paid: 1,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
import { parseAmountToFen } from '../../../utils/money.js'
|
||||
import { resolveKuaishouEticketShopConfig } from '../kuaishou-eticket/source-config-service.js'
|
||||
|
||||
export function mapKhhaoOrderPreviewList(items = []) {
|
||||
return (Array.isArray(items) ? items : []).map((item) => mapKhhaoOrderPreview(item))
|
||||
@@ -10,11 +11,13 @@ export function mapKhhaoOrderToSourceEvent(item = {}) {
|
||||
const preview = mapKhhaoOrderPreview(item)
|
||||
const orderStatus = resolveKhhaoOrderStatus(preview.status)
|
||||
const payStatus = resolveKhhaoPayStatus(preview.status)
|
||||
const sourceShopId = String(preview.khhaoShopId || preview.shopId || '').trim()
|
||||
|
||||
return {
|
||||
provider: 'khhao',
|
||||
platform: preview.platform || 'unknown',
|
||||
shopId: preview.shopId,
|
||||
shopId: sourceShopId,
|
||||
shopIdAliases: uniqueNonEmptyValues([sourceShopId, preview.shopId, ...(preview.shopIdAliases || [])]),
|
||||
shopName: preview.shopName,
|
||||
platformOrderId: preview.platformOrderId,
|
||||
orderStatus,
|
||||
@@ -46,14 +49,25 @@ export function mapKhhaoOrderPreview(item = {}) {
|
||||
const raw = isPlainObject(item) ? item : {}
|
||||
const platform = resolveKhhaoPlatform(raw.pingtai)
|
||||
const quantity = normalizeQuantity(raw.num)
|
||||
const internalShopId = String(raw.shopid || '').trim()
|
||||
const shopName = String(raw.shopName || '').trim()
|
||||
const resolvedShopIdentity = resolveKhhaoShopIdentity({
|
||||
platform,
|
||||
internalShopId,
|
||||
shopName,
|
||||
})
|
||||
|
||||
return {
|
||||
provider: 'khhao',
|
||||
platform,
|
||||
platformLabel: String(raw.pingtaiName || '').trim(),
|
||||
platformOrderId: String(raw.ordersn || '').trim(),
|
||||
shopId: String(raw.shopid || '').trim(),
|
||||
shopName: String(raw.shopName || '').trim(),
|
||||
shopId: resolvedShopIdentity.shopId,
|
||||
kuaishouShopId: resolvedShopIdentity.officialShopId,
|
||||
shopIdAliases: resolvedShopIdentity.shopIdAliases,
|
||||
khhaoShopId: internalShopId,
|
||||
internalShopId,
|
||||
shopName,
|
||||
itemId: String(raw.goodid || '').trim(),
|
||||
itemTitle: String(raw.goodName || '').trim(),
|
||||
skuCode: String(raw.sku || '').trim(),
|
||||
@@ -66,6 +80,32 @@ export function mapKhhaoOrderPreview(item = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveKhhaoShopIdentity({ platform = '', internalShopId = '', shopName = '' } = {}) {
|
||||
const fallbackShopId = String(internalShopId || '').trim()
|
||||
const normalizedShopName = String(shopName || '').trim()
|
||||
|
||||
if (String(platform || '').trim() !== 'kuaishou') {
|
||||
return {
|
||||
shopId: fallbackShopId,
|
||||
officialShopId: '',
|
||||
shopIdAliases: fallbackShopId ? [fallbackShopId] : [],
|
||||
}
|
||||
}
|
||||
|
||||
const matchedShop = resolveKuaishouEticketShopConfig({
|
||||
shopId: fallbackShopId,
|
||||
shopName: normalizedShopName,
|
||||
})
|
||||
const resolvedShopId = String(matchedShop?.shopId || '').trim() || fallbackShopId
|
||||
const shopIdAliases = uniqueNonEmptyValues([resolvedShopId, fallbackShopId])
|
||||
|
||||
return {
|
||||
shopId: resolvedShopId,
|
||||
officialShopId: String(matchedShop?.shopId || '').trim(),
|
||||
shopIdAliases,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveKhhaoPlatform(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
|
||||
@@ -121,3 +161,9 @@ function normalizePaidAt(value) {
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
|
||||
function uniqueNonEmptyValues(values) {
|
||||
return [...new Set((Array.isArray(values) ? values : [])
|
||||
.map((value) => String(value || '').trim())
|
||||
.filter(Boolean))]
|
||||
}
|
||||
|
||||
@@ -36,6 +36,32 @@ export function findKuaishouEticketShopConfig(shopId, source = getKuaishouEticke
|
||||
return listKuaishouEticketShopConfigs(source).find((item) => String(item.shopId || '').trim() === normalizedShopId) || null
|
||||
}
|
||||
|
||||
export function findKuaishouEticketShopConfigByName(kshopName, source = getKuaishouEticketSourceConfig()) {
|
||||
const normalizedName = String(kshopName || '').trim()
|
||||
if (!normalizedName) {
|
||||
return null
|
||||
}
|
||||
|
||||
return listKuaishouEticketShopConfigs(source).find((item) => String(item.kshopName || '').trim() === normalizedName) || null
|
||||
}
|
||||
|
||||
export function resolveKuaishouEticketShopConfig(
|
||||
{ shopId = '', kshopName = '', shopName = '' } = {},
|
||||
source = getKuaishouEticketSourceConfig(),
|
||||
) {
|
||||
const byId = findKuaishouEticketShopConfig(shopId, source)
|
||||
if (byId) {
|
||||
return byId
|
||||
}
|
||||
|
||||
const normalizedName = String(kshopName || shopName || '').trim()
|
||||
if (!normalizedName) {
|
||||
return null
|
||||
}
|
||||
|
||||
return findKuaishouEticketShopConfigByName(normalizedName, source)
|
||||
}
|
||||
|
||||
export function getFirstAvailableKuaishouEticketShop(source = getKuaishouEticketSourceConfig()) {
|
||||
return listKuaishouEticketShopConfigs(source).find((item) => item.enabled !== false && String(item.cookie || '').trim()) || null
|
||||
}
|
||||
|
||||
@@ -300,6 +300,8 @@ export {}
|
||||
* provider?: string
|
||||
* platform?: string
|
||||
* shopId?: string
|
||||
* shopName?: string
|
||||
* khhaoShopId?: string
|
||||
* skuCode?: string
|
||||
* skuName?: string
|
||||
* profileKey?: string
|
||||
|
||||
Reference in New Issue
Block a user