优化
This commit is contained in:
+40
@@ -0,0 +1,40 @@
|
||||
package com.baiyue.order.controller;
|
||||
|
||||
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.dto.OrderSplitDtos;
|
||||
import com.baiyue.order.service.OrderFulfillmentSplitService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
/** Firstleg 装柜结算调用的订单域内部契约。 */
|
||||
@RestController
|
||||
@RequestMapping("/internal/order/v1/fulfillment-splits")
|
||||
public class InternalOrderFulfillmentSplitController {
|
||||
@Resource private OrderFulfillmentSplitService service;
|
||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||
private byte[] expectedToken;
|
||||
@PostConstruct void initializeToken(){expectedToken=internalToken.getBytes(StandardCharsets.UTF_8);}
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<OrderSplitDtos.Response> create(
|
||||
@RequestHeader(value="X-Internal-Service-Token",required=false) String token,
|
||||
@RequestHeader(value="X-Operator-Id",defaultValue="0") long operatorId,
|
||||
@Valid @RequestBody OrderSplitDtos.CreateRequest request) {
|
||||
requireToken(token); return ApiResponse.success(service.split(request,operatorId));
|
||||
}
|
||||
|
||||
@GetMapping("/{tenantId}/{parentOrderId}")
|
||||
public ApiResponse<OrderSplitDtos.Tree> tree(
|
||||
@RequestHeader(value="X-Internal-Service-Token",required=false) String token,
|
||||
@PathVariable long tenantId,@PathVariable long parentOrderId){
|
||||
requireToken(token); return ApiResponse.success(service.tree(tenantId,parentOrderId));
|
||||
}
|
||||
private void requireToken(String token){if(token==null||!MessageDigest.isEqual(expectedToken,token.getBytes(StandardCharsets.UTF_8)))throw new BizException(ResultCode.FORBIDDEN,"内部服务凭证无效");}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import com.baiyue.order.dto.UnifiedOrderQuery;
|
||||
import com.baiyue.order.dto.UnifiedOrderResponse;
|
||||
import com.baiyue.order.service.OrderScopeGuard;
|
||||
import com.baiyue.order.service.UnifiedOrderQueryService;
|
||||
import com.baiyue.order.service.OrderFulfillmentSplitService;
|
||||
import com.baiyue.order.dto.OrderSplitDtos;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -24,6 +26,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequestMapping("/api/merchant/v1/orders")
|
||||
public class MerchantUnifiedOrderController {
|
||||
@Resource private UnifiedOrderQueryService service;
|
||||
@Resource private OrderFulfillmentSplitService splitService;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('merchant:order:list')")
|
||||
@@ -42,4 +45,10 @@ public class MerchantUnifiedOrderController {
|
||||
@PathVariable long orderId) {
|
||||
return ApiResponse.success(service.detail(OrderScopeGuard.merchant(user), orderId, false));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/fulfillment-tree")
|
||||
@PreAuthorize("hasAuthority('merchant:order:view')")
|
||||
@Operation(summary = "我的父子订单与柜次数量摘要", description = "不返回装柜照片、锁码或内部审计字段。")
|
||||
public ApiResponse<OrderSplitDtos.Tree> fulfillmentTree(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable long orderId){return ApiResponse.success(splitService.tree(OrderScopeGuard.merchant(user),orderId));}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.service.OrderApplicationService;
|
||||
import com.baiyue.order.service.OrderScopeGuard;
|
||||
import com.baiyue.order.service.UnifiedOrderQueryService;
|
||||
import com.baiyue.order.service.OrderFulfillmentSplitService;
|
||||
import com.baiyue.order.dto.OrderSplitDtos;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -31,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
public class PlatformUnifiedOrderController {
|
||||
@Resource private UnifiedOrderQueryService service;
|
||||
@Resource private OrderApplicationService orderService;
|
||||
@Resource private OrderFulfillmentSplitService splitService;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:order:list')")
|
||||
@@ -54,6 +57,14 @@ public class PlatformUnifiedOrderController {
|
||||
return ApiResponse.success(service.detail(OrderScopeGuard.platform(user, tenantId), orderId, true));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/fulfillment-tree")
|
||||
@PreAuthorize("hasAuthority('platform:order:list')")
|
||||
@Operation(summary = "父子订单与柜次数量摘要")
|
||||
public ApiResponse<OrderSplitDtos.Tree> fulfillmentTree(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,@PathVariable long orderId){
|
||||
return ApiResponse.success(splitService.tree(OrderScopeGuard.platform(user,tenantId),orderId));
|
||||
}
|
||||
|
||||
@PostMapping("/{orderId}/internal-notes")
|
||||
@BusinessAudit(event = "ORDER_INTERNAL_NOTE", resourceType = "FIRST_LEG_ORDER", resourceId = "#orderId", description = "Add internal note / 添加内部备注")
|
||||
@PreAuthorize("hasAuthority('platform:order:note')")
|
||||
|
||||
@@ -8,12 +8,16 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface OrderFulfillmentSplitMapper extends BaseMapper<OrderFulfillmentSplitEntity> {
|
||||
@Select("SELECT * FROM ord_order WHERE tenant_id=#{tenantId} AND id=#{orderId} AND deleted=0 FOR UPDATE")
|
||||
OrderEntity lockOrder(@Param("tenantId") long tenantId, @Param("orderId") long orderId);
|
||||
|
||||
@Select("SELECT * FROM ord_order WHERE tenant_id=#{tenantId} AND id=#{orderId} AND deleted=0")
|
||||
OrderEntity findOrder(@Param("tenantId") long tenantId, @Param("orderId") long orderId);
|
||||
|
||||
@Select("SELECT * FROM ord_order_fulfillment_split WHERE tenant_id=#{tenantId} AND parent_order_id=#{parentOrderId} AND idempotency_key=#{key} ORDER BY id")
|
||||
List<OrderFulfillmentSplitEntity> findByIdempotency(@Param("tenantId") long tenantId,
|
||||
@Param("parentOrderId") long parentOrderId,
|
||||
@@ -22,4 +26,7 @@ public interface OrderFulfillmentSplitMapper extends BaseMapper<OrderFulfillment
|
||||
@Select("SELECT * FROM ord_order_fulfillment_split WHERE tenant_id=#{tenantId} AND parent_order_id=#{parentOrderId} ORDER BY created_at,id")
|
||||
List<OrderFulfillmentSplitEntity> findByParent(@Param("tenantId") long tenantId,
|
||||
@Param("parentOrderId") long parentOrderId);
|
||||
|
||||
@Select("SELECT COALESCE(MAX(split_sequence),0) FROM ord_order WHERE tenant_id=#{tenantId} AND (id=#{parentOrderId} OR parent_order_id=#{parentOrderId}) AND deleted=0")
|
||||
Integer maxSplitSequence(@Param("tenantId") long tenantId, @Param("parentOrderId") long parentOrderId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.dto.OrderSplitDtos;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import com.baiyue.order.mapper.OrderFulfillmentSplitMapper;
|
||||
import com.baiyue.order.mapper.OrderMapper;
|
||||
import com.baiyue.order.entity.OrderFulfillmentSplitEntity;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/** 订单域父子单/柜次数量契约。仅生成一层 CHILD,不复制原订单身份。 */
|
||||
@Service
|
||||
public class OrderFulfillmentSplitService {
|
||||
@Resource private OrderFulfillmentSplitMapper splits;
|
||||
@Resource private OrderMapper orders;
|
||||
@Resource private ObjectMapper json;
|
||||
|
||||
@Transactional
|
||||
public OrderSplitDtos.Response split(OrderSplitDtos.CreateRequest req, long operatorId) {
|
||||
validate(req);
|
||||
String hash = hash(req);
|
||||
List<OrderFulfillmentSplitEntity> replay = splits.findByIdempotency(req.tenantId(), req.parentOrderId(), req.idempotencyKey());
|
||||
if (!replay.isEmpty()) {
|
||||
if (replay.stream().anyMatch(x -> x.getRequestHash()!=null && !hash.equals(x.getRequestHash())))
|
||||
throw new BizException(ResultCode.CONFLICT, "幂等键对应的请求内容不一致");
|
||||
return response(req, splits.lockOrder(req.tenantId(), req.parentOrderId()), replay);
|
||||
}
|
||||
OrderEntity parent = splits.lockOrder(req.tenantId(), req.parentOrderId());
|
||||
if (parent == null) throw new BizException(ResultCode.NOT_FOUND, "父订单不存在或不属于该客户");
|
||||
if (!req.parentOrderId().equals(req.sourceOrderId()) && !isDirectChild(req.sourceOrderId(), parent))
|
||||
throw new BizException(ResultCode.CONFLICT, "来源订单必须是父订单或其直接子订单");
|
||||
if (!"MASTER".equals(parent.getOrderRole())) throw new BizException(ResultCode.CONFLICT, "子订单不能继续拆分");
|
||||
if (parent.getVersion()!=null && !req.expectedParentVersion().equals(parent.getVersion()))
|
||||
throw new BizException(ResultCode.CONFLICT, "父订单版本已变化,请刷新后重试");
|
||||
int next = Optional.ofNullable(splits.maxSplitSequence(req.tenantId(), req.parentOrderId())).orElse(0) + 1;
|
||||
List<OrderFulfillmentSplitEntity> rows = new ArrayList<>();
|
||||
OrderSplitDtos.Child loaded = createChild(req, parent, next++, "LOADED", req.loadedPackageCount(), hash, operatorId, rows);
|
||||
OrderSplitDtos.Child remainder = req.remainingPackageCount() > 0
|
||||
? createChild(req, parent, next, "REMAINDER", req.remainingPackageCount(), hash, operatorId, rows) : null;
|
||||
for (OrderFulfillmentSplitEntity row : rows) splits.insert(row);
|
||||
parent.setVersion((parent.getVersion()==null?0:parent.getVersion()) + 1);
|
||||
LambdaUpdateWrapper<OrderEntity> guard = new LambdaUpdateWrapper<OrderEntity>()
|
||||
.eq(OrderEntity::getId, parent.getId())
|
||||
.eq(OrderEntity::getTenantId, parent.getTenantId())
|
||||
.eq(OrderEntity::getVersion, req.expectedParentVersion());
|
||||
int updated = orders.update(parent, guard);
|
||||
if (updated != 1) throw new BizException(ResultCode.CONFLICT, "父订单版本已变化,请刷新后重试");
|
||||
return new OrderSplitDtos.Response(parent.getId(), parent.getOrderNo(), parent.getVersion(), loaded, remainder,
|
||||
req.sourcePackageCount(), req.loadedPackageCount(), req.remainingPackageCount(), req.loadingPlanId(), req.containerBookingId(), req.idempotencyKey(), LocalDateTime.now());
|
||||
}
|
||||
|
||||
public OrderSplitDtos.Tree tree(long tenantId, long parentId) {
|
||||
OrderEntity parent = splits.findOrder(tenantId, parentId);
|
||||
if (parent == null) throw new BizException(ResultCode.NOT_FOUND, "订单不存在");
|
||||
if (!"MASTER".equals(parent.getOrderRole())) parentId = Optional.ofNullable(parent.getParentOrderId()).orElse(parentId);
|
||||
parent = splits.findOrder(tenantId, parentId);
|
||||
List<OrderFulfillmentSplitEntity> rows = splits.findByParent(tenantId, parentId);
|
||||
Map<Long,OrderEntity> children = new HashMap<>();
|
||||
for (OrderFulfillmentSplitEntity row: rows) { OrderEntity child=orders.selectById(row.getChildOrderId()); if(child!=null&&tenantId==child.getTenantId()) children.put(child.getId(),child); }
|
||||
List<OrderSplitDtos.Child> result = rows.stream().map(r -> { OrderEntity c=children.get(r.getChildOrderId()); return new OrderSplitDtos.Child(r.getChildOrderId(), c==null?null:c.getOrderNo(), c==null?null:c.getSplitSequence(), r.getSplitRole(), "LOADED".equals(r.getSplitRole())?r.getLoadedPackageCount():r.getRemainingPackageCount()); }).toList();
|
||||
int allocated = rows.stream().filter(r -> "LOADED".equals(r.getSplitRole())).mapToInt(OrderFulfillmentSplitEntity::getLoadedPackageCount).sum();
|
||||
return new OrderSplitDtos.Tree(parent.getId(), parent.getOrderNo(), parent.getVersion(), result, allocated, Math.max(0, rows.stream().mapToInt(OrderFulfillmentSplitEntity::getSourcePackageCount).findFirst().orElse(0)-allocated));
|
||||
}
|
||||
|
||||
private boolean isDirectChild(long source, OrderEntity parent) { OrderEntity c=orders.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<OrderEntity>().eq(OrderEntity::getId,source).eq(OrderEntity::getTenantId,parent.getTenantId())); return c!=null && "CHILD".equals(c.getOrderRole()) && parent.getId().equals(c.getParentOrderId()); }
|
||||
private OrderSplitDtos.Child createChild(OrderSplitDtos.CreateRequest r, OrderEntity p, int seq, String role, int count, String hash, long op, List<OrderFulfillmentSplitEntity> rows) {
|
||||
OrderEntity c = new OrderEntity(); c.setId(IdWorker.getId()); c.setTenantId(p.getTenantId()); c.setOrderNo(p.getOrderNo()+"-"+seq); c.setOrderRole("CHILD"); c.setParentOrderId(p.getId()); c.setRootOrderId(p.getId()); c.setSplitSequence(seq); c.setStatus(p.getStatus()); c.setCustomerId(p.getCustomerId()); c.setCustomerName(p.getCustomerName()); c.setTransportMode(p.getTransportMode()); c.setTradeTerm(p.getTradeTerm()); c.setDestinationCountryCode(p.getDestinationCountryCode()); c.setDestinationWarehouseId(p.getDestinationWarehouseId()); c.setDestinationWarehouseName(p.getDestinationWarehouseName()); c.setReceivingWarehouseId(p.getReceivingWarehouseId()); c.setReceivingWarehouseName(p.getReceivingWarehouseName()); c.setChannelCode(p.getChannelCode()); c.setChannelName(p.getChannelName()); c.setDestinationType(p.getDestinationType()); c.setCreatedBy(op); c.setUpdatedBy(op); orders.insert(c);
|
||||
OrderFulfillmentSplitEntity row=new OrderFulfillmentSplitEntity(); row.setId(IdWorker.getId()); row.setTenantId(r.tenantId()); row.setParentOrderId(p.getId()); row.setChildOrderId(c.getId()); row.setLoadingPlanId(r.loadingPlanId()); row.setContainerBookingId(r.containerBookingId()); row.setSourcePackageCount(r.sourcePackageCount()); row.setLoadedPackageCount(r.loadedPackageCount()); row.setRemainingPackageCount(r.remainingPackageCount()); row.setOperationId(Optional.ofNullable(r.operationId()).orElse("split-"+r.idempotencyKey())); row.setIdempotencyKey(r.idempotencyKey()); row.setRequestHash(hash); row.setSplitRole(role); row.setExpectedParentVersion(r.expectedParentVersion()); row.setCreatedBy(op); row.setCreatedByName(r.operatorName()); row.setCreatedAt(LocalDateTime.now()); rows.add(row); return new OrderSplitDtos.Child(c.getId(), c.getOrderNo(), seq, role, count);
|
||||
}
|
||||
private void validate(OrderSplitDtos.CreateRequest r) { if(r==null||r.tenantId()==null||r.parentOrderId()==null||r.sourceOrderId()==null||r.loadingPlanId()==null||r.containerBookingId()==null||r.sourcePackageCount()==null||r.loadedPackageCount()==null||r.remainingPackageCount()==null||r.expectedParentVersion()==null||r.idempotencyKey()==null||r.idempotencyKey().isBlank()) throw new BizException(ResultCode.BAD_REQUEST,"拆分请求字段不完整"); if(r.loadedPackageCount()<1||r.remainingPackageCount()<0||r.loadedPackageCount()+r.remainingPackageCount()!=r.sourcePackageCount()) throw new BizException(ResultCode.VALIDATION_FAILED,"装柜数量与剩余数量必须守恒"); }
|
||||
private String hash(Object o) { try { byte[] b=json.writeValueAsString(o).getBytes(StandardCharsets.UTF_8); byte[] d=MessageDigest.getInstance("SHA-256").digest(b); StringBuilder s=new StringBuilder(); for(byte x:d)s.append(String.format("%02x",x)); return s.toString(); } catch(Exception e){throw new BizException("无法生成请求指纹");} }
|
||||
private OrderSplitDtos.Response response(OrderSplitDtos.CreateRequest r, OrderEntity p, List<OrderFulfillmentSplitEntity> rows) { OrderSplitDtos.Child l=null,m=null; for(var x:rows){OrderEntity c=orders.selectById(x.getChildOrderId()); var z=new OrderSplitDtos.Child(x.getChildOrderId(),c==null?null:c.getOrderNo(),c==null?null:c.getSplitSequence(),x.getSplitRole(),"LOADED".equals(x.getSplitRole())?x.getLoadedPackageCount():x.getRemainingPackageCount()); if("LOADED".equals(x.getSplitRole()))l=z;else m=z;} return new OrderSplitDtos.Response(p.getId(),p.getOrderNo(),p.getVersion(),l,m,r.sourcePackageCount(),r.loadedPackageCount(),r.remainingPackageCount(),r.loadingPlanId(),r.containerBookingId(),r.idempotencyKey(),rows.get(0).getCreatedAt()); }
|
||||
}
|
||||
Reference in New Issue
Block a user