项目总结-瑞吉外卖
Posted Lee_ing
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了项目总结-瑞吉外卖相关的知识,希望对你有一定的参考价值。
软件开发基础
分工:
流程:
01项目介绍
组成部分:系统管理后台、移动端
开发分期:
技术选型:
架构:
角色:
后台系统分析:
登录页面:
登录成功后,进入首页面(员工管理):
分类管理页面:
菜品管理页面:
套餐管理页面:
订单明细页面:
02开发环境搭建
数据库搭建
创建数据库-->导入资料中的表文件(db_reggie.sql)
命令行形式:
mysql> use reggie.sql;
Database changed
mysql> source D:\\db_reggie.sql;
maven项目搭建
- 创建新项目:检查项目的编码、maven仓库配置、jdk配置
- 导入pom.xml文件
父功能--springboot
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.5</version><relativePath/> </parent>
jdk版本
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>
maven依赖坐标及插件
pom部分代码
<dependencies>
<!--spring-boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--spring-boot单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--spring-boot-web应用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<!--将对象转为json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<!--通用语言包-->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!--mysql驱动包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--ali数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
</dependencies>
<!--spring-boot-maven插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.5</version>
</plugin>
</plugins>
</build>
- 配置文件application.yml
server:
port: 8089 #tomcat端口号
spring:
application:
name: reggie_take_out #应用的名称
datasource: #数据源相关配置
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/riggle?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
cache:
redis:
time-to-live: 1800000 #设置缓存有效期
#mybatis-plus配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
#在映射实体或者属性时,将数据库中表名和宇段名中的下划线去掉,按照驼峰命名法映射(address_book表名--->AddressBook实体类名)
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
riggle:
path: D:\\img\\
- 编写启动类
@Slf4j //使用日志log.(同理:lombok库中,编写实体类时,加入注解,get\\set方法可以省略,)
@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)
//@ServletComponentScan
//@EnableTransactionManagement
//@EnableCaching //开启缓存注解功能
public class ReggieApplication
public static void main(String[] args)
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功");
- 导入前端资源
配置类设置静态资源的映射
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport
/**
* 静态映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry)
log.info("开始静态资源映射");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
//请求命令的格式-->映射到资源地址
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
03后台登录功能开发
需求分析
1. 需求:\\src\\main\\resources\\backend\\page\\login\\login.html
2. 响应:点击登录后,会出现404,因为还没有写响应请求的处理器
以json的格式提交到服务端
3. 后端相关类:服务端创建相关的类:
通过controller把信息接收到,最后到数据库DB中查询
4. 数据模型:employee表
前端部分:
1. 点击登录时,代码中会调用loginApi方法(封装到了js文件中)
login.html核心代码
methods:
async handleLogin()
this.$refs.loginForm.validate(async (valid) =>
if (valid)
this.loading = true
let res = await loginApi(this.loginForm)
if (String(res.code) === \'1\') //1表示登录成功
localStorage.setItem(\'userInfo\',JSON.stringify(res.data))
window.location.href= \'/backend/index.html\'
else
this.$message.error(res.msg)
this.loading = false
)
loginForm为提交的json数据
响应返回值res,有code、data、msg等属性;(所以后端处理最后的返回值需要有这些)
数据交互:页面response响应回的数据是json数据,后端将R对象转变为json
把数据存储在localStorage【F12中application里可以查看】
2. login.js文件中,通过ajax服务来发送请求;(对应上面的404错误)
js文件code
function loginApi(data)
return $axios(
\'url\': \'/employee/login\',
\'method\': \'post\',
data
)
后端开发
通用结果类:导入返回结果类R(响应前端)【common包】
R类
/**
* 通用返回结果,服务端响应的数据最终都会封装成此对象
*
* @param <T>
*/
@Data
public class R<T> implements Serializable
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
public static <T> R<T> success(T object)
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
public static <T> R<T> error(String msg)
R r = new R();
r.msg = msg;
r.code = 0;
return r;
public R<T> add(String key, Object value)
this.map.put(key, value);
return this;
1. 实体类:创建实体类Employee,和表employee进行映射(entity包中)
Employee
/**
* 员工实体
*/
@Data
public class Employee implements Serializable
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String name;
private String password;
private String phone;
private String sex;
private String idNumber;//身份证号码//驼峰命名映射(在应用配置中已配置)
private Integer status;
//这些都是公共字段,加入TableField注解,FieldFill.INSERT表示插入时填充字段
//创建时间
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
//更新时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
2. mapper接口(mapper包)
基于mybatis-plus,提供了相应的基础父类or接口
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee>
3. service接口以及impl实现类(service包)
service接口:
public interface EmployeeService extends IService<Employee>
实现类:
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService
4. controller类(controller包)
@Slf4j
@RestController
@RequestMapping("/employee")//根据请求url
public class EmployeeController
@Autowired //注入service接口
private EmployeeService employeeService;
登录方法:(controller类的方法)
1. 逻辑:
2. 代码
- 前端传入了一个json数据,接收数据时,需要加注解@RequestBody
- HttpServletRequest request:如果登录成功后,把对象id存到session一份,想获取当前登录用户的话,可以随时获取(request.getSession)
- 查询数据库:employeeService.getOne(wrapper);【索引中username是unique类型的,所以唯一,使用getOne】
/**
* 员工登录
*
* @param request
* @param employee
* @return
*/
@PostMapping("login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) //传入需要和Employee类中的名称相对应
//获取用户名和密码
String username = employee.getUsername();
String password = employee.getPassword();
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
log.info("登录失败");
return R.error("登录失败");
password = DigestUtils.md5DigestAsHex(password.getBytes());
//查询数据库
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
//LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>();
//wrapper.eq(Employee::getUsername, employee.getUsername());
Employee result = employeeService.getOne(wrapper);
//如果没有查到
if (result == null)
log.info("登录失败没有查询结果");
return R.error("登录失败");
//查到了,对比密码
if (!result.getPassword().equals(password))
log.info("登录失败密码不对");
return R.error("登录失败");
//查到了,对比状态
if (result.getStatus() != 1)
log.info("登录失败禁用");
return R.error("账号不可用");
//登录成功;将员工id存入session中
request.getSession().setAttribute("employee", result.getId());
return R.success(result);
退出功能:
1. 前端分析:
index.html
methods:
logout()
logoutApi().then((res)=>
if(res.code === 1)
localStorage.removeItem(\'userInfo\')
window.location.href = \'/backend/page/login/login.html\'
)
logout()方法:logoutApi()
api/login.js
function logoutApi()
return $axios(
\'url\': \'/employee/logout\',
\'method\': \'post\',
)
logoutApi()中有请求方式
2. 退出方法:(controller类的方法)接收前端发送的请求
清理session中的用户id:操作session,需要HttpServletRequest request
返回结果:R
/**
* 退出登录,移除session
*
* @param request
* @return
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request)
request.getSession().removeAttribute("employee");
return R.success("退出成功");
完善功能:(过滤器/拦截器)
必须登录成功后才能进入系统首页面中;如果没有登录,需要跳转到登录页面
实现:
1、创建自定义过滤器LoginCheckFilter
2、在启动类上加入注解@ServletComponentScan
3、完善过滤器的处理逻辑
代码:
- 创建过滤器:(filter包)
注解:@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/")
"/":所有的请求都拦截
/**
* 检查用户是否已经完成登录
*/
@Slf4j
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException
HttpServletRequest request = (HttpServletRequest) servletRequest;//向下转型
HttpServletResponse response = (HttpServletResponse) servletResponse;
//获取本次请求的URI
String requestURI = request.getRequestURI();// /backend/index.html
log.info("拦截到请求:",requestURI);
filterChain.doFilter(request,response);//放行
-
在启动类上加入注解@ServletComponentScan
-
处理拦截到的请求
LoginCheckFilter类
/**
* 检查用户是否已经完成登录
*/
@Slf4j
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1、获取本次请求的URI
String requestURI = request.getRequestURI();// /backend/index.html
log.info("拦截到请求:",requestURI);
//定义不需要处理的请求路径
String[] urls = new String[]
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/common/**",
"/user/sendMsg",
"/user/login",
;
//2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);
//3、如果不需要处理,则直接放行
if(check)
log.info("本次请求不需要处理",requestURI);
filterChain.doFilter(request,response);
return;
//4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null)
log.info("用户已登录,用户id为:",request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
log.info("用户未登录");
//5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
/**
* 路径匹配,检查本次请求是否需要放行
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls,String requestURI)
for (String url : urls)
boolean match = PATH_MATCHER.match(url, requestURI);
if(match)
return true;
return false;
路径匹配器:
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
用户没有登录,并不是直接跳页面。结合前端js代码,前端也有拦截器:输出流的方式往回写数据,前端接收到会自动页面跳转:
resources\\backend\\js\\request.js
// 响应拦截器(前端拦截器)
service.interceptors.response.use(res =>
console.log(\'---响应拦截器---\',res)
// 未设置状态码则默认成功状态
const code = res.data.code;
// 获取错误信息
const msg = res.data.msg
console.log(\'---code---\',code)
if (res.data.code === 0 && res.data.msg === "NOTLOGIN") // 返回登录页面
// MessageBox.confirm(\'登录状态已过期,您可以继续留在该页面,或者重新登录\', \'系统提示\',
// confirmButtonText: \'重新登录\',
// cancelButtonText: \'取消\',
// type: \'warning\'
//
// ).then(() =>
// )
console.log(\'---/backend/page/login/login.html---\',code)
localStorage.removeItem(\'userInfo\')
window.top.location.href = \'/backend/page/login/login.html\'
else
return res.data
,
.......
04员工管理业务开发
新增员工
1. 数据模型:是将新增页面录入的员工数据插入到employee表。
需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的
状态值默认为1
2. 开发逻辑
1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
3. 代码实现
json格式数据需要@RequestBody Employee employee
/**
* 新增员工
*
* @param employee
* @return
*/
@PostMapping
public R<String> save(HttpServletRequest request, @RequestBody Employee employee)
log.info("新增员工,员工信息:", employee.toString());
//设置初始密码123456,需要进行md5加密处理
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//获得当前登录用户的id
Long empId = (Long) request.getSession().getAttribute("employee");//强转
employee.setCreateUser(empId);
employee.setUpdateUser(empId);
employeeService.save(employee);
return R.success("新增员工成功");
- 完善
①解决异常:(提交重复unique字段会报异常)
1、在Controller方法中加入try、catch进行异常捕获
2、使用异常处理器进行全局异常捕获
全局异常处理类(common包)
GlobalExceptionHandler
@Slf4j
@ControllerAdvice(annotations = RestController.class, Controller.class)//不管哪个类,只要加了这两个注解,就会被异常处理器处理
@ResponseBody//需要返回json数据
/**
* 全局异常捕获
*/
public class GlobalExceptionHandler
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex)
log.error(ex.getMessage());
//Duplicate entry \'zhangsan\' for key \'idx_username\'
/**
* 对于添加员工已存在的名字
*/
if (ex.getMessage().contains("Duplicate entry"))
String[] split = ex.getMessage().split(" ");//数组对象
String msg = split[2] + "已存在";
return R.error(msg);
return R.error("未知错误");
总结:请求-响应式模式
员工信息分页
1. 需求:
分页的方式来展示列表数据;
根据过滤条件进行查询
2. 代码逻辑
1、页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUl的Table组件展示到页面上
前端分析:list.html
前端的request.js在拦截器:拦截get请求的处理:把json数据解析出来,动态的追加到url地址后面【
Request URL: http://localhost:8080/employce/page?page=1&pagesize=10】
Vue中钩子函数:
......
created()
this.init()
this.user = JSON.parse(localStorage.getItem(\'userInfo\')).username
,
mounted()
,
methods:
async init () #自定义init()
#构造数据json
const params =
page: this.page,
pageSize: this.pageSize,
name: this.input ? this.input : undefined
#getMemberList封装到了member.js文件中
await getMemberList(params).then(res =>
if (String(res.code) === \'1\')
#前端需要这样的数据
this.tableData = res.data.records || []
this.counts = res.data.total
).catch(err =>
this.$message.error(\'请求出错了:\' + err)
)
,
..........
3. 代码实现
使用mybatis-plus提供的分页插件
配置分页插件:(config包下存放配置类)
/**
* 配置MP的分页插件
*/
@Configuration//配置类的注解
public class MybatisPlusConfig
@Bean //表示需要spring来管理它
public MybatisPlusInterceptor mybatisPlusInterceptor() //拦截器
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
//加入一个拦截器插件
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
分页方法:(EmployeeController类)
使用Page泛型:根据前端,响应字段需要有records、total等字段
发送请求:刷新、查询、跳转到下一页【都会重新发送请求】
/**
* 员工信息分页查询
*
* @param page 当前查询页码
* @param pageSize 每页展示记录数
* @param name 员工姓名 - 可选参数
* @return
*/
@GetMapping("/page")//get方式请求
public R<Page> page(int page, int pageSize, String name)
//Page类是mybatis-plus封装好的
log.info("page = ,pageSize = ,name = ", page, pageSize, name);
//构造分页构造器
Page pageInfo = new Page(page, pageSize);
//构造条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
//添加过滤条件
queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
//添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
启用or禁用员工账号
1. 需求
对某个员工账号进行启用或者禁用操作。
需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用禁用按钮不显示。
2.代码逻辑
1、页面发送ajax请求,将参数(id、status)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service更新数据
3、Service调用Mapper操作数据库
前端分析:
点击启用/禁用按钮,如何发送请求
member/list.html
//状态修改
statusHandle (row)
this.id = row.id
this.status = row.status
this.$confirm(\'确认调整该账号的状态?\', \'提示\',
\'confirmButtonText\': \'确定\',
\'cancelButtonText\': \'取消\',
\'type\': \'warning\'
).then(() =>
//enableOrDisableEmployee封装到了member.js中
enableOrDisableEmployee( \'id\': this.id, \'status\': !this.status ? 1 : 0 ).then(res =>
console.log(\'enableOrDisableEmployee\',res)
if (String(res.code) === \'1\')
this.$message.success(\'账号状态更改成功!\')
this.handleQuery()
).catch(err =>
this.$message.error(\'请求出错了:\' + err)
)
)
,
member.js
// 修改---启用禁用接口
// 与 修改---添加员工 使用的是一个方法:所以路径是一样的,
function enableOrDisableEmployee (params)
return $axios(
url: \'/employee\',
method: \'put\',
data: ...params
)
已经实现了只有管理员才能看到 启用/禁用 按钮
<el-button
type="text"
size="small"
class="delBut non"
@click="statusHandle(scope.row)"
v-if="user === \'admin\'"
>
scope.row.status == \'1\' ? \'禁用\' : \'启用\'
</el-button>
3. 代码实现
本质上就是一个更新操作,也就是对status状态字段进行操作在Controller中创建update方法,此方法是一个通用的修改员工信息的方法【该方法可以与编辑员工信息通用,都是更新操作】
/**
* 根据id修改员工信息,如禁用,启用
* 这是一个通用的方法,在修改员工信息的时候,可以直接用,
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request, @RequestBody Employee employee)
//因为返回值只要一个res.code,所以R<String>
log.info(employee.toString());
Long id = (Long) request.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(id);//对当前登录用户进行修改
employeeService.updateById(employee);
return R.success("修改成功");
4. 代码修复
数据丢失问题
id从分页列表中取出来,页面返回数据没有问题;
但是点禁用按钮的时候,发送给我们的id就变化了【js对数据处理的时候会丢失精度,只能保证前16位,使得提交的id与数据库中的id不一致】
解决:在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串
1)提供对象转换器]acksonObjectMapper,基于Jackson进行Java对象到json数据的转换 (资料中已经提供,直接复制到项目中使用)
2)在WebMvcConfia配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行lava对象到
json数据的转换
对象转换器:
common/JacksonObjectMapper.java
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper()
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(BigInteger.class, ToStringSerializer.instance)
//Long序列化器
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
配置类中扩展消息转换器:
config/WebMvcConfig.java
........
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters)
log.info("扩展消息转换器...");
//创建消息转换器对象,webmvc包里提供的【将controller返回结果转为相应的json数据,输出流的方式响应给页面】
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
.......
编辑员工信息
1. 需求
在员工管理列表页面点击编辑按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击保存按钮完成编辑操作
2. 代码逻辑
1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
【注意: add.html页面为公共页面,新增员工和编辑员工都是在此页面操作】
2、在add.html页面获取url中的参数[员工id]
3、发送ajax请求【一次请求】,请求服务端,同时提交员工id参数
4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
6、点击保存按钮,发送ajax请求【两次请求】,将页面中的员工信息以json方式提交给服务端
7、服务端接收员工信息,并进行处理,完成后给页面响应【R.success】
8、页面接收到服务端响应信息后进行相应处理【提示修改成功】
前端分析:【页面跳转到add.html,并在url中携带参数[员工id]】
member/add.html
created()
this.id = requestUrlParam(\'id\')//requestUrlParam封装到了index.js中
this.actionType = this.id ? \'edit\' : \'add\'
if (this.id)
this.init()
,
mounted()
,
methods:
async init ()
queryEmployeeById(this.id).then(res =>
console.log(res)
if (String(res.code) === \'1\')
console.log(res.data)
this.ruleForm = res.data
this.ruleForm.sex = res.data.sex === \'0\' ? \'女\' : \'男\'
// this.ruleForm.password = \'\'
else
this.$message.error(res.msg || \'操作失败\')
)
,
获取url中id信息的方法【this.id = requestUrlParam(\'id\')】
js/index.js
//获取url地址上面的参数
function requestUrlParam(argname)
var url = location.href //获取完整的请求url路径
var arrStr = url.substring(url.indexOf("?")+1).split("&")
for(var i =0;i<arrStr.length;i++)
var loc = arrStr[i].indexOf(argname+"=")
if(loc!=-1)
return arrStr[i].replace(argname+"=","").replace("?","")
return ""
发送ajax请求:queryEmployeeById(this.id)
api/member.js
// 修改页面反查详情接口
function queryEmployeeById (id)
return $axios(
url: `/employee/$id`,
method: \'get\'
)
3. 代码实现【创建方法处理请求】
使用路径变量@PathVariable("id") Long id
url地址栏方式的请求:@GetMapping("/id")
回显数据:【第一次请求】
/**
* 根据id查询员工信息
*
* @param id
* @return
*/
@GetMapping("/id")
public R<Employee> getById(@PathVariable("id") Long id) //@PathVariable路径变量
log.info("根据id查询员工信息...");
Employee employee = employeeService.getById(id);
if (employee != null)
return R.success(employee);
return R.error("没有查询到对应员工信息");
保存数据:【第二次请求】
与启用/禁用使用的是同一个update方法;@PutMapping
问题完善:公共字段自动填充
1. 问题
在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,
在编辑员工时需要设置修改时间和修改人等字段。
这些字段属于公共字段,也就是很多表中都有这些字段,
2.解决方法:Mybatis plus提供的公共字段自动填充功能
在插入或者更新的时候为指定字段赋予指定的值,
好处:统一对这些字段进行处理,避免了重复代码
实现步骤:
1、在实体类的属性上加入@TableField注解,指定自动填充的策略
【默认不处理DEFAULT、插入时填充字段INSERI、更新时填充字段UPDATE、插入和更新时填充字段INSERT_UPDATE】
2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetabiectHandler接口
- Employee类:【在公共属性上 加入@TableField注解】
//这些都是公共字段,加入TableField注解,FieldFill.INSERT表示插入时填充字段
//创建时间
@TableField(fill = FieldFill.INSERT)//填充策略:插入时填充字段
private LocalDateTime createTime;
//更新时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
- 元数据对象处理器:【common包】
@Component让spring框架管理它
因为没有request对象,所以使用线程工具类获取当前id【ThreadLocal类】
MyMetaObjecthandler
/**
* 自定义元数据对象处理器
*/
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler
/**
* 插入操作,自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject)
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime",LocalDateTime.now());
//该类中不能获得Session中的对象【因为没有request对象】。所以使用线程工具类
metaObject.setValue("createUser",BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
/**
* 更新操作,自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject)
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
- 修改之前方法:把新增员工、更新方法中的字段注释掉:
// //这些都是公共字段,因为许多表中都有这些字段
//设置了公共字段填充,所以就不需要再写了,
// employee.setCreateTime(LocalDateTime.now());
// employee.setUpdateTime(LocalDateTime.now());
//
// //获得当前登录用户的id
// Long empId = (Long) request.getSession().getAttribute("employee");
//
// employee.setCreateUser(empId);
// employee.setUpdateUser(empId);
ThreadLocal类:
- ThreadLocal类:获取当前线程id
我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,
并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),
然后在MyMetaobjectHandler的updateFil方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值 (用户id)
由于线程相同:
客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
1、LoginCheckFilter的doFilter方法
2、EmployeeController的update方法
3、MyMetaObjectHandler的updateFill方法
可以在上面的三个方法中分别加入下面代码 (获取当前线程id)来证明相同:
long id = Thread. currentThread().getId();
log. info("线程id:",id);
正是由于线程相同,所以可以使用ThreadLocal类:【线程的局部变量】
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
ThreadLocal常用方法:
public void set(T value)设置当前线程的线程局部变量的值
public T get()返回当前线程所对应的线程局部变量的值
- 实现步骤
1、编写BaseContext工具类,基于ThreadLoca[封装的工具类
2、在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
3、在MyMetaobiectHandler的方法中调用BaseContext获取登录用户的id
BaseContext工具类:【common包】
作用范围:某个线程之内;【每次请求都是一个新的线程】
common/BaseContext.java
/**
* 基于ThreadLocal封装的工具类,用户保存和获取当前登录用户id
*/
public class BaseContext
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id)
threadLocal.set(id);
public static Long getCurrentId()
return threadLocal.get();
Set调用:LoginCheckFilter的doFilter方法中调用BaseContext
filter\\LoginCheckFilter.java
//4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null)
log.info("用户已登录,用户id为:",request.getSession().getAttribute("employee"));
//程序走到这里,表示已经登陆了,可以把用户id存到BaseContext,基于ThreadLocal封装的工具类
//这样就可以在公共字段自动填充的方法中得到用户id了。
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);//自己写的BaseContext类
filterChain.doFilter(request,response);
return;
Get调用:MyMetaobiectHandler的方法中调用BaseContext
common\\MyMetaObjecthandler.java
//该类中不能获得Session中的对象。所以使用线程工具类
metaObject.setValue("createUser",BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
05分类管理业务
新增分类
1. 需求分析
后台系统中可以管理分类信息,分别是菜品分类和套餐分类。
添加菜品时需要选择一个菜品分类;添加一个套餐时需要选择一个套餐分类
在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐
2. 数据模型
category表:name[unique]
3. 代码逻辑
需要用到的类和接口基本结构创建好
实体类Category、Mapper接口CategoryMapper、业务层接口CategoryService、业务层实现类CategoryServicelmpl、控制层CategoryController
entity\\Category.java
/**
* 分类
*/
@Data
public class Category implements Serializable
private static final long serialVersionUID = 1L;
private Long id;
//类型 1 菜品分类 2 套餐分类
private Integer type;
//分类名称
private String name;
//顺序
private Integer sort;
//创建时间
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
//更新时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
//创建人
@TableField(fill = FieldFill.INSERT)
private Long createUser;
//修改人
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
mapper\\CategoryMapper.java
@Mapper
public interface CategoryMapper extends BaseMapper<Category>
Service\\CategoryService.java
public interface CategoryService extends IService<Category>
Service\\impl\\CategoryServiceImpl.java
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService
controller\\javaCategoryController.java
/**
* 分类管理
*/
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController
@Autowired
private CategoryService categoryService;
实现步骤:
1、页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
前端分析:
点击确定按钮的时候,执行submitForm方法
category\\list.html
//数据提交
submitForm(st)
const classData = this.classData
const valid = (classData.name === 0 ||classData.name) && (classData.sort === 0 || classData.sort)
if (this.action === \'add\')
if (valid)
const reg = /^\\d+$/
if (reg.test(classData.sort))
addCategory(\'name\': classData.name,\'type\':this.type, sort: classData.sort).then(res =>
console.log(res)
if (res.code === 1)
this.$message.success(\'分类添加成功!\')
if (!st)
this.classData.dialogVisible = false
else
this.classData.name = \'\'
this.classData.sort = \'\'
this.handleQuery()
else
............
其中,addCategory方法来发送请求。
api\\category.js
// 新增接口
const addCategory = (params) =>
return $axios(
url: \'/category\',
method: \'post\',
data: ...params
)
4. 代码实现【controller包CategoryController.java】
返回值类型R<String>:根据前端代码可见,只用到了一个code【res.code】
@RequestBody Category category:json形式的数据
如果unique字段重复,会进入全局异常处理器中
/**
* 新增分类
*
* @param category
* @return
*/
@PostMapping
public R<String> save(@RequestBody Category category)
log.info("category:", category);
categoryService.save(category);
return R.success("新增分类成功");
分类信息分页查询
同员工管理的分页一样,只是操作的表不一样。
1. 代码逻辑
1、页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUl的Table组件展示到页面上
前端分析:
list.html的钩子函数内的方法(getCategoryPage方法)【该方法封装在了category.js里】
2. 代码实现
/**
* 分页查询
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize)
//分页构造器
Page<Category> pageInfo = new Page<>(page, pageSize);
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加排序条件,根据sort进行排序
queryWrapper.orderByAsc(Category::getSort);
//分页查询
categoryService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
删除分类
1. 需求分析
对某个分类进行删除操作。【需要判断是否关联菜品】
需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除
2. 代码逻辑
1、页面发送ajax请求,将参数(id)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service删除数据
3、Service调用Mapper操作数据库
前端分析:
list.html删除按钮绑定了deleteHandle事件,并把id动态的传过去。
category\\list.html
.........
//删除
deleteHandle(id)
this.$confirm(\'此操作将永久删除该文件, 是否继续?\', \'提示\',
\'confirmButtonText\': \'确定\',
\'cancelButtonText\': \'取消\',
\'type\': \'warning\'
).then(() =>
deleCategory(id).then(res =>
if (res.code === 1)
this.$message.success(\'删除成功!\')
this.handleQuery()
else
this.$message.error(res.msg || \'操作失败\')
).catch(err =>
this.$message.error(\'请求出错了:\' + err)
)
)
,
.........
执行deleCategory方法发送ajax请求
api\\category.js
// 删除当前列的接口
const deleCategory = (ids) =>
return $axios(
url: \'/category\',
method: \'delete\',
params: ids
)
3. 代码实现
R<String>:只要返回code就可以【if (res.code === 1)】
Long ids:参数通过url地址?的形式传过来,不用RequestBody注解。
/**
* 根据id删除分类
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(Long ids) //只要返回code就可以,所以类型String
log.info("删除分类,id为:", ids);
//根据id删除
categoryService.removeById(id);
return R.success("分类信息删除成功");
4. 代码完善
需要检查 要删除的分类 是否 关联了菜品或者套餐【所以需要菜品和套餐的相关类:需要使用两个类中的categoryId属性】
要完善分类删除功能,需要先准备基础的类和接口:
1、实体类Dish和Setmeal
2、Mapper接口DishMapper和SetmealMapper
3、Service接口DishService和SetmealService
4、Service实现类DishServicelmpl和SetmealServicelmpl
Dish基础的类和接口:
entity/Dish.java
/**
菜品
*/
@Data
public class Dish implements Serializable
private static final long serialVersionUID = 1L;
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//商品码
private String code;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//顺序
private Integer sort;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
mapper/DishMapper.java
@Mapper
public interface DishMapper extends BaseMapper<Dish>
service/DishService.java
public interface DishService extends IService<Dish>
service/impl/DishServiceImpl.java
@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService
controller/DishController.java
/**
* 菜品管理
*/
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController
@Autowired
private DishService dishService;
Setmeal基础的类和接口:
entity/Setmeal.java
/**
* 套餐
*/
@Data
public class Setmeal implements Serializable
private static final long serialVersionUID = 1L;
private Long id;
//分类id
private Long categoryId;
//套餐名称
private String name;
//套餐价格
private BigDecimal price;
//状态 0:停用 1:启用
private Integer status;
//编码
private String code;
//描述信息
private String description;
//图片
private String image;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
mapper/SetmealMapper.java
@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal>
service/SetmealService.java
public interface SetmealService extends IService<Setmeal>
service/impl/SetmealServiceImpl.java
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService
controller/SetmealController.java
/**
* 套餐管理
*/
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController
@Autowired
private SetmealService setmealService;
@Autowired
private SetmealDishService setmealDishService;
@Autowired
private CategoryService categoryService;
完善代码:【加入判断】
- 在CategoryService中扩展方法
//根据ID删除分类
public void remove(Long ids);
- 在CategoryServiceImpl中实现 扩展的方法
查Dish这张表[category_id]:
mysgl> select count(*) from dish where category id=?
查Setmeal这张表[category_id]:
mysgl> select count(*) from Setmeal where category id=?
查不到的话,需要报异常:自定义相关异常
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
/**
* 根据id删除分类以上是关于项目总结-瑞吉外卖的主要内容,如果未能解决你的问题,请参考以下文章
基于Springboot和MybatisPlus的外卖项目 瑞吉外卖Day4
Java项目瑞吉外卖保姆级学习笔记(改项目名称+改邮件验证码登录+功能补充)
基于Springboot和mybatis的外卖项目瑞吉外卖Day5