Lottery抽奖系统

Posted by lily's blog on June 11, 2024

Chapter1:DDD分层基础手脚架搭建

DDD分层是项目基础架构设计的一种模式,类似于web开发中的MVC架构,我觉得将DDD分层架构与曾经学习过的spring clould Alibaba微服务架构类比会更相似些。

为什么使用DDD架构

DDD架构分层包括

项目基础手脚架

应用层-application

这是应用启动和配置的一层,如一些 aop 切面或者 config 配置,以及打包镜像都是在这一层处理。你可以把它理解为专门为了启动服务而存在的。 逻辑包装,编排、任务、领域事件的发布和订阅

通用层-common

通用类型定义层,在我们的系统开发中,会有很多类型的定义,包括;基本的 Response、Constants 和枚举。它会被其他的层进行引用使用。

领域层-domian

封装具体的业务领域功能实现,是聚合的,充血的。 领域模型服务,是一个非常重要的模块。无论怎么做DDD的分层架构,domain 都是肯定存在的。在一层中会有一个个细分的领域服务,在每个服务包中会有【模型、仓库、服务】这样3部分。

  • model 模型对象;
    • aggreate:聚合对象,实体对象、值对象的协同组织,就是聚合对象。
    • entity:实体对象,大多数情况下,实体对象(Entity)与数据库持久化对象(PO)是1v1的关系,但也有为了封装一些属性信息,会出现1vn的关系。
    • valobj:值对象,通过对象属性值来识别的对象 By 《实现领域驱动设计》
  • repository 仓储服务;从数据库等数据源中获取数据,传递的对象可以是聚合对象、实体对象,返回的结果可以是;实体对象、值对象。因为仓储服务是由基础层(infrastructure) 引用领域层(domain),是一种依赖倒置的结构,但它可以天然的隔离PO数据库持久化对象被引用。

  • service 服务设计;把一些重核心业务方到 service 里实现。

  • 对象解释

    • DTO 数据传输对象 (data transfer object),DAO与业务对象或数据访问对象的区别是:DTO的数据的变异子与访问子(mutator和accessor)、语法分析(parser)、序列化(serializer)时不会有任何存储、获取、序列化和反序列化的异常。即DTO是简单对象,不含任何业务逻辑,但可包含序列化和反序列化以用于传输数据.

基础层-infrastructure

基础层依赖于 domain 领域层,因为在 domain 层定义了仓储接口需要在基础层实现。这是依赖倒置的一种设计方式。 提供基础的功能和服务,包括:数据库,Redis,ES等。

接口层-interfaces

因为微服务中引用的 RPC 需要对外提供接口的描述信息,也就是调用方在使用的时候,需要引入 Jar 包,让调用方好能依赖接口的定义做代理。 实现rpc的接口定义,引入应用层服务,封装具体的接口。(各个模块的接口)

RPC定义接口-trigger (rpc层)

触发器层,一般也被叫做 adapter 适配器层。用于提供接口实现、消息接收、任务执行等。所以对于这样的操作,把它叫做触发器层。 描述RPC接口文件,用于打包后外部引入POM配置。(内部互相调用的接口)

分层关系

![[Pasted image 20240612133022.png]]

Chapter2:RPC框架的跑通

为什么使用dubbo?

dubbo是一款阿里自研的RPC框架

  1. 因为dubbo底层通信是Socket而不是http所以通信效率会更高
  2. dubbo有分布式高可用的设计,在某组服务宕机后会被从注册中心摘除,之后流量会打到其他的服务上 类比于openfeign
    OpenFeign是一个基于REST的远程过程调用(RPC)框架,它可帮助开发人员发起HTTP请求并调用远程服务。它可以简化与远程服务的通信,并提供了一种声明性的方式来定义客户端与服务端之间的交互。
    

    dubbo如何使用

    dubbo版本更新

    注意: 目前项目使用dubbo版本为2.7.15 因为原版本dubbo项目版本已经不再维护@service注解,此时再使用广播直连模式会找不到服务端 (原代码中@Service被标记已过时)从而报错

dubbo的使用分为接口调用方以及接口提供方

接口提供方

提供接口的描述信息:接口定义以及内部方法的定义 一般在DDD结构中定义为 api:Dubbo接口定义模块。只定义接口,当然也可以包含一些定义传输的dto,返回、接收类型类(Types)。这部分结构需要对外提供jar包,也就是接口的描述信息(在另一方调用之前需要先install到本地中央仓库去)。 但是需要注意:

- 所有的 Dubbo 接口,出入参,默认都需要继承 Serializable 接口。也就是 UserReqDTO、UserResDTO、Response 这3个类,都得继承 Serializable 序列化接口。

app:应用程序启动模块。 springboot的项目启动入口 trigger:接口实现模块。 此处的接口启用是实际调用的函数/代码的地方,可以自定义实现代码,类比于service层的实现

@Slf4j
@DubboService(version = "1.0.0")
public class UserService implements IUserService {

    @Override
    public Response<UserResDTO> queryUserInfo(UserReqDTO reqDTO) {
        log.info("查询用户信息 userId: {} reqStr: {}", reqDTO.getUserId(), JSON.toJSONString(reqDTO));
        try {
            // 1. 模拟查询【你可以从数据库或者Redis缓存获取数据】
            UserResDTO resDTO = UserResDTO.builder()
                    .userId(reqDTO.getUserId())
                    .userName("小傅哥")
                    .userAge(20)
                    .build();

            // 2. 返回结果
            return Response.<UserResDTO>builder()
                    .code(Constants.ResponseCode.SUCCESS.getCode())
                    .info(Constants.ResponseCode.SUCCESS.getInfo())
                    .data(resDTO).build();
        } catch (Exception e) {
            log.error("查询用户信息失败 userId: {} reqStr: {}", reqDTO.getUserId(), JSON.toJSONString(reqDTO), e);
            return Response.<UserResDTO>builder()
                    .code(Constants.ResponseCode.UN_ERROR.getCode())
                    .info(Constants.ResponseCode.UN_ERROR.getInfo())
                    .build();
        }
    }

}

但此时为RPC调用所以service需要使用Dubbo自定义的注解

工程配置yml

application.yml(主启动类中的springboot配置方式) 项目使用dubbo的配置:

  1. 其中服务注册发现使用zookeeper的直连测试模式
  2. 包扫描要扫描到对外提供服务的jar包接口定义处 ```yml dubbo: application: name: xfg-dev-tech-dubbo version: 1.0.0 registry: address: zookeeper://127.0.0.1:2181 # N/A - 无zookeeper可配置 N/A 走直连模式测试 protocol: name: dubbo port: 20881 scan: base-packages: cn.bugstack.dev.tech.dubbo.api
或者是消费者和服务端都使用无注册中心的广播模式
```yml
# Dubbo 广播方式配置  
dubbo:  
  application:  
    name: Lottery  
    version: 1.0.0  
  registry:  
    address: N/A #multicast://224.5.6.7:1234  
  protocol:  
    name: dubbo  
    port: 8101  
  scan:  
    base-packages: com.lily.lottery.rpc

注意

  • base-packages配置的是spring约定扫描的包,dubbo的接口模块要想被扫描到,主项目的pom中应该要间接或直接的依赖接口模块。

  • spring讲究约定大于配置,Application的应用的包名应该要涵盖覆盖到其他的包名。例如Application所在包名为 com.lily.dev.dubbo 那么api接口的包名层级应该比该层级多一层或者同层级才可扫到com.lily.dev.dubbo.api。如果此时Application所在包名为 com.lily.dev.dubbo.a.b.c 那么api接口的包名将不能够被扫描到。

  • address:如果配置的是 N/A 就是不走任何注册中心,就是个直连,主要用于本地验证的。如果你配置了 zookeeper://127.0.0.1:2181 就需要先安装一个 zookeeper 。另外,即使配置了注册中心的方式,也可以直连测试。

    应用构建

    整个项目执行install,jar包就会进入本地的Maven仓库,本地调用可以使用该jar包;其次就可以通过deploy将本地的jar包发布到中央仓库,即使不在本地环境也可以使用

    服务使用方

    1. pom引入 ```xml
com.lily xxx-dubbo-api 1.0-SNAPSHOT
2. 消费配置
使用方也需要配置对应的dubbo属性
```yml
dubbo:
  application:
    name: xfg-dev-tech-dubbo
    version: 1.0.0
  registry:
     address: zookeeper://127.0.0.1:2181
#    address: N/A
  protocol:
    name: dubbo
    port: 20881
  1. 代码调用 ```java // 直连模式;@DubboReference(interfaceClass = IUserService.class, url = “dubbo://127.0.0.1:20881”, version = “1.0.0”) @DubboReference(interfaceClass = IUserService.class, version = “1.0.0”) private IUserService userService;

@Test public void test_userService() { UserReqDTO reqDTO = UserReqDTO.builder().userId(“10001”).build(); Response resDTO = userService.queryUserInfo(reqDTO); log.info("测试结果 req: {} res: {}", JSON.toJSONString(reqDTO), JSON.toJSONString(resDTO)); }


- 配置了 zookeeper 就用第一个,代码中对应 `@DubboReference(interfaceClass = IUserService.class, version = "1.0.0")`
- 配置了 N/A 就用第二个,代码中必须指定直连。`@DubboReference(interfaceClass = IUserService.class, url = "dubbo://127.0.0.1:20881", version = "1.0.0")`
## 项目完善
### 创建抽奖活动表
用于测试简单的rpc流程, 但是注意,插入数据时会发生乱码问题。
活动表:是一个用于配置抽奖活动的总表,用于存放活动信息,包括:ID、名称、描述、时间、库存、参与次数等。
```sql
CREATE TABLE `activity` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `activity_id` bigint(20) NOT NULL COMMENT '活动ID',
  `activity_name` varchar(64) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '活动名称',
  `activity_desc` varchar(128) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '活动描述',
  `begin_date_time` datetime DEFAULT NULL COMMENT '开始时间',
  `end_date_time` datetime DEFAULT NULL COMMENT '结束时间',
  `stock_count` int(11) DEFAULT NULL COMMENT '库存',
  `take_count` int(11) DEFAULT NULL COMMENT '每人可参与次数',
  `state` tinyint(2) DEFAULT NULL COMMENT '活动状态:1编辑、2提审、3撤审、4通过、5运行(审核通过后worker扫描状态)、6拒绝、7关闭、8开启',
  `creator` varchar(64) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_activity_id` (`activity_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='活动配置';

子模块依赖配置


现有工程模块依赖关系:

  • lottery-application,应用层,引用:domain
  • lottery-common,通用包,引用:
  • lottery-domain,领域层,引用:infrastructure
  • lottery-infrastructure,基础层,引用:
  • lottery-interfaces,接口层,引用:applicationrpc
  • lottery-rpc,RPC接口定义层,引用:common

此时项目着重配置以下三个模块

  • lottery-infrastructure
  • lottery-interfaces
  • lottery-rpc

    interfaces接口模块

    模块结构

    定义了主启动类,由于该模块是整个项目的出口。

  • 该模块实现了RPC层定义的接口
  • 在配置文件中定义mybatis配置(在接口实现类中会通过mybatis访问数据仓),项目启动端口号,以及dubbo对应配置
  • 对应的需要mybatis的 mapper配置文件 ![[Pasted image 20240613210022.png]] dubbo对应的yml配置
    # Dubbo 广播方式配置
    dubbo:
    application:
      name: Lottery
      version: 1.0.0
    registry:
      address: N/A #multicast://224.5.6.7:1234
    protocol:
      name: dubbo
      port: 20880
    scan:
      base-packages: com.lily.lottery.rpc
    
  • 广播模式的配置唯一区别在于注册地址,registry.address = multicast://224.5.6.7:1234(此时为注册地址),服务提供者和服务调用者都需要配置相同的📢广播地址。或者配置为 N/A 用于直连模式使用
  • application,配置应用名称和版本
  • protocol,配置的通信协议和端口
  • scan,相当于 Spring 中自动扫描包的地址,可以把此包下的所有 rpc 接口都注册到服务中
    pom文件

    lottery-interfaces 作为系统的 war 包工程,在构建工程时候需要依赖于 POM 中配置的相关信息。那这里就需要注意下,作为 Lottery 工程下的主 pom.xml 需要完成对 SpringBoot 父文件的依赖,此外还需要定义一些用于其他模块可以引入的配置信息,比如:jdk版本、编码方式等。而其他层在依赖于工程总 pom.xml 后还需要配置自己的信息。

  • lottery-interfaces 是整个程序的出口,也是用于构建 War 包的工程模块,所以你会看到一个 <packaging>war</packaging> 的配置。
  • 在 dependencies 会包含所有需要用到的 SpringBoot 配置,也会包括对其他各个模块的引入。
  • 在 build 构建配置上还会看到一些关于测试包的处理,比如这里包括了资源的引入也可以包括构建时候跳过测试包的配置。 ```xml
lottery-interfaces war org.springframework.boot spring-boot-starter-web ... Lottery src/main/resources true **/** src/test/resources true **/** org.springframework.boot spring-boot-maven-plugin org.apache.maven.plugins maven-compiler-plugin 8</source> 8
#### RPC模块
##### 模块结构
该模块是消费者调用需要依赖的模块,其中定义了一个活动类接口以及对应的传输时的包装类
模块.项目结构如下:
![[Pasted image 20240613205636.png]]
##### pom文件
```xml
<parent>
    <artifactId>Lottery</artifactId>
    <groupId>cn.itedus.lottery</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>lottery-rpc</artifactId>
<packaging>jar</packaging>
<dependencies>
    <dependency>
        <groupId>cn.itedus.lottery</groupId>
        <artifactId>lottery-common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>
<build>
    <finalName>lottery-rpc</finalName>
    <plugins>
        <!-- 编译plugin -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>${jdk.version}</source>
                <target>${jdk.version}</target>
                <compilerVersion>1.8</compilerVersion>
            </configuration>
        </plugin>
    </plugins>
</build>

定义开发 RPC 接口

开发接口

@DubboService
public class ActivityBooth implements IActivityBooth {

    @Resource
    private IActivityDao activityDao;

    @Override
    public ActivityRes queryActivityById(ActivityReq req) {

        Activity activity = activityDao.queryActivityById(req.getActivityId());

        ActivityDto activityDto = new ActivityDto();
        activityDto.setActivityId(activity.getActivityId());
        activityDto.setActivityName(activity.getActivityName());
        activityDto.setActivityDesc(activity.getActivityDesc());
        activityDto.setBeginDateTime(activity.getBeginDateTime());
        activityDto.setEndDateTime(activity.getEndDateTime());
        activityDto.setStockAllTotal(activity.getStockAllTotal());
        activityDto.setStockDayTotal(activity.getStockDayTotal());
        activityDto.setTakeAllCount(activity.getStockAllTotal());
        activityDto.setTakeDayCount(activity.getStockDayTotal());

        return new ActivityRes(new Result(Constants.ResponseCode.SUCCESS.getCode(), Constants.ResponseCode.SUCCESS.getInfo()), activityDto);
    }

}
  • 用于实现 RPC 接口的实现类 ActivityBooth 上有一个注解 @DubboService,这个注解是来自于 Dubbo 的 org.apache.dubbo.config.annotation.Service,也就是这个包下含有此注解配置的类可以被 Dubbo 管理。
  • 在 queryActivityById 功能实现中目前还比较粗糙,但大体可以看出这是对数据库的操作以及对结果的封装,提供 DTO 的对象并返回 Res 结果。目前dto的创建后续可以使用门面模式和工具类进行处理

    抽奖活动库表设计

    需要哪些库表?

  • 活动配置,activity:提供活动的基本配置
  • 策略配置,strategy:用于配置抽奖策略,概率、玩法、库存、奖品
  • 策略明细,strategy_detail:抽奖策略的具体明细配置
  • 奖品配置,award:用于配置具体可以得到的奖品
  • 用户参与活动记录表,user_take_activity:每个用户参与活动都会记录下他的参与信息,时间、次数
  • 用户活动参与次数表,user_take_activity_count:用于记录当前参与了多少次
  • 用户策略计算结果表,user_strategy_export_001~004:最终策略结果的一个记录,也就是奖品中奖信息的内容

1. lottery.sql

活动信息表

create database lottery;

-- auto-generated definition
create table activity
(
    id            bigint auto_increment comment '自增ID',
    activityId    bigint       null comment '活动ID',
    activityName  varchar(64)  not null comment '活动名称',
    activityDesc  varchar(128) null comment '活动描述',
    beginDateTime datetime     not null comment '开始时间',
    endDateTime   datetime     not null comment '结束时间',
    stockCount    int          not null comment '库存',
    takeCount     int          null comment '每人可参与次数',
    state         int          null comment '活动状态:编辑、提审、撤审、通过、运行、拒绝、关闭、开启',
    creator       varchar(64)  not null comment '创建人',
    createTime    datetime     not null comment '创建时间',
    updateTime    datetime     not null comment '修改时间',
    constraint activity_id_uindex
        unique (id)
)
    comment '活动配置';

alter table activity
    add primary key (id);

奖品信息表

-- auto-generated definition
create table award
(
    id           bigint(11) auto_increment comment '自增ID'
        primary key,
    awardId      bigint                             null comment '奖品ID',
    awardType    int(4)                             null comment '奖品类型(文字描述、兑换码、优惠券、实物奖品暂无)',
    awardCount   int                                null comment '奖品数量',
    awardName    varchar(64)                        null comment '奖品名称',
    awardContent varchar(128)                       null comment '奖品内容「文字描述、Key、码」',
    createTime   datetime default CURRENT_TIMESTAMP null comment '创建时间',
    updateTime   datetime default CURRENT_TIMESTAMP null comment 'updateTime'
)
    comment '奖品配置';

抽奖策略表

-- auto-generated definition
create table strategy
(
    id           bigint(11) auto_increment comment '自增ID'
        primary key,
    strategyId   bigint(11)   not null comment '策略ID',
    strategyDesc varchar(128) null comment '策略描述',
    strategyMode int(4)       null comment '策略方式「1:单项概率、2:总体概率」',
    grantType    int(4)       null comment '发放奖品方式「1:即时、2:定时[含活动结束]、3:人工」',
    grantDate    datetime     null comment '发放奖品时间',
    extInfo      varchar(128) null comment '扩展信息',
    createTime   datetime     null comment '创建时间',
    updateTime   datetime     null comment '修改时间',
    constraint strategy_strategyId_uindex
        unique (strategyId)
)
    comment '策略配置';

-- auto-generated definition

抽奖详情表

比如抽奖时选择的抽奖策略是哪个 比如奖品

create table strategy_detail  
(  
    id                bigint(11) auto_increment comment '自增ID'  
        primary key,  
    strategyId        bigint(11)    not null comment '策略ID',  
    awardId           bigint(11)    null comment '奖品ID',  
    awardCount        int           null comment '奖品数量',  
    awardRate         decimal(5, 2) null comment '中奖概率',  
    createTime        datetime      null comment '创建时间',  
    updateTime        datetime      null comment '修改时间',  
    awardSurplusCount int default 0 null comment '奖品剩余库存'  
)  
    comment '抽奖情况表';

2. lottery_01.sql ~ lottery_02.sql

create database lottery_01;

-- auto-generated definition
create table user_take_activity
(
    id           bigint    null,
    uId          tinytext  null,
    takeId       bigint    null,
    activityId   bigint    null,
    activityName tinytext  null,
    takeDate     timestamp null,
    takeCount    int       null,
    uuid         tinytext  null,
    createTime   timestamp null,
    updateTime   timestamp null
)
    comment '用户参与活动记录表';

-- auto-generated definition
create table user_take_activity_count
(
    id         bigint    null,
    uId        tinytext  null,
    activityId bigint    null,
    totalCount int       null,
    leftCount  int       null,
    createTime timestamp null,
    updateTime timestamp null
)
    comment '用户活动参与次数表';

-- auto-generated definition
create table user_strategy_export_001(id           bigint     null,uId          mediumtext null,activityId   bigint     null,orderId      bigint     null,strategyId   bigint     null,strategyType int        null,grantType    int        null,grantDate    timestamp  null,grantState   int        null,awardId      bigint     null,awardType    int        null,awardName    mediumtext null,awardContent mediumtext null,uuid         mediumtext null,createTime   timestamp  null,updateTime   timestamp  null) comment '用户策略计算结果表';
create table user_strategy_export_002(id           bigint     null,uId          mediumtext null,activityId   bigint     null,orderId      bigint     null,strategyId   bigint     null,strategyType int        null,grantType    int        null,grantDate    timestamp  null,grantState   int        null,awardId      bigint     null,awardType    int        null,awardName    mediumtext null,awardContent mediumtext null,uuid         mediumtext null,createTime   timestamp  null,updateTime   timestamp  null) comment '用户策略计算结果表';
create table user_strategy_export_003(id           bigint     null,uId          mediumtext null,activityId   bigint     null,orderId      bigint     null,strategyId   bigint     null,strategyType int        null,grantType    int        null,grantDate    timestamp  null,grantState   int        null,awardId      bigint     null,awardType    int        null,awardName    mediumtext null,awardContent mediumtext null,uuid         mediumtext null,createTime   timestamp  null,updateTime   timestamp  null) comment '用户策略计算结果表';
create table user_strategy_export_004(id           bigint     null,uId          mediumtext null,activityId   bigint     null,orderId      bigint     null,strategyId   bigint     null,strategyType int        null,grantType    int        null,grantDate    timestamp  null,grantState   int        null,awardId      bigint     null,awardType    int        null,awardName    mediumtext null,awardContent mediumtext null,uuid         mediumtext null,createTime   timestamp  null,updateTime   timestamp  null) comment '用户策略计算结果表';


  1. 这些库表是用于支撑起抽奖系统开发的必备表,后续可能会随着功能的开发做适当的调整。
  2. 接下来我们会围绕这些库表一点点实现各个领域的功能,包括:抽奖策略领域、奖品发放领域、活动信息领域等

    抽奖模块实现

代码结构

首先是domian模块中抽奖模块的实现 抽奖系统工程采用DDD架构 + Module模块方式搭建,lottery-domain 是专门用于开发领域服务的模块,不限于目前的抽奖策略在此模块下实现还有以后需要实现的活动领域、规则引擎、用户服务等都需要在这个模块实现对应的领域功能。

strategy 是第1个在 domain 下实现的抽奖策略领域,在领域功能开发的服务下主要含有model、repository、service三块区域,接下来分别介绍下在抽奖领域中这三块区域都做了哪些事情。

  • model,用于提供vo、req、res 和 aggregates 聚合对象。
  • repository,提供仓储服务,其实也就是对Mysql、Redis等数据的统一包装。
  • service,是具体的业务领域逻辑实现层,在这个包下定义了algorithm抽奖算法实现和具体的抽奖策略包装 draw 层,对外提供抽奖接口 IDrawExec#doDrawExec

    抽奖策略算法

    两种抽奖算法描述,场景A20%、B30%、C50%

  • 总体概率:如果A奖品抽空后,B和C奖品的概率按照 3:5 均分,相当于B奖品中奖概率由 0.3 升为 0.375
  • 单项概率:如果A奖品抽空后,B和C保持目前中奖概率,用户抽奖扔有20%中为A,因A库存抽空则结果展示为未中奖。为了运营成本,通常这种情况的使用的比较多\

斐波那契算法

用于初始化

总体概率抽奖

  • 首先要从总的中奖列表中排除掉那些被排除掉的奖品,这些奖品会涉及到概率的值重新计算。
  • 如果排除后剩下的奖品列表小于等于1,则可以直接返回对应信息
  • 接下来就使用随机数工具生产一个100内的随值与奖品列表中的值进行循环比对,算法时间复杂度O(n) 代码 ```java @Component(“defaultRandomDrawAlgorithm”)
    public class DefaultRandomDrawAlgorithm extends BaseAlgorithm {
    //
    /**
    • 随机抽奖
    • 父类中未实现的抽象方法
    • @param strategyId 策略ID
    • @param excludeAwardIds 已经被抽完的奖品ID列表
    • 不用于构建概率元组的奖品信息
    • @return 奖品ID
      */ @Override
      public String randomDraw(Long strategyId, List excludeAwardIds) {

      /**

      • 排除已经被抽完的奖品ID,保存剩下的还可以抽奖的ID
      • 若全部奖品都被抽完了,则直接返回空字符串,说明没有获奖
      • 如果只剩下一个奖品了,那么之间获得该奖品
      • 若有多个排除奖品,则新建抽奖规则随机抽取一个奖品
      • todo: 优化
        */
        // 初始化差异分母
        BigDecimal differenceDenominator = BigDecimal.ZERO;
        // 初始化非抽奖奖品列表
        List differenceAwardRateList = new ArrayList<>(); // 获取概率元组中所有的奖品种类 List awardRateInfos = awardRateInfoMap.get(strategyId); // 遍历所有奖品种类 for (AwardRateInfo awardRateInfo : awardRateInfos) { // 获取奖品ID String awardId = awardRateInfo.getAwardId(); // 如果奖品还没有被抽完则加入差异奖品列表 if (!excludeAwardIds.contains(awardId)) { differenceAwardRateList.add(awardRateInfo); differenceDenominator = differenceDenominator.add(awardRateInfo.getAwardRate()); } } // 如果差异奖品列表为空则返回空字符串 if (differenceAwardRateList.isEmpty()) return ""; // 如果差异奖品列表只有一个则返回该奖品ID if (differenceAwardRateList.size() == 1) return differenceAwardRateList.get(0).getAwardId(); /**
      • 新建规则重新分配概率抽奖
      • 从差异奖品列表中随机抽取奖品
        /
        // 获取随机概率值为0-100的随机数
        SecureRandom secureRandom = new SecureRandom();
        int randomVal = secureRandom.nextInt(100) + 1;
        // 循环获取奖品
        String awardId = “”;
        int cursorVal = 0;
        for (AwardRateInfo awardRateInfo : differenceAwardRateList) {
        // 计算奖品概率值
        // Divide the award rate by the difference denominator, multiply by 100, and round up to the nearest integer
        int rateVal = awardRateInfo.getAwardRate().divide(differenceDenominator, 2, BigDecimal.ROUND_UP).multiply(new BigDecimal(100)).intValue();
        // 如果随机数小于等于当前奖品概率值则返回该奖品ID
        if (randomVal <= (cursorVal + rateVal)) {
        awardId = awardRateInfo.getAwardId();
        break;
        }
        cursorVal += rateVal;
        }
        // 抽中当前奖品ID
        return awardId;
        }
        }
        *** ```

单项概率抽奖

算法描述:单项概率算法不涉及奖品概率重新计算的问题,分配好的概率结果是可以固定下来的 主要是直接获取和查询之前的概率元组

/**  
 * Created by lily via on 2024/6/15 14:59 * 单次抽奖随机抽奖算法实现  
 * 策略模式与工厂模式结合  
 *  
 * 场景A20%、B30%、C50%  
 * 总体概率:如果A奖品抽空后,B和C奖品的概率按照 3:5 均分,  
 * 相当于B奖品中奖概率由 0.3 升为 0.375  
 */@Component("singleRateRandomDrawAlgorithm")  
//@Primary  
public class SingleRateRandomDrawAlgorithm extends BaseAlgorithm {  
  
    /**  
     * @description: 单项随机概率抽奖  
     * @author Lily Via  
     * @date 2024/6/15 17:38  
     * @version 1.0  
     */    @Override  
    public String randomDraw(Long strategyId, List<String> excludeAwardIds) {  
        // 获取策略对应的元祖  
        String[] rateTuple = rateTupleMap.get(strategyId);  
        // todo: 校验rateTuple不为空  
        assert rateTuple != null;  
        // 生成随机索引  
        int randomVal = new SecureRandom().nextInt(100) + 1;  
        // 转换为斐波那契散列索引  
        int idx = hashIdx(randomVal);  
        // 查询是否中奖  
        String awardId = rateTuple[idx];  
        // 如果抽中奖品在排除奖品列表中则返回未中奖(说明这个奖品已经抽完)  
        if (excludeAwardIds.contains(awardId)) {  
            return "未中奖";  
        }  
        return awardId;  
    }  
}

抽奖模块设计模式

模板模式

策略模式