<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.jtgroupId>
<artifactId>jtartifactId>
<version>1.0-SNAPSHOTversion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.5.3version>
<relativePath/>
parent>
<properties>
<java.version>1.8java.version>
<skipTests>trueskipTests>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.3version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>2.5.3version>
plugin>
plugins>
build>
project>
说明: 从码云中下载jt的完整代码,之后粘贴到本地项目中,如图所示:
server:
port: 8091
servlet:
context-path: /
#配置数据源
spring:
datasource:
#如果使用高版本驱动 则添加cj
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/jt?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
username: root
password: root
#mybatis-plush配置
mybatis-plus:
type-aliases-package: com.jt.pojo
mapper-locations: classpath:/mappers/*.xml
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.jt.mapper: debug
最好统一项目路径.
要求: 将前端项目放到IDEA维护的工作目录中.
如图所示:
注意事项: 不要出现目录的嵌套,要求目录的根 就是项目. 如图
目的: 经常性的出现本来编辑的是李逵的代码,但是由于代码不熟练,编辑的李鬼. 所以有如下的测试.
编辑App.vue文件
代码测试:
前端项目网址: http://localhost:8080/
后端项目网址: http://localhost:8091/
用户操作项目请求的流程: 用户----前端服务器------后端服务器
注意事项:
1.密码 加密处理
2.created/updated 每张表中都有该字段
人员配置:
1.项目经理(统筹规划协调资源,控制项目进度)
2.职能经理: 凭技术吃饭的 项目组长.
3.产品经理: 根据甲方要求,绘制项目原型. 出原型图
4.UI设计: 根据产品的图,用美学的眼光 绘制图片/按钮/logo
5.前端开发: 绘制页面(html/css/js)
6.后端开发: 根据业务接口文档.实现项目
7.测试工程师: 工具测试(黑百盒测试) 测试机型!!!
8.实施/运维工程师 事情杂/工资低
请求路径: /user/login
请求方式: POST
请求参数
响应数据 SysResult对象
返回值格式如下:
{"status":200,"msg":"服务器调用成功!","data":"1e893a97634847b3a8b499b173bea620"}
说明: 该对象主要负责前端项目与后端项目的数据交互. 几乎所有的后台服务器的返回值都是SysResult对象.
package com.jt.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class SysResult implements Serializable {
private Integer status; //200业务成功 201业务失败
private String msg; //服务器提示信息
private Object data; //封装后台返回值
public static SysResult fail(){
return new SysResult(201,"业务执行失败",null);
}
public static SysResult success(){
return new SysResult(200,"业务执行成功!", null);
}
//服务器返回值业务数据
public static SysResult success(Object data){
return new SysResult(200,"业务执行成功!", data);
}
public static SysResult success(String msg,Object data){
return new SysResult(200,msg,data);
}
}
1.路由跳转规则
MD5信息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。MD5由美国密码学家罗纳德·李维斯特(Ronald Linn Rivest)设计,于1992年公开,用以取代MD4算法。这套算法的程序在 RFC 1321 标准中被加以规范。1996年后该算法被证实存在弱点,可以被加以破解,对于需要高度安全性的数据,专家一般建议改用其他算法,如SHA-2。2004年,证实MD5算法无法防止碰撞(collision),因此不适用于安全性认证,如SSL公开密钥认证或是数字签名等用途。
理论: 1.MD5不可以被破解的. 只能由明文加密为密文. 不可以反向编译
用户名: admin123
密码: admin123456
package com.jt.controller;
import com.jt.pojo.User;
import com.jt.service.UserService;
import com.jt.vo.SysResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @author
* 时间 2021/5/11
*/
@RestController
@CrossOrigin//跨域
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/hello")
public List<User> hello(){
return userService.findAll();
}
/**
* 业务说明:
* 思想:根据参数,查询数据库
* 有值:用户名和密码正确
* 没有值:用户名和密码错误
* URL:/user/login
* 参数:username/password
* 类型:post
* 返回值:SysResult对象(token)
*/
@PostMapping("/login")
public SysResult login(@RequestBody User user){
//需求:要求登陆成功之后,返回值标识符信息
String token = userService.login(user);
//如果token为null,说明登录失败
if(token == null || token.length() ==0){
return SysResult.fail();
}
//否则 正确返回
return SysResult.success(token);
}
}
package com.jt.service;
import com.jt.pojo.User;
import java.util.List;
/**
* @author
* 时间 2021/5/11
*/
public interface UserService {
List<User> findAll();
String login(User user);
}
package com.jt.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jt.mapper.UserMapper;
import com.jt.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.util.List;
/**
* @author
* 时间 2021/5/11
*/
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Override
public List<User> findAll() {
return userMapper.selectList(null);
}
/**
* 思路:
* 1.将密码进行加密处理
* 2.根据username/password查询数据库
* 3.有值:
* 登录成功
* 没有值:
* 登录失败
*
*/
@Override
public String login(User user) {
//加密算法 MD5
//1.获取明文
String password = user.getPassword();
//2.加密处理
String md5 = DigestUtils.md5DigestAsHex(password.getBytes());
user.setPassword(md5);
//3.查询数据库
QueryWrapper<User> queryWrapper =
new QueryWrapper<>(user);
//4.获取数据对象
User userDB = userMapper.selectOne(queryWrapper);
//5.判断登录是否正确
if(userDB == null){
return null;
}
String token = "我是秘钥";
return token;
}
}
1.由于服务器需要标识已经登录的用户,所以服务器动态生成一个独一无二的token,返回给用户.
2.用户将token保存到本地,方便下次访问时携带.
package com.jt.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jt.mapper.UserMapper;
import com.jt.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.util.List;
import java.util.UUID;
/**
* @author
* 时间 2021/5/11
*/
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Override
public List<User> findAll() {
return userMapper.selectList(null);
}
/**
* 思路:
* 1.将密码进行加密处理
* 2.根据username/password查询数据库
* 3.有值:
* 登录成功
* 没有值:
* 登录失败
*
*/
@Override
public String login(User user) {
//加密算法 MD5
//1.获取明文
String password = user.getPassword();
//2.加密处理
String md5 = DigestUtils.md5DigestAsHex(password.getBytes());
user.setPassword(md5);
//3.查询数据库
QueryWrapper<User> queryWrapper =
new QueryWrapper<>(user);
//4.获取数据对象
User userDB = userMapper.selectOne(queryWrapper);
//5.判断登录是否正确
if(userDB == null){
return null;
}
//6.动态使用UUID生成TOKEN,根据当前毫秒数+随机数利用hash算法生成
// 几乎可以保证不重复
String token = UUID.randomUUID().toString()
.replace("-", "");
return token;
}
}
**Session:在计算机中,尤其是在网络应用中,称为“会话控制”。**Session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页之间跳转时,存储在Session对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web页时,如果该用户还没有会话,则Web服务器将自动创建一个 Session对象。当会话过期或被放弃后,服务器将终止该会话。Session 对象最常见的一个用法就是存储用户的首选项。例如,如果用户指明不喜欢查看图形,就可以将该信息存储在Session对象中。有关使用Session 对象的详细信息,请参阅“ASP应用程序”部分的“管理会话”。注意会话状态仅在支持cookie的浏览器中保留。
小结:
1.Session称之为 “会话机制”
2.在浏览器中打开网页 就是一个会话.
3.用户的数据可以保存到会话中,但是有生命周期. 当会话关闭,则数据消失.
Cookie,有时也用其复数形式 Cookies。类型为“小型文本文件”,是某些网站为了辨别用户身份,进行Session跟踪而储存在用户本地终端上的数据(通常经过加密),由用户客户端计算机暂时或永久保存的信息 [1] 。
说明:
1.cookie是一个小型的文本文件
2.cookie中存储的数据一般都是密文.
3.cookie中的数据的生命周期可控. 几天.几年!!!
1.session的数据是临时存储.cookie的数据可以永久保存. (生命周期不同)
2.sesion是浏览器中的一个内存对象!而cookie是一个实际的本地文件. (形式不同).
3.session一般存储是一些涉密数据.cookie一般存储相对公开的数据(免密登录). (安全性)
login(){
//获取表单对象之后进行数据校验
//valid 表示校验的结果 true表示通过 false表示失败
this.$refs.loginFormRef.validate(async valid => {
//如果没有完成校验则直接返回
if(!valid) return
//如果校验成功,则发起ajax请求
const {data: result} = await this.$http.post('/user/login',this.loginForm)
if(result.status !== 200) return this.$message.error("用户登录失败")
this.$message.success("用户登录成功")
//获取用户token信息
let token = result.data
window.sessionStorage.setItem("token",token)
//用户登录成功之后,跳转到home页面
this.$router.push("/home")
})
}
编辑router/index.js 添加组件信息,实现首页跳转
前端页面跳转是通过路由进行控制,规定:如果用户没有登录,则只允许访问登录页面,只有登录之后才能访问其他页面
难点:如何实现用户请求的拦截
拦截器的作用:拦截用户的请求。
结果1:请求放行
结果2:请求拦截,一般配合重定向使用
//路由导航守卫!!!!!!!
const router = new VueRouter({
routes
})
/**
* 定义路由导航守卫
* 参数1: to 路由跳转的网址
* 参数2: from 路由·从哪里来
* 参数3: next 是一个函数,表示放行或者重定向
* next() 放行
* next("/login") 重定向
* 业务实现:
* 核心逻辑: 检查是否有token
* 有token 表示已经登录,放行请求
* 没有token 表示用户没有登录,重定向到登录页面
* 如果没有访问login页面 直接放行
*/
router.beforeEach((to,from,next)=> {
if(to.path === "/login"){
return next()
}
//说明用户访问的页面不是login 请求需要校验
//获取token数据
let token = window.sessionStorage.getItem("token")
//if(token != null && token.length>0)
//下列if的判断 解释:如果token不为null 字符串的判断
if(token){
return next()
}
next("/login")
})
说明:
name: 权限名称
parent_id: 父级权限 实现了父子级关系.
path: 一级菜单没有路径的, 二级/三级才有路径.
说明:
1.当前对象中children属性不属于字段,所以使用@TableField(exist = false)进行标识.
2.权限列表中有父子级关系,所以通过children封装子级
只查询一级和二级的信息
请求路径 /rights/getRightsList
请求类型 GET
请求参数 无
响应数据 SysResult对象
/**
* 实现思路:
* 1.先查询一级菜单信息 parent_id = 0
* 2.将一级菜单循环遍历 一级菜单对象.
* 3.根据一级菜单信息,查询当前菜单下的二级.
* 4.将查询得到的二级菜单,封装到一级对象中
* 实现思路二(扩展):
* 利用左连接 实现关联查询 封装数据.
* @return
*/
@Override
public List<Rights> getRightsList() {
//1.查询一级列表信息
QueryWrapper<Rights> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("parent_id",0);
List<Rights> oneList = rightsMapper.selectList(queryWrapper);
//2.遍历一级列表
for (Rights oneRights : oneList){
//根据一级查询二级
queryWrapper.clear(); //清除之前的条件
queryWrapper.eq("parent_id",oneRights.getId());
List<Rights> twoList = rightsMapper.selectList(queryWrapper);
//将查询的结果封装一级对象中
oneRights.setChildren(twoList);
}
return oneList;
}
说明: debug是编程中常用的代码的调试工具. 可以让代码一行一行执行.
业务说明: URL:/user 要求跳转组件 User.vue组件,组件的跳转应该在Home组件内部跳转.
Home组件说明: 在main的区域中,设定 作用 Home的子级标签将来组件在该区域展现.
<template>
<div>
<el-breadcrumb separator-class="el-icon-arrow-right" >
<el-breadcrumb-item :to="{ path: '/' }">首页el-breadcrumb-item>
<el-breadcrumb-item>活动管理el-breadcrumb-item>
<el-breadcrumb-item>活动列表el-breadcrumb-item>
<el-breadcrumb-item>活动详情el-breadcrumb-item>
el-breadcrumb>
<el-card class="box-card">
<div class="text item">
<el-row :gutter="20">
<el-col :span="8">
<el-input placeholder="请输入内容" v-model="input3" class="input-with-select">
<el-button slot="append" icon="el-icon-search">el-button>
el-input>
el-col>
<el-col :span="3">
<el-button type="primary">主要按钮el-button>
el-col>
el-row>
div>
<h1>定义表格h1>
<el-table :data="tableData"style="width: 100%" stripe="true">
<el-table-column
label="编号" prop="id">
el-table-column>
<el-table-column
label="名称" prop="name">
el-table-column>
<el-table-column
label="年龄" prop="age">
el-table-column>
<el-table-column
label="性别" prop="sex">
el-table-column>
el-table>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="100"
:page-sizes="[100, 200, 300, 400]"
:page-size="100"
layout="total, sizes, prev, pager, next, jumper"
:total="1000">
el-pagination>
el-card>
div>
template>
<script>
// 对外声明组件属性/方法等参数,要被根组件调用
export default {
data () {
return {
msg: '我是vue子组件'
}
}
}
script>
<script>
//对外声明组件属性/方法等参数.要被根组件调用
export default{
data(){
return {
tableData:[
{id:1,name:"01",age:100,sex:"男"},
{id:2,name:"02",age:100,sex:"男"},
{id:3,name:"03",age:100,sex:"男"},
{id:4,name:"04",age:100,sex:"男"},
]
}
},
methods:{
handleSizeChange(size){
alert("当前条数"+size)
},
handleCurrentChange(current){
alert("当前页数"+current)
}
}
}
script>
<style lang="lesss" scoped="">
style>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="100"
:page-sizes="[10, 20, 30, 40]"
:page-size="10"
layout="total, sizes, prev, pager, next, jumper"
:total="1000">
el-pagination>
1.生命周期函数
//利用钩子函数实现数据查询
mounted(){
this.getUserList()
}
2.获取用户列表数据
async getUserList(){
const {data: result} = await this.$http.get('/user/list',{
params: this.queryInfo
})
if(result.status !== 200) return this.$message.error("用户列表查询失败")
this.userList = result.data.rows
this.total = result.data.total
console.log("总记录数:"+this.total)
},
请求路径: /user/list
请求类型: GET
请求参数: 后台使用PageResult对象接收
请求案例: http://localhost:8091/user/list?query=查询关键字&pageNum=1&pageSize=10
响应参数: SysResult对象 需要携带分页对象 PageResult
PageResult 对象介绍
返回值效果
{"status":200,
"msg":"服务器调用成功!",
"data":
{"query":"",
"pageNum":1,
"pageSize":2,
"total":4,
"rows":[
{"created":"2021-02-18T11:17:23.000+00:00",
"updated":"2021-03-26T06:47:20.000+00:00",
"id":1,
"username":"admin",
"password":"a66abb5684c45962d887564f08346e8d",
"phone":"13111112222",
"email":"[email protected]",
"status":true,
"role":null
},
{"created":"2021-02-18T11:17:23.000+00:00",
"updated":"2021-03-13T08:50:30.000+00:00",
"id":2,
"username":"admin123",
"password":"a66abb5684c45962d887564f08346e8d",
"phone":"13111112223",
"email":"[email protected]",
"status":false,
"role":null
}
]
}
}
package com.jt.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
public class PageResult implements Serializable {
private String query;//用户查询的数据
private Integer pageNum;//查询的页数
private Integer pageSize;//查询的条数
private Long total;//查询总记录数
private Object rows;//分页查询的结果
}
package com.jt.controller;
import com.jt.pojo.User;
import com.jt.service.UserService;
import com.jt.vo.PageResult;
import com.jt.vo.SysResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @author
* 时间 2021/5/11
*/
@RestController
@CrossOrigin//跨域
//@CrossOrigin({"http://localhost:8080"})
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/hello")
public List<User> hello(){
return userService.findAll();
}
/**
* 业务说明:
* 思想:根据参数,查询数据库
* 有值:用户名和密码正确
* 没有值:用户名和密码错误
* URL:/user/login
* 参数:username/password
* 类型:post
* 返回值:SysResult对象(token)
*/
@PostMapping("/login")
public SysResult login(@RequestBody User user){
//需求:要求登陆成功之后,返回值标识符信息
String token = userService.login(user);
//如果token为null,说明登录失败
if(token == null || token.length() ==0){
return SysResult.fail();
}
//否则 正确返回
return SysResult.success(token);
}
/**
* 业务说明:实现用户列表的分页查询
* url:http://localhost:8080/#/user/list?query=查询关键字&pageResult
* 参数:pageResult
*/
@GetMapping("/list")
public SysResult getUserList(PageResult pageResult){//3个参数
//pageResult 引用类型
pageResult = userService.getUserList(pageResult);
return SysResult.success(pageResult);//封装5个参数
}
}
/**
* 分页sql:
* 语法:select * from user limit 起始位置,每页条数
* 规则:含头不含尾
* 查询第一页:
* select * from user limit o,10
* 查询第二页:
* select * from user limit 10,10 10-19
* 查询第三页:
* select * from user limit 20,10 20-29
* 查询第n页:
* * select * from user limit (n-1)*条数,条数
*
* @param pageResult
* @return
*/
@Override
public PageResult getUserList(PageResult pageResult) {
//1.获取总记录数
//必须是long,大写Long类型不匹配
long total = userMapper.selectCount(null);
//2.获取分页结果
int size = pageResult.getPageSize();
int start = (pageResult.getPageNum()-1) * size;
List<User> userList = userMapper.findListByPage( start,size);
pageResult.setTotal(total)
.setRows(userList);
return pageResult;
}
说明: 利用注解实现Sql查询, 映射文件和注解标签二选一 不能同时使用
package com.jt.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.pojo.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* @author
* 时间 2021/2/2
*/
public interface UserMapper extends BaseMapper<User> {
//注解方式查询数据库!!! 限制条件:只适用简单sql 注解/映射文件二选一
@Select("SELECT * FROM user limit #{start},#{size}")
// @Insert()
// @Update()
// @Delete()
List<User> findListByPage(int start,int size);
}
/**
* 业务说明: 实现用户列表的分页查询
* URL地址: http://localhost:8091/user/list?query=查询关键字&pageNum=1&pageSize=10
* 参数: pageResult接收
* 返回值: SysResult对象(pageResult)
*/
@GetMapping("/list")
public SysResult getUserList(PageResult pageResult){//3
pageResult = userService.getUserList(pageResult);
return SysResult.success(pageResult);//5
}
/*
* 业务说明: 利用MP方式查询数据库.
* 步骤梳理:
* 1.构建MP的分页对象
* 2.根据分页对象查询数据.
* 3.从分页对象中获取数据
* 4.封装PageResult对象
* 5.编辑配置类 封装分页拦截器
* */
@Override
public PageResult getUserList(PageResult pageResult) {
//1.定义分页对象
IPage<User> page = new Page<>(pageResult.getPageNum(),
pageResult.getPageSize());
//2.定义条件构造器 指定动态查询Sql
boolean flag = StringUtils.hasLength(pageResult.getQuery());
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like(flag, "username",pageResult.getQuery());
//3.进行分页查询
page = userMapper.selectPage(page,queryWrapper);
//4.从封装后的分页对象中获取数据
pageResult.setTotal(page.getTotal())
.setRows(page.getRecords());
return pageResult;
}
package com.jt.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false
* 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
//MyBatisPlus在执行分页操作时,会被该拦截器拦截
//拦截器的作用 动态拼接where条件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return interceptor;
}
}
说明: 通过开关 可以控制数据类型的 true/false
参数说明: 1.传递userId主键
2.传递当前状态信息 true/false
<el-table-column prop="status" label="状态">
<!-- 定义作用于插槽-->
<template slot-scope="scope">
<el-switch v-model="scope.row.status" @change="updateStatus(scope.row)"
active-color="#13ce66" inactive-color="#ff4949">
</el-switch>
</template>
</el-table-column>
async updateStatus(user){
//实现用户状态修改 注意使用模版字符串 ES6中提出的新用法 ${key}
const {data: result} = await this.$http.put(`/user/status/${user.id}/${user.status}`)
if(result.status !== 200) return this.$message.error("用户状态修改失败!")
this.$message.success("用户状态修改成功!")
},
{"status":200,"msg":"服务器调用成功!","data":null}
/**
* 业务说明: 修改状态信息
* URL: /user/status/{id}/{status}
* 参数: id/status
* 返回值: SysResult
*/
@PutMapping("/status/{id}/{status}")
public SysResult updateStatus(User user){
userService.updateStatus(user);
return SysResult.success();
}
//规则: 根据对象中不为null的元素当作set条件,
// Id当作唯一where条件
//update user set status=true where id=xxx
@Override
public void updateStatus(User user) {//id/status
userMapper.updateById(user);
}
说明: 通过属性dialogVisible 控制对话框是否可见.
<!--
:visible.sync 控制对话框是否可见 true/false
-->
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="30%">
<span>这是一段信息</span>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
</span>
</el-dialog>
请求路径 /user/addUser
请求类型 POST
请求参数: 整个form表单数据
返回值结果: SysResult对象
{"status":200,"msg":"服务器调用成功!","data":null}
/**
* 业务: 实现用户新增
* url: /user/addUser
* 参数: 整个form表单 对象 json
* 返回值: SysResult对象
*/
@PostMapping("/addUser")
public SysResult addUser(@RequestBody User user){
userService.addUser(user);
return SysResult.success();
}
/**
* 说明:
* 1.用户入库操作需要手动补齐的数据有
* 创建时间/修改时间 保证一致.
* status=true 手动填充.
* 2.密码加密处理 新增和登录加密算法必须一致
* @param user
*/
@Override
public void addUser(User user) {
String password = user.getPassword();
//加密处理
password = DigestUtils.md5DigestAsHex(password.getBytes());
user.setPassword(password)
.setStatus(true)
.setCreated(new Date())
.setUpdated(user.getCreated());
userMapper.insert(user);
}
当用户点击修改按钮时该出现弹出框,其中展现用户的数据信息
展现方式:
1.获取用户ID,动态查询后台数据库信息。之后实现数据回显 最新数据
2.可以利用作用域插槽,获取当前行的对象,之后实现数据回显, 页面数据
<el-table-column label="操作">
<template slot-scope="scope">
<el-button type="primary" icon="el-icon-edit" size="small" @click="updateUserBtn(scope.row)"></el-button>
<el-button type="danger" icon="el-icon-delete" size="small" @click="deleteUser(scope.row)"></el-button>
</template>
</el-table-column>
//JS方法
async updateUserBtn(user){
this.updateDialogVisible = true
const {data: result} = await this.$http.get("/user/"+user.id)
if(result.status !== 200) return this.$message.error("用户查询失败")
this.updateUserModel = result.data
},
请求路径: /user/{id}
请求类型: GET
返回值: SysResult对象
JSON格式如下:
{
"status":200,
"msg":"服务器调用成功!",
"data":{
"created":"2021-02-18T11:17:23.000+00:00",
"updated":"2021-05-17T11:33:46.000+00:00",
"id":1,
"username":"admin",
"password":"a66abb5684c45962d887564f08346e8d",
"phone":"13111112222",
"email":"[email protected]",
"status":true,
"role":null
}
}
/**
* 业务:实现用户id查询
* 请求路径: /user/{id}
* 请求类型: GET
* 参数:主键id
* 返回值: SysResult(User)对象
*/
@GetMapping("/{id}")
public SysResult getUserById(@PathVariable Integer id){
User user =userService.getUserById(id);
return SysResult.success(user);
}
@Override
public User getUserById(Integer id) {
return userMapper.selectById(id);
}
说明: 当用户点击确定按钮时,会触发点击事件
<span slot="footer" class="dialog-footer">
<el-button @click="updateDialogVisible = false" >取 消</el-button>
<el-button type="primary" @click="updateUser">确 定</el-button>
</span>
请求路径: /user/updateUser
请求类型: PUT
请求参数: User对象结构
返回值: SysResult对象
JSON格式如下:
{
"status":200,
"msg":"服务器调用成功!",
"data":{}
}
/**
* 业务:实现用户修改操作
* 请求路径: /user/updateUser
* 请求类型: PUT
* 请求参数: User对象提交 JSON字符串 注解接收
* 返回值:SysResult
*/
@PutMapping("/updateUser")
public SysResult updateUser(@RequestBody User user){
userService.updateUser(user);
return SysResult.success();
}
//不为null的元素当作set条件,Id当作唯一where条件
@Override
public void updateUser(User user) {
userMapper.updateById(user);
}
<el-table-column label="操作">
<template slot-scope="scope">
<el-button type="primary" icon="el-icon-edit" size="small" @click="updateUserBtn(scope.row)"></el-button>
<el-button type="danger" icon="el-icon-delete" size="small" @click="deleteUser(scope.row)"></el-button>
</template>
</el-table-column>
async deleteUser(user){
//1.消息确认框
const result = await this.$confirm('此操作将永久删除 '+user.username+', 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).catch(error => error)
//如果确认 confirm 如果取消 cancel
if(result !== 'confirm'){
return this.$message.info("删除取消")
}
const {data: result2} = await this.$http.delete(`/user/${user.id}`)
if(result2.status !== 200) return this.$message.error("删除失败")
this.$message.success("删除成功")
//重新加载 数据
this.getUserList()
}
}
请求路径: /user/{id}
请求类型: delete
请求参数:
返回值: SysResult对象
/**
* 业务:实现用户删除
* 请求路径:/user/${user.id}
* 请求类型: delete
* 参数: User对象结构
* 返回值:SysResult对象
*
*
* post:新增,表单提交
* delete:删除操作
* 使用相同的路径可以实现不同的功能
*/
@DeleteMapping("/{id}")
public SysResult deleteById(@PathVariable Integer id){
userService.deleteById(id);
return SysResult.success();
}
@Override
public void deleteById(Integer id) {
userMapper.deleteById(id);
}
说明: 如果后台服务器发生运行时异常,应该第一时间通知用户.否则用户没有任何提示.影响用户体验.
分析:
1.页面没有提示的原因是服务器没有返回201的报错数据.
2.后端服务器报错之后,没有异常处理机制
总结:
异常的处理是项目中经常用到的机制,但是如果将大量的异常处理代码写在方法中,则影响程序的结构.导致代码混乱.
AOP说明:
名称: “面向切面编程”
作用: 在不影响源码的条件下,对方法进行扩展,降低了业务的耦合性.
通知:
1.前置通知: before
2.后置通知: afterReturning
3.异常通知: afterThrowing
4.最终通知: after
上述的四大通知,不能改变程序的运行的状态.
5.环绕通知: around
环绕通知是功能最为强大的通知方法,可以控制程序的流转过程.
package com.jt.aop;
import com.jt.vo.SysResult;
import org.apache.ibatis.jdbc.SQL;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLException;
/**
* 注解的作用:
* 1.该注解只拦截Controller层抛出的异常信息
* Controller---Service---Mapper 异常向上抛出
* 2.需要配合指定异常的类型
*
*/
@RestControllerAdvice
public class SystemAOP {
//当前Controller层,只拦截运行时异常
//@ExceptionHandler({RuntimeException.class, SQLException.class})
@ExceptionHandler({RuntimeException.class})
public SysResult exception(Exception e){
//控制台打印异常
e.printStackTrace();
return SysResult.fail();
}
}
如果后台服务器发生问题,则数据信息应该回滚,而不是提交操作.
事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起,并用形如begin transaction和end transaction语句(或函数调用)来界定。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。
事务作用: 可以保证数据的 /持久性(永久性)/一致性/隔离性/原子性
原始代码结构:出现2层循环结构,如果外层循环10个,每个内层循环也是10个,完成这项业务需要查询100次数据库
矛盾点:多次查询数据库!!!
优化的策略:能否将查询的次数降低到1次,就可以获取所有的数据信息
说明:数据裤只查询一次,就可以获取商品分类三级嵌套结构
程序设计:
1.数据结构Map<父级ID,子级列表> 列表信息中不包含嵌套关系
例如:Map<0,一级列表信息> 一级列表不包含二级/三级
Map<一级ID,二级列表信息> 只有2级 二级列表信息不包含三级/四级
Map<二级ID,三级列表信息> 只有23级 三级列表信息不包含四级
2.根据数据结构动态根据level查询自己
/**
* 1.查询所有的商品分类列表, 查询一次数据库,
* 2.循环遍历所有的数据,按照parentId,List方式封装数据
* @return
*/
private Map<Integer, List<ItemCat>> getMap() {
Map<Integer,List<ItemCat>> map = new HashMap<>();
List<ItemCat> list = itemCatMapper.selectList(null);
for(ItemCat itemCat : list){
//获取parentId
int parentId = itemCat.getParentId();
if(map.containsKey(parentId)){
//key存在
//追加到map集合中
map.get(parentId).add(itemCat);
}else{
//key不存在
//1.new List封装list集合
List<ItemCat> childrenList = new ArrayList<>();
//2.存入数据
childrenList.add(itemCat);
//3.将第一个元素封装到map中
map.put(parentId,childrenList);
}
}
return map;
}
/**
* 1.数据结构:Map key=password value="List"
* 2.封装Map的数据类型
* 3.如果level=1 只获取一级
* 4.如果level=2 只获取二级
* 5.如果level=3 只获取三级
* @param level
* @return
*/
@Override
public List<ItemCat> findItemCatList(Integer level) {
long startTime = System.currentTimeMillis();
//1.封装Map集合
Map<Integer,List<ItemCat>> map = getMap();
//2.判断level的值
if(level == 1){
return map.get(0);
}
//
if(level == 2){
return getTwoList(map);
}
//如果level不是1,2级,一定是3级
List<ItemCat> list = getThreeList(map);
long endTime = System.currentTimeMillis();
System.out.println("总计时:"+(endTime-startTime));
return list;
}
private List<ItemCat> getThreeList(Map<Integer, List<ItemCat>> map) {
//获取一级和二级
List<ItemCat> oneList = getTwoList(map);
//封装三级,遍历二级菜单,之后封装
for(ItemCat oneItemCat : oneList){
//获取二级集合
List<ItemCat> twoList = oneItemCat.getChildren();
if(twoList == null || twoList.size() == 0){
//由于业务的数据不合理,跳过本次循环,执行下一次
continue;
//break;直接结束
}
for(ItemCat twoItemCat : twoList){
//查询三级列表,需要的三级的parentId是二级Id
int parentId = twoItemCat.getId();
List<ItemCat> threeList = map.get(parentId);
//封装数据
twoItemCat.setChildren(threeList);
}
}
return oneList;
}
private List<ItemCat> getTwoList(Map<Integer, List<ItemCat>> map) {
//1.先获取一级列表
List<ItemCat> oneList = map.get(0);
//2.遍历一级列表,根据一级查询二级
for (ItemCat oneItemCat: oneList){
//查询二级,所以二级的parentId是一级的Id
int parentId = oneItemCat.getId();
List<ItemCat> twoList = map.get(parentId);
//封装数据
oneItemCat.setChildren(twoList);
}
return oneList;
}
/**
* 1.查询所有的商品分类列表, 查询一次数据库,
* 2.循环遍历所有的数据,按照parentId,List方式封装数据
* @return
*/
private Map<Integer, List<ItemCat>> getMap() {
Map<Integer,List<ItemCat>> map = new HashMap<>();
List<ItemCat> list = itemCatMapper.selectList(null);
for(ItemCat itemCat : list){
//获取parentId
int parentId = itemCat.getParentId();
if(map.containsKey(parentId)){
//key存在
//追加到map集合中
map.get(parentId).add(itemCat);
}else{
//key不存在
//1.new List封装list集合
List<ItemCat> childrenList = new ArrayList<>();
//2.存入数据
childrenList.add(itemCat);
//3.将第一个元素封装到map中
map.put(parentId,childrenList);
}
}
return map;
}
<el-table-column prop="status" label="状态">
<!-- 定义作用域插槽 展现数据 scope.row展现行级元素 -->
<template slot-scope="scope">
<el-switch v-model="scope.row.status" active-color="#13ce66" inactive-color="#ff4949"
@change="updateStatus(scope.row)"></el-switch>
</template>
</el-table-column>
//根据ID修改状态信息
async updateStatus(itemCat) {
const {
data: result
} = await this.$http.put(`/itemCat/status/${itemCat.id}/${itemCat.status}`)
if (result.status !== 200) return this.$message.error("修改状态失败")
this.$message.success("状态修改成功")
},
请求路径: /itemCat/status/{id}/{status}
请求类型: put
请求参数:
返回值: SysResult对象
/**
*
* url:/itemCat/status/{id}/{status}
* 请求类型: put
* 请求参数:/{id}/{status}
* 返回值: SysResult对象
*/
@PutMapping("/status/{id}/{status}")
public SysResult updateStatus(ItemCat itemCat){
itemCatService.updateStatus(itemCat);
return SysResult.success(itemCat);
}
void updateStatus(ItemCat itemCat);
@Override
@Transactional//事务控制
public void updateStatus(ItemCat itemCat) {
itemCatMapper.updateById(itemCat);
}
说明: 商品分类实现中,需要添加一级/二级/三级分类信息. 但是父级下拉框中勾选1-2级菜单. 因为三级菜单不能当作父级. 当用户编辑完成之后,点击确定实现商品分类入库.
<!-- 2.1定义一行 使用栅格-->
<el-row>
<el-col :span="24">
<el-button type="primary" @click="showAddItemCatDialog">新增分类</el-button>
</el-col>
</el-row>
//当展现新增商品分类时,应该渲染级联框数据
showAddItemCatDialog() {
this.findParentItemCatList()
this.addItemCatDialogVisible = true
},
async findParentItemCatList() {
//动态获取商品分类信息 type=2表示获取2级商品分类信息
const {
data: result
} = await this.$http.get("/itemCat/findItemCatList/2")
if (result.status !== 200) return this.$message.error("获取商品分类列表失败!!")
this.parentItemCatList = result.data
},
//商品分类确定按钮实现
<span slot="footer" class="dialog-footer">
<el-button @click="addItemCatDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addItemCatForm">确 定</el-button>
</span>
//新增商品分类JS
async addItemCatForm() {
//先将整个表单进行校验
this.$refs.itemCatFormRef.validate(async validate => {
if (!validate) return
const {
data: result
} = await this.$http.post("/itemCat/saveItemCat", this.itemCatForm)
if (result.status !== 200) return this.$message.error("新增商品分类失败")
this.$message.success("新增商品分类成功!!!")
//新增成功,则刷新分类列表信息
this.findItemCatList();
this.addItemCatDialogVisible = false
})
},
请求路径: /itemCat/saveItemCat
请求类型: post
请求参数: 表单数据
返回值: SysResult对象
/**
* 需求:实现商品分类新增操作
* url:/itemCat/saveItemCat
* 请求类型:post
* 请求参数: 表单数据ItemCat对象接受
* 返回值:SysResult对象
*/
@PostMapping("/saveItemCat")
public SysResult saveItemCat(@RequestBody ItemCat itemCat){
itemCatService.saveItemCat(itemCat);
return SysResult.success();
}
void saveItemCat(ItemCat itemCat);
@Override
@Transactional
public void saveItemCat(ItemCat itemCat) {
itemCat.setStatus(true);
itemCatMapper.insert(itemCat);
}
<el-table-column label="操作">
<!-- 定义作用域插槽 定义标签等级-->
<template slot-scope="scope">
<el-button type="success" icon="el-icon-edit" @click="updateItemCatBtn(scope.row)">编辑</el-button>
<el-button type="danger" icon="el-icon-delete" @click="deleteItemCatBtn(scope.row)">删除</el-button>
</template>
</el-table-column>
//由于有层级关系,所有修改只能修改名称
updateItemCatBtn(itemCat) {
this.updateItemCatForm = itemCat
this.updateItemCatDialogVisible = true
},
<!-- 添加修改分类对话框 -->
<el-dialog title="修改商品分类" :visible.sync="updateItemCatDialogVisible" width="50%">
<!-- 定义分类表单 -->
<el-form :model="updateItemCatForm" :rules="rules" ref="upDateItemCatForm" label-width="100px">
<el-form-item label="分类名称:" prop="name">
<el-input v-model="updateItemCatForm.name"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="updateItemCatDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="updateItemCat">确 定</el-button>
</span>
</el-dialog>
async updateItemCat() {
//修改商品分类信息
const {
data: result
} = await this.$http.put('/itemCat/updateItemCat', this.updateItemCatForm)
if (result.status !== 200) return this.$message.error("更新商品分类失败")
this.$message.success("更新商品分类成功")
this.findItemCatList();
this.updateItemCatDialogVisible = false;
},
请求路径: /itemCat/updateItemCat
请求类型: put
请求参数: 表单数据 ItemCat对象
返回值: SysResult对象
/**
* 需求:实现商品分类修改操作
* url:/itemCat/updateItemCat
* 请求类型: put
* 请求参数: form表单数据 json ItemCat对象
* 返回值: SysResult对象
*/
@PutMapping("/updateItemCat")
public SysResult updateItemCat(@RequestBody ItemCat itemCat){
itemCatService.updateItemCat(itemCat);
return SysResult.success();
}
void updateItemCat(ItemCat itemCat);
@Override
@Transactional
public void updateItemCat(ItemCat itemCat) {
itemCatMapper.updateById(itemCat);
}
规则:
1.如果删除的商品分类是三级,则可以直接删除.
2.如果删除的商品分类是二级,则先删除三级,在删除二级.
3.如果删除的商品分类是一级,则先删除三级/二级/一级
注意事务的控制.
deleteItemCatBtn (itemCat) {
// 删除商品分类信息,如果为父级节点则需要删除所有的子级信息
this.$confirm('此操作将永久删除该数据, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
// 传递分类id
const { data: result } = await this.$http.delete('/itemCat/deleteItemCat', { params: { id: itemCat.id, level: itemCat.level } })
if (result.status !== 200) return this.$message.error('删除商品分类失败')
this.$message.success('删除数据成功')
// 删除成功之后,刷新页面数据
this.findItemCatList()
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
}
请求路径: /itemCat/deleteItemCat
请求类型: delete
业务描述: 当删除节点为父级时,应该删除自身和所有的子节点
请求参数:
返回值结果 SysResult对象11
/**
* url:/itemCat/deleteItemCat
* 请求类型: delete
* 业务描述: 当删除节点为父级时,应该删除自身和所有的子节点
* 请求参数: id/level
* 返回值: SysResult对象
*/
@DeleteMapping("/deleteItemCat")
public SysResult deleteItemCat(ItemCat itemCat){
itemCatService.deleteItemCat(itemCat);
return SysResult.success();
}
void deleteItemCat(ItemCat itemCat);
/**
* 需求:删除商品分类信息
* 条件:如果有子级,应该先删除子级
* Sql:
* * DELETE FROM item_cat WHERE (parent_id
* IN (?,?) OR parent_id = ? OR id = ?)
* @param itemCat
*/
@Override
@Transactional
public void deleteItemCat(ItemCat itemCat) {
//1 2 3
int level = itemCat.getLevel();
if (level == 3){
//表示需要删除的数据是3级菜单,可以直接删除
itemCatMapper.deleteById(itemCat.getId());
}
if(level == 2 ){
QueryWrapper<ItemCat> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("parent_id", itemCat.getId())
.or()
.eq("id", itemCat.getId());
itemCatMapper.delete(queryWrapper);
}
if(level == 1){
//1.必须获取二级id
QueryWrapper<ItemCat> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("parent_id",itemCat.getId());
//2.获取结果的第一列字段(主键) 二级id
List twoIdsList = itemCatMapper.selectObjs(queryWrapper);
//3.删除三级的数据
queryWrapper.clear();//清空之前的条件
queryWrapper.in(twoIdsList.size()>0,"parent_id",twoIdsList)
.or()
//删除parent_id 等于一级ID的,实则删除的是二级数据
.eq("parent_id", itemCat.getId())
.or()
//删除id=一级Id 则删除一级数据.
.eq("id",itemCat.getId());
itemCatMapper.delete(queryWrapper);
}
}
/**
* @author 刘昱江
* 时间 2021/4/7
*/
@TableName("item")
@Data
@Accessors(chain = true)
public class Item extends BasePojo{
@TableId(type = IdType.AUTO)
private Integer id; //商品Id号
private String title; //商品标题信息
private String sellPoint; //卖点信息
private Integer price; //商品价格 扩大100倍 缩小100倍值不变
private Integer num; //商品数量
private String images; //商品图片
private Integer itemCatId; //商品分类ID号
private Boolean status; //状态信息 0 下架 1 上架
}
1.生命周期函数
created() {
//1.获取商品列表数据
this.getItemList()
},
2.实现页面Ajax请求
//实现商品信息分页查询
async getItemList() {
const {
data: result
} = await this.$http.get("/item/getItemList", {
params: this.queryItemInfo
})
if (result.status !== 200) return this.$message.error("商品列表查询失败")
this.itemList = result.data.rows
this.total = result.data.total
},
请求路径: /item/getItemList?query=&pageNum=1&pageSize=10
请求类型: get
请求参数: 使用pageResult对象接收
返回值结果:
/**
* 业务逻辑: 查询商品分页
* URL: /item/getItemList?query=&pageNum=1&pageSize=10
* 类型: GET
* 接收参数: PageResult对象
* 返回值: SysResult(pageResult)
*/
@GetMapping("/getItemList")
public SysResult getItemList(PageResult pageResult){//3个参数
//参照user模块完成商品分页查询.
pageResult = itemService.getItemList(pageResult);//+2
return SysResult.success(pageResult);
}
/**
* 需求: 实现商品分页查询
* @param pageResult
* @return
*/
@Override
public PageResult getItemList(PageResult pageResult) {
//1.定义分页对象
IPage page = new Page<>(pageResult.getPageNum(),
pageResult.getPageSize() );
//2.定义条件构造器
//判断条件: 用户传递query 则添加where条件
QueryWrapper<Item> queryWrapper = new QueryWrapper<>();
boolean flag = StringUtils.hasLength(pageResult.getQuery());
//leftLike:%_ 以什么结尾
//rightLike:_% 以什么开头
queryWrapper.like(flag,"title",pageResult.getQuery());
//
//3.进行分页查询
//page接口原来只有2个,经过分页查询之后,有四个结果
page = itemMapper.selectPage(page,queryWrapper);
long total = page.getTotal();
List<Item> rows = page.getRecords();
/* pageResult.setTotal(page.getTotal())
.setRows(page.getRecords());*/
return pageResult.setTotal(total).setRows(rows);
}
列宽度修改: 由于按钮显示不全,所以可以将width改为300px.
过滤器定义
说明: 由于过滤器是全局变量,写在main.js中
/* 定义过滤器
1.参数: 将|左侧的数据,当作参数传递给函数.
2.返回值: 必须添加返回值!!!
Vue.filter("过滤器名称",过滤器动作函数(参数){
})
*/
Vue.filter("priceFormat",function(price){
return (price / 100).toFixed(2)
})
1.点击按钮分析
<!-- 定义添加商品按钮-->
<el-button type="primary" class="addItemBtnClass" @click="addItemBtn">添加商品</el-button>
2.检查JS函数
/* 添加商品按钮 */
async addItemBtn(){
//console.log(this.addItemForm)
//1.完成表单校验
this.$refs.addItemFormRef.validate( valid => {
if(!valid) return this.$message.error("请输入商品必填项")
})
//2.完成商品参数的封装
//2.0 将商品价格扩大100倍
this.addItemForm.price = this.addItemForm.price * 100
//2.1 将商品图片的数据转化为字符串
this.addItemForm.images = this.addItemForm.images.join(",")
//2.5 实现商品数据提交
let submitAddItem = {
item : this.addItemForm,
itemDesc: this.itemDesc
}
console.log(submitAddItem)
let {data: result} = await this.$http.post("/item/saveItem",submitAddItem)
if(result.status !== 200) return this.$message.error("商品添加失败")
this.$message.success("商品添加成功")
//2.5添加完成之后,将数据重定向到商品展现页面
this.$router.push("/item")
}
请求路径: http://localhost:8091/item/saveItem
请求类型: post
前端传递参数分析
{
item: {
images: "/2021/05/20/da0c1d4781c1499399f090da8b60f359.jpg,/2021/05/20/2ac1c34776a7465887eb019655354c3c.jpg"
itemCatId: 560
num: "100"
price: 718800
sellPoint: "【华为官方直供,至高12期免息0首付,原装正品】送华为原装无线充+运动蓝牙耳机+蓝牙音箱+三合一多功能数据线+钢化膜等!"
title: "华为P40 Pro 5G手机【12期免息可选送豪礼】全网通智能手机"
},
itemDesc: {
itemDesc: "
请求参数: 使用ItemVO对象接收
ItemVO参数详解:
Item对象
itemDesc 对象
为了降低商品提交代码的耦合性,将大字段信息详情,采用ItemDesc对象进行封装
/**
* @author 刘昱江
* 时间 2021/4/16
*/
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class ItemVO { //该对象封装商品所有的参数信息
private Item item;
private ItemDesc itemDesc;
private ItemParam itemParam;
}
//1.修改按钮的JS
<el-table-column prop="status" label="状态" width="80px">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" active-color="#13ce66" inactive-color="#ff4949"
@change="updateStatus(scope.row)"></el-switch>
</template>
</el-table-column>
async updateStatus(item) {
const {
data: result
} = await this.$http.put("/item/updateItemStatus", {
id: item.id,
status: item.status
})
if (result.status !== 200) return this.$message.error("更新状态失败")
this.$message.success("更新状态成功")
},
请求路径: /item/updateItemStatus
请求类型: put
请求参数: 使用对象接收
返回值结果:
/**
* 需求:商品状态修改
* 请求路径: /item/updateItemStatus
* 请求类型: put
* 请求参数: {id,status} json串 使用对象接收
* id: item.id,
* status: item.status
* 返回值结果:SysResult
*
* 问题:什么时候使用resultful,什么时候使用get/put/delete请求方式
*
* resultful是传参的一种方式
*/
@PutMapping("/updateItemStatus")
public SysResult updateItemStatus(@RequestBody Item item){
itemService.updateItemStatus(item);
return SysResult.success();
}
@Override
@Transactional
public void updateItemStatus(Item item) {
itemMapper.updateById(item);
}
async deleteItemBtn (item) {
// 消息确认框
this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
// 根据id删除数据
const {
data: result
} = await this.$http.delete('/item/deleteItemById', {
params: {
id: item.id
}
})
if (result.status !== 200) return this.$message.error('商品删除失败')
this.$message.success('商品删除成功')
// 重新获取商品列表信息
this.getItemList()
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
},
商品数据删除
请求路径: /item/deleteItemById
请求类型: delete
请求参数:
返回值结果:
/**
* 请求路径: /item/deleteItemById
* 请求类型: delete
* 参数: User对象结构
* 返回值:SysResult对象
*/
@DeleteMapping("/deleteItemById")
public SysResult deleteItemById(Item item){
itemService.deleteItemById(item);
return SysResult.success();
}
@Override
@Transactional
public void deleteItemById(Item item) {
itemMapper.deleteById(item);
}
说明: Item中存储的是商品的基本信息,通常用于检索,或者数据展现. itemDesc一般存储的都是商品的详情信息. 为了用户的检索效率更好,所以一般先查询item数据,(itemDesc一般采用大字段的形式,存储html代码的片段).
要求: 一个商品对应一个详情信息, 所以 item和itemDesc一对一
一个商品对应一个详情
一个详情对应一个商品
隐藏属性: item.id = itemDesc.id
补充知识: 数据库中常见对应关系
一对一: 老婆和老公(通常)
一对多: 老公和情人
多对多: 老师和学生
一个老师对应多个学生
一个学生对应多个老师
package com.jt.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @author
* 时间 2021/4/7
*/
@TableName("item_desc")
@Data
@Accessors(chain = true)
public class ItemDesc extends BasePojo{
//要求:item和itemDesc一对一 id相同
// 简化了外键约束
private Integer id;
private String itemDesc;
}
/**
* 业务分析: 实现商品新增操作
* URL地址: http://localhost:8091/item/saveItem
* 请求:post
* 参数: ItemVO的JSON串 {item,itemDesc}
* 返回值: SysResult对象
*/
@PostMapping("/saveItem")
public SysResult saveItem(@RequestBody ItemVO itemVO){
itemService.saveItem(itemVO);
return SysResult.success();
}
//实现商品入库
@Override
@Transactional
public void saveItem(ItemVO itemVO) {
//item 主键自增 数据库入库之后,才会有主键!!!
Item item = itemVO.getItem();
item.setStatus(true);
itemMapper.insert(item);
//问题:入库之后ID,现阶段item.id=null
//mybatis实现业务功能,自动回显主键
//MP自动的将主键的回显功能实现!!!
// itemMapper.selectOne(new QueryWrapper<>(item));
//规则:itemId与ItemDescId是一样的
ItemDesc itemDesc = itemVO.getItemDesc();
itemDesc.setId(item.getId());
itemDescMapper.insert(itemDesc);
}
说明: 使用富文本编辑器,可以得到 “所见即所得”
用法:
1.引入JS
/* 导入富文本编辑器 */
import VueQuillEditor from 'vue-quill-editor'
/* 导入富文本编辑器对应的样式 */
import 'quill/dist/quill.core.css' // import styles
import 'quill/dist/quill.snow.css' // for snow theme
import 'quill/dist/quill.bubble.css' // for bubble theme
2.页面展现
<!-- 定义富文本编辑器-->
<quill-editor ref="myQuillEditor" v-model="itemDesc.itemDesc">
</quill-editor>
注意事项: 图片/视频默认转化为01字节信息,并且以16进制的方式展现.
说明:
<el-upload
class="upload-demo"
action="https://jsonplaceholder.typicode.com/posts/"
:on-preview="handlePreview"
:on-remove="handleRemove"
:file-list="fileList"
list-type="picture">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
</el-upload>
<script>
//对外声明组件属性/方法等参数.要被根组件调用
export default {
data(){
return {
tableData: [
{id:100, name:"黑熊精", age: 3000, sex:"男"},
{id:100, name:"黑旋风", age: 3000, sex:"男"},
{id:100, name:"黑心肠", age: 3000, sex:"男"},
{id:100, name:"黑桃A", age: 3000, sex:"男"}
],
dialogVisible: true,
fileList: [{name: 'food.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100'}, {name: 'food2.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100'}]
}
},
methods: {
handleSizeChange(size){
alert("当前条数:"+size)
},
handleCurrentChange(current){
alert("当前页数:"+current)
},
handleRemove(file, fileList) {
console.log("点击删除按钮时,触发JS")
console.log(file, fileList);
},
handlePreview(file) {
console.log("点击url地址之后,触发!!!!!")
console.log(file);
}
}
}
</script>
<!--
了解:
action: 文件上传提交的地址
name: 默认文件上传的名称 file
-->
<el-upload class="upload-demo" :action="uploadUrl" :on-preview="handlePreview" :on-remove="handleRemove"
:on-success="handleSuccess" list-type="picture" multiple drag>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
</el-upload>
请求路径: http://localhost:8091/file/upload
请求类型: post
请求参数:
返回值结果:
ImageVO对象说明
说明: 为了封装文件上传之后的结果,需要封装VO对象 格式如下
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class ImageVO implements Serializable {
private String virtualPath; //虚拟地址,不包含磁盘地址
private String urlPath; //URL地址信息.
private String fileName; //文件名称
}
package com.jt.controller;
import com.jt.vo.SysResult;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@RestController
@CrossOrigin
@RequestMapping("/file")
public class FileController {
/**
* 业务: 实现文件上传
* url: /file/upload
* 请求类型: POST
* 参数: file
* 返回值: SysResult(imageVO)
* 高级API:MultipartFile 自动维护了缓存流/自动开关
*
* 文件上传步骤:
* 1.获取文件名称.
* 2.准备上传文件的目录
* 3.封装文件全路径 目录/文件名称
* 4.实现文件上传
*/
@PostMapping("/upload")
public SysResult upload(MultipartFile file) throws IOException {
//1.获取文件名称 a.jpg
String fileName = file.getOriginalFilename();
//2.准备文件目录
String fileDir = "G:/images/";
//2.1 判断目录是否存在
File dir = new File(fileDir);
if(!dir.exists()){
//如果目录不存在,则创建多级目录
dir.mkdirs();
}
//3.准备文件全路径
String localPath = fileDir + fileName;
//4.实现文件输出
file.transferTo(new File(localPath));
System.out.println("文件上传成功!!!!");
return SysResult.success();
}
}
控制文件上传的类型(后台为主)
控制恶意程序的上传 通过宽度和高度
为了提高检索的速度,应该分目录存储.
动态生成文件名称 UUID,防止文件重名.
实现文件上传 注意路径.
正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
标识开头/结尾/匹配多次
regex: abc 标识只能匹配固定的字符abc
regex: abc* 标识匹配 ab,c可以任意次.
regex: abc? 标识匹配 ab,c可以1次或者0次.
2.匹配确定次
regex: c{3} c只能出现3次
regex: c{3,} c出现>=3次
regex: c{3,10} c出现>=3 <=10次
regex: .* 匹配所有字符.
regex:
a[ab] 匹配2个字符 第一个字符必须为a, 第二个字符 必须a 或者b
[a-z][0-9] 第一个字符是a-z 第二个字符必须是0-9
demo: (png|jpg|gif) 字符要么是png,要么是ipg 要么是gif.
package com.jt.controller;
import com.jt.service.FileService;
import com.jt.vo.ImageVO;
import com.jt.vo.SysResult;
import org.apache.ibatis.jdbc.Null;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.swing.plaf.multi.MultiInternalFrameUI;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@RestController
@CrossOrigin
@RequestMapping("/file")
public class FileController {
@Autowired
private FileService fileService;
/**
* 业务:实现文件上传
* url:/file/upload
* 请求类型:post
* 参数:file
* 返回值:SysResult(imageVO)
* 高级api:MultipartFile 自动维护了缓存流/自动开关
*
* 文件上传步骤:
* 1.获取文件名称
* 2.准备上传文件的目录
* 3.封装文件的全路径 目录/文件名称
* 4.实现文件上传
*
*/
/*
@PostMapping("/upload")
public SysResult upload(MultipartFile file) throws IOException {
// 1.获取文件名称
String fileName = file.getOriginalFilename();
// 准备文件目录
String fileDir = "E:/Third/images/";
//2.1 判断目录是否存在
File dir = new File(fileDir);
if (!dir.exists()){
//如果目录不存在,则创建目录
dir.mkdirs();//创建多级目录
// dir.mkdir();//创建一级目录
}
//3.准备文件全路径
String localPath = fileDir + fileName;
//4.实现文件输出 输出流
file.transferTo(new File(localPath));
System.out.println("上传文件成功!!!!!!!!!");
return SysResult.success();
}*/
@PostMapping("/upload")
public SysResult upload(MultipartFile file) throws IOException{
ImageVO imageVO = fileService.upload(file);
if (imageVO == null){//说明业务执行有误
return SysResult.fail();
}
return SysResult.success(imageVO);
}
}
package com.jt.service;
import com.jt.vo.ImageVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
@Service
public class FileServiceImpl implements FileService{
private String localDir = "E:/Third/images";
/*@Autowired
private FileMapper fileMapper;*/
/**
* 1.校验是否为图片
* 2.木马.exe.jpg 判断是否满足图片固有属性 高度/宽度
* 3.为了提高查询效率,要求分目录存储
* 3.1 按照后缀名分配 jpg,png,gif 效率提升不能满足要求
* 3.2 按照日期分 yyy/MM/dd/HH/ 可以
* 3.3 商品分类 出现分布不均现象
* 3.4 根据名称hash 之后截串
* demmo:hash(a)=qw|er|as|dg/a.jpg
* 弊端:hash码可能出现分布不均的现象
* 4.防止文件重名 使用uuid代替名称
* @param file
* @return
*/
@Override
public ImageVO upload(MultipartFile file) {
//1.获取图片名称 demo: abc.jpg abc.JPG
String fileName = file.getOriginalFilename();
//bug说明:由于windows系统不区分大小写,所以将字母全部转化为小写
fileName = fileName.toLowerCase();
//利用正则判断是否为图片
if(!fileName.matches("^.+\\.(jpg|png|gif)$")){
//如果不是图片,则返回null
return null;
}
//2.检查文件是否为恶意程序
try {
BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
if (width == 0 || height == 0){
//说明文件不是图片
return null;
}
//3.根据时间实现目录的创建 时间---yyyy/MM/dd
String dateDir = new SimpleDateFormat("/yyyy/MM/dd/")
.format(new Date());
//"G:/images/2021/9/8"
String localDirPath = localDir+ dateDir;
//创建目录:
File dirFile = new File(localDirPath);
if(!dirFile.exists()){
dirFile.mkdirs();
}
//4.使用uuid替换文件名称 唯一:系统内部唯一
String uuid = UUID.randomUUID().toString()
.replace("-", "");
//截取文件的后缀 abc.jpg
int index = fileName.lastIndexOf(".");
//获取类型 .jpg
String fileType = fileName.substring(index);
String newFileName = uuid + fileType;
//5.实现文件上传操作 目录/文件名称
String realFilePath = localDirPath + newFileName;
file.transferTo(new File(realFilePath));
System.out.println("文件上传成功!!!!");
//6.封装返回值
/**6.1
* 封装虚拟路径,在各个系统之间可以灵活切换,只保存动态的目录
* path = 时间/uuid.type
*
*/
String virtualPath = dateDir + newFileName;
String url = "https://img14.360buyimg.com/n0/jfs/t1/203725/5/5245/117493/61370b24E0b7558ce/4f6716a30595b275.jpg";
return new ImageVO(virtualPath,url,newFileName);
} catch (IOException e) {
e.printStackTrace();
return null;//表示程序有问题
}
}
}
重点: 业务需求. 业务顺序
用户点击添加商品按钮时, 1.获取商品分类3级列表信息. 2.跳转到商品新增页面.
用户录入基本的商品信息.
用户录入价格时 需要在后期将数据扩大100倍.
当用户点击删除图片时,应该发起请求同时删除服务器的数据.
重点: 如果需要删除数据,则应该传递虚拟路径信息
//移除图片的方法
async handleRemove(file) {
//移除数组中的数据
let virtualPath = file.response.data.virtualPath
//通过findIndex函数 获取数组中指定数据的位置
let index = this.addItemForm.images.findIndex(x => x === virtualPath)
//删除数组中指定的数据
this.addItemForm.images.splice(index, 1)
//删除服务中的文件
let {
data: result
} = await this.$http.delete("/file/deleteFile", {
params: {
virtualPath: virtualPath
}
})
if (result.status !== 200) return this.$message.error("删除图片失败")
this.$message.success("删除图片成功")
},
请求路径: http://localhost:8091/file/deleteFile
请求类型: delete
请求参数:
返回值结果:
/**
* 业务:删除图片
* url: http://localhost:8091/file/deleteFile
* 参数:虚拟路径
* 返回值:SysResult 对象
*
*/
@DeleteMapping("/deleteFile")
public SysResult deleteFile(String virtualPath){
fileService.deleteFile(virtualPath);
return SysResult.success();
}
/**
* 实现思路:
* 1.根据虚拟地址,拼接磁盘地址
* 2.判断文件是否存在
* 3.实现文件删除
* @param virtualPath
*/
@Override
public void deleteFile(String virtualPath) {
//1.生成本地磁盘地址
String path = localDir + virtualPath;
File file = new File(path);
if (file.exists()){
file.delete();
}
}
如果用户需要查看服务器中的图片,则一般使用网络地址.
难点: 图片真实存在于磁盘地址中. 如何实现磁盘地址与网络地址的映射!
业务分析:
1.本地磁盘地址: G:\images\2021\09\09\abc.jpg
2.网络访问地址: http://image.jt.com\2021\09\09\abc.jpg
说明: 编辑完整的FileServiceImpl
package com.jt.service;
import com.jt.vo.ImageVO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
@Service
@PropertySource("classpath:/image.properties")
public class FileServiceImpl implements FileService{
//封装路径的前缀
@Value("${image.localDir}")
private String localDir;/* = "E:/Third/images";*/
@Value("${image.perUrl}")
private String perUrl;/* = "http://image.jt.com";*/
/**
* 1.校验是否为图片
* 2.木马.exe.jpg 判断是否满足图片固有属性 高度/宽度
* 3.为了提高查询效率,要求分目录存储
* 3.1 按照后缀名分配 jpg,png,gif 效率提升不能满足要求
* 3.2 按照日期分 yyy/MM/dd/HH/ 可以
* 3.3 商品分类 出现分布不均现象
* 3.4 根据名称hash 之后截串
* demmo:hash(a)=qw|er|as|dg/a.jpg
* 弊端:hash码可能出现分布不均的现象
* 4.防止文件重名 使用uuid代替名称
* @param file
* @return
*/
@Override
public ImageVO upload(MultipartFile file) {
//1.获取图片名称 demo: abc.jpg abc.JPG
String fileName = file.getOriginalFilename();
//bug说明:由于windows系统不区分大小写,所以将字母全部转化为小写
fileName = fileName.toLowerCase();
//利用正则判断是否为图片
if(!fileName.matches("^.+\\.(jpg|png|gif)$")){
//如果不是图片,则返回null
return null;
}
//2.检查文件是否为恶意程序
try {
BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
if (width == 0 || height == 0){
//说明文件不是图片
return null;
}
//3.根据时间实现目录的创建 时间---yyyy/MM/dd
String dateDir = new SimpleDateFormat("/yyyy/MM/dd/")
.format(new Date());
//"G:/images/2021/9/8"
String localDirPath = localDir+ dateDir;
//创建目录:
File dirFile = new File(localDirPath);
if(!dirFile.exists()){
dirFile.mkdirs();
}
//4.使用uuid替换文件名称 唯一:系统内部唯一
String uuid = UUID.randomUUID().toString()
.replace("-", "");
//截取文件的后缀 abc.jpg
int index = fileName.lastIndexOf(".");
//获取类型 .jpg
String fileType = fileName.substring(index);
String newFileName = uuid + fileType;
//5.实现文件上传操作 目录/文件名称
String realFilePath = localDirPath + newFileName;
file.transferTo(new File(realFilePath));
System.out.println("文件上传成功!!!!");
//6.封装返回值
/**6.1
* 封装虚拟路径,在各个系统之间可以灵活切换,只保存动态的目录
* path = 时间/uuid.type
* 网络地址://http:image.jt.com/2021/11/11/a.jpg
*
*/
String virtualPath = dateDir + newFileName;
//http://image.jt.com/2021/11/11/a.jpg
String url = perUrl + virtualPath;
System.out.println("磁盘地址"+realFilePath);
System.out.println("网络地址:"+url);
return new ImageVO(virtualPath,url,newFileName);
} catch (IOException e) {
e.printStackTrace();
return null;//表示程序有问题
}
}
/**
* 实现思路:
* 1.根据虚拟地址,拼接磁盘地址
* 2.判断文件是否存在
* 3.实现文件删除
* @param virtualPath
*/
@Override
public void deleteFile(String virtualPath) {
//1.生成本地磁盘地址
String path = localDir + virtualPath;
File file = new File(path);
if (file.exists()){
file.delete();
}
}
}
说明: 如果路径信息会发生变化,则最好的方式通过动态赋值的方式完成.
解决方案:
#通过配置文件 动态赋值
image.localDir=G:/images
image.preUrl=http://image.jt.com
网络地址: http://image.jt.com/2021/09/09/8a3e2666cfbc4f1c9f0136339d42a562.jpg
磁盘地址: G:/images/2021/09/09/8a3e2666cfbc4f1c9f0136339d42a562.jpg
如果用户直接通过网络地址进行访问,是无法直接获取图片信息. 如果需要获取图片则应该实现 域名与磁盘地址的映射.
反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源。同时,用户不需要知道目标服务器的地址,也无须在用户端作任何设定。反向代理服务器通常可用来作为Web加速,即使用反向代理作为Web服务器的前置机来降低网络和服务器的负载,提高访问效率。
特点:
1.反向代理服务器位于用户和服务器之间.
2.用户访问反向代理服务器,就可以获取真实的资源.
3.反向代理机制 用户无需了解真实的服务器信息.
4.反向代理保护了服务器端信息,也称之为服务器端代理.
正向代理,意思是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理。
特点:
说明: 用户每一次请求都包含了正向代理和反向代理.
正向代理一般适用于网络的通信.
反向代理一般适用于服务器获取信息.
Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru站点(俄文:Рамблер)开发的,第一个公开版本0.1.0发布于2004年10月4日。
其将源代码以类BSD许可证的形式发布,因它的稳定性、丰富的功能集、简单的配置文件和低系统资源的消耗而闻名。2011年6月1日,nginx 1.0.4发布。
Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like 协议下发行。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。
注意事项:
1.nginx开发语言 C语言,对中文不友好.所以注意程序员操守
2.nginx启动时会默认占用80端口
说明:nginx启动时会占用80端口.所以需要释放80资源.
步骤1: 查询 80端口被哪个进程占用
步骤2: 关闭进程
步骤3: 如果80端口 被PID=4占用,则需要升级驱动配置.
说明: 在windows中nginx服务每次点击启动之后,都会生成2个进程项.
注意事项: 在windows中nginx只能启动一次.
异常信息如下:
关于启动2项说明:
进程项1: nginx主要进程信息.
进程项2: nginx的守护进程 主要的任务防止主进程意外关闭.
关闭nginx 应该先关闭守护(内存晓得)再关闭主进程(内存大的).
说明: nginx命令执行 需要nginx.exe所在的根目录中执行.
命令:
知识点:
反向代理配置:
http {
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
}
}
磁盘地址:G:/images/2021/09/09/a.jpg
网络地址:http://image.jt.com/2021/09/09/a.jpg
问题: 如何通过网络地址访问磁盘的图片?
用户通过域名访问真实的服务器.
2.nginx根据自身的配置进行拦截,根据配置文件将域名http://image.jt.com转化为具体的磁盘地址 G:/
3.根据磁盘地址访问真实的服务器资源.
4/5. 服务器将数据交给nginx,之后nginx将数据返回给用户.至此实现了反向代理.
注意事项:
1.启动时没有报错信息
2.重启时才会有报错. 所有最好先执行启动,再执行重启
#配置图片服务器
#拦截域名:http://image.jt.com:80
#代理路径:G:/images
server {
listen 80;
server_name image.jt.com;
location / {
root G:/images;
}
}
业务说明: 操作系统为了测试方便,在计算中保留了hosts文件. 该文件的主要的作用就是实现域名与IP地址的映射.但是该映射,只对本机有效.
路径: C:\Windows\System32\drivers\etc
# IP 与 域名映射
# 127.0.0.1 localhost
# ::1 localhost
#图片服务器配置
127.0.0.1 image.jt.com
#前端服务器配置
127.0.0.1 www.jt.com
#后端服务器配置
127.0.0.1 manage.jt.com
解决方案:
1.添加管理权限 选中hosts文件之后 右键属性.
方式2: 以超级管理员的方式运行Switch hosts软件
方式3: 添加指定的用户权限 步骤 1.获取当前计算机的名称 PC 2.添加用户信息.
注意事项: 计算机名称不要写中文.
方式4: 取消只读属性
说明:文件上传正确,但是回显出问题,应该按照如下的方式进行调试
1.检查磁盘地址/网络地址 除了前缀不同之处,其他的必须相同
磁盘地址:E:/Third/images/2021/09/10/84c43d94354a4feb9c982d9e5ddbe609.jpg
网络地址:http://image.jt.com/2021/09/10/84c43d94354a4feb9c982d9e5ddbe609.jpg
2.检查前端业务地址
复制地址之后,将前缀换位本地磁盘地址,再次检查路径是否正确.
检查路径.
4.检查Nginx配置文件
域名与磁盘的映射必须正确
5.检查hosts文件是否正确
6.检查hosts文件是否有效 如图表示hosts文件一切正常. 如果不能显示,则重启计算机.
说明: 前端向后端发起请求时,网址 http://localhost:8091/xxxx,实际开发中服务器都是通过域名的方式访问,所以需要将前端的网址统一改为域名.
修改main.js 修改ajax请求的前缀
改AddItem.vue文件 修改文件上传的路径
说明: 将前端生成的dist目录 复制到nginx根目录中 如图所示:
需求: 用户通过域名http://www.jt.com 访问系统的首页index.html
配置信息:
#配置前端服务器 www.jt.com
server {
listen 80;
server_name www.jt.com;
location / {
root dist;
index index.html;
}
}
说明: 现在需要准备2台tomcat服务器,需要执行main方法2次.如果有热部署,则修改代码之后重启会影响配置流程. 所有关闭热部署.
说明: 由于nginx中有负载均衡的策略 所以需要动态获取端口,验证是否负载均衡.
@RestController
public class PortController {
@Value("${server.port}")
private Integer port;
@GetMapping("/getPort")
public String getPort(){
return "访问端口:" + port;
}
}
1.根据8091启动服务器.
2.修改yml文件中的端口号8092,之后再次启动服务
如图所示:
#定义tomcat集群
# 负载均衡策略: 1.轮询策略
upstream tomcats {
server 127.0.0.1:8091;
server 127.0.0.1:8092;
}
#配置后台服务器 manage.jt.com 8091/8092
server {
listen 80;
server_name manage.jt.com;
location / {
#代理的是一个请求路径
proxy_pass http://tomcats;
}
}
说明: tomcat服务器依次访问.
#定义tomcat集群
# 负载均衡策略: 1.轮询策略
upstream tomcats {
server 127.0.0.1:8091;
server 127.0.0.1:8092;
}
说明: 根据权重设定,分配网络请求到不同的服务器中. 值越大访问越多.
#定义tomcat集群
# 负载均衡策略: 1.轮询策略 2.权重策略
upstream tomcats {
server 127.0.0.1:8091 weight=10;
server 127.0.0.1:8092 weight=1;
}
需求: 如果需要让用户与服务器绑定.则可以使用ip_hash策略
使用说明:
1.方便进行压力测试.
2.某些用户的数据保存到服务器的Session中时,需要绑定数据.
3.公司特殊业务场景可能用到iphash.
#定义tomcat集群
# 负载均衡策略: 1.轮询策略 2.权重策略 3.iphash策略
upstream tomcats {
ip_hash;
server 127.0.0.1:8091;
server 127.0.0.1:8092;
}
上传JDK安装包 /usr/local/src
解压安装包
修改Linux环境变量
JDK环境测试
命令: tar -xvf jdk-8u51-linux-x64.tar.gz
2.修改文件名称 mv jdk1.8.0_51 jdk1.8
#设定jdk环境
export JAVA_HOME=/usr/local/src/jdk1.8
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib