This commit is contained in:
2026-08-18 11:41:10 +08:00
parent 3e1010fa26
commit ec4b1e0fe5
28 changed files with 412 additions and 42 deletions
@@ -14,6 +14,6 @@ public interface FirstlegPricingClient {
@RequestBody ChannelValidationRequest request);
record ChannelValidationRequest(long tenantId, long originWarehouseId, String channelCode,
String destinationType) {}
String destinationType, boolean requireInboundCodePrefix) {}
record ChannelValidationResponse(String channelCode, String inboundCodePrefix) {}
}
@@ -8,6 +8,7 @@ import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.order.dto.BatchOrderReviewResponse;
import com.baiyue.order.dto.OrderDetailResponse;
import com.baiyue.order.dto.OrderRequests;
import com.baiyue.order.dto.InboundCodePolicyResponse;
import com.baiyue.order.dto.TenantOrderCountResponse;
import com.baiyue.order.dto.TenantOrderStatisticsResponse;
import com.baiyue.order.entity.OrderEntity;
@@ -47,6 +48,12 @@ public class PlatformFirstLegOrderController {
@Resource
private OrderApplicationService service;
@GetMapping("/inbound-code-policy")
@PreAuthorize("hasAnyAuthority('platform:order:list', 'platform:order:create', 'platform:order:edit', 'platform:order:submit')")
public ApiResponse<InboundCodePolicyResponse> inboundCodePolicy() {
return ApiResponse.success(service.inboundCodePolicy());
}
@BusinessAudit(event = "ORDER_CREATE", resourceType = "FIRST_LEG_ORDER", description = "平台代客创建草稿")
@Operation(summary = "平台代客创建草稿", description = "在明确指定的目标租户内创建头程订单草稿,必须携带幂等键。")
@ApiResponses({
@@ -121,10 +121,9 @@ public class OrderRepository {
.eq(OrderEntity::getId, orderId));
}
public boolean inboundCodeExists(long tenantId, String inboundCode, Long excludedOrderId) {
public boolean inboundCodeExists(String inboundCode, Long excludedOrderId) {
if (!StringUtils.hasText(inboundCode)) return false;
return orderMapper.selectCount(new LambdaQueryWrapper<OrderEntity>()
.eq(OrderEntity::getTenantId, tenantId)
.eq(OrderEntity::getInboundCode, inboundCode)
.ne(excludedOrderId != null, OrderEntity::getId, excludedOrderId)) > 0;
}
@@ -3,7 +3,10 @@ package com.baiyue.order.service;
import com.baiyue.order.dto.OrderRequests;
import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.repository.OrderRepository;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import jakarta.annotation.Resource;
import jakarta.validation.Validator;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@@ -13,9 +16,19 @@ import org.springframework.transaction.annotation.Transactional;
public class MerchantBatchDraftItemService {
@Resource private OrderApplicationService orders;
@Resource private OrderRepository repository;
@Resource private Validator validator;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public OrderEntity create(long tenantId, long userId, OrderRequests.SaveRequest draft, String idempotencyKey) {
var violations = validator.validate(draft);
if (!violations.isEmpty()) {
String message = violations.stream()
.sorted(java.util.Comparator.comparing(violation -> violation.getPropertyPath().toString()))
.findFirst()
.map(violation -> violation.getPropertyPath() + ": " + violation.getMessage())
.orElse("草稿参数不合法");
throw new BizException(ResultCode.VALIDATION_FAILED, message);
}
Long id = orders.createDraft(tenantId, userId, draft, idempotencyKey);
return repository.findOwned(tenantId, id);
}
@@ -80,7 +80,11 @@ public class OrderApplicationService {
order.setStatus(OrderStatus.DRAFT);
apply(order, tenantId, request, snapshots);
order.setCreatedBy(userId); order.setUpdatedBy(userId);
repository.insertOrder(order);
try {
repository.insertOrder(order);
} catch (DuplicateKeyException duplicate) {
throw duplicateInboundCode(order, duplicate);
}
saveChildren(tenantId, userId, order.getId(), request);
record(tenantId, order.getId(), null, OrderStatus.DRAFT, "CREATE", null, userId, null);
return order.getId();
@@ -118,7 +122,11 @@ public class OrderApplicationService {
if (before == OrderStatus.SUBMITTED) {
order.setStatus(OrderStatus.SUBMITTED);
}
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
try {
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
} catch (DuplicateKeyException duplicate) {
throw duplicateInboundCode(order, duplicate);
}
repository.replaceDraftChildren(tenantId, id);
saveChildren(tenantId, userId, id, request);
record(tenantId, id, before, order.getStatus(), "UPDATE", null, userId, null);
@@ -473,7 +481,7 @@ public class OrderApplicationService {
}
return;
}
if (normalized != null && repository.inboundCodeExists(order.getTenantId(), normalized, order.getId())) {
if (normalized != null && repository.inboundCodeExists(normalized, order.getId())) {
throw new BizException(ResultCode.CONFLICT, "入仓码已存在,请核对旧系统数据");
}
order.setInboundCode(normalized);
@@ -489,6 +497,12 @@ public class OrderApplicationService {
}
return pricingChannelGuard.requireAvailable(tenantId, receivingWarehouseId, channelCode, destinationType);
}
private RuntimeException duplicateInboundCode(OrderEntity order, DuplicateKeyException duplicate) {
if (StringUtils.hasText(order.getInboundCode())) {
return new BizException(ResultCode.CONFLICT, "入仓码已存在,请核对旧系统数据");
}
return duplicate;
}
private void requirePrintableInboundCode(OrderEntity order) {
String inboundCode = order.getInboundCode();
if (!StringUtils.hasText(inboundCode) || !inboundCode.matches("[A-Za-z0-9._:-]+")) {
@@ -16,10 +16,20 @@ public class OrderPricingChannelGuard {
public String requireAvailable(long tenantId, long originWarehouseId, String channelCode,
String destinationType) {
return validate(tenantId, originWarehouseId, channelCode, destinationType, true);
}
public void requireActive(long tenantId, long originWarehouseId, String channelCode,
String destinationType) {
validate(tenantId, originWarehouseId, channelCode, destinationType, false);
}
private String validate(long tenantId, long originWarehouseId, String channelCode,
String destinationType, boolean requireInboundCodePrefix) {
try {
ApiResponse<FirstlegPricingClient.ChannelValidationResponse> response = client.validateChannel(internalToken,
new FirstlegPricingClient.ChannelValidationRequest(
tenantId, originWarehouseId, channelCode, destinationType));
tenantId, originWarehouseId, channelCode, destinationType, requireInboundCodePrefix));
if (response == null) {
throw unavailable();
}
@@ -29,8 +39,8 @@ public class OrderPricingChannelGuard {
? code.message() : response.message();
throw new BizException(code, message);
}
if (response.data() == null || response.data().inboundCodePrefix() == null
|| response.data().inboundCodePrefix().isBlank()) {
if (response.data() == null || requireInboundCodePrefix
&& (response.data().inboundCodePrefix() == null || response.data().inboundCodePrefix().isBlank())) {
throw new BizException(ResultCode.CONFLICT, "所选渠道未配置入仓码前缀");
}
return response.data().inboundCodePrefix();
+30 -3
View File
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS ord_order (
id BIGINT UNSIGNED NOT NULL COMMENT '订单ID(雪花ID)',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
order_no VARCHAR(32) NOT NULL COMMENT '客户范围内唯一订单号',
inbound_code VARCHAR(40) NULL COMMENT '提交后生成并固定的入仓码;未提交草稿为空',
inbound_code VARCHAR(40) NULL COMMENT '全平台唯一入仓码;来源由全局模式决定,提交后固定',
customer_id BIGINT UNSIGNED NULL COMMENT '客户逻辑ID',
customer_name VARCHAR(128) NOT NULL COMMENT '下单时客户名称快照',
status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订单业务状态:DRAFT草稿、SUBMITTED已提交、APPROVED审核通过、ACCEPTED已接单、REJECTED审核驳回、PROCESSING履约中、COMPLETED已完成、CANCELLED已取消',
@@ -54,13 +54,40 @@ CREATE TABLE IF NOT EXISTS ord_order (
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (id),
UNIQUE KEY uk_order_tenant_no (tenant_id, order_no),
UNIQUE KEY uk_order_tenant_inbound_code (tenant_id, inbound_code),
UNIQUE KEY uk_order_inbound_code (inbound_code),
KEY idx_order_tenant_status_time (tenant_id, status, created_at),
KEY idx_order_tenant_role_submitted (tenant_id, order_role, submitted_at),
KEY idx_order_tenant_customer (tenant_id, customer_id),
KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id)
) ENGINE=InnoDB COMMENT='头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态';
-- 既有开发库必须先确认不存在跨客户重复码;若存在则在此处失败,不自动改写业务数据。
SET @order_global_inbound_index_sql = IF(
NOT EXISTS(
SELECT 1 FROM information_schema.statistics
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
AND index_name = 'uk_order_inbound_code'
),
'ALTER TABLE baiyue_order.ord_order ADD UNIQUE KEY uk_order_inbound_code (inbound_code)',
'SELECT 1'
);
PREPARE order_global_inbound_index_stmt FROM @order_global_inbound_index_sql;
EXECUTE order_global_inbound_index_stmt;
DEALLOCATE PREPARE order_global_inbound_index_stmt;
SET @order_tenant_inbound_index_drop_sql = IF(
EXISTS(
SELECT 1 FROM information_schema.statistics
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
AND index_name = 'uk_order_tenant_inbound_code'
),
'ALTER TABLE baiyue_order.ord_order DROP INDEX uk_order_tenant_inbound_code',
'SELECT 1'
);
PREPARE order_tenant_inbound_index_drop_stmt FROM @order_tenant_inbound_index_drop_sql;
EXECUTE order_tenant_inbound_index_drop_stmt;
DEALLOCATE PREPARE order_tenant_inbound_index_drop_stmt;
-- Existing development databases may still have the former NOT NULL contract.
SET @order_inbound_code_nullable_sql = IF(
EXISTS(
@@ -68,7 +95,7 @@ SET @order_inbound_code_nullable_sql = IF(
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
AND column_name = 'inbound_code' AND is_nullable = 'NO'
),
'ALTER TABLE baiyue_order.ord_order MODIFY COLUMN inbound_code VARCHAR(40) NULL COMMENT ''提交后生成并固定的入仓码;未提交草稿为空''',
'ALTER TABLE baiyue_order.ord_order MODIFY COLUMN inbound_code VARCHAR(40) NULL COMMENT ''全平台唯一入仓码;来源由全局模式决定,提交后固定''',
'SELECT 1'
);
PREPARE order_inbound_code_nullable_stmt FROM @order_inbound_code_nullable_sql;
@@ -6,6 +6,21 @@ info:
security:
- bearerAuth: []
paths:
/api/platform/v1/orders/first-leg/inbound-code-policy:
get:
summary: 平台端查询全系统入仓码模式
description: 与客户下单端返回同一份全局策略,不按客户区分。
responses:
'200':
description: 查询成功
content:
application/json:
schema:
type: object
properties:
code: {type: string, example: '200'}
data: {$ref: '#/components/schemas/InboundCodePolicy'}
'403': {description: 需要平台订单列表、创建、编辑或提交权限之一}
/api/platform/v1/orders:
get:
summary: 平台统一订单列表