* 当前状态为{@code CouponStatus.Created 初始状态},并且卡券在有效期期间,卡券转移到到{@code CouponStatus.Activated 可用状态}。 * 如果成功转移到{@code CouponStatus.Activated 可用状态},则发布{@link CouponActivated}领域事件 */ public void activate() { if (currentStatus() == CouponStatus.Created && container.effectiveTime().inRange(LocalDateTime.now())) { if (this.changeStatus(CouponStatus.Activated)) { DomainRegistry.instance().eventPublisher().publish(new CouponActivated(container.couponId())); } } } /** * 消费卡券状态转移 *
* 状态转移之前,先尝试可用状态转移{@link #activate()}和过期状态转移{@link #expire()} * 如果成功转移到{@code CouponStatus.Consumed 已消费状态},则发布{@link CouponConsumed}领域事件 */ public void consume() { // 先可用状态尝试 this.activate(); // 在过期状态尝试 this.expire(); // if (this.changeStatus(CouponStatus.Consumed)) { DomainRegistry.instance().eventPublisher().publish(new CouponConsumed(container.couponId())); } } /** * 尝试转移到{@code CouponStatus.Expired 过期状态} *
* 当前状态为{@code CouponStatus.Created 初始状态}或{@code CouponStatus.Activated 可用状态},并且超出卡券失效时间,卡券转移到到{@code CouponStatus.Expired 过期状态}。 * 如果成功转移到{@code CouponStatus.Expired 过期状态},则发布{@link CouponExpired}领域事件 */ public void expire() { if ((currentStatus() == CouponStatus.Created || currentStatus() == CouponStatus.Activated) && LocalDateTime.now().isAfter(container.effectiveTime().getToTime())) { if (this.changeStatus(CouponStatus.Expired)) { DomainRegistry.instance().eventPublisher().publish(new CouponExpired(container.couponId())); } } } /** * 取消卡券消费状态转移 *
* 当前状态为{@code CouponStatus.Consumed 已消费状态},卡券转移到到{@code CouponStatus.Activated 可用状态}。 * 同时发布{@link CouponActivated}领域事件 */ public void back() { if (currentStatus() == CouponStatus.Consumed) { if (this.setStatusDirectly(CouponStatus.Activated)) { DomainRegistry.instance().eventPublisher().publish(new CouponActivated(container.couponId())); } } } /** * 废弃卡券状态转移 *
* 如果成功转移到{@code CouponStatus.Cancelled 废弃状态},则发布{@link CouponCancelled}领域事件
*/
public void cancel() {
if (this.changeStatus(CouponStatus.Cancelled)) {
DomainRegistry.instance().eventPublisher().publish(new CouponCancelled(container.couponId()));
}
}
}
```
DomainEventPublisher接口实现类可以基于消息队列,该示例中仅打印日志。
```java
@Bean
public DomainEventPublisher domainEventPublisher() {
return event -> logger.info("{} {} {}", event.getClazz(), event.getKey(), event.getCreateTime());
}
```
基于消息队列实现示例。
```java
public class RabbitMqEventPublisher implements DomainEventPublisher {
@Autowired
RabbitTemplate rabbitTemplate;
@Autowired
MonitorService monitorService;
@Autowired
ObjectMapperWrapper jsonWrapper;
@Override
public boolean publish(DomainEvent event) {
Message message = createMessage(event);
rabbitTemplate.convertAndSend(MqConst.EXCHANGE_NAME, event.getClass().getSimpleName(), message);
return true;
}
@Override
public boolean publishSafe(DomainEvent event) {
try {
Message message = createMessage(event);
rabbitTemplate.convertAndSend(MqConst.EXCHANGE_NAME, event.getClass().getSimpleName(), message);
} catch (Exception ex) {
monitorService.addPublishFailedEvent(event);
}
return true;
}
private Message createMessage(DomainEvent event) {
MessageProperties properties = new MessageProperties();
properties.setHeader(MqConst.EVENT_KEY, event.getKey());
byte[] bytes = jsonWrapper.toBytes(event.getEventData());
Message message = new Message(bytes, properties);
return message;
}
}
```
DomainRegistry初始化需要各个接口的实例对象,可以在装配工程中初始化,可以通过DomainRegistryFactoryBean创建DomainRegistry对象。
```java
public class DomainRegistryFactoryBean implements FactoryBean, ApplicationContextAware {
ApplicationContext applicationContext;
@Autowired
DomainEventPublisher eventPublisher;
@Autowired
CouponArchiveRepository couponArchiveRepository;
@Autowired
CouponRepository couponRepository;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public DomainRegistry getObject() throws Exception {
BiFunction
通过分销员推荐分享注册会员时,会触发分销员晋级计算;分销员可以管理一级下级分销员,下级分销员晋级计算同时都会触发上级分销员晋级计算。

### 晋级领域模型

> 晋级计算时序图

### 测试
> 测试数据
```json
{
"name": "分销员级别二晋级方案",
"levelId": "Level-two",
"keepLevelIfMismatching": true,
"upgradeRules": [
{ "id": 1, "parentRuleId": 2, "ruleType": "MEMBER_COUNT", "value": 20},
{ "id": 3, "parentRuleId": 2, "ruleType": "TRADE_COUNT", "value": 40},
{ "id": 2, "parentRuleId": 4, "ruleType": "LOGICAL", "operator": "OR"},
{ "id": 4, "parentRuleId": null, "ruleType": "LOGICAL", "operator": "AND"},
{ "id": 5, "parentRuleId": 4, "ruleType": "TRADE_SUM", "value": 10000}
]
}
```
> 晋级方案执行树
```bash
# 分销员 Level-two 晋级方案 执行树
AND
├──OR
│ ├──MEMBER_COUNT 20
│ ├──TRADE_COUNT 40
├──TRADE_SUM 10000
```
> 测试数据推演
|#|会员数量|结果|交易数量|结果|交易金额|结果|是否晋级|
|---|---|---|---|---|---|---|---|
|1th|20|True|10*20|True|500*20|True|True|
|2th|20|True|10*20|True|400*20|False|False|
|3th|20|True|1*20|False|500*20|True|True|
|4th|20|True|1*20|False|400*20|False|False|
|5th|10|False|10*10|True|1000*10|True|True|
|6th|10|False|10*10|True|800*10|False|False|
|7th|10|False|3*10|False|1000*10|True|False|
|8th|10|False|3*10|False|800*10|False|False|
> 测试代码
```java
public class UpgradeTest {
@Test
public void test01_upgrade() throws IOException {
UpgradeSchema schema = loadUpgradeSchema();
UpgradeService upgradeService = new UpgradeService();
UpgradeExecutionContext executionContext;
boolean canUpgrade;
// 20 满足 200 满足 10000 满足
executionContext = createExecutionContext(BigDecimal.valueOf(500), 10, 20);
canUpgrade = upgradeService.upgrade(schema, executionContext);
assertTrue(canUpgrade);
// 20 满足 200 满足 8000 不满足
executionContext = createExecutionContext(BigDecimal.valueOf(400), 10, 20);
canUpgrade = upgradeService.upgrade(schema, executionContext);
assertFalse(canUpgrade);
// 20 满足 20 不满足 10000 满足
executionContext = createExecutionContext(BigDecimal.valueOf(500), 1, 20);
canUpgrade = upgradeService.upgrade(schema, executionContext);
assertTrue(canUpgrade);
// 20 满足 20 不满足 8000 不满足
executionContext = createExecutionContext(BigDecimal.valueOf(400), 1, 20);
canUpgrade = upgradeService.upgrade(schema, executionContext);
assertFalse(canUpgrade);
// 10 不满足 100 满足 10000 满足
executionContext = createExecutionContext(BigDecimal.valueOf(1000), 10, 10);
canUpgrade = upgradeService.upgrade(schema, executionContext);
assertTrue(canUpgrade);
// 10 不满足 100 满足 8000 不满足
executionContext = createExecutionContext(BigDecimal.valueOf(800), 10, 10);
canUpgrade = upgradeService.upgrade(schema, executionContext);
assertFalse(canUpgrade);
// 10 不满足 30 不满足 10000 满足
executionContext = createExecutionContext(BigDecimal.valueOf(1000), 3, 10);
canUpgrade = upgradeService.upgrade(schema, executionContext);
assertFalse(canUpgrade);
// 10 不满足 30 不满足 8000 不满足
executionContext = createExecutionContext(BigDecimal.valueOf(800), 3, 10);
canUpgrade = upgradeService.upgrade(schema, executionContext);
assertFalse(canUpgrade);
}
private UpgradeExecutionContext createExecutionContext(BigDecimal moneyPerMember, Integer countPerMember, Integer membersCount) {
TradeRepository tradeRepository = new MockTradeRepository(moneyPerMember, countPerMember);
DistributionRepository distributionRepository = new MockDistributionRepository(membersCount);
UpgradeObject upgradeObject = UpgradeObject.of(DistributorId.of("10001L"));
return new UpgradeExecutionContext(upgradeObject, tradeRepository, distributionRepository);
}
private UpgradeSchema loadUpgradeSchema() throws IOException {
InputStream is = UpgradeTest.class.getClassLoader().getResourceAsStream("upgrade-schema.json");
ObjectMapper objectMapper = new ObjectMapper();
UpgradeSchema schema = objectMapper.readValue(is, UpgradeSchema.class);
return schema;
}
}
```
[返回目录](#目录)
## 基于DDD和六边形架构的会员系统微服务结构

`设计时`:Rest和Dubbo适配器依赖应用层服务,应用层服务依赖领域服务,资源库适配器依赖领域服务。
`运行时`:Rest和Dubbo适配器的对象实例依赖应用层服务的对象实例,应用层服务的对象实例依赖资源库适配器的对象实例,应用层服务的对象实例依赖领域模型的对象实例。
`设计关键`:领域模型强调封装(encapsulation, act as architect),应用服务强调协调(collaboration, act as bridge),端口适配器强调技术实现(realization via tech, act as labor)
## 项目结构
```bash
├── uhy-application-starter # [装配容器] 装配器与运行容器,配置文件,启动类
├── uhy-application-service # [应用层] 应用服务层,提供场景化服务接口 Scenario-Oriented Facade
├── uhy-domain-model # [领域模型] 最核心部分,存粹的业务,不涉及框架与技术实现
├── uhy-rest-adapter # [适配器] REST适配器,为外部系统提供基于HTTP协议的REST服务
├── uhy-repository-adapter # [适配器] 资源库适配器,系统自身使用,采用数据库存储,使用mybatis实现
├── uhy-mock-repository-adapter # [适配器] 资源库适配器,系统自身使用,采用内存存储,使用caffeine实现
├── uhy-rpc-adapter # [适配器] RPC适配器,为外部系统提供基于Dubbo的RPC服务
└── uhy-rpc-api # [RPC接口] 提供给外部系统,便于RPC调用本服务,与REST不同,RPC客户端需要使用上游服务提供的API接口定义组件
```
|组件|含义|功能|
|---|---|---|
|uhy-application-starter|集成组件|将所有组件集成起来,提供内置的Tomcat容器,提供配置文件等资源,
应用程序最终通过该组件运行,并提供运行时的单元测试|
|uhy-application-service|应用服务组件|提供粗粒度接口Facade,提供事务一致性,负责连接领域模型与端口适配器|
|uhy-domain-model|领域模型组件|定义领域概念,封装业务规则,负责业务完整性|
|uhy-repository-adapter|资源库适配器组件|实现领域模型中定义的资源库接口,可以采用多种存储形式,
例如JVM缓存、Redis缓存、数据库多级存储|
|uhy-mock-repository-adapter|模拟资源库适配器组件|实现领域模型中定义的资源库接口,使用RAM存储对象|
|uhy-rest-adpater|REST适配器组件|通过REST风格HTTP协议向外部暴露接口,负责与外部交互|
|uhy-rpc-adpater|RCP适配器组件|通过RPC风格Dubbo协议向外部暴露接口,负责与外部交互|
|uhy-rpc-api|RCP适配器组件|提供给外部系统,便于RPC调用本服务|
*各个组件间的依赖关系参见“部署架构”。*
## 部署架构

[返回目录](#目录)
## 测试
单元测试是系统设计开发过程中一个非常重要的环节,展现了以客户端的角度如何使用系统,可以拿来与领域专家进行沟通交流。
### 测试卡券接口
```java
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {UhyApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public class CouponTest {
private static final String test_access_token = "a-access-token";
protected MockMvc mvc;
@Autowired
WebApplicationContext webApplicationContext;
@Autowired
ObjectMapper objectMapper;
@Before
public void setUp() {
mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
/**
* 新增卡券定义
*/
@Test
public void test01_add_coupon_archive() {
// 创建卡券定义
String id = create_an_coupon_archive();
// 查看卡券定义
find_coupon_archive(id, 1000);
}
/**
* 收取卡券
*/
@Test
public void test03_receive_coupon() {
// 创建卡券定义
String archiveId = create_an_coupon_archive();
// 领取卡券
ResponseDto responseDto = receive_a_coupon(archiveId);
assertEquals("200", responseDto.getCode());
assertTrue(responseDto.getData() instanceof String);
String couponId = (String) responseDto.getData();
// 查看卡券
find_coupon(couponId);
// 卡券定义可用数量
find_coupon_archive(archiveId, 999);
// 消费卡券
consume_coupon(couponId);
// 查看卡券状态
ResponseDto resp2 = find_coupon(couponId);
assertEquals(CouponStatus.Consumed.ordinal(), ((Map