diff --git a/apps/backend/src/db/migrate.ts b/apps/backend/src/db/migrate.ts
index 118ef5be..39c51e44 100644
--- a/apps/backend/src/db/migrate.ts
+++ b/apps/backend/src/db/migrate.ts
@@ -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() {
diff --git a/apps/backend/src/db/migrations/142_consignment_matching.sql b/apps/backend/src/db/migrations/142_consignment_matching.sql
deleted file mode 100644
index 380c36e4..00000000
--- a/apps/backend/src/db/migrations/142_consignment_matching.sql
+++ /dev/null
@@ -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';
diff --git a/apps/backend/src/db/migrations/142_consignment_platform.sql b/apps/backend/src/db/migrations/142_consignment_platform.sql
new file mode 100644
index 00000000..5d0aa95b
--- /dev/null
+++ b/apps/backend/src/db/migrations/142_consignment_platform.sql
@@ -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';
+
diff --git a/apps/backend/src/db/migrations/143_consignment_dynamic_specs.sql b/apps/backend/src/db/migrations/143_consignment_dynamic_specs.sql
deleted file mode 100644
index f904329d..00000000
--- a/apps/backend/src/db/migrations/143_consignment_dynamic_specs.sql
+++ /dev/null
@@ -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 = 不限制';
diff --git a/apps/backend/src/db/migrations/144_consignment_surnan_default_profile.sql b/apps/backend/src/db/migrations/144_consignment_surnan_default_profile.sql
deleted file mode 100644
index 0dcf9173..00000000
--- a/apps/backend/src/db/migrations/144_consignment_surnan_default_profile.sql
+++ /dev/null
@@ -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$;
diff --git a/apps/backend/src/db/migrations/145_consignment_inventory_multi_binding.sql b/apps/backend/src/db/migrations/145_consignment_inventory_multi_binding.sql
deleted file mode 100644
index 7b287a59..00000000
--- a/apps/backend/src/db/migrations/145_consignment_inventory_multi_binding.sql
+++ /dev/null
@@ -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;
diff --git a/apps/backend/src/db/migrations/146_consignment_share_gift.sql b/apps/backend/src/db/migrations/146_consignment_share_gift.sql
deleted file mode 100644
index 5cc1a11d..00000000
--- a/apps/backend/src/db/migrations/146_consignment_share_gift.sql
+++ /dev/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
- '寄卖拼单份额:赠送冷却结束时间;到点后按时间实时推导为可赠送';
diff --git a/apps/backend/src/db/migrations/147_consignment_match_and_rematch.sql b/apps/backend/src/db/migrations/147_consignment_match_and_rematch.sql
deleted file mode 100644
index 29946f7a..00000000
--- a/apps/backend/src/db/migrations/147_consignment_match_and_rematch.sql
+++ /dev/null
@@ -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';
diff --git a/apps/backend/src/services/worker-platform/consignment-profile-service.test.ts b/apps/backend/src/services/worker-platform/consignment-profile-service.test.ts
index 15eb50f2..531e0d17 100644
--- a/apps/backend/src/services/worker-platform/consignment-profile-service.test.ts
+++ b/apps/backend/src/services/worker-platform/consignment-profile-service.test.ts
@@ -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}`)
}
})
diff --git a/apps/backend/src/services/worker-platform/consignment-spec-service.test.ts b/apps/backend/src/services/worker-platform/consignment-spec-service.test.ts
index 75166de2..088b19a3 100644
--- a/apps/backend/src/services/worker-platform/consignment-spec-service.test.ts
+++ b/apps/backend/src/services/worker-platform/consignment-spec-service.test.ts
@@ -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}`)
}
})
diff --git a/apps/backend/src/services/worker-platform/consignment-spec-service.ts b/apps/backend/src/services/worker-platform/consignment-spec-service.ts
index 0f5b753f..e31642f7 100644
--- a/apps/backend/src/services/worker-platform/consignment-spec-service.ts
+++ b/apps/backend/src/services/worker-platform/consignment-spec-service.ts
@@ -3,9 +3,9 @@
*
* 设计约定:
* - 没有任何「内置字段」:配置初始 specs 为空,字段全部由运营在寄卖配置里维护;
- * 苏尔南手枪的五个字段由迁移 131 作为种子配置写入;
+ * 苏尔南手枪的五个字段由迁移 142 作为种子配置写入;
* - 字段类型支持:文本 / 长文本 / 下拉 / 单选 / 数字 / 日期;
- * - 价格只有两种:接单模板价打折、模板价固定下调;打手只能按系统建议价往下调;
+ * - 价格只有两种:接单模板价打折、模板价固定下调;打手只能按系统建议价格往下调;
* - 旧列兼容:字段 key 仍为 system/serverZone/series/diamondLevel/brillianceCount 时,
* 继续同步旧列并沿用旧匹配器,保证存量库存与工单不回归。
*/
diff --git a/apps/backend/src/services/worker-platform/consignment-worker-service.ts b/apps/backend/src/services/worker-platform/consignment-worker-service.ts
index 2d62b58f..f84395ed 100644
--- a/apps/backend/src/services/worker-platform/consignment-worker-service.ts
+++ b/apps/backend/src/services/worker-platform/consignment-worker-service.ts
@@ -1045,7 +1045,7 @@ export async function submitWorkerConsignmentGiftEvidence(
/**
* 发布寄卖的建议价:
* - 模板折扣:接单模板价 × 折扣;
- * - 固定价格:直接使用固定价,有模板价时校验不得高于模板价;
+ * - 模板价固定下调:接单模板价 − 固定下调金额,有模板价才允许发布;
* - 同时返回改价上限(等于建议价,打手只能往下调)。
*/
export async function getWorkerConsignmentPriceSuggestion(payload: {
diff --git a/apps/frontend/src/pages/admin/panels/ConsignmentConfigPanel.tsx b/apps/frontend/src/pages/admin/panels/ConsignmentConfigPanel.tsx
index 80bac582..91037864 100644
--- a/apps/frontend/src/pages/admin/panels/ConsignmentConfigPanel.tsx
+++ b/apps/frontend/src/pages/admin/panels/ConsignmentConfigPanel.tsx
@@ -1312,7 +1312,7 @@ export default function ConsignmentConfigPanel() {
label="允许打手改价"
name={['pricing', 'allowWorkerPrice']}
valuePropName="checked"
- extra="打手只能按系统建议价往下调"
+ extra="打手只能按系统建议价格往下调"
style={{ marginBottom: 0 }}
>