From a95e48b70851cb10d214fa21c14b19ad1927ddc8 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 13 May 2026 20:18:58 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=A8=E4=B8=80=E4=BA=9B=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/kuaishou-cloud-fulfillment.json | 26 ++ .../backend/data/kuaishou-eticket-source.json | 7 + .../src/routes/admin/platform-config.js | 58 +++ .../platform-config/ninetyone-service.js | 41 +++ .../services/admin/platform-config/service.js | 6 + .../services/open-91/order-create-service.js | 88 ++--- .../services/open-91/order-query-service.js | 51 +++ .../platforms/ninetyone/order-service.js | 346 ++++++++++++++++++ .../platforms/ninetyone/order-service.test.js | 31 ++ apps/backend/src/types/admin-route-inputs.js | 4 + apps/frontend/src/components.d.ts | 2 + .../admin/AdminPlatformNinetyoneSection.vue | 119 ++++++ .../composables/admin/platform-shops/types.ts | 2 +- .../useAdminNinetyonePlatform.ts | 117 ++++++ .../src/services/admin/platform-config.ts | 37 ++ .../src/styles/admin-platform-shops.css | 9 +- apps/frontend/src/types/admin.ts | 33 ++ .../AdminKuaishouCloudFulfillmentView.vue | 235 +++++++++++- .../views/admin/AdminPlatformShopsView.vue | 54 +++ docs/91卡券/3.异步卡密下单.md | 11 +- docs/91卡券/4.查询订单接口.md | 10 +- docs/91卡券/5.当前项目对接配置.md | 17 +- docs/91卡券/6.开发设计.md | 22 +- 23 files changed, 1237 insertions(+), 89 deletions(-) create mode 100644 apps/backend/src/services/admin/platform-config/ninetyone-service.js create mode 100644 apps/backend/src/services/platforms/ninetyone/order-service.js create mode 100644 apps/backend/src/services/platforms/ninetyone/order-service.test.js create mode 100644 apps/frontend/src/components/admin/AdminPlatformNinetyoneSection.vue create mode 100644 apps/frontend/src/composables/admin/platform-shops/useAdminNinetyonePlatform.ts diff --git a/apps/backend/data/kuaishou-cloud-fulfillment.json b/apps/backend/data/kuaishou-cloud-fulfillment.json index f04b2d49..341b2d2a 100644 --- a/apps/backend/data/kuaishou-cloud-fulfillment.json +++ b/apps/backend/data/kuaishou-cloud-fulfillment.json @@ -1,6 +1,32 @@ { "enabled": true, "items": [ + { + "id": "khhao 订单取样导入", + "enabled": true, + "priority": 100, + "provider": "91kaquan", + "platform": "kuaishou", + "shopId": "91kaquan", + "khhaoShopId": "", + "internalSkuCode": "套装-浪漫天命", + "internalSkuName": "套装-浪漫天命", + "externalSkuCode": "183999074510936", + "externalItemId": "183999074510936", + "externalSkuName": "183999074510936", + "resolvedSkuName": "183999074510936", + "cloudSourceKey": "default", + "cloudSkuId": 28, + "cloudSkuName": "套装-浪漫天命", + "vnKey": "1", + "autoBuyEnabled": true, + "minAssetReserve": 0, + "autoReturnNumberAfterDispatch": true, + "autoConsumeAfterDispatch": true, + "kuaishouConsumeShopId": "3676797936", + "kuaishouConsumeShopName": "小鱼儿游戏服务店", + "notes": "从 91卡券订单 2613300010204561 导入" + }, { "id": "套装-浪漫天命", "enabled": true, diff --git a/apps/backend/data/kuaishou-eticket-source.json b/apps/backend/data/kuaishou-eticket-source.json index f19b03ef..ff03aa5c 100644 --- a/apps/backend/data/kuaishou-eticket-source.json +++ b/apps/backend/data/kuaishou-eticket-source.json @@ -2,6 +2,13 @@ "enabled": true, "baseUrl": "https://s.kwaixiaodian.com", "shops": [ + { + "shopId": "3676797936", + "kshopName": "小鱼儿游戏服务店", + "cookie": "_did=web_977722931F45F3C4; did=web_mxsox80uwsj3o4bkdsi2xhbxnx5gdlpn; sid=kshop.api; kshop.api_st=Cgxrc2hvcC5hcGkuc3QSoAHnDz5KbyPrdR8uWNB0MspVtX2zUOBrDcysluKZ9P6NT0IaEtnV-24otPiLyxcbH1iStuNg84bSxClfdu63cgMKzi0AovJn5DFqJUjXPfJZ1XxL8xDXOIdvv73SVr0eQhPKrD_twqnWJu-1HLgxl294XMhDPhO8JbNcFhv_cOwBsxjZYUNMs_7kpRHIpDEU_adQH0zQOBVQYu4AzU0rhF2EGhI9zclILZeuwZLmGG1mEKpcYGMiIOkT8avvcn-qSsIRdLyBf0U1Fjoud_FP9qcCFMjohN1AKAUwAQ; kshop.api_ph=a5c01f4da50e7eda8facb70fd1b13af57eb3; bUserId=1000324969453; userId=3676797936; kuaishou.shop.b_st=ChJrdWFpc2hvdS5zaG9wLmIuc3QSoAHbRIrpTvTjW-nbSsGDVimfmFK01ohV83Wh1v9vCN89z4-bJqpa_r7jmXBgL53hZ0OD5Y8Jz5fUPxWT-6PYNCfLVB20XogIDibxfXXjJSW5d4nchty1_veIl6r4NsUhHu0N93XRsx8vUxfQvAwlczsvefFqvPeZ3iAQnACDEVqJ7m7oI5BK9MCobzGGGuznAgtY8l0Z10qy3e5WmPgiSaI4GhLSubyi-uEBAthqCoUoWxNxjR0iINJ8MTa8OcsVUjQ5AuIuH9BtGr4JuS89zMjEz3McjsjmKAUwAQ; kuaishou.shop.b_ph=0a29c066148190b0759c776da8e94775e7dd", + "userAvatar": "https://p2-pro.a.yximgs.com/uhead/AB/2024/11/17/20/BMjAyNDExMTcyMDQ3NTdfMzY3Njc5NzkzNl8yX2hkODUyXzIwNg==_s.jpg", + "enabled": true + }, { "shopId": "140000112682050", "kshopName": "洋洋运营", diff --git a/apps/backend/src/routes/admin/platform-config.js b/apps/backend/src/routes/admin/platform-config.js index 5401a53d..dcd9f544 100644 --- a/apps/backend/src/routes/admin/platform-config.js +++ b/apps/backend/src/routes/admin/platform-config.js @@ -20,12 +20,15 @@ import { getAdminAgisoShopConfigs, getAdminFulfillmentBindingConfigs, getAdminKhhaoSourceConfig, + getAdminNinetyoneOrders, consumeAdminKuaishouEticket, + failAdminNinetyoneOrder, listAdminCloudtentaclesVirtualNumbers, lookupAdminFulfillmentBindingOrder, queryAdminKuaishouEticketShopInfo, queryAdminKuaishouEticketDetail, queryAdminKhhaoOrders, + retryAdminNinetyoneOrder, runAdminCloudtentaclesFullFlow, sendAdminCloudtentaclesSmsCode, syncAdminKhhaoOrders, @@ -48,6 +51,7 @@ import { createJsonHandler, requireAdminRoles } from './shared.js' /** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoOrderQueryRouteBody} AdminKhhaoOrderQueryRouteBody */ /** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoSourceConfigRouteBody} AdminKhhaoSourceConfigRouteBody */ /** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoTestLoginRouteBody} AdminKhhaoTestLoginRouteBody */ +/** @typedef {import('../../types/admin-route-inputs.js').AdminEntityRouteParams} AdminEntityRouteParams */ /** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketSourceConfigRouteBody} AdminKuaishouEticketSourceConfigRouteBody */ /** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketDetailQueryRouteBody} AdminKuaishouEticketDetailQueryRouteBody */ /** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketConsumeRouteBody} AdminKuaishouEticketConsumeRouteBody */ @@ -234,6 +238,60 @@ router.post('/platform-config/khhao/sync-orders', createJsonHandler( }, )) +router.get('/platform-config/ninetyone/orders', createJsonHandler( + (req) => getAdminNinetyoneOrders(req.query), + { + successMessage: 'ok', + errorMessage: '读取 91卡券订单失败', + scope: '[admin/platform-config/ninetyone/orders]', + }, +)) + +router.post('/platform-config/ninetyone/orders/:id/retry', createJsonHandler( + (req) => retryAdminNinetyoneOrder((/** @type {AdminEntityRouteParams} */ (req.params)).id), + { + successMessage: '91卡券订单已重试', + errorMessage: '重试 91卡券订单失败', + scope: '[admin/platform-config/ninetyone/orders/:id/retry]', + audit: (_req, data) => { + const result = /** @type {{ orderId?: number, orderNo?: string, taskCount?: number }} */ (data) + return { + action: 'platform_ninetyone_order_retried', + targetType: 'order', + targetId: String(result.orderId || '').trim(), + data: { + orderNo: String(result.orderNo || '').trim(), + taskCount: Number(result.taskCount || 0), + }, + } + }, + }, +)) + +router.post('/platform-config/ninetyone/orders/:id/fail', createJsonHandler( + (req) => failAdminNinetyoneOrder( + (/** @type {AdminEntityRouteParams} */ (req.params)).id, + /** @type {{ reason?: string }} */ (req.body), + ), + { + successMessage: '91卡券订单已标记失败', + errorMessage: '标记 91卡券订单失败', + scope: '[admin/platform-config/ninetyone/orders/:id/fail]', + audit: (req, data) => { + const result = /** @type {{ orderId?: number, orderNo?: string }} */ (data) + return { + action: 'platform_ninetyone_order_failed', + targetType: 'order', + targetId: String(result.orderId || '').trim(), + data: { + orderNo: String(result.orderNo || '').trim(), + reason: String((/** @type {{ reason?: string }} */ (req.body)).reason || '').trim(), + }, + } + }, + }, +)) + router.post('/platform-config/kuaishou-eticket/query-detail', createJsonHandler( (req) => queryAdminKuaishouEticketDetail(/** @type {AdminKuaishouEticketDetailQueryRouteBody} */ (req.body)), { diff --git a/apps/backend/src/services/admin/platform-config/ninetyone-service.js b/apps/backend/src/services/admin/platform-config/ninetyone-service.js new file mode 100644 index 00000000..66cfe540 --- /dev/null +++ b/apps/backend/src/services/admin/platform-config/ninetyone-service.js @@ -0,0 +1,41 @@ +// @ts-check + +import { + failOpen91Order, + listOpen91Orders, + retryOpen91Order, +} from '../../platforms/ninetyone/order-service.js' + +/** @param {{ page?: number | string, pageSize?: number | string, status?: string }} [query] */ +export async function getAdminNinetyoneOrders(query = {}) { + return listOpen91Orders({ + page: Number(query.page || 1) || 1, + pageSize: Number(query.pageSize || 20) || 20, + status: query.status || 'pending_config', + }) +} + +/** @param {number | string} orderId */ +export async function retryAdminNinetyoneOrder(orderId) { + const result = await retryOpen91Order(orderId) + return { + orderId: Number(result.order?.id || 0), + orderNo: String(result.order?.platform_order_id || '').trim(), + orderStatus: String(result.order?.order_status || '').trim(), + orderItemCount: Array.isArray(result.orderItems) ? result.orderItems.length : 0, + taskCount: Array.isArray(result.tasks) ? result.tasks.length : 0, + } +} + +/** @param {number | string} orderId */ +/** @param {{ reason?: string }} [payload] */ +export async function failAdminNinetyoneOrder(orderId, payload = {}) { + const result = await failOpen91Order(orderId, payload.reason) + return { + orderId: Number(result.order?.id || 0), + orderNo: String(result.order?.platform_order_id || '').trim(), + orderStatus: String(result.order?.order_status || '').trim(), + orderItemCount: Array.isArray(result.orderItems) ? result.orderItems.length : 0, + taskCount: Array.isArray(result.tasks) ? result.tasks.length : 0, + } +} diff --git a/apps/backend/src/services/admin/platform-config/service.js b/apps/backend/src/services/admin/platform-config/service.js index 658f73b8..4ab03dd3 100644 --- a/apps/backend/src/services/admin/platform-config/service.js +++ b/apps/backend/src/services/admin/platform-config/service.js @@ -11,6 +11,12 @@ export { syncAdminKhhaoOrders, } from './khhao-service.js' +export { + getAdminNinetyoneOrders, + retryAdminNinetyoneOrder, + failAdminNinetyoneOrder, +} from './ninetyone-service.js' + export { getAdminKuaishouEticketSourceConfig, updateAdminKuaishouEticketSourceConfig, diff --git a/apps/backend/src/services/open-91/order-create-service.js b/apps/backend/src/services/open-91/order-create-service.js index c1a676b8..f977a292 100644 --- a/apps/backend/src/services/open-91/order-create-service.js +++ b/apps/backend/src/services/open-91/order-create-service.js @@ -1,8 +1,10 @@ -import { hasConfiguredOrderItems } from '../order/product-match-service.js' import { upsertOrderFromSource } from '../order/order-service.js' -import { nowIso } from '../../utils/time.js' import { logWebhook } from '../../utils/logger.js' import { parseAmountToFen } from '../../utils/money.js' +import { + buildOpen91SourceEvent, + upsertOpen91PendingOrder, +} from '../platforms/ninetyone/order-service.js' import { assertOpen91Config, assertOpen91CreatePayload, @@ -14,8 +16,6 @@ import { normalizeOpen91CreatePayload, OPEN_91_COST_EXCEED_FAIL_CODE, OPEN_91_DEFAULT_FAIL_CODE, - OPEN_91_PLATFORM, - OPEN_91_PROVIDER, } from './shared.js' export async function createOpen91Order(payload = {}, { requestId = '' } = {}) { @@ -26,42 +26,6 @@ export async function createOpen91Order(payload = {}, { requestId = '' } = {}) { assertOpen91Timestamp(normalized.timestamp, config.timestampToleranceSeconds) assertOpen91Signature(payload, config) - const now = nowIso() - const items = [ - { - externalItemId: normalized.productNo, - externalSkuCode: normalized.productNo, - externalSkuName: normalized.productNo, - quantity: normalized.buyNum, - spec: { - source: OPEN_91_PROVIDER, - }, - snapshot: { - source: OPEN_91_PROVIDER, - orderNo: normalized.orderNo, - productNo: normalized.productNo, - callbackUrl: normalized.callbackUrl, - }, - }, - ] - - const configured = await hasConfiguredOrderItems({ - provider: OPEN_91_PROVIDER, - platform: OPEN_91_PLATFORM, - shopId: config.shopId, - items, - }) - - if (!configured) { - return buildOpen91SuccessResponse({ - orderNo: normalized.orderNo, - outTradeNo: '', - orderStatus: 30, - failCode: OPEN_91_DEFAULT_FAIL_CODE, - failReason: '商品未配置或未匹配到履约规则', - }) - } - if (normalized.maxAmount) { const maxAmountFen = parseAmountToFen(normalized.maxAmount) if (maxAmountFen < 0) { @@ -75,26 +39,8 @@ export async function createOpen91Order(payload = {}, { requestId = '' } = {}) { } } - const result = await upsertOrderFromSource({ - provider: OPEN_91_PROVIDER, - platform: OPEN_91_PLATFORM, - shopId: config.shopId, - shopName: config.shopName, - platformOrderId: normalized.orderNo, - orderStatus: 'paid', - payStatus: 'paid', - buyerId: '', - buyerName: '', - receiverContact: '', - totalAmount: 0, - currency: 'CNY', - rawPayload: { - source: OPEN_91_PROVIDER, - ...normalized, - }, - paidAt: now, - items, - }, { + const sourceEvent = buildOpen91SourceEvent(normalized, config) + const result = await upsertOrderFromSource(sourceEvent, { sourceLabel: 'open91', }) @@ -103,13 +49,31 @@ export async function createOpen91Order(payload = {}, { requestId = '' } = {}) { orderNo: normalized.orderNo, productNo: normalized.productNo, buyNum: normalized.buyNum, - configured, ignored: Boolean(result.ignored), + ignoreReason: result.ignoreReason || '', orderId: result.order?.id || null, taskCount: Array.isArray(result.tasks) ? result.tasks.length : 0, }) - if (result.ignored || !result.order || !Array.isArray(result.tasks) || result.tasks.length === 0) { + if (result.ignored) { + const pending = await upsertOpen91PendingOrder(normalized, config) + logWebhook('[open-91/create]', '91卡券订单已进入待补全队列', { + requestId, + orderNo: normalized.orderNo, + productNo: normalized.productNo, + orderId: pending.order?.id || null, + }) + + return buildOpen91SuccessResponse({ + orderNo: normalized.orderNo, + outTradeNo: buildOpen91OutTradeNo(pending.order), + orderStatus: 10, + orderCost: buildOpen91OrderCost(0), + cards: '', + }) + } + + if (!result.order || !Array.isArray(result.tasks) || result.tasks.length === 0) { return buildOpen91SuccessResponse({ orderNo: normalized.orderNo, outTradeNo: '', diff --git a/apps/backend/src/services/open-91/order-query-service.js b/apps/backend/src/services/open-91/order-query-service.js index 530ce791..61ab2262 100644 --- a/apps/backend/src/services/open-91/order-query-service.js +++ b/apps/backend/src/services/open-91/order-query-service.js @@ -3,6 +3,10 @@ import { listTasksByOrderId } from '../../repositories/task-repo.js' import { buildClaimUrl } from '../claim/claim-service.js' import { ensureTaskClaimLink } from '../fulfillment/kuaishou-cloud-task-service.js' import { logWebhook } from '../../utils/logger.js' +import { + OPEN_91_MANUAL_FAILED_STATUS, + OPEN_91_PENDING_CONFIG_STATUS, +} from '../platforms/ninetyone/order-service.js' import { assertOpen91Config, assertOpen91QueryPayload, @@ -46,6 +50,31 @@ export async function queryOpen91Order(payload = {}, { requestId = '' } = {}) { } const tasks = await listTasksByOrderId(order.id) + + if (tasks.length === 0) { + if (String(order.order_status || '').trim() === OPEN_91_PENDING_CONFIG_STATUS) { + return buildOpen91SuccessResponse({ + orderNo: normalized.orderNo, + outTradeNo: buildOpen91OutTradeNo(order), + orderStatus: 10, + failCode: 0, + failReason: '', + orderCost: buildOpen91OrderCost(0), + cards: '', + }) + } + + if (String(order.order_status || '').trim() === OPEN_91_MANUAL_FAILED_STATUS) { + return buildOpen91SuccessResponse({ + orderNo: normalized.orderNo, + outTradeNo: buildOpen91OutTradeNo(order), + orderStatus: 30, + failCode: OPEN_91_DEFAULT_FAIL_CODE, + failReason: resolveOpen91OrderFailReason(order) || '商家手动标记无法履约', + }) + } + } + const cardItems = [] const readyTaskIds = [] @@ -123,3 +152,25 @@ export async function queryOpen91Order(payload = {}, { requestId = '' } = {}) { cards: buildOpen91Cards(cardItems, config), }) } + +function resolveOpen91OrderFailReason(order) { + const payload = parseJsonObject(order?.raw_payload_json) + return String(payload.manualFailedReason || '').trim() +} + +function parseJsonObject(value) { + if (!value) { + return {} + } + + if (typeof value === 'object' && !Array.isArray(value)) { + return value + } + + try { + const parsed = JSON.parse(String(value || '{}')) + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} + } catch { + return {} + } +} diff --git a/apps/backend/src/services/platforms/ninetyone/order-service.js b/apps/backend/src/services/platforms/ninetyone/order-service.js new file mode 100644 index 00000000..9a9124c1 --- /dev/null +++ b/apps/backend/src/services/platforms/ninetyone/order-service.js @@ -0,0 +1,346 @@ +// @ts-check + +import { query } from '../../../db/client.js' +import { createOrder, findOrderByPlatformOrderId, getOrderById, updateOrder } from '../../../repositories/order-repo.js' +import { listOrderItemsByOrderId, replaceOrderItems } from '../../../repositories/order-item-repo.js' +import { listTasksByOrderId } from '../../../repositories/task-repo.js' +import { upsertOrderFromSource } from '../../order/order-service.js' +import { + buildOpen91OutTradeNo, + normalizeOpen91CreatePayload, + OPEN_91_PLATFORM, + OPEN_91_PROVIDER, +} from '../../open-91/shared.js' +import { createHttpError } from '../../../utils/http.js' +import { nowIso } from '../../../utils/time.js' + +export const OPEN_91_PENDING_CONFIG_STATUS = 'pending_config' +export const OPEN_91_MANUAL_FAILED_STATUS = 'manual_failed' + +export function buildOpen91SourceEvent(payload = {}, config = {}) { + const normalized = normalizeOpen91CreatePayload(payload) + const productNo = normalized.productNo + + return { + provider: OPEN_91_PROVIDER, + platform: OPEN_91_PLATFORM, + shopId: String(config.shopId || OPEN_91_PROVIDER).trim() || OPEN_91_PROVIDER, + shopName: String(config.shopName || '91卡券').trim() || '91卡券', + platformOrderId: normalized.orderNo, + orderStatus: 'paid', + payStatus: 'paid', + buyerId: '', + buyerName: '', + receiverContact: '', + totalAmount: 0, + currency: 'CNY', + paidAt: nowIso(), + rawPayload: { + source: OPEN_91_PROVIDER, + receivedAt: nowIso(), + body: normalized, + }, + items: [ + { + itemId: productNo, + externalItemId: productNo, + externalSkuCode: productNo, + externalSkuName: productNo, + skuCode: productNo, + skuName: productNo, + quantity: normalized.buyNum, + spec: { + source: OPEN_91_PROVIDER, + orderNo: normalized.orderNo, + productNo, + }, + snapshot: { + source: OPEN_91_PROVIDER, + orderNo: normalized.orderNo, + productNo, + externalItemId: productNo, + externalSkuCode: productNo, + externalSkuName: productNo, + callbackUrl: normalized.callbackUrl, + }, + }, + ], + } +} + +export async function upsertOpen91PendingOrder(payload = {}, config = {}) { + const event = buildOpen91SourceEvent(payload, config) + const now = nowIso() + const existing = await findOrderByPlatformOrderId({ + provider: event.provider, + platform: event.platform, + shopId: event.shopId, + platformOrderId: event.platformOrderId, + }) + + const rawPayloadJson = JSON.stringify({ + ...event.rawPayload, + pendingReason: 'unconfigured_items', + pendingAt: now, + }) + const orderPayload = { + provider: event.provider, + platform: event.platform, + shopId: event.shopId, + shopName: event.shopName, + platformOrderId: event.platformOrderId, + orderStatus: OPEN_91_PENDING_CONFIG_STATUS, + payStatus: 'paid', + buyerId: '', + buyerName: '', + receiverContact: '', + totalAmount: 0, + currency: 'CNY', + rawPayloadJson, + paidAt: existing?.paid_at || now, + } + + const order = existing + ? await updateOrder(existing.id, { ...orderPayload, updatedAt: now }) + : await createOrder({ ...orderPayload, createdAt: now, updatedAt: now }) + + if (!order) { + throw createHttpError('91卡券待处理订单保存失败', { + statusCode: 500, + errorCode: 'open91_pending_order_save_failed', + }) + } + + const [item] = event.items + const orderItems = await replaceOrderItems(order.id, [ + { + skuCode: item.skuCode, + skuName: item.skuName, + quantity: item.quantity, + specJson: JSON.stringify(item.spec || {}), + itemSnapshotJson: JSON.stringify(item.snapshot || item.spec || {}), + createdAt: now, + updatedAt: now, + }, + ]) + + return { + order, + orderItems, + tasks: [], + } +} + +export async function retryOpen91Order(orderId) { + const order = await getRequiredOpen91Order(orderId) + const items = await listOrderItemsByOrderId(order.id) + const event = buildOpen91SourceEventFromOrder(order, items) + const result = await upsertOrderFromSource(event, { sourceLabel: 'open91-admin-retry' }) + + if (result.ignored) { + throw createHttpError('当前 91 订单仍未命中履约配置,请先补全商品规则', { + statusCode: 409, + errorCode: 'open91_order_still_unconfigured', + context: { + ignoreReason: result.ignoreReason || 'unconfigured_items', + }, + }) + } + + return { + order: result.order, + orderItems: result.orderItems || [], + tasks: result.tasks || [], + } +} + +export async function failOpen91Order(orderId, reason = '') { + const order = await getRequiredOpen91Order(orderId) + const now = nowIso() + const rawPayload = parseJsonObject(order.raw_payload_json) + const updated = await updateOrder(order.id, { + provider: order.provider, + platform: order.platform, + shopId: order.shop_id, + shopName: order.shop_name, + platformOrderId: order.platform_order_id, + orderStatus: OPEN_91_MANUAL_FAILED_STATUS, + payStatus: order.pay_status, + buyerId: order.buyer_id, + buyerName: order.buyer_name, + receiverContact: order.receiver_contact, + totalAmount: order.total_amount, + currency: order.currency, + rawPayloadJson: JSON.stringify({ + ...rawPayload, + manualFailedAt: now, + manualFailedReason: String(reason || '').trim() || '商家手动标记无法履约', + }), + paidAt: order.paid_at, + updatedAt: now, + }) + + return { + order: updated, + orderItems: await listOrderItemsByOrderId(order.id), + tasks: await listTasksByOrderId(order.id), + } +} + +export async function listOpen91Orders({ page = 1, pageSize = 20, status = 'pending_config' } = {}) { + const normalizedPage = Math.max(1, Number(page) || 1) + const normalizedPageSize = Math.min(100, Math.max(1, Number(pageSize) || 20)) + const normalizedStatus = String(status || 'pending_config').trim() + const filters = [ + 'o.provider = $1', + 'o.platform = $2', + ] + /** @type {Array} */ + const params = [OPEN_91_PROVIDER, OPEN_91_PLATFORM] + + if (normalizedStatus && normalizedStatus !== 'all') { + params.push(normalizedStatus) + filters.push(`o.order_status = $${params.length}`) + } + + const whereClause = `WHERE ${filters.join(' AND ')}` + const totalResult = await query(`SELECT COUNT(*)::int AS total FROM orders o ${whereClause}`, params) + + params.push(normalizedPageSize) + params.push((normalizedPage - 1) * normalizedPageSize) + const rowsResult = await query( + ` + SELECT + o.*, + COALESCE(item_summary.items, '[]'::jsonb) AS items_json, + COALESCE(task_summary.task_count, 0)::int AS task_count + FROM orders o + LEFT JOIN LATERAL ( + SELECT jsonb_agg(jsonb_build_object( + 'orderItemId', oi.id, + 'skuCode', oi.sku_code, + 'skuName', oi.sku_name, + 'quantity', oi.quantity, + 'snapshot', oi.item_snapshot_json + ) ORDER BY oi.id ASC) AS items + FROM order_items oi + WHERE oi.order_id = o.id + ) item_summary ON TRUE + LEFT JOIN LATERAL ( + SELECT COUNT(*)::int AS task_count + FROM fulfillment_tasks ft + WHERE ft.order_id = o.id + ) task_summary ON TRUE + ${whereClause} + ORDER BY o.id DESC + LIMIT $${params.length - 1} OFFSET $${params.length} + `, + params, + ) + + return { + page: normalizedPage, + pageSize: normalizedPageSize, + total: Number(totalResult.rows[0]?.total || 0), + items: rowsResult.rows.map((row) => mapOpen91AdminOrderRow(row)), + } +} + +export async function getRequiredOpen91Order(orderId) { + const order = await getOrderById(Number(orderId)) + + if (!order || order.provider !== OPEN_91_PROVIDER || order.platform !== OPEN_91_PLATFORM) { + throw createHttpError('91卡券订单不存在', { + statusCode: 404, + errorCode: 'open91_order_not_found', + }) + } + + return order +} + +function buildOpen91SourceEventFromOrder(order, items = []) { + const rawPayload = parseJsonObject(order.raw_payload_json) + const body = parseJsonObject(rawPayload.body) + const normalizedItems = Array.isArray(items) ? items : [] + const eventItems = normalizedItems.map((item) => { + const snapshot = parseJsonObject(item.item_snapshot_json) + const productNo = String(snapshot.productNo || snapshot.externalSkuCode || item.sku_code || '').trim() + + return { + itemId: productNo, + externalItemId: String(snapshot.externalItemId || productNo).trim(), + externalSkuCode: String(snapshot.externalSkuCode || productNo).trim(), + externalSkuName: String(snapshot.externalSkuName || item.sku_name || productNo).trim(), + skuCode: productNo, + skuName: String(item.sku_name || productNo).trim(), + quantity: Math.max(1, Number(item.quantity || body.buyNum || 1) || 1), + spec: parseJsonObject(item.spec_json), + snapshot, + } + }) + + return { + provider: order.provider, + platform: order.platform, + shopId: order.shop_id, + shopName: order.shop_name, + platformOrderId: order.platform_order_id, + orderStatus: 'paid', + payStatus: 'paid', + buyerId: order.buyer_id || '', + buyerName: order.buyer_name || '', + receiverContact: order.receiver_contact || '', + totalAmount: Number(order.total_amount || 0) || 0, + currency: order.currency || 'CNY', + paidAt: order.paid_at || nowIso(), + rawPayload: { + ...rawPayload, + retriedAt: nowIso(), + }, + items: eventItems, + } +} + +function mapOpen91AdminOrderRow(row) { + const rawPayload = parseJsonObject(row.raw_payload_json) + const items = Array.isArray(row.items_json) ? row.items_json : [] + const firstItem = items[0] || {} + const firstSnapshot = parseJsonObject(firstItem.snapshot) + const failReason = String(rawPayload.manualFailedReason || '').trim() + + return { + orderId: Number(row.id || 0), + orderNo: String(row.platform_order_id || '').trim(), + outTradeNo: buildOpen91OutTradeNo(row), + orderStatus: String(row.order_status || '').trim(), + payStatus: String(row.pay_status || '').trim(), + shopId: String(row.shop_id || '').trim(), + shopName: String(row.shop_name || '').trim(), + productNo: String(firstSnapshot.productNo || firstSnapshot.externalSkuCode || firstItem.skuCode || '').trim(), + productName: String(firstSnapshot.externalSkuName || firstItem.skuName || '').trim(), + buyNum: Number(firstItem.quantity || 0), + taskCount: Number(row.task_count || 0), + failReason, + createdAt: row.created_at, + updatedAt: row.updated_at, + items, + } +} + +function parseJsonObject(value) { + if (!value) { + return {} + } + + if (typeof value === 'object' && !Array.isArray(value)) { + return value + } + + try { + const parsed = JSON.parse(String(value || '{}')) + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} + } catch { + return {} + } +} diff --git a/apps/backend/src/services/platforms/ninetyone/order-service.test.js b/apps/backend/src/services/platforms/ninetyone/order-service.test.js new file mode 100644 index 00000000..27a8cb52 --- /dev/null +++ b/apps/backend/src/services/platforms/ninetyone/order-service.test.js @@ -0,0 +1,31 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { buildOpen91SourceEvent } from './order-service.js' + +test('buildOpen91SourceEvent maps 91 productNo into source item identities', () => { + const event = buildOpen91SourceEvent({ + orderNo: 'P91KS202605130001', + productNo: 'KS-CLOUD-SKU-001', + buyNum: 2, + maxAmount: '0.01', + timestamp: 1778670308, + version: '1.0', + sign: 'SIGN', + }, { + shopId: '91kaquan', + shopName: '91卡券', + }) + + assert.equal(event.provider, '91kaquan') + assert.equal(event.platform, 'kuaishou') + assert.equal(event.shopId, '91kaquan') + assert.equal(event.platformOrderId, 'P91KS202605130001') + assert.equal(event.payStatus, 'paid') + assert.equal(event.items.length, 1) + assert.equal(event.items[0].externalItemId, 'KS-CLOUD-SKU-001') + assert.equal(event.items[0].externalSkuCode, 'KS-CLOUD-SKU-001') + assert.equal(event.items[0].externalSkuName, 'KS-CLOUD-SKU-001') + assert.equal(event.items[0].quantity, 2) + assert.equal(event.items[0].snapshot.productNo, 'KS-CLOUD-SKU-001') +}) diff --git a/apps/backend/src/types/admin-route-inputs.js b/apps/backend/src/types/admin-route-inputs.js index efbb86da..31c52bc7 100644 --- a/apps/backend/src/types/admin-route-inputs.js +++ b/apps/backend/src/types/admin-route-inputs.js @@ -157,3 +157,7 @@ export {} /** * @typedef {{ eventId?: string }} AdminWebhookEventRouteParams */ + +/** + * @typedef {{ id?: string }} AdminEntityRouteParams + */ diff --git a/apps/frontend/src/components.d.ts b/apps/frontend/src/components.d.ts index 95a6f5d3..64def359 100644 --- a/apps/frontend/src/components.d.ts +++ b/apps/frontend/src/components.d.ts @@ -16,9 +16,11 @@ declare module 'vue' { AdminPlatformCloudtentaclesSection: typeof import('./components/admin/AdminPlatformCloudtentaclesSection.vue')['default'] AdminPlatformKhhaoSection: typeof import('./components/admin/AdminPlatformKhhaoSection.vue')['default'] AdminPlatformKuaishouEticketSection: typeof import('./components/admin/AdminPlatformKuaishouEticketSection.vue')['default'] + AdminPlatformNinetyoneSection: typeof import('./components/admin/AdminPlatformNinetyoneSection.vue')['default'] AdminResultCard: typeof import('./components/admin/AdminResultCard.vue')['default'] AdminStatusTag: typeof import('./components/admin/AdminStatusTag.vue')['default'] ElButton: typeof import('element-plus/es')['ElButton'] + ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCollapse: typeof import('element-plus/es')['ElCollapse'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] diff --git a/apps/frontend/src/components/admin/AdminPlatformNinetyoneSection.vue b/apps/frontend/src/components/admin/AdminPlatformNinetyoneSection.vue new file mode 100644 index 00000000..a05bf167 --- /dev/null +++ b/apps/frontend/src/components/admin/AdminPlatformNinetyoneSection.vue @@ -0,0 +1,119 @@ + + + diff --git a/apps/frontend/src/composables/admin/platform-shops/types.ts b/apps/frontend/src/composables/admin/platform-shops/types.ts index 223b62ce..c3169048 100644 --- a/apps/frontend/src/composables/admin/platform-shops/types.ts +++ b/apps/frontend/src/composables/admin/platform-shops/types.ts @@ -1,4 +1,4 @@ -export type PlatformTab = 'agiso' | 'khhao' | 'kuaishouEticket' | 'cloudtentacles' +export type PlatformTab = 'agiso' | 'khhao' | 'ninetyone' | 'kuaishouEticket' | 'cloudtentacles' export type EditableDefaults = { messageTemplate: string diff --git a/apps/frontend/src/composables/admin/platform-shops/useAdminNinetyonePlatform.ts b/apps/frontend/src/composables/admin/platform-shops/useAdminNinetyonePlatform.ts new file mode 100644 index 00000000..026bfc89 --- /dev/null +++ b/apps/frontend/src/composables/admin/platform-shops/useAdminNinetyonePlatform.ts @@ -0,0 +1,117 @@ +import { computed, ref } from 'vue' + +import { showConfirm, showError, showSuccess } from '@/lib/feedback' +import { + failAdminNinetyoneOrder, + fetchAdminNinetyoneOrders, + retryAdminNinetyoneOrder, +} from '@/services/admin' +import type { AdminNinetyoneOrderItem, AdminNinetyoneOrderListResult } from '@/types/admin' + +export function useAdminNinetyonePlatform() { + const ninetyoneLoading = ref(false) + const ninetyoneActionLoadingId = ref(null) + const ninetyoneResultError = ref('') + const ninetyoneStatus = ref<'pending_config' | 'all' | 'manual_failed'>('pending_config') + const ninetyoneOrders = ref({ + page: 1, + pageSize: 20, + total: 0, + items: [], + }) + + const ninetyoneStats = computed(() => { + const pendingCount = ninetyoneOrders.value.items.filter((item) => item.orderStatus === 'pending_config').length + const failedCount = ninetyoneOrders.value.items.filter((item) => item.orderStatus === 'manual_failed').length + const taskReadyCount = ninetyoneOrders.value.items.filter((item) => item.taskCount > 0).length + + return { + total: ninetyoneOrders.value.total, + currentCount: ninetyoneOrders.value.items.length, + pendingCount, + failedCount, + taskReadyCount, + } + }) + + async function loadNinetyoneOrders(page = 1) { + ninetyoneLoading.value = true + ninetyoneResultError.value = '' + + try { + const response = await fetchAdminNinetyoneOrders({ + page, + pageSize: ninetyoneOrders.value.pageSize || 20, + status: ninetyoneStatus.value, + }) + ninetyoneOrders.value = response.data + } catch (error) { + ninetyoneResultError.value = error instanceof Error ? error.message : '91卡券订单读取失败' + showError(ninetyoneResultError.value) + } finally { + ninetyoneLoading.value = false + } + } + + async function handleNinetyoneStatusChange(status: 'pending_config' | 'all' | 'manual_failed') { + ninetyoneStatus.value = status + await loadNinetyoneOrders(1) + } + + async function handleNinetyoneRetryOrder(item: AdminNinetyoneOrderItem) { + ninetyoneActionLoadingId.value = item.orderId + ninetyoneResultError.value = '' + + try { + const response = await retryAdminNinetyoneOrder(item.orderId) + showSuccess(`91卡券订单已重试,生成任务 ${response.data.taskCount} 个`) + await loadNinetyoneOrders(ninetyoneOrders.value.page || 1) + } catch (error) { + ninetyoneResultError.value = error instanceof Error ? error.message : '91卡券订单重试失败' + showError(ninetyoneResultError.value) + } finally { + ninetyoneActionLoadingId.value = null + } + } + + async function handleNinetyoneFailOrder(item: AdminNinetyoneOrderItem) { + try { + await showConfirm(`确认把 91卡券订单 ${item.orderNo} 标记为无法履约吗?查询接口会返回失败状态。`, '确认标记失败', { + type: 'warning', + confirmButtonText: '标记失败', + cancelButtonText: '取消', + }) + } catch { + return + } + + ninetyoneActionLoadingId.value = item.orderId + ninetyoneResultError.value = '' + + try { + await failAdminNinetyoneOrder(item.orderId, { + reason: '商家后台手动标记无法履约', + }) + showSuccess('91卡券订单已标记失败') + await loadNinetyoneOrders(ninetyoneOrders.value.page || 1) + } catch (error) { + ninetyoneResultError.value = error instanceof Error ? error.message : '91卡券订单标记失败失败' + showError(ninetyoneResultError.value) + } finally { + ninetyoneActionLoadingId.value = null + } + } + + return { + ninetyoneLoading, + ninetyoneActionLoadingId, + ninetyoneResultError, + ninetyoneStatus, + ninetyoneOrders, + ninetyoneStats, + loadNinetyoneOrders, + handleNinetyoneStatusChange, + handleNinetyoneRetryOrder, + handleNinetyoneFailOrder, + } +} diff --git a/apps/frontend/src/services/admin/platform-config.ts b/apps/frontend/src/services/admin/platform-config.ts index ea9137ac..2ea90837 100644 --- a/apps/frontend/src/services/admin/platform-config.ts +++ b/apps/frontend/src/services/admin/platform-config.ts @@ -16,6 +16,8 @@ import type { AdminKhhaoOrderSyncResult, AdminKhhaoSourceConfig, AdminKhhaoSyncState, + AdminNinetyoneOrderActionResult, + AdminNinetyoneOrderListResult, AdminKuaishouCloudFulfillmentConfig, AdminKuaishouEticketConsumeResult, AdminKuaishouEticketDetailResult, @@ -94,6 +96,41 @@ export function syncAdminKhhaoOrders(payload: { return apiPost('/api/v1/admin/platform-config/khhao/sync-orders', payload) } +export function fetchAdminNinetyoneOrders(params: { + page?: number + pageSize?: number + status?: string +} = {}) { + const searchParams = new URLSearchParams() + if (params.page) { + searchParams.set('page', String(params.page)) + } + if (params.pageSize) { + searchParams.set('pageSize', String(params.pageSize)) + } + if (params.status) { + searchParams.set('status', params.status) + } + const queryString = searchParams.toString() + return apiGet( + `/api/v1/admin/platform-config/ninetyone/orders${queryString ? `?${queryString}` : ''}`, + ) +} + +export function retryAdminNinetyoneOrder(orderId: number | string) { + return apiPost( + `/api/v1/admin/platform-config/ninetyone/orders/${orderId}/retry`, + {}, + ) +} + +export function failAdminNinetyoneOrder(orderId: number | string, payload: { reason?: string }) { + return apiPost( + `/api/v1/admin/platform-config/ninetyone/orders/${orderId}/fail`, + payload, + ) +} + export function fetchAdminKuaishouEticketSourceConfig() { return apiGet<{ filePath: string diff --git a/apps/frontend/src/styles/admin-platform-shops.css b/apps/frontend/src/styles/admin-platform-shops.css index f3c7a3db..6bdfc8c5 100644 --- a/apps/frontend/src/styles/admin-platform-shops.css +++ b/apps/frontend/src/styles/admin-platform-shops.css @@ -54,6 +54,13 @@ flex-wrap: wrap; } +.admin-platform-shops .row-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + .admin-platform-shops .overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); @@ -550,4 +557,4 @@ .admin-platform-shops .shop-summary-side { justify-items: start; } -} \ No newline at end of file +} diff --git a/apps/frontend/src/types/admin.ts b/apps/frontend/src/types/admin.ts index 76568ead..8e8617b5 100644 --- a/apps/frontend/src/types/admin.ts +++ b/apps/frontend/src/types/admin.ts @@ -271,6 +271,39 @@ export interface AdminKhhaoOrderSyncResult { results: AdminKhhaoOrderSyncItem[] } +export interface AdminNinetyoneOrderItem { + orderId: number + orderNo: string + outTradeNo: string + orderStatus: string + payStatus: string + shopId: string + shopName: string + productNo: string + productName: string + buyNum: number + taskCount: number + failReason: string + createdAt: string + updatedAt: string + items: Array> +} + +export interface AdminNinetyoneOrderListResult { + page: number + pageSize: number + total: number + items: AdminNinetyoneOrderItem[] +} + +export interface AdminNinetyoneOrderActionResult { + orderId: number + orderNo: string + orderStatus: string + orderItemCount: number + taskCount: number +} + export interface AdminCloudtentaclesSourceConfig { enabled: boolean baseUrl: string diff --git a/apps/frontend/src/views/admin/AdminKuaishouCloudFulfillmentView.vue b/apps/frontend/src/views/admin/AdminKuaishouCloudFulfillmentView.vue index 37cfca4d..4b3a8afc 100644 --- a/apps/frontend/src/views/admin/AdminKuaishouCloudFulfillmentView.vue +++ b/apps/frontend/src/views/admin/AdminKuaishouCloudFulfillmentView.vue @@ -6,6 +6,7 @@ import { fetchAdminCloudtentaclesSkuList, fetchAdminKhhaoSourceConfig, fetchAdminKuaishouCloudFulfillmentConfig, + fetchAdminNinetyoneOrders, queryAdminKhhaoOrders, saveAdminKuaishouCloudFulfillmentConfig, } from '@/services/admin' @@ -14,6 +15,7 @@ import type { AdminKhhaoOrderPreview, AdminKuaishouCloudFulfillmentConfig, AdminKuaishouCloudFulfillmentItem, + AdminNinetyoneOrderItem, } from '@/types/admin' import { hasAdminRole } from '@/utils/admin-auth' @@ -41,6 +43,10 @@ const khhaoLookupLoading = ref(false) const khhaoLookupErrorMessage = ref('') const khhaoLookupResults = ref([]) const importedKhhaoOrderKeys = ref([]) +const ninetyoneLookupLoading = ref(false) +const ninetyoneLookupErrorMessage = ref('') +const ninetyoneLookupResults = ref([]) +const importedNinetyoneOrderKeys = ref([]) const cloudSkuCatalogLoading = ref(false) const cloudSkuCatalogErrorMessage = ref('') const cloudSkuCatalog = ref([]) @@ -52,6 +58,11 @@ const khhaoLookupForm = reactive({ limit: 10, maxCaptchaAttempts: 3, }) +const ninetyoneLookupForm = reactive({ + status: 'pending_config' as 'pending_config' | 'all' | 'manual_failed', + page: 1, + pageSize: 20, +}) const metrics = computed(() => { const readyCount = items.value.filter(isItemComplete).length @@ -90,6 +101,17 @@ const khhaoLookupMetrics = computed(() => { } }) +const ninetyoneLookupMetrics = computed(() => { + const importedCount = ninetyoneLookupResults.value.filter((item) => isNinetyoneProductImported(item)).length + const pendingCount = ninetyoneLookupResults.value.filter((item) => item.orderStatus === 'pending_config').length + return { + total: ninetyoneLookupResults.value.length, + importedCount, + availableCount: Math.max(ninetyoneLookupResults.value.length - importedCount, 0), + pendingCount, + } +}) + function createEmptyItem(): EditableItem { return { localId: crypto.randomUUID(), @@ -423,6 +445,38 @@ function createItemFromKhhaoOrder(item: AdminKhhaoOrderPreview): EditableItem { } } +function createItemFromNinetyoneOrder(item: AdminNinetyoneOrderItem): EditableItem { + const productNo = String(item.productNo || '').trim() + const productName = String(item.productName || productNo).trim() + return { + localId: crypto.randomUUID(), + id: '', + enabled: true, + priority: 100, + provider: '91kaquan', + platform: 'kuaishou', + shopId: item.shopId || '91kaquan', + khhaoShopId: '', + internalSkuCode: '', + internalSkuName: productName, + externalSkuCode: productNo, + externalItemId: productNo, + externalSkuName: productName, + resolvedSkuName: productName, + cloudSourceKey: 'default', + cloudSkuId: 0, + cloudSkuName: '', + vnKey: '1', + autoBuyEnabled: true, + minAssetReserve: 0, + autoReturnNumberAfterDispatch: false, + autoConsumeAfterDispatch: false, + kuaishouConsumeShopId: '', + kuaishouConsumeShopName: '', + notes: `从 91卡券订单 ${item.orderNo} 导入`, + } +} + function findExistingItemFromKhhaoOrder(order: AdminKhhaoOrderPreview) { const orderShopId = String(order.kuaishouShopId || order.shopId || '').trim() const orderItemId = String(order.itemId || '').trim() @@ -441,6 +495,23 @@ function findExistingItemFromKhhaoOrder(order: AdminKhhaoOrderPreview) { }) || null } +function findExistingItemFromNinetyoneOrder(order: AdminNinetyoneOrderItem) { + const productNo = String(order.productNo || '').trim() + const productName = String(order.productName || '').trim() + const shopId = String(order.shopId || '91kaquan').trim() + + return items.value.find((item) => { + const sameSource = item.provider.trim() === '91kaquan' && item.platform.trim() === 'kuaishou' + const sameShop = !shopId || item.shopId.trim() === shopId + const sameProductNo = productNo && ( + item.externalSkuCode.trim() === productNo + || item.externalItemId.trim() === productNo + ) + const sameName = productName && [item.externalSkuName, item.internalSkuName, item.resolvedSkuName].some((value) => value.trim() === productName) + return sameSource && sameShop && (sameProductNo || sameName) + }) || null +} + async function importKhhaoProduct(item: AdminKhhaoOrderPreview) { validationState.value = null const importKey = getKhhaoOrderImportKey(item) @@ -466,6 +537,31 @@ async function importKhhaoProduct(item: AdminKhhaoOrderPreview) { await focusValidationTarget(next.localId) } +async function importNinetyoneProduct(item: AdminNinetyoneOrderItem) { + validationState.value = null + const importKey = getNinetyoneOrderImportKey(item) + const existing = findExistingItemFromNinetyoneOrder(item) + if (existing) { + setCollapsed(existing.localId, false) + if (!importedNinetyoneOrderKeys.value.includes(importKey)) { + importedNinetyoneOrderKeys.value = [...importedNinetyoneOrderKeys.value, importKey] + } + showSuccess('已定位到现有 91卡券规则草稿,直接继续完善即可') + await focusValidationTarget(existing.localId) + return + } + + const next = createItemFromNinetyoneOrder(item) + items.value.unshift(next) + setCollapsed(next.localId, false) + + if (!importedNinetyoneOrderKeys.value.includes(importKey)) { + importedNinetyoneOrderKeys.value = [...importedNinetyoneOrderKeys.value, importKey] + } + + await focusValidationTarget(next.localId) +} + function getKhhaoOrderImportKey(item: AdminKhhaoOrderPreview) { return [ item.platformOrderId, @@ -481,6 +577,20 @@ function isKhhaoProductImported(item: AdminKhhaoOrderPreview) { return importedKhhaoOrderKeys.value.includes(getKhhaoOrderImportKey(item)) } +function getNinetyoneOrderImportKey(item: AdminNinetyoneOrderItem) { + return [ + item.orderNo, + item.productNo, + item.shopId || '91kaquan', + ] + .map((value) => String(value || '').trim()) + .join(':') +} + +function isNinetyoneProductImported(item: AdminNinetyoneOrderItem) { + return importedNinetyoneOrderKeys.value.includes(getNinetyoneOrderImportKey(item)) +} + async function ensureCloudSkuCatalogLoaded(force = false) { if (!force && cloudSkuCatalog.value.length > 0) { return cloudSkuCatalog.value @@ -686,6 +796,26 @@ async function lookupKhhaoProducts() { } } +async function lookupNinetyoneProducts() { + ninetyoneLookupLoading.value = true + ninetyoneLookupErrorMessage.value = '' + + try { + const response = await fetchAdminNinetyoneOrders({ + page: Number(ninetyoneLookupForm.page || 1), + pageSize: Number(ninetyoneLookupForm.pageSize || 20), + status: ninetyoneLookupForm.status, + }) + ninetyoneLookupResults.value = response.data.items + importedNinetyoneOrderKeys.value = [] + } catch (error) { + ninetyoneLookupResults.value = [] + ninetyoneLookupErrorMessage.value = error instanceof Error ? error.message : '91卡券订单查询失败' + } finally { + ninetyoneLookupLoading.value = false + } +} + onMounted(loadConfigs) @@ -745,6 +875,7 @@ onMounted(loadConfigs)
khhao 订单只用于取样导入,不写入订单库 + 91卡券订单来自已接收的待补全队列 内部 SKU 决定最终任务与库存绑定 cloud SKU 决定自动购买、发货与退号资源
@@ -860,6 +991,104 @@ onMounted(loadConfigs) +
+
+
+

91卡券订单取样导入

+

查询已接收的 91卡券订单,把 productNo 导入为规则草稿,再补齐内部 SKU 和 cloud 资源。

+
+
+ 结果 {{ ninetyoneLookupMetrics.total }} + 待补 {{ ninetyoneLookupMetrics.pendingCount }} + 待导入 {{ ninetyoneLookupMetrics.availableCount }} +
+
+ +
+ + + +
+ 操作 + 查询 91卡券订单 +
+
+ +

导入后会创建 provider=91kaquan、platform=kuaishou、外部 SKU=productNo 的规则草稿;保存规则后,回到“平台配置 -> 91卡券接入”重试订单。

+ +

{{ ninetyoneLookupErrorMessage }}

+
正在查询 91卡券订单…
+ + + + + + + + + + + + + + + + + + + + + + + +
订单来源商品状态操作
+
+ {{ item.orderNo || '-' }} + {{ item.outTradeNo || '-' }} +
+
+
+ {{ item.shopName || '91卡券' }} + provider: 91kaquan + shopId: {{ item.shopId || '91kaquan' }} +
+
+
+ {{ item.productName || item.productNo || '-' }} + productNo: {{ item.productNo || '-' }} + 数量:{{ item.buyNum || 0 }} +
+
+
+ {{ item.orderStatus || '-' }} + 任务 {{ item.taskCount || 0 }} +
+
+ + 导入到规则 + + 已导入草稿 +
还没有 91卡券查询结果。
+
+
@@ -924,13 +1153,13 @@ onMounted(loadConfigs)
外部商品 - khhao / 快手侧命中条件 + 来源平台 / 快手侧命中条件
+ + +