This commit is contained in:
2026-08-30 17:02:19 +08:00
parent 0aa77327d5
commit ca9e7b60e8
40 changed files with 1471 additions and 6 deletions
@@ -0,0 +1,44 @@
package com.baiyue.lastleg.inventory.query.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.inventory.query.dto.InboundCodeRelationshipResponse;
import com.baiyue.lastleg.inventory.query.service.InboundCodeRelationshipService;
import com.baiyue.lastleg.security.OverseasPermissions;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@RestController
@Tag(name = "Inventory queries")
public class InboundCodeRelationshipController {
static final String PATH = "/api/platform/v1/last-leg/inventory/inbound-code-relationships/{inboundCode}";
@Resource private InboundCodeRelationshipService service;
@GetMapping(PATH)
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_LIST + "')")
@Operation(summary = "Resolve overseas relationships by inbound code",
description = "Platform-only Lastleg relationship read model. Required authority: "
+ OverseasPermissions.INVENTORY_LIST)
public ApiResponse<InboundCodeRelationshipResponse> find(
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String inboundCode) {
requirePlatform(user);
return ApiResponse.success(service.find(inboundCode));
}
private static void requirePlatform(AuthenticatedUser user) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform application required");
}
}
}
@@ -0,0 +1,21 @@
package com.baiyue.lastleg.inventory.query.dto;
import java.util.List;
public record InboundCodeRelationshipResponse(
String inboundCode,
boolean found,
Customer customer,
OverseasWarehouse overseasWarehouse,
String overseasReceivingStatus,
List<TaillegOrder> taillegOrders,
List<InboundPlan> inboundPlans,
List<String> palletCodes,
List<InventorySku> inventory
) {
public record Customer(String customerId, String customerName) { }
public record OverseasWarehouse(String warehouseId, String warehouseCode, String warehouseName) { }
public record TaillegOrder(String orderNo, String orderStatus) { }
public record InboundPlan(String planNo, String planStatus, String receivingStatus) { }
public record InventorySku(String skuCode, String productNameZh, String productNameEn, long packageCount) { }
}
@@ -0,0 +1,41 @@
package com.baiyue.lastleg.inventory.query.mapper;
import com.baiyue.lastleg.inventory.query.model.InboundCodeRelationshipRows;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface InboundCodeRelationshipMapper {
@Select("SELECT o.customer_id AS customerId,o.customer_name AS customerName,o.order_no AS orderNo," +
"o.current_status AS orderStatus,p.plan_no AS planNo,p.status AS planStatus," +
"COALESCE(p.status,o.current_status) AS receivingStatus," +
"COALESCE(b.overseas_warehouse_id,o.overseas_warehouse_id,CAST(p.overseas_warehouse_id AS CHAR)) AS overseasWarehouseId," +
"b.overseas_warehouse_code AS overseasWarehouseCode," +
"COALESCE(b.overseas_warehouse_name,o.overseas_warehouse_name) AS overseasWarehouseName " +
"FROM ll_order o " +
"LEFT JOIN ll_inbound_plan_line l ON l.customer_id=o.customer_id AND l.order_id=o.id AND l.inbound_code=o.inbound_code " +
"LEFT JOIN ll_inbound_plan p ON p.customer_id=o.customer_id AND p.id=l.plan_id " +
"LEFT JOIN ll_inventory_balance b ON b.customer_id=o.customer_id AND b.inbound_order_id=o.id AND b.inbound_code=o.inbound_code " +
"WHERE o.inbound_code=#{inboundCode} ORDER BY o.customer_id,o.id,p.id,b.id")
List<InboundCodeRelationshipRows.Base> bases(@Param("inboundCode") String inboundCode);
@Select("SELECT DISTINCT r.pallet_no FROM ll_inbound_stock_receipt r " +
"JOIN ll_order o ON o.customer_id=r.customer_id AND o.id=r.order_id " +
"WHERE o.inbound_code=#{inboundCode} AND o.customer_id=#{customerId} " +
"AND r.pallet_no IS NOT NULL AND TRIM(r.pallet_no)<>'' ORDER BY r.pallet_no")
List<String> palletCodes(@Param("customerId") String customerId,
@Param("inboundCode") String inboundCode);
@Select("SELECT r.sku_code AS skuCode,MAX(r.product_name_zh) AS productNameZh," +
"MAX(r.product_name_en) AS productNameEn,SUM(r.received_packages) AS packageCount " +
"FROM ll_inbound_stock_receipt r " +
"JOIN ll_order o ON o.customer_id=r.customer_id AND o.id=r.order_id " +
"WHERE o.inbound_code=#{inboundCode} AND o.customer_id=#{customerId} " +
"GROUP BY r.sku_code ORDER BY r.sku_code")
List<InboundCodeRelationshipRows.InventorySku> inventory(
@Param("customerId") String customerId,
@Param("inboundCode") String inboundCode);
}
@@ -0,0 +1,29 @@
package com.baiyue.lastleg.inventory.query.model;
import lombok.Data;
public final class InboundCodeRelationshipRows {
private InboundCodeRelationshipRows() { }
@Data
public static class Base {
private String customerId;
private String customerName;
private String orderNo;
private String orderStatus;
private String planNo;
private String planStatus;
private String receivingStatus;
private String overseasWarehouseId;
private String overseasWarehouseCode;
private String overseasWarehouseName;
}
@Data
public static class InventorySku {
private String skuCode;
private String productNameZh;
private String productNameEn;
private Long packageCount;
}
}
@@ -0,0 +1,85 @@
package com.baiyue.lastleg.inventory.query.service;
import com.baiyue.lastleg.inventory.query.dto.InboundCodeRelationshipResponse;
import com.baiyue.lastleg.inventory.query.mapper.InboundCodeRelationshipMapper;
import com.baiyue.lastleg.inventory.query.model.InboundCodeRelationshipRows;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Objects;
@Service
public class InboundCodeRelationshipService {
@Resource private InboundCodeRelationshipMapper mapper;
public InboundCodeRelationshipResponse find(String inboundCode) {
String normalized = required(inboundCode);
List<InboundCodeRelationshipRows.Base> bases = mapper.bases(normalized);
if (bases.isEmpty()) return empty(normalized);
List<String> customerIds = bases.stream().map(InboundCodeRelationshipRows.Base::getCustomerId)
.filter(Objects::nonNull).distinct().toList();
if (customerIds.size() != 1) {
throw new ResponseStatusException(HttpStatus.CONFLICT,
"Inbound code resolves multiple customer scopes");
}
String customerId = customerIds.get(0);
String customerName = unique(bases.stream().map(InboundCodeRelationshipRows.Base::getCustomerName).toList(),
"customer name");
String warehouseId = unique(bases.stream().map(InboundCodeRelationshipRows.Base::getOverseasWarehouseId).toList(),
"overseas warehouse ID");
String warehouseCode = unique(bases.stream().map(InboundCodeRelationshipRows.Base::getOverseasWarehouseCode).toList(),
"overseas warehouse code");
String warehouseName = unique(bases.stream().map(InboundCodeRelationshipRows.Base::getOverseasWarehouseName).toList(),
"overseas warehouse name");
List<InboundCodeRelationshipResponse.TaillegOrder> orders = bases.stream()
.filter(value -> trim(value.getOrderNo()) != null)
.map(value -> new InboundCodeRelationshipResponse.TaillegOrder(
value.getOrderNo().trim(), trim(value.getOrderStatus())))
.distinct().sorted(java.util.Comparator.comparing(InboundCodeRelationshipResponse.TaillegOrder::orderNo))
.toList();
List<InboundCodeRelationshipResponse.InboundPlan> plans = bases.stream()
.filter(value -> trim(value.getPlanNo()) != null)
.map(value -> new InboundCodeRelationshipResponse.InboundPlan(
value.getPlanNo().trim(), trim(value.getPlanStatus()), trim(value.getReceivingStatus())))
.distinct().sorted(java.util.Comparator.comparing(InboundCodeRelationshipResponse.InboundPlan::planNo)).toList();
List<InboundCodeRelationshipResponse.InventorySku> inventory = mapper.inventory(customerId, normalized).stream()
.map(value -> new InboundCodeRelationshipResponse.InventorySku(value.getSkuCode(),
value.getProductNameZh(), value.getProductNameEn(), number(value.getPackageCount()))).toList();
String status = unique(bases.stream().map(InboundCodeRelationshipRows.Base::getReceivingStatus).toList(),
"overseas receiving status");
InboundCodeRelationshipResponse.OverseasWarehouse warehouse = warehouseId == null
&& warehouseCode == null && warehouseName == null ? null
: new InboundCodeRelationshipResponse.OverseasWarehouse(warehouseId, warehouseCode, warehouseName);
return new InboundCodeRelationshipResponse(normalized, true,
new InboundCodeRelationshipResponse.Customer(customerId, customerName), warehouse, status,
orders, plans, mapper.palletCodes(customerId, normalized), inventory);
}
private static InboundCodeRelationshipResponse empty(String inboundCode) {
return new InboundCodeRelationshipResponse(inboundCode, false, null, null, null,
List.of(), List.of(), List.of(), List.of());
}
private static String unique(List<String> values, String field) {
List<String> normalized = values.stream().map(InboundCodeRelationshipService::trim)
.filter(Objects::nonNull).distinct().toList();
if (normalized.size() > 1) {
throw new ResponseStatusException(HttpStatus.CONFLICT,
"Inbound code has inconsistent " + field + " snapshots");
}
return normalized.isEmpty() ? null : normalized.get(0);
}
private static String required(String value) {
String normalized = trim(value);
if (normalized == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "inboundCode is required");
if (normalized.length() > 128) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "inboundCode is too long");
return normalized;
}
private static String trim(String value) { return value == null || value.isBlank() ? null : value.trim(); }
private static long number(Long value) { return value == null ? 0L : value; }
}