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 }} > diff --git a/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx b/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx index bd8f15a3..b2980113 100644 --- a/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx @@ -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 ? ( - - ) : null} - 规格与价格 @@ -846,12 +838,16 @@ export default function WorkerConsignmentPage() { })} -
+
{ 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 = {} - 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={[ diff --git a/apps/frontend/src/services/worker.ts b/apps/frontend/src/services/worker.ts index bc29691f..c8f0244a 100644 --- a/apps/frontend/src/services/worker.ts +++ b/apps/frontend/src/services/worker.ts @@ -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 diff --git a/apps/frontend/src/styles/worker.css b/apps/frontend/src/styles/worker.css index 0762e2d7..e9e9913c 100644 --- a/apps/frontend/src/styles/worker.css +++ b/apps/frontend/src/styles/worker.css @@ -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; diff --git a/apps/frontend/tests/consignment-spec-options.test.ts b/apps/frontend/tests/consignment-spec-options.test.ts index bc4e9514..dc982092 100644 --- a/apps/frontend/tests/consignment-spec-options.test.ts +++ b/apps/frontend/tests/consignment-spec-options.test.ts @@ -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',