最新要闻
- 聚焦廊坊经洽会 | 务实高效办会 彰显河北高水平开放新形象
- 天天快消息!带宽翻倍更能超!影驰HOF Classic D5-7000内存评测:超至7800MHz仍有余力
- 年轻人第一款奢侈品?《王者荣耀》联名宝格丽:首款数字珠宝皮肤来了
- 环球今日讯!比亚迪宋PLUS冠军版一惊喜变化:日系SUV崩溃倒计时开始!
- i5/i7该选谁?差距大不大?i5-13490F、i7-13790F深度测试
- 6月19日老酒价格|飞天次新酒上涨 生肖节气下跌 十七大老酒市场价 当前视点
- 环球观察:《庆余年2》又新增七位女角色,金晨出演叶灵儿
- 赛力斯SERES 5出海欧洲市场:德国不限速高速飙到225Km/h
- 颜值超高碾压一众国产!长安启源A07路试:上市就打比亚迪汉
- 宝马5系首获半自动驾驶认证:变道只需看眼后视镜|世界短讯
- 100元记3分!杭州对“加塞”司机开罚 网友:请全国推广 消息
- 广东荔枝价格跌至5年来最低:仅需3、4元一斤
- 街拍,游走在法律和道德边缘
- 世界观热点:6月20日 11:03分 迈普医学(301033)股价快速拉升
- 日系还香吗?新一代本田皓影混动/插混上市:19.99万起要打比亚迪_世界报资讯
- 16.5亿打造!《封神三部曲》第一部7月20上映:预告片发布
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
快报:项目总结-瑞吉外卖
软件开发基础
分工:
流程:
01项目介绍
组成部分:系统管理后台、移动端
开发分期:
技术选型:
架构:
角色:
后台系统分析:
登录页面:
登录成功后,进入首页面(员工管理):
分类管理页面:
菜品管理页面:
套餐管理页面:
订单明细页面:
02开发环境搭建
数据库搭建
创建数据库-->导入资料中的表文件(db_reggie.sql)
命令行形式:
mysql> use reggie.sql;Database changedmysql> source D:\db_reggie.sql;
maven项目搭建
- 创建新项目:检查项目的编码、maven仓库配置、jdk配置
- 导入pom.xml文件
父功能--springboot
org.springframework.boot spring-boot-starter-parent 2.4.5
jdk版本
UTF-8 1.8 1.8
maven依赖坐标及插件
pom部分代码
org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-web compile com.baomidou mybatis-plus-boot-starter 3.4.2 org.projectlombok lombok 1.18.20 com.alibaba fastjson 1.2.76 commons-lang commons-lang 2.6 mysql mysql-connector-java runtime com.alibaba druid-spring-boot-starter 1.1.23 org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-cache org.springframework.boot spring-boot-maven-plugin 2.4.5
- 配置文件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_IDriggle: 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@Configurationpublic 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 */@Datapublic class R implements Serializable { private Integer code; //编码:1成功,0和其它数字为失败 private String msg; //错误信息 private T data; //数据 private Map map = new HashMap(); //动态数据 public static R success(T object) { R r = new R(); r.data = object; r.code = 1; return r;} public static R error(String msg) { R r = new R(); r.msg = msg; r.code = 0; return r; } public R add(String key, Object value) { this.map.put(key, value); return this; }}
1. 实体类:创建实体类Employee,和表employee进行映射(entity包中)
Employee
/** * 员工实体 */@Datapublic 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接口
@Mapperpublic interface EmployeeMapper extends BaseMapper{}
3. service接口以及impl实现类(service包)
service接口:
public interface EmployeeService extends IService {}
实现类:
@Servicepublic class EmployeeServiceImpl extends ServiceImpl implements EmployeeService {}
4. controller类(controller包)
@Slf4j@RestController@RequestMapping("/employee")//根据请求urlpublic 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 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 wrapper = new QueryWrapper<>(); wrapper.eq("username", username); //LambdaQueryWrapper 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 logout(HttpServletRequest request) { request.getSession().removeAttribute("employee"); return R.success("退出成功"); }
完善功能:(过滤器/拦截器)
必须登录成功后才能进入系统首页面中;如果没有登录,需要跳转到登录页面
实现:
1、创建自定义过滤器LoginCheckFilter2、在启动类上加入注解@ServletComponentScan3、完善过滤器的处理逻辑
代码:
- 创建过滤器:(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 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 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(int page, int pageSize, String name) { //Page类是mybatis-plus封装好的 log.info("page = {},pageSize = {},name = {}", page, pageSize, name); //构造分页构造器 Page pageInfo = new Page(page, pageSize); //构造条件构造器 LambdaQueryWrapper 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 } })}
已经实现了只有管理员才能看到 启用/禁用 按钮
{{ scope.row.status == "1" ? "禁用" : "启用" }}
3. 代码实现
本质上就是一个更新操作,也就是对status状态字段进行操作在Controller中创建update方法,此方法是一个通用的修改员工信息的方法【该方法可以与编辑员工信息通用,都是更新操作】
/** * 根据id修改员工信息,如禁用,启用 * 这是一个通用的方法,在修改员工信息的时候,可以直接用, * @param employee * @return */ @PutMapping public R update(HttpServletRequest request, @RequestBody Employee employee) { //因为返回值只要一个res.code,所以R 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> 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
发送ajax请求:queryEmployeeById(this.id)
api/member.js
// 修改页面反查详情接口function queryEmployeeById (id) { return $axios({ url: `/employee/${id}`, method: "get" })}
3. 代码实现【创建方法处理请求】
使用路径变量@PathVariable("id") Long idurl地址栏方式的请求:@GetMapping("/{id}")
回显数据:【第一次请求】
/** * 根据id查询员工信息 * * @param id * @return */ @GetMapping("/{id}") public R 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@Slf4jpublic 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来设置当前登录用户的id3、在MyMetaobiectHandler的方法中调用BaseContext获取登录用户的id
BaseContext工具类:【common包】
作用范围:某个线程之内;【每次请求都是一个新的线程】
common/BaseContext.java
/** * 基于ThreadLocal封装的工具类,用户保存和获取当前登录用户id */public class BaseContext { private static ThreadLocal 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
/** * 分类 */@Datapublic 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
@Mapperpublic interface CategoryMapper extends BaseMapper {}
Service\CategoryService.java
public interface CategoryService extends IService {}
Service\impl\CategoryServiceImpl.java
@Servicepublic class CategoryServiceImpl extends ServiceImpl implements CategoryService {}
controller\javaCategoryController.java
/** * 分类管理 */@RestController@RequestMapping("/category")@Slf4jpublic 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
:根据前端代码可见,只用到了一个code【res.code】@RequestBody Category category:json形式的数据如果unique字段重复,会进入全局异常处理器中
/** * 新增分类 * * @param category * @return */ @PostMapping public R 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(int page, int pageSize) { //分页构造器 Page pageInfo = new Page<>(page, pageSize); //条件构造器 LambdaQueryWrapper 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
:只要返回code就可以【if (res.code === 1)】Long ids:参数通过url地址?的形式传过来,不用RequestBody注解。
/** * 根据id删除分类 * @param ids * @return */ @DeleteMapping public R delete(Long ids) {//只要返回code就可以,所以类型String log.info("删除分类,id为:{}", ids); //根据id删除 categoryService.removeById(id); return R.success("分类信息删除成功"); }
4. 代码完善
需要检查 要删除的分类 是否 关联了菜品或者套餐【所以需要菜品和套餐的相关类:需要使用两个类中的categoryId属性】
要完善分类删除功能,需要先准备基础的类和接口:1、实体类Dish和Setmeal2、Mapper接口DishMapper和SetmealMapper3、Service接口DishService和SetmealService4、Service实现类DishServicelmpl和SetmealServicelmpl
Dish基础的类和接口:
entity/Dish.java
/** 菜品 */@Datapublic 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
@Mapperpublic interface DishMapper extends BaseMapper {}
service/DishService.java
public interface DishService extends IService {}
service/impl/DishServiceImpl.java
@Service@Slf4jpublic class DishServiceImpl extends ServiceImpl implements DishService {}
controller/DishController.java
/** * 菜品管理 */@RestController@RequestMapping("/dish")@Slf4jpublic class DishController { @Autowired private DishService dishService;}
Setmeal基础的类和接口:
entity/Setmeal.java
/** * 套餐 */@Datapublic 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
@Mapperpublic interface SetmealMapper extends BaseMapper {}
service/SetmealService.java
public interface SetmealService extends IService {}
service/impl/SetmealServiceImpl.java
@Service@Slf4jpublic class SetmealServiceImpl extends ServiceImpl implements SetmealService {}
controller/SetmealController.java
/** * 套餐管理 */@RestController@RequestMapping("/setmeal")@Slf4jpublic 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 dishLambdaQueryWrapper = new LambdaQueryWrapper<>();//查的是Dish dishLambdaQueryWrapper.eq(Dish::getCategoryId, ids);//等值查询 int count1 = dishService.count(dishLambdaQueryWrapper); //如果已经关联,抛出一个业务异常(自定义业务异常) if (count1 > 0) { throw new CustomException("当前分类下关联了菜品,不能删除");//已经关联菜品,抛出一个业务异常 } //查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常 LambdaQueryWrapper 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 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 update(@RequestBody Category category) { log.info("修改分类信息:{}", category); categoryService.updateById(category); return R.success("修改分类信息成功"); }
菜品管理业务
文件上传下载
文件上传
指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。
前端
文件上传时,对页面的form表单有如下要求:
method="post" 采用post方式提交数据enctype="multipart/form-data" 采用multipart格式上传文件使用input的file控件上传type="file" 使用input的file控件上传
举例:
前端代码的一些组件库提供了相应的上传组件:如ElementUI提供的upload上传组件
服务端
服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:
commons-fileuploadcommons-io
Spring框架在spring-web包中对文件上传进行了封装。
需要在Controller的方法中声个MultipartFile类型的参数即可接收上传的文件:
/***文件上传*@param file*@return*/@PostMapping(value = "/upload")public R upload(MultipartFile file){ System.out.println(file); return null;}
- 代码实现
前端:提交表单的时候,发送一次请求
前端:backend/page/demo/upload.html
........ ........
动态获取保存地址:@Value("${riggle.path}"):读取到配置文件的内容,需要此注解动态获取文件名:使用UUID重新生成文件名
controller/CommonController.java
/** * 文件上传和下载 */@RestController@RequestMapping("/common")@Slf4jpublic class CommonController { @Value("${riggle.path}")//使用配置文件中的路径 private String basePath; @PostMapping("/upload") public R 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
//上传完成后会展示图片:......... 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
/**菜品口味 */@Datapublic 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
@Mapperpublic interface DishMapper extends BaseMapper {}
业务层接口:service/DishFlavorService.java
public interface DishFlavorService extends IService {}
业务层实现类:service/impl/DishFlavorServiceImpl.java
@Servicepublic class DishFlavorServiceImpl extends ServiceImpl implements DishFlavorService {}
控制层:controller/DishController.java
/** * 菜品管理 */@RestController@RequestMapping("/dish")@Slf4jpublic 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>】
4. 代码实现
第一次请求的响应方法:
controller/CategoryController.java接收参数:String type\Category category[会对应到实体中的type属性]
/** * 在菜品管理页面中,筛选出菜品分类的数据 * 根据条件查询菜品分类数据 * @param category * @return */ @GetMapping("/list") public R> list(Category category) { //条件构造器 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); //添加条件 queryWrapper.eq(category.getType() != null, Category::getType, category.getType()); //添加排序条件;根据sort进行排序 queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime); List list = categoryService.list(queryWrapper); return R.success(list); }
第二次和第三次请求
文件上传下载已写好。
第四次请求
会操作两张表:新增的dish和dish flavor接收的数据是包括两张表的数据:创建一个新的类,接收所有请求参数
dto/DishDto.java
@Datapublic class DishDto extends Dish { /** * 继承了Dish,又扩展了一些属性 */ private List 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 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 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(int page,int pageSize,String name){ //构造分页构造器 Page pageInfo = new Page<>(page,pageSize); Page dishDtoPage = new Page<>(page,pageSize); //条件构造器 LambdaQueryWrapper 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变为List类型 List records = pageInfo.getRecords(); List 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 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 queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(DishFlavor::getDishId, dish.getId()); List flavors = dishFlavorService.list(queryWrapper); dishDto.setFlavors(flavors); return dishDto; }
第四次请求【保存】:
更新:两张表,不单单更新dish。还要更新flavor
/** * 修改菜品(与新增菜品差不多) * @param dishDto * @return */ @PutMapping public R 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 wrapper = new LambdaQueryWrapper<>(); wrapper.eq(DishFlavor::getDishId, dishDto.getId()); dishFlavorService.remove(wrapper); //获取当前提交过来的口味数据 List 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. 代码实现
套餐管理业务
新增套餐
1. 需求
在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐
2. 数据模型
将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal dish表插入套餐和菜品关联数据。
setmeal套餐表setmeal_dish套餐菜品关系表
3. 代码逻辑
创建需要的类和接口
SetMeal相关的类和接口在分类管理的时候已经创建;现只需SetmealDish【主表:SetMeal】
实体类:SetmealDish
/** * 套餐菜品关系 */@Datapublic 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
@Mapperpublic interface SetmealDishMapper extends BaseMapper {}
业务层接口:SetmealDishService
public interface SetmealDishService extends IService {}
业务层实现类:SetmealDishServicelmpl
@Service@Slf4jpublic class SetmealDishServiceImpl extends ServiceImpl implements SetmealDishService { }
控制层:SetmealController
/** * 套餐管理 */@RestController@RequestMapping("/setmeal")@Slf4jpublic 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 dish) { //构造查询条件 LambdaQueryWrapper 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 list = dishService.list(queryWrapper); return R.success(list); }
第六次请求【保存】
SetmealDto类型接收数据:提交的数据除了setmeal信息还有setmeal_dish信息[当前套餐对应哪些菜品]
/** * 添加套餐 * @param setmealDto * @return */ @PostMapping @CacheEvict(value = "setmealCache",allEntries = true) public R 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两个表 */@Datapublic class SetmealDto extends Setmeal { private List 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 setmealDishes = setmealDto.getSetmealDishes();//setmealDto中有这个集合 //保存之前,处理数据:因为缺少SetmealDishes表中的setmealId。只存了dishId,对每个数据需要添加setmealId List 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:@Autowiredprivate CategoryService categoryService;
/** * 套餐分页查询 * @param page * @param pageSize * @param name * @return */ @GetMapping("/page") public R querySetmealDish(int page, int pageSize, String name) { //分页构造器对象 Page pageInfo = new Page<>(page, pageSize); Page setmealDtoPage = new Page<>(); //添加查询条件 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotEmpty(name), Setmeal::getName, name); wrapper.orderByDesc(Setmeal::getUpdateTime); setmealService.page(pageInfo, wrapper);//pageInfo已经有结果了 //进行对象的copy【除了records,泛型不一样】 List records = pageInfo.getRecords();//需要的setmeal的list BeanUtils.copyProperties(pageInfo, setmealDtoPage, "records"); List 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 delete(@RequestParam List ids) { log.info("ids:{}", ids); setmealService.removeWithDish(ids); return R.success("套餐数据删除成功"); }
扩展service方法:删除套餐的同时,把相关联的菜品关系数据也删除掉
service/SetmealService.java
public void removeWithDish(List ids);
实现扩展的方法加入事务管理@Transactional
service/impl/SetmealServiceImpl.java
/** * 删除套餐,同时需要删除套餐和菜品的关联数据 * 两个表的操作:在实现类中扩展方法 * @param ids */ @Override @Transactional public void removeWithDish(List ids) { //查询套餐状态,确定是否可用删除 LambdaQueryWrapper 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 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. 代码实现
移动端
手机验证码登录
1. 需求
短信发送、验证码登录
2. 短信发送
短信服务:
第三方短信服务会和各个运营商(移动、联通、电信)对接,我们只需要注册成为会员并且按照提供的开发文档进行调用就可以发送短信。常用短信服务:阿里云、华为云、腾讯云、京东、梦网、乐信
阿里云短信服务:注册账号-->短信服务-->设置短信签名-->设置AccessKey(子用户)
实现:
1、导入maven坐标2、调用API
pom.xml加入坐标依赖
com.aliyun aliyun-java-sdk-core 4.5.16 com.aliyun aliyun-java-sdk-dysmsapi 2.1.0
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
/** * 用户信息 */@Datapublic 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
@Mapperpublic interface UserMapper extends BaseMapper{}
service/UserService.java
public interface UserService extends IService {}
service/impl
@Servicepublic class UserServiceImpl extends ServiceImpl implements UserService{}
controller/UserController.java
@RestController@RequestMapping("/user")@Slf4jpublic 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 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 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 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
/** * 地址簿 */@Datapublic 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
@Mapperpublic interface AddressBookMapper extends BaseMapper {}
service/AddressBookService.java
public interface AddressBookService extends IService {}
service/impl/AddressBookServiceImpl.java
@Servicepublic class AddressBookServiceImpl extends ServiceImpl implements AddressBookService {}
controller/AddressBookController.java
/** * 地址簿管理 */@Slf4j@RestController@RequestMapping("/addressBook")public class AddressBookController { @Autowired private AddressBookService addressBookService;}
4. 代码实现
新增地址【保存地址按钮】
/** * 新增 */ @PostMapping public R 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 setDefault(@RequestBody AddressBook addressBook) { log.info("addressBook:{}", addressBook); LambdaUpdateWrapper 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 get(@PathVariable Long id) { AddressBook addressBook = addressBookService.getById(id); if (addressBook != null) { return R.success(addressBook); } else { return R.error("没有找到该对象"); } }
查询默认地址
/** * 查询默认地址 */ @GetMapping("default") public R getDefault() { LambdaQueryWrapper 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 addressBook) { addressBook.setUserId(BaseContext.getCurrentId()); log.info("addressBook:{}", addressBook); //条件构造器 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(null != addressBook.getUserId(), AddressBook::getUserId, addressBook.getUserId()); queryWrapper.orderByDesc(AddressBook::getUpdateTime); List 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(Dish dish) { //为了移动端页面可以选择口味,返回值变为DishDto带有口味的属性 //构造查询条件 LambdaQueryWrapper 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 list = dishService.list(queryWrapper); List 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 lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(DishFlavor::getDishId, dishId); //SQL: select * from dish_flavor where dish_id = ? //查出来口味的集合 List 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 setmeal) {//url方式传进来的不需要加@RequestBody LambdaQueryWrapper 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 list = setmealService.list(queryWrapper); return R.success(list); }
购物车
1. 需求
对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车:对于套餐来说,可以直接点击[+]将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量也可以清空购物车。
2. 数据模型
shopping_cart表
3. 代码逻辑
交互过程
三次请求
1、点击[加入购物车]或者[+]按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车菜品数据:dishId套餐数据:setmealId2、点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐http://localhost:8080/shoppingCart/list [get]3、点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作
构建shopping_cart基础类和接口
entity/ShoppingCart.java
/** * 购物车 */@Datapublic 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
@Mapperpublic interface ShoppingCartMapper extends BaseMapper {}
service/ShoppingCartService.java
public interface ShoppingCartService extends IService {}
service/impl/ShoppingCartServiceImpl.java
@Servicepublic class ShoppingCartServiceImpl extends ServiceImpl implements ShoppingCartService {}
controller/ShoppingCartController.java
/** * 购物车 */@Slf4j@RestController@RequestMapping("/shoppingCart")public class ShoppingCartController { @Autowired private ShoppingCartService shoppingCartService;}
4. 代码实现
第一次请求【加入购物车】
/** * 添加购物车数据 * @param shoppingCart * @return */ @PostMapping("add") public R addShort(@RequestBody ShoppingCart shoppingCart) { log.info("购物车数据:{}",shoppingCart); // 获取用户id(通过session已经存起来了,可以通过session获得/ 通过basecontext也能获得) Long userId = BaseContext.getCurrentId(); shoppingCart.setUserId(userId); //查询购物车数据是否存在 Long dishId = shoppingCart.getDishId(); LambdaQueryWrapper 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() { log.info("查看购物车..."); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId()); queryWrapper.orderByAsc(ShoppingCart::getCreateTime);//升序 List list = shoppingCartService.list(queryWrapper); return R.success(list); }
第三次请求【清空购物车】
/** * 清空购物车 * @return */ @DeleteMapping("/clean") public R clean(){ LambdaQueryWrapper 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
/** * 订单 */@Datapublic 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
@Mapperpublic interface OrderMapper extends BaseMapper {}
service/OrderService.java
public interface OrderService extends IService {}
service/impl/OrderServiceImpl.java
@Service@Slf4jpublic class OrderServiceImpl extends ServiceImpl implements OrderService {}
controller/OrderController.java
/** * 订单 */@Slf4j@RestController@RequestMapping("/order")public class OrderController { @Autowired private OrderService orderService;}
order_detail
entity/OrderDetail.java
/** * 订单明细 */@Datapublic 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
@Mapperpublic interface OrderDetailMapper extends BaseMapper {}
service/OrderDetailService.java
public interface OrderDetailService extends IService {}
service/impl/OrderDetailServiceImpl.java
@Servicepublic class OrderDetailServiceImpl extends ServiceImpl 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 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 wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ShoppingCart::getUserId, userId); List 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 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); }
Java操作Redis
Redis的Java 客户端很多,官方推荐的有三种Jedis、Lettuce、Redisson
Jedis
Jedis的maven坐标:
redis.clients jedis 2.8.0
使用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-redismaven坐标:
org.springframework.boot spring-boot-starter-data-redis
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
@Configurationpublic class RedisConfig extends CachingConfigurerSupport { @Bean public 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类型数据:
@Testpublic 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. 通用操作
@Testpublic void testCommon(){ //获取Redis中所有的key Set 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")@PostMappingpublic 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-cache2、配置application.ymlspring: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对象,用于操作Redis2、在服务端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 dishDtoList=null; /** * 动态构造key:根据菜品类别id */ String key = "dish_"+dish.getCategoryId()+"_"+dish.getStatus(); //先从redis中获取缓存数据 dishDtoList = (List) 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坐标
org.springframework.boot spring-boot-starter-cache
配置文件:
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)//所有套餐的缓存数据都清理掉
读写分离
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 -prootchange 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依赖:
org.apache.shardingsphere sharding-jdbc-spring-boot-starter 4.0.0-RC1
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
Nginx-服务器
Nginx简介
Nginx是一款轻量级的we 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用nginx的网站有: 百度、京东新浪、网易、腾讯、淘宝等。官网: https://nginx.org/
安装过程:
1、在虚拟机里安装依赖包 yum -y install gcc pcre-devel zlib-devel openssl openssl-devel2、下载Nginx安装包wget https://nginx.org/download/nginx-1.16.1.tar.gz3、解压 tar -zxvf nginx-1.16.1.tar.gz4、cd nginx-1.16.15、./configure --prefix=/usr/local/nginx6、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; #反向代理配置,将请求转发到指定服务 }}
负载均衡
需要多台服务器组成应用集群#行性能的水平扩展以及避免单点故障出现。
应用集群:将同一应用部署到多台机器上,组成应用集群,接收负载均衡器分发的请求,进行业务处理并返回响应数据负载均衡器:将用户请求根据对应的负载均衡算法分发到应用集群中的一台服务器进行处理
前后端分离
问题:
开发人员同时负责前端和后端代码开发,分工不明确开发效率低前后端代码混合在一个工程中,不便于管理对开发人员要求高,人员招聘困难
前后端分离开发后,从工程结构上也会发生变化,即前后端代码不再混合在同一个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坐标
com.github.xiaoymin knife4j-spring-boot-starter 3.0.2
- 相关配置类【在webMvcConfig类的上面添加注解:@EnableSwagger2、@EnableKnife4j】
@Beanpublic 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: 缓存中间件
关键词:
强化学习从基础到进阶-常见问题和面试必知必答[2]:马尔科夫决策、贝尔曼方程、动态规划、策略价值迭代
Linux安装MongoDB 4.0.3
当前观点:C#12“实用”的新功能:类型的别名
聚焦廊坊经洽会 | 务实高效办会 彰显河北高水平开放新形象
天天快消息!带宽翻倍更能超!影驰HOF Classic D5-7000内存评测:超至7800MHz仍有余力
年轻人第一款奢侈品?《王者荣耀》联名宝格丽:首款数字珠宝皮肤来了
环球今日讯!比亚迪宋PLUS冠军版一惊喜变化:日系SUV崩溃倒计时开始!
i5/i7该选谁?差距大不大?i5-13490F、i7-13790F深度测试
6月19日老酒价格|飞天次新酒上涨 生肖节气下跌 十七大老酒市场价 当前视点
ChatGPT 初探 - 冰蓝老师 天天微头条
天天新资讯:线性结构中的栈、队列和串是怎么回事?
当前看点!如何将PCM格式的原始音频采样数据编码为MP3格式或AAC格式的音频文件?
全球头条:maven构建报错:Unable to load the mojo 'install' (or one of its required compone
每日聚焦:“降息”靴子落地!
环球观察:《庆余年2》又新增七位女角色,金晨出演叶灵儿
赛力斯SERES 5出海欧洲市场:德国不限速高速飙到225Km/h
颜值超高碾压一众国产!长安启源A07路试:上市就打比亚迪汉
宝马5系首获半自动驾驶认证:变道只需看眼后视镜|世界短讯
100元记3分!杭州对“加塞”司机开罚 网友:请全国推广 消息
广东荔枝价格跌至5年来最低:仅需3、4元一斤
街拍,游走在法律和道德边缘
CSS3有哪些新特性_全球最资讯
全球即时看!数据库三大范式(考试必备)
世界要闻:使用 JMX-Exporter 监控 Kafka 和 Zookeeper
A Practical Methodology, HSM, Handler,Service,Model, for Golang Backend Developm
【环球聚看点】直播源码搭建技术弹幕消息功能的实现
世界观热点:6月20日 11:03分 迈普医学(301033)股价快速拉升
日系还香吗?新一代本田皓影混动/插混上市:19.99万起要打比亚迪_世界报资讯
16.5亿打造!《封神三部曲》第一部7月20上映:预告片发布
全脂/低脂可选:特仑苏纯牛奶2.7元/盒大促(商超6元)
世界快看点丨微软明确不会涉足VR:市场实在太小
红魔8S Pro首发高频版骁龙8 Gen2!170万跑分比骁龙8 Gen3还猛 焦点速讯
【全球新要闻】对在建工程“全面体检”
全球新资讯:关于线性结构中的双向链表如何实现?
NCalc 学习笔记 (六)|天天观热点
也说一说IDEA热部署Web项目最终解决方案,确实大大提高工作效率
每日视点!详解在 Linux 启动时,如何自动执行命令或脚本
最资讯丨失乐园电影迅雷下载 失乐园电影未删减版迅雷下载
债市相对更强,股市估值处相对低位-焦点速递
墓地无人汽车探测到“鬼影”!真相到底是什么?
上海双层敞篷观光巴士将永久退役:已达13年强制报废标准 后继无车
首创双枪充电遥遥领先!比亚迪腾势N7首批量产车下线
高考过后 多所知名大学校长纷纷出镜招生|全球短讯
新买不到一个月特斯拉充电冒烟爆炸 女车主:很失望 产生心理阴影_天天动态
世界观点:菲律宾多方人士反对日本强推核污染水排海:不要污染我们的海洋
springboot~http请求头中如何放中文 当前快报
“泰坦尼克”号残骸观光潜艇氧气仅剩96小时 美加部署飞机搜寻
“全球第一吊”挑战191米最大陆上风力发电机 仅17分钟升至40层楼高
每日短讯:男子长城藏时间胶囊12年多人留纸条 网友直呼奇妙交流:很浪漫
屏摄电影被男子怒斥 影院称屏摄会对胶片有损伤 网友质疑:侮辱智商?-速讯
每日热门:美系开卷国产电动车!别克中大型轿跑E4上市:18.99万起
今日阵雨叨扰,周三周四阳光又将登场,抓紧洗晒! 环球热消息
【读财报】券商资管基金透视:财通、国泰君安资管年内收益领跑 中银证券业绩垫底 视讯
水电大省遭遇“水荒” 四川云南5月水电仍在下降
全球首例!杭州医生用5G帮5000公里外的新疆病人切除肝脏 画面网友惊叹
海口一特斯拉撞飞小车致一死一伤 现场视频被撞车360度旋转、有孩子被甩出-天天时快讯
电瓶车室内充电爆炸 墙都裂了 轮椅老人被吓得拔腿就跑 全球时讯
自救失败!“海航系”公司退市…
读发布!设计与部署稳定的分布式系统(第2版)笔记06_用户_世界观焦点
信息:手机可拆卸电池即将回归:利大于弊 别再被苹果牵着走
环球热资讯!回忆杀!高圆圆晒与贾静雯私照 梦回《倚天屠龙记》周芷若和赵敏
今日看点:早泄能治好吗?
100个物联网项目(基于ESP32)2快速入门
【linux命令】“瑞士军刀”nc的用法简介-天天关注
STL
马云指出淘宝天猫未来三个方向:回归淘宝、回归用户、回归互联网
花了1330万 还有600万只:巴黎向老鼠投降了 要“同居”_全球热点
冷清的618 焦虑的手机厂商:未来只能靠苹果创新了?
跳桥救人小哥引来女网友公开示爱:网友警告切勿炒作 世界热点评
中国男足亚运队1-0胜韩国U24队:孙沁涵抽射建功
美国能单挑全世界吗(美国军力全球第一敢于与世界敌么)
观焦点:登陆百度网盘错误 1550017 百度云同步盘登录失败155010
趋之若鹜的鹜什么意思_趋之若鹜
《王者荣耀》发布S32赛季漂泊之剑预告PV 两款战令皮肤奖励公布
韩国首尔教育厅将对学校供餐用水产品进行全面辐射检测 旨在保证食品安全
微信上线“安静模式” 专为有听力障碍的人创造更好的环境
《庆余年2》公布喜相逢版角色海报 增加一些重要新人物
JUC同步锁原理源码解析五----Phaser 今日热搜
ASP.NET Core MVC 从入门到精通之日志管理_世界热闻
计算几何之两条线段的交点|世界时快讯
看点:博客项目01
“狗狗嫌天热自己坐电梯回家”登上热搜 主人急里忙慌寻找
炒菜用什么油好?
车企卖衣服 不务正业? 焦点信息
微软Win11处理器要求变动:AMD、英特尔一大波新U加入支持
InnoDB 缓冲池
天天资讯:俄罗斯天然气工业银行拟参与无担保人民币债券市场
做小吃前途如何?惠记粉汤羊血加盟开店,开哪儿都火!
天天短讯!特斯拉车祸后复出 演员林志颖首次现身内地商演
专家称年轻人撑不起车市:中老年人才有足够能力拉动市场|视讯
今日精选:予以的拼音(予以)
【财经分析】数据赋能城市升级——2023中国资源型老工业城市转型发展指数研讨会在北京举办
每天喝咖啡的人 20年后都怎么样了?三大好处、三大不要 焦点精选
2024年见 龙芯也要做显卡了:IP设计已完成 还在优化
冷知识!大熊猫近视高达800度:只能看清几米之内物体 看热讯
索尼粉丝迷惑行为:请愿Xbox第一方游戏《星空》成PS5独占
开票!2023年安阳首场演唱会等你来抢!附购票入口
Liunx nginx服务|环球要闻
Manacher算法学习笔记
世界最新:799元价格屠夫!小米电视把国外品牌全打趴了