This commit is contained in:
2026-08-13 12:22:31 +08:00
parent 16dc03a2e2
commit e742abd1cf
49 changed files with 821 additions and 50 deletions
@@ -0,0 +1,31 @@
package com.baiyue.firstleg.customerstatistics.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.customerstatistics.dto.CustomerTransitStatisticsDtos.TransitSummary;
import com.baiyue.firstleg.customerstatistics.service.CustomerTransitStatisticsService;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
import jakarta.annotation.Resource;
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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Validated
@RestController
@RequestMapping("/api/platform/v1/customers")
public class PlatformCustomerTransitStatisticsController {
@Resource private CustomerTransitStatisticsService service;
@GetMapping("/{customerId}/container-transit-summary")
@PreAuthorize("hasAuthority('platform:firstleg:container:list')")
public ApiResponse<TransitSummary> summary(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long customerId) {
FirstlegScopeGuard.platform(user, customerId);
return ApiResponse.success(service.summarize(customerId));
}
}
@@ -0,0 +1,11 @@
package com.baiyue.firstleg.customerstatistics.dto;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public final class CustomerTransitStatisticsDtos {
private CustomerTransitStatisticsDtos() {}
public record TransitSummary(Long customerId, long containerCount, BigDecimal totalTeu,
long unknownContainerCount, LocalDateTime asOf, String definition) {}
}
@@ -0,0 +1,56 @@
package com.baiyue.firstleg.customerstatistics.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Mapper
public interface CustomerTransitStatisticsMapper {
@Select("""
SELECT #{customerId} AS customerId,
COUNT(*) AS containerCount,
COALESCE(SUM(teu), 0) AS totalTeu,
SUM(CASE WHEN teu IS NULL THEN 1 ELSE 0 END) AS unknownContainerCount,
CURRENT_TIMESTAMP(3) AS asOf
FROM (
SELECT DISTINCT x.customer_id, x.container_booking_id,
CASE
WHEN UPPER(TRIM(x.container_type)) LIKE '20%' THEN 1.0
WHEN UPPER(TRIM(x.container_type)) LIKE '40%' THEN 2.0
WHEN UPPER(TRIM(x.container_type)) LIKE '45%' THEN 2.25
ELSE NULL
END AS teu
FROM (
SELECT b.customer_id, c.id AS container_booking_id, c.container_type
FROM fl_container_booking c
JOIN fl_ocean_booking b ON b.id = c.ocean_booking_id
WHERE c.actual_sailing_at IS NOT NULL
AND c.actual_warehouse_arrival_at IS NULL
AND b.status NOT IN ('CANCELLED', 'FAILED')
AND NOT EXISTS (
SELECT 1 FROM fl_loading_plan p
JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id
WHERE p.container_booking_id = c.id AND i.deleted = 0
)
UNION ALL
SELECT i.account_id AS customer_id, c.id AS container_booking_id, c.container_type
FROM fl_container_booking c
JOIN fl_ocean_booking b ON b.id = c.ocean_booking_id
JOIN fl_loading_plan p ON p.container_booking_id = c.id
JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
WHERE c.actual_sailing_at IS NOT NULL
AND c.actual_warehouse_arrival_at IS NULL
AND b.status NOT IN ('CANCELLED', 'FAILED')
AND i.account_id IS NOT NULL AND i.account_id > 0
) x
WHERE x.customer_id = #{customerId}
) scoped
""")
SummaryRow summarize(@Param("customerId") long customerId);
record SummaryRow(Long customerId, Long containerCount, BigDecimal totalTeu,
Long unknownContainerCount, LocalDateTime asOf) {}
}
@@ -0,0 +1,28 @@
package com.baiyue.firstleg.customerstatistics.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.customerstatistics.dto.CustomerTransitStatisticsDtos.TransitSummary;
import com.baiyue.firstleg.customerstatistics.mapper.CustomerTransitStatisticsMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class CustomerTransitStatisticsService {
private static final String DEFINITION = "actual_sailing_at IS NOT NULL 且 actual_warehouse_arrival_at IS NULL;排除取消/失败订舱;客户+containerBookingId去重;拼柜按active LoadingPlanItem.accountId归属,整柜按OceanBooking.customerId归属。TEU:20*=1,40*=2,45*=2.25,未知箱型不计totalTeu。";
@Resource private CustomerTransitStatisticsMapper mapper;
public TransitSummary summarize(long customerId) {
if (customerId <= 0) throw new BizException(ResultCode.BAD_REQUEST, "customerId必须为正数");
var row = mapper.summarize(customerId);
return new TransitSummary(customerId,
value(row == null ? null : row.containerCount()),
row == null || row.totalTeu() == null ? BigDecimal.ZERO : row.totalTeu(),
value(row == null ? null : row.unknownContainerCount()),
row == null ? null : row.asOf(), DEFINITION);
}
private static long value(Long value) { return value == null ? 0L : value; }
}