此项目为一个springboot+vue入门级小项目,视频地址为:https://www.bilibili.com/video/BV1Nt4y127Jh
业务简单,对提升业务能力没什么大的帮助,更多的是可以熟悉开发流程和编码。
仅仅三张表,分别为用户表,省份表和景点表,其中省份表和景点表为一对多的关系。
用户表(t_user):
省份表(t_province):
景点表(t_place):
server.port=8989
spring.application.name=travels
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/travels?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
mybatis.mapper-locations=classpath:org/hz/travels/mapper/*.xml
mybatis.type-aliases-package=org.hz.travels.entity
#图片存储到本地位置
spring.resources.static-locations=file:${upload.dir}
upload.dir=E://images
原始验证码的实现较为简单,通过验证码工具类生成一个验证码,通过 session.setAttribute(“code”, randomCode.toString());将code设置到session域中,当用户注册的时候,输入的验证码与session中的验证码进行比较即可。
**但是此项目由于使用的是前后端分离项目,前端采用vue+axios方式,axios底层封装了Ajax请求,但是Ajax请求是XMLHTTPRequest
对象发起的而不是浏览器,导致每次请求都会生成一个seesion会话,这样当点击注册按钮之后,对生成一个新的会话,而这个新的会话中不存在“code”,所以在比对用户提交的验证码的时候就会出现异常。**原生Ajax的解决方法:提供了xhrFields: {withCredentials: true} 属性用于携带认证信息,即携带上一次请求生成的Cookie去发出请求,保证了Session的唯一性。
但是在axios中暂时知道怎么使用,所以此处先临时将验证码设置到了ServletContext对象中。
后端代码如下:
/*
获取验证码,通过map将验证码的base64编码响应给客户端
*/
@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);
//进行Base64的一个编码
String string = Base64Utils.encodeToString(bos.toByteArray());
result.put("key", key);
result.put("image", string);
return result;
}
前端渲染:
动态绑定src
<img :src="src" id="img-vcode" @click="getImage" :key="key">
const app = new Vue({
el: "#app",
data:{
user:{},
code:"",
src:"",
key:"",
},
methods:{
getImage(){ //获取验证码
_this = this;
axios.get("http://localhost:8989/user/getImage").then((res)=>{
console.log(res.data.key);
//绑定图片的src属性。
_this.src = "data:image/png;base64,"+res.data.image;
_this.key = res.data.key;
});
}
},
//生命周期函数,实例生成以后执行
created(){
this.getImage();//获取验证码
}
});
_this.src = “data:image/png;base64,”+res.data.image; //解码的时候必须指定前缀
_this.key = res.data.key; //这个key是后台设置的ServletContext
created(){
this.getImage(); //created()中方法在vue实例创建之前调用,即访问界面就加载验证码
}
<form action="province/provincelist.html" method="post">
<label>
<div class="label-text">账 号:div>
<input type="text" v-model="user.username" name="username">
label>
<label>
<div class="label-text">密 码:div>
<input type="password" v-model="user.password" name="password">
label>
<label>
<div class="label-text">邮 箱:div>
<input type="text" v-model="user.email" name="email">
label>
<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>
<button type="button" @click="saveUserInfo">提 交button>
<a href="login.html">去登录a>
form>
使用v-model和this.user属性进行绑定,图片的绑定key属性
javacript代码:
const app = new Vue({
el: "#app",
data:{
user:{}, //关联的用户对象
code:"", //用户输入的验证码
src:"", //绑定的src属性
key:"", //后台返回的时间戳key,前台重新提交,根据key判断ServletContext是否存在
},
methods:{
saveUserInfo(){ //注册
console.log(this.user.username + this.user.password + this.user.email);
console.log(this.code);
if(!this.user.username){
alert('用户名不能为空!!!!');
return;
}
if(!this.user.password){
alert('密码不能为空!!!!');
return;
}
//发送axios
axios.post("http://localhost:8989/user/register?code="+this.code+"&key="+this.key,this.user).then((res)=>{
console.log(res);
if(res.data.state){
alert(res.data.msg+",点击确定跳转到登录页面!!!");
location.href='./login.html';
}else{
alert(res.data.msg);
}
});
}
},
//生命周期函数,实例生成以后执行
created(){
this.getImage();//获取验证码
}
});
后端注册代码:
@PostMapping("register")
public Result register(String code, String key, @RequestBody User user, HttpServletRequest request) {
Result result = new Result();
//获取ServletContex中存储的验证码
String keyCode = (String) request.getServletContext().getAttribute(key);
//验证
try {
if (code.equalsIgnoreCase(keyCode)) {
//注册用户
userService.register(user);
result.setState(true);
result.setMsg("注册成功!!!");
} else {
throw new RuntimeException("验证码错误!!!");
}
} catch (Exception e) {
e.printStackTrace();
//捕获异常信息,并将其设置到响应信息中
result.setMsg(e.getMessage()).setState(false);
}
return result;
}
最终效果:
分页查询后端代码:
@RequestMapping("findByPage")
public Map<String ,Object> findByPage(Integer page,Integer rows){
page = page == null ? 1 : page;
rows = rows == null ? 4 : rows;
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;
}
使用map集合返回给前端,其中需要的数据有:
分页查询Service层代码:
@Override
public List<Province> findByPage(Integer page, Integer rows) {
//比如查询第1页,那么就查询的是limit 0,rows
int start = (page-1)*rows;
return provinceMapper.findByPage(start,rows);
}
UserMapping.xml代码:
<resultMap id="BaseResultMap" type="org.hz.travels.entity.Province" >
<id column="id" property="id" jdbcType="INTEGER" />
<result column="name" property="name" jdbcType="VARCHAR" />
<result column="tags" property="tags" jdbcType="VARCHAR" />
<result column="placecounts" property="placecounts" jdbcType="INTEGER" />
resultMap>
<select id="findByPage" resultType="Province">
select id,name,tags,placecounts
from t_province
order by placecounts
limit #{start},#{rows}
select>
前端实现:
html:
<div id="pages">
<a href="javascript:;" @click="findAll(page-1)" v-if="page>1" class="page"><上一页a>
<a class="page" href="javascript:;" v-for="indexpage in totalPage" @click="findAll(indexpage)" 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: 2, //指定每页加载两条数据
totalPage: 0, //总共多少页
totals: 0, //共多少条数据
},
methods: {
//用户点击第几页,就传这个页码的值去做请求。
findAll(indexpage) { //查询所有
if (indexpage) {
this.page = indexpage;
}
_this = this;
axios.get("http://localhost:8989/province/findByPage?page=" + this.page + "&rows=" + this.rows).then((res) => {
_this.provinces = res.data.provinces;
_this.page = res.data.page;
_this.totalPage = res.data.totalPage;
_this.totals = res.data.totals;
});
}
},
//vue实例化前加载列表
created() {
this.findAll();
}
})
修改省份信息:
业务代码比较简单,需要注意的只是前端的一点:
<a :href="'./updateprovince.html?id='+province.id">修改省份a>
在省份列表界面点击修改,跳转到修改界面,传过去一个省份id。
在修改界面拿到这个id,进行修改操作,修改之前先要把该省份信息加载到修改界面:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDfKsn0J-1592974475684)(E:\MarkDown\项目分析篇\2.springboot+vue旅游项目\images\img8.png)]
new Vue({
el: "#app",
data: {
id: "",
province: {}
},
methods: {
findOneProvince(id) {
_this = this;
axios.get("http://localhost:8989/province/findOne?id=" + id).then((res) => {
console.log(res.data);
_this.province = res.data;
});
},
updateProvince() {
axios.post("http://localhost:8989/province/updateProvince", this.province).then((res) => {
if (res.data.state) {
alert(res.data.msg + ",点击确定回到主页!!!");
location.href = './provincelist.html';
} else{
alert(res.data.msg);
}
});
}
},
created() {
this.id = location.href.substring(location.href.indexOf("=") + 1);
this.findOneProvince(this.id);
}
})
添加模块主要牵扯到文件上传的问题,另外需要更新省份的placeCount字段
后端代码如下:
//将图片保存到本地位置
@Value("${upload.dir}")
private String realPath;
@PostMapping("save")
public Result save(MultipartFile pic,Place place) throws IOException {
Result result=new Result();
try {
//对图片文件进行Base64的转化
String picpath = Base64Utils.encodeToString(pic.getBytes());
place.setPicpath(picpath);
//处理文件上传
String extension = FilenameUtils.getExtension(pic.getOriginalFilename());
String newFileName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + extension;
//将这个文件写入到本地磁盘的具体位置
pic.transferTo(new File(realPath,newFileName));
Integer id = place.getProvince().getId();
Province oneProvince = provinceService.findOneProvince(id);
oneProvince.setPlacecounts(oneProvince.getPlacecounts()+1);
//更新省份信息
provinceService.updateProvince(oneProvince);
placeService.save(place);
result.setMsg("保存景点信息成功");
}catch (Exception e){
result.setMsg(e.getMessage());
}
return result;
}
前端代码:
使用vue中的ref注册组件信息:
<input type="file" ref="myFile" id="imgfile" style="display: none" onchange="imgfileChange()">
由于使用了文件,所以提交的时候需要使用FormData的方式提交:script如下:
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("province.id", 1);
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 + ",点击确定回到景点列表");
}
});
}
formData.append(“province.id”, 1); 外键关联属性绑定的时候使用这个
在添加的时候需要绑定省份id,绑定方法如下:
<select v-model="place.province.id">
<option v-for="(pro,index) in provinces" :value="pro.id" v-text="pro.name">option>select>
分页和省份列表一样,不同的是需要渲染一个base64的图片信息。
渲染的时候必须在属性前加上如下代码:
<td><img :src="'data:image/png;base64,'+place.picpath" class="viewspotimg">td>
注:此文仅仅为个人的一些编码总结。