【抽奖系统开发实战】Spring Boot 项目的设计思路、技术选型与公共模块处理

文章目录

一、背景

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

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

要不要我把这段内容整理成Markdown格式,直接嵌入到你的博客文章里?

二、核心需求描述

  1. 管理员注册与登录
    • 注册信息:姓名、邮箱、手机号、密码。
    • 登录方式:① 电话+密码登录;② 电话+短信登录(需获取验证码)。
    • 登录校验:需验证管理员身份。
  2. 人员管理
    • 管理员可创建普通用户,创建信息包括姓名、邮箱、手机号。
    • 支持查看人员列表,展示人员id、姓名、身份(普通用户/管理员)。
  3. 奖品管理
    • 管理员可创建奖品,信息包括奖品名称、描述、价格、奖品图(支持上传)。
    • 支持奖品列表展示(可翻页),展示内容包括奖品id、奖品图、奖品名、奖品描述、奖品价值(元)。
  4. 活动管理
    • 管理员可创建活动,信息包括:活动名称、活动描述、圈选奖品(勾选奖品并设置等级及数量)、圈选参与人员。
    • 支持活动列表展示(可翻页),展示活动名称、描述、活动状态:
      • 进行中:点击"活动进行中,去抽奖"按钮跳转抽奖页。
      • 已完成:点击"活动已完成,查看中奖名单"按钮跳转查看结果。
  5. 抽奖页面
    • 仅管理员可对进行中的活动发起抽奖。
    • 每轮抽奖中奖人数与当前奖品数量一致,每人仅能中一次奖。
    • 多轮抽奖流程:① 展示奖品信息(奖品图、份数)→ ② 人名闪动 → ③ 停止闪动确定中奖名单:
      • 点击"开始抽奖"跳转至人名闪动画面;
      • 点击"点我确定"确认中奖名单;
      • 点击"已抽完,下一步",若有未抽取奖品则展示下一个,否则展示全部中奖名单;
      • 点击"查看上一奖项"展示上一个奖品信息。
    • 异常处理:抽奖过程中刷新页面,已抽取成功的奖项不可重新抽取;刷新后若当前奖品已抽完,点击"开始抽奖"直接展示该奖品中奖名单。
    • 活动已完成:展示所有奖项的全部中奖名单,新增"分享结果"按钮,点击可复制链接,链接打开后仅展示活动名称与中奖结果,保留"分享结果"按钮。
  6. 通知功能
    • 抽奖完成后,需通过邮件和短信通知中奖者。
  7. 登录强制校验
    • 管理端所有页面(含抽奖页)需强制管理员登录后方可访问,未登录则强制跳转登录页面。

三、系统设计

3.1 系统架构

  • 前端:使用静态 HTML + CSS + JavaScript 构建页面,基于 Bootstrap 4 完成布局与样式,使用 jQuery 及其表单校验插件通过 AJAX 调用后端 REST 接口,实现活动管理、用户注册登录、抽奖展示等交互功能。
  • 后端:基于 Spring Boot 3 + Spring MVC 搭建 Web 应用,使用 MyBatis 作为持久层框架,实现业务逻辑。
  • 数据库:使用MySQL作为主数据库,存储用户数据和活动信息,降低数据库压力、提升读写性能。
  • 缓存:使用Redis作为缓存层,减少数据库访问次数。
  • 消息队列:使用RabbitMQ处理异步任务(如抽奖行为),提高系统解耦性与吞吐量。
  • 日志与安全:使用JWT进行用户认证,SLF4J+logback完成日志记录。
  • 其他组件:使用 Lombok 减少样板代码,使用 Hutool 提供通用工具能力,使用 Spring Validation 进行接口参数校验,使用 Spring Mail 发送邮件通知,结合 阿里云短信 SDK 实现短信验证码能力。

3.2 项目环境

  • 编程语言:Java(后端)、JavaScript(前端)。
  • 开发工具包:JDK 17。
  • 后端框架:Spring Boot3。
  • 数据库:MySQL。
  • 缓存:Redis。
  • 消息队列:RabbitMQ。
  • 日志:logback。
  • 安全:JWT + 数据加密。

3.3 业务功能模块

  • 人员业务模块:管理员注册、登录,普通用户创建。
  • 活动业务模块:活动管理及活动状态管理。
  • 奖品业务模块:奖品管理、奖品分配及奖品图上传。
  • 通知业务模块:短信、邮件发送(如验证码发送、中奖通知)。
  • 抽奖业务模块:抽奖动作执行及抽奖结果展示。

3.4 数据库设计

核心表结构:系统包含6张核心数据表。

  • 用户表(user):存储用户信息,如用户名,密码,邮箱等。

  • 活动表(activity):存储活动信息,如活动名称,描述,活动状态等。

  • 奖品表(prize):存储奖品信息,如奖品名称,奖品图等。

  • 活动奖品关联表(activity_prize):存储活动与奖品的关联关系

  • 活动用户关联表(activity_user):存储活动与参与人员的关联关系

  1. 中奖记录表(winning_record):存储中奖信息,如活动Id,奖品Id,中奖者Id等。

3.5代码结构设计

四、功能模块设计

4.1 通用处理

1) 错误码设计

错误码的作用:

  • 明确性:错误码提供了一种明确的方式来表示错误的状态。与模糊的异常消息相比,错误码能够精确地指出问题所在。
  • 易检索:错误码通常是数字,便于在日志、监控系统或错误跟踪系统中检索和过滤。
  • 客户端适配:客户端可以根据错误码进行特定的错误处理,而不是依赖于通用的异常处理。
  • 维护性:集中管理错误码使得它们更容易维护和更新。如果业务逻辑发生变化,只需要更新错误码的定义,而不需要修改每个使用它们的地方。在接口文档中,错误码也可以清晰地列出所有可能的错误情况,使开发者更容易理解和使用接口。
  • 调试测试:支持自动化测试,确保错误场景被正确处理。
  • 错误分类:错误码可以帮助将错误分类为不同的级别或类型,如客户端错误、服务器错误、业务逻辑错误等。

错误码实现:

  1. 错误码实体类
java 复制代码
@Data
public class ErrorCode {
    /**
     * 错误码
     */
    private final Integer code;
    /**
     * 错误提示
     */
    private final String msg;

    public ErrorCode(Integer code, String message) {
        this.code = code;
        this.msg = message;
    }
}
  1. 全局错误码常量
java 复制代码
public interface GlobalErrorCodeConstants {
    ErrorCode SUCCESS = new ErrorCode(200, "成功");

    // ========== 服务端错误段 ==========
    ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
    ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启");
    ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "错误的配置项");

    ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
}
  1. 业务错误码(预留接口)
  • Controller层错误码:
java 复制代码
public interface ControllerErrorCodeConstants {
}
  • Service层错误码:
java 复制代码
public interface ServiceErrorCodeConstants {
}
2) 自定义异常类

ControllerException(Controller层异常):

java 复制代码
@Data
@EqualsAndHashCode(callSuper = true)
public class ControllerException extends RuntimeException {
    /**
     * 业务错误码
     * @see com.example.common.exception.errorcode.ControllerErrorCodeConstants
     */
    private Integer code;
    /**
     * 错误提示
     */
    private String message;

    public ControllerException() {
    }

    public ControllerException(ErrorCode errorCode) {
        this.code = errorCode.getCode();
        this.message = errorCode.getMsg();
    }

    public ControllerException(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

ServiceException(Service层异常):

java 复制代码
@Data
@EqualsAndHashCode(callSuper = true)
public class ServiceException extends RuntimeException {
    /**
     * 业务错误码
     * @see com.example.common.exception.errorcode.ServiceErrorCodeConstants
     */
    private Integer code;
    /**
     * 错误提示
     */
    private String message;

    /**
     * 空构造方法,避免反序列化问题
     */
    public ServiceException() {
    }

    public ServiceException(ErrorCode errorCode) {
        this.code = errorCode.getCode();
        this.message = errorCode.getMsg();
    }

    public ServiceException(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}
3) 统一响应结果(CommonResult)

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

设计目的:

  • 统一返回格式:确保客户端收到的响应结构一致,无关业务逻辑。
  • 包含错误信息:提供错误码(code)和错误消息(msg),便于客户端识别处理。
  • 泛型数据返回:支持返回任意类型数据,提升灵活性。
  • 便捷创建方法:提供error()success()静态方法,快速创建响应对象。
  • 错误码集成:复用预定义错误码,保持一致性。
  • 支持序列化:实现Serializable接口,可转换为JSON/XML等格式传输。
  • 业务逻辑解耦:分离业务逻辑与HTTP响应构建,后端专注业务实现。
  • 客户端友好:降低客户端错误处理复杂度。

实现代码:

java 复制代码
@Data
public class CommonResult<T> implements Serializable {
    /**
     * 错误码
     * @see ErrorCode#getCode()
     */
    private Integer code;
    /**
     * 返回数据
     */
    private T data;
    /**
     * 错误提示,用户可阅读
     * @see ErrorCode#getMsg()
     */
    private String msg;

    /**
     * 将传入的 result 对象,转换成另外一个泛型结果的对象
     * @param result 传入的 result 对象
     * @param <T> 返回的泛型
     * @return 新的 CommonResult 对象
     */
    public static <T> CommonResult<T> error(CommonResult<?> result) {
        return error(result.getCode(), result.getMsg());
    }

    public static <T> CommonResult<T> error(Integer code, String message) {
        Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!");
        CommonResult<T> result = new CommonResult<>();
        result.code = code;
        result.msg = message;
        return result;
    }

    public static <T> CommonResult<T> error(ErrorCode errorCode) {
        return error(errorCode.getCode(), errorCode.getMsg());
    }

    public static <T> CommonResult<T> success(T data) {
        CommonResult<T> result = new CommonResult<>();
        result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
        result.data = data;
        result.msg = "";
        return result;
    }

    public static boolean isSuccess(Integer code) {
        return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode());
    }
}
4) JSON序列化工具类(JacksonUtil)
java 复制代码
public class JacksonUtil {
    private JacksonUtil() {
    }

    /**
     * 静态代码块单例
     */
    private final static ObjectMapper OBJECT_MAPPER;

    static {
        OBJECT_MAPPER = new ObjectMapper();
    }

    private static ObjectMapper getObjectMapper() {
        return OBJECT_MAPPER;
    }

    private static <T> T tryParse(Callable<T> parser) {
        return tryParse(parser, JacksonException.class);
    }

    private static <T> T tryParse(Callable<T> parser, Class<? extends Exception> check) {
        try {
            return parser.call();
        } catch (Exception ex) {
            if (check.isAssignableFrom(ex.getClass())) {
                throw new JsonParseException(ex);
            }
            throw new IllegalStateException(ex);
        }
    }

    /**
     * 反序列化
     * @param content
     * @param valueType
     * @return
     * @param <T>
     */
    public static <T> T readValue(String content, Class<T> valueType) {
        return JacksonUtil.tryParse(() ->
                JacksonUtil.getObjectMapper().readValue(content, valueType));
    }

    /**
     * 反序列化list
     * @param content
     * @param parameterClasses
     * @return
     * @param <T>
     */
    public static <T> T readListValue(String content, Class<?> parameterClasses) {
        return JacksonUtil.tryParse(() ->
                JacksonUtil.getObjectMapper().readValue(content,
                        JacksonUtil.getObjectMapper()
                                .getTypeFactory()
                                .constructParametricType(List.class, parameterClasses)));
    }

    /**
     * 序列化
     * @param value
     * @return
     */
    public static String writeValueAsString(Object value) {
        return JacksonUtil.tryParse(() ->
                JacksonUtil.getObjectMapper().writeValueAsString(value));
    }
}
5) 日志处理

日志是程序开发与维护的重要工具,从 JavaSE 阶段的System.out.print到 Spring 框架的控制台日志,均用于发现、定位和分析问题。随着项目复杂度提升,日志的用途不再局限于问题排查,还需满足用户操作记录、数据统计、安全审计等需求,而System.out.print无法适配这些场景,因此需要专业的日志框架。

日志的核心用途:

  1. 系统监控:
    记录系统运行状态、方法响应时间与响应状态,通过数据分析设置阈值报警(如统计关键字出现次数触发报警),是成熟系统的标配能力。
  2. 采集的数据可用于多场景应用:
    数据统计:统计页面浏览量(PV)、访客量(UV)、点击量等,优化运营策略;
    推荐排序:记录用户浏览历史、停留时长等行为数据,为算法模型训练提供支撑,适用于购物、广告、新闻等领域。
  3. 日志审计:
    响应国家政策法规与行业标准要求,保障系统安全:
    追溯操作行为:记录运营人员对数据的删除、修改操作,明确责任主体;
    防范安全风险:通过日志分析非法攻击、非法调用及内部违规行为(如客户信息泄露),为事后调查提供依据。

日志配置

Spring Boot 内置 SLF4J 日志框架,可直接调用,配置步骤如下:

  • 新增 application.properties 配置:
bash 复制代码
## logback xml 配置路径
logging.config=classpath:logback-spring.xml
## 环境激活(开发环境)
spring.profiles.active=dev

# 部署后切换为测试/生产环境
# spring.profiles.active=test
# spring.profiles.active=prod
  • 新增配置文件: logback-spring.xml
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!-- 开发环境(dev)配置:日志输出到控制台 -->
    <springProfile name="dev">
        <!-- 控制台输出器 -->
        <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <!-- 日志格式:时间 线程 级别 日志器 消息 异常信息 -->
                <pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n%ex</pattern>
            </encoder>
        </appender>
        <!-- 根日志级别:INFO(仅输出INFO及以上级别日志) -->
        <root level="info">
            <appender-ref ref="console" />
        </root>
    </springProfile>

    <!-- 测试/生产环境(prod、test)配置:日志按级别输出到文件 -->
    <springProfile name="prod,test">
        <!-- 日志存储路径与应用名称配置 -->
        <property name="logback.logErrorDir" value="/root/lottery-system/logs/error"/>
        <property name="logback.logInfoDir" value="/root/lottery-system/logs/info"/>
        <property name="logback.appName" value="lotterySystem"/>
        <contextName>${logback.appName}</contextName>

        <!-- 1. ERROR级别日志配置 -->
        <appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 当天ERROR日志文件路径 -->
            <File>${logback.logErrorDir}/error.log</File>
            <!-- 日志级别过滤器:仅接收ERROR级别日志 -->
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                <level>ERROR</level>
                <onMatch>ACCEPT</onMatch> <!-- 匹配级别则接收 -->
                <onMismatch>DENY</onMismatch> <!-- 不匹配则拒绝 -->
            </filter>
            <!-- 滚动策略:按时间切分,归档每日日志 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!-- 归档日志文件名格式:error.年-月-日.log -->
                <FileNamePattern>${logback.logErrorDir}/error.%d{yyyy-MM-dd}.log</FileNamePattern>
                <maxHistory>14</maxHistory> <!-- 保留最近14天日志 -->
                <!-- <totalSizeCap>1GB</totalSizeCap> --> <!-- 可选:日志总大小上限,超量删除旧日志 -->
            </rollingPolicy>
            <!-- 日志编码与格式 -->
            <encoder>
                <charset>UTF-8</charset>
                <pattern>%d [%thread] %-5level %logger{36} %line - %msg%n%ex</pattern>
            </encoder>
        </appender>

        <!-- 2. INFO级别日志配置 -->
        <appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 当天INFO日志文件路径 -->
            <File>${logback.logInfoDir}/info.log</File>
            <!-- 自定义过滤器:仅接收INFO级别日志(需实现InfoLevelFilter类) -->
            <filter class="com.example.xxxx"/> <!-- 填写自定义过滤器全限定路径 -->
            <!-- 滚动策略:与ERROR日志一致 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <FileNamePattern>${logback.logInfoDir}/info.%d{yyyy-MM-dd}.log</FileNamePattern>
                <maxHistory>14</maxHistory>
                <!-- <totalSizeCap>1GB</totalSizeCap> -->
            </rollingPolicy>
            <!-- 日志编码与格式 -->
            <encoder>
                <charset>UTF-8</charset>
                <pattern>%d [%thread] %-5level %logger{36} %line - %msg%n%ex</pattern>
            </encoder>
        </appender>

        <!-- 根日志级别:INFO(输出INFO及以上级别日志到对应文件) -->
        <root level="info">
            <appender-ref ref="fileErrorLog" /> <!-- ERROR日志输出到文件 -->
            <appender-ref ref="fileInfoLog"/> <!-- INFO日志输出到文件 -->
        </root>
    </springProfile>
</configuration>

自定义过滤器(InfoLevelFilter):

为实现测试 / 生产环境中info.log仅打印 INFO 级别日志,需自定义过滤器类:

java 复制代码
public class InfoLevelFilter extends Filter<ILoggingEvent> {
    @Override
    public FilterReply decide(ILoggingEvent iLoggingEvent) {
        // 仅接收INFO级别日志,其他级别拒绝
        if (iLoggingEvent.getLevel().toInt() == Level.INFO.toInt()){
            return FilterReply.ACCEPT;
        }
        return FilterReply.DENY;
    }
}
  • 注:需将logback-spring.xml中fileInfoLog的替换为该类的全限定路径。
相关推荐
健康平安的活着2 小时前
java8案例对list[过滤、分组,转换,查找等]清洗逻辑
java·数据结构·list
花间相见2 小时前
【JAVA基础09】—— 赋值与三元运算符:从基础到实操的避坑指南
java·开发语言·python
ywlovecjy2 小时前
windows配置永久路由
java
草莓熊Lotso2 小时前
Linux 进程间通信之命名管道(FIFO):跨进程通信的实用方案
android·java·linux·运维·服务器·数据库·c++
苍何2 小时前
《OpenClaw 从入门到精通指南》正式发布,开源免费!
后端
小江的记录本2 小时前
【AOP】AOP-面向切面编程 (系统性知识体系全解)
java·前端·后端·python·网络协议·青少年编程·代理模式
XiaoLeisj2 小时前
Android 文件与数据存储实战:SharedPreferences、SQLite 与 Room 的渐进式实现
android·java·数据库·ui·sqlite·room·sp
MegaDataFlowers2 小时前
认识O(NlogN)的排序
java·开发语言·排序算法
苍何2 小时前
用 AI 多角度出图,电商产品图有救了!
后端