优化
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user