fix(finance): 收敛内部 SKU 履约路由与载体状态
统一内部 SKU 与渠道主/兜底解析,禁止并列主渠道。 运营指定渠道改为先创建真实载体,失败保持明确 pending。 历史缺载体、多载体冲突和自动发货兜底统一由管道修复。 迁移人工配置与旧 sku_code 规则,补充严格预检和集成测试。
This commit is contained in:
@@ -4,6 +4,7 @@ import type { JsonObject } from '../../types/json.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { formatShanghaiDate, shiftDate } from '../../utils/finance-date-range.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { upsertInternalSkuChannel } from '../fulfillment/internal-sku-channel-service.js'
|
||||
|
||||
/**
|
||||
* 人工履约 SKU 配置服务。
|
||||
@@ -125,7 +126,7 @@ export async function scanManualSkus(options: ScanOptions) {
|
||||
oi.sku_name,
|
||||
COALESCE(oi.revenue_amount_fen, 0)::bigint AS revenue_fen,
|
||||
(COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date AS stat_date,
|
||||
oi.manual_sku_key AS sku_key
|
||||
resolve_internal_sku(oi.manual_sku_key, oi.sku_name, oi.sku_code) AS sku_key
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.fulfillment_status = 'assigned' AND oi.fulfillment_channel = 'manual'
|
||||
@@ -243,7 +244,7 @@ export async function listManualSkus(input: Record<string, unknown> = {}) {
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.fulfillment_status = 'assigned' AND oi.fulfillment_channel = 'manual'
|
||||
AND o.provider = $${params.length}
|
||||
AND oi.manual_sku_key = r.sku_key
|
||||
AND resolve_internal_sku(oi.manual_sku_key, oi.sku_name, oi.sku_code) = r.sku_key
|
||||
)`)
|
||||
}
|
||||
|
||||
@@ -408,6 +409,15 @@ export async function deleteManualFulfillmentConfig(skuKey: string) {
|
||||
[key],
|
||||
)
|
||||
await query(`DELETE FROM sku_channel_rules WHERE source = 'manual' AND match_key = $1`, [key])
|
||||
await upsertInternalSkuChannel({
|
||||
internalSku: key,
|
||||
channel: 'manual',
|
||||
priority: 90,
|
||||
isFallback: true,
|
||||
enabled: false,
|
||||
source: 'manual_config',
|
||||
configJson: { manualConfigId: null },
|
||||
})
|
||||
await syncRegistryConfigId(key, null)
|
||||
return { skuKey: key, deleted: deleted.rowCount || 0 }
|
||||
}
|
||||
@@ -435,7 +445,7 @@ export async function rematchManualFulfillments(input: Record<string, unknown> =
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
JOIN manual_fulfillment_configs cfg
|
||||
ON cfg.sku_key = oi.manual_sku_key
|
||||
ON cfg.sku_key = resolve_internal_sku(oi.manual_sku_key, oi.sku_name, oi.sku_code)
|
||||
WHERE oi.fulfillment_status = 'assigned' AND oi.fulfillment_channel = 'manual'
|
||||
AND cfg.enabled = TRUE
|
||||
AND ($1 = '' OR cfg.sku_key = $1)
|
||||
@@ -719,27 +729,57 @@ function requiredRow<T>(row: T | undefined, message: string): T {
|
||||
async function syncManualChannelRule(config: {
|
||||
id: number
|
||||
sku_key: string
|
||||
canonical_product_id: number | null
|
||||
enabled: boolean
|
||||
note: string
|
||||
}): Promise<void> {
|
||||
const skuKey = String(config.sku_key || '').trim()
|
||||
if (!skuKey) return
|
||||
|
||||
const otherChannel = await query<{ exists: boolean }>(
|
||||
`
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM internal_sku_channels
|
||||
WHERE btrim(internal_sku) = $1 AND enabled = TRUE AND channel <> 'manual'
|
||||
) AS exists
|
||||
`,
|
||||
[skuKey],
|
||||
)
|
||||
const hasOtherChannel = otherChannel.rows[0]?.exists === true
|
||||
await upsertInternalSkuChannel({
|
||||
internalSku: skuKey,
|
||||
channel: 'manual',
|
||||
priority: hasOtherChannel ? 90 : 10,
|
||||
isFallback: hasOtherChannel,
|
||||
enabled: config.enabled !== false,
|
||||
canonicalProductId: config.canonical_product_id,
|
||||
source: 'manual_config',
|
||||
configJson: { manualConfigId: config.id, note: String(config.note || '').slice(0, 200) },
|
||||
replacePriority: true,
|
||||
})
|
||||
|
||||
await query(
|
||||
`
|
||||
INSERT INTO sku_channel_rules (
|
||||
provider, shop_id, match_key_type, match_key, channel, priority,
|
||||
source, source_ref_id, enabled, note, created_at, updated_at
|
||||
)
|
||||
VALUES ('', '', 'internal_sku', $1, 'manual', 70, 'manual', $2, $3, $4, NOW(), NOW())
|
||||
VALUES ('', '', 'internal_sku', $1, 'manual', $5, 'manual', $2, $3, $4, NOW(), NOW())
|
||||
ON CONFLICT (provider, shop_id, match_key_type, match_key, channel)
|
||||
DO UPDATE SET
|
||||
enabled = EXCLUDED.enabled,
|
||||
priority = EXCLUDED.priority,
|
||||
source = 'manual',
|
||||
source_ref_id = EXCLUDED.source_ref_id,
|
||||
note = EXCLUDED.note,
|
||||
updated_at = NOW()
|
||||
`,
|
||||
[skuKey, config.id, config.enabled !== false, String(config.note || '').slice(0, 200)],
|
||||
[
|
||||
skuKey,
|
||||
config.id,
|
||||
config.enabled !== false,
|
||||
String(config.note || '').slice(0, 200),
|
||||
hasOtherChannel ? 90 : 10,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -215,21 +215,6 @@ async function ensureVoucherProductForItem(item: ItemRow): Promise<number | null
|
||||
[internalSku],
|
||||
)
|
||||
|
||||
const skuCode = String(item.sku_code || '').trim()
|
||||
if (skuCode) {
|
||||
await query(
|
||||
`
|
||||
INSERT INTO sku_channel_rules (
|
||||
provider, shop_id, match_key_type, match_key, channel, priority,
|
||||
source, enabled, note, created_at, updated_at
|
||||
)
|
||||
VALUES ($1, $2, 'sku_code', $3, 'third_party_voucher', 10, 'voucher', TRUE,
|
||||
'卡券订单自动建商品后生成', NOW(), NOW())
|
||||
ON CONFLICT (provider, shop_id, match_key_type, match_key, channel) DO NOTHING
|
||||
`,
|
||||
[item.provider, item.shop_id, skuCode],
|
||||
)
|
||||
}
|
||||
return productId
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user