实现一个在线抽奖系统,就算是个小白看了也能做出来(附源码)

2,793 阅读7分钟

1、项目介绍:

(1)功能介绍:

主要业务:为公司活动(如年会等)提供在线抽奖功能,满足奖品、抽奖人员的管理,及抽奖活动的需要。

  • 用户注册
  • 用户登录、会话管理
  • 抽奖设置:奖品管理,抽奖人员管理
  • 人员抽奖

(2)开发环境与技术栈:

  • windows
  • Maven
  • Lombok
  • Spring、SpringMVC、SpringBoot
  • MySQL、Mybatis、Druid

(3)项目演示:

用户登录:

用户注册:

奖项设置:

抽奖人员设置:

抽奖:

2、项目准备:

(1)代码框架:(源码)

全部代码源码 Github链接:github.com/JACK-QBS/Pr…

(2)数据库设计:

数据库表关系图:

(业务上一对一)

为什么要有设置表?

  • 1对1关联的表,其实可以只使用1张表保存所有字段;但是在一些可能出现的业务扩展,方便系统扩展使用,所以设计表时,考虑1对1设计。
  • 拓展业务 ,一个用户进来设置,当多个用户进来设置的时候(多个用户属于同一公司),如果是只有用户表的话,及无法支撑业务,用户表关联公司,设置表在关联公司

3、后端对前端接口的实现:

要实现功能,需要先明确前后端约定好的接口。需要说明的是,接口的定义一般是前后端约定好的,所以也和前端代码息息相关,前端需要什么数据,需要什么格式的数据,也会在接口中体现。

接口主要体现在:

  • 请求需要的信息:请求方法,请求路径,请求数据
  • 响应数据

(1)用户的登录、注册、注销

用户登录:

前端请求:

POST api/user/login (请求路径) Content-Type: application/json {username: “qbs”, password: “123”}

响应:

{ “success” : true }

后端实现接口:

用户注册:

前端请求:

POST api/user/register (请求路径) Content-Type: multipart/form-data; boundary=---- WebKitFormBoundarypOUwkGIMUyL0aOZT

username: qbs
password: 123
nickname: 帅哥
email: 666@163.com
age: 18
headFile: (binary)

响应:

{ “success” : true }

后端实现:

用户注销:

前端请求:

POST api/user/login (请求路径)

后端实现:

(2)查询奖项设置、修改抽奖人数:

查询奖项设置:

前端请求:

GET api/setting/query(请求路径)

后端实现:

修改抽奖人数:

前端请求:

GET api/setting/update?batchNumber=5(请求路径) (接口对应抽奖设置页面中,点每次抽奖人数下拉菜单切换时修改)

后端实现:

(3)新增、修改、删除奖项:

新增奖项:

前端请求:

POST api/award/add (请求路径) Content-Type: application/json

{name: “特等奖”, count: 1, award: “全球旅行7日游”}

后端实现:

修改奖项:

前端请求:

POST api/award/update (请求路径) Content-Type: application/json

后端响应:

删除奖项:

前端请求:

GET api/award/delete/4(请求路径)

最后的数字4,对应奖项的id

后端实现:

(4)新增、修改、删除抽奖人员:

新增抽奖人员:

前端请求:

POST api/member/add (请求路径) Content-Type: application/json

后端实现:

修改抽奖人员

前端请求:

POST api/member/update Content-Type: application/json

后端实现:

删除抽奖人员

前端请求:

GET api/member/delete/97 (最后的数字为抽奖人员的id)

后端实现:

(5)抽奖、删除获奖人员:

抽奖:

前端请求:

POST api/record/add/3 Content-Type: application/json

(以上路径中最后的数字代表奖项id,请求数据为抽奖人员id组成的数组)

后端实现:

抽奖后端只是插入记录(人员id、奖项id),具体的抽奖是前端实现的,而且也是简单的实现方式,没有任何算法。(只是在当前奖项剩余名额中,每次抽奖人数,在所有未中奖的人员列表中,随机抽取)

删除获奖人员:

前端请求:

GET api/record/delete/member?id=22 (根据 人员id 删除对应的获奖记录)

GET api/record/delete/award?id=3 (根据 奖项id 删除对应所有获奖人员记录)

后端实现:

4、代码设计:

(1)设计数据库的实体类:

通过 mybatis 生成工具(tool包):生成 mapper、数据库表实体类(model包)、xml 文件

(2)设计统一响应类:

主要是为了返回数据的统一字段设计

/**
 * 统一响应的数据格式
 */
public class JSONResponse {
    private boolean success;
    private String code;
    private String message;
    private Object data;
}
/**
 * 统一数据封装
 */
public class RequestResponseBodyMethodProcessorWrapper implements HandlerMethodReturnValueHandler {

    private final HandlerMethodReturnValueHandler delegate;

    public RequestResponseBodyMethodProcessorWrapper(HandlerMethodReturnValueHandler delegate) {
        this.delegate = delegate;
    }

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return delegate.supportsReturnType(returnType);
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        //returnValue是Controller请求方法执行完,返回值
        if(!(returnValue instanceof JSONResponse)){//返回值本身就是需要的类型,不进行处理
            JSONResponse json = new JSONResponse();
            json.setSuccess(true);
            json.setData(returnValue);
            returnValue = json;
        }
        delegate.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
    }
}

(3)设计自定义异常:

主要针对不同的场景,需要抛异常来处理时,能定位业务含义。

主要分为:

  • 1、 客户端请求错误时的异常:需要给定错误码,方便前端提示用户,如用户名存在不允许注册
  • 2、 业务发生错误时的异常:需要给定错误码,方便后端定位问题,一般如程序上的业务错误都可以抛(BUG)
  • 3、 系统发生错误时的异常:需要给定错误码,方便后端定位问题,程序出错,如数据库连接获取失败都可以抛(一般是系统发生错误,如网络断了,数据库挂了等等)自定义异常前端需要显示错误码和错误消息,用户可以根据提示信息判断原因。
  • 4、非自定义异常,异常信息一般是框架或JDK抛出的英文,是给开发人员描述错误的,无法给用户提示,所以错误信息提示为未知异常。
/**
 * 自定义异常:保存错误码和错误消息
 */
@Getter
@Setter
public class AppException extends RuntimeException {

    private String code;

    public AppException( String code, String message) {
        super(message);
        this.code = code;
    }

    public AppException( String code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }
}
//统一异常处理
@ControllerAdvice
@Slf4j//使用lombok日志日志注解,之后使用log属性来完成日志打印
public class ExceptionAdvice {

    //自定义异常报错错误码和错误消息
    @ExceptionHandler(AppException.class)
    @ResponseBody
    public Object handle1(AppException e){
        JSONResponse json = new JSONResponse();
        json.setCode(e.getCode());
        json.setMessage(e.getMessage());
        log.debug("自定义异常", e);
        return json;
    }

    //非自定义异常(英文错误信息,堆栈信息,不能给用户看):
    // 指定一个错误码,错误消息(未知错误,请联系管理员)
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object handle2(Exception e){
        JSONResponse json = new JSONResponse();
        json.setCode("ERR000");
        json.setMessage("未知错误,请联系管理员");
        log.error("未知错误", e);
        return json;
    }
}

(4)设计统一会话管理的拦截器

public class LoginInterceptor implements HandlerInterceptor {

    private ObjectMapper objectMapper;

    public LoginInterceptor(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if(session != null){//获取登录时设置的用户信息
            User user = (User) session.getAttribute("user");
            if(user != null){//登录了,允许访问
                return true;
            }
        }
        //登录失败,不允许访问的业务:区分前后端
        //TODO:前端跳转登录页面,后端返回json
//        new ObjectMapper().writeValueAsString(object);//序列化对象为json字符串
        //请求的服务路径
        String servletPath = request.getServletPath();//   /apiXXX.html
        if(servletPath.startsWith("/api/")){//后端逻辑:返回json
            response.setCharacterEncoding("UTF-8");
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            JSONResponse json = new JSONResponse();
            json.setCode("USR000");
            json.setMessage("用户没有登录,不允许访问");
            String s = objectMapper.writeValueAsString(json);
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            PrintWriter pw = response.getWriter();
            pw.println(s);
            pw.flush();
        }else{//前端逻辑:跳转到登录页面 /views/index.html
            //相对路径的写法,一定是请求路径作为相对位置的参照点
            //使用绝对路径来重定向,不建议使用相对路径和转发
            String schema = request.getScheme();//http
            String host = request.getServerName();//ip
            int port = request.getServerPort();//port
            String contextPath = request.getContextPath();//application Context path应用上下文路径
            String basePath = schema+"://"+host+":"+port+contextPath;
            //重定向到登录页面
            response.sendRedirect(basePath+"/index.html");
        }
        return false;
    }
}

(5)设计Mybatis中Mapper的基类:

使用Mybatis的接口方法,所有接口方法都是类似,只是传入参数和返回值不同,可以考虑设计统一的基类,以泛型的方式定义出不同的参数类型、返回类型

/**
 * 所有 mapper 父接口
 */
public interface BaseMapper<T> {

    int deleteByPrimaryKey(Integer id);

    int insert(T record);

    int insertSelective(T record);

    T selectByPrimaryKey(Integer id);//通过主键查询

    int updateByPrimaryKeySelective(T record);//根据主键修改其他非主键字段

    int updateByPrimaryKey(T record);
}