1. 为什么CPS/SPS系统需要特别关注包结构设计
在电商、供应链和金融支付领域,CPS(Commission Per Sale)和SPS(Subscription Per Sale)系统作为典型的高复杂度业务系统,其代码组织方式直接影响着团队的协作效率和系统的可维护性。这类系统通常具有以下特征:
- 多业务域交叉:涉及订单、结算、分佣、会员等多个强关联领域
- 频繁的计费规则变更:促销策略、分账规则等业务逻辑迭代速度快
- 分布式事务密集:需要处理跨服务的资金操作和状态同步
- 报表复杂度高:需要支持多维度实时统计和历史数据分析
我经历过的一个跨境电商分佣系统重构案例中,原始的单模块工程随着业务扩张逐渐演变成"大泥球"架构,出现了这些典型问题:
- 核心业务逻辑分散在15个不同层级的包中
- 同类型组件(如DTO)存在3种不同命名规范
- 新成员需要2周时间才能定位基础功能的代码位置
- 修改佣金计算规则时引发连锁编译错误
通过采用合理的多模块包结构设计,我们最终实现了:
- 编译时间减少40%
- 功能定位效率提升60%
- 跨团队协作冲突降低75%
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多模块工程的基础划分原则
2.1 按业务能力垂直切割
对于CPS/SPS系统,推荐采用"业务域+技术层次"的双维度划分方式。以下是一个电商分佣系统的典型模块划分示例:
code复制cps-parent
├── cps-order-core // 订单基础能力
├── cps-commission-core // 佣金计算引擎
├── cps-settlement-core // 结算处理中心
├── cps-report-core // 报表服务
├── cps-api // 对外接口聚合
├── cps-batch // 定时任务
└── cps-web // Web入口
每个核心业务模块内部,建议保持完整的三层架构:
code复制cps-commission-core
├── src/main/java
│ ├── com.company.cps.commission
│ │ ├── application // 应用服务层
│ │ ├── domain // 领域模型层
│ │ ├── infrastructure // 基础设施层
│ │ └── interfaces // 内部接口定义
关键经验:模块划分的粒度应该保持"单一业务变更通常只影响1-2个模块"的原则。过细的划分会导致模块间依赖复杂化。
2.2 依赖关系的管控策略
在多模块工程中,依赖管理不当会导致循环引用等问题。建议采用以下约束:
-
严格限定模块依赖方向:
- Web层 → API层 → 业务模块
- 业务模块之间通过接口模块通信
- 基础设施实现下沉到底层
-
在父pom中定义dependencyManagement统一管理版本:
xml复制<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.company.cps</groupId>
<artifactId>cps-commission-api</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
- 使用maven-enforcer-plugin防止非法依赖:
xml复制<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>enforce-dependencies</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<bannedDependencies>
<excludes>
<exclude>cps-web:cps-commission-core</exclude>
</excludes>
</bannedDependencies>
</rules>
</configuration>
</execution>
</executions>
</plugin>
3. 包结构的实战设计模式
3.1 领域驱动设计的包布局
对于核心业务模块,推荐采用DDD风格的包结构设计。以佣金计算模块为例:
code复制com.company.cps.commission
├── domain
│ ├── model
│ │ ├── CommissionRule.java // 佣金规则聚合根
│ │ ├── Distributor.java // 分销商实体
│ │ └── valueobject // 值对象包
│ │ └── TieredRate.java // 阶梯费率值对象
│ ├── service // 领域服务
│ │ └── RuleCalculationService.java
│ └── repository // 仓储接口
│ └── CommissionRuleRepository.java
├── application
│ ├── command // CQRS模式命令
│ │ ├── CalculateCommissionCommand.java
│ │ └── AdjustCommissionCommand.java
│ ├── service // 应用服务
│ │ └── CommissionAppService.java
│ └── event // 领域事件
│ └── CommissionCalculatedEvent.java
└── infrastructure
├── persistence // 持久化实现
│ ├── jpa // JPA实现
│ │ └── JpaCommissionRuleRepository.java
│ └── mybatis // MyBatis实现
├── client // 外部服务调用
│ └── OrderServiceClient.java
└── config // 模块配置
└── CommissionConfig.java
这种结构的优势在于:
- 业务语义明确,包名直接反映领域概念
- 技术实现细节被隔离在infrastructure层
- 适合复杂业务规则的持续演进
3.2 公共组件的特殊处理
对于多模块共享的公共组件,建议采用独立模块+分包的方式:
- 创建common模块专门存放通用组件:
code复制cps-common
├── src/main/java
│ ├── com.company.cps.common
│ │ ├── exception // 异常体系
│ │ ├── util // 工具类
│ │ ├── constants // 全局常量
│ │ └── domain // 基础领域对象
- 在业务模块中建立adapter包隔离公共组件使用:
code复制cps-commission-core
└── src/main/java
└── com.company.cps.commission
└── infrastructure
├── adapter
│ ├── CommonDateAdapter.java // 日期工具适配
│ └── ExceptionTranslator.java // 异常转换
避坑提示:避免在common模块中引入业务相关代码,否则会导致模块间隐性耦合。我曾经遇到过一个项目因为把分佣计算工具类放在common中,导致所有模块被迫升级的困境。
4. 代码组织的进阶技巧
4.1 应对业务扩展的包结构设计
当系统需要支持多业务线时,可以采用"业务线+领域"的混合包结构。以下是一个支持跨境电商和本地生活的SPS系统示例:
code复制com.company.sps
├── crossborder // 跨境电商业务线
│ ├── order
│ │ ├── domain
│ │ └── application
│ └── subscription
│ ├── domain
│ └── application
└── local // 本地生活业务线
├── order
│ ├── domain
│ └── application
└── subscription
├── domain
└── application
配套的模块划分策略:
- 公共领域下沉到core模块
- 业务线特有实现放在各自模块
- 通过接口模块定义契约
4.2 自动化包规范检查
通过Checkstyle和ArchUnit可以自动维护包结构规范:
- 定义包关系约束(ArchUnit示例):
java复制@ArchTest
static final ArchRule layer_dependencies_are_respected = layeredArchitecture()
.layer("Application").definedBy("..application..")
.layer("Domain").definedBy("..domain..")
.layer("Infrastructure").definedBy("..infrastructure..")
.whereLayer("Application").mayOnlyBeAccessedByLayers("Web")
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure")
.whereLayer("Infrastructure").mayNotBeAccessedByAnyLayer();
- 配置包命名检查(Checkstyle示例):
xml复制<module name="RegexpSinglelineJava">
<property name="format" value="com\.company\.(cps|sps)\.[a-z]+\.(domain|application|infrastructure)\..+"/>
<property name="message" value="包结构不符合规范"/>
</module>
4.3 文档化包结构规范
建议在项目README中维护包结构示意图和设计原则:
code复制# 包结构规范
## 模块划分
└── cps-parent
├── cps-{业务}-core // 核心业务模块
├── cps-api // 接口聚合
└── cps-web // 入口层
## 核心模块内部结构
src/main/java
└── com.company.cps.{业务}
├── application // 应用服务
├── domain // 领域模型
└── infrastructure // 技术实现
## 命名约定
- 接口:XxxService
- 实现:XxxServiceImpl
- DTO:XxxDTO
- 实体:XxxEntity
在实际项目中,我们通过这种规范将新功能的代码定位时间从平均4小时缩短到30分钟以内。特别是在处理紧急业务需求时,清晰的包结构能显著降低沟通成本。
