二期内容提交:客户端订单列表

This commit is contained in:
2026-08-04 20:49:46 +08:00
parent 7e544780b4
commit de8acc419f
14 changed files with 563 additions and 5 deletions
@@ -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();
}
}
@@ -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,
@@ -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);
}
@@ -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;
}
}