支持91商品携带店铺编号
This commit is contained in:
@@ -184,6 +184,8 @@ test('syncDeliveryTasksForOrderWithDeps creates kuaishou cloud task from cloudte
|
|||||||
assert.equal(context.kuaishouCloudFulfillment.binding.skuId, 28)
|
assert.equal(context.kuaishouCloudFulfillment.binding.skuId, 28)
|
||||||
assert.equal(context.kuaishouCloudFulfillment.binding.skuName, '套装-浪漫天命')
|
assert.equal(context.kuaishouCloudFulfillment.binding.skuName, '套装-浪漫天命')
|
||||||
assert.deepEqual(context.kuaishouCloudFulfillment.binding.cloudSourceKeys, ['account-a', 'account-b'])
|
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, [
|
assert.deepEqual(context.kuaishouCloudFulfillment.deliveryItems, [
|
||||||
{
|
{
|
||||||
cloudSkuId: 28,
|
cloudSkuId: 28,
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
|||||||
},
|
},
|
||||||
consume: {
|
consume: {
|
||||||
status: 'pending',
|
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(),
|
shopName: String(kuaishouConsumeConfig.shopName || kuaishouShopConfig.kshopName || order.shop_name || '').trim(),
|
||||||
autoConsumeEnabled: kuaishouConsumeConfig.autoConsumeAfterDispatch === true,
|
autoConsumeEnabled: kuaishouConsumeConfig.autoConsumeAfterDispatch === true,
|
||||||
consumedAt: null,
|
consumedAt: null,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
import assert from 'node:assert/strict'
|
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', () => {
|
test('buildOpen91SourceEvent maps 91 productNo into source item identities', () => {
|
||||||
const event = buildOpen91SourceEvent({
|
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].quantity, 2)
|
||||||
assert.equal(event.items[0].snapshot.productNo, 'KS-CLOUD-SKU-001')
|
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 { createHttpError } from '../../../utils/http.js'
|
||||||
import { parseAmountToFen } from '../../../utils/money.js'
|
import { parseAmountToFen } from '../../../utils/money.js'
|
||||||
import { nowIso } from '../../../utils/time.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'
|
import type { OrderItemRow, OrderRow } from '../../../types/repository/rows.js'
|
||||||
|
|
||||||
export const OPEN_91_PENDING_CONFIG_STATUS = 'pending_config'
|
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 = {}) {
|
export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObject = {}) {
|
||||||
const normalized = normalizeOpen91CreatePayload(payload)
|
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)
|
const maxAmountFen = parseAmountToFen(normalized.maxAmount)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provider: OPEN_91_PROVIDER,
|
provider: OPEN_91_PROVIDER,
|
||||||
platform: OPEN_91_PLATFORM,
|
platform: OPEN_91_PLATFORM,
|
||||||
shopId: String(config.shopId || OPEN_91_PROVIDER).trim() || OPEN_91_PROVIDER,
|
shopId,
|
||||||
shopName: String(config.shopName || '91卡券').trim() || '91卡券',
|
shopName,
|
||||||
platformOrderId: normalized.orderNo,
|
platformOrderId: normalized.orderNo,
|
||||||
orderStatus: 'paid',
|
orderStatus: 'paid',
|
||||||
payStatus: 'paid',
|
payStatus: 'paid',
|
||||||
@@ -48,25 +54,29 @@ export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObj
|
|||||||
itemId: productNo,
|
itemId: productNo,
|
||||||
externalItemId: productNo,
|
externalItemId: productNo,
|
||||||
externalSkuCode: productNo,
|
externalSkuCode: productNo,
|
||||||
externalSkuName: productNo,
|
externalSkuName: productName,
|
||||||
skuCode: productNo,
|
skuCode: productName,
|
||||||
skuName: productNo,
|
skuName: productName,
|
||||||
quantity: normalized.buyNum,
|
quantity: normalized.buyNum,
|
||||||
spec: {
|
spec: {
|
||||||
source: OPEN_91_PROVIDER,
|
source: OPEN_91_PROVIDER,
|
||||||
orderNo: normalized.orderNo,
|
orderNo: normalized.orderNo,
|
||||||
productNo,
|
productNo,
|
||||||
|
productName,
|
||||||
|
shopId: productInfo.shopId,
|
||||||
maxAmount: normalized.maxAmount,
|
maxAmount: normalized.maxAmount,
|
||||||
},
|
},
|
||||||
snapshot: {
|
snapshot: {
|
||||||
source: OPEN_91_PROVIDER,
|
source: OPEN_91_PROVIDER,
|
||||||
orderNo: normalized.orderNo,
|
orderNo: normalized.orderNo,
|
||||||
productNo,
|
productNo,
|
||||||
|
productName,
|
||||||
|
shopId: productInfo.shopId,
|
||||||
maxAmount: normalized.maxAmount,
|
maxAmount: normalized.maxAmount,
|
||||||
maxAmountFen,
|
maxAmountFen,
|
||||||
externalItemId: productNo,
|
externalItemId: productNo,
|
||||||
externalSkuCode: productNo,
|
externalSkuCode: productNo,
|
||||||
externalSkuName: productNo,
|
externalSkuName: productName,
|
||||||
callbackUrl: normalized.callbackUrl,
|
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 = {}) {
|
export async function upsertOpen91PendingOrder(payload: JsonObject = {}, config: JsonObject = {}) {
|
||||||
const event = buildOpen91SourceEvent(payload, config)
|
const event = buildOpen91SourceEvent(payload, config)
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
|
|||||||
+20
-1
@@ -141,6 +141,13 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalog-toolbar-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2);
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.match-checker {
|
.match-checker {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(260px, 1fr) auto;
|
grid-template-columns: minmax(260px, 1fr) auto;
|
||||||
@@ -156,6 +163,14 @@
|
|||||||
width: 100%;
|
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 {
|
.sku-name-cell {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@@ -186,7 +201,10 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
min-width: 0;
|
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 {
|
.override-actions {
|
||||||
@@ -242,6 +260,7 @@
|
|||||||
|
|
||||||
.overview-actions,
|
.overview-actions,
|
||||||
.catalog-toolbar,
|
.catalog-toolbar,
|
||||||
|
.catalog-toolbar-actions,
|
||||||
.override-actions {
|
.override-actions {
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-43
@@ -362,14 +362,16 @@ function createRuleId() {
|
|||||||
<main class="catalog-panel">
|
<main class="catalog-panel">
|
||||||
<div class="catalog-toolbar">
|
<div class="catalog-toolbar">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h3>商品列表与匹配状态</h3>
|
<h3>匹配状态与覆盖规则</h3>
|
||||||
<p v-if="selectedSource">
|
<p v-if="selectedSource">
|
||||||
当前账号:{{ formatSourceLabel(selectedSource) }}
|
当前账号:{{ formatSourceLabel(selectedSource) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<el-button :loading="skuLoading" :disabled="!selectedSourceKey" @click="loadSkuList()">
|
<div class="catalog-toolbar-actions">
|
||||||
刷新商品
|
<el-button :loading="skuLoading" :disabled="!selectedSourceKey" @click="loadSkuList()">
|
||||||
</el-button>
|
刷新商品
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-alert
|
<el-alert
|
||||||
@@ -394,45 +396,6 @@ function createRuleId() {
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</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">
|
<section class="override-panel">
|
||||||
<div class="catalog-toolbar">
|
<div class="catalog-toolbar">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
@@ -522,6 +485,53 @@ function createRuleId() {
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</main>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"orderNo": "2614700005853561",
|
"orderNo": "2614700005853561",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"maxAmount": "0.01",
|
"maxAmount": "0.01",
|
||||||
"productNo": "测试-关联商品1",
|
"productNo": "测试-关联商品1----3676797936",
|
||||||
"timestamp": 1779853484,
|
"timestamp": 1779853484,
|
||||||
"callbackUrl": ""
|
"callbackUrl": ""
|
||||||
},
|
},
|
||||||
@@ -16,6 +16,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
这是91 卡券发送来的查询信息, 可以获取 商品名字, 订单号 orderNo 和 最大允许金额(可以看作正常金额) maxAmount 和时间等等
|
这是91 卡券发送来的查询信息, 可以获取 商品名字, 订单号 orderNo 和 最大允许金额(可以看作正常金额) maxAmount 和时间等等。
|
||||||
这是最重要的信息
|
|
||||||
|
|
||||||
|
`productNo` 可以使用四个 `----` 携带快手店铺编号,例如:
|
||||||
|
|
||||||
|
`测试-关联商品1----3676797936`
|
||||||
|
|
||||||
|
系统会把 `测试-关联商品1` 当作商品名字匹配 cloudtentacles,把 `3676797936` 当作快手小店 shopId 用于核销。
|
||||||
|
|
||||||
|
这是最重要的信息
|
||||||
|
|||||||
Reference in New Issue
Block a user