项目总结-瑞吉外卖
软件开发基础
分工:
流程:
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、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
- 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删除分类,删除之前需要进行判断
*
* @param ids
*/
@Override
public void remove(Long ids) {
//查询当前分类是否关联了菜品
//添加查询条件,根据分类id进行查询菜品数据
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();//查的是Dish
dishLambdaQueryWrapper.eq(Dish::getCategoryId, ids);//等值查询
int count1 = dishService.count(dishLambdaQueryWrapper);
//如果已经关联,抛出一个业务异常(自定义业务异常)
if (count1 > 0) {
throw new CustomException("当前分类下关联了菜品,不能删除");//已经关联菜品,抛出一个业务异常
}
//查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, ids);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
if (count2 > 0) {
throw new CustomException("当前分类下关联了套餐,不能删除");//已经关联套餐,抛出一个业务异常
}
//正常删除分类
super.removeById(ids);//父类:categoryService
}
- 修改delete方法:
//根据id删除
// categoryService.removeById(id);
//分菜品删除,完善code
categoryService.remove(ids);
- 自定义业务异常类:【common/CustomException.java】
如果没有关联相关菜品/套餐,抛出异常
定义一个通用的业务异常:目的【把异常提示相关信息传进来】
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException {
/**
* 提示的异常信息
* @param message
*/
public CustomException(String message) {
super(message);
}
}
- 在全局异常处理器添加捕获业务异常的方法:【common/GlobalExceptionHandler.java】
/**
* 自定义异常处理方法
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex) {
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
修改分类
1. 需求分析
点击修改按钮,弹出修改窗口。【task:回显信息and修改】
2. 代码逻辑
前端分析:【已实现回显】
修改按钮,执行editHandle方法【输入框与模型数据进行了绑定】
category\list.html
editHandle(dat) {
this.classData.title = '修改分类'
this.action = 'edit'
this.classData.name = dat.name
this.classData.sort = dat.sort
this.classData.id = dat.id
this.classData.dialogVisible = true
},
点击确定按钮,会发送ajxax请求
3. 代码实现
json数据:@RequestBody
其余公共字段可以自动填充
/**
* 根据id修改分类信息
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category) {
log.info("修改分类信息:{}", category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
06菜品管理业务
文件上传下载
文件上传
指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。
前端
文件上传时,对页面的form表单有如下要求:
method="post" 采用post方式提交数据
enctype="multipart/form-data" 采用multipart格式上传文件使用input的file控件上传
type="file" 使用input的file控件上传
举例:
<form method="post" action="/common/upload" enctype="multipart/form-data">
<input name="myFile" type="file"/>
<input type="submit" value="提交"/>
</form>
前端代码的一些组件库提供了相应的上传组件:如ElementUI提供的upload上传组件
服务端
服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:
commons-fileupload
commons-io
Spring框架在spring-web包中对文件上传进行了封装。
需要在Controller的方法中声个MultipartFile类型的参数即可接收上传的文件:
/**
*文件上传
*@param file
*@return
*/
@PostMapping(value = "/upload")
public R<String> upload(MultipartFile file){
System.out.println(file);
return null;
}
- 代码实现
前端:提交表单的时候,发送一次请求
前端:backend/page/demo/upload.html
........
<el-upload class="avatar-uploader"
action="/common/upload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeUpload"
ref="upload">
<img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
........
动态获取保存地址:
@Value("${riggle.path}"):读取到配置文件的内容,需要此注解
动态获取文件名:使用UUID重新生成文件名
controller/CommonController.java
/**
* 文件上传和下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${riggle.path}")//使用配置文件中的路径
private String basePath;
@PostMapping("/upload")
public R<String> upload(MultipartFile file) {//参数名file必须与前端的一致
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());
//获取文件原始名称
String originalFilename = file.getOriginalFilename();
//截取.jpg后缀
String suffix= originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID重新生成文件名,防止文件名重复造成文件覆盖
String fileName = UUID.randomUUID().toString()+suffix;
//创建一个目录对象
File path = new File(basePath);
//判断当前目录是否存在,不存在,就创建一个
if (!path.exists()) {
path.mkdirs();
}
//临时文件存放的路径
try {
file.transferTo(new File(basePath + fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);//页面需要用到图片名称,保存到相关菜品的行中
}
}
灵活修改地址
#application.yml配置文件中加入以下
riggle:
path: D:\img\
文件下载
指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,通常有两种表现形式:
以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
直接在浏览器中打开,本质上服务端将文件以流的形式写回浏览器的过程
前端分析:
backend/page/demo/upload.html
//上传完成后会展示图片:
<img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
.........
methods: {
handleAvatarSuccess (response, file, fileList) {
this.imageUrl = `/common/download?name=${response.data}`
},
......
服务端(后端):处理请求
String name:前端请求提交过来了一个参数,后端需要接收到
HttpServletResponse response:输出流需要response获得,因为向浏览器响应
controller/CommonController.java
/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response) throws Exception {
//输入流,读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
//输出流,将文件写入到浏览器
ServletOutputStream outputStream = response.getOutputStream();
//设置响应方式
response.setContentType("image/jpeg");//"image/jpeg"代表图片文件
//通过输入流,来读取一行一行
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
outputStream.flush();
}
//关闭资源
outputStream.close();
fileInputStream.close();
}
新增菜品
1. 需求
通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息。
2. 数据模型
将新增页面录入的菜品信息插入到dish表。如果添加了口味做法,还需要向dish flavor表插入数据.所以在新增菜品时,涉及到两个表:
dish 菜品表
dish flavor 菜品口味表
3. 代码逻辑
需要的类和接口基本结构:
之前已经创建了Dish相关类和接口;现需要创建DishFlavor相关的
实体类:entity/DishFlavor.java
/**
菜品口味
*/
@Data
public class DishFlavor implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//菜品id
private Long dishId;
//口味名称
private String name;
//口味数据list
private String value;
@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;
//是否删除
private Integer isDeleted;
}
Mapper接口:mapper/DishMapper.java
@Mapper
public interface DishMapper extends BaseMapper<Dish> {
}
业务层接口:service/DishFlavorService.java
public interface DishFlavorService extends IService<DishFlavor> {
}
业务层实现类:service/impl/DishFlavorServiceImpl.java
@Service
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper,DishFlavor> implements DishFlavorService {
}
控制层:controller/DishController.java
/**
* 菜品管理
*/
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private DishFlavorService dishFlavorService;
@Autowired
private CategoryService categoryService;
}
交互过程
开发新增菜品功能,在服务端编写代码去处理前端页面发送的4次请求
1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
2、页面发送请求进行图片上传,请求服务端将图片保存到服务器
3、页面发送请求进行图片下载,将上传的图片进行回显
4、点击保存按钮,发送ajax请求,将菜品相关数据以ison形式提交到服务端
前端分析
第一次请求:food\add.html钩子函数(getDishList方法)-->getDishList方法(getCategoryList方法[封装到了js文件中]-->api\food.js(发送ajax请求))【需要的响应,this.dishLish=res.data,所以方法返回值R<List
4. 代码实现
第一次请求的响应方法:
controller/CategoryController.java
接收参数:String type\Category category[会对应到实体中的type属性]
/**
* 在菜品管理页面中,筛选出菜品分类的数据
* 根据条件查询菜品分类数据
* @param category
* @return
*/
@GetMapping("/list")
public R<List<Category>> list(Category category) {
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加条件
queryWrapper.eq(category.getType() != null, Category::getType, category.getType());
//添加排序条件;根据sort进行排序
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}
第二次和第三次请求
文件上传下载已写好。
第四次请求
会操作两张表:新增的dish和dish flavor
接收的数据是包括两张表的数据:创建一个新的类,接收所有请求参数
dto/DishDto.java
@Data
public class DishDto extends Dish {
/**
* 继承了Dish,又扩展了一些属性
*/
private List<DishFlavor> flavors = new ArrayList<>();//接收传过来的name、value
private String categoryName;
private Integer copies;
}
操作两个表:在service里扩展方法、实现方法;
多张表操作:方法上加入事务控制@Transactional;并且在启动类上开始事务支持@EnableTransactionManagement
service/DishService.java
/**
* 新增菜品,同时插入口味数据,需要操作两个表
* @param dishDto
*/
void saveWithFlavor(DishDto dishDto);
service/impl/DishServiceImpl.java
@Autowired
private DishFlavorService dishFlavorService;//注入之后才能操作另外一个表
/**
* 新增菜品,同时保存对应的口味数据
* @param dishDto
*/
@Override
@Transactional
//由于关于两张表的处理,加入事务处理
//使得事务注解生效。需要在启动类中开启事务
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到菜品表dish
this.save(dishDto);//因为继承了dish
//但是DishFlavor表中,还有dishid这个,如果之间批量保存,就会缺少dishid的值[因为Flavors没有给赋值]
//dishFlavorService.saveBatch(dishDto.getFlavors());
//得到菜品id,disId也就是口味id
Long dishId = dishDto.getId();
//保存菜品口味数据到菜品口味表:dish_flavor
//dishDto中有DishFlavor类型的变量,就是保存list到DishFlavor表中
List<DishFlavor> flavors = dishDto.getFlavors();
//处理数据:使用foreach循环 or stream流的方式
//stream流的方式
flavors = flavors.stream().map(item -> {//lamda表达式
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
controller/DishController.java
/**
* 新增菜品
* @param dishDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody DishDto dishDto) {//因为数据为json数据,@RequestBody注解一定要加,不然不能封装上
log.info(dishDto.toString());
//因为要操作两张表
//在dishservice里面要扩展方法;使用扩展的方法
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
菜品信息分页查询
1. 需求
展示信息以及图片、分类【需要查询分类表获取分类名称】
2. 代码逻辑
交互过程
在服务端编写代码去处理前端页面发送的这2次请求
1、页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
3. 代码实现
controller/DishController.java
直接操作Dish:页面没有categoryName的显示;【因为Dish返回值里没有此字段,不能与前端字段相匹配】
所以要操作DishDto:继承Dish并且里面有扩展属性:private String categoryName;【得到Dish pageInfo后对象拷贝到DishDto pageInfo:copy除了records之外的属性:因为需要带有分类名称的records】
注入CategoryService【得到分类名称】
/**
* 菜品信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
//构造分页构造器
Page<Dish> pageInfo = new Page<>(page,pageSize);
Page<DishDto> dishDtoPage = new Page<>(page,pageSize);
//条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(name != null,Dish::getName,name);
//形如Class::methodname,符号左边是调用方法所处的类名,符号右边是调用的静态方法。简单的说,就是逐一传入参数值到某个类的静态方法并调用该静态方法。
//添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);
//执行分页查询
dishService.page(pageInfo,queryWrapper);
//需要呈现菜品分类的名称,所以使用Dto类,因为dishdto继承了dish属性,还有附属属性
//进行分页对象copy
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");//copy除了records之外的属性
//使得List<Dish>变为List<DishDto>类型
List<Dish> records = pageInfo.getRecords();
List<DishDto> list = records.stream().map((item)->{
DishDto dishDto = new DishDto();
//对象copy
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类名称
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//最后dishDto对象里面,除了有分类名称,也有dish的基础属性
return dishDto;
}).collect(Collectors.toList());
//把dishdto分页类中的records补充完整
dishDtoPage.setRecords(list);
return R.success(dishDtoPage);
}
修改菜品
1. 需求
在修改页面回显菜品相关信息并进行修改,最后点保存
2. 代码逻辑
交互过程(add.html):4次请求
1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示【新增功能的时候已完成】
2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
3、页面发送get请求,请求服务端进行图片下载,用于页图片回显
4、点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以ison形式提交到服务端
3. 代码实现
第一次【获取分类】和第三次请求【文件下载】已经写好
第二次请求【回显】:
修改数据的时候,需要回显数据、涉及口味等属性;只有一个dish类是不可以的,所以使用dishDto类
数据在请求url中:需要注解@PathVariable接收
【controller/DishController.java】
/**
* 根据id查询菜品信息和对应的口味信息:回显数据的请求
* 修改数据的时候,需要回显数据、涉及口味等属性;只有一个dish类是不可以的,所以使用dishDto类
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id) {
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
由于查询需要查询到两表的操作:需要在DishService中扩展方法
controller/DishController.java
//根据id查询菜品信息和对应的口味信息
DishDto getByIdWithFlavor(Long id);
实现方法:先查询菜品信息;后查询口味信息
service/impl/DishServiceImpl.java
/**
* 扩展方法:根据id查询菜品信息和对应的口味信息
* 涉及两个表,
* @param id
* @return
*/
@Override
public DishDto getByIdWithFlavor(Long id) {
//查询菜品基本信息,从dish表查询
Dish dish = this.getById(id);
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish, dishDto);
//查询当前菜品对应的口味信息,从dish_flavor表查询
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId, dish.getId());
List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(flavors);
return dishDto;
}
第四次请求【保存】:
更新:两张表,不单单更新dish。还要更新flavor
/**
* 修改菜品(与新增菜品差不多)
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto) {
log.info(dishDto.toString());
//dishService.saveWithFlavor(dishDto);
//不单单更新dish。还要更新flavor;两个表的操作,扩展方法
dishService.updateWithFlavor(dishDto);
return R.success("修改菜品成功");
}
两表的操作:需要在DishService中扩展方法
service/DishService.java
void updateWithFlavor(DishDto dishDto);
实现方法:先更新dish菜品表;后更新dish_flavor口味表
加入事务管理注解@Transactional
service/impl/DishServiceImpl.java
/**
* 修改菜品扩展方法
* @param dishDto
*/
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
//更新dish表
this.updateById(dishDto);
//修改口味表,先删除口味信息【delete】
LambdaQueryWrapper<DishFlavor> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DishFlavor::getDishId, dishDto.getId());
dishFlavorService.remove(wrapper);
//获取当前提交过来的口味数据
List<DishFlavor> flavors = dishDto.getFlavors();
//不能直接批量保存,因为dishflavor中还有别的属性。dishId并没有封装上
flavors.stream().map(item -> {
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
//insert操作
dishFlavorService.saveBatch(flavors);
}
起售/停售
1. 需求
2. 数据模型
3. 代码逻辑
4. 代码实现
删除菜品
1. 需求
2. 数据模型
3. 代码逻辑
4. 代码实现
07套餐管理业务
新增套餐
1. 需求
在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐
2. 数据模型
将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal dish表插入套餐和菜品关联数据。
setmeal套餐表
setmeal_dish套餐菜品关系表
3. 代码逻辑
创建需要的类和接口
SetMeal相关的类和接口在分类管理的时候已经创建;
现只需SetmealDish【主表:SetMeal】
实体类:SetmealDish
/**
* 套餐菜品关系
*/
@Data
public class SetmealDish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//套餐id
private Long setmealId;
//菜品id
private Long dishId;
//菜品名称 (冗余字段)
private String name;
//菜品原价
private BigDecimal price;
//份数
private Integer copies;
//排序
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;
//是否删除
private Integer isDeleted;
}
}
Mapper接口:SetmealDishMapper
@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}
业务层接口:SetmealDishService
public interface SetmealDishService extends IService<SetmealDish> {
}
业务层实现类:SetmealDishServicelmpl
@Service
@Slf4j
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishService {
}
控制层:SetmealController
/**
* 套餐管理
*/
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
@Autowired
private SetmealDishService setmealDishService;
}
交互过程
6次请求
1、页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中【菜品管理业务的时候完成了该请求的响应】
2、页面发送aiax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
3、页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
4、页面发送请求进行图片上传,请求服务端将图片保存到服务器
5、页面发送请求进行图片下载,将上传的图片进行回显
6、点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
4. 代码实现
第三次请求【查询对应的菜品数据】
controller/DishController.java
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<Dish>> list(Dish dish) {
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus, 1);
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}
第六次请求【保存】
SetmealDto类型接收数据:提交的数据除了setmeal信息还有setmeal_dish信息[当前套餐对应哪些菜品]
/**
* 添加套餐
* @param setmealDto
* @return
*/
@PostMapping
@CacheEvict(value = "setmealCache",allEntries = true)
public R<String> saveWithDish(@RequestBody SetmealDto setmealDto) {//由于提交的是json数据。加注解@RequestBody
log.info("setmealDto:{}", setmealDto);
setmealService.saveWithDish(setmealDto);
return R.success("添加套餐成功");
}
SetmealDto:为了操作setmeal和setmealdish两个表
dto/SetmealDto.java
/**
* setmealdto:为了操作setmeal和setmealdish两个表
*/
@Data
public class SetmealDto extends Setmeal {
private List<SetmealDish> setmealDishes;//套餐关联的菜品集合
private String categoryName;//分类名称
}
两表的操作:需要在DishService中扩展方法
service/SetmealService.java
public void saveWithDish(SetmealDto setmealDto);
实现方法:先新增setmeal表;后新增setmeal_dish表
加入事务管理注解@Transactional
service/impl/SetmealServiceImpl.java
@Autowired
private SetmealDishService setmealDishService;
/**
* 将套餐的基本信息以及关联的菜品信息一起保存
* @param setmealDto
*/
@Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的基本信息,操作setmeal,执行insert
this.save(setmealDto);
//获取套餐和菜品的关联信息
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();//setmealDto中有这个集合
//保存之前,处理数据:因为缺少SetmealDishes表中的setmealId。只存了dishId,对每个数据需要添加setmealId
List<SetmealDish> setmealDishList = setmealDishes.stream().map(item -> {
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
//操作setmeal_dish,执行insert;关联关系可能有多条,批量保存
setmealDishService.saveBatch(setmealDishes);
}
套餐信息分页查询
1. 代码逻辑
交互过程
2次请求
1、页面(backend/page/combo/list.html)发送ajax请求,将分页查询参数(page、pageSize.name)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
2. 代码实现
如果只操作Setmeal,页面的套餐分类不会展示出来【Setmeal里只有CategoryId没有name】
注入CategoryService:
@Autowired
private CategoryService categoryService;
/**
* 套餐分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> querySetmealDish(int page, int pageSize, String name) {
//分页构造器对象
Page<Setmeal> pageInfo = new Page<>(page, pageSize);
Page<SetmealDto> setmealDtoPage = new Page<>();
//添加查询条件
LambdaQueryWrapper<Setmeal> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotEmpty(name), Setmeal::getName, name);
wrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo, wrapper);//pageInfo已经有结果了
//进行对象的copy【除了records,泛型不一样】
List<Setmeal> records = pageInfo.getRecords();//需要的setmeal的list
BeanUtils.copyProperties(pageInfo, setmealDtoPage, "records");
List<SetmealDto> setmealDtoList = records.stream().map(item -> {
SetmealDto setmealDto = new SetmealDto();
//对象copy
BeanUtils.copyProperties(item,setmealDto);
//分类id
Long categoryId = item.getCategoryId();
//根据id获取分类对象
Category category = categoryService.getById(categoryId);
if (category != null) {
//得到分类名称
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;//最终需要list集合,集合中是setmealDto类型的元素
}).collect(Collectors.toList());
//把list集合给进去
setmealDtoPage.setRecords(setmealDtoList);
return R.success(setmealDtoPage);
}
删除套餐
1. 需求
点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。
注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。
2. 代码逻辑
交互过程
2次请求【使用同一个方法:请求地址和方式都是一样的,只是传递id的个数不同】
1、删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐
2、删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐
3. 代码实现
controller/SetmealController.java
除了删除setmeal表中的数据,还要删除setmeal_dish表中相关的数据
两个表的操作:扩展方法
/**
* 删除套餐:
* 批量删除和删除单个,请求是一样的,就是id个数不同
* 所以使用一个方法就ok
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids) {
log.info("ids:{}", ids);
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");
}
扩展service方法:删除套餐的同时,把相关联的菜品关系数据也删除掉
service/SetmealService.java
public void removeWithDish(List<Long> ids);
实现扩展的方法
加入事务管理@Transactional
service/impl/SetmealServiceImpl.java
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
* 两个表的操作:在实现类中扩展方法
* @param ids
*/
@Override
@Transactional
public void removeWithDish(List<Long> ids) {
//查询套餐状态,确定是否可用删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
queryWrapper.in(Setmeal::getId, ids);//select count(*) from setmeal where id in(1,2,3) and status=1
queryWrapper.eq(Setmeal::getStatus, 1);
int count = this.count(queryWrapper);//ServieceImpl中有count()方法,框架中的方法
if (count > 0) {
//如果不能删除,抛出一个业务异常
throw new CustomException("套餐正在售卖中,不能删除");
}
//如果可以删除,先删除套餐表中的数据---setmeal
this.removeByIds(ids);
//删除关系表中的数据----setmeal_dish;注入setmealDishService来操作setmeal_dish表
//delect from setmeal_dish where setmeal_id in(...)
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(SetmealDish::getSetmealId, ids);//ids对应mealdish关系表中的SetmealId
//不能调setmealDishService.removeById(ids)方法, ids是套餐的id,并不是mealdish关系表中的主键值
setmealDishService.remove(lambdaQueryWrapper);
}
起售/停售
1. 需求
2. 数据模型
3. 代码逻辑
4. 代码实现
批量起售/停售
1. 需求
2. 数据模型
3. 代码逻辑
4. 代码实现
08移动端
手机验证码登录
1. 需求
短信发送、验证码登录
2. 短信发送
短信服务:
第三方短信服务会和各个运营商(移动、联通、电信)对接,我们只需要注册成为会员并且按照提供的开发文档进行调用就可以发送短信。
常用短信服务:阿里云、华为云、腾讯云、京东、梦网、乐信
阿里云短信服务:
注册账号-->短信服务-->设置短信签名-->设置AccessKey(子用户)
实现:
1、导入maven坐标
2、调用API
pom.xml加入坐标依赖
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>
utils\SMSUtils.java短信发送工具类
/**
* 短信发送工具类
*/
public class SMSUtils {
/**
* 发送验证码短信
* signName 签名
* @param templateCode模板
* @paramphoneNumbers手机号
* @param param参数
*/
public static void sendMessage(String signName, String templateCode, String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam(":\"code\":\""+param+"\"}");
try{
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e){
e.printStackTrace();
}
}
}
utils/ValidateCodeUtils.java随机生成验证码工具类
/**
* 随机生成验证码工具类
*/
public class ValidateCodeUtils {
/**
* 随机生成验证码
* @param length 长度为4位或者6位
* @return
*/
public static Integer generateValidateCode(int length){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}
/**
* 随机生成指定长度字符串验证码
* @param length 长度
* @return
*/
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}
3. 数据模型
user表
4. 代码逻辑
交互过程
2次请求
1、在登录页面(front/page/login.html)输入手机号,点击[获取验证码] 按钮,页面发送ajax请求,在服务端调用短信服务API给指定手机号发送验证码短信【http://localhost:8080/user/sendMsg;post】
2、在登录页面输入验证码,点击[登录] 按钮,发送ajax请求,在服务端处理登录请求
创建基础类和接口
entity/User.java
/**
* 用户信息
*/
@Data
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//姓名
private String name;
//手机号
private String phone;
//性别 0 女 1 男
private String sex;
//身份证号
private String idNumber;
//头像
private String avatar;
//状态 0:禁用,1:正常
private Integer status;
}
mapper/UserMapper.java
@Mapper
public interface UserMapper extends BaseMapper<User>{
}
service/UserService.java
public interface UserService extends IService<User> {
}
service/impl
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService{
}
controller/UserController.java
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
}
修改过滤器:LoginCheckFilter
String[]增加不需要处理的请求路径
"/user/sendMsg",//移动端发送短信
"/user/login",//移动端登录
增加“判断移动端是否登录”
/*
* 移动端用户
*/
if (request.getSession().getAttribute("user") != null) {
log.info("用户已登录,用户id为:{}", request.getSession().getAttribute("user"));
Long userId = (Long) request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);
filterChain.doFilter(request, response);
return;
}
5. 代码实现
第一次请求【发送验证码】
/**
* 发送手机短信验证码
*
* @param user
* @return
*/
@PostMapping("/sendMsg")
public R<String> sendPhone(@RequestBody User user, HttpSession session) {
//获取手机号
String phone = user.getPhone();
if (StringUtils.isNotEmpty(phone)) {
//生成随机的4位验证码
String code = ValidateCodeUtils.generateValidateCode(6).toString();
log.info("验证码code:"+code);
System.out.println("验证码code:" + code);
//调用阿里云提供的短信服务API完成发送短信
//SMSUtils. sendMessage("瑞吉外卖", "",phone,code) ;
//需要将生成的验证码保存到Session
session.setAttribute(phone, code);
return R.success("发送短信成功");
}
return R.error("发送短信失败");
}
第二个请求【登录】
不能使用user接收数据,因为类中没有code属性:可以使用userdto扩展属性or map
使用user表查询:注入userService
返回值User类:需要给页面返回当前用户信息
登录成功后,需要把userid存入session中,这样过滤器才能允许通过
/**
* 用户登录
* @param map
* @param session
* @return
*/
@PostMapping("/login")
public R<User> login(@RequestBody Map map, HttpSession session) {//可以使用dto形式扩展user属性,也可以使用map
// 获取用户信息
String phone = map.get("phone").toString();
String code = map.get("code").toString();
//从Session中获取保存的验证码
Object codeInSession = session.getAttribute(phone);
//对比验证码
if (codeInSession != null && codeInSession.equals(code)) {
//登录成功,判断是否为新用户
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getPhone, phone);
User user = userService.getOne(wrapper);
if (user == null) {
// 新用户
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
//这个不可以缺少,过滤器需要检查seeion中的值,检查是否放行
session.setAttribute("user", user.getId());
return R.success(user);
}
return R.error("登录失败");
}
用户地址簿
1. 需求
用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址【新增、设为默认地址、修改地址】
2. 数据模型
address_book表
3. 代码逻辑
基础类和接口
entity/AddressBook.java
/**
* 地址簿
*/
@Data
public class AddressBook implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//用户id
private Long userId;
//收货人
private String consignee;
//手机号
private String phone;
//性别 0 女 1 男
private String sex;
//省级区划编号
private String provinceCode;
//省级名称
private String provinceName;
//市级区划编号
private String cityCode;
//市级名称
private String cityName;
//区级区划编号
private String districtCode;
//区级名称
private String districtName;
//详细地址
private String detail;
//标签
private String label;
//是否默认 0 否 1是
private Integer isDefault;
//创建时间
@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;
//是否删除
private Integer isDeleted;
}
mapper/AddressBookMapper.java
@Mapper
public interface AddressBookMapper extends BaseMapper<AddressBook> {
}
service/AddressBookService.java
public interface AddressBookService extends IService<AddressBook> {
}
service/impl/AddressBookServiceImpl.java
@Service
public class AddressBookServiceImpl extends ServiceImpl<AddressBookMapper, AddressBook> implements AddressBookService {
}
controller/AddressBookController.java
/**
* 地址簿管理
*/
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {
@Autowired
private AddressBookService addressBookService;
}
4. 代码实现
新增地址【保存地址按钮】
/**
* 新增
*/
@PostMapping
public R<AddressBook> save(@RequestBody AddressBook addressBook) {
//获取用户id作为收获地址的标识
addressBook.setUserId(BaseContext.getCurrentId());
log.info("addressBook:{}", addressBook);
addressBookService.save(addressBook);
return R.success(addressBook);
}
设置默认地址【按钮】
is_default字段
/**
* 设置默认地址
*/
@PutMapping("default")
public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
log.info("addressBook:{}", addressBook);
LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
//把所有的Default属性都改成0
wrapper.set(AddressBook::getIsDefault, 0);
//SQL:update address_book set is_default = 0 where user_id = ?
addressBookService.update(wrapper);
addressBook.setIsDefault(1);//当前需要的地址信息设为默认1
//SQL:update address_book set is_default = 1 where id = ?
addressBookService.updateById(addressBook);//单独执行update
return R.success(addressBook);
}
根据id【地址id不是用户id】查询地址
/**
* 根据id查询地址
*/
@GetMapping("/{id}")
public R<AddressBook> get(@PathVariable Long id) {
AddressBook addressBook = addressBookService.getById(id);
if (addressBook != null) {
return R.success(addressBook);
} else {
return R.error("没有找到该对象");
}
}
查询默认地址
/**
* 查询默认地址
*/
@GetMapping("default")
public R<AddressBook> getDefault() {
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
queryWrapper.eq(AddressBook::getIsDefault, 1);
//SQL:select * from address_book where user_id = ? and is_default = 1
AddressBook addressBook = addressBookService.getOne(queryWrapper);
if (null == addressBook) {
return R.error("没有找到该对象");
} else {
return R.success(addressBook);
}
}
查询指定用户的全部地址【地址管理页面】
/**
* 查询指定用户的全部地址
*/
@GetMapping("/list")
public R<List<AddressBook>> list(AddressBook addressBook) {
addressBook.setUserId(BaseContext.getCurrentId());
log.info("addressBook:{}", addressBook);
//条件构造器
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(null != addressBook.getUserId(), AddressBook::getUserId, addressBook.getUserId());
queryWrapper.orderByDesc(AddressBook::getUpdateTime);
List<AddressBook> addressBookList = addressBookService.list(queryWrapper);
//SQL:select * from address_book where user_id = ? order by update_time desc
return R.success(addressBookList);
}
移动端菜品展示
1. 需求
在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示[选择规格]按钮,否则显示[+]按钮
2. 代码逻辑
交互过程
2次请求
1、页面(front/index.html)发送ajax请求,获取分类数据 (菜品分类和套餐分类:侧边栏)【之前开发中已经写好了】
2、页面发送ajax请求,获取第一个分类下的菜品或者套餐【默认查询的菜品展示】【dish中list开发也写过了,但是之前写的只是dishlist,没有口味信息】
注意:首页加载完成后,还发送了一次ajax请求用于加载购物车数据,此处可以将这次请求的地址暂时修改一下,从静态ison文件获取数据,等后续开发购物车功能时再修改回来;
4. 代码实现
改造DishController中list方法;【类似于category分页,但是需要的是flavors数据】
不会影响后台方法的调用,只是追加了数据
为了移动端页面可以选择口味,返回值变为DishDto带有口味的属性
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish) {
//为了移动端页面可以选择口味,返回值变为DishDto带有口味的属性
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus, 1);
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
List<DishDto> dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item, dishDto);
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if (category != null) {
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//当前菜品的id
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(DishFlavor::getDishId, dishId);
//SQL: select * from dish_flavor where dish_id = ?
//查出来口味的集合
List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
return R.success(dishDtoList);
}
setmealController中添加list方法
/**
* 根据条件查询套餐数据: 移动端页面显示
* @param setmeal
* @return
*/
@GetMapping("/list")
public R<List<Setmeal>> list(Setmeal setmeal) {//url方式传进来的不需要加@RequestBody
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(setmeal.getCategoryId() != null, Setmeal::getCategoryId, setmeal.getCategoryId());
queryWrapper.eq(setmeal.getStatus() != null, Setmeal::getStatus, setmeal.getStatus());
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> list = setmealService.list(queryWrapper);
return R.success(list);
}
购物车
1. 需求
对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车:
对于套餐来说,可以直接点击[+]将当前套餐加入购物车。
在购物车中可以修改菜品和套餐的数量也可以清空购物车。
2. 数据模型
shopping_cart表
3. 代码逻辑
交互过程
三次请求
1、点击[加入购物车]或者[+]按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车
菜品数据:dishId
套餐数据:setmealId
2、点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
http://localhost:8080/shoppingCart/list [get]
3、点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作
构建shopping_cart基础类和接口
entity/ShoppingCart.java
/**
* 购物车
*/
@Data
public class ShoppingCart implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//名称
private String name;
//用户id
private Long userId;
//菜品id
private Long dishId;
//套餐id
private Long setmealId;
//口味
private String dishFlavor;
//数量
private Integer number;
//金额
private BigDecimal amount;
//图片
private String image;
private LocalDateTime createTime;
}
mapper/ShoppingCartMapper.java
@Mapper
public interface ShoppingCartMapper extends BaseMapper<ShoppingCart> {
}
service/ShoppingCartService.java
public interface ShoppingCartService extends IService<ShoppingCart> {
}
service/impl/ShoppingCartServiceImpl.java
@Service
public class ShoppingCartServiceImpl extends ServiceImpl<ShoppingCartMapper, ShoppingCart> implements ShoppingCartService {
}
controller/ShoppingCartController.java
/**
* 购物车
*/
@Slf4j
@RestController
@RequestMapping("/shoppingCart")
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
}
4. 代码实现
第一次请求【加入购物车】
/**
* 添加购物车数据
* @param shoppingCart
* @return
*/
@PostMapping("add")
public R<ShoppingCart> addShort(@RequestBody ShoppingCart shoppingCart) {
log.info("购物车数据:{}",shoppingCart);
// 获取用户id(通过session已经存起来了,可以通过session获得/ 通过basecontext也能获得)
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);
//查询购物车数据是否存在
Long dishId = shoppingCart.getDishId();
LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
//需要联合查询:userid和菜品id/套餐id
//先在外面封装一下,userid的等值查询
wrapper.eq(ShoppingCart::getUserId, userId);
if (dishId != null) {
wrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());
} else {
wrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
//查询当前菜品或者套餐是否在购物车中
ShoppingCart shop = shoppingCartService.getOne(wrapper);//只可能有一个数据
if (shop != null) {
//存在,数量加一; 更新操作
Integer number = shop.getNumber();
shop.setNumber(number + 1);
shoppingCartService.updateById(shop);
} else {
//不存在, 添加数据保存到数据库 添加操作
shoppingCart.setNumber(1);
shoppingCartService.save(shoppingCart);
shoppingCart.setCreateTime(LocalDateTime.now());
shop = shoppingCart;
}
return R.success(shop);
}
第二次请求【查看购物车】
/**
* 查看购物车:根据userid查询
* @return
*/
@GetMapping("/list")
public R<List<ShoppingCart>> list() {
log.info("查看购物车...");
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
queryWrapper.orderByAsc(ShoppingCart::getCreateTime);//升序
List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
return R.success(list);
}
第三次请求【清空购物车】
/**
* 清空购物车
* @return
*/
@DeleteMapping("/clean")
public R<String> clean(){
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
shoppingCartService.remove(queryWrapper);
return R.success("清空购物车成功");
}
用户下单
1. 需求
点击购物车中的[结算]按钮,页面跳转到订单确认页面,点击[支付]按钮则完成下单操作
2. 数据模型
orders订单表、order_detail订单明细表
number订单号
name:菜品/套餐名称
3. 代码逻辑
交互过程
1、在购物车中点击[结算]按钮,页面跳转到订单确认页面【只是一个简单的页面跳转】
2、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址【前面已写】
3、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据【前面已写】
4、在订单确认页面点击[支付]按钮,发送ajax请求,请求服务端完成下单操作
创建orders、order_detail基础类和接口
orders
entity/Orders.java
/**
* 订单
*/
@Data
public class Orders implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//订单号
private String number;
//订单状态 1待付款,2待派送,3已派送,4已完成,5已取消
private Integer status;
//下单用户id
private Long userId;
//地址id
private Long addressBookId;
//下单时间
private LocalDateTime orderTime;
//结账时间
private LocalDateTime checkoutTime;
//支付方式 1微信,2支付宝
private Integer payMethod;
//实收金额
private BigDecimal amount;
//备注
private String remark;
//用户名
private String userName;
//手机号
private String phone;
//地址
private String address;
//收货人
private String consignee;
}
mapper/OrderMapper.java
@Mapper
public interface OrderMapper extends BaseMapper<Orders> {
}
service/OrderService.java
public interface OrderService extends IService<Orders> {
}
service/impl/OrderServiceImpl.java
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {
}
controller/OrderController.java
/**
* 订单
*/
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
}
order_detail
entity/OrderDetail.java
/**
* 订单明细
*/
@Data
public class OrderDetail implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//名称
private String name;
//订单id
private Long orderId;
//菜品id
private Long dishId;
//套餐id
private Long setmealId;
//口味
private String dishFlavor;
//数量
private Integer number;
//金额
private BigDecimal amount;
//图片
private String image;
}
mapper/OrderDetailMapper.java
@Mapper
public interface OrderDetailMapper extends BaseMapper<OrderDetail> {
}
service/OrderDetailService.java
public interface OrderDetailService extends IService<OrderDetail> {
}
service/impl/OrderDetailServiceImpl.java
@Service
public class OrderDetailServiceImpl extends ServiceImpl<OrderDetailMapper, OrderDetail> implements OrderDetailService {
}
controller/OrderDetailController.java
/**
* 订单明细
*/
@Slf4j
@RestController
@RequestMapping("/orderDetail")
public class OrderDetailController {
@Autowired
private OrderDetailService orderDetailService;
}
4. 代码实现
虽然传过来的json数据只有三个,但是其他没有传的可以从表中查询到,如当前用户信息(session)、购物车的数据(可通过userid查到)
但是需要涉及多个表的查询、操作,所以在OrderService中扩展方法
@Autowired
private OrderService orderService;
/**
* 用户下单
* @param orders
* @return
*/
@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders) {
log.info("订单数据:{}",orders)
orderService.saveWithOrder(orders);
return R.success("用户下单成功");
}
OrderService中扩展方法
service/OrderService.java
void saveWithOrder(Orders orders);
实现OrderService中扩展的方法
service/impl/OrderServiceImpl.java
@Autowired
private ShoppingCartService cartService;
@Autowired
private AddressBookService addressBookService
@Autowired
private UserService userService
@Autowired
private OrderDetailService orderDetailService;
/**
* 用户下单:
* 会操作三张表:订单表、订单明细表、购物车表
* @param orders
*/
@Override
@Transactional
public void saveWithOrder(Orders orders) {
//获取用户id
Long userId = BaseContext.getCurrentId();
//使用userid 查询购物车数据
LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ShoppingCart::getUserId, userId);
List<ShoppingCart> carts = cartService.list(wrapper);
if(carts == null|| carts.size()==0){
throw new CustomException("购物车为空,不能下单");
}
//orders中需要用户信息、还需要地址信息,所以先查询一下,保存想要的数据。
//查询用户数据
User user = userService.getById(userId);
//查询用户地址数据
Long bookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(bookId);
if (addressBook == null) {
throw new CustomException("用户地址信息有误,不能下单");
}
//向订单表和订单明细表插入数据
//this.save(orders);
//但是页面传过来的数据不是完整的,不能直接保存;填充完整后再保存
//通过mybatis-plus生成订单号
long orderId = IdWorker.getId();
//计算金额价格
AtomicInteger amount = new AtomicInteger(0);//原子操作。保证在多线程情况下计算没得问题
//遍历购物车数据,计算金额;同时保存订单明细表(多条数据)
List<OrderDetail> orderDetails = carts.stream().map((item)->{
//创建订单明细数据,并补充完整
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setNumber(item.getNumber());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setAmount(item.getAmount());//单份金额
//计算总金额:累加操作
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
//保存订单表(一条数据)填充完整后再保存
orders.setId(orderId);//id
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2);//订单状态
orders.setAmount(new BigDecimal(amount.get()));//总金额
orders.setUserId(userId);//用户id
orders.setNumber(String.valueOf(orderId));//设置订单号
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());//收件人名称
orders.setPhone(addressBook.getPhone());
//拼接地址
orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
+ (addressBook.getCityName() == null ? "" : addressBook.getCityName())
+ (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
+ (addressBook.getDetail() == null ? "" : addressBook.getDetail()));
//订单表插入数据
this.save(orders);
//订单明细表插入数据
orderDetailService.saveBatch(orderDetails);
//删除购物车
cartService.remove(wrapper);
}
09数据缓存
Java操作Redis
Redis的Java 客户端很多,官方推荐的有三种Jedis、Lettuce、Redisson
Jedis
Jedis的maven坐标:
<dependency>
<groupld>redis.clients</groupld>
<artifactld>jedis</artifactld>
<version>2.8.0</version>
</dependency>
使用Jedis操作Redis的步骤:获取连接、执行操作、关闭连接
public void testRedis(){
//1 获取连接
Jedis jedis = new Jedis("localhost", 6379);
//2、执行具体的操作
jedis.set("username","xiaoming");
String value = jedis.get("username");
System.out.println(value);
jedis.del("username") ;
//3 关闭连接
jedis.close();
}
Spring Data Redis
1. 基础环境
spring 对 Redis客户端进行了整合,提供了 Spring Data Redis,在Spring Boot项目中还提供了对应的Starter,即spring-boot-starter-data-redis
maven坐标:
<dependency>
<groupld>org.springframework.boot</groupld>
<artifactld>spring-boot-starter-data-redis</artifactld>
</dependency>
Spring Data Redis中提供了一个高度封装的类:RedisTemplate,针对jedis客户端中大量api进行了归类封装将同一类型操作封装为operation接口,
接口具体分类如下:
ValueOperations:简单K-V操作
SetOperations: set类型数据操作
ZSetOperations: zset类型数据操作
HashOperations:针对map类型的数据操作
ListOperations:针对list类型的数据操作
application.yml配置文件
#Redis相关配置
redis:
host: localhost
port: 6379
#password: 123456
database:0#默认16个数据库;0代表第0号个数据库
jedis:
#Redis连接池配置
pool:
max-active:8 #最大连接数
max-wait: 1ms #连接池最大阻塞等待时间
max-idle: 4 #连接池中的最大空闲连接
min-idle: 0#连接池中的最小空闲连接
改变key序列化方式:
config/RedisConfig.java
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
//默认的Key序列化器为:JdkSerializationRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
2. 数据类型操作
String类型数据:
public class SpringDataRedisTest{
@Autowired
private RedisTemplate redisTemplate;
/**
*操作String类型数据
*/
@Test
public void testString(){
redisTemplate.opsForValue().set("city123","beijing");
ValueOperations valueOperations = redisTemplate.opsForValue();
String value = (String)valueOperations.get("city123");
System.out.println(value);
//设置时间
redisTemplate.opsForValue().set("key1", "value1", 10l,TimeUnist.SECONDS);
//是否存在key
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("city1234","nanjing");
System.out.println(aBoolean);
}
}
Hash类型数据:
@Test
public void testHash(){
HashOperations hashOperations = redisTemplate.opsForHash();
//存值
hashOperations.put("002","name","xaoming");
hashOperations.put("002","age","20");
hashOperations.put("002", "address","bj");
//取值
String age = (String) hashOperations.get("002","age");
System.out.println(age);
//获得hash结构中的所有字段
Set keys = hashoperations.keys("002");
for (Object key : keys) {
System .out.printin(key);
}
//获得hash结构中的所有值
List values = hashOperations.values("002");
for (Object value : values) {
System .out.printin(value);
}
}
3. 通用操作
@Test
public void testCommon(){
//获取Redis中所有的key
Set<String> keys = redisTemplate.keys("*");
for (String key : keys) {
System.out.println(key);
}
//判断某个key是否存在
Boolean itcast = redisTemplate.hasKey("itcast");
//判断某个ey是否存在
Boolean itcast = redisTemplate.hasKey("itcast");
System.out.println(itcast);
//删除指定key
redisTemplate.delete("myZset");
//获取指定key对应的vaLue的数据类型DataType
dataType = redisTemplate.type("myset");
System.out.println(dataType.name());
}
Spring Cache
Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。Spring Cache提供了一层抽象,底层可以切换不同的cache实现。具体就是通过CacheManager接口来统一不同的缓存技术。
CacheManager是Spring提供的各种缓存技术抽象接口。
针对不同的缓存技术需要实现不同的CacheManager:
常用注解
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。
例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可
示例
@CachePut
/**
* CachePut:将方法返回值放入缓存
* value: 缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@CachePut (value = "userCache",key = "#user.id")
@PostMapping
public User save(User user){
userService.save(user);
return user;
}
@CacheEvict
/**
* CacheEvict:清理指定缓存
* value: 缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@CacheEvict(value = "userCache",key ="#id")
//@CacheEvict(value = "userCache",key = "#p0")
//@CacheEvict(value = "userCache",key =#root.args[0]")
//@CacheEvict(value = "userCache",key ="#result.id")[return user;存在的时候]
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id){
userService.removeById(id);
}
@Cacheable
/**
* @Cacheable: 在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
* value: 缓存的名称,每个缓存名称下面可以有多个key
* key: 缓存的key
* condition:条件,满足条件是才缓存数据【condition中没有result对象】
* unless: 满足条件则不缓存
*/
@Cacheable(value = "userCache",key = "#id",unless = "#result == null")
//@Cacheable(value = "userCache",key = "#id",condition = "#result != null")
@GetMapping("/{id)")
public User getById(@PathVariable Long id){
User user = userService.getById(id);
return user;
}
使用方式
在Spring Boot项目中使用Spring Cache的操作步骤(使用redis缓存技术):
1、导入maven坐标 spring-boot-starter-data-redis、spring-boot-starter-cache
2、配置application.yml
spring:
cache:
redis:
time-to-live: 1800000 #设置缓存有效期
3、在启动类上加入@EnableCaching注解,开启缓存注解功能
4、在Controller的方法上加入@Cacheable、@CacheEvict等注解,进行缓存操作
缓存数据【使用redis】
环境配置以及序列化类
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
短信验证码
1. 实现思路
前面我们已经实现了移动端手机验证码登录,随机生成的验证码我们是保存在HttpSession中的。现在需要改造为将验证码缓存在Redis中,具体的实现思路如下:
1、在服务端UserController中注入RedisTemplate对象,用于操作Redis
2、在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟3、在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码
2. 代码修改
controller/UserController.java/sendMsg方法
//需要将生成的验证码保存到Session
//session.setAttribute(phone, code);
/**
*redis修改,将生成的验证码缓存到redis中,并设置有效期5分钟
*/
redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);
controller/UserController.java/login方法
//修改1:
//从Session中获取保存的验证码
//Object codeInSession = session.getAttribute(phone);
/**
*redis中获取缓存的验证码
*/
Object codeInSession = redisTemplate.opsForValue().get(phone);
//修改2:
//登录成功,删除redis中缓存的验证码
redisTemplate.delete(phone);
菜品数据
1. 实现思路
前面我们已经实现了移动端菜品查看功能,对应的服务端方法为DishController的list方法,此方法会根据前端提交的查询条件进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。
具体的实现思路如下:
1、改造DishController的list方法,先从Redis中获取菜品数据,如果有则直接返回,无需查询数据库;如果没有则查询数据库,并将查询到的菜品数据放入Redis.
2、改造DishController的save和update方法,加入清理缓存的逻辑
注意事项:在使用缓存过程中,要注意保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。
2. 代码修改
按分类缓存菜品,缓存的菜品数据是多份【要根据分类查询,每一个分类为一份】
controller/DishController.java/list方法
List<DishDto> dishDtoList=null;
/**
* 动态构造key:根据菜品类别id
*/
String key = "dish_"+dish.getCategoryId()+"_"+dish.getStatus();
//先从redis中获取缓存数据
dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);
if(dishDtoList != null){
//如果存在,直接返回,无需查询数据库
return R.success(dishDtoList);
}
//如果不存在,需要查询数据库
.......
//将查洵到的菜品数据缓存到Redis
redisTemplate.opsForValue().set(key,dishDtoList,60,TimeUnit.MINUTES);
controller/DishController.java/save方法
//insert表之后:
//删除单个菜品缓存
String key = "dish_" + dishDto.getCategoryId() + "_1";
redisTemplate.delete(key);
controller/DishController.java/update方法
//update操作后:
//方法一:删除所有菜品缓存
//Set keys = redisTemplate.keys("dish_*");
//redisTemplate.delete(keys);
//删除单个菜品缓存
String key = "dish_" + dishDto.getCategoryId() + "_1";
redisTemplate.delete(key);
缓存数据【使用spring cache】
套餐数据
1. 代码逻辑
具体的实现思路如下:
1、导入Spring Cache和Redis相关maven坐标
2、在application.yml中配置缓存数据的过期时间
3、在启动类上加入@EnableCaching注解,开启缓存注解功能
4、在SetmealController的list方法上加入@Cacheable注解
5、在SetmealController的save和delete方法上加入CacheEvict注解
2. 代码实现
maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
配置文件:
spring:
cache:
redis:
time-to-live: 1800000 #设置缓存有效期
list方法【载入缓存】
@Cacheable(value = "setmealCache",key = "#setmeal.categoryId+'_'+#setmeal.status")//返回值为R,不能序列化。所以需要R类实现序列化接口
public class R
implements Serializable {...}
save方法
@CacheEvict(value = "setmealCache",allEntries = true)
delect方法
@CacheEvict(value = "setmealCache",allEntries = true)//所有套餐的缓存数据都清理掉
10读写分离
MySQL主从复制结构搭建
MySQL主从复制是一个异步的复制过程,底层是基于Mysql数据库自带的二进制日志功能。就是一台或多台MySQL数据库(slave,即从库)从另一台MvSOL数据库(master,即主库)进行日志的复制然后再解析日志并应用到自身,最终实现从库的数据和主库的数据保持一致。MySQL主从复制是MySQL数据库自带功能,无需借助第三方工具。
配置
1.两台服务器,分别安装Mysql并启动服务成功
【CentOS7创建虚拟机,finalShell进行配置】
主库Master 192.168.138.100
从库slave 192.168.138.101
2.配置主库Master
第一步:修改Mysq1数据库的配置文件/etc/my.cnf
[mysqld]
log-bin=mysql-bin#[必须]启用二进制日志
server-id=100 #[必须]服务器唯一ID
第二步:重启MySql服务
systemctl restart mysqld
第三步: 登录Mysql数据库,执行下面SQL
GRANT REPLICATION SLAVE ON *.* to 'xiaoming'@'%' identified by Root@123456
注:上面SQL的作用是创建一个用户xiaoming,密码为Root@123456,并且给xiaoming用户授予REPLICATION SLAVE权限。常用于建立复制时所需要用到的用户权限,也就是slave必须被master授权具有该权限的用户,才能通过该用户复制。
第四步: 登录Mysql数据库,执行下面SQL,记录下结果中File和Position的值
show master status;
注:上面SQL的作用是查看Master的状态,执行完此SOL后不要再执行任何操作
3.配置从库Slave
第一步: 修改Mysq1数据库的配置文件/etc/my.cnf
[mysqld]
server-id=11 #[必须]服务器唯一ID
第二步:重启MySql服务
systemctl restart mysqld
第三步:登录Mysq1数据库,执行下面SQL
mysql -uroot -proot
change master to master_host='192.168.138.100',master_user='xiaoming',master_password='Root@123456',master_log_file='mysql-bin.00001',master_log_pos=439;
start slave;
读写分离介绍
对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
Sharding-JDBC
Sharding-DBC定位为轻量级Java框架,在Java的]DBC层提供的额外服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容]DBC和各种ORM框架使用Sharding-JDBC可以在程序中轻松的实现数据库读写分离。
使用sharding-JDBC实现读写分离步骤
1、导入maven坐标
2、在配置文件中配置读写分离规则
3、在配置文件中配置允许bean定义覆盖配置项
main:
allow-bean-definition-overriding: true
程序角度:实现读写分离
数据库环境准备
直接使用我们前面在虚拟机中搭建的主从复制的数据库环境即可
在主库中创建瑞吉外卖项目的业务数据库reggie并导入相关表结构和数据
在v1.0分支修改,测试没有问题后,再合并merge回到主分支master branch
- Maven依赖:
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
2.配置文件
spring:
# 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: Lys981126.
shardingsphere:
datasource:
names:
master,slave
#主数据源
master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.138.100:3306/reggie?characterEncoding=utf-8
username: root
password: root
#从数据源
slave:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.138.101:3306/reggie?characterEncoding=utf-8
username: root
password:root
masterslave:
#读写分离配置
load-balance-algorithm-type: round _robin #轮询
#最终的数据源名称
name: dataSource
#主库数据源名称
master-datasource-name: master
#从库数据源名称列表,多个逗号分隔
slave-data-source-names: slave
props :
sql:
show: true #开SQL显示,默认false
main:
allow-bean-definition-overriding: true
11Nginx-服务器
Nginx简介
Nginx是一款轻量级的we 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用nginx的网站有: 百度、京东新浪、网易、腾讯、淘宝等。
官网: https://nginx.org/
安装过程:
1、在虚拟机里安装依赖包 yum -y install gcc pcre-devel zlib-devel openssl openssl-devel
2、下载Nginx安装包wget https://nginx.org/download/nginx-1.16.1.tar.gz
3、解压 tar -zxvf nginx-1.16.1.tar.gz
4、cd nginx-1.16.1
5、./configure --prefix=/usr/local/nginx
6、make && make install
Nginx目录结构
重点目录/文件:
conf/nginx.conf nginx配置文件
html 存放静态文件 (html、CSS、Js等)
logs 日志目录,存放日志文件
sbin/nginx 二进制文件,用于启动、停止Nginx服务
配置文件结构:
Nginx配置文件(conf/nginx.conf)整体分为三部分和Nginx运行相关的全局配置
- 全局块 和Nginx运行相关的全局配置
- events块 和网络连接相关的配置
- http块 代理、缓存、日志记录、虚拟主机配置
http全局块
Server块:Server全局块、location块
注意: http块中可以配置多个Server块,每个Server块中可以配置多个location块。
编辑配置文件命令:vim nginx.conf
重新加载:nginx -s reload
具体应用
部署静态资源
Nginx可以作为静态web服务器来部署静态资源。静态资源指在服务端真实存在并且能够直接展示的一些文件,比如常见的html页面、css文件、js文件、图片、视频等资源。
只需要将文件复制到Nginx安装目录下的html目录中即可
进入nginx-->cd html/-->cp hello.html /usr/local/nginx/html/-->访问1192.168.138.100/hello.html
反向代理
正向代理:
正向代理的典型用途是为在防火墙内的局域网客户端提供访问Internet的途径
正向代理一般是在客户端设置代理服务器,通过代理服务器转发请求,最终访问到目标服务器
反向代理:
反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源,反向代理服务器负责将请求转发给目标服务器。用户不需要知道目标服务器的地址,也无须在用户端作任何设定。
区别:正向代理知道客户端的存在,反向代理并不知道反向代理服务器的存在
配置反向代理服务器
server {
listen 82;
server_name localhost;
location /{
proxy_pass http://192.168.138.101:8080; #反向代理配置,将请求转发到指定服务
}
}
负载均衡
需要多台服务器组成应用集群#行性能的水平扩展以及避免单点故障出现。
应用集群:将同一应用部署到多台机器上,组成应用集群,接收负载均衡器分发的请求,进行业务处理并返回响应数据
负载均衡器:将用户请求根据对应的负载均衡算法分发到应用集群中的一台服务器进行处理
12前后端分离
问题:
开发人员同时负责前端和后端代码开发,分工不明确
开发效率低
前后端代码混合在一个工程中,不便于管理
对开发人员要求高,人员招聘困难
前后端分离开发后,从工程结构上也会发生变化,即前后端代码不再混合在同一个maven工程中,而是分为前端工程和后端工程
开发流程:
接口
YApi
YApi 是高效、易用、功能强大的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 AP,YApi 还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。YApi让接口开发更简单高效,让接口的管理更具可读性、可维护性,让团队协作更合理
源码地址: https://github.com/YMFE/yapi
要使用YApi,需要自己进行部署
Swagger
使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,再通过Swagger衍生出来的一系列项目和工具就可以做到生成各种格式的接口文档,以及在线接口调试页面等等。官网: https://swagger.io/
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案
knife4j使用方式
操作步骤:
1、导入knife4j的maven坐标
2、导入knife4j相关配置类
3、设置静态资源,否则接口文档页面无法访问
4、在LoginCheckFilter中设置不需要处理的请求路径
- pom.xml中加入maven坐标
<dependency>
<groupld>com.github.xiaoymin</groupld>
<artifactld>knife4j-spring-boot-starter</artifactld>
<version>3.0.2</version>
</dependency>
- 相关配置类
【在webMvcConfig类的上面添加注解:@EnableSwagger2、@EnableKnife4j】
@Bean
public Docket createRestApi(){
//文档类型
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.itheima.reggie.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apilnfo() {
return new ApiInfoBuilder()
.title("瑞吉外卖")
.version("1.0")
.description("瑞吉外卖接口文档")
.build();
}
- 设置静态资源映射(WebMvcConfig类中的addResourceHandlers方法),否则接口文档页面无法访问
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
- 在LoginCheckFilter中设置不需要处理的请求路径
"/doc. html"
"/webjars/**"
"/swagger-resources"
"/v2/api-docs"
常用注解
项目部署
服务器:
- 192.168.138.100 (服务器A)
Nginx: 部署前端项目、配置反向代理
Mysql: 主从复制结构中的主库
- 192.168.138.101 (服务器B)
jdk: 运行Java项目
git:版本控制工具
maven: 项目构建工具
jar: Spring Boot项目打成jar包基于内置Tomcat运行
Mysql: 主从复制结构中的从库
- 172.17.2.94(服务器C)
Redis: 缓存中间件