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` 仅表示第三方主体未知或尚未解析的哨兵值,不是平台客户记录或平台操作作用域,绝不能据此授权、查询或写入客户业务数据。上线后可只读统计:
```sql
+512 -24
View File
@@ -4310,9 +4310,10 @@ USE baiyue_firstleg;
CREATE TABLE IF NOT EXISTS fl_fee_item (
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 '费用分类',
trade_scope VARCHAR(20) NOT NULL COMMENT 'DOMESTIC国内、INTERNATIONAL国际',
fee_code VARCHAR(50) NOT NULL COMMENT '稳定费用编码;平台公共主数据固定编码',
fee_type_name VARCHAR(128) NOT NULL COMMENT '费用类型名称',
pickup_payable 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,
version INT NOT NULL DEFAULT 0,
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_name (tenant_id, fee_type_name, deleted)
) ENGINE=InnoDB COMMENT='费用项目主数据';
) ENGINE=InnoDB COMMENT='平台公共费用项目主数据';
CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail (
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',
fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '绑定费用项目ID',
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
@@ -6417,6 +6419,379 @@ END$$
DELIMITER ;
CALL unified_publish_fee_add_column('fl_fee_item', 'fee_code',
"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',
"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',
@@ -6465,7 +6840,7 @@ BEGIN
END$$
DELIMITER ;
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',
'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',
@@ -6518,10 +6893,15 @@ CREATE TABLE IF NOT EXISTS fl_loading_evidence (
tenant_id BIGINT UNSIGNED NOT NULL,
inbound_id BIGINT UNSIGNED NOT NULL,
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,
seal_no VARCHAR(64) 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,
created_by BIGINT UNSIGNED NOT 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),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_loading_evidence_type (tenant_id, inbound_id, evidence_type),
UNIQUE KEY uk_loading_evidence_seal (tenant_id, seal_no),
KEY idx_loading_evidence_type (tenant_id, inbound_id, evidence_type, status),
KEY idx_loading_evidence_seal (tenant_id, seal_no, status),
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)
) ENGINE=InnoDB COMMENT='订单装柜四步照片和铅封证据';
@@ -8623,23 +9003,57 @@ WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')
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;
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 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(
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.statistics WHERE table_schema=DATABASE()
AND table_name='fl_loading_evidence' AND index_name='uk_loading_evidence_seal'),
'ALTER TABLE fl_loading_evidence ADD UNIQUE KEY uk_loading_evidence_seal (tenant_id,seal_no)',
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence'
AND index_name='uk_loading_evidence_type'),
'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');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -8792,10 +9206,28 @@ BEGIN
'fl_loading_pick_operation')) <> 18 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:三期出仓、报关或船期表未完整创建';
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'
AND index_name='uk_loading_evidence_seal' AND non_unique=0
AND ((seq_in_index=1 AND column_name='tenant_id') OR (seq_in_index=2 AND column_name='seal_no'))) <> 2
AND column_name IN ('status','void_reason','voided_by','voided_by_name','voided_at')) <> 5
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
WHERE table_schema='baiyue_firstleg' AND table_name='fl_outbound_dispatch'
AND index_name='uk_outbound_dispatch_owner' AND non_unique=0) <> 3
@@ -9625,6 +10057,62 @@ BEGIN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:智能装柜权限 ID 或父子关系不正确';
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$$
DELIMITER ;
CALL assert_unified_final_schema();
@@ -41,55 +41,51 @@ public class PlatformFeeItemController {
@Operation(summary = "查询费用项目")
public ApiResponse<PageResult<FeeItemEntity>> list(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@RequestParam(required = false) String category,
@RequestParam(required = false) String tradeScope,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String status,
@RequestParam(defaultValue = "1") @Min(1) long page,
@RequestParam(defaultValue = "20") @Min(1) @Max(200) long size) {
return ApiResponse.success(service.list(FirstlegScopeGuard.platform(user, accountId),
category, tradeScope, keyword, status, page, size));
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.list(category, tradeScope, keyword, status, page, size));
}
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('firstleg:fee-item:view')")
public ApiResponse<FeeItemEntity> detail(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long id) {
return ApiResponse.success(service.detail(FirstlegScopeGuard.platform(user, accountId), id));
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.detail(id));
}
@PostMapping
@BusinessAudit(event = "FEE_ITEM_CREATE", resourceType = "FEE_ITEM", description = "新增费用项目", captureBefore = false)
@PreAuthorize("hasAuthority('firstleg:fee-item:create')")
public ApiResponse<FeeItemEntity> create(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@Valid @RequestBody SaveRequest request) {
long scope = FirstlegScopeGuard.platform(user, accountId);
return ApiResponse.success(service.create(scope, user.userId(), user.username(), request));
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.create(user.userId(), user.username(), request));
}
@PutMapping("/{id}")
@BusinessAudit(event = "FEE_ITEM_UPDATE", resourceType = "FEE_ITEM", resourceId = "#id", description = "编辑费用项目")
@PreAuthorize("hasAuthority('firstleg:fee-item:edit')")
public ApiResponse<FeeItemEntity> update(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long id,
@Valid @RequestBody SaveRequest request) {
long scope = FirstlegScopeGuard.platform(user, accountId);
return ApiResponse.success(service.update(scope, id, user.userId(), user.username(), request));
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.update(id, user.userId(), user.username(), request));
}
@PatchMapping("/{id}/status")
@BusinessAudit(event = "FEE_ITEM_STATUS_CHANGE", resourceType = "FEE_ITEM", resourceId = "#id", description = "启停费用项目")
@PreAuthorize("hasAuthority('firstleg:fee-item:edit')")
public ApiResponse<Void> changeStatus(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long id,
@Valid @RequestBody StatusRequest request) {
service.changeStatus(FirstlegScopeGuard.platform(user, accountId), id,
user.userId(), user.username(), request.status());
FirstlegScopeGuard.platform(user);
service.changeStatus(id, user.userId(), user.username(), request.status());
return ApiResponse.success(null);
}
@@ -97,9 +93,9 @@ public class PlatformFeeItemController {
@BusinessAudit(event = "FEE_ITEM_DELETE", resourceType = "FEE_ITEM", resourceId = "#id", description = "删除费用项目")
@PreAuthorize("hasAuthority('firstleg:fee-item:delete')")
public ApiResponse<Void> delete(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long id) {
service.delete(FirstlegScopeGuard.platform(user, accountId), id);
FirstlegScopeGuard.platform(user);
service.delete(id);
return ApiResponse.success(null);
}
}
@@ -13,6 +13,7 @@ public final class FeeItemDtos {
public record SaveRequest(
@NotBlank @Size(max = 64) String category,
@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,
@NotNull Boolean pickupPayable,
@NotNull Boolean pickupReceivable,
@@ -21,6 +22,14 @@ public final class FeeItemDtos {
@NotNull Boolean allocationFee,
@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(
@@ -27,7 +27,7 @@ public interface InboundSplitSettlementMapper {
inbound_code AS inboundCode, inbound_role AS inboundRole,
root_inbound_id AS rootInboundId,
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
WHERE id = #{inboundId} AND deleted = 0
FOR UPDATE
@@ -158,7 +158,8 @@ public interface InboundSplitSettlementMapper {
@Param("expectedVersion") int expectedVersion);
@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,
@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,
String inboundCode, String inboundRole, Long rootInboundId,
String processStatus, Integer totalPackageCount, Integer packageCount,
Integer version) {
String splitStatus, Integer version) {
public ParentRow(Long id, Long tenantId, String inboundCode, String inboundRole,
String processStatus, Integer totalPackageCount, Integer packageCount, Integer version) {
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
public class FeeItemRepository {
private static final long PLATFORM_TENANT_ID = 0L;
@Resource
private FeeItemMapper feeItems;
@Resource
@@ -27,11 +28,11 @@ public class FeeItemRepository {
@Resource
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) {
return PageResult.from(feeItems.selectPage(new Page<>(page, size),
new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId)
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(StringUtils.hasText(category), FeeItemEntity::getCategory, category)
.eq(StringUtils.hasText(tradeScope), FeeItemEntity::getTradeScope, tradeScope)
.like(StringUtils.hasText(keyword), FeeItemEntity::getFeeTypeName, keyword)
@@ -41,45 +42,53 @@ public class FeeItemRepository {
.orderByDesc(FeeItemEntity::getId)));
}
public FeeItemEntity findItem(long tenantId, long id) {
public FeeItemEntity findItem(long id) {
return feeItems.selectOne(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId)
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getId, id));
}
public FeeItemEntity findItemByCode(long tenantId, String feeCode) {
public FeeItemEntity findItemByCode(String feeCode) {
return feeItems.selectOne(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId)
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.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>()
.eq(FeeItemEntity::getTenantId, tenantId)
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getTradeScope, tradeScope)
.eq(FeeItemEntity::getFeeTypeName, feeTypeName)
.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) {
item.setTenantId(PLATFORM_TENANT_ID);
feeItems.insert(item);
}
public int updateItem(FeeItemEntity item) {
item.setTenantId(PLATFORM_TENANT_ID);
return feeItems.update(item, new LambdaUpdateWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, item.getTenantId())
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getId, item.getId()));
}
public int deleteItem(long tenantId, long id) {
public int deleteItem(long id) {
return feeItems.delete(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId)
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getId, id));
}
public long countBindings(long tenantId, long feeItemId) {
public long countBindings(long feeItemId) {
return feeDetails.selectCount(new LambdaQueryWrapper<InboundFeeDetailEntity>()
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
.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.repository.FeeItemRepository;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
/** 费用项目主数据应用服务。 */
@Service
public class FeeItemService {
private static final long PLATFORM_TENANT_ID = 0L;
@Resource
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) {
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) {
return required(tenantId, id);
public FeeItemEntity detail(long id) {
return required(id);
}
@Transactional
public FeeItemEntity create(long tenantId, long operatorId, String operatorName, SaveRequest request) {
ensureUnique(tenantId, request, null);
public FeeItemEntity create(long operatorId, String operatorName, SaveRequest request) {
ensureUnique(request, null);
FeeItemEntity value = new FeeItemEntity();
value.setTenantId(tenantId);
value.setTenantId(PLATFORM_TENANT_ID);
copy(value, request);
if (value.getFeeCode() == null || value.getFeeCode().isBlank()) {
value.setFeeCode("MANUAL_" + UUID.randomUUID().toString().replace("-", "").substring(0, 24));
}
value.setStatus("ACTIVE");
value.setCreatedBy(operatorId);
value.setUpdatedBy(operatorId);
value.setCreatedByName(operatorName);
value.setUpdatedByName(operatorName);
repository.insertItem(value);
try {
repository.insertItem(value);
} catch (DuplicateKeyException exception) {
throw conflict("费用项目编码已存在");
}
return value;
}
@Transactional
public FeeItemEntity update(long tenantId, long id, long operatorId, String operatorName,
public FeeItemEntity update(long id, long operatorId, String operatorName,
SaveRequest request) {
FeeItemEntity value = required(tenantId, id);
ensureUnique(tenantId, request, id);
FeeItemEntity value = required(id);
ensureUnique(request, id);
copy(value, request);
value.setUpdatedBy(operatorId);
value.setUpdatedByName(operatorName);
if (repository.updateItem(value) != 1) throw conflict("费用项目已被其他人修改,请刷新后重试");
return required(tenantId, id);
try {
if (repository.updateItem(value) != 1) throw conflict("费用项目已被其他人修改,请刷新后重试");
} catch (DuplicateKeyException exception) {
throw conflict("费用项目编码已存在");
}
return required(id);
}
@Transactional
public void changeStatus(long tenantId, long id, long operatorId, String operatorName, String status) {
FeeItemEntity value = required(tenantId, id);
public void changeStatus(long id, long operatorId, String operatorName, String status) {
FeeItemEntity value = required(id);
value.setStatus(status);
value.setUpdatedBy(operatorId);
value.setUpdatedByName(operatorName);
@@ -62,22 +77,26 @@ public class FeeItemService {
}
@Transactional
public void delete(long tenantId, long id) {
required(tenantId, id);
if (repository.countBindings(tenantId, id) > 0) {
public void delete(long id) {
required(id);
if (repository.countBindings(id) > 0) {
throw conflict("费用项目已被入库费用明细使用,只能停用,不能删除");
}
if (repository.deleteItem(tenantId, id) != 1) throw conflict("费用项目删除冲突");
if (repository.deleteItem(id) != 1) throw conflict("费用项目删除冲突");
}
private FeeItemEntity required(long tenantId, long id) {
FeeItemEntity value = repository.findItem(tenantId, id);
private FeeItemEntity required(long id) {
FeeItemEntity value = repository.findItem(id);
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "费用项目不存在");
return value;
}
private void ensureUnique(long tenantId, SaveRequest request, Long excludedId) {
if (repository.itemNameExists(tenantId, request.tradeScope(), request.feeTypeName(), excludedId)) {
private void ensureUnique(SaveRequest request, Long 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("相同国内国际范围下的费用类型名称已存在");
}
}
@@ -85,6 +104,8 @@ public class FeeItemService {
private static void copy(FeeItemEntity value, SaveRequest request) {
value.setCategory(request.category().trim());
value.setTradeScope(request.tradeScope());
value.setFeeCode(request.feeCode() == null || request.feeCode().isBlank()
? value.getFeeCode() : request.feeCode().trim().toUpperCase());
value.setFeeTypeName(request.feeTypeName().trim());
value.setPickupPayable(request.pickupPayable());
value.setPickupReceivable(request.pickupReceivable());
@@ -22,6 +22,7 @@ import java.util.List;
/** 入库单人工费用明细应用服务。 */
@Service
public class InboundFeeDetailService {
private static final long PLATFORM_TENANT_ID = 0L;
private static final String FREIGHT_FEE_CODE = "FBA_HEAD_FREIGHT";
@Resource
private FeeItemRepository repository;
@@ -56,7 +57,7 @@ public class InboundFeeDetailService {
throw conflict("只有已发送状态的计费快照才能生成客户运费明细");
}
LocalDateTime now = publishedAt == null ? LocalDateTime.now() : publishedAt;
FeeItemEntity feeItem = requiredFreightItem(tenantId, operatorId, operatorName);
FeeItemEntity feeItem = requiredFreightItem(operatorId, operatorName);
InboundFeeItemEntity baseItem = pricingRepository.findBaseItem(tenantId, feeId);
repository.supersedePublishedFreight(tenantId, inboundId, feeId, operatorId, now);
@@ -123,7 +124,7 @@ public class InboundFeeDetailService {
public InboundFeeDetailEntity create(long tenantId, long inboundId, long operatorId,
SaveRequest request) {
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
FeeItemEntity item = requiredActiveItem(tenantId, request.feeItemId());
FeeItemEntity item = requiredActiveItem(request.feeItemId());
InboundFeeDetailEntity value = new InboundFeeDetailEntity();
value.setTenantId(tenantId);
value.setInboundId(inboundId);
@@ -142,7 +143,7 @@ public class InboundFeeDetailService {
long operatorId, SaveRequest request) {
InboundFeeDetailEntity value = requiredEditable(tenantId, inboundId, detailId);
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
FeeItemEntity item = requiredActiveItem(tenantId, request.feeItemId());
FeeItemEntity item = requiredActiveItem(request.feeItemId());
apply(value, inbound, item, request, operatorId);
value.setReviewStatus("PENDING");
value.setReviewerId(null);
@@ -185,21 +186,21 @@ public class InboundFeeDetailService {
return value;
}
private FeeItemEntity requiredActiveItem(long tenantId, long itemId) {
FeeItemEntity value = repository.findItem(tenantId, itemId);
private FeeItemEntity requiredActiveItem(long itemId) {
FeeItemEntity value = repository.findItem(itemId);
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "费用项目不存在");
if (!"ACTIVE".equals(value.getStatus())) throw conflict("费用项目已停用,不能绑定");
return value;
}
private FeeItemEntity requiredFreightItem(long tenantId, long operatorId, String operatorName) {
FeeItemEntity item = repository.findItemByCode(tenantId, FREIGHT_FEE_CODE);
private FeeItemEntity requiredFreightItem(long operatorId, String operatorName) {
FeeItemEntity item = repository.findItemByCode(FREIGHT_FEE_CODE);
if (item != null) {
if (!"ACTIVE".equals(item.getStatus())) throw conflict("系统费用项目FBA头程运费已停用");
return item;
}
FeeItemEntity created = new FeeItemEntity();
created.setTenantId(tenantId);
created.setTenantId(PLATFORM_TENANT_ID);
created.setCategory("头程运费");
created.setTradeScope("INTERNATIONAL");
created.setFeeCode(FREIGHT_FEE_CODE);
@@ -219,8 +220,13 @@ public class InboundFeeDetailService {
repository.insertItem(created);
return created;
} catch (DuplicateKeyException exception) {
FeeItemEntity concurrent = repository.findItemByCode(tenantId, FREIGHT_FEE_CODE);
if (concurrent != null) return concurrent;
FeeItemEntity concurrent = repository.findItemByCode(FREIGHT_FEE_CODE);
if (concurrent != null) {
if (!"ACTIVE".equals(concurrent.getStatus())) {
throw conflict("系统费用项目FBA头程运费已停用");
}
return concurrent;
}
throw exception;
}
}
@@ -98,7 +98,8 @@ public class PdaPickSettlementService {
ParentRow parent = rootInboundId == parentInboundId ? root : splits.lockParent(parentInboundId);
if (parent == null || parent.tenantId() == null || parent.tenantId() <= 0
|| !("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("待拆分主入库单不在可拆分状态");
}
String operationId = "PDA-SPLIT-" + taskId + '-' + parentInboundId;
@@ -109,7 +110,7 @@ public class PdaPickSettlementService {
long loadingPlanId = task.getLoadingPlanId();
Long containerBookingId = splits.containerBookingId(loadingPlanId);
if (containerBookingId == null) throw conflict("PDA任务装柜计划缺少真实柜次");
int parentAvailable = count(parent.packageCount(), "主入库单可用箱数");
int parentAvailable = availablePackageCount(parent);
if (pickedPackageCount <= 0 || pickedPackageCount > parentAvailable) {
throw conflict("本次已拣箱数与主入库单可用箱数不一致");
}
@@ -457,6 +458,14 @@ public class PdaPickSettlementService {
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) {
if (affected != 1) {
throw conflict(resource + "已被并发修改,请刷新后重试");
@@ -3,9 +3,10 @@ SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS fl_fee_item (
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 '费用分类',
trade_scope VARCHAR(20) NOT NULL COMMENT 'DOMESTIC国内、INTERNATIONAL国际',
fee_code VARCHAR(50) NOT NULL COMMENT '稳定费用编码;平台公共主数据固定编码',
fee_type_name VARCHAR(128) NOT NULL COMMENT '费用类型名称',
pickup_payable 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,
version INT NOT NULL DEFAULT 0,
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_name (tenant_id, fee_type_name, deleted)
) ENGINE=InnoDB COMMENT='费用项目主数据';
) ENGINE=InnoDB COMMENT='平台公共费用项目主数据';
CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail (
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',
fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '绑定费用项目ID',
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)
) ENGINE=InnoDB COMMENT='入库单应收应付费用明细';
-- 既有表同步字段注释:tenant_id 当前承载客户 ID,不是上级租户 ID。
SET @fee_tenant_comment = '当前客户ID(客户隔离键);物理字段名沿用 tenant_id,当前不表示上级租户';
-- 费用项目是平台公共主数据;费用明细仍按客户隔离。
SET @fee_tenant_comment = '平台公共费用项目作用域,固定为0;物理字段名沿用 tenant_id';
SET @ddl = IF(
(SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_fee_item'
AND column_name = 'tenant_id' AND column_comment = @fee_tenant_comment) = 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;
SET @fee_tenant_comment = '客户ID(客户隔离键);费用明细引用平台公共费用项目';
SET @ddl = IF(
(SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_fee_detail'
AND column_name = 'tenant_id' AND column_comment = @fee_tenant_comment) = 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;
@@ -22,6 +22,379 @@ DELIMITER ;
CALL phase2_publish_add_column('fl_fee_item', 'fee_code',
"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',
"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',
@@ -74,10 +447,39 @@ END$$
DELIMITER ;
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',
'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',
'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_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.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.yaml.snakeyaml.Yaml;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@@ -34,6 +39,53 @@ class FeeManagementContractTest {
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) {
Method method = Arrays.stream(PlatformFeeItemController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals(methodName))
@@ -41,4 +93,29 @@ class FeeManagementContractTest {
assertThat(method.getAnnotation(PreAuthorize.class).value())
.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 java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
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("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 org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
@@ -76,4 +77,16 @@ class PdaSplitSettlementSqlContractTest {
.contains("#{childOrderId},#{childOrderNo}")
.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_review")
.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("trg_port_schedule_history_before_update")
.contains("trg_port_schedule_history_before_delete")
@@ -59,6 +59,7 @@ class TenantScopedMutationRepositoryTest {
initialize(FulfillmentTaskEntity.class);
initialize(InboundWeightResultEntity.class);
initialize(InboundOrderEntity.class);
initialize(FeeItemEntity.class);
initialize(InboundFeeDetailEntity.class);
}
@@ -122,6 +123,30 @@ class TenantScopedMutationRepositoryTest {
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) {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), "test"), entityType);
@@ -60,10 +60,10 @@ class FeeManagementServiceTest {
@DisplayName("费用项目列表返回分页结果")
void pagesItems() {
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);
assertThat(feeItems.list(10001L, "仓储成本", "INTERNATIONAL", "人工", "ACTIVE", 2, 20))
assertThat(feeItems.list("仓储成本", "INTERNATIONAL", "人工", "ACTIVE", 2, 20))
.isSameAs(expected);
}
@@ -75,21 +75,74 @@ class FeeManagementServiceTest {
return null;
}).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.getAllocationFee()).isTrue();
assertThat(created.getWarehouseReceivable()).isTrue();
assertThat(created.getFeeCode()).startsWith("MANUAL_");
assertThat(created.getTenantId()).isZero();
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
@DisplayName("入库费用绑定项目并生成订单快照")
void bindsInboundFee() {
InboundOrderEntity inbound = inbound();
FeeItemEntity item = item(true, true);
when(repository.findInbound(10001L, 21L)).thenReturn(inbound);
when(repository.findItem(10001L, 11L)).thenReturn(item);
when(repository.findItem(11L)).thenReturn(item);
doAnswer(invocation -> {
((InboundFeeDetailEntity) invocation.getArgument(0)).setId(31L);
return null;
@@ -114,9 +167,10 @@ class FeeManagementServiceTest {
inbound.setCompanyName("客户A有限公司");
when(repository.findInbound(10001L, 21L)).thenReturn(inbound);
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 -> {
FeeItemEntity value = invocation.getArgument(0);
assertThat(value.getTenantId()).isZero();
value.setId(11L);
return null;
}).when(repository).insertItem(any());
@@ -167,7 +221,7 @@ class FeeManagementServiceTest {
FeeItemEntity freightItem = item(true, false);
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();
fee.setId(501L);
@@ -195,7 +249,7 @@ class FeeManagementServiceTest {
@DisplayName("拒绝绑定未启用仓储应收的项目")
void rejectsInapplicableItem() {
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,
detailRequest("RECEIVABLE")))
@@ -152,6 +152,45 @@ class PdaPickSettlementServiceTest {
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
void mixedSkippedInboundIsNotRestored() {
LoadingPickItemEntity picked = item(11L, 21L, 31L, 41L, 8, 8, "PICKED");
@@ -250,7 +289,12 @@ class PdaPickSettlementServiceTest {
}
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) {
@@ -227,31 +227,7 @@ export async function run(context) {
claimed?.workflowStatus === 'IN_PROGRESS' && Boolean(claimed?.assignment?.assigneeId),
`workflow=${claimed?.workflowStatus || 'missing'}`, OPTIONS)
const itemClaimBody = {
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)
// P3-Q11 uses task-level assignment as the single-operator guard; no item claim API exists.
const canonicalPick = await request(context, 'POST',
`${pdaBase}/tasks/${canonicalTaskId}/items/${id(canonicalItem.id)}/pick`, {