From 4525790534b5730a711e08ae6c46d87eb036f269 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sun, 27 Sep 2026 18:55:19 +0800 Subject: [PATCH] feat(firstleg): add sailing calendar facts and APIs --- deploy/sql/头程数据库统一增量-MySQL8.sql | 274 +++++++++- .../PlatformSailingAssignmentController.java | 34 ++ .../PlatformSailingCalendarController.java | 104 ++++ .../PlatformSailingChannelController.java | 30 ++ .../PlatformSailingForecastController.java | 74 +++ .../PlatformSailingTemplateController.java | 40 ++ .../dto/SailingCalendarDtos.java | 283 ++++++++++ .../entity/SailingChannelEntity.java | 32 ++ .../sailingcalendar/entity/SailingEntity.java | 43 ++ .../entity/SailingHistoryEntity.java | 25 + .../entity/SailingTemplateEntity.java | 45 ++ .../fact/entity/SailingAssignmentEntity.java | 40 ++ .../fact/entity/SailingBookingLinkEntity.java | 32 ++ .../SailingInboundVolumeForecastEntity.java | 35 ++ .../entity/SailingReceiptBatchFactEntity.java | 36 ++ .../fact/mapper/SailingAssignmentMapper.java | 24 + .../fact/mapper/SailingBookingLinkMapper.java | 8 + .../SailingInboundVolumeForecastMapper.java | 8 + .../mapper/SailingReceiptBatchFactMapper.java | 18 + .../repository/SailingFactsRepository.java | 195 +++++++ .../mapper/SailingChannelMapper.java | 34 ++ .../mapper/SailingHistoryMapper.java | 8 + .../sailingcalendar/mapper/SailingMapper.java | 75 +++ .../mapper/SailingTemplateMapper.java | 36 ++ .../repository/SailingCalendarRepository.java | 112 ++++ .../service/SailingCalendarService.java | 482 ++++++++++++++++++ .../service/MeasurementReviewService.java | 105 ++++ .../db/phase4-sailing-calendar-schema.sql | 143 ++++++ .../db/phase4-sailing-facts-schema.sql | 126 +++++ .../openapi/sailing-calendar-v1.yaml | 138 +++++ .../Phase3FinalUnifiedSqlContractTest.java | 2 +- .../fact/SailingFactsSqlContractTest.java | 52 ++ gateway/src/main/resources/application.yml | 2 +- .../initialization-baseline-contract.test.mjs | 4 +- 34 files changed, 2694 insertions(+), 5 deletions(-) create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingAssignmentController.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingCalendarController.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingChannelController.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingForecastController.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingTemplateController.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/dto/SailingCalendarDtos.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/entity/SailingChannelEntity.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/entity/SailingEntity.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/entity/SailingHistoryEntity.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/entity/SailingTemplateEntity.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/fact/entity/SailingAssignmentEntity.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/fact/entity/SailingBookingLinkEntity.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/fact/entity/SailingInboundVolumeForecastEntity.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/fact/entity/SailingReceiptBatchFactEntity.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/fact/mapper/SailingAssignmentMapper.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/fact/mapper/SailingBookingLinkMapper.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/fact/mapper/SailingInboundVolumeForecastMapper.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/fact/mapper/SailingReceiptBatchFactMapper.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/fact/repository/SailingFactsRepository.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/mapper/SailingChannelMapper.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/mapper/SailingHistoryMapper.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/mapper/SailingMapper.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/mapper/SailingTemplateMapper.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/repository/SailingCalendarRepository.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/service/SailingCalendarService.java create mode 100644 firstleg/src/main/resources/db/phase4-sailing-calendar-schema.sql create mode 100644 firstleg/src/main/resources/db/phase4-sailing-facts-schema.sql create mode 100644 firstleg/src/main/resources/openapi/sailing-calendar-v1.yaml create mode 100644 firstleg/src/test/java/com/baiyue/firstleg/sailingcalendar/fact/SailingFactsSqlContractTest.java diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 5d85f593d..c60b2b3b0 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -7778,4 +7778,276 @@ USE baiyue_finance; -- 系统基线 fin_subject:1 行。 INSERT INTO fin_subject (id, subject_code, subject_name, status, version, created_by, updated_by, created_at, updated_at) VALUES (1,'PLATFORM_DEFAULT','平台','ACTIVE',0,0,0,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3)); -SELECT 'PASS' AS initialization_status, 'CN' AS region, 210 AS initialized_tables; +-- phase4-sailing-calendar-schema.sql + +SET NAMES utf8mb4; +USE baiyue_firstleg; + +-- 船期计划日历首版基线。与 fl_port_schedule(港务船期观测)分离。 +CREATE TABLE fl_sailing_channel ( + id BIGINT NOT NULL, + channel_code VARCHAR(64) NOT NULL, + channel_name VARCHAR(128) NOT NULL, + source_type VARCHAR(24) NOT NULL, + mapped_channel_code VARCHAR(64) NULL, + default_departure_port_code VARCHAR(32) NULL, + default_destination_port_code VARCHAR(32) NULL, + default_cutoff_days TINYINT NOT NULL DEFAULT 3, + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', + remark VARCHAR(1000) NULL, + created_by BIGINT NOT NULL, + created_by_name VARCHAR(128) NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_by BIGINT NOT NULL, + updated_by_name VARCHAR(128) NULL, + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + version INT NOT NULL DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY uk_fl_sailing_channel_code (channel_code), + KEY idx_fl_sailing_channel_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE fl_sailing_template ( + id BIGINT NOT NULL, + template_no VARCHAR(64) NOT NULL, + template_name VARCHAR(128) NOT NULL, + channel_id BIGINT NOT NULL, + channel_code_snapshot VARCHAR(64) NOT NULL, + channel_name_snapshot VARCHAR(128) NOT NULL, + warehouse_id BIGINT NOT NULL, + warehouse_name_snapshot VARCHAR(128) NULL, + departure_port_code VARCHAR(32) NOT NULL, + destination_port_code VARCHAR(32) NOT NULL, + weekday TINYINT NOT NULL, + weekday_mask TINYINT NOT NULL DEFAULT 1, + etd_local_time TIME NOT NULL, + cutoff_days TINYINT NOT NULL DEFAULT 3, + cutoff_local_time TIME NOT NULL DEFAULT '18:00:00', + valid_from DATE NOT NULL, + valid_to DATE NULL, + voyage_rule VARCHAR(64) NULL, + vessel_name VARCHAR(128) NULL, + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', + remark VARCHAR(1000) NULL, + created_by BIGINT NOT NULL, + created_by_name VARCHAR(128) NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_by BIGINT NOT NULL, + updated_by_name VARCHAR(128) NULL, + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + version INT NOT NULL DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY uk_fl_sailing_template_no (template_no), + KEY idx_fl_sailing_template_filter (warehouse_id,channel_id,status,weekday), + CONSTRAINT ck_fl_sailing_template_weekday CHECK (weekday BETWEEN 1 AND 7), + CONSTRAINT ck_fl_sailing_template_cutoff_days CHECK (cutoff_days BETWEEN 0 AND 30) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE fl_sailing ( + id BIGINT NOT NULL, + sailing_no VARCHAR(64) NOT NULL, + template_id BIGINT NULL, + channel_id BIGINT NOT NULL, + channel_code_snapshot VARCHAR(64) NOT NULL, + channel_name_snapshot VARCHAR(128) NOT NULL, + warehouse_id BIGINT NOT NULL, + warehouse_name_snapshot VARCHAR(128) NULL, + departure_port_code VARCHAR(32) NOT NULL, + destination_port_code VARCHAR(32) NOT NULL, + vessel_name VARCHAR(128) NOT NULL, + voyage_no VARCHAR(64) NOT NULL, + etd_at DATETIME(3) NOT NULL, + warehouse_cutoff_at DATETIME(3) NOT NULL, + eta_at DATETIME(3) NULL, + status VARCHAR(16) NOT NULL DEFAULT 'OPEN', + source_type VARCHAR(24) NOT NULL DEFAULT 'MANUAL', + generated_key VARCHAR(192) NULL, + planned_capacity_cbm DECIMAL(14,3) NULL, + remark VARCHAR(1000) NULL, + created_by BIGINT NOT NULL, + created_by_name VARCHAR(128) NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_by BIGINT NOT NULL, + updated_by_name VARCHAR(128) NULL, + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + version INT NOT NULL DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY uk_fl_sailing_no (sailing_no), + UNIQUE KEY uk_fl_sailing_generated_key (generated_key), + UNIQUE KEY uk_fl_sailing_business (warehouse_id,channel_code_snapshot,departure_port_code,destination_port_code,etd_at,voyage_no), + KEY idx_fl_sailing_calendar (etd_at,warehouse_id,channel_id,status), + KEY idx_fl_sailing_route (departure_port_code,destination_port_code), + CONSTRAINT ck_fl_sailing_status CHECK (status IN ('DRAFT','OPEN','CLOSED','DEPARTED','CANCELLED','COMPLETED')) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE fl_sailing_history ( + id BIGINT NOT NULL, + sailing_id BIGINT NOT NULL, + action VARCHAR(32) NOT NULL, + from_status VARCHAR(16) NULL, + to_status VARCHAR(16) NULL, + reason VARCHAR(1000) NULL, + operator_id BIGINT NULL, + operator_name VARCHAR(128) NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + KEY idx_fl_sailing_history_sailing (sailing_id,created_at,id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +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 + (5102206,0,'PLATFORM','MENU',5102000,'platform:firstleg:sailing-calendar:list','船期日历','/sailing-calendar','GET',125,'ACTIVE',1,0), + (5102207,0,'PLATFORM','BUTTON',5102206,'platform:firstleg:sailing-calendar:create','新增船期','/sailing-calendar','POST',10,'ACTIVE',1,0), + (5102208,0,'PLATFORM','BUTTON',5102206,'platform:firstleg:sailing-calendar:edit','编辑船期','/sailing-calendar','PUT',20,'ACTIVE',1,0), + (5102209,0,'PLATFORM','BUTTON',5102206,'platform:firstleg:sailing-template:manage','管理渠道与周模板','/sailing-calendar','POST',30,'ACTIVE',1,0), + (5102210,0,'PLATFORM','BUTTON',5102206,'platform:firstleg:sailing-booking:link','关联订舱','/sailing-calendar','POST',40,'ACTIVE',1,0), + (5102211,0,'PLATFORM','BUTTON',5102206,'platform:firstleg:sailing-volume:forecast-manage','维护货量预报','/sailing-calendar','POST',50,'ACTIVE',1,0), + (5102212,0,'PLATFORM','BUTTON',5102206,'platform:firstleg:sailing-volume:exception-reassign','异常重新安排','/sailing-calendar','POST',60,'ACTIVE',1,0); + +INSERT INTO iam_role_permission(role_id,permission_id,tenant_id) +SELECT r.id,p.id,0 FROM iam_role r JOIN iam_permission p + ON p.tenant_id=0 AND p.app_code='PLATFORM' + AND p.permission_code IN ( + 'platform:firstleg:sailing-calendar:list', + 'platform:firstleg:sailing-calendar:create', + 'platform:firstleg:sailing-calendar:edit', + 'platform:firstleg:sailing-template:manage', + 'platform:firstleg:sailing-booking:link', + 'platform:firstleg:sailing-volume:forecast-manage', + 'platform:firstleg:sailing-volume:exception-reassign' + ) +WHERE r.tenant_id=0 AND r.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR'); + +-- phase4-sailing-facts-schema.sql + +SET NAMES utf8mb4; +USE baiyue_firstleg; + +/* + * 船期日历的事实契约。 + * + * 这些表不修改入库单或海运订舱聚合:入库单仍是履约主数据,订舱仍是 + * RELEASED/DRAFT 聚合。船期日历只消费这里的稳定事实和审计版本。 + */ + +CREATE TABLE fl_sailing_receipt_batch_fact ( + id BIGINT UNSIGNED NOT NULL COMMENT '稳定收货批次事实ID(receiptBatchId)', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离键,继承入库事实', + receipt_batch_key VARCHAR(64) NOT NULL COMMENT '同一物理收货批次的不可变业务键', + fact_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '同一批次复测/冲正版本', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', + measurement_id BIGINT UNSIGNED NULL COMMENT '最终审核测量批次ID', + receiving_warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '实际收货仓ID', + channel_code VARCHAR(64) NULL COMMENT '收货渠道编码快照', + channel_name VARCHAR(128) NULL COMMENT '收货渠道名称快照', + departure_port_code VARCHAR(32) NULL COMMENT '起运港编码快照', + destination_port_code VARCHAR(32) NULL COMMENT '目的港编码快照', + received_at DATETIME(3) NOT NULL COMMENT '实际收货完成时间,不使用入库单inbounded_at替代', + actual_volume_cbm DECIMAL(18,4) NOT NULL COMMENT '最终审核实测体积m3', + fact_status VARCHAR(24) NOT NULL DEFAULT 'APPROVED' COMMENT '事实状态:APPROVED/REVERSED/VOIDED/SUPERSEDED', + reversal_of_fact_id BIGINT UNSIGNED NULL COMMENT '冲正所替代的事实ID', + source_type VARCHAR(32) NOT NULL DEFAULT 'FIRSTLEG_RECEIPT' COMMENT '事实来源', + created_by BIGINT UNSIGNED NULL COMMENT '写入人或系统主体', + 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_sailing_receipt_batch_version (tenant_id, receipt_batch_key, fact_version), + KEY idx_sailing_receipt_batch_current (tenant_id, receipt_batch_key, fact_status, fact_version), + KEY idx_sailing_receipt_match (tenant_id, receiving_warehouse_id, channel_code, received_at, id), + KEY idx_sailing_receipt_inbound (tenant_id, inbound_id, received_at, id), + CONSTRAINT chk_sailing_receipt_volume CHECK (actual_volume_cbm > 0), + CONSTRAINT chk_sailing_receipt_status CHECK (fact_status IN ('APPROVED','REVERSED','VOIDED','SUPERSEDED')) +) ENGINE=InnoDB COMMENT='船期日历稳定收货批次实测事实及版本历史'; + +CREATE TABLE fl_sailing_inbound_volume_forecast ( + id BIGINT UNSIGNED NOT NULL COMMENT '货量预报事实ID(forecastId)', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离键,继承入库事实', + forecast_key VARCHAR(64) NOT NULL COMMENT '同一未收货对象的不可变业务键', + fact_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '预报修订版本', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', + receiving_warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '预计收货仓ID', + channel_code VARCHAR(64) NULL COMMENT '预计渠道编码快照', + channel_name VARCHAR(128) NULL COMMENT '预计渠道名称快照', + departure_port_code VARCHAR(32) NULL COMMENT '预计起运港编码快照', + destination_port_code VARCHAR(32) NULL COMMENT '预计目的港编码快照', + expected_arrival_at DATETIME(3) NOT NULL COMMENT '预计到仓时间', + forecast_volume_cbm DECIMAL(18,4) NOT NULL COMMENT '未收货预计体积m3,不得使用申报体积代替', + source_type VARCHAR(32) NOT NULL COMMENT '预报来源,例如OPERATIONS/UPSTREAM_MEASUREMENT;不得使用客户申报体积冒充预报', + fact_status VARCHAR(24) NOT NULL DEFAULT 'VALID' COMMENT '事实状态:VALID/SUPERSEDED/VOIDED/CONSUMED', + effective_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '本版本生效时间', + created_by BIGINT UNSIGNED NULL COMMENT '写入人或系统主体', + 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_sailing_forecast_version (tenant_id, forecast_key, fact_version), + KEY idx_sailing_forecast_current (tenant_id, forecast_key, fact_status, fact_version), + KEY idx_sailing_forecast_match (tenant_id, receiving_warehouse_id, channel_code, expected_arrival_at, id), + KEY idx_sailing_forecast_inbound (tenant_id, inbound_id, expected_arrival_at, id), + CONSTRAINT chk_sailing_forecast_volume CHECK (forecast_volume_cbm > 0), + CONSTRAINT chk_sailing_forecast_status CHECK (fact_status IN ('VALID','SUPERSEDED','VOIDED','CONSUMED')) +) ENGINE=InnoDB COMMENT='船期日历未收货量预报事实及版本历史'; + +CREATE TABLE fl_sailing_assignment ( + id BIGINT UNSIGNED NOT NULL COMMENT '归班事实ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离键', + sailing_id BIGINT UNSIGNED NULL COMMENT '船期实例ID,UNMATCHED为空', + origin_sailing_id BIGINT UNSIGNED NULL COMMENT '异常重排/取消前的原船期实例ID', + inbound_id BIGINT UNSIGNED NULL COMMENT '归班对象入库单ID', + inbound_code VARCHAR(64) NULL COMMENT '入仓码快照,仅用于展示', + receipt_batch_id BIGINT UNSIGNED NULL COMMENT '收货批次事实ID', + forecast_id BIGINT UNSIGNED NULL COMMENT '货量预报事实ID', + assignment_source VARCHAR(24) NOT NULL DEFAULT 'AUTO' COMMENT '归班来源:AUTO/MANUAL/ROLLOVER', + assignment_version INT UNSIGNED NOT NULL COMMENT '同一对象的归班版本', + match_status VARCHAR(24) NOT NULL COMMENT 'FORECAST_MATCHED/RECEIVED/LATE/UNMATCHED/SUPERSEDED', + volume_cbm DECIMAL(18,4) NULL COMMENT '已收实测体积', + forecast_volume_cbm DECIMAL(18,4) NULL COMMENT '未收有效预计体积', + route_source VARCHAR(32) NULL COMMENT '航线来源:INBOUND_FACT/FORECAST_FACT/CHANNEL_DEFAULT', + volume_source VARCHAR(32) NULL COMMENT 'MEASURED_ACTUAL或预报来源', + rule_version VARCHAR(64) NOT NULL COMMENT '自动归班规则版本', + matched_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + superseded_at DATETIME(3) NULL, + locked_at DATETIME(3) NULL COMMENT '截止/订舱/关闭后冻结时间', + lock_reason VARCHAR(32) NULL COMMENT 'CUTOFF/BOOKING_CONFIRMED/CLOSED', + operator_id BIGINT UNSIGNED NULL, + reason VARCHAR(500) NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + KEY idx_sailing_assignment_current (tenant_id, sailing_id, match_status, matched_at, id), + KEY idx_sailing_assignment_origin (tenant_id, origin_sailing_id, match_status, id), + KEY idx_sailing_assignment_receipt (tenant_id, receipt_batch_id, assignment_version, id), + KEY idx_sailing_assignment_forecast (tenant_id, forecast_id, assignment_version, id), + KEY idx_sailing_assignment_inbound (tenant_id, inbound_id, match_status, id), + CONSTRAINT chk_sailing_assignment_status CHECK (match_status IN ('FORECAST_MATCHED','RECEIVED','LATE','UNMATCHED','SUPERSEDED')), + CONSTRAINT chk_sailing_assignment_source CHECK (assignment_source IN ('AUTO','MANUAL','ROLLOVER')) +) ENGINE=InnoDB COMMENT='船期日历自动归班事实及不可变版本历史'; + +CREATE TABLE fl_sailing_booking_link ( + id BIGINT UNSIGNED NOT NULL COMMENT '船期-订舱关联ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台关联隔离键', + sailing_id BIGINT UNSIGNED NOT NULL COMMENT '计划船期实例ID', + ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '海运订舱ID', + link_status VARCHAR(24) NOT NULL DEFAULT 'ACTIVE' COMMENT '关联状态:ACTIVE/REVOKED', + active_booking_guard TINYINT NULL COMMENT 'ACTIVE为1,REVOKED为空,用于唯一约束', + booking_status_snapshot VARCHAR(24) NOT NULL COMMENT '关联时订舱状态快照;只有RELEASED可建立', + warehouse_id BIGINT UNSIGNED NULL COMMENT '关联仓库快照', + channel_code VARCHAR(64) NULL COMMENT '关联渠道快照', + departure_port_code VARCHAR(32) NULL COMMENT '关联起运港快照', + destination_port_code VARCHAR(32) NULL COMMENT '关联目的港快照', + linked_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + revoked_at DATETIME(3) NULL, + operator_id BIGINT UNSIGNED NULL, + operator_name VARCHAR(64) NULL, + reason VARCHAR(500) NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_sailing_booking_active (ocean_booking_id, active_booking_guard), + KEY idx_sailing_booking_link (sailing_id, link_status, ocean_booking_id), + KEY idx_sailing_booking_booking (ocean_booking_id, link_status, id), + CONSTRAINT chk_sailing_booking_link_status CHECK (link_status IN ('ACTIVE','REVOKED')), + CONSTRAINT chk_sailing_booking_link_guard CHECK ((link_status='ACTIVE' AND active_booking_guard=1) OR (link_status='REVOKED' AND active_booking_guard IS NULL)), + CONSTRAINT chk_sailing_booking_released CHECK (booking_status_snapshot='RELEASED') +) ENGINE=InnoDB COMMENT='船期计划与已放舱订舱的最小关联及历史'; + +SELECT 'PASS' AS initialization_status, 'CN' AS region, 219 AS initialized_tables; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingAssignmentController.java b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingAssignmentController.java new file mode 100644 index 000000000..cb031a952 --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingAssignmentController.java @@ -0,0 +1,34 @@ +package com.baiyue.firstleg.sailingcalendar.controller; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.security.AuthenticatedUser; +import com.baiyue.firstleg.sailingcalendar.dto.SailingCalendarDtos.ExceptionPage; +import com.baiyue.firstleg.sailingcalendar.dto.SailingCalendarDtos.ExceptionQuery; +import com.baiyue.firstleg.sailingcalendar.service.SailingCalendarService; +import com.baiyue.firstleg.service.FirstlegScopeGuard; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Validated +@RestController +@Tag(name = "船期异常货量") +@RequestMapping("/api/platform/v1/sailing-assignments") +public class PlatformSailingAssignmentController { + @Resource private SailingCalendarService service; + + @GetMapping("/exceptions") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-calendar:list')") + @Operation(summary = "查询未安排和晚到货量") + public ApiResponse exceptions(@AuthenticationPrincipal AuthenticatedUser user, @Valid ExceptionQuery query) { + FirstlegScopeGuard.platform(user); + return ApiResponse.success(service.exceptions(query)); + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingCalendarController.java b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingCalendarController.java new file mode 100644 index 000000000..74d2555dd --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingCalendarController.java @@ -0,0 +1,104 @@ +package com.baiyue.firstleg.sailingcalendar.controller; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.security.AuthenticatedUser; +import com.baiyue.firstleg.sailingcalendar.dto.SailingCalendarDtos; +import com.baiyue.firstleg.sailingcalendar.dto.SailingCalendarDtos.*; +import com.baiyue.firstleg.sailingcalendar.entity.SailingEntity; +import com.baiyue.firstleg.sailingcalendar.service.SailingCalendarService; +import com.baiyue.firstleg.service.FirstlegScopeGuard; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; + +@Validated +@RestController +@Tag(name = "船期日历") +@RequestMapping("/api/platform/v1/sailings") +public class PlatformSailingCalendarController { + @Resource private SailingCalendarService service; + + @GetMapping("/calendar") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-calendar:list')") + @Operation(summary = "查询船期月历") + public ApiResponse calendar(@AuthenticationPrincipal AuthenticatedUser user, @Valid SailingQuery query) { + FirstlegScopeGuard.platform(user); return ApiResponse.success(service.calendar(query)); + } + + @GetMapping("/{id}") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-calendar:list')") + public ApiResponse detail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id) { + FirstlegScopeGuard.platform(user); return ApiResponse.success(service.detail(id)); + } + + @GetMapping("/{id}/history") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-calendar:list')") + @Operation(summary = "查询船期变更历史") + public ApiResponse> history(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id) { + FirstlegScopeGuard.platform(user); return ApiResponse.success(service.history(id)); + } + + @PostMapping + @PreAuthorize("hasAuthority('platform:firstleg:sailing-calendar:create')") + public ApiResponse create(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody SailingCreateRequest request) { + FirstlegScopeGuard.platform(user); return ApiResponse.success(service.detail(service.createSailing(user.userId(), user.username(), request).getId())); + } + + @PutMapping("/{id}") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-calendar:edit')") + public ApiResponse update(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id, + @Valid @RequestBody SailingUpdateRequest request) { + FirstlegScopeGuard.platform(user); service.updateSailing(id, user.userId(), user.username(), request); return ApiResponse.success(service.detail(id)); + } + + @PostMapping("/{id}/cancel") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-calendar:edit')") + public ApiResponse cancel(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id, + @Valid @RequestBody CancelRequest request) { + FirstlegScopeGuard.platform(user); return ApiResponse.success(service.cancel(id, user.userId(), user.username(), request)); + } + + @PostMapping("/{id}/exception-reassign") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-volume:exception-reassign')") + public ApiResponse exceptionReassign(@AuthenticationPrincipal AuthenticatedUser user, + @PathVariable @Positive long id, + @Valid @RequestBody ExceptionReassignRequest request) { + FirstlegScopeGuard.platform(user); return ApiResponse.success(service.exceptionReassign(id, user.userId(), user.username(), request)); + } + + @PostMapping("/{id}/copy") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-calendar:create')") + public ApiResponse copy(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id, + @Valid @RequestBody CopyRequest request) { + FirstlegScopeGuard.platform(user); return ApiResponse.success(service.copy(id, user.userId(), user.username(), request)); + } + + @GetMapping("/{id}/volume-summary") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-calendar:list')") + public ApiResponse volumeSummary(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id) { + FirstlegScopeGuard.platform(user); return ApiResponse.success(service.volumeSummary(id)); + } + + @GetMapping("/{id}/booking-advice") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-calendar:list')") + public ApiResponse bookingAdvice(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id) { + FirstlegScopeGuard.platform(user); return ApiResponse.success(service.bookingAdvice(id)); + } + + @PostMapping("/{id}/booking-links") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-booking:link')") + public ApiResponse> bookingLink(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id, + @Valid @RequestBody BookingLinkRequest request) { + FirstlegScopeGuard.platform(user); return ApiResponse.success(service.linkBooking(id, user.userId(), user.username(), request)); + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingChannelController.java b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingChannelController.java new file mode 100644 index 000000000..a55b925b7 --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingChannelController.java @@ -0,0 +1,30 @@ +package com.baiyue.firstleg.sailingcalendar.controller; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.firstleg.sailingcalendar.dto.SailingCalendarDtos.*; +import com.baiyue.firstleg.sailingcalendar.entity.SailingChannelEntity; +import com.baiyue.firstleg.sailingcalendar.service.SailingCalendarService; +import com.baiyue.firstleg.service.FirstlegScopeGuard; +import com.baiyue.common.security.AuthenticatedUser; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Validated +@RestController +@RequestMapping("/api/platform/v1/sailing-channels") +public class PlatformSailingChannelController { + @Resource private SailingCalendarService service; + @GetMapping @PreAuthorize("hasAuthority('platform:firstleg:sailing-calendar:list')") + public ApiResponse> list(@AuthenticationPrincipal AuthenticatedUser user, @Valid ChannelQuery query) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.channels(query).stream().map(ChannelView::channel).toList()); } + @PostMapping @PreAuthorize("hasAuthority('platform:firstleg:sailing-template:manage')") + public ApiResponse create(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody ChannelCreateRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.createChannel(user.userId(), user.username(), request)); } + @PutMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:sailing-template:manage')") + public ApiResponse update(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id, @Valid @RequestBody ChannelUpdateRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.updateChannel(id, user.userId(), user.username(), request)); } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingForecastController.java b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingForecastController.java new file mode 100644 index 000000000..3bdbc64c8 --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingForecastController.java @@ -0,0 +1,74 @@ +package com.baiyue.firstleg.sailingcalendar.controller; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.security.AuthenticatedUser; +import com.baiyue.firstleg.sailingcalendar.dto.SailingCalendarDtos.ForecastCreateRequest; +import com.baiyue.firstleg.sailingcalendar.dto.SailingCalendarDtos.ForecastRevisionRequest; +import com.baiyue.firstleg.sailingcalendar.dto.SailingCalendarDtos.ForecastReceiptBindRequest; +import com.baiyue.firstleg.sailingcalendar.dto.SailingCalendarDtos.ForecastVoidRequest; +import com.baiyue.firstleg.sailingcalendar.fact.entity.SailingInboundVolumeForecastEntity; +import com.baiyue.firstleg.sailingcalendar.service.SailingCalendarService; +import com.baiyue.firstleg.service.FirstlegScopeGuard; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** 平台运营维护船期日历使用的正式未收货量预报事实入口。 */ +@Validated +@RestController +@Tag(name = "船期货量预报") +@RequestMapping("/api/platform/v1/sailing-forecasts") +public class PlatformSailingForecastController { + @Resource private SailingCalendarService service; + + @PostMapping + @PreAuthorize("hasAuthority('platform:firstleg:sailing-volume:forecast-manage')") + @Operation(summary = "新增未收货量预报", description = "保存版本化预报事实并按仓库、渠道、港口和截止时间自动归班;无法解释时保留UNMATCHED。") + public ApiResponse create(@AuthenticationPrincipal AuthenticatedUser user, + @Valid @RequestBody ForecastCreateRequest request) { + FirstlegScopeGuard.platform(user, request.tenantId()); + return ApiResponse.success(service.createForecast(user.userId(), request)); + } + + @PutMapping("/{id}") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-volume:forecast-manage')") + @Operation(summary = "修订未收货量预报", description = "旧版本标记为SUPERSEDED,新版本重新归班;不覆盖历史版本。") + public ApiResponse revise(@AuthenticationPrincipal AuthenticatedUser user, + @PathVariable long id, + @Valid @RequestBody ForecastRevisionRequest request) { + FirstlegScopeGuard.platform(user, request.tenantId()); + return ApiResponse.success(service.reviseForecast(user.userId(), id, request)); + } + + @PostMapping("/{id}/void") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-volume:forecast-manage')") + @Operation(summary = "作废未收货量预报", description = "作废有效预报并将其当前归班版本标记为SUPERSEDED。") + public ApiResponse voidForecast(@AuthenticationPrincipal AuthenticatedUser user, + @PathVariable long id, + @Valid @RequestBody ForecastVoidRequest request) { + FirstlegScopeGuard.platform(user, request.tenantId()); + service.voidForecast(user.userId(), id, request); + return ApiResponse.success((Void) null); + } + + @PostMapping("/{id}/bind-receipt") + @PreAuthorize("hasAuthority('platform:firstleg:sailing-volume:forecast-manage')") + @Operation(summary = "将实收批次绑定到预报批次", description = "多批预报无法由实测体积唯一解释时,由运营显式选择预报批次;服务端保留原历史版本。") + public ApiResponse bindReceipt(@AuthenticationPrincipal AuthenticatedUser user, + @PathVariable long id, + @Valid @RequestBody ForecastReceiptBindRequest request) { + FirstlegScopeGuard.platform(user, request.tenantId()); + service.bindReceiptToForecast(user.userId(), id, request); + return ApiResponse.success((Void) null); + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingTemplateController.java b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingTemplateController.java new file mode 100644 index 000000000..08699772e --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/controller/PlatformSailingTemplateController.java @@ -0,0 +1,40 @@ +package com.baiyue.firstleg.sailingcalendar.controller; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.core.api.PageResult; +import com.baiyue.common.security.AuthenticatedUser; +import com.baiyue.firstleg.sailingcalendar.dto.SailingCalendarDtos.*; +import com.baiyue.firstleg.sailingcalendar.entity.SailingEntity; +import com.baiyue.firstleg.sailingcalendar.entity.SailingTemplateEntity; +import com.baiyue.firstleg.sailingcalendar.service.SailingCalendarService; +import com.baiyue.firstleg.service.FirstlegScopeGuard; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; + +@Validated +@RestController +@RequestMapping("/api/platform/v1/sailing-templates") +public class PlatformSailingTemplateController { + @Resource private SailingCalendarService service; + @GetMapping @PreAuthorize("hasAuthority('platform:firstleg:sailing-template:manage')") + public ApiResponse> page(@AuthenticationPrincipal AuthenticatedUser user, @Valid TemplateQuery query) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.templateViews(query)); } + @PostMapping @PreAuthorize("hasAuthority('platform:firstleg:sailing-template:manage')") + public ApiResponse create(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody TemplateCreateRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.templateView(service.createTemplate(user.userId(), user.username(), request).getId())); } + @PutMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:sailing-template:manage')") + public ApiResponse update(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id, @Valid @RequestBody TemplateUpdateRequest request) { FirstlegScopeGuard.platform(user); service.updateTemplate(id, user.userId(), user.username(), request); return ApiResponse.success(service.templateView(id)); } + @PostMapping("/{id}/generation-preview") @PreAuthorize("hasAuthority('platform:firstleg:sailing-template:manage')") + public ApiResponse preview(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id, + @Valid @RequestBody GenerationRange range) { FirstlegScopeGuard.platform(user); return ApiResponse.success(new GenerationPreviewResult(id, service.generationPreview(id, range.from(), range.to()))); } + @PostMapping("/{id}/generate") @PreAuthorize("hasAuthority('platform:firstleg:sailing-template:manage')") + public ApiResponse generate(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id, + @Valid @RequestBody GenerationRange range) { FirstlegScopeGuard.platform(user); List generated = service.generate(user.userId(), user.username(), id, range.from(), range.to()); List records = service.generationPreview(id, range.from(), range.to()); return ApiResponse.success(new GenerationResult(id, records, generated.size(), Math.max(0, records.size() - generated.size()))); } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/dto/SailingCalendarDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/dto/SailingCalendarDtos.java new file mode 100644 index 000000000..715b7e293 --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/dto/SailingCalendarDtos.java @@ -0,0 +1,283 @@ +package com.baiyue.firstleg.sailingcalendar.dto; + +import com.baiyue.firstleg.sailingcalendar.entity.SailingChannelEntity; +import com.baiyue.firstleg.sailingcalendar.entity.SailingEntity; +import com.baiyue.firstleg.sailingcalendar.entity.SailingTemplateEntity; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +public final class SailingCalendarDtos { + private SailingCalendarDtos() {} + + public record ChannelQuery(@Size(max = 64) String keyword, String status) {} + public record ChannelData(@NotBlank @Size(max = 64) String channelCode, + @NotBlank @Size(max = 128) String channelName, + @NotBlank String sourceType, + @Size(max = 64) String mappedChannelCode, + @Size(max = 32) String defaultDeparturePortCode, + @Size(max = 32) String defaultDestinationPortCode, + @Min(0) @Max(30) Integer defaultCutoffDays, + @Size(max = 1000) String remark) {} + /** Flat JSON contract; data() keeps the service boundary grouped by domain object. */ + public record ChannelCreateRequest(@Min(0) Integer expectedVersion, + @NotBlank @Size(max = 64) String channelCode, + @NotBlank @Size(max = 128) String channelName, + @NotBlank String sourceType, + @Size(max = 64) String mappedChannelCode, + @Size(max = 32) String defaultDeparturePortCode, + @Size(max = 32) String defaultDestinationPortCode, + @Min(0) @Max(30) Integer defaultCutoffDays, + @Size(max = 1000) String remark) { + public ChannelData data() { return new ChannelData(channelCode, channelName, sourceType, + mappedChannelCode, defaultDeparturePortCode, defaultDestinationPortCode, defaultCutoffDays, remark); } + } + public record ChannelUpdateRequest(@Min(0) Integer expectedVersion, + @NotBlank @Size(max = 64) String channelCode, + @NotBlank @Size(max = 128) String channelName, + @NotBlank String sourceType, + @Size(max = 64) String mappedChannelCode, + @Size(max = 32) String defaultDeparturePortCode, + @Size(max = 32) String defaultDestinationPortCode, + @Min(0) @Max(30) Integer defaultCutoffDays, + @Size(max = 1000) String remark, + @NotBlank String status) { + public ChannelData data() { return new ChannelData(channelCode, channelName, sourceType, + mappedChannelCode, defaultDeparturePortCode, defaultDestinationPortCode, defaultCutoffDays, remark); } + } + public record ChannelView(SailingChannelEntity channel, boolean editable) {} + + public record TemplateQuery(@Positive Long warehouseId, @Positive Long channelId, + String status, @Min(1) long page, @Min(1) @Max(200) long size) { + public TemplateQuery { if (page == 0) page = 1; if (size == 0) size = 20; } + } + public record TemplateData(@Size(max = 128) String templateName, + @NotNull @Positive Long channelId, + @NotNull @Positive Long warehouseId, + @Size(max = 128) String warehouseName, + List<@NotNull @Min(1) @Max(7) Integer> weekdays, + @NotNull LocalDate effectiveFrom, + LocalDate effectiveTo, + @NotNull LocalTime departureTime, + @NotNull @Min(0) @Max(30) Integer cutoffDays, + @NotNull LocalTime cutoffLocalTime, + @Size(max = 64) String voyageRule, + @Size(max = 128) String vesselName, + @Size(max = 32) String departurePortCode, + @Size(max = 32) String destinationPortCode, + @Size(max = 1000) String remark) {} + public record TemplateCreateRequest(@Min(0) Integer expectedVersion, + @Size(max = 128) String templateName, + @NotNull @Positive Long channelId, + @NotNull @Positive Long warehouseId, + @Size(max = 128) String warehouseName, + List<@NotNull @Min(1) @Max(7) Integer> weekdays, + @NotNull LocalDate effectiveFrom, + LocalDate effectiveTo, + @NotNull LocalTime departureTime, + @NotNull @Min(0) @Max(30) Integer cutoffDays, + @NotNull LocalTime cutoffLocalTime, + @Size(max = 64) String voyageRule, + @Size(max = 128) String vesselName, + @Size(max = 32) String departurePortCode, + @Size(max = 32) String destinationPortCode, + @Size(max = 1000) String remark) { + public TemplateData data() { return new TemplateData(templateName, channelId, warehouseId, warehouseName, + weekdays, effectiveFrom, effectiveTo, departureTime, cutoffDays, cutoffLocalTime, voyageRule, + vesselName, departurePortCode, destinationPortCode, remark); } + } + public record TemplateUpdateRequest(@Min(0) Integer expectedVersion, + @Size(max = 128) String templateName, + @NotNull @Positive Long channelId, + @NotNull @Positive Long warehouseId, + @Size(max = 128) String warehouseName, + List<@NotNull @Min(1) @Max(7) Integer> weekdays, + @NotNull LocalDate effectiveFrom, + LocalDate effectiveTo, + @NotNull LocalTime departureTime, + @NotNull @Min(0) @Max(30) Integer cutoffDays, + @NotNull LocalTime cutoffLocalTime, + @Size(max = 64) String voyageRule, + @Size(max = 128) String vesselName, + @Size(max = 32) String departurePortCode, + @Size(max = 32) String destinationPortCode, + @Size(max = 1000) String remark, + String status) { + public TemplateData data() { return new TemplateData(templateName, channelId, warehouseId, warehouseName, + weekdays, effectiveFrom, effectiveTo, departureTime, cutoffDays, cutoffLocalTime, voyageRule, + vesselName, departurePortCode, destinationPortCode, remark); } + } + public record TemplateView(Long id, String templateNo, String templateName, Long channelId, String channelCode, + String channelName, Long warehouseId, String warehouseName, List weekdays, + LocalDate effectiveFrom, LocalDate effectiveTo, LocalTime departureTime, + Integer cutoffDays, LocalTime cutoffLocalTime, String voyageRule, String vesselName, + String departurePortCode, String destinationPortCode, String remark, + String status, Integer version) {} + + public record SailingQuery(@NotBlank String month, + @Positive Long warehouseId, @Positive Long channelId, + @Size(max = 64) String channelCode, + @Size(max = 32) String departurePortCode, + @Size(max = 32) String destinationPortCode, + @Size(max = 128) String keyword, String status) {} + public record SailingData(@Positive Long templateId, @Positive Long channelId, + @NotNull @Positive Long warehouseId, + @Size(max = 128) String warehouseName, + @NotBlank @Size(max = 64) String channelCode, + @NotBlank @Size(max = 128) String channelName, + @NotBlank @Size(max = 32) String departurePortCode, + @Size(max = 128) String departurePortName, + @NotBlank @Size(max = 32) String destinationPortCode, + @Size(max = 128) String destinationPortName, + @NotBlank @Size(max = 128) String vesselName, + @NotBlank @Size(max = 64) String voyageNo, + @NotNull LocalDateTime etdAt, + LocalDateTime etaAt, + @NotNull LocalDateTime cutoffAt, + @Size(max = 64) String sourceType, + @Size(max = 128) String remark, + BigDecimal plannedCapacityCbm) {} + public record SailingCreateRequest(@Min(0) Integer expectedVersion, + @Positive Long templateId, + @Positive Long channelId, + @NotNull @Positive Long warehouseId, + @Size(max = 128) String warehouseName, + @NotBlank @Size(max = 64) String channelCode, + @NotBlank @Size(max = 128) String channelName, + @NotBlank @Size(max = 32) String departurePortCode, + @Size(max = 128) String departurePortName, + @NotBlank @Size(max = 32) String destinationPortCode, + @Size(max = 128) String destinationPortName, + @NotBlank @Size(max = 128) String vesselName, + @NotBlank @Size(max = 64) String voyageNo, + @NotNull LocalDateTime etdAt, + LocalDateTime etaAt, + @NotNull LocalDateTime cutoffAt, + @Size(max = 64) String sourceType, + @Size(max = 128) String remark, + BigDecimal plannedCapacityCbm) { + public SailingData data() { return new SailingData(templateId, channelId, warehouseId, warehouseName, + channelCode, channelName, departurePortCode, departurePortName, destinationPortCode, destinationPortName, + vesselName, voyageNo, etdAt, etaAt, cutoffAt, sourceType, remark, plannedCapacityCbm); } + } + public record SailingUpdateRequest(@Min(0) Integer expectedVersion, + @Positive Long templateId, + @Positive Long channelId, + @NotNull @Positive Long warehouseId, + @Size(max = 128) String warehouseName, + @NotBlank @Size(max = 64) String channelCode, + @NotBlank @Size(max = 128) String channelName, + @NotBlank @Size(max = 32) String departurePortCode, + @Size(max = 128) String departurePortName, + @NotBlank @Size(max = 32) String destinationPortCode, + @Size(max = 128) String destinationPortName, + @NotBlank @Size(max = 128) String vesselName, + @NotBlank @Size(max = 64) String voyageNo, + @NotNull LocalDateTime etdAt, + LocalDateTime etaAt, + @NotNull LocalDateTime cutoffAt, + @Size(max = 64) String sourceType, + @Size(max = 128) String remark, + BigDecimal plannedCapacityCbm, + String status) { + public SailingData data() { return new SailingData(templateId, channelId, warehouseId, warehouseName, + channelCode, channelName, departurePortCode, departurePortName, destinationPortCode, destinationPortName, + vesselName, voyageNo, etdAt, etaAt, cutoffAt, sourceType, remark, plannedCapacityCbm); } + } + public record CalendarItem(Long id, String sailingNo, Long warehouseId, String warehouseName, + String channelCode, String channelName, String vesselName, String voyageNo, + String departurePortCode, String destinationPortCode, LocalDateTime etdAt, + LocalDateTime cutoffAt, String status, Integer version, + VolumeSummary volumeSummary) {} + public record VolumeSummary(BigDecimal actualReceivedCbm, BigDecimal catchableForecastCbm, + BigDecimal riskUnmatchedCbm, int bookedContainerCount, + int recommendedContainerCount, int pendingContainerCount, + int lateBatchCount, boolean overCapacity, BigDecimal planningCapacityCbm, + BigDecimal bookedPlanningCapacityCbm, LocalDateTime snapshotAt, + BigDecimal catchableReceivedCbm, BigDecimal lateReceivedCbm) {} + public record CalendarResult(String month, String timezone, List records, + Summary summary, FilterOptions filterOptions) { + public record Summary(BigDecimal actualReceivedCbm, BigDecimal catchableForecastCbm, BigDecimal riskUnmatchedCbm, + int bookedContainerCount, int recommendedContainerCount, int pendingContainerCount, + int lateBatchCount, boolean overCapacity, BigDecimal planningCapacityCbm, int sailingCount, + BigDecimal catchableReceivedCbm, BigDecimal lateReceivedCbm) {} + public record FilterOptions(List