自己在跟着视频学习完SSM,SpringBoot时,感觉不实践一下所学的知识一直停留在学习理论阶段,没有真正的会用在项目中,自己一直坚信实践是检验知识学习的有效方式。所以自己跟着视频学习了这个管理系统,总结一下自己的学习过程以及重要知识点。
视频链接: https://www.bilibili.com/video/BV1Nt4y127Jh?p=19
用户表 t_user
—— 独立表
省份表t_province
—— 省份表:景点表 === 1:n
景点表 t_place
数据库名: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 项目
<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
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
在上面的代码中对于 a
标签,经常会出现以下的这种写法
<a href="javascript:;" @click="deletePlace(place.id)">删除景点a>
首先对于 标签的 href 属性用于指定超链接目标的 URL,href 属性的值可以是任何有效文档的相对或绝对 URL,包括片段标识符和 JavaScript 代码段
这里的 href="javascript:;"
其中 javascript: 是伪协议,它可以让我们通过一个链接来调用 javascript
函数。而采用这个方式 javascript:;
可以实现 a 标签的点击事件 运行时,如果页面内容很多,有滚动条时,页面不会乱跳,用户体验更好
javascript:;
表示什么都不执行,这样点击时就没有任何反应,相当于去掉 a 标签的默认行为在地址栏中我们会看到这样的路径
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)
v-for 的三种使用方法分别是:
<p v-for="(item,index) in list1" :key="index">索引值:{{index}}:id:{{item.userid}} 姓名:{{item.username}}p>
<p v-for="(val,key,index) in list2" :key="index">id:{{val}},name:{{key}},index:{{index}}p>
<p v-for="count in 10" :key="count">这是第{{count}}次循环p>