一、架构解析
在前面我们实现了策略领域、活动领域、发奖领域,但是整体流程是没有串联起来的,只对外提供了接口,但是没有把流程串联起来。
说到流程串联,可能第一时间就会想到用抽象类去规定业务骨架,但是也不要忘了,Controller也具备这样的职能 ,这里就要提到Controller和Service在DDD中的职责区别了,Service是负责执行逻辑的,比如具体的责任链、规则树,或者去调用仓储操作数据库;而Controller属于接口层,充当适配器,是负责串联服务的,这也是一个误区,在前面写苍穹外卖、黑马商城、黑马点评的时候基本上在Controller中不写任何内容,仅仅就是透传,直接调用Service,但是在DDD中,由于是将领域作为载体来划分业务域的,所以想要串联这几个领域就需要在高于领域的上一层执行。
在这里先重新讲一下整洁架构和DDD四层架构:
整洁架构: 这个架构主要是提供一个依赖方向的指导,传统整洁架构大致分为四层(从里到外):实体、用例、适配器、框架 。实体和用例比较好理解,这两个合起来就是DDD的领域层。强调一下这里的适配器,适配器包括了对外部的协议处理及组件调用和内部数据库调用。
比如在大营销中,入站(外到内)适配器是trigger,用于适配http、rpc协议以及MQ、job的组件;出站(内到外)适配器是infrasctructure,用于适配mysql、redis。

DDD四层架构:这个架构主要是提供一个业务方向的指导,比如规定业务的哪个部分应该放在哪里,主要分为(从内到外):领域层、应用层、接口层、基础设施层。
明显的,我们完全是按照四层架构来设计的,领域层(domain)、应用层(app)、接口层(充当适配器:trigger+api)、基础设施层(infrastructure)。领域层规定具体业务,应用层配置、编排服务,接口层适配外部服务和组件,基础设施层直接操作数据库,这里就不过多赘述了,因为我们前面建立的所有架构思维都是基于DDD的。
由此,在DDD四层架构下,凌驾于领域之上的应该是接口层,因此对于领域的串联骨架,我们选择写在trigger中的Controller中。
二、抽奖流程串联
先看架构图:

1.Controller串联领域
在Controller中我们设计了两个对外的接口,一个是装配,一个是抽奖。
这是因为装配往往只需要一次,而抽奖是高频的,所以不能把装配和抽奖写一起。
而在抽奖接口中,我们真正的串联了整个抽奖流程,从partake领域到strategy领域最后award领域,通过调用三个接口实现整个抽奖业务。
java
/**
* @author 印东升
* @description 抽奖活动服务
* @create 2026-08-03 11:35
*/
public interface IRaffleActivityService {
/**
* 活动装配,装配数据预热
* @param activityId
* @return
*/
Response<Boolean> armory(Long activityId);
/**
* 活动抽奖接口
* @param request
* @return
*/
Response<ActivityDrawResponseDTO> draw(ActivityDrawRequestDTO request);
}
java
/**
* @author 印东升
* @description 抽奖活动服务
* @create 2026-08-03 11:40
*/
@Slf4j
@RestController()
@CrossOrigin("${app.config.cross-origin}")
@RequestMapping("/api/${app.config.api-version}/raffle/activity")
public class RaffleActivityController implements IRaffleActivityService {
@Resource
private IActivityArmory activityArmory;
@Resource
private IStrategyArmory strategyArmory;
@Resource
private IRaffleActivityAccountQuotaService raffleActivityAccountQuotaService;
@Resource
private IRaffleActivityPartakeService raffleActivityPartakeService;
@Resource
private IRaffleActivitySkuStockService raffleActivitySkuStockService;
@Resource
private IRaffleStrategy raffleStrategy;
@Resource
private IAwardService awardService;
@Override
@RequestMapping(value = "armory", method = RequestMethod.GET)
public Response<Boolean> armory(@RequestParam Long activityId) {
try {
log.info("活动装配,数据预热,开始 activityId:{}", activityId);
//1.活动装配
activityArmory.assembleActivitySkuByActivityId(activityId);
//2.策略装配
strategyArmory.assembleLotteryStrategyByActivityId(activityId);
Response<Boolean> response = Response.<Boolean>builder()
.code(ResponseCode.SUCCESS.getCode())
.info(ResponseCode.SUCCESS.getInfo())
.build();
log.info("活动装配,数据预热,完成 activityId:{}", activityId);
return response;
} catch (Exception e) {
log.error("活动装配,数据预热,失败 activityId:{}", activityId, e);
return Response.<Boolean>builder()
.code(ResponseCode.UN_ERROR.getCode())
.info(ResponseCode.UN_ERROR.getInfo())
.build();
}
}
@Override
@RequestMapping(value = "draw", method = RequestMethod.POST)
public Response<ActivityDrawResponseDTO> draw(@RequestBody ActivityDrawRequestDTO request) {
try {
log.info("活动抽奖,校验userId:{} activityId:{}", request.getUserId(), request.getActivityId());
//1.参数校验
if (StrUtil.isBlank(request.getUserId()) || null == request.getActivityId()) {
throw new AppException(ResponseCode.ILLEGAL_PARAMETER.getCode(), ResponseCode.ILLEGAL_PARAMETER.getInfo());
}
//2.参与活动 - 创建参与记录订单
UserRaffleOrderEntity userRaffleOrder = raffleActivityPartakeService.createOrder(request.getUserId(), request.getActivityId());
log.info("活动抽奖,创建订单 userId:{} activityId:{}", request.getUserId(), request.getActivityId());
//3.抽奖策略 - 执行抽奖
RaffleFactorEntity raffleFactorEntity = new RaffleFactorEntity();
raffleFactorEntity.setUserId(userRaffleOrder.getUserId());
raffleFactorEntity.setStrategyId(userRaffleOrder.getStrategyId());
RaffleAwardEntity raffleAwardEntity = raffleStrategy.performRaffle(raffleFactorEntity);
//4.存放结果 - 写入中奖记录(聚合订单信息和奖品信息)
UserAwardRecordEntity userAwardRecordEntity = UserAwardRecordEntity.builder()
.userId(userRaffleOrder.getUserId())
.activityId(userRaffleOrder.getActivityId())
.strategyId(userRaffleOrder.getStrategyId())
.orderId(userRaffleOrder.getOrderId())
.awardId(raffleAwardEntity.getAwardId())
.awardTitle(raffleAwardEntity.getAwardTitle())
.awardTime(new Date())
.awardState(AwardStateVO.create)
.build();
awardService.saveUserAwardRecord(userAwardRecordEntity);
log.info("活动抽奖,写入中奖记录 userId:{} activityId:{}", request.getUserId(), request.getActivityId());
//5.返回结果
ActivityDrawResponseDTO activityDrawResponseDTO = new ActivityDrawResponseDTO();
activityDrawResponseDTO.setAwardId(raffleAwardEntity.getAwardId());
activityDrawResponseDTO.setAwardTitle(raffleAwardEntity.getAwardTitle());
activityDrawResponseDTO.setAwardIndex(raffleAwardEntity.getSort());
log.info("活动抽奖,完成 userId:{} activityId:{}", request.getUserId(), request.getActivityId());
return Response.<ActivityDrawResponseDTO>builder()
.code(ResponseCode.SUCCESS.getCode())
.info(ResponseCode.SUCCESS.getInfo())
.data(activityDrawResponseDTO)
.build();
} catch (AppException e) {
log.info("活动抽奖,失败 userId:{} activityId:{}", request.getUserId(), request.getActivityId());
return Response.<ActivityDrawResponseDTO>builder()
.code(e.getCode())
.info(e.getInfo())
.build();
} catch (Exception e) {
log.info("活动抽奖,失败 userId:{} activityId:{}", request.getUserId(), request.getActivityId());
return Response.<ActivityDrawResponseDTO>builder()
.code(ResponseCode.UN_ERROR.getCode())
.info(ResponseCode.UN_ERROR.getInfo())
.build();
}
}
}
2.细节处理
这里需要注意,前端传回的参数是以用户为中心 的,我们设想的是用户点进一个活动,然后点击抽奖,用户不关心活动内部真正用的什么策略,只关心参与了哪个活动,因此前端传回的参数应该是activityId而不是strategyId。
但是由于我们的策略和活动是一对一的,因此我们可以用strategy查activityId,用activityId查strategyId,所以这一点处理起来也不会很麻烦。
java
@Mapper
public interface IRaffleActivityDao {
RaffleActivity queryRaffleActivityByActivityId(Long activityId);
Long queryStrategyIdByActivityId(Long activityId);
Long queryActivityIdByStrategyId(Long strategyId);
}
同时,对于比较简单的参数,比如我们这里创建记录订单只需要activityId和userId,没必要去填充一个实体对象来强行套用领域服务接口,我们只需要重载一个接口就可以了:
java
@Override
public UserRaffleOrderEntity createOrder(String userId, Long activityId) {
PartakeRaffleActivityEntity partakeRaffleActivityEntity = new PartakeRaffleActivityEntity();
partakeRaffleActivityEntity.setUserId(userId);
partakeRaffleActivityEntity.setActivityId(activityId);
return this.createOrder(partakeRaffleActivityEntity);
}
@Override
public UserRaffleOrderEntity createOrder(PartakeRaffleActivityEntity partakeRaffleActivityEntity) {...}
而对于响应结果,其实就只需要考虑前端需要显示给用户什么,按道理来说这里面的字段应该是前端提供的,因此不用去深究这里面为什么这样设计。
java
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ActivityDrawResponseDTO {
// 奖品ID
private Integer awardId;
// 奖品标题
private String awardTitle;
// 排序编号【策略奖品配置的奖品顺序编号】
private Integer awardIndex;
}
三、测试
最后测试,用apifox直接导入curl测试即可:



四、题外
在写这篇文章时虚拟机突然连不上主机了,测试连通性、防火墙,发现都是正常的,最后发现是ip变了,应该是虚拟机重启导致的,似乎ip是由VMWare动态改变的,这里建议去虚拟机把ip设置死,具体设置细节可以直接问ai。