diff --git a/deploy/sql/README.md b/deploy/sql/README.md index b67910b54..c68bb91a8 100644 --- a/deploy/sql/README.md +++ b/deploy/sql/README.md @@ -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 diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 85e98a88e..e7f6acec2 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -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(); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformFeeItemController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformFeeItemController.java index 362297b0d..e39e16442 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformFeeItemController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformFeeItemController.java @@ -41,55 +41,51 @@ public class PlatformFeeItemController { @Operation(summary = "查询费用项目") public ApiResponse> 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 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 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 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 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 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); } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/FeeItemDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/FeeItemDtos.java index 01a6d8a4f..e5f608dbb 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/FeeItemDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/FeeItemDtos.java @@ -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( diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundSplitSettlementMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundSplitSettlementMapper.java index bb95ddd06..47dd33d21 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundSplitSettlementMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundSplitSettlementMapper.java @@ -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); } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/FeeItemRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/FeeItemRepository.java index 439b2110b..5c82a61b4 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/FeeItemRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/FeeItemRepository.java @@ -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 listItems(long tenantId, String category, String tradeScope, + public PageResult listItems(String category, String tradeScope, String keyword, String status, long page, long size) { return PageResult.from(feeItems.selectPage(new Page<>(page, size), new LambdaQueryWrapper() - .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() - .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() - .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() - .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() + .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() - .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() - .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() - .eq(InboundFeeDetailEntity::getTenantId, tenantId) .eq(InboundFeeDetailEntity::getFeeItemId, feeItemId)); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FeeItemService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FeeItemService.java index f9ccba6ad..f049883cb 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FeeItemService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FeeItemService.java @@ -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 list(long tenantId, String category, String tradeScope, + public PageResult 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()); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java index 3a843f4a5..3f778f211 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java @@ -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; } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java index 554ee2ab4..8b06113d8 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java @@ -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 + "已被并发修改,请刷新后重试"); diff --git a/firstleg/src/main/resources/db/phase2-fee-item-and-inbound-detail.sql b/firstleg/src/main/resources/db/phase2-fee-item-and-inbound-detail.sql index 8971af49b..1fdcfe01e 100644 --- a/firstleg/src/main/resources/db/phase2-fee-item-and-inbound-detail.sql +++ b/firstleg/src/main/resources/db/phase2-fee-item-and-inbound-detail.sql @@ -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; diff --git a/firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql b/firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql index 2c77f22c6..b47ce73c4 100644 --- a/firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql +++ b/firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql @@ -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; diff --git a/firstleg/src/main/resources/openapi/fee-item-v1.yaml b/firstleg/src/main/resources/openapi/fee-item-v1.yaml new file mode 100644 index 000000000..76988399a --- /dev/null +++ b/firstleg/src/main/resources/openapi/fee-item-v1.yaml @@ -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} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java index 3ac2f303f..e229728ff 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java @@ -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 document = stringMap(new Yaml().load(source)); + assertThat(document.get("openapi")).isEqualTo("3.0.3"); + assertThat(source).doesNotContain("accountId", "AccountId"); + + Map 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 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 paths, String path, + String operation, String schema) { + Map endpoint = stringMap(paths.get(path)); + Map response = stringMap(stringMap(endpoint.get(operation)).get("responses")); + Map success = stringMap(response.get("200")); + Map content = stringMap(success.get("content")); + Map mediaType = stringMap(content.get("application/json")); + assertThat(stringMap(mediaType.get("schema"))) + .containsEntry("$ref", "#/components/schemas/" + schema); + } + + private static Map properties(Map schemas, String schema) { + return stringMap(stringMap(schemas.get(schema)).get("properties")); + } + + @SuppressWarnings("unchecked") + private static Map stringMap(Object value) { + return (Map) value; + } + + @SuppressWarnings("unchecked") + private static List list(Object value) { + return (List) value; + } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/loading/repository/LoadingEvidenceSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/loading/repository/LoadingEvidenceSqlContractTest.java index 4125a5414..71511c16b 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/loading/repository/LoadingEvidenceSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/loading/repository/LoadingEvidenceSqlContractTest.java @@ -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"); + } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java index 43025ed3b..a2c1d9287 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java @@ -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')"); + } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java index 776f638b6..f08c26ecd 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java @@ -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") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/TenantScopedMutationRepositoryTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/TenantScopedMutationRepositoryTest.java index 6d63d8b23..681291bf9 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/TenantScopedMutationRepositoryTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/TenantScopedMutationRepositoryTest.java @@ -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 = 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 = 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); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java index d2354a815..a6ab27163 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java @@ -60,10 +60,10 @@ class FeeManagementServiceTest { @DisplayName("费用项目列表返回分页结果") void pagesItems() { PageResult 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"))) diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java index fabb58004..f391aec20 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java @@ -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) { diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index 2dd7a46c7..b026f9523 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -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`, {