Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test
This commit is contained in:
+19
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
|
||||
import com.baiyue.firstleg.dto.InboundOrderReference;
|
||||
import com.baiyue.firstleg.service.InboundOrderCreationService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -11,6 +12,9 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
@@ -41,4 +45,19 @@ public class InternalInboundOrderController {
|
||||
}
|
||||
return ApiResponse.success(service.createFromApprovedOrder(command));
|
||||
}
|
||||
|
||||
@PostMapping("/order-references")
|
||||
public ApiResponse<List<InboundOrderReference.Item>> references(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody InboundOrderReference.BatchRequest request) {
|
||||
requireToken(token);
|
||||
return ApiResponse.success(service.findReferences(
|
||||
request.tenantId(), request.orderIds().stream().distinct().toList()));
|
||||
}
|
||||
|
||||
private void requireToken(String token) {
|
||||
if (token == null || !MessageDigest.isEqual(expectedToken, token.getBytes(StandardCharsets.UTF_8))) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 订单服务批量读取头程入库引用的内部契约。 */
|
||||
public final class InboundOrderReference {
|
||||
private InboundOrderReference() {
|
||||
}
|
||||
|
||||
public record BatchRequest(@Positive long tenantId,
|
||||
@NotEmpty @Size(max = 200) List<@Positive Long> orderIds) {
|
||||
}
|
||||
|
||||
public record Item(Long orderId, Long inboundId, String inboundCode, String processStatus) {
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
|
||||
import com.baiyue.firstleg.dto.InboundOrderReference;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||
@@ -10,6 +11,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 将已审核的订单快照幂等转换为待测量入库单。 */
|
||||
@Service
|
||||
@@ -86,4 +88,16 @@ public class InboundOrderCreationService {
|
||||
histories.insert(history);
|
||||
return inbound.getId();
|
||||
}
|
||||
|
||||
public List<InboundOrderReference.Item> findReferences(long tenantId, List<Long> orderIds) {
|
||||
if (orderIds.isEmpty()) return List.of();
|
||||
return orders.selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<InboundOrderEntity>()
|
||||
.eq(InboundOrderEntity::getTenantId, tenantId)
|
||||
.in(InboundOrderEntity::getOrderId, orderIds)
|
||||
.orderByDesc(InboundOrderEntity::getCreatedAt))
|
||||
.stream()
|
||||
.map(inbound -> new InboundOrderReference.Item(
|
||||
inbound.getOrderId(), inbound.getId(), inbound.getInboundCode(), inbound.getProcessStatus()))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.InboundOrderReference;
|
||||
import com.baiyue.firstleg.service.InboundOrderCreationService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class InternalInboundOrderReferenceControllerTest {
|
||||
private final InboundOrderCreationService service = mock(InboundOrderCreationService.class);
|
||||
private final InternalInboundOrderController controller =
|
||||
new InternalInboundOrderController(service, "internal-token");
|
||||
|
||||
@Test
|
||||
void 使用内部令牌按租户批量查询订单入仓编码() {
|
||||
var request = new InboundOrderReference.BatchRequest(10001L, List.of(77L, 77L, 78L));
|
||||
when(service.findReferences(10001L, List.of(77L, 78L))).thenReturn(List.of(
|
||||
new InboundOrderReference.Item(77L, 88L, "RK-77", "WAIT_MEASURE")));
|
||||
|
||||
var response = controller.references("internal-token", request);
|
||||
|
||||
assertThat(response.data()).extracting(InboundOrderReference.Item::inboundCode)
|
||||
.containsExactly("RK-77");
|
||||
verify(service).findReferences(10001L, List.of(77L, 78L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void 错误内部令牌不能查询任意租户的入仓编码() {
|
||||
var request = new InboundOrderReference.BatchRequest(10001L, List.of(77L));
|
||||
assertThatThrownBy(() -> controller.references("bad-token", request))
|
||||
.isInstanceOf(BizException.class);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 订单审核事件投递到头程入库域的内部客户端。 */
|
||||
@FeignClient(name = "firstleg-service", contextId = "orderFirstlegInboundClient",
|
||||
@@ -16,6 +17,17 @@ public interface FirstlegInboundClient {
|
||||
ApiResponse<Long> create(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody OrderApprovedInboundCommand command);
|
||||
|
||||
@PostMapping("/order-references")
|
||||
ApiResponse<List<InboundReference>> references(
|
||||
@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody InboundReferenceRequest request);
|
||||
|
||||
record InboundReferenceRequest(long tenantId, List<Long> orderIds) {
|
||||
}
|
||||
|
||||
record InboundReference(Long orderId, Long inboundId, String inboundCode, String processStatus) {
|
||||
}
|
||||
|
||||
record OrderApprovedInboundCommand(
|
||||
long tenantId, long orderId, String orderNo, Long customerId, String customerName,
|
||||
Long warehouseId, String warehouseName, Long deliveryWarehouseId, String deliveryWarehouseName,
|
||||
|
||||
+9
-4
@@ -6,10 +6,12 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.order.dto.OrderDetailResponse;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import com.baiyue.order.dto.MerchantInboundOrderQuery;
|
||||
import com.baiyue.order.dto.MerchantInboundOrderRow;
|
||||
import com.baiyue.order.entity.OrderStatus;
|
||||
import com.baiyue.order.service.OrderApplicationService;
|
||||
import com.baiyue.order.service.OrderScopeGuard;
|
||||
import com.baiyue.order.service.MerchantInboundOrderListService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
@@ -39,6 +41,8 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
public class MerchantFirstLegOrderController {
|
||||
@Resource
|
||||
private OrderApplicationService service;
|
||||
@Resource
|
||||
private MerchantInboundOrderListService inboundOrderListService;
|
||||
|
||||
@BusinessAudit(event = "ORDER_CREATE", resourceType = "FIRST_LEG_ORDER", description = "Create draft / 新建草稿")
|
||||
@Operation(summary = "Create draft / 新建草稿", description = "Creates a first-leg draft order in the authenticated merchant tenant. / 在当前认证商户租户下创建头程订单草稿。")
|
||||
@@ -80,9 +84,10 @@ public class MerchantFirstLegOrderController {
|
||||
})
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('merchant:order:list')")
|
||||
public ApiResponse<PageResult<OrderEntity>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid OrderRequests.QueryRequest query) {
|
||||
return ApiResponse.success(service.page(OrderScopeGuard.merchant(user), query));
|
||||
public ApiResponse<PageResult<MerchantInboundOrderRow>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid MerchantInboundOrderQuery query) {
|
||||
return ApiResponse.success(inboundOrderListService.page(OrderScopeGuard.merchant(user), query));
|
||||
}
|
||||
|
||||
@Operation(summary = "My order detail / 我的订单详情", description = "Returns the order aggregate only when it belongs to the authenticated merchant tenant. / 仅当订单属于当前认证商户租户时返回订单聚合详情。")
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.order.dto;
|
||||
|
||||
import com.baiyue.order.entity.OrderStatus;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 客户端入库订单列表查询条件。 */
|
||||
public record MerchantInboundOrderQuery(
|
||||
@Size(max = 64) String keyword,
|
||||
@Size(max = 64) String orderNo,
|
||||
Long receivingWarehouseId,
|
||||
@Size(max = 128) String channelName,
|
||||
@Size(max = 128) String customerContact,
|
||||
Long customerId,
|
||||
OrderStatus status,
|
||||
OrderStatus operationStatus,
|
||||
@Size(max = 32) String destinationType,
|
||||
Long destinationWarehouseId,
|
||||
LocalDateTime createdFrom,
|
||||
LocalDateTime createdTo,
|
||||
@Min(1) long page,
|
||||
@Min(1) @Max(200) long size
|
||||
) {
|
||||
public MerchantInboundOrderQuery {
|
||||
if (page == 0) page = 1;
|
||||
if (size == 0) size = 20;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.baiyue.order.dto;
|
||||
|
||||
import com.baiyue.order.entity.OrderStatus;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 客户端入库订单列表行,聚合订单、货物、收货方和头程入库编码。 */
|
||||
public record MerchantInboundOrderRow(
|
||||
Long orderId,
|
||||
String orderNo,
|
||||
Integer packageCount,
|
||||
BigDecimal weightKg,
|
||||
String inboundCode,
|
||||
String channelName,
|
||||
String destinationType,
|
||||
String customerName,
|
||||
String receivingCompanyName,
|
||||
String specificationModel,
|
||||
OrderStatus status,
|
||||
Integer totalQuantity,
|
||||
BigDecimal totalGrossWeightKg,
|
||||
BigDecimal totalNetWeightKg,
|
||||
BigDecimal totalVolumeCbm,
|
||||
BigDecimal customsDeclaredUnitPrice,
|
||||
BigDecimal customsDeclaredAmount,
|
||||
String currency,
|
||||
String material,
|
||||
String productUsage,
|
||||
String brand,
|
||||
LocalDateTime createdAt,
|
||||
String trackingNo,
|
||||
String internationalOrderNo,
|
||||
String referenceId,
|
||||
Long operatorId,
|
||||
String operatorName,
|
||||
String remark,
|
||||
List<CargoColumn> cargoes
|
||||
) {
|
||||
public record CargoColumn(
|
||||
Long cargoId, Integer itemNo, String cargoName, Integer packageCount,
|
||||
Integer totalQuantity, BigDecimal grossWeightKg, BigDecimal netWeightKg,
|
||||
BigDecimal volumeCbm, BigDecimal unitDeclaredValue, BigDecimal declaredValue,
|
||||
String currency, String model, String material, String productUsage, String brand) {
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.baiyue.order.repository;
|
||||
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.dto.MerchantInboundOrderQuery;
|
||||
import com.baiyue.order.entity.*;
|
||||
import com.baiyue.order.mapper.*;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
@@ -113,6 +114,62 @@ public class OrderRepository {
|
||||
return PageResult.from(orderMapper.selectPage(new Page<>(query.page(), query.size()), wrapper));
|
||||
}
|
||||
|
||||
public PageResult<OrderEntity> merchantInboundPage(long tenantId, MerchantInboundOrderQuery query) {
|
||||
com.baiyue.order.entity.OrderStatus effectiveStatus = query.operationStatus() == null
|
||||
? query.status() : query.operationStatus();
|
||||
LambdaQueryWrapper<OrderEntity> wrapper = new LambdaQueryWrapper<OrderEntity>()
|
||||
.eq(OrderEntity::getTenantId, tenantId)
|
||||
.like(StringUtils.hasText(query.orderNo()), OrderEntity::getOrderNo, query.orderNo())
|
||||
.eq(query.receivingWarehouseId() != null, OrderEntity::getReceivingWarehouseId,
|
||||
query.receivingWarehouseId())
|
||||
.eq(StringUtils.hasText(query.channelName()), OrderEntity::getChannelName, query.channelName())
|
||||
.eq(query.customerId() != null, OrderEntity::getCustomerId, query.customerId())
|
||||
.eq(effectiveStatus != null, OrderEntity::getStatus, effectiveStatus)
|
||||
.eq(StringUtils.hasText(query.destinationType()), OrderEntity::getDestinationType,
|
||||
query.destinationType())
|
||||
.eq(query.destinationWarehouseId() != null, OrderEntity::getDestinationWarehouseId,
|
||||
query.destinationWarehouseId())
|
||||
.ge(query.createdFrom() != null, OrderEntity::getCreatedAt, query.createdFrom())
|
||||
.le(query.createdTo() != null, OrderEntity::getCreatedAt, query.createdTo())
|
||||
.and(StringUtils.hasText(query.keyword()), nested -> nested
|
||||
.like(OrderEntity::getOrderNo, query.keyword())
|
||||
.or().like(OrderEntity::getCustomerName, query.keyword()))
|
||||
.and(StringUtils.hasText(query.customerContact()), nested -> nested
|
||||
.like(OrderEntity::getCustomerName, query.customerContact())
|
||||
.or().apply("EXISTS (SELECT 1 FROM ord_order_party p "
|
||||
+ "WHERE p.tenant_id = {0} AND p.order_id = ord_order.id AND p.deleted = 0 "
|
||||
+ "AND (p.contact_name LIKE CONCAT('%', {1}, '%') "
|
||||
+ "OR p.phone LIKE CONCAT('%', {1}, '%')))",
|
||||
tenantId, query.customerContact()))
|
||||
.orderByDesc(OrderEntity::getCreatedAt)
|
||||
.orderByDesc(OrderEntity::getId);
|
||||
return PageResult.from(orderMapper.selectPage(new Page<>(query.page(), query.size()), wrapper));
|
||||
}
|
||||
|
||||
public List<OrderCargoEntity> findCargoByOrderIds(long tenantId, List<Long> orderIds) {
|
||||
if (orderIds.isEmpty()) return List.of();
|
||||
return cargoMapper.selectList(new LambdaQueryWrapper<OrderCargoEntity>()
|
||||
.eq(OrderCargoEntity::getTenantId, tenantId)
|
||||
.in(OrderCargoEntity::getOrderId, orderIds)
|
||||
.orderByAsc(OrderCargoEntity::getOrderId)
|
||||
.orderByAsc(OrderCargoEntity::getItemNo));
|
||||
}
|
||||
|
||||
public List<OrderPartyEntity> findPartiesByOrderIds(long tenantId, List<Long> orderIds) {
|
||||
if (orderIds.isEmpty()) return List.of();
|
||||
return partyMapper.selectList(new LambdaQueryWrapper<OrderPartyEntity>()
|
||||
.eq(OrderPartyEntity::getTenantId, tenantId)
|
||||
.in(OrderPartyEntity::getOrderId, orderIds));
|
||||
}
|
||||
|
||||
public List<OrderStatusHistoryEntity> findHistoryByOrderIds(long tenantId, List<Long> orderIds) {
|
||||
if (orderIds.isEmpty()) return List.of();
|
||||
return historyMapper.selectList(new LambdaQueryWrapper<OrderStatusHistoryEntity>()
|
||||
.eq(OrderStatusHistoryEntity::getTenantId, tenantId)
|
||||
.in(OrderStatusHistoryEntity::getOrderId, orderIds)
|
||||
.orderByDesc(OrderStatusHistoryEntity::getOccurredAt));
|
||||
}
|
||||
|
||||
public Map<Long, Long> countByTenantIds(List<Long> tenantIds) {
|
||||
if (tenantIds.isEmpty()) return Map.of();
|
||||
var rows = orderMapper.selectMaps(new QueryWrapper<OrderEntity>()
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.client.FirstlegInboundClient;
|
||||
import com.baiyue.order.dto.MerchantInboundOrderQuery;
|
||||
import com.baiyue.order.dto.MerchantInboundOrderRow;
|
||||
import com.baiyue.order.entity.OrderCargoEntity;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import com.baiyue.order.entity.OrderPartyEntity;
|
||||
import com.baiyue.order.entity.OrderStatusHistoryEntity;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import feign.FeignException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class MerchantInboundOrderListService {
|
||||
private final OrderRepository repository;
|
||||
private final FirstlegInboundClient firstleg;
|
||||
private final String internalToken;
|
||||
|
||||
public MerchantInboundOrderListService(
|
||||
OrderRepository repository,
|
||||
FirstlegInboundClient firstleg,
|
||||
@Value("${baiyue.security.internal-service-token}") String internalToken) {
|
||||
this.repository = repository;
|
||||
this.firstleg = firstleg;
|
||||
this.internalToken = internalToken;
|
||||
}
|
||||
|
||||
public PageResult<MerchantInboundOrderRow> page(long tenantId, MerchantInboundOrderQuery query) {
|
||||
PageResult<OrderEntity> source = repository.merchantInboundPage(tenantId, query);
|
||||
List<Long> orderIds = source.records().stream().map(OrderEntity::getId).toList();
|
||||
if (orderIds.isEmpty()) {
|
||||
return new PageResult<>(List.of(), source.page(), source.size(), source.total(), source.pages());
|
||||
}
|
||||
|
||||
Map<Long, List<OrderCargoEntity>> cargoes = repository.findCargoByOrderIds(tenantId, orderIds)
|
||||
.stream().collect(Collectors.groupingBy(OrderCargoEntity::getOrderId));
|
||||
Map<Long, OrderPartyEntity> consignees = repository.findPartiesByOrderIds(tenantId, orderIds)
|
||||
.stream().filter(party -> "RECEIVER".equals(party.getPartyType()))
|
||||
.collect(Collectors.toMap(OrderPartyEntity::getOrderId, Function.identity(), (left, right) -> left));
|
||||
Map<Long, OrderStatusHistoryEntity> latestHistory = repository.findHistoryByOrderIds(tenantId, orderIds)
|
||||
.stream().collect(Collectors.toMap(
|
||||
OrderStatusHistoryEntity::getOrderId, Function.identity(), (latest, ignored) -> latest));
|
||||
Map<Long, FirstlegInboundClient.InboundReference> inboundReferences = references(tenantId, orderIds);
|
||||
|
||||
List<MerchantInboundOrderRow> records = source.records().stream()
|
||||
.map(order -> row(order, cargoes.getOrDefault(order.getId(), List.of()),
|
||||
consignees.get(order.getId()), latestHistory.get(order.getId()),
|
||||
inboundReferences.get(order.getId())))
|
||||
.toList();
|
||||
return new PageResult<>(records, source.page(), source.size(), source.total(), source.pages());
|
||||
}
|
||||
|
||||
private Map<Long, FirstlegInboundClient.InboundReference> references(long tenantId, List<Long> orderIds) {
|
||||
try {
|
||||
var response = firstleg.references(internalToken,
|
||||
new FirstlegInboundClient.InboundReferenceRequest(tenantId, orderIds));
|
||||
if (response == null || !response.successful() || response.data() == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "头程入库引用查询响应无效");
|
||||
}
|
||||
return response.data().stream().collect(Collectors.toMap(
|
||||
FirstlegInboundClient.InboundReference::orderId, Function.identity(), (left, right) -> left));
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "头程服务暂时不可用");
|
||||
}
|
||||
}
|
||||
|
||||
private MerchantInboundOrderRow row(
|
||||
OrderEntity order,
|
||||
List<OrderCargoEntity> cargoes,
|
||||
OrderPartyEntity consignee,
|
||||
OrderStatusHistoryEntity latestHistory,
|
||||
FirstlegInboundClient.InboundReference inbound) {
|
||||
int packageCount = cargoes.stream().map(OrderCargoEntity::getPackageCount)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum();
|
||||
int totalQuantity = cargoes.stream().map(OrderCargoEntity::getTotalQuantity)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum();
|
||||
BigDecimal grossWeight = sum(cargoes, OrderCargoEntity::getGrossWeightKg);
|
||||
BigDecimal netWeight = sum(cargoes, OrderCargoEntity::getNetWeightKg);
|
||||
BigDecimal volume = sum(cargoes, OrderCargoEntity::getVolumeCbm);
|
||||
BigDecimal declaredAmount = sum(cargoes, OrderCargoEntity::getDeclaredValue);
|
||||
|
||||
List<MerchantInboundOrderRow.CargoColumn> cargoColumns = cargoes.stream()
|
||||
.map(cargo -> new MerchantInboundOrderRow.CargoColumn(
|
||||
cargo.getId(), cargo.getItemNo(), cargo.getCargoName(), cargo.getPackageCount(),
|
||||
cargo.getTotalQuantity(), cargo.getGrossWeightKg(), cargo.getNetWeightKg(),
|
||||
cargo.getVolumeCbm(), cargo.getUnitDeclaredValue(), cargo.getDeclaredValue(),
|
||||
cargo.getCurrency(), cargo.getModel(), cargo.getMaterial(), cargo.getProductUsage(),
|
||||
cargo.getBrand()))
|
||||
.toList();
|
||||
|
||||
return new MerchantInboundOrderRow(
|
||||
order.getId(), order.getOrderNo(), packageCount, grossWeight,
|
||||
inbound == null ? null : inbound.inboundCode(), order.getChannelName(),
|
||||
order.getDestinationType(), order.getCustomerName(),
|
||||
consignee == null ? null : consignee.getCompanyName(), joined(cargoes, OrderCargoEntity::getModel),
|
||||
order.getStatus(), totalQuantity, grossWeight, netWeight, volume,
|
||||
commonUnitPrice(cargoes), declaredAmount, joined(cargoes, OrderCargoEntity::getCurrency),
|
||||
joined(cargoes, OrderCargoEntity::getMaterial),
|
||||
joined(cargoes, OrderCargoEntity::getProductUsage),
|
||||
joined(cargoes, OrderCargoEntity::getBrand), order.getCreatedAt(),
|
||||
order.getExpressTrackingNo(), order.getShipmentId(), order.getReferenceId(),
|
||||
latestHistory == null ? order.getCreatedBy() : latestHistory.getOperatorId(),
|
||||
latestHistory == null ? null : latestHistory.getOperatorName(), order.getCustomerRemark(),
|
||||
cargoColumns);
|
||||
}
|
||||
|
||||
private BigDecimal sum(List<OrderCargoEntity> cargoes,
|
||||
Function<OrderCargoEntity, BigDecimal> getter) {
|
||||
return cargoes.stream().map(getter).filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
private String joined(List<OrderCargoEntity> cargoes, Function<OrderCargoEntity, String> getter) {
|
||||
String value = cargoes.stream().map(getter).filter(text -> text != null && !text.isBlank())
|
||||
.distinct().collect(Collectors.joining("、"));
|
||||
return value.isEmpty() ? null : value;
|
||||
}
|
||||
|
||||
private BigDecimal commonUnitPrice(List<OrderCargoEntity> cargoes) {
|
||||
List<BigDecimal> values = cargoes.stream().map(OrderCargoEntity::getUnitDeclaredValue)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
return values.size() == 1 ? values.get(0) : null;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,30 @@ security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/api/merchant/v1/orders/first-leg:
|
||||
get:
|
||||
summary: 客户端入库订单聚合列表
|
||||
description: 按当前登录租户分页返回订单、货物汇总、收货方快照及头程入仓编码,需要 merchant:order:list 权限。
|
||||
parameters:
|
||||
- {name: keyword, in: query, required: false, schema: {type: string, maxLength: 64}}
|
||||
- {name: orderNo, in: query, required: false, schema: {type: string, maxLength: 64}}
|
||||
- {name: receivingWarehouseId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: channelName, in: query, required: false, schema: {type: string, maxLength: 128}}
|
||||
- {name: customerContact, in: query, required: false, description: 客户名称、联系人或手机号, schema: {type: string, maxLength: 128}}
|
||||
- {name: customerId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: status, in: query, required: false, schema: {type: string}}
|
||||
- {name: operationStatus, in: query, required: false, description: 操作状态,优先于兼容参数 status, schema: {type: string}}
|
||||
- {name: destinationType, in: query, required: false, schema: {type: string, maxLength: 32}}
|
||||
- {name: destinationWarehouseId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: createdFrom, in: query, required: false, schema: {type: string, format: date-time}}
|
||||
- {name: createdTo, in: query, required: false, schema: {type: string, format: date-time}}
|
||||
- {name: page, in: query, required: false, schema: {type: integer, minimum: 1, default: 1}}
|
||||
- {name: size, in: query, required: false, schema: {type: integer, minimum: 1, maximum: 200, default: 20}}
|
||||
responses:
|
||||
'200':
|
||||
description: 成功或业务失败,响应体 code 为字符串 200 时成功
|
||||
content:
|
||||
application/json:
|
||||
schema: {type: object}
|
||||
post:
|
||||
summary: 创建订单草稿
|
||||
parameters:
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.baiyue.order.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.dto.MerchantInboundOrderQuery;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -16,7 +17,7 @@ class OrderControllerPermissionTest {
|
||||
@Test
|
||||
void merchantOrderListAndDetailRequireCorrespondingPermissions() throws Exception {
|
||||
assertAuthority(MerchantFirstLegOrderController.class, "page", "hasAuthority('merchant:order:list')",
|
||||
AuthenticatedUser.class, OrderRequests.QueryRequest.class);
|
||||
AuthenticatedUser.class, MerchantInboundOrderQuery.class);
|
||||
assertAuthority(MerchantFirstLegOrderController.class, "detail", "hasAuthority('merchant:order:view')",
|
||||
AuthenticatedUser.class, long.class);
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.baiyue.order.repository;
|
||||
|
||||
import com.baiyue.order.dto.MerchantInboundOrderQuery;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import com.baiyue.order.entity.OrderStatus;
|
||||
import com.baiyue.order.mapper.OrderCargoMapper;
|
||||
import com.baiyue.order.mapper.OrderChildPurgeMapper;
|
||||
import com.baiyue.order.mapper.OrderIdempotencyRecordMapper;
|
||||
import com.baiyue.order.mapper.OrderInternalNoteMapper;
|
||||
import com.baiyue.order.mapper.OrderMapper;
|
||||
import com.baiyue.order.mapper.OrderPartyMapper;
|
||||
import com.baiyue.order.mapper.OrderStatusHistoryMapper;
|
||||
import com.baiyue.order.mapper.OutboxEventMapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MerchantInboundOrderQueryRepositoryTest {
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void 查询始终带租户并覆盖页面筛选条件() {
|
||||
TableInfoHelper.initTableInfo(
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), "merchant-inbound-query-test"),
|
||||
OrderEntity.class);
|
||||
OrderMapper mapper = mock(OrderMapper.class);
|
||||
when(mapper.selectPage(any(Page.class), any(Wrapper.class))).thenReturn(new Page<>());
|
||||
OrderRepository repository = new OrderRepository(
|
||||
mapper, mock(OrderCargoMapper.class), mock(OrderPartyMapper.class),
|
||||
mock(OrderInternalNoteMapper.class), mock(OrderStatusHistoryMapper.class),
|
||||
mock(OutboxEventMapper.class), mock(OrderChildPurgeMapper.class),
|
||||
mock(OrderIdempotencyRecordMapper.class));
|
||||
MerchantInboundOrderQuery query = new MerchantInboundOrderQuery(
|
||||
"客户", "FL-", 11L, "美森", "13800000000", 30001L, null, OrderStatus.APPROVED,
|
||||
"FBA", 22L, null, null, 1, 20);
|
||||
|
||||
repository.merchantInboundPage(10001L, query);
|
||||
|
||||
ArgumentCaptor<Wrapper<OrderEntity>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(mapper).selectPage(any(Page.class), captor.capture());
|
||||
String sql = captor.getValue().getSqlSegment();
|
||||
assertThat(sql).contains("tenant_id", "order_no", "receiving_warehouse_id", "channel_name",
|
||||
"customer_id", "customer_name", "contact_name", "phone", "status",
|
||||
"destination_type", "destination_warehouse_id");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.order.client.FirstlegInboundClient;
|
||||
import com.baiyue.order.dto.MerchantInboundOrderQuery;
|
||||
import com.baiyue.order.entity.OrderCargoEntity;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import com.baiyue.order.entity.OrderPartyEntity;
|
||||
import com.baiyue.order.entity.OrderStatus;
|
||||
import com.baiyue.order.entity.OrderStatusHistoryEntity;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MerchantInboundOrderListServiceTest {
|
||||
private final OrderRepository repository = mock(OrderRepository.class);
|
||||
private final FirstlegInboundClient firstleg = mock(FirstlegInboundClient.class);
|
||||
private final MerchantInboundOrderListService service =
|
||||
new MerchantInboundOrderListService(repository, firstleg, "internal-token");
|
||||
|
||||
@Test
|
||||
void 聚合客户端列表所需的订单货物收货方和入仓编码() {
|
||||
MerchantInboundOrderQuery query = new MerchantInboundOrderQuery(
|
||||
null, null, null, null, null, null, null, null, null, null, null, null, 1, 20);
|
||||
OrderEntity order = new OrderEntity();
|
||||
order.setId(77L);
|
||||
order.setOrderNo("FL-77");
|
||||
order.setStatus(OrderStatus.APPROVED);
|
||||
order.setChannelName("美森");
|
||||
order.setDestinationType("FBA");
|
||||
order.setCustomerName("测试客户");
|
||||
order.setShipmentId("FBA-SHIPMENT-77");
|
||||
order.setExpressTrackingNo("TRACK-77");
|
||||
order.setCustomerRemark("客户备注");
|
||||
when(repository.merchantInboundPage(10001L, query))
|
||||
.thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1));
|
||||
|
||||
OrderCargoEntity first = cargo(1L, 1, 2, 20, "10.000", "9.000", "0.1200", "10.00", "20.00", "A");
|
||||
OrderCargoEntity second = cargo(2L, 2, 3, 30, "15.000", "14.000", "0.1800", "10.00", "30.00", "B");
|
||||
when(repository.findCargoByOrderIds(10001L, List.of(77L))).thenReturn(List.of(first, second));
|
||||
OrderPartyEntity consignee = new OrderPartyEntity();
|
||||
consignee.setOrderId(77L);
|
||||
consignee.setPartyType("RECEIVER");
|
||||
consignee.setCompanyName("收货公司");
|
||||
when(repository.findPartiesByOrderIds(10001L, List.of(77L))).thenReturn(List.of(consignee));
|
||||
OrderStatusHistoryEntity history = new OrderStatusHistoryEntity();
|
||||
history.setOrderId(77L);
|
||||
history.setOperatorId(9L);
|
||||
history.setOperatorName("操作员");
|
||||
when(repository.findHistoryByOrderIds(10001L, List.of(77L))).thenReturn(List.of(history));
|
||||
when(firstleg.references(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(List.of(
|
||||
new FirstlegInboundClient.InboundReference(77L, 88L, "RK-77", "WAIT_MEASURE"))));
|
||||
|
||||
var row = service.page(10001L, query).records().get(0);
|
||||
|
||||
assertThat(row.inboundCode()).isEqualTo("RK-77");
|
||||
assertThat(row.packageCount()).isEqualTo(5);
|
||||
assertThat(row.totalQuantity()).isEqualTo(50);
|
||||
assertThat(row.totalGrossWeightKg()).isEqualByComparingTo("25.000");
|
||||
assertThat(row.totalNetWeightKg()).isEqualByComparingTo("23.000");
|
||||
assertThat(row.totalVolumeCbm()).isEqualByComparingTo("0.3000");
|
||||
assertThat(row.customsDeclaredAmount()).isEqualByComparingTo("50.00");
|
||||
assertThat(row.customsDeclaredUnitPrice()).isEqualByComparingTo("10.00");
|
||||
assertThat(row.specificationModel()).isEqualTo("A、B");
|
||||
assertThat(row.receivingCompanyName()).isEqualTo("收货公司");
|
||||
assertThat(row.operatorName()).isEqualTo("操作员");
|
||||
assertThat(row.cargoes()).hasSize(2);
|
||||
verify(repository).merchantInboundPage(10001L, query);
|
||||
}
|
||||
|
||||
private static OrderCargoEntity cargo(Long id, int itemNo, int packages, int quantity,
|
||||
String gross, String net, String volume,
|
||||
String unitPrice, String amount, String model) {
|
||||
OrderCargoEntity cargo = new OrderCargoEntity();
|
||||
cargo.setId(id);
|
||||
cargo.setOrderId(77L);
|
||||
cargo.setItemNo(itemNo);
|
||||
cargo.setPackageCount(packages);
|
||||
cargo.setTotalQuantity(quantity);
|
||||
cargo.setGrossWeightKg(new BigDecimal(gross));
|
||||
cargo.setNetWeightKg(new BigDecimal(net));
|
||||
cargo.setVolumeCbm(new BigDecimal(volume));
|
||||
cargo.setUnitDeclaredValue(new BigDecimal(unitPrice));
|
||||
cargo.setDeclaredValue(new BigDecimal(amount));
|
||||
cargo.setModel(model);
|
||||
return cargo;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user