This commit is contained in:
2026-08-04 14:54:57 +08:00
29 changed files with 664 additions and 178 deletions
@@ -31,11 +31,11 @@ import org.springframework.web.bind.annotation.PutMapping;
import java.util.List;
/**
* 平台端头程订单管理接口,在明确的目标租户范围内提供查询、审核和履约状态流转能力。
* 平台端头程订单管理接口,提供跨租户列表查询,并在明确的目标租户范围内执行详情查询和状态流转。
*/
@Tag(
name = "Platform first-leg orders / 平台头程订单",
description = "Platform lifecycle management for orders in an explicit target tenant. / 平台端针对明确目标租户管理头程订单生命周期。"
description = "Platform review queue across tenants; detail and lifecycle actions require an explicit target tenant. / 平台端跨租户审核队列;详情和生命周期操作需要明确目标租户。"
)
@RestController
@RequestMapping("/api/platform/v1/orders/first-leg")
@@ -98,7 +98,7 @@ public class PlatformFirstLegOrderController {
return ApiResponse.success(null);
}
@Operation(summary = "Tenant order query / 租户订单查询", description = "Queries a page of first-leg orders in the explicit target tenant. / 分页查询明确目标租户下的头程订单。")
@Operation(summary = "Platform order query / 平台订单查询", description = "Queries all tenant orders when tenantId is omitted, or filters one tenant when supplied. / tenantId 省略时分页查询全部客户租户订单,传入时仅查询指定租户。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Query succeeded / 查询成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限")
@@ -107,9 +107,14 @@ public class PlatformFirstLegOrderController {
@PreAuthorize("hasAuthority('platform:order:list')")
public ApiResponse<PageResult<OrderEntity>> page(
@AuthenticationPrincipal AuthenticatedUser user,
@Parameter(description = "Target tenant ID / 目标租户 ID", example = "10001", required = true) @RequestParam long tenantId,
@Parameter(description = "Optional target tenant filter / 可选的目标租户筛选", example = "10001")
@RequestParam(required = false) Long tenantId,
@Valid OrderRequests.QueryRequest query) {
return ApiResponse.success(service.page(OrderScopeGuard.platform(user, tenantId), query));
OrderScopeGuard.platform(user);
if (tenantId != null) {
OrderScopeGuard.platform(user, tenantId);
}
return ApiResponse.success(service.page(tenantId, query));
}
@GetMapping("/tenant-counts")
@@ -44,10 +44,10 @@ public final class OrderRequests {
LocalDate expectedShipDate,
@Schema(description = "Customer remark / 客户备注", example = "Notify before pickup / 提货前联系", maxLength = 1000)
@Size(max = 1000) String customerRemark,
@Schema(description = "Sender information / 发货方信息", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull @Valid Party sender,
@Schema(description = "Receiver information / 收货方信息", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull @Valid Party receiver,
@Schema(description = "Sender information; required when pickupMethod is PICKUP / 发货方信息,取货方式为 PICKUP 时必填")
@Valid Party sender,
@Schema(description = "Receiver information; required for PRIVATE destination / 收货方信息,私人地址目的地时必填")
@Valid Party receiver,
@Schema(description = "Cargo lines / 货物明细", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty @Valid List<Cargo> cargoes,
@NotBlank @Pattern(regexp = "SELF_EXPRESS|PICKUP|SELF_TRUCK") String pickupMethod,
@@ -73,9 +73,9 @@ public class OrderRepository {
.eq(OrderEntity::getId, orderId));
}
public PageResult<OrderEntity> page(long tenantId, OrderRequests.QueryRequest query) {
public PageResult<OrderEntity> page(Long tenantId, OrderRequests.QueryRequest query) {
LambdaQueryWrapper<OrderEntity> wrapper = new LambdaQueryWrapper<OrderEntity>()
.eq(OrderEntity::getTenantId, tenantId)
.eq(tenantId != null, OrderEntity::getTenantId, tenantId)
.eq(query.status() != null, OrderEntity::getStatus, query.status())
.eq(query.customerId() != null, OrderEntity::getCustomerId, query.customerId())
.eq(query.destinationWarehouseId() != null, OrderEntity::getDestinationWarehouseId, query.destinationWarehouseId())
@@ -106,7 +106,7 @@ public class OrderApplicationService {
completeIdempotency(idempotency, id);
}
public PageResult<OrderEntity> page(long tenantId, OrderRequests.QueryRequest query) { return repository.page(tenantId, query); }
public PageResult<OrderEntity> page(Long tenantId, OrderRequests.QueryRequest query) { return repository.page(tenantId, query); }
public List<TenantOrderCountResponse> countByTenantIds(List<Long> tenantIds) {
var counts = repository.countByTenantIds(tenantIds);
@@ -145,6 +145,12 @@ public class OrderApplicationService {
}
private void validatePrototypeFields(OrderRequests.SaveRequest request) {
if ("PICKUP".equals(request.pickupMethod()) && request.sender() == null) {
throw new BizException("上门提货时必须填写发货信息");
}
if ("PRIVATE".equals(request.destinationType()) && request.receiver() == null) {
throw new BizException("私人地址必须填写收货信息");
}
if ("FBA".equals(request.destinationType())) {
if (!StringUtils.hasText(request.amazonWarehouseCode()) || !StringUtils.hasText(request.shipmentId())
|| !StringUtils.hasText(request.referenceId())) {
@@ -168,6 +174,13 @@ public class OrderApplicationService {
}
var cargoes = repository.findCargo(tenantId, order.getId());
if (cargoes == null || cargoes.isEmpty()) throw new BizException("At least one cargo line is required");
var parties = repository.findParties(tenantId, order.getId());
if ("PICKUP".equals(order.getPickupMethod()) && !hasParty(parties, "SENDER")) {
throw new BizException("上门提货时必须填写发货信息");
}
if ("PRIVATE".equals(order.getDestinationType()) && !hasParty(parties, "RECEIVER")) {
throw new BizException("私人地址必须填写收货信息");
}
if ("FBA".equals(order.getDestinationType())) {
if (!StringUtils.hasText(order.getAmazonWarehouseCode()) || !StringUtils.hasText(order.getShipmentId())
|| !StringUtils.hasText(order.getReferenceId())) {
@@ -181,6 +194,11 @@ public class OrderApplicationService {
}
}
private boolean hasParty(List<OrderPartyEntity> parties, String partyType) {
return parties != null && parties.stream()
.anyMatch(party -> party != null && partyType.equals(party.getPartyType()));
}
private OrderEntity require(long tenantId, long id) {
OrderEntity order = repository.findOwned(tenantId, id);
if (order == null) throw new BizException(com.baiyue.common.core.api.ResultCode.NOT_FOUND, "Order not found");
@@ -272,7 +290,8 @@ public class OrderApplicationService {
};
}
private void apply(OrderEntity order, OrderRequests.SaveRequest request, OrderMasterDataGuard.Snapshots snapshots) {
order.setCustomerId(snapshots.partner().id()); order.setCustomerName(snapshots.partner().partnerName());
order.setCustomerId(snapshots.partner() == null ? null : snapshots.partner().id());
order.setCustomerName(snapshots.partner() == null ? request.customerName() : snapshots.partner().partnerName());
order.setTransportMode(request.transportMode()); order.setTradeTerm(request.tradeTerm());
order.setDestinationCountryCode(snapshots.destination() == null ? request.destinationCountryCode()
: snapshots.destination().warehouse().countryCode());
@@ -324,8 +343,8 @@ public class OrderApplicationService {
item.setProductUsage(cargo.usage()); item.setModel(cargo.model()); item.setBrand(cargo.brand());
item.setCreatedBy(userId); item.setUpdatedBy(userId); repository.insertCargo(item);
}
saveParty(tenantId, userId, orderId, "SENDER", request.sender());
saveParty(tenantId, userId, orderId, "RECEIVER", request.receiver());
if (request.sender() != null) saveParty(tenantId, userId, orderId, "SENDER", request.sender());
if (request.receiver() != null) saveParty(tenantId, userId, orderId, "RECEIVER", request.receiver());
}
private void saveParty(long tenantId, long userId, long orderId, String type, OrderRequests.Party party) {
OrderPartyEntity item = new OrderPartyEntity();
@@ -28,9 +28,8 @@ public class OrderMasterDataGuard {
this.warehouses=warehouses; this.partners=partners; this.token=token;
}
public Snapshots validate(long tenantId, OrderRequests.SaveRequest request) {
if (request.customerId() == null) throw new BizException(ResultCode.VALIDATION_FAILED, "必须选择往来单位");
try {
var partner=loadPartner(tenantId,request.customerId());
PartnerClient.Partner partner=request.customerId()==null ? null : loadPartner(tenantId,request.customerId());
var receiving=loadWarehouse(request.receivingWarehouseId(),"收货仓库");
requireWarehouseType(receiving, "DOMESTIC", "收货仓库");
if (receiving.addresses()==null || receiving.addresses().stream().noneMatch(Objects::nonNull))
@@ -62,8 +61,7 @@ public class OrderMasterDataGuard {
public void validatePersisted(long tenantId, Long partnerId, Long receivingWarehouseId,
Long destinationWarehouseId, String destinationType) {
try {
if (partnerId==null) throw new BizException(ResultCode.NOT_FOUND,"往来单位已停用或不可用");
loadPartner(tenantId,partnerId);
if (partnerId!=null) loadPartner(tenantId,partnerId);
if (receivingWarehouseId==null) throw new BizException(ResultCode.NOT_FOUND,"收货仓库已停用或不可用");
WarehouseClient.Detail receiving=loadWarehouse(receivingWarehouseId,"收货仓库");
requireWarehouseType(receiving,"DOMESTIC","收货仓库");
@@ -5,6 +5,7 @@ import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exce
*/
public final class OrderScopeGuard { private OrderScopeGuard(){}
public static long merchant(AuthenticatedUser u){requireApp(u,"MERCHANT");if(u.tenantId()==null||u.tenantId()<=0)throw new BizException(ResultCode.FORBIDDEN,"Merchant tenant is required");return u.tenantId();}
public static void platform(AuthenticatedUser u){requireApp(u,"PLATFORM");}
public static long platform(AuthenticatedUser u,long targetTenantId){requireApp(u,"PLATFORM");if(targetTenantId<=0)throw new BizException(ResultCode.BAD_REQUEST,"Target tenant is required");return targetTenantId;}
private static void requireApp(AuthenticatedUser u,String expected){if(u==null||!expected.equals(u.appCode()))throw new BizException(ResultCode.FORBIDDEN,"Wrong application scope");}
}
@@ -27,6 +27,18 @@ paths:
responses:
'200': {description: 成功或业务失败,按响应体 code 判断}
/api/platform/v1/orders/first-leg:
get:
summary: 平台分页查询头程订单
description: tenantId 省略时查询全部客户租户订单,传入时仅查询指定租户。
parameters:
- name: tenantId
in: query
required: false
description: 可选的目标租户筛选
schema: {type: integer, format: int64, minimum: 1}
responses:
'200': {description: 查询成功或业务失败,按响应体 code 判断}
'403': {description: 需要 platform:order:list 权限和平台身份}
post:
summary: 平台代客创建订单草稿
parameters: