springboot项目实战——书城项目

文章目录

  • 一、主要技术
  • 二、项目描述
    • 1、项目目录
    • 2、项目思路
  • 三、项目部分类
    • 1、pojo实体类
    • 2、拦截器
    • 3、Util工具类
    • 4、VO层
    • 5、控制层的基类
    • 6、业务时间计算
  • 四、功能演示
    • 1、登录
    • 2、注册页面
    • 3、首页:
    • 4、个人基本信息
    • 5、头像修改
    • 6、地址信息
    • 7、购物页面
    • 8、购物车
    • 9、结算
  • 五、总结

一、主要技术

项目后端主要使用 springboot
数据库:mysql
使用mybaits操作数据库(xml方式)
缓存:redis
安全框架:shrio
前端:jq+bootstrap
数据库中密码字段使用 md5加密技术
前后端交互使用 ajax
thymeleaf模板引擎 显示某些变量 和引用相同代码
使用aop面向切面编程思想确定业务所需的的时间

二、项目描述

1、项目目录

springboot项目实战——书城项目_第1张图片
springboot项目实战——书城项目_第2张图片
springboot项目实战——书城项目_第3张图片

springboot项目实战——书城项目_第4张图片
springboot项目实战——书城项目_第5张图片
springboot项目实战——书城项目_第6张图片

2、项目思路

项目主要分为 用户、商品(书籍)、购物车、订单 四大模块。
(1)、用户:用户的登录、注册、收货地址的增删改查和设置默认收货地址、头像的修改
(2)、商品:书籍的查询、展示到页面
(3)、购物车:添加到购物车 删除购物车 增加数量 提交购物车
(4)、订单:创建订单、查询订单

springboot项目实战——书城项目_第7张图片

项目编写主要先从 DAO持久层–>Service业务层–>控制层–>前端的思路
编写业务层和控制层都需要先规划异常

三、项目部分类

1、pojo实体类

所有实体类都继承于BaseEntity
BaseEntity有四个基本数据

用户类、地址类、区域类、商品类、购物车类、订单类、订单项类
区域类:

package com.atmae.store.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
 * 省市区的数据实体类
 * @author Mae
 */
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class District{
    private Integer id;
    private String parent;
    private String code;
    private String name;
}

对应的数据库数据(3523条):
springboot项目实战——书城项目_第8张图片
主要用来动态获得省市区

2、拦截器

项目需要对没有登录的用户进行拦截,即其不能访问除登录、注册以外的其他页面

package com.atmae.store.config;

import com.atmae.store.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.ArrayList;
import java.util.List;

/**
 * 处理器拦截器的注册
 * @author Mae
 */
@Configuration
public class LoginInterceptorConfigurer implements WebMvcConfigurer {
    @Override
    /** 将自定义拦截器进行注册*/
    public void addInterceptors(InterceptorRegistry registry) {
        /** 自定义拦截器对象*/
        HandlerInterceptor interceptor=new LoginInterceptor();
        /** 配置白名单:存放在List集合中*/
        List<String> patterns =new ArrayList<>();
        patterns.add("/assets/**");
        patterns.add("/users/login");
        patterns.add("/users/register");
        patterns.add("/register");
        patterns.add("/login");
        patterns.add("/index");
        patterns.add("/district/**");
        /** 完成拦截器的注册*/
        /** 要拦截url  /**: 表示项目下所有的请求被拦截*/
        registry.addInterceptor(interceptor).addPathPatterns("/**")
                .excludePathPatterns(patterns);
    }
}
package com.atmae.store.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author Mae
 */
public class LoginInterceptor implements HandlerInterceptor {
    /**
     * 检测全局Session是否有uid数据,如果有则放行,没有重定向到登录页面
     *
     * @param request  请求对象
     * @param response 响应对象
     * @param handler  处理器(url+Controller:映射)
     * @return true表示放行当前的请求 false表示拦截当前的请求
     * @throws Exception 异常
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object user = request.getSession().getAttribute("userId");
        if (user == null) {
            /** 说明用户没有登录 则重定向到登陆页*/
            response.sendRedirect("/login");
            return false;
        }
            /** 表示放行*/
        return true;
    }
}

3、Util工具类

JsonResult类:主要在控制层以JSON方式响应前端数据

package com.atmae.store.util;

import java.io.Serializable;

/**
 * Json格式的数据进行响应
 *
 * @author Mae
 */
public class JsonResult<E> implements Serializable {
    /**
     * 状态码
     */
    private Integer state;
    /**
     * 描述信息
     */
    private String message;
    /**
     * 数据
     */
    private E data;

    public JsonResult() {

    }

    public JsonResult(Integer state) {
        this.state = state;
    }

    public JsonResult(Integer state, String message, E data) {
        this.state = state;
        this.message = message;
        this.data = data;
    }

    public JsonResult(String message, E data) {
        this.message = message;
        this.data = data;
    }

    public JsonResult(Throwable e) {
        this.message = e.getMessage();
    }

    public JsonResult(Integer state, E data) {
        this.state = state;
        this.data = data;
    }

    public Integer getState() {
        return state;
    }

    public void setState(Integer state) {
        this.state = state;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public E getData() {
        return data;
    }

    public void setData(E data) {
        this.data = data;
    }
}

4、VO层

package com.atmae.store.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;


/**
 * value object 值对象
 * 当进行select查询时 查询的结果数据时多张表中的内容,此时不能使用一个pojo实体类来接受
 * 则重新建一个新的对象存储查询出来的结果对应的映射
 * “ 多表操作 ”
 * @author Mae
 */
@AllArgsConstructor
@NoArgsConstructor
@Data
public class CartVO implements Serializable {
    private Integer userId;
    private Integer cartId;
    private Integer productId;
    private Long price;
    private Integer num;
    private String title;
    private String image;
    private Long realPrice;
}

5、控制层的基类

主要用来规划异常响应前端

package com.atmae.store.controller;

import com.atmae.store.controller.ex.*;
import com.atmae.store.service.ex.*;
import com.atmae.store.util.JsonResult;
import org.apache.tomcat.util.http.fileupload.FileUploadException;
import org.springframework.web.bind.annotation.ExceptionHandler;

import javax.servlet.http.HttpSession;

/**
 * 控制层类的基类
 *
 * @author Mae
 */
public class BaseController {
    /**
     * 操作成功的状态码
     */
    public static final int OK = 200;

    /**
     * 请求处理方法
     * 当前项目产生了异常 被统一拦截到此方法中,
     * 这个方法就充当了请求处理的方法,方法的返回值直接返回到前端
     */
    @ExceptionHandler(value = {ServiceException.class, FileUploadException.class})  //用于统一处理抛出的异常
    public JsonResult<Void> handleException(Throwable e) {
        JsonResult<Void> result = new JsonResult<>();
        if (e instanceof UsernameDuplicatedException) {
            result.setState(4000);
            result.setMessage("用户名已经被占用");
        } else if (e instanceof AddressCountLimitException) {
            result.setState(4001);
            result.setMessage("用户收货地址超出上限异常");
        } else if (e instanceof AccessDeniedException) {
            result.setState(4002);
            result.setMessage("非法访问异常");
        } else if (e instanceof AddressNotFoundException) {
            result.setState(4003);
            result.setMessage("收货地址没有发现异常");
        } else if (e instanceof ProductNotFoundException) {
            result.setState(4004);
            result.setMessage("商品没有被发现异常");
        } else if (e instanceof CartNotFoundException) {
            result.setState(4005);
            result.setMessage("购物车商品没有找到");
        } else if (e instanceof InsertException) {
            result.setState(5000);
            result.setMessage("注册时产生未知的异常");
        } else if (e instanceof UserNotFoundException) {
            result.setState(5001);
            result.setMessage("用户没有找到的异常");
        } else if (e instanceof PasswordNotMatchException) {
            result.setState(5002);
            result.setMessage("用户名的密码不匹配的异常");
        } else if (e instanceof UpdateException) {
            result.setState(5003);
            result.setMessage("更新数据时产生未知的异常");
        } else if (e instanceof FileEmptyException) {
            result.setState(6001);
            result.setMessage("文件为空异常");
        } else if (e instanceof FileSizeException) {
            result.setState(6002);
            result.setMessage("文件大小异常");
        } else if (e instanceof FileStateException) {
            result.setState(6003);
            result.setMessage("文件状态异常");
        } else if (e instanceof FileUploadIOException) {
            result.setState(6004);
            result.setMessage("文件加载IO异常");
        } else if (e instanceof FileTypeException) {
            result.setState(6005);
            result.setMessage("文件类型异常");
        }
        return result;
    }

    /**
     * 获取Session对象中的Uid
     *
     * @param session session对象
     * @return 当前对象的Uid
     */
    protected final Integer getUidFromSession(HttpSession session) {
        return Integer.valueOf(session.getAttribute("userId")
                .toString());
    }

    /**
     * 获取Session对象的用户名
     *
     * @param session
     * @return
     */
    protected final String getUsernameFromSession(HttpSession session) {
        return session.getAttribute("username").toString();
    }
}

6、业务时间计算

主要使用面向切面编程思想

package com.atmae.store.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * @author Mae
 * 将当前类的对象创建使用维护交由spring容器
 * 将当前类标记为切面类
 */
@Aspect
@Component
@Slf4j
public class TimerAspect {
    /**
     * 映射到那个方法上
     */
    @Around("execution(* com.atmae.store.service.impl.*.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        /** 调用目标发发 login*/
        Object result = pjp.proceed();
        /** 后记录时间*/
        long end = System.currentTimeMillis();
        log.info("耗时" + (end - start) + "ms");
        return result;
    }
}

日志打印
springboot项目实战——书城项目_第9张图片

四、功能演示

1、登录

登录页面:
springboot项目实战——书城项目_第10张图片

2、注册页面

同登录页面差不多

3、首页:

springboot项目实战——书城项目_第11张图片
首页的两列 新货和热销 主要是通过后台的查询商品操作 根据具体条件查询前四名
部分mapper文件:

<mapper namespace="com.atmae.store.mapper.ProductMapper">
    <resultMap id="ProductEntityMap" type="com.atmae.store.entity.Product">
        <id column="id" property="id"/>
        <result column="category_id" property="categoryId"/>
        <result column="item_type" property="itemType"/>
        <result column="sell_point" property="sellPoint"/>
        <result column="created_user" property="createdUser"/>
        <result column="created_time" property="createdTime"/>
        <result column="modified_user" property="modifiedUser"/>
        <result column="modified_time" property="modifiedTime"/>
    resultMap>

    
    <select id="findHotList" resultMap="ProductEntityMap">
        SELECT *
        FROM t_book
        WHERE status = 1
        ORDER BY priority DESC LIMIT 0,4
    select>

    <select id="findNewList" resultMap="ProductEntityMap">
        select *
        from t_book
        where status = 1
        order by created_time desc limit 0,4
    select>

部分页面代码:

使用ajax响应数据

          
            <div class="row">
                <div class="col-md-6">
                    <div class="panel panel-default">
                        <div class="panel-heading">
                            <p class="panel-title">新到好货p>
                        div>
                        <div id="new-list" class="panel-body panel-item">
                        div>
                    div>
                div>
                <div class="col-md-6">
                    <div class="panel panel-default">
                        <div class="panel-heading">
                            <p class="panel-title">热销排行p>
                        div>
                        <div id="hot-list" class="panel-body panel-item">
                        div>
                    div>
                div>
            div>
        div>
    div>
div>

<div th:replace="common::commonscipt">div>

<script type="text/javascript">
    $(function () {
        showNewList();
        showHotList();
    });

    function showNewList() {
        $("#new-list").empty();
        $.ajax({
            url: "/products/newList",
            type: "GET",
            dataType: "JSON",
            success: function (json) {
                let list = json.data;
                console.log("count=" + list.length);
                for (let i = 0; i < list.length; i++) {
                    console.log(list[i].title);
                    let html = '
' + '' + '
¥#{price}
'
+ '
'
+ '
'
; html = html.replace(/#{id}/g, list[i].id); html = html.replace(/#{title}/g, list[i].title); html = html.replace(/#{price}/g, list[i].price); html = html.replace(/#{image}/g, list[i].image); $("#new-list").append(html); } } }); } function showHotList() { $("#hot-list").empty(); $.ajax({ url: "/products/hotList", type: "GET", dataType: "JSON", success: function (json) { let list = json.data; console.log("count=" + list.length); for (let i = 0; i < list.length; i++) { console.log(list[i].title); let html = '
' + '' + '
¥#{price}
'
+ '
'
+ '
'
; html = html.replace(/#{id}/g, list[i].id); html = html.replace(/#{title}/g, list[i].title); html = html.replace(/#{price}/g, list[i].price); html = html.replace(/#{image}/g, list[i].image); $("#hot-list").append(html); } } }); }
script>

4、个人基本信息

springboot项目实战——书城项目_第12张图片
加载页面即显示数据:ajax实现.

  $.ajax({
            url: "/users/getByUserId",
            data: $("#form-date").serialize(),
            type: "GET",
            dataType: "JSON",
            success: function (json) {
                if (json.state === 200) {
                    //将查询到的数据重新设置到控件中
                    $("#phone").val(json.data.phone);
                    $("#email").val(json.data.email);
                    if (json.data.gender === 0) {
                        let radio = $("#gender-female");
                        radio.prop("checked", "checked");
                    } else if (json.data.gender === 1) {
                        let radio = $("#gender-male");
                        radio.prop("checked", "checked");
                    }
                    // prop:给某个元素添加属性及属性值!
                } else {
                    alert("数据不存在!");
                }
            },
            error: function (xhr) {
                alert("查询用户信息时产生未知的异常!" + xhr.status);
            }
        });
        return false;
    });

5、头像修改

springboot项目实战——书城项目_第13张图片
控制层代码:

 /**
     * 上传文件的最大值
     */
    private static final long AVATAR_MAX_SIZE = 10 * 1024 * 1024;

    /**
     * 限制上传文件的类型
     */
    private static final List<String> AVATAR_TYPE = new ArrayList<>();

    /** 静态块给集合初始化*/
    static {
        AVATAR_TYPE.add("image/png");
        AVATAR_TYPE.add("image/jpeg");
        AVATAR_TYPE.add("image/bmp");
        AVATAR_TYPE.add("image/gif");
    }

    @RequestMapping("/changeAvatar")
    public JsonResult<String> changeAvatar(HttpServletResponse response, HttpSession session, MultipartFile file) throws FileNotFoundException {
        /** 判断文件是否为空*/
        if (file.isEmpty()) {
            throw new FileEmptyException("文件为空");
        }
        /** 判断文件是否超出大小*/
        if (file.getSize() > AVATAR_MAX_SIZE) {
            throw new FileSizeException("文件超出限制");
        }
        /** 判断文件类型是否是规定的和后缀类型*/
        String contentType = file.getContentType();
        /** 如何集合包含某个元素则返回true*/
        if (!AVATAR_TYPE.contains(contentType)) {
            throw new FileTypeException("文件类型不支持");
        }
        /** 上传的文件放在 .../assets/img/avatar/  .(后缀)下*/
        String property = System.getProperty("user.dir");
        String path = property + "\\store\\src\\main\\resources\\static\\assets\\img\\avatar";
        /** File对象指向这个路径,File是否存在*/
        File dir = new File(path);
        /** 检测目录是否存在*/
        if (!dir.exists()) {
            /** 创建当前的目录*/
            dir.mkdirs();
        }
        /** 获取到这个文件名称 UUID工具来将生成一个新的字符串作为文件名*/
        String originalFilename = file.getOriginalFilename();
        /** 文件后缀*/
        int index = originalFilename.lastIndexOf(".");
        String suffix = originalFilename.substring(index);
        String filename = UUID.randomUUID().toString().toUpperCase() + suffix;
        /** 这是一个空文件*/
        File dest = new File(dir, filename);
        /** 参数file中数据写入到这个空文件中*/
        try {
            /** 将file文件中的数据写入到dest文件中*/
            file.transferTo(dest);
        } catch (IOException e) {
            throw new FileUploadIOException("文件读写异常");
        } catch (FileStateException e) {
            throw new FileSizeException("文件状态异常");
        }
        Integer userId = getUidFromSession(session);
        String username = getUsernameFromSession(session);
        /** 返回头像的路径*/
        String avatar = "assets/img/avatar/" + filename;
        userService.changeAvatar(userId, avatar, username);
        /** 存储cookie 响应给客户端*/
        Cookie cookie = new Cookie("img", avatar);
        response.addCookie(cookie);
        /** 返回用户头像的路径给前端页面,将来用于头像展示使用*/
        return new JsonResult<>(OK, avatar);
    }

将头像存储到cookie中,而用户的id和用户名存储在session中
头像会存放在服务器中,数据库中存放的是头像当前项目的相对地址

6、地址信息

springboot项目实战——书城项目_第14张图片
可以设置默认收货地址和增加收货地址
springboot项目实战——书城项目_第15张图片
页面的省市区通过ajax方式加载

没有选择大的区域则不显示小区域

   $(function () {
        /*value 属性用于表示当前区域的code值*/
        let defaultOption = "";
        showProvinceList();
        $("#city-list").append(defaultOption);
        $("#area-list").append(defaultOption);
        /** change():监听某个控件一旦发生改变 就会触发*/
        $("#province-list").change(function () {
            // 获取行政区的父代码
            let parent = $("#province-list").val();
            // 清空select下拉列表的所有 option元素
            $("#city-list").empty();
            $("#area-list").empty();
            //填充默认值
            $("#city-list").append(defaultOption);
            $("#area-list").append(defaultOption);
            if (parent === 0) {
                return;
            }
            $.ajax({
                url: "/district",
                type: "POST",
                data: "parent=" + parent,
                dataType: "JSON",
                success: function (json) {
                    if (json.state === 200) {
                        let list = json.data;
                        for (let i = 0; i < list.length; i++) {
                            let opt = " + list[i].name + "";
                            $("#city-list").append(opt);
                        }
                    } else {
                        alert("信息加载失败!");
                    }
                }
            });
        });

        $("#city-list").change(function () {
            // 获取行政区的父代码
            let parent = $("#city-list").val();
            // 清空select下拉列表的所有 option元素
            $("#area-list").empty();
            //填充默认值
            $("#area-list").append(defaultOption);
            if (parent === 0) {
                return;
            }
            $.ajax({
                url: "/district",
                type: "POST",
                data: "parent=" + parent,
                dataType: "JSON",
                success: function (json) {
                    if (json.state === 200) {
                        let list = json.data;
                        for (let i = 0; i < list.length; i++) {
                            let opt = " + list[i].name + "";
                            $("#area-list").append(opt);
                        }
                    } else {
                        alert("信息加载失败!");
                    }
                }
            });
        });

        function showProvinceList() {
            $.ajax({
                url: "/district",
                type: "POST",
                data: "parent=86",
                dataType: "JSON",
                success: function (json) {
                    if (json.state === 200) {
                        let list = json.data;
                        for (let i = 0; i < list.length; i++) {
                            let opt = " + list[i].name + "";
                            $("#province-list").append(opt);
                        }
                    } else {
                        alert("信息加载失败!");
                    }
                }
            });
            return false;
        }

        $("#btn-add-new-address").click(function () {
            $.ajax({
                url: "/address/addNewAddress",
                type: "POST",
                data: $("#form-add-new-address").serialize(),
                dataType: "JSON",
                success: function (json) {
                    if (json.state === 200) {
                        alert("新增收货地址成功!");
                        location.href = "/addAddress";
                    } else {
                        alert("新增收货地址失败!");
                    }
                },
                error: function (xhr) {
                    alert("新增收货地址产生未知的异常!" + xhr.status);
                }
            });
            return false;
        });
    });

springboot项目实战——书城项目_第16张图片
在这里插入图片描述

7、购物页面

查询所有书籍即可
springboot项目实战——书城项目_第17张图片

8、购物车

springboot项目实战——书城项目_第18张图片
springboot项目实战——书城项目_第19张图片
也可以增加数量和删除

9、结算

springboot项目实战——书城项目_第20张图片
springboot项目实战——书城项目_第21张图片

五、总结

纸上得来终觉浅
用来练手,很好的复习了学过的知识

你可能感兴趣的:(springboot,spring,boot,java,后端)