feat(cost-center): 商品与成本按履约载体分区,补充分页与删除能力
- 新增迁移 107_product_catalog_carrier:product_catalog 增加 carrier_type, 自动发货只认 fulfillment_tasks.executor_key='affiliate_dash' 的履约证据 (28 个上游 SKU),接单模板商品统一归为 work_order(662 个) - 页面按「全部 / 自动发货 / 接单平台 / 人工客服 / 共用成本」分区; 自动发货只展示自动发货 SKU 列表与采购成本,不展示商品目录/渠道映射 - 新增「同步自动发货商品」接口与按钮,按 aff:<skuCode> 幂等补建 - 列表统一接入公共分页 buildAdminTablePagination,支持 10/20/50/100 每页条数 - 标准商品、自动发货 SKU、渠道映射、成本项目、成本版本支持删除, 带引用保护(映射/版本/快照/订单/接单规则)与生效版本先停用约束
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
-- 107_product_catalog_carrier.sql —— 标准商品增加「默认履约载体」。
|
||||
--
|
||||
-- 背景:商品主数据此前只有游戏/品类分类,成本项目与成本版本却按
|
||||
-- 自动发货 / 接单平台 / 人工 三类执行载体配置,后台配置时找不到对应商品。
|
||||
--
|
||||
-- 口径(2026-10-22 修正):
|
||||
-- 1. 只有真正走 affiliate-dash(自动发货执行器)的商品才算 affiliate_dash;
|
||||
-- 以 fulfillment_tasks.executor_key = 'affiliate_dash' 的履约证据为准,
|
||||
-- 按 context_json.skuCode / skuName 建标准商品,SKU 固定为 `aff:<skuCode>`。
|
||||
-- 2. 其余标准商品(接单模板初始化出来的 tpl:*)全部是 work_order;
|
||||
-- 渠道映射只代表「平台 SKU 能归一到标准商品」,不代表自动发货。
|
||||
-- 3. manual / shared 不回填,由后台按需手工调整。
|
||||
--
|
||||
-- 2026-10-22 生产快照口径:自动发货任务 41,676 条,涉及 28 个 skuCode。
|
||||
|
||||
ALTER TABLE product_catalog
|
||||
ADD COLUMN IF NOT EXISTS carrier_type TEXT NOT NULL DEFAULT 'work_order';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_product_catalog_carrier_type'
|
||||
) THEN
|
||||
ALTER TABLE product_catalog
|
||||
ADD CONSTRAINT chk_product_catalog_carrier_type
|
||||
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'manual', 'shared'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_product_catalog_status_carrier
|
||||
ON product_catalog (status, carrier_type, canonical_name);
|
||||
|
||||
-- 1) 先把非自动发货商品统一归到接单平台(模板商品、历史误标都回到 work_order)。
|
||||
UPDATE product_catalog
|
||||
SET carrier_type = 'work_order',
|
||||
updated_at = NOW()
|
||||
WHERE carrier_type IS DISTINCT FROM 'work_order'
|
||||
AND canonical_sku NOT LIKE 'aff:%';
|
||||
|
||||
-- 2) 按自动发货履约证据建商品:每个 skuCode 取一个最像人名的名称。
|
||||
WITH auto_task AS (
|
||||
SELECT
|
||||
btrim(context_json->>'skuCode') AS sku_code,
|
||||
btrim(context_json->>'skuName') AS sku_name,
|
||||
created_at
|
||||
FROM fulfillment_tasks
|
||||
WHERE executor_key = 'affiliate_dash'
|
||||
AND btrim(COALESCE(context_json->>'skuCode', '')) <> ''
|
||||
AND btrim(COALESCE(context_json->>'skuName', '')) <> ''
|
||||
),
|
||||
agg AS (
|
||||
SELECT sku_code, count(*)::int AS task_count, max(created_at) AS last_fulfilled_at
|
||||
FROM auto_task
|
||||
GROUP BY sku_code
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
sku_code,
|
||||
sku_name,
|
||||
row_number() OVER (
|
||||
PARTITION BY sku_code
|
||||
ORDER BY (sku_name <> sku_code) DESC, count(*) DESC, sku_name
|
||||
) AS rn
|
||||
FROM auto_task
|
||||
GROUP BY sku_code, sku_name
|
||||
)
|
||||
INSERT INTO product_catalog (
|
||||
canonical_sku, canonical_name, category, unit_name, carrier_type, status,
|
||||
attributes_json, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
'aff:' || ranked.sku_code,
|
||||
ranked.sku_name,
|
||||
'',
|
||||
CASE WHEN ranked.sku_name LIKE '%个' THEN '个' ELSE '件' END,
|
||||
'affiliate_dash',
|
||||
'active',
|
||||
jsonb_build_object(
|
||||
'source', 'affiliate_dash_task',
|
||||
'skuCode', ranked.sku_code,
|
||||
'taskCount', agg.task_count,
|
||||
'lastFulfilledAt', agg.last_fulfilled_at
|
||||
),
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM ranked
|
||||
JOIN agg ON agg.sku_code = ranked.sku_code
|
||||
WHERE ranked.rn = 1
|
||||
ON CONFLICT (canonical_sku) DO UPDATE
|
||||
SET carrier_type = 'affiliate_dash',
|
||||
attributes_json = EXCLUDED.attributes_json,
|
||||
updated_at = NOW();
|
||||
|
||||
COMMENT ON COLUMN product_catalog.carrier_type IS
|
||||
'默认履约载体:affiliate_dash=自动发货,work_order=接单平台,manual=人工,shared=多载体共用';
|
||||
Reference in New Issue
Block a user