实战项目-抽奖系统

项目介绍

随着数字营销的兴起,企业越来越重视通过在线活动来吸引和留住客户。抽奖活动作为⼀种有效的营销手段,能够显著提升用户参与度和品牌曝光率。于是我们就开发了以抽奖活动作为背景的Spring Boot项目,通过这个项目提供⼀个全面、可靠、易于维护的抽奖平台,该平台将采用以下策略:

  • 集成多种技术组件:利用MySQL、Redis、RabbitMQ等常用组件,构建一个稳定、高效、可扩展的抽奖系统
  • 活动、奖品与人员管理:允许管理人员创建配置抽奖活动,管理奖品信息,管理人员信息
  • 实现状态机的管理:通过精心设计的状态机,精确控制活动及奖品状态的转换,提高系统的可控性和客测试性
  • 保障数据的一致性:通过事务管理和数据同步机制,确保数据的一致性和完整性
  • 加强安全性:实施安全措施,包括数据加密,用户认证,保护用户数据和系统安全
  • 降低维护成本:提供全面的日志记录和异常处理机制,简化问题诊断和系统维护
  • 提高扩展性:采用模块化设计与设计模式的使用,提高系统的灵活性和扩展性

系统设计

系统架构

项目环境:

  • 编程语言:java,javaScript
  • 开发语言包:JDK17
  • 后端框架:SpringBoot
  • 数据库:MySQL
  • 缓存:Redis
  • 消息队列:RabbitMQ
  • 日志:lobback
  • 安全:JWT+加密

业务模块功能

  • 人员业务模块:管理员注册、登录,以及普通用户的创建
  • 活动业务模块:活动管理以及活动状态管理
  • 奖品业务模块:奖品管理与奖品的分配,还包括奖品图的上传
  • 通知业务模块:发送短信、邮件等业务美丽如验证码的发送,中奖通知
  • 抽奖业务模块:完成抽奖动作,以及抽奖后的结果展示

数据库设计

  • 用户表:存储用户信息
  • 活动表:存储活动信息
  • 奖品表:存储奖品信息
  • 活动奖品关联表:存储一个活动下关联了哪些奖品
  • 活动用户关联表:存储一个活动下设置的参与人员
  • 中奖记录表:存储一个活动的中奖名单

安全设计

  • 用户登录身份验证:使用JWT进行用户身份验证,需强制用户在某些页面必须进行登录操作
  • 加密:敏感信息数据加密,例如手机号、用户密码登敏感数据落库需要加密

项目启动

通用处理

1. 错误码

定义错误码类型

定义全局错误码

定义业务错误码—controller层(随业务代码补充)

public interface ControllerErrorCodeConstants {
}

定义业务错误码—service层(随业务代码补充)

public interface ServiceErrorCodeConstants {
}

2. 自定义异常类

ControllerException:controller 层异常类

ServiceException:service 层异常类

3. CommonResult<T>

CommonResult作为控制器层⽅法的返回类型,封装HTTP接口调用的结果,包括成功数据、错误信息和状态码。它可以被SpringBoot框架等自动转换为JSON或其他格式的响应体,发送给客户端。

4. jackson

5. 日志处理

使用SLF4J+logback

Logback 就是 Java 应用中最常用的高性能日志框架,它实现了 SLF4J,配置简单、功能强大,是生产环境日志管理的首选方案之一

用户模块

1. 注册

约定前后端接口

[请求]  /register  POST
{
   "name":"张三",
   "mail":"451@qq.com",
   "phoneNumber":"13188888888",
   "password":"123456789",
   "identity":"ADMIN"
}

[响应] 
{
    "code": 200,
    "data": {
        "userId": 22
    },
    "msg": ""
}

Controller层

实体类


业务代码

service层

实体类


业务代码

校验信息

dao层

实体类


Mapper


TypeHandler

2. 控制层通用异常处理

3. 登录

3.1发送验证码

配置

##短信##
sms.access-key-id=填写自己申请的
sms.access-key-secret=填写自己申请的
sms.sign-name=填写自己申请的

SMSUtil工具类(用来发送短信)

@Component
public class SMSUtil {
    private static final Logger logger = LoggerFactory.getLogger(SMSUtil.class);

    @Value(value = "${sms.sign-name}")
    private String signName;
    @Value(value = "${sms.access-key-id}")
    private String accessKeyId;
    @Value(value = "${sms.access-key-secret}")
    private String accessKeySecret;

    /**
     * 发送短信
............

RedisUtil工具类

CaptchaUtil工具类(用来生成验证码)

Hutool提供

约定前后端接口

[请求]  /verification-code/send?phoneNumber=13199999999   GET

[响应] 
{
   "code": 200,
   "data": true,
   "msg": ""
}

Controller层

Service层

 

 

 

3.2 JWTUtil工具类

3.3管理员登录有两种方式

约定前后端交互接口

密码登录

[请求]  /password/login    POST   

{
   "loginName":"13199999999",
   "password":"123456",
   "mandatoryIdentity":"ADMIN"
}

[响应] 
{
   "code": 200,
   "data": {
   "token": 
     "eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGl0eSI6Ik5PUk1BTCIsInVzZXJJZCI6MjEsImlhdCI6MTcxN
     jI2MjI5OCwiZXhwIjoxNzE2MjY0MDk4fQ.QfiZmZcfzd5ls_t8lg7bsTF7kA0daK-psjUt1QRj9d4",
  "identity": "ADMIN"
    },
   "msg": ""
}

验证码登录

[请求]  /message/login       
{
   POST
   "loginMobile":"13199999999",
   "verificationCode":"0475",
   "mandatoryIdentity":"ADMIN"
}

[响应] 
{
   "code": 200,
   "data": {
   "token": 
   "eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGl0eSI6Ik5PUk1BTCIsInVzZXJJZCI6MjEsImlhdCI6MTcxN
          jI2MjUyMywiZXhwIjoxNzE2MjY0MzIzfQ.XEuwO8AvNcqstbOrkI9kWaMhbN-HN2DfnUYGhJthA3I",
   "identity": "ADMIN"
},
   "msg": ""
}

Controller层

实体类


业务代码


Service层

实体类


接口及其实现

dao层

4. 强制登录

当用户访问非登录注册页面时,例如抽奖页面,如果用户当前尚未登陆,我们希望自动跳转到登陆页面

添加拦截器

注册拦截器

5. 用户管理

人员列表展示

约定前后端交互接口

[请求]  /base-user/find-list  GET

[响应] 
{
   "code": 200,
   "data": [
       {
          "userId": 15,
          "userName": "郭靖",
          "identity": "NORMAL"
      },

      {
          "userId": 14,
          "userName": "王五",
         "identity": "ADMIN"
      }
   ],
   "msg": ""
}

Controller层

实体类


业务代码

Service层

实体类


接口及其实现

Dao层

奖品模块

1. 图片上传

配置路径

## 文件上传#
# 目标路径#
pic.local-path=D:/java/PIC
# springboot3升级配置名#
spring.web.resources.static-locations=classpath:/static/,file:$(pic.local-path}

Controller层

Service层

2. 创建奖品

约定前后端交互接口

[请求]  /prize/create  POST
param: {"prizeName":"吹⻛机","description":"吹⻛机","price":100}
prizePic: Obj-C.jpg  (FILE) 

[响应] 
{
   "code": 200,
   "data": 17,
   "msg": ""
}

Controller层

实体类


Service层

Dao层

3. 奖品列表展示(翻页)

约定前后端接口

[请求]  /prize/find-list?currentPage=1&pageSize=10  GET

[响应] 
{
    "code": 200,
    "data": {
        "total": 3,
        "records": [
            {
                "prizeId": 17,
                "prizeName": "吹⻛机",
                "description": "吹⻛机",
                "price": 100,
                "imageUrl": "d11fa79c-9cfb-46b9-8fb6-3226ba1ff6d6.jpg"
            },
            {
                "prizeId": 13,
                "prizeName": "华为⼿机",
                "description": "华为⼿机",
                "price": 5000,
                "imageUrl": "5a85034b-91b7-48fe-953d-67aef2bdcc2d.jpg"
            },
            {
                "prizeId": 12,
                "prizeName": "咖啡机", "description": "家⽤咖啡机",
                "price": 3000,
                "imageUrl": "https://ts1.cn.mm.bing.net/th/id/R
C.59493f741a4d956f354d241ec1034624?
rik=JpdNO%2bfC3NMONw&riu=http%3a%2f%2fcdn02.ehaier.com%2fproduct%2f5600fa6c1a0a
2ebc278b47e8_1200_1200.jpg&ehk=8MptQ5r5ILWiL4v%2f5mn3s0%2f1H05r1yp%2fL6feezFw89
Q%3d&risl=&pid=ImgRaw&r=0"
            }
        ]
    },
    "msg": ""
}

Controller 层

Service层

Dao层

活动模块

1. 活动创建

约定前后端接口

[请求]  /activity/create POST
{
   "activityName": "抽奖测试",
   "description": "年会抽奖活动",
   "activityPrizeList": [
      {
         "prizeId": 13,
         "prizeAmount": 1,
         "prizeTiers": "FIRST_PRIZE"
      },
      {
         "prizeId": 12,
         "prizeAmount": 1,
         "prizeTiers": "SECOND_PRIZE"
      }
  ],
    "activityUserList": [
        {
            "userId": 25,
            "userName": "郭靖"
        },
        {
            "userId": 23,
            "userName": "杨康"
        }
    ]
}

[响应] 
{
    "code": 200,
    "data": {
        "activityId": 23
    },
    "msg": ""
}

Controller层

Service层

Dao层

2. 活动列表页(翻页)

约定前后端交互接口

[请求]  /activity/find-list?currentPage=1&pageSize=10 GET

[响应] 
{
    "code": 200,
    "data": {
        "total": 10,
        "records": [
            {
                "activityId": 23,
                "activityName": "抽奖测试",
                "description": "年会抽奖活动",
                "valid": true
            },
            {
                "activityId": 22,
                "activityName": "抽奖测试",
                "description": "年会抽奖活动",
                "valid": true
            },
            {
                "activityId": 21,
                "activityName": "节⽇抽奖",
                "description": "⽐特年会抽奖活动",
                "valid": true
            }
        ]
    },
    "msg": ""
}

Contorller层

Service 层

Dao层

抽奖模块

1. 抽奖设计

抽奖过程是 抽奖系统中最重要的核心环节,它需要确保公平、透明且高效

前端:控制抽奖流程,确定中奖人

后端:

  1. 查询活动完整信息
  2. 抽奖:保存中奖信息、扭转对应状态、完成通知行为
  3. 查询中奖列表

1. 参与者注册于奖品创建

  • 参与者注册:管理员通过管理端新增用户,填写必要的信息
  • 奖品建立:奖品需要提前建立好

2. 抽奖活动设置

  • 活动创建:管理员在系统中创建抽奖活动,输入活动名称、描述、奖品列表等信息
  • 圈选人员:关联该抽奖活动的参与者
  • 圈选奖品:圈选该抽奖活动的奖品,设置奖品等级、个数等
  • 活动发布:活动信息发布后,系统通过管理端界面展示活动列表

3. 抽奖请求处理(重要)

  • 随机抽取:前端随机选择后端提供的参与者,确保每次抽取的结果是公平的
  • 请求提交:活动进行时,管理员可发起抽奖请求
  • 消息队列通知:有效的抽奖请求被发送到MQ队列中,等待MQ消费者真正处理抽奖逻辑
  • 请求返回:抽奖的请求处理接口不再完成任何事情,直接返回(时效性)

4. 抽奖结果公布

  • 前端展示:中奖名单通过前端随机抽取的人员,公布展示出来

5. 抽奖逻辑的执行(重要)

  • 消息消费:MQ消费者收到异步消息,系统开始执行以下抽奖逻辑

6. 中奖结果处理

  • 请求验证:系统验证抽奖请求的有效性,如是否满足系统根据设定的规则,幂等性:若消息多发,已抽取的内容不能再次抽取
  • 状态扭转:根据中奖结果扭转活动、奖品、参与者状态
  • 结果记录:中奖结果被记录在数据库中,并同步更新Redis缓存

7. 中奖者通知

  • 通知中奖者:通知中奖者和其他相关系统,需要保证事务的一致性
  • 补救措施:抽奖行为是一次性的,因此异步处理抽奖任务必须保证成功,若过程异常,需采取补救措施

技术实现细节

  • 异步处理:提高抽奖性能,不影响抽奖流程,将抽奖处理放入队列中进行异步处理,且保证了幂等性
  • 活动状态扭转处理:状态扭转会涉及活动及奖品等横向维度扭转 ,不能避免未来不会有其他内容牵扯进活动中,因此对于状态扭转处理,需要提高扩展性(设计模式)与维护性
  • 并发处理:中奖者通知,可能要通知多系统,但相互解耦,可以设计为并发处理,加快抽奖效率
  • 事务处理:在抽奖逻辑执行时,如若发生异常,需要保证数据库原子性,事务一致性,因此要做好事务处理

查询活动完整信息

抽奖

2. RabbitMQ

DirectRabbitConfig配置

@Configuration
public class DirectRabbitConfig {
    public static final String QUEUE_NAME = "DirectQueue";
    public static final String EXCHANGE_NAME = "DirectExchange";
    public static final String ROUTING = "DirectRouting";

    public static final String DLX_QUEUE_NAME = "DlxDirectQueue";
    public static final String DLX_EXCHANGE_NAME = "DlxDirectExchange";
    public static final String DLX_ROUTING = "DlxDirectRouting";

    /**
     * 队列 起名:DirectQueue
     *
     * @return
     */
    @Bean
    public Queue directQueue() {
        // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
        //   return new Queue("DirectQueue",true,true,false);

        // 一般设置一下队列的持久化就好,其余两个就是默认false
        // return new Queue(QUEUE_NAME,true);

        // 普通队列绑定死信交换机
        return QueueBuilder.durable(QUEUE_NAME)
                .deadLetterExchange(DLX_EXCHANGE_NAME)
                .deadLetterRoutingKey(DLX_ROUTING).build();
    }

    /**
     * Direct交换机 起名:DirectExchange
     *
     * @return
     */
    @Bean
..............

3. 抽奖请求处理

约定前后端接口


[请求]  /draw-prize  POST

{
   "winnerList":[
      {
         "userId":15,
         "userName":"胡⼀博"
      },
      {
         "userId":21,
         "userName":"范闲"
      }
  ],
   "activityId":23,
   "prizeId":13,
   "prizeTiers":"FIRST_PRIZE",
   "winningTime":"2024-05-21T11:55:10.000Z"
}


[响应] 

{
   "code": 200,
   "data": true,
   "msg": ""
}

Controller层

Service层

4. MQ异步抽奖逻辑执行

4.1 消费MQ消息

消费者类MqReceiver实现

4.2 请求验证(核对抽奖信息有效性)

4.3 状态转换

新增转换方法

新增策略

ActivityOperator

PrizeOperator

UserOperator


责任链实现

注意:Map

类被 Spring 管理,且 operatorMap 上标注了 @Autowired Spring 容器在初始化 Bean 时,会自动将特定接口或抽象类的所有实现类注入到这个 Map 中

4.4 结果记录

4.5 中奖者通知

1. 邮件服务

2.短信服务

阿里云提供的服务

Hi,${name}。恭喜你在${activityName}活动中获得${prizeTiers},奖品为
${prizeName}。获奖时间为${winningTime},请尽快领取您的奖励!

3. 配置线程池

4. 并发通知

4.6 事务一致性-异常回滚

在业务的返回场景中,MQ消费过程里,不仅仅修改了数据库表的内容,还想redis缓存中新增了很多热点数据。例如扭转了奖品及活动的状态,还将中奖名单落入库中。在这个过程中,一旦出现异常,必须要保证该事务的特性

在proccess中,抛出异常前先catch,运行rollback方法,再抛出

抛出异常的核心目的:触发消息重试(Nack)

在 Spring-Rabbit 中,监听器方法的执行结果决定了消息的命运:

  • 方法正常返回(无异常):RabbitMQ 自动发送 Ack 确认,消息从队列中移除

  • 方法抛出异常(且未捕获):RabbitMQ 自动发送 Nack(否定确认),消息会重新入队(等待下次消费,直到重试次数耗尽)。

 是为了告诉 MQ:“这条消息我处理失败了,请稍后重试”

ManageService层

DrawPrizeService层

WinningRecordMapper

4.7 保证消费消息成功(加入死信队列)

虽然已经保证了事务的一致性,但目前未能保证该消息被成功消费,因此加入死信队列

消费死信队列

5. 中奖名单

约定前后端接口

[请求]  /winning-records/show   POST
{
   "activityId":23
}

[响应] 
{
    "code": 200,
    "data": [
        {
            "winnerId": 15,
            "winnerName": "胡⼀博",
            "prizeName": "华为⼿机",
            "prizeTier": "⼀等奖",
            "winningTime": "2024-05-21T11:55:10.000+00:00"
        },
        {
            "winnerId": 21,
            "winnerName": "范闲",
            "prizeName": "华为⼿机",
            "prizeTier": "⼀等奖",
            "winningTime": "2024-05-21T11:55:10.000+00:00"
        }
    ],
    "msg": ""
}

controller层

实体类

Service层

Dao层

项目成功部署

由于阿里云短信申请限制,无法成功发送验证码,其余均可正常运行

 

内容概要:本文围绕可变桨叶四旋翼无人机的规范控制与点对点运动模拟展开,重点研究优化推力分配策略在翻转动作中的应用与性能比较。通过Matlab代码实现,构建了四旋翼动力学模型,并设计了多种控制算法以实现精确的姿态调整与轨迹跟踪。研究对比了不同推力分配方案在执行高机动性翻转动作时的稳定性、能耗效率与响应速度,旨在提升无人机在复杂飞行任务中的动态性能与控制精度。该仿真研究为无人机飞控系统的设计与优化提供了理论依据和技术支持。; 适合人群:具备一定自动控制理论基础和Matlab编程能力,从事无人机控制、飞行器动力学或机器人系统研究的科研人员及研究生。; 使用场景及目标:① 实现四旋翼无人机在三维空间中的精确点对点运动控制;② 对比分析不同推力分配策略在执行翻转等高难度动作时的控制效果与能耗表现,优化飞行性能;③ 为无人机自主飞行、特技飞行及复杂环境下的机动控制提供算法验证平台。; 阅读建议:此资源以Matlab仿真为核心,建议读者结合相关控制理论知识,深入理解代码实现细节,重点关注动力学建模、控制律设计与推力分配模块。在学习过程中,应动手调试参数,复现文中翻转动作的仿真结果,并尝试拓展至其他复杂飞行任务,以加深对无人机控制机理的理解。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值