暂存提交
This commit is contained in:
+4
-3
@@ -9,8 +9,8 @@
|
||||
|
||||
| 功能 | 入口 |
|
||||
|---|---|
|
||||
| 商户订单创建、编辑、列表、详情、提交、取消 | `controller/MerchantFirstLegOrderController.java` |
|
||||
| 平台订单列表、详情、审核、接单、开始、完成、取消 | `controller/PlatformFirstLegOrderController.java` |
|
||||
| 商户订单创建、编辑、列表、详情、提交、取消 | `controller/MerchantFirstLegOrderController.java`;列表校验 `merchant:order:list`,详情校验 `merchant:order:view` |
|
||||
| 平台订单列表、详情、审核、接单、开始、完成、取消 | `controller/PlatformFirstLegOrderController.java`;列表和详情校验 `platform:order:list` |
|
||||
| 内部备注与快递信息 | `PlatformFirstLegOrderController.java` |
|
||||
| 核心编排与状态机 | `service/OrderApplicationService.java`、`entity/OrderStatus.java` |
|
||||
| 企业下单资格 | `service/OrderEligibilityGuard.java`、`client/TenantEligibilityClient.java` |
|
||||
@@ -20,11 +20,12 @@
|
||||
| 主单、货物、参与方、历史、备注、Outbox | `entity/`、`mapper/`、`repository/OrderRepository.java` |
|
||||
| 表结构与一期扩展 | `src/main/resources/db/order.sql`、`phase1-masterdata-extension.sql`、`phase1-order-extension.sql` |
|
||||
| 模块说明 | `README-ORDER.md` |
|
||||
| 状态与主数据测试 | `src/test/java/com/baiyue/order/service/` |
|
||||
| 状态、主数据与接口权限测试 | `src/test/java/com/baiyue/order/service/`、`src/test/java/com/baiyue/order/controller/` |
|
||||
|
||||
## 修改与验证
|
||||
|
||||
- 所有商户查询和写入必须执行租户/访问范围校验;平台操作必须验证权限及合法状态迁移。
|
||||
- 查询接口同样必须校验权限码;沿用 IAM 已初始化的权限资源,新增权限码时同步补充 IAM 初始化脚本与角色授权。
|
||||
- 已提交订单中的仓库、往来单位等快照不能随主数据修改而变化。
|
||||
- 跨服务只通过 `client/` 或事件演进,不跨库查询 IAM/Warehouse;跨域通知优先使用 Outbox。
|
||||
- 新增状态或动作时同步修改 Controller、Service 状态机、历史记录、DTO、SQL、前端消费者和测试。
|
||||
|
||||
@@ -80,6 +80,7 @@ public class MerchantFirstLegOrderController {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Merchant scope required / 需要商户端权限")
|
||||
})
|
||||
@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));
|
||||
@@ -92,6 +93,7 @@ public class MerchantFirstLegOrderController {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Order not found / 订单不存在")
|
||||
})
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('merchant:order:view')")
|
||||
public ApiResponse<OrderDetailResponse> detail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Parameter(description = "Order ID / 订单 ID", example = "190000000000000001", required = true) @PathVariable long id) {
|
||||
|
||||
@@ -47,6 +47,7 @@ public class PlatformFirstLegOrderController {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限")
|
||||
})
|
||||
@GetMapping
|
||||
@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,
|
||||
@@ -61,6 +62,7 @@ public class PlatformFirstLegOrderController {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Order not found in target tenant / 目标租户下订单不存在")
|
||||
})
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('platform:order:list')")
|
||||
public ApiResponse<OrderDetailResponse> detail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Parameter(description = "Target tenant ID / 目标租户 ID", example = "10001", required = true) @RequestParam long tenantId,
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.baiyue.order.dto.OrderDetailResponse;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.entity.*;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -14,10 +15,8 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@@ -33,7 +32,8 @@ public class OrderApplicationService {
|
||||
validatePrototypeFields(request);
|
||||
var snapshots = masterDataGuard.validate(tenantId, request);
|
||||
OrderEntity order = new OrderEntity();
|
||||
order.setTenantId(tenantId); order.setOrderNo(nextNo()); order.setStatus(OrderStatus.DRAFT);
|
||||
long orderId = IdWorker.getId();
|
||||
order.setId(orderId); order.setTenantId(tenantId); order.setOrderNo(nextNo(orderId)); order.setStatus(OrderStatus.DRAFT);
|
||||
apply(order, request, snapshots); order.setCreatedBy(userId); order.setUpdatedBy(userId);
|
||||
repository.insertOrder(order);
|
||||
saveChildren(tenantId, userId, order.getId(), request);
|
||||
@@ -182,8 +182,9 @@ public class OrderApplicationService {
|
||||
order.setShipmentId(request.shipmentId()); order.setReferenceId(request.referenceId());
|
||||
}
|
||||
private String formatAddress(com.baiyue.order.client.WarehouseClient.Detail detail) {
|
||||
if (detail.addresses() == null || detail.addresses().isEmpty()) return null;
|
||||
var a = detail.addresses().get(0);
|
||||
if (detail.addresses() == null) return null;
|
||||
var a = detail.addresses().stream().filter(java.util.Objects::nonNull).findFirst().orElse(null);
|
||||
if (a == null) return null;
|
||||
return java.util.stream.Stream.of(a.countryCode(), a.stateProvince(), a.city(), a.district(),
|
||||
a.addressLine1(), a.addressLine2(), a.postalCode())
|
||||
.filter(StringUtils::hasText).collect(Collectors.joining(" "));
|
||||
@@ -191,7 +192,7 @@ public class OrderApplicationService {
|
||||
private String formatReceivingTime(com.baiyue.order.client.WarehouseClient.Detail detail) {
|
||||
if (detail.capabilities() == null) return null;
|
||||
return detail.capabilities().stream()
|
||||
.filter(c -> "RECEIVING".equals(c.capabilityCode()) && Boolean.TRUE.equals(c.enabled()))
|
||||
.filter(c -> c != null && "RECEIVING".equals(c.capabilityCode()) && Boolean.TRUE.equals(c.enabled()))
|
||||
.map(c -> c.cutoffTime() == null ? "支持收货" : "每日 " + c.cutoffTime() + " 前收货")
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
@@ -241,8 +242,7 @@ public class OrderApplicationService {
|
||||
event.setPublishStatus("PENDING"); event.setRetryCount(0); event.setCreatedAt(LocalDateTime.now());
|
||||
repository.insertOutbox(event);
|
||||
}
|
||||
private String nextNo() {
|
||||
return "FL" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
|
||||
+ ThreadLocalRandom.current().nextInt(100, 1000);
|
||||
private String nextNo(long orderId) {
|
||||
return "FL" + orderId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import com.baiyue.order.dto.OrderRequests;
|
||||
import feign.FeignException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class OrderMasterDataGuard {
|
||||
@@ -23,10 +26,10 @@ public class OrderMasterDataGuard {
|
||||
try {
|
||||
var partner=loadPartner(tenantId,request.customerId());
|
||||
var receiving=loadWarehouse(tenantId,request.receivingWarehouseId(),"收货仓库");
|
||||
if (receiving.addresses()==null || receiving.addresses().isEmpty())
|
||||
if (receiving.addresses()==null || receiving.addresses().stream().noneMatch(Objects::nonNull))
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,"收货仓库未配置地址");
|
||||
if (receiving.capabilities()==null || receiving.capabilities().stream().noneMatch(c ->
|
||||
"RECEIVING".equals(c.capabilityCode()) && Boolean.TRUE.equals(c.enabled())))
|
||||
c != null && "RECEIVING".equals(c.capabilityCode()) && Boolean.TRUE.equals(c.enabled())))
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,"仓库未启用收货能力");
|
||||
WarehouseClient.Detail destination=null;
|
||||
if (request.destinationWarehouseId()!=null) {
|
||||
@@ -57,6 +60,7 @@ public class OrderMasterDataGuard {
|
||||
var value=response.data();
|
||||
if (!Long.valueOf(tenantId).equals(value.tenantId()) || !"ACTIVE".equals(value.status()))
|
||||
throw new BizException(ResultCode.FORBIDDEN,"往来单位不属于当前企业或已停用");
|
||||
if (!StringUtils.hasText(value.partnerName())) throw unavailable("往来单位");
|
||||
return value;
|
||||
}
|
||||
private WarehouseClient.Detail loadWarehouse(long tenantId,long id,String name) {
|
||||
@@ -66,6 +70,9 @@ public class OrderMasterDataGuard {
|
||||
var value=response.data();
|
||||
if (!Long.valueOf(tenantId).equals(value.warehouse().tenantId()) || !"ACTIVE".equals(value.warehouse().status()))
|
||||
throw new BizException(ResultCode.FORBIDDEN,name+"不属于当前企业或已停用");
|
||||
if (!Long.valueOf(id).equals(value.warehouse().id()) || !StringUtils.hasText(value.warehouse().warehouseName())) {
|
||||
throw unavailable(name);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
private BizException unavailable(String name) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
SET NAMES utf8mb4;
|
||||
-- 订单服务独占数据库。其他服务只能通过 OpenAPI 或领域事件获取订单数据。
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS baiyue_order
|
||||
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
USE baiyue_order;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.client.PartnerClient;
|
||||
import com.baiyue.order.client.WarehouseClient;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
@@ -32,6 +34,31 @@ class OrderMasterDataGuardTest {
|
||||
assertThatThrownBy(() -> new OrderMasterDataGuard(warehouses,partners,"token").validate(10002,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("服务暂时不可用");
|
||||
}
|
||||
@Test
|
||||
void 主数据响应含空元素时返回受控业务错误而不是空指针异常() {
|
||||
WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class);
|
||||
var partner=new PartnerClient.Partner(7L,10001L,"服务端客户名","ACTIVE");
|
||||
var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,10001L,"深圳仓","ACTIVE"),
|
||||
Collections.singletonList(null), Collections.singletonList(null));
|
||||
when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(partner));
|
||||
when(warehouses.active("token",10001,8)).thenReturn(ApiResponse.success(wh));
|
||||
|
||||
assertThatThrownBy(() -> new OrderMasterDataGuard(warehouses,partners,"token").validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(exception -> ((BizException) exception).getResultCode())
|
||||
.isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||
}
|
||||
@Test
|
||||
void 缺失快照名称的主数据响应按服务不可用处理() {
|
||||
WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class);
|
||||
when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(new PartnerClient.Partner(7L,10001L,null,"ACTIVE")));
|
||||
|
||||
assertThatThrownBy(() -> new OrderMasterDataGuard(warehouses,partners,"token").validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(exception -> ((BizException) exception).getResultCode())
|
||||
.isEqualTo(ResultCode.SERVICE_UNAVAILABLE);
|
||||
verifyNoInteractions(warehouses);
|
||||
}
|
||||
private OrderRequests.SaveRequest request(Long partner,Long warehouse) {
|
||||
var party=new OrderRequests.Party("公司","联系人","13800138000",null,"CN",null,"深圳",null,"地址",null);
|
||||
var cargo=new OrderRequests.Cargo("货物",null,"CARTON",1,BigDecimal.ONE,BigDecimal.ONE,
|
||||
|
||||
Reference in New Issue
Block a user