This commit is contained in:
2026-08-10 15:36:47 +08:00
parent 013de5e921
commit c9f1fe0093
20 changed files with 1480 additions and 135 deletions
+1 -1
View File
@@ -125,7 +125,7 @@ SELECT COUNT(*) = 8 AS scheduler_columns_ready
## 当前主体字段语义核验 ## 当前主体字段语义核验
- 业务单租户阶段,业务表中的 `tenant_id > 0` 是客户 ID 和客户隔离键,不表示上级租户;费用项目和入库费用明细沿用该物理字段名。 - 业务单租户阶段,业务表中的 `tenant_id > 0` 是客户 ID 和客户隔离键,不表示上级租户;费用项目是 `tenant_id = 0` 的平台公共主数据,入库费用明细仍以 `tenant_id > 0` 隔离客户并引用公共 `fee_item_id`。
- `fl_third_party_call_log.tenant_id = 0` 仅表示第三方主体未知或尚未解析的哨兵值,不是平台客户记录或平台操作作用域,绝不能据此授权、查询或写入客户业务数据。上线后可只读统计: - `fl_third_party_call_log.tenant_id = 0` 仅表示第三方主体未知或尚未解析的哨兵值,不是平台客户记录或平台操作作用域,绝不能据此授权、查询或写入客户业务数据。上线后可只读统计:
```sql ```sql
+512 -24
View File
@@ -4310,9 +4310,10 @@ USE baiyue_firstleg;
CREATE TABLE IF NOT EXISTS fl_fee_item ( CREATE TABLE IF NOT EXISTS fl_fee_item (
id BIGINT UNSIGNED NOT NULL COMMENT '费用项目ID', id BIGINT UNSIGNED NOT NULL COMMENT '费用项目ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '当前客户ID(客户隔离键);物理字段名沿用 tenant_id,当前不表示上级租户', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共费用项目作用域,固定为0;物理字段名沿用 tenant_id',
category VARCHAR(64) NOT NULL COMMENT '费用分类', category VARCHAR(64) NOT NULL COMMENT '费用分类',
trade_scope VARCHAR(20) NOT NULL COMMENT 'DOMESTIC国内、INTERNATIONAL国际', trade_scope VARCHAR(20) NOT NULL COMMENT 'DOMESTIC国内、INTERNATIONAL国际',
fee_code VARCHAR(50) NOT NULL COMMENT '稳定费用编码;平台公共主数据固定编码',
fee_type_name VARCHAR(128) NOT NULL COMMENT '费用类型名称', fee_type_name VARCHAR(128) NOT NULL COMMENT '费用类型名称',
pickup_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应付是否参与', pickup_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应付是否参与',
pickup_receivable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应收是否参与', pickup_receivable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应收是否参与',
@@ -4330,13 +4331,14 @@ CREATE TABLE IF NOT EXISTS fl_fee_item (
deleted TINYINT(1) NOT NULL DEFAULT 0, deleted TINYINT(1) NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE KEY uk_fee_item_code (fee_code),
KEY idx_fee_item_query (tenant_id, trade_scope, category, status, deleted), KEY idx_fee_item_query (tenant_id, trade_scope, category, status, deleted),
KEY idx_fee_item_name (tenant_id, fee_type_name, deleted) KEY idx_fee_item_name (tenant_id, fee_type_name, deleted)
) ENGINE=InnoDB COMMENT='费用项目主数据'; ) ENGINE=InnoDB COMMENT='平台公共费用项目主数据';
CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail ( CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail (
id BIGINT UNSIGNED NOT NULL COMMENT '入库费用明细ID', id BIGINT UNSIGNED NOT NULL COMMENT '入库费用明细ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '当前客户ID(客户隔离键);物理字段名沿用 tenant_id,当前不表示上级租户', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID(客户隔离键);费用明细引用平台公共费用项目',
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '绑定费用项目ID', fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '绑定费用项目ID',
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照', inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
@@ -6417,6 +6419,379 @@ END$$
DELIMITER ; DELIMITER ;
CALL unified_publish_fee_add_column('fl_fee_item', 'fee_code', CALL unified_publish_fee_add_column('fl_fee_item', 'fee_code',
"VARCHAR(50) NULL COMMENT '稳定费用编码;系统费用使用固定编码' AFTER trade_scope"); "VARCHAR(50) NULL COMMENT '稳定费用编码;系统费用使用固定编码' AFTER trade_scope");
ALTER TABLE fl_fee_item
MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共费用项目作用域,固定为0;物理字段名沿用 tenant_id',
COMMENT = '平台公共费用项目主数据';
ALTER TABLE fl_inbound_fee_detail
MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID(客户隔离键);费用明细引用平台公共费用项目';
-- 费用类型.xls(SHA-256 395C9CA71816E79C7745E224E63CF79A4AB782140364F63C8374B396DF38CDB4)
-- 单表费用类型:110条(国内84、国际26);源表无编码列,按受控行序生成稳定编码。
DROP TEMPORARY TABLE IF EXISTS tmp_fee_item_seed;
CREATE TEMPORARY TABLE tmp_fee_item_seed (
seed_no INT NOT NULL PRIMARY KEY,
id BIGINT UNSIGNED NOT NULL,
fee_code VARCHAR(50) NOT NULL,
category VARCHAR(64) NOT NULL,
trade_scope VARCHAR(20) NOT NULL,
fee_type_name VARCHAR(128) NOT NULL,
pickup_payable TINYINT(1) NOT NULL,
pickup_receivable TINYINT(1) NOT NULL,
warehouse_payable TINYINT(1) NOT NULL,
warehouse_receivable TINYINT(1) NOT NULL,
allocation_fee TINYINT(1) NOT NULL,
description VARCHAR(500) NULL,
created_by BIGINT NULL,
created_by_name VARCHAR(64) NULL,
created_at DATETIME(3) NOT NULL,
updated_by BIGINT NULL,
updated_by_name VARCHAR(64) NULL,
updated_at DATETIME(3) NOT NULL,
UNIQUE KEY uk_tmp_fee_seed_code (fee_code),
UNIQUE KEY uk_tmp_fee_seed_semantic (trade_scope, fee_type_name)
) ENGINE=MEMORY;
INSERT INTO tmp_fee_item_seed (seed_no, id, fee_code, category, trade_scope, fee_type_name, pickup_payable, pickup_receivable, warehouse_payable, warehouse_receivable, allocation_fee, description, created_by, created_by_name, created_at, updated_by, updated_by_name, updated_at) VALUES
(1,193000000001000001,'FEE_TYPE_001','洛杉矶仓储成本','INTERNATIONAL','拖车费2',0,0,1,0,0,NULL,NULL,'admin','2023-07-10 00:00:00.000',NULL,'admin','2023-07-10 00:00:00.000'),
(2,193000000001000002,'FEE_TYPE_002','洛杉矶仓储成本','INTERNATIONAL','仓库水电费',0,0,1,0,0,NULL,NULL,'admin','2023-07-08 00:00:00.000',NULL,'admin','2023-07-08 00:00:00.000'),
(3,193000000001000003,'FEE_TYPE_003','洛杉矶仓储成本','INTERNATIONAL','人工费',0,0,1,0,0,'人工工资费用',NULL,'admin','2023-07-08 00:00:00.000',NULL,'admin','2023-07-08 00:00:00.000'),
(4,193000000001000004,'FEE_TYPE_004','洛杉矶仓储成本','INTERNATIONAL','仓库租赁费',0,0,1,0,0,'应付 仓库租赁费',NULL,'admin','2023-07-08 00:00:00.000',NULL,'admin','2023-07-08 00:00:00.000'),
(5,193000000001000005,'FEE_TYPE_005','默认','DOMESTIC','删单费',1,1,0,0,1,NULL,15268360352,'15268360352','2026-05-29 00:00:00.000',15268360352,'15268360352','2026-05-29 00:00:00.000'),
(6,193000000001000006,'FEE_TYPE_006','默认','INTERNATIONAL','换FBA标费用',1,1,1,1,1,NULL,NULL,'zhaoting','2026-03-04 00:00:00.000',NULL,'zhaoting','2026-03-04 00:00:00.000'),
(7,193000000001000007,'FEE_TYPE_007','默认','INTERNATIONAL','换SKU费用',1,1,1,1,1,NULL,NULL,'zhaoting','2026-03-04 00:00:00.000',NULL,'zhaoting','2026-03-04 00:00:00.000'),
(8,193000000001000008,'FEE_TYPE_008','默认','INTERNATIONAL','开箱费',0,1,1,1,1,NULL,NULL,'zhaoting','2026-03-04 00:00:00.000',NULL,'zhaoting','2026-03-04 00:00:00.000'),
(9,193000000001000009,'FEE_TYPE_009','默认','DOMESTIC','天翔八提',1,1,0,0,1,NULL,15268360352,'15268360352','2025-08-19 00:00:00.000',15268360352,'15268360352','2025-08-19 00:00:00.000'),
(10,193000000001000010,'FEE_TYPE_010','默认','DOMESTIC','天翔五提',1,1,0,0,1,NULL,15268360352,'15268360352','2025-08-05 00:00:00.000',15268360352,'15268360352','2025-08-05 00:00:00.000'),
(11,193000000001000011,'FEE_TYPE_011','默认','DOMESTIC','荣盛三提',1,1,0,0,1,NULL,15268360352,'15268360352','2025-08-05 00:00:00.000',15268360352,'15268360352','2025-08-05 00:00:00.000'),
(12,193000000001000012,'FEE_TYPE_012','默认','DOMESTIC','两地费',1,1,0,0,1,NULL,15268360352,'15268360352','2025-08-04 00:00:00.000',15268360352,'15268360352','2025-08-04 00:00:00.000'),
(13,193000000001000013,'FEE_TYPE_013','默认','DOMESTIC','绕路费',1,1,0,0,1,NULL,15268360352,'15268360352','2025-08-04 00:00:00.000',15268360352,'15268360352','2025-08-04 00:00:00.000'),
(14,193000000001000014,'FEE_TYPE_014','默认','DOMESTIC','朝华提',1,1,0,0,1,NULL,15268360352,'15268360352','2025-07-31 00:00:00.000',15268360352,'15268360352','2025-07-31 00:00:00.000'),
(15,193000000001000015,'FEE_TYPE_015','默认','DOMESTIC','长胜提',1,1,0,0,1,NULL,15268360352,'15268360352','2025-07-31 00:00:00.000',15268360352,'15268360352','2025-07-31 00:00:00.000'),
(16,193000000001000016,'FEE_TYPE_016','默认','DOMESTIC','港杂费',1,1,0,0,1,NULL,15268360352,'15268360352','2025-07-24 00:00:00.000',15268360352,'15268360352','2025-07-24 00:00:00.000'),
(17,193000000001000017,'FEE_TYPE_017','默认','DOMESTIC','极偏远费',1,1,1,1,1,NULL,NULL,'zhaoting','2025-06-11 00:00:00.000',NULL,'zhaoting','2025-06-11 00:00:00.000'),
(18,193000000001000018,'FEE_TYPE_018','默认','DOMESTIC','退舱费',1,0,0,0,0,NULL,15690305912,'15690305912','2025-05-22 00:00:00.000',15690305912,'15690305912','2025-05-22 00:00:00.000'),
(19,193000000001000019,'FEE_TYPE_019','默认','DOMESTIC','推送费',1,1,0,0,0,NULL,15690305912,'15690305912','2025-04-07 00:00:00.000',15690305912,'15690305912','2025-04-07 00:00:00.000'),
(20,193000000001000020,'FEE_TYPE_020','默认','DOMESTIC','产品附加费',1,1,1,1,1,NULL,NULL,'zhaoting','2025-04-02 00:00:00.000',NULL,'zhaoting','2025-04-02 00:00:00.000'),
(21,193000000001000021,'FEE_TYPE_021','默认','DOMESTIC','联单费',1,1,0,0,1,NULL,15268360352,'15268360352','2025-04-01 00:00:00.000',15268360352,'15268360352','2025-04-01 00:00:00.000'),
(22,193000000001000022,'FEE_TYPE_022','默认','DOMESTIC','竹木制品小额商检',1,1,0,0,1,NULL,15268360352,'15268360352','2025-04-01 00:00:00.000',15268360352,'15268360352','2025-04-01 00:00:00.000'),
(23,193000000001000023,'FEE_TYPE_023','默认','DOMESTIC','放单费',1,0,0,0,1,NULL,15268360352,'15268360352','2025-03-27 00:00:00.000',15268360352,'15268360352','2025-03-27 00:00:00.000'),
(24,193000000001000024,'FEE_TYPE_024','默认','DOMESTIC','退关费',1,1,1,1,1,NULL,15690305912,'15690305912','2025-03-17 00:00:00.000',15690305912,'15690305912','2025-03-17 00:00:00.000'),
(25,193000000001000025,'FEE_TYPE_025','默认','DOMESTIC','补收税金',0,1,0,0,0,NULL,NULL,'lijiayin','2025-03-07 00:00:00.000',NULL,'lijiayin','2025-03-07 00:00:00.000'),
(26,193000000001000026,'FEE_TYPE_026','默认','INTERNATIONAL','HC',1,1,1,1,1,NULL,15268360352,'15268360352','2025-02-27 00:00:00.000',15268360352,'15268360352','2025-02-27 00:00:00.000'),
(27,193000000001000027,'FEE_TYPE_027','默认','DOMESTIC','舱单数据传输费',1,1,1,1,1,NULL,15268360352,'15268360352','2025-02-27 00:00:00.000',15268360352,'15268360352','2025-02-27 00:00:00.000'),
(28,193000000001000028,'FEE_TYPE_028','默认','DOMESTIC','转船费',1,1,1,1,1,NULL,15268360352,'15268360352','2025-02-27 00:00:00.000',15268360352,'15268360352','2025-02-27 00:00:00.000'),
(29,193000000001000029,'FEE_TYPE_029','默认','DOMESTIC','改配费',1,1,1,1,1,NULL,15268360352,'15268360352','2025-02-27 00:00:00.000',15268360352,'15268360352','2025-02-27 00:00:00.000'),
(30,193000000001000030,'FEE_TYPE_030','默认','INTERNATIONAL','巴拿马附加费',1,1,1,1,1,NULL,15268360352,'15268360352','2025-02-21 00:00:00.000',15268360352,'15268360352','2025-02-21 00:00:00.000'),
(31,193000000001000031,'FEE_TYPE_031','默认','DOMESTIC','报关退点',1,1,0,0,1,NULL,15268360352,'15268360352','2025-02-12 00:00:00.000',15268360352,'15268360352','2025-02-12 00:00:00.000'),
(32,193000000001000032,'FEE_TYPE_032','默认','INTERNATIONAL','滞箱费',1,1,0,0,1,NULL,15268360352,'15268360352','2025-02-11 00:00:00.000',15268360352,'15268360352','2025-02-11 00:00:00.000'),
(33,193000000001000033,'FEE_TYPE_033','默认','DOMESTIC','大榭提',1,0,0,0,1,NULL,15268360352,'15268360352','2025-02-10 00:00:00.000',15268360352,'15268360352','2025-02-10 00:00:00.000'),
(34,193000000001000034,'FEE_TYPE_034','默认','DOMESTIC','进四期',1,0,0,0,1,NULL,15268360352,'15268360352','2025-02-10 00:00:00.000',15268360352,'15268360352','2025-02-10 00:00:00.000'),
(35,193000000001000035,'FEE_TYPE_035','默认','DOMESTIC','入仓费',1,1,1,1,1,NULL,NULL,'zhaoting','2025-01-22 00:00:00.000',NULL,'zhaoting','2025-01-22 00:00:00.000'),
(36,193000000001000036,'FEE_TYPE_036','默认','DOMESTIC','晚截关',1,0,0,0,0,NULL,15268360352,'15268360352','2025-01-16 00:00:00.000',15268360352,'15268360352','2025-01-16 00:00:00.000'),
(37,193000000001000037,'FEE_TYPE_037','默认','DOMESTIC','装柜费',0,1,0,0,1,NULL,15268360352,'15268360352','2024-12-14 00:00:00.000',15268360352,'15268360352','2024-12-14 00:00:00.000'),
(38,193000000001000038,'FEE_TYPE_038','默认','DOMESTIC','亏舱费',0,1,0,0,1,NULL,NULL,'zhaoting','2024-09-23 00:00:00.000',NULL,'zhaoting','2024-09-23 00:00:00.000'),
(39,193000000001000039,'FEE_TYPE_039','默认','INTERNATIONAL','暂扣费用',0,1,0,0,0,NULL,NULL,'lvhonglu','2024-08-08 00:00:00.000',NULL,'lvhonglu','2024-08-08 00:00:00.000'),
(40,193000000001000040,'FEE_TYPE_040','默认','DOMESTIC','改单费',1,1,0,0,1,NULL,NULL,'zhaoting','2024-07-30 00:00:00.000',NULL,'zhaoting','2024-07-30 00:00:00.000'),
(41,193000000001000041,'FEE_TYPE_041','默认','DOMESTIC','叉车费',1,1,0,0,0,NULL,NULL,'zhaoting','2024-07-03 00:00:00.000',NULL,'zhaoting','2024-07-03 00:00:00.000'),
(42,193000000001000042,'FEE_TYPE_042','默认','INTERNATIONAL','全包价',1,0,1,0,1,'提柜+派送',NULL,'huangmengting','2024-06-27 00:00:00.000',NULL,'huangmengting','2024-06-27 00:00:00.000'),
(43,193000000001000043,'FEE_TYPE_043','默认','INTERNATIONAL','快递费',1,0,0,0,0,'快递打单费用',NULL,'lvhonglu','2024-05-10 00:00:00.000',NULL,'lvhonglu','2024-05-10 00:00:00.000'),
(44,193000000001000044,'FEE_TYPE_044','默认','DOMESTIC','超偏远费',0,1,0,0,0,'海派超偏远地区收费(超偏远费+¥50/件)',NULL,'lijiayin','2024-05-10 00:00:00.000',NULL,'lijiayin','2024-05-10 00:00:00.000'),
(45,193000000001000045,'FEE_TYPE_045','默认','DOMESTIC','偏远费',0,1,0,0,0,'海派偏远地区收费(偏远费+¥30/件)',NULL,'lijiayin','2024-05-10 00:00:00.000',NULL,'lijiayin','2024-05-10 00:00:00.000'),
(46,193000000001000046,'FEE_TYPE_046','默认','DOMESTIC','超周长附加费',0,1,0,0,0,'(最短边+次长边)*2+最长边>260CM',NULL,'lijiayin','2024-05-10 00:00:00.000',NULL,'lijiayin','2024-05-10 00:00:00.000'),
(47,193000000001000047,'FEE_TYPE_047','默认','DOMESTIC','超重费',0,1,0,0,0,'海派单箱超22KG',NULL,'lijiayin','2024-05-10 00:00:00.000',NULL,'lijiayin','2024-05-10 00:00:00.000'),
(48,193000000001000048,'FEE_TYPE_048','默认','DOMESTIC','超长费',0,1,0,0,0,'箱子单边超120cm',NULL,'lijiayin','2024-05-10 00:00:00.000',NULL,'lijiayin','2024-05-10 00:00:00.000'),
(49,193000000001000049,'FEE_TYPE_049','默认','INTERNATIONAL','ISF',1,1,1,1,1,NULL,NULL,'lvhonglu','2023-07-21 00:00:00.000',NULL,'lvhonglu','2023-07-21 00:00:00.000'),
(50,193000000001000050,'FEE_TYPE_050','默认','INTERNATIONAL','国外查验费',1,1,0,0,0,NULL,NULL,'lvhonglu','2023-07-21 00:00:00.000',NULL,'lvhonglu','2023-07-21 00:00:00.000'),
(51,193000000001000051,'FEE_TYPE_051','默认','INTERNATIONAL','目的港关税',1,1,0,0,0,NULL,NULL,'lvhonglu','2023-07-21 00:00:00.000',NULL,'lvhonglu','2023-07-21 00:00:00.000'),
(52,193000000001000052,'FEE_TYPE_052','默认','DOMESTIC','停车费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(53,193000000001000053,'FEE_TYPE_053','默认','DOMESTIC','过磅费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(54,193000000001000054,'FEE_TYPE_054','默认','DOMESTIC','提箱费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(55,193000000001000055,'FEE_TYPE_055','默认','DOMESTIC','进港费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(56,193000000001000056,'FEE_TYPE_056','默认','DOMESTIC','保险费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(57,193000000001000057,'FEE_TYPE_057','默认','DOMESTIC','舱单费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(58,193000000001000058,'FEE_TYPE_058','默认','DOMESTIC','EDI',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(59,193000000001000059,'FEE_TYPE_059','默认','DOMESTIC','箱单费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(60,193000000001000060,'FEE_TYPE_060','默认','DOMESTIC','文件费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(61,193000000001000061,'FEE_TYPE_061','默认','DOMESTIC','THC',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(62,193000000001000062,'FEE_TYPE_062','默认','DOMESTIC','订舱费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(63,193000000001000063,'FEE_TYPE_063','默认','DOMESTIC','包干费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(64,193000000001000064,'FEE_TYPE_064','默认','DOMESTIC','换单费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(65,193000000001000065,'FEE_TYPE_065','默认','DOMESTIC','整柜运费',1,1,1,1,1,NULL,NULL,'lvhonglu','2023-07-13 00:00:00.000',NULL,'lvhonglu','2023-07-13 00:00:00.000'),
(66,193000000001000066,'FEE_TYPE_066','默认','DOMESTIC','查验费',1,1,1,1,0,'国外海外查验',NULL,'zhaoting','2023-02-16 00:00:00.000',NULL,'admin','2023-05-18 00:00:00.000'),
(67,193000000001000067,'FEE_TYPE_067','默认','DOMESTIC','FDA申报费',1,1,1,1,0,'FDA申报费',NULL,'zhaoting','2023-02-16 00:00:00.000',NULL,'admin','2023-05-18 00:00:00.000'),
(68,193000000001000068,'FEE_TYPE_068','默认','DOMESTIC','LACEY ACT申报费',1,1,1,1,0,'LACEY ACT申报费',NULL,'zhaoting','2023-02-16 00:00:00.000',NULL,'zhaoting','2023-02-16 00:00:00.000'),
(69,193000000001000069,'FEE_TYPE_069','默认','DOMESTIC','报关费',1,1,1,1,0,'退税报关',NULL,'zhaoting','2023-02-16 00:00:00.000',NULL,'zhaoting','2023-02-16 00:00:00.000'),
(70,193000000001000070,'FEE_TYPE_070','默认','DOMESTIC','提货费',1,1,1,1,0,'提货费',NULL,'zhaoting','2023-02-16 00:00:00.000',NULL,'admin','2023-07-06 00:00:00.000'),
(71,193000000001000071,'FEE_TYPE_071','默认','INTERNATIONAL','贴标费',1,0,1,1,0,'贴标费',NULL,'admin','2022-11-25 00:00:00.000',NULL,'admin','2023-07-05 00:00:00.000'),
(72,193000000001000072,'FEE_TYPE_072','默认','DOMESTIC','补运费',1,1,1,1,0,'移仓补运费',NULL,'admin','2022-11-23 00:00:00.000',NULL,'admin','2022-11-23 00:00:00.000'),
(73,193000000001000073,'FEE_TYPE_073','默认','DOMESTIC','其他扣费',1,1,1,1,0,'其他扣费',NULL,'admin','2022-11-23 00:00:00.000',NULL,'admin','2022-11-23 00:00:00.000'),
(74,193000000001000074,'FEE_TYPE_074','默认','DOMESTIC','抵扣费用',1,1,1,1,0,'应付-抵扣费用',NULL,'admin','2022-11-23 00:00:00.000',NULL,'admin','2022-11-23 00:00:00.000'),
(75,193000000001000075,'FEE_TYPE_075','默认','INTERNATIONAL','派送费',1,0,1,1,0,'派送费',NULL,'admin','2022-04-16 00:00:00.000',NULL,'admin','2023-07-05 00:00:00.000'),
(76,193000000001000076,'FEE_TYPE_076','默认','DOMESTIC','运费',1,1,1,1,0,'头程运费',NULL,'admin','2022-04-16 00:00:00.000',NULL,'admin','2023-07-06 00:00:00.000'),
(77,193000000001000077,'FEE_TYPE_077','默认','INTERNATIONAL','仓储费',1,0,1,1,0,'仓储费',NULL,'admin','2022-04-16 00:00:00.000',NULL,'admin','2023-07-05 00:00:00.000'),
(78,193000000001000078,'FEE_TYPE_078','集装箱费用','INTERNATIONAL','提柜-额外费用',1,1,1,1,1,NULL,NULL,'jinshanshan','2024-12-05 00:00:00.000',NULL,'jinshanshan','2024-12-05 00:00:00.000'),
(79,193000000001000079,'FEE_TYPE_079','集装箱费用','INTERNATIONAL','移柜费',1,1,1,1,1,NULL,NULL,'jinshanshan','2024-11-22 00:00:00.000',NULL,'jinshanshan','2024-11-22 00:00:00.000'),
(80,193000000001000080,'FEE_TYPE_080','集装箱费用','DOMESTIC','四期提',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-12 00:00:00.000',NULL,'yingchaoqun','2024-08-12 00:00:00.000'),
(81,193000000001000081,'FEE_TYPE_081','集装箱费用','DOMESTIC','进大榭',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-12 00:00:00.000',NULL,'yingchaoqun','2024-08-12 00:00:00.000'),
(82,193000000001000082,'FEE_TYPE_082','集装箱费用','DOMESTIC','过夜费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(83,193000000001000083,'FEE_TYPE_083','集装箱费用','DOMESTIC','搞定费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(84,193000000001000084,'FEE_TYPE_084','集装箱费用','DOMESTIC','梅山提',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(85,193000000001000085,'FEE_TYPE_085','集装箱费用','DOMESTIC','进梅山',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(86,193000000001000086,'FEE_TYPE_086','集装箱费用','DOMESTIC','舱单录入费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(87,193000000001000087,'FEE_TYPE_087','集装箱费用','DOMESTIC','进洋山',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(88,193000000001000088,'FEE_TYPE_088','集装箱费用','DOMESTIC','洋山提',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(89,193000000001000089,'FEE_TYPE_089','集装箱费用','DOMESTIC','落箱费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(90,193000000001000090,'FEE_TYPE_090','集装箱费用','DOMESTIC','预进港',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-09 00:00:00.000',NULL,'yingchaoqun','2024-08-09 00:00:00.000'),
(91,193000000001000091,'FEE_TYPE_091','集装箱费用','DOMESTIC','改运抵',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-09 00:00:00.000',NULL,'yingchaoqun','2024-08-09 00:00:00.000'),
(92,193000000001000092,'FEE_TYPE_092','集装箱费用','DOMESTIC','预提费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-08 00:00:00.000',NULL,'yingchaoqun','2024-08-08 00:00:00.000'),
(93,193000000001000093,'FEE_TYPE_093','集装箱费用','DOMESTIC','设备交接单',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-08 00:00:00.000',NULL,'yingchaoqun','2024-08-08 00:00:00.000'),
(94,193000000001000094,'FEE_TYPE_094','集装箱费用','DOMESTIC','操作费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-08 00:00:00.000',NULL,'yingchaoqun','2024-08-08 00:00:00.000'),
(95,193000000001000095,'FEE_TYPE_095','集装箱费用','DOMESTIC','VGM',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-08 00:00:00.000',NULL,'yingchaoqun','2024-08-08 00:00:00.000'),
(96,193000000001000096,'FEE_TYPE_096','集装箱费用','INTERNATIONAL','托盘费',1,0,0,0,1,NULL,NULL,'jinshanshan','2023-08-14 00:00:00.000',NULL,'jinshanshan','2023-08-14 00:00:00.000'),
(97,193000000001000097,'FEE_TYPE_097','集装箱费用','INTERNATIONAL','拆柜费',1,0,0,0,1,NULL,NULL,'jinshanshan','2023-08-14 00:00:00.000',NULL,'jinshanshan','2023-08-14 00:00:00.000'),
(98,193000000001000098,'FEE_TYPE_098','集装箱费用','DOMESTIC','加封费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-08-03 00:00:00.000',NULL,'yingchaoqun','2023-08-03 00:00:00.000'),
(99,193000000001000099,'FEE_TYPE_099','集装箱费用','DOMESTIC','电子锁',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-08-03 00:00:00.000',NULL,'yingchaoqun','2023-08-03 00:00:00.000'),
(100,193000000001000100,'FEE_TYPE_100','集装箱费用','DOMESTIC','吊机费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-19 00:00:00.000',NULL,'yingchaoqun','2023-07-19 00:00:00.000'),
(101,193000000001000101,'FEE_TYPE_101','集装箱费用','DOMESTIC','条码费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-19 00:00:00.000',NULL,'yingchaoqun','2023-07-19 00:00:00.000'),
(102,193000000001000102,'FEE_TYPE_102','集装箱费用','DOMESTIC','AMS',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-19 00:00:00.000',NULL,'yingchaoqun','2023-07-19 00:00:00.000'),
(103,193000000001000103,'FEE_TYPE_103','集装箱费用','DOMESTIC','海运费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-19 00:00:00.000',NULL,'yingchaoqun','2023-07-19 00:00:00.000'),
(104,193000000001000104,'FEE_TYPE_104','集装箱费用','DOMESTIC','堆存费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-19 00:00:00.000',NULL,'yingchaoqun','2023-07-19 00:00:00.000'),
(105,193000000001000105,'FEE_TYPE_105','集装箱费用','DOMESTIC','电子装箱单',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-19 00:00:00.000',NULL,'yingchaoqun','2023-07-19 00:00:00.000'),
(106,193000000001000106,'FEE_TYPE_106','集装箱费用','INTERNATIONAL','提柜费',1,1,1,1,0,'提柜费',NULL,'admin','2023-05-18 00:00:00.000',NULL,'admin','2023-05-18 00:00:00.000'),
(107,193000000001000107,'FEE_TYPE_107','集装箱费用','INTERNATIONAL','清关费',1,1,1,1,0,'清关费',NULL,'admin','2023-05-18 00:00:00.000',NULL,'admin','2023-05-18 00:00:00.000'),
(108,193000000001000108,'FEE_TYPE_108','集装箱费用','DOMESTIC','拖车费',1,1,1,1,0,'拖车费',NULL,'admin','2023-05-18 00:00:00.000',NULL,'admin','2023-07-06 00:00:00.000'),
(109,193000000001000109,'FEE_TYPE_109','集装箱费用','DOMESTIC','加载费',1,1,1,1,0,'加载费',NULL,'admin','2023-05-18 00:00:00.000',NULL,'admin','2023-05-18 00:00:00.000'),
(110,193000000001000110,'FEE_TYPE_110','BOL费用','INTERNATIONAL','装车费',1,1,1,1,1,NULL,NULL,'jinshanshan','2025-01-17 00:00:00.000',NULL,'jinshanshan','2025-01-17 00:00:00.000');
DROP PROCEDURE IF EXISTS unified_publish_assert_fee_item_preflight;
DELIMITER $$
CREATE PROCEDURE unified_publish_assert_fee_item_preflight()
BEGIN
IF (SELECT COUNT(*) FROM tmp_fee_item_seed) <> 110
OR (SELECT COUNT(*) FROM tmp_fee_item_seed WHERE trade_scope = 'DOMESTIC') <> 84
OR (SELECT COUNT(*) FROM tmp_fee_item_seed WHERE trade_scope = 'INTERNATIONAL') <> 26 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:Excel种子数量或国内国际范围不正确';
END IF;
IF EXISTS (SELECT fee_code FROM tmp_fee_item_seed GROUP BY fee_code HAVING COUNT(*) > 1)
OR EXISTS (SELECT trade_scope, fee_type_name FROM tmp_fee_item_seed GROUP BY trade_scope, fee_type_name HAVING COUNT(*) > 1) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:Excel种子编码或语义键重复';
END IF;
IF EXISTS (
SELECT f.id
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.id = f.id
WHERE f.tenant_id <> 0 OR f.fee_code <> s.fee_code
OR f.category <> s.category OR f.trade_scope <> s.trade_scope
OR f.fee_type_name <> s.fee_type_name
OR f.pickup_payable <> s.pickup_payable OR f.pickup_receivable <> s.pickup_receivable
OR f.warehouse_payable <> s.warehouse_payable OR f.warehouse_receivable <> s.warehouse_receivable
OR f.allocation_fee <> s.allocation_fee OR NOT (f.description <=> s.description)
) THEN
SELECT 'FIXED_ID_CONFLICT' AS conflict_type, f.id, f.tenant_id, f.fee_code,
f.trade_scope, f.fee_type_name
FROM fl_fee_item f JOIN tmp_fee_item_seed s ON s.id = f.id
WHERE f.tenant_id <> 0 OR f.fee_code <> s.fee_code
OR f.category <> s.category OR f.trade_scope <> s.trade_scope
OR f.fee_type_name <> s.fee_type_name
OR f.pickup_payable <> s.pickup_payable OR f.pickup_receivable <> s.pickup_receivable
OR f.warehouse_payable <> s.warehouse_payable OR f.warehouse_receivable <> s.warehouse_receivable
OR f.allocation_fee <> s.allocation_fee OR NOT (f.description <=> s.description);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:固定主键已被不同费用项目占用';
END IF;
IF EXISTS (
SELECT f.id
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.fee_code = f.fee_code
WHERE f.tenant_id = 0
AND (f.id <> s.id OR f.category <> s.category OR f.trade_scope <> s.trade_scope
OR f.fee_type_name <> s.fee_type_name OR f.pickup_payable <> s.pickup_payable
OR f.pickup_receivable <> s.pickup_receivable OR f.warehouse_payable <> s.warehouse_payable
OR f.warehouse_receivable <> s.warehouse_receivable OR f.allocation_fee <> s.allocation_fee
OR NOT (f.description <=> s.description) OR f.status <> 'ACTIVE' OR f.deleted <> 0)
) THEN
SELECT 'PUBLIC_CODE_CONFLICT' AS conflict_type, f.id, f.tenant_id, f.fee_code,
f.trade_scope, f.fee_type_name, s.id AS expected_id, s.fee_type_name AS expected_name
FROM fl_fee_item f JOIN tmp_fee_item_seed s ON s.fee_code = f.fee_code
WHERE f.tenant_id = 0
AND (f.id <> s.id OR f.category <> s.category OR f.trade_scope <> s.trade_scope
OR f.fee_type_name <> s.fee_type_name OR f.pickup_payable <> s.pickup_payable
OR f.pickup_receivable <> s.pickup_receivable OR f.warehouse_payable <> s.warehouse_payable
OR f.warehouse_receivable <> s.warehouse_receivable OR f.allocation_fee <> s.allocation_fee
OR NOT (f.description <=> s.description) OR f.status <> 'ACTIVE' OR f.deleted <> 0);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:平台费用编码存在语义冲突';
END IF;
IF EXISTS (
SELECT f.id
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.trade_scope = f.trade_scope AND s.fee_type_name = f.fee_type_name
WHERE f.tenant_id = 0 AND (f.fee_code <> s.fee_code OR f.category <> s.category
OR f.pickup_payable <> s.pickup_payable OR f.pickup_receivable <> s.pickup_receivable
OR f.warehouse_payable <> s.warehouse_payable OR f.warehouse_receivable <> s.warehouse_receivable
OR f.allocation_fee <> s.allocation_fee OR NOT (f.description <=> s.description))
) THEN
SELECT 'PUBLIC_SEMANTIC_CONFLICT' AS conflict_type, f.id, f.tenant_id, f.fee_code,
f.trade_scope, f.fee_type_name, s.fee_code AS expected_code
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.trade_scope = f.trade_scope AND s.fee_type_name = f.fee_type_name
WHERE f.tenant_id = 0 AND (f.fee_code <> s.fee_code OR f.category <> s.category
OR f.pickup_payable <> s.pickup_payable OR f.pickup_receivable <> s.pickup_receivable
OR f.warehouse_payable <> s.warehouse_payable OR f.warehouse_receivable <> s.warehouse_receivable
OR f.allocation_fee <> s.allocation_fee OR NOT (f.description <=> s.description));
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:平台费用语义键存在编码或字段冲突';
END IF;
IF EXISTS (SELECT 1 FROM fl_fee_item WHERE tenant_id = 0 AND (fee_code IS NULL OR TRIM(fee_code) = '')) THEN
SELECT 'PUBLIC_EMPTY_CODE' AS conflict_type, id, tenant_id, fee_code, trade_scope, fee_type_name
FROM fl_fee_item
WHERE tenant_id = 0 AND (fee_code IS NULL OR TRIM(fee_code) = '');
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:平台既有费用项目存在空编码';
END IF;
IF EXISTS (
SELECT f.id
FROM fl_fee_item f
LEFT JOIN tmp_fee_item_seed by_code ON by_code.fee_code = NULLIF(TRIM(f.fee_code), '')
LEFT JOIN tmp_fee_item_seed by_semantic ON by_semantic.trade_scope = f.trade_scope
AND by_semantic.fee_type_name = f.fee_type_name
WHERE f.tenant_id > 0 AND by_code.seed_no IS NULL AND by_semantic.seed_no IS NULL
) THEN
SELECT 'UNMAPPED_CUSTOMER_ITEM' AS conflict_type, f.id, f.tenant_id, f.fee_code,
f.trade_scope, f.fee_type_name
FROM fl_fee_item f
LEFT JOIN tmp_fee_item_seed by_code ON by_code.fee_code = NULLIF(TRIM(f.fee_code), '')
LEFT JOIN tmp_fee_item_seed by_semantic ON by_semantic.trade_scope = f.trade_scope
AND by_semantic.fee_type_name = f.fee_type_name
WHERE f.tenant_id > 0 AND by_code.seed_no IS NULL AND by_semantic.seed_no IS NULL;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:存在无法匹配公共种子的客户费用项目';
END IF;
IF EXISTS (
SELECT f.id
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.fee_code = NULLIF(TRIM(f.fee_code), '')
WHERE f.tenant_id > 0 AND (f.category <> s.category OR f.trade_scope <> s.trade_scope
OR f.fee_type_name <> s.fee_type_name OR f.pickup_payable <> s.pickup_payable
OR f.pickup_receivable <> s.pickup_receivable OR f.warehouse_payable <> s.warehouse_payable
OR f.warehouse_receivable <> s.warehouse_receivable OR f.allocation_fee <> s.allocation_fee
OR NOT (f.description <=> s.description))
) THEN
SELECT 'CUSTOMER_CODE_CONFLICT' AS conflict_type, f.id, f.tenant_id, f.fee_code,
f.trade_scope, f.fee_type_name, s.fee_type_name AS expected_name
FROM fl_fee_item f JOIN tmp_fee_item_seed s ON s.fee_code = NULLIF(TRIM(f.fee_code), '')
WHERE f.tenant_id > 0 AND (f.category <> s.category OR f.trade_scope <> s.trade_scope
OR f.fee_type_name <> s.fee_type_name OR f.pickup_payable <> s.pickup_payable
OR f.pickup_receivable <> s.pickup_receivable OR f.warehouse_payable <> s.warehouse_payable
OR f.warehouse_receivable <> s.warehouse_receivable OR f.allocation_fee <> s.allocation_fee
OR NOT (f.description <=> s.description));
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:客户费用编码存在语义冲突';
END IF;
IF EXISTS (
SELECT f.id
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.trade_scope = f.trade_scope AND s.fee_type_name = f.fee_type_name
WHERE f.tenant_id > 0 AND (NULLIF(TRIM(f.fee_code), '') IS NOT NULL
AND f.fee_code <> s.fee_code AND f.fee_code <> CONCAT('MANUAL_', f.id)
OR f.category <> s.category OR f.pickup_payable <> s.pickup_payable
OR f.pickup_receivable <> s.pickup_receivable OR f.warehouse_payable <> s.warehouse_payable
OR f.warehouse_receivable <> s.warehouse_receivable OR f.allocation_fee <> s.allocation_fee
OR NOT (f.description <=> s.description))
) THEN
SELECT 'CUSTOMER_SEMANTIC_CONFLICT' AS conflict_type, f.id, f.tenant_id, f.fee_code,
f.trade_scope, f.fee_type_name, s.fee_code AS expected_code
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.trade_scope = f.trade_scope AND s.fee_type_name = f.fee_type_name
WHERE f.tenant_id > 0 AND (NULLIF(TRIM(f.fee_code), '') IS NOT NULL
AND f.fee_code <> s.fee_code AND f.fee_code <> CONCAT('MANUAL_', f.id)
OR f.category <> s.category OR f.pickup_payable <> s.pickup_payable
OR f.pickup_receivable <> s.pickup_receivable OR f.warehouse_payable <> s.warehouse_payable
OR f.warehouse_receivable <> s.warehouse_receivable OR f.allocation_fee <> s.allocation_fee
OR NOT (f.description <=> s.description));
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:客户费用语义键存在编码或字段冲突';
END IF;
IF EXISTS (
SELECT d.id
FROM fl_inbound_fee_detail d
JOIN fl_fee_item f ON f.id = d.fee_item_id
WHERE d.tenant_id = 0 OR (f.tenant_id > 0 AND f.tenant_id <> d.tenant_id)
) THEN
SELECT 'CROSS_CUSTOMER_DETAIL_REFERENCE' AS conflict_type, d.id AS fee_detail_id,
d.tenant_id AS detail_customer_id, d.fee_item_id, f.tenant_id AS fee_item_scope
FROM fl_inbound_fee_detail d JOIN fl_fee_item f ON f.id = d.fee_item_id
WHERE d.tenant_id = 0 OR (f.tenant_id > 0 AND f.tenant_id <> d.tenant_id);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:发现跨客户或平台作用域费用明细引用';
END IF;
END$$
DELIMITER ;
CALL unified_publish_assert_fee_item_preflight();
DROP PROCEDURE IF EXISTS unified_publish_assert_fee_item_preflight;
DROP PROCEDURE IF EXISTS unified_publish_migrate_fee_items;
DELIMITER $$
CREATE PROCEDURE unified_publish_migrate_fee_items()
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
INSERT INTO fl_fee_item (id, tenant_id, fee_code, category, trade_scope, fee_type_name,
pickup_payable, pickup_receivable, warehouse_payable, warehouse_receivable, allocation_fee,
description, status, created_by, created_by_name, created_at, updated_by, updated_by_name, updated_at)
SELECT s.id, 0, s.fee_code, s.category, s.trade_scope, s.fee_type_name,
s.pickup_payable, s.pickup_receivable, s.warehouse_payable, s.warehouse_receivable, s.allocation_fee,
s.description, 'ACTIVE', s.created_by, s.created_by_name, s.created_at,
s.updated_by, s.updated_by_name, s.updated_at
FROM tmp_fee_item_seed s
WHERE NOT EXISTS (SELECT 1 FROM fl_fee_item f WHERE f.id = s.id);
DROP TEMPORARY TABLE IF EXISTS tmp_fee_item_map;
CREATE TEMPORARY TABLE tmp_fee_item_map (
old_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
public_id BIGINT UNSIGNED NOT NULL
) ENGINE=MEMORY;
INSERT INTO tmp_fee_item_map (old_id, public_id)
SELECT f.id, s.id
FROM fl_fee_item f
JOIN tmp_fee_item_seed s
ON (s.fee_code = NULLIF(TRIM(f.fee_code), '')
OR (s.trade_scope = f.trade_scope AND s.fee_type_name = f.fee_type_name))
WHERE f.tenant_id > 0;
UPDATE fl_inbound_fee_detail d
JOIN tmp_fee_item_map m ON m.old_id = d.fee_item_id
SET d.fee_item_id = m.public_id;
DELETE f FROM fl_fee_item f JOIN tmp_fee_item_map m ON m.old_id = f.id;
COMMIT;
END$$
DELIMITER ;
CALL unified_publish_migrate_fee_items();
DROP PROCEDURE IF EXISTS unified_publish_migrate_fee_items;
SET @fee_code_ddl = CASE
WHEN
(SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE()
AND table_name = 'fl_fee_item' AND index_name = 'uk_fee_item_code'
AND non_unique = 0 AND seq_in_index = 1 AND column_name = 'fee_code') = 1
AND (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE()
AND table_name = 'fl_fee_item' AND index_name = 'uk_fee_item_code') = 1
THEN 'SELECT 1'
WHEN EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
AND table_name = 'fl_fee_item' AND index_name = 'uk_fee_item_code')
THEN 'ALTER TABLE fl_fee_item DROP INDEX uk_fee_item_code, ADD UNIQUE KEY uk_fee_item_code (fee_code)'
ELSE 'ALTER TABLE fl_fee_item ADD UNIQUE KEY uk_fee_item_code (fee_code)'
END;
PREPARE statement FROM @fee_code_ddl;
EXECUTE statement;
DEALLOCATE PREPARE statement;
DROP TEMPORARY TABLE IF EXISTS tmp_fee_item_map;
DROP TEMPORARY TABLE IF EXISTS tmp_fee_item_seed;
SET @fee_code_ddl = IF(
(SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_fee_item'
AND column_name = 'fee_code' AND is_nullable = 'NO') = 1,
'SELECT 1',
'ALTER TABLE fl_fee_item MODIFY COLUMN fee_code VARCHAR(50) NOT NULL COMMENT ''稳定费用编码;平台公共主数据固定编码'' AFTER trade_scope'
);
PREPARE statement FROM @fee_code_ddl;
EXECUTE statement;
DEALLOCATE PREPARE statement;
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'source_type', CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'source_type',
"VARCHAR(32) NOT NULL DEFAULT 'MANUAL' COMMENT 'MANUAL或FBA_PRICING' AFTER fee_item_id"); "VARCHAR(32) NOT NULL DEFAULT 'MANUAL' COMMENT 'MANUAL或FBA_PRICING' AFTER fee_item_id");
CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'source_fee_id', CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'source_fee_id',
@@ -6465,7 +6840,7 @@ BEGIN
END$$ END$$
DELIMITER ; DELIMITER ;
CALL unified_publish_fee_add_index('fl_fee_item', 'uk_fee_item_code', CALL unified_publish_fee_add_index('fl_fee_item', 'uk_fee_item_code',
'UNIQUE KEY `uk_fee_item_code` (`tenant_id`, `fee_code`)'); 'UNIQUE KEY `uk_fee_item_code` (`fee_code`)');
CALL unified_publish_fee_add_index('fl_inbound_fee_detail', 'uk_inbound_fee_source', CALL unified_publish_fee_add_index('fl_inbound_fee_detail', 'uk_inbound_fee_source',
'UNIQUE KEY `uk_inbound_fee_source` (`tenant_id`, `source_type`, `source_fee_id`)'); 'UNIQUE KEY `uk_inbound_fee_source` (`tenant_id`, `source_type`, `source_fee_id`)');
CALL unified_publish_fee_add_index('fl_inbound_fee_detail', 'idx_inbound_fee_published', CALL unified_publish_fee_add_index('fl_inbound_fee_detail', 'idx_inbound_fee_published',
@@ -6518,10 +6893,15 @@ CREATE TABLE IF NOT EXISTS fl_loading_evidence (
tenant_id BIGINT UNSIGNED NOT NULL, tenant_id BIGINT UNSIGNED NOT NULL,
inbound_id BIGINT UNSIGNED NOT NULL, inbound_id BIGINT UNSIGNED NOT NULL,
evidence_type VARCHAR(32) NOT NULL COMMENT 'PHOTO_STEP_1..4/SEAL', evidence_type VARCHAR(32) NOT NULL COMMENT 'PHOTO_STEP_1..4/SEAL',
file_id BIGINT UNSIGNED NULL COMMENT 'File 服务逻辑文件ID', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File 服务逻辑文件ID',
file_name VARCHAR(255) NULL, file_name VARCHAR(255) NULL,
seal_no VARCHAR(64) NULL, seal_no VARCHAR(64) NULL,
remark VARCHAR(1000) NULL, remark VARCHAR(1000) NULL,
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE/VOID',
void_reason VARCHAR(500) NULL,
voided_by BIGINT UNSIGNED NULL,
voided_by_name VARCHAR(64) NULL,
voided_at DATETIME(3) NULL,
version INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0,
created_by BIGINT UNSIGNED NOT NULL, created_by BIGINT UNSIGNED NOT NULL,
created_by_name VARCHAR(64) NULL, created_by_name VARCHAR(64) NULL,
@@ -6530,8 +6910,8 @@ CREATE TABLE IF NOT EXISTS fl_loading_evidence (
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE KEY uk_loading_evidence_type (tenant_id, inbound_id, evidence_type), KEY idx_loading_evidence_type (tenant_id, inbound_id, evidence_type, status),
UNIQUE KEY uk_loading_evidence_seal (tenant_id, seal_no), KEY idx_loading_evidence_seal (tenant_id, seal_no, status),
KEY idx_loading_evidence_time (tenant_id, inbound_id, updated_at), KEY idx_loading_evidence_time (tenant_id, inbound_id, updated_at),
CONSTRAINT fk_loading_evidence_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id) CONSTRAINT fk_loading_evidence_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
) ENGINE=InnoDB COMMENT='订单装柜四步照片和铅封证据'; ) ENGINE=InnoDB COMMENT='订单装柜四步照片和铅封证据';
@@ -8623,23 +9003,57 @@ WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')
USE baiyue_firstleg; USE baiyue_firstleg;
-- Phase 3 loading evidence hardening for existing databases. -- Phase 3 loading evidence hardening for existing databases. Evidence is append-only:
-- multiple active/void records of the same type are allowed; voiding is an audit action.
USE baiyue_firstleg; USE baiyue_firstleg;
DROP PROCEDURE IF EXISTS phase3_loading_evidence_add_column;
DELIMITER $$
CREATE PROCEDURE phase3_loading_evidence_add_column(IN p_column VARCHAR(64), IN p_definition VARCHAR(255))
BEGIN
SET @ddl = IF(
EXISTS(SELECT 1 FROM information_schema.tables
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence')
AND NOT EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence' AND column_name=p_column),
CONCAT('ALTER TABLE fl_loading_evidence ADD COLUMN `', p_column, '` ', p_definition),
'SELECT 1');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
END$$
DELIMITER ;
CALL phase3_loading_evidence_add_column('status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''ACTIVE/VOID''');
CALL phase3_loading_evidence_add_column('void_reason', 'VARCHAR(500) NULL');
CALL phase3_loading_evidence_add_column('voided_by', 'BIGINT UNSIGNED NULL');
CALL phase3_loading_evidence_add_column('voided_by_name', 'VARCHAR(64) NULL');
CALL phase3_loading_evidence_add_column('voided_at', 'DATETIME(3) NULL');
DROP PROCEDURE IF EXISTS phase3_loading_evidence_add_column;
UPDATE fl_loading_evidence SET status='ACTIVE' WHERE status IS NULL OR status='';
UPDATE fl_loading_evidence SET seal_no=NULL WHERE evidence_type<>'SEAL' AND seal_no IS NOT NULL; UPDATE fl_loading_evidence SET seal_no=NULL WHERE evidence_type<>'SEAL' AND seal_no IS NOT NULL;
UPDATE fl_loading_evidence duplicate_evidence
JOIN (
SELECT tenant_id,seal_no,MIN(id) keep_id
FROM fl_loading_evidence
WHERE evidence_type='SEAL' AND seal_no IS NOT NULL AND seal_no<>''
GROUP BY tenant_id,seal_no HAVING COUNT(*)>1
) duplicates ON duplicates.tenant_id=duplicate_evidence.tenant_id
AND duplicates.seal_no=duplicate_evidence.seal_no AND duplicate_evidence.id<>duplicates.keep_id
SET duplicate_evidence.seal_no=NULL;
SET @ddl = IF( SET @ddl = IF(
EXISTS(SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence') EXISTS(SELECT 1 FROM information_schema.statistics
AND NOT EXISTS(SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence'
AND table_name='fl_loading_evidence' AND index_name='uk_loading_evidence_seal'), AND index_name='uk_loading_evidence_type'),
'ALTER TABLE fl_loading_evidence ADD UNIQUE KEY uk_loading_evidence_seal (tenant_id,seal_no)', 'ALTER TABLE fl_loading_evidence DROP INDEX uk_loading_evidence_type', 'SELECT 1');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence'
AND index_name='uk_loading_evidence_seal'),
'ALTER TABLE fl_loading_evidence DROP INDEX uk_loading_evidence_seal', 'SELECT 1');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(
NOT EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence'
AND index_name='idx_loading_evidence_type'),
'ALTER TABLE fl_loading_evidence ADD KEY idx_loading_evidence_type (tenant_id,inbound_id,evidence_type,status)',
'SELECT 1');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = IF(
NOT EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence'
AND index_name='idx_loading_evidence_seal'),
'ALTER TABLE fl_loading_evidence ADD KEY idx_loading_evidence_seal (tenant_id,seal_no,status)',
'SELECT 1'); 'SELECT 1');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -8792,10 +9206,28 @@ BEGIN
'fl_loading_pick_operation')) <> 18 THEN 'fl_loading_pick_operation')) <> 18 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:三期出仓、报关或船期表未完整创建'; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:三期出仓、报关或船期表未完整创建';
END IF; END IF;
IF (SELECT COUNT(*) FROM information_schema.statistics IF (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_evidence' WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_evidence'
AND index_name='uk_loading_evidence_seal' AND non_unique=0 AND column_name IN ('status','void_reason','voided_by','voided_by_name','voided_at')) <> 5
AND ((seq_in_index=1 AND column_name='tenant_id') OR (seq_in_index=2 AND column_name='seal_no'))) <> 2 OR (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_evidence'
AND index_name='uk_loading_evidence_type') <> 0
OR (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_evidence'
AND index_name='uk_loading_evidence_seal') <> 0
OR (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_evidence'
AND index_name='idx_loading_evidence_type' AND non_unique=1
AND ((seq_in_index=1 AND column_name='tenant_id')
OR (seq_in_index=2 AND column_name='inbound_id')
OR (seq_in_index=3 AND column_name='evidence_type')
OR (seq_in_index=4 AND column_name='status'))) <> 4
OR (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_evidence'
AND index_name='idx_loading_evidence_seal' AND non_unique=1
AND ((seq_in_index=1 AND column_name='tenant_id')
OR (seq_in_index=2 AND column_name='seal_no')
OR (seq_in_index=3 AND column_name='status'))) <> 3
OR (SELECT COUNT(*) FROM information_schema.statistics OR (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema='baiyue_firstleg' AND table_name='fl_outbound_dispatch' WHERE table_schema='baiyue_firstleg' AND table_name='fl_outbound_dispatch'
AND index_name='uk_outbound_dispatch_owner' AND non_unique=0) <> 3 AND index_name='uk_outbound_dispatch_owner' AND non_unique=0) <> 3
@@ -9625,6 +10057,62 @@ BEGIN
SIGNAL SQLSTATE '45000' SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:智能装柜权限 ID 或父子关系不正确'; SET MESSAGE_TEXT = '统一迁移失败:智能装柜权限 ID 或父子关系不正确';
END IF; END IF;
IF (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_fee_item'
AND column_name IN ('id', 'tenant_id', 'trade_scope', 'fee_code', 'fee_type_name')) <> 5
OR (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_fee_item'
AND column_name = 'fee_code' AND is_nullable = 'NO') <> 1 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:费用项目编码字段未完成兼容迁移';
END IF;
IF (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_fee_item'
AND index_name = 'uk_fee_item_code' AND non_unique = 0) <> 1
OR (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_fee_item'
AND index_name = 'uk_fee_item_code' AND non_unique = 0
AND seq_in_index = 1 AND column_name = 'fee_code') <> 1 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:费用项目编码唯一索引未创建';
END IF;
IF (SELECT COUNT(*) FROM baiyue_firstleg.fl_fee_item WHERE tenant_id <> 0) <> 0
OR (SELECT COUNT(*) FROM baiyue_firstleg.fl_fee_item
WHERE tenant_id = 0 AND status = 'ACTIVE' AND deleted = 0
AND id BETWEEN 193000000001000001 AND 193000000001000110
AND fee_code = CONCAT('FEE_TYPE_', LPAD(id - 193000000001000000, 3, '0'))) <> 110
OR EXISTS (
SELECT 1
FROM baiyue_firstleg.fl_inbound_fee_detail detail
JOIN baiyue_firstleg.fl_fee_item item ON item.id = detail.fee_item_id
WHERE detail.tenant_id = 0 OR item.tenant_id <> 0
) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:费用项目公共作用域、110条种子或客户明细引用不正确';
END IF;
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission
WHERE tenant_id = 0 AND app_code = 'PLATFORM' AND status = 'ACTIVE' AND deleted = 0
AND ((id = 5102070 AND permission_type = 'MENU' AND parent_id = 5103000
AND permission_code = 'firstleg:fee-item:view')
OR (id = 5102071 AND permission_type = 'BUTTON' AND parent_id = 5102070
AND permission_code = 'firstleg:fee-item:create')
OR (id = 5102072 AND permission_type = 'BUTTON' AND parent_id = 5102070
AND permission_code = 'firstleg:fee-item:edit')
OR (id = 5102073 AND permission_type = 'BUTTON' AND parent_id = 5102070
AND permission_code = 'firstleg:fee-item:delete'))) <> 4 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:费用项目权限父子关系未完整创建';
END IF;
IF (SELECT COUNT(DISTINCT p.permission_code)
FROM baiyue_iam.iam_role_permission rp
JOIN baiyue_iam.iam_role r ON r.id = rp.role_id AND r.tenant_id = rp.tenant_id
JOIN baiyue_iam.iam_permission p ON p.id = rp.permission_id AND p.tenant_id = rp.tenant_id
WHERE r.tenant_id = 0 AND r.app_code = 'PLATFORM' AND r.role_code = 'PLATFORM_ADMIN'
AND p.permission_code IN ('firstleg:fee-item:view', 'firstleg:fee-item:create',
'firstleg:fee-item:edit', 'firstleg:fee-item:delete')) <> 4 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:平台管理员未获得费用项目完整授权';
END IF;
END$$ END$$
DELIMITER ; DELIMITER ;
CALL assert_unified_final_schema(); CALL assert_unified_final_schema();
@@ -41,55 +41,51 @@ public class PlatformFeeItemController {
@Operation(summary = "查询费用项目") @Operation(summary = "查询费用项目")
public ApiResponse<PageResult<FeeItemEntity>> list( public ApiResponse<PageResult<FeeItemEntity>> list(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@RequestParam(required = false) String category, @RequestParam(required = false) String category,
@RequestParam(required = false) String tradeScope, @RequestParam(required = false) String tradeScope,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String status, @RequestParam(required = false) String status,
@RequestParam(defaultValue = "1") @Min(1) long page, @RequestParam(defaultValue = "1") @Min(1) long page,
@RequestParam(defaultValue = "20") @Min(1) @Max(200) long size) { @RequestParam(defaultValue = "20") @Min(1) @Max(200) long size) {
return ApiResponse.success(service.list(FirstlegScopeGuard.platform(user, accountId), FirstlegScopeGuard.platform(user);
category, tradeScope, keyword, status, page, size)); return ApiResponse.success(service.list(category, tradeScope, keyword, status, page, size));
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("hasAuthority('firstleg:fee-item:view')") @PreAuthorize("hasAuthority('firstleg:fee-item:view')")
public ApiResponse<FeeItemEntity> detail(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<FeeItemEntity> detail(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long id) { @PathVariable long id) {
return ApiResponse.success(service.detail(FirstlegScopeGuard.platform(user, accountId), id)); FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.detail(id));
} }
@PostMapping @PostMapping
@BusinessAudit(event = "FEE_ITEM_CREATE", resourceType = "FEE_ITEM", description = "新增费用项目", captureBefore = false) @BusinessAudit(event = "FEE_ITEM_CREATE", resourceType = "FEE_ITEM", description = "新增费用项目", captureBefore = false)
@PreAuthorize("hasAuthority('firstleg:fee-item:create')") @PreAuthorize("hasAuthority('firstleg:fee-item:create')")
public ApiResponse<FeeItemEntity> create(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<FeeItemEntity> create(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@Valid @RequestBody SaveRequest request) { @Valid @RequestBody SaveRequest request) {
long scope = FirstlegScopeGuard.platform(user, accountId); FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.create(scope, user.userId(), user.username(), request)); return ApiResponse.success(service.create(user.userId(), user.username(), request));
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@BusinessAudit(event = "FEE_ITEM_UPDATE", resourceType = "FEE_ITEM", resourceId = "#id", description = "编辑费用项目") @BusinessAudit(event = "FEE_ITEM_UPDATE", resourceType = "FEE_ITEM", resourceId = "#id", description = "编辑费用项目")
@PreAuthorize("hasAuthority('firstleg:fee-item:edit')") @PreAuthorize("hasAuthority('firstleg:fee-item:edit')")
public ApiResponse<FeeItemEntity> update(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<FeeItemEntity> update(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long id, @PathVariable long id,
@Valid @RequestBody SaveRequest request) { @Valid @RequestBody SaveRequest request) {
long scope = FirstlegScopeGuard.platform(user, accountId); FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.update(scope, id, user.userId(), user.username(), request)); return ApiResponse.success(service.update(id, user.userId(), user.username(), request));
} }
@PatchMapping("/{id}/status") @PatchMapping("/{id}/status")
@BusinessAudit(event = "FEE_ITEM_STATUS_CHANGE", resourceType = "FEE_ITEM", resourceId = "#id", description = "启停费用项目") @BusinessAudit(event = "FEE_ITEM_STATUS_CHANGE", resourceType = "FEE_ITEM", resourceId = "#id", description = "启停费用项目")
@PreAuthorize("hasAuthority('firstleg:fee-item:edit')") @PreAuthorize("hasAuthority('firstleg:fee-item:edit')")
public ApiResponse<Void> changeStatus(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<Void> changeStatus(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long id, @PathVariable long id,
@Valid @RequestBody StatusRequest request) { @Valid @RequestBody StatusRequest request) {
service.changeStatus(FirstlegScopeGuard.platform(user, accountId), id, FirstlegScopeGuard.platform(user);
user.userId(), user.username(), request.status()); service.changeStatus(id, user.userId(), user.username(), request.status());
return ApiResponse.success(null); return ApiResponse.success(null);
} }
@@ -97,9 +93,9 @@ public class PlatformFeeItemController {
@BusinessAudit(event = "FEE_ITEM_DELETE", resourceType = "FEE_ITEM", resourceId = "#id", description = "删除费用项目") @BusinessAudit(event = "FEE_ITEM_DELETE", resourceType = "FEE_ITEM", resourceId = "#id", description = "删除费用项目")
@PreAuthorize("hasAuthority('firstleg:fee-item:delete')") @PreAuthorize("hasAuthority('firstleg:fee-item:delete')")
public ApiResponse<Void> delete(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<Void> delete(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long id) { @PathVariable long id) {
service.delete(FirstlegScopeGuard.platform(user, accountId), id); FirstlegScopeGuard.platform(user);
service.delete(id);
return ApiResponse.success(null); return ApiResponse.success(null);
} }
} }
@@ -13,6 +13,7 @@ public final class FeeItemDtos {
public record SaveRequest( public record SaveRequest(
@NotBlank @Size(max = 64) String category, @NotBlank @Size(max = 64) String category,
@NotBlank @Pattern(regexp = "DOMESTIC|INTERNATIONAL") String tradeScope, @NotBlank @Pattern(regexp = "DOMESTIC|INTERNATIONAL") String tradeScope,
@Size(max = 50) @Pattern(regexp = "[A-Za-z0-9_-]{1,50}") String feeCode,
@NotBlank @Size(max = 128) String feeTypeName, @NotBlank @Size(max = 128) String feeTypeName,
@NotNull Boolean pickupPayable, @NotNull Boolean pickupPayable,
@NotNull Boolean pickupReceivable, @NotNull Boolean pickupReceivable,
@@ -21,6 +22,14 @@ public final class FeeItemDtos {
@NotNull Boolean allocationFee, @NotNull Boolean allocationFee,
@Size(max = 500) String description @Size(max = 500) String description
) { ) {
/** Backward-compatible constructor for callers predating the stable fee code. */
public SaveRequest(String category, String tradeScope, String feeTypeName,
Boolean pickupPayable, Boolean pickupReceivable,
Boolean warehousePayable, Boolean warehouseReceivable,
Boolean allocationFee, String description) {
this(category, tradeScope, null, feeTypeName, pickupPayable, pickupReceivable,
warehousePayable, warehouseReceivable, allocationFee, description);
}
} }
public record StatusRequest( public record StatusRequest(
@@ -27,7 +27,7 @@ public interface InboundSplitSettlementMapper {
inbound_code AS inboundCode, inbound_role AS inboundRole, inbound_code AS inboundCode, inbound_role AS inboundRole,
root_inbound_id AS rootInboundId, root_inbound_id AS rootInboundId,
process_status AS processStatus, total_package_count AS totalPackageCount, process_status AS processStatus, total_package_count AS totalPackageCount,
package_count AS packageCount, version package_count AS packageCount, split_status AS splitStatus, version
FROM fl_inbound_order FROM fl_inbound_order
WHERE id = #{inboundId} AND deleted = 0 WHERE id = #{inboundId} AND deleted = 0
FOR UPDATE FOR UPDATE
@@ -158,7 +158,8 @@ public interface InboundSplitSettlementMapper {
@Param("expectedVersion") int expectedVersion); @Param("expectedVersion") int expectedVersion);
@Update("UPDATE fl_inbound_order SET package_count=0,split_status='FULLY_SPLIT',updated_by=#{operatorId},version=version+1 " @Update("UPDATE fl_inbound_order SET package_count=0,split_status='FULLY_SPLIT',updated_by=#{operatorId},version=version+1 "
+ "WHERE id=#{parentInboundId} AND tenant_id=#{accountId} AND inbound_role='MASTER' AND deleted=0 AND version=#{expectedVersion}") + "WHERE id=#{parentInboundId} AND tenant_id=#{accountId} AND inbound_role='MASTER' AND deleted=0 "
+ "AND process_status IN ('INBOUNDED','LOADING') AND version=#{expectedVersion}")
int markRootSplit(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId, int markRootSplit(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId,
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion); @Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
@@ -269,11 +270,19 @@ public interface InboundSplitSettlementMapper {
record ParentRow(Long id, Long tenantId, Long orderId, String orderNo, record ParentRow(Long id, Long tenantId, Long orderId, String orderNo,
String inboundCode, String inboundRole, Long rootInboundId, String inboundCode, String inboundRole, Long rootInboundId,
String processStatus, Integer totalPackageCount, Integer packageCount, String processStatus, Integer totalPackageCount, Integer packageCount,
Integer version) { String splitStatus, Integer version) {
public ParentRow(Long id, Long tenantId, String inboundCode, String inboundRole, public ParentRow(Long id, Long tenantId, String inboundCode, String inboundRole,
String processStatus, Integer totalPackageCount, Integer packageCount, Integer version) { String processStatus, Integer totalPackageCount, Integer packageCount, Integer version) {
this(id, tenantId, 901L, "ORDER-001", inboundCode, inboundRole, id, this(id, tenantId, 901L, "ORDER-001", inboundCode, inboundRole, id,
processStatus, totalPackageCount, packageCount, version); processStatus, totalPackageCount, packageCount, "NOT_SPLIT", version);
}
public ParentRow(Long id, Long tenantId, Long orderId, String orderNo,
String inboundCode, String inboundRole, Long rootInboundId,
String processStatus, Integer totalPackageCount, Integer packageCount,
Integer version) {
this(id, tenantId, orderId, orderNo, inboundCode, inboundRole, rootInboundId,
processStatus, totalPackageCount, packageCount, "NOT_SPLIT", version);
} }
} }
@@ -20,6 +20,7 @@ import java.util.List;
/** 费用项目与入库费用明细仓储。 */ /** 费用项目与入库费用明细仓储。 */
@Repository @Repository
public class FeeItemRepository { public class FeeItemRepository {
private static final long PLATFORM_TENANT_ID = 0L;
@Resource @Resource
private FeeItemMapper feeItems; private FeeItemMapper feeItems;
@Resource @Resource
@@ -27,11 +28,11 @@ public class FeeItemRepository {
@Resource @Resource
private InboundOrderMapper inboundOrders; private InboundOrderMapper inboundOrders;
public PageResult<FeeItemEntity> listItems(long tenantId, String category, String tradeScope, public PageResult<FeeItemEntity> listItems(String category, String tradeScope,
String keyword, String status, long page, long size) { String keyword, String status, long page, long size) {
return PageResult.from(feeItems.selectPage(new Page<>(page, size), return PageResult.from(feeItems.selectPage(new Page<>(page, size),
new LambdaQueryWrapper<FeeItemEntity>() new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId) .eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(StringUtils.hasText(category), FeeItemEntity::getCategory, category) .eq(StringUtils.hasText(category), FeeItemEntity::getCategory, category)
.eq(StringUtils.hasText(tradeScope), FeeItemEntity::getTradeScope, tradeScope) .eq(StringUtils.hasText(tradeScope), FeeItemEntity::getTradeScope, tradeScope)
.like(StringUtils.hasText(keyword), FeeItemEntity::getFeeTypeName, keyword) .like(StringUtils.hasText(keyword), FeeItemEntity::getFeeTypeName, keyword)
@@ -41,45 +42,53 @@ public class FeeItemRepository {
.orderByDesc(FeeItemEntity::getId))); .orderByDesc(FeeItemEntity::getId)));
} }
public FeeItemEntity findItem(long tenantId, long id) { public FeeItemEntity findItem(long id) {
return feeItems.selectOne(new LambdaQueryWrapper<FeeItemEntity>() return feeItems.selectOne(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId) .eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getId, id)); .eq(FeeItemEntity::getId, id));
} }
public FeeItemEntity findItemByCode(long tenantId, String feeCode) { public FeeItemEntity findItemByCode(String feeCode) {
return feeItems.selectOne(new LambdaQueryWrapper<FeeItemEntity>() return feeItems.selectOne(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId) .eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getFeeCode, feeCode)); .eq(FeeItemEntity::getFeeCode, feeCode));
} }
public boolean itemNameExists(long tenantId, String tradeScope, String feeTypeName, Long excludedId) { public boolean itemNameExists(String tradeScope, String feeTypeName, Long excludedId) {
return feeItems.selectCount(new LambdaQueryWrapper<FeeItemEntity>() return feeItems.selectCount(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId) .eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getTradeScope, tradeScope) .eq(FeeItemEntity::getTradeScope, tradeScope)
.eq(FeeItemEntity::getFeeTypeName, feeTypeName) .eq(FeeItemEntity::getFeeTypeName, feeTypeName)
.ne(excludedId != null, FeeItemEntity::getId, excludedId)) > 0; .ne(excludedId != null, FeeItemEntity::getId, excludedId)) > 0;
} }
public boolean itemCodeExists(String feeCode, Long excludedId) {
return feeItems.selectCount(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getFeeCode, feeCode)
.ne(excludedId != null, FeeItemEntity::getId, excludedId)) > 0;
}
public void insertItem(FeeItemEntity item) { public void insertItem(FeeItemEntity item) {
item.setTenantId(PLATFORM_TENANT_ID);
feeItems.insert(item); feeItems.insert(item);
} }
public int updateItem(FeeItemEntity item) { public int updateItem(FeeItemEntity item) {
item.setTenantId(PLATFORM_TENANT_ID);
return feeItems.update(item, new LambdaUpdateWrapper<FeeItemEntity>() return feeItems.update(item, new LambdaUpdateWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, item.getTenantId()) .eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getId, item.getId())); .eq(FeeItemEntity::getId, item.getId()));
} }
public int deleteItem(long tenantId, long id) { public int deleteItem(long id) {
return feeItems.delete(new LambdaQueryWrapper<FeeItemEntity>() return feeItems.delete(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId) .eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getId, id)); .eq(FeeItemEntity::getId, id));
} }
public long countBindings(long tenantId, long feeItemId) { public long countBindings(long feeItemId) {
return feeDetails.selectCount(new LambdaQueryWrapper<InboundFeeDetailEntity>() return feeDetails.selectCount(new LambdaQueryWrapper<InboundFeeDetailEntity>()
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
.eq(InboundFeeDetailEntity::getFeeItemId, feeItemId)); .eq(InboundFeeDetailEntity::getFeeItemId, feeItemId));
} }
@@ -7,54 +7,69 @@ import com.baiyue.firstleg.dto.FeeItemDtos.SaveRequest;
import com.baiyue.firstleg.entity.FeeItemEntity; import com.baiyue.firstleg.entity.FeeItemEntity;
import com.baiyue.firstleg.repository.FeeItemRepository; import com.baiyue.firstleg.repository.FeeItemRepository;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
/** 费用项目主数据应用服务。 */ /** 费用项目主数据应用服务。 */
@Service @Service
public class FeeItemService { public class FeeItemService {
private static final long PLATFORM_TENANT_ID = 0L;
@Resource @Resource
private FeeItemRepository repository; private FeeItemRepository repository;
public PageResult<FeeItemEntity> list(long tenantId, String category, String tradeScope, public PageResult<FeeItemEntity> list(String category, String tradeScope,
String keyword, String status, long page, long size) { String keyword, String status, long page, long size) {
return repository.listItems(tenantId, category, tradeScope, keyword, status, page, size); return repository.listItems(category, tradeScope, keyword, status, page, size);
} }
public FeeItemEntity detail(long tenantId, long id) { public FeeItemEntity detail(long id) {
return required(tenantId, id); return required(id);
} }
@Transactional @Transactional
public FeeItemEntity create(long tenantId, long operatorId, String operatorName, SaveRequest request) { public FeeItemEntity create(long operatorId, String operatorName, SaveRequest request) {
ensureUnique(tenantId, request, null); ensureUnique(request, null);
FeeItemEntity value = new FeeItemEntity(); FeeItemEntity value = new FeeItemEntity();
value.setTenantId(tenantId); value.setTenantId(PLATFORM_TENANT_ID);
copy(value, request); copy(value, request);
if (value.getFeeCode() == null || value.getFeeCode().isBlank()) {
value.setFeeCode("MANUAL_" + UUID.randomUUID().toString().replace("-", "").substring(0, 24));
}
value.setStatus("ACTIVE"); value.setStatus("ACTIVE");
value.setCreatedBy(operatorId); value.setCreatedBy(operatorId);
value.setUpdatedBy(operatorId); value.setUpdatedBy(operatorId);
value.setCreatedByName(operatorName); value.setCreatedByName(operatorName);
value.setUpdatedByName(operatorName); value.setUpdatedByName(operatorName);
repository.insertItem(value); try {
repository.insertItem(value);
} catch (DuplicateKeyException exception) {
throw conflict("费用项目编码已存在");
}
return value; return value;
} }
@Transactional @Transactional
public FeeItemEntity update(long tenantId, long id, long operatorId, String operatorName, public FeeItemEntity update(long id, long operatorId, String operatorName,
SaveRequest request) { SaveRequest request) {
FeeItemEntity value = required(tenantId, id); FeeItemEntity value = required(id);
ensureUnique(tenantId, request, id); ensureUnique(request, id);
copy(value, request); copy(value, request);
value.setUpdatedBy(operatorId); value.setUpdatedBy(operatorId);
value.setUpdatedByName(operatorName); value.setUpdatedByName(operatorName);
if (repository.updateItem(value) != 1) throw conflict("费用项目已被其他人修改,请刷新后重试"); try {
return required(tenantId, id); if (repository.updateItem(value) != 1) throw conflict("费用项目已被其他人修改,请刷新后重试");
} catch (DuplicateKeyException exception) {
throw conflict("费用项目编码已存在");
}
return required(id);
} }
@Transactional @Transactional
public void changeStatus(long tenantId, long id, long operatorId, String operatorName, String status) { public void changeStatus(long id, long operatorId, String operatorName, String status) {
FeeItemEntity value = required(tenantId, id); FeeItemEntity value = required(id);
value.setStatus(status); value.setStatus(status);
value.setUpdatedBy(operatorId); value.setUpdatedBy(operatorId);
value.setUpdatedByName(operatorName); value.setUpdatedByName(operatorName);
@@ -62,22 +77,26 @@ public class FeeItemService {
} }
@Transactional @Transactional
public void delete(long tenantId, long id) { public void delete(long id) {
required(tenantId, id); required(id);
if (repository.countBindings(tenantId, id) > 0) { if (repository.countBindings(id) > 0) {
throw conflict("费用项目已被入库费用明细使用,只能停用,不能删除"); throw conflict("费用项目已被入库费用明细使用,只能停用,不能删除");
} }
if (repository.deleteItem(tenantId, id) != 1) throw conflict("费用项目删除冲突"); if (repository.deleteItem(id) != 1) throw conflict("费用项目删除冲突");
} }
private FeeItemEntity required(long tenantId, long id) { private FeeItemEntity required(long id) {
FeeItemEntity value = repository.findItem(tenantId, id); FeeItemEntity value = repository.findItem(id);
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "费用项目不存在"); if (value == null) throw new BizException(ResultCode.NOT_FOUND, "费用项目不存在");
return value; return value;
} }
private void ensureUnique(long tenantId, SaveRequest request, Long excludedId) { private void ensureUnique(SaveRequest request, Long excludedId) {
if (repository.itemNameExists(tenantId, request.tradeScope(), request.feeTypeName(), excludedId)) { if (request.feeCode() != null && !request.feeCode().isBlank()
&& repository.itemCodeExists(request.feeCode().trim().toUpperCase(), excludedId)) {
throw conflict("费用项目编码已存在");
}
if (repository.itemNameExists(request.tradeScope(), request.feeTypeName(), excludedId)) {
throw conflict("相同国内国际范围下的费用类型名称已存在"); throw conflict("相同国内国际范围下的费用类型名称已存在");
} }
} }
@@ -85,6 +104,8 @@ public class FeeItemService {
private static void copy(FeeItemEntity value, SaveRequest request) { private static void copy(FeeItemEntity value, SaveRequest request) {
value.setCategory(request.category().trim()); value.setCategory(request.category().trim());
value.setTradeScope(request.tradeScope()); value.setTradeScope(request.tradeScope());
value.setFeeCode(request.feeCode() == null || request.feeCode().isBlank()
? value.getFeeCode() : request.feeCode().trim().toUpperCase());
value.setFeeTypeName(request.feeTypeName().trim()); value.setFeeTypeName(request.feeTypeName().trim());
value.setPickupPayable(request.pickupPayable()); value.setPickupPayable(request.pickupPayable());
value.setPickupReceivable(request.pickupReceivable()); value.setPickupReceivable(request.pickupReceivable());
@@ -22,6 +22,7 @@ import java.util.List;
/** 入库单人工费用明细应用服务。 */ /** 入库单人工费用明细应用服务。 */
@Service @Service
public class InboundFeeDetailService { public class InboundFeeDetailService {
private static final long PLATFORM_TENANT_ID = 0L;
private static final String FREIGHT_FEE_CODE = "FBA_HEAD_FREIGHT"; private static final String FREIGHT_FEE_CODE = "FBA_HEAD_FREIGHT";
@Resource @Resource
private FeeItemRepository repository; private FeeItemRepository repository;
@@ -56,7 +57,7 @@ public class InboundFeeDetailService {
throw conflict("只有已发送状态的计费快照才能生成客户运费明细"); throw conflict("只有已发送状态的计费快照才能生成客户运费明细");
} }
LocalDateTime now = publishedAt == null ? LocalDateTime.now() : publishedAt; LocalDateTime now = publishedAt == null ? LocalDateTime.now() : publishedAt;
FeeItemEntity feeItem = requiredFreightItem(tenantId, operatorId, operatorName); FeeItemEntity feeItem = requiredFreightItem(operatorId, operatorName);
InboundFeeItemEntity baseItem = pricingRepository.findBaseItem(tenantId, feeId); InboundFeeItemEntity baseItem = pricingRepository.findBaseItem(tenantId, feeId);
repository.supersedePublishedFreight(tenantId, inboundId, feeId, operatorId, now); repository.supersedePublishedFreight(tenantId, inboundId, feeId, operatorId, now);
@@ -123,7 +124,7 @@ public class InboundFeeDetailService {
public InboundFeeDetailEntity create(long tenantId, long inboundId, long operatorId, public InboundFeeDetailEntity create(long tenantId, long inboundId, long operatorId,
SaveRequest request) { SaveRequest request) {
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId); InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
FeeItemEntity item = requiredActiveItem(tenantId, request.feeItemId()); FeeItemEntity item = requiredActiveItem(request.feeItemId());
InboundFeeDetailEntity value = new InboundFeeDetailEntity(); InboundFeeDetailEntity value = new InboundFeeDetailEntity();
value.setTenantId(tenantId); value.setTenantId(tenantId);
value.setInboundId(inboundId); value.setInboundId(inboundId);
@@ -142,7 +143,7 @@ public class InboundFeeDetailService {
long operatorId, SaveRequest request) { long operatorId, SaveRequest request) {
InboundFeeDetailEntity value = requiredEditable(tenantId, inboundId, detailId); InboundFeeDetailEntity value = requiredEditable(tenantId, inboundId, detailId);
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId); InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
FeeItemEntity item = requiredActiveItem(tenantId, request.feeItemId()); FeeItemEntity item = requiredActiveItem(request.feeItemId());
apply(value, inbound, item, request, operatorId); apply(value, inbound, item, request, operatorId);
value.setReviewStatus("PENDING"); value.setReviewStatus("PENDING");
value.setReviewerId(null); value.setReviewerId(null);
@@ -185,21 +186,21 @@ public class InboundFeeDetailService {
return value; return value;
} }
private FeeItemEntity requiredActiveItem(long tenantId, long itemId) { private FeeItemEntity requiredActiveItem(long itemId) {
FeeItemEntity value = repository.findItem(tenantId, itemId); FeeItemEntity value = repository.findItem(itemId);
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "费用项目不存在"); if (value == null) throw new BizException(ResultCode.NOT_FOUND, "费用项目不存在");
if (!"ACTIVE".equals(value.getStatus())) throw conflict("费用项目已停用,不能绑定"); if (!"ACTIVE".equals(value.getStatus())) throw conflict("费用项目已停用,不能绑定");
return value; return value;
} }
private FeeItemEntity requiredFreightItem(long tenantId, long operatorId, String operatorName) { private FeeItemEntity requiredFreightItem(long operatorId, String operatorName) {
FeeItemEntity item = repository.findItemByCode(tenantId, FREIGHT_FEE_CODE); FeeItemEntity item = repository.findItemByCode(FREIGHT_FEE_CODE);
if (item != null) { if (item != null) {
if (!"ACTIVE".equals(item.getStatus())) throw conflict("系统费用项目FBA头程运费已停用"); if (!"ACTIVE".equals(item.getStatus())) throw conflict("系统费用项目FBA头程运费已停用");
return item; return item;
} }
FeeItemEntity created = new FeeItemEntity(); FeeItemEntity created = new FeeItemEntity();
created.setTenantId(tenantId); created.setTenantId(PLATFORM_TENANT_ID);
created.setCategory("头程运费"); created.setCategory("头程运费");
created.setTradeScope("INTERNATIONAL"); created.setTradeScope("INTERNATIONAL");
created.setFeeCode(FREIGHT_FEE_CODE); created.setFeeCode(FREIGHT_FEE_CODE);
@@ -219,8 +220,13 @@ public class InboundFeeDetailService {
repository.insertItem(created); repository.insertItem(created);
return created; return created;
} catch (DuplicateKeyException exception) { } catch (DuplicateKeyException exception) {
FeeItemEntity concurrent = repository.findItemByCode(tenantId, FREIGHT_FEE_CODE); FeeItemEntity concurrent = repository.findItemByCode(FREIGHT_FEE_CODE);
if (concurrent != null) return concurrent; if (concurrent != null) {
if (!"ACTIVE".equals(concurrent.getStatus())) {
throw conflict("系统费用项目FBA头程运费已停用");
}
return concurrent;
}
throw exception; throw exception;
} }
} }
@@ -98,7 +98,8 @@ public class PdaPickSettlementService {
ParentRow parent = rootInboundId == parentInboundId ? root : splits.lockParent(parentInboundId); ParentRow parent = rootInboundId == parentInboundId ? root : splits.lockParent(parentInboundId);
if (parent == null || parent.tenantId() == null || parent.tenantId() <= 0 if (parent == null || parent.tenantId() == null || parent.tenantId() <= 0
|| !("MASTER".equals(parent.inboundRole()) || "CHILD".equals(parent.inboundRole())) || !("MASTER".equals(parent.inboundRole()) || "CHILD".equals(parent.inboundRole()))
|| !"INBOUNDED".equals(parent.processStatus())) { || !("INBOUNDED".equals(parent.processStatus()) || "LOADING".equals(parent.processStatus()))
|| ("MASTER".equals(parent.inboundRole()) && "FULLY_SPLIT".equals(parent.splitStatus()))) {
throw conflict("待拆分主入库单不在可拆分状态"); throw conflict("待拆分主入库单不在可拆分状态");
} }
String operationId = "PDA-SPLIT-" + taskId + '-' + parentInboundId; String operationId = "PDA-SPLIT-" + taskId + '-' + parentInboundId;
@@ -109,7 +110,7 @@ public class PdaPickSettlementService {
long loadingPlanId = task.getLoadingPlanId(); long loadingPlanId = task.getLoadingPlanId();
Long containerBookingId = splits.containerBookingId(loadingPlanId); Long containerBookingId = splits.containerBookingId(loadingPlanId);
if (containerBookingId == null) throw conflict("PDA任务装柜计划缺少真实柜次"); if (containerBookingId == null) throw conflict("PDA任务装柜计划缺少真实柜次");
int parentAvailable = count(parent.packageCount(), "主入库单可用箱数"); int parentAvailable = availablePackageCount(parent);
if (pickedPackageCount <= 0 || pickedPackageCount > parentAvailable) { if (pickedPackageCount <= 0 || pickedPackageCount > parentAvailable) {
throw conflict("本次已拣箱数与主入库单可用箱数不一致"); throw conflict("本次已拣箱数与主入库单可用箱数不一致");
} }
@@ -457,6 +458,14 @@ public class PdaPickSettlementService {
return value; return value;
} }
private static int availablePackageCount(ParentRow parent) {
if ("FULLY_SPLIT".equals(parent.splitStatus())) return 0;
Integer available = parent.packageCount();
if (available != null && available > 0) return available;
if (available != null && available == 0 && !"NOT_SPLIT".equals(parent.splitStatus())) return 0;
return count(parent.totalPackageCount(), "主入库单可用箱数");
}
private static void requireAffected(int affected, String resource) { private static void requireAffected(int affected, String resource) {
if (affected != 1) { if (affected != 1) {
throw conflict(resource + "已被并发修改,请刷新后重试"); throw conflict(resource + "已被并发修改,请刷新后重试");
@@ -3,9 +3,10 @@ SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS fl_fee_item ( CREATE TABLE IF NOT EXISTS fl_fee_item (
id BIGINT UNSIGNED NOT NULL COMMENT '费用项目ID', id BIGINT UNSIGNED NOT NULL COMMENT '费用项目ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '当前客户ID(客户隔离键);物理字段名沿用 tenant_id,当前不表示上级租户', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共费用项目作用域,固定为0;物理字段名沿用 tenant_id',
category VARCHAR(64) NOT NULL COMMENT '费用分类', category VARCHAR(64) NOT NULL COMMENT '费用分类',
trade_scope VARCHAR(20) NOT NULL COMMENT 'DOMESTIC国内、INTERNATIONAL国际', trade_scope VARCHAR(20) NOT NULL COMMENT 'DOMESTIC国内、INTERNATIONAL国际',
fee_code VARCHAR(50) NOT NULL COMMENT '稳定费用编码;平台公共主数据固定编码',
fee_type_name VARCHAR(128) NOT NULL COMMENT '费用类型名称', fee_type_name VARCHAR(128) NOT NULL COMMENT '费用类型名称',
pickup_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应付是否参与', pickup_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应付是否参与',
pickup_receivable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应收是否参与', pickup_receivable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应收是否参与',
@@ -23,13 +24,14 @@ CREATE TABLE IF NOT EXISTS fl_fee_item (
deleted TINYINT(1) NOT NULL DEFAULT 0, deleted TINYINT(1) NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE KEY uk_fee_item_code (fee_code),
KEY idx_fee_item_query (tenant_id, trade_scope, category, status, deleted), KEY idx_fee_item_query (tenant_id, trade_scope, category, status, deleted),
KEY idx_fee_item_name (tenant_id, fee_type_name, deleted) KEY idx_fee_item_name (tenant_id, fee_type_name, deleted)
) ENGINE=InnoDB COMMENT='费用项目主数据'; ) ENGINE=InnoDB COMMENT='平台公共费用项目主数据';
CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail ( CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail (
id BIGINT UNSIGNED NOT NULL COMMENT '入库费用明细ID', id BIGINT UNSIGNED NOT NULL COMMENT '入库费用明细ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '当前客户ID(客户隔离键);物理字段名沿用 tenant_id,当前不表示上级租户', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID(客户隔离键);费用明细引用平台公共费用项目',
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '绑定费用项目ID', fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '绑定费用项目ID',
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照', inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
@@ -71,22 +73,23 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail (
CONSTRAINT fk_inbound_fee_detail_item FOREIGN KEY (fee_item_id) REFERENCES fl_fee_item (id) CONSTRAINT fk_inbound_fee_detail_item FOREIGN KEY (fee_item_id) REFERENCES fl_fee_item (id)
) ENGINE=InnoDB COMMENT='入库单应收应付费用明细'; ) ENGINE=InnoDB COMMENT='入库单应收应付费用明细';
-- 既有表同步字段注释:tenant_id 当前承载客户 ID,不是上级租户 ID。 -- 费用项目是平台公共主数据;费用明细仍按客户隔离。
SET @fee_tenant_comment = '当前客户ID(客户隔离键);物理字段名沿用 tenant_id,当前不表示上级租户'; SET @fee_tenant_comment = '平台公共费用项目作用域,固定为0;物理字段名沿用 tenant_id';
SET @ddl = IF( SET @ddl = IF(
(SELECT COUNT(*) FROM information_schema.columns (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_fee_item' WHERE table_schema = DATABASE() AND table_name = 'fl_fee_item'
AND column_name = 'tenant_id' AND column_comment = @fee_tenant_comment) = 1, AND column_name = 'tenant_id' AND column_comment = @fee_tenant_comment) = 1,
'SELECT 1', 'SELECT 1',
'ALTER TABLE fl_fee_item MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL COMMENT ''当前客户ID(客户隔离键);物理字段名沿用 tenant_id,当前不表示上级租户''' 'ALTER TABLE fl_fee_item MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL COMMENT ''平台公共费用项目作用域,固定为0;物理字段名沿用 tenant_id'''
); );
PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement; PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement;
SET @fee_tenant_comment = '客户ID(客户隔离键);费用明细引用平台公共费用项目';
SET @ddl = IF( SET @ddl = IF(
(SELECT COUNT(*) FROM information_schema.columns (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_fee_detail' WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_fee_detail'
AND column_name = 'tenant_id' AND column_comment = @fee_tenant_comment) = 1, AND column_name = 'tenant_id' AND column_comment = @fee_tenant_comment) = 1,
'SELECT 1', 'SELECT 1',
'ALTER TABLE fl_inbound_fee_detail MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL COMMENT ''当前客户ID(客户隔离键);物理字段名沿用 tenant_id,当前不表示上级租户''' 'ALTER TABLE fl_inbound_fee_detail MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL COMMENT ''客户ID(客户隔离键);费用明细引用平台公共费用项目'''
); );
PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement; PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement;
@@ -22,6 +22,379 @@ DELIMITER ;
CALL phase2_publish_add_column('fl_fee_item', 'fee_code', CALL phase2_publish_add_column('fl_fee_item', 'fee_code',
"VARCHAR(50) NULL COMMENT '稳定费用编码;系统费用使用固定编码' AFTER trade_scope"); "VARCHAR(50) NULL COMMENT '稳定费用编码;系统费用使用固定编码' AFTER trade_scope");
ALTER TABLE fl_fee_item
MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共费用项目作用域,固定为0;物理字段名沿用 tenant_id',
COMMENT = '平台公共费用项目主数据';
ALTER TABLE fl_inbound_fee_detail
MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID(客户隔离键);费用明细引用平台公共费用项目';
-- 费用类型.xls(SHA-256 395C9CA71816E79C7745E224E63CF79A4AB782140364F63C8374B396DF38CDB4)
-- 单表费用类型:110条(国内84、国际26);源表无编码列,按受控行序生成稳定编码。
DROP TEMPORARY TABLE IF EXISTS tmp_fee_item_seed;
CREATE TEMPORARY TABLE tmp_fee_item_seed (
seed_no INT NOT NULL PRIMARY KEY,
id BIGINT UNSIGNED NOT NULL,
fee_code VARCHAR(50) NOT NULL,
category VARCHAR(64) NOT NULL,
trade_scope VARCHAR(20) NOT NULL,
fee_type_name VARCHAR(128) NOT NULL,
pickup_payable TINYINT(1) NOT NULL,
pickup_receivable TINYINT(1) NOT NULL,
warehouse_payable TINYINT(1) NOT NULL,
warehouse_receivable TINYINT(1) NOT NULL,
allocation_fee TINYINT(1) NOT NULL,
description VARCHAR(500) NULL,
created_by BIGINT NULL,
created_by_name VARCHAR(64) NULL,
created_at DATETIME(3) NOT NULL,
updated_by BIGINT NULL,
updated_by_name VARCHAR(64) NULL,
updated_at DATETIME(3) NOT NULL,
UNIQUE KEY uk_tmp_fee_seed_code (fee_code),
UNIQUE KEY uk_tmp_fee_seed_semantic (trade_scope, fee_type_name)
) ENGINE=MEMORY;
INSERT INTO tmp_fee_item_seed (seed_no, id, fee_code, category, trade_scope, fee_type_name, pickup_payable, pickup_receivable, warehouse_payable, warehouse_receivable, allocation_fee, description, created_by, created_by_name, created_at, updated_by, updated_by_name, updated_at) VALUES
(1,193000000001000001,'FEE_TYPE_001','洛杉矶仓储成本','INTERNATIONAL','拖车费2',0,0,1,0,0,NULL,NULL,'admin','2023-07-10 00:00:00.000',NULL,'admin','2023-07-10 00:00:00.000'),
(2,193000000001000002,'FEE_TYPE_002','洛杉矶仓储成本','INTERNATIONAL','仓库水电费',0,0,1,0,0,NULL,NULL,'admin','2023-07-08 00:00:00.000',NULL,'admin','2023-07-08 00:00:00.000'),
(3,193000000001000003,'FEE_TYPE_003','洛杉矶仓储成本','INTERNATIONAL','人工费',0,0,1,0,0,'人工工资费用',NULL,'admin','2023-07-08 00:00:00.000',NULL,'admin','2023-07-08 00:00:00.000'),
(4,193000000001000004,'FEE_TYPE_004','洛杉矶仓储成本','INTERNATIONAL','仓库租赁费',0,0,1,0,0,'应付 仓库租赁费',NULL,'admin','2023-07-08 00:00:00.000',NULL,'admin','2023-07-08 00:00:00.000'),
(5,193000000001000005,'FEE_TYPE_005','默认','DOMESTIC','删单费',1,1,0,0,1,NULL,15268360352,'15268360352','2026-05-29 00:00:00.000',15268360352,'15268360352','2026-05-29 00:00:00.000'),
(6,193000000001000006,'FEE_TYPE_006','默认','INTERNATIONAL','换FBA标费用',1,1,1,1,1,NULL,NULL,'zhaoting','2026-03-04 00:00:00.000',NULL,'zhaoting','2026-03-04 00:00:00.000'),
(7,193000000001000007,'FEE_TYPE_007','默认','INTERNATIONAL','换SKU费用',1,1,1,1,1,NULL,NULL,'zhaoting','2026-03-04 00:00:00.000',NULL,'zhaoting','2026-03-04 00:00:00.000'),
(8,193000000001000008,'FEE_TYPE_008','默认','INTERNATIONAL','开箱费',0,1,1,1,1,NULL,NULL,'zhaoting','2026-03-04 00:00:00.000',NULL,'zhaoting','2026-03-04 00:00:00.000'),
(9,193000000001000009,'FEE_TYPE_009','默认','DOMESTIC','天翔八提',1,1,0,0,1,NULL,15268360352,'15268360352','2025-08-19 00:00:00.000',15268360352,'15268360352','2025-08-19 00:00:00.000'),
(10,193000000001000010,'FEE_TYPE_010','默认','DOMESTIC','天翔五提',1,1,0,0,1,NULL,15268360352,'15268360352','2025-08-05 00:00:00.000',15268360352,'15268360352','2025-08-05 00:00:00.000'),
(11,193000000001000011,'FEE_TYPE_011','默认','DOMESTIC','荣盛三提',1,1,0,0,1,NULL,15268360352,'15268360352','2025-08-05 00:00:00.000',15268360352,'15268360352','2025-08-05 00:00:00.000'),
(12,193000000001000012,'FEE_TYPE_012','默认','DOMESTIC','两地费',1,1,0,0,1,NULL,15268360352,'15268360352','2025-08-04 00:00:00.000',15268360352,'15268360352','2025-08-04 00:00:00.000'),
(13,193000000001000013,'FEE_TYPE_013','默认','DOMESTIC','绕路费',1,1,0,0,1,NULL,15268360352,'15268360352','2025-08-04 00:00:00.000',15268360352,'15268360352','2025-08-04 00:00:00.000'),
(14,193000000001000014,'FEE_TYPE_014','默认','DOMESTIC','朝华提',1,1,0,0,1,NULL,15268360352,'15268360352','2025-07-31 00:00:00.000',15268360352,'15268360352','2025-07-31 00:00:00.000'),
(15,193000000001000015,'FEE_TYPE_015','默认','DOMESTIC','长胜提',1,1,0,0,1,NULL,15268360352,'15268360352','2025-07-31 00:00:00.000',15268360352,'15268360352','2025-07-31 00:00:00.000'),
(16,193000000001000016,'FEE_TYPE_016','默认','DOMESTIC','港杂费',1,1,0,0,1,NULL,15268360352,'15268360352','2025-07-24 00:00:00.000',15268360352,'15268360352','2025-07-24 00:00:00.000'),
(17,193000000001000017,'FEE_TYPE_017','默认','DOMESTIC','极偏远费',1,1,1,1,1,NULL,NULL,'zhaoting','2025-06-11 00:00:00.000',NULL,'zhaoting','2025-06-11 00:00:00.000'),
(18,193000000001000018,'FEE_TYPE_018','默认','DOMESTIC','退舱费',1,0,0,0,0,NULL,15690305912,'15690305912','2025-05-22 00:00:00.000',15690305912,'15690305912','2025-05-22 00:00:00.000'),
(19,193000000001000019,'FEE_TYPE_019','默认','DOMESTIC','推送费',1,1,0,0,0,NULL,15690305912,'15690305912','2025-04-07 00:00:00.000',15690305912,'15690305912','2025-04-07 00:00:00.000'),
(20,193000000001000020,'FEE_TYPE_020','默认','DOMESTIC','产品附加费',1,1,1,1,1,NULL,NULL,'zhaoting','2025-04-02 00:00:00.000',NULL,'zhaoting','2025-04-02 00:00:00.000'),
(21,193000000001000021,'FEE_TYPE_021','默认','DOMESTIC','联单费',1,1,0,0,1,NULL,15268360352,'15268360352','2025-04-01 00:00:00.000',15268360352,'15268360352','2025-04-01 00:00:00.000'),
(22,193000000001000022,'FEE_TYPE_022','默认','DOMESTIC','竹木制品小额商检',1,1,0,0,1,NULL,15268360352,'15268360352','2025-04-01 00:00:00.000',15268360352,'15268360352','2025-04-01 00:00:00.000'),
(23,193000000001000023,'FEE_TYPE_023','默认','DOMESTIC','放单费',1,0,0,0,1,NULL,15268360352,'15268360352','2025-03-27 00:00:00.000',15268360352,'15268360352','2025-03-27 00:00:00.000'),
(24,193000000001000024,'FEE_TYPE_024','默认','DOMESTIC','退关费',1,1,1,1,1,NULL,15690305912,'15690305912','2025-03-17 00:00:00.000',15690305912,'15690305912','2025-03-17 00:00:00.000'),
(25,193000000001000025,'FEE_TYPE_025','默认','DOMESTIC','补收税金',0,1,0,0,0,NULL,NULL,'lijiayin','2025-03-07 00:00:00.000',NULL,'lijiayin','2025-03-07 00:00:00.000'),
(26,193000000001000026,'FEE_TYPE_026','默认','INTERNATIONAL','HC',1,1,1,1,1,NULL,15268360352,'15268360352','2025-02-27 00:00:00.000',15268360352,'15268360352','2025-02-27 00:00:00.000'),
(27,193000000001000027,'FEE_TYPE_027','默认','DOMESTIC','舱单数据传输费',1,1,1,1,1,NULL,15268360352,'15268360352','2025-02-27 00:00:00.000',15268360352,'15268360352','2025-02-27 00:00:00.000'),
(28,193000000001000028,'FEE_TYPE_028','默认','DOMESTIC','转船费',1,1,1,1,1,NULL,15268360352,'15268360352','2025-02-27 00:00:00.000',15268360352,'15268360352','2025-02-27 00:00:00.000'),
(29,193000000001000029,'FEE_TYPE_029','默认','DOMESTIC','改配费',1,1,1,1,1,NULL,15268360352,'15268360352','2025-02-27 00:00:00.000',15268360352,'15268360352','2025-02-27 00:00:00.000'),
(30,193000000001000030,'FEE_TYPE_030','默认','INTERNATIONAL','巴拿马附加费',1,1,1,1,1,NULL,15268360352,'15268360352','2025-02-21 00:00:00.000',15268360352,'15268360352','2025-02-21 00:00:00.000'),
(31,193000000001000031,'FEE_TYPE_031','默认','DOMESTIC','报关退点',1,1,0,0,1,NULL,15268360352,'15268360352','2025-02-12 00:00:00.000',15268360352,'15268360352','2025-02-12 00:00:00.000'),
(32,193000000001000032,'FEE_TYPE_032','默认','INTERNATIONAL','滞箱费',1,1,0,0,1,NULL,15268360352,'15268360352','2025-02-11 00:00:00.000',15268360352,'15268360352','2025-02-11 00:00:00.000'),
(33,193000000001000033,'FEE_TYPE_033','默认','DOMESTIC','大榭提',1,0,0,0,1,NULL,15268360352,'15268360352','2025-02-10 00:00:00.000',15268360352,'15268360352','2025-02-10 00:00:00.000'),
(34,193000000001000034,'FEE_TYPE_034','默认','DOMESTIC','进四期',1,0,0,0,1,NULL,15268360352,'15268360352','2025-02-10 00:00:00.000',15268360352,'15268360352','2025-02-10 00:00:00.000'),
(35,193000000001000035,'FEE_TYPE_035','默认','DOMESTIC','入仓费',1,1,1,1,1,NULL,NULL,'zhaoting','2025-01-22 00:00:00.000',NULL,'zhaoting','2025-01-22 00:00:00.000'),
(36,193000000001000036,'FEE_TYPE_036','默认','DOMESTIC','晚截关',1,0,0,0,0,NULL,15268360352,'15268360352','2025-01-16 00:00:00.000',15268360352,'15268360352','2025-01-16 00:00:00.000'),
(37,193000000001000037,'FEE_TYPE_037','默认','DOMESTIC','装柜费',0,1,0,0,1,NULL,15268360352,'15268360352','2024-12-14 00:00:00.000',15268360352,'15268360352','2024-12-14 00:00:00.000'),
(38,193000000001000038,'FEE_TYPE_038','默认','DOMESTIC','亏舱费',0,1,0,0,1,NULL,NULL,'zhaoting','2024-09-23 00:00:00.000',NULL,'zhaoting','2024-09-23 00:00:00.000'),
(39,193000000001000039,'FEE_TYPE_039','默认','INTERNATIONAL','暂扣费用',0,1,0,0,0,NULL,NULL,'lvhonglu','2024-08-08 00:00:00.000',NULL,'lvhonglu','2024-08-08 00:00:00.000'),
(40,193000000001000040,'FEE_TYPE_040','默认','DOMESTIC','改单费',1,1,0,0,1,NULL,NULL,'zhaoting','2024-07-30 00:00:00.000',NULL,'zhaoting','2024-07-30 00:00:00.000'),
(41,193000000001000041,'FEE_TYPE_041','默认','DOMESTIC','叉车费',1,1,0,0,0,NULL,NULL,'zhaoting','2024-07-03 00:00:00.000',NULL,'zhaoting','2024-07-03 00:00:00.000'),
(42,193000000001000042,'FEE_TYPE_042','默认','INTERNATIONAL','全包价',1,0,1,0,1,'提柜+派送',NULL,'huangmengting','2024-06-27 00:00:00.000',NULL,'huangmengting','2024-06-27 00:00:00.000'),
(43,193000000001000043,'FEE_TYPE_043','默认','INTERNATIONAL','快递费',1,0,0,0,0,'快递打单费用',NULL,'lvhonglu','2024-05-10 00:00:00.000',NULL,'lvhonglu','2024-05-10 00:00:00.000'),
(44,193000000001000044,'FEE_TYPE_044','默认','DOMESTIC','超偏远费',0,1,0,0,0,'海派超偏远地区收费(超偏远费+¥50/件)',NULL,'lijiayin','2024-05-10 00:00:00.000',NULL,'lijiayin','2024-05-10 00:00:00.000'),
(45,193000000001000045,'FEE_TYPE_045','默认','DOMESTIC','偏远费',0,1,0,0,0,'海派偏远地区收费(偏远费+¥30/件)',NULL,'lijiayin','2024-05-10 00:00:00.000',NULL,'lijiayin','2024-05-10 00:00:00.000'),
(46,193000000001000046,'FEE_TYPE_046','默认','DOMESTIC','超周长附加费',0,1,0,0,0,'(最短边+次长边)*2+最长边>260CM',NULL,'lijiayin','2024-05-10 00:00:00.000',NULL,'lijiayin','2024-05-10 00:00:00.000'),
(47,193000000001000047,'FEE_TYPE_047','默认','DOMESTIC','超重费',0,1,0,0,0,'海派单箱超22KG',NULL,'lijiayin','2024-05-10 00:00:00.000',NULL,'lijiayin','2024-05-10 00:00:00.000'),
(48,193000000001000048,'FEE_TYPE_048','默认','DOMESTIC','超长费',0,1,0,0,0,'箱子单边超120cm',NULL,'lijiayin','2024-05-10 00:00:00.000',NULL,'lijiayin','2024-05-10 00:00:00.000'),
(49,193000000001000049,'FEE_TYPE_049','默认','INTERNATIONAL','ISF',1,1,1,1,1,NULL,NULL,'lvhonglu','2023-07-21 00:00:00.000',NULL,'lvhonglu','2023-07-21 00:00:00.000'),
(50,193000000001000050,'FEE_TYPE_050','默认','INTERNATIONAL','国外查验费',1,1,0,0,0,NULL,NULL,'lvhonglu','2023-07-21 00:00:00.000',NULL,'lvhonglu','2023-07-21 00:00:00.000'),
(51,193000000001000051,'FEE_TYPE_051','默认','INTERNATIONAL','目的港关税',1,1,0,0,0,NULL,NULL,'lvhonglu','2023-07-21 00:00:00.000',NULL,'lvhonglu','2023-07-21 00:00:00.000'),
(52,193000000001000052,'FEE_TYPE_052','默认','DOMESTIC','停车费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(53,193000000001000053,'FEE_TYPE_053','默认','DOMESTIC','过磅费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(54,193000000001000054,'FEE_TYPE_054','默认','DOMESTIC','提箱费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(55,193000000001000055,'FEE_TYPE_055','默认','DOMESTIC','进港费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(56,193000000001000056,'FEE_TYPE_056','默认','DOMESTIC','保险费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(57,193000000001000057,'FEE_TYPE_057','默认','DOMESTIC','舱单费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(58,193000000001000058,'FEE_TYPE_058','默认','DOMESTIC','EDI',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(59,193000000001000059,'FEE_TYPE_059','默认','DOMESTIC','箱单费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(60,193000000001000060,'FEE_TYPE_060','默认','DOMESTIC','文件费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(61,193000000001000061,'FEE_TYPE_061','默认','DOMESTIC','THC',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(62,193000000001000062,'FEE_TYPE_062','默认','DOMESTIC','订舱费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(63,193000000001000063,'FEE_TYPE_063','默认','DOMESTIC','包干费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(64,193000000001000064,'FEE_TYPE_064','默认','DOMESTIC','换单费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-14 00:00:00.000',NULL,'yingchaoqun','2023-07-14 00:00:00.000'),
(65,193000000001000065,'FEE_TYPE_065','默认','DOMESTIC','整柜运费',1,1,1,1,1,NULL,NULL,'lvhonglu','2023-07-13 00:00:00.000',NULL,'lvhonglu','2023-07-13 00:00:00.000'),
(66,193000000001000066,'FEE_TYPE_066','默认','DOMESTIC','查验费',1,1,1,1,0,'国外海外查验',NULL,'zhaoting','2023-02-16 00:00:00.000',NULL,'admin','2023-05-18 00:00:00.000'),
(67,193000000001000067,'FEE_TYPE_067','默认','DOMESTIC','FDA申报费',1,1,1,1,0,'FDA申报费',NULL,'zhaoting','2023-02-16 00:00:00.000',NULL,'admin','2023-05-18 00:00:00.000'),
(68,193000000001000068,'FEE_TYPE_068','默认','DOMESTIC','LACEY ACT申报费',1,1,1,1,0,'LACEY ACT申报费',NULL,'zhaoting','2023-02-16 00:00:00.000',NULL,'zhaoting','2023-02-16 00:00:00.000'),
(69,193000000001000069,'FEE_TYPE_069','默认','DOMESTIC','报关费',1,1,1,1,0,'退税报关',NULL,'zhaoting','2023-02-16 00:00:00.000',NULL,'zhaoting','2023-02-16 00:00:00.000'),
(70,193000000001000070,'FEE_TYPE_070','默认','DOMESTIC','提货费',1,1,1,1,0,'提货费',NULL,'zhaoting','2023-02-16 00:00:00.000',NULL,'admin','2023-07-06 00:00:00.000'),
(71,193000000001000071,'FEE_TYPE_071','默认','INTERNATIONAL','贴标费',1,0,1,1,0,'贴标费',NULL,'admin','2022-11-25 00:00:00.000',NULL,'admin','2023-07-05 00:00:00.000'),
(72,193000000001000072,'FEE_TYPE_072','默认','DOMESTIC','补运费',1,1,1,1,0,'移仓补运费',NULL,'admin','2022-11-23 00:00:00.000',NULL,'admin','2022-11-23 00:00:00.000'),
(73,193000000001000073,'FEE_TYPE_073','默认','DOMESTIC','其他扣费',1,1,1,1,0,'其他扣费',NULL,'admin','2022-11-23 00:00:00.000',NULL,'admin','2022-11-23 00:00:00.000'),
(74,193000000001000074,'FEE_TYPE_074','默认','DOMESTIC','抵扣费用',1,1,1,1,0,'应付-抵扣费用',NULL,'admin','2022-11-23 00:00:00.000',NULL,'admin','2022-11-23 00:00:00.000'),
(75,193000000001000075,'FEE_TYPE_075','默认','INTERNATIONAL','派送费',1,0,1,1,0,'派送费',NULL,'admin','2022-04-16 00:00:00.000',NULL,'admin','2023-07-05 00:00:00.000'),
(76,193000000001000076,'FEE_TYPE_076','默认','DOMESTIC','运费',1,1,1,1,0,'头程运费',NULL,'admin','2022-04-16 00:00:00.000',NULL,'admin','2023-07-06 00:00:00.000'),
(77,193000000001000077,'FEE_TYPE_077','默认','INTERNATIONAL','仓储费',1,0,1,1,0,'仓储费',NULL,'admin','2022-04-16 00:00:00.000',NULL,'admin','2023-07-05 00:00:00.000'),
(78,193000000001000078,'FEE_TYPE_078','集装箱费用','INTERNATIONAL','提柜-额外费用',1,1,1,1,1,NULL,NULL,'jinshanshan','2024-12-05 00:00:00.000',NULL,'jinshanshan','2024-12-05 00:00:00.000'),
(79,193000000001000079,'FEE_TYPE_079','集装箱费用','INTERNATIONAL','移柜费',1,1,1,1,1,NULL,NULL,'jinshanshan','2024-11-22 00:00:00.000',NULL,'jinshanshan','2024-11-22 00:00:00.000'),
(80,193000000001000080,'FEE_TYPE_080','集装箱费用','DOMESTIC','四期提',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-12 00:00:00.000',NULL,'yingchaoqun','2024-08-12 00:00:00.000'),
(81,193000000001000081,'FEE_TYPE_081','集装箱费用','DOMESTIC','进大榭',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-12 00:00:00.000',NULL,'yingchaoqun','2024-08-12 00:00:00.000'),
(82,193000000001000082,'FEE_TYPE_082','集装箱费用','DOMESTIC','过夜费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(83,193000000001000083,'FEE_TYPE_083','集装箱费用','DOMESTIC','搞定费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(84,193000000001000084,'FEE_TYPE_084','集装箱费用','DOMESTIC','梅山提',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(85,193000000001000085,'FEE_TYPE_085','集装箱费用','DOMESTIC','进梅山',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(86,193000000001000086,'FEE_TYPE_086','集装箱费用','DOMESTIC','舱单录入费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(87,193000000001000087,'FEE_TYPE_087','集装箱费用','DOMESTIC','进洋山',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(88,193000000001000088,'FEE_TYPE_088','集装箱费用','DOMESTIC','洋山提',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(89,193000000001000089,'FEE_TYPE_089','集装箱费用','DOMESTIC','落箱费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-10 00:00:00.000',NULL,'yingchaoqun','2024-08-10 00:00:00.000'),
(90,193000000001000090,'FEE_TYPE_090','集装箱费用','DOMESTIC','预进港',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-09 00:00:00.000',NULL,'yingchaoqun','2024-08-09 00:00:00.000'),
(91,193000000001000091,'FEE_TYPE_091','集装箱费用','DOMESTIC','改运抵',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-09 00:00:00.000',NULL,'yingchaoqun','2024-08-09 00:00:00.000'),
(92,193000000001000092,'FEE_TYPE_092','集装箱费用','DOMESTIC','预提费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-08 00:00:00.000',NULL,'yingchaoqun','2024-08-08 00:00:00.000'),
(93,193000000001000093,'FEE_TYPE_093','集装箱费用','DOMESTIC','设备交接单',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-08 00:00:00.000',NULL,'yingchaoqun','2024-08-08 00:00:00.000'),
(94,193000000001000094,'FEE_TYPE_094','集装箱费用','DOMESTIC','操作费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-08 00:00:00.000',NULL,'yingchaoqun','2024-08-08 00:00:00.000'),
(95,193000000001000095,'FEE_TYPE_095','集装箱费用','DOMESTIC','VGM',1,1,1,1,1,NULL,NULL,'yingchaoqun','2024-08-08 00:00:00.000',NULL,'yingchaoqun','2024-08-08 00:00:00.000'),
(96,193000000001000096,'FEE_TYPE_096','集装箱费用','INTERNATIONAL','托盘费',1,0,0,0,1,NULL,NULL,'jinshanshan','2023-08-14 00:00:00.000',NULL,'jinshanshan','2023-08-14 00:00:00.000'),
(97,193000000001000097,'FEE_TYPE_097','集装箱费用','INTERNATIONAL','拆柜费',1,0,0,0,1,NULL,NULL,'jinshanshan','2023-08-14 00:00:00.000',NULL,'jinshanshan','2023-08-14 00:00:00.000'),
(98,193000000001000098,'FEE_TYPE_098','集装箱费用','DOMESTIC','加封费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-08-03 00:00:00.000',NULL,'yingchaoqun','2023-08-03 00:00:00.000'),
(99,193000000001000099,'FEE_TYPE_099','集装箱费用','DOMESTIC','电子锁',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-08-03 00:00:00.000',NULL,'yingchaoqun','2023-08-03 00:00:00.000'),
(100,193000000001000100,'FEE_TYPE_100','集装箱费用','DOMESTIC','吊机费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-19 00:00:00.000',NULL,'yingchaoqun','2023-07-19 00:00:00.000'),
(101,193000000001000101,'FEE_TYPE_101','集装箱费用','DOMESTIC','条码费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-19 00:00:00.000',NULL,'yingchaoqun','2023-07-19 00:00:00.000'),
(102,193000000001000102,'FEE_TYPE_102','集装箱费用','DOMESTIC','AMS',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-19 00:00:00.000',NULL,'yingchaoqun','2023-07-19 00:00:00.000'),
(103,193000000001000103,'FEE_TYPE_103','集装箱费用','DOMESTIC','海运费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-19 00:00:00.000',NULL,'yingchaoqun','2023-07-19 00:00:00.000'),
(104,193000000001000104,'FEE_TYPE_104','集装箱费用','DOMESTIC','堆存费',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-19 00:00:00.000',NULL,'yingchaoqun','2023-07-19 00:00:00.000'),
(105,193000000001000105,'FEE_TYPE_105','集装箱费用','DOMESTIC','电子装箱单',1,1,1,1,1,NULL,NULL,'yingchaoqun','2023-07-19 00:00:00.000',NULL,'yingchaoqun','2023-07-19 00:00:00.000'),
(106,193000000001000106,'FEE_TYPE_106','集装箱费用','INTERNATIONAL','提柜费',1,1,1,1,0,'提柜费',NULL,'admin','2023-05-18 00:00:00.000',NULL,'admin','2023-05-18 00:00:00.000'),
(107,193000000001000107,'FEE_TYPE_107','集装箱费用','INTERNATIONAL','清关费',1,1,1,1,0,'清关费',NULL,'admin','2023-05-18 00:00:00.000',NULL,'admin','2023-05-18 00:00:00.000'),
(108,193000000001000108,'FEE_TYPE_108','集装箱费用','DOMESTIC','拖车费',1,1,1,1,0,'拖车费',NULL,'admin','2023-05-18 00:00:00.000',NULL,'admin','2023-07-06 00:00:00.000'),
(109,193000000001000109,'FEE_TYPE_109','集装箱费用','DOMESTIC','加载费',1,1,1,1,0,'加载费',NULL,'admin','2023-05-18 00:00:00.000',NULL,'admin','2023-05-18 00:00:00.000'),
(110,193000000001000110,'FEE_TYPE_110','BOL费用','INTERNATIONAL','装车费',1,1,1,1,1,NULL,NULL,'jinshanshan','2025-01-17 00:00:00.000',NULL,'jinshanshan','2025-01-17 00:00:00.000');
DROP PROCEDURE IF EXISTS phase2_publish_assert_fee_item_preflight;
DELIMITER $$
CREATE PROCEDURE phase2_publish_assert_fee_item_preflight()
BEGIN
IF (SELECT COUNT(*) FROM tmp_fee_item_seed) <> 110
OR (SELECT COUNT(*) FROM tmp_fee_item_seed WHERE trade_scope = 'DOMESTIC') <> 84
OR (SELECT COUNT(*) FROM tmp_fee_item_seed WHERE trade_scope = 'INTERNATIONAL') <> 26 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:Excel种子数量或国内国际范围不正确';
END IF;
IF EXISTS (SELECT fee_code FROM tmp_fee_item_seed GROUP BY fee_code HAVING COUNT(*) > 1)
OR EXISTS (SELECT trade_scope, fee_type_name FROM tmp_fee_item_seed GROUP BY trade_scope, fee_type_name HAVING COUNT(*) > 1) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:Excel种子编码或语义键重复';
END IF;
IF EXISTS (
SELECT f.id
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.id = f.id
WHERE f.tenant_id <> 0 OR f.fee_code <> s.fee_code
OR f.category <> s.category OR f.trade_scope <> s.trade_scope
OR f.fee_type_name <> s.fee_type_name
OR f.pickup_payable <> s.pickup_payable OR f.pickup_receivable <> s.pickup_receivable
OR f.warehouse_payable <> s.warehouse_payable OR f.warehouse_receivable <> s.warehouse_receivable
OR f.allocation_fee <> s.allocation_fee OR NOT (f.description <=> s.description)
) THEN
SELECT 'FIXED_ID_CONFLICT' AS conflict_type, f.id, f.tenant_id, f.fee_code,
f.trade_scope, f.fee_type_name
FROM fl_fee_item f JOIN tmp_fee_item_seed s ON s.id = f.id
WHERE f.tenant_id <> 0 OR f.fee_code <> s.fee_code
OR f.category <> s.category OR f.trade_scope <> s.trade_scope
OR f.fee_type_name <> s.fee_type_name
OR f.pickup_payable <> s.pickup_payable OR f.pickup_receivable <> s.pickup_receivable
OR f.warehouse_payable <> s.warehouse_payable OR f.warehouse_receivable <> s.warehouse_receivable
OR f.allocation_fee <> s.allocation_fee OR NOT (f.description <=> s.description);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:固定主键已被不同费用项目占用';
END IF;
IF EXISTS (
SELECT f.id
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.fee_code = f.fee_code
WHERE f.tenant_id = 0
AND (f.id <> s.id OR f.category <> s.category OR f.trade_scope <> s.trade_scope
OR f.fee_type_name <> s.fee_type_name OR f.pickup_payable <> s.pickup_payable
OR f.pickup_receivable <> s.pickup_receivable OR f.warehouse_payable <> s.warehouse_payable
OR f.warehouse_receivable <> s.warehouse_receivable OR f.allocation_fee <> s.allocation_fee
OR NOT (f.description <=> s.description) OR f.status <> 'ACTIVE' OR f.deleted <> 0)
) THEN
SELECT 'PUBLIC_CODE_CONFLICT' AS conflict_type, f.id, f.tenant_id, f.fee_code,
f.trade_scope, f.fee_type_name, s.id AS expected_id, s.fee_type_name AS expected_name
FROM fl_fee_item f JOIN tmp_fee_item_seed s ON s.fee_code = f.fee_code
WHERE f.tenant_id = 0
AND (f.id <> s.id OR f.category <> s.category OR f.trade_scope <> s.trade_scope
OR f.fee_type_name <> s.fee_type_name OR f.pickup_payable <> s.pickup_payable
OR f.pickup_receivable <> s.pickup_receivable OR f.warehouse_payable <> s.warehouse_payable
OR f.warehouse_receivable <> s.warehouse_receivable OR f.allocation_fee <> s.allocation_fee
OR NOT (f.description <=> s.description) OR f.status <> 'ACTIVE' OR f.deleted <> 0);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:平台费用编码存在语义冲突';
END IF;
IF EXISTS (
SELECT f.id
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.trade_scope = f.trade_scope AND s.fee_type_name = f.fee_type_name
WHERE f.tenant_id = 0 AND (f.fee_code <> s.fee_code OR f.category <> s.category
OR f.pickup_payable <> s.pickup_payable OR f.pickup_receivable <> s.pickup_receivable
OR f.warehouse_payable <> s.warehouse_payable OR f.warehouse_receivable <> s.warehouse_receivable
OR f.allocation_fee <> s.allocation_fee OR NOT (f.description <=> s.description))
) THEN
SELECT 'PUBLIC_SEMANTIC_CONFLICT' AS conflict_type, f.id, f.tenant_id, f.fee_code,
f.trade_scope, f.fee_type_name, s.fee_code AS expected_code
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.trade_scope = f.trade_scope AND s.fee_type_name = f.fee_type_name
WHERE f.tenant_id = 0 AND (f.fee_code <> s.fee_code OR f.category <> s.category
OR f.pickup_payable <> s.pickup_payable OR f.pickup_receivable <> s.pickup_receivable
OR f.warehouse_payable <> s.warehouse_payable OR f.warehouse_receivable <> s.warehouse_receivable
OR f.allocation_fee <> s.allocation_fee OR NOT (f.description <=> s.description));
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:平台费用语义键存在编码或字段冲突';
END IF;
IF EXISTS (SELECT 1 FROM fl_fee_item WHERE tenant_id = 0 AND (fee_code IS NULL OR TRIM(fee_code) = '')) THEN
SELECT 'PUBLIC_EMPTY_CODE' AS conflict_type, id, tenant_id, fee_code, trade_scope, fee_type_name
FROM fl_fee_item
WHERE tenant_id = 0 AND (fee_code IS NULL OR TRIM(fee_code) = '');
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:平台既有费用项目存在空编码';
END IF;
IF EXISTS (
SELECT f.id
FROM fl_fee_item f
LEFT JOIN tmp_fee_item_seed by_code ON by_code.fee_code = NULLIF(TRIM(f.fee_code), '')
LEFT JOIN tmp_fee_item_seed by_semantic ON by_semantic.trade_scope = f.trade_scope
AND by_semantic.fee_type_name = f.fee_type_name
WHERE f.tenant_id > 0 AND by_code.seed_no IS NULL AND by_semantic.seed_no IS NULL
) THEN
SELECT 'UNMAPPED_CUSTOMER_ITEM' AS conflict_type, f.id, f.tenant_id, f.fee_code,
f.trade_scope, f.fee_type_name
FROM fl_fee_item f
LEFT JOIN tmp_fee_item_seed by_code ON by_code.fee_code = NULLIF(TRIM(f.fee_code), '')
LEFT JOIN tmp_fee_item_seed by_semantic ON by_semantic.trade_scope = f.trade_scope
AND by_semantic.fee_type_name = f.fee_type_name
WHERE f.tenant_id > 0 AND by_code.seed_no IS NULL AND by_semantic.seed_no IS NULL;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:存在无法匹配公共种子的客户费用项目';
END IF;
IF EXISTS (
SELECT f.id
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.fee_code = NULLIF(TRIM(f.fee_code), '')
WHERE f.tenant_id > 0 AND (f.category <> s.category OR f.trade_scope <> s.trade_scope
OR f.fee_type_name <> s.fee_type_name OR f.pickup_payable <> s.pickup_payable
OR f.pickup_receivable <> s.pickup_receivable OR f.warehouse_payable <> s.warehouse_payable
OR f.warehouse_receivable <> s.warehouse_receivable OR f.allocation_fee <> s.allocation_fee
OR NOT (f.description <=> s.description))
) THEN
SELECT 'CUSTOMER_CODE_CONFLICT' AS conflict_type, f.id, f.tenant_id, f.fee_code,
f.trade_scope, f.fee_type_name, s.fee_type_name AS expected_name
FROM fl_fee_item f JOIN tmp_fee_item_seed s ON s.fee_code = NULLIF(TRIM(f.fee_code), '')
WHERE f.tenant_id > 0 AND (f.category <> s.category OR f.trade_scope <> s.trade_scope
OR f.fee_type_name <> s.fee_type_name OR f.pickup_payable <> s.pickup_payable
OR f.pickup_receivable <> s.pickup_receivable OR f.warehouse_payable <> s.warehouse_payable
OR f.warehouse_receivable <> s.warehouse_receivable OR f.allocation_fee <> s.allocation_fee
OR NOT (f.description <=> s.description));
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:客户费用编码存在语义冲突';
END IF;
IF EXISTS (
SELECT f.id
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.trade_scope = f.trade_scope AND s.fee_type_name = f.fee_type_name
WHERE f.tenant_id > 0 AND (NULLIF(TRIM(f.fee_code), '') IS NOT NULL
AND f.fee_code <> s.fee_code AND f.fee_code <> CONCAT('MANUAL_', f.id)
OR f.category <> s.category OR f.pickup_payable <> s.pickup_payable
OR f.pickup_receivable <> s.pickup_receivable OR f.warehouse_payable <> s.warehouse_payable
OR f.warehouse_receivable <> s.warehouse_receivable OR f.allocation_fee <> s.allocation_fee
OR NOT (f.description <=> s.description))
) THEN
SELECT 'CUSTOMER_SEMANTIC_CONFLICT' AS conflict_type, f.id, f.tenant_id, f.fee_code,
f.trade_scope, f.fee_type_name, s.fee_code AS expected_code
FROM fl_fee_item f
JOIN tmp_fee_item_seed s ON s.trade_scope = f.trade_scope AND s.fee_type_name = f.fee_type_name
WHERE f.tenant_id > 0 AND (NULLIF(TRIM(f.fee_code), '') IS NOT NULL
AND f.fee_code <> s.fee_code AND f.fee_code <> CONCAT('MANUAL_', f.id)
OR f.category <> s.category OR f.pickup_payable <> s.pickup_payable
OR f.pickup_receivable <> s.pickup_receivable OR f.warehouse_payable <> s.warehouse_payable
OR f.warehouse_receivable <> s.warehouse_receivable OR f.allocation_fee <> s.allocation_fee
OR NOT (f.description <=> s.description));
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:客户费用语义键存在编码或字段冲突';
END IF;
IF EXISTS (
SELECT d.id
FROM fl_inbound_fee_detail d
JOIN fl_fee_item f ON f.id = d.fee_item_id
WHERE d.tenant_id = 0 OR (f.tenant_id > 0 AND f.tenant_id <> d.tenant_id)
) THEN
SELECT 'CROSS_CUSTOMER_DETAIL_REFERENCE' AS conflict_type, d.id AS fee_detail_id,
d.tenant_id AS detail_customer_id, d.fee_item_id, f.tenant_id AS fee_item_scope
FROM fl_inbound_fee_detail d JOIN fl_fee_item f ON f.id = d.fee_item_id
WHERE d.tenant_id = 0 OR (f.tenant_id > 0 AND f.tenant_id <> d.tenant_id);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期费用项目迁移失败:发现跨客户或平台作用域费用明细引用';
END IF;
END$$
DELIMITER ;
CALL phase2_publish_assert_fee_item_preflight();
DROP PROCEDURE IF EXISTS phase2_publish_assert_fee_item_preflight;
DROP PROCEDURE IF EXISTS phase2_publish_migrate_fee_items;
DELIMITER $$
CREATE PROCEDURE phase2_publish_migrate_fee_items()
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
INSERT INTO fl_fee_item (id, tenant_id, fee_code, category, trade_scope, fee_type_name,
pickup_payable, pickup_receivable, warehouse_payable, warehouse_receivable, allocation_fee,
description, status, created_by, created_by_name, created_at, updated_by, updated_by_name, updated_at)
SELECT s.id, 0, s.fee_code, s.category, s.trade_scope, s.fee_type_name,
s.pickup_payable, s.pickup_receivable, s.warehouse_payable, s.warehouse_receivable, s.allocation_fee,
s.description, 'ACTIVE', s.created_by, s.created_by_name, s.created_at,
s.updated_by, s.updated_by_name, s.updated_at
FROM tmp_fee_item_seed s
WHERE NOT EXISTS (SELECT 1 FROM fl_fee_item f WHERE f.id = s.id);
DROP TEMPORARY TABLE IF EXISTS tmp_fee_item_map;
CREATE TEMPORARY TABLE tmp_fee_item_map (
old_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
public_id BIGINT UNSIGNED NOT NULL
) ENGINE=MEMORY;
INSERT INTO tmp_fee_item_map (old_id, public_id)
SELECT f.id, s.id
FROM fl_fee_item f
JOIN tmp_fee_item_seed s
ON (s.fee_code = NULLIF(TRIM(f.fee_code), '')
OR (s.trade_scope = f.trade_scope AND s.fee_type_name = f.fee_type_name))
WHERE f.tenant_id > 0;
UPDATE fl_inbound_fee_detail d
JOIN tmp_fee_item_map m ON m.old_id = d.fee_item_id
SET d.fee_item_id = m.public_id;
DELETE f FROM fl_fee_item f JOIN tmp_fee_item_map m ON m.old_id = f.id;
COMMIT;
END$$
DELIMITER ;
CALL phase2_publish_migrate_fee_items();
DROP PROCEDURE IF EXISTS phase2_publish_migrate_fee_items;
SET @fee_code_ddl = CASE
WHEN
(SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE()
AND table_name = 'fl_fee_item' AND index_name = 'uk_fee_item_code'
AND non_unique = 0 AND seq_in_index = 1 AND column_name = 'fee_code') = 1
AND (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE()
AND table_name = 'fl_fee_item' AND index_name = 'uk_fee_item_code') = 1
THEN 'SELECT 1'
WHEN EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
AND table_name = 'fl_fee_item' AND index_name = 'uk_fee_item_code')
THEN 'ALTER TABLE fl_fee_item DROP INDEX uk_fee_item_code, ADD UNIQUE KEY uk_fee_item_code (fee_code)'
ELSE 'ALTER TABLE fl_fee_item ADD UNIQUE KEY uk_fee_item_code (fee_code)'
END;
PREPARE statement FROM @fee_code_ddl;
EXECUTE statement;
DEALLOCATE PREPARE statement;
DROP TEMPORARY TABLE IF EXISTS tmp_fee_item_map;
DROP TEMPORARY TABLE IF EXISTS tmp_fee_item_seed;
SET @fee_code_ddl = IF(
(SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_fee_item'
AND column_name = 'fee_code' AND is_nullable = 'NO') = 1,
'SELECT 1',
'ALTER TABLE fl_fee_item MODIFY COLUMN fee_code VARCHAR(50) NOT NULL COMMENT ''稳定费用编码;平台公共主数据固定编码'' AFTER trade_scope'
);
PREPARE statement FROM @fee_code_ddl;
EXECUTE statement;
DEALLOCATE PREPARE statement;
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'source_type', CALL phase2_publish_add_column('fl_inbound_fee_detail', 'source_type',
"VARCHAR(32) NOT NULL DEFAULT 'MANUAL' COMMENT 'MANUAL或FBA_PRICING' AFTER fee_item_id"); "VARCHAR(32) NOT NULL DEFAULT 'MANUAL' COMMENT 'MANUAL或FBA_PRICING' AFTER fee_item_id");
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'source_fee_id', CALL phase2_publish_add_column('fl_inbound_fee_detail', 'source_fee_id',
@@ -74,10 +447,39 @@ END$$
DELIMITER ; DELIMITER ;
CALL phase2_publish_add_index('fl_fee_item', 'uk_fee_item_code', CALL phase2_publish_add_index('fl_fee_item', 'uk_fee_item_code',
'UNIQUE KEY `uk_fee_item_code` (`tenant_id`, `fee_code`)'); 'UNIQUE KEY `uk_fee_item_code` (`fee_code`)');
CALL phase2_publish_add_index('fl_inbound_fee_detail', 'uk_inbound_fee_source', CALL phase2_publish_add_index('fl_inbound_fee_detail', 'uk_inbound_fee_source',
'UNIQUE KEY `uk_inbound_fee_source` (`tenant_id`, `source_type`, `source_fee_id`)'); 'UNIQUE KEY `uk_inbound_fee_source` (`tenant_id`, `source_type`, `source_fee_id`)');
CALL phase2_publish_add_index('fl_inbound_fee_detail', 'idx_inbound_fee_published', CALL phase2_publish_add_index('fl_inbound_fee_detail', 'idx_inbound_fee_published',
'KEY `idx_inbound_fee_published` (`tenant_id`, `inbound_id`, `publication_status`, `source_fee_version`)'); 'KEY `idx_inbound_fee_published` (`tenant_id`, `inbound_id`, `publication_status`, `source_fee_version`)');
DROP PROCEDURE IF EXISTS phase2_publish_add_index; DROP PROCEDURE IF EXISTS phase2_publish_add_index;
DROP PROCEDURE IF EXISTS phase2_publish_assert_fee_schema;
DELIMITER $$
CREATE PROCEDURE phase2_publish_assert_fee_schema()
BEGIN
IF (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_fee_item'
AND column_name = 'fee_code' AND is_nullable = 'NO') <> 1
OR (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'fl_fee_item'
AND index_name = 'uk_fee_item_code' AND non_unique = 0) <> 1
OR (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'fl_fee_item'
AND index_name = 'uk_fee_item_code' AND non_unique = 0
AND seq_in_index = 1 AND column_name = 'fee_code') <> 1 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期迁移失败:费用项目编码字段或唯一索引不正确';
END IF;
IF (SELECT COUNT(*) FROM fl_fee_item WHERE tenant_id <> 0) <> 0
OR (SELECT COUNT(*) FROM fl_fee_item WHERE tenant_id = 0 AND status = 'ACTIVE' AND deleted = 0
AND id BETWEEN 193000000001000001 AND 193000000001000110
AND fee_code = CONCAT('FEE_TYPE_', LPAD(id - 193000000001000000, 3, '0'))) <> 110
OR EXISTS (SELECT 1 FROM fl_inbound_fee_detail d JOIN fl_fee_item f ON f.id = d.fee_item_id
WHERE d.tenant_id = 0 OR f.tenant_id <> 0) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '二期迁移失败:费用项目未完成平台公共作用域或明细跨客户引用校验';
END IF;
END$$
DELIMITER ;
CALL phase2_publish_assert_fee_schema();
DROP PROCEDURE IF EXISTS phase2_publish_assert_fee_schema;
@@ -0,0 +1,174 @@
openapi: 3.0.3
info:
title: 柏悦费用项目 API
version: 1.0.0
description: 平台费用项目主数据;业务结果统一通过响应体 code 返回。
security:
- bearerAuth: []
paths:
/api/platform/v1/fee-items:
get:
summary: 查询费用项目
description: 需要 firstleg:fee-item:view 权限;费用项目属于平台公共主数据。
parameters:
- {name: category, in: query, schema: {type: string, maxLength: 64}}
- {name: tradeScope, in: query, schema: {type: string, enum: [DOMESTIC, INTERNATIONAL]}}
- {name: keyword, in: query, schema: {type: string, maxLength: 128}}
- {name: status, in: query, schema: {type: string, enum: [ACTIVE, DISABLED]}}
- {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
- {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 200, default: 20}}
responses:
'200':
description: 成功或业务失败
content:
application/json:
schema: {$ref: '#/components/schemas/FeeItemPageApiResponse'}
'403': {description: 无权限}
post:
summary: 新增费用项目
description: 需要 firstleg:fee-item:create 权限;feeCode 可省略,服务端生成唯一人工编码;传入时统一转为大写并在平台公共范围内保持唯一。
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/FeeItemSaveRequest'}
responses:
'200':
description: 成功或业务失败
content:
application/json:
schema: {$ref: '#/components/schemas/FeeItemApiResponse'}
'403': {description: 无权限}
/api/platform/v1/fee-items/{id}:
get:
summary: 查询费用项目详情
description: 需要 firstleg:fee-item:view 权限。
parameters: [{$ref: '#/components/parameters/Id'}]
responses:
'200':
description: 成功或业务失败
content:
application/json:
schema: {$ref: '#/components/schemas/FeeItemApiResponse'}
'403': {description: 无权限}
put:
summary: 编辑费用项目
description: 需要 firstleg:fee-item:edit 权限;省略 feeCode 时保留原编码,传入时统一转为大写并校验平台公共范围内唯一性。
parameters: [{$ref: '#/components/parameters/Id'}]
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/FeeItemSaveRequest'}}}}
responses:
'200':
description: 成功或业务失败
content:
application/json:
schema: {$ref: '#/components/schemas/FeeItemApiResponse'}
'403': {description: 无权限}
delete:
summary: 删除费用项目
description: 需要 firstleg:fee-item:delete 权限;已被费用明细引用的项目只能停用。
parameters: [{$ref: '#/components/parameters/Id'}]
responses:
'200':
description: 成功或业务失败
content:
application/json:
schema: {$ref: '#/components/schemas/VoidApiResponse'}
'403': {description: 无权限}
/api/platform/v1/fee-items/{id}/status:
patch:
summary: 启用或停用费用项目
description: 需要 firstleg:fee-item:edit 权限。
parameters: [{$ref: '#/components/parameters/Id'}]
requestBody: {required: true, content: {application/json: {schema: {type: object, required: [status], properties: {status: {type: string, enum: [ACTIVE, DISABLED]}}}}}}
responses:
'200':
description: 成功或业务失败
content:
application/json:
schema: {$ref: '#/components/schemas/VoidApiResponse'}
'403': {description: 无权限}
components:
securitySchemes:
bearerAuth: {type: http, scheme: bearer}
parameters:
Id: {name: id, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
schemas:
FeeItemSaveRequest:
type: object
required: [category, tradeScope, feeTypeName, pickupPayable, pickupReceivable, warehousePayable, warehouseReceivable, allocationFee]
properties:
category: {type: string, maxLength: 64}
tradeScope: {type: string, enum: [DOMESTIC, INTERNATIONAL]}
feeCode: {type: string, maxLength: 50, pattern: '^[A-Za-z0-9_-]+$'}
feeTypeName: {type: string, maxLength: 128}
pickupPayable: {type: boolean}
pickupReceivable: {type: boolean}
warehousePayable: {type: boolean}
warehouseReceivable: {type: boolean}
allocationFee: {type: boolean}
description: {type: string, maxLength: 500}
FeeItemResource:
type: object
required: [id, category, tradeScope, feeCode, feeTypeName, pickupPayable, pickupReceivable, warehousePayable, warehouseReceivable, allocationFee, status]
properties:
id: {type: integer, format: int64}
category: {type: string}
tradeScope: {type: string, enum: [DOMESTIC, INTERNATIONAL]}
feeCode: {type: string}
feeTypeName: {type: string}
pickupPayable: {type: boolean}
pickupReceivable: {type: boolean}
warehousePayable: {type: boolean}
warehouseReceivable: {type: boolean}
allocationFee: {type: boolean}
description: {type: string, nullable: true}
status: {type: string, enum: [ACTIVE, DISABLED]}
createdBy: {type: integer, format: int64, nullable: true}
createdByName: {type: string, nullable: true}
updatedBy: {type: integer, format: int64, nullable: true}
updatedByName: {type: string, nullable: true}
createdAt: {type: string, format: date-time}
updatedAt: {type: string, format: date-time}
deleted: {type: boolean, nullable: true}
version: {type: integer, nullable: true}
FeeItemPage:
type: object
required: [records, page, size, total, pages]
properties:
records:
type: array
items: {$ref: '#/components/schemas/FeeItemResource'}
page: {type: integer, format: int64}
size: {type: integer, format: int64}
total: {type: integer, format: int64}
pages: {type: integer, format: int64}
FeeItemApiResponse:
type: object
required: [code, message, timestamp]
properties:
code: {type: string}
message: {type: string}
data: {$ref: '#/components/schemas/FeeItemResource'}
requestId: {type: string, nullable: true}
traceId: {type: string, nullable: true}
timestamp: {type: string, format: date-time}
FeeItemPageApiResponse:
type: object
required: [code, message, timestamp]
properties:
code: {type: string}
message: {type: string}
data: {$ref: '#/components/schemas/FeeItemPage'}
requestId: {type: string, nullable: true}
traceId: {type: string, nullable: true}
timestamp: {type: string, format: date-time}
VoidApiResponse:
type: object
required: [code, message, timestamp]
properties:
code: {type: string}
message: {type: string}
data: {nullable: true}
requestId: {type: string, nullable: true}
traceId: {type: string, nullable: true}
timestamp: {type: string, format: date-time}
@@ -2,10 +2,15 @@ package com.baiyue.firstleg.controller;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.yaml.snakeyaml.Yaml;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -34,6 +39,53 @@ class FeeManagementContractTest {
assertPermission("delete", "firstleg:fee-item:delete"); assertPermission("delete", "firstleg:fee-item:delete");
} }
@Test
@DisplayName("平台费用项目接口不接受客户作用域")
void platformFeeItemEndpointsDoNotExposeAccountId() {
Arrays.stream(PlatformFeeItemController.class.getDeclaredMethods())
.filter(method -> List.of("list", "detail", "create", "update", "changeStatus", "delete")
.contains(method.getName()))
.flatMap(method -> Arrays.stream(method.getParameters()))
.forEach(parameter -> assertThat(parameter.getName()).isNotEqualTo("accountId"));
}
@Test
@DisplayName("费用项目OpenAPI包含完整响应和空响应契约")
void openApiIsComplete() throws Exception {
String source = new ClassPathResource("openapi/fee-item-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
Map<String, Object> document = stringMap(new Yaml().load(source));
assertThat(document.get("openapi")).isEqualTo("3.0.3");
assertThat(source).doesNotContain("accountId", "AccountId");
Map<String, Object> paths = stringMap(document.get("paths"));
assertThat(paths).containsOnlyKeys(
"/api/platform/v1/fee-items",
"/api/platform/v1/fee-items/{id}",
"/api/platform/v1/fee-items/{id}/status");
assertResponseSchema(paths, "/api/platform/v1/fee-items", "get", "FeeItemPageApiResponse");
assertResponseSchema(paths, "/api/platform/v1/fee-items", "post", "FeeItemApiResponse");
assertResponseSchema(paths, "/api/platform/v1/fee-items/{id}", "get", "FeeItemApiResponse");
assertResponseSchema(paths, "/api/platform/v1/fee-items/{id}", "put", "FeeItemApiResponse");
assertResponseSchema(paths, "/api/platform/v1/fee-items/{id}", "delete", "VoidApiResponse");
assertResponseSchema(paths, "/api/platform/v1/fee-items/{id}/status", "patch", "VoidApiResponse");
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
assertThat(schemas).containsKeys("FeeItemSaveRequest", "FeeItemResource", "FeeItemPage",
"FeeItemApiResponse", "FeeItemPageApiResponse", "VoidApiResponse");
for (String response : List.of("FeeItemApiResponse", "FeeItemPageApiResponse", "VoidApiResponse")) {
assertThat(list(stringMap(schemas.get(response)).get("required"))).contains("timestamp");
assertThat(properties(schemas, response)).containsKeys(
"code", "message", "data", "requestId", "traceId", "timestamp");
}
assertThat(properties(schemas, "VoidApiResponse").get("data"))
.isInstanceOf(Map.class)
.satisfies(value -> assertThat(stringMap(value)).containsEntry("nullable", true));
assertThat(stringMap(properties(schemas, "FeeItemSaveRequest").get("feeCode")))
.containsEntry("maxLength", 50)
.containsEntry("pattern", "^[A-Za-z0-9_-]+$");
}
private static void assertPermission(String methodName, String permission) { private static void assertPermission(String methodName, String permission) {
Method method = Arrays.stream(PlatformFeeItemController.class.getDeclaredMethods()) Method method = Arrays.stream(PlatformFeeItemController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals(methodName)) .filter(candidate -> candidate.getName().equals(methodName))
@@ -41,4 +93,29 @@ class FeeManagementContractTest {
assertThat(method.getAnnotation(PreAuthorize.class).value()) assertThat(method.getAnnotation(PreAuthorize.class).value())
.isEqualTo("hasAuthority('" + permission + "')"); .isEqualTo("hasAuthority('" + permission + "')");
} }
private static void assertResponseSchema(Map<String, Object> paths, String path,
String operation, String schema) {
Map<String, Object> endpoint = stringMap(paths.get(path));
Map<String, Object> response = stringMap(stringMap(endpoint.get(operation)).get("responses"));
Map<String, Object> success = stringMap(response.get("200"));
Map<String, Object> content = stringMap(success.get("content"));
Map<String, Object> mediaType = stringMap(content.get("application/json"));
assertThat(stringMap(mediaType.get("schema")))
.containsEntry("$ref", "#/components/schemas/" + schema);
}
private static Map<String, Object> properties(Map<String, Object> schemas, String schema) {
return stringMap(stringMap(schemas.get(schema)).get("properties"));
}
@SuppressWarnings("unchecked")
private static Map<String, Object> stringMap(Object value) {
return (Map<String, Object>) value;
}
@SuppressWarnings("unchecked")
private static List<String> list(Object value) {
return (List<String>) value;
}
} }
@@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -25,4 +27,27 @@ class LoadingEvidenceSqlContractTest {
.contains("idx_loading_evidence_seal (tenant_id, seal_no, status)") .contains("idx_loading_evidence_seal (tenant_id, seal_no, status)")
.contains("fl_inbound_loading_pick_task"); .contains("fl_inbound_loading_pick_task");
} }
@Test
void unifiedEntryMigratesLegacySingleSlotEvidenceWithoutRecreatingUniqueIndexes() throws Exception {
Path direct = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
Path unified = Files.exists(direct) ? direct : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
String sql = Files.readString(unified, StandardCharsets.UTF_8);
assertThat(sql)
.contains("file_id BIGINT UNSIGNED NOT NULL")
.contains("status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE'")
.contains("phase3_loading_evidence_add_column('status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''ACTIVE/VOID''')")
.contains("phase3_loading_evidence_add_column('void_reason'")
.contains("phase3_loading_evidence_add_column('voided_by'")
.contains("phase3_loading_evidence_add_column('voided_by_name'")
.contains("phase3_loading_evidence_add_column('voided_at'")
.contains("DROP INDEX uk_loading_evidence_type")
.contains("DROP INDEX uk_loading_evidence_seal")
.contains("ADD KEY idx_loading_evidence_type (tenant_id,inbound_id,evidence_type,status)")
.contains("ADD KEY idx_loading_evidence_seal (tenant_id,seal_no,status)")
.doesNotContain("phase3_loading_evidence_add_column('status', \"VARCHAR(16)")
.doesNotContain("UNIQUE KEY uk_loading_evidence_type")
.doesNotContain("UNIQUE KEY uk_loading_evidence_seal");
}
} }
@@ -4,6 +4,7 @@ import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper; import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper;
import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
@@ -76,4 +77,16 @@ class PdaSplitSettlementSqlContractTest {
.contains("#{childOrderId},#{childOrderNo}") .contains("#{childOrderId},#{childOrderNo}")
.contains("CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'INBOUNDED' END"); .contains("CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'INBOUNDED' END");
} }
@Test
void readsSplitStatusAndAllowsPdaSettlementAfterPreloadLoading() throws Exception {
String lockParent = InboundSplitSettlementMapper.class.getMethod("lockParent", long.class)
.getAnnotation(Select.class).value()[0];
assertThat(lockParent).contains("split_status AS splitStatus");
String markRoot = InboundSplitSettlementMapper.class.getMethod("markRootSplit",
long.class, long.class, long.class, int.class)
.getAnnotation(Update.class).value()[0];
assertThat(markRoot).contains("process_status IN ('INBOUNDED','LOADING')");
}
} }
@@ -27,7 +27,12 @@ class Phase3FinalUnifiedSqlContractTest {
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment") .contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment")
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_review") .contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_review")
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_operation") .contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_operation")
.contains("uk_loading_evidence_seal (tenant_id,seal_no)") .contains("phase3_loading_evidence_add_column")
.contains("DROP INDEX uk_loading_evidence_type")
.contains("DROP INDEX uk_loading_evidence_seal")
.contains("ADD KEY idx_loading_evidence_type (tenant_id,inbound_id,evidence_type,status)")
.contains("ADD KEY idx_loading_evidence_seal (tenant_id,seal_no,status)")
.contains("column_name IN ('status','void_reason','voided_by','voided_by_name','voided_at')")
.contains("CALL assert_phase3_final_schema()") .contains("CALL assert_phase3_final_schema()")
.contains("trg_port_schedule_history_before_update") .contains("trg_port_schedule_history_before_update")
.contains("trg_port_schedule_history_before_delete") .contains("trg_port_schedule_history_before_delete")
@@ -59,6 +59,7 @@ class TenantScopedMutationRepositoryTest {
initialize(FulfillmentTaskEntity.class); initialize(FulfillmentTaskEntity.class);
initialize(InboundWeightResultEntity.class); initialize(InboundWeightResultEntity.class);
initialize(InboundOrderEntity.class); initialize(InboundOrderEntity.class);
initialize(FeeItemEntity.class);
initialize(InboundFeeDetailEntity.class); initialize(InboundFeeDetailEntity.class);
} }
@@ -122,6 +123,30 @@ class TenantScopedMutationRepositoryTest {
assertTenantAndId(wrapper.getValue()); assertTenantAndId(wrapper.getValue());
} }
@Test
@SuppressWarnings({"rawtypes", "unchecked"})
void feeItemLookupIsAlwaysPlatformScoped() {
feeRepository.findItem(ID);
ArgumentCaptor<Wrapper<FeeItemEntity>> wrapper = ArgumentCaptor.forClass(Wrapper.class);
verify(feeItems).selectOne(wrapper.capture());
assertThat(wrapper.getValue().getCustomSqlSegment()).contains("tenant_id");
assertThat(((AbstractWrapper<?, ?, ?>) wrapper.getValue()).getParamNameValuePairs().values())
.contains(0L, ID);
}
@Test
@SuppressWarnings({"rawtypes", "unchecked"})
void feeItemBindingCountChecksAllCustomers() {
feeRepository.countBindings(ID);
ArgumentCaptor<Wrapper<InboundFeeDetailEntity>> wrapper = ArgumentCaptor.forClass(Wrapper.class);
verify(feeDetails).selectCount(wrapper.capture());
assertThat(wrapper.getValue().getCustomSqlSegment()).doesNotContain("tenant_id");
assertThat(((AbstractWrapper<?, ?, ?>) wrapper.getValue()).getParamNameValuePairs().values())
.contains(ID);
}
private static void initialize(Class<?> entityType) { private static void initialize(Class<?> entityType) {
TableInfoHelper.initTableInfo( TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), "test"), entityType); new MapperBuilderAssistant(new MybatisConfiguration(), "test"), entityType);
@@ -60,10 +60,10 @@ class FeeManagementServiceTest {
@DisplayName("费用项目列表返回分页结果") @DisplayName("费用项目列表返回分页结果")
void pagesItems() { void pagesItems() {
PageResult<FeeItemEntity> expected = new PageResult<>(List.of(), 2, 20, 21, 2); PageResult<FeeItemEntity> expected = new PageResult<>(List.of(), 2, 20, 21, 2);
when(repository.listItems(10001L, "仓储成本", "INTERNATIONAL", "人工", "ACTIVE", 2, 20)) when(repository.listItems("仓储成本", "INTERNATIONAL", "人工", "ACTIVE", 2, 20))
.thenReturn(expected); .thenReturn(expected);
assertThat(feeItems.list(10001L, "仓储成本", "INTERNATIONAL", "人工", "ACTIVE", 2, 20)) assertThat(feeItems.list("仓储成本", "INTERNATIONAL", "人工", "ACTIVE", 2, 20))
.isSameAs(expected); .isSameAs(expected);
} }
@@ -75,21 +75,74 @@ class FeeManagementServiceTest {
return null; return null;
}).when(repository).insertItem(any()); }).when(repository).insertItem(any());
FeeItemEntity created = feeItems.create(10001L, 9L, "admin", itemRequest()); FeeItemEntity created = feeItems.create(9L, "admin", itemRequest());
assertThat(created.getId()).isEqualTo(11L); assertThat(created.getId()).isEqualTo(11L);
assertThat(created.getAllocationFee()).isTrue(); assertThat(created.getAllocationFee()).isTrue();
assertThat(created.getWarehouseReceivable()).isTrue(); assertThat(created.getWarehouseReceivable()).isTrue();
assertThat(created.getFeeCode()).startsWith("MANUAL_");
assertThat(created.getTenantId()).isZero();
assertThat(created.getCreatedByName()).isEqualTo("admin"); assertThat(created.getCreatedByName()).isEqualTo("admin");
} }
@Test
@DisplayName("自定义费用编码统一转为大写并检查客户内唯一性")
void normalizesCustomFeeCode() {
FeeItemDtos.SaveRequest request = new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL",
"storage_fee", "仓库租赁费", false, false, true, true, true, "应付仓库租赁费");
FeeItemEntity created = feeItems.create(9L, "admin", request);
verify(repository).itemCodeExists("STORAGE_FEE", null);
assertThat(created.getFeeCode()).isEqualTo("STORAGE_FEE");
}
@Test
@DisplayName("并发创建相同费用编码返回业务冲突")
void rejectsConcurrentDuplicateFeeCode() {
FeeItemDtos.SaveRequest request = new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL",
"STORAGE_FEE", "仓库租赁费", false, false, true, true, true, null);
doThrow(new DuplicateKeyException("duplicate fee code")).when(repository).insertItem(any());
assertThatThrownBy(() -> feeItems.create(9L, "admin", request))
.isInstanceOf(BizException.class)
.hasMessageContaining("费用项目编码已存在");
}
@Test
@DisplayName("编辑时省略费用编码保留原稳定编码")
void preservesFeeCodeWhenUpdateOmitsIt() {
FeeItemEntity existing = item(true, true);
existing.setFeeCode("STORAGE_FEE");
when(repository.findItem(11L)).thenReturn(existing);
when(repository.updateItem(any())).thenReturn(1);
FeeItemEntity updated = feeItems.update(11L, 9L, "admin", itemRequest());
assertThat(updated.getFeeCode()).isEqualTo("STORAGE_FEE");
verify(repository, never()).itemCodeExists("STORAGE_FEE", 11L);
}
@Test
@DisplayName("任一客户引用公共费用项目后都禁止删除")
void rejectsDeletingItemBoundByAnyCustomer() {
when(repository.findItem(11L)).thenReturn(item(true, true));
when(repository.countBindings(11L)).thenReturn(1L);
assertThatThrownBy(() -> feeItems.delete(11L))
.isInstanceOf(BizException.class)
.hasMessageContaining("已被入库费用明细使用");
verify(repository, never()).deleteItem(11L);
}
@Test @Test
@DisplayName("入库费用绑定项目并生成订单快照") @DisplayName("入库费用绑定项目并生成订单快照")
void bindsInboundFee() { void bindsInboundFee() {
InboundOrderEntity inbound = inbound(); InboundOrderEntity inbound = inbound();
FeeItemEntity item = item(true, true); FeeItemEntity item = item(true, true);
when(repository.findInbound(10001L, 21L)).thenReturn(inbound); when(repository.findInbound(10001L, 21L)).thenReturn(inbound);
when(repository.findItem(10001L, 11L)).thenReturn(item); when(repository.findItem(11L)).thenReturn(item);
doAnswer(invocation -> { doAnswer(invocation -> {
((InboundFeeDetailEntity) invocation.getArgument(0)).setId(31L); ((InboundFeeDetailEntity) invocation.getArgument(0)).setId(31L);
return null; return null;
@@ -114,9 +167,10 @@ class FeeManagementServiceTest {
inbound.setCompanyName("客户A有限公司"); inbound.setCompanyName("客户A有限公司");
when(repository.findInbound(10001L, 21L)).thenReturn(inbound); when(repository.findInbound(10001L, 21L)).thenReturn(inbound);
when(repository.findBySourceFee(10001L, "FBA_PRICING", 501L)).thenReturn(null); when(repository.findBySourceFee(10001L, "FBA_PRICING", 501L)).thenReturn(null);
when(repository.findItemByCode(10001L, "FBA_HEAD_FREIGHT")).thenReturn(null); when(repository.findItemByCode("FBA_HEAD_FREIGHT")).thenReturn(null);
doAnswer(invocation -> { doAnswer(invocation -> {
FeeItemEntity value = invocation.getArgument(0); FeeItemEntity value = invocation.getArgument(0);
assertThat(value.getTenantId()).isZero();
value.setId(11L); value.setId(11L);
return null; return null;
}).when(repository).insertItem(any()); }).when(repository).insertItem(any());
@@ -167,7 +221,7 @@ class FeeManagementServiceTest {
FeeItemEntity freightItem = item(true, false); FeeItemEntity freightItem = item(true, false);
freightItem.setFeeCode("FBA_HEAD_FREIGHT"); freightItem.setFeeCode("FBA_HEAD_FREIGHT");
when(repository.findItemByCode(10001L, "FBA_HEAD_FREIGHT")).thenReturn(freightItem); when(repository.findItemByCode("FBA_HEAD_FREIGHT")).thenReturn(freightItem);
InboundFeeEntity fee = new InboundFeeEntity(); InboundFeeEntity fee = new InboundFeeEntity();
fee.setId(501L); fee.setId(501L);
@@ -195,7 +249,7 @@ class FeeManagementServiceTest {
@DisplayName("拒绝绑定未启用仓储应收的项目") @DisplayName("拒绝绑定未启用仓储应收的项目")
void rejectsInapplicableItem() { void rejectsInapplicableItem() {
when(repository.findInbound(10001L, 21L)).thenReturn(inbound()); when(repository.findInbound(10001L, 21L)).thenReturn(inbound());
when(repository.findItem(10001L, 11L)).thenReturn(item(false, true)); when(repository.findItem(11L)).thenReturn(item(false, true));
assertThatThrownBy(() -> details.create(10001L, 21L, 9L, assertThatThrownBy(() -> details.create(10001L, 21L, 9L,
detailRequest("RECEIVABLE"))) detailRequest("RECEIVABLE")))
@@ -152,6 +152,45 @@ class PdaPickSettlementServiceTest {
verify(splits).removeSkippedPlanItem(22L, 9L, "operator"); verify(splits).removeSkippedPlanItem(22L, 9L, "operator");
} }
@Test
void usesTotalPackageCountWhenUnsplitMeasuredInboundHasZeroAvailableColumn() {
LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 20, 20, "PICKED");
resolved(List.of(item));
when(splits.lockParent(31L)).thenReturn(parent(20, 0, "INBOUNDED", "NOT_SPLIT"));
when(splits.lockLatestApprovedItemsForInbound(10001L, 31L))
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20)));
when(allocations.lockConfirmed(10001L, 31L))
.thenReturn(List.of(allocation(1L, 1, 501L, 10), allocation(2L, 2, 502L, 10)));
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new WarehousePalletLocationClient.SettlementResult(
List.of(released(91L, 501L), released(92L, 502L)), List.of())));
writes(0, 20);
service.settle(1L, 9L, "operator");
verify(splits).cloneChildWithPurpose(eq(10001L), eq(31L), eq(31L), anyLong(),
eq(1001L), eq("ORDER-LOADED"), anyString(), eq(20), anyInt(), eq("LOADED"), eq(9L), eq("operator"));
}
@Test
void allowsPdaSettlementAfterPreloadMovesMasterToLoading() {
LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 20, 20, "PICKED");
resolved(List.of(item));
when(splits.lockParent(31L)).thenReturn(parent(20, 20, "LOADING", "NOT_SPLIT"));
when(splits.lockLatestApprovedItemsForInbound(10001L, 31L))
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20)));
when(allocations.lockConfirmed(10001L, 31L))
.thenReturn(List.of(allocation(1L, 1, 501L, 10), allocation(2L, 2, 502L, 10)));
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new WarehousePalletLocationClient.SettlementResult(
List.of(released(91L, 501L), released(92L, 502L)), List.of())));
writes(0, 20);
service.settle(1L, 9L, "operator");
verify(splits).markRootSplit(10001L, 31L, 9L, 3);
}
@Test @Test
void mixedSkippedInboundIsNotRestored() { void mixedSkippedInboundIsNotRestored() {
LoadingPickItemEntity picked = item(11L, 21L, 31L, 41L, 8, 8, "PICKED"); LoadingPickItemEntity picked = item(11L, 21L, 31L, 41L, 8, 8, "PICKED");
@@ -250,7 +289,12 @@ class PdaPickSettlementServiceTest {
} }
private static ParentRow parent(int packageCount) { private static ParentRow parent(int packageCount) {
return new ParentRow(31L, 10001L, "IN-001", "MASTER", "INBOUNDED", packageCount, packageCount, 3); return parent(packageCount, packageCount, "INBOUNDED", "NOT_SPLIT");
}
private static ParentRow parent(int totalPackageCount, int packageCount, String processStatus, String splitStatus) {
return new ParentRow(31L, 10001L, 901L, "ORDER-001", "IN-001", "MASTER", 31L,
processStatus, totalPackageCount, packageCount, splitStatus, 3);
} }
private static InboundPalletAllocationEntity allocation(long id, int no, long locationId, int allocated) { private static InboundPalletAllocationEntity allocation(long id, int no, long locationId, int allocated) {
@@ -227,31 +227,7 @@ export async function run(context) {
claimed?.workflowStatus === 'IN_PROGRESS' && Boolean(claimed?.assignment?.assigneeId), claimed?.workflowStatus === 'IN_PROGRESS' && Boolean(claimed?.assignment?.assigneeId),
`workflow=${claimed?.workflowStatus || 'missing'}`, OPTIONS) `workflow=${claimed?.workflowStatus || 'missing'}`, OPTIONS)
const itemClaimBody = { // P3-Q11 uses task-level assignment as the single-operator guard; no item claim API exists.
expectedAssignmentVersion: 0,
operationId: `${context.marker}-bridge-item-claim`.slice(0, 128),
}
const itemClaim = await request(context, 'POST',
`${pdaBase}/tasks/${canonicalTaskId}/items/${id(canonicalItem.id)}/claim`, itemClaimBody)
const itemClaimed = context.apiSuccess(MODULE, '领取柜次 PDA 明细单人作业权', itemClaim, '', OPTIONS)
const claimedItem = itemClaimed?.items?.find?.((item) => id(item.id) === id(canonicalItem.id))
context.assert(MODULE, 'PDA 明细作业员与任务领取人一致', itemClaim,
id(claimedItem?.assignment?.assigneeId) === id(itemClaimed?.assignment?.assigneeId)
&& Number(claimedItem?.assignment?.version) === 0,
`taskAssignee=${id(itemClaimed?.assignment?.assigneeId) || 'missing'}, itemAssignee=${id(claimedItem?.assignment?.assigneeId) || 'missing'}`, OPTIONS)
const itemClaimReplay = await request(context, 'POST',
`${pdaBase}/tasks/${canonicalTaskId}/items/${id(canonicalItem.id)}/claim`, itemClaimBody)
context.assert(MODULE, 'PDA 明细领取 operationId 幂等重放', itemClaimReplay,
codeOf(itemClaimReplay) === '200'
&& id(itemClaimReplay.body?.data?.items?.find?.((item) => id(item.id) === id(canonicalItem.id))?.assignment?.assigneeId)
=== id(claimedItem?.assignment?.assigneeId),
`HTTP=${itemClaimReplay.http}, code=${codeOf(itemClaimReplay)}`, OPTIONS)
const claimHashConflict = await request(context, 'POST',
`${pdaBase}/tasks/${canonicalTaskId}/items/${id(canonicalItem.id)}/claim`, {
...itemClaimBody, expectedAssignmentVersion: 1,
})
context.addStep(MODULE, 'PDA 明细同 operationId 不同版本请求被拒绝', claimHashConflict,
failure(claimHashConflict), `HTTP=${claimHashConflict.http}, code=${codeOf(claimHashConflict)}`, OPTIONS)
const canonicalPick = await request(context, 'POST', const canonicalPick = await request(context, 'POST',
`${pdaBase}/tasks/${canonicalTaskId}/items/${id(canonicalItem.id)}/pick`, { `${pdaBase}/tasks/${canonicalTaskId}/items/${id(canonicalItem.id)}/pick`, {