SpringBoot + Vue前后端分离之旅游管理系统

旅游管理系统

自己在跟着视频学习完SSM,SpringBoot时,感觉不实践一下所学的知识一直停留在学习理论阶段,没有真正的会用在项目中,自己一直坚信实践是检验知识学习的有效方式。所以自己跟着视频学习了这个管理系统,总结一下自己的学习过程以及重要知识点。

视频链接: https://www.bilibili.com/video/BV1Nt4y127Jh?p=19


项目简介

技术要求:

  • 后端技术栈:Springboot + mybatis
  • 前后端分析:axios、json
  • 前端技术栈:Vue、nodejs

前置知识

  • vue 组件之间的知识
  • springboot + mybatis 知识

开发流程

  1. 需求分析
  2. 库表设计
  3. 编码(项目环境搭建 + 项目编码)
  4. 项目调试

需求分析

  • 用户模块:登陆和注册
  • 省份模块:一个省份可以有多个景点 CRUD
  • 景点模块:一个景点对应多个省份

库表设计

  1. 分析系统中有哪些表? —> 表的个数
  2. 分析系统中表与表之间的关联关系
  3. 分析每个表中字段(显性字段 隐性字段(业务字段、经验字段))

数据库建表

用户表 t_user —— 独立表

  • id、username、password、email

省份表t_province —— 省份表:景点表 === 1:n

  • id、 name、 tags、 placecounts

景点表 t_place

  • id、name、picpath、hottime、hotticket、dimticket、placedes、provinceid

数据库名:travels

用户SQL:

CREATE TABLE t_user(
	id INT(6) PRIMARY KEY AUTO_INCREMENT,
	username VARCHAR(60),
	PASSWORD VARCHAR(60),
	email VARCHAR(60)
)

省份表:t_province

CREATE TABLE t_province(
	id INT(6) PRIMARY KEY AUTO_INCREMENT,
	NAME VARCHAR(60),
	tags VARCHAR(80),
	placecounts INT(4)
)

景点表:t_place

CREATE TABLE t_place(
	id INT(6) PRIMARY KEY AUTO_INCREMENT,
	NAME VARCHAR(60),
	picpath VARCHAR(100),
	hottime TIMESTAMP,
	hotticket DOUBLE(7,2),
	dimticket DOUBLE(7,1),
	placedes VARCHAR(300),
	provinceid INT(6) REFERENCES t_province(id)
)

项目编码

环境搭建

利用 Spring Initializr 快速搭建 SpringBoot 项目
SpringBoot + Vue前后端分离之旅游管理系统_第1张图片

引入依赖


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.2.6.RELEASEversion>
        <relativePath/> 
    parent>
    <groupId>com.baihzigroupId>
    <artifactId>travelsartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>travelsname>
    <description>Demo project for Spring Bootdescription>

    <properties>
        <java.version>1.8java.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.1.2version>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.1.19version>
        dependency>
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.12version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
        dependency>

        <dependency>
            <groupId>commons-fileuploadgroupId>
            <artifactId>commons-fileuploadartifactId>
            <version>1.4version>
        dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombokgroupId>
                            <artifactId>lombokartifactId>
                        exclude>
                    excludes>
                configuration>
            plugin>
        plugins>
    build>

project>

注意:开发项目过程中遇到版本问题,导致项目在启动时成功,经过查询是 springboot 版本与 mybatis 版本有冲突,因此在这里设置 springboot 版本为 2.2.6.RELEASE mybatis 版本为 2.1.2

配置文件(application.properties)

server.port=8989
spring.application.name=travels

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/travels?serverTimezone=UTC&userSSL=true&userUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=li12345

mybatis.mapper-locations=classpath:com/baizhi/travels/mapper/*.xml
mybatis.type-aliases-package=com.baizhi.travels.entity
spring.web.resources.static-locations=classpath:/static/,file:${upload.dir}

upload.dir=D:\\project_work\\iamges

登陆注册的验证码功能

验证码工具类:

package com.baizhi.travels.utils;


import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;

import javax.imageio.ImageIO;


public class CreateImageCode {
    // 图片的宽度。
    private int width = 160;
    // 图片的高度。
    private int height = 40;
    // 验证码字符个数
    private int codeCount = 4;
    // 验证码干扰线数
    private int lineCount = 20;
    // 验证码
    private String code = null;
    // 验证码图片Buffer
    private BufferedImage buffImg = null;
    Random random = new Random();

    public CreateImageCode() {
        creatImage();
    }

    public CreateImageCode(int width, int height) {
        this.width = width;
        this.height = height;
        creatImage();
    }

    public CreateImageCode(int width, int height, int codeCount) {
        this.width = width;
        this.height = height;
        this.codeCount = codeCount;
        creatImage();
    }

    public CreateImageCode(int width, int height, int codeCount, int lineCount) {
        this.width = width;
        this.height = height;
        this.codeCount = codeCount;
        this.lineCount = lineCount;
        creatImage();
    }

    // 生成图片
    private void creatImage() {
        int fontWidth = width / codeCount;// 字体的宽度
        int fontHeight = height - 5;// 字体的高度
        int codeY = height - 8;

        // 图像buffer
        buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = buffImg.getGraphics();
        //Graphics2D g = buffImg.createGraphics();
        // 设置背景色
        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);



        // 设置字体
        //Font font1 = getFont(fontHeight);
        Font font = new Font("Fixedsys", Font.BOLD, fontHeight);
        g.setFont(font);

        // 设置干扰线
        for (int i = 0; i < lineCount; i++) {
            int xs = random.nextInt(width);
            int ys = random.nextInt(height);
            int xe = xs + random.nextInt(width);
            int ye = ys + random.nextInt(height);
            g.setColor(getRandColor(1, 255));
            g.drawLine(xs, ys, xe, ye);
        }

        // 添加噪点
        float yawpRate = 0.01f;// 噪声率
        int area = (int) (yawpRate * width * height);
        for (int i = 0; i < area; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);

            buffImg.setRGB(x, y, random.nextInt(255));
        }


        String str1 = randomStr(codeCount);// 得到随机字符
        this.code = str1;
        for (int i = 0; i < codeCount; i++) {
            String strRand = str1.substring(i, i + 1);
            g.setColor(getRandColor(1, 255));
            // g.drawString(a,x,y);
            // a为要画出来的东西,x和y表示要画的东西最左侧字符的基线位于此图形上下文坐标系的 (x, y) 位置处

            g.drawString(strRand, i*fontWidth+3, codeY);
        }


    }

    // 得到随机字符
    private String randomStr(int n) {
        String str1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
        String str2 = "";
        int len = str1.length() - 1;
        double r;
        for (int i = 0; i < n; i++) {
            r = (Math.random()) * len;
            str2 = str2 + str1.charAt((int) r);
        }
        return str2;
    }

    // 得到随机颜色
    private Color getRandColor(int fc, int bc) {// 给定范围获得随机颜色
        if (fc > 255)
            fc = 255;
        if (bc > 255)
            bc = 255;
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }

    /**
     * 产生随机字体
     */
    private Font getFont(int size) {
        Random random = new Random();
        Font font[] = new Font[5];
        font[0] = new Font("Ravie", Font.PLAIN, size);
        font[1] = new Font("Antique Olive Compact", Font.PLAIN, size);
        font[2] = new Font("Fixedsys", Font.PLAIN, size);
        font[3] = new Font("Wide Latin", Font.PLAIN, size);
        font[4] = new Font("Gill Sans Ultra Bold", Font.PLAIN, size);
        return font[random.nextInt(5)];
    }

    // 扭曲方法
    private void shear(Graphics g, int w1, int h1, Color color) {
        shearX(g, w1, h1, color);
        shearY(g, w1, h1, color);
    }

    private void shearX(Graphics g, int w1, int h1, Color color) {

        int period = random.nextInt(2);

        boolean borderGap = true;
        int frames = 1;
        int phase = random.nextInt(2);

        for (int i = 0; i < h1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(0, i, w1, 1, (int) d, 0);
            if (borderGap) {
                g.setColor(color);
                g.drawLine((int) d, i, 0, i);
                g.drawLine((int) d + w1, i, w1, i);
            }
        }

    }

    private void shearY(Graphics g, int w1, int h1, Color color) {

        int period = random.nextInt(40) + 10; // 50;

        boolean borderGap = true;
        int frames = 20;
        int phase = 7;
        for (int i = 0; i < w1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(i, 0, 1, h1, 0, (int) d);
            if (borderGap) {
                g.setColor(color);
                g.drawLine(i, (int) d, i, 0);
                g.drawLine(i, (int) d + h1, i, h1);
            }

        }

    }



    public void write(OutputStream sos) throws IOException {
        ImageIO.write(buffImg, "png", sos);
        sos.close();
    }

    public BufferedImage getBuffImg() {
        return buffImg;
    }

    public String getCode() {
        return code.toLowerCase();
    }

    //使用方法
 /*public void getCode3(HttpServletRequest req, HttpServletResponse response,HttpSession session) throws IOException{
        // 设置响应的类型格式为图片格式
            response.setContentType("image/jpeg");
            //禁止图像缓存。
            response.setHeader("Pragma", "no-cache");
            response.setHeader("Cache-Control", "no-cache");
            response.setDateHeader("Expires", 0);


            CreateImageCode vCode = new CreateImageCode(100,30,5,10);
            session.setAttribute("code", vCode.getCode());
            vCode.write(response.getOutputStream());
     }*/
}

在后台中,我们需要对生成的验证码进行 Base64 编码之后传到前端页面进行展示

@RestController
@RequestMapping("user")
@CrossOrigin        // 允许跨域
@Slf4j		// 日志对象
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("getImage")
    public Map<String,String> getImage(HttpServletRequest request) throws IOException {
        Map<String,String> result = new HashMap<>();
        CreateImageCode createImageCode = new CreateImageCode();
        // 获取验证码
        String securityCode = createImageCode.getCode();
        // 验证码存入 session
        String key = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
        request.getServletContext().setAttribute(key, securityCode);
        // 生成图片
        BufferedImage image = createImageCode.getBuffImg();
        // 进行Base64编码
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ImageIO.write(image,"png",bos);
        String string = Base64Utils.encodeToString(bos.toByteArray());
        result.put("key", key);
        result.put("image", string);
        return result;
    }
}

点端页面:

<img :src="src" id="img-vcode" @click="getImage" :key="key">
<label>
    <div class="label-text">验证码:div>
    <input type="text" v-model="code" name="vcode" style="width: 100px">
label>
<script>
    const app = new Vue({
        el:"#app",
        data:{
            src:"",
            key:""
        },
        methods:{
            getImage(){
                _this = this
                axios.get("http://localhost:8989/user/getImage").then(res => {
                    console.log(res.data)
                    _this.src = "data:image/png;base64,"+res.data.image
                    _this.key = res.data.key
                })
            },
        },
        created(){		// 生命周期函数,提前初始化 data 和 methods 中的数据
            this.getImage();		// 获取验证码图片
        }
    })
</script>

解释:在 vue 代码块中 路径解析部分,使用字符串拼接形式对图片相当于解码操作 data:image/png;base64, 最后的 逗号( , ) 不可以缺少,否则会出错

点后端分页查询功能

在我们使用查询语句的时候,经常要返回前几条或者中间某几行数据

LIMIT 5,10; 检索记录行 6-15
意思是从第6行开始,给下面查询10行数据
LIMIT 5 查询前 5 行数据

分页查询的SQL语句:参数1是开始查询的数据行,参数2是查询数据条数


<select id="findByPage" resultType="Province">
    select id, name, tags, placecounts
    from t_province
    order by placecounts
    limit #{start}, #{rows}
select>

后台 业务层(serviceImpl) 代码
传入的参数是当前所在页数,以及页面显示数量,无法直接应用MySQL的 limit

@Override
public List<Province> findByPage(Integer page, Integer rows) {
    int start = (page - 1) * rows;	// 计算要查询的数据是从第几条开始的
    return provinceDao.findByPage(start, rows);
}

后台 控制层(controller) 代码

// 分页查需
@GetMapping("findByPage")
 public Map<String, Object> findByPage(Integer page, Integer rows){
     page = page == null ? 1 : page;     //
     rows = rows == null ? 4 : rows;     // 4 条数据显示一行
     HashMap<String, Object> map = new HashMap<>();
     // 分页处理
     List<Province> provinces = provinceService.findByPage(page, rows);
     // 计算总页数
     Integer totals = provinceService.findTotals();
     Integer totalPage = totals % rows == 0? totals / rows : totals / rows + 1;
     map.put("provinces", provinces);
     map.put("totals", totals);
     map.put("totalPage", totalPage);
     map.put("page", page);
     return map;
 }

前端页面代码

<div id="pages">
	<a href="javascript:;" @click="findAll(page-1)" v-if="page>1" class="page">上一页a>
	<a href="javascript:;" @click="findAll(indexpage)" class="page" v-for="indexpage in totalPage" v-text="indexpage">a>
	<a href="javascript:;" v-if="page" @click="findAll(page+1)" class="page">下一页a>
div>
<script>
    const app = new Vue({
        el:"#app",
        data:{
            provinces:[],
            page:1,
            rows:4,
            totalPage:0,
            totals:0
        },
        methods:{
            findAll(indexpage){      // 查询所有
                if(indexpage){
                    this.page = indexpage
                }
                _this = this
                axios.get("http://localhost:8989/province/findByPage?page="+this.page).then(res => {
                    _this.provinces = res.data.provinces
                    _this.page = res.data.page
                    _this.totalPage = res.data.totalPage
                    _this.totals = res.data.totals
                })
            }
        },
        created(){
            this.findAll();
        }
    })
</script>

前后端分离项目——文件上传

在后台中controller 实行文件注入方式,并实现文件上传(用 Base64 编码进行处理)

配置文件中 application.properties 中配置文件上传的路径

# 路径中最好不要出现中文,否则会有出现乱码的可能性
spring.web.resources.static-locations=classpath:/static/,file:${upload.dir}
upload.dir=D:\\project_work\\iamges
// 添加景点信息
@PostMapping("save")
public Result save(MultipartFile pic, Place place) throws IOException {
    Result result = new Result();
    try {
        // 文件上传
        System.out.println(pic);
        String extension = FilenameUtils.getExtension(pic.getOriginalFilename());
        String newFileName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + extension;
        //base64编码处理
        place.setPicpath(Base64Utils.encodeToString(pic.getBytes()));
        pic.transferTo(new File(realPath, newFileName));
        // 保存place对象
        placeService.save(place);
        result.setMsg("保存景点信息成功!!!!!");
    } catch (IOException e) {
        result.setState(false).setMsg(e.getMessage());
    }
    return result;
}

前端文件上传:给标签添加 ref 属性 ref = "myFile"

<label>
    <div class="label-text">印象图片:div>
    <div style="text-align: center;padding-left: 36%">
        <div id="upload-tip">+div>
        <img src="" alt="" id="img-show" style="display: none">
        <input type="file" id="imgfile" ref="myFile" style="display: none" onchange="imgfileChange()">
    div>
label>
<script>
    const app = new Vue({
        el:"#app",
        data:{
            provinces:[],
            place:{},
            id:""
        },
        methods:{
            savePlaceInfo(){
                console.log(this.place);
                let myFile = this.$refs.myFile
                let files = myFile.files
                let file = files[0]
                let formData = new FormData()
                formData.append("pic",file)
                formData.append("name",this.place.name)
                formData.append("hottime",this.place.hottime)
                formData.append("hotticket",this.place.hotticket)
                formData.append("dimticket",this.place.dimticket)
                formData.append("placedes",this.place.placedes)
                formData.append("provinceid",this.place.provinceid)
                // axios
                axios({
                    method:'post',
                    url:'http://localhost:8989/place/save',
                    data:formData,
                    headers:{
                        'Content-Type':'multipart/form-data'
                    }
                }).then(res => {
                    console.log(res.data);
                    if (res.data.state){
                        alert(res.data.msg+",点击确定回到景点列表")
                        location.href='./viewspotlist.html?id='+this.place.provinceid
                    }else{
                        alert(res.data.msg+",点击确定回到景点列表")
                    }
                })
            }
        },
    })
</script>

在上传文件时,文件的路径大小设置,将数据库中 picpath 字段需要设置的足够大,这里我设置为 MEDIUMTEXT

href=“javascript:;” 的含义

在上面的代码中对于 a 标签,经常会出现以下的这种写法

<a href="javascript:;" @click="deletePlace(place.id)">删除景点a>

首先对于 标签的 href 属性用于指定超链接目标的 URL,href 属性的值可以是任何有效文档的相对或绝对 URL,包括片段标识符和 JavaScript 代码段
这里的 href="javascript:;" 其中 javascript: 是伪协议,它可以让我们通过一个链接来调用 javascript 函数。而采用这个方式 javascript:; 可以实现 a 标签的点击事件 运行时,如果页面内容很多,有滚动条时,页面不会乱跳,用户体验更好

  • javascript:; 表示什么都不执行,这样点击时就没有任何反应,相当于去掉 a 标签的默认行为

关于 resultType 与 parameterType 的基本使用的区别 :

  • 使用 resultType:主要针对于从数据库中提取相应的数据出来
  • 使用 parameterType:主要针对于将信息存入到数据库中 如: insert 增加数据到数据库中 Update更新等

Vue 获取地址栏跳转的参数

在地址栏中我们会看到这样的路径
http://localhost:8989/viewspot/viewspotlist.html?id=9
这里的 id 会根据切换进行改动,那么这里的 id 怎么直接获取到

 <a :href="'viewspotlist.html?id='+id">返回a>

通过字符串截取的方法进行获取 id

let id = location.href.substring(location.href.indexOf("=")+1)

Vue v-for 的三种用法

v-for 的三种使用方法分别是:

  1. 使用 v-for 循环数组
<p v-for="(item,index) in list1" :key="index">索引值:{{index}}:id:{{item.userid}} 姓名:{{item.username}}p>
  1. 使用 v-for 循环对象
<p v-for="(val,key,index) in list2" :key="index">id:{{val}},name:{{key}},index:{{index}}p>
  1. v-for 循环一个迭代的数字
<p v-for="count in 10" :key="count">这是第{{count}}次循环p>

页面效果展示

登陆页面
SpringBoot + Vue前后端分离之旅游管理系统_第2张图片

景点添加
SpringBoot + Vue前后端分离之旅游管理系统_第3张图片
SpringBoot + Vue前后端分离之旅游管理系统_第4张图片
景点列表展示
SpringBoot + Vue前后端分离之旅游管理系统_第5张图片

省份列表展示
SpringBoot + Vue前后端分离之旅游管理系统_第6张图片

你可能感兴趣的:(mybatis,spring,boot)