大营销平台 —— 架构解析和抽奖流程串联

一、架构解析

在前面我们实现了策略领域、活动领域、发奖领域,但是整体流程是没有串联起来的,只对外提供了接口,但是没有把流程串联起来。

说到流程串联,可能第一时间就会想到用抽象类去规定业务骨架,但是也不要忘了,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。

相关推荐
_Evan_Yao21 小时前
Redis 缓存穿透、击穿、雪崩,一次讲清!
java·spring·mybatis
梓豪csd1 天前
消息订阅中心:谁该收到哪条通知
java·开发语言·人工智能·低代码·自动化·1024程序员节
秋饼1 天前
Spring AI 2.1 Message Parts 深度实战:推理签名回放与跨轮记忆持久化的源码级落地
java·ai·技术分享·后端开发
thefool1122661 天前
TCP 1
java·1024程序员节
金銀銅鐵1 天前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(下)
java·后端·ai编程
AI人工智能+电脑小能手1 天前
一次请求打几个下游,这个Java并行任务工具类6个方法按序聚合
java·线程池·并发编程·1024程序员节·批量处理·并行任务
paopaokaka_luck1 天前
心理咨询管理系统(WebSocket实时沟通、基于用户的协同过滤、 ECharts多维分析、 预约业务流程、预约校验与定时处理)
java·vue.js·spring boot·mysql·spring·echarts·1024程序员节
2301_32241428041 天前
springboot农产品交易平台的设计与实现08055-计算机毕设 原创(免费领源码+部署教程+开发环境)
java·spring boot·redis·mysql·tomcat·maven·mybatis
考虑考虑1 天前
JDK26的JEP 500属性
java·后端·java ee
LMT流氓兔1 天前
第五单元——第十二课:把脚本整理成项目
java·数据库·python·langchain·1024程序员节