支持91商品携带店铺编号

This commit is contained in:
yml
2026-05-27 13:30:47 +08:00
parent c1f3a48634
commit 64b663a8f6
7 changed files with 173 additions and 56 deletions
@@ -184,6 +184,8 @@ test('syncDeliveryTasksForOrderWithDeps creates kuaishou cloud task from cloudte
assert.equal(context.kuaishouCloudFulfillment.binding.skuId, 28)
assert.equal(context.kuaishouCloudFulfillment.binding.skuName, '套装-浪漫天命')
assert.deepEqual(context.kuaishouCloudFulfillment.binding.cloudSourceKeys, ['account-a', 'account-b'])
assert.equal(context.kuaishouCloudFulfillment.consume.shopId, 'shop-1')
assert.equal(context.kuaishouCloudFulfillment.consume.shopName, '测试店铺')
assert.deepEqual(context.kuaishouCloudFulfillment.deliveryItems, [
{
cloudSkuId: 28,
@@ -225,7 +225,7 @@ export async function syncDeliveryTasksForOrderWithDeps(
},
consume: {
status: 'pending',
shopId: String(kuaishouConsumeConfig.shopId || kuaishouShopConfig.shopId || '').trim(),
shopId: String(kuaishouConsumeConfig.shopId || kuaishouShopConfig.shopId || order.shop_id || '').trim(),
shopName: String(kuaishouConsumeConfig.shopName || kuaishouShopConfig.kshopName || order.shop_name || '').trim(),
autoConsumeEnabled: kuaishouConsumeConfig.autoConsumeAfterDispatch === true,
consumedAt: null,
@@ -1,7 +1,10 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { buildOpen91SourceEvent } from './order-service.js'
import {
buildOpen91SourceEvent,
parseOpen91ProductNo,
} from './order-service.js'
test('buildOpen91SourceEvent maps 91 productNo into source item identities', () => {
const event = buildOpen91SourceEvent({
@@ -29,3 +32,39 @@ test('buildOpen91SourceEvent maps 91 productNo into source item identities', ()
assert.equal(event.items[0].quantity, 2)
assert.equal(event.items[0].snapshot.productNo, 'KS-CLOUD-SKU-001')
})
test('parseOpen91ProductNo splits product name and kuaishou shop id by four dashes', () => {
assert.deepEqual(
parseOpen91ProductNo('测试-关联商品1----3676797936'),
{
rawProductNo: '测试-关联商品1----3676797936',
productName: '测试-关联商品1',
shopId: '3676797936',
},
)
})
test('buildOpen91SourceEvent uses productNo suffix as order shop id', () => {
const event = buildOpen91SourceEvent({
orderNo: 'P91KS202605130002',
productNo: '测试-关联商品1----3676797936',
buyNum: 1,
maxAmount: '0.01',
timestamp: 1778670308,
version: '1.0',
sign: 'SIGN',
}, {
shopId: '91kaquan',
shopName: '91卡券',
})
assert.equal(event.shopId, '3676797936')
assert.equal(event.items[0].externalItemId, '测试-关联商品1----3676797936')
assert.equal(event.items[0].externalSkuCode, '测试-关联商品1----3676797936')
assert.equal(event.items[0].externalSkuName, '测试-关联商品1')
assert.equal(event.items[0].skuCode, '测试-关联商品1')
assert.equal(event.items[0].skuName, '测试-关联商品1')
assert.equal(event.items[0].snapshot.productNo, '测试-关联商品1----3676797936')
assert.equal(event.items[0].snapshot.productName, '测试-关联商品1')
assert.equal(event.items[0].snapshot.shopId, '3676797936')
})
@@ -12,6 +12,7 @@ import { buildOpen91OutTradeNo } from '../../open-91/response.js'
import { createHttpError } from '../../../utils/http.js'
import { parseAmountToFen } from '../../../utils/money.js'
import { nowIso } from '../../../utils/time.js'
import { findKuaishouEticketShopConfig } from '../kuaishou-eticket/source-config-service.js'
import type { OrderItemRow, OrderRow } from '../../../types/repository/rows.js'
export const OPEN_91_PENDING_CONFIG_STATUS = 'pending_config'
@@ -21,14 +22,19 @@ type JsonObject = Record<string, any>
export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObject = {}) {
const normalized = normalizeOpen91CreatePayload(payload)
const productNo = normalized.productNo
const productInfo = parseOpen91ProductNo(normalized.productNo)
const productNo = productInfo.rawProductNo
const productName = productInfo.productName
const shopId = productInfo.shopId || String(config.shopId || OPEN_91_PROVIDER).trim() || OPEN_91_PROVIDER
const shopConfig = productInfo.shopId ? findKuaishouEticketShopConfig(productInfo.shopId) : null
const shopName = String(shopConfig?.kshopName || config.shopName || '91卡券').trim() || '91卡券'
const maxAmountFen = parseAmountToFen(normalized.maxAmount)
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卡券',
shopId,
shopName,
platformOrderId: normalized.orderNo,
orderStatus: 'paid',
payStatus: 'paid',
@@ -48,25 +54,29 @@ export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObj
itemId: productNo,
externalItemId: productNo,
externalSkuCode: productNo,
externalSkuName: productNo,
skuCode: productNo,
skuName: productNo,
externalSkuName: productName,
skuCode: productName,
skuName: productName,
quantity: normalized.buyNum,
spec: {
source: OPEN_91_PROVIDER,
orderNo: normalized.orderNo,
productNo,
productName,
shopId: productInfo.shopId,
maxAmount: normalized.maxAmount,
},
snapshot: {
source: OPEN_91_PROVIDER,
orderNo: normalized.orderNo,
productNo,
productName,
shopId: productInfo.shopId,
maxAmount: normalized.maxAmount,
maxAmountFen,
externalItemId: productNo,
externalSkuCode: productNo,
externalSkuName: productNo,
externalSkuName: productName,
callbackUrl: normalized.callbackUrl,
},
},
@@ -74,6 +84,37 @@ export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObj
}
}
export function parseOpen91ProductNo(value: unknown) {
const rawProductNo = String(value || '').trim()
const delimiter = '----'
const delimiterIndex = rawProductNo.lastIndexOf(delimiter)
if (delimiterIndex <= 0) {
return {
rawProductNo,
productName: rawProductNo,
shopId: '',
}
}
const productName = rawProductNo.slice(0, delimiterIndex).trim()
const shopId = rawProductNo.slice(delimiterIndex + delimiter.length).trim()
if (!productName || !shopId) {
return {
rawProductNo,
productName: rawProductNo,
shopId: '',
}
}
return {
rawProductNo,
productName,
shopId,
}
}
export async function upsertOpen91PendingOrder(payload: JsonObject = {}, config: JsonObject = {}) {
const event = buildOpen91SourceEvent(payload, config)
const now = nowIso()
@@ -141,6 +141,13 @@
align-items: flex-start;
}
.catalog-toolbar-actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
justify-content: flex-end;
}
.match-checker {
display: grid;
grid-template-columns: minmax(260px, 1fr) auto;
@@ -156,6 +163,14 @@
width: 100%;
}
.sku-list-panel {
display: grid;
gap: var(--space-3);
min-width: 0;
padding-top: var(--space-2);
border-top: 1px solid var(--border-default);
}
.sku-name-cell {
display: grid;
gap: 4px;
@@ -186,7 +201,10 @@
display: grid;
gap: var(--space-4);
min-width: 0;
padding-top: var(--space-2);
padding: var(--space-4);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background: var(--bg-subtle);
}
.override-actions {
@@ -242,6 +260,7 @@
.overview-actions,
.catalog-toolbar,
.catalog-toolbar-actions,
.override-actions {
justify-content: stretch;
}
@@ -362,14 +362,16 @@ function createRuleId() {
<main class="catalog-panel">
<div class="catalog-toolbar">
<div class="section-head">
<h3>商品列表与匹配状态</h3>
<h3>匹配状态与覆盖规则</h3>
<p v-if="selectedSource">
当前账号{{ formatSourceLabel(selectedSource) }}
</p>
</div>
<el-button :loading="skuLoading" :disabled="!selectedSourceKey" @click="loadSkuList()">
刷新商品
</el-button>
<div class="catalog-toolbar-actions">
<el-button :loading="skuLoading" :disabled="!selectedSourceKey" @click="loadSkuList()">
刷新商品
</el-button>
</div>
</div>
<el-alert
@@ -394,45 +396,6 @@ function createRuleId() {
</el-tag>
</div>
<el-table
v-loading="skuLoading"
:data="skuItems"
border
stripe
class="sku-table"
empty-text="当前账号暂无可展示商品"
>
<el-table-column label="cloudtentacles 商品" min-width="260">
<template #default="{ row }">
<div class="sku-name-cell">
<strong>{{ row.name || '-' }}</strong>
<small>#{{ row.id }} · {{ row.description || row.name || '-' }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="91 自动匹配名" min-width="220">
<template #default="{ row }">
<code class="match-name">{{ row.name || '-' }}</code>
</template>
</el-table-column>
<el-table-column label="价格" width="120">
<template #default="{ row }">{{ formatCloudPrice(row.price) }}</template>
</el-table-column>
<el-table-column label="库存" prop="inventory" width="120" sortable />
<el-table-column label="发货限制" width="150">
<template #default="{ row }">
{{ row.buyLimitMin || 1 }} - {{ row.buyLimitMax || 1 }}
</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tag :type="Number(row.inventory || 0) > 0 ? 'success' : 'danger'" effect="light">
{{ Number(row.inventory || 0) > 0 ? '可履约' : '无库存' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<section class="override-panel">
<div class="catalog-toolbar">
<div class="section-head">
@@ -522,6 +485,53 @@ function createRuleId() {
</article>
</div>
</section>
<section class="sku-list-panel">
<div class="section-head">
<h3>cloudtentacles 商品列表</h3>
<p>用于核对商品名库存和覆盖规则里的发货商品</p>
</div>
<el-table
v-loading="skuLoading"
:data="skuItems"
height="420"
border
stripe
class="sku-table"
empty-text="当前账号暂无可展示商品"
>
<el-table-column label="cloudtentacles 商品" min-width="260">
<template #default="{ row }">
<div class="sku-name-cell">
<strong>{{ row.name || '-' }}</strong>
<small>#{{ row.id }} · {{ row.description || row.name || '-' }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="91 自动匹配名" min-width="220">
<template #default="{ row }">
<code class="match-name">{{ row.name || '-' }}</code>
</template>
</el-table-column>
<el-table-column label="价格" width="120">
<template #default="{ row }">{{ formatCloudPrice(row.price) }}</template>
</el-table-column>
<el-table-column label="库存" prop="inventory" width="120" sortable />
<el-table-column label="发货限制" width="150">
<template #default="{ row }">
{{ row.buyLimitMin || 1 }} - {{ row.buyLimitMax || 1 }}
</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tag :type="Number(row.inventory || 0) > 0 ? 'success' : 'danger'" effect="light">
{{ Number(row.inventory || 0) > 0 ? '可履约' : '无库存' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</section>
</main>
</section>
</template>