chore(db): 合并寄卖迁移为 152,保留 main 147-151 编号

- main 分支 147-151 保持原编号不变
- dev 寄卖迁移 147-158 合并为单一 152_consignment_platform.sql
- 同步迁移头注释与测试文件引用
- 开发库已补跑 main 147-151,并将 schema_migrations 收敛为 152 单条
This commit is contained in:
2026-11-09 13:50:06 +08:00
parent 335501d0c2
commit e5d6e92966
10 changed files with 201 additions and 192 deletions
@@ -1,38 +0,0 @@
-- 148_consignment_sms_logs.sql(原 143_consignment_sms_logs,合并 main 后顺延)
-- 寄卖匹配成功短信发送记录:用于幂等、审计与 Mock 联调验证。
-- 同一工单 + 同一库存只允许一条记录,避免重试/重复调度重复发短信。
CREATE TABLE IF NOT EXISTS consignment_sms_logs (
id BIGSERIAL PRIMARY KEY,
work_order_id BIGINT REFERENCES work_orders(id) ON DELETE SET NULL,
work_order_no TEXT NOT NULL DEFAULT '',
inventory_id BIGINT REFERENCES consignment_inventories(id) ON DELETE SET NULL,
inventory_no TEXT NOT NULL DEFAULT '',
worker_id BIGINT REFERENCES worker_users(id) ON DELETE SET NULL,
phone TEXT NOT NULL DEFAULT '',
template_key TEXT NOT NULL DEFAULT 'consignment_matched',
template_text TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '',
params_json JSONB NOT NULL DEFAULT '{}'::jsonb,
provider TEXT NOT NULL DEFAULT 'mock',
status TEXT NOT NULL DEFAULT 'sending'
CHECK (status IN ('sending', 'sent', 'failed', 'skipped')),
error_text TEXT NOT NULL DEFAULT '',
match_type TEXT NOT NULL DEFAULT 'auto'
CHECK (match_type IN ('auto', 'manual')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
sent_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_consignment_sms_logs_work_order_inventory
ON consignment_sms_logs (work_order_id, inventory_id);
CREATE INDEX IF NOT EXISTS idx_consignment_sms_logs_created
ON consignment_sms_logs (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_consignment_sms_logs_work_order
ON consignment_sms_logs (work_order_id, created_at DESC);
COMMENT ON TABLE consignment_sms_logs IS '寄卖匹配成功短信发送记录(幂等键:工单 + 库存)';
COMMENT ON COLUMN consignment_sms_logs.status IS 'sending/sent/failed/skipped';
COMMENT ON COLUMN consignment_sms_logs.match_type IS 'auto/manual';
COMMENT ON COLUMN consignment_sms_logs.template_key IS '短信场景键,寄卖匹配成功固定为 consignment_matched';
@@ -1,66 +0,0 @@
-- 149_consignment_inventory_category_backfill.sql(原 144_consignment_inventory_category_backfill,合并 main 后顺延)
-- 寄售仓库按「寄卖配置分类」筛选的前置数据修复。
--
-- 背景:分类级寄卖配置(142)上线前入库的提报与库存没有写 category_id/profile_id,
-- 线上 784 条库存、877 条提报全部为空;不补的话「按分类筛选」在老数据上永远是空列表。
--
-- 口径:这批历史数据全部是苏尔南手枪(item_name 含「苏尔南」,spec_values_json 固定
-- system/serverZone/series/diamondLevel/brillianceCount 五个键),因此按该分类的
-- 已发布配置回填,幂等且只影响 category_id 为空的记录。
DO $migration$
DECLARE
v_category_id BIGINT;
v_profile_id BIGINT;
v_version_id BIGINT;
v_snapshot 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;
SELECT p.id, p.current_version_id, v.config_json
INTO v_profile_id, v_version_id, v_snapshot
FROM consignment_profiles p
LEFT JOIN consignment_profile_versions v ON v.id = p.current_version_id
WHERE p.category_id = v_category_id
AND p.status = 'published'
LIMIT 1;
IF v_profile_id IS NULL THEN
RETURN;
END IF;
UPDATE consignment_inventories
SET category_id = v_category_id,
profile_id = COALESCE(profile_id, v_profile_id),
profile_version_id = COALESCE(profile_version_id, v_version_id),
profile_snapshot_json = COALESCE(profile_snapshot_json, v_snapshot)
WHERE category_id IS NULL
AND (
item_name LIKE '%苏尔南%'
OR (spec_values_json ? 'series' AND spec_values_json ? 'diamondLevel')
);
UPDATE consignment_submissions
SET category_id = v_category_id,
profile_id = COALESCE(profile_id, v_profile_id),
profile_version_id = COALESCE(profile_version_id, v_version_id),
profile_snapshot_json = COALESCE(profile_snapshot_json, v_snapshot)
WHERE category_id IS NULL
AND (
item_name LIKE '%苏尔南%'
OR (spec_values_json ? 'series' AND spec_values_json ? 'diamondLevel')
);
END $migration$;
CREATE INDEX IF NOT EXISTS idx_consignment_inventories_category
ON consignment_inventories (category_id, status);
COMMENT ON COLUMN consignment_inventories.category_id IS
'寄卖配置分类:寄售仓库按分类筛选的依据;142 之前的历史数据由 144 回填';
@@ -1,11 +0,0 @@
-- 150_work_order_consignment_original_reward.sql(原 145→146→150,合并 main 后顺延)
-- 寄卖内部匹配会改写 work_orders.reward_amount(按库存售价-服务费结算)。
-- 这里固化“匹配前的原始工单报酬”,所有释放库存的路径恢复它,避免超时/取消/退款后
-- 大厅继续展示寄卖结算价、或二次匹配在已被改写的金额上继续扣服务费。
-- 幂等:可安全重跑。
ALTER TABLE work_orders
ADD COLUMN IF NOT EXISTS consignment_original_reward_fen BIGINT;
COMMENT ON COLUMN work_orders.consignment_original_reward_fen IS
'寄卖内部匹配前的原始工单报酬(分);匹配改写 reward_amount 时写入,释放库存时恢复并清空';
@@ -1,34 +0,0 @@
-- 151_work_order_internal_matching_deadline_guard.sql(原 147,合并 main 后顺延)
-- 工单状态不变量:internal_matching 必须有内部匹配截止时间。
--
-- 背景:手动匹配/重新匹配曾先把工单改成 internal_matching,再补截止时间;
-- 中途失败会留下「internal_matching 且 deadline 为空」的工单,
-- 既不在大厅,也不会被超时任务处理。这里先修复存量,再用 CHECK 约束兜底。
-- 1. 修复存量:给缺失截止时间的内部匹配工单补 30 分钟等待窗口。
UPDATE work_orders
SET
internal_match_deadline_at = NOW() + INTERVAL '30 minutes',
hall_visible_at = COALESCE(hall_visible_at, NOW() + INTERVAL '30 minutes'),
updated_at = NOW()
WHERE status = 'internal_matching'
AND internal_match_deadline_at IS NULL;
-- 2. 新写入保护:internal_matching 不允许 deadline 为空。
-- 使用 NOT VALID + VALIDATE:先约束新写入,再校验存量(若并发写入异常会在此暴露)。
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'work_orders_internal_matching_deadline_check'
) THEN
ALTER TABLE work_orders
ADD CONSTRAINT work_orders_internal_matching_deadline_check
CHECK (status <> 'internal_matching' OR internal_match_deadline_at IS NOT NULL)
NOT VALID;
END IF;
END $$;
ALTER TABLE work_orders
VALIDATE CONSTRAINT work_orders_internal_matching_deadline_check;
@@ -1,12 +0,0 @@
-- 152_consignment_internal_match_cleanup.sql
-- 「超时后回大厅」改为手动匹配的固定行为:清理配置里的 internalMatch.fallbackToHall。
-- 147–151 已应用不可回改,这里用数据迁移清理历史草稿与已发布快照。
-- 幂等:仅删除存在该 key 的记录。
UPDATE consignment_profiles
SET config_json = config_json #- '{internalMatch,fallbackToHall}'
WHERE config_json #> '{internalMatch,fallbackToHall}' IS NOT NULL;
UPDATE consignment_profile_versions
SET config_json = config_json #- '{internalMatch,fallbackToHall}'
WHERE config_json #> '{internalMatch,fallbackToHall}' IS NOT NULL;
@@ -1,4 +1,5 @@
-- 147_consignment_platform.sql(原 142_consignment_platform,合并 main 后顺延)
-- 152_consignment_platform.sql(原 147_consignment_platform;153–158 已合并入本文件,main 保留 147–151 后顺延)
-- 2026-11-09 合并:短信日志 / 分类回填 / 原始报酬 / 截止时间不变量 / 内部匹配清理 / 匹配日志快照。
-- 寄卖平台完整迁移(原 142–148 收敛为一个文件,2026-11-06)
-- 142 寄卖配置/库存/内部匹配
-- 143 动态规格
@@ -643,3 +644,189 @@ FROM consignment_profiles AS p
WHERE v.id = p.current_version_id
AND jsonb_typeof(v.config_json->'specs') = 'array';
-- ===== 来源:153_consignment_sms_logs.sql =====
-- 153_consignment_sms_logs.sql(原 148_consignment_sms_logs,main 保留 147–151 后顺延)
-- 寄卖匹配成功短信发送记录:用于幂等、审计与 Mock 联调验证。
-- 同一工单 + 同一库存只允许一条记录,避免重试/重复调度重复发短信。
CREATE TABLE IF NOT EXISTS consignment_sms_logs (
id BIGSERIAL PRIMARY KEY,
work_order_id BIGINT REFERENCES work_orders(id) ON DELETE SET NULL,
work_order_no TEXT NOT NULL DEFAULT '',
inventory_id BIGINT REFERENCES consignment_inventories(id) ON DELETE SET NULL,
inventory_no TEXT NOT NULL DEFAULT '',
worker_id BIGINT REFERENCES worker_users(id) ON DELETE SET NULL,
phone TEXT NOT NULL DEFAULT '',
template_key TEXT NOT NULL DEFAULT 'consignment_matched',
template_text TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '',
params_json JSONB NOT NULL DEFAULT '{}'::jsonb,
provider TEXT NOT NULL DEFAULT 'mock',
status TEXT NOT NULL DEFAULT 'sending'
CHECK (status IN ('sending', 'sent', 'failed', 'skipped')),
error_text TEXT NOT NULL DEFAULT '',
match_type TEXT NOT NULL DEFAULT 'auto'
CHECK (match_type IN ('auto', 'manual')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
sent_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_consignment_sms_logs_work_order_inventory
ON consignment_sms_logs (work_order_id, inventory_id);
CREATE INDEX IF NOT EXISTS idx_consignment_sms_logs_created
ON consignment_sms_logs (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_consignment_sms_logs_work_order
ON consignment_sms_logs (work_order_id, created_at DESC);
COMMENT ON TABLE consignment_sms_logs IS '寄卖匹配成功短信发送记录(幂等键:工单 + 库存)';
COMMENT ON COLUMN consignment_sms_logs.status IS 'sending/sent/failed/skipped';
COMMENT ON COLUMN consignment_sms_logs.match_type IS 'auto/manual';
COMMENT ON COLUMN consignment_sms_logs.template_key IS '短信场景键,寄卖匹配成功固定为 consignment_matched';
-- ===== 来源:154_consignment_inventory_category_backfill.sql =====
-- 154_consignment_inventory_category_backfill.sql(原 149_consignment_inventory_category_backfill,main 保留 147–151 后顺延)
-- 寄售仓库按「寄卖配置分类」筛选的前置数据修复。
--
-- 背景:分类级寄卖配置(142)上线前入库的提报与库存没有写 category_id/profile_id,
-- 线上 784 条库存、877 条提报全部为空;不补的话「按分类筛选」在老数据上永远是空列表。
--
-- 口径:这批历史数据全部是苏尔南手枪(item_name 含「苏尔南」,spec_values_json 固定
-- system/serverZone/series/diamondLevel/brillianceCount 五个键),因此按该分类的
-- 已发布配置回填,幂等且只影响 category_id 为空的记录。
DO $migration$
DECLARE
v_category_id BIGINT;
v_profile_id BIGINT;
v_version_id BIGINT;
v_snapshot 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;
SELECT p.id, p.current_version_id, v.config_json
INTO v_profile_id, v_version_id, v_snapshot
FROM consignment_profiles p
LEFT JOIN consignment_profile_versions v ON v.id = p.current_version_id
WHERE p.category_id = v_category_id
AND p.status = 'published'
LIMIT 1;
IF v_profile_id IS NULL THEN
RETURN;
END IF;
UPDATE consignment_inventories
SET category_id = v_category_id,
profile_id = COALESCE(profile_id, v_profile_id),
profile_version_id = COALESCE(profile_version_id, v_version_id),
profile_snapshot_json = COALESCE(profile_snapshot_json, v_snapshot)
WHERE category_id IS NULL
AND (
item_name LIKE '%苏尔南%'
OR (spec_values_json ? 'series' AND spec_values_json ? 'diamondLevel')
);
UPDATE consignment_submissions
SET category_id = v_category_id,
profile_id = COALESCE(profile_id, v_profile_id),
profile_version_id = COALESCE(profile_version_id, v_version_id),
profile_snapshot_json = COALESCE(profile_snapshot_json, v_snapshot)
WHERE category_id IS NULL
AND (
item_name LIKE '%苏尔南%'
OR (spec_values_json ? 'series' AND spec_values_json ? 'diamondLevel')
);
END $migration$;
CREATE INDEX IF NOT EXISTS idx_consignment_inventories_category
ON consignment_inventories (category_id, status);
COMMENT ON COLUMN consignment_inventories.category_id IS
'寄卖配置分类:寄售仓库按分类筛选的依据;142 之前的历史数据由 144 回填';
-- ===== 来源:155_work_order_consignment_original_reward.sql =====
-- 155_work_order_consignment_original_reward.sql(原 150_work_order_consignment_original_reward,main 保留 147–151 后顺延)
-- 寄卖内部匹配会改写 work_orders.reward_amount(按库存售价-服务费结算)。
-- 这里固化“匹配前的原始工单报酬”,所有释放库存的路径恢复它,避免超时/取消/退款后
-- 大厅继续展示寄卖结算价、或二次匹配在已被改写的金额上继续扣服务费。
-- 幂等:可安全重跑。
ALTER TABLE work_orders
ADD COLUMN IF NOT EXISTS consignment_original_reward_fen BIGINT;
COMMENT ON COLUMN work_orders.consignment_original_reward_fen IS
'寄卖内部匹配前的原始工单报酬(分);匹配改写 reward_amount 时写入,释放库存时恢复并清空';
-- ===== 来源:156_work_order_internal_matching_deadline_guard.sql =====
-- 156_work_order_internal_matching_deadline_guard.sql(原 151_work_order_internal_matching_deadline_guard,main 保留 147–151 后顺延)
-- 工单状态不变量:internal_matching 必须有内部匹配截止时间。
--
-- 背景:手动匹配/重新匹配曾先把工单改成 internal_matching,再补截止时间;
-- 中途失败会留下「internal_matching 且 deadline 为空」的工单,
-- 既不在大厅,也不会被超时任务处理。这里先修复存量,再用 CHECK 约束兜底。
-- 1. 修复存量:给缺失截止时间的内部匹配工单补 30 分钟等待窗口。
UPDATE work_orders
SET
internal_match_deadline_at = NOW() + INTERVAL '30 minutes',
hall_visible_at = COALESCE(hall_visible_at, NOW() + INTERVAL '30 minutes'),
updated_at = NOW()
WHERE status = 'internal_matching'
AND internal_match_deadline_at IS NULL;
-- 2. 新写入保护:internal_matching 不允许 deadline 为空。
-- 使用 NOT VALID + VALIDATE:先约束新写入,再校验存量(若并发写入异常会在此暴露)。
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'work_orders_internal_matching_deadline_check'
) THEN
ALTER TABLE work_orders
ADD CONSTRAINT work_orders_internal_matching_deadline_check
CHECK (status <> 'internal_matching' OR internal_match_deadline_at IS NOT NULL)
NOT VALID;
END IF;
END $$;
ALTER TABLE work_orders
VALIDATE CONSTRAINT work_orders_internal_matching_deadline_check;
-- ===== 来源:157_consignment_internal_match_cleanup.sql =====
-- 157_consignment_internal_match_cleanup.sql(原 152_consignment_internal_match_cleanup,main 保留 147–151 后顺延)
-- 「超时后回大厅」改为手动匹配的固定行为:清理配置里的 internalMatch.fallbackToHall。
-- 历史迁移已应用不可回改,这里用数据迁移清理历史草稿与已发布快照。
-- 幂等:仅删除存在该 key 的记录。
UPDATE consignment_profiles
SET config_json = config_json #- '{internalMatch,fallbackToHall}'
WHERE config_json #> '{internalMatch,fallbackToHall}' IS NOT NULL;
UPDATE consignment_profile_versions
SET config_json = config_json #- '{internalMatch,fallbackToHall}'
WHERE config_json #> '{internalMatch,fallbackToHall}' IS NOT NULL;
-- ===== 来源:158_consignment_match_log_snapshot.sql =====
-- 158_consignment_match_log_snapshot.sql(原 153_consignment_match_log_snapshot,main 保留 147–151 后顺延)
-- 匹配日志快照化:记录匹配发生时生效的寄卖配置版本与匹配策略,
-- 便于配置改版后还原当时的判定依据(profileId 已由 152 提供)。
ALTER TABLE consignment_match_logs
ADD COLUMN IF NOT EXISTS profile_version_id BIGINT,
ADD COLUMN IF NOT EXISTS matching_mode TEXT,
ADD COLUMN IF NOT EXISTS exact_only BOOLEAN;
COMMENT ON COLUMN consignment_match_logs.profile_version_id IS
'匹配发生时生效的寄卖配置版本 ID(consignment_profile_versions.id)';
COMMENT ON COLUMN consignment_match_logs.matching_mode IS
'匹配发生时生效的匹配模式快照:off / manual / auto';
COMMENT ON COLUMN consignment_match_logs.exact_only IS
'匹配发生时生效的 exactOnly 开关快照';
@@ -1,14 +0,0 @@
-- 153_consignment_match_log_snapshot.sql
-- 匹配日志快照化:记录匹配发生时生效的寄卖配置版本与匹配策略,
-- 便于配置改版后还原当时的判定依据(profileId 已由 147 提供)。
ALTER TABLE consignment_match_logs
ADD COLUMN IF NOT EXISTS profile_version_id BIGINT,
ADD COLUMN IF NOT EXISTS matching_mode TEXT,
ADD COLUMN IF NOT EXISTS exact_only BOOLEAN;
COMMENT ON COLUMN consignment_match_logs.profile_version_id IS
'匹配发生时生效的寄卖配置版本 ID(consignment_profile_versions.id)';
COMMENT ON COLUMN consignment_match_logs.matching_mode IS
'匹配发生时生效的匹配模式快照:off / manual / auto';
COMMENT ON COLUMN consignment_match_logs.exact_only IS
'匹配发生时生效的 exactOnly 开关快照';
@@ -12,10 +12,7 @@ const ISSUE_REPO = new URL(
import.meta.url,
)
const FULFILLMENT = new URL('./consignment-fulfillment-service.ts', import.meta.url)
const MIGRATION = new URL(
'../../db/migrations/151_work_order_internal_matching_deadline_guard.sql',
import.meta.url,
)
const MIGRATION = new URL('../../db/migrations/152_consignment_platform.sql', import.meta.url)
test('手动匹配不再在事务外改状态:数量不足/并发占库失败不能留下无截止时间的 internal_matching', async () => {
const source = await readFile(ENGINE, 'utf8')
@@ -149,9 +149,9 @@ test('降价提示:未售出时长支持一位小数,按小数小时判断',
assert.equal(suggestion?.suggestedPriceFen, 9_000)
})
test('合并迁移 147 必须包含寄卖配置、库存数量/售价与分配表', async () => {
test('合并迁移 152 必须包含寄卖配置、库存数量/售价与分配表', async () => {
const source = await readFile(
new URL('../../db/migrations/147_consignment_platform.sql', import.meta.url),
new URL('../../db/migrations/152_consignment_platform.sql', import.meta.url),
'utf8',
)
for (const fragment of [
@@ -163,7 +163,7 @@ test('合并迁移 147 必须包含寄卖配置、库存数量/售价与分配
'internal_match_deadline_at',
'consignment_inventory_id',
]) {
assert.ok(source.includes(fragment), `合并迁移 147 缺少 ${fragment}`)
assert.ok(source.includes(fragment), `合并迁移 152 缺少 ${fragment}`)
}
})
@@ -214,9 +214,9 @@ test('寄售数量上限按分类统计,并在打手配置接口返回 activeQ
assert.ok(source.includes('GROUP BY profile_id'), '各分类数量必须一次分组取回')
})
test('合并迁移 147 的苏尔南种子:降价提示默认开启、24 小时、按比例 10%', async () => {
test('合并迁移 152 的苏尔南种子:降价提示默认开启、24 小时、按比例 10%', async () => {
const sql = await readFile(
new URL('../../db/migrations/147_consignment_platform.sql', import.meta.url),
new URL('../../db/migrations/152_consignment_platform.sql', import.meta.url),
'utf8',
)
assert.match(
@@ -36,7 +36,7 @@ function option(label: string, value: string) {
return { label, value, enabled: true }
}
/** 苏尔南手枪种子配置的五个字段(与合并迁移 147 一致)。 */
/** 苏尔南手枪种子配置的五个字段(与合并迁移 152 一致)。 */
function surnanSpecs(): ConsignmentSpecField[] {
return [
{
@@ -522,9 +522,9 @@ test('动态规格:旧列仍由兼容 key 同步', () => {
})
})
test('合并迁移 147 必须包含动态规格的 JSON、匹配 key、文本与改价上限', async () => {
test('合并迁移 152 必须包含动态规格的 JSON、匹配 key、文本与改价上限', async () => {
const sql = await readFile(
new URL('../../db/migrations/147_consignment_platform.sql', import.meta.url),
new URL('../../db/migrations/152_consignment_platform.sql', import.meta.url),
'utf8',
)
for (const fragment of [
@@ -534,7 +534,7 @@ test('合并迁移 147 必须包含动态规格的 JSON、匹配 key、文本与
'price_cap_fen',
'CREATE INDEX IF NOT EXISTS idx_consignment_inventories_spec_key',
]) {
assert.ok(sql.includes(fragment), `合并迁移 147 缺少 ${fragment}`)
assert.ok(sql.includes(fragment), `合并迁移 152 缺少 ${fragment}`)
}
})
@@ -549,9 +549,9 @@ test('发布数量:关闭「允许修改寄售数量」时固定为 1,开启
assert.equal(resolveConsignmentPublishQuantity(true, 1.6), 2, '四舍五入')
})
test('合并迁移 147 必须为苏尔南手枪写入已发布的五个单选字段配置', async () => {
test('合并迁移 152 必须为苏尔南手枪写入已发布的五个单选字段配置', async () => {
const sql = await readFile(
new URL('../../db/migrations/147_consignment_platform.sql', import.meta.url),
new URL('../../db/migrations/152_consignment_platform.sql', import.meta.url),
'utf8',
)
for (const fragment of [
@@ -561,6 +561,6 @@ test('合并迁移 147 必须为苏尔南手枪写入已发布的五个单选字
"'published'",
'"key": "brillianceCount"',
]) {
assert.ok(sql.includes(fragment), `合并迁移 147 缺少 ${fragment}`)
assert.ok(sql.includes(fragment), `合并迁移 152 缺少 ${fragment}`)
}
})