文章目录
-
- 一、背景
- 二、核心需求描述
- 三、系统设计
-
- [3.1 系统架构](#3.1 系统架构)
- [3.2 项目环境](#3.2 项目环境)
- [3.3 业务功能模块](#3.3 业务功能模块)
- [3.4 数据库设计](#3.4 数据库设计)
- 3.5代码结构设计
- 四、功能模块设计
-
- [4.1 通用处理](#4.1 通用处理)
-
- 1) 错误码设计 错误码设计)
- 2) 自定义异常类 自定义异常类)
- 3) 统一响应结果(CommonResult<T>) 统一响应结果(CommonResult<T>))
- 4) JSON序列化工具类(JacksonUtil) JSON序列化工具类(JacksonUtil))
- 5) 日志处理 日志处理)

一、背景
随着数字营销的兴起,企业越来越重视通过在线活动来吸引和留住客户。抽奖活动作为一种有效的营销手段,能够显著提升用户参与度和品牌曝光率。于是我们就开发了以抽奖活动作为背景的Spring Boot项目,通过这个项目提供一个全面、可靠、易于维护的抽奖平台,该平台将采用以下策略:
- 集成多种技术组件:利用MySQL、Redis、RabbitMQ等常用组件,构建一个稳定、高效、可扩展的抽奖系统。
- 活动、奖品与人员管理:允许管理员创建配置抽奖活动;管理奖品信息;管理人员信息。
- 实现状态机管理:通过精心设计的状态机,精确控制活动及奖品状态的转换,提高系统的可控性和可预测性。
- 保障数据一致性:通过事务管理和数据同步机制,确保数据的一致性和完整性。
- 加强安全性:实施安全措施,包括数据加密、用户认证,保护用户数据和系统安全。
- 降低维护成本:提供全面的日志记录和异常处理机制,简化问题诊断和系统维护。
- 提高扩展性:采用模块化设计与设计模式的使用,提高系统的灵活性和扩展性。
要不要我把这段内容整理成Markdown格式,直接嵌入到你的博客文章里?
二、核心需求描述
- 管理员注册与登录
- 注册信息:姓名、邮箱、手机号、密码。
- 登录方式:① 电话+密码登录;② 电话+短信登录(需获取验证码)。
- 登录校验:需验证管理员身份。
- 人员管理
- 管理员可创建普通用户,创建信息包括姓名、邮箱、手机号。
- 支持查看人员列表,展示人员id、姓名、身份(普通用户/管理员)。
- 奖品管理
- 管理员可创建奖品,信息包括奖品名称、描述、价格、奖品图(支持上传)。
- 支持奖品列表展示(可翻页),展示内容包括奖品id、奖品图、奖品名、奖品描述、奖品价值(元)。
- 活动管理
- 管理员可创建活动,信息包括:活动名称、活动描述、圈选奖品(勾选奖品并设置等级及数量)、圈选参与人员。
- 支持活动列表展示(可翻页),展示活动名称、描述、活动状态:
- 进行中:点击"活动进行中,去抽奖"按钮跳转抽奖页。
- 已完成:点击"活动已完成,查看中奖名单"按钮跳转查看结果。
- 抽奖页面
- 仅管理员可对进行中的活动发起抽奖。
- 每轮抽奖中奖人数与当前奖品数量一致,每人仅能中一次奖。
- 多轮抽奖流程:① 展示奖品信息(奖品图、份数)→ ② 人名闪动 → ③ 停止闪动确定中奖名单:
- 点击"开始抽奖"跳转至人名闪动画面;
- 点击"点我确定"确认中奖名单;
- 点击"已抽完,下一步",若有未抽取奖品则展示下一个,否则展示全部中奖名单;
- 点击"查看上一奖项"展示上一个奖品信息。
- 异常处理:抽奖过程中刷新页面,已抽取成功的奖项不可重新抽取;刷新后若当前奖品已抽完,点击"开始抽奖"直接展示该奖品中奖名单。
- 活动已完成:展示所有奖项的全部中奖名单,新增"分享结果"按钮,点击可复制链接,链接打开后仅展示活动名称与中奖结果,保留"分享结果"按钮。
- 通知功能
- 抽奖完成后,需通过邮件和短信通知中奖者。
- 登录强制校验
- 管理端所有页面(含抽奖页)需强制管理员登录后方可访问,未登录则强制跳转登录页面。
三、系统设计
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):存储活动与参与人员的关联关系
- 中奖记录表(winning_record):存储中奖信息,如活动Id,奖品Id,中奖者Id等。
3.5代码结构设计

四、功能模块设计
4.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;
}
}
- 全局错误码常量
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, "未知错误");
}
- 业务错误码(预留接口)
- 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无法适配这些场景,因此需要专业的日志框架。
日志的核心用途:
- 系统监控:
记录系统运行状态、方法响应时间与响应状态,通过数据分析设置阈值报警(如统计关键字出现次数触发报警),是成熟系统的标配能力。 - 采集的数据可用于多场景应用:
数据统计:统计页面浏览量(PV)、访客量(UV)、点击量等,优化运营策略;
推荐排序:记录用户浏览历史、停留时长等行为数据,为算法模型训练提供支撑,适用于购物、广告、新闻等领域。 - 日志审计:
响应国家政策法规与行业标准要求,保障系统安全:
追溯操作行为:记录运营人员对数据的删除、修改操作,明确责任主体;
防范安全风险:通过日志分析非法攻击、非法调用及内部违规行为(如客户信息泄露),为事后调查提供依据。
日志配置
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的替换为该类的全限定路径。