This commit is contained in:
2026-08-25 15:08:34 +08:00
parent e815abcbd7
commit 44198866d9
64 changed files with 1382 additions and 360 deletions
+4
View File
@@ -10,6 +10,7 @@
| 目标 | 入口 |
|---|---|
| 内部模块化基线与依赖分析 | `docs/firstleg-internal-modularization-baseline.md`、`scripts/analyze-firstleg-dependencies.mjs`、`scripts/analyze-firstleg-dependencies.test.mjs`、`src/test/resources/architecture/firstleg-slice-dependencies.json`;只约束本服务内部包依赖,不改变部署单元或数据库边界 |
| 履约任务/分配 | `controller/PlatformFulfillmentAssignmentController.java`、`service/FulfillmentTaskService.java` |
| 入库单查询/聚合 | `controller/PlatformInboundOrderController.java`、`service/InboundOrderQueryService.java` |
| 平台测量/OCR | `controller/PlatformInboundMeasurementController.java`、`service/InboundMeasurementService.java`、`service/CartonMeasurementService.java`;手工单箱测量也从该 Controller 与 `CartonMeasurementService` 进入 |
@@ -22,6 +23,7 @@
| 客户费用查询 | `controller/MerchantInboundBillingController.java`、`service/MerchantInboundBillingService.java` |
| 客户运营统计/在途柜量 | `customerstatistics/controller/PlatformCustomerTransitStatisticsController.java`、`customerstatistics/service/CustomerTransitStatisticsService.java`;契约见 `src/main/resources/openapi/customer-statistics-v1.yaml` |
| 费用项目与价格簿 | `controller/PlatformFeeItemController.java`、`PlatformPriceBookController.java`、`MerchantPriceBookController.java` |
| 价格簿查询内部切片 | `pricebookquery/application/port/in/PriceBookQueryUseCase.java`、`application/PriceBookQueryService.java`、`application/port/out/PriceBookQueryReader.java`、`adapter/out/persistence/PriceBookQueryMapper.java`;查询行模型位于 `application/model/PriceBookQueryRows.java` |
| 财务费用来源快照 | `controller/InternalFinanceFeeSourceController.java`;入库费用与柜次费用分别按来源 ID、版本向 Finance 提供不可变快照 |
| 柜次费用明细 | `controller/PlatformContainerFeeDetailController.java`、`service/ContainerFeeDetailService.java`、`dto/ContainerFeeDetailDtos.java`;Admin 消费端见 `../../apps/admin-web/src/modules/loading-lists/api/containerFees.ts` |
| 财务整柜成本分摊快照 | `loading/controller/InternalFinanceCostSnapshotController.java`、`loading/service/FinanceCostSnapshotService.java` |
@@ -44,7 +46,9 @@
## 修改约束
- 移动包、拆分端口/适配器或引入新的跨切片依赖时,先核对内部模块化基线并运行 `node --test scripts/analyze-firstleg-dependencies.test.mjs`;确需更新依赖基线时必须说明稳定边界变化,不能把临时循环依赖登记为长期豁免。
- 平台接口必须校验目标客户、权限、乐观锁、幂等键、历史和审计;商户查询只能使用登录态客户。
- 价格簿查询消费者依赖 `PriceBookQueryUseCase` 或稳定查询端口,不得反向依赖持久化 Mapper;移动旧查询类时同步更新生产调用、测试、Mock 与 import,避免同一职责在根 `repository/mapper` 和 `pricebookquery/` 双写。
- 第三方回传必须签名、时间窗、防重放、请求哈希,并绑定订单货物和 File 证据;不得信任外部客户字段。
- 稳定箱码映射以入库快照和 Order 货物明细为事实;订单审核创建入库单时必须幂等生成,打印接口只复用或补齐同一映射,用户是否下载箱唛不得决定箱码有效性。手工测量、墨家设备和 PDA 扫码统一通过 `InboundCartonLabelService` 校验客户、入库单、货物明细、总箱数/下标和有效批次,不得在测量时临时伪造箱码或接受跨客户、跨入库箱码。
- 测量审核通过后直接进入 `WAIT_SPLIT`,费用仅作为独立初算和人工维护结果,不得阻塞或回退订单主流程;计费量、单价和附加费调整必须版本化并由服务端重算金额。状态或 DTO 变更时逐个检查 Order/Warehouse/File/Integration 消费者。
@@ -1,4 +1,4 @@
# Firstleg 内部模块化基线与首切片方案
# Firstleg 内部模块化基线与切片实施记录
## 1. 范围与生成方式
@@ -198,3 +198,53 @@ mvn -pl firstleg -am -Dtest=CustomerTransitStatisticsServiceTest,CustomerTransit
- 根 Mapper 扫描只把旧精确包替换为新精确包,没有扩大扫描范围或增加兼容 Bean。
- 验证结果:Node 合同 3/3;`test-compile` 成功;客户统计加根扫描定向测试 7/7;`mvn -pl firstleg -am test` 共 854 个测试,0 失败、0 错误、3 跳过,构建成功。
- 首次并发执行 `test-compile` 时,其他专项同时写入共享 `target/classes` 导致若干无关 class 文件瞬时缺失;源符号核对正常,独立重跑 `test-compile` 与完整测试均成功。
## 12. 第二切片:价格簿只读查询
### 12.1 选择证据
第二切片选择 `pricebook-query`,而没有选择报关、拖车、出仓或船节点:
- 原实现只有 `PriceBookQueryRows`、`PriceBookQueryRepository`、`PriceBookQueryMapper` 三个核心文件,全部为只读查询,不承载 Controller、权限或状态机。
- `PriceBookQueryMapper -> repository.PriceBookQueryRows` 是主源码唯一 `mapper -> repository` 反向依赖,也是物理包 SCC `mapper <-> repository` 的唯一成因。
- 报关和拖车均涉及 MultipartFile、Feign、装柜或出仓门禁;出仓和船节点属于当前业务热点,风险高于只读价格簿查询。
- 本切片不触碰 `LoadingPickService`、客户尺寸初始化、IAM 解环、POM、CI、AGENTS、SQL 或部署配置。
### 12.2 目标结构与依赖方向
```text
pricebookquery/
application/
PriceBookQueryService.java
model/PriceBookQueryRows.java
port/in/PriceBookQueryUseCase.java
port/out/PriceBookQueryReader.java
adapter/out/persistence/
PriceBookQueryMapper.java
```
依赖由“多个业务 Service -> 全局 Repository -> 全局 Mapper -> Repository Rows”改为:
```text
业务消费者 -> PriceBookQueryUseCase
PriceBookQueryService -> PriceBookQueryReader
PriceBookQueryMapper -> PriceBookQueryReader + PriceBookQueryRows
```
消费者只能访问 In Port 和只读模型;application 不依赖 Spring MVC、MyBatis、MyBatis-Plus 或其他 Firstleg 切片。Mapper 的七段 `@Select` SQL 文本和方法参数保持不变。
### 12.3 依赖图变化
- 主源码由 430 类/949 条内部依赖/67 包变为 432 类/953 条内部依赖/72 包;新增项为 In Port、Out Port 和 Application Service,原 Repository 被删除。
- 物理包多节点 SCC 从 1 个降为 0 个。
- `pricebook-query` 是零出向边的叶子,不进入逻辑 SCC;显式入向仅为订舱、测量计价、分柜装柜三个消费者切片。
- 原 `split-loading -> measurement-pricing` 两条价格簿引用被替换为显式 `split-loading -> pricebook-query`;Node 合同没有简单放宽旧边。
- 既有 88 条跨切片 Entity/DTO 泄漏和 31 条 Mapper 泄漏均未增加。
### 12.4 棘轮与回滚边界
- Node 合同锁定五个目标类位置、application 依赖禁令、零出向/三类入向和消费者只能访问 In Port/模型,并断言物理包 SCC 为空。
- `PriceBookQueryArchitectureTest` 用 ArchUnit 执行同等边界,持久化 Mapper 必须实现 Out Port。
- 回滚仅恢复原三文件及消费者类型引用;不涉及 URI、权限、JSON、SQL、表结构、业务状态或环境配置。
- 只读模型仍保留原 Lombok 可变 Bean 形态以维持 MyBatis 映射行为;本轮不顺带改造成不可变领域对象。
- 验证结果:七段 `@Select` SQL 文本逐块完全一致;Node 合同 5/5;`test-compile` 成功;价格簿、ArchUnit、根扫描及三个消费者切片定向测试 78/78;Firstleg 聚合全量 878 个测试,0 失败、0 错误、3 跳过,构建成功。
@@ -13,6 +13,7 @@ const internalPrefix = 'com.baiyue.firstleg.';
const sliceRules = [
['customer-statistics', /(?:^|\.)(?:customerstatistics)(?:\.|$)/],
['pricebook-query', /(?:^|\.)(?:pricebookquery)(?:\.|$)/],
['finance-integration', /(?:InternalFinance|FinanceFeeSource|FinanceCostSnapshot|FeePaymentVoucher|FeePaymentSnapshot|ContainerFeeSourceSnapshot)/],
['shipping-nodes', /(?:CarrierScheduleQuery|CarrierScheduleNode|CarrierScheduleAdapter|CarrierScheduleAttempt|CarrierScheduleException)/],
['booking-customs-trucking', /(?:^|\.)(?:oceanbooking|customs)(?:\.|$)|(?:TruckAppointment|WarehouseSupplier|PortSchedule|DispatchGate)/],
@@ -273,8 +274,9 @@ function checkContract(report, contractPath) {
if (!actualClass) violations.push(`required class missing: ${fqcn}`);
else if (actualClass.file !== expectedFile) violations.push(`class location changed: ${fqcn} (${actualClass.file})`);
}
const applicationRule = contract.applicationDependencyRule;
if (applicationRule) {
const applicationRules = contract.applicationDependencyRules
?? (contract.applicationDependencyRule ? [contract.applicationDependencyRule] : []);
for (const applicationRule of applicationRules) {
const applicationClasses = report.classes.filter(entry => entry.packageName.startsWith(applicationRule.packagePrefix));
for (const entry of applicationClasses) {
for (const imported of entry.imports) {
@@ -291,6 +293,15 @@ function checkContract(report, contractPath) {
}
}
}
for (const exposureRule of contract.sliceExposureRules ?? []) {
for (const edge of report.classDependencies) {
if (!edge.source.startsWith(exposureRule.slicePrefix)
&& edge.target.startsWith(exposureRule.slicePrefix)
&& !(exposureRule.allowedTargetPrefixes ?? []).some(prefix => edge.target.startsWith(prefix))) {
violations.push(`forbidden slice implementation dependency: ${edge.source} -> ${edge.target}`);
}
}
}
for (const [slice, rule] of Object.entries(contract.sliceSpecificRules ?? {})) {
const outgoing = report.sliceEdges.filter(edge => edge.source === slice).map(edge => edge.target).sort();
const incoming = report.sliceEdges.filter(edge => edge.target === slice).map(edge => edge.source).sort();
@@ -52,3 +52,43 @@ test('customer statistics application layer only depends inward', () => {
&& edge.target.startsWith('com.baiyue.firstleg.') && !edge.target.startsWith(prefix));
assert.deepEqual(outwardDependencies, []);
});
test('price book query is a leaf slice and removes the package cycle', () => {
const report = analyze();
const classes = report.classes.filter(entry => entry.slice === 'pricebook-query');
assert.deepEqual(classes.map(entry => entry.fqcn).sort(), [
'com.baiyue.firstleg.pricebookquery.adapter.out.persistence.PriceBookQueryMapper',
'com.baiyue.firstleg.pricebookquery.application.PriceBookQueryService',
'com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows',
'com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase',
'com.baiyue.firstleg.pricebookquery.application.port.out.PriceBookQueryReader',
]);
assert.deepEqual(report.sliceEdges.filter(edge => edge.source === 'pricebook-query'), []);
assert.deepEqual(report.sliceEdges.filter(edge => edge.target === 'pricebook-query')
.map(edge => edge.source).sort(), ['booking-customs-trucking', 'measurement-pricing', 'split-loading']);
assert.equal(report.sliceSccs.some(component => component.includes('pricebook-query')), false);
assert.deepEqual(report.packageSccs, []);
});
test('price book query application and consumers respect ports', () => {
const report = analyze();
const prefix = 'com.baiyue.firstleg.pricebookquery.application';
const applicationClasses = report.classes.filter(entry => entry.packageName.startsWith(prefix));
assert.equal(applicationClasses.length, 4);
for (const entry of applicationClasses) {
assert.equal(entry.imports.some(imported => imported.startsWith('org.springframework.web')), false);
assert.equal(entry.imports.some(imported => imported.startsWith('org.apache.ibatis')), false);
assert.equal(entry.imports.some(imported => imported.startsWith('com.baomidou.mybatisplus')), false);
}
const outwardDependencies = report.classDependencies.filter(edge => edge.source.startsWith(prefix)
&& edge.target.startsWith('com.baiyue.firstleg.') && !edge.target.startsWith(prefix));
assert.deepEqual(outwardDependencies, []);
const allowedTargets = [
'com.baiyue.firstleg.pricebookquery.application.model.',
'com.baiyue.firstleg.pricebookquery.application.port.in.',
];
const implementationLeaks = report.classDependencies.filter(edge => !edge.source.startsWith('com.baiyue.firstleg.pricebookquery.')
&& edge.target.startsWith('com.baiyue.firstleg.pricebookquery.')
&& !allowedTargets.some(prefixValue => edge.target.startsWith(prefixValue)));
assert.deepEqual(implementationLeaks, []);
});
@@ -18,7 +18,8 @@ import org.springframework.scheduling.annotation.EnableScheduling;
"com.baiyue.firstleg.outbound.dispatch.mapper",
"com.baiyue.firstleg.portschedule.mapper",
"com.baiyue.firstleg.customs.mapper",
"com.baiyue.firstleg.customerstatistics.adapter.out.persistence"
"com.baiyue.firstleg.customerstatistics.adapter.out.persistence",
"com.baiyue.firstleg.pricebookquery.adapter.out.persistence"
})
@EnableFeignClients(basePackages = "com.baiyue.firstleg")
@EnableScheduling
@@ -11,9 +11,9 @@ import com.baiyue.firstleg.domain.ContainerSpecificationCatalog;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.*;
import com.baiyue.firstleg.oceanbooking.entity.*;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
import com.baiyue.firstleg.service.WarehouseSupplierSnapshotService;
import com.baiyue.firstleg.service.FirstlegDocumentNumberGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -39,7 +39,7 @@ public class OceanBookingService {
private static final Set<OceanBookingStatus> OPERATIONAL_LIST_STATUSES = Set.of(
OceanBookingStatus.DRAFT, OceanBookingStatus.RELEASED);
@Resource private OceanBookingRepository repository;
@Resource private PriceBookQueryRepository priceBooks;
@Resource private PriceBookQueryUseCase priceBooks;
@Resource private ObjectMapper objectMapper;
@Resource private WarehouseSupplierSnapshotService suppliers;
@Resource private WarehouseMasterDataClient warehouses;
@@ -1,9 +1,10 @@
package com.baiyue.firstleg.mapper;
package com.baiyue.firstleg.pricebookquery.adapter.out.persistence;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelPrefixRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelPrefixRow;
import com.baiyue.firstleg.pricebookquery.application.port.out.PriceBookQueryReader;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@@ -12,7 +13,7 @@ import java.util.List;
/** Dedicated read model for merchant channel selection and reference estimates. */
@Mapper
public interface PriceBookQueryMapper {
public interface PriceBookQueryMapper extends PriceBookQueryReader {
@Select("""
SELECT channel_code AS channelCode, inbound_code_prefix AS inboundCodePrefix,
overseas_inbound_code_prefix AS overseasInboundCodePrefix
@@ -0,0 +1,45 @@
package com.baiyue.firstleg.pricebookquery.application;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelPrefixRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
import com.baiyue.firstleg.pricebookquery.application.port.out.PriceBookQueryReader;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class PriceBookQueryService implements PriceBookQueryUseCase {
@Resource private PriceBookQueryReader reader;
@Override public Long activeVersionId() { return reader.findActiveVersionId(); }
@Override public List<ChannelRow> channels(long versionId, long originWarehouseId) {
return reader.findChannels(versionId, originWarehouseId);
}
@Override public List<ChannelTaxModeRow> channelTaxModes(long versionId, long originWarehouseId) {
return reader.findChannelTaxModes(versionId, originWarehouseId);
}
@Override public List<RateRow> fbaRates(long versionId, long originWarehouseId, String channelCode,
String destinationCode, String pricingTaxMode) {
return reader.findFbaRates(versionId, originWarehouseId, channelCode, destinationCode, pricingTaxMode);
}
@Override public List<RateRow> overseasRates(long versionId, long originWarehouseId, String channelCode,
long destinationWarehouseId, String pricingTaxMode) {
return reader.findOverseasRates(versionId, originWarehouseId, channelCode, destinationWarehouseId,
pricingTaxMode);
}
@Override public List<RateRow> regionTiers(long versionId, long originWarehouseId, String channelCode,
String regionCode, String pricingTaxMode) {
return reader.findRegionTiers(versionId, originWarehouseId, channelCode, regionCode, pricingTaxMode);
}
@Override public boolean channelAvailable(long versionId, long originWarehouseId, String channelCode) {
return channels(versionId, originWarehouseId).stream()
.anyMatch(channel -> channelCode.equals(channel.getChannelCode()));
}
@Override public ChannelPrefixRow channelPrefix(String channelCode) {
return reader.findChannelPrefix(channelCode);
}
}
@@ -1,4 +1,4 @@
package com.baiyue.firstleg.repository;
package com.baiyue.firstleg.pricebookquery.application.model;
import lombok.Getter;
import lombok.Setter;
@@ -0,0 +1,22 @@
package com.baiyue.firstleg.pricebookquery.application.port.in;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelPrefixRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import java.util.List;
public interface PriceBookQueryUseCase {
Long activeVersionId();
List<ChannelRow> channels(long versionId, long originWarehouseId);
List<ChannelTaxModeRow> channelTaxModes(long versionId, long originWarehouseId);
List<RateRow> fbaRates(long versionId, long originWarehouseId, String channelCode,
String destinationCode, String pricingTaxMode);
List<RateRow> overseasRates(long versionId, long originWarehouseId, String channelCode,
long destinationWarehouseId, String pricingTaxMode);
List<RateRow> regionTiers(long versionId, long originWarehouseId, String channelCode,
String regionCode, String pricingTaxMode);
boolean channelAvailable(long versionId, long originWarehouseId, String channelCode);
ChannelPrefixRow channelPrefix(String channelCode);
}
@@ -0,0 +1,21 @@
package com.baiyue.firstleg.pricebookquery.application.port.out;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelPrefixRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import java.util.List;
public interface PriceBookQueryReader {
Long findActiveVersionId();
List<ChannelRow> findChannels(long versionId, long originWarehouseId);
List<ChannelTaxModeRow> findChannelTaxModes(long versionId, long originWarehouseId);
List<RateRow> findFbaRates(long versionId, long originWarehouseId, String channelCode,
String destinationCode, String pricingTaxMode);
List<RateRow> findOverseasRates(long versionId, long originWarehouseId, String channelCode,
long destinationWarehouseId, String pricingTaxMode);
List<RateRow> findRegionTiers(long versionId, long originWarehouseId, String channelCode,
String regionCode, String pricingTaxMode);
ChannelPrefixRow findChannelPrefix(String channelCode);
}
@@ -7,9 +7,9 @@ import com.baiyue.firstleg.domain.PriceRegion;
import com.baiyue.firstleg.domain.PricingDestinationType;
import com.baiyue.firstleg.domain.PricingTaxMode;
import com.baiyue.firstleg.dto.PriceBookDtos.EstimateRequest;
import com.baiyue.firstleg.repository.PriceBookQueryRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@@ -21,7 +21,7 @@ import java.util.Locale;
/** Single active-price-book lookup used by merchant estimates and inbound automatic billing. */
@Component
public class ActivePriceBookEstimator {
@Resource private PriceBookQueryRepository repository;
@Resource private PriceBookQueryUseCase repository;
@Resource private FbaWarehouseRateEstimator fbaEstimator;
@Resource private RegionWeightTierEstimator regionEstimator;
@Resource private PriceBookWarehouseRegionResolver warehouseRegionResolver;
@@ -2,7 +2,7 @@ package com.baiyue.firstleg.pricing;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
@@ -2,7 +2,7 @@ package com.baiyue.firstleg.pricing;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
@@ -1,53 +0,0 @@
package com.baiyue.firstleg.repository;
import com.baiyue.firstleg.mapper.PriceBookQueryMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class PriceBookQueryRepository {
@Resource
private PriceBookQueryMapper mapper;
public Long activeVersionId() {
return mapper.findActiveVersionId();
}
public List<PriceBookQueryRows.ChannelRow> channels(long versionId, long originWarehouseId) {
return mapper.findChannels(versionId, originWarehouseId);
}
public List<PriceBookQueryRows.ChannelTaxModeRow> channelTaxModes(long versionId, long originWarehouseId) {
return mapper.findChannelTaxModes(versionId, originWarehouseId);
}
public List<PriceBookQueryRows.RateRow> fbaRates(long versionId, long originWarehouseId,
String channelCode, String destinationCode,
String pricingTaxMode) {
return mapper.findFbaRates(versionId, originWarehouseId, channelCode, destinationCode, pricingTaxMode);
}
public List<PriceBookQueryRows.RateRow> overseasRates(long versionId, long originWarehouseId,
String channelCode, long destinationWarehouseId,
String pricingTaxMode) {
return mapper.findOverseasRates(versionId, originWarehouseId, channelCode, destinationWarehouseId,
pricingTaxMode);
}
public List<PriceBookQueryRows.RateRow> regionTiers(long versionId, long originWarehouseId,
String channelCode, String regionCode,
String pricingTaxMode) {
return mapper.findRegionTiers(versionId, originWarehouseId, channelCode, regionCode, pricingTaxMode);
}
public boolean channelAvailable(long versionId, long originWarehouseId, String channelCode) {
return channels(versionId, originWarehouseId).stream()
.anyMatch(channel -> channelCode.equals(channel.getChannelCode()));
}
public PriceBookQueryRows.ChannelPrefixRow channelPrefix(String channelCode) {
return mapper.findChannelPrefix(channelCode);
}
}
@@ -22,8 +22,8 @@ import com.baiyue.firstleg.mapper.ContainerTransportHistoryMapper;
import com.baiyue.firstleg.repository.ContainerRepository;
import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity;
@@ -44,7 +44,7 @@ public class ContainerService {
@Resource private SmartLoadingRepository smartLoadingRepository;
@Resource private LoadingPickRepository loadingPickRepository;
@Resource private LoadingPickService loadingPickService;
@Resource private PriceBookQueryRepository priceBooks;
@Resource private PriceBookQueryUseCase priceBooks;
@Resource private OceanBookingRepository oceanBookings;
@Resource private WarehouseSupplierSnapshotService suppliers;
@Resource private FirstlegDocumentNumberGenerator documentNumbers;
@@ -14,7 +14,7 @@ import com.baiyue.firstleg.entity.InboundFeeItemEntity;
import com.baiyue.firstleg.entity.InboundPricingEntity;
import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
import com.baiyue.firstleg.pricing.ActivePriceBookEstimator;
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -9,9 +9,9 @@ import com.baiyue.firstleg.dto.PriceBookDtos.EstimateRequest;
import com.baiyue.firstleg.dto.PriceBookDtos.EstimateResponse;
import com.baiyue.firstleg.dto.PriceBookChannelValidation.Response;
import com.baiyue.firstleg.pricing.ActivePriceBookEstimator;
import com.baiyue.firstleg.repository.PriceBookQueryRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@@ -26,7 +26,7 @@ import java.util.Map;
public class MerchantPriceBookService {
public static final String DISCLAIMER = "仅供参考,不含附加费,最终以实际结算为准";
@Resource private PriceBookQueryRepository repository;
@Resource private PriceBookQueryUseCase repository;
@Resource private ActivePriceBookEstimator estimator;
public List<CurrentChannel> channels(long merchantTenantId, long originWarehouseId) {
@@ -21,6 +21,7 @@ class BaiyueFirstlegApplicationContractTest {
.contains("com.baiyue.firstleg.portschedule.mapper")
.contains("com.baiyue.firstleg.customs.mapper")
.contains("com.baiyue.firstleg.customerstatistics.adapter.out.persistence")
.contains("com.baiyue.firstleg.pricebookquery.adapter.out.persistence")
.doesNotContain("com.baiyue.firstleg.customerstatistics.mapper");
}
@@ -6,7 +6,7 @@ import com.baiyue.firstleg.domain.PriceModel;
import com.baiyue.firstleg.domain.PricingTaxMode;
import com.baiyue.firstleg.domain.PricingDestinationType;
import com.baiyue.firstleg.dto.PriceBookDtos;
import com.baiyue.firstleg.mapper.PriceBookQueryMapper;
import com.baiyue.firstleg.pricebookquery.adapter.out.persistence.PriceBookQueryMapper;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -9,8 +9,8 @@ import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementE
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingOperationEntity;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
import com.baiyue.firstleg.service.WarehouseSupplierSnapshotService;
import com.baiyue.firstleg.service.FirstlegDocumentNumberGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -33,7 +33,7 @@ import static org.mockito.Mockito.*;
class OceanBookingServiceTest {
@Mock private OceanBookingRepository repository;
@Mock private PriceBookQueryRepository priceBooks;
@Mock private PriceBookQueryUseCase priceBooks;
@Mock private WarehouseSupplierSnapshotService suppliers;
@Mock private WarehouseMasterDataClient warehouses;
@Mock private FirstlegDocumentNumberGenerator documentNumbers;
@@ -0,0 +1,67 @@
package com.baiyue.firstleg.pricebookquery;
import com.baiyue.firstleg.pricebookquery.application.port.out.PriceBookQueryReader;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideOutsideOfPackage;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
class PriceBookQueryArchitectureTest {
private static final String FIRSTLEG = "com.baiyue.firstleg..";
private static final String SLICE = "com.baiyue.firstleg.pricebookquery..";
private static final String APPLICATION = "com.baiyue.firstleg.pricebookquery.application..";
private static final String INPUT_PORT = "com.baiyue.firstleg.pricebookquery.application.port.in..";
private static final String OUTPUT_PORT = "com.baiyue.firstleg.pricebookquery.application.port.out..";
private static final String MODEL = "com.baiyue.firstleg.pricebookquery.application.model..";
private static final String PERSISTENCE = "com.baiyue.firstleg.pricebookquery.adapter.out.persistence..";
private static final JavaClasses PRODUCTION_CLASSES = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
.importPackages("com.baiyue.firstleg");
@Test
void applicationStaysFreeOfWebAndPersistenceFrameworks() {
noClasses().that().resideInAPackage(APPLICATION)
.should().dependOnClassesThat().resideInAnyPackage(
"org.springframework.web..",
"org.apache.ibatis..",
"com.baomidou.mybatisplus..")
.check(PRODUCTION_CLASSES);
}
@Test
void applicationOnlyDependsOnItsOwnSliceWithinFirstleg() {
classes().that().resideInAPackage(APPLICATION)
.should().onlyDependOnClassesThat(
resideOutsideOfPackage(FIRSTLEG).or(resideInAPackage(APPLICATION)))
.check(PRODUCTION_CLASSES);
}
@Test
void persistenceAdapterCollaboratesThroughTheOutputPortAndModel() {
classes().that().resideInAPackage(PERSISTENCE)
.should().onlyDependOnClassesThat(
resideOutsideOfPackage(SLICE)
.or(resideInAPackage(PERSISTENCE))
.or(resideInAPackage(OUTPUT_PORT))
.or(resideInAPackage(MODEL)))
.andShould().beAssignableTo(PriceBookQueryReader.class)
.check(PRODUCTION_CLASSES);
}
@Test
void consumersOnlySeeTheInputPortAndReadModel() {
noClasses().that().resideOutsideOfPackage(SLICE)
.should().dependOnClassesThat(
resideInAPackage(SLICE)
.and(resideOutsideOfPackage(INPUT_PORT))
.and(resideOutsideOfPackage(MODEL)))
.check(PRODUCTION_CLASSES);
}
}
@@ -0,0 +1,46 @@
package com.baiyue.firstleg.pricebookquery;
import com.baiyue.firstleg.pricebookquery.application.PriceBookQueryService;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelPrefixRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.port.out.PriceBookQueryReader;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PriceBookQueryServiceTest {
@Mock private PriceBookQueryReader reader;
@InjectMocks private PriceBookQueryService service;
@Test
void delegatesStableReadOperationsToTheOutputPort() {
ChannelPrefixRow prefix = new ChannelPrefixRow("SEA", "IN", "OUT");
when(reader.findActiveVersionId()).thenReturn(9L);
when(reader.findChannelPrefix("SEA")).thenReturn(prefix);
assertThat(service.activeVersionId()).isEqualTo(9L);
assertThat(service.channelPrefix("SEA")).isSameAs(prefix);
verify(reader).findActiveVersionId();
verify(reader).findChannelPrefix("SEA");
}
@Test
void keepsChannelAvailabilityAsApplicationLogic() {
ChannelRow channel = new ChannelRow();
channel.setChannelCode("SEA");
when(reader.findChannels(9L, 3L)).thenReturn(List.of(channel));
assertThat(service.channelAvailable(9L, 3L, "SEA")).isTrue();
assertThat(service.channelAvailable(9L, 3L, "AIR")).isFalse();
verify(reader, org.mockito.Mockito.times(2)).findChannels(9L, 3L);
}
}
@@ -1,7 +1,7 @@
package com.baiyue.firstleg.pricing;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
@@ -1,7 +1,7 @@
package com.baiyue.firstleg.pricing;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
@@ -17,8 +17,8 @@ import com.baiyue.firstleg.mapper.ContainerTransportHistoryMapper;
import com.baiyue.firstleg.repository.ContainerRepository;
import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
@@ -51,7 +51,7 @@ class ContainerServiceTest {
@MockBean private SmartLoadingRepository smartLoadingRepository;
@MockBean private LoadingPickRepository loadingPickRepository;
@MockBean private LoadingPickService loadingPickService;
@MockBean private PriceBookQueryRepository priceBooks;
@MockBean private PriceBookQueryUseCase priceBooks;
@MockBean private OceanBookingRepository oceanBookings;
@MockBean private WarehouseSupplierSnapshotService suppliers;
@MockBean private FirstlegDocumentNumberGenerator documentNumbers;
@@ -4,7 +4,7 @@ import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
import com.baiyue.firstleg.pricing.FbaWarehouseRateEstimator;
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
@@ -11,7 +11,7 @@ import com.baiyue.firstleg.entity.InboundPricingEntity;
import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
import com.baiyue.firstleg.pricing.ActivePriceBookEstimator;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -13,11 +13,11 @@ import com.baiyue.firstleg.pricing.FbaWarehouseRateEstimator;
import com.baiyue.firstleg.pricing.PriceBookWarehouseRegionResolver;
import com.baiyue.firstleg.pricing.PriceRegionResolver;
import com.baiyue.firstleg.pricing.RegionWeightTierEstimator;
import com.baiyue.firstleg.repository.PriceBookQueryRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelPrefixRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelPrefixRow;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
@@ -38,7 +38,7 @@ import static org.mockito.Mockito.when;
PriceBookWarehouseRegionResolver.class})
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class MerchantPriceBookServiceTest {
@MockBean private PriceBookQueryRepository repository;
@MockBean private PriceBookQueryUseCase repository;
@MockBean private PriceBookWarehouseClient warehouses;
@Resource private MerchantPriceBookService service;
@@ -5,6 +5,7 @@
"booking-customs-trucking->inbound",
"booking-customs-trucking->measurement-pricing",
"booking-customs-trucking->outbound",
"booking-customs-trucking->pricebook-query",
"booking-customs-trucking->shared-technical",
"booking-customs-trucking->split-loading",
"customer-statistics->shared-technical",
@@ -19,6 +20,7 @@
"measurement-pricing->booking-customs-trucking",
"measurement-pricing->finance-integration",
"measurement-pricing->inbound",
"measurement-pricing->pricebook-query",
"measurement-pricing->shared-technical",
"outbound->booking-customs-trucking",
"outbound->shared-technical",
@@ -35,8 +37,8 @@
"shipping-nodes->split-loading",
"split-loading->booking-customs-trucking",
"split-loading->inbound",
"split-loading->measurement-pricing",
"split-loading->outbound",
"split-loading->pricebook-query",
"split-loading->shared-technical"
],
"requiredClassLocations": {
@@ -44,21 +46,50 @@
"com.baiyue.firstleg.customerstatistics.adapter.out.persistence.CustomerTransitStatisticsMapper": "src/main/java/com/baiyue/firstleg/customerstatistics/adapter/out/persistence/CustomerTransitStatisticsMapper.java",
"com.baiyue.firstleg.customerstatistics.application.CustomerTransitStatisticsService": "src/main/java/com/baiyue/firstleg/customerstatistics/application/CustomerTransitStatisticsService.java",
"com.baiyue.firstleg.customerstatistics.application.model.CustomerTransitStatisticsDtos": "src/main/java/com/baiyue/firstleg/customerstatistics/application/model/CustomerTransitStatisticsDtos.java",
"com.baiyue.firstleg.customerstatistics.application.port.out.CustomerTransitStatisticsReader": "src/main/java/com/baiyue/firstleg/customerstatistics/application/port/out/CustomerTransitStatisticsReader.java"
},
"applicationDependencyRule": {
"packagePrefix": "com.baiyue.firstleg.customerstatistics.application",
"allowedInternalPrefix": "com.baiyue.firstleg.customerstatistics.application",
"forbiddenImportPrefixes": [
"org.springframework.web",
"org.apache.ibatis",
"com.baomidou.mybatisplus"
]
"com.baiyue.firstleg.customerstatistics.application.port.out.CustomerTransitStatisticsReader": "src/main/java/com/baiyue/firstleg/customerstatistics/application/port/out/CustomerTransitStatisticsReader.java",
"com.baiyue.firstleg.pricebookquery.adapter.out.persistence.PriceBookQueryMapper": "src/main/java/com/baiyue/firstleg/pricebookquery/adapter/out/persistence/PriceBookQueryMapper.java",
"com.baiyue.firstleg.pricebookquery.application.PriceBookQueryService": "src/main/java/com/baiyue/firstleg/pricebookquery/application/PriceBookQueryService.java",
"com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows": "src/main/java/com/baiyue/firstleg/pricebookquery/application/model/PriceBookQueryRows.java",
"com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase": "src/main/java/com/baiyue/firstleg/pricebookquery/application/port/in/PriceBookQueryUseCase.java",
"com.baiyue.firstleg.pricebookquery.application.port.out.PriceBookQueryReader": "src/main/java/com/baiyue/firstleg/pricebookquery/application/port/out/PriceBookQueryReader.java"
},
"applicationDependencyRules": [
{
"packagePrefix": "com.baiyue.firstleg.customerstatistics.application",
"allowedInternalPrefix": "com.baiyue.firstleg.customerstatistics.application",
"forbiddenImportPrefixes": [
"org.springframework.web",
"org.apache.ibatis",
"com.baomidou.mybatisplus"
]
},
{
"packagePrefix": "com.baiyue.firstleg.pricebookquery.application",
"allowedInternalPrefix": "com.baiyue.firstleg.pricebookquery.application",
"forbiddenImportPrefixes": [
"org.springframework.web",
"org.apache.ibatis",
"com.baomidou.mybatisplus"
]
}
],
"sliceExposureRules": [
{
"slicePrefix": "com.baiyue.firstleg.pricebookquery.",
"allowedTargetPrefixes": [
"com.baiyue.firstleg.pricebookquery.application.model.",
"com.baiyue.firstleg.pricebookquery.application.port.in."
]
}
],
"sliceSpecificRules": {
"customer-statistics": {
"allowedOutgoing": ["shared-technical"],
"allowedIncoming": []
},
"pricebook-query": {
"allowedOutgoing": [],
"allowedIncoming": ["booking-customs-trucking", "measurement-pricing", "split-loading"]
}
}
}