修改套餐价格

This commit is contained in:
wuweihua
2026-09-16 17:14:19 +08:00
parent 7172621fd3
commit 4441aa12bf
48 changed files with 4258 additions and 682 deletions
@@ -0,0 +1,34 @@
# Price set version tables
## Wizard (2 steps)
1. Basic info + view bound customers (read-only)
2. Edit product prices (library pull on next)
- No manual effective_time input.
- Draft version: status=0, effective_time null until activate.
- Activate: sets effective_time=now; previous version expires. Does not change package header status.
- Copy as new version: `POST /price/set/version/copy/{psvId}` inserts new draft `prc_price_set_version` and copies `prc_price_set_detail` (+ surcharge snapshots) from source psv_id.
- Audit submit/approve/reject removed from UI save path.
## Graph
```
prc_price_set
-> prc_price_set_version draft / active / expired
-> prc_price_set_detail product prices (psv_id)
-> prc_price_set_version_surcharge
```
## Status
Version: 0 draft, 1 active, 2 expired. Active when effective_time <= now and (expire_time null or > now).
## SQL
1. `price_set_version.sql`
2. `price_set_version_eff_nullable.sql` (allow null `prc_price_set.effective_date` and `prc_price_set_version.effective_time` for drafts)
## Billing
Match by price_set_id; multi-version quote should filter active psv_id by bill time.
+7 -1
View File
@@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS prc_price_set (
default_discount DECIMAL(5,2) DEFAULT NULL COMMENT '默认折扣百分数 1-99,仅 DISCOUNT',
default_type CHAR(1) DEFAULT NULL COMMENT '默认套餐 Y=默认',
calculate_sign CHAR(1) DEFAULT NULL COMMENT '试算标记 Y=外部试算套餐',
effective_date DATE NOT NULL COMMENT '生效日期',
effective_date DATE DEFAULT NULL COMMENT 'effective date (null until activate)',
expire_date DATE DEFAULT NULL COMMENT '失效日期(空=长期)',
status CHAR(1) NOT NULL DEFAULT 'D' COMMENT 'D草稿 A审核中 E生效 R驳回 S停用',
tms_id BIGINT(20) DEFAULT NULL COMMENT '多租户系统ID',
@@ -46,17 +46,23 @@ CREATE TABLE IF NOT EXISTS prc_price_set_detail (
zd_id BIGINT(20) DEFAULT NULL COMMENT 'legacy zone division id',
wsd_id BIGINT(20) DEFAULT NULL COMMENT '重量段明细id',
weight_label VARCHAR(50) DEFAULT NULL COMMENT '重量段展示',
weight_min DECIMAL(10,3) DEFAULT NULL COMMENT '重量段最小值KG(含)',
weight_max DECIMAL(10,3) DEFAULT NULL COMMENT '重量段最大值KG(含)',
product_country_id BIGINT(20) DEFAULT NULL COMMENT '关联 bms_product_country.detail_id',
fv_value DECIMAL(10,4) DEFAULT NULL COMMENT '运费单价 RMB/KG',
fv_registration_fee DECIMAL(10,2) DEFAULT NULL COMMENT '挂号费 RMB/票',
discount_value DECIMAL(5,2) DEFAULT NULL COMMENT '明细折扣百分数,DISCOUNT 可覆盖默认',
round_mode VARCHAR(20) NOT NULL DEFAULT 'HALF_UP' COMMENT '取值方式(字典bms_round_mode)',
pr_id INT(11) DEFAULT NULL COMMENT '关联价格规则id',
effective_date DATE DEFAULT NULL COMMENT '生效日期',
expire_date DATE DEFAULT NULL COMMENT '失效日期(空=长期)',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (psd_id),
INDEX idx_psd_set (price_set_id),
INDEX idx_psd_product (price_set_id, product_code, country_code),
INDEX idx_psd_weight_match (price_set_id, product_code, country_code, weight_min, weight_max),
INDEX idx_psd_eff (price_set_id, effective_date, expire_date),
INDEX idx_psd_product_country (product_country_id),
CONSTRAINT fk_psd_set FOREIGN KEY (price_set_id) REFERENCES prc_price_set (price_set_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='价格套餐明细-固定价/折扣范围';
@@ -0,0 +1,22 @@
-- Prevent duplicate FIXED details within one version (same product_country snapshot row)
-- MySQL allows multiple NULLs in UNIQUE, so DISCOUNT rows (product_country_id null) are OK.
SET NAMES utf8mb4;
SET @db := DATABASE();
-- Clean exact duplicates before adding unique key (keep min psd_id)
DELETE d1 FROM prc_price_set_detail d1
INNER JOIN prc_price_set_detail d2
ON d1.psv_id = d2.psv_id
AND d1.product_country_id = d2.product_country_id
AND d1.psd_id > d2.psd_id
WHERE d1.psv_id IS NOT NULL
AND d1.product_country_id IS NOT NULL;
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND INDEX_NAME = 'uk_psd_psv_product_country'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD UNIQUE KEY uk_psd_psv_product_country (psv_id, product_country_id)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -0,0 +1,38 @@
-- prc_price_set_detail: weight_min/weight_max + effective_date/expire_date
-- same types as bms_product_country
SET NAMES utf8mb4;
SET @db := DATABASE();
SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='prc_price_set_detail' AND COLUMN_NAME='weight_min');
SET @sql := IF(@has=0, 'ALTER TABLE prc_price_set_detail ADD COLUMN weight_min DECIMAL(10,3) DEFAULT NULL COMMENT ''重量段最小值KG(含)'' AFTER weight_label', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='prc_price_set_detail' AND COLUMN_NAME='weight_max');
SET @sql := IF(@has=0, 'ALTER TABLE prc_price_set_detail ADD COLUMN weight_max DECIMAL(10,3) DEFAULT NULL COMMENT ''重量段最大值KG(含)'' AFTER weight_min', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='prc_price_set_detail' AND COLUMN_NAME='effective_date');
SET @sql := IF(@has=0, 'ALTER TABLE prc_price_set_detail ADD COLUMN effective_date DATE DEFAULT NULL COMMENT ''生效日期'' AFTER pr_id', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='prc_price_set_detail' AND COLUMN_NAME='expire_date');
SET @sql := IF(@has=0, 'ALTER TABLE prc_price_set_detail ADD COLUMN expire_date DATE DEFAULT NULL COMMENT ''失效日期(空=长期)'' AFTER effective_date', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='prc_price_set_detail' AND INDEX_NAME='idx_psd_weight_match');
SET @sql := IF(@has=0, 'ALTER TABLE prc_price_set_detail ADD INDEX idx_psd_weight_match (price_set_id, product_code, country_code, weight_min, weight_max)', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='prc_price_set_detail' AND INDEX_NAME='idx_psd_eff');
SET @sql := IF(@has=0, 'ALTER TABLE prc_price_set_detail ADD INDEX idx_psd_eff (price_set_id, effective_date, expire_date)', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 回填: 从 bms_product_country 带入重量段与有效期
UPDATE prc_price_set_detail d
INNER JOIN bms_product_country c ON c.detail_id = d.product_country_id
SET
d.weight_min = IFNULL(d.weight_min, c.weight_min),
d.weight_max = IFNULL(d.weight_max, c.weight_max),
d.effective_date = IFNULL(d.effective_date, c.effective_date),
d.expire_date = IFNULL(d.expire_date, c.expire_date)
WHERE d.product_country_id IS NOT NULL;
+152
View File
@@ -0,0 +1,152 @@
-- 价格套餐版本中间表 + 版本附加费
-- 关系:
-- prc_price_set
-- -> prc_price_set_version (步骤2: 版本号自动生成, 生效时间)
-- -> prc_price_set_detail (步骤3: 产品价格, 通过 psv_id)
-- -> prc_price_set_version_surcharge (psd_id 关联明细)
-- 可重复执行
SET NAMES utf8mb4;
SET @db := DATABASE();
CREATE TABLE IF NOT EXISTS prc_price_set_version (
psv_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '套餐版本id',
price_set_id BIGINT(20) NOT NULL COMMENT '价格套餐id',
version_no VARCHAR(20) NOT NULL COMMENT '版本号 V1/V2,自动+1',
version_seq INT(11) NOT NULL COMMENT '版本序号,用于自动+1',
effective_time DATETIME DEFAULT NULL COMMENT 'version start time (null until activate)',
expire_time DATETIME DEFAULT NULL COMMENT '失效时间(空=长期;下一版本启动时写入该启动时间)',
status CHAR(1) NOT NULL DEFAULT '0' COMMENT '0草稿 1生效 2失效',
created_by VARCHAR(64) DEFAULT '' COMMENT '创建人',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
updated_by VARCHAR(64) DEFAULT '' COMMENT '更新人',
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (psv_id),
UNIQUE KEY uk_psv_set_no (price_set_id, version_no),
UNIQUE KEY uk_psv_set_seq (price_set_id, version_seq),
INDEX idx_psv_set_eff (price_set_id, effective_time),
INDEX idx_psv_status (status),
CONSTRAINT fk_psv_set FOREIGN KEY (price_set_id) REFERENCES prc_price_set (price_set_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='价格套餐版本';
CREATE TABLE IF NOT EXISTS prc_price_set_version_surcharge (
psvs_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '版本附加费id',
psv_id BIGINT(20) NOT NULL COMMENT '套餐版本id',
psd_id BIGINT(20) NOT NULL COMMENT '套餐明细id',
sc_id BIGINT(20) DEFAULT NULL COMMENT '来源附加费配置id',
product_code VARCHAR(20) DEFAULT NULL COMMENT '产品代码(与明细一致)',
sc_name VARCHAR(100) NOT NULL COMMENT '附加费名称快照',
fee_type VARCHAR(30) NOT NULL DEFAULT 'SURCHARGE' COMMENT '费用类型(字典bms_fee_type)',
charge_type VARCHAR(20) NOT NULL COMMENT '计费方式 FIXED固定金额/WEIGHT按重量/RATE按比例',
charge_value DECIMAL(18,6) NOT NULL COMMENT '计费数值快照',
currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种',
round_mode VARCHAR(20) NOT NULL DEFAULT 'HALF_UP' COMMENT '取值方式(字典bms_round_mode)',
country_code CHAR(2) DEFAULT NULL COMMENT '适用国家ISO2(空=全部)',
sc_note VARCHAR(500) DEFAULT NULL COMMENT '备注',
created_by VARCHAR(64) DEFAULT '' COMMENT '创建人',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
updated_by VARCHAR(64) DEFAULT '' COMMENT '更新人',
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (psvs_id),
INDEX idx_psvs_psv (psv_id),
INDEX idx_psvs_psd (psd_id),
INDEX idx_psvs_product (psv_id, product_code),
INDEX idx_psvs_sc (sc_id),
CONSTRAINT fk_psvs_psv FOREIGN KEY (psv_id) REFERENCES prc_price_set_version (psv_id) ON DELETE CASCADE,
CONSTRAINT fk_psvs_psd FOREIGN KEY (psd_id) REFERENCES prc_price_set_detail (psd_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='价格套餐版本附加费快照';
-- expire_time: previous version expire = next version effective_time (start)
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_version' AND COLUMN_NAME = 'expire_time'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_version ADD COLUMN expire_time DATETIME DEFAULT NULL COMMENT ''失效时间(空=长期;下一版本启动时写入该启动时间)'' AFTER effective_time',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- backfill: expire_time = next version start time (by version_seq)
UPDATE prc_price_set_version cur
INNER JOIN prc_price_set_version nxt
ON nxt.price_set_id = cur.price_set_id
AND nxt.version_seq = cur.version_seq + 1
SET cur.expire_time = nxt.effective_time;
-- prc_price_set_detail.psv_id
SET @has := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND COLUMN_NAME = 'psv_id'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD COLUMN psv_id BIGINT(20) DEFAULT NULL COMMENT ''套餐版本id'' AFTER price_set_id',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND INDEX_NAME = 'idx_psd_psv'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD INDEX idx_psd_psv (psv_id)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (
SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_detail' AND CONSTRAINT_NAME = 'fk_psd_psv'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_detail ADD CONSTRAINT fk_psd_psv FOREIGN KEY (psv_id) REFERENCES prc_price_set_version (psv_id) ON DELETE CASCADE',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 已建表补关联: 附加费.psd_id -> 套餐明细
DELETE FROM prc_price_set_version_surcharge
WHERE psd_id IS NULL
OR NOT EXISTS (
SELECT 1 FROM prc_price_set_detail d WHERE d.psd_id = prc_price_set_version_surcharge.psd_id
);
SET @sql := CONCAT(
'ALTER TABLE prc_price_set_version_surcharge MODIFY COLUMN psd_id BIGINT(20) NOT NULL COMMENT ''套餐明细id'''
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @has := (
SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_version_surcharge' AND CONSTRAINT_NAME = 'fk_psvs_psd'
);
SET @sql := IF(@has = 0,
'ALTER TABLE prc_price_set_version_surcharge ADD CONSTRAINT fk_psvs_psd FOREIGN KEY (psd_id) REFERENCES prc_price_set_detail (psd_id) ON DELETE CASCADE',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 回填: 每个已有套餐生成一条 V1
INSERT INTO prc_price_set_version (
price_set_id, version_no, version_seq, effective_time, status, created_by, create_time
)
SELECT
s.price_set_id,
'V1',
1,
COALESCE(CAST(s.effective_date AS DATETIME), s.create_time, NOW()),
CASE
WHEN s.status = 'S' THEN '2'
WHEN s.expire_date IS NOT NULL AND s.expire_date <= CURDATE() THEN '2'
WHEN s.status = 'E' AND (s.effective_date IS NULL OR s.effective_date <= CURDATE()) THEN '1'
ELSE '0'
END,
IFNULL(s.created_by, ''),
NOW()
FROM prc_price_set s
WHERE NOT EXISTS (
SELECT 1 FROM prc_price_set_version v WHERE v.price_set_id = s.price_set_id
);
-- 将已有明细绑定到同一套餐的 V1
UPDATE prc_price_set_detail d
INNER JOIN prc_price_set_version v
ON v.price_set_id = d.price_set_id AND v.version_seq = 1
SET d.psv_id = v.psv_id
WHERE d.psv_id IS NULL;
@@ -0,0 +1,23 @@
-- Draft package / version: effective dates nullable until activate
SET NAMES utf8mb4;
SET @db := DATABASE();
-- prc_price_set.effective_date
SET @nullable := (
SELECT IS_NULLABLE FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set' AND COLUMN_NAME = 'effective_date'
);
SET @sql := IF(@nullable = 'NO',
'ALTER TABLE prc_price_set MODIFY COLUMN effective_date DATE DEFAULT NULL COMMENT ''effective date (null until version activate)''',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- prc_price_set_version.effective_time
SET @nullable := (
SELECT IS_NULLABLE FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'prc_price_set_version' AND COLUMN_NAME = 'effective_time'
);
SET @sql := IF(@nullable = 'NO',
'ALTER TABLE prc_price_set_version MODIFY COLUMN effective_time DATETIME DEFAULT NULL COMMENT ''version start time (null until activate)''',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;