This commit is contained in:
2026-08-05 16:26:10 +08:00
parent 06f7f9d2f2
commit 5ba9716e12
155 changed files with 2883 additions and 551 deletions
@@ -34,7 +34,12 @@ public class OrderRepository {
@Resource private OrderIdempotencyRecordMapper idempotencyMapper;
public void insertOrder(OrderEntity order) { orderMapper.insert(order); }
public int saveOrder(OrderEntity order) { return orderMapper.updateById(order); }
public int saveOrder(long tenantId, OrderEntity order) {
if (order.getTenantId() == null || order.getTenantId() != tenantId) return 0;
return orderMapper.update(order, new LambdaUpdateWrapper<OrderEntity>()
.eq(OrderEntity::getId, order.getId())
.eq(OrderEntity::getTenantId, tenantId));
}
public void insertCargo(OrderCargoEntity cargo) { cargoMapper.insert(cargo); }
public void insertParty(OrderPartyEntity party) { partyMapper.insert(party); }
public void insertNote(OrderInternalNoteEntity note) { noteMapper.insert(note); }
@@ -42,27 +47,38 @@ public class OrderRepository {
public void insertOutbox(OutboxEventEntity event) { outboxMapper.insert(event); }
public List<OutboxEventEntity> pendingInboundEvents(int limit) {
return outboxMapper.selectList(new LambdaQueryWrapper<OutboxEventEntity>()
.eq(OutboxEventEntity::getEventType, "FIRST_LEG_ORDER_APPROVED")
.eq(OutboxEventEntity::getAggregateType, "FIRST_LEG_ORDER")
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
.and(v -> v.isNull(OutboxEventEntity::getNextRetryAt)
.or().le(OutboxEventEntity::getNextRetryAt, LocalDateTime.now()))
.orderByAsc(OutboxEventEntity::getCreatedAt)
.last("LIMIT " + Math.min(Math.max(limit, 1), 100)));
}
public void markOutboxPublished(long id, LocalDateTime publishedAt) {
public void markOutboxPublished(long tenantId, long id, LocalDateTime publishedAt) {
outboxMapper.update(null, new LambdaUpdateWrapper<OutboxEventEntity>()
.eq(OutboxEventEntity::getId, id)
.eq(OutboxEventEntity::getTenantId, tenantId)
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
.set(OutboxEventEntity::getPublishStatus, "PUBLISHED")
.set(OutboxEventEntity::getPublishedAt, publishedAt));
}
public void markOutboxRetry(long id, LocalDateTime nextRetryAt) {
public void markOutboxRetry(long tenantId, long id, LocalDateTime nextRetryAt) {
outboxMapper.update(null, new LambdaUpdateWrapper<OutboxEventEntity>()
.eq(OutboxEventEntity::getId, id)
.eq(OutboxEventEntity::getTenantId, tenantId)
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
.setSql("retry_count = retry_count + 1")
.set(OutboxEventEntity::getNextRetryAt, nextRetryAt));
}
public void markOutboxFailed(Long tenantId, long id) {
outboxMapper.update(null, new LambdaUpdateWrapper<OutboxEventEntity>()
.eq(OutboxEventEntity::getId, id)
.eq(tenantId != null, OutboxEventEntity::getTenantId, tenantId)
.isNull(tenantId == null, OutboxEventEntity::getTenantId)
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
.set(OutboxEventEntity::getPublishStatus, "FAILED")
.set(OutboxEventEntity::getNextRetryAt, null));
}
public void insertIdempotency(OrderIdempotencyRecordEntity record) { idempotencyMapper.insert(record); }
public OrderIdempotencyRecordEntity findIdempotency(
@@ -74,11 +90,15 @@ public class OrderRepository {
.eq(OrderIdempotencyRecordEntity::getIdempotencyKey, idempotencyKey));
}
public int completeIdempotency(OrderIdempotencyRecordEntity record, Long resultOrderId) {
public int completeIdempotency(long tenantId, OrderIdempotencyRecordEntity record, Long resultOrderId) {
if (record.getTenantId() == null || record.getTenantId() != tenantId) return 0;
record.setStatus("COMPLETED");
record.setResultOrderId(resultOrderId);
record.setCompletedAt(java.time.LocalDateTime.now());
return idempotencyMapper.updateById(record);
return idempotencyMapper.update(record, new LambdaUpdateWrapper<OrderIdempotencyRecordEntity>()
.eq(OrderIdempotencyRecordEntity::getId, record.getId())
.eq(OrderIdempotencyRecordEntity::getTenantId, tenantId)
.eq(OrderIdempotencyRecordEntity::getStatus, "PROCESSING"));
}
public OrderEntity findOwned(long tenantId, long orderId) {
@@ -67,7 +67,7 @@ public class OrderApplicationService {
throw new BizException("Only draft or rejected orders can be edited");
}
apply(order, request, snapshots); order.setStatus(OrderStatus.DRAFT); order.setUpdatedBy(userId);
if (repository.saveOrder(order) != 1) throw new BizException("Concurrent order update");
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
repository.replaceDraftChildren(tenantId, id);
saveChildren(tenantId, userId, id, request);
}
@@ -90,7 +90,7 @@ public class OrderApplicationService {
order.setStatus(target); order.setUpdatedBy(userId);
if (target == OrderStatus.SUBMITTED) order.setSubmittedAt(LocalDateTime.now());
if (target == OrderStatus.COMPLETED) order.setCompletedAt(LocalDateTime.now());
if (repository.saveOrder(order) != 1) throw new BizException("Concurrent order update");
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
record(tenantId, id, from, target, target.name(), reason, userId, username);
if (target == OrderStatus.APPROVED || target == OrderStatus.CANCELLED) writeOutbox(order, target);
}
@@ -142,7 +142,7 @@ public class OrderApplicationService {
order.setExpressCompany(request.expressCompany().trim());
order.setExpressTrackingNo(request.expressTrackingNo().trim());
order.setUpdatedBy(userId);
if (repository.saveOrder(order) != 1) throw new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT, "订单已被并发修改");
if (repository.saveOrder(tenantId, order) != 1) throw new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT, "订单已被并发修改");
}
private void validatePrototypeFields(OrderRequests.SaveRequest request) {
@@ -248,7 +248,7 @@ public class OrderApplicationService {
private void completeIdempotency(IdempotencyContext context, Long resultOrderId) {
if (context.enabled() && !context.replayed()) {
if (repository.completeIdempotency(context.record(), resultOrderId) != 1) {
if (repository.completeIdempotency(context.record().getTenantId(), context.record(), resultOrderId) != 1) {
throw new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT,
"幂等记录已被并发修改");
}
@@ -7,6 +7,7 @@ import com.baiyue.order.client.FirstlegInboundClient;
import com.baiyue.order.entity.OutboxEventEntity;
import com.baiyue.order.repository.OrderRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -15,6 +16,7 @@ import java.time.LocalDateTime;
/** 可靠投递订单审核事件;接收端以订单唯一键保证重复投递安全。 */
@Component
@Slf4j
public class OrderInboundOutboxPublisher {
@Resource private OrderRepository repository;
@Resource private FirstlegInboundClient client;
@@ -24,17 +26,40 @@ public class OrderInboundOutboxPublisher {
@Scheduled(fixedDelayString = "${baiyue.order.inbound-outbox.fixed-delay-ms:3000}")
public void publishApprovedOrders() {
for (OutboxEventEntity event : repository.pendingInboundEvents(50)) {
if (!"FIRST_LEG_ORDER".equals(event.getAggregateType())
|| !"FIRST_LEG_ORDER_APPROVED".equals(event.getEventType())) {
failPermanently(event, "unsupported order event");
continue;
}
FirstlegInboundClient.OrderApprovedInboundCommand command;
try {
FirstlegInboundClient.OrderApprovedInboundCommand command = json.readValue(
command = json.readValue(
event.getPayloadJson(), FirstlegInboundClient.OrderApprovedInboundCommand.class);
} catch (Exception exception) {
failPermanently(event, "invalid event payload");
continue;
}
if (event.getTenantId() == null || event.getAggregateId() == null
|| event.getTenantId() != command.tenantId()
|| event.getAggregateId() != command.orderId()) {
failPermanently(event, "event ownership does not match payload");
continue;
}
try {
ApiResponse<Long> response = client.create(internalToken, command);
if (response == null || !response.successful()) {
throw new IllegalStateException("头程入库服务返回失败");
}
repository.markOutboxPublished(event.getId(), LocalDateTime.now());
repository.markOutboxPublished(event.getTenantId(), event.getId(), LocalDateTime.now());
} catch (Exception exception) {
repository.markOutboxRetry(event.getId(), LocalDateTime.now().plusSeconds(10));
repository.markOutboxRetry(event.getTenantId(), event.getId(), LocalDateTime.now().plusSeconds(10));
}
}
}
private void failPermanently(OutboxEventEntity event, String reason) {
log.warn("Order outbox event rejected: reason={}, eventId={}, tenantId={}, aggregateId={}, eventType={}",
reason, event.getId(), event.getTenantId(), event.getAggregateId(), event.getEventType());
repository.markOutboxFailed(event.getTenantId(), event.getId());
}
}
@@ -23,11 +23,11 @@ public class OrderMasterDataGuard {
@Resource private WarehouseClient warehouses;
@Resource private PartnerClient partners;
@Value("${baiyue.security.internal-service-token}") private String token;
@Value("${baiyue.warehouse.public-tenant-id:10001}")
private long publicTenantId = 10001L;
@Value("${baiyue.warehouse.public-tenant-id:0}")
private long publicTenantId = 0L;
public Snapshots validate(long tenantId, OrderRequests.SaveRequest request) {
try {
PartnerClient.Partner partner=request.customerId()==null ? null : loadPartner(tenantId,request.customerId());
PartnerClient.Partner partner=request.customerId()==null ? null : loadPartner(request.customerId());
var receiving=loadWarehouse(request.receivingWarehouseId(),"收货仓库");
requireWarehouseType(receiving, "DOMESTIC", "收货仓库");
if (receiving.addresses()==null || receiving.addresses().stream().noneMatch(Objects::nonNull))
@@ -59,7 +59,7 @@ public class OrderMasterDataGuard {
public void validatePersisted(long tenantId, Long partnerId, Long receivingWarehouseId,
Long destinationWarehouseId, String destinationType) {
try {
if (partnerId!=null) loadPartner(tenantId,partnerId);
if (partnerId!=null) loadPartner(partnerId);
if (receivingWarehouseId==null) throw new BizException(ResultCode.NOT_FOUND,"收货仓库已停用或不可用");
WarehouseClient.Detail receiving=loadWarehouse(receivingWarehouseId,"收货仓库");
requireWarehouseType(receiving,"DOMESTIC","收货仓库");
@@ -78,18 +78,18 @@ public class OrderMasterDataGuard {
throw downstreamFailure(exception);
}
}
private PartnerClient.Partner loadPartner(long tenantId,long id) {
private PartnerClient.Partner loadPartner(long id) {
ApiResponse<PartnerClient.Partner> response;
try {
response=partners.active(token,tenantId,id);
response=partners.active(token,publicTenantId,id);
} catch (FeignException.NotFound exception) {
throw new BizException(ResultCode.NOT_FOUND,"\u672a\u7ed1\u5b9a\u53ef\u7528\u5f80\u6765\u5ba2\u6237");
throw new BizException(ResultCode.NOT_FOUND,"平台往来单位不存在或不可用");
}
requireSuccessful(response,"往来单位");
if (response.data()==null) throw unavailable("往来单位");
var value=response.data();
if (!Long.valueOf(tenantId).equals(value.tenantId()))
throw new BizException(ResultCode.FORBIDDEN,"往来单位不属于当前企业");
if (!Long.valueOf(publicTenantId).equals(value.tenantId()))
throw new BizException(ResultCode.FORBIDDEN,"往来单位不属于平台公共主数据");
if (!"ACTIVE".equals(value.status()))
throw new BizException(ResultCode.CONFLICT,"往来单位已停用");
if (value.types() == null || !value.types().contains("SHIPPER"))
@@ -5,7 +5,8 @@ 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;}
public static void platform(AuthenticatedUser u){requireApp(u,"PLATFORM");requirePlatformTenant(u);}
public static long platform(AuthenticatedUser u,long targetTenantId){platform(u);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");}
private static void requirePlatformTenant(AuthenticatedUser u){if(u.tenantId()==null||u.tenantId()!=0)throw new BizException(ResultCode.FORBIDDEN,"Platform tenant must be 0");}
}
@@ -9,7 +9,7 @@ paths:
/api/merchant/v1/orders/first-leg:
get:
summary: 客户端入库订单聚合列表
description: 按当前登录租户分页返回订单、货物汇总、收货方快照及头程入仓编码,需要 merchant:order:list 权限。
description: 按当前登录客户分页返回订单、货物汇总、收货方快照及头程入仓编码,需要 merchant:order:list 权限;客户 ID 从 JWT 的 tenantId 兼容字段取得。
parameters:
- {name: keyword, in: query, required: false, schema: {type: string, maxLength: 64}}
- {name: orderNo, in: query, required: false, schema: {type: string, maxLength: 64}}
@@ -53,12 +53,12 @@ paths:
/api/platform/v1/orders/first-leg:
get:
summary: 平台分页查询头程订单
description: tenantId 省略时查询全部客户租户订单,传入时仅查询指定租户。
description: tenantId 是目标客户 ID 的兼容参数名;省略时查询全部客户订单,传入时仅查询指定客户。
parameters:
- name: tenantId
in: query
required: false
description: 可选的目标租户筛选
description: 可选的目标客户筛选;tenantId 为历史兼容参数名
schema: {type: integer, format: int64, minimum: 1}
responses:
'200': {description: 查询成功或业务失败,按响应体 code 判断}
@@ -79,7 +79,7 @@ paths:
/api/platform/v1/orders/first-leg/tenant-counts:
get:
summary: 批量查询客户运单数
description: 按客户租户 ID 返回真实头程运单数量,最多 200 个租户。
description: 按客户 ID 返回真实头程运单数量,tenantIds 为历史兼容参数名;最多 200 个客户。
parameters:
- name: tenantIds
in: query
@@ -133,5 +133,5 @@ components:
name: Idempotency-Key
in: header
required: true
description: 同一租户和操作者重试同一请求时必须保持不变
description: 同一客户和操作者重试同一请求时必须保持不变
schema: {type: string, minLength: 1, maxLength: 128}