优化
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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: 平台统一订单列表
|
||||
|
||||
Reference in New Issue
Block a user