From f31a0494448af13c36c20e458fd495ad53cd0225 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 13:54:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/头程数据库统一增量-MySQL8.sql | 24 +++++ .../PlatformTerminalDrayageController.java | 27 ++++++ .../dto/TerminalDrayageDtos.java | 1 + .../mapper/TerminalDrayageMapper.java | 61 +++++++++++++ .../TerminalDrayageMailDispatcher.java | 62 +++++++++++++ .../service/TerminalDrayageMailService.java | 88 +++++++++++++++++++ .../service/TerminalDrayageMailTemplates.java | 20 +++++ .../service/TerminalDrayageService.java | 25 ++++++ .../db/phase3-terminal-drayage-schema.sql | 24 +++++ .../ContainerWorkbenchServiceTest.java | 4 +- .../service/TerminalDrayageServiceTest.java | 4 +- 11 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageMailDispatcher.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageMailService.java create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageMailTemplates.java diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 47161a1ca..3b6c100d1 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -18133,6 +18133,30 @@ CREATE TABLE IF NOT EXISTS fl_terminal_drayage ( CONSTRAINT chk_terminal_drayage_times CHECK((actual_pickup_at IS NULL OR pickup_appointment_at IS NOT NULL) AND (actual_empty_return_at IS NULL OR actual_pickup_at IS NULL OR actual_empty_return_at >= actual_pickup_at)) ) ENGINE=InnoDB COMMENT='目的港货柜级码头拖车与还空事实'; +CREATE TABLE IF NOT EXISTS fl_terminal_drayage_mail ( + id BIGINT UNSIGNED NOT NULL, + container_booking_id BIGINT UNSIGNED NOT NULL, + fleet_supplier_id BIGINT UNSIGNED NOT NULL, + fleet_name VARCHAR(128) NOT NULL, + recipient VARCHAR(254) NOT NULL, + kind VARCHAR(32) NOT NULL, + subject VARCHAR(255) NOT NULL, + body TEXT NOT NULL, + status VARCHAR(16) NOT NULL DEFAULT 'PENDING', + attempts INT NOT NULL DEFAULT 0, + failure_reason VARCHAR(500) NULL, + created_by BIGINT UNSIGNED NOT NULL, + updated_by BIGINT UNSIGNED NOT 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), + sent_at DATETIME(3) NULL, + PRIMARY KEY(id), + KEY idx_terminal_mail_dispatch(status,id), + KEY idx_terminal_mail_container(container_booking_id,id,status), + CONSTRAINT fk_terminal_mail_container FOREIGN KEY(container_booking_id) REFERENCES fl_container_booking(id), + CONSTRAINT chk_terminal_mail_kind CHECK(kind IN ('REQUEST_PICKUP','CANCEL_PICKUP')), + CONSTRAINT chk_terminal_mail_status CHECK(status IN ('PENDING','SENDING','SENT','FAILED')) +) ENGINE=InnoDB COMMENT='码头车队邮件任务与投递记录'; CREATE TABLE IF NOT EXISTS fl_terminal_drayage_history ( id BIGINT UNSIGNED NOT NULL COMMENT '历史ID', drayage_id BIGINT UNSIGNED NOT NULL COMMENT '码头拖车聚合ID', container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '货柜ID快照', action VARCHAR(32) NOT NULL COMMENT '业务动作', diff --git a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/controller/PlatformTerminalDrayageController.java b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/controller/PlatformTerminalDrayageController.java index 3bcf2952d..bfbfb36d7 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/controller/PlatformTerminalDrayageController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/controller/PlatformTerminalDrayageController.java @@ -36,6 +36,33 @@ import java.util.List; @Tag(name = "目的港码头拖车") @RequestMapping("/api/platform/v1/terminal-drayages") public class PlatformTerminalDrayageController { + @jakarta.annotation.Resource private com.baiyue.firstleg.terminaldrayage.service.TerminalDrayageMailService mails; + + @PostMapping("/{containerBookingId}/pickup-email") + @PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:fleet:edit')") + public ApiResponse sendPickupEmail(@AuthenticationPrincipal AuthenticatedUser user, + @PathVariable @Positive long containerBookingId, @RequestHeader("Idempotency-Key") String key, + @Valid @RequestBody com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.EmailRequest request) { + FirstlegScopeGuard.platform(user); + return ApiResponse.success(service.authorize(service.sendPickupEmail(containerBookingId, user.userId(), user.username(), key, request), user.permissions())); + } + + @GetMapping("/{containerBookingId}/emails") + @PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:list')") + public ApiResponse emails( + @AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long containerBookingId) { + FirstlegScopeGuard.platform(user); service.detail(containerBookingId); + return ApiResponse.success(mails.list(containerBookingId)); + } + + @PostMapping("/{containerBookingId}/emails/{mailId}/retry") + @PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:fleet:edit')") + public ApiResponse retryEmail(@AuthenticationPrincipal AuthenticatedUser user, + @PathVariable @Positive long containerBookingId, @PathVariable @Positive long mailId) { + FirstlegScopeGuard.platform(user); service.detail(containerBookingId); + mails.retry(containerBookingId, mailId, user.userId()); + return ApiResponse.success(null); + } private final TerminalDrayageService service; @GetMapping diff --git a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/dto/TerminalDrayageDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/dto/TerminalDrayageDtos.java index 60d2c36b3..4db1f2972 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/dto/TerminalDrayageDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/dto/TerminalDrayageDtos.java @@ -11,6 +11,7 @@ import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerAc public final class TerminalDrayageDtos { private TerminalDrayageDtos() {} + public record EmailRequest(@NotNull @Min(0) Integer expectedVersion) {} public record FleetSelectionRequest(@NotNull @Min(0) Integer expectedVersion, @NotNull @Positive Long fleetSupplierId, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/mapper/TerminalDrayageMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/mapper/TerminalDrayageMapper.java index 6bf23c0dd..f2d7f3182 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/mapper/TerminalDrayageMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/mapper/TerminalDrayageMapper.java @@ -110,4 +110,65 @@ public interface TerminalDrayageMapper extends BaseMapper record Context(Long containerBookingId, String containerNo, Long oceanBookingId, String recordStatus, LocalDateTime unloadingCompletedAt) {} record Operation(String operation, String requestHash, Integer resultVersion) {} + + record MailTask(Long id, Long containerBookingId, Long fleetSupplierId, String fleetName, + String recipient, String kind, String subject, String body, String status, + Integer attempts, String failureReason, LocalDateTime createdAt, LocalDateTime sentAt) {} + + @Insert(""" + INSERT INTO fl_terminal_drayage_mail(id,container_booking_id,fleet_supplier_id,fleet_name, + recipient,kind,subject,body,status,created_by,updated_by) + VALUES(#{id},#{containerId},#{supplierId},#{name},#{email},#{kind},#{subject},#{body},'PENDING',#{operatorId},#{operatorId}) + """) + int insertMail(@Param("id") long id, @Param("containerId") long containerId, + @Param("supplierId") long supplierId, @Param("name") String name, @Param("email") String email, + @Param("kind") String kind, @Param("subject") String subject, @Param("body") String body, + @Param("operatorId") long operatorId); + + @Select("SELECT * FROM fl_terminal_drayage_mail WHERE container_booking_id=#{containerId} ORDER BY id DESC LIMIT 100") + List mails(@Param("containerId") long containerId); + + @Select(""" + SELECT * FROM fl_terminal_drayage_mail WHERE container_booking_id=#{containerId} + AND fleet_supplier_id=#{supplierId} AND kind='REQUEST_PICKUP' ORDER BY id DESC LIMIT 1 + """) + MailTask lastPickupMail(@Param("containerId") long containerId, @Param("supplierId") long supplierId); + + @Select(""" + SELECT m.* FROM fl_terminal_drayage_mail m WHERE m.status='PENDING' + AND NOT EXISTS(SELECT 1 FROM fl_terminal_drayage_mail earlier + WHERE earlier.container_booking_id=m.container_booking_id AND earlier.id'SENT') + ORDER BY m.id LIMIT 20 + """) + List pendingMails(); + + @Update(""" + UPDATE fl_terminal_drayage_mail SET status='SENDING',attempts=attempts+1,updated_at=CURRENT_TIMESTAMP(3) + WHERE id=#{id} AND status='PENDING' + """) + int claimMail(@Param("id") long id); + + @Update(""" + UPDATE fl_terminal_drayage_mail SET status='SENT',sent_at=CURRENT_TIMESTAMP(3),failure_reason=NULL, + updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND status='SENDING' + """) + int completeMail(@Param("id") long id); + + @Update(""" + UPDATE fl_terminal_drayage_mail SET status='FAILED',failure_reason=#{reason},updated_at=CURRENT_TIMESTAMP(3) + WHERE id=#{id} AND status='SENDING' + """) + int failMail(@Param("id") long id, @Param("reason") String reason); + + @Update(""" + UPDATE fl_terminal_drayage_mail SET status='FAILED',failure_reason='发送中断,结果待核对;确认未收到后再重试', + updated_at=CURRENT_TIMESTAMP(3) WHERE status='SENDING' AND updated_at records) {} + public Mailbox list(long containerBookingId) { return new Mailbox(enabled, mapper.mails(containerBookingId)); } + + public void request(long containerId, String containerNo, long supplierId, long operatorId) { + var contact = contact(supplierId, true); + enqueue(containerId, containerNo, supplierId, contact.name(), contact.email(), "REQUEST_PICKUP", operatorId); + } + + public void replaceFleet(long containerId, String containerNo, long oldSupplierId, long newSupplierId, long operatorId) { + var next = contact(newSupplierId, true); + // 取消通知发往原请求的收件地址;尚未发过请求则读取原车队资料,允许原车队已停用。 + MailTask previous = mapper.lastPickupMail(containerId, oldSupplierId); + var old = previous == null ? contact(oldSupplierId, false) + : new WarehouseSupplierClient.TruckingContact(oldSupplierId, previous.fleetName(), previous.recipient()); + enqueue(containerId, containerNo, oldSupplierId, old.name(), old.email(), "CANCEL_PICKUP", operatorId); + enqueue(containerId, containerNo, newSupplierId, next.name(), next.email(), "REQUEST_PICKUP", operatorId); + } + + private WarehouseSupplierClient.TruckingContact contact(long supplierId, boolean requireActive) { + try { + var response = suppliers.truckingContact(internalToken, supplierId, requireActive); + if (response == null || !response.successful() || response.data() == null + || response.data().supplierId() != supplierId) { + throw new BizException(ResultCode.CONFLICT, "车队联系资料不可用,请检查供应商资料"); + } + var value = response.data(); + validateAddress(value.email()); + if (!StringUtils.hasText(value.name())) throw new BizException(ResultCode.CONFLICT, "车队名称缺失"); + return value; + } catch (FeignException exception) { + throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "车队联系资料服务暂不可用"); + } + } + + static void validateAddress(String email) { + try { + if (!StringUtils.hasText(email) || email.contains("\r") || email.contains("\n")) throw new IllegalArgumentException(); + InternetAddress[] parsed = InternetAddress.parse(email.trim(), true); + if (parsed.length != 1 || parsed[0].isGroup() || !email.trim().equals(parsed[0].getAddress())) throw new IllegalArgumentException(); + parsed[0].validate(); + if (!parsed[0].getAddress().contains("@")) throw new IllegalArgumentException(); + } catch (Exception exception) { + throw new BizException(ResultCode.BAD_REQUEST, "车队未配置有效邮箱,请先在供应商资料中维护单个收件邮箱"); + } + } + + private void enqueue(long containerId, String containerNo, long supplierId, String fleetName, + String email, String kind, long operatorId) { + if (!StringUtils.hasText(containerNo)) throw new BizException(ResultCode.CONFLICT, "请先补齐真实集装箱号再发送邮件"); + validateAddress(email); + var content = TerminalDrayageMailTemplates.render(kind, containerNo, fleetName); + mapper.insertMail(IdWorker.getId(), containerId, supplierId, fleetName, email.trim(), kind, + content.subject(), content.body(), operatorId); + } + + @Transactional + public void retry(long containerId, long mailId, long operatorId) { + if (mapper.retryMail(containerId, mailId, operatorId) != 1) { + throw new BizException(ResultCode.CONFLICT, "只有发送失败的邮件可以重试,请刷新邮件记录"); + } + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageMailTemplates.java b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageMailTemplates.java new file mode 100644 index 000000000..2c4b73f2d --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageMailTemplates.java @@ -0,0 +1,20 @@ +package com.baiyue.firstleg.terminaldrayage.service; + +/** 临时英文模板。正式邮件文案到位后只替换此处,不改变任务与发送流程。 */ +public final class TerminalDrayageMailTemplates { + private TerminalDrayageMailTemplates() {} + public record Content(String subject, String body) {} + + public static Content render(String kind, String containerNo, String fleetName) { + boolean cancel = "CANCEL_PICKUP".equals(kind); + String subject = (cancel ? "Cancel port pickup - " : "Port pickup request - ") + containerNo; + String body = "Dear " + fleetName + ",\n\n" + + (cancel ? "Please cancel our port pickup request for container " + : "Please arrange port pickup for container ") + + containerNo + ".\n" + + (cancel ? "Please do not dispatch a truck for this request.\n" + : "Please contact our operations team to confirm the pickup details before dispatch.\n") + + "\nPlease reply to confirm receipt.\nThank you.\nOperations Team"; + return new Content(subject, body); + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageService.java b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageService.java index fe95c4aed..d6a6a272b 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageService.java @@ -46,6 +46,7 @@ public class TerminalDrayageService { private final WarehouseSupplierSnapshotService suppliers; private final CarrierScheduleQueryService carrierSchedules; private final ObjectMapper objectMapper; + private final TerminalDrayageMailService mails; private Clock clock = Clock.systemDefaultZone(); public View detail(long containerBookingId) { @@ -95,6 +96,10 @@ public class TerminalDrayageService { requireVersion(value, request.expectedVersion()); if (actualPickupAt(context, value) != null) throw conflict("实际提柜后不可更换码头车队"); var fleet = suppliers.trucking(request.fleetSupplierId(), request.fleetSupplierName()); + if (value != null && Objects.equals(value.getFleetSupplierId(), fleet.supplierId())) return view(context, value); + if (value != null && value.getFleetSupplierId() != null) { + mails.replaceFleet(containerBookingId, context.base().containerNo(), value.getFleetSupplierId(), fleet.supplierId(), operatorId); + } int fromVersion = version(value); String fromStatus = status(context, value); if (value == null) value = create(containerBookingId, operatorId, operatorName); @@ -197,6 +202,23 @@ public class TerminalDrayageService { actualAt, null, null, StringUtils.hasText(reason) ? reason.trim() : "码头拖车操作同步")); } + @Transactional + public View sendPickupEmail(long containerBookingId, long operatorId, String operatorName, String key, + com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.EmailRequest request) { + String action = "SEND_PICKUP_EMAIL"; + WorkContext context = requiredLockedContext(containerBookingId); + View replay = replay(context, action, key, request); + if (replay != null) return replay; + TerminalDrayageEntity value = requiredAggregate(containerBookingId); + requireVersion(value, request.expectedVersion()); + if (!allowedActions(context, value).contains(action)) throw conflict("当前货柜不能发送提柜请求邮件"); + int fromVersion = version(value); + String fromStatus = status(context, value); + mails.request(containerBookingId, context.base().containerNo(), value.getFleetSupplierId(), operatorId); + persist(value, fromVersion, operatorId, operatorName); + return finish(context, value, action, key, request, fromStatus, fromVersion, "提柜邮件已加入发送队列", operatorId, operatorName); + } + private View finish(WorkContext context, TerminalDrayageEntity value, String action, String key, Object request, String fromStatus, int fromVersion, String reason, long operatorId, String operatorName) { @@ -267,6 +289,8 @@ public class TerminalDrayageService { private List allowedActions(WorkContext context, TerminalDrayageEntity value) { List result = new ArrayList<>(); + if (value != null && value.getFleetSupplierId() != null && actualPickupAt(context, value) == null + && actualEmptyReturnAt(context, value) == null && context.base().unloadingCompletedAt() == null) result.add("SEND_PICKUP_EMAIL"); CarrierScheduleNode current = node(context.facts().latestActualNode()); if (current != null && current.sequence() >= CarrierScheduleNode.ARRIVED_DESTINATION_PORT.sequence() && current.sequence() < CarrierScheduleNode.PICKED_UP.sequence() @@ -371,6 +395,7 @@ public class TerminalDrayageService { private static String permission(String action) { return switch (action) { case "SELECT_FLEET" -> "platform:firstleg:terminal-drayage:fleet:edit"; + case "SEND_PICKUP_EMAIL" -> "platform:firstleg:terminal-drayage:fleet:edit"; case "SCHEDULE_PICKUP" -> "platform:firstleg:terminal-drayage:pickup-appointment:edit"; case "RECORD_PICKUP" -> "platform:firstleg:terminal-drayage:pickup:confirm"; case "RECORD_EMPTY_RETURN" -> "platform:firstleg:terminal-drayage:empty-return:confirm"; diff --git a/firstleg/src/main/resources/db/phase3-terminal-drayage-schema.sql b/firstleg/src/main/resources/db/phase3-terminal-drayage-schema.sql index 4b779b7fa..ab8619c7c 100644 --- a/firstleg/src/main/resources/db/phase3-terminal-drayage-schema.sql +++ b/firstleg/src/main/resources/db/phase3-terminal-drayage-schema.sql @@ -26,6 +26,30 @@ CREATE TABLE IF NOT EXISTS fl_terminal_drayage ( (actual_empty_return_at IS NULL OR actual_pickup_at IS NULL OR actual_empty_return_at >= actual_pickup_at)) ) ENGINE=InnoDB COMMENT='目的港货柜级码头拖车与还空事实'; +CREATE TABLE IF NOT EXISTS fl_terminal_drayage_mail ( + id BIGINT UNSIGNED NOT NULL, + container_booking_id BIGINT UNSIGNED NOT NULL, + fleet_supplier_id BIGINT UNSIGNED NOT NULL, + fleet_name VARCHAR(128) NOT NULL, + recipient VARCHAR(254) NOT NULL, + kind VARCHAR(32) NOT NULL, + subject VARCHAR(255) NOT NULL, + body TEXT NOT NULL, + status VARCHAR(16) NOT NULL DEFAULT 'PENDING', + attempts INT NOT NULL DEFAULT 0, + failure_reason VARCHAR(500) NULL, + created_by BIGINT UNSIGNED NOT NULL, + updated_by BIGINT UNSIGNED NOT 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), + sent_at DATETIME(3) NULL, + PRIMARY KEY(id), + KEY idx_terminal_mail_dispatch(status,id), + KEY idx_terminal_mail_container(container_booking_id,id,status), + CONSTRAINT fk_terminal_mail_container FOREIGN KEY(container_booking_id) REFERENCES fl_container_booking(id), + CONSTRAINT chk_terminal_mail_kind CHECK(kind IN ('REQUEST_PICKUP','CANCEL_PICKUP')), + CONSTRAINT chk_terminal_mail_status CHECK(status IN ('PENDING','SENDING','SENT','FAILED')) +) ENGINE=InnoDB COMMENT='码头车队邮件任务与投递记录'; CREATE TABLE IF NOT EXISTS fl_terminal_drayage_history ( id BIGINT UNSIGNED NOT NULL COMMENT '历史ID', drayage_id BIGINT UNSIGNED NOT NULL COMMENT '码头拖车聚合ID', diff --git a/firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/service/ContainerWorkbenchServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/service/ContainerWorkbenchServiceTest.java index 628f38447..ef54e5fd8 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/service/ContainerWorkbenchServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/service/ContainerWorkbenchServiceTest.java @@ -103,7 +103,7 @@ class ContainerWorkbenchServiceTest { var mapper = mock(com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper.class); var json = new com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules(); var realDrayages = new TerminalDrayageService(mapper, - mock(com.baiyue.firstleg.service.WarehouseSupplierSnapshotService.class), carrierSchedules, json); + mock(com.baiyue.firstleg.service.WarehouseSupplierSnapshotService.class), carrierSchedules, json, mock(TerminalDrayageMailService.class)); ReflectionTestUtils.setField(service, "terminalDrayages", realDrayages); ContainerBookingEntity container = container(); container.setUnloadingCompletedAt(LocalDateTime.of(2026, 9, 5, 8, 0)); @@ -148,7 +148,7 @@ class ContainerWorkbenchServiceTest { var mapper = mock(com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper.class); var realDrayages = new TerminalDrayageService(mapper, mock(com.baiyue.firstleg.service.WarehouseSupplierSnapshotService.class), carrierSchedules, - new com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules()); + new com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules(), mock(TerminalDrayageMailService.class)); ReflectionTestUtils.setField(service, "terminalDrayages", realDrayages); ContainerBookingEntity container = container(); LocalDateTime unloadedAt = LocalDateTime.of(2026, 9, 5, 8, 0); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageServiceTest.java index 16fae4d27..d8e86def6 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/service/TerminalDrayageServiceTest.java @@ -57,7 +57,7 @@ class TerminalDrayageServiceTest { if (facts.isEmpty()) return List.of(); return List.of(facts.size() > 1 ? facts.removeFirst() : facts.peekFirst()); }); - service = new TerminalDrayageService(mapper, suppliers, carrierSchedules, new ObjectMapper().findAndRegisterModules()); + service = new TerminalDrayageService(mapper, suppliers, carrierSchedules, new ObjectMapper().findAndRegisterModules(), mock(TerminalDrayageMailService.class)); service.setClock(Clock.fixed(Instant.parse("2026-09-05T02:00:00Z"), ZoneOffset.UTC)); } @@ -95,7 +95,7 @@ class TerminalDrayageServiceTest { assertThat(result.version()).isEqualTo(1); assertThat(result.fleetSupplierId()).isEqualTo(91L); assertThat(result.fleetSupplierName()).isEqualTo("服务端车队"); - assertThat(result.allowedActions()).containsExactlyInAnyOrder("SELECT_FLEET", "SCHEDULE_PICKUP"); + assertThat(result.allowedActions()).containsExactlyInAnyOrder("SELECT_FLEET", "SCHEDULE_PICKUP", "SEND_PICKUP_EMAIL"); verify(mapper).insertHistory(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString(), anyInt(), anyInt(), any(), anyLong(), anyString()); verify(mapper).insertOperation(anyLong(), anyLong(), anyString(), anyString(), anyString(), anyInt(), anyLong());