This commit is contained in:
2026-08-14 14:50:39 +08:00
parent 261a8bf2df
commit 36a048a2bc
34 changed files with 263 additions and 120 deletions
@@ -1,16 +0,0 @@
package com.baiyue.order.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
/** 全平台入仓码票序;自增主键保证并发创建时不会重复。 */
@Getter
@Setter
@TableName("ord_inbound_code_sequence")
public class InboundCodeSequenceEntity {
@TableId(type = IdType.AUTO)
private Long id;
}
@@ -1,13 +1,25 @@
package com.baiyue.order.mapper;
import com.baiyue.order.entity.InboundCodeSequenceEntity;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDate;
@Mapper
public interface InboundCodeSequenceMapper {
@Insert("INSERT INTO ord_inbound_code_sequence (created_at) VALUES (CURRENT_TIMESTAMP(3))")
@Options(useGeneratedKeys = true, keyProperty = "id")
int allocate(InboundCodeSequenceEntity value);
@Insert("""
INSERT INTO ord_inbound_code_daily_sequence
(business_date, current_value, created_at, updated_at)
VALUES
(#{businessDate}, 1, CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3))
ON DUPLICATE KEY UPDATE
current_value = current_value + 1,
updated_at = CURRENT_TIMESTAMP(3)
""")
int increment(@Param("businessDate") LocalDate businessDate);
@Select("SELECT current_value FROM ord_inbound_code_daily_sequence WHERE business_date = #{businessDate}")
Long currentValue(@Param("businessDate") LocalDate businessDate);
}
@@ -13,6 +13,8 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.List;
@@ -35,12 +37,14 @@ public class OrderRepository {
@Resource private OrderIdempotencyRecordMapper idempotencyMapper;
@Resource private InboundCodeSequenceMapper inboundCodeSequenceMapper;
public long nextInboundCodeSequence() {
InboundCodeSequenceEntity value = new InboundCodeSequenceEntity();
if (inboundCodeSequenceMapper.allocate(value) != 1 || value.getId() == null) {
@Transactional(propagation = Propagation.MANDATORY)
public long nextInboundCodeSequence(java.time.LocalDate businessDate) {
if (businessDate == null || inboundCodeSequenceMapper.increment(businessDate) < 1) {
throw new IllegalStateException("Failed to allocate inbound code sequence");
}
return value.getId();
Long value = inboundCodeSequenceMapper.currentValue(businessDate);
if (value == null || value < 1) throw new IllegalStateException("Failed to read inbound code sequence");
return value;
}
public void insertOrder(OrderEntity order) { orderMapper.insert(order); }
@@ -163,7 +163,8 @@ public class OrderApplicationService {
public PageResult<OrderEntity> page(Long tenantId, OrderRequests.QueryRequest query) { return repository.page(tenantId, query); }
String nextInboundCode(String prefix) {
return formatInboundCode(prefix, LocalDate.now(INBOUND_CODE_ZONE), repository.nextInboundCodeSequence());
LocalDate businessDate = LocalDate.now(INBOUND_CODE_ZONE);
return formatInboundCode(prefix, businessDate, repository.nextInboundCodeSequence(businessDate));
}
static String formatInboundCode(String prefix, LocalDate date, long sequence) {
+8 -6
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) NOT NULL COMMENT '订单创建时生成并固定的入仓码:物流前缀+YYMMDD+全局票序',
inbound_code VARCHAR(40) NOT NULL COMMENT '订单创建时生成并固定的入仓码:物流前缀+YYMMDD+当天全平台票序',
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已取消',
@@ -60,11 +60,13 @@ CREATE TABLE IF NOT EXISTS ord_order (
KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id)
) ENGINE=InnoDB COMMENT='头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态';
CREATE TABLE IF NOT EXISTS ord_inbound_code_sequence (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '全平台入仓码票序',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '票序分配时间',
PRIMARY KEY (id)
) ENGINE=InnoDB COMMENT='入仓码全局票序;不同客户和物流渠道共用且不按日期清零';
CREATE TABLE IF NOT EXISTS ord_inbound_code_daily_sequence (
business_date DATE NOT NULL COMMENT '上海时区业务日期',
current_value BIGINT UNSIGNED NOT NULL COMMENT '当天全平台已分配票数',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '当天首次分配时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最近分配时间',
PRIMARY KEY (business_date)
) ENGINE=InnoDB COMMENT='入仓码每日票序;不同客户和物流渠道共用,按上海日期重新从1开始';
CREATE TABLE IF NOT EXISTS ord_order_fulfillment_split (
id BIGINT UNSIGNED NOT NULL COMMENT '拆分台账ID',
@@ -11,7 +11,7 @@ paths:
summary: 平台统一订单列表
description: >-
统一返回七态产品主状态、下一步动作及测量/预装/装柜/派送摘要,需要 platform:order:list。
订单创建时按物流渠道生成固定 inboundCode,格式为物流前缀+YYMMDD+全局票序;有入库单时同时返回
订单创建时按物流渠道生成固定 inboundCode,格式为物流前缀+YYMMDD+当天全平台票序;每日按上海日期从0001重新开始;有入库单时同时返回
inboundId、inboundVersion、loadingVersion、pickingVersion;inboundVersion
才是 C04 loading/complete 的 expectedVersion,loadingVersion 当前等于 pickingVersion(兼容别名)。
tenantId 省略时跨全部客户;mainStatus 省略时按订单创建时间分页,指定 PENDING_REVIEW 时由 Order 查询,
@@ -620,7 +620,7 @@ components:
success: {type: boolean}
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
orderNo: {type: string}
inboundCode: {type: string, pattern: '^[A-Z]{4,6}[0-9]{10,}$', example: ZBYHK2608140897}
inboundCode: {type: string, pattern: '^[A-Z]{4,6}[0-9]{10,}$', example: ZBYHK2608140001}
errorCode: {type: string}
errorMessage: {type: string}
FirstLegOrderCargo:
@@ -0,0 +1,35 @@
package com.baiyue.order.repository;
import com.baiyue.order.mapper.InboundCodeSequenceMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class InboundCodeSequenceRepositoryTest {
@Mock private InboundCodeSequenceMapper mapper;
@Test
void incrementsAndReadsTheCounterForTheSameShanghaiBusinessDate() {
LocalDate businessDate = LocalDate.of(2026, 8, 14);
OrderRepository repository = new OrderRepository();
ReflectionTestUtils.setField(repository, "inboundCodeSequenceMapper", mapper);
when(mapper.increment(businessDate)).thenReturn(2);
when(mapper.currentValue(businessDate)).thenReturn(2L);
assertThat(repository.nextInboundCodeSequence(businessDate)).isEqualTo(2L);
InOrder ordered = inOrder(mapper);
ordered.verify(mapper).increment(businessDate);
ordered.verify(mapper).currentValue(businessDate);
}
}
@@ -0,0 +1,59 @@
package com.baiyue.order.repository;
import com.baiyue.order.mapper.InboundCodeSequenceMapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import static org.assertj.core.api.Assertions.assertThat;
class InboundCodeSequenceSqlContractTest {
@Test
void mapperAtomicallyIncrementsOneCounterPerBusinessDate() throws Exception {
Method increment = InboundCodeSequenceMapper.class.getMethod("increment", LocalDate.class);
String incrementSql = normalize(String.join(" ", increment.getAnnotation(Insert.class).value()));
Method current = InboundCodeSequenceMapper.class.getMethod("currentValue", LocalDate.class);
String currentSql = normalize(String.join(" ", current.getAnnotation(Select.class).value()));
assertThat(incrementSql)
.contains("business_date, current_value")
.contains("values (#{businessdate}, 1")
.contains("on duplicate key update")
.contains("current_value = current_value + 1");
assertThat(currentSql)
.contains("where business_date = #{businessdate}")
.doesNotContain("channel_code")
.doesNotContain("tenant_id");
}
@Test
void baselineAndUnifiedSqlDefineTheShanghaiDailyCounter() throws Exception {
String baseline = new ClassPathResource("db/order.sql").getContentAsString(StandardCharsets.UTF_8);
String unified = Files.readString(resolveUnified(), StandardCharsets.UTF_8);
for (String sql : java.util.List.of(baseline, unified)) {
assertThat(sql)
.contains("CREATE TABLE IF NOT EXISTS ord_inbound_code_daily_sequence")
.contains("business_date DATE NOT NULL")
.contains("current_value BIGINT UNSIGNED NOT NULL")
.contains("PRIMARY KEY (business_date)")
.contains("按上海日期重新从1开始");
}
}
private static String normalize(String sql) {
return sql.replaceAll("\\s+", " ").trim().toLowerCase();
}
private static Path resolveUnified() {
Path direct = Path.of("deploy/sql/头程数据库统一增量-MySQL8.sql");
return Files.exists(direct) ? direct : Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql");
}
}
@@ -3,6 +3,7 @@ package com.baiyue.order.repository;
import com.baiyue.order.dto.MerchantInboundOrderQuery;
import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.mapper.InboundCodeSequenceMapper;
import com.baiyue.order.mapper.OrderCargoMapper;
import com.baiyue.order.mapper.OrderChildPurgeMapper;
import com.baiyue.order.mapper.OrderIdempotencyRecordMapper;
@@ -38,6 +39,7 @@ class MerchantInboundOrderQueryRepositoryTest {
@MockBean private OutboxEventMapper outboxMapper;
@MockBean private OrderChildPurgeMapper purgeMapper;
@MockBean private OrderIdempotencyRecordMapper idempotencyMapper;
@MockBean private InboundCodeSequenceMapper inboundCodeSequenceMapper;
@Resource private OrderRepository repository;
@Test
@SuppressWarnings({"rawtypes", "unchecked"})
@@ -2,6 +2,7 @@ package com.baiyue.order.repository;
import com.baiyue.order.dto.OrderRequests;
import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.mapper.InboundCodeSequenceMapper;
import com.baiyue.order.mapper.OrderCargoMapper;
import com.baiyue.order.mapper.OrderChildPurgeMapper;
import com.baiyue.order.mapper.OrderIdempotencyRecordMapper;
@@ -34,6 +35,7 @@ class PlatformOrderPageRepositoryTest {
@MockBean private OutboxEventMapper outboxMapper;
@MockBean private OrderChildPurgeMapper purgeMapper;
@MockBean private OrderIdempotencyRecordMapper idempotencyMapper;
@MockBean private InboundCodeSequenceMapper inboundCodeSequenceMapper;
@Resource private OrderRepository repository;
private final OrderRequests.QueryRequest query = new OrderRequests.QueryRequest(
null, null, null, null, null, null, 1, 20);
@@ -58,7 +58,7 @@ class OrderApplicationServiceStateTest {
assertThat(orderId).isNotNull();
verify(eligibility, never()).requireApproved(anyLong());
verify(repository).insertOrder(argThat(order -> order.getStatus() == OrderStatus.DRAFT
&& order.getInboundCode().matches("PCHWK\\d{6}0897")));
&& order.getInboundCode().matches("PCHWK\\d{6}0001")));
}
@Test
@@ -247,7 +247,7 @@ class OrderApplicationServiceStateTest {
.transit(20002L, 2L, "u", 1L, OrderStatus.SUBMITTED, null);
assertThat(order.getStatus()).isEqualTo(OrderStatus.SUBMITTED);
assertThat(order.getInboundCode()).matches("PCHWK\\d{6}0897");
assertThat(order.getInboundCode()).matches("PCHWK\\d{6}0001");
assertThat(order.getSubmittedAt()).isNotNull();
verify(repository).saveOrder(20002L, order);
}
@@ -490,11 +490,15 @@ class OrderApplicationServiceStateTest {
}
@Test
void inboundCodeUsesChannelPrefixShanghaiDateAndGlobalTicketSequence() {
assertThat(OrderApplicationService.formatInboundCode("ZBYHK", LocalDate.of(2026, 8, 14), 897L))
.isEqualTo("ZBYHK2608140897");
assertThat(OrderApplicationService.formatInboundCode("HDHK", LocalDate.of(2026, 8, 13), 10000L))
.isEqualTo("HDHK26081310000");
void inboundCodeUsesChannelPrefixShanghaiDateAndDailyTicketSequence() {
assertThat(OrderApplicationService.formatInboundCode("PCHK", LocalDate.of(2026, 8, 14), 1L))
.isEqualTo("PCHK2608140001");
assertThat(OrderApplicationService.formatInboundCode("PCHK", LocalDate.of(2026, 8, 14), 2L))
.isEqualTo("PCHK2608140002");
assertThat(OrderApplicationService.formatInboundCode("PCHK", LocalDate.of(2026, 8, 15), 1L))
.isEqualTo("PCHK2608150001");
assertThat(OrderApplicationService.formatInboundCode("HDHK", LocalDate.of(2026, 8, 14), 10000L))
.isEqualTo("HDHK26081410000");
}
private OrderApplicationService service(OrderRepository repository, OrderEligibilityGuard eligibility,
@@ -505,7 +509,7 @@ class OrderApplicationServiceStateTest {
ReflectionTestUtils.setField(service, "masterDataGuard", masterData);
OrderPricingChannelGuard pricing = mock(OrderPricingChannelGuard.class);
when(pricing.requireAvailable(anyLong(), anyLong(), anyString(), anyString())).thenReturn("PCHWK");
when(repository.nextInboundCodeSequence()).thenReturn(897L);
when(repository.nextInboundCodeSequence(any(LocalDate.class))).thenReturn(1L);
ReflectionTestUtils.setField(service, "pricingChannelGuard", pricing);
ReflectionTestUtils.setField(service, "productImageGuard", mock(ProductImageGuard.class));
ReflectionTestUtils.setField(service, "json", new ObjectMapper());