feat(fulfillment): 建立正式履约影子模型

This commit is contained in:
yml2213
2026-11-05 21:07:11 +08:00
parent b424333a30
commit bd7dd4efcc
6 changed files with 1110 additions and 5 deletions
@@ -0,0 +1,46 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
const migration = readFileSync(
new URL('../../db/migrations/141_fulfillment_shadow_model.sql', import.meta.url),
'utf8',
)
const auditScript = readFileSync(
new URL('../../../scripts/audit-fulfillment-state.ts', import.meta.url),
'utf8',
)
test('正式履约影子模型包含履约、尝试和异常三张表', () => {
assert.ok(migration.includes('CREATE TABLE IF NOT EXISTS fulfillments'))
assert.ok(migration.includes('CREATE TABLE IF NOT EXISTS fulfillment_attempts'))
assert.ok(migration.includes('CREATE TABLE IF NOT EXISTS fulfillment_issues'))
assert.ok(migration.includes('uq_fulfillment_attempts_current_unit'))
assert.ok(migration.includes('UNIQUE (fulfillment_id, issue_code)'))
})
test('影子模型从现有真源回填并持续双写', () => {
assert.ok(migration.includes('SELECT sync_fulfillment_shadow(state.order_item_id)'))
assert.ok(migration.includes('trg_order_items_fulfillment_shadow'))
assert.ok(migration.includes('trg_fulfillment_tasks_shadow'))
assert.ok(migration.includes('trg_work_orders_fulfillment_shadow'))
assert.ok(migration.includes('assign_fulfillment_event_shadow'))
})
test('明确渠道即视为已路由,不用渠道配置缺失覆盖已选渠道', () => {
assert.ok(migration.includes('v_fulfillment_item_state_raw'))
assert.ok(migration.includes("WHEN raw.intended_channel IS NOT NULL THEN 'configured'"))
assert.ok(
migration.includes(
"WHEN raw.route_status = 'missing_channel'\n AND raw.intended_channel IS NOT NULL\n THEN ''",
),
)
})
test('审计脚本核对影子表、执行尝试、开放异常和事件关联', () => {
assert.ok(auditScript.includes('影子履约缺失'))
assert.ok(auditScript.includes('影子履约状态差异'))
assert.ok(auditScript.includes('影子执行尝试缺失'))
assert.ok(auditScript.includes('影子开放异常缺失'))
assert.ok(auditScript.includes('履约事件未关联影子记录'))
})
@@ -27,6 +27,7 @@ test(
let orderItemId = 0
let manualConfigId = 0
let productId = 0
let profileId = 0
try {
const order = await query<{ id: number }>(
@@ -51,6 +52,19 @@ test(
)
orderItemId = Number(item.rows[0]!.id)
const initialShadow = await query<{
route_status: string
carrier_status: string
issue_code: string
}>(
`SELECT route_status, carrier_status, issue_code
FROM fulfillments
WHERE order_item_id = $1`,
[orderItemId],
)
assert.equal(initialShadow.rows[0]?.route_status, 'unrouted')
assert.equal(initialShadow.rows[0]?.issue_code, 'missing_channel')
const pendingDetail = {
internalSku: `状态事件测试-${suffix}`,
channel: 'manual',
@@ -146,6 +160,33 @@ test(
assert.equal(events[0]?.previous_channel, null)
assert.equal(events[0]?.next_channel, 'third_party_voucher')
const assignedShadow = await query<{
route_status: string
channel: string
carrier_status: string
issue_code: string
event_count: number
}>(
`SELECT
f.route_status,
f.channel,
f.carrier_status,
f.issue_code,
(
SELECT count(*)::int
FROM fulfillment_state_events event
WHERE event.fulfillment_id = f.id
) AS event_count
FROM fulfillments f
WHERE f.order_item_id = $1`,
[orderItemId],
)
assert.equal(assignedShadow.rows[0]?.route_status, 'configured')
assert.equal(assignedShadow.rows[0]?.channel, 'third_party_voucher')
assert.equal(assignedShadow.rows[0]?.carrier_status, 'not_required')
assert.equal(assignedShadow.rows[0]?.issue_code, '')
assert.equal(Number(assignedShadow.rows[0]?.event_count), 3)
const product = await query<{ id: number }>(
`INSERT INTO product_catalog (
canonical_sku, canonical_name, category, unit_name, status,
@@ -193,6 +234,112 @@ test(
assert.equal(Number(configured[0]?.fulfillment_config_id), manualConfigId)
assert.equal(Number(configured[0]?.canonical_product_id), productId)
const profile = await query<{ id: number }>(
`INSERT INTO fulfillment_profiles (
profile_key, name, executor_key, requires_claim, auto_dispatch,
inventory_strategy, config_json, created_at, updated_at
) VALUES ($1, '状态事件任务', 'affiliate_dash', TRUE, FALSE,
'external_platform', '{}'::jsonb, NOW(), NOW())
RETURNING id`,
[`ff-state-profile-${suffix}`],
)
profileId = Number(profile.rows[0]!.id)
await query(
`INSERT INTO fulfillment_tasks (
order_id, order_item_id, unit_index, task_no, provider, platform,
shop_id, shop_name, platform_order_id, profile_id, executor_key,
task_status, attempt_no, is_current, created_at, updated_at
)
SELECT
item.order_id, item.id, 1, $2, source.provider, source.platform,
source.shop_id, source.shop_name, source.platform_order_id, $3, 'affiliate_dash',
'paid', 1, TRUE, NOW(), NOW()
FROM order_items item
JOIN orders source ON source.id = item.order_id
WHERE item.id = $1`,
[orderItemId, `FF-SHADOW-TASK-${suffix}`, profileId],
)
const attempts = await query<{
attempt_type: string
unit_index: number
attempt_no: number
source_id: number
is_current: boolean
}>(
`SELECT attempt_type, unit_index, attempt_no, source_id, is_current
FROM fulfillment_attempts
WHERE fulfillment_id = (
SELECT id FROM fulfillments WHERE order_item_id = $1
)
ORDER BY id`,
[orderItemId],
)
assert.equal(attempts.rows.length, 1)
assert.equal(attempts.rows[0]?.attempt_type, 'task')
assert.equal(attempts.rows[0]?.unit_index, 1)
assert.equal(attempts.rows[0]?.attempt_no, 1)
assert.equal(attempts.rows[0]?.is_current, true)
const shadowDiff = await query<{
fulfillment_state_diffs: number
missing_attempts: number
missing_open_issues: number
unlinked_events: number
}>(
`SELECT
COUNT(*) FILTER (
WHERE ROW(
fulfillment.route_status,
fulfillment.channel,
fulfillment.carrier_status,
fulfillment.current_attempt_type,
fulfillment.current_attempt_source_id,
fulfillment.issue_code,
fulfillment.issue_category
) IS DISTINCT FROM ROW(
CASE WHEN state.route_status = 'configured' THEN 'configured' ELSE 'unrouted' END,
state.intended_channel,
state.carrier_status,
state.current_attempt_type,
NULLIF(state.current_attempt_id, 0),
state.issue_code,
state.issue_category
)
)::int AS fulfillment_state_diffs,
(
SELECT COUNT(*)::int
FROM fulfillment_tasks task
WHERE task.order_item_id = $1
AND NOT EXISTS (
SELECT 1 FROM fulfillment_attempts attempt
WHERE attempt.attempt_type = 'task' AND attempt.source_id = task.id
)
) AS missing_attempts,
COUNT(*) FILTER (
WHERE state.issue_code <> ''
AND NOT EXISTS (
SELECT 1 FROM fulfillment_issues issue
WHERE issue.fulfillment_id = fulfillment.id
AND issue.issue_code = state.issue_code
AND issue.status <> 'resolved'
)
)::int AS missing_open_issues,
(
SELECT COUNT(*)::int
FROM fulfillment_state_events event
WHERE event.order_item_id = $1
AND event.fulfillment_id IS NULL
) AS unlinked_events
FROM v_fulfillment_item_state state
JOIN fulfillments fulfillment ON fulfillment.order_item_id = state.order_item_id
WHERE state.order_item_id = $1`,
[orderItemId],
)
assert.equal(Number(shadowDiff.rows[0]?.fulfillment_state_diffs), 0)
assert.equal(Number(shadowDiff.rows[0]?.missing_attempts), 0)
assert.equal(Number(shadowDiff.rows[0]?.missing_open_issues), 0)
assert.equal(Number(shadowDiff.rows[0]?.unlinked_events), 0)
await assert.rejects(
query(`UPDATE fulfillment_state_events SET reason = 'tampered' WHERE order_item_id = $1`, [
orderItemId,
@@ -209,6 +356,9 @@ test(
if (productId > 0) {
await query(`DELETE FROM product_catalog WHERE id = $1`, [productId])
}
if (profileId > 0) {
await query(`DELETE FROM fulfillment_profiles WHERE id = $1`, [profileId])
}
if (orderId > 0) {
await query(`DELETE FROM orders WHERE id = $1`, [orderId])
}