重构:合并数据库迁移为三个领域文件
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+108
-3
@@ -1,5 +1,16 @@
|
||||
-- 163_cutover_fulfillment_truth.sql
|
||||
-- 148_fulfillment_execution_truth.sql
|
||||
--
|
||||
-- 正式履约执行真源硬切:
|
||||
-- 1. fulfillments 成为状态、渠道、载体、配置和当前尝试的唯一真源;
|
||||
-- 2. 任务与工单只追加 fulfillment_attempts;
|
||||
-- 3. 订单项旧履约列冻结为 legacy_*,统一读取 v_order_item_current;
|
||||
-- 4. 履约事件、外键级联和无法解析内部 SKU 的口径一次收口。
|
||||
--
|
||||
-- 合并自未上生产的 163–166。
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 履约状态硬切
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 履约状态硬切:
|
||||
-- 1. fulfillments 成为履约状态、渠道、配置关联和当前尝试的唯一真源;
|
||||
-- 2. order_items 只保留原始订单项数据,旧履约列改名冻结,不再读写;
|
||||
@@ -1199,9 +1210,9 @@ ALTER TABLE order_items
|
||||
DROP CONSTRAINT IF EXISTS order_items_fulfillment_decided_by_fkey;
|
||||
|
||||
COMMENT ON COLUMN order_items.legacy_canonical_product_id IS
|
||||
'冻结的历史商品归一值;迁移 163 后唯一真源是 fulfillments.canonical_product_id';
|
||||
'冻结的历史商品归一值;本迁移后唯一真源是 fulfillments.canonical_product_id';
|
||||
COMMENT ON COLUMN order_items.legacy_fulfillment_status IS
|
||||
'冻结的历史履约状态;迁移 163 后唯一真源是 fulfillments.status';
|
||||
'冻结的历史履约状态;本迁移后唯一真源是 fulfillments.status';
|
||||
COMMENT ON TABLE fulfillments IS
|
||||
'履约唯一真源:每订单项一行,保存状态、渠道、配置关联、载体、当前尝试和异常摘要';
|
||||
|
||||
@@ -1349,3 +1360,97 @@ WHERE fulfillment.status IN ('pending', 'assigned');
|
||||
|
||||
COMMENT ON VIEW v_fulfillment_item_state IS
|
||||
'履约统一读模型:直接投影 fulfillments、fulfillment_attempts、fulfillment_issues 和 sku_channel_routes';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 履约事件外键级联
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 履约事件已经由 fulfillment_id 直接归属正式履约记录。
|
||||
-- 订单项删除时事件应随履约记录级联清理,不能先 SET NULL 再触发 append-only 更新保护。
|
||||
|
||||
ALTER TABLE fulfillment_state_events
|
||||
DROP CONSTRAINT IF EXISTS fk_fulfillment_state_events_fulfillment;
|
||||
|
||||
ALTER TABLE fulfillment_state_events
|
||||
ADD CONSTRAINT fk_fulfillment_state_events_fulfillment
|
||||
FOREIGN KEY (fulfillment_id)
|
||||
REFERENCES fulfillments(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 履约事件只禁止更新
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 事件语义是“应用不可改”,父记录删除时允许数据库级联清理。
|
||||
-- 旧环境里曾存在 BEFORE DELETE OR UPDATE 的漂移定义,这里统一收回为只阻止 UPDATE。
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_fulfillment_state_events_append_only
|
||||
ON fulfillment_state_events;
|
||||
|
||||
CREATE TRIGGER trg_fulfillment_state_events_append_only
|
||||
BEFORE UPDATE ON fulfillment_state_events
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION reject_fulfillment_state_event_mutation();
|
||||
|
||||
COMMENT ON TRIGGER trg_fulfillment_state_events_append_only
|
||||
ON fulfillment_state_events IS
|
||||
'禁止应用修改履约事件;数据库外键级联删除不受影响';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 无法解析内部 SKU 的兜底
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- resolve_internal_sku 可能返回 NULL。正式履约行的 internal_sku 是不带 NULL 的稳定键,
|
||||
-- 无法解析时统一落空字符串,并立即进入 missing_internal_sku 异常口径。
|
||||
|
||||
CREATE OR REPLACE FUNCTION ensure_fulfillment_for_order_item()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO fulfillments (
|
||||
order_item_id,
|
||||
order_id,
|
||||
internal_sku,
|
||||
route_status,
|
||||
channel,
|
||||
carrier_status,
|
||||
current_attempt_type,
|
||||
current_attempt_source_id,
|
||||
current_attempt_status,
|
||||
lock_reason,
|
||||
issue_code,
|
||||
issue_category,
|
||||
retryable,
|
||||
owner_role,
|
||||
created_at,
|
||||
updated_at,
|
||||
status,
|
||||
source,
|
||||
locked,
|
||||
channel_switch_reason
|
||||
) VALUES (
|
||||
NEW.id,
|
||||
NEW.order_id,
|
||||
COALESCE(resolve_internal_sku(NEW.manual_sku_key, NEW.sku_name, NEW.sku_code), ''),
|
||||
'unrouted',
|
||||
NULL,
|
||||
'missing',
|
||||
'',
|
||||
NULL,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
FALSE,
|
||||
'',
|
||||
NEW.created_at,
|
||||
NOW(),
|
||||
'pending',
|
||||
'',
|
||||
FALSE,
|
||||
''
|
||||
)
|
||||
ON CONFLICT (order_item_id) DO NOTHING;
|
||||
|
||||
PERFORM refresh_fulfillment_derived(NEW.id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
+6
-1
@@ -1,5 +1,10 @@
|
||||
-- 167_finance_derived_scope.sql
|
||||
-- 149_finance_derived_scope.sql
|
||||
--
|
||||
-- 财务销售范围唯一读模型:
|
||||
-- v_finance_order_item_scope 统一收入、退款、商品、渠道和业务日,
|
||||
-- 销售事实、重建与审计共用这一口径。
|
||||
--
|
||||
-- 合并自未上生产的 167。
|
||||
-- 财务派生范围唯一读模型:
|
||||
-- 1. 只纳入 pending / assigned 履约项;
|
||||
-- 2. 只纳入 paid / refunded 商业订单;
|
||||
@@ -1,426 +0,0 @@
|
||||
-- 154_internal_sku_registry_and_routes.sql
|
||||
--
|
||||
-- 统一内部 SKU 身份与履约路由:
|
||||
-- 1. internal_skus 成为内部 SKU 的稳定身份;
|
||||
-- 2. sku_channel_routes 成为主渠道/兜底渠道的唯一结构化表达;
|
||||
-- 3. 兼容期由旧 internal_sku_channels 通过触发器同步;
|
||||
-- 4. product_catalog 增加 source_kind 与 internal_sku_id,不再依赖前缀判断业务来源。
|
||||
|
||||
CREATE TABLE internal_skus (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
internal_sku_key TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
merged_into_id BIGINT REFERENCES internal_skus(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT internal_skus_key_not_blank CHECK (BTRIM(internal_sku_key) <> ''),
|
||||
CONSTRAINT internal_skus_status_check CHECK (status IN ('active', 'disabled', 'merged'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_internal_skus_key
|
||||
ON internal_skus(internal_sku_key);
|
||||
|
||||
COMMENT ON TABLE internal_skus IS
|
||||
'中文内部 SKU 稳定身份;所有路由、配置和商品关联最终通过 internal_sku_id 引用';
|
||||
COMMENT ON COLUMN internal_skus.internal_sku_key IS
|
||||
'NFKC、去空白、转小写后的稳定键';
|
||||
|
||||
WITH source_rows AS (
|
||||
SELECT internal_sku AS raw_value, updated_at FROM internal_sku_channels
|
||||
UNION ALL
|
||||
SELECT internal_sku AS raw_value, updated_at FROM internal_sku_mappings
|
||||
UNION ALL
|
||||
SELECT sku_key AS raw_value, updated_at FROM manual_fulfillment_configs
|
||||
UNION ALL
|
||||
SELECT sku_key AS raw_value, updated_at FROM manual_sku_registry
|
||||
UNION ALL
|
||||
SELECT canonical_name AS raw_value, updated_at FROM product_catalog
|
||||
),
|
||||
normalized AS (
|
||||
SELECT
|
||||
canonicalize_internal_sku(raw_value) AS internal_sku_key,
|
||||
BTRIM(raw_value) AS display_name,
|
||||
updated_at
|
||||
FROM source_rows
|
||||
WHERE canonicalize_internal_sku(raw_value) <> ''
|
||||
),
|
||||
picked AS (
|
||||
SELECT DISTINCT ON (internal_sku_key)
|
||||
internal_sku_key,
|
||||
display_name,
|
||||
updated_at
|
||||
FROM normalized
|
||||
ORDER BY internal_sku_key, updated_at DESC NULLS LAST, display_name
|
||||
)
|
||||
INSERT INTO internal_skus (
|
||||
internal_sku_key,
|
||||
display_name,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
internal_sku_key,
|
||||
display_name,
|
||||
'active',
|
||||
COALESCE(updated_at, NOW()),
|
||||
COALESCE(updated_at, NOW())
|
||||
FROM picked
|
||||
ON CONFLICT (internal_sku_key) DO UPDATE SET
|
||||
display_name = CASE
|
||||
WHEN internal_skus.display_name = '' THEN EXCLUDED.display_name
|
||||
ELSE internal_skus.display_name
|
||||
END,
|
||||
updated_at = GREATEST(internal_skus.updated_at, EXCLUDED.updated_at);
|
||||
|
||||
ALTER TABLE internal_sku_mappings
|
||||
ADD COLUMN internal_sku_id BIGINT REFERENCES internal_skus(id) ON DELETE SET NULL;
|
||||
ALTER TABLE manual_fulfillment_configs
|
||||
ADD COLUMN internal_sku_id BIGINT REFERENCES internal_skus(id) ON DELETE SET NULL;
|
||||
ALTER TABLE product_catalog
|
||||
ADD COLUMN internal_sku_id BIGINT REFERENCES internal_skus(id) ON DELETE SET NULL,
|
||||
ADD COLUMN source_kind TEXT;
|
||||
|
||||
UPDATE internal_sku_mappings mapping
|
||||
SET internal_sku_id = sku.id
|
||||
FROM internal_skus sku
|
||||
WHERE sku.internal_sku_key = canonicalize_internal_sku(mapping.internal_sku);
|
||||
|
||||
UPDATE manual_fulfillment_configs config
|
||||
SET internal_sku_id = sku.id
|
||||
FROM internal_skus sku
|
||||
WHERE sku.internal_sku_key = canonicalize_internal_sku(config.sku_key);
|
||||
|
||||
UPDATE product_catalog product
|
||||
SET source_kind = CASE
|
||||
WHEN product.canonical_sku LIKE 'aff:%' THEN 'affiliate_dash'
|
||||
WHEN product.canonical_sku LIKE 'tpl:%' OR product.canonical_sku LIKE 'wo:%' THEN 'work_order'
|
||||
WHEN product.canonical_sku LIKE 'tpv:%' THEN 'third_party_voucher'
|
||||
WHEN product.canonical_sku LIKE 'man:%' THEN 'manual'
|
||||
ELSE 'unclassified'
|
||||
END;
|
||||
|
||||
UPDATE product_catalog product
|
||||
SET internal_sku_id = sku.id
|
||||
FROM internal_skus sku
|
||||
WHERE sku.internal_sku_key = canonicalize_internal_sku(
|
||||
CASE
|
||||
WHEN product.canonical_sku LIKE 'man:%' THEN substring(product.canonical_sku from 5)
|
||||
WHEN product.canonical_sku LIKE 'tpv:%' THEN substring(product.canonical_sku from 5)
|
||||
ELSE NULL
|
||||
END
|
||||
);
|
||||
|
||||
ALTER TABLE product_catalog
|
||||
ALTER COLUMN source_kind SET DEFAULT 'unclassified',
|
||||
ALTER COLUMN source_kind SET NOT NULL;
|
||||
|
||||
CREATE INDEX idx_product_catalog_internal_sku
|
||||
ON product_catalog(internal_sku_id)
|
||||
WHERE internal_sku_id IS NOT NULL;
|
||||
CREATE INDEX idx_product_catalog_source_kind
|
||||
ON product_catalog(source_kind, status);
|
||||
CREATE INDEX idx_internal_sku_mappings_internal_sku
|
||||
ON internal_sku_mappings(internal_sku_id)
|
||||
WHERE internal_sku_id IS NOT NULL;
|
||||
CREATE INDEX idx_manual_configs_internal_sku
|
||||
ON manual_fulfillment_configs(internal_sku_id)
|
||||
WHERE internal_sku_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE sku_channel_routes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
internal_sku_id BIGINT NOT NULL REFERENCES internal_skus(id) ON DELETE CASCADE,
|
||||
channel TEXT NOT NULL,
|
||||
route_role TEXT NOT NULL DEFAULT 'fallback',
|
||||
priority INTEGER NOT NULL DEFAULT 100,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE SET NULL,
|
||||
carrier_config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
source TEXT NOT NULL DEFAULT 'legacy_sync',
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
created_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT sku_channel_routes_channel_check
|
||||
CHECK (channel IN ('work_order', 'affiliate_dash', 'third_party_voucher', 'manual', 'on_account')),
|
||||
CONSTRAINT sku_channel_routes_role_check
|
||||
CHECK (route_role IN ('primary', 'fallback')),
|
||||
CONSTRAINT sku_channel_routes_priority_check
|
||||
CHECK (priority >= 0)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_sku_channel_routes_channel
|
||||
ON sku_channel_routes(internal_sku_id, channel);
|
||||
CREATE UNIQUE INDEX uq_sku_channel_routes_primary
|
||||
ON sku_channel_routes(internal_sku_id)
|
||||
WHERE enabled = TRUE AND route_role = 'primary';
|
||||
CREATE INDEX idx_sku_channel_routes_lookup
|
||||
ON sku_channel_routes(channel, enabled, priority)
|
||||
WHERE enabled = TRUE;
|
||||
|
||||
COMMENT ON TABLE sku_channel_routes IS
|
||||
'内部 SKU 履约路由:一个主渠道加若干显式兜底渠道';
|
||||
COMMENT ON COLUMN sku_channel_routes.carrier_config_json IS
|
||||
'渠道载体参数,例如 workRuleKey / affiliateSku / manualConfigId / voucherSku';
|
||||
|
||||
WITH legacy_rows AS (
|
||||
SELECT
|
||||
sku.id AS internal_sku_id,
|
||||
channel.channel,
|
||||
channel.priority,
|
||||
channel.is_fallback,
|
||||
channel.enabled,
|
||||
channel.canonical_product_id,
|
||||
channel.config_json,
|
||||
channel.source,
|
||||
channel.note,
|
||||
channel.created_by_admin_user_id,
|
||||
channel.created_at,
|
||||
channel.updated_at,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY sku.id, channel.channel
|
||||
ORDER BY channel.is_fallback, channel.priority, channel.id
|
||||
) AS channel_rank
|
||||
FROM internal_sku_channels channel
|
||||
JOIN internal_skus sku
|
||||
ON sku.internal_sku_key = canonicalize_internal_sku(channel.internal_sku)
|
||||
WHERE channel.enabled = TRUE
|
||||
),
|
||||
deduplicated AS (
|
||||
SELECT * FROM legacy_rows WHERE channel_rank = 1
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
*,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY internal_sku_id
|
||||
ORDER BY is_fallback, priority, channel
|
||||
) AS overall_rank
|
||||
FROM deduplicated
|
||||
)
|
||||
INSERT INTO sku_channel_routes (
|
||||
internal_sku_id,
|
||||
channel,
|
||||
route_role,
|
||||
priority,
|
||||
enabled,
|
||||
canonical_product_id,
|
||||
carrier_config_json,
|
||||
source,
|
||||
note,
|
||||
created_by_admin_user_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
internal_sku_id,
|
||||
channel,
|
||||
CASE WHEN overall_rank = 1 THEN 'primary' ELSE 'fallback' END,
|
||||
CASE WHEN overall_rank = 1 THEN priority ELSE GREATEST(priority, 90) END,
|
||||
TRUE,
|
||||
canonical_product_id,
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
COALESCE(NULLIF(source, ''), 'legacy_sync'),
|
||||
COALESCE(note, ''),
|
||||
created_by_admin_user_id,
|
||||
COALESCE(created_at, NOW()),
|
||||
COALESCE(updated_at, NOW())
|
||||
FROM ranked
|
||||
ON CONFLICT (internal_sku_id, channel) DO NOTHING;
|
||||
|
||||
CREATE OR REPLACE FUNCTION ensure_canonical_internal_sku(p_raw TEXT)
|
||||
RETURNS BIGINT
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_key TEXT := canonicalize_internal_sku(p_raw);
|
||||
v_display_name TEXT := BTRIM(COALESCE(p_raw, ''));
|
||||
v_id BIGINT;
|
||||
BEGIN
|
||||
IF v_key = '' THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
INSERT INTO internal_skus (internal_sku_key, display_name, status, created_at, updated_at)
|
||||
VALUES (v_key, v_display_name, 'active', NOW(), NOW())
|
||||
ON CONFLICT (internal_sku_key) DO UPDATE SET
|
||||
display_name = CASE
|
||||
WHEN internal_skus.display_name = '' THEN EXCLUDED.display_name
|
||||
ELSE internal_skus.display_name
|
||||
END,
|
||||
updated_at = GREATEST(internal_skus.updated_at, EXCLUDED.updated_at)
|
||||
RETURNING id INTO v_id;
|
||||
|
||||
RETURN v_id;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION sync_sku_channel_routes_for_key(p_raw TEXT)
|
||||
RETURNS VOID
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_sku_id BIGINT;
|
||||
BEGIN
|
||||
SELECT id INTO v_sku_id
|
||||
FROM internal_skus
|
||||
WHERE internal_sku_key = canonicalize_internal_sku(p_raw)
|
||||
LIMIT 1;
|
||||
|
||||
IF v_sku_id IS NULL THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
DELETE FROM sku_channel_routes WHERE internal_sku_id = v_sku_id;
|
||||
|
||||
WITH legacy_rows AS (
|
||||
SELECT
|
||||
channel.channel,
|
||||
channel.priority,
|
||||
channel.is_fallback,
|
||||
channel.canonical_product_id,
|
||||
channel.config_json,
|
||||
channel.source,
|
||||
channel.note,
|
||||
channel.created_by_admin_user_id,
|
||||
channel.created_at,
|
||||
channel.updated_at,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY channel.channel
|
||||
ORDER BY channel.is_fallback, channel.priority, channel.id
|
||||
) AS channel_rank
|
||||
FROM internal_sku_channels channel
|
||||
WHERE channel.enabled = TRUE
|
||||
AND canonicalize_internal_sku(channel.internal_sku)
|
||||
= canonicalize_internal_sku(p_raw)
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
*,
|
||||
ROW_NUMBER() OVER (
|
||||
ORDER BY is_fallback, priority, channel
|
||||
) AS overall_rank
|
||||
FROM legacy_rows
|
||||
WHERE channel_rank = 1
|
||||
)
|
||||
INSERT INTO sku_channel_routes (
|
||||
internal_sku_id,
|
||||
channel,
|
||||
route_role,
|
||||
priority,
|
||||
enabled,
|
||||
canonical_product_id,
|
||||
carrier_config_json,
|
||||
source,
|
||||
note,
|
||||
created_by_admin_user_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
v_sku_id,
|
||||
channel,
|
||||
CASE WHEN overall_rank = 1 THEN 'primary' ELSE 'fallback' END,
|
||||
CASE WHEN overall_rank = 1 THEN priority ELSE GREATEST(priority, 90) END,
|
||||
TRUE,
|
||||
canonical_product_id,
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
COALESCE(NULLIF(source, ''), 'legacy_sync'),
|
||||
COALESCE(note, ''),
|
||||
created_by_admin_user_id,
|
||||
COALESCE(created_at, NOW()),
|
||||
COALESCE(updated_at, NOW())
|
||||
FROM ranked;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION sync_sku_channel_routes_trigger()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF TG_OP <> 'INSERT' THEN
|
||||
PERFORM sync_sku_channel_routes_for_key(OLD.internal_sku);
|
||||
END IF;
|
||||
IF TG_OP <> 'DELETE' THEN
|
||||
PERFORM ensure_canonical_internal_sku(NEW.internal_sku);
|
||||
PERFORM sync_sku_channel_routes_for_key(NEW.internal_sku);
|
||||
END IF;
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trg_sync_sku_channel_routes
|
||||
AFTER INSERT OR UPDATE OR DELETE ON internal_sku_channels
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION sync_sku_channel_routes_trigger();
|
||||
|
||||
CREATE OR REPLACE FUNCTION set_internal_sku_id_from_text()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.internal_sku_id := ensure_canonical_internal_sku(NEW.internal_sku);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trg_internal_sku_mapping_identity
|
||||
BEFORE INSERT OR UPDATE OF internal_sku ON internal_sku_mappings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_internal_sku_id_from_text();
|
||||
|
||||
CREATE OR REPLACE FUNCTION set_manual_config_internal_sku_id()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.internal_sku_id := ensure_canonical_internal_sku(NEW.sku_key);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trg_manual_config_internal_sku_identity
|
||||
BEFORE INSERT OR UPDATE OF sku_key ON manual_fulfillment_configs
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_manual_config_internal_sku_id();
|
||||
|
||||
CREATE OR REPLACE FUNCTION normalize_product_identity()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.source_kind := COALESCE(
|
||||
NULLIF(NEW.source_kind, ''),
|
||||
CASE
|
||||
WHEN NEW.canonical_sku LIKE 'aff:%' THEN 'affiliate_dash'
|
||||
WHEN NEW.canonical_sku LIKE 'tpl:%' OR NEW.canonical_sku LIKE 'wo:%' THEN 'work_order'
|
||||
WHEN NEW.canonical_sku LIKE 'tpv:%' THEN 'third_party_voucher'
|
||||
WHEN NEW.canonical_sku LIKE 'man:%' THEN 'manual'
|
||||
ELSE 'unclassified'
|
||||
END
|
||||
);
|
||||
|
||||
IF NEW.internal_sku_id IS NULL THEN
|
||||
SELECT sku.id INTO NEW.internal_sku_id
|
||||
FROM internal_skus sku
|
||||
WHERE sku.internal_sku_key = canonicalize_internal_sku(
|
||||
CASE
|
||||
WHEN NEW.canonical_sku LIKE 'man:%' THEN substring(NEW.canonical_sku from 5)
|
||||
WHEN NEW.canonical_sku LIKE 'tpv:%' THEN substring(NEW.canonical_sku from 5)
|
||||
ELSE NULL
|
||||
END
|
||||
)
|
||||
LIMIT 1;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trg_normalize_product_identity
|
||||
BEFORE INSERT OR UPDATE OF canonical_sku, canonical_name, source_kind, internal_sku_id
|
||||
ON product_catalog
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION normalize_product_identity();
|
||||
@@ -1,96 +0,0 @@
|
||||
-- 155_product_current_projection.sql
|
||||
--
|
||||
-- 统一商品当前渠道投影,并清理已被其他渠道接管且没有人工兜底配置的旧 man 商品。
|
||||
|
||||
UPDATE sku_channel_routes route
|
||||
SET canonical_product_id = product.id,
|
||||
updated_at = NOW()
|
||||
FROM product_catalog product
|
||||
WHERE route.channel = 'work_order'
|
||||
AND route.canonical_product_id IS NULL
|
||||
AND product.status = 'active'
|
||||
AND product.canonical_sku = 'tpl:' || COALESCE(route.carrier_config_json->>'workRuleKey', '');
|
||||
|
||||
CREATE OR REPLACE VIEW v_product_current AS
|
||||
SELECT
|
||||
product.*,
|
||||
sku.internal_sku_key,
|
||||
COALESCE(
|
||||
primary_route.channel,
|
||||
CASE product.source_kind
|
||||
WHEN 'affiliate_dash' THEN 'affiliate_dash'
|
||||
WHEN 'work_order' THEN 'work_order'
|
||||
WHEN 'third_party_voucher' THEN 'third_party_voucher'
|
||||
WHEN 'manual' THEN 'manual'
|
||||
ELSE NULL
|
||||
END
|
||||
) AS active_primary_channel,
|
||||
COALESCE(fallback_routes.channels, ARRAY[]::text[]) AS fallback_channels,
|
||||
manual_config.id AS manual_config_id,
|
||||
(manual_config.id IS NOT NULL) AS has_active_manual_config
|
||||
FROM product_catalog product
|
||||
LEFT JOIN internal_skus sku ON sku.id = product.internal_sku_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT route.channel
|
||||
FROM sku_channel_routes route
|
||||
WHERE route.internal_sku_id = product.internal_sku_id
|
||||
AND route.enabled = TRUE
|
||||
AND route.route_role = 'primary'
|
||||
ORDER BY route.priority, route.channel
|
||||
LIMIT 1
|
||||
) primary_route ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT ARRAY_AGG(route.channel ORDER BY route.priority, route.channel) AS channels
|
||||
FROM sku_channel_routes route
|
||||
WHERE route.internal_sku_id = product.internal_sku_id
|
||||
AND route.enabled = TRUE
|
||||
AND route.route_role = 'fallback'
|
||||
) fallback_routes ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT config.id
|
||||
FROM manual_fulfillment_configs config
|
||||
WHERE config.enabled = TRUE
|
||||
AND (
|
||||
config.canonical_product_id = product.id
|
||||
OR (
|
||||
sku.internal_sku_key IS NOT NULL
|
||||
AND canonicalize_internal_sku(config.sku_key) = sku.internal_sku_key
|
||||
)
|
||||
)
|
||||
ORDER BY config.id
|
||||
LIMIT 1
|
||||
) manual_config ON TRUE;
|
||||
|
||||
COMMENT ON VIEW v_product_current IS
|
||||
'标准商品统一投影:来源、当前主渠道、兜底渠道和人工配置状态同源输出';
|
||||
|
||||
WITH candidates AS (
|
||||
SELECT product.id
|
||||
FROM v_product_current product
|
||||
WHERE product.status = 'active'
|
||||
AND product.canonical_sku LIKE 'man:%'
|
||||
AND product.active_primary_channel IS NOT NULL
|
||||
AND product.active_primary_channel <> 'manual'
|
||||
AND product.has_active_manual_config = FALSE
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM sku_channel_routes route
|
||||
WHERE route.internal_sku_id = product.internal_sku_id
|
||||
AND route.enabled = TRUE
|
||||
AND route.route_role = 'primary'
|
||||
AND route.channel <> 'manual'
|
||||
)
|
||||
)
|
||||
UPDATE product_catalog product
|
||||
SET status = 'archived',
|
||||
attributes_json = COALESCE(product.attributes_json, '{}'::jsonb) || jsonb_build_object(
|
||||
'legacyManualArchived', TRUE,
|
||||
'legacyManualArchiveReason', 'superseded_by_current_primary_channel',
|
||||
'legacyManualArchivedAt', NOW()
|
||||
),
|
||||
updated_at = NOW()
|
||||
FROM candidates
|
||||
WHERE product.id = candidates.id;
|
||||
|
||||
COMMENT ON COLUMN product_catalog.source_kind IS
|
||||
'标准商品来源类型;来源不等于当前履约渠道,当前渠道统一读取 v_product_current';
|
||||
@@ -1,53 +0,0 @@
|
||||
-- 156_fix_product_source_kind_default.sql
|
||||
--
|
||||
-- source_kind 的列默认值会让 BEFORE INSERT 触发器看到“非空但未分类”,
|
||||
-- 从而跳过前缀回填。这里保留默认值兼容直接写入,同时让已知前缀始终覆盖
|
||||
-- unclassified,并修复兼容期内已经写入的误判行。
|
||||
|
||||
CREATE OR REPLACE FUNCTION normalize_product_identity()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.source_kind := CASE
|
||||
WHEN NEW.canonical_sku LIKE 'aff:%' THEN 'affiliate_dash'
|
||||
WHEN NEW.canonical_sku LIKE 'tpl:%' OR NEW.canonical_sku LIKE 'wo:%' THEN 'work_order'
|
||||
WHEN NEW.canonical_sku LIKE 'tpv:%' THEN 'third_party_voucher'
|
||||
WHEN NEW.canonical_sku LIKE 'man:%' THEN 'manual'
|
||||
ELSE COALESCE(NULLIF(NEW.source_kind, ''), 'unclassified')
|
||||
END;
|
||||
|
||||
IF NEW.internal_sku_id IS NULL THEN
|
||||
SELECT sku.id INTO NEW.internal_sku_id
|
||||
FROM internal_skus sku
|
||||
WHERE sku.internal_sku_key = canonicalize_internal_sku(
|
||||
CASE
|
||||
WHEN NEW.canonical_sku LIKE 'man:%' THEN substring(NEW.canonical_sku from 5)
|
||||
WHEN NEW.canonical_sku LIKE 'tpv:%' THEN substring(NEW.canonical_sku from 5)
|
||||
ELSE NULL
|
||||
END
|
||||
)
|
||||
LIMIT 1;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
UPDATE product_catalog
|
||||
SET source_kind = CASE
|
||||
WHEN canonical_sku LIKE 'aff:%' THEN 'affiliate_dash'
|
||||
WHEN canonical_sku LIKE 'tpl:%' OR canonical_sku LIKE 'wo:%' THEN 'work_order'
|
||||
WHEN canonical_sku LIKE 'tpv:%' THEN 'third_party_voucher'
|
||||
WHEN canonical_sku LIKE 'man:%' THEN 'manual'
|
||||
ELSE source_kind
|
||||
END,
|
||||
updated_at = NOW()
|
||||
WHERE source_kind = 'unclassified'
|
||||
AND (
|
||||
canonical_sku LIKE 'aff:%'
|
||||
OR canonical_sku LIKE 'tpl:%'
|
||||
OR canonical_sku LIKE 'wo:%'
|
||||
OR canonical_sku LIKE 'tpv:%'
|
||||
OR canonical_sku LIKE 'man:%'
|
||||
);
|
||||
@@ -1,105 +0,0 @@
|
||||
-- 157_preserve_fallback_only_routes.sql
|
||||
--
|
||||
-- 兼容同步不能把“只有兜底渠道”的旧配置自动升级成主渠道。
|
||||
-- 只有旧表中明确 is_fallback = FALSE 的渠道才允许竞争 primary;
|
||||
-- 如果某个内部 SKU 没有主渠道,则所有启用路由都保持 fallback。
|
||||
|
||||
CREATE OR REPLACE FUNCTION sync_sku_channel_routes_for_key(p_raw TEXT)
|
||||
RETURNS VOID
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_sku_id BIGINT;
|
||||
BEGIN
|
||||
SELECT id INTO v_sku_id
|
||||
FROM internal_skus
|
||||
WHERE internal_sku_key = canonicalize_internal_sku(p_raw)
|
||||
LIMIT 1;
|
||||
|
||||
IF v_sku_id IS NULL THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
DELETE FROM sku_channel_routes WHERE internal_sku_id = v_sku_id;
|
||||
|
||||
WITH legacy_rows AS (
|
||||
SELECT
|
||||
channel.channel,
|
||||
channel.priority,
|
||||
channel.is_fallback,
|
||||
channel.canonical_product_id,
|
||||
channel.config_json,
|
||||
channel.source,
|
||||
channel.note,
|
||||
channel.created_by_admin_user_id,
|
||||
channel.created_at,
|
||||
channel.updated_at,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY channel.channel
|
||||
ORDER BY channel.priority, channel.id
|
||||
) AS channel_rank
|
||||
FROM internal_sku_channels channel
|
||||
WHERE channel.enabled = TRUE
|
||||
AND canonicalize_internal_sku(channel.internal_sku)
|
||||
= canonicalize_internal_sku(p_raw)
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
*,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY is_fallback
|
||||
ORDER BY priority, channel
|
||||
) AS role_rank
|
||||
FROM legacy_rows
|
||||
WHERE channel_rank = 1
|
||||
)
|
||||
INSERT INTO sku_channel_routes (
|
||||
internal_sku_id,
|
||||
channel,
|
||||
route_role,
|
||||
priority,
|
||||
enabled,
|
||||
canonical_product_id,
|
||||
carrier_config_json,
|
||||
source,
|
||||
note,
|
||||
created_by_admin_user_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
v_sku_id,
|
||||
channel,
|
||||
CASE
|
||||
WHEN is_fallback = FALSE AND role_rank = 1 THEN 'primary'
|
||||
ELSE 'fallback'
|
||||
END,
|
||||
CASE
|
||||
WHEN is_fallback = FALSE AND role_rank = 1 THEN priority
|
||||
ELSE GREATEST(priority, 90)
|
||||
END,
|
||||
TRUE,
|
||||
canonical_product_id,
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
COALESCE(NULLIF(source, ''), 'legacy_sync'),
|
||||
COALESCE(note, ''),
|
||||
created_by_admin_user_id,
|
||||
COALESCE(created_at, NOW()),
|
||||
COALESCE(updated_at, NOW())
|
||||
FROM ranked;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_internal_sku TEXT;
|
||||
BEGIN
|
||||
FOR v_internal_sku IN
|
||||
SELECT DISTINCT internal_sku
|
||||
FROM internal_sku_channels
|
||||
WHERE enabled = TRUE
|
||||
LOOP
|
||||
PERFORM sync_sku_channel_routes_for_key(v_internal_sku);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
@@ -1,57 +0,0 @@
|
||||
-- 158_manual_source_is_not_manual_route.sql
|
||||
--
|
||||
-- man: 只表示商品来源,不能证明当前由人工履约。
|
||||
-- 兼容期内,自动发货 / 接单平台 / 第三方卡券这类直连来源允许暂时由 source_kind
|
||||
-- 推导当前渠道;人工客服必须有真实启用的 primary route,否则就是“未路由”。
|
||||
|
||||
CREATE OR REPLACE VIEW v_product_current AS
|
||||
SELECT
|
||||
product.*,
|
||||
sku.internal_sku_key,
|
||||
COALESCE(
|
||||
primary_route.channel,
|
||||
CASE product.source_kind
|
||||
WHEN 'affiliate_dash' THEN 'affiliate_dash'
|
||||
WHEN 'work_order' THEN 'work_order'
|
||||
WHEN 'third_party_voucher' THEN 'third_party_voucher'
|
||||
ELSE NULL
|
||||
END
|
||||
) AS active_primary_channel,
|
||||
COALESCE(fallback_routes.channels, ARRAY[]::text[]) AS fallback_channels,
|
||||
manual_config.id AS manual_config_id,
|
||||
(manual_config.id IS NOT NULL) AS has_active_manual_config
|
||||
FROM product_catalog product
|
||||
LEFT JOIN internal_skus sku ON sku.id = product.internal_sku_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT route.channel
|
||||
FROM sku_channel_routes route
|
||||
WHERE route.internal_sku_id = product.internal_sku_id
|
||||
AND route.enabled = TRUE
|
||||
AND route.route_role = 'primary'
|
||||
ORDER BY route.priority, route.channel
|
||||
LIMIT 1
|
||||
) primary_route ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT ARRAY_AGG(route.channel ORDER BY route.priority, route.channel) AS channels
|
||||
FROM sku_channel_routes route
|
||||
WHERE route.internal_sku_id = product.internal_sku_id
|
||||
AND route.enabled = TRUE
|
||||
AND route.route_role = 'fallback'
|
||||
) fallback_routes ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT config.id
|
||||
FROM manual_fulfillment_configs config
|
||||
WHERE config.enabled = TRUE
|
||||
AND (
|
||||
config.canonical_product_id = product.id
|
||||
OR (
|
||||
sku.internal_sku_key IS NOT NULL
|
||||
AND canonicalize_internal_sku(config.sku_key) = sku.internal_sku_key
|
||||
)
|
||||
)
|
||||
ORDER BY config.id
|
||||
LIMIT 1
|
||||
) manual_config ON TRUE;
|
||||
|
||||
COMMENT ON VIEW v_product_current IS
|
||||
'标准商品统一投影:来源不等于当前渠道;manual 必须有真实主路由,直连来源兼容期允许回退 source_kind';
|
||||
@@ -1,49 +0,0 @@
|
||||
-- 159_archive_orphan_manual_products.sql
|
||||
--
|
||||
-- man: 只表示来源,不能证明当前可履约。
|
||||
-- 没有主路由、没有启用人工配置、也没有启用商品映射的 man 商品属于孤儿商品,
|
||||
-- 只归档,不删除;历史订单、成本版本和成本快照继续保留。
|
||||
|
||||
WITH candidates AS (
|
||||
SELECT product.id
|
||||
FROM product_catalog product
|
||||
WHERE product.status = 'active'
|
||||
AND product.source_kind = 'manual'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sku_channel_routes route
|
||||
WHERE route.internal_sku_id = product.internal_sku_id
|
||||
AND route.enabled = TRUE
|
||||
AND route.route_role = 'primary'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM manual_fulfillment_configs config
|
||||
WHERE config.enabled = TRUE
|
||||
AND (
|
||||
config.canonical_product_id = product.id
|
||||
OR config.internal_sku_id = product.internal_sku_id
|
||||
OR canonicalize_internal_sku(config.sku_key)
|
||||
= canonicalize_internal_sku(substring(product.canonical_sku from 5))
|
||||
)
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM channel_product_mappings mapping
|
||||
WHERE mapping.canonical_product_id = product.id
|
||||
AND mapping.enabled = TRUE
|
||||
)
|
||||
)
|
||||
UPDATE product_catalog product
|
||||
SET status = 'archived',
|
||||
attributes_json = COALESCE(product.attributes_json, '{}'::jsonb) || jsonb_build_object(
|
||||
'legacyManualArchived', TRUE,
|
||||
'legacyManualArchiveReason', 'no_primary_route_no_config_no_mapping',
|
||||
'legacyManualArchivedAt', NOW()
|
||||
),
|
||||
updated_at = NOW()
|
||||
FROM candidates
|
||||
WHERE product.id = candidates.id;
|
||||
|
||||
COMMENT ON COLUMN product_catalog.source_kind IS
|
||||
'标准商品来源类型;来源不等于当前履约渠道,孤儿 man 商品只归档不删除';
|
||||
@@ -1,433 +0,0 @@
|
||||
-- 160_switch_fulfillment_routing_to_routes.sql
|
||||
--
|
||||
-- 硬切换履约路由真源:
|
||||
-- 1. 所有标准商品补齐 internal_sku_id;
|
||||
-- 2. 运行时唯一路由真源改为 sku_channel_routes;
|
||||
-- 3. 商品当前渠道不再回退 source_kind;
|
||||
-- 4. 履约读模型不再读取 internal_sku_channels;
|
||||
-- 5. 停止旧表触发同步,不再双写。
|
||||
|
||||
UPDATE product_catalog product
|
||||
SET internal_sku_id = ensure_canonical_internal_sku(product.canonical_name),
|
||||
updated_at = NOW()
|
||||
WHERE product.internal_sku_id IS NULL
|
||||
AND canonicalize_internal_sku(product.canonical_name) <> '';
|
||||
|
||||
CREATE OR REPLACE FUNCTION normalize_product_identity()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.source_kind := CASE
|
||||
WHEN NEW.canonical_sku LIKE 'aff:%' THEN 'affiliate_dash'
|
||||
WHEN NEW.canonical_sku LIKE 'tpl:%' OR NEW.canonical_sku LIKE 'wo:%' THEN 'work_order'
|
||||
WHEN NEW.canonical_sku LIKE 'tpv:%' THEN 'third_party_voucher'
|
||||
WHEN NEW.canonical_sku LIKE 'man:%' THEN 'manual'
|
||||
ELSE COALESCE(NULLIF(NEW.source_kind, ''), 'unclassified')
|
||||
END;
|
||||
|
||||
IF NEW.internal_sku_id IS NULL THEN
|
||||
NEW.internal_sku_id := ensure_canonical_internal_sku(NEW.canonical_name);
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_sync_sku_channel_routes ON internal_sku_channels;
|
||||
DROP FUNCTION IF EXISTS sync_sku_channel_routes_trigger();
|
||||
DROP FUNCTION IF EXISTS sync_sku_channel_routes_for_key(TEXT);
|
||||
|
||||
COMMENT ON TABLE internal_sku_channels IS
|
||||
'冻结的旧路由输入表;迁移 160 后运行时不读不写,仅保留历史数据供追溯';
|
||||
|
||||
CREATE OR REPLACE VIEW v_product_current AS
|
||||
SELECT
|
||||
product.*,
|
||||
sku.internal_sku_key,
|
||||
primary_route.channel AS active_primary_channel,
|
||||
COALESCE(fallback_routes.channels, ARRAY[]::text[]) AS fallback_channels,
|
||||
manual_config.id AS manual_config_id,
|
||||
(manual_config.id IS NOT NULL) AS has_active_manual_config
|
||||
FROM product_catalog product
|
||||
LEFT JOIN internal_skus sku ON sku.id = product.internal_sku_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT route.channel
|
||||
FROM sku_channel_routes route
|
||||
WHERE route.internal_sku_id = product.internal_sku_id
|
||||
AND route.enabled = TRUE
|
||||
AND route.route_role = 'primary'
|
||||
ORDER BY route.priority, route.channel
|
||||
LIMIT 1
|
||||
) primary_route ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT ARRAY_AGG(route.channel ORDER BY route.priority, route.channel) AS channels
|
||||
FROM sku_channel_routes route
|
||||
WHERE route.internal_sku_id = product.internal_sku_id
|
||||
AND route.enabled = TRUE
|
||||
AND route.route_role = 'fallback'
|
||||
) fallback_routes ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT config.id
|
||||
FROM manual_fulfillment_configs config
|
||||
WHERE config.enabled = TRUE
|
||||
AND (
|
||||
config.canonical_product_id = product.id
|
||||
OR (
|
||||
sku.internal_sku_key IS NOT NULL
|
||||
AND canonicalize_internal_sku(config.sku_key) = sku.internal_sku_key
|
||||
)
|
||||
)
|
||||
ORDER BY config.id
|
||||
LIMIT 1
|
||||
) manual_config ON TRUE;
|
||||
|
||||
COMMENT ON VIEW v_product_current IS
|
||||
'标准商品统一投影:来源与当前渠道分离,当前渠道只来自 sku_channel_routes';
|
||||
|
||||
CREATE OR REPLACE VIEW v_fulfillment_item_state AS
|
||||
WITH active_items AS (
|
||||
SELECT
|
||||
oi.*,
|
||||
resolve_internal_sku(oi.manual_sku_key, oi.sku_name, oi.sku_code) AS internal_sku
|
||||
FROM order_items oi
|
||||
WHERE oi.fulfillment_status IN ('pending', 'assigned')
|
||||
),
|
||||
base AS (
|
||||
SELECT
|
||||
oi.id AS order_item_id,
|
||||
oi.order_id,
|
||||
o.id AS local_order_id,
|
||||
o.provider,
|
||||
o.platform,
|
||||
o.shop_id,
|
||||
o.shop_name,
|
||||
o.platform_order_id,
|
||||
o.pay_status,
|
||||
o.order_status,
|
||||
oi.fulfillment_status AS item_fulfillment_status,
|
||||
oi.fulfillment_channel AS stored_channel,
|
||||
oi.fulfillment_source,
|
||||
oi.fulfillment_locked,
|
||||
oi.fulfillment_pending_reason,
|
||||
oi.fulfillment_pending_detail,
|
||||
oi.sku_code,
|
||||
oi.sku_name,
|
||||
oi.quantity,
|
||||
oi.revenue_amount_fen,
|
||||
oi.created_at,
|
||||
oi.updated_at,
|
||||
oi.internal_sku,
|
||||
channel_plan.channel AS configured_channel,
|
||||
channel_plan.config_json AS configured_channel_config,
|
||||
COALESCE(channel_stats.channel_count, 0) AS channel_count,
|
||||
COALESCE(latest_task.task_id, 0) AS latest_task_id,
|
||||
latest_task.executor_key AS latest_task_executor_key,
|
||||
latest_task.task_status AS latest_task_status,
|
||||
latest_task.result_code AS latest_task_result_code,
|
||||
latest_task.result_message AS latest_task_result_message,
|
||||
latest_task.updated_at AS latest_task_updated_at,
|
||||
COALESCE(latest_work_order.work_order_id, 0) AS latest_work_order_id,
|
||||
latest_work_order.status AS latest_work_order_status,
|
||||
latest_work_order.result_code AS latest_work_order_result_code,
|
||||
latest_work_order.updated_at AS latest_work_order_updated_at,
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM fulfillment_tasks active_task
|
||||
WHERE active_task.order_item_id = oi.id
|
||||
AND active_task.executor_key IN ('affiliate_dash', 'manual_dispatch')
|
||||
AND active_task.task_status NOT IN (
|
||||
'closed', 'expired', 'failed', 'manual_review', 'retry_pending', 'cancelled'
|
||||
)
|
||||
) AS has_active_task,
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM work_orders active_work_order
|
||||
WHERE active_work_order.order_item_id = oi.id
|
||||
AND active_work_order.status <> 'cancelled'
|
||||
) AS has_active_work_order,
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM manual_fulfillment_configs cfg
|
||||
WHERE cfg.enabled = TRUE
|
||||
AND canonicalize_internal_sku(cfg.sku_key) = oi.internal_sku
|
||||
) AS has_manual_config
|
||||
FROM active_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT route.channel, route.carrier_config_json AS config_json
|
||||
FROM sku_channel_routes route
|
||||
JOIN internal_skus sku ON sku.id = route.internal_sku_id
|
||||
WHERE route.enabled = TRUE
|
||||
AND sku.internal_sku_key = oi.internal_sku
|
||||
ORDER BY
|
||||
(route.route_role = 'fallback'),
|
||||
route.priority,
|
||||
route.channel,
|
||||
route.id
|
||||
LIMIT 1
|
||||
) channel_plan ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COUNT(*)::int AS channel_count
|
||||
FROM sku_channel_routes route
|
||||
JOIN internal_skus sku ON sku.id = route.internal_sku_id
|
||||
WHERE route.enabled = TRUE
|
||||
AND sku.internal_sku_key = oi.internal_sku
|
||||
) channel_stats ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
task.id AS task_id,
|
||||
task.executor_key,
|
||||
task.task_status,
|
||||
task.result_code,
|
||||
task.result_message,
|
||||
task.updated_at
|
||||
FROM fulfillment_tasks task
|
||||
WHERE task.order_item_id = oi.id
|
||||
ORDER BY task.id DESC
|
||||
LIMIT 1
|
||||
) latest_task ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
work_order.id AS work_order_id,
|
||||
work_order.status,
|
||||
''::text AS result_code,
|
||||
work_order.updated_at
|
||||
FROM work_orders work_order
|
||||
WHERE work_order.order_item_id = oi.id
|
||||
ORDER BY work_order.id DESC
|
||||
LIMIT 1
|
||||
) latest_work_order ON TRUE
|
||||
),
|
||||
classified AS (
|
||||
SELECT
|
||||
base.*,
|
||||
COALESCE(base.stored_channel, base.configured_channel) AS intended_channel,
|
||||
CASE
|
||||
WHEN base.pay_status = 'refunded'
|
||||
OR base.order_status IN ('refunded', 'cancelled')
|
||||
OR base.latest_task_result_code = 'USER_APPLY_REFUND'
|
||||
THEN TRUE
|
||||
ELSE FALSE
|
||||
END AS is_user_refund,
|
||||
CASE
|
||||
WHEN base.pay_status = 'cancelled'
|
||||
OR base.order_status = 'cancelled'
|
||||
OR base.latest_task_result_code = 'affiliate_dash_cancelled'
|
||||
THEN TRUE
|
||||
ELSE FALSE
|
||||
END AS is_platform_cancelled
|
||||
FROM base
|
||||
),
|
||||
current_attempt AS (
|
||||
SELECT
|
||||
classified.*,
|
||||
CASE
|
||||
WHEN classified.latest_task_id > 0
|
||||
AND (
|
||||
classified.latest_work_order_id = 0
|
||||
OR classified.latest_task_updated_at >= classified.latest_work_order_updated_at
|
||||
)
|
||||
THEN 'task'
|
||||
WHEN classified.latest_work_order_id > 0 THEN 'work_order'
|
||||
ELSE ''
|
||||
END AS current_attempt_type,
|
||||
CASE
|
||||
WHEN classified.latest_task_id > 0
|
||||
AND (
|
||||
classified.latest_work_order_id = 0
|
||||
OR classified.latest_task_updated_at >= classified.latest_work_order_updated_at
|
||||
)
|
||||
THEN classified.latest_task_id
|
||||
WHEN classified.latest_work_order_id > 0 THEN classified.latest_work_order_id
|
||||
ELSE 0
|
||||
END AS current_attempt_id,
|
||||
CASE
|
||||
WHEN classified.latest_task_id > 0
|
||||
AND (
|
||||
classified.latest_work_order_id = 0
|
||||
OR classified.latest_task_updated_at >= classified.latest_work_order_updated_at
|
||||
)
|
||||
THEN classified.latest_task_status
|
||||
WHEN classified.latest_work_order_id > 0 THEN classified.latest_work_order_status
|
||||
ELSE ''
|
||||
END AS current_attempt_status,
|
||||
CASE
|
||||
WHEN classified.latest_task_id > 0
|
||||
AND (
|
||||
classified.latest_work_order_id = 0
|
||||
OR classified.latest_task_updated_at >= classified.latest_work_order_updated_at
|
||||
)
|
||||
THEN classified.latest_task_result_code
|
||||
WHEN classified.latest_work_order_id > 0 THEN classified.latest_work_order_result_code
|
||||
ELSE ''
|
||||
END AS current_attempt_result_code
|
||||
FROM classified
|
||||
),
|
||||
state_classified AS (
|
||||
SELECT
|
||||
current_attempt.*,
|
||||
CASE
|
||||
WHEN current_attempt.intended_channel IN ('third_party_voucher', 'on_account')
|
||||
THEN 'not_required'
|
||||
WHEN current_attempt.intended_channel = 'manual'
|
||||
AND current_attempt.has_manual_config
|
||||
THEN 'ready'
|
||||
WHEN current_attempt.has_active_task OR current_attempt.has_active_work_order
|
||||
THEN 'ready'
|
||||
WHEN current_attempt.latest_task_status IN (
|
||||
'closed', 'expired', 'failed', 'manual_review', 'retry_pending', 'cancelled'
|
||||
)
|
||||
THEN 'terminal'
|
||||
WHEN current_attempt.latest_work_order_status = 'cancelled'
|
||||
THEN 'terminal'
|
||||
WHEN current_attempt.intended_channel IS NOT NULL
|
||||
THEN 'missing'
|
||||
ELSE 'missing'
|
||||
END AS carrier_status,
|
||||
CASE
|
||||
WHEN COALESCE(current_attempt.internal_sku, '') = ''
|
||||
THEN 'missing_internal_sku'
|
||||
WHEN current_attempt.channel_count = 0
|
||||
THEN 'missing_channel'
|
||||
ELSE 'configured'
|
||||
END AS route_status
|
||||
FROM current_attempt
|
||||
),
|
||||
issue_classified AS (
|
||||
SELECT
|
||||
state_classified.*,
|
||||
CASE
|
||||
WHEN state_classified.is_user_refund THEN 'user_refund'
|
||||
WHEN state_classified.is_platform_cancelled THEN 'platform_cancelled'
|
||||
WHEN state_classified.route_status = 'missing_internal_sku' THEN 'missing_internal_sku'
|
||||
WHEN state_classified.route_status = 'missing_channel' THEN 'missing_channel'
|
||||
WHEN state_classified.intended_channel = 'manual'
|
||||
AND NOT state_classified.has_manual_config
|
||||
THEN 'manual_config_missing'
|
||||
WHEN state_classified.intended_channel = 'work_order'
|
||||
AND BTRIM(COALESCE(
|
||||
state_classified.configured_channel_config->>'workRuleKey',
|
||||
''
|
||||
)) = ''
|
||||
THEN 'work_rule_missing'
|
||||
WHEN state_classified.intended_channel = 'affiliate_dash'
|
||||
AND BTRIM(COALESCE(
|
||||
state_classified.configured_channel_config->>'affiliateSku',
|
||||
''
|
||||
)) = ''
|
||||
THEN 'affiliate_mapping_missing'
|
||||
WHEN state_classified.carrier_status IN ('ready', 'not_required') THEN ''
|
||||
WHEN state_classified.latest_task_status = 'manual_review'
|
||||
THEN 'carrier_manual_review'
|
||||
WHEN state_classified.latest_task_status IN ('failed', 'retry_pending')
|
||||
THEN 'carrier_retry_pending'
|
||||
WHEN state_classified.latest_task_status = 'expired' THEN 'carrier_expired'
|
||||
WHEN state_classified.latest_task_status = 'closed'
|
||||
OR state_classified.latest_work_order_status = 'cancelled'
|
||||
THEN 'carrier_closed'
|
||||
WHEN state_classified.item_fulfillment_status = 'assigned'
|
||||
THEN 'state_inconsistent'
|
||||
WHEN state_classified.intended_channel IN (
|
||||
'affiliate_dash', 'work_order', 'manual'
|
||||
)
|
||||
THEN 'carrier_missing'
|
||||
ELSE 'state_inconsistent'
|
||||
END AS issue_code
|
||||
FROM state_classified
|
||||
),
|
||||
categorized AS (
|
||||
SELECT
|
||||
issue_classified.*,
|
||||
CASE
|
||||
WHEN issue_code = '' THEN ''
|
||||
WHEN issue_code IN ('user_refund', 'platform_cancelled') THEN 'after_sales'
|
||||
WHEN issue_code = 'carrier_manual_review' THEN 'manual_review'
|
||||
WHEN issue_code IN ('missing_internal_sku', 'missing_channel') THEN 'classification'
|
||||
WHEN issue_code IN (
|
||||
'manual_config_missing', 'work_rule_missing', 'affiliate_mapping_missing'
|
||||
) THEN 'configuration'
|
||||
WHEN issue_code = 'state_inconsistent' THEN 'system'
|
||||
ELSE 'carrier'
|
||||
END AS issue_category
|
||||
FROM issue_classified
|
||||
)
|
||||
SELECT
|
||||
order_item_id,
|
||||
order_id,
|
||||
local_order_id,
|
||||
provider,
|
||||
platform,
|
||||
shop_id,
|
||||
shop_name,
|
||||
platform_order_id,
|
||||
pay_status,
|
||||
order_status,
|
||||
item_fulfillment_status AS fulfillment_status,
|
||||
internal_sku,
|
||||
intended_channel,
|
||||
stored_channel,
|
||||
configured_channel,
|
||||
route_status,
|
||||
carrier_status,
|
||||
current_attempt_type,
|
||||
current_attempt_id,
|
||||
current_attempt_status,
|
||||
current_attempt_result_code,
|
||||
latest_task_id,
|
||||
latest_task_executor_key,
|
||||
latest_task_status,
|
||||
latest_task_result_code,
|
||||
latest_work_order_id,
|
||||
latest_work_order_status,
|
||||
issue_code,
|
||||
issue_category,
|
||||
CASE
|
||||
WHEN issue_code = '' THEN FALSE
|
||||
WHEN issue_code IN (
|
||||
'user_refund', 'platform_cancelled', 'carrier_manual_review',
|
||||
'manual_config_missing', 'work_rule_missing', 'affiliate_mapping_missing',
|
||||
'missing_internal_sku', 'missing_channel'
|
||||
) THEN FALSE
|
||||
ELSE TRUE
|
||||
END AS retryable,
|
||||
CASE
|
||||
WHEN issue_code IN ('carrier_missing', 'state_inconsistent') THEN TRUE
|
||||
ELSE FALSE
|
||||
END AS auto_retryable,
|
||||
CASE
|
||||
WHEN issue_code IN ('user_refund', 'platform_cancelled', 'carrier_manual_review')
|
||||
THEN 'support'
|
||||
WHEN issue_code IN (
|
||||
'missing_internal_sku', 'missing_channel', 'manual_config_missing',
|
||||
'work_rule_missing', 'affiliate_mapping_missing'
|
||||
) THEN 'operator'
|
||||
WHEN issue_code = '' THEN ''
|
||||
ELSE 'system'
|
||||
END AS owner_role,
|
||||
fulfillment_locked,
|
||||
CASE
|
||||
WHEN NOT fulfillment_locked THEN ''
|
||||
ELSE COALESCE(
|
||||
NULLIF(fulfillment_pending_detail->>'carrierIssue', ''),
|
||||
NULLIF(fulfillment_pending_detail->>'reason', ''),
|
||||
'operator_assigned'
|
||||
)
|
||||
END AS lock_reason,
|
||||
fulfillment_pending_reason,
|
||||
fulfillment_pending_detail,
|
||||
fulfillment_source,
|
||||
sku_code,
|
||||
sku_name,
|
||||
quantity,
|
||||
revenue_amount_fen,
|
||||
created_at,
|
||||
updated_at,
|
||||
GREATEST(
|
||||
0,
|
||||
FLOOR(EXTRACT(EPOCH FROM (NOW() - created_at)) / 60)
|
||||
)::bigint AS age_minutes
|
||||
FROM categorized;
|
||||
|
||||
COMMENT ON VIEW v_fulfillment_item_state IS
|
||||
'履约统一读模型:每个订单项一行,仅读取 sku_channel_routes 的当前路由';
|
||||
@@ -1,46 +0,0 @@
|
||||
-- 161_move_shadow_sync_to_sku_routes.sql
|
||||
--
|
||||
-- 路由唯一真源已经切换到 sku_channel_routes。
|
||||
-- 履约影子模型必须跟随新表变化,旧表不再拥有任何同步触发器。
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_internal_sku_channel_shadow ON internal_sku_channels;
|
||||
DROP FUNCTION IF EXISTS sync_fulfillment_shadow_for_internal_channel();
|
||||
|
||||
CREATE OR REPLACE FUNCTION sync_fulfillment_shadow_for_sku_route()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_key TEXT;
|
||||
new_key TEXT;
|
||||
BEGIN
|
||||
IF TG_OP <> 'INSERT' THEN
|
||||
SELECT internal_sku_key INTO old_key
|
||||
FROM internal_skus
|
||||
WHERE id = OLD.internal_sku_id;
|
||||
END IF;
|
||||
IF TG_OP <> 'DELETE' THEN
|
||||
SELECT internal_sku_key INTO new_key
|
||||
FROM internal_skus
|
||||
WHERE id = NEW.internal_sku_id;
|
||||
END IF;
|
||||
|
||||
PERFORM sync_fulfillment_shadow(item.id)
|
||||
FROM order_items item
|
||||
WHERE item.fulfillment_status IN ('pending', 'assigned')
|
||||
AND canonicalize_internal_sku(
|
||||
resolve_internal_sku(item.manual_sku_key, item.sku_name, item.sku_code)
|
||||
) IN (old_key, new_key);
|
||||
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_sku_route_fulfillment_shadow ON sku_channel_routes;
|
||||
CREATE TRIGGER trg_sku_route_fulfillment_shadow
|
||||
AFTER INSERT OR UPDATE OR DELETE ON sku_channel_routes
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION sync_fulfillment_shadow_for_sku_route();
|
||||
|
||||
COMMENT ON FUNCTION sync_fulfillment_shadow_for_sku_route() IS
|
||||
'sku_channel_routes 变化后刷新同内部 SKU 的履约影子';
|
||||
@@ -1,12 +0,0 @@
|
||||
-- 162_drop_legacy_routing_read_surfaces.sql
|
||||
--
|
||||
-- 160/161 已把业务读模型和影子同步切到 sku_channel_routes。
|
||||
-- 这里删除最后一个旧表只读视图,以及旧表最后一个规范化触发器。
|
||||
|
||||
DROP VIEW IF EXISTS v_fulfillment_item_state_raw;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_normalize_internal_sku_channel_key ON internal_sku_channels;
|
||||
DROP FUNCTION IF EXISTS normalize_internal_sku_channel_key();
|
||||
|
||||
COMMENT ON TABLE internal_sku_channels IS
|
||||
'冻结的历史路由输入;无触发器、无运行时读取、无运行时写入';
|
||||
@@ -1,13 +0,0 @@
|
||||
-- 164_fulfillment_event_cascade.sql
|
||||
--
|
||||
-- 履约事件已经由 fulfillment_id 直接归属正式履约记录。
|
||||
-- 订单项删除时事件应随履约记录级联清理,不能先 SET NULL 再触发 append-only 更新保护。
|
||||
|
||||
ALTER TABLE fulfillment_state_events
|
||||
DROP CONSTRAINT IF EXISTS fk_fulfillment_state_events_fulfillment;
|
||||
|
||||
ALTER TABLE fulfillment_state_events
|
||||
ADD CONSTRAINT fk_fulfillment_state_events_fulfillment
|
||||
FOREIGN KEY (fulfillment_id)
|
||||
REFERENCES fulfillments(id)
|
||||
ON DELETE CASCADE;
|
||||
@@ -1,16 +0,0 @@
|
||||
-- 165_fulfillment_event_append_only_update.sql
|
||||
--
|
||||
-- 事件语义是“应用不可改”,父记录删除时允许数据库级联清理。
|
||||
-- 旧环境里曾存在 BEFORE DELETE OR UPDATE 的漂移定义,这里统一收回为只阻止 UPDATE。
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_fulfillment_state_events_append_only
|
||||
ON fulfillment_state_events;
|
||||
|
||||
CREATE TRIGGER trg_fulfillment_state_events_append_only
|
||||
BEFORE UPDATE ON fulfillment_state_events
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION reject_fulfillment_state_event_mutation();
|
||||
|
||||
COMMENT ON TRIGGER trg_fulfillment_state_events_append_only
|
||||
ON fulfillment_state_events IS
|
||||
'禁止应用修改履约事件;数据库外键级联删除不受影响';
|
||||
@@ -1,59 +0,0 @@
|
||||
-- 166_fulfillment_insert_resolved_sku.sql
|
||||
--
|
||||
-- resolve_internal_sku 可能返回 NULL。正式履约行的 internal_sku 是不带 NULL 的稳定键,
|
||||
-- 无法解析时统一落空字符串,并立即进入 missing_internal_sku 异常口径。
|
||||
|
||||
CREATE OR REPLACE FUNCTION ensure_fulfillment_for_order_item()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO fulfillments (
|
||||
order_item_id,
|
||||
order_id,
|
||||
internal_sku,
|
||||
route_status,
|
||||
channel,
|
||||
carrier_status,
|
||||
current_attempt_type,
|
||||
current_attempt_source_id,
|
||||
current_attempt_status,
|
||||
lock_reason,
|
||||
issue_code,
|
||||
issue_category,
|
||||
retryable,
|
||||
owner_role,
|
||||
created_at,
|
||||
updated_at,
|
||||
status,
|
||||
source,
|
||||
locked,
|
||||
channel_switch_reason
|
||||
) VALUES (
|
||||
NEW.id,
|
||||
NEW.order_id,
|
||||
COALESCE(resolve_internal_sku(NEW.manual_sku_key, NEW.sku_name, NEW.sku_code), ''),
|
||||
'unrouted',
|
||||
NULL,
|
||||
'missing',
|
||||
'',
|
||||
NULL,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
FALSE,
|
||||
'',
|
||||
NEW.created_at,
|
||||
NOW(),
|
||||
'pending',
|
||||
'',
|
||||
FALSE,
|
||||
''
|
||||
)
|
||||
ON CONFLICT (order_item_id) DO NOTHING;
|
||||
|
||||
PERFORM refresh_fulfillment_derived(NEW.id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
@@ -1,29 +0,0 @@
|
||||
-- 168_product_source_kind_constraint.sql
|
||||
--
|
||||
-- 商品来源统一由 product_catalog.source_kind 表达。
|
||||
-- 运行期不再解析 canonical_sku 前缀;数据库同步收紧允许值。
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'chk_product_catalog_source_kind'
|
||||
) THEN
|
||||
ALTER TABLE product_catalog
|
||||
ADD CONSTRAINT chk_product_catalog_source_kind
|
||||
CHECK (
|
||||
source_kind IN (
|
||||
'affiliate_dash',
|
||||
'work_order',
|
||||
'third_party_voucher',
|
||||
'manual',
|
||||
'unclassified'
|
||||
)
|
||||
)
|
||||
NOT VALID;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE product_catalog
|
||||
VALIDATE CONSTRAINT chk_product_catalog_source_kind;
|
||||
@@ -157,7 +157,7 @@ test('销售毛利必须同时暴露缺失成本收入,避免只看毛利绝
|
||||
test('销售事实必须只统计已支付订单,并扣减已确认退款', async () => {
|
||||
const [source, scopeMigration] = await Promise.all([
|
||||
read(new URL('./sales-facts-channel-service.ts', import.meta.url)),
|
||||
read(new URL('../../db/migrations/167_finance_derived_scope.sql', import.meta.url)),
|
||||
read(new URL('../../db/migrations/149_finance_derived_scope.sql', import.meta.url)),
|
||||
])
|
||||
assert.ok(
|
||||
scopeMigration.includes("order_record.pay_status IN ('paid', 'refunded')"),
|
||||
|
||||
@@ -6,7 +6,7 @@ function read(relativePath: string) {
|
||||
return readFileSync(new URL(relativePath, import.meta.url), 'utf8')
|
||||
}
|
||||
|
||||
const migration = read('../../db/migrations/167_finance_derived_scope.sql')
|
||||
const migration = read('../../db/migrations/149_finance_derived_scope.sql')
|
||||
const salesFacts = read('./sales-facts-channel-service.ts')
|
||||
const ledger = read('./finance-ledger-service.ts')
|
||||
const rebuild = read('../../../scripts/rebuild-finance-derived.ts')
|
||||
|
||||
@@ -7,7 +7,7 @@ function read(relativePath: string) {
|
||||
}
|
||||
|
||||
const migration = read('../../db/migrations/140_fulfillment_state_events.sql')
|
||||
const cutoverMigration = read('../../db/migrations/163_cutover_fulfillment_truth.sql')
|
||||
const cutoverMigration = read('../../db/migrations/148_fulfillment_execution_truth.sql')
|
||||
const stateService = read('./fulfillment-state-service.ts')
|
||||
const channelAssign = read('./channel-assign-service.ts')
|
||||
const carrierReconciliation = read('./carrier-reconciliation-service.ts')
|
||||
|
||||
@@ -15,15 +15,15 @@ const configSyncMigration = readFileSync(
|
||||
'utf8',
|
||||
)
|
||||
const routeCutoverMigration = readFileSync(
|
||||
new URL('../../db/migrations/161_move_shadow_sync_to_sku_routes.sql', import.meta.url),
|
||||
new URL('../../db/migrations/147_internal_sku_product_routing.sql', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const truthCutoverMigration = readFileSync(
|
||||
new URL('../../db/migrations/163_cutover_fulfillment_truth.sql', import.meta.url),
|
||||
new URL('../../db/migrations/148_fulfillment_execution_truth.sql', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const insertSkuMigration = readFileSync(
|
||||
new URL('../../db/migrations/166_fulfillment_insert_resolved_sku.sql', import.meta.url),
|
||||
new URL('../../db/migrations/148_fulfillment_execution_truth.sql', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ test('正式履约模型包含履约、尝试和异常三张表', () => {
|
||||
assert.ok(migration.includes('UNIQUE (fulfillment_id, issue_code)'))
|
||||
})
|
||||
|
||||
test('迁移 163 删除影子双写并直接维护正式履约真源', () => {
|
||||
test('迁移 148 删除影子双写并直接维护正式履约真源', () => {
|
||||
assert.ok(
|
||||
truthCutoverMigration.includes('DROP TRIGGER IF EXISTS trg_order_items_fulfillment_shadow'),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user