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

一、架构解析

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

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

相关推荐
小贤plus1 小时前
MyBatis‑Plus IPage 分页踩坑实战:分页与全量查询兼容方案
java
Java内核笔记1 小时前
BeanRegistrar:Spring Boot 4 最被低估的新特性,彻底改变 Bean 注册方式
java·后端
还是鼠鼠1 小时前
【Spring AI智能体实战 02】Spring Boot 3.4整合Spring AI:项目创建与依赖配置
java·人工智能·spring boot·maven·spring ai
搬砖小匠1 小时前
SpringBoot 通过自定义注解 + AOP 实现数据字典自动翻译(通用方案)
java·后端
今天AI了吗1 小时前
【AI智能体】Hermes Agent 从部署到项目实战操作详解
java·人工智能·python·milvus
青山木2 小时前
Hot 100 --- 搜索二维矩阵
java·算法·leetcode·矩阵
刘名喜2 小时前
第21篇-N+1问题与fetch-join-EntityGraph
kotlin·springboot
CoderYanger2 小时前
Java EE:9.JVM(课件内容-上篇)
java·jvm·程序人生·面试·职场和发展·java-ee·程序员创富
xiaoqiMikko2 小时前
Spring Cloud Config 这个 CVE 的补丁,Maven Central 上根本没有
java·spring boot