【抽奖系统开发实战】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的替换为该类的全限定路径。
相关推荐
骄马之死1 小时前
SpringMVC + SpringBoot 核心知识点总结
java·spring boot·后端
GoGeekBaird2 小时前
Anthropic技能"(Skills)的经验分享
后端
王码码20352 小时前
多台服务器怎么统一看状态?Beszel 轻量监控,搭起来不费事
运维·服务器·后端·安全·阿里云·接口·web
郑洁文2 小时前
基于Spring Boot的流浪动物救助网站
java·spring boot·后端·毕设·流浪动物救助
螺丝钉code3 小时前
JAVA项目 Claude code CLAUDE.md 到底应该怎么写
java·人工智能·claude code
指令集梦境4 小时前
Cursor + Spring Boot实战:从零写一个RESTful API
spring boot·后端·restful
摇滚侠4 小时前
Maven 入门+高深 单一架构案例 54-59
java·架构·maven·intellij-idea
VidDown4 小时前
Webhook 调试器:让第三方回调“原形毕露”
java·开发语言·javascript·编辑器·postman
码云之上4 小时前
聊聊如何设计一个高效、稳定的 Node.js 接入层
前端·后端·node.js
折哥的程序人生 · 物流技术专研5 小时前
Java 23 种设计模式:从踩坑到精通 | 原型模式 —— 克隆对象,深拷贝与浅拷贝的坑你踩过吗?
java·设计模式·架构·原型模式·单一职责原则