feat(consignment): 收敛寄卖迁移并调整发布页价格区布局

- 142–148 收敛为 142_consignment_platform,旧账本自动清理

- 价格三卡保持一行等宽,其余发布页布局回退

- 修复发布 tab 空白并统一价格文案为系统建议价
This commit is contained in:
2026-11-06 14:28:35 +08:00
parent 374e2a69df
commit 8b7bcca36a
17 changed files with 711 additions and 614 deletions
+21 -7
View File
@@ -25,24 +25,38 @@ const MIGRATIONS_TABLE = 'schema_migrations'
const SHORT_NUMERIC_PREFIX_MESSAGE = /^Can't determine timestamp for \d+$/
const MIGRATION_FILE_PATTERN = /^(\d{3})_.+\.sql$/i
/**
* 合并 main 后寄卖迁移编号顺延:
* 合并 main 后寄卖迁移编号顺延,以及 142–148 收敛为单一文件:
* 129-131 -> 135-137(main 占用 129-134)
* 135-137 -> 142-144(main 又占用 135-141)
* 142-148 -> 142_consignment_platform(2026-11-06 合并为单文件)
* 已跑过旧编号的环境要清掉旧记录,让新编号按正常顺序重跑(迁移本身幂等)。
*/
const RENAMED_MIGRATIONS: Array<{ legacyName: string; currentName: string }> = [
{ legacyName: '129_consignment_matching', currentName: '142_consignment_matching' },
{ legacyName: '135_consignment_matching', currentName: '142_consignment_matching' },
{ legacyName: '130_consignment_dynamic_specs', currentName: '143_consignment_dynamic_specs' },
{ legacyName: '136_consignment_dynamic_specs', currentName: '143_consignment_dynamic_specs' },
{ legacyName: '129_consignment_matching', currentName: '142_consignment_platform' },
{ legacyName: '135_consignment_matching', currentName: '142_consignment_platform' },
{ legacyName: '130_consignment_dynamic_specs', currentName: '142_consignment_platform' },
{ legacyName: '136_consignment_dynamic_specs', currentName: '142_consignment_platform' },
{
legacyName: '131_consignment_surnan_default_profile',
currentName: '144_consignment_surnan_default_profile',
currentName: '142_consignment_platform',
},
{
legacyName: '137_consignment_surnan_default_profile',
currentName: '144_consignment_surnan_default_profile',
currentName: '142_consignment_platform',
},
{ legacyName: '142_consignment_matching', currentName: '142_consignment_platform' },
{ legacyName: '143_consignment_dynamic_specs', currentName: '142_consignment_platform' },
{
legacyName: '144_consignment_surnan_default_profile',
currentName: '142_consignment_platform',
},
{
legacyName: '145_consignment_inventory_multi_binding',
currentName: '142_consignment_platform',
},
{ legacyName: '146_consignment_share_gift', currentName: '142_consignment_platform' },
{ legacyName: '147_consignment_match_and_rematch', currentName: '142_consignment_platform' },
{ legacyName: '148_consignment_remove_zone_help_text', currentName: '142_consignment_platform' },
]
export async function runDatabaseMigrations() {
@@ -1,167 +0,0 @@
-- 142_consignment_matching.sql
-- 寄卖配置化 + 库存数量/售价 + 内部匹配/自动匹配 + 拼单库存分配。
-- 全部语句幂等,可安全重跑;不改动历史迁移。
-- ===========================================================================
-- 1. 分类级寄卖配置(参考 order_service_profiles 的草稿/发布版本模式)
-- ===========================================================================
CREATE TABLE IF NOT EXISTS consignment_profiles (
id BIGSERIAL PRIMARY KEY,
category_id BIGINT NOT NULL REFERENCES work_categories(id) ON DELETE CASCADE,
name TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
tab_title TEXT NOT NULL DEFAULT '',
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
status TEXT NOT NULL DEFAULT 'draft',
current_version_id BIGINT,
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 consignment_profiles_status_check
CHECK (status IN ('draft', 'published', 'archived'))
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_consignment_profiles_category
ON consignment_profiles (category_id);
CREATE INDEX IF NOT EXISTS idx_consignment_profiles_status
ON consignment_profiles (status);
CREATE TABLE IF NOT EXISTS consignment_profile_versions (
id BIGSERIAL PRIMARY KEY,
profile_id BIGINT NOT NULL REFERENCES consignment_profiles(id) ON DELETE CASCADE,
version_no INTEGER NOT NULL,
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
published_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL,
published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_consignment_profile_versions_no UNIQUE (profile_id, version_no)
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_consignment_profiles_current_version'
) THEN
ALTER TABLE consignment_profiles
ADD CONSTRAINT fk_consignment_profiles_current_version
FOREIGN KEY (current_version_id)
REFERENCES consignment_profile_versions(id) ON DELETE SET NULL;
END IF;
END $$;
-- ===========================================================================
-- 2. 提报与库存:配置快照、数量、寄卖售价
-- ===========================================================================
ALTER TABLE consignment_submissions
ADD COLUMN IF NOT EXISTS profile_id BIGINT REFERENCES consignment_profiles(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS profile_version_id BIGINT
REFERENCES consignment_profile_versions(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS profile_snapshot_json JSONB,
ADD COLUMN IF NOT EXISTS quantity INTEGER NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS requested_sale_price_fen BIGINT NOT NULL DEFAULT 0;
ALTER TABLE consignment_inventories
ADD COLUMN IF NOT EXISTS profile_id BIGINT REFERENCES consignment_profiles(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS profile_version_id BIGINT
REFERENCES consignment_profile_versions(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS profile_snapshot_json JSONB,
ADD COLUMN IF NOT EXISTS quantity INTEGER NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS remaining_quantity INTEGER NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS sale_price_fen BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS listed_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS price_updated_at TIMESTAMPTZ;
-- 存量库存:一件一条,剩余数量 = 总数量,寄卖售价回填收购价(后续可单独改价)。
UPDATE consignment_inventories
SET remaining_quantity = GREATEST(0, COALESCE(quantity, 1)),
sale_price_fen = CASE WHEN COALESCE(sale_price_fen, 0) = 0 THEN COALESCE(cost_price_fen, 0) ELSE sale_price_fen END,
listed_at = COALESCE(listed_at, inbound_at)
WHERE remaining_quantity IS DISTINCT FROM GREATEST(0, COALESCE(quantity, 1))
OR listed_at IS NULL
OR (COALESCE(sale_price_fen, 0) = 0 AND COALESCE(cost_price_fen, 0) > 0);
CREATE INDEX IF NOT EXISTS idx_consignment_inventories_profile_matching
ON consignment_inventories (profile_id, status, allow_matching, remaining_quantity)
WHERE status = 'in_stock';
CREATE INDEX IF NOT EXISTS idx_consignment_inventories_worker_active
ON consignment_inventories (worker_id, status, remaining_quantity)
WHERE worker_id IS NOT NULL;
-- ===========================================================================
-- 3. 工单:寄卖配置、内部匹配截止、大厅可见时间
-- ===========================================================================
ALTER TABLE work_orders
ADD COLUMN IF NOT EXISTS consignment_profile_id BIGINT
REFERENCES consignment_profiles(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS internal_match_deadline_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS hall_visible_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS internal_match_attempts INTEGER NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_work_orders_internal_matching
ON work_orders (internal_match_deadline_at)
WHERE status = 'internal_matching';
CREATE INDEX IF NOT EXISTS idx_work_orders_hall_visible
ON work_orders (hall_visible_at)
WHERE hall_visible_at IS NOT NULL;
-- ===========================================================================
-- 4. 库存-工单分配(一个工单/份额可占用一条或多条库存)
-- ===========================================================================
CREATE TABLE IF NOT EXISTS consignment_inventory_allocations (
id BIGSERIAL PRIMARY KEY,
inventory_id BIGINT NOT NULL REFERENCES consignment_inventories(id) ON DELETE CASCADE,
work_order_id BIGINT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
work_order_share_id BIGINT REFERENCES work_order_shares(id) ON DELETE SET NULL,
quantity INTEGER NOT NULL DEFAULT 1,
unit_price_fen BIGINT NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'reserved',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT consignment_inventory_allocations_status_check
CHECK (status IN ('reserved', 'consumed', 'released')),
CONSTRAINT consignment_inventory_allocations_quantity_check
CHECK (quantity > 0)
);
CREATE INDEX IF NOT EXISTS idx_consignment_allocations_work_order
ON consignment_inventory_allocations (work_order_id, status);
CREATE INDEX IF NOT EXISTS idx_consignment_allocations_inventory
ON consignment_inventory_allocations (inventory_id, status);
CREATE INDEX IF NOT EXISTS idx_consignment_allocations_share
ON consignment_inventory_allocations (work_order_share_id)
WHERE work_order_share_id IS NOT NULL;
ALTER TABLE work_order_shares
ADD COLUMN IF NOT EXISTS consignment_inventory_id BIGINT
REFERENCES consignment_inventories(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_work_order_shares_consignment_inventory
ON work_order_shares (consignment_inventory_id)
WHERE consignment_inventory_id IS NOT NULL;
-- ===========================================================================
-- 5. 注释
-- ===========================================================================
COMMENT ON TABLE consignment_profiles IS
'分类级寄卖配置:打手端 tab、发布字段、价格、保证金、服务费、数量限制、匹配与超时策略';
COMMENT ON TABLE consignment_profile_versions IS
'寄卖配置发布版本快照;提报/库存/工单固化 profile_version_id,后续改配置不影响在途数据';
COMMENT ON COLUMN consignment_inventories.quantity IS
'该寄卖批次总数量;同批次共享卖家账号资料与截图';
COMMENT ON COLUMN consignment_inventories.remaining_quantity IS
'剩余可匹配数量;部分匹配后继续保留 in_stock,扣到 0 才 consumed';
COMMENT ON COLUMN consignment_inventories.sale_price_fen IS
'寄卖售价(分):匹配工单时的结算价,可由打手/后台修改';
COMMENT ON COLUMN consignment_inventories.listed_at IS
'开始计算未售出时长的时间点(审核入库/发布)';
COMMENT ON COLUMN work_orders.internal_match_deadline_at IS
'内部匹配截止时间;超过后调度任务把工单发回大厅';
COMMENT ON COLUMN work_orders.hall_visible_at IS
'大厅可见时间;NULL = 立即可见,未来时间 = 延迟进入大厅';
COMMENT ON TABLE consignment_inventory_allocations IS
'库存占用明细:整单履约一条,拼单时每个份额一条;释放时回补 remaining_quantity';
COMMENT ON COLUMN work_order_shares.consignment_inventory_id IS
'拼单份额对应的寄卖库存;非寄卖份额为 NULL';
@@ -0,0 +1,645 @@
-- 142_consignment_platform.sql
-- 寄卖平台完整迁移(由 142–148 收敛,2026-11-06)
-- 142 寄卖配置/库存/内部匹配
-- 143 动态规格
-- 144 苏尔南默认寄卖配置
-- 145 拼单库存多绑定
-- 146 份额级赠送
-- 147 重新匹配与匹配日志
-- 148 清理旧区服提示文案
-- 全部语句幂等;新库直接执行本文件即可。
-- ===========================================================================
-- 原 142_consignment_matching.sql
-- ===========================================================================
-- 142_consignment_matching.sql
-- 寄卖配置化 + 库存数量/售价 + 内部匹配/自动匹配 + 拼单库存分配。
-- 全部语句幂等,可安全重跑;不改动历史迁移。
-- ===========================================================================
-- 1. 分类级寄卖配置(参考 order_service_profiles 的草稿/发布版本模式)
-- ===========================================================================
CREATE TABLE IF NOT EXISTS consignment_profiles (
id BIGSERIAL PRIMARY KEY,
category_id BIGINT NOT NULL REFERENCES work_categories(id) ON DELETE CASCADE,
name TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
tab_title TEXT NOT NULL DEFAULT '',
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
status TEXT NOT NULL DEFAULT 'draft',
current_version_id BIGINT,
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 consignment_profiles_status_check
CHECK (status IN ('draft', 'published', 'archived'))
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_consignment_profiles_category
ON consignment_profiles (category_id);
CREATE INDEX IF NOT EXISTS idx_consignment_profiles_status
ON consignment_profiles (status);
CREATE TABLE IF NOT EXISTS consignment_profile_versions (
id BIGSERIAL PRIMARY KEY,
profile_id BIGINT NOT NULL REFERENCES consignment_profiles(id) ON DELETE CASCADE,
version_no INTEGER NOT NULL,
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
published_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL,
published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_consignment_profile_versions_no UNIQUE (profile_id, version_no)
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_consignment_profiles_current_version'
) THEN
ALTER TABLE consignment_profiles
ADD CONSTRAINT fk_consignment_profiles_current_version
FOREIGN KEY (current_version_id)
REFERENCES consignment_profile_versions(id) ON DELETE SET NULL;
END IF;
END $$;
-- ===========================================================================
-- 2. 提报与库存:配置快照、数量、寄卖售价
-- ===========================================================================
ALTER TABLE consignment_submissions
ADD COLUMN IF NOT EXISTS profile_id BIGINT REFERENCES consignment_profiles(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS profile_version_id BIGINT
REFERENCES consignment_profile_versions(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS profile_snapshot_json JSONB,
ADD COLUMN IF NOT EXISTS quantity INTEGER NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS requested_sale_price_fen BIGINT NOT NULL DEFAULT 0;
ALTER TABLE consignment_inventories
ADD COLUMN IF NOT EXISTS profile_id BIGINT REFERENCES consignment_profiles(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS profile_version_id BIGINT
REFERENCES consignment_profile_versions(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS profile_snapshot_json JSONB,
ADD COLUMN IF NOT EXISTS quantity INTEGER NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS remaining_quantity INTEGER NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS sale_price_fen BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS listed_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS price_updated_at TIMESTAMPTZ;
-- 存量库存:一件一条,剩余数量 = 总数量,寄卖售价回填收购价(后续可单独改价)。
UPDATE consignment_inventories
SET remaining_quantity = GREATEST(0, COALESCE(quantity, 1)),
sale_price_fen = CASE WHEN COALESCE(sale_price_fen, 0) = 0 THEN COALESCE(cost_price_fen, 0) ELSE sale_price_fen END,
listed_at = COALESCE(listed_at, inbound_at)
WHERE remaining_quantity IS DISTINCT FROM GREATEST(0, COALESCE(quantity, 1))
OR listed_at IS NULL
OR (COALESCE(sale_price_fen, 0) = 0 AND COALESCE(cost_price_fen, 0) > 0);
CREATE INDEX IF NOT EXISTS idx_consignment_inventories_profile_matching
ON consignment_inventories (profile_id, status, allow_matching, remaining_quantity)
WHERE status = 'in_stock';
CREATE INDEX IF NOT EXISTS idx_consignment_inventories_worker_active
ON consignment_inventories (worker_id, status, remaining_quantity)
WHERE worker_id IS NOT NULL;
-- ===========================================================================
-- 3. 工单:寄卖配置、内部匹配截止、大厅可见时间
-- ===========================================================================
ALTER TABLE work_orders
ADD COLUMN IF NOT EXISTS consignment_profile_id BIGINT
REFERENCES consignment_profiles(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS internal_match_deadline_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS hall_visible_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS internal_match_attempts INTEGER NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_work_orders_internal_matching
ON work_orders (internal_match_deadline_at)
WHERE status = 'internal_matching';
CREATE INDEX IF NOT EXISTS idx_work_orders_hall_visible
ON work_orders (hall_visible_at)
WHERE hall_visible_at IS NOT NULL;
-- ===========================================================================
-- 4. 库存-工单分配(一个工单/份额可占用一条或多条库存)
-- ===========================================================================
CREATE TABLE IF NOT EXISTS consignment_inventory_allocations (
id BIGSERIAL PRIMARY KEY,
inventory_id BIGINT NOT NULL REFERENCES consignment_inventories(id) ON DELETE CASCADE,
work_order_id BIGINT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
work_order_share_id BIGINT REFERENCES work_order_shares(id) ON DELETE SET NULL,
quantity INTEGER NOT NULL DEFAULT 1,
unit_price_fen BIGINT NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'reserved',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT consignment_inventory_allocations_status_check
CHECK (status IN ('reserved', 'consumed', 'released')),
CONSTRAINT consignment_inventory_allocations_quantity_check
CHECK (quantity > 0)
);
CREATE INDEX IF NOT EXISTS idx_consignment_allocations_work_order
ON consignment_inventory_allocations (work_order_id, status);
CREATE INDEX IF NOT EXISTS idx_consignment_allocations_inventory
ON consignment_inventory_allocations (inventory_id, status);
CREATE INDEX IF NOT EXISTS idx_consignment_allocations_share
ON consignment_inventory_allocations (work_order_share_id)
WHERE work_order_share_id IS NOT NULL;
ALTER TABLE work_order_shares
ADD COLUMN IF NOT EXISTS consignment_inventory_id BIGINT
REFERENCES consignment_inventories(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_work_order_shares_consignment_inventory
ON work_order_shares (consignment_inventory_id)
WHERE consignment_inventory_id IS NOT NULL;
-- ===========================================================================
-- 5. 注释
-- ===========================================================================
COMMENT ON TABLE consignment_profiles IS
'分类级寄卖配置:打手端 tab、发布字段、价格、保证金、服务费、数量限制、匹配与超时策略';
COMMENT ON TABLE consignment_profile_versions IS
'寄卖配置发布版本快照;提报/库存/工单固化 profile_version_id,后续改配置不影响在途数据';
COMMENT ON COLUMN consignment_inventories.quantity IS
'该寄卖批次总数量;同批次共享卖家账号资料与截图';
COMMENT ON COLUMN consignment_inventories.remaining_quantity IS
'剩余可匹配数量;部分匹配后继续保留 in_stock,扣到 0 才 consumed';
COMMENT ON COLUMN consignment_inventories.sale_price_fen IS
'寄卖售价(分):匹配工单时的结算价,可由打手/后台修改';
COMMENT ON COLUMN consignment_inventories.listed_at IS
'开始计算未售出时长的时间点(审核入库/发布)';
COMMENT ON COLUMN work_orders.internal_match_deadline_at IS
'内部匹配截止时间;超过后调度任务把工单发回大厅';
COMMENT ON COLUMN work_orders.hall_visible_at IS
'大厅可见时间;NULL = 立即可见,未来时间 = 延迟进入大厅';
COMMENT ON TABLE consignment_inventory_allocations IS
'库存占用明细:整单履约一条,拼单时每个份额一条;释放时回补 remaining_quantity';
COMMENT ON COLUMN work_order_shares.consignment_inventory_id IS
'拼单份额对应的寄卖库存;非寄卖份额为 NULL';
-- ===========================================================================
-- 原 143_consignment_dynamic_specs.sql
-- ===========================================================================
-- 143_consignment_dynamic_specs.sql
-- 寄卖动态规格:提报/库存增加 spec_values_json、spec_key、spec_text 与改价上限。
-- 全部语句幂等,可安全重跑;不改动历史迁移。
-- ===========================================================================
-- 1. 提报:动态规格值、匹配 key、展示文本、改价上限
-- ===========================================================================
ALTER TABLE consignment_submissions
ADD COLUMN IF NOT EXISTS spec_values_json JSONB NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS spec_key TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS spec_text TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS price_cap_fen BIGINT NOT NULL DEFAULT 0;
-- ===========================================================================
-- 2. 库存:同提报;入库后规格值继续随库存走
-- ===========================================================================
ALTER TABLE consignment_inventories
ADD COLUMN IF NOT EXISTS spec_values_json JSONB NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS spec_key TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS spec_text TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS price_cap_fen BIGINT NOT NULL DEFAULT 0;
-- ===========================================================================
-- 3. 存量回填:内置 5 规格 -> JSON / key / 文本;改价上限取当前售价
-- ===========================================================================
UPDATE consignment_submissions
SET
spec_values_json = jsonb_strip_nulls(
jsonb_build_object(
'system', NULLIF(BTRIM(system), ''),
'serverZone', NULLIF(BTRIM(server_zone), ''),
'series', NULLIF(BTRIM(series), ''),
'diamondLevel', diamond_level,
'brillianceCount', brilliance_count
)
),
spec_key = NULLIF(
concat_ws(
'|',
CASE WHEN COALESCE(NULLIF(BTRIM(system), ''), '') <> '' THEN 'system=' || BTRIM(system) END,
CASE WHEN COALESCE(NULLIF(BTRIM(server_zone), ''), '') <> '' THEN 'serverZone=' || BTRIM(server_zone) END,
CASE WHEN COALESCE(NULLIF(BTRIM(series), ''), '') <> '' THEN 'series=' || BTRIM(series) END,
CASE WHEN diamond_level IS NOT NULL THEN 'diamondLevel=' || diamond_level::text END,
CASE WHEN brilliance_count IS NOT NULL THEN 'brillianceCount=' || brilliance_count::text END
),
''
),
spec_text = COALESCE(NULLIF(BTRIM(item_name), ''), ''),
price_cap_fen = CASE
WHEN COALESCE(requested_sale_price_fen, 0) > 0 THEN requested_sale_price_fen
WHEN COALESCE(asking_price_fen, 0) > 0 THEN asking_price_fen
ELSE 0
END
WHERE spec_key = '';
UPDATE consignment_inventories
SET
spec_values_json = jsonb_strip_nulls(
jsonb_build_object(
'system', NULLIF(BTRIM(system), ''),
'serverZone', NULLIF(BTRIM(server_zone), ''),
'series', NULLIF(BTRIM(series), ''),
'diamondLevel', diamond_level,
'brillianceCount', brilliance_count
)
),
spec_key = NULLIF(
concat_ws(
'|',
CASE WHEN COALESCE(NULLIF(BTRIM(system), ''), '') <> '' THEN 'system=' || BTRIM(system) END,
CASE WHEN COALESCE(NULLIF(BTRIM(server_zone), ''), '') <> '' THEN 'serverZone=' || BTRIM(server_zone) END,
CASE WHEN COALESCE(NULLIF(BTRIM(series), ''), '') <> '' THEN 'series=' || BTRIM(series) END,
CASE WHEN diamond_level IS NOT NULL THEN 'diamondLevel=' || diamond_level::text END,
CASE WHEN brilliance_count IS NOT NULL THEN 'brillianceCount=' || brilliance_count::text END
),
''
),
spec_text = COALESCE(NULLIF(BTRIM(item_name), ''), ''),
price_cap_fen = COALESCE(sale_price_fen, 0)
WHERE spec_key = '';
-- ===========================================================================
-- 4. 索引:按配置 + 规格 key 快速筛选在库可匹配库存
-- ===========================================================================
CREATE INDEX IF NOT EXISTS idx_consignment_inventories_spec_key
ON consignment_inventories (profile_id, spec_key)
WHERE status = 'in_stock' AND allow_matching;
CREATE INDEX IF NOT EXISTS idx_consignment_submissions_spec_key
ON consignment_submissions (profile_id, spec_key)
WHERE status = 'pending_review';
-- ===========================================================================
-- 5. 注释
-- ===========================================================================
COMMENT ON COLUMN consignment_submissions.spec_values_json IS
'动态规格字段值(key -> 选项值/数字/文本),随配置版本快照固化';
COMMENT ON COLUMN consignment_submissions.spec_key IS
'参与匹配的规格字段稳定 key(按字段 key 排序的 key=value 串),用于候选筛选';
COMMENT ON COLUMN consignment_submissions.spec_text IS
'规格展示/搜索文本(字段名称 + 选项名称拼接)';
COMMENT ON COLUMN consignment_submissions.price_cap_fen IS
'打手改价上限(分):默认取接单模板价/系统建议价;0 = 不限制';
COMMENT ON COLUMN consignment_inventories.spec_values_json IS
'动态规格字段值(key -> 选项值/数字/文本),随配置版本快照固化';
COMMENT ON COLUMN consignment_inventories.spec_key IS
'参与匹配的规格字段稳定 key(按字段 key 排序的 key=value 串),用于候选筛选';
COMMENT ON COLUMN consignment_inventories.spec_text IS
'规格展示/搜索文本(字段名称 + 选项名称拼接)';
COMMENT ON COLUMN consignment_inventories.price_cap_fen IS
'打手改价上限(分):默认取接单模板价/系统建议价;0 = 不限制';
-- ===========================================================================
-- 原 144_consignment_surnan_default_profile.sql
-- ===========================================================================
-- 144_consignment_surnan_default_profile.sql
-- 苏尔南手枪默认寄卖配置:五个规格字段(与旧发布页一致),直接发布 v1。
-- 幂等:该分类已有寄卖配置时不做任何事,避免覆盖运营已保存/已发布的配置。
DO $migration$
DECLARE
v_category_id BIGINT;
v_profile_id BIGINT;
v_version_id BIGINT;
v_config JSONB;
BEGIN
SELECT id INTO v_category_id
FROM work_categories
WHERE category_key = 'shouqiang'
LIMIT 1;
IF v_category_id IS NULL THEN
RETURN;
END IF;
IF EXISTS (SELECT 1 FROM consignment_profiles WHERE category_id = v_category_id) THEN
RETURN;
END IF;
v_config := $config${
"version": 1,
"tabTitle": "苏尔南寄卖",
"tabDescription": "苏尔南手枪寄卖:选择规格后按接单模板价带出建议价",
"specs": [
{
"key": "system",
"label": "操作系统",
"type": "radio",
"required": true,
"options": [
{ "label": "苹果", "value": "苹果", "enabled": true },
{ "label": "安卓", "value": "安卓", "enabled": true }
],
"placeholder": "",
"helpText": "",
"matchMode": "exact",
"wildcardValues": [],
"showInItemName": true,
"sortOrder": 10
},
{
"key": "serverZone",
"label": "游戏区服",
"type": "radio",
"required": true,
"options": [
{ "label": "互通(QQ/微信)", "value": "互通", "enabled": true }
],
"placeholder": "",
"helpText": "苏尔南手枪当前统一为互通区服,旧库存的微信区/QQ区仍可参与匹配",
"matchMode": "wildcard",
"wildcardValues": ["互通", "通用", "全区", "不限"],
"showInItemName": false,
"sortOrder": 20
},
{
"key": "series",
"label": "系列 / 枪系",
"type": "radio",
"required": true,
"options": [
{ "label": "暗潮", "value": "暗潮", "enabled": true },
{ "label": "怒嚎", "value": "怒嚎", "enabled": true },
{ "label": "血宴", "value": "血宴", "enabled": true }
],
"placeholder": "",
"helpText": "",
"matchMode": "exact",
"wildcardValues": [],
"showInItemName": true,
"sortOrder": 30
},
{
"key": "diamondLevel",
"label": "钻石等级",
"type": "radio",
"required": true,
"options": [
{ "label": "1钻", "value": "1", "enabled": true },
{ "label": "2钻", "value": "2", "enabled": true },
{ "label": "3钻", "value": "3", "enabled": true },
{ "label": "4钻", "value": "4", "enabled": true },
{ "label": "5钻", "value": "5", "enabled": true },
{ "label": "6钻", "value": "6", "enabled": true },
{ "label": "7钻", "value": "7", "enabled": true }
],
"placeholder": "",
"helpText": "",
"matchMode": "minimum",
"wildcardValues": [],
"showInItemName": true,
"sortOrder": 40
},
{
"key": "brillianceCount",
"label": "璀璨数",
"type": "radio",
"required": true,
"options": [
{ "label": "无", "value": "0", "enabled": true },
{ "label": "1璀", "value": "1", "enabled": true },
{ "label": "2璀", "value": "2", "enabled": true },
{ "label": "3璀", "value": "3", "enabled": true },
{ "label": "4璀", "value": "4", "enabled": true },
{ "label": "5璀", "value": "5", "enabled": true },
{ "label": "6璀", "value": "6", "enabled": true },
{ "label": "7璀", "value": "7", "enabled": true }
],
"placeholder": "",
"helpText": "",
"matchMode": "minimum",
"wildcardValues": [],
"showInItemName": true,
"sortOrder": 50
}
],
"nameTemplate": "{system}{series}{diamondLevel}钻{brillianceCount}璀璨 苏尔南",
"pricing": {
"priceType": "template_discount",
"discount": 10,
"fixedPriceFen": 0,
"allowWorkerPrice": true,
"minPriceFen": 0,
"missingPricePolicy": "manual"
},
"fields": {
"system": true,
"serverZone": true,
"series": true,
"diamondLevel": true,
"brillianceCount": true,
"quantity": true
},
"deposit": {
"enabled": false,
"mode": "fixed",
"fixedAmountFen": 0,
"unitAmountFen": 0
},
"serviceFee": {
"enabled": false,
"mode": "percent",
"percent": 0,
"amountFen": 0
},
"inventoryLimit": {
"enabled": false,
"maxActiveQuantity": 0
},
"matching": {
"mode": "manual",
"autoMatchEnabled": false,
"exactOnly": true,
"preferLowReward": true
},
"internalMatch": {
"delayMinutes": 30,
"fallbackToHall": true
},
"timeout": {
"minutes": 30,
"policy": "reopen"
},
"priceDrop": {
"enabled": false,
"hours": 72,
"mode": "percent",
"value": 10
}
}$config$::jsonb;
INSERT INTO consignment_profiles (
category_id, name, title, description, tab_title, config_json, status, created_at, updated_at
) VALUES (
v_category_id,
'苏尔南手枪寄卖',
'苏尔南手枪寄卖',
'苏尔南手枪默认配置:选择规格后按接单模板价带出建议价',
'苏尔南寄卖',
v_config,
'published',
now(),
now()
)
RETURNING id INTO v_profile_id;
INSERT INTO consignment_profile_versions (
profile_id, version_no, config_json, published_at, created_at
) VALUES (
v_profile_id, 1, v_config, now(), now()
)
RETURNING id INTO v_version_id;
UPDATE consignment_profiles
SET current_version_id = v_version_id, updated_at = now()
WHERE id = v_profile_id;
END
$migration$;
-- ===========================================================================
-- 原 145_consignment_inventory_multi_binding.sql
-- ===========================================================================
-- 145_consignment_inventory_multi_binding.sql
-- 拼单匹配允许同一张工单绑定多条寄卖库存;旧的“一单一件库存”唯一索引会挡住第二个份额。
-- 保留普通索引用于按工单反查库存;数据完整性由分配表和拼单份额表保证。
DROP INDEX IF EXISTS uniq_consignment_inventories_bound_work_order;
CREATE INDEX IF NOT EXISTS idx_consignment_inventories_bound_work_order
ON consignment_inventories (bound_work_order_id)
WHERE bound_work_order_id IS NOT NULL;
-- ===========================================================================
-- 原 146_consignment_share_gift.sql
-- ===========================================================================
-- 146_consignment_share_gift.sql
--
-- 拼单寄卖:每个卖家的库存对应一条 work_order_share,好友赠送的“加好友时间/冷却截止”
-- 必须落在份额上,不能再用 work_orders.friend_added_at 这种整单字段。
-- 这样 A 确认加好友只影响 A 的份额,B 仍可独立确认、独立提交赠送凭证。
ALTER TABLE work_order_shares
ADD COLUMN IF NOT EXISTS friend_added_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS gift_available_at TIMESTAMPTZ;
COMMENT ON COLUMN work_order_shares.friend_added_at IS
'寄卖拼单份额:卖家确认加好友时间(冷却起点);非寄卖份额为 NULL';
COMMENT ON COLUMN work_order_shares.gift_available_at IS
'寄卖拼单份额:赠送冷却结束时间;到点后按时间实时推导为可赠送';
-- ===========================================================================
-- 原 147_consignment_match_and_rematch.sql
-- ===========================================================================
-- 147_consignment_match_and_rematch.sql
-- 1) 超时「重新匹配」排除原卖家/原库存;
-- 2) 手动/自动匹配统一留痕,供后台排查问题。
-- 全部语句幂等,可安全重跑;不改动历史迁移。
ALTER TABLE work_orders
ADD COLUMN IF NOT EXISTS rematch_excluded_worker_id BIGINT,
ADD COLUMN IF NOT EXISTS rematch_excluded_inventory_id BIGINT;
COMMENT ON COLUMN work_orders.rematch_excluded_worker_id IS
'超时重新匹配时排除的原接单打手;匹配成功后清空';
COMMENT ON COLUMN work_orders.rematch_excluded_inventory_id IS
'超时重新匹配时排除的原寄卖库存;匹配成功后清空';
CREATE TABLE IF NOT EXISTS consignment_match_logs (
id BIGSERIAL PRIMARY KEY,
work_order_id BIGINT,
work_order_no TEXT NOT NULL DEFAULT '',
profile_id BIGINT,
category_id BIGINT,
trigger_type TEXT NOT NULL,
result TEXT NOT NULL,
reason_code TEXT NOT NULL DEFAULT '',
reason_text TEXT NOT NULL DEFAULT '',
candidate_count INTEGER NOT NULL DEFAULT 0,
matched_count INTEGER NOT NULL DEFAULT 0,
actor_type TEXT NOT NULL DEFAULT 'system',
actor_id TEXT NOT NULL DEFAULT '',
duration_ms INTEGER NOT NULL DEFAULT 0,
payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT consignment_match_logs_trigger_check
CHECK (trigger_type IN ('auto', 'manual', 'rematch')),
CONSTRAINT consignment_match_logs_result_check
CHECK (result IN ('matched', 'failed', 'skipped'))
);
CREATE INDEX IF NOT EXISTS idx_consignment_match_logs_created
ON consignment_match_logs (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_consignment_match_logs_work_order
ON consignment_match_logs (work_order_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_consignment_match_logs_profile
ON consignment_match_logs (profile_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_consignment_match_logs_type_result
ON consignment_match_logs (trigger_type, result, created_at DESC);
COMMENT ON TABLE consignment_match_logs IS
'寄卖匹配日志:手动/自动/重新匹配的每次尝试(成功、失败、跳过)与原因,用于留痕和问题分析';
COMMENT ON COLUMN consignment_match_logs.trigger_type IS
'触发方式:auto 自动调度 / manual 后台手动 / rematch 超时重新匹配';
COMMENT ON COLUMN consignment_match_logs.reason_code IS
'机器可读原因码,例如 no_candidate / matching_disabled / spec_mismatch / exception';
-- ===========================================================================
-- 原 148_consignment_remove_zone_help_text.sql
-- ===========================================================================
-- 148_consignment_remove_zone_help_text.sql
-- 删除「游戏区服」字段上的旧提示文案:
-- 「苏尔南手枪当前统一为互通区服,旧库存的微信区/QQ区仍可参与匹配」。
-- 只清理已发布配置的当前快照,不改动历史版本与历史提报/库存数据;幂等可重跑。
UPDATE consignment_profiles AS p
SET config_json = jsonb_set(
p.config_json,
'{specs}',
COALESCE(
(
SELECT jsonb_agg(
CASE
WHEN elem->>'key' = 'serverZone'
AND BTRIM(COALESCE(elem->>'helpText', '')) =
'苏尔南手枪当前统一为互通区服,旧库存的微信区/QQ区仍可参与匹配'
THEN jsonb_set(elem, '{helpText}', '""'::jsonb)
ELSE elem
END
ORDER BY ord
)
FROM jsonb_array_elements(p.config_json->'specs') WITH ORDINALITY AS t(elem, ord)
),
'[]'::jsonb
)
)
WHERE jsonb_typeof(p.config_json->'specs') = 'array';
UPDATE consignment_profile_versions AS v
SET config_json = jsonb_set(
v.config_json,
'{specs}',
COALESCE(
(
SELECT jsonb_agg(
CASE
WHEN elem->>'key' = 'serverZone'
AND BTRIM(COALESCE(elem->>'helpText', '')) =
'苏尔南手枪当前统一为互通区服,旧库存的微信区/QQ区仍可参与匹配'
THEN jsonb_set(elem, '{helpText}', '""'::jsonb)
ELSE elem
END
ORDER BY ord
)
FROM jsonb_array_elements(v.config_json->'specs') WITH ORDINALITY AS t(elem, ord)
),
'[]'::jsonb
)
)
FROM consignment_profiles AS p
WHERE v.id = p.current_version_id
AND jsonb_typeof(v.config_json->'specs') = 'array';
@@ -1,111 +0,0 @@
-- 143_consignment_dynamic_specs.sql
-- 寄卖动态规格:提报/库存增加 spec_values_json、spec_key、spec_text 与改价上限。
-- 全部语句幂等,可安全重跑;不改动历史迁移。
-- ===========================================================================
-- 1. 提报:动态规格值、匹配 key、展示文本、改价上限
-- ===========================================================================
ALTER TABLE consignment_submissions
ADD COLUMN IF NOT EXISTS spec_values_json JSONB NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS spec_key TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS spec_text TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS price_cap_fen BIGINT NOT NULL DEFAULT 0;
-- ===========================================================================
-- 2. 库存:同提报;入库后规格值继续随库存走
-- ===========================================================================
ALTER TABLE consignment_inventories
ADD COLUMN IF NOT EXISTS spec_values_json JSONB NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS spec_key TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS spec_text TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS price_cap_fen BIGINT NOT NULL DEFAULT 0;
-- ===========================================================================
-- 3. 存量回填:内置 5 规格 -> JSON / key / 文本;改价上限取当前售价
-- ===========================================================================
UPDATE consignment_submissions
SET
spec_values_json = jsonb_strip_nulls(
jsonb_build_object(
'system', NULLIF(BTRIM(system), ''),
'serverZone', NULLIF(BTRIM(server_zone), ''),
'series', NULLIF(BTRIM(series), ''),
'diamondLevel', diamond_level,
'brillianceCount', brilliance_count
)
),
spec_key = NULLIF(
concat_ws(
'|',
CASE WHEN COALESCE(NULLIF(BTRIM(system), ''), '') <> '' THEN 'system=' || BTRIM(system) END,
CASE WHEN COALESCE(NULLIF(BTRIM(server_zone), ''), '') <> '' THEN 'serverZone=' || BTRIM(server_zone) END,
CASE WHEN COALESCE(NULLIF(BTRIM(series), ''), '') <> '' THEN 'series=' || BTRIM(series) END,
CASE WHEN diamond_level IS NOT NULL THEN 'diamondLevel=' || diamond_level::text END,
CASE WHEN brilliance_count IS NOT NULL THEN 'brillianceCount=' || brilliance_count::text END
),
''
),
spec_text = COALESCE(NULLIF(BTRIM(item_name), ''), ''),
price_cap_fen = CASE
WHEN COALESCE(requested_sale_price_fen, 0) > 0 THEN requested_sale_price_fen
WHEN COALESCE(asking_price_fen, 0) > 0 THEN asking_price_fen
ELSE 0
END
WHERE spec_key = '';
UPDATE consignment_inventories
SET
spec_values_json = jsonb_strip_nulls(
jsonb_build_object(
'system', NULLIF(BTRIM(system), ''),
'serverZone', NULLIF(BTRIM(server_zone), ''),
'series', NULLIF(BTRIM(series), ''),
'diamondLevel', diamond_level,
'brillianceCount', brilliance_count
)
),
spec_key = NULLIF(
concat_ws(
'|',
CASE WHEN COALESCE(NULLIF(BTRIM(system), ''), '') <> '' THEN 'system=' || BTRIM(system) END,
CASE WHEN COALESCE(NULLIF(BTRIM(server_zone), ''), '') <> '' THEN 'serverZone=' || BTRIM(server_zone) END,
CASE WHEN COALESCE(NULLIF(BTRIM(series), ''), '') <> '' THEN 'series=' || BTRIM(series) END,
CASE WHEN diamond_level IS NOT NULL THEN 'diamondLevel=' || diamond_level::text END,
CASE WHEN brilliance_count IS NOT NULL THEN 'brillianceCount=' || brilliance_count::text END
),
''
),
spec_text = COALESCE(NULLIF(BTRIM(item_name), ''), ''),
price_cap_fen = COALESCE(sale_price_fen, 0)
WHERE spec_key = '';
-- ===========================================================================
-- 4. 索引:按配置 + 规格 key 快速筛选在库可匹配库存
-- ===========================================================================
CREATE INDEX IF NOT EXISTS idx_consignment_inventories_spec_key
ON consignment_inventories (profile_id, spec_key)
WHERE status = 'in_stock' AND allow_matching;
CREATE INDEX IF NOT EXISTS idx_consignment_submissions_spec_key
ON consignment_submissions (profile_id, spec_key)
WHERE status = 'pending_review';
-- ===========================================================================
-- 5. 注释
-- ===========================================================================
COMMENT ON COLUMN consignment_submissions.spec_values_json IS
'动态规格字段值(key -> 选项值/数字/文本),随配置版本快照固化';
COMMENT ON COLUMN consignment_submissions.spec_key IS
'参与匹配的规格字段稳定 key(按字段 key 排序的 key=value 串),用于候选筛选';
COMMENT ON COLUMN consignment_submissions.spec_text IS
'规格展示/搜索文本(字段名称 + 选项名称拼接)';
COMMENT ON COLUMN consignment_submissions.price_cap_fen IS
'打手改价上限(分):默认取接单模板价/系统建议价;0 = 不限制';
COMMENT ON COLUMN consignment_inventories.spec_values_json IS
'动态规格字段值(key -> 选项值/数字/文本),随配置版本快照固化';
COMMENT ON COLUMN consignment_inventories.spec_key IS
'参与匹配的规格字段稳定 key(按字段 key 排序的 key=value 串),用于候选筛选';
COMMENT ON COLUMN consignment_inventories.spec_text IS
'规格展示/搜索文本(字段名称 + 选项名称拼接)';
COMMENT ON COLUMN consignment_inventories.price_cap_fen IS
'打手改价上限(分):默认取接单模板价/系统建议价;0 = 不限制';
@@ -1,203 +0,0 @@
-- 144_consignment_surnan_default_profile.sql
-- 苏尔南手枪默认寄卖配置:五个规格字段(与旧发布页一致),直接发布 v1。
-- 幂等:该分类已有寄卖配置时不做任何事,避免覆盖运营已保存/已发布的配置。
DO $migration$
DECLARE
v_category_id BIGINT;
v_profile_id BIGINT;
v_version_id BIGINT;
v_config JSONB;
BEGIN
SELECT id INTO v_category_id
FROM work_categories
WHERE category_key = 'shouqiang'
LIMIT 1;
IF v_category_id IS NULL THEN
RETURN;
END IF;
IF EXISTS (SELECT 1 FROM consignment_profiles WHERE category_id = v_category_id) THEN
RETURN;
END IF;
v_config := $config${
"version": 1,
"tabTitle": "苏尔南寄卖",
"tabDescription": "苏尔南手枪寄卖:选择规格后按接单模板价带出建议价",
"specs": [
{
"key": "system",
"label": "操作系统",
"type": "radio",
"required": true,
"options": [
{ "label": "苹果", "value": "苹果", "enabled": true },
{ "label": "安卓", "value": "安卓", "enabled": true }
],
"placeholder": "",
"helpText": "",
"matchMode": "exact",
"wildcardValues": [],
"showInItemName": true,
"sortOrder": 10
},
{
"key": "serverZone",
"label": "游戏区服",
"type": "radio",
"required": true,
"options": [
{ "label": "互通(QQ/微信)", "value": "互通", "enabled": true }
],
"placeholder": "",
"helpText": "苏尔南手枪当前统一为互通区服,旧库存的微信区/QQ区仍可参与匹配",
"matchMode": "wildcard",
"wildcardValues": ["互通", "通用", "全区", "不限"],
"showInItemName": false,
"sortOrder": 20
},
{
"key": "series",
"label": "系列 / 枪系",
"type": "radio",
"required": true,
"options": [
{ "label": "暗潮", "value": "暗潮", "enabled": true },
{ "label": "怒嚎", "value": "怒嚎", "enabled": true },
{ "label": "血宴", "value": "血宴", "enabled": true }
],
"placeholder": "",
"helpText": "",
"matchMode": "exact",
"wildcardValues": [],
"showInItemName": true,
"sortOrder": 30
},
{
"key": "diamondLevel",
"label": "钻石等级",
"type": "radio",
"required": true,
"options": [
{ "label": "1钻", "value": "1", "enabled": true },
{ "label": "2钻", "value": "2", "enabled": true },
{ "label": "3钻", "value": "3", "enabled": true },
{ "label": "4钻", "value": "4", "enabled": true },
{ "label": "5钻", "value": "5", "enabled": true },
{ "label": "6钻", "value": "6", "enabled": true },
{ "label": "7钻", "value": "7", "enabled": true }
],
"placeholder": "",
"helpText": "",
"matchMode": "minimum",
"wildcardValues": [],
"showInItemName": true,
"sortOrder": 40
},
{
"key": "brillianceCount",
"label": "璀璨数",
"type": "radio",
"required": true,
"options": [
{ "label": "无", "value": "0", "enabled": true },
{ "label": "1璀", "value": "1", "enabled": true },
{ "label": "2璀", "value": "2", "enabled": true },
{ "label": "3璀", "value": "3", "enabled": true },
{ "label": "4璀", "value": "4", "enabled": true },
{ "label": "5璀", "value": "5", "enabled": true },
{ "label": "6璀", "value": "6", "enabled": true },
{ "label": "7璀", "value": "7", "enabled": true }
],
"placeholder": "",
"helpText": "",
"matchMode": "minimum",
"wildcardValues": [],
"showInItemName": true,
"sortOrder": 50
}
],
"nameTemplate": "{system}{series}{diamondLevel}钻{brillianceCount}璀璨 苏尔南",
"pricing": {
"priceType": "template_discount",
"discount": 10,
"fixedPriceFen": 0,
"allowWorkerPrice": true,
"minPriceFen": 0,
"missingPricePolicy": "manual"
},
"fields": {
"system": true,
"serverZone": true,
"series": true,
"diamondLevel": true,
"brillianceCount": true,
"quantity": true
},
"deposit": {
"enabled": false,
"mode": "fixed",
"fixedAmountFen": 0,
"unitAmountFen": 0
},
"serviceFee": {
"enabled": false,
"mode": "percent",
"percent": 0,
"amountFen": 0
},
"inventoryLimit": {
"enabled": false,
"maxActiveQuantity": 0
},
"matching": {
"mode": "manual",
"autoMatchEnabled": false,
"exactOnly": true,
"preferLowReward": true
},
"internalMatch": {
"delayMinutes": 30,
"fallbackToHall": true
},
"timeout": {
"minutes": 30,
"policy": "reopen"
},
"priceDrop": {
"enabled": false,
"hours": 72,
"mode": "percent",
"value": 10
}
}$config$::jsonb;
INSERT INTO consignment_profiles (
category_id, name, title, description, tab_title, config_json, status, created_at, updated_at
) VALUES (
v_category_id,
'苏尔南手枪寄卖',
'苏尔南手枪寄卖',
'苏尔南手枪默认配置:选择规格后按接单模板价带出建议价',
'苏尔南寄卖',
v_config,
'published',
now(),
now()
)
RETURNING id INTO v_profile_id;
INSERT INTO consignment_profile_versions (
profile_id, version_no, config_json, published_at, created_at
) VALUES (
v_profile_id, 1, v_config, now(), now()
)
RETURNING id INTO v_version_id;
UPDATE consignment_profiles
SET current_version_id = v_version_id, updated_at = now()
WHERE id = v_profile_id;
END
$migration$;
@@ -1,8 +0,0 @@
-- 145_consignment_inventory_multi_binding.sql
-- 拼单匹配允许同一张工单绑定多条寄卖库存;旧的“一单一件库存”唯一索引会挡住第二个份额。
-- 保留普通索引用于按工单反查库存;数据完整性由分配表和拼单份额表保证。
DROP INDEX IF EXISTS uniq_consignment_inventories_bound_work_order;
CREATE INDEX IF NOT EXISTS idx_consignment_inventories_bound_work_order
ON consignment_inventories (bound_work_order_id)
WHERE bound_work_order_id IS NOT NULL;
@@ -1,13 +0,0 @@
-- 146_consignment_share_gift.sql
--
-- 拼单寄卖:每个卖家的库存对应一条 work_order_share,好友赠送的“加好友时间/冷却截止”
-- 必须落在份额上,不能再用 work_orders.friend_added_at 这种整单字段。
-- 这样 A 确认加好友只影响 A 的份额,B 仍可独立确认、独立提交赠送凭证。
ALTER TABLE work_order_shares
ADD COLUMN IF NOT EXISTS friend_added_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS gift_available_at TIMESTAMPTZ;
COMMENT ON COLUMN work_order_shares.friend_added_at IS
'寄卖拼单份额:卖家确认加好友时间(冷却起点);非寄卖份额为 NULL';
COMMENT ON COLUMN work_order_shares.gift_available_at IS
'寄卖拼单份额:赠送冷却结束时间;到点后按时间实时推导为可赠送';
@@ -1,52 +0,0 @@
-- 147_consignment_match_and_rematch.sql
-- 1) 超时「重新匹配」排除原卖家/原库存;
-- 2) 手动/自动匹配统一留痕,供后台排查问题。
-- 全部语句幂等,可安全重跑;不改动历史迁移。
ALTER TABLE work_orders
ADD COLUMN IF NOT EXISTS rematch_excluded_worker_id BIGINT,
ADD COLUMN IF NOT EXISTS rematch_excluded_inventory_id BIGINT;
COMMENT ON COLUMN work_orders.rematch_excluded_worker_id IS
'超时重新匹配时排除的原接单打手;匹配成功后清空';
COMMENT ON COLUMN work_orders.rematch_excluded_inventory_id IS
'超时重新匹配时排除的原寄卖库存;匹配成功后清空';
CREATE TABLE IF NOT EXISTS consignment_match_logs (
id BIGSERIAL PRIMARY KEY,
work_order_id BIGINT,
work_order_no TEXT NOT NULL DEFAULT '',
profile_id BIGINT,
category_id BIGINT,
trigger_type TEXT NOT NULL,
result TEXT NOT NULL,
reason_code TEXT NOT NULL DEFAULT '',
reason_text TEXT NOT NULL DEFAULT '',
candidate_count INTEGER NOT NULL DEFAULT 0,
matched_count INTEGER NOT NULL DEFAULT 0,
actor_type TEXT NOT NULL DEFAULT 'system',
actor_id TEXT NOT NULL DEFAULT '',
duration_ms INTEGER NOT NULL DEFAULT 0,
payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT consignment_match_logs_trigger_check
CHECK (trigger_type IN ('auto', 'manual', 'rematch')),
CONSTRAINT consignment_match_logs_result_check
CHECK (result IN ('matched', 'failed', 'skipped'))
);
CREATE INDEX IF NOT EXISTS idx_consignment_match_logs_created
ON consignment_match_logs (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_consignment_match_logs_work_order
ON consignment_match_logs (work_order_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_consignment_match_logs_profile
ON consignment_match_logs (profile_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_consignment_match_logs_type_result
ON consignment_match_logs (trigger_type, result, created_at DESC);
COMMENT ON TABLE consignment_match_logs IS
'寄卖匹配日志:手动/自动/重新匹配的每次尝试(成功、失败、跳过)与原因,用于留痕和问题分析';
COMMENT ON COLUMN consignment_match_logs.trigger_type IS
'触发方式:auto 自动调度 / manual 后台手动 / rematch 超时重新匹配';
COMMENT ON COLUMN consignment_match_logs.reason_code IS
'机器可读原因码,例如 no_candidate / matching_disabled / spec_mismatch / exception';
@@ -89,9 +89,9 @@ test('降价提示:未到时长不提示,到点后按比例给出建议售
assert.equal(suggestion?.dropHours, 24)
})
test('迁移 142 必须包含寄卖配置、库存数量/售价与分配表', async () => {
test('合并迁移 142 必须包含寄卖配置、库存数量/售价与分配表', async () => {
const source = await readFile(
new URL('../../db/migrations/142_consignment_matching.sql', import.meta.url),
new URL('../../db/migrations/142_consignment_platform.sql', import.meta.url),
'utf8',
)
for (const fragment of [
@@ -103,6 +103,6 @@ test('迁移 142 必须包含寄卖配置、库存数量/售价与分配表', as
'internal_match_deadline_at',
'consignment_inventory_id',
]) {
assert.ok(source.includes(fragment), `迁移 142 缺少 ${fragment}`)
assert.ok(source.includes(fragment), `合并迁移 142 缺少 ${fragment}`)
}
})
@@ -35,7 +35,7 @@ function option(label: string, value: string) {
return { label, value, enabled: true }
}
/** 苏尔南手枪种子配置的五个字段(与迁移 144 一致)。 */
/** 苏尔南手枪种子配置的五个字段(与合并迁移 142 一致)。 */
function surnanSpecs(): ConsignmentSpecField[] {
return [
{
@@ -457,9 +457,9 @@ test('动态规格:旧列仍由兼容 key 同步', () => {
})
})
test('迁移 143 必须包含 JSON、匹配 key、文本与改价上限', async () => {
test('合并迁移 142 必须包含动态规格的 JSON、匹配 key、文本与改价上限', async () => {
const sql = await readFile(
new URL('../../db/migrations/143_consignment_dynamic_specs.sql', import.meta.url),
new URL('../../db/migrations/142_consignment_platform.sql', import.meta.url),
'utf8',
)
for (const fragment of [
@@ -469,13 +469,13 @@ test('迁移 143 必须包含 JSON、匹配 key、文本与改价上限', async
'price_cap_fen',
'CREATE INDEX IF NOT EXISTS idx_consignment_inventories_spec_key',
]) {
assert.ok(sql.includes(fragment), `迁移 143 缺少 ${fragment}`)
assert.ok(sql.includes(fragment), `合并迁移 142 缺少 ${fragment}`)
}
})
test('迁移 144 必须为苏尔南手枪写入已发布的五个单选字段配置', async () => {
test('合并迁移 142 必须为苏尔南手枪写入已发布的五个单选字段配置', async () => {
const sql = await readFile(
new URL('../../db/migrations/144_consignment_surnan_default_profile.sql', import.meta.url),
new URL('../../db/migrations/142_consignment_platform.sql', import.meta.url),
'utf8',
)
for (const fragment of [
@@ -485,6 +485,6 @@ test('迁移 144 必须为苏尔南手枪写入已发布的五个单选字段配
"'published'",
'"key": "brillianceCount"',
]) {
assert.ok(sql.includes(fragment), `迁移 144 缺少 ${fragment}`)
assert.ok(sql.includes(fragment), `合并迁移 142 缺少 ${fragment}`)
}
})
@@ -3,9 +3,9 @@
*
* 设计约定:
* - 没有任何「内置字段」:配置初始 specs 为空,字段全部由运营在寄卖配置里维护;
* 苏尔南手枪的五个字段由迁移 131 作为种子配置写入;
* 苏尔南手枪的五个字段由迁移 142 作为种子配置写入;
* - 字段类型支持:文本 / 长文本 / 下拉 / 单选 / 数字 / 日期;
* - 价格只有两种:接单模板价打折、模板价固定下调;打手只能按系统建议价往下调;
* - 价格只有两种:接单模板价打折、模板价固定下调;打手只能按系统建议价格往下调;
* - 旧列兼容:字段 key 仍为 system/serverZone/series/diamondLevel/brillianceCount 时,
* 继续同步旧列并沿用旧匹配器,保证存量库存与工单不回归。
*/
@@ -1045,7 +1045,7 @@ export async function submitWorkerConsignmentGiftEvidence(
/**
* 发布寄卖的建议价:
* - 模板折扣:接单模板价 × 折扣;
* - 固定价格:直接使用固定价,有模板价时校验不得高于模板价;
* - 模板价固定下调:接单模板价 − 固定下调金额,有模板价才允许发布;
* - 同时返回改价上限(等于建议价,打手只能往下调)。
*/
export async function getWorkerConsignmentPriceSuggestion(payload: {
@@ -1312,7 +1312,7 @@ export default function ConsignmentConfigPanel() {
label="允许打手改价"
name={['pricing', 'allowWorkerPrice']}
valuePropName="checked"
extra="打手只能按系统建议价往下调"
extra="打手只能按系统建议价格往下调"
style={{ marginBottom: 0 }}
>
<Switch checkedChildren="允许" unCheckedChildren="不允许" />
@@ -162,7 +162,7 @@ export default function WorkerConsignmentPage() {
const [cooldownNow, setCooldownNow] = useState(() => Date.now())
const [form] = Form.useForm()
const currentDiamondLevel = Form.useWatch('diamondLevel', form)
// 寄售价:按后台配置(模板折扣价 / 固定价)自动带出,打手只能往下调。
// 寄售价:按后台配置(模板折扣价 / 模板价固定下调)自动带出,打手只能往下调。
const [priceState, setPriceState] = useState<
| { status: 'idle' }
| { status: 'loading' }
@@ -740,14 +740,6 @@ export default function WorkerConsignmentPage() {
}
/>
{!consignmentContactsQuery.isLoading && contactSupporters.length === 0 ? (
<Alert
type="warning"
showIcon
message="暂无可选的对接客服,请联系管理员在「配置 - 对接客服」中添加后再发布"
/>
) : null}
<Divider plain className="worker-consignment-divider">
规格与价格
</Divider>
@@ -846,12 +838,16 @@ export default function WorkerConsignmentPage() {
})}
</div>
<div className="worker-consignment-price-grid">
<div
className={`worker-consignment-price-grid${
selectedProfile?.config.fields.quantity === false ? ' is-two' : ''
}`}
>
<Form.Item
label="寄售价格"
tooltip={
manualPriceAllowed
? `默认按后台配置带出(模板折扣价 / 固定价),可下调,不能高于系统建议价${selectedProfile?.config.pricing.minPriceFen ? `;最低 ¥${(selectedProfile.config.pricing.minPriceFen / 100).toFixed(2)}` : ''}。`
? `默认按后台配置带出(模板折扣价 / 模板价固定下调),可下调,不能高于系统建议价${selectedProfile?.config.pricing.minPriceFen ? `;最低 ¥${(selectedProfile.config.pricing.minPriceFen / 100).toFixed(2)}` : ''}。`
: '由后台寄卖配置自动带出,卖家不可修改;需要调整请联系客服。'
}
className="worker-consignment-spec-card is-price"
@@ -1126,33 +1122,12 @@ export default function WorkerConsignmentPage() {
<Card bordered={false} className="worker-orders-card">
<Tabs
className="worker-order-tabs"
activeKey={
activeTab === 'records'
? 'records'
: selectedProfile
? `publish-${selectedProfile.profileId}`
: 'publish'
}
activeKey={activeTab === 'records' ? 'records' : 'publish'}
onChange={(key) => {
if (key === 'records') {
setActiveTab('records')
return
}
const profileId = Number(String(key).replace('publish-', ''))
if (Number.isInteger(profileId) && profileId > 0) {
if (profileId !== selectedProfileId) {
const nextProfile = consignmentProfiles.find((item) => item.profileId === profileId)
const nextSpecs = nextProfile?.config.specs ?? []
const cleared: Record<string, undefined> = {}
for (const field of nextSpecs) cleared[field.key] = undefined
cleared.askingPrice = undefined
cleared.itemName = undefined
form.setFieldsValue(cleared)
setPriceState({ status: 'idle' })
lastPriceQueryKeyRef.current = ''
}
setSelectedProfileId(profileId)
}
setActiveTab('publish')
}}
items={[
+1 -1
View File
@@ -615,7 +615,7 @@ export function fetchWorkerConsignmentPriceSuggestion(params: {
}>('/api/v1/worker/consignment/price-suggestion', params)
}
/** 动态规格发布:按 profileId + specValues 算价(模板折扣价 / 固定价)。 */
/** 动态规格发布:按 profileId + specValues 算价(模板折扣价 / 模板价固定下调)。 */
export function fetchWorkerConsignmentPricePreview(payload: {
profileId?: number
specValues: ConsignmentSpecValues
+21 -4
View File
@@ -4498,11 +4498,16 @@ body {
gap: 12px;
}
/* 寄售价格 / 寄售数量 / 履约押金始终一行三列等宽 */
.worker-consignment-price-grid {
display: grid;
grid-template-columns: 240px 240px;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
justify-content: start;
}
/* 寄售数量被后台关闭时只剩两张卡,按两列等宽,避免第三列留空 */
.worker-consignment-price-grid.is-two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.worker-consignment-spec-card {
@@ -4635,9 +4640,14 @@ body {
}
}
@media (max-width: 768px) {
.worker-consignment-spec-grid,
@media (max-width: 1024px) {
.worker-consignment-price-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.worker-consignment-spec-grid {
grid-template-columns: 1fr;
}
@@ -4646,6 +4656,13 @@ body {
}
}
@media (max-width: 640px) {
.worker-consignment-price-grid,
.worker-consignment-price-grid.is-two {
grid-template-columns: 1fr;
}
}
.worker-consignment-filter-row {
display: flex;
flex-wrap: wrap;
@@ -20,7 +20,7 @@ test('后台全量选项:系统含通用,区服含互通', () => {
assert.ok(source.includes("value: '互通'"), '区服选项必须包含「互通」')
})
test('苏尔南默认配置:取消代码内置字段,由迁移 144 提供五个单选字段', () => {
test('苏尔南默认配置:取消代码内置字段,由合并迁移 142 提供五个单选字段', () => {
const utils = readFileSync(
new URL('../src/utils/consignment-spec.ts', import.meta.url).pathname,
'utf8',