优化
This commit is contained in:
@@ -7541,6 +7541,406 @@ DROP PROCEDURE IF EXISTS phase3_assert_inbound_parent_child_schema;
|
||||
CALL assert_loading_pick_task_shapes(1);
|
||||
DROP PROCEDURE IF EXISTS assert_loading_pick_task_shapes;
|
||||
|
||||
-- ============================================================
|
||||
-- 三期服务中心订舱与拖车预约
|
||||
-- 来源:firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql
|
||||
-- 来源:firstleg/src/main/resources/db/phase3-truck-appointment-schema.sql
|
||||
-- ============================================================
|
||||
SET NAMES utf8mb4;
|
||||
USE baiyue_firstleg;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
booking_no VARCHAR(40) NOT NULL,
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'DRAFT',
|
||||
customer_id BIGINT UNSIGNED NOT NULL COMMENT '当前产品客户ID',
|
||||
customer_name VARCHAR(128) NOT NULL,
|
||||
bill_of_lading_no VARCHAR(128) NULL,
|
||||
shipping_company_id BIGINT UNSIGNED NULL,
|
||||
shipping_company_name VARCHAR(128) NULL,
|
||||
booking_agent_id BIGINT UNSIGNED NULL,
|
||||
booking_agent_name VARCHAR(128) NULL,
|
||||
vessel_name VARCHAR(128) NULL,
|
||||
voyage_no VARCHAR(64) NULL,
|
||||
departure_port_code VARCHAR(32) NULL,
|
||||
departure_port_name VARCHAR(128) NULL,
|
||||
destination_port_code VARCHAR(32) NULL,
|
||||
destination_port_name VARCHAR(128) NULL,
|
||||
place_of_delivery VARCHAR(128) NULL,
|
||||
service_term VARCHAR(32) NULL,
|
||||
freight_term VARCHAR(32) NULL,
|
||||
estimated_sailing_at DATETIME(3) NULL,
|
||||
estimated_arrival_at DATETIME(3) NULL,
|
||||
pending_vessel_name VARCHAR(128) NULL,
|
||||
pending_voyage_no VARCHAR(64) NULL,
|
||||
pending_estimated_sailing_at DATETIME(3) NULL,
|
||||
pending_estimated_arrival_at DATETIME(3) NULL,
|
||||
change_reason VARCHAR(1000) NULL,
|
||||
failure_reason VARCHAR(1000) NULL,
|
||||
cancellation_reason VARCHAR(1000) NULL,
|
||||
remark VARCHAR(1000) NULL,
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT UNSIGNED NOT NULL,
|
||||
updated_by_name VARCHAR(64) NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ocean_booking_no (booking_no),
|
||||
KEY idx_ocean_booking_customer (customer_id,status,updated_at,id),
|
||||
KEY idx_ocean_booking_voyage (vessel_name,voyage_no,updated_at),
|
||||
CONSTRAINT chk_ocean_booking_customer CHECK (customer_id > 0),
|
||||
CONSTRAINT chk_ocean_booking_status CHECK (status IN
|
||||
('DRAFT','SUBMITTED','CONFIRMED','RELEASED','FAILED','CHANGE_PENDING','CANCELLED')),
|
||||
CONSTRAINT chk_ocean_booking_schedule CHECK
|
||||
(estimated_sailing_at IS NULL OR estimated_arrival_at IS NULL OR estimated_arrival_at >= estimated_sailing_at),
|
||||
CONSTRAINT chk_ocean_booking_pending_schedule CHECK
|
||||
(pending_estimated_sailing_at IS NULL OR pending_estimated_arrival_at IS NULL
|
||||
OR pending_estimated_arrival_at >= pending_estimated_sailing_at)
|
||||
) ENGINE=InnoDB COMMENT='独立海运订舱聚合;柜次仍由fl_container_booking承载';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking_party (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL,
|
||||
party_type VARCHAR(24) NOT NULL COMMENT 'SHIPPER/CONSIGNEE/NOTIFY_PARTY',
|
||||
party_name VARCHAR(255) NOT NULL,
|
||||
address VARCHAR(1000) NULL,
|
||||
contact_name VARCHAR(128) NULL,
|
||||
contact_phone VARCHAR(64) NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ocean_booking_party (ocean_booking_id,party_type),
|
||||
CONSTRAINT fk_ocean_booking_party FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id),
|
||||
CONSTRAINT chk_ocean_booking_party_type CHECK (party_type IN ('SHIPPER','CONSIGNEE','NOTIFY_PARTY'))
|
||||
) ENGINE=InnoDB COMMENT='订舱提单关系方快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_requirement (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL,
|
||||
container_type VARCHAR(32) NOT NULL,
|
||||
container_kind VARCHAR(32) NOT NULL,
|
||||
quantity INT UNSIGNED NOT NULL,
|
||||
sort_no INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_ocean_requirement_booking (ocean_booking_id,sort_no,id),
|
||||
CONSTRAINT fk_ocean_requirement_booking FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id),
|
||||
CONSTRAINT chk_ocean_requirement_quantity CHECK (quantity > 0)
|
||||
) ENGINE=InnoDB COMMENT='订舱箱型箱种及数量需求';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_allocation (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL,
|
||||
requirement_id BIGINT UNSIGNED NOT NULL,
|
||||
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '明确引用现有柜次ID',
|
||||
allocated_by BIGINT UNSIGNED NOT NULL,
|
||||
allocated_by_name VARCHAR(64) NULL,
|
||||
allocated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ocean_allocation_container (container_booking_id),
|
||||
KEY idx_ocean_allocation_booking (ocean_booking_id,requirement_id,allocated_at),
|
||||
CONSTRAINT fk_ocean_allocation_booking FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id),
|
||||
CONSTRAINT fk_ocean_allocation_requirement FOREIGN KEY (requirement_id)
|
||||
REFERENCES fl_ocean_booking_container_requirement(id),
|
||||
CONSTRAINT fk_ocean_allocation_container FOREIGN KEY (container_booking_id) REFERENCES fl_container_booking(id)
|
||||
) ENGINE=InnoDB COMMENT='订舱需求与现有柜次的唯一分配关系';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking_status_history (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL,
|
||||
from_status VARCHAR(24) NULL,
|
||||
to_status VARCHAR(24) NOT NULL,
|
||||
action VARCHAR(40) NOT NULL,
|
||||
reason VARCHAR(1000) NULL,
|
||||
before_snapshot JSON NULL,
|
||||
after_snapshot JSON NOT NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_ocean_history_booking (ocean_booking_id,created_at,id),
|
||||
CONSTRAINT fk_ocean_history_booking FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id)
|
||||
) ENGINE=InnoDB COMMENT='海运订舱不可变状态历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking_operation (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
ocean_booking_id BIGINT UNSIGNED NULL COMMENT '创建请求完成前允许为空',
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operation VARCHAR(40) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
request_hash CHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING',
|
||||
result_json JSON NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
completed_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ocean_operation (operator_id,operation,idempotency_key),
|
||||
KEY idx_ocean_operation_booking (ocean_booking_id,created_at),
|
||||
CONSTRAINT fk_ocean_operation_booking FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id),
|
||||
CONSTRAINT chk_ocean_operation_status CHECK (status IN ('PROCESSING','SUCCESS'))
|
||||
) ENGINE=InnoDB COMMENT='海运订舱写操作幂等与请求哈希';
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_ocean_booking_before_update;
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER trg_ocean_booking_before_update BEFORE UPDATE ON fl_ocean_booking FOR EACH ROW
|
||||
BEGIN
|
||||
IF OLD.status IN ('RELEASED','CANCELLED') THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='已放舱或已取消订舱不可修改';
|
||||
END IF;
|
||||
IF OLD.status <> NEW.status AND NOT (
|
||||
(OLD.status='DRAFT' AND NEW.status IN ('SUBMITTED','CANCELLED'))
|
||||
OR (OLD.status='SUBMITTED' AND NEW.status IN ('CONFIRMED','FAILED','CANCELLED'))
|
||||
OR (OLD.status='CONFIRMED' AND NEW.status IN ('RELEASED','CHANGE_PENDING','CANCELLED'))
|
||||
OR (OLD.status='CHANGE_PENDING' AND NEW.status IN ('CONFIRMED','FAILED','CANCELLED'))
|
||||
OR (OLD.status='FAILED' AND NEW.status='CANCELLED')) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海运订舱状态迁移非法';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_ocean_party_before_insert;
|
||||
DROP TRIGGER IF EXISTS trg_ocean_party_before_update;
|
||||
DROP TRIGGER IF EXISTS trg_ocean_party_before_delete;
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER trg_ocean_party_before_insert BEFORE INSERT ON fl_ocean_booking_party FOR EACH ROW
|
||||
BEGIN IF (SELECT status FROM fl_ocean_booking WHERE id=NEW.ocean_booking_id) <> 'DRAFT' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='仅草稿可维护订舱方'; END IF; END$$
|
||||
CREATE TRIGGER trg_ocean_party_before_update BEFORE UPDATE ON fl_ocean_booking_party FOR EACH ROW
|
||||
BEGIN IF (SELECT status FROM fl_ocean_booking WHERE id=OLD.ocean_booking_id) <> 'DRAFT' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='仅草稿可维护订舱方'; END IF; END$$
|
||||
CREATE TRIGGER trg_ocean_party_before_delete BEFORE DELETE ON fl_ocean_booking_party FOR EACH ROW
|
||||
BEGIN IF (SELECT status FROM fl_ocean_booking WHERE id=OLD.ocean_booking_id) <> 'DRAFT' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='仅草稿可维护订舱方'; END IF; END$$
|
||||
DELIMITER ;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_ocean_requirement_before_insert;
|
||||
DROP TRIGGER IF EXISTS trg_ocean_requirement_before_update;
|
||||
DROP TRIGGER IF EXISTS trg_ocean_requirement_before_delete;
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER trg_ocean_requirement_before_insert BEFORE INSERT ON fl_ocean_booking_container_requirement FOR EACH ROW
|
||||
BEGIN IF (SELECT status FROM fl_ocean_booking WHERE id=NEW.ocean_booking_id) <> 'DRAFT' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='仅草稿可维护箱型需求'; END IF; END$$
|
||||
CREATE TRIGGER trg_ocean_requirement_before_update BEFORE UPDATE ON fl_ocean_booking_container_requirement FOR EACH ROW
|
||||
BEGIN IF (SELECT status FROM fl_ocean_booking WHERE id=OLD.ocean_booking_id) <> 'DRAFT' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='仅草稿可维护箱型需求'; END IF; END$$
|
||||
CREATE TRIGGER trg_ocean_requirement_before_delete BEFORE DELETE ON fl_ocean_booking_container_requirement FOR EACH ROW
|
||||
BEGIN IF (SELECT status FROM fl_ocean_booking WHERE id=OLD.ocean_booking_id) <> 'DRAFT' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='仅草稿可维护箱型需求'; END IF; END$$
|
||||
DELIMITER ;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_ocean_allocation_before_insert;
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER trg_ocean_allocation_before_insert BEFORE INSERT ON fl_ocean_booking_container_allocation FOR EACH ROW
|
||||
BEGIN
|
||||
IF (SELECT ocean_booking_id FROM fl_ocean_booking_container_requirement WHERE id=NEW.requirement_id) <> NEW.ocean_booking_id THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='箱型需求不属于当前订舱';
|
||||
END IF;
|
||||
IF (SELECT status FROM fl_ocean_booking WHERE id=NEW.ocean_booking_id) NOT IN
|
||||
('SUBMITTED','CONFIRMED','CHANGE_PENDING') THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='当前订舱状态不可分配柜次';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_ocean_history_before_update;
|
||||
DROP TRIGGER IF EXISTS trg_ocean_history_before_delete;
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER trg_ocean_history_before_update BEFORE UPDATE ON fl_ocean_booking_status_history FOR EACH ROW
|
||||
BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='订舱状态历史不可修改'; END$$
|
||||
CREATE TRIGGER trg_ocean_history_before_delete BEFORE DELETE ON fl_ocean_booking_status_history FOR EACH ROW
|
||||
BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='订舱状态历史不可删除'; END$$
|
||||
DELIMITER ;
|
||||
|
||||
USE baiyue_iam;
|
||||
DROP PROCEDURE IF EXISTS assert_ocean_booking_permission_ids_available;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE assert_ocean_booking_permission_ids_available()
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM iam_permission
|
||||
WHERE (id BETWEEN 5102140 AND 5102149
|
||||
AND permission_code <> CASE id
|
||||
WHEN 5102140 THEN 'platform:firstleg:ocean-booking:list'
|
||||
WHEN 5102141 THEN 'platform:firstleg:ocean-booking:view'
|
||||
WHEN 5102142 THEN 'platform:firstleg:ocean-booking:create'
|
||||
WHEN 5102143 THEN 'platform:firstleg:ocean-booking:edit'
|
||||
WHEN 5102144 THEN 'platform:firstleg:ocean-booking:submit'
|
||||
WHEN 5102145 THEN 'platform:firstleg:ocean-booking:confirm'
|
||||
WHEN 5102146 THEN 'platform:firstleg:ocean-booking:release'
|
||||
WHEN 5102147 THEN 'platform:firstleg:ocean-booking:cancel'
|
||||
WHEN 5102148 THEN 'platform:firstleg:ocean-booking:change'
|
||||
WHEN 5102149 THEN 'platform:firstleg:ocean-booking:allocate'
|
||||
END)
|
||||
OR (permission_code LIKE 'platform:firstleg:ocean-booking:%'
|
||||
AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR id NOT BETWEEN 5102140 AND 5102149))
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '三期订舱权限迁移失败:5102140-5102149 ID 或权限码被其他语义占用';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL assert_ocean_booking_permission_ids_available();
|
||||
DROP PROCEDURE IF EXISTS assert_ocean_booking_permission_ids_available;
|
||||
|
||||
INSERT INTO iam_permission
|
||||
(id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,
|
||||
route_path,http_method,sort_no,status,built_in,deleted)
|
||||
VALUES
|
||||
(5102140,0,'PLATFORM','MENU',5102000,'platform:firstleg:ocean-booking:list','订舱管理','/first-leg/ocean-bookings','GET',80,'ACTIVE',1,0),
|
||||
(5102141,0,'PLATFORM','BUTTON',5102140,'platform:firstleg:ocean-booking:view','查看订舱',NULL,'GET',10,'ACTIVE',1,0),
|
||||
(5102142,0,'PLATFORM','BUTTON',5102140,'platform:firstleg:ocean-booking:create','创建订舱',NULL,'POST',20,'ACTIVE',1,0),
|
||||
(5102143,0,'PLATFORM','BUTTON',5102140,'platform:firstleg:ocean-booking:edit','编辑订舱',NULL,'PUT',30,'ACTIVE',1,0),
|
||||
(5102144,0,'PLATFORM','BUTTON',5102140,'platform:firstleg:ocean-booking:submit','提交订舱',NULL,'POST',40,'ACTIVE',1,0),
|
||||
(5102145,0,'PLATFORM','BUTTON',5102140,'platform:firstleg:ocean-booking:confirm','确认订舱',NULL,'POST',50,'ACTIVE',1,0),
|
||||
(5102146,0,'PLATFORM','BUTTON',5102140,'platform:firstleg:ocean-booking:release','确认放舱',NULL,'POST',60,'ACTIVE',1,0),
|
||||
(5102147,0,'PLATFORM','BUTTON',5102140,'platform:firstleg:ocean-booking:cancel','取消订舱',NULL,'POST',70,'ACTIVE',1,0),
|
||||
(5102148,0,'PLATFORM','BUTTON',5102140,'platform:firstleg:ocean-booking:change','变更船期',NULL,'POST',80,'ACTIVE',1,0),
|
||||
(5102149,0,'PLATFORM','BUTTON',5102140,'platform:firstleg:ocean-booking:allocate','分配柜次',NULL,'POST',90,'ACTIVE',1,0)
|
||||
ON DUPLICATE KEY UPDATE tenant_id=0,app_code='PLATFORM',permission_type=VALUES(permission_type),
|
||||
permission_code=VALUES(permission_code),permission_name=VALUES(permission_name),parent_id=VALUES(parent_id),
|
||||
route_path=VALUES(route_path),http_method=VALUES(http_method),sort_no=VALUES(sort_no),status='ACTIVE',built_in=1,deleted=0;
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission(role_id,permission_id,tenant_id)
|
||||
SELECT role.id,permission.id,0 FROM iam_role role JOIN iam_permission permission
|
||||
ON permission.tenant_id=0 AND permission.permission_code LIKE 'platform:firstleg:ocean-booking:%'
|
||||
WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR');
|
||||
|
||||
USE baiyue_firstleg;
|
||||
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
CREATE TABLE IF NOT EXISTS fl_truck_appointment (
|
||||
id BIGINT UNSIGNED NOT NULL, appointment_no VARCHAR(40) NOT NULL, outbound_order_id BIGINT UNSIGNED NULL,
|
||||
ocean_booking_id BIGINT UNSIGNED NULL, container_booking_id BIGINT UNSIGNED NULL, loading_plan_id BIGINT UNSIGNED NULL,
|
||||
warehouse_id BIGINT UNSIGNED NULL, customer_id BIGINT UNSIGNED NULL, customer_name VARCHAR(128) NULL,
|
||||
carrier_id BIGINT UNSIGNED NULL, carrier_name VARCHAR(128) NULL, fleet_contact VARCHAR(64) NULL, fleet_phone VARCHAR(32) NULL,
|
||||
driver_name VARCHAR(64) NULL, driver_phone VARCHAR(32) NULL, driver_id_card VARCHAR(32) NULL, vehicle_no VARCHAR(32) NULL,
|
||||
store_name VARCHAR(128) NULL, store_contact VARCHAR(64) NULL, store_phone VARCHAR(32) NULL, store_address VARCHAR(255) NULL,
|
||||
store_city VARCHAR(64) NULL, store_zipcode VARCHAR(16) NULL, package_count INT UNSIGNED NULL, gross_weight_kg DECIMAL(18,3) NULL,
|
||||
volume_cbm DECIMAL(18,4) NULL, pallet_count INT UNSIGNED NULL, po_no VARCHAR(255) NULL, planned_loading_at DATETIME(3) NULL,
|
||||
actual_loading_at DATETIME(3) NULL, transit_minutes INT UNSIGNED NULL, payable_amount DECIMAL(18,2) NULL, receivable_amount DECIMAL(18,2) NULL,
|
||||
currency VARCHAR(8) NOT NULL DEFAULT 'RMB', status VARCHAR(32) NOT NULL DEFAULT 'DRAFT', revision INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0, remark VARCHAR(1000) NULL, 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_truck_appointment_no(appointment_no),
|
||||
KEY idx_truck_appointment_status(status,updated_at), KEY idx_truck_appointment_container(container_booking_id), KEY idx_truck_appointment_customer(customer_id),
|
||||
CONSTRAINT chk_truck_appointment_status CHECK(status IN ('DRAFT','PENDING_DISPATCH','DISPATCHED','PICKUP_IN_PROGRESS','ARRIVED_WAREHOUSE','LOADING','DEPARTED','COMPLETED','RESCHEDULED','EXCEPTION','CANCELLED'))
|
||||
) ENGINE=InnoDB COMMENT='三期拖车预约派车聚合';
|
||||
CREATE TABLE IF NOT EXISTS fl_truck_appointment_history (
|
||||
id BIGINT UNSIGNED NOT NULL, appointment_id BIGINT UNSIGNED NOT NULL, revision INT UNSIGNED NOT NULL, action VARCHAR(32) NOT NULL,
|
||||
from_status VARCHAR(32) NULL, to_status VARCHAR(32) NULL, reason VARCHAR(500) NULL, operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_name VARCHAR(64) NULL, request_id VARCHAR(128) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY(id), KEY idx_truck_history(appointment_id,revision)
|
||||
) ENGINE=InnoDB COMMENT='拖车状态与改约历史';
|
||||
CREATE TABLE IF NOT EXISTS fl_truck_appointment_attachment (
|
||||
id BIGINT UNSIGNED NOT NULL, appointment_id BIGINT UNSIGNED NOT NULL, file_id BIGINT UNSIGNED NOT NULL, attachment_type VARCHAR(32) NOT NULL,
|
||||
file_name VARCHAR(255) NULL, file_hash CHAR(64) NULL, visibility VARCHAR(24) NOT NULL DEFAULT 'INTERNAL', uploaded_by BIGINT UNSIGNED NOT NULL,
|
||||
uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY(id), UNIQUE KEY uk_truck_attachment(appointment_id,file_id), KEY idx_truck_attachment(appointment_id)
|
||||
) ENGINE=InnoDB COMMENT='拖车附件文件引用';
|
||||
CREATE TABLE IF NOT EXISTS fl_truck_appointment_operation (
|
||||
id BIGINT UNSIGNED NOT NULL, appointment_id BIGINT UNSIGNED NOT NULL, idempotency_key VARCHAR(128) NOT NULL, request_hash CHAR(64) NOT NULL,
|
||||
action VARCHAR(32) NOT NULL, response_snapshot JSON NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY(id), UNIQUE KEY uk_truck_operation(idempotency_key), KEY idx_truck_operation_appointment(appointment_id)
|
||||
) ENGINE=InnoDB COMMENT='拖车操作幂等记录';
|
||||
|
||||
USE baiyue_iam;
|
||||
DROP PROCEDURE IF EXISTS assert_truck_appointment_permission_ids_available;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE assert_truck_appointment_permission_ids_available()
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM iam_permission
|
||||
WHERE (id BETWEEN 5102150 AND 5102154
|
||||
AND permission_code <> CASE id
|
||||
WHEN 5102150 THEN 'platform:firstleg:truck-appointment:list'
|
||||
WHEN 5102151 THEN 'platform:firstleg:truck-appointment:view'
|
||||
WHEN 5102152 THEN 'platform:firstleg:truck-appointment:create'
|
||||
WHEN 5102153 THEN 'platform:firstleg:truck-appointment:edit'
|
||||
WHEN 5102154 THEN 'platform:firstleg:truck-appointment:operate'
|
||||
END)
|
||||
OR (permission_code LIKE 'platform:firstleg:truck-appointment:%'
|
||||
AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR id NOT BETWEEN 5102150 AND 5102154))
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '三期拖车权限迁移失败:5102150-5102154 ID 或权限码被其他语义占用';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL assert_truck_appointment_permission_ids_available();
|
||||
DROP PROCEDURE IF EXISTS assert_truck_appointment_permission_ids_available;
|
||||
|
||||
INSERT INTO iam_permission
|
||||
(id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,
|
||||
route_path,http_method,sort_no,status,built_in,deleted)
|
||||
VALUES
|
||||
(5102150,0,'PLATFORM','MENU',5102000,'platform:firstleg:truck-appointment:list','拖车预约','/first-leg/truck-appointments','GET',90,'ACTIVE',1,0),
|
||||
(5102151,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:view','查看拖车预约',NULL,'GET',10,'ACTIVE',1,0),
|
||||
(5102152,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:create','创建拖车预约',NULL,'POST',20,'ACTIVE',1,0),
|
||||
(5102153,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:edit','编辑拖车预约',NULL,'PUT',30,'ACTIVE',1,0),
|
||||
(5102154,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:operate','操作拖车预约',NULL,'POST',40,'ACTIVE',1,0)
|
||||
ON DUPLICATE KEY UPDATE tenant_id=0,app_code='PLATFORM',permission_type=VALUES(permission_type),
|
||||
permission_code=VALUES(permission_code),permission_name=VALUES(permission_name),parent_id=VALUES(parent_id),
|
||||
route_path=VALUES(route_path),http_method=VALUES(http_method),sort_no=VALUES(sort_no),status='ACTIVE',built_in=1,deleted=0;
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission(role_id,permission_id,tenant_id)
|
||||
SELECT role.id,permission.id,0 FROM iam_role role JOIN iam_permission permission
|
||||
ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
AND permission.permission_code LIKE 'platform:firstleg:truck-appointment:%'
|
||||
WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR');
|
||||
|
||||
USE baiyue_firstleg;
|
||||
|
||||
USE baiyue_iam;
|
||||
INSERT INTO iam_permission
|
||||
(id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,
|
||||
route_path,http_method,sort_no,status,built_in,deleted)
|
||||
VALUES
|
||||
(5102150,0,'PLATFORM','MENU',5102000,'platform:firstleg:truck-appointment:list','拖车预约','/trucking','GET',90,'ACTIVE',1,0),
|
||||
(5102151,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:view','查看拖车预约',NULL,'GET',10,'ACTIVE',1,0),
|
||||
(5102152,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:create','创建拖车预约',NULL,'POST',20,'ACTIVE',1,0),
|
||||
(5102153,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:edit','编辑拖车预约',NULL,'PUT',30,'ACTIVE',1,0),
|
||||
(5102154,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:operate','操作拖车状态',NULL,'POST',40,'ACTIVE',1,0)
|
||||
ON DUPLICATE KEY UPDATE permission_name=VALUES(permission_name),parent_id=VALUES(parent_id),
|
||||
route_path=VALUES(route_path),http_method=VALUES(http_method),sort_no=VALUES(sort_no),status='ACTIVE',built_in=1,deleted=0;
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission(role_id,permission_id,tenant_id)
|
||||
SELECT role.id,permission.id,0 FROM iam_role role JOIN iam_permission permission
|
||||
ON permission.tenant_id=0 AND permission.permission_code LIKE 'platform:firstleg:truck-appointment:%'
|
||||
WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR');
|
||||
|
||||
USE baiyue_firstleg;
|
||||
|
||||
DROP PROCEDURE IF EXISTS assert_phase3_service_center_schema;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE assert_phase3_service_center_schema()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.tables
|
||||
WHERE table_schema='baiyue_firstleg'
|
||||
AND table_name IN ('fl_ocean_booking','fl_ocean_booking_party',
|
||||
'fl_ocean_booking_container_requirement','fl_ocean_booking_container_allocation',
|
||||
'fl_ocean_booking_status_history','fl_ocean_booking_operation',
|
||||
'fl_truck_appointment','fl_truck_appointment_history',
|
||||
'fl_truck_appointment_attachment','fl_truck_appointment_operation')) <> 10 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:订舱或拖车预约表未完整创建';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission
|
||||
WHERE tenant_id=0 AND app_code='PLATFORM' AND id BETWEEN 5102140 AND 5102149
|
||||
AND permission_code LIKE 'platform:firstleg:ocean-booking:%') <> 10
|
||||
OR (SELECT COUNT(*) FROM baiyue_iam.iam_permission
|
||||
WHERE tenant_id=0 AND app_code='PLATFORM' AND id BETWEEN 5102150 AND 5102154
|
||||
AND permission_code LIKE 'platform:firstleg:truck-appointment:%') <> 5 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.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')
|
||||
AND p.permission_code LIKE 'platform:firstleg:ocean-booking:%') <> 10
|
||||
OR (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.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')
|
||||
AND p.permission_code LIKE 'platform:firstleg:truck-appointment:%') <> 5 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:订舱或拖车预约角色授权未完整写入';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL assert_phase3_service_center_schema();
|
||||
DROP PROCEDURE IF EXISTS assert_phase3_service_center_schema;
|
||||
|
||||
|
||||
-- WAIT_SPLIT 是二期当前有效终点;统一迁移不得改写主表或历史状态。
|
||||
SELECT 'PASS' AS migration_status,
|
||||
'统一增量全部执行完成;箱号映射、单箱测量、三期出仓、费用、公共资源及可靠审计结构校验通过' AS message;
|
||||
'统一增量全部执行完成;箱号映射、单箱测量、三期订舱、拖车预约、出仓、费用、公共资源及可靠审计结构校验通过' AS message;
|
||||
|
||||
@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baiyue.common.
|
||||
public PageResult<TruckAppointmentEntity> page(Query q){return repo.page(q);} public TruckAppointmentEntity detail(long id){return required(id);}
|
||||
@Transactional public TruckAppointmentEntity create(long uid,String name,SaveRequest r){if(r.expectedVersion()!=0)throw bad("新增 expectedVersion 必须为0"); TruckAppointmentEntity e=new TruckAppointmentEntity(); e.setId(IdWorker.getId()); e.setAppointmentNo("TA"+e.getId()); e.setStatus(TruckAppointmentStatus.DRAFT); e.setRevision(0); e.setVersion(0); e.setCreatedAt(LocalDateTime.now()); e.setUpdatedAt(e.getCreatedAt()); copy(e,r); repo.insert(e); record(e,null,e.getStatus().name(),"CREATE",null,uid,name); return e;}
|
||||
@Transactional public TruckAppointmentEntity edit(long id,long uid,String name,SaveRequest r){TruckAppointmentEntity e=required(id); checkVersion(e,r.expectedVersion()); if(!(e.getStatus()==TruckAppointmentStatus.DRAFT||e.getStatus()==TruckAppointmentStatus.PENDING_DISPATCH||e.getStatus()==TruckAppointmentStatus.RESCHEDULED))throw conflict("当前状态不可编辑"); copy(e,r); if(repo.updateEdit(e,r.expectedVersion())!=1)throw conflict("版本已变化,请刷新后重试"); record(e,e.getStatus().name(),e.getStatus().name(),"EDIT",null,uid,name); return required(id);}
|
||||
@Transactional public TruckAppointmentEntity action(long id,String action,ActionRequest r,String idem,String hash,long uid,String name){TruckAppointmentEntity e=required(id); checkVersion(e,r.expectedVersion()); TruckAppointmentStatus from=e.getStatus(),to=next(from,action); if("RESCHEDULE".equals(action)){throw conflict("改约请使用专用接口");} if(repo.transition(id,from.name(),to.name(),e.getVersion())!=1)throw conflict("状态或版本已变化,请刷新后重试"); e.setStatus(to);e.setVersion(e.getVersion()+1);e.setRevision(e.getRevision()+1); record(e,from.name(),to.name(),action,r.reason(),uid,name); operation(e,action,idem,hash); return e;}
|
||||
@Transactional public TruckAppointmentEntity action(long id,String action,ActionRequest r,String idem,String hash,long uid,String name){TruckAppointmentEntity e=required(id); checkVersion(e,r.expectedVersion()); if("RESCHEDULE".equals(action)){throw conflict("改约请使用专用接口");} TruckAppointmentStatus from=e.getStatus(),to=next(from,action); if(repo.transition(id,from.name(),to.name(),e.getVersion())!=1)throw conflict("状态或版本已变化,请刷新后重试"); e.setStatus(to);e.setVersion(e.getVersion()+1);e.setRevision(e.getRevision()+1); record(e,from.name(),to.name(),action,r.reason(),uid,name); operation(e,action,idem,hash); return e;}
|
||||
@Transactional public TruckAppointmentEntity reschedule(long id,LocalDateTime at,ActionRequest r,long uid,String name){TruckAppointmentEntity e=required(id);checkVersion(e,r.expectedVersion()); if(e.getStatus()==TruckAppointmentStatus.COMPLETED||e.getStatus()==TruckAppointmentStatus.CANCELLED)throw conflict("当前状态不可改约"); String from=e.getStatus().name(); if(repo.reschedule(id,at,r.reason(),e.getVersion())!=1)throw conflict("状态或版本已变化,请刷新后重试");e.setStatus(TruckAppointmentStatus.RESCHEDULED);e.setPlannedLoadingAt(at);e.setVersion(e.getVersion()+1);e.setRevision(e.getRevision()+1);record(e,from,"RESCHEDULED","RESCHEDULE",r.reason(),uid,name);return e;}
|
||||
private TruckAppointmentStatus next(TruckAppointmentStatus s,String a){return switch(a){case"SUBMIT"->s==TruckAppointmentStatus.DRAFT?TruckAppointmentStatus.PENDING_DISPATCH:badStatus();case"DISPATCH"->s==TruckAppointmentStatus.PENDING_DISPATCH||s==TruckAppointmentStatus.RESCHEDULED?TruckAppointmentStatus.DISPATCHED:badStatus();case"PICKUP"->s==TruckAppointmentStatus.DISPATCHED?TruckAppointmentStatus.PICKUP_IN_PROGRESS:badStatus();case"ARRIVE"->s==TruckAppointmentStatus.PICKUP_IN_PROGRESS?TruckAppointmentStatus.ARRIVED_WAREHOUSE:badStatus();case"LOAD"->s==TruckAppointmentStatus.ARRIVED_WAREHOUSE?TruckAppointmentStatus.LOADING:badStatus();case"DEPART"->s==TruckAppointmentStatus.LOADING?TruckAppointmentStatus.DEPARTED:badStatus();case"COMPLETE"->s==TruckAppointmentStatus.DEPARTED?TruckAppointmentStatus.COMPLETED:badStatus();case"EXCEPTION"->s!=TruckAppointmentStatus.COMPLETED&&s!=TruckAppointmentStatus.CANCELLED?TruckAppointmentStatus.EXCEPTION:badStatus();case"CANCEL"->s!=TruckAppointmentStatus.COMPLETED?TruckAppointmentStatus.CANCELLED:badStatus();default->badStatus();};}
|
||||
private TruckAppointmentStatus badStatus(){throw conflict("不允许的状态动作");} private TruckAppointmentEntity required(long id){TruckAppointmentEntity e=repo.find(id);if(e==null)throw new BizException(ResultCode.NOT_FOUND,"拖车预约不存在");return e;} private void checkVersion(TruckAppointmentEntity e,int v){if(e.getVersion()!=v)throw conflict("版本已变化,请刷新后重试");} private static BizException conflict(String m){return new BizException(ResultCode.CONFLICT,m);} private static BizException bad(String m){return new BizException(ResultCode.BAD_REQUEST,m);}
|
||||
|
||||
@@ -76,23 +76,3 @@ SELECT role.id,permission.id,0 FROM iam_role role JOIN iam_permission permission
|
||||
WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR');
|
||||
|
||||
USE baiyue_firstleg;
|
||||
|
||||
USE baiyue_iam;
|
||||
INSERT INTO iam_permission
|
||||
(id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,
|
||||
route_path,http_method,sort_no,status,built_in,deleted)
|
||||
VALUES
|
||||
(5102150,0,'PLATFORM','MENU',5102000,'platform:firstleg:truck-appointment:list','拖车预约','/trucking','GET',90,'ACTIVE',1,0),
|
||||
(5102151,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:view','查看拖车预约',NULL,'GET',10,'ACTIVE',1,0),
|
||||
(5102152,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:create','创建拖车预约',NULL,'POST',20,'ACTIVE',1,0),
|
||||
(5102153,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:edit','编辑拖车预约',NULL,'PUT',30,'ACTIVE',1,0),
|
||||
(5102154,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:operate','操作拖车状态',NULL,'POST',40,'ACTIVE',1,0)
|
||||
ON DUPLICATE KEY UPDATE permission_name=VALUES(permission_name),parent_id=VALUES(parent_id),
|
||||
route_path=VALUES(route_path),http_method=VALUES(http_method),sort_no=VALUES(sort_no),status='ACTIVE',built_in=1,deleted=0;
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission(role_id,permission_id,tenant_id)
|
||||
SELECT role.id,permission.id,0 FROM iam_role role JOIN iam_permission permission
|
||||
ON permission.tenant_id=0 AND permission.permission_code LIKE 'platform:firstleg:truck-appointment:%'
|
||||
WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR');
|
||||
|
||||
USE baiyue_firstleg;
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baiyue.firstleg.controller.PlatformTruckAppointmentController;
|
||||
import com.baiyue.firstleg.oceanbooking.controller.PlatformOceanBookingController;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class Phase3BookingTruckPermissionSqlContractTest {
|
||||
private static final Pattern AUTHORITY = Pattern.compile("hasAuthority\\('([^']+)'\\)");
|
||||
private static final Pattern OCEAN_CODE = Pattern.compile("platform:firstleg:ocean-booking:[a-z-]+");
|
||||
private static final Pattern TRUCK_CODE = Pattern.compile("platform:firstleg:truck-appointment:[a-z-]+");
|
||||
|
||||
private static final Set<String> OCEAN_PERMISSIONS = Set.of(
|
||||
"platform:firstleg:ocean-booking:list",
|
||||
"platform:firstleg:ocean-booking:view",
|
||||
"platform:firstleg:ocean-booking:create",
|
||||
"platform:firstleg:ocean-booking:edit",
|
||||
"platform:firstleg:ocean-booking:submit",
|
||||
"platform:firstleg:ocean-booking:confirm",
|
||||
"platform:firstleg:ocean-booking:release",
|
||||
"platform:firstleg:ocean-booking:cancel",
|
||||
"platform:firstleg:ocean-booking:change",
|
||||
"platform:firstleg:ocean-booking:allocate");
|
||||
|
||||
private static final Set<String> TRUCK_PERMISSIONS = Set.of(
|
||||
"platform:firstleg:truck-appointment:list",
|
||||
"platform:firstleg:truck-appointment:view",
|
||||
"platform:firstleg:truck-appointment:create",
|
||||
"platform:firstleg:truck-appointment:edit",
|
||||
"platform:firstleg:truck-appointment:operate");
|
||||
|
||||
@Test
|
||||
void controllerAuthoritiesExactlyMatchModuleAndUnifiedPermissionCodes() throws Exception {
|
||||
assertThat(authorities(PlatformOceanBookingController.class)).isEqualTo(OCEAN_PERMISSIONS);
|
||||
assertThat(authorities(PlatformTruckAppointmentController.class)).isEqualTo(TRUCK_PERMISSIONS);
|
||||
|
||||
String oceanModule = resource("db/phase3-ocean-booking-schema.sql");
|
||||
String truckModule = resource("db/phase3-truck-appointment-schema.sql");
|
||||
String unified = unifiedSql();
|
||||
assertThat(codes(oceanModule, OCEAN_CODE)).isEqualTo(OCEAN_PERMISSIONS);
|
||||
assertThat(codes(truckModule, TRUCK_CODE)).isEqualTo(TRUCK_PERMISSIONS);
|
||||
assertThat(codes(unified, OCEAN_CODE)).isEqualTo(OCEAN_PERMISSIONS);
|
||||
assertThat(codes(unified, TRUCK_CODE)).isEqualTo(TRUCK_PERMISSIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedSqlLocksCanonicalIdsScopeParentsAndRoleGrants() throws Exception {
|
||||
String sql = unifiedSql();
|
||||
assertThat(sql)
|
||||
.contains("UNIQUE KEY uk_permission_scope_code (tenant_id, app_code, permission_code)")
|
||||
.contains("CREATE PROCEDURE assert_phase3_booking_truck_permission_ids_available()")
|
||||
.contains("三期订舱/拖车权限迁移失败:5102140-5102154 ID 或权限码被其他语义占用")
|
||||
.contains("(5102140,0,'PLATFORM','MENU',5102000,'platform:firstleg:ocean-booking:list'")
|
||||
.contains("(5102149,0,'PLATFORM','BUTTON',5102140,'platform:firstleg:ocean-booking:allocate'")
|
||||
.contains("(5102150,0,'PLATFORM','MENU',5102000,'platform:firstleg:truck-appointment:list'")
|
||||
.contains("(5102154,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:operate'")
|
||||
.contains("role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')")
|
||||
.contains("CREATE PROCEDURE assert_phase3_booking_truck_permissions()")
|
||||
.contains("统一迁移失败:三期订舱/拖车 15 项权限映射不完整")
|
||||
.contains("统一迁移失败:三期订舱/拖车角色授权不完整")
|
||||
.contains("CALL assert_phase3_booking_truck_permissions()")
|
||||
.contains("DROP PROCEDURE IF EXISTS assert_phase3_booking_truck_permissions;");
|
||||
}
|
||||
|
||||
private static Set<String> authorities(Class<?> controller) {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
Arrays.stream(controller.getDeclaredMethods())
|
||||
.map(method -> method.getAnnotation(PreAuthorize.class))
|
||||
.filter(annotation -> annotation != null)
|
||||
.map(PreAuthorize::value)
|
||||
.forEach(expression -> {
|
||||
Matcher matcher = AUTHORITY.matcher(expression);
|
||||
assertThat(matcher.find()).as(expression).isTrue();
|
||||
result.add(matcher.group(1));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Set<String> codes(String sql, Pattern pattern) {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
Matcher matcher = pattern.matcher(sql);
|
||||
while (matcher.find()) {
|
||||
result.add(matcher.group());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String resource(String path) throws Exception {
|
||||
return new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String unifiedSql() throws Exception {
|
||||
Path path = Path.of("deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
if (!Files.exists(path)) {
|
||||
path = Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql").normalize();
|
||||
}
|
||||
return Files.readString(path, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ServiceCenterUnifiedSqlContractTest {
|
||||
@Test
|
||||
void unifiedMigrationContainsServiceCenterSchemaAndGrants() throws Exception {
|
||||
Path working = Path.of(System.getProperty("user.dir"));
|
||||
Path path = working.resolve("deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
if (!Files.exists(path)) {
|
||||
path = working.resolve("../deploy/sql/头程数据库统一增量-MySQL8.sql").normalize();
|
||||
}
|
||||
String sql = Files.readString(path, StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_party")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_requirement")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_allocation")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_status_history")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_operation")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_truck_appointment")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_truck_appointment_history")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_truck_appointment_attachment")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_truck_appointment_operation")
|
||||
.contains("5102140")
|
||||
.contains("5102149")
|
||||
.contains("5102150")
|
||||
.contains("5102154")
|
||||
.contains("PLATFORM_ADMIN")
|
||||
.contains("ORDER_OPERATOR")
|
||||
.contains("assert_phase3_service_center_schema()")
|
||||
.contains("CALL assert_phase3_service_center_schema();");
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class TruckAppointmentServiceTest {
|
||||
when(repository.find(1L)).thenReturn(e);
|
||||
when(repository.transition(1L, "DRAFT", "PENDING_DISPATCH", 0)).thenReturn(1);
|
||||
|
||||
var result = service.action(1L, "submit", new ActionRequest(0, "提交"), "idem-1", "hash-1", 7L, "admin");
|
||||
var result = service.action(1L, "SUBMIT", new ActionRequest(0, "提交"), "idem-1", "hash-1", 7L, "admin");
|
||||
|
||||
assertThat(result.getStatus()).isEqualTo(TruckAppointmentStatus.PENDING_DISPATCH);
|
||||
assertThat(result.getVersion()).isEqualTo(1);
|
||||
|
||||
@@ -0,0 +1,727 @@
|
||||
{
|
||||
"marker": "20260808062035-760e776d",
|
||||
"gateway": "http://8.163.30.128:9000",
|
||||
"generatedAt": "2026-08-08T06:20:35.779Z",
|
||||
"completedAt": "2026-08-08T06:20:38.048Z",
|
||||
"success": false,
|
||||
"fatal": null,
|
||||
"testAccount": {
|
||||
"username": "merchant_admin"
|
||||
},
|
||||
"expectedTerminalState": {
|
||||
"processStatus": "WAIT_SPLIT",
|
||||
"measureReviewStatus": "APPROVED",
|
||||
"feeStatus": "CONFIRMED"
|
||||
},
|
||||
"resources": {
|
||||
"tenantId": "10001",
|
||||
"terminalInboundId": "2085909619958919170",
|
||||
"terminalOrderId": "2085909608158265345",
|
||||
"terminalInboundCode": "RK-2085909608158265345",
|
||||
"terminalMeasurementId": "2085909625084358657",
|
||||
"pendingOrderId": "2085962256093872130",
|
||||
"foreignOrderId": "2085665774056833025",
|
||||
"measurementImageFileIds": [],
|
||||
"weightImageFileIds": []
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"module": "phase2-session",
|
||||
"name": "client login",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:35.781Z",
|
||||
"durationMs": 412,
|
||||
"http": 200,
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"requestId": "client-phase2-2a854847-f135-4c48-9f38-4ebd134847cd, client-phase2-2a854847-f135-4c48-9f38-4ebd134847cd",
|
||||
"traceId": "544367752d7e438d906928bd2f1c13ca",
|
||||
"evidence": {
|
||||
"username": "merchant_admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-session",
|
||||
"name": "load current enterprise profile",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.193Z",
|
||||
"durationMs": 45,
|
||||
"http": 200,
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"requestId": "client-phase2-ba9836bc-1ee7-4169-80de-81619ea930f0, client-phase2-ba9836bc-1ee7-4169-80de-81619ea930f0",
|
||||
"traceId": "ee177b1b0bbb47bb80a9d885024b6f1b",
|
||||
"evidence": {
|
||||
"tenantId": "10001",
|
||||
"tenantName": "一期演示企业",
|
||||
"status": "ACTIVE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-session",
|
||||
"name": "client has order view permission",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.238Z",
|
||||
"durationMs": 44,
|
||||
"http": 200,
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"requestId": "client-phase2-79e2fae4-9149-4907-969a-c9e432b09dbf, client-phase2-79e2fae4-9149-4907-969a-c9e432b09dbf",
|
||||
"traceId": "db355c884ffc4878b5df63be012984f7",
|
||||
"evidence": {
|
||||
"permissionCount": 27
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-fixture",
|
||||
"name": "platform fixture account login",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.283Z",
|
||||
"durationMs": 342,
|
||||
"http": 200,
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"requestId": "client-phase2-4076b5e9-01aa-4ae6-b5a7-be519e32e59a, client-phase2-4076b5e9-01aa-4ae6-b5a7-be519e32e59a",
|
||||
"traceId": "8206fa371a274e19b4ed37aa57d00d6f",
|
||||
"evidence": {
|
||||
"username": "platform_admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-fixture",
|
||||
"name": "discover current enterprise inbound fixtures",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.625Z",
|
||||
"durationMs": 85,
|
||||
"http": 200,
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"requestId": "client-phase2-7428b82e-bf10-4c31-a118-5c14c766ade0, client-phase2-7428b82e-bf10-4c31-a118-5c14c766ade0",
|
||||
"traceId": "f24ae5dcddb446d1b88dd32c5faee5d6",
|
||||
"evidence": {
|
||||
"total": "38",
|
||||
"statusCounts": {
|
||||
"INBOUNDED|APPROVED|UNPRICED": 8,
|
||||
"MEASURING|NOT_SUBMITTED|UNPRICED": 14,
|
||||
"WAIT_MEASURE|NOT_SUBMITTED|UNPRICED": 12,
|
||||
"WAIT_SPLIT|APPROVED|UNPRICED": 1,
|
||||
"WAIT_SPLIT|APPROVED|CONFIRMED": 2,
|
||||
"WAIT_SPLIT|APPROVED|CALCULATED": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-fixture",
|
||||
"name": "terminal state filters find selected inbound",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.711Z",
|
||||
"durationMs": 48,
|
||||
"http": 200,
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"requestId": "client-phase2-9ef014fe-ef45-4dbb-94b7-d931fc10b47d, client-phase2-9ef014fe-ef45-4dbb-94b7-d931fc10b47d",
|
||||
"traceId": "4c52bbc80b4f4ae49022d074eff854a5",
|
||||
"evidence": {
|
||||
"total": "2",
|
||||
"inboundId": "2085909619958919170"
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-fixture",
|
||||
"name": "load platform terminal inbound reference detail",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.759Z",
|
||||
"durationMs": 38,
|
||||
"http": 200,
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"requestId": "client-phase2-536ff03a-2233-405a-8ac0-28cafa13e7fc, client-phase2-536ff03a-2233-405a-8ac0-28cafa13e7fc",
|
||||
"traceId": "b6fc5ba4288b4361b8d0a85566ab21bb",
|
||||
"evidence": {
|
||||
"inboundId": "2085909619958919170",
|
||||
"orderId": "2085909608158265345",
|
||||
"processStatus": "WAIT_SPLIT",
|
||||
"measureReviewStatus": "APPROVED",
|
||||
"feeStatus": "CONFIRMED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-measurement-fixture",
|
||||
"name": "load four customer dimension weighting rules",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.797Z",
|
||||
"durationMs": 40,
|
||||
"http": 200,
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"requestId": "client-phase2-92588486-fed5-41fb-9322-a67416fdb478, client-phase2-92588486-fed5-41fb-9322-a67416fdb478",
|
||||
"traceId": "b225b9bcb92d46fd9502eef48596a4f7",
|
||||
"evidence": {
|
||||
"tenantId": "10001",
|
||||
"rules": [
|
||||
{
|
||||
"boxType": "SMALL",
|
||||
"inner": [
|
||||
30,
|
||||
20,
|
||||
20
|
||||
],
|
||||
"edgeAdditionCm": 2,
|
||||
"outer": [
|
||||
34,
|
||||
24,
|
||||
24
|
||||
],
|
||||
"volumeCbm": 0.02,
|
||||
"version": 0
|
||||
},
|
||||
{
|
||||
"boxType": "MEDIUM",
|
||||
"inner": [
|
||||
40,
|
||||
30,
|
||||
30
|
||||
],
|
||||
"edgeAdditionCm": 2,
|
||||
"outer": [
|
||||
44,
|
||||
34,
|
||||
34
|
||||
],
|
||||
"volumeCbm": 0.051,
|
||||
"version": 0
|
||||
},
|
||||
{
|
||||
"boxType": "LARGE",
|
||||
"inner": [
|
||||
50,
|
||||
40,
|
||||
40
|
||||
],
|
||||
"edgeAdditionCm": 2,
|
||||
"outer": [
|
||||
54,
|
||||
44,
|
||||
44
|
||||
],
|
||||
"volumeCbm": 0.105,
|
||||
"version": 0
|
||||
},
|
||||
{
|
||||
"boxType": "EXTRA_LARGE",
|
||||
"inner": [
|
||||
60,
|
||||
50,
|
||||
50
|
||||
],
|
||||
"edgeAdditionCm": 2,
|
||||
"outer": [
|
||||
64,
|
||||
54,
|
||||
54
|
||||
],
|
||||
"volumeCbm": 0.187,
|
||||
"version": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-measurement-fixture",
|
||||
"name": "load approved terminal measurement detail",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.837Z",
|
||||
"durationMs": 38,
|
||||
"http": 200,
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"requestId": "client-phase2-fc3f4698-b49b-4f3f-b946-557fdc250803, client-phase2-fc3f4698-b49b-4f3f-b946-557fdc250803",
|
||||
"traceId": "66371b88f5ba4511824b947163518393",
|
||||
"evidence": {
|
||||
"inboundId": "2085909619958919170",
|
||||
"measurementId": "2085909625084358657",
|
||||
"measurementStatus": "APPROVED",
|
||||
"actualPackageCount": 4,
|
||||
"actualGrossWeightKg": 1383,
|
||||
"actualVolumeCbm": 0.1895,
|
||||
"volumeWeightKg": 31.647,
|
||||
"itemCount": 4,
|
||||
"weightResultCount": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-measurement-fixture",
|
||||
"name": "carton measurements contain complete traceable data",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.875Z",
|
||||
"durationMs": 0,
|
||||
"http": null,
|
||||
"code": null,
|
||||
"message": null,
|
||||
"requestId": null,
|
||||
"traceId": null,
|
||||
"evidence": {
|
||||
"itemCount": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-measurement-fixture",
|
||||
"name": "carton volume and package aggregates are consistent",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.875Z",
|
||||
"durationMs": 0,
|
||||
"http": null,
|
||||
"code": null,
|
||||
"message": null,
|
||||
"requestId": null,
|
||||
"traceId": null,
|
||||
"evidence": {
|
||||
"actualPackageCount": 4,
|
||||
"actualVolumeCbm": 0.1895,
|
||||
"volumeWeightKg": 31.647
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-measurement-fixture",
|
||||
"name": "confirmed weight results form an independent weight line",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.875Z",
|
||||
"durationMs": 0,
|
||||
"http": null,
|
||||
"code": null,
|
||||
"message": null,
|
||||
"requestId": null,
|
||||
"traceId": null,
|
||||
"evidence": {
|
||||
"actualGrossWeightKg": 1383,
|
||||
"weightResultCount": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-measurement-fixture",
|
||||
"name": "carton billing volume uses current customer dimension weighting",
|
||||
"passed": false,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.876Z",
|
||||
"durationMs": 0,
|
||||
"http": null,
|
||||
"code": null,
|
||||
"message": null,
|
||||
"requestId": null,
|
||||
"traceId": null,
|
||||
"evidence": {
|
||||
"measurementId": "2085909625084358657",
|
||||
"items": [
|
||||
{
|
||||
"itemNo": "B1",
|
||||
"measured": [
|
||||
10,
|
||||
10,
|
||||
10
|
||||
],
|
||||
"storedOuter": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"unitVolumeCbm": 0.001,
|
||||
"expected": {
|
||||
"boxType": "SMALL",
|
||||
"ruleVersion": 0,
|
||||
"outer": [
|
||||
34,
|
||||
24,
|
||||
24
|
||||
],
|
||||
"unitVolumeCbm": 0.02
|
||||
}
|
||||
},
|
||||
{
|
||||
"itemNo": "B2",
|
||||
"measured": [
|
||||
35,
|
||||
25,
|
||||
25
|
||||
],
|
||||
"storedOuter": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"unitVolumeCbm": 0.0219,
|
||||
"expected": {
|
||||
"boxType": "MEDIUM",
|
||||
"ruleVersion": 0,
|
||||
"outer": [
|
||||
44,
|
||||
34,
|
||||
34
|
||||
],
|
||||
"unitVolumeCbm": 0.051
|
||||
}
|
||||
},
|
||||
{
|
||||
"itemNo": "B3",
|
||||
"measured": [
|
||||
45,
|
||||
35,
|
||||
35
|
||||
],
|
||||
"storedOuter": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"unitVolumeCbm": 0.0552,
|
||||
"expected": {
|
||||
"boxType": "LARGE",
|
||||
"ruleVersion": 0,
|
||||
"outer": [
|
||||
54,
|
||||
44,
|
||||
44
|
||||
],
|
||||
"unitVolumeCbm": 0.105
|
||||
}
|
||||
},
|
||||
{
|
||||
"itemNo": "B4",
|
||||
"measured": [
|
||||
55,
|
||||
45,
|
||||
45
|
||||
],
|
||||
"storedOuter": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"unitVolumeCbm": 0.1114,
|
||||
"expected": {
|
||||
"boxType": "EXTRA_LARGE",
|
||||
"ruleVersion": 0,
|
||||
"outer": [
|
||||
64,
|
||||
54,
|
||||
54
|
||||
],
|
||||
"unitVolumeCbm": 0.187
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"name": "Error",
|
||||
"message": "B1 weighted unit volume: expected=0.02 actual=0.001"
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-security",
|
||||
"name": "client cannot read platform measurement workspace directly",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.876Z",
|
||||
"durationMs": 25,
|
||||
"http": 403,
|
||||
"code": "A0403",
|
||||
"message": "无权访问该资源",
|
||||
"requestId": "client-phase2-9108c904-ee43-40c2-bc11-288455d4c191",
|
||||
"traceId": null,
|
||||
"evidence": {
|
||||
"rejectionCode": "A0403"
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-security",
|
||||
"name": "client cannot bypass billing publication to read platform image",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.901Z",
|
||||
"durationMs": 31,
|
||||
"http": 403,
|
||||
"code": "A0403",
|
||||
"message": "无权访问该资源",
|
||||
"requestId": "client-phase2-1a11b1f0-afd4-4091-b52e-2a2fa1006fdf",
|
||||
"traceId": null,
|
||||
"evidence": {
|
||||
"fileId": "2085909625048727553",
|
||||
"rejectionCode": "A0403"
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-fixture",
|
||||
"name": "discover foreign terminal fixture for isolation",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:36.932Z",
|
||||
"durationMs": 113,
|
||||
"http": 200,
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"requestId": "client-phase2-46e3bf79-d804-409a-ad00-ebc36c50bd40, client-phase2-46e3bf79-d804-409a-ad00-ebc36c50bd40",
|
||||
"traceId": "2983ab506e59449cb57e342ab58ccb20",
|
||||
"evidence": {
|
||||
"total": "278"
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-visibility",
|
||||
"name": "unfinished inbound billing remains unavailable",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:37.045Z",
|
||||
"durationMs": 43,
|
||||
"http": 200,
|
||||
"code": "A0404",
|
||||
"message": "入库费用尚未由客服审核并发布",
|
||||
"requestId": "client-phase2-c50771d6-4041-4eb1-984e-a4a0885bf432, client-phase2-c50771d6-4041-4eb1-984e-a4a0885bf432",
|
||||
"traceId": "5bff6ee6f75242d9abdc7649c3202911",
|
||||
"evidence": {
|
||||
"orderId": "2085962256093872130",
|
||||
"processStatus": "INBOUNDED",
|
||||
"feeStatus": "UNPRICED",
|
||||
"rejectionCode": "A0404"
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-visibility",
|
||||
"name": "unknown order inbound billing is rejected",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:37.089Z",
|
||||
"durationMs": 45,
|
||||
"http": 200,
|
||||
"code": "A0404",
|
||||
"message": "订单尚未生成入库单",
|
||||
"requestId": "client-phase2-4ab809c7-ff3b-43f8-a6a9-a1cb4fa2367d, client-phase2-4ab809c7-ff3b-43f8-a6a9-a1cb4fa2367d",
|
||||
"traceId": "08541a78635746319916499da91df6a9",
|
||||
"evidence": {
|
||||
"orderId": 1,
|
||||
"rejectionCode": "A0404"
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-billing",
|
||||
"name": "terminal inbound billing is visible to client",
|
||||
"passed": false,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:37.134Z",
|
||||
"durationMs": 84,
|
||||
"http": 200,
|
||||
"code": "A0404",
|
||||
"message": "入库运费明细尚未发布",
|
||||
"requestId": "client-phase2-0283d6e1-1ed2-4480-8a02-880459fb5f5c, client-phase2-0283d6e1-1ed2-4480-8a02-880459fb5f5c",
|
||||
"traceId": "d6edff82e4834ec6ba483ff29b86e244",
|
||||
"evidence": {
|
||||
"rejectionCode": "A0404"
|
||||
},
|
||||
"error": {
|
||||
"name": "Error",
|
||||
"message": "business code: expected=\"200\" actual=\"A0404\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-billing",
|
||||
"name": "terminal billing aggregate contract",
|
||||
"passed": true,
|
||||
"skipped": true,
|
||||
"startedAt": "2026-08-08T06:20:37.219Z",
|
||||
"durationMs": 0,
|
||||
"http": null,
|
||||
"code": null,
|
||||
"message": "Terminal billing endpoint did not return a visible aggregate",
|
||||
"requestId": null,
|
||||
"traceId": null,
|
||||
"evidence": null
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-billing",
|
||||
"name": "published measurement summary matches approved platform snapshot",
|
||||
"passed": true,
|
||||
"skipped": true,
|
||||
"startedAt": "2026-08-08T06:20:37.219Z",
|
||||
"durationMs": 0,
|
||||
"http": null,
|
||||
"code": null,
|
||||
"message": "Terminal billing endpoint did not return a visible aggregate",
|
||||
"requestId": null,
|
||||
"traceId": null,
|
||||
"evidence": null
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-billing",
|
||||
"name": "published carton measurements match platform snapshot",
|
||||
"passed": true,
|
||||
"skipped": true,
|
||||
"startedAt": "2026-08-08T06:20:37.219Z",
|
||||
"durationMs": 0,
|
||||
"http": null,
|
||||
"code": null,
|
||||
"message": "Terminal billing endpoint did not return a visible aggregate",
|
||||
"requestId": null,
|
||||
"traceId": null,
|
||||
"evidence": null
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-billing",
|
||||
"name": "published carton volumes follow current weighting rules",
|
||||
"passed": true,
|
||||
"skipped": true,
|
||||
"startedAt": "2026-08-08T06:20:37.219Z",
|
||||
"durationMs": 0,
|
||||
"http": null,
|
||||
"code": null,
|
||||
"message": "Terminal billing endpoint did not return a visible aggregate",
|
||||
"requestId": null,
|
||||
"traceId": null,
|
||||
"evidence": null
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-billing",
|
||||
"name": "published weight results match confirmed platform weights",
|
||||
"passed": true,
|
||||
"skipped": true,
|
||||
"startedAt": "2026-08-08T06:20:37.219Z",
|
||||
"durationMs": 0,
|
||||
"http": null,
|
||||
"code": null,
|
||||
"message": "Terminal billing endpoint did not return a visible aggregate",
|
||||
"requestId": null,
|
||||
"traceId": null,
|
||||
"evidence": null
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-billing",
|
||||
"name": "weight and volume billing lines are independently consistent",
|
||||
"passed": true,
|
||||
"skipped": true,
|
||||
"startedAt": "2026-08-08T06:20:37.219Z",
|
||||
"durationMs": 0,
|
||||
"http": null,
|
||||
"code": null,
|
||||
"message": "Terminal billing endpoint did not return a visible aggregate",
|
||||
"requestId": null,
|
||||
"traceId": null,
|
||||
"evidence": null
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-billing",
|
||||
"name": "published fee detail matches confirmed fee snapshot",
|
||||
"passed": true,
|
||||
"skipped": true,
|
||||
"startedAt": "2026-08-08T06:20:37.219Z",
|
||||
"durationMs": 0,
|
||||
"http": null,
|
||||
"code": null,
|
||||
"message": "Terminal billing endpoint did not return a visible aggregate",
|
||||
"requestId": null,
|
||||
"traceId": null,
|
||||
"evidence": null
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-billing",
|
||||
"name": "client aggregate excludes platform internal collections",
|
||||
"passed": true,
|
||||
"skipped": true,
|
||||
"startedAt": "2026-08-08T06:20:37.219Z",
|
||||
"durationMs": 0,
|
||||
"http": null,
|
||||
"code": null,
|
||||
"message": "Terminal billing endpoint did not return a visible aggregate",
|
||||
"requestId": null,
|
||||
"traceId": null,
|
||||
"evidence": null
|
||||
},
|
||||
{
|
||||
"module": "phase2-client-images",
|
||||
"name": "published measurement and weight image access",
|
||||
"passed": true,
|
||||
"skipped": true,
|
||||
"startedAt": "2026-08-08T06:20:37.219Z",
|
||||
"durationMs": 0,
|
||||
"http": null,
|
||||
"code": null,
|
||||
"message": "Terminal billing endpoint did not return a visible aggregate",
|
||||
"requestId": null,
|
||||
"traceId": null,
|
||||
"evidence": null
|
||||
},
|
||||
{
|
||||
"module": "phase2-unified-orders",
|
||||
"name": "unified order list exposes phase-two terminal state",
|
||||
"passed": false,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:37.219Z",
|
||||
"durationMs": 711,
|
||||
"http": 200,
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"requestId": "client-phase2-02fe53ec-c024-42b6-841b-22c145879272, client-phase2-02fe53ec-c024-42b6-841b-22c145879272",
|
||||
"traceId": "75cd022c9f6f43e4b5595575753b3597",
|
||||
"evidence": {
|
||||
"orderId": "2085909608158265345",
|
||||
"mainStatus": "WAIT_SPLIT",
|
||||
"inboundId": "2085909619958919170",
|
||||
"measurementReviewStatus": "APPROVED",
|
||||
"actualGrossWeightKg": 1383,
|
||||
"actualVolumeCbm": 0.1895
|
||||
},
|
||||
"error": {
|
||||
"name": "Error",
|
||||
"message": "unified main status: expected=\"INBOUNDED\" actual=\"WAIT_SPLIT\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-unified-orders",
|
||||
"name": "unified order detail exposes phase-two summary and timeline",
|
||||
"passed": false,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:37.930Z",
|
||||
"durationMs": 91,
|
||||
"http": 200,
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"requestId": "client-phase2-ad957e7f-9520-40cc-a1c9-30ec978f058e, client-phase2-ad957e7f-9520-40cc-a1c9-30ec978f058e",
|
||||
"traceId": "f5f613e3459e427aac9f24e80baedec4",
|
||||
"evidence": {
|
||||
"orderId": "2085909608158265345",
|
||||
"mainStatus": "WAIT_SPLIT",
|
||||
"timelineCount": 5
|
||||
},
|
||||
"error": {
|
||||
"name": "Error",
|
||||
"message": "unified main status: expected=\"INBOUNDED\" actual=\"WAIT_SPLIT\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "phase2-isolation",
|
||||
"name": "cross-enterprise terminal billing is rejected",
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"startedAt": "2026-08-08T06:20:38.021Z",
|
||||
"durationMs": 27,
|
||||
"http": 200,
|
||||
"code": "A0404",
|
||||
"message": "订单尚未生成入库单",
|
||||
"requestId": "client-phase2-16f5e503-bb89-4b9d-8992-038d489dec0c, client-phase2-16f5e503-bb89-4b9d-8992-038d489dec0c",
|
||||
"traceId": "30a61022096b48a299edf68a59e455a9",
|
||||
"evidence": {
|
||||
"orderId": "2085665774056833025",
|
||||
"tenantId": "2085308606829346818",
|
||||
"rejectionCode": "A0404"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user