瑞吉外卖项目详细分析笔记及所有功能补充代码

目录

  • 项目刨析简介
    • 技术栈
    • 项目介绍
    • 项目源码
  • 一.架构搭建
    • 1.初始化项目结构
    • 2.数据库表结构设计
    • 3.项目基本配置信息添加
      • 公共字段的自动填充
      • 全局异常处理类
      • 返回结果封装的实体类
  • 二.管理端业务开发
    • 1.员工管理相关业务
      • 1.1员工登录
      • 1.2员工退出
      • 1.3过滤器拦截
      • 1.4员工信息修改
      • 1.5员工信息分页查询
      • 1.6新增员工
    • 2.分类管理相关业务
      • 2.1分类的分页查询
      • 2.2新增分类
      • 2.3菜品或套餐的分类修改
      • 2.4菜品或套餐的分类删除
    • 3.菜品管理相关业务
      • 3.1分页查询
      • 3.2图片上传下载
      • 3.3新增菜品
      • 3.4修改菜品
      • 3.5删除菜品
      • 3.6菜品停售与起售(补充)
    • 4.套餐管理相关业务
      • 4.1分页查询
      • 4.2新增套餐
      • 4.3修改套餐
      • 4.4删除套餐
      • 4.5套餐停售与起售(补充)
    • 5.订单明细(补充)
  • 三.移动端业务开发
      • 1.用户登录与退出(退出为补充)
      • 2.阿里云短信验证码
      • 3.收货地址
      • 4.菜品和套餐展示
      • 5.菜品选规格
      • 6.套餐点击展示(补充)
      • 7.购物车
      • 8.下订单
      • 9.收货地址删除(补充)
      • 10.用户支付后查看订单(补充)
      • 11.再来一单(补充)
  • 四.项目优化
    • 1.使用Redis缓存
      • 1.1缓存验证码
      • 1.2缓存菜品查询数据
      • 1.3Spring Cache缓存套餐数据
    • 2.读写分离
      • 2.1mysql主从复制
      • 2.2Sharding-JDBC实现读写分离
      • 2.3项目实现读写分离
    • 3.使用Nginx服务器
      • 3.1Nginx部署静态资源
      • 3.2反向代理
      • 3.3负载均衡
    • 4.前后端分离开发
      • 4.1YApi
      • 4.2Swagger
      • 4.3项目部署

项目刨析简介

#2022年末了,记录一下学习的项目实战经验和笔记吧
这个是瑞吉外卖项目,补充一些视频里面没有定义的功能和记录一些功能实现逻辑的笔记;仅供学习参考,本人代码可能不太规范,也有可能自己写了有些错误自己没有察觉,但是功能自己测试是没有问题的;感谢各位的阅览,如有问题欢迎指正,如有遗漏后续继续补充


技术栈

涉及到的技术有Spring,Springboot,Mybatis-plus,MySQL,Redis,Linux,Git,Spring Cache,Sharding-JDBC,Nginx,Swagger。(Apifox这些工具应该不算技术吧,用的工具就不列举了)


项目介绍

该项目是一个外卖点餐系统,它分为后台管理端和用户移动端两方面开发,后台管理端为商家提供管理菜品套餐的服务,移动端为用户提供点菜下单功能。最终通过git管理项目,并用nginx部署前端,tomcat部署后端,使用mysql主从复制,从库读取,主库写入,再用shell脚本部署到服务器上。


项目源码

项目码云地址:https://gitee.com/dkgk8/reggie-git


一.架构搭建

1.初始化项目结构

新建一个springboot项目
pom导入的坐标

		<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>3.0.2</version></dependency><!--		<dependency>-->
<!--			<groupId>org.apache.shardingsphere</groupId>-->
<!--			<artifactId>sharding-jdbc-spring-boot-starter</artifactId>-->
<!--			<version>4.1.1</version>-->
<!--		</dependency>--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId></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-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><scope>compile</scope></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.2</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.20</version></dependency><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><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.23</version></dependency><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>

yml配置文件添加的信息

server:port: 8080
spring:
#  application:
#    name: reggie_take_outdatasource:druid:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=trueusername: rootpassword: 123456redis:host: localhostport: 6379database: 0cache:redis:time-to-live: 1800000  #ms ->30minmybatis-plus:configuration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:id-type: ASSIGN_ID
reggie:path: D:\SpringBoot_Reggie\reggie_take_out\src\main\resources\static\front\hello\

我后面将项目运行在服务器上了所以用了多环境开发,本地跑的不用在意这步
在这里插入图片描述
项目大致结构如下
在这里插入图片描述

感觉使用mybatis-plus之后就是
实体类->mapper->service->serviceImpl->controller
这个步骤写程序了

2.数据库表结构设计

在这里插入图片描述
在这里插入图片描述
不每个表展示了,这里拿典型的员工表来看
在这里插入图片描述

3.项目基本配置信息添加

导入前端资源
在默认页面和前台页面的情况下,直接把这俩拖到resource目录下直接访问是访问不到的,因为被mvc框架拦截了,其实用springboot,可以直接放在static目录下,但是仍然不能直接访问前端页面,所以这里也可以直接放行static就好了
所以我们要编写一个映射类放行这些资源
WebMvcConfig类
在这里插入图片描述

公共字段的自动填充

在这里插入图片描述

这个我在另一篇文章写了很详细,链接:自动填充公共字段

全局异常处理类

虽然遇到异常后可以使用try-catch来处理,但是,代码量一大起来,许多的try catch就会很乱,代码也不简洁,不容易阅读,所以我们使用全局异常处理,在Common包下
在这里插入图片描述
自定义异常类
在这里插入图片描述

返回结果封装的实体类

为了便于前后端数据传递,使用对象的形式封装数据更合适

@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.员工管理相关业务

1.1员工登录

登录逻辑如下
在这里插入图片描述

    @PostMapping("/login")public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){//1.将页面提交的明文密码进行md5加密String password = employee.getPassword();password = DigestUtils.md5DigestAsHex(password.getBytes());//2.根据页面提交的用户名username查数据库LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Employee::getUsername,employee.getUsername());Employee emp = employeeService.getOne(queryWrapper);//3.如果没有查询到则返回登录失败结果if (emp == null){return R.error("登录失败");}//4.密码比对,如果不一致则返回登录失败结果if (!emp.getPassword().equals(password)){return R.error("登录失败");}//5.查看员工账号状态是否锁定,若是禁用状态返回禁用信息if (emp.getStatus() == 0){return R.error("账号异常,已锁定");}//6.登录成功,将员工id存入Session  并返回登录成功结果request.getSession().setAttribute("employee",emp.getId());return R.success(emp);}

1.2员工退出

就是清除员工登录时存入session的员工id

    @PostMapping("/logout")public R<String> logout(HttpServletRequest request){//1.清理Session中保存的当前登录员工idrequest.getSession().removeAttribute("employee");return R.success("退出成功");}

1.3过滤器拦截

现在没有过滤器,用户直接不用登录通过url+资源名可以随便访问,所以要加个过滤器,没有登陆时,拦截请求,不给访问,自动跳转到登陆页面
过滤器处理逻辑
在这里插入图片描述
在启动类上添加注解@ServletComponentScan
过滤器配置类注解@WebFilter(filterName=“拦截器类名首字母小写”,urlPartten=“要拦截的路径,比如/*”)

判断用户是否已经登录,之前因为存入session里面有一个名为employee的对象,里面放的时用户id,那么只需要用getAttribute,看看session里get的数据是否为null就知道他是否在登陆状态

这里提一嘴
调用Spring核心包的字符串匹配类的对象,对路径进行匹配,并且返回比较结果
如果相等就为true

public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

在这里插入图片描述
直接上代码

/*** 检查用户是否登录的过滤器*/@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {//路径匹配器,支持通配符public static final AntPathMatcher PATH_MATCHER =new AntPathMatcher();@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request=(HttpServletRequest) servletRequest;HttpServletResponse response=(HttpServletResponse) servletResponse;//1.获取本次请求uriString requestURI = request.getRequestURI();//定义不需要处理的请求路径String[] urls=new String[]{"/employee/login","/employee/logout","/backend/**","/front/**","/common/**","/user/sendMsg","/user/login","/doc.html","/webjars/**","/swagger-resources","/v2/api-docs"};//2.判断本次请求是否需要处理boolean check = check(urls, requestURI);//3.如果不需要处理则直接放行if (check){filterChain.doFilter(request,response);return;}//4-1.判断登录状态,如果已经登录,则直接放行if (request.getSession().getAttribute("employee")!=null){Long empId = (Long) request.getSession().getAttribute("employee");BaseContext.setCurrentId(empId);filterChain.doFilter(request,response);return;}//4-2.判断移动端登录状态,如果已经登录,则直接放行if (request.getSession().getAttribute("user")!=null){Long userId = (Long) request.getSession().getAttribute("user");BaseContext.setCurrentId(userId);filterChain.doFilter(request,response);return;}//5如果未登录则,通过输出流方式向客户端页面响应数据response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));return;}/*** 路径匹配,检查本次请求是否需要放行*/public boolean check(String[] urls,String requestURI){//遍历的同时调用PATH_MATCHER来对路径进行匹配for (String url : urls){boolean match = PATH_MATCHER.match(url,requestURI);if (match){//匹配到了可以放行的路径,直接放行return true;}}return false;}
}

1.4员工信息修改

员工状态修改
在这里插入图片描述

遇到了问题,数据库id根据雪花算法有19位,而js对Long型数据处理时会丢失精度,只能保证前16位
解决办法: 服务端给页面响应json数据时,将Long型数据统一转为String字符串
在这里插入图片描述
在这里插入图片描述
将Long型的Id转换为String类型的数据

/*** 对象映射器:基于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).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);}
}

在MVC配置类中扩展一个消息转换器

    /*** 扩展mvc框架的消息转换器* @param converters*/@Overrideprotected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {//创建消息转换器对象MappingJackson2HttpMessageConverter messageConverter=new MappingJackson2HttpMessageConverter();//设置对象转换器,底层使用Jackson将Java对象转为jsonmessageConverter.setObjectMapper(new JacksonObjectMapper());//将上面的消息转换器对象追加到mvc框架的转换器集合中converters.add(0,messageConverter);}

员工信息修改
修改逻辑分为数据回显和数据保存
数据回显就是根据传来的员工id查询员工信息,返回员工对象

    /*** 回显用户信息到修改框*/@GetMapping("/{id}")public R<Employee> getById(@PathVariable Long id){Employee employee = employeeService.getById(id);if (employee!=null){return R.success(employee);}else {return R.error("没查到该员工");}}

数据保存就是将更改后的数据再update到员工表中

    /*** 修改员工信息*/@PutMappingpublic R<String> update(@RequestBody Employee employee){log.info(employee.toString());employeeService.updateById(employee);return R.success("员工信息修改成功");}

测试结果
在这里插入图片描述

1.5员工信息分页查询

分页查询,老生常谈了
分页查询业务逻辑
在这里插入图片描述
浏览器发送的url
在这里插入图片描述
分页插件配置类
先弄个MP分页插件配置类,config包下创建MybatisPlusConfig类

/*** 配置MP的分页插件*/
@Configuration
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(){MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());return mybatisPlusInterceptor;}
}

在这里插入图片描述
page对象内部

在这里插入图片描述

上代码

    /*** 员工信息的分页查询*/@GetMapping("/page")public R<Page> page(int page, int pageSize, String name){log.info("page={},pageSize={},name={}",page,pageSize,name);//构造分页构造器Page pageInfo = new Page(page,pageSize);//构造条件构造器LambdaQueryWrapper<Employee> queryWrapper=new LambdaQueryWrapper();//添加过滤条件queryWrapper.like(StringUtils.isNotBlank(name),Employee::getName,name);//添加排序条件queryWrapper.orderByDesc(Employee::getUpdateTime);//执行查询employeeService.page(pageInfo,queryWrapper);return R.success(pageInfo);}

1.6新增员工

前端传递过来的数据,这里我们可以用一个employee员工对象将数据全部接收到
请求 URL: http://localhost:9001/employee (POST请求)
在这里插入图片描述

基本上都是mp封装好的CRUD,直接调用save方法就行了,这里不需要改造Employee实体类,通用id雪花自增算法来新增id,不需要像下面一样,因为最开始我们yml中已经配置了雪花自增算法来新增id,如下图,当然也可以两个方法任选其一。
在这里插入图片描述
在这里插入图片描述

    /*** 新增员工*/@PostMappingpublic R<String> save(@RequestBody Employee employee){log.info("新增员工,员工信息:{}",employee.toString());//设置初始密码123456,需要进行md5加密处理employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));employeeService.save(employee);return R.success("新增员工成功");}

2.分类管理相关业务

2.1分类的分页查询

在这里插入图片描述
还是那几步,老生常谈

1.创建分页构造器 Page pageInfo = new Page(page,pageSize);
2.如果有需要条件过滤的加入条件过滤器LambaQueryWarpper
3.注入的service对象(已经继承MP的BaseMapper接口)去调用Page对象
4.service对象.page(分页信息,条件过滤器)
返回结果就可以了

    @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);}

2.2新增分类

在这里插入图片描述
根据前端发送的请求接收传递的数据,再调用mp封装好的crud,往数据库表里插入数据就完了,没什么好说的

    @PostMappingpublic R<String> save(@RequestBody Category category){log.info("category:{}",category);categoryService.save(category);return R.success("新增分类成功");}

2.3菜品或套餐的分类修改

修改又是老套路,先回显数据再修改数据
在这里插入图片描述
前端发送的请求
在这里插入图片描述
就两步走,都是调用mp封装的方法

2.4菜品或套餐的分类删除

在这里插入图片描述
完善一下,如果当前菜品分类下有菜品的话,就不许删除
删除之前需要先做判断才可以删除,若当前分类下有菜品,我们要抛出异常进行提示
因为没有返回异常信息的类,我们这里要做一个自定义的专门返回异常信息的类CustomerException
因为我们之前创建了一个全局异常处理,也要用上,因为要拦截异常统一处理
在这里插入图片描述

    /*** 根据id删除分类,删除之前需要进行判断是否由关联* @param id*/@Overridepublic void remove(Long id) {LambdaQueryWrapper<Dish> dishLambdaQueryWrapper=new LambdaQueryWrapper<>();//添加查询条件,根据分类id进行查询dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);int count1 = dishService.count(dishLambdaQueryWrapper);//查询当前分类是否关联了菜品,如果已经关联,抛出业务异常if (count1>0){//已关联菜品,抛出一个业务异常throw new CustomException("当前分类下关联了菜品,不能删除");}//查询当前分类是否关联了套餐,如果已经关联,抛出业务异常LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper=new LambdaQueryWrapper<>();setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);int count2 = setmealService.count(setmealLambdaQueryWrapper);if (count2>0){//已关联套餐,抛出一个业务异常throw new CustomException("当前分类下关联了套餐,不能删除");}//正常删除分类super.removeById(id);}

3.菜品管理相关业务

3.1分页查询

这个分页查询不是老生常谈,如果只是菜品表的分页查询,你会发现,最后分页查询出来的菜品分类一栏为空白,因为前端需要的菜品分类名称的数据,dish表的分页查询数据中并没有,所以我们需要使用到DishDto的分页查询了,根据dish表的分类id来条件查询
在这里插入图片描述

打开dish表可以看到,只有菜品分类id字段,并没有菜品名称
在这里插入图片描述

创建一个DishDto类
在这里插入图片描述

在这里插入图片描述

这个是经典的Dto的分页查询,上代码

    /*** 菜品管理的分页查询*/@GetMapping("/page")public R<Page> page(int page, int pageSize, String name){//构造分页构造器对象Page<Dish> pageInfo = new Page<>(page,pageSize);Page<DishDto> dishDtoPage = new Page<>();//条件构造器LambdaQueryWrapper<Dish> queryWrapper=new LambdaQueryWrapper<>();//添加过滤条件queryWrapper.like(name!=null,Dish::getName,name);//添加排序条件queryWrapper.orderByDesc(Dish::getUpdateTime);dishService.page(pageInfo,queryWrapper);BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");List<Dish> records = pageInfo.getRecords();List<DishDto> list = records.stream().map((item)->{DishDto dishDto = new DishDto();BeanUtils.copyProperties(item,dishDto);Long categoryId = item.getCategoryId();Category category=categoryService.getById(categoryId);if(category!=null){String categoryName=category.getName();dishDto.setCategoryName(categoryName);}return dishDto;}).collect(Collectors.toList());dishDtoPage.setRecords(list);return R.success(dishDtoPage);}

3.2图片上传下载

在这里插入图片描述

具体的存储路径写在配置文件里了,用@Value注入到业务里就可以了

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

此时我们上传图片后,是存放在临时位置,关闭浏览器,图片文件就不存在了,无法再次浏览,我们需要将上传的图片下载到本地磁盘存储,这样浏览器上就可以进行图片回显,访问的时候才能看到图片
在这里插入图片描述
前端展示图片发送请求的代码

在这里插入图片描述

在这里插入图片描述

用到了I/O的输入输出流,算是复习了

    /*** 文件下载,图片回显浏览器*/@GetMapping("/download")public void download(String name, HttpServletResponse response){try {//输入流,通过输入流读取文件内容FileInputStream fileInputStream = new FileInputStream(new File(basePath+name));//输出流,通过输出流将文件写回浏览器,在浏览器展示图片ServletOutputStream outputStream = response.getOutputStream();response.setContentType("image/jpeg");//设置响应的文件类型int len = 0;byte[] bytes = new byte[1024];while ((len = fileInputStream.read(bytes)) != -1){//用while循环一直写,写到-1证明写完了outputStream.write(bytes,0,len);outputStream.flush();}//关闭资源outputStream.close();fileInputStream.close();} catch (Exception e) {e.printStackTrace();}}

3.3新增菜品

场景描述
在这里插入图片描述
开发逻辑
在这里插入图片描述
在这里插入图片描述
type为1是菜品,type为2是套餐
在这里插入图片描述

    /*** 根据条件查询分类数据,返回到菜品管理的下拉框里去*/@GetMapping("/list")public R<List<Category>> list(Category category){//条件构造器LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();//添加条件 type为1是菜品,为2是套餐queryWrapper.eq(category.getType()!= null,Category::getType,category.getType());//添加排序条件queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);List<Category> list = categoryService.list(queryWrapper);return R.success(list);}

接下来是多表存入,mp没有提供对应的api接口,需要自己写,老套路,service接口声明方法,实现类完善实现业务,控制层的controller直接调用

注意:这里还要加入事务进行控制,防止多表操作崩溃,同成功或同失败。@Transactional 开启事务;
@EnableTransactionManagement 在启动类加入,支持事务开启
根据前端传递数据,我们可以用DishDto对象来接收
分两张表存入,先将基本信息存入dish表,再将口味信息存入dish_flavor表
根据dish_flavor的表结构可以看到,我们还需要把dish_id存入dish_flavor表中
在这里插入图片描述

    /*** 新增菜品,同时保存对应的口味数据* @param dishDto*/@Overridepublic void saveWithFlavor(DishDto dishDto) {//保存菜品的基本信息到菜品表dish中this.save(dishDto);Long dishId = dishDto.getId();//菜品口味List<DishFlavor> flavors = dishDto.getFlavors();flavors = flavors.stream().map((item)->{item.setDishId(dishId);//把dish_id存入dish_flavor表中return item;}).collect(Collectors.toList());//保存菜品口味数据到菜品表中去dish_flavordishFlavorService.saveBatch(flavors);}

3.4修改菜品

修改菜品第一步回显数据,第二步更新数据

这里回显数据就涉及到了多表联查,先根据前端传递的菜品id查询出菜品基本信息,将菜品信息拷贝到dishDto对象中,再根据菜品id对dish_flavor表进行条件查询,将查询出来的口味信息也拷贝到dishDto对象里,最终返回dishDto

更新数据也是两表分别更新,先调用mp的updateById方法,更新dish表里的数据;然后dish_flavor表需要先清除该菜品下的口味信息,再将修改的口味信息插入到dish_flavor表中,更新dish_flavor表时,对于前端没有传递的字段数据,需要我们自己set进去
在这里插入图片描述

在这里插入图片描述
回显代码

    @Overridepublic DishDto getByIdWithFlavor(Long id) {//查询菜品基本信息,从dish表查询Dish dish = this.getById(id);DishDto dishDto = new DishDto();//将菜品基本信息拷贝到dishDto中BeanUtils.copyProperties(dish,dishDto);//查询当前菜品对应的口味信息,从dish_flavor表查询LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(DishFlavor::getDishId,dish.getId());//将该菜品的口味信息查询出来存入list集合中List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);//set到dto的属性里dishDto.setFlavors(flavors);return dishDto;}

更新代码

    @Override@Transactionalpublic void updateWithFlavor(DishDto dishDto) {//更新dish表的基本信息this.updateById(dishDto);//清理当前菜品对应口味数据---dish_flavor表的delete操作LambdaQueryWrapper<DishFlavor> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());dishFlavorService.remove(queryWrapper);//添加当前提交过来的口味数据---dish_flavor表的insert操作List<DishFlavor> flavors = dishDto.getFlavors();flavors = flavors.stream().map((item)->{item.setId(IdWorker.getId());item.setDishId(dishDto.getId());return item;}).collect(Collectors.toList());dishFlavorService.saveBatch(flavors);//批量保存}

3.5删除菜品

前端发送请求的url
在这里插入图片描述

在DishFlavor实体类中,在private Integer isDeleted;字段上加上@TableLogic注解,表示删除是逻辑删除,由mybatis-plus提供的,由于删除前需要判断菜品的售状态,这里将remove方法抽出来写再service实现类中

   /***套餐批量删除和单个删除* @param ids*/@Override@Transactionalpublic void deleteByIds(List<Long> ids) {//构造条件查询器LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();//先查询该菜品是否在售卖,如果是则抛出业务异常queryWrapper.in(ids!=null,Dish::getId,ids);List<Dish> list = this.list(queryWrapper);for (Dish dish : list) {Integer status = dish.getStatus();//如果不是在售卖,则可以删除if (status == 0){this.removeById(dish.getId());}else {//此时应该回滚,因为可能前面的删除了,但是后面的是正在售卖throw new CustomException("删除菜品中有正在售卖菜品,无法全部删除");}}}

controller直接调用service中的方法即可

    /*** 套餐批量删除和单个删除* @return*/@DeleteMappingpublic R<String> delete(@RequestParam("ids") List<Long> ids){//删除菜品  这里的删除是逻辑删除dishService.deleteByIds(ids);//删除菜品对应的口味  也是逻辑删除LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.in(DishFlavor::getDishId,ids);dishFlavorService.remove(queryWrapper);//清理所有菜品的缓存数据Set keys = redisTemplate.keys("dish_*");redisTemplate.delete(keys);return R.success("菜品删除成功");}

3.6菜品停售与起售(补充)

前端发送的url
在这里插入图片描述

业务逻辑
将前端传递的id集合来进行菜品的条件查询,然后遍历查询出来的数据集合,将前端传递的status直接set到每个dish对象中,完成菜品状态修改

    /*** 对菜品批量或者是单个 进行停售或者是起售* @return*/@PostMapping("/status/{status}")
//这个参数这里一定记得加注解才能获取到参数,否则这里非常容易出问题public R<String> status(@PathVariable("status") Integer status,@RequestParam List<Long> ids){//log.info("status:{}",status);//log.info("ids:{}",ids);LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper();queryWrapper.in(ids !=null,Dish::getId,ids);//根据传入的id集合进行批量查询List<Dish> list = dishService.list(queryWrapper);for (Dish dish : list) {if (dish != null){dish.setStatus(status);dishService.updateById(dish);}}//清理所有菜品的缓存数据Set keys = redisTemplate.keys("dish_*");redisTemplate.delete(keys);return R.success("售卖状态修改成功");}

4.套餐管理相关业务

4.1分页查询

和菜品分页差不多,将套餐信息分页查询出来,通过stream流方式,将套餐信息拷贝到SetmealDto中,再根据套餐id查询套餐分类对象,将套餐分类信息也拷贝到SetmealDto中,最后返回一个dtoPage,和菜品管理的分页几乎一样,就不上代码了

4.2新增套餐

在这里插入图片描述

和新增菜品差不多,根据前端发送的url请求,这里也是多表的操作,分别操作setmeal表和setmeal_dish表,前端提交的数据save到setmeal_dish表,再自己set补全etmeal_dish表数据。

4.3修改套餐

老套路,将setmeal表和setmeal_dish表两表数据查询出来,回显;再分别更新两表内容。和dish菜品管理一样的套路,不再赘述
我把它俩都抽到service实现类里写了

    @Overridepublic SetmealDto getByIdWithDishes(Long id) {//查询套餐基本信息,从setmeal表查询Setmeal setmeal = this.getById(id);SetmealDto setmealDto = new SetmealDto();BeanUtils.copyProperties(setmeal,setmealDto);//查询当前套餐对应的菜品信息,从setmeal_dish表查询LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(SetmealDish::getSetmealId,setmeal.getId());List<SetmealDish> dishes = setmealDishService.list(queryWrapper);setmealDto.setSetmealDishes(dishes);return setmealDto;}@Override@Transactionalpublic void updateWithDishes(SetmealDto setmealDto) {//更新setmeal表的基本信息this.updateById(setmealDto);//清理当前套餐对应的菜品数据---setmeal_dish表的delete操作LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(SetmealDish::getSetmealId,setmealDto.getId());setmealDishService.remove(queryWrapper);//添加当前提交过来的菜品数据---setmeal_dish表的insert操作List<SetmealDish> dishes = setmealDto.getSetmealDishes();dishes = dishes.stream().map((item)->{item.setId(IdWorker.getId());item.setSetmealId(setmealDto.getId());return item;}).collect(Collectors.toList());setmealDishService.saveBatch(dishes);}

4.4删除套餐

和删除菜品一样,也是需要先判断套餐状态,删除的时候,套餐下的关联关系也要删除掉,要处理setmeal和setmeal_dish两张表

   /*** 删除套餐,同时删除套餐和菜品关联数据* @param ids*/@Transactional@Overridepublic void removeWithDish(List<Long> ids) {//select count(*) from setmeal where id in (1,2,3) and status = 1;//查询套餐状态,确定是否可以删除LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.in(Setmeal::getId,ids);queryWrapper.eq(Setmeal::getStatus,1);int count = this.count(queryWrapper);if (count>0){//如果不能删除,抛出一个业务异常throw new CustomException("套餐正在售卖中,不能删除");}//如果可以删除,先删除套餐表中的数据——setmealthis.removeByIds(ids);//delete from setmeal_dish where setmeal_id in (1,2,3)LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);//删除关系表中的数据——setmeal_dish}

4.5套餐停售与起售(补充)

前端发送的url请求
在这里插入图片描述
跟菜品的起售和停售差不多

    /*** 批量起售停售*/@PostMapping("/status/{status}")@CacheEvict(value = "setmealCache", allEntries = true)public R<String> status(@PathVariable("status") Integer status,@RequestParam List<Long> ids) {LambdaQueryWrapper<Setmeal> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.in(ids!=null,Setmeal::getId,ids);List<Setmeal> setmeals = setmealService.list(queryWrapper);for (Setmeal setmeal : setmeals) {if (setmeal!=null){setmeal.setStatus(status);setmealService.updateById(setmeal);}}return R.success("售卖状态修改成功");}

5.订单明细(补充)

根据后台管理端的订单明细发出的url,可以判断就是个order表的分页查询
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

其实这样就很简单了,只是order的单表分页查询

    /*** 后台显示订单信息*/@GetMapping("/page")public R<Page> page(int page, int pageSize, Long number, String beginTime, String endTime) {log.info("page={},pageSize={},number={},beginTime={},endTime={}",page,pageSize,number,beginTime,endTime);//分页构造器对象Page<Orders> pageInfo = new Page<>(page,pageSize);//构造条件查询对象LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();//链式编程写查询条件queryWrapper.like(number!=null,Orders::getNumber,number)//前面加上判定条件是十分必要的,用户没有填写该数据,查询条件上就不添加它.gt(StringUtils.isNotBlank(beginTime),Orders::getOrderTime,beginTime)//大于起始时间.lt(StringUtils.isNotBlank(endTime),Orders::getOrderTime,endTime);//小于结束时间ordersService.page(pageInfo,queryWrapper);return R.success(pageInfo);}

但是我们发现一个问题,后端展示数据并没有用户名
在这里插入图片描述

其实没有用户名很正常因为我们的order表中并没有username字段,所以自然是查不出来,但是可以看到order表里是有user_id字段的,但是user表里也没有username这个字段,因此我想到了两个方法
在这里插入图片描述
方法一:user表中先添加username字段,然后后台显示订单信息这个分页查询得数据username

方法二:我喜欢简单(偷懒)点,直接把order表里的consignee(收货人)的名字取出来作为这里分页查询页面的用户名

在这里插入图片描述
这里将前端的username换成consignee,就可以显示用户名了
在这里插入图片描述

在这里插入图片描述

后台订单状态的修改

在这里插入图片描述

携带参数为status,这样就很明了,是根据订单id修改订单的状态,就是一个修改操作
在这里插入图片描述

    /*** 修改订单状态*/@PutMappingpublic R<String> orderStatusChange(@RequestBody Map<String,String> map){String id = map.get("id");Long orderId = Long.parseLong(id);//将接收到的id转为Long型Integer status = Integer.parseInt(map.get("status"));//转为Integer型if(orderId == null || status==null){return R.error("传入信息非法");}Orders orders = ordersService.getById(orderId);//根据订单id查询订单数据orders.setStatus(status);//修改订单对象里的数据ordersService.updateById(orders);return R.success("订单状态修改成功");}

三.移动端业务开发

1.用户登录与退出(退出为补充)

用户登录
点击登录发送请求
在这里插入图片描述

负载将手机号和验证码一起提交过来,我们可以用map的key-value的形式来接收,key是phone,value是code
在这里插入图片描述
代码实现
在这里插入图片描述

用户退出,根据请求的url编写退出功能
在这里插入图片描述

    /*** 用户退出* @param request* @return*/@PostMapping("/loginout")public R<String> loginout(HttpServletRequest request){request.getSession().removeAttribute("user");return R.success("退出成功");}

在这里插入图片描述

2.阿里云短信验证码

utils包下导入这个两个工具类,其实也可以去阿里云api文档复制,
在这里插入图片描述

在这里插入图片描述

至于如何获取accessKeyId,如下步骤
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
给新建用户添加权限
在这里插入图片描述

在UserController的发送验证码的方法中,调用阿里云提供的短信发送的api

在这里插入图片描述

@PostMapping("/sendMsg")public R<String> sendMsg(@RequestBody User user, HttpSession session){//获取手机号String phone = user.getPhone();if (StringUtils.isNotBlank(phone)){//生成随机的4位验证码String code = ValidateCodeUtils.generateValidateCode(4).toString();log.info("code={}",code);//调用阿里云提供的短信服务API完成发送短信SMSUtils.sendMessage("你自己的签名","你自己的模板code",phone,code);//需要将生成的验证码保存到Session//session.setAttribute(phone,code);//将将生成的验证码保存到Redis中,并且设置有效期为5分钟   phone是key,code是valueredisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);return R.success("手机验证码短信发送成功");}return R.error("手机验证码短信发送失败");}

在这里插入图片描述

最后效果如下
在这里插入图片描述

3.收货地址

首先分析这个地址管理要实现的功能,这里肯定是要把所有地址展示出来,所以需要一个查询所有的功能,其次就是新增地址有一个新增操作,还有修改地址,修改地址逻辑又分回显和修改,回显就是根据传的id查询,基本的sql可以通过mp提供的方法直接调用即可。
注意:这个查询所有,并不能直接用mp提供的list方法来查询,需要使用到条件查询,因为这里是根据user_id来查询的,并不是要展示地址表里所有的数据

在这里插入图片描述
查询所有的代码如下

@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);//SQL:select * from address_book where user_id = ? order by update_time descreturn R.success(addressBookService.list(queryWrapper));}

还有一个是将地址设置为默认地址,其实就是根据条件修改地址表里的is_default字段,给设置为1即为默认地址,但默认地址只能由一个,其逻辑为,若要更改默认地址,则将该用户的所有地址的is_default字段给更新为0,在把用户传来要设置成默认地址的地址id来修改该条地址的is_default字段为1即可。

    @PutMapping("default")public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {log.info("addressBook:{}", addressBook);LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<>();wrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());wrapper.set(AddressBook::getIsDefault, 0);//SQL:update address_book set is_default = 0 where user_id = ?addressBookService.update(wrapper);addressBook.setIsDefault(1);//SQL:update address_book set is_default = 1 where id = ?addressBookService.updateById(addressBook);return R.success(addressBook);}

下面是查询默认地址的代码

    /*** 查询默认地址*/@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 = 1AddressBook addressBook = addressBookService.getOne(queryWrapper);if (null == addressBook) {return R.error("没有找到该对象");} else {return R.success(addressBook);}}

4.菜品和套餐展示

此时后端已经将数据传递过来了,从负载可以看到传来的json,但前端没有进行展示
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

但是现在之前写后台管理端的那段查询所有菜品的代码已经不适用了,因为移动端还需要展示分类名称和口味数据,所以这里查询不能再返回dish对象,应该使用DishDto,返回一个DishDto泛型的list集合,代码需要修改一下

    @GetMapping("/list")public R<List<DishDto>> list(Dish dish){List<DishDto> dishDtoList = null;//动态构造keyString key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();//dish_13494852934_1//从redis中获取缓存数据(移动端使用redis缓存,将每个分类下查询的数据都放到缓存,避免重复查询,降低服务器压力)dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);//如果从redis获取的数据不为空,证明redis缓存了该数据,直接取出来返回,就无需查询数据库if (dishDtoList != null){return R.success(dishDtoList);}//构造查询条件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);dishDtoList = list.stream().map((item)->{DishDto dishDto = new DishDto();BeanUtils.copyProperties(item,dishDto);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());//如果redis不存在该数据,需要查询数据库,将查询菜品数据缓存到redis中redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);return R.success(dishDtoList);}

套餐数据的展示也是类似,根据发送的url,用setmeal对象来接收数据,写后端代码,展示的套餐没有口味之类的数据,所以直接返回Setmeal的list集合就行
在这里插入图片描述

    @GetMapping("/list")@Cacheable(value = "setmealCache", key = "#setmeal.categoryId + '_' + #setmeal.status")public R<List<Setmeal>> list(Setmeal setmeal){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);}

5.菜品选规格

点击菜品旁边的选规格,需要展示选择口味数据弹窗,在list查询方法追加以下代码
在这里插入图片描述

在这里插入图片描述

代码debug调试可以看到
在这里插入图片描述

6.套餐点击展示(补充)

当点击套餐时,会发送一个get请求,url如下,我猜测应该是展示该套餐中的菜品数据,我们可以给他返回一个DishDto,因为这个展示还涉及到一个菜品份数数据,根据f12响应那栏看出,这个数据光dish是展示不出来的,所以我们返回DishDto对象。
在这里插入图片描述

在这里插入图片描述

    /*** 点击查看套餐中的菜品*/@GetMapping("/dish/{id}")public R<List<DishDto>> dish(@PathVariable("id") Long SetmealId) {LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(SetmealDish::getSetmealId, SetmealId);//获取套餐里面的所有菜品  这个就是SetmealDish表里面的数据List<SetmealDish> list = setmealDishService.list(queryWrapper);List<DishDto> dishDtos = list.stream().map((setmealDish) -> {DishDto dishDto = new DishDto();//将套餐菜品关系表中的数据拷贝到dishDto中BeanUtils.copyProperties(setmealDish, dishDto);//这里是为了把套餐中的菜品的基本信息填充到dto中,比如菜品描述,菜品图片等菜品的基本信息Long dishId = setmealDish.getDishId();Dish dish = dishService.getById(dishId);//将菜品信息拷贝到dishDto中BeanUtils.copyProperties(dish, dishDto);return dishDto;}).collect(Collectors.toList());return R.success(dishDtos);}

在这里插入图片描述

7.购物车

购物车的表结构
在这里插入图片描述

记得把之前main.js下的注释打开,前面再展示菜品套餐时给注释了。
在这里插入图片描述

需求分析
按下图来看,首先加入购物车,是新增操作,往购物车表里save数据,还有购物车展示,是查询该用户下的购物车所有数据,按用户id查询,加和减,就是对应的修改表中的number字段。

但是注意加的时候,如果购物车没有数据,就需要save,如果有数据则直接修改number字段加1即可;减的时候如果购物车中的number为1了,再减就是remove该条数据,否则,直接修改number字段减1即可。

    /*** 从购物车中减掉*/@PostMapping("/sub")public R<String> remove(@RequestBody ShoppingCart shoppingCart){//设置用户id,指定当前时哪个用户的购物车数据Long currentId = BaseContext.getCurrentId();shoppingCart.setUserId(currentId);//查询当前菜品或者套餐是否在购物车中Long dishId = shoppingCart.getDishId();LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(ShoppingCart::getUserId,currentId);if (dishId!=null){//添加到购物车的是菜品queryWrapper.eq(ShoppingCart::getDishId,dishId);}else {//添加到购物车的是套餐queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());}//SQL:select * from shopping_cart where user_id = ? and dish_id = ?ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);if (cartServiceOne.getNumber()>1){//如果已经存在,就在原来数量基础上减去一Integer number = cartServiceOne.getNumber();cartServiceOne.setNumber(number-1);shoppingCartService.updateById(cartServiceOne);}else {shoppingCartService.remove(queryWrapper);}return R.success("减去成功");}

清空购物车,就是根据user_id,remove该用户下的所有数据

在这里插入图片描述

在这里插入图片描述

    @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);}
    /*** 清空购物车*/@DeleteMapping("/clean")public R<String> clean(){//SQL:delete from shopping_cart where user_id = ?LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());shoppingCartService.remove(queryWrapper);return R.success("清空购物车成功");}

8.下订单

需求分析
在这里插入图片描述

在这里插入图片描述

点击去支付根据发送的url请求,根据前端传递的数据,我们可以用order对象来接收
在这里插入图片描述
由于传过来的只有三条数据,order对象的其他属性需要我们补全再插入到订单表中
在这里插入图片描述

具体编码步骤如下
1.获取当前用户id
2.查询当前用户的购物车数据
3.查询用户数据
4.查询地址数据
5.向订单表插入数据,一条数据(由于前端只传递了addressBookId,payMethod,remark三条数据,其他的需要我们上面查询出来的购物车,用户,地址数据set到order对象中去,然后调用mp的save方法向数据库存入order)
6.向订单明细表插入数据,多条数据(和上面一样,需要手动将数据存入orderDetails对象中,然后调用mp的saveBatch方法将orderDetails插入表中)
7.清空购物车数据

9.收货地址删除(补充)

根据浏览器发送的url,不难看出大概就是一个地址表根据id删除的操作,但这个ids不是restful风格,是直接拼接的形式,所以形参那里传值为 @RequestParam(“ids”) Long id
在这里插入图片描述

    @DeleteMapping()public R<String> detele(@RequestParam("ids") Long id){log.info("id={}",id);//        if (id == null){
//            return R.error("请求异常");
//        }  //感觉这个判断没太大必要,前端传的id必不能为空,为空的话地址就不会展示出来,更不会有这个删除按钮存在,简单说为空的话,连删除的机会都没有,所以判断没太大必要//        LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
//        queryWrapper.eq(AddressBook::getId,id).eq(AddressBook::getUserId,BaseContext.getCurrentId());
//        addressBookService.remove(queryWrapper);//别人说直接使用这个removeById不太严谨,但我个人认为就是没登录状态进入该页面,是执行不了删除操作的,别说删除连查询,这个地址信息都不会展示,全被过滤器拦截了//所以用上面的条件查询好像意义不大,当然你也可以放弃这个简单的removeById,用上面注释的条件删除addressBookService.removeById(id);return R.success("删除成功");}

在这里插入图片描述

10.用户支付后查看订单(补充)

浏览器发送的url如下,就是将order_detail表里的数据根据订单id进行条件查询,本来以为只是一个简单单表分页查询,结果踩坑了

orderDetail的表结构
在这里插入图片描述

order.html前端页面还需要下面的数据,需要后端传递过去,只是一个单表分页查询出来的orderDetail对象是没有订单名称等数据的

在这里插入图片描述

在这里插入图片描述

因此这里我们需要使用OrderDto对象,将订单的数据和订单明细的数据都存入OrderDto对象中,返回的是Dto分页查询的数据
创建一个OrderDto

@Data
public class OrderDto extends Orders {private List<OrderDetail> orderDetails;
}

OrderService接口声明一个根据订单id来查询订单明细的数据的方法

public List<OrderDetail> getOrderDetailListByOrderId(Long orderId);

OrdersServiceImpl实现类中实现这个条件查询方法

    public List<OrderDetail> getOrderDetailListByOrderId(Long orderId){LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(OrderDetail::getOrderId, orderId);//根据order表的条件查询出order_detail的数据,因为一个订单可能有多条菜品数据List<OrderDetail> orderDetailList = orderDetailService.list(queryWrapper);return orderDetailList;}

在这里插入图片描述
订单数据的分页查询
在这里插入图片描述

OrderController类下,支付后查看订单功能的代码

    @GetMapping("/userPage")public R<Page> page(int page, int pageSize){//分页构造器对象Page<Orders> pageInfo = new Page<>(page,pageSize);Page<OrderDto> pageDto = new Page<>(page,pageSize);//构造条件查询对象LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Orders::getUserId, BaseContext.getCurrentId());//这里是直接把当前用户分页的全部结果查询出来,要添加用户id作为查询条件,否则会出现用户可以查询到其他用户的订单情况//添加排序条件,根据更新时间降序排列queryWrapper.orderByDesc(Orders::getOrderTime);//这里是把所有的订单分页查询出来ordersService.page(pageInfo,queryWrapper);//对OrderDto进行属性赋值List<Orders> records = pageInfo.getRecords();List<OrderDto> orderDtoList = records.stream().map((item) ->{//item其实就是分页查询出来的每个订单对象OrderDto orderDto = new OrderDto();//此时的orderDto对象里面orderDetails属性还是空 下面准备为它赋值Long orderId = item.getId();//获取订单id//调用根据订单id条件查询订单明细数据的方法,把查询出来订单明细数据存入orderDetailListList<OrderDetail> orderDetailList = ordersService.getOrderDetailListByOrderId(orderId);BeanUtils.copyProperties(item,orderDto);//把订单对象的数据复制到orderDto中//对orderDto进行OrderDetails属性的赋值orderDto.setOrderDetails(orderDetailList);return orderDto;}).collect(Collectors.toList());//将订单分页查询的订单数据以外的内容复制到pageDto中,不清楚可以对着图看BeanUtils.copyProperties(pageInfo,pageDto,"records");pageDto.setRecords(orderDtoList);return R.success(pageDto);}

因为前端页面传的是分页数据,所以后端就实现了OrderDto的分页查询,其实我也有想过只查询当前支付的这个订单,就是根据订单id,查询OrderDto,也不是很难,就根据订单id将查询的订单数据和订单明细数据都存入DishDto对象里,然后返回DishDto对象就完了。

但前端传递过来的是分页查询的数据有pageSize和page,摆明了是分页查询,并没有传订单id参数,所以我也无法判断当前订单对应的id,无法根据订单id查询订单明细数据。
在这里插入图片描述
在这里插入图片描述

11.再来一单(补充)

在这里插入图片描述

传递过来的是订单id

在这里插入图片描述
①通过上面传递的orderId获取订单明细的数据
②把订单明细的数据的数据塞到购物车表中,不过在此之前要先把购物车表中的数据给清除(清除的是当前登录用户的购物车表中的数据)

    /*** 再来一单* @param map* @return*/@PostMapping("/again")public R<String> againSubmit(@RequestBody Map<String,String> map){String ids = map.get("id");long id = Long.parseLong(ids);LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(OrderDetail::getOrderId,id);//获取该订单对应的所有的订单明细表List<OrderDetail> orderDetailList = orderDetailService.list(queryWrapper);//通过用户id把原来的购物车给清空,这里的clean方法就是之前的购物车清空方法,我给写到service中去了,这样可以通过接口复用代码shoppingCartService.clean();//获取用户idLong userId = BaseContext.getCurrentId();List<ShoppingCart> shoppingCartList = orderDetailList.stream().map((item) -> {//把从order表中和order_details表中获取到的数据赋值给这个购物车对象ShoppingCart shoppingCart = new ShoppingCart();shoppingCart.setUserId(userId);shoppingCart.setImage(item.getImage());Long dishId = item.getDishId();Long setmealId = item.getSetmealId();if (dishId != null) {//如果是菜品那就添加菜品的查询条件shoppingCart.setDishId(dishId);} else {//添加到购物车的是套餐shoppingCart.setSetmealId(setmealId);}shoppingCart.setName(item.getName());shoppingCart.setDishFlavor(item.getDishFlavor());shoppingCart.setNumber(item.getNumber());shoppingCart.setAmount(item.getAmount());shoppingCart.setCreateTime(LocalDateTime.now());return shoppingCart;}).collect(Collectors.toList());//把携带数据的购物车批量插入购物车表  这个批量保存的方法要使用熟练!!!shoppingCartService.saveBatch(shoppingCartList);return R.success("操作成功");}

在order.html中可以看见这样一段前端代码:


<div class="btn" v-if="order.status === 4">  //状态是4才会让你点击下面这个再来一单<div class="btnAgain" @click="addOrderAgain(order)">再来一单</div>
</div>

在这里插入图片描述
由于这里没有写后台的确认订单功能,所以这里通过数据库修改订单状态来完成测试!
在这里插入图片描述

测试结果,购物车回显了数据
在这里插入图片描述

四.项目优化

1.使用Redis缓存

1.1缓存验证码

1.pom中导入redis坐标

		<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>

2.UserController中加入对应操作
先自动装配RedisTemplate

@Autowiredprivate RedisTemplate redisTemplate;

然后把之前验证码放入session中给替换成redis

redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);

在这里插入图片描述

接下来在用户登录的方法里,我们需要从redis中获取验证码,而且登录成功后把该验证码删除

Object codeInSession = redisTemplate.opsForValue().get(phone);
            //如果登录成功,删除redis中的验证码redisTemplate.delete(phone);

在这里插入图片描述

创建一个redis的配置类,对key进行序列化,不然redis客户端中可以看到key并非就是你创建的key的名称,它是\xAC\xED\x00\x05t\x00\key名,大概是这种类型,不便于阅读,我们让它序列化,value就不必序列化了,因为最后idea会对其序列化

@Configuration
public class RedisConfig extends CachingConfigurerSupport {@Beanpublic RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();//默认的Key序列化器为:JdkSerializationRedisSerializerredisTemplate.setKeySerializer(new StringRedisSerializer()); // key序列化//redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // value序列化redisTemplate.setConnectionFactory(connectionFactory);return redisTemplate;}
}

在这里插入图片描述

1.2缓存菜品查询数据

首先分析移动端菜品查询,每个分类,比如湘菜,川菜,每次点击都需要再次重新查询数据库,不仅压力更大而且造成资源浪费,我们可以把这些查询的数据,按菜品分类给存入redis中,设置其30分钟生存周期,这样再次点击查看,就不会再查询数据库,直接从redis中获取数据,降低服务器压力也避免资源浪费。

缓存逻辑
我们先动态构造唯一key值,然后根据key来获取value,接下来判断value是否为空,若不为空则表示redis中有该分类下的数据,直接返回;若为空则需要去数据库查询数据,然后再把查询的数据放入redis缓存中,下次再查询直接走redis缓存,不用再次查询数据库。

在这里插入图片描述

在这里插入图片描述

可以看到redis已经缓存了菜品信息

在这里插入图片描述

但是使用缓存的话,修改,新增和删除是要清理缓存的,不然如果后端数据更改后,再次查询仍然是走的缓存,而缓存的数据没有改变,查出来的就不是最新的数据了,造成了数据偏差。因此我们在修改,新增,删除方法中清理缓存

        //清理所有菜品的缓存数据//Set keys = redisTemplate.keys("dish_*");//redisTemplate.delete(keys);//只清理该修改菜品的分类下缓存的数据,精确清理,因为redis可能已经缓存了好几个分类下的数据,全删太浪费String key = "dish_" + dishDto.getCategoryId() + "_1";redisTemplate.delete(key);

在这里插入图片描述

1.3Spring Cache缓存套餐数据

1.pom文件中导入Spring Cache坐标

		<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId></dependency>

2.启动类上加上注解开启缓存功能

@EnableCaching

在这里插入图片描述

3.使用Spring Cache注解的方式开启缓存
在这里插入图片描述

注意:这里方法返回的结果R,它的类需要实现序列化接口,否则会报错,无法缓存。
在这里插入图片描述

在这里插入图片描述

缓存查询的套餐信息

@Cacheable(value = "setmealCache", key = "#setmeal.categoryId + '_' + #setmeal.status")

在这里插入图片描述

清除缓存,需要在新增,修改,删除方法上添加该注解

@CacheEvict(value = "setmealCache", allEntries = true)

在这里插入图片描述

再次访问,可以看到redis下已经有了套餐数据,仍然是按套餐分类缓存的
在这里插入图片描述

2.读写分离

读与写所有压力都由一台数据库承担,压力大,数据库服务器磁盘损坏或数据丢失,单点故障
在这里插入图片描述

2.1mysql主从复制

MySQL主从复制就是一个异步复制过程,从库slave从主库master进行日志的复制然后再解析日志并应用到自身,最终实现从库数据和主库数据保持一致。MySQL主从复制是MySQL数据库自带功能。
在这里插入图片描述

注意:这里至少要有两台服务器分别安装MySQL并且启动成功,可以用虚拟机再克隆出一个作为从库

1.master将改变记录到二进制日志binary log
修改MySQL数据库的配置文件 vim /etc/my.cnf
[mysqld]下添加如下代码

log-bin=mysql-bin  #启用二进制日志
server-id=100  #id作为服务器唯一标识,不一定要100,只要不重复即可

在这里插入图片描述
保存退出后重启mysql服务

systemctl restart mysqld

我们需要在master下创建一个用户,给他授予权限,slave才能通过该用户来拷贝它记录的日志文件,先登录到mysql

mysql -uroot -p

创建一个用户叫xiaoming,密码是Root@123456,并给该用户授予REPLICATION SLAVE权限

GRANT REPLICATION SLAVE ON *.* to 'xiaoming'@'%' identified by 'Root@123456';

如果报错
在这里插入图片描述
mysql8需要先创建用户才能授权,用下面的代码

create user xiaoming identified by 'Root@123456';
grant replication slave on *.* to xiaoming;

查看主库状态

show master status;

在这里插入图片描述
接下里主库就不要操作了,一旦执行操作,记录位置会发生变化,就不是698了,这个位置和文件名一会在从库中使用到。

2.slave将master的binary log拷贝到它的中继日志relay log
先修改MySQL数据库的配置文件 vim /etc/my.cnf
[mysqld]下添加如下代码

server-id=101 #必须是唯一的id,不能重复

在这里插入图片描述

保存退出后重启mysql服务

systemctl restart mysqld

登录MySQL,执行以下代码

mysql -uroot -p
change master to master_host='填入master的ip',master_user='上面创建的用户',master_password='上面设置的',master_log_file='主库刚查的日志名称',master_log_pos=刚查的记录位置;

启动slave

start slave

在这里插入图片描述

查看从库状态

show slave status\G

如果你是MySQL8,且报错信息为
在这里插入图片描述
可以看看这个解决方法https://www.modb.pro/db/29919

Slave_IO_Running和Slave_SQL_Running都为no,可以看看这个解决方法https://www.cnblogs.com/MENGSHIYU/p/11978489.html
完事后一定要重启MySQL服务

systemctl restart mysqld

改好后从库的两个io都为yes即可
在这里插入图片描述

3.slave重做中继日志中的事件,将改变应用到自身的数据库中

Navicat中根据ip新建主从两个连接,主库创建数据库和表后,从库刷新直接显示出来
在这里插入图片描述
主库对表的任何修改操作,从库的表F5即可看到更新。

2.2Sharding-JDBC实现读写分离

主库执行写操作,从库执行读操作,Sharding-JDBC是轻量级java框架,在java的JDBC层提供服务,只需要导入它的坐标即可使用它封装的api,轻松实现数据库的读写分离。
1.pom文件导入Sharding-JDBC坐标

		<dependency><groupId>org.apache.shardingsphere</groupId><artifactId>sharding-jdbc-spring-boot-starter</artifactId>  </dependency>

2.yml配置文件中添加配置信息,ip换成你自己的主从库ip,password也替换成自己的密码

spring:shardingsphere:datasource:names:master,slave# 主数据源master:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.231.128:3306/rw?characterEncoding=utf-8username: rootpassword: 123456# 从数据源slave:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.231.129:3306/rw?characterEncoding=utf-8username: rootpassword: 123456masterslave:# 读写分离配置load-balance-algorithm-type: round_robin  #轮询策略,负载均衡# 最终的数据源名称name: dataSource# 主库数据源名称master-data-source-name: master# 从库数据源名称列表,多个逗号分隔slave-data-source-names: slaveprops:sql:show: true #开启SQL显示,默认false

3.配置文件中开启允许bean定义覆盖
因为Sharding-JDBC和Druid都有数据源定义的配置类,都想创建数据源对象,产生冲突,我们要打开允许bean的覆盖,后创建的datasource会覆盖前面的,具体代码如下

spring:main:allow-bean-definition-overriding: true

配置完毕就可以了,自动读写分离,查询走从库,增删改走主库。

2.3项目实现读写分离

1.在前面建主库中,新建一个reggie数据库,运行之前的sql文件,刷新可以看到项目的表结构和数据已经导入了,从库刷新,数据也出来了。
2.和上面一样,导坐标,yml加入对应的配置信息,把数据库名字改成reggie就行了,就完成了读写分离。

3.使用Nginx服务器

Nginx是一款轻量级Web服务器/反向代理服务器及电子邮件代理服务器。其优点是占用内存少,并发能力强
nginx的配置文件分为三个部分
全局块:events块之前的配置,和nginx运行相关的全局配置
events块:和网络连接相关配置
http块:代理,缓存,日志记录,虚拟主机配置,一般主要是配置这块内容

3.1Nginx部署静态资源

只需要把静态资源直接放在nginx的html目录下即可
在这里插入图片描述
访问时就是ip/页面名称

3.2反向代理

正向代理,说简单点就是梯子,客户端通过代理服务器向目标服务器访问,发生在客户端,客户端知晓代理服务。

反向代理是客户端访问代理服务器,代理服务器转发给目标服务器,发生在服务端,客户端并不知道代理服务器的存在。
正向代理隐藏的是用户,反向代理隐藏的是服务器

在这里插入图片描述

配置反向代理就是在反向代理的服务器上,配置其nginx.conf配置文件,在http块中加入以下代码

    server {listen       82;server_name  localhost;location / {proxy_pass http://目标服务器ip:8080;#将请求转发到指定服务器}}

并开放代理服务器的82端口号,重新加载防火墙

firewall-cmd --zone=public --add-port=82/tcp --permanentfirewall-cmd --reload

重新加载nginx
注意这里nginx要给配置成环境变量,详情见nginx的基本配置

nginx -s reload

访问时访问的是反向代理服务器的ip,反向代理服务器再把请求转发到目标服务器上

3.3负载均衡

应用集群:将同一应用部署到多台服务器上,组成应用集群,接收负载均衡器分发的请求,进行业务处理并返回响应数据
负载均衡器:将用户请求根据对应的负载均衡算法分发到应用集群中的一台服务器进行处理
在这里插入图片描述

负载均衡服务器上配置nginx.conf配置

在这里插入图片描述
默认是轮询策略,第一次这台服务器,第二次下台服务器
配置完毕后重新加载nginx服务

nginx -s reload

这样使用集群方式,降低单台服务器的压力,提高访问效率,避免了单点故障问题。

4.前后端分离开发

工程结构进行拆分,项目部署也发生变化
在这里插入图片描述

开发流程
在这里插入图片描述
接口(API接口)就是一个http的请求地址,主要就是去定义:请求路径,请求方式,请求参数,响应数据等内容。

4.1YApi

就是提供接口管理服务,让接口开发更简单高效,让接口管理更具可读性和维护性。感觉还不如Apifox方便,这里我就用Apifox了。

4.2Swagger

1.pom导入swagger的解决方案Knife4j的坐标

		<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>3.0.2</version></dependency>

2.导入kknife4j相关配置(WebMvcConfig)
WebMvcConfig配置类下开启swagger文档功能加上以下注解

@EnableKnife4j
@Configuration

定义以下两个方法

@Beanpublic Docket createRestApi() {// 文档类型return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.controller"))//扫描controller包下的所有api接口.paths(PathSelectors.any()).build();}private ApiInfo apiInfo() {return new ApiInfoBuilder().title("瑞吉外卖").version("1.0").description("瑞吉外卖接口文档").build();}

3.设置静态资源映射,否则接口文档页面无法访问
就是在addResourceHandlers方法中添加以下两行代码

        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");

在这里插入图片描述

4.LoginCheckFilter过滤器设置放行这些url,设置为不需要处理的请求路径
在LoginCheckFilter的放行列表中添加以下url

                "/doc.html","/webjars/**","/swagger-resources","/v2/api-docs"

项目启动后直接访问http://localhost:8080/doc.html即可看到。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.3项目部署

部署架构
![在这里插入图片描述](https://img-blog.csdnimg.cn/5e6e464e0a164384a34bd15f01163891.png

部署环境说明,三台服务器
部署环境说明

前端项目部署
1.前端服务器安装nginx,将前端资源上传到nginx下的html目录中

2.配置conf目录下的nginx.conf文件
配置信息如下

server{listen 80;server_name localhost;
#静态资源配置location /{root html/dist;index index.html;}
#请求转发代理,重写URL+转发location ^~ /api/{rewrite ^/api/(.*)$ /$1 break;proxy_pass http://后端服务ip:端口号;}
#其他error_page 500 502 503 504 /50x.html;location = /50x.html{root html;}
}

在这里插入图片描述

反向代理的配置分析
在这里插入图片描述

后端项目部署
采用脚本自动部署
1.后端服务器要安装jdk,maven,git,mysql,从git仓库克隆项目下来

git clone 远程仓库的url

2.添加一个reggieStart脚本,具体代码如下,放在/usr/local/app目录下,执行脚本即可自动拉取代码,打包并后台部署。

#!/bin/sh
echo =================================
echo  自动化部署脚本启动
echo =================================echo 停止原来运行中的工程
APP_NAME=reggie_take_outtpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'`
if [ ${tpid} ]; thenecho 'Stop Process...'kill -15 $tpid
fi
sleep 2
tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'`
if [ ${tpid} ]; thenecho 'Kill Process!'kill -9 $tpid
elseecho 'Stop Success!'
fiecho 准备从Git仓库拉取最新代码
cd /usr/local/app/reggie_take_outecho 开始从Git仓库拉取最新代码
git pull
echo 代码拉取完成echo 开始打包
output=`mvn clean package -Dmaven.test.skip=true`cd targetecho 启动项目
nohup java -jar reggie_take_out-0.0.1-SNAPSHOT.jar &> server.log &
echo 项目启动完成

给脚本授予执行权限

chmod 777 reggieStart.sh

运行脚本即完成后台项目部署,具体的jar包名称在项目文件夹下的target目录下
在这里插入图片描述

不知不觉已经写了5w字了,后面如果我对项目有新的理解,再继续补充完善吧,个人水平有限,不足之处多多包涵,也欢迎各位指正,感谢您的阅览。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/496588.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java基础八股

基础概念与常识 Java 语言有哪些特点? 简单易学&#xff1b;面向对象&#xff08;封装&#xff0c;继承&#xff0c;多态&#xff09;&#xff1b;平台无关性&#xff08; Java 虚拟机实现平台无关性&#xff09;&#xff1b;支持多线程&#xff08; C 语言没有内置的多线程…

Python入门到精通(九)——Python数据可视化

Python数据可视化 一、JSON数据格式 1、定义 2、python数据和JSON数据转换 二、pyecharts 三、折线图 四、地图 五、动态柱状图 一、JSON数据格式 1、定义 JSON是一种轻量级的数据交互格式。可以按照JSON指定的格式去组织和封装数据JSON本质上是一个带有特定格式的字符…

Python学习系列 -初探标准库之logging库

系列文章目录 第一章 初始 Python 第二章 认识 Python 变量、类型、运算符 第三章 认识 条件分支、循环结构 第四章 认识 Python的五种数据结构 第五章 认识 Python 函数、模块 第六章 认识面向对象三大特性 第七章 初探标准库之os库 第八章 初探标准库之pathlib库 第九章 初探…

卷积神经网络介绍

卷积神经网络(Convolutional Neural Networks&#xff0c;CNN) 网络的组件&#xff1a;卷积层&#xff0c;池化层&#xff0c;激活层和全连接层。 CNN主要由以下层构造而成&#xff1a; 卷积层&#xff1a;Convolutional layer&#xff08;CONV&#xff09;池化层&#xff1a…

如何在windows系统部署Lychee网站,并结合内网穿透打造个人云图床

文章目录 1.前言2. Lychee网站搭建2.1. Lychee下载和安装2.2 Lychee网页测试2.3 cpolar的安装和注册 3.本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4.公网访问测试5.结语 1.前言 图床作为图片集中存放的服务网站&#xff0c;可以看做是云存储的一部分&#xff0c;既可…

批量自动加好友,轻松拓展微信人脉圈子

在当今社交化的时代&#xff0c;拓展社交圈子已经成为许多人努力追求的目标。而微信作为中国人群中最主流的社交工具之一&#xff0c;更是成为人们拓展社交圈子的重要场所。在这样的背景下&#xff0c;有没有一种简单而高效的方式来扩大微信人脉圈子呢&#xff1f;答案是肯定的…

【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)

目录 1、锁的策略 1.1、乐观锁和悲观锁 1.2、轻量级锁和重量级锁 1.3、自旋锁和挂起等待锁 1.4、普通互斥锁和读写锁 1.5、公平锁和非公平锁 1.6、可重入锁和不可重入锁 2、synchronized 内部的升级与优化过程 2.1、锁的升级/膨胀 2.1.1、偏向锁阶段 2.1.2、轻量级锁…

RunnerGo UI自动化测试脚本如何配置

RunnerGo提供从API管理到API性能再到可视化的API自动化、UI自动化测试功能模块&#xff0c;覆盖了整个产品测试周期。 RunnerGo UI自动化基于Selenium浏览器自动化方案构建&#xff0c;内嵌高度可复用的测试脚本&#xff0c;测试团队无需复杂的代码编写即可开展低代码的自动化…

第十四天-网络爬虫基础

1.什么是爬虫 1.爬虫&#xff08;又被称为网页蜘蛛&#xff0c;网络机器人&#xff09;&#xff0c;是按照一定规则&#xff0c;自动的抓取万维网中的程序或者脚本&#xff0c;是搜索引擎的重要组成&#xff1b;比如&#xff1a;百度、 2.爬虫应用&#xff1a;1.搜索引擎&…

【web APIs】3、(学习笔记)有案例!

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、概念其他事件页面加载事件元素滚动事件页面尺寸事件 元素尺寸与位置 二、案例举例电梯导航 前言 掌握阻止事件冒泡的方法理解事件委托的实现原理 一、概念…

✈️ 运输行业有哪些令人无法接受的网络安全事件?

Positive Technologies 专家举例说明 去年 9 月&#xff0c;Leonardo 订票系统遭到大规模 DDoS 攻击&#xff0c;导致全球网络中断&#xff0c;Aeroflot 航班难以办理登机手续。这导致谢列梅捷沃机场多趟航班延误起飞。 这只是 Positive Technologies 专家在其 2023 年交通领…

Golang使用Swag搭建api文档

1. 简介 Gin是Golang目前最为常用的Web框架之一。 公司项目验收需要API接口设计说明书&#xff08;Golang后端服务基于Gin框架编写&#xff09;&#xff0c;编写任务自然就落到了我们研发人员身上。 项目经理提供了文档模板&#xff0c;让我们参考模板来手动编写&#xff0c;要…