通用营销抽奖模块-设计实现(第二部分:活动领域)

通用营销抽奖模块-设计实现(第二部分:活动领域)

引言

这篇文章主要讲解的是通用营销抽奖模块的设计实现的第一部分,抽奖部分的实现。

流程设计(流程图)

详细设计

数据库设计


create table raffle_activity
(
    id              bigint unsigned auto_increment comment '自增ID'
        primary key,
    activity_id     bigint                               not null comment '活动ID',
    activity_name   varchar(64)                          not null comment '活动名称',
    activity_desc   varchar(128)                         not null comment '活动描述',
    begin_date_time datetime                             not null comment '开始时间',
    end_date_time   datetime                             not null comment '结束时间',
    strategy_id     bigint                               not null comment '抽奖策略ID',
    state           varchar(8) default 'create'          not null comment '活动状态',
    create_time     datetime   default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time     datetime   default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    constraint uq_activity_id
        unique (activity_id),
    constraint uq_strategy_id
        unique (strategy_id)
)
    comment '抽奖活动表';

create index idx_begin_date_time
    on raffle_activity (begin_date_time);

create index idx_end_date_time
    on raffle_activity (end_date_time);


create table raffle_activity_count
(
    id                bigint unsigned auto_increment comment '自增ID'
        primary key,
    activity_count_id bigint                             not null comment '活动次数编号',
    total_count       int                                not null comment '总次数',
    day_count         int                                not null comment '日次数',
    month_count       int                                not null comment '月次数',
    create_time       datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time       datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    constraint uq_activity_count_id
        unique (activity_count_id)
)
    comment '抽奖活动次数配置表';



create table raffle_activity_order_001
(
    id              bigint unsigned auto_increment comment '自增ID'
        primary key,
    user_id         varchar(32)                           not null comment '用户ID',
    sku             bigint                                not null comment '商品sku',
    activity_id     bigint                                not null comment '活动ID',
    activity_name   varchar(64)                           not null comment '活动名称',
    strategy_id     bigint                                not null comment '抽奖策略ID',
    order_id        varchar(12)                           not null comment '订单ID',
    order_time      datetime                              not null comment '下单时间',
    total_count     int                                   not null comment '总次数',
    day_count       int                                   not null comment '日次数',
    month_count     int                                   not null comment '月次数',
    state           varchar(16) default 'complete'        not null comment '订单状态(complete)',
    out_business_no varchar(64)                           not null comment '业务仿重ID - 外部透传的,确保幂等',
    create_time     datetime    default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time     datetime    default CURRENT_TIMESTAMP not null comment '更新时间',
    constraint uq_order_id
        unique (order_id),
    constraint uq_out_business_no
        unique (out_business_no)
)
    comment '抽奖活动单';

create index idx_user_id_activity_id
    on raffle_activity_order_001 (user_id, activity_id, state);


create table raffle_activity_sku
(
    id                  int unsigned auto_increment comment '自增ID'
        primary key,
    sku                 bigint                             not null comment '商品sku - 把每一个组合当做一个商品',
    activity_id         bigint                             not null comment '活动ID',
    activity_count_id   bigint                             not null comment '活动个人参与次数ID',
    stock_count         int                                not null comment '商品库存',
    stock_count_surplus int                                not null comment '剩余库存',
    product_amount      decimal(10, 2)                     not null comment '商品金额【积分】',
    create_time         datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time         datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    constraint uq_sku
        unique (sku)
);

create index idx_activity_id_activity_count_id
    on raffle_activity_sku (activity_id, activity_count_id);



create table raffle_activity_account
(
    id                  bigint unsigned auto_increment comment '自增ID'
        primary key,
    user_id             varchar(32)                        not null comment '用户ID',
    activity_id         bigint                             not null comment '活动ID',
    total_count         int                                not null comment '总次数',
    total_count_surplus int                                not null comment '总次数-剩余',
    day_count           int                                not null comment '日次数',
    day_count_surplus   int                                not null comment '日次数-剩余',
    month_count         int                                not null comment '月次数',
    month_count_surplus int                                not null comment '月次数-剩余',
    create_time         datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time         datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    constraint uq_user_id_activity_id
        unique (user_id, activity_id)
)
    comment '抽奖活动账户表';


create table raffle_activity_account_day
(
    id                int unsigned auto_increment comment '自增ID'
        primary key,
    user_id           varchar(32)                        not null comment '用户ID',
    activity_id       bigint                             not null comment '活动ID',
    day               varchar(10)                        not null comment '日期(yyyy-mm-dd)',
    day_count         int                                not null comment '日次数',
    day_count_surplus int                                not null comment '日次数-剩余',
    create_time       datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time       datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    constraint uq_user_id_activity_id_day
        unique (user_id, activity_id, day)
)
    comment '抽奖活动账户表-日次数';


create table raffle_activity_account_month
(
    id                  int unsigned auto_increment comment '自增ID'
        primary key,
    user_id             varchar(32)                        not null comment '用户ID',
    activity_id         bigint                             not null comment '活动ID',
    month               varchar(7)                         not null comment '月(yyyy-mm)',
    month_count         int                                not null comment '月次数',
    month_count_surplus int                                not null comment '月次数-剩余',
    create_time         datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time         datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    constraint uq_user_id_activity_id_month
        unique (user_id, activity_id, month)
)
    comment '抽奖活动账户表-月次数';


create table daily_behavior_rebate
(
    id            int unsigned auto_increment comment '自增ID'
        primary key,
    behavior_type varchar(16)                        not null comment '行为类型(sign 签到、openai_pay 支付)',
    rebate_desc   varchar(128)                       not null comment '返利描述',
    rebate_type   varchar(16)                        not null comment '返利类型(sku 活动库存充值商品、integral 用户活动积分)',
    rebate_config varchar(32)                        not null comment '返利配置',
    state         varchar(12)                        not null comment '状态(open 开启、close 关闭)',
    create_time   datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time   datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
    comment '日常行为返利活动配置';

create index idx_behavior_type
    on daily_behavior_rebate (behavior_type);




create table task
(
    id          int unsigned auto_increment comment '自增ID'
        primary key,
    user_id     varchar(32)                           not null comment '用户ID',
    topic       varchar(32)                           not null comment '消息主题',
    message_id  varchar(11)                           null comment '消息编号',
    message     varchar(512)                          not null comment '消息主体',
    state       varchar(16) default 'create'          not null comment '任务状态;create-创建、completed-完成、fail-失败',
    create_time datetime    default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time datetime    default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    constraint uq_message_id
        unique (message_id)
)
    comment '任务表,发送MQ';

create index idx_create_time
    on task (update_time);

create index idx_state
    on task (state);



create table user_raffle_order_002
(
    id            int unsigned auto_increment
        primary key,
    user_id       varchar(32)                           not null comment '用户ID',
    activity_id   bigint                                not null comment '活动ID',
    activity_name varchar(64)                           not null comment '活动名称',
    strategy_id   bigint                                not null comment '抽奖策略ID',
    order_id      varchar(12)                           not null comment '订单ID',
    order_time    datetime                              not null comment '下单时间',
    order_state   varchar(16) default 'create'          not null comment '订单状态;create-创建、used-已使用、cancel-已作废',
    create_time   datetime    default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time   datetime    default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    constraint uq_order_id
        unique (order_id)
)
    comment '用户抽奖订单表';

create index idx_user_id_activity_id
    on user_raffle_order_002 (user_id, activity_id);


create table user_behavior_rebate_order_000
(
    id              int unsigned auto_increment comment '自增ID'
        primary key,
    user_id         varchar(32)                        not null comment '用户ID',
    order_id        varchar(12)                        not null comment '订单ID',
    behavior_type   varchar(16)                        not null comment '行为类型(sign 签到、openai_pay 支付)',
    rebate_desc     varchar(128)                       not null comment '返利描述',
    rebate_type     varchar(16)                        not null comment '返利类型(sku 活动库存充值商品、integral 用户活动积分)',
    rebate_config   varchar(32)                        not null comment '返利配置【sku值,积分值】',
    out_business_no varchar(64)                        not null comment '业务仿重ID - 外部透传,方便查询使用',
    biz_id          varchar(128)                       not null comment '业务ID - 拼接的唯一值。拼接 out_business_no + 自身枚举',
    create_time     datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time     datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    constraint uq_biz_id
        unique (biz_id),
    constraint uq_order_id
        unique (order_id)
)
    comment '用户行为返利流水订单表';

create index idx_user_id
    on user_behavior_rebate_order_000 (user_id);

分库分表设计

配置库(单库单表)​

存储活动配置等静态数据(如活动规则,活动参与次数,),无需分库分表。

分库分表(用户行为数据)​

比如 用户活动单表

  • 分库数:2个物理库(big_market_01big_market_02)。

  • 分表数:每个库包含4张表(如 order_00order_01 等)。

  • 总逻辑分片数2库 × 4表 = 8个分片。​

  • 路由规则:哈希取模法

    • 分片键:用户ID(userId)。

    • 哈希计算逻辑
      userId 进行哈希取模,确定数据所属分片位置:

      int hash = userId.hashCode();
      int shardIndex = hash & (总逻辑分片数 - 1);  // 等价于 hash % 8

      例如:hash = 6shardIndex = 6

    • 分片索引映射库表

      • 库编号shardIndex / 4(取商,整数除法)

        • 结果范围:0或1 → 对应 big_market_01big_market_02

      • 表编号shardIndex % 4(取余)

        • 结果范围:0-3 → 对应表后缀 _00_03

      shardIndex=6 为例:

      库编号 = 6 / 4 = 1 → big_market_02
      表编号 = 6 % 4 = 2 → order_02

      最终数据路由到 big_market_02.order_02

缓存设计

活动信息

活动抽奖次数

活动的sku剩余数量

详细设计

充值活动账户(领取活动抽奖机会)

功能诉求

在基于活动、次数所组合的活动 sku,用户参与活动就相当于,下单sku给自己的活动账户充值可参与的额度次数。实现活动的下单的过程落入到数据库中。

我们可以把所有获取抽奖次数的事件成为一个个sku 【打卡、签到、分享、对话、积分兑换】 那么这些都是sku 。

在【打卡、签到、分享、对话、积分兑换】等行为动作下,创建出活动订单,给用户的活动账户【日、月】充值可用的抽奖次数。

对于用户可获得的抽奖次数,比如首次进来就有一次,则是依赖于运营配置的动作,在前端页面上。用户点击后,可以获得一次抽奖次数。

详细设计

在这里我们需要注意的就是其中有一个活动规则责任链校验,并且我们在保存订单的同时还要添加用户的活动账号里的 抽奖次数。

queryNoUsedRaffleOrder​ 的作用是 ​避免用户重复参与活动,确保用户在同一时间只能有一个有效的抽奖订单。

持久化到数据库的逻辑

活动规则责任链校验

责任链的设计在上一篇文章已经使用过了,我这里就不详细讲了。

活动规则过滤【日期、状态】

商品库存规则节点,在这里扣减sku的方式是什么呢?

在这里我们可以使用 decr 操作,在给每一个库存设置 setnx。

你在每一段设置一个锁是为什么呢?

避免redis宕机时,运维恢复错误库存。

活动装配预热

这里和上一张的策略装配类似,以后会写成一个接口,这里就是查询数据 缓存到 redis 之中。

总结流程图

参与活动抽奖(进行抽奖之前的对活动的判断)

功能诉求

这里呢 就是用户在我们点击抽奖后 就会创建一条参与的订单,同时扣减用户的额度,并且在用户的活动账户总数量中记录用户的抽奖剩余次数,在月数量和日数量中进行写操作。

功能设计

这个就按照流程图中的设计就好了。

记录抽奖流水记录(用户抽奖完成记录订单号对应的抽奖结果)

功能诉求

在这里我们实现的记录抽奖流水记录,一整个抽奖过程先从活动账户额度充值、活动参与、抽奖执行【策略】,之后就是中奖记录写入和后续的发奖。实现的就是这里的奖品记录写入和异步消息发送。

会用到 task 任务表,在写入奖品记录的时候,写入一条 task 消息发送任务,作为补偿使用。当 mq 发送失败的时候,则由任务扫描 task 消息进行发送。

我们要实现的流程就如上图一样。

详细设计

这里我们就是记录了一个用户中奖的流水,也就是用户这个订单 中了什么奖品,我们中了某一个奖品,比如awardId = 101 我们就会发送一个mq 去完成我们的奖品发放。

对于我们发送mq失败的,我们会有一个定时任务扫描 那个在task任务表中 状态为失败的 记录 ,让他重新发送。

总的抽奖流程回顾

这个就是我们目前完成的一个抽奖流程了,对于这个降级大家先不用考虑。

查询抽奖列表数据

功能诉求

根据用户ID和活动ID查询抽奖列表数据,满足后续前端展示抽奖列表时可以渲染出奖品是否被加锁并提示用户还需要抽奖几次才能解锁奖品。

详细设计

创建用户行为返利

功能诉求

我们会在这个签到这样的日常行为中 进行返利,像是这样的行为 我们需要去进行用户行为返利流水的一个记录工作。

详细设计

我们可以看这个流程图。

消费用户行为返利

我们创建接收mq。

LICENSED UNDER CC BY-NC-SA 4.0