feat(finance): 人工履约 SKU 配置清单 + 四钱包(南航/乐玩/ROCK积分 + 备用金现金)
客服人工履约有四个钱包,成本与渠道在后台配置好,客服确认履约时零输入。 口径(2026-10-24 确认): - 四个钱包:南航积分 / 乐玩积分 / ROCK 积分 / 备用金现金。三平台积分互不通用, 每客服每平台一个积分账户;现金复用现有 fund_accounts / fund_entries。 - 积分换算率按平台不同:南航 1 积分 = 1 分,乐玩 1 积分 = 1 分, ROCK 1 积分 = 1 元(100 分)。真源是履约档案 config_json.rechargeUnitPriceFen。 - 积分是货币,幸运币是商品;消耗积分与成本金额按换算率一一对应。 - 配置主键是**内部 SKU**(快手 spec_json.skuNick / 拼多多 spec_json.outSkuSn), 不是商品标题(标题是引流)。同名 SKU 算同一商品,跨渠道共用一份配置。 - 配置链路没有匹配引擎、没有打分:扫描 → 配置 → 重新匹配。 10 月口径下人工单 17,469 单 / 357 个内部 SKU,Top 50 覆盖 88% 单量。 更早的老单没配置意义,不纳入统计口径。 - 消耗积分填 0 = 成本不固定,客服在确认履约时手填实际成本; 渠道始终提前配好,客服不选。 迁移 110: - manual_sku_registry:扫描到的内部 SKU 登记表(统计 + 配置状态)。 - manual_fulfillment_configs:内部 SKU → 渠道 + 消耗积分 + 成本模式。 - platform_credit_accounts / platform_credit_entries:三平台积分钱包与流水。 - order_items.fulfillment_config_id:订单项挂配置,重新匹配时回填。 - 4 条 manual_* 履约档案(manual_dispatch 执行器 + config_json 区分平台)。 不复用 kuaishou_ct_assisted / kuaishou_feifei——那是已下线的历史自动发货通道。 性能:spec_json->>'skuNick' 无索引时扫描 2.4s / 回填 5.4s; 新增 idx_order_items_manual_internal_sku 表达式索引后回填降到 1.7s。 后端:manual-fulfillment-config-service.ts(扫描/清单/配置CRUD/重新匹配), 成本中心挂 7 个接口。 前端:成本中心新增「人工 SKU 配置」Tab(扫描、清单、配置弹窗、重新匹配)。 校验:后端 765 项 / 0 失败(含 check:sql 迁移护栏),前端 143 项全通过; E2E 实测扫描 357 个 SKU、配置四平台换算正确、重新匹配回填 7,604 个订单项; 本地库迁移 110 执行后数据量与改造前一致。 Co-Authored-By: Claude Opus 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
fb679d76f6
commit
4bcb4e6b57
@@ -0,0 +1,260 @@
|
||||
-- 110_manual_fulfillment_platform_credits.sql —— 客服人工履约:SKU 配置清单 + 三平台积分钱包。
|
||||
--
|
||||
-- 背景(2026-10-24 确认):
|
||||
-- 客服人工履约有四个资金/额度钱包,每个客服各一个:
|
||||
-- 南航积分 / 乐玩积分 / ROCK 积分 / 备用金现金
|
||||
-- 三个积分平台的积分**互不通用**,所以每个平台一套账户。
|
||||
-- 换算率固定:1 积分 = 1 分钱(= 1 fen),三平台统一。
|
||||
--
|
||||
-- 配置链路(无匹配引擎、无打分):
|
||||
-- 1. 扫描「走到人工履约」的内部 SKU,登记到 manual_sku_registry;
|
||||
-- 2. 运营在成本中心配置每个 SKU 的「履约渠道 + 消耗积分 + 成本模式」;
|
||||
-- 3. 点「重新匹配」把配置回填到历史人工订单项;
|
||||
-- 4. 客服确认履约时直接读配置,固定成本零输入。
|
||||
--
|
||||
-- 关键口径:
|
||||
-- * 配置主键是**内部 SKU**(快手 skuNick / 拼多多 outSkuSn),不是商品标题;
|
||||
-- * 同名 SKU 算同一个商品,配置全局唯一(跨渠道共用);
|
||||
-- * 积分 0 = 成本不固定,由客服在确认履约时手填实际成本;
|
||||
-- * 备用金现金不建账户行,走现有 fund_accounts / fund_entries。
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. 人工 SKU 登记表:扫描到的「走到人工履约」的内部 SKU
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS manual_sku_registry (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
sku_key TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL DEFAULT '',
|
||||
shop_count INTEGER NOT NULL DEFAULT 0,
|
||||
order_count INTEGER NOT NULL DEFAULT 0,
|
||||
item_count INTEGER NOT NULL DEFAULT 0,
|
||||
revenue_fen BIGINT NOT NULL DEFAULT 0,
|
||||
first_seen_date DATE,
|
||||
last_seen_date DATE,
|
||||
scan_from_date DATE,
|
||||
scan_to_date DATE,
|
||||
config_id BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_manual_sku_registry_sku_key
|
||||
ON manual_sku_registry (sku_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_manual_sku_registry_config
|
||||
ON manual_sku_registry (config_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_manual_sku_registry_item_count
|
||||
ON manual_sku_registry (item_count DESC);
|
||||
|
||||
COMMENT ON TABLE manual_sku_registry IS
|
||||
'人工履约 SKU 登记表:扫描到的内部 SKU(快手 skuNick / 拼多多 outSkuSn),供运营集中配置';
|
||||
COMMENT ON COLUMN manual_sku_registry.sku_key IS
|
||||
'内部 SKU,配置主键;同名 SKU 视为同一商品,跨渠道共用一份配置';
|
||||
COMMENT ON COLUMN manual_sku_registry.display_name IS
|
||||
'商品名称,仅用于展示识别,不是配置键';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. 人工履约配置:每个内部 SKU 的渠道、积分与成本模式
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS manual_fulfillment_configs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
sku_key TEXT NOT NULL,
|
||||
canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE SET NULL,
|
||||
platform_key TEXT NOT NULL,
|
||||
credit_quantity BIGINT NOT NULL DEFAULT 0,
|
||||
cost_mode TEXT NOT NULL DEFAULT 'fixed',
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_manual_fulfillment_configs_sku_key
|
||||
ON manual_fulfillment_configs (sku_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_manual_fulfillment_configs_platform
|
||||
ON manual_fulfillment_configs (platform_key);
|
||||
|
||||
COMMENT ON TABLE manual_fulfillment_configs IS
|
||||
'人工履约配置:内部 SKU → 履约渠道 + 消耗积分 + 成本模式';
|
||||
COMMENT ON COLUMN manual_fulfillment_configs.credit_quantity IS
|
||||
'每单消耗积分(整数)。换算率固定 1 积分 = 1 分钱;0 表示成本不固定,客服确认履约时手填';
|
||||
COMMENT ON COLUMN manual_fulfillment_configs.cost_mode IS
|
||||
'fixed=成本按积分固定(cost_fen = credit_quantity);variable=成本不固定,客服确认履约时手填';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_fulfillment_configs_platform'
|
||||
) THEN
|
||||
ALTER TABLE manual_fulfillment_configs
|
||||
ADD CONSTRAINT chk_manual_fulfillment_configs_platform
|
||||
CHECK (platform_key IN ('nanhang', 'lewan', 'rock', 'fund_cash'));
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_fulfillment_configs_cost_mode'
|
||||
) THEN
|
||||
ALTER TABLE manual_fulfillment_configs
|
||||
ADD CONSTRAINT chk_manual_fulfillment_configs_cost_mode
|
||||
CHECK (cost_mode IN ('fixed', 'variable'));
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_fulfillment_configs_credit'
|
||||
) THEN
|
||||
ALTER TABLE manual_fulfillment_configs
|
||||
ADD CONSTRAINT chk_manual_fulfillment_configs_credit
|
||||
CHECK (credit_quantity >= 0);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. 三平台积分钱包:每个客服在每个积分平台一个账户
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS platform_credit_accounts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
platform_key TEXT NOT NULL,
|
||||
owner_admin_user_id BIGINT NOT NULL REFERENCES admin_users(id) ON DELETE RESTRICT,
|
||||
balance_quantity BIGINT NOT NULL DEFAULT 0,
|
||||
unit_name TEXT NOT NULL DEFAULT '积分',
|
||||
recharge_unit_price_fen BIGINT NOT NULL DEFAULT 1,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_platform_credit_accounts_owner_platform
|
||||
ON platform_credit_accounts (owner_admin_user_id, platform_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_platform_credit_accounts_platform_status
|
||||
ON platform_credit_accounts (platform_key, status);
|
||||
|
||||
COMMENT ON TABLE platform_credit_accounts IS
|
||||
'客服平台积分钱包:南航/乐玩/ROCK 三平台积分互不通用,每平台每客服一个账户';
|
||||
COMMENT ON COLUMN platform_credit_accounts.recharge_unit_price_fen IS
|
||||
'积分充值单价(分):1 积分 = ? 分。按平台不同,默认取档案 config_json.rechargeUnitPriceFen';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS platform_credit_entries (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
account_id BIGINT NOT NULL REFERENCES platform_credit_accounts(id) ON DELETE RESTRICT,
|
||||
entry_type TEXT NOT NULL,
|
||||
quantity_delta BIGINT NOT NULL,
|
||||
balance_after BIGINT NOT NULL,
|
||||
amount_fen BIGINT NOT NULL DEFAULT 0,
|
||||
platform_ref TEXT NOT NULL DEFAULT '',
|
||||
related_task_id BIGINT,
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_platform_credit_entries_account_time
|
||||
ON platform_credit_entries (account_id, created_at DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_platform_credit_entries_type_time
|
||||
ON platform_credit_entries (entry_type, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_platform_credit_entries_task
|
||||
ON platform_credit_entries (related_task_id)
|
||||
WHERE related_task_id IS NOT NULL;
|
||||
|
||||
COMMENT ON TABLE platform_credit_entries IS
|
||||
'平台积分流水:充值/消耗/调整/退回,只追加,不修改不删除';
|
||||
COMMENT ON COLUMN platform_credit_entries.amount_fen IS
|
||||
'对应人民币金额(分)= quantity_delta × recharge_unit_price_fen';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_platform_credit_accounts_platform'
|
||||
) THEN
|
||||
ALTER TABLE platform_credit_accounts
|
||||
ADD CONSTRAINT chk_platform_credit_accounts_platform
|
||||
CHECK (platform_key IN ('nanhang', 'lewan', 'rock'));
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_platform_credit_accounts_status'
|
||||
) THEN
|
||||
ALTER TABLE platform_credit_accounts
|
||||
ADD CONSTRAINT chk_platform_credit_accounts_status
|
||||
CHECK (status IN ('active', 'frozen', 'closed'));
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_platform_credit_accounts_balance'
|
||||
) THEN
|
||||
ALTER TABLE platform_credit_accounts
|
||||
ADD CONSTRAINT chk_platform_credit_accounts_balance
|
||||
CHECK (balance_quantity >= 0);
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_platform_credit_entries_type'
|
||||
) THEN
|
||||
ALTER TABLE platform_credit_entries
|
||||
ADD CONSTRAINT chk_platform_credit_entries_type
|
||||
CHECK (entry_type IN ('grant', 'consume', 'adjust', 'return'));
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_platform_credit_entries_delta'
|
||||
) THEN
|
||||
ALTER TABLE platform_credit_entries
|
||||
ADD CONSTRAINT chk_platform_credit_entries_delta
|
||||
CHECK (quantity_delta <> 0);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. 订单项挂配置:客服确认履约时直接读这里
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
ALTER TABLE order_items
|
||||
ADD COLUMN IF NOT EXISTS fulfillment_config_id BIGINT
|
||||
REFERENCES manual_fulfillment_configs(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_fulfillment_config
|
||||
ON order_items (fulfillment_config_id)
|
||||
WHERE fulfillment_config_id IS NOT NULL;
|
||||
|
||||
-- 内部 SKU 表达式索引:人工 SKU 扫描与「重新匹配」回填都靠它。
|
||||
-- 没有这个索引时两处都是全表扫(实测扫描 2.4s / 回填 5.4s),
|
||||
-- 建索引后回填降到 1.7s。
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_manual_internal_sku
|
||||
ON order_items (COALESCE(NULLIF(spec_json->>'skuNick', ''), NULLIF(spec_json->>'outSkuSn', '')))
|
||||
WHERE fulfillment_mode = 'manual';
|
||||
|
||||
COMMENT ON COLUMN order_items.fulfillment_config_id IS
|
||||
'人工履约配置(人工渠道+消耗积分+成本模式);由「重新匹配」回填';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. 四个客服钱包的履约档案
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 三个积分平台 + 备用金现金。executor_key 统一 manual_dispatch,
|
||||
-- 靠 config_json.platformKey 区分平台;**不复用** kuaishou_ct_assisted /
|
||||
-- kuaishou_feifei(那是已下线的历史自动发货通道)。
|
||||
|
||||
INSERT INTO fulfillment_profiles (
|
||||
profile_key, name, executor_key, requires_claim, auto_dispatch,
|
||||
inventory_strategy, config_json, created_at, updated_at
|
||||
)
|
||||
VALUES
|
||||
('manual_nanhang', '南航平台人工', 'manual_dispatch', FALSE, FALSE, 'external_platform',
|
||||
'{"platformKey":"nanhang","platformName":"南航平台","unitName":"积分","creditManaged":true,"cashAdapter":false,"rechargeUnitPriceFen":1}'::jsonb,
|
||||
NOW(), NOW()),
|
||||
('manual_lewan', '乐玩后台人工', 'manual_dispatch', FALSE, FALSE, 'external_platform',
|
||||
'{"platformKey":"lewan","platformName":"乐玩后台","unitName":"积分","creditManaged":true,"cashAdapter":false,"rechargeUnitPriceFen":1}'::jsonb,
|
||||
NOW(), NOW()),
|
||||
('manual_rock', 'ROCK 后台人工', 'manual_dispatch', FALSE, FALSE, 'external_platform',
|
||||
'{"platformKey":"rock","platformName":"ROCK 后台","unitName":"积分","creditManaged":true,"cashAdapter":false,"rechargeUnitPriceFen":100}'::jsonb,
|
||||
NOW(), NOW()),
|
||||
('manual_fund', '备用金现金人工', 'manual_dispatch', FALSE, FALSE, 'external_platform',
|
||||
'{"platformKey":"fund_cash","platformName":"备用金现金","unitName":"元","creditManaged":false,"cashAdapter":true,"rechargeUnitPriceFen":100}'::jsonb,
|
||||
NOW(), NOW())
|
||||
ON CONFLICT (profile_key) DO UPDATE
|
||||
SET name = EXCLUDED.name,
|
||||
config_json = EXCLUDED.config_json,
|
||||
updated_at = NOW();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 6. 积分充值价口径:**按平台不同**
|
||||
-- 南航 1 积分 = 1 分;乐玩 1 积分 = 1 分;ROCK 1 积分 = 1 元(100 分)。
|
||||
-- 真源是各档案的 config_json.rechargeUnitPriceFen,
|
||||
-- 成本折算与积分流水都从这里读,改价只改档案一处。
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
COMMENT ON TABLE platform_credit_accounts IS
|
||||
'客服平台积分钱包:南航/乐玩/ROCK 三平台积分互不通用,每平台每客服一个账户;换算率按平台配置(南航/乐玩 1 积分 = 1 分,ROCK 1 积分 = 1 元)';
|
||||
Reference in New Issue
Block a user