用户登录业务实现
用户登陆的业务本质就是输入账号密码,用来对比数据库中是否有。因为数据库存储密码经过加密,比如MD5。那么输入的密码查询前,也需要使用加密算法转换。如果账号密码无误,需要用UUID获取密匙。其他网页需要用路由守卫保护,有密匙才能通过
页面JS分析
用户登录JS
加密算法MD5
规则说明: MD5加密算法,只能由明文转化为密文. 不可以反向编译.
破解MD5加密算法:利用MD5密码与乱码一致性,用穷举法破解
关于秘钥说明
说明: 当用户登录之后,可以跳转到系统的首页. 到了系统首页之后,用户可以进行其它的业务操作. 系统如何判断正在操作业务的用户 已经登录?
业务说明:
一般在登录认证系统中,都会返回秘钥信息.来作为用户登录的凭证.
秘钥特点: 最好独一无二.
动态生成秘钥: UUID
/**
* 业务需求:
* 1.将密码进行加密处理
* 2.根据username/password 查询数据库获取数据.
* 3. 有数据 用户名密码正确
* 无数据 用户名和密码错误
* @param user
* @return
*/
@Override
public String login(User user) {
//1.将密码加密处理
String password = user.getPassword();
//2.利用md5加密算法 进行加密
String md5Pass = DigestUtils.md5DigestAsHex(password.getBytes());
user.setPassword(md5Pass);
//3.查询数据库数据
User userDB = userMapper.findUserByUP(user);
if(userDB == null){
//说明: 用户名和密码错误
return null;
}
//说明: 用户名和密码正确,返回秘钥
String uuid = UUID.randomUUID().toString()
.replace("-","");
return uuid;
}
关于Session和Cookie说明
业务需求说明
用户的请求是一次请求,一次响应. 当响应结束时,服务器返回的数据 也会销毁. 问题: 如果销毁了token 则认为用户没有登录.需要重复登录.
如何解决该问题: 应该持久化token信息.
Session
Session:在计算机中,尤其是在网络应用中,称为“会话控制”。Session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页之间跳转时,存储在Session对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web页时,如果该用户还没有会话,则Web服务器将自动创建一个 Session对象。当会话过期或被放弃后,服务器将终止该会话。Session 对象最常见的一个用法就是存储用户的首选项。例如,如果用户指明不喜欢查看图形,就可以将该信息存储在Session对象中。有关使用Session 对象的详细信息,请参阅“ASP应用程序”部分的“管理会话”。注意会话状态仅在支持cookie的浏览器中保留。
特点: Session总结
- Session 称之为 会话控制 技术
- Session生命周期, 会话结束 对象销毁.
- Session的数据存储在内存中.
- Session只可以临时存储数据.不能永久存储.
Cookie总结
Cookie,有时也用其复数形式 Cookies。类型为“小型文本文件”,是某些网站为了辨别用户身份,进行Session跟踪而储存在用户本地终端上的数据(通常经过加密),由用户客户端计算机暂时或永久保存的信息 [1] 。
特点:
- 类型: 小型文本文件.
- 文件通常是加密的.
- cookie 可以临时或者永久存储.
关于Cookie和Session说明
- 手机银行的登录信息? Session存储. 数据安全性高
- 腾讯视频会员登录信息? Cookie存储 1个月免密登录.
- 公司的财务系统登录信息? Session存储
- 购物系统的登录信息? Cookie存储.
用户登录信息存储
//获取用户token信息
let token = result.data
window.sessionStorage.setItem("token",token)
用户登录模块实现
用户业务接口文档说明
- 请求路径: /user/login
- 请求方式: POST
- 请求参数
参数名称 参数说明 备注
- 响应数据 SysResult对象
返回值格式如下:
{"status":200,"msg":"服务器调用成功!","data":"1e893a97634847b3a8b499b173bea620"}
编辑SysResult对象
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.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@CrossOrigin
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/findAll")
public List<User> findAll(){
return userService.findAll();
}
public SysResult aa(){
return new SysResult(201,"xxxx",null);
}
}
编辑UserController
/**
* URL地址: /user/login
* 请求类型: post
* 参数: JSON串 username/password
* 返回值: SysResult对象
*/
@PostMapping("/login")
public SysResult login(@RequestBody User user){
//返回值一个字符串 token
String token = userService.login(user);
if(token == null){
return SysResult.fail(); //201
}
return SysResult.success(token); //200
}
编辑UserService
/**
* 业务需求:
* 1.将密码进行加密处理
* 2.根据username/password 查询数据库获取数据.
* 3. 有数据 用户名密码正确
* 无数据 用户名和密码错误
* @param user
* @return
*/
@Override
public String login(User user) {
//1.将密码加密处理
String password = user.getPassword();
//2.利用md5加密算法 进行加密
String md5Pass = DigestUtils.md5DigestAsHex(password.getBytes());
user.setPassword(md5Pass);
//3.查询数据库数据
User userDB = userMapper.findUserByUP(user);
if(userDB == null){
//说明: 用户名和密码错误
return null;
}
//说明: 用户名和密码正确
return "秘钥";
}
编辑UserMapper
public interface UserMapper {
@Select("select * from user")
List<User> findAll();
@Select("select * from user where username=#{username} and password=#{password}")
User findUserByUP(User user);
}
系统跳转
系统首页跳转
编辑路由JS
首页跳转效果
路由导航守卫
需求说明
说明: 当用户在没有登录的条件下. 用户可以手动输入请求地址. 可以直接跳转项目. 这样的方式非常不安全.
解决方案: 前端通过拦截器 控制用户是否登录.
拦截器说明: 用户拦截的是URL中跳转的路径.
结果: 1.拦截 跳转到登录页面.
2.放行 跳转用户目标页面.
路由导航守卫
说明: 编辑index.js文件
/**
* 参数说明:
* 1.to 到哪里去
* 2.from 从哪里来
* 3.next 请求放行
* 拦截器策略:
* 1.如果用户访问/login登录页面 直接放行
* 2.如果访问其它页面,则校验是否有token
* 有token 放行
* 没有token 跳转到登录页面
*/
router.beforeEach((to,from,next) => {
if(to.path === '/login') return next()
//获取token数据信息
let token = window.sessionStorage.getItem('token')
if(token === null || token === ''){
return next("/login")
}
//放行请求
next()
})
左侧菜单展现
需要完成的业务为,网页渲染时就查询一级菜单,并展示出来。点击一级菜单,可以触发展示对应的二级菜单
菜单为层级结构,菜单的数据库表为自关联。查询每次查询时都需要上一级的关联ID作为查询条件,该ID可以通过作用域插槽获取,发送给后端。
一对多关系,POJO类有一个集合属性。
搭建层级代码
表设计说明
关于Rights POJO说明
@Data
@Accessors(chain = true)
public class Rights extends BasePojo{
private Integer id;
private String name;
private Integer parentId;
private String path;
private Integer level;
private List<Rights> children; //不是表格固有属性
}
关于层级代码结构
前端JS说明
- 生命周期函数调用JS函数
created() {
//动态获取左侧菜单信息
this.getMenuList()
//设定模式选中按钮
this.defaultActive = window.sessionStorage.getItem("activeMenu")
},
- 发起Ajax请求获取服务器数据
async getMenuList() {
const {data: result} = await this.$http.get('/rights/getRightsList')
if(result.status !== 200) return this.$message.error("左侧菜单查询失败")
this.menuList = result.data
},
接口文档说明
-
请求路径 /rights/getRightsList
-
请求类型 GET
-
请求参数 无
-
响应数据 SysResult对象
-
响应数据如图所示
父子关系封装/Sql语句写法
要求: 查询所有一级菜单和一级菜单所对应的二级菜单 要求关联查询
SELECT p.id,p.name,p.parent_id,p.path,p.level,p.created,p.updated,
c.id c_id,c.name c_name,c.parent_id c_parent_id,c.path c_path,
c.level c_level,c.created c_created,c.updated c_updated
FROM
rights p
LEFT JOIN
rights c
ON
c.parent_id = p.id
WHERE p.parent_id = 0
编辑RightsController
@RestController
@CrossOrigin
@RequestMapping("/rights")
public class RightsController {
@Autowired
private RightsService rightsService;
/**
* 查询一级二级数据
* URL: /rights/getRightsList
* 参数: 无
* 返回值: SysResult(List<Rights>)
*/
@GetMapping("/getRightsList")
public SysResult getRightsList(){
List<Rights> rights = rightsService.getRightsList();
return SysResult.success(rights);
}
}
编辑RightsService
@Service
public class RightsServiceImpl implements RightsService{
@Autowired
private RightsMapper rightsMapper;
@Override
public List<Rights> getRightsList() {
return rightsMapper.getRightsList();
}
}
编辑RightsMapper/xml映射文件
RightsMapper接口
public interface RightsMapper {
public List<Rights> getRightsList();
}
编辑Rights映射文件
<mapper namespace="com.jt.mapper.RightsMapper">
<select id="getRightsList" resultMap="rightsRM">
select p.id,p.name,p.parent_id,p.path,p.level,p.created,p.updated,
c.id c_id,c.name c_name,c.parent_id c_parent_id,c.path c_path,
c.level c_level,c.created c_created,c.updated c_updated
from
rights p
left join
rights c
on
c.parent_id = p.id
where p.parent_id = 0
</select>
<resultMap id="rightsRM" type="Rights" autoMapping="true">
<id column="id" property="id"/>
<!--一对一封装子级菜单List集合-->
<collection property="children" ofType="Rights">
<!--封装主键ID-->
<id column="c_id" property="id"/>
<result column="c_name" property="name"/>
<result column="c_parent_id" property="parentId"/>
<result column="c_path" property="path"/>
<result column="c_level" property="level"/>
<result column="c_created" property="created"/>
<result column="c_updated" property="updated"/>
</collection>
</resultMap>
</mapper>
页面效果展现
关于项目报错调试步骤
说明: 通过控制台 检查请求路径/响应信息/及JS报错信息. 后台服务器端口号固定 8091
关于页面跳转子级路由说明
现象说明
1.页面路由跳转 用户点击子级菜单时.页面将整个后端页面进行覆盖. 效果如下.
2.实际效果: 应该在首页的右侧 展现新的页面信息.
知识点讲解
功能说明: 组件之间的嵌套问题.
定义路由步骤:
- 定义路由url地址.
- 路由填充位(占位符)
- 定义组件(了解)
- 定义路由策略
- 实现路由挂载
父子组件嵌套总结:
-
定义父级组件
-
路由策略:
-
如果需要嵌套 通过 router-view 进行占位, 通过children属性定义父子关系的结构. 当点击子组件时,会在父级组件的router-view中展现子组件.
首页嵌套规则
在Home组件中定义路由的占位符
定义父子组件的策略
用户管理
业务涉及分页查询,用户新增,信息修改,修操作时的数据回显,以及用户删除操作。
- 新增时需要使用MD5加密。
- 新增和修改操作都需要将时间写入。因为新增需要多写入个建立时间,为了时间的一致性。可以new 一个date对象
更新操作新增或许修改,都需要用事务控制和加上更新时间。
用户列表展现
页面JS分析
生命周期函数
//利用钩子函数实现数据查询
mounted(){
this.getUserList()
}
获取数据函数分析
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":"1235678@qq.com",
"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":"1235678@qq.com",
"status":false,
"role":null
}
]
}
}
封装PageResult对象
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class PageResult { //封装VO对象
private String query;
private Integer pageNum;
private Integer pageSize;
private Long total;
private Object rows;
}
编辑UserController
/**
* 业务说明:
* 1. /user/list
* 2.请求类型: GET
* 3.参数接收: 后台使用PageResult对象接收
* 3.返回值: SysResult<PageResult>
*/
@GetMapping("/list")
public SysResult getUserList(PageResult pageResult){//参数3
//业务查询总数.分页条数.
pageResult = userService.getUserList(pageResult);
return SysResult.success(pageResult);//参数5个
}
编辑UserService
/**
* 要求查询 1页10条
* 特点: 数组的结果 口诀: 含头不含尾
* 语 法: select * from user limit 起始位置,查询的条数
* 第一页: select * from user limit 0,10 0-9
* 第二页: select * from user limit 10,10 10-19
* 第三页: select * from user limit 20,10 20-29
* 第N页: select * from user limit (n-1)*10,10
* @param pageResult
* @return
*/
@Override
public PageResult getUserList(PageResult pageResult) {
//1.记录总数 total
long total = userMapper.getTotal();
//2.分页后的数据
int size = pageResult.getPageSize();
int start = (pageResult.getPageNum() - 1) * size;
List<User> rows = userMapper.findUserListByPage(start,size);
return pageResult.setTotal(total).setRows(rows);
}
编辑UserMapper接口
@Select("select * from user limit #{start},#{size}")
List<User> findUserListByPage(@Param("start") int start,@Param("size") int size);
页面效果展现
分页查询补充
用户需求说明
说明: 用户的文本输入框,可能有值,也可能没有数据. 则在后端服务器中应该使用动态Sql的方式实现数据的查询.
编辑UserController
/**
* 业务说明:
* 1. /user/list
* 2.请求类型: GET
* 3.参数接收: 后台使用PageResult对象接收
* 3.返回值: SysResult<PageResult>
*/
@GetMapping("/list")
public SysResult getUserList(PageResult pageResult){//参数3
//业务查询总数.分页条数.
pageResult = userService.getUserList(pageResult);
return SysResult.success(pageResult);//参数5个
}
编辑UserService
@Override
public PageResult getUserList(PageResult pageResult) {
//1.记录总数 total
long total = userMapper.getTotal();
//2.分页后的数据
//2.1获取每页条数
int size = pageResult.getPageSize();
//2.2获取起始位置
int start = (pageResult.getPageNum() - 1) * size;
//2.3 获取用户查询的数据
String query = pageResult.getQuery();
List<User> rows = userMapper.findUserListByPage(start,size,query);
return pageResult.setTotal(total).setRows(rows);
}
编辑Mapper接口
List<User> findUserListByPage(@Param("start") int start,
@Param("size") int size,
@Param("query") String query);
编辑UserMapper.xml 映射文件
<mapper namespace="com.jt.mapper.UserMapper">
<!--
resultType: 适合单表查询
resultMap: 1.多表关联查询 2.字段名称和属性不一致的时候使用
if 判断条件
test="query !=null and query !='' 表示同时不满足时条件成立
-->
<select id="findUserListByPage" resultType="User">
select * from user
<where>
<if test="query !=null and query !='' ">username like "%"#{query}"%"</if>
</where>
limit #{start},#{size}
</select>
</mapper>
完成状态修改
业务说明
说明: 通过开关 控制数据库中的 status=true/false 数据库中显示1/0 1/0 与 true/false 对象映射可以互相转化.
根据用户的ID 实现状态的修改.
前端JS分析
- 知识点
作用域插槽: 一般在表格数据展现时,可以动态获取当前行对象.
用法:
1.template
2.slot-scope属性=“变量” - 页面JS分析
<el-table-column prop="status" label="状态">
<!-- <template slot-scope="scope">
{{scope.row.status}}
</template> -->
<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)
const {data: result} = await this.$http.put(`/user/status/${user.id}/${user.status}`)
if(result.status !== 200) return this.$message.error("用户状态修改失败!")
this.$message.success("用户状态修改成功!")
},
业务接口文档说明
-
请求路径 /user/status/{id}/{status}
-
请求类型 PUT
-
请求参数: 用户ID/状态值数据
-
返回值结果: SysResult对象
{"status":200,"msg":"服务器调用成功!","data":null}
编辑UserController
/**
* 业务: 实现用户状态的修改
* 参数: /user/status/{id}/{status}
* 返回值: SysResult对象
* 类型: put 类型
*/
@PutMapping("/status/{id}/{status}")
public SysResult updateStatus(User user){
userService.updateStatus(user);
return SysResult.success();
}
编辑UserService
//更新操作时修改 status/updated 更新时间
@Override
public void updateStatus(User user) {
user.setUpdated(new Date());
userMapper.updateStatus(user);
}
编辑UserMapper
@Update("update user set status = #{status},updated = #{updated} where id=#{id}")
void updateStatus(User user);
用户新增操作
页面JS分析
编辑新增页面
新增页面JS分析
新增业务接口说明
-
请求路径 /user/addUser
-
请求类型 POST
-
请求参数: 整个form表单数据封装为js对象进行参数传递
-
返回值结果: SysResult对象
{"status":200,"msg":"服务器调用成功!","data":null}
编辑UserController
/**
* 业务: 实现用户新增操作
* url: /user/addUser post类型
* 参数: 使用User对象接收
* 返回值: SysResult对象
*/
@PostMapping("/addUser")
public SysResult addUser(@RequestBody User user){
userService.addUser(user);
return SysResult.success();
}
编辑UserService
/**
* 1.密码进行加密
* 2.添加状态码信息
* 3.添加创建时间/修改时间
* 4.完成入库操作 xml方式
* @param user
*/
@Override
public void addUser(User user) {
//1.密码加密处理
Date date = new Date();
String md5Pass = DigestUtils.md5DigestAsHex(user.getPassword().getBytes());
user.setPassword(md5Pass)
.setStatus(true)
.setCreated(date)
.setUpdated(date); //最好保证时间唯一性.
userMapper.addUser(user);
}
编辑UserMapper/xml映射文件
编辑mapper接口
void addUser(User user);
编辑xml映射文件
<!--完成用户新增操作-->
<insert id="addUser">
insert into user(id,username,password,phone,email,status,created,updated)
value
(null,#{username},#{password},#{phone},#{email},#{status},#{created},#{updated})
</insert>
修改操作数据回显
页面JS分析
- 按钮点击事件
<el-button type="primary" icon="el-icon-edit" size="small" @click="updateUserBtn(scope.row)"></el-button>
- 数据回显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":"1235678@qq.com",
"status":true,
"role":null
}
}
编辑UserController
/**
* 根据ID查询数据库
* URL:/user/{id}
* 参数: id
* 返回值: SysResult(user对象)
*/
@GetMapping("/{id}")
public SysResult findUserById(@PathVariable Integer id){
User user = userService.findUserById(id);
return SysResult.success(user);
}
编辑UserService
@Override
public User findUserById(Integer id) {
return userMapper.findUserById(id);
}
编辑UserMapper
//原理: mybatis在进行单值传递时(int等基本类型/string) 取值时名称任意
// 底层通过下标[0]获取的数据和名称无关.
@Select("select * from user where id=#{id}")
User findUserById(Integer id);
页面效果展现
实现用户的更新操作
页面JS分析
-
页面JS
-
发起Ajax请求
修改的业务接口
-
请求路径: /user/updateUser
-
请求类型: PUT
-
请求参数: User对象结构
-
返回值: SysResult对象
-
JSON格式如下:
{
"status":200,
"msg":"服务器调用成功!",
"data":{}
}
编辑UserController
/**
* 业务说明: 实现数据的修改操作
* URL: /user/updateUser
* 参数: user对象
* 返回值: SysResult对象
* 请求类型: PUT
*/
@PutMapping("/updateUser")
public SysResult updateUser(@RequestBody User user){
userService.updateUser(user);
return SysResult.success();
}
编辑UserService
//id/phone/email
@Override
public void updateUser(User user) {
userMapper.updateUser(user);
}
编辑UserMapper
@Update("update user set phone=#{phone},email=#{email} where id=#{id}")
void updateUser(User user);
用户删除操作
页面JS修改
业务接口文档
-
请求路径: /user/{id}
-
请求类型: delete
-
请求参数:
-
返回值: SysResult对象
编辑UserController
/**
* 关于请求的小结
* 1.常规请求方式 get/delete ?key=value&key2=value2
* 2.post/put data: JS对象 后端接收@RequestBody
* 3.restFul风格 /url/arg1/arg2/arg3 使用对象接收
* 完成用户删除操作
* 1.URL地址 /user/{id}
* 2.参数: id
* 3.返回值: SysResult
*/
@DeleteMapping("/{id}")
public SysResult deleteUserById(@PathVariable Integer id){
userService.deleteUserById(id);
return SysResult.success();
}
编辑UserService
@Override
public void deleteUserById(Integer id) {
userMapper.deleteUserById(id);
}
编辑UserMapper
@Delete("delete from user where id=#{id}")
void deleteUserById(Integer id);
商品管理
业务涉及商品分类展现,新增,删除,修改。更新操作新增或许修改,都需要用事务控制和加上更新时间。
- 商品分类为树状结构,并且表关系涉及自关联,外键。
- 在展现时,默认展现一级目录。点击可以打开下级内容,有可能二级为最后一级。二级继续点击可以获得三级信息
遍历嵌套法:频繁访问数据库.导致数据库压力增大.严重时可能导致数据库服务器宕机. 不能接受的
- 查询出所有一级对象集合。
- 遍历一级对象结合,拿到一级对象,在根据一级对象获取一级ID,作为二级父ID,查询出二级对象集合
- 遍历二级对象结合,拿到二级对象,在根据二级对象获取二级ID,作为三级父ID,查询出三级对象集合
Map集合封装法:查询出所有数据,将父ID封装为Map集合的key,将ID封装为Map结合的value
新建封装方法 - 查询出所有对象,获取对象集合。
- 遍历对象集合,获取对象父ID
- 同一个父ID有多个子对象,所以在封装要前判断该父ID是否存过值。
- 存过:通过Map集合和父ID获取对应ID对象,直接传入
- 没存过:==建立一个实例为ArrayList的List集合,方便后面有序添加值。==往集合中封装对象,然后将父ID和集合封装到Map集合
- 返回封装Map集合
在核心方法 - 调用封装方法,获取封装Map集合
- 根据参数判断是否是一级,是则通过Map获取一级对象集合。判断是否为二级,是则调用二级方法,返回封装了二级对象的一级对象集合
- 因为上面判断一二级并有返回值,所以不是三级下方代码不会执行,可以直接调用三级方法,返回封装了三级对象的一级集合
在二级方法中 - 通过Map集合获取一级对象集合
- 遍历一级对象集合获取一级对象,并获得二级父ID。通过二级父ID获得二级对象集合
- 将二级集合封装到一级对象中
- 返回已封装二级的一级集合
- 在三级方法中,调用二级方法,获得已封装二级对象的一级集合
- 遍历一级集合得到一级对象,获得二级对象集合
- ==因为有时没有二级直接最后一级,所以要判断二级对象是否为空。==为空这跳过本次循环
- 不为空,遍历二级集合得到二级对象,获得三级对象集合,将三级对象集合封装到二级对象中
- 放回已封装三级的一级集合
- ==因为商品分类为树状结构,所以只能从最低级对象一层一层删除。==非倒数两级业务,需要层层获取到倒数第二级集合。从而确保有每一级的ID或父ID
商品列表展现
编辑映射文件
- 子查询Sql语句写法
/*查询一级菜单信息*/
SELECT * FROM rights WHERE parent_id = 0
/* 查询从表数据 */
SELECT * FROM rights WHERE parent_id = 3
- xml映射文件写法
<!--利用子查询的方式实现数据获取
1.查询主表信息
-->
<select id="getRightsList" resultMap="rightsRM">
select * from rights where parent_id = 0
</select>
<resultMap id="rightsRM" type="Rights" autoMapping="true">
<!--主键信息-->
<id property="id" column="id"></id>
<collection property="children" ofType="Rights"
select="findChildren" column="id"/>
</resultMap>
<select id="findChildren" resultType="Rights">
select * from rights where parent_id = #{id}
</select>
商品分类页面跳转
说明: 编辑路由 index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../components/Login.vue'
import ElementUI from '../components/ElementUI.vue'
import Home from '../components/Home.vue'
import User from '../components/user/user.vue'
import ItemCat from '../components/items/ItemCat.vue'
//使用路由机制
Vue.use(VueRouter)
const routes = [
{path: '/', redirect: '/login'},
{path: '/login', component: Login},
{path: '/elementUI', component: ElementUI},
{path: '/home', component: Home, children: [
{path: '/user', component: User},
{path: '/itemCat', component: ItemCat}
]}
]
页面效果展现
完成商品分类业务
页面JS分析
- 生命周期函数
//定义初始化函数
created() {
//默认获取商品分类列表数据
this.findItemCatList()
},
- 获取数据函数说明
async findItemCatList() {
const {
data: result
} = await this.$http.get("/itemCat/findItemCatList/3")
if (result.status !== 200) return this.$message.error("获取商品分类列表失败!!")
this.itemCatList = result.data
},
业务接口文档
-
请求路径: /itemCat/findItemCatList/{level}
-
请求类型: get
-
请求参数: level
-
业务说明: 查询3级分类菜单数据 要求三层结构嵌套
-
返回值: SysResult对象
商品分类表结构说明
- 表结构
sql案例练习
/*所有的一级菜单 parent_id=0*/
SELECT * FROM item_cat WHERE parent_id = 0
/*查询汽车用户的二级菜单*/
SELECT * FROM item_cat WHERE parent_id = 249
/*查询车载电器的三级菜单*/
SELECT * FROM item_cat WHERE parent_id = 281
小结: 商品分类表,通过parent_id 来指定父子级关系.
编辑ItemCatController
@RestController
@CrossOrigin
@RequestMapping("/itemCat")
public class ItemCatController {
@Autowired
private ItemCatService itemCatService;
/**
* 需求: 查询商品分类信息
* 参数: /{level} 1一级 2 一二级 3 一二三级
* url: /itemCat/findItemCatList/{level} restFul
* 返回值: SysResult(3级列表信息)
*/
@GetMapping("findItemCatList/{level}")
public SysResult findItemCatList(@PathVariable Integer level){
List<ItemCat> itemCatList = itemCatService.findItemCatList(level);
return SysResult.success(itemCatList);
}
}
编辑ItemCatService
package com.jt.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jt.mapper.ItemCatMapper;
import com.jt.pojo.ItemCat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ItemCatServiceImpl implements ItemCatService{
@Autowired
private ItemCatMapper itemCatMapper;
/**
* 步骤1.查询一级菜单列表
* @param level
* @return
*/
@Override
public List<ItemCat> findItemCatList(Integer level) {
//1.查询一级菜单
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("parent_id",0);
List<ItemCat> oneList = itemCatMapper.selectList(queryWrapper);
//2.查询二级菜单 二级数据是一级数据的子级 封装到一级数据中.
for(ItemCat oneItemCat : oneList){
int oneId = oneItemCat.getId(); //一级对象ID
//清空原始条件 必须有
queryWrapper.clear();
queryWrapper.eq("parent_id",oneId);
List<ItemCat> twoList = itemCatMapper.selectList(queryWrapper);
for(ItemCat twoItemCat : twoList){
//获取二级分类ID
int twoId = twoItemCat.getId();
//查询三级列表信息
queryWrapper.clear();
queryWrapper.eq("parent_id",twoId);
List<ItemCat> threeList = itemCatMapper.selectList(queryWrapper);
//将三级列表 封装到二级对象中
twoItemCat.setChildren(threeList);
}
//将二级数据封装到一级对象中
oneItemCat.setChildren(twoList);
}
return oneList;
}
}
上述案例分析
- 上述的案例 采用多级循环的方式. 将来会耗费服务器资源 100次 内层100次 总循环1万次. 暂时可以接受
- 上述的代码 频繁访问数据库.导致数据库压力增大.严重时可能导致数据库服务器宕机. 不能接受的
优化策略: 降低数据库访问的次数
采用数据结构优化代码
思路:
- 用户查询所有的数据库信息. (1-2-3所有数据)
- 数据结构 Map<k,v> key唯一的, value可以任意类型.
思路: Map<parentId,List>
例子: - Map<0,List<一级ItemCat对象>>
- Map<249,List<二级ItemCat对象>>
- Map<281,List<三级ItemCat对象>>
利用map 封装父子关系.
代码具体实现
@Service
public class ItemCatServiceImpl implements ItemCatService{
@Autowired
private ItemCatMapper itemCatMapper;
/**
* 利用Map集合封装所有的数据库记录
* 封装数据:
* 1.遍历所有的数据信息.
* 2.获取每一个parentId的值.
* 例子:
* 1.{id=1,parentId=0,name="张三"}
* 2.{id=2,parentId=0,name="李四"}
* 3.{id=3,parentId=1,name="王五"}
* Map= {
* key : value
* 0 : List[张三对象,李四对象.....],
* 1 : List[王五对象......]
* }
* @return
*/
public Map<Integer,List<ItemCat>> getMap(){
Map<Integer,List<ItemCat>> map = new HashMap<>();
//1.查询所有的数据库信息
List<ItemCat> itemCatList = itemCatMapper.selectList(null);
//2.将数据封装到map集合中
for (ItemCat itemCat : itemCatList){
Integer key = itemCat.getParentId(); //获取parentId当做key
//3.判断map集合中是否有值.
if(map.containsKey(key)){
//有值: 获取List集合,将自己追加到其中
map.get(key).add(itemCat);
}else{
//没值: 添加数据.将自己作为第一个元素填充
List<ItemCat> list = new ArrayList<>();
list.add(itemCat);
map.put(key,list);
}
}
return map;
}
@Override
public List<ItemCat> findItemCatList(Integer level) {
long startTime = System.currentTimeMillis();
Map<Integer,List<ItemCat>> map = getMap();
//根据level获取子级信息
if(level == 1){ //只获取一级列表信息
return map.get(0);
}
if(level == 2){ //获取一级和二级数据
return getTwoList(map);
}
List<ItemCat> oneList = getThreeList(map);
long endTime = System.currentTimeMillis();
System.out.println("优化前的耗时: 500ms,优化后耗时:"+(endTime - startTime)+"ms");
return oneList;
}
//获取三级列表信息 先获取1级数据,再获取2级数据.再获取3级数据
private List<ItemCat> getThreeList(Map<Integer, List<ItemCat>> map) {
//1.调用2级菜单方法.
List<ItemCat> oneList = getTwoList(map);
//2.实现思路 遍历一级集合,获取二级数据. 封装三级菜单
for(ItemCat oneItemCat : oneList){
//2.1 获取二级数据
List<ItemCat> twoList = oneItemCat.getChildren();
if(twoList == null || twoList.size()==0){
//判断二级集合是否为null.如果为null,表示没有二级菜单.
continue;
}
for (ItemCat twoItemCat : twoList){
int twoId = twoItemCat.getId();
List<ItemCat> threeList = map.get(twoId);
twoItemCat.setChildren(threeList);
}
}
return oneList;
}
//通过map集合 获取一级二级菜单信息.
private List<ItemCat> getTwoList(Map<Integer, List<ItemCat>> map) {
List<ItemCat> oneList = map.get(0);
//获取二级信息,应该先遍历一级集合
for (ItemCat oneItemCat : oneList){
int oneId = oneItemCat.getId();
//根据一级Id,获取二级集合
List<ItemCat> twoList = map.get(oneId);
oneItemCat.setChildren(twoList);
}
return oneList;
}
/**
* 步骤1.查询一级菜单列表
* @param level
* @return
*/
/* @Override
public List<ItemCat> findItemCatList(Integer level) {
long startTime = System.currentTimeMillis();
//1.查询一级菜单
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("parent_id",0);
List<ItemCat> oneList = itemCatMapper.selectList(queryWrapper);
//2.查询二级菜单 二级数据是一级数据的子级 封装到一级数据中.
for(ItemCat oneItemCat : oneList){
int oneId = oneItemCat.getId(); //一级对象ID
//清空原始条件 必须有
queryWrapper.clear();
queryWrapper.eq("parent_id",oneId);
List<ItemCat> twoList = itemCatMapper.selectList(queryWrapper);
for(ItemCat twoItemCat : twoList){
//获取二级分类ID
int twoId = twoItemCat.getId();
//查询三级列表信息
queryWrapper.clear();
queryWrapper.eq("parent_id",twoId);
List<ItemCat> threeList = itemCatMapper.selectList(queryWrapper);
//将三级列表 封装到二级对象中
twoItemCat.setChildren(threeList);
}
//将二级数据封装到一级对象中
oneItemCat.setChildren(twoList);
}
long endTime = System.currentTimeMillis();
System.out.println("耗时:"+ (endTime - startTime)+"ms");
return oneList;
}*/
}
商品分类新增实现
页面JS分析
//定义商品分类新增对象
itemCatForm: {
name: '', //定义商品分类名称
parentId: 0, //默认父级ID=0
level: 1 //默认是一级菜单
},
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对象
编辑ItemCatController
/**
* 业务需求: 实现商品分类新增
* URL: /itemCat/saveItemCat
* 类型: post
* 参数: {"name":"AAAAAA","parentId":0,"level":1} json串
* 返回值: SysResult对象
*/
@PostMapping("/saveItemCat")
public SysResult saveItem(@RequestBody ItemCat itemCat){
itemCatService.saveItem(itemCat);
return SysResult.success();
}
编辑ItemCatService
@Override
@Transactional //事务控制
public void saveItem(ItemCat itemCat) {
Date date = new Date();
itemCat.setStatus(true).setCreated(date).setUpdated(date);//启动
itemCatMapper.insert(itemCat);
}
商品分类删除操作
删除业务接口
-
请求路径: /itemCat/deleteItemCat
-
请求类型: delete
-
业务描述: 当删除节点为父级时,应该删除自身和所有的子节点
-
请求参数:
-
返回值结果 SysResult对象
前端页面JS
1. 页面JS
<el-button type="danger" icon="el-icon-delete" @click="deleteItemCatBtn(scope.row)">删除</el-button>
2. 发起Ajax请求
//传递分类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()
编辑ItemCatController
/**
* 完成商品分类的删除操作
* 1. 编辑URL: /itemCat/deleteItemCat
* 2. 参数: id/level
* 3. 返回值: SysResult()
*/
@DeleteMapping("/deleteItemCat")
public SysResult deleteItemCat(Integer id,Integer level){
itemCatService.deleteItemCat(id,level);
return SysResult.success();
}
编辑ItemCatService
//删除商品分类数据
@Override
public void deleteItemCat(Integer id, Integer level) {
//判断是否为3级菜单
if(level == 3){
itemCatMapper.deleteById(id);
}
if(level == 2){
//如果是二级,应该先获取三级数据之后删除,再删除自己
//delete from item_cat where parent_id=#{id} or id = #{id}
QueryWrapper<ItemCat> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("parent_id",id)
.or()
.eq("id",id);
itemCatMapper.delete(queryWrapper);
}
/**
* 如何删除一级菜单?
* 1.获取二级ID
* 终极sql: delete from item_cat where parent_id in (twoIds)
* or id in (twoIds)
* or id = #{id}
*/
if(level == 1){
QueryWrapper<ItemCat> queryWrapper = new QueryWrapper();
queryWrapper.eq("parent_id",id);
List twoIds = itemCatMapper.selectObjs(queryWrapper);
//清空数据
queryWrapper.clear();
//规则: 如果2级菜单有值,才会删除 2级和三级
queryWrapper.in(twoIds.size()>0,"parent_id",twoIds)
.or()
.in(twoIds.size()>0,"id",twoIds)
.or()
.eq("id",id);
itemCatMapper.delete(queryWrapper);
}
}
商品分类修改操作
页面JS分析
1.指定修改的按钮
<el-button type="success" icon="el-icon-edit" @click="updateItemCatBtn(scope.row)">编辑</el-button>
2. 数据的回显
//由于有层级关系,所有修改只能修改名称
updateItemCatBtn(itemCat) {
this.updateItemCatForm = itemCat
this.updateItemCatDialogVisible = true
},
3. 修改页面的JS
<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>
4. 修改按钮的JS
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对象
数据解析:
编辑ItemCatController
/**
* 修改商品分类名称
* URL: /itemCat/updateItemCat
* 参数: 整个form表单 JSON串
* 返回值: SysResult对象
*/
@PutMapping("/updateItemCat")
public SysResult updateItemCat(@RequestBody ItemCat itemCat){
itemCatService.updateItemCat(itemCat);
return SysResult.success();
}
编辑ItemCatService
//由于页面只修改的name名称.所以sql也只修改name/updated
@Override
@Transactional
public void updateItemCat(ItemCat itemCat) {
//用户只修改name,updated by id
ItemCat temp = new ItemCat();
temp.setId(itemCat.getId())
.setName(itemCat.getName())
.setUpdated(new Date());
itemCatMapper.updateById(temp);
}
商品模块业务实现
业务涉及商品信息展示,删除,修改,新增
商品信息别存于多个表,子表ID跟随主表ID,所以操作都有可能涉及多表。
接收信息时会需要一个大对象,包含基本信息对象和详细信息对象。
详细信息用的时富文本编辑器,传入后端的是字符串。为了存储大量字符,详细信息表需要用到mediumtext类型
==主键回显问题:==在新增操作时就会出现入库之后才有主键的问题,使用MP的情况下可以自动化回显
图片,视频存储一般不使用数据库,而是指定对应的文档储存。
前端图片向后端传输为输入流的方式传输,接收图片时需要用流对象接收。普通流需要手动关闭,代码繁琐,所以SpringMVC一般使用MultipartFile对象接收
文件传输四步:
- 文件类型校验,使用正则表达式
- 防止恶意程序,如:图片的话可以检查图片宽高
- 分目录储存,可以按类型分或者按时间分
- 自定义文件名,使用UUID
商品页面跳转
编辑index.js的路由文件
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../components/Login.vue'
import ElementUI from '../components/ElementUI.vue'
import Home from '../components/Home.vue'
import User from '../components/user/user.vue'
import ItemCat from '../components/items/ItemCat.vue'
import Item from '../components/items/Item.vue'
//使用路由机制
Vue.use(VueRouter)
const routes = [
{path: '/', redirect: '/login'},
{path: '/login', component: Login},
{path: '/elementUI', component: ElementUI},
{path: '/home', component: Home, children: [
{path: '/user', component: User},
{path: '/itemCat', component: ItemCat},
{path: '/item', component: Item}
]}
]
页面效果
构建商品层级代码
item 表设计
编辑Item POJO
@Data
@Accessors(chain = true)
@TableName("item")
public class Item extends BasePojo{
@TableId(type= IdType.AUTO)
private Integer id; //商品Id号
private String title; //商品标题信息
private String sellPoint; //卖点信息
private Integer price; //商品价格
private Integer num; //商品数量
private String images; //商品图片
private Integer itemCatId; //商品分类ID号
private Boolean status; //状态信息 0 下架 1 上架
}
编辑层级代码结构
完成商品列表展现
页面分析
//1. 生命周期函数
created() {
//1.获取商品列表数据
this.getItemList()
},
//2. 调用 this.getItemList()
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对象接收
-
返回值结果:
编辑ItemController
@RestController
@CrossOrigin
@RequestMapping("/item")
public class ItemController {
@Autowired
private ItemService itemService;
/**
* 业务: 实现商品的分页查询
* URL: /item/getItemList?query=&pageNum=1&pageSize=10
* 参数: query=&pageNum=1&pageSize=10
* 返回值: SysResult(PageResult)
*/
@GetMapping("/getItemList")
public SysResult getItemList(PageResult pageResult){//3
//3+2(总记录数,分页结果)
pageResult = itemService.getItemList(pageResult);
return SysResult.success(pageResult);//5
}
}
编辑ItemService
@Service
public class ItemServiceImpl implements ItemService{
@Autowired
private ItemMapper itemMapper;
/**
* 要求: 3+2(总记录数,分页结果)
* 关于selectPage(参数说明)
* 参数1: page MP提供的分页对象
* 参数2: 条件构造器
* @param pageResult
* @return
*/
@Override
public PageResult getItemList(PageResult pageResult) {
//1.构建分页对象 参数1: 第几页 参数2: 多少条
Page<Item> page = new Page<>(pageResult.getPageNum(),pageResult.getPageSize());
//2.准备条件构造器 构建模糊查询
QueryWrapper queryWrapper = new QueryWrapper();
String query = pageResult.getQuery();
boolean flag = StringUtils.hasLength(query);
queryWrapper.like(flag,"title",query);
//3.根据MP查询 实现分页数据的自动封装
page = itemMapper.selectPage(page,queryWrapper);
//4.获取数据,返回分页对象
long total = page.getTotal();
//获取分页结果
List<Item> rows = page.getRecords();
return pageResult.setTotal(total).setRows(rows);
}
}
编辑分页配置类
@Configuration //这是配置类
public class MybatisConfig {
//需要通过配置文件 指定数据库类型.
// 最新版
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MARIADB));
return interceptor;
}
}
页面效果展现
商品状态的修改
页面JS分析
<template slot-scope="scope">
<el-switch v-model="scope.row.status" active-color="#13ce66" inactive-color="#ff4949"
@change="updateStatus(scope.row)"></el-switch>
</template>
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
-
请求参数: 使用对象接收
-
返回值结果:
编辑ItemController
/**
* 修改商品的状态信息
* URL: /item/updateItemStatus
* 参数: JSON串 {id:xx,status:xx}
* 返回值: SysResult对象
*/
@PutMapping("/updateItemStatus")
public SysResult updateItemStatus(@RequestBody Item item){
itemService.updateItemStatus(item);
return SysResult.success();
}
编辑ItemService
@Override
@Transactional //控制事务
public void updateItemStatus(Item item) {
itemMapper.updateById(item);
}
商品删除操作
页面分析
//根据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()
业务接口文档
-
请求路径: /item/deleteItemById
-
请求类型: delete
-
请求参数:
-
返回值结果:
编辑ItemController
/**
* 业务需求: 根据Id 删除数据
* URL: /item/deleteItemById
* 参数: id
* 返回值: SysResult对象
*/
@DeleteMapping("/deleteItemById")
public SysResult deleteItemById(Integer id){
itemService.deleteItemById(id);
return SysResult.success();
}
编辑ItemService
@Override
@Transactional
public void deleteItemById(Integer id) {
itemMapper.deleteById(id);
itemDescMapper.deleteById(id);
}
商品新增操作
商品基本信息入库
实现页面跳转
const routes = [
{path: '/', redirect: '/login'},
{path: '/login', component: Login},
{path: '/elementUI', component: ElementUI},
{path: '/home', component: Home, children: [
{path: '/user', component: User},
{path: '/itemCat', component: ItemCat},
{path: '/item', component: Item},
{path: '/item/addItem', component: AddItem}
]}
]
页面效果:
新增页面JS
表结构说明:
- 商品的基本信息 保存到item表
- 商品的详细信息 保存到item_desc表中.
/* 添加商品按钮 */
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 实现商品数据提交 用一个大对象 包裹2个小对象
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: "<ul><li>品牌: <a href=https://list.jd.com/list.html"....... "
}
}
-
请求参数: 使用ItemVO对象接收
-
ItemVO参数详解:
-
Item对象
-
itemDesc 对象
-
为了降低商品提交代码的耦合性,将大字段信息详情,采用ItemDesc对象进行封装
-
返回值结果:
编辑ItemController
/**
* 完成商品新增操作
* 1.URL地址 http://localhost:8091/item/saveItem
* 2.参数 post itemVO JSON串
* 3.返回值 SysResult对象
*/
@PostMapping("/saveItem")
public SysResult saveItem(@RequestBody ItemVO itemVO){
itemService.saveItem(itemVO);
return SysResult.success();
}
编辑ItemService
@Override
@Transactional
public void saveItem(ItemVO itemVO) {
Item item = itemVO.getItem();
//设定状态
item.setStatus(true);
itemMapper.insert(item);
}
商品详情入库
富文本编辑器
说明: 富文本可以在页面中,实现 “所见即所得” 的效果
引入步骤
引入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
/* 将富文本编辑器注册为全局可用的组件 */
Vue.use(VueQuillEditor)
使用富文本编辑器
<!-- 定义富文本编辑器-->
<quill-editor ref="myQuillEditor" v-model="itemDesc.itemDesc">
</quill-editor>
关于ItemDesc 的说明
说明: 由于Item和ItemDesc 是典型的一对一. 所以要求 item.id = itemDesc.id
@Data
@Accessors(chain = true)
@TableName("item_desc")
public class ItemDesc extends BasePojo{
@TableId
private Integer id;
private String itemDesc;
}
编辑ItemDescMapper
public interface ItemDescMapper extends BaseMapper<ItemDesc>{
}
编辑ItemService
/**
* 问题: id是主键自增. 入库之后才有主键所以
* 应该让主键动态回显
* 1.Mybatis 动态实现回显
* <insert id="xxxx" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
* insertinto xxxx
* </insert>
* 2.MP是mybatis的增强版本.所以可以实现自动的主键回显!!!
* @param itemVO
*/
@Override
@Transactional
public void saveItem(ItemVO itemVO) {
Item item = itemVO.getItem();
//设定状态
item.setStatus(true);
itemMapper.insert(item);
//获取商品详情
ItemDesc itemDesc = itemVO.getItemDesc();
itemDesc.setId(item.getId());
itemDescMapper.insert(itemDesc);
}
修改表类型.
说明: 为了存储大字段.修改数据库类型
商品修改操作
编辑页面HTML
//1. 定义页面html
<!-- 定义商品修改的对话框 -->
<!-- 定义商品修改的对话框 -->
<el-dialog title="商品修改" :visible.sync="updateDialogVisible" width="60%">
<!-- 准备修改的表单-->
<el-form :model="updateItem" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="标题信息" prop="title">
<el-input v-model="updateItem.title"></el-input>
</el-form-item>
<el-form-item label="卖点信息" prop="sellPoint">
<el-input v-model="updateItem.sellPoint"></el-input>
</el-form-item>
<el-form-item label="价格信息" prop="price">
<el-input v-model="updateItem.price"></el-input>
</el-form-item>
<el-form-item label="数量信息" prop="num">
<el-input v-model="updateItem.num"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="updateDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
</span>
</el-dialog>
//2. 定义页面JS
updateDialogVisible: false,
updateItem: {},
//准备一个校验规则
rules: {
title: [
{ required: true, message: '请输入商品标题信息', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
],
sellPoint: [
{ required: true, message: '请输入商品卖点信息', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
],
price: [
{ required: true, message: '请输入商品价格信息', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
],
num: [
{ required: true, message: '请输入商品数量信息', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
],
}
//定义JS按钮
updateItemBtn(item){
console.log("扩展案例,自己实现 只需要修改 标题/卖点/价格/数量")
this.updateDialogVisible = true
this.updateItem = item
this.updateItem.price = (this.updateItem.price / 100).toFixed(2)
}
商品图片上传(不完全)
编辑页面
//1. 官网图片JS说明
<!-- 图片上传的JS
1. action: 代表图片上传的地址url
2. file-list: 图片列表数据的集合[{name:"xx",url:"xxx"},{}]
3. 钩子函数: 满足某些条件时触发.
4. on-preview 当点击已上传列表的信息时触发
5. on-remove 当移除列表中的图片时触发
-->
<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>
2. 页面JS补充知识
handlePreview(){
console.log("触发查看函数!!!!")
},
handleRemove(){
console.log("移除时触发!!!!")
}
图片上传项目说明
<!--
一.文件上传组件说明
1.action: 上传图片地址 http://localhost:8091/xxx/xxx
2.on-preview 点击图片时触发
3.on-remove 移除图片时触发
4.on-success 图片上传成功时触发
5.multiple 可以支持多张图片上传
6.drag 是否允许拖拽
二.请求类型: 一般上传字节信息时,首选post请求
三.上传文件key 说明: 文件上传时的key=file.
后端接收数据时采用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>
//定义文件上传路径地址
uploadUrl: "http://localhost:8091/file/upload",
图片上传接口文档说明
-
请求路径: http://localhost:8091/file/upload
-
请求类型: post
-
请求参数:
-
返回值结果:
-
ImageVO对象说明
编辑ImageVO
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class ImageVO {
private String virtualPath; //图片虚拟路径 动态的路径
private String urlPath; //图片回显的URL地址
private String fileName; //文件上传后的文件名称
}
编辑ItemController
@RestController
@CrossOrigin
@RequestMapping("/file")
public class FileController {
/**
* 业务说明: 实现图片上传
* URL: http://localhost:8091/file/upload
* 类型: post
* 参数: file 字节信息
* 返回值: SysResult.success()
* 扩展:
* 一般情况下:
* 一般前端向后端服务器发送字节信息.由外到内实现数据传输.
* 采用输入流信息. InputStream file
* 使用字节流的弊端: 1.必须手动关闭, 2.代码操作繁琐
* 底层代码的实现.
* SpringMVC高级API MultipartFile 专门处理IO流操作
* 文件上传步骤:
* 1.获取文件名称
* 2.准备文件上传的目录
* 3.判断目录是否存在 存在目录: 实现上传 没有目录:创建目录
* 4.利用工具API方法,实现文件上传.
* 注意事项: MultipartFile 默认支持1M的数据
*/
@PostMapping("/upload")
public SysResult upload(MultipartFile file) throws IOException {
//1.获取文件名称
String fileName = file.getOriginalFilename();
//2.准备磁盘地址
String dirPath = "E:/project3/images/";
//3.将这个文件目录 封装为File对象
File dirFile = new File(dirPath);
//4.判断对象是否存在
if(!dirFile.exists()){
//如果文件目录不存在,则创建目录
dirFile.mkdirs(); //表示多级目录上传.
}
//5.封装文件全路径 E:xxx/xxx/a.jpg
String path = dirPath + fileName;
File allFile = new File(path);
//6.实现文件上传 将IO流按照指定的对象格式进行输出.
file.transferTo(allFile);
return SysResult.success();
}
}
文件上传实现
编辑FileController
@RestController
@CrossOrigin
@RequestMapping("/file")
public class FileController {
@Autowired
private FileService fileService;
/**
* 业务说明: 实现图片上传
* URL: http://localhost:8091/file/upload
* 类型: post
* 参数: MultipartFile file 字节信息
* 返回值: SysResult.success()
* 问题思考:
* 1.完成图片类型校验 jpg|png|gif....
* 2.防止恶意程序 a.exe.jpg
* 3.将图片分目录存储
* 3.1.按照类型分 理论可以但是得多分配几个
* 3.2.按照时间划分. yyyy/MM/dd
* 4.自定义文件名称. 利用UUID充当图片名称.
*/
@PostMapping("/upload")
public SysResult upload(MultipartFile file) throws IOException {
ImageVO imageVO = fileService.upload(file);
if(imageVO == null){
return SysResult.fail();
}
return SysResult.success(imageVO);
}
/**
* 业务说明: 实现图片上传
* URL: http://localhost:8091/file/upload
* 类型: post
* 参数: file 字节信息
* 返回值: SysResult.success()
* 扩展:
* 一般情况下:
* 一般前端向后端服务器发送字节信息.由外到内实现数据传输.
* 采用输入流信息. InputStream file
* 使用字节流的弊端: 1.必须手动关闭, 2.代码操作繁琐
* 底层代码的实现.
* SpringMVC高级API MultipartFile 专门处理IO流操作
* 文件上传步骤:
* 1.获取文件名称
* 2.准备文件上传的目录
* 3.判断目录是否存在 存在目录: 实现上传 没有目录:创建目录
* 4.利用工具API方法,实现文件上传.
* 注意事项: MultipartFile 默认支持1M的数据
*/
/*@PostMapping("/upload")
public SysResult upload(MultipartFile file) throws IOException {
//1.获取文件名称
String fileName = file.getOriginalFilename();
//2.准备磁盘地址
String dirPath = "E:/project3/images/";
//3.将这个文件目录 封装为File对象
File dirFile = new File(dirPath);
//4.判断对象是否存在
if(!dirFile.exists()){
//如果文件目录不存在,则创建目录
dirFile.mkdirs(); //表示多级目录上传.
}
//5.封装文件全路径 E:xxx/xxx/a.jpg
String path = dirPath + fileName;
File allFile = new File(path);
//6.实现文件上传 将IO流按照指定的对象格式进行输出.
file.transferTo(allFile);
return SysResult.success();
}*/ }
编辑FileService
package com.jt.service;
import com.jt.vo.ImageVO;
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.net.FileNameMap;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Service
public class FileServiceImpl implements FileService{
private String localDirPath = "E:/project3/images";
//1.校验图片类型 xxx.jpg 校验后缀是否为jpg
@Override
public ImageVO upload(MultipartFile file) {
//1.1 获取文件名称 abc.jpg
String fileName = file.getOriginalFilename();
//1.2 全部转化为小写字母
fileName = fileName.toLowerCase();
//1.3正则校验是否为图片类型
if(!fileName.matches("^.+\\.(jpg|png|gif)$")){
//图片类型 不匹配 程序应该终止
return null;
}
//2.校验是否为恶意程序 怎么判断就是一张图 高度和宽度
//2.1 通过图片对象进行处理
try {
BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
int height = bufferedImage.getHeight();
int width = bufferedImage.getWidth();
if(height == 0 || width == 0){
return null;
}
//3.将图片分目录存储 yyyy/MM/dd
String dateDir = new SimpleDateFormat("/yyyy/MM/dd/")
.format(new Date());
String dateDirPath = localDirPath + dateDir;
File dirFile = new File(dateDirPath);
if(!dirFile.exists()){
dirFile.mkdirs();
}
//4.防止文件重名 动态生成UUID.类型
//4.1 动态生成UUID
String uuid = UUID.randomUUID().toString()
.replace("-","");
//4.2 获取图片类型 abc.jpg .jpg
String fileType = fileName.substring(fileName.lastIndexOf("."));
// uuid.jpg
String newFileName = uuid + fileType;
//5.实现文件上传 1.准备全文件路径 2. 封装对象实现上传
String path = dateDirPath + newFileName;
file.transferTo(new File(path));
} catch (IOException e) {
e.printStackTrace();
return null;
}
return null;
}
}
文件删除操作
文件删除JS
文件删除业务接口
-
请求路径: http://localhost:8091/file/deleteFile
-
请求类型: delete
-
请求参数:
-
返回值结果:
编辑FileController
/**
* 业务说明: 文件删除操作
* URL地址: http://localhost:8091/file/deleteFile
* 请求类型: delete
* 参数: virtualPath 虚拟路径
* 返回值: SysResult对象
*/
@DeleteMapping("/deleteFile")
public SysResult deleteFile(String virtualPath){
fileService.deleteFile(virtualPath);
return SysResult.success();
}
编辑FileService
@Override
public void deleteFile(String virtualPath) {
String filePath = localDirPath + virtualPath;
File file = new File(filePath);
if(file.exists()){ //如果文件存在,则删除数据
file.delete();
}
}
图片路径封装
路径分析
- 图片网络地址: https://img14.360buyimg.com/n0/jfs/t2/ac4a3f32ea776da3.jpg
协议://域名:80/虚拟地址 - 图片地址封装: http://image.jt.com:80/2021/11/11/uuid.jpg.
页面URL地址封装
package com.jt.service;
import com.jt.vo.ImageVO;
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.net.FileNameMap;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Service
public class FileServiceImpl implements FileService{
private String localDirPath = "E:/project3/images";
private String preUrl = "http://image.jt.com";
//1.校验图片类型 xxx.jpg 校验后缀是否为jpg
@Override
public ImageVO upload(MultipartFile file) {
//1.1 获取文件名称 abc.jpg
String fileName = file.getOriginalFilename();
//1.2 全部转化为小写字母
fileName = fileName.toLowerCase();
//1.3正则校验是否为图片类型
if(!fileName.matches("^.+\\.(jpg|png|gif)$")){
//图片类型 不匹配 程序应该终止
return null;
}
//2.校验是否为恶意程序 怎么判断就是一张图 高度和宽度
//2.1 通过图片对象进行处理
try {
BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
int height = bufferedImage.getHeight();
int width = bufferedImage.getWidth();
if(height == 0 || width == 0){
return null;
}
//3.将图片分目录存储 yyyy/MM/dd
String dateDir = new SimpleDateFormat("/yyyy/MM/dd/")
.format(new Date());
String dateDirPath = localDirPath + dateDir;
File dirFile = new File(dateDirPath);
if(!dirFile.exists()){
dirFile.mkdirs();
}
//4.防止文件重名 动态生成UUID.类型
//4.1 动态生成UUID
String uuid = UUID.randomUUID().toString()
.replace("-","");
//4.2 获取图片类型 abc.jpg .jpg
String fileType = fileName.substring(fileName.lastIndexOf("."));
// uuid.jpg
String newFileName = uuid + fileType;
//5.实现文件上传 1.准备全文件路径 2. 封装对象实现上传
String path = dateDirPath + newFileName;
file.transferTo(new File(path));
//6. 实现ImageVO数据的返回
//6.1 准备虚拟路径 /2021/11/11/uuid.jpg
String virtualPath = dateDir + newFileName;
//6.2 准备URL地址 域名前缀 + 虚拟路径
String url = preUrl + virtualPath;
System.out.println(url);
return new ImageVO(virtualPath,url,newFileName);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
@Override
public void deleteFile(String virtualPath) {
String filePath = localDirPath + virtualPath;
File file = new File(filePath);
if(file.exists()){ //如果文件存在,则删除数据
file.delete();
}
}
}
动态为属性赋值
业务需求
说明: 如果将属性写死到java类中,后期维护时 导致维护不方便.
优化: 可以通过@value注解动态赋值.
编辑properties配置文件
image.localDirPath=E:/project3/images
image.preUrl=http://image.jt.com
属性动态赋值
正则表达式(复习)
正则表达式说明
正则表达式,又称规则表达式。(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。
总结: 正则表达式就是一种特殊格式的字符串.校验文本信息的.
匹配不确定次数
匹配固定次数
匹配取值区间
分组匹配
(jpg|png|gif)
正则案例练习
- 要求匹配电话号码 11位 开头都是1
正则表达式: 1[3-9][0-9]{9} - 要求匹配邮箱 xxxx@qq.com
正则表达式:
^[a-zA-Z0-9-_]+@[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+$