当前位置: 首页 > news >正文

瑞吉外卖项目

目录

Day01业务开发

一、项目总体介绍与展示

二、软件开发整体介绍

(一)软件开发流程

三、瑞吉外卖项目介绍

(一)项目介绍

(二)技术选型功能架构

1.技术选型——

​编辑2.功能架构—— 

​编辑 (三)角色

四、开发环境搭建

(一)数据库环境搭建

(二)maven项目搭建

启动测试

导入

创建配置映射类

五、后台登录功能开发

(一)需求分析

(二)代码开发

1.创建实体类Employee,和employee表进行映射

2.编写controller层

3.导入通用返回结果类

六、后台退出功能开发

(一)需求分析

完善登陆(添加过滤器)

Day02员工管理业务开发

一、启用/禁用员工账号显示不同

(一)需求分析

管理员——

普通员工——

(二)代码开发

(四)代码修复

1.问题描述

2.具体修复步骤

1.自定义的全局转化器

2.在MVC配置文件中追加上面的自定义的全局转化器

二、编辑员工信息

Day03分类业务开发

一、公共字段自动填充

(一)需求分析

(二)代码开发

(1)目前问题——createTime和UpdateTime是固定值,应该设置为动态值

(2)解决方案——threadlocal类解决

(4)什么是Threadlocal——Thread的局部变量

(5)代码流程

二、新增分类

 三、分类信息分页查询

 四、删除分类

五、修改分类 

Day04菜品管理业务开发

一、文件上传下载

二、新增菜品

 三、菜品信息分页查询

四. 修改菜品(回显和保存修改都是两张表)

菜品信息的回显:

保存修改:(重点)

 Day05套餐管理

1. 新增套餐

2. 套餐分页查询

 3.删除套餐信息

前台开发(手机端)

账户登陆

短信发送

代码实现

短信验证码登陆

发送验证码(给的资料有点残缺,这里修改了)

地址管理

导入用户地址簿

手机端展示

菜品展示

 套餐展示

购物车

添加菜品和套餐进购物车

查看购物车

清空购物车

减少购物车点菜品或者套餐

用户订单

用户下单功能

 用户查看自己订单


Day01业务开发

一、项目总体介绍与展示

1.移动端与管理后台展示

 2.项目上线后

 3.管理后台登录

 4.管理后台详细页面

二、软件开发整体介绍

(一)软件开发流程

 二)角色分工

 (三)软件环境

三、瑞吉外卖项目介绍

(一)项目介绍

 两端应用

(二)技术选型功能架构
1.技术选型——
2.功能架构—— 
 (三)角色

四、开发环境搭建

(一)数据库环境搭建

(二)maven项目搭建

pom文件

server:
  port: 9001
spring:
  application:
    name: ccTakeOut
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/ruiji?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: 333

  redis:
    host: localhost # 本地IP 或是 虚拟机IP
    port: 6379
    #    password: root
    database: 0  # 默认使用 0号db
  cache:
    redis:
      time-to-live: 1800000  # 设置缓存数据的过期时间,30分钟

mybatis-plus:
  configuration:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,开启按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID

启动测试

创建测试类并启动

导入前端页面 

导入

在默认页面和前台页面的情况下,直接把这俩拖到resource目录下直接访问是访问不到的,因为被mvc框架拦截了
所以我们要编写一个映射类放行这些资源

创建配置映射类

 

 

访问成功 

五、后台登录功能开发

(一)需求分析

 2.浏览器F12进入追踪服务

 

3.整体流程梳理如下——> 

 为什么点击了登录按钮会转发请求到后端路径/employee/login?

 

4.code、data、meg信息 

(二)代码开发
1.创建实体类Employee,和employee表进行映射

1.MP的智能命名映射

2.编写controller层

使用final+@RequiredArgsconstructor注入

3.导入通用返回结果类

通用返回结果,服务端响应的数据最终都会封装成此对象

 

4.controller具体实现 

 5.加密算法与登录全流程

这里两个字符串的比较没法用!=来实现,只能equals再取反来判断
直接上代码,这里没有涉及service层的操作 

查看缓存在浏览器里面的信息—— 

六、后台退出功能开发

(一)需求分析

 

    /*** @param request 删除request作用域中的session对象,就按登陆的request.getSession().setAttribute("employ",empResult.getId());删除employee就行* @return*/@PostMapping("/logout")public Result login(HttpServletRequest request) {//尝试删除try {request.getSession().removeAttribute("employ");}catch (Exception e){//删除失败return Result.error("登出失败");}return Result.success("登出成功");}

完善登陆(添加过滤器)

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

判断用户的登陆状态这块之前因为存入session里面有一个名为employee的对象,那么只需要看看这个session还在不在就知道他是否在登陆状态
注意,想存或者想获取的话,就都得用HttpServletRequest的对象来进行获取,别的request对象拿不到的 

 前端拦截器完成跳转到登陆页面,不在后端做处理

Day02员工管理业务开发

本章内容介绍

新增员工
员工信息分页查询
启用/禁用员工账号
编辑员工信息

新增员工功能,(前端对手机号和身份证号长度做了一个校验)

 

改造一下Employee实体类,通用id雪花自增算法来新增id
在这里插入图片描述
这里用service接口继承的MybatisPlus的功能
在这里插入图片描述
注入一下就可以使用了,插入方法
在这里插入图片描述
基本上都是自动CRUD,访问路径:com.cc.controller.EmployeeController 

@PostMapping
public R<String> addEmployee(HttpServletRequest request,@RequestBody Employee employee){//        log.info(employee.toString());//设置初始密码employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes(StandardCharsets.UTF_8)));employee.setCreateTime(LocalDateTime.now());employee.setUpdateTime(LocalDateTime.now());employee.setCreateUser((long)request.getSession().getAttribute("id"));employee.setUpdateUser((long)request.getSession().getAttribute("id"));employeeService.save(employee);return R.success("新增员工成功");
}

处理数据库插入重复名字异常

image-20230424195410588

全局异常处理器来处理异常

关键点在@ControllerAdvice和@ExceptionHandler,一个用来拦截方法,一个用来处理异常

@ControllerAdvice捕获方法后,有异常就处理

@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody//java对象转为json格式的数据
@Slf4j
public class GlobalExceptionHandler {

    //用来捕获插入重复数据异常
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler (SQLIntegrityConstraintViolationException exception){
        log.error(exception.getMessage());
        return R.error("failed");
    }
}

//用来捕获插入重复数据异常
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler (SQLIntegrityConstraintViolationException exception){
    if (exception.getMessage().contains("Duplicate entry")){
        String[] split = exception.getMessage().split(" ");//根据空格符分割数组
        String msg = split[2] + "已存在";
        return R.error(msg);
    }
    return R.error("unknown error");
}
 

 

三、员工信息分页查询 

page对象内部 

 里面包含了查询构造器的使用
具体的细节在这个包下:com.cc.controller.EmployeeController.page

    /*** 分页展示员工列表接口、查询某个员工* @param page 查询第几页* @param pageSize 每页一共几条数据* @param name 查询名字=name的数据* @return 返回Page页*/@GetMapping("/page")public Result<Page> page(int page, int pageSize,String name){//分页构造器,Page(第几页, 查几条)Page pageInfo = new Page(page, pageSize);//查询构造器LambdaQueryWrapper<Employee> lambdaQueryWrapper = new LambdaQueryWrapper();//过滤条件.like(什么条件下启用模糊查询,模糊查询字段,被模糊插叙的名称)lambdaQueryWrapper.like(!StringUtils.isEmpty(name), Employee::getName, name);//添加排序lambdaQueryWrapper.orderByDesc(Employee::getCreateTime);//查询分页、自动更新employeeService.page(pageInfo, lambdaQueryWrapper);//返回查询结果 return Result.success(pageInfo);}

 

一、启用/禁用员工账号显示不同

(一)需求分析
管理员——

在这里插入图片描述

普通员工——

在这里插入图片描述

前端页面禁用启用按钮实现分析
在这里插入图片描述

在这里插入图片描述

(二)代码开发

后端流程梳理

 

  /*** 根据id修改员工信息* @param employee* @return*/@PutMappingpublic R<String> update(HttpServletRequest request,@RequestBody Employee employee){log.info(employee.toString());Long empId = (Long)request.getSession().getAttribute("employee");employee.setUpdateTime(LocalDateTime.now());employee.setUpdateUser(empId);employeeService.updateById(employee);return R.success("员工信息修改成功");}

运行时发现错误
在这里插入图片描述

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

(四)代码修复
1.问题描述

在这里插入图片描述

2.具体修复步骤

在这里插入图片描述

1.自定义的全局转化器

在这里插入图片描述

2.在MVC配置文件中追加上面的自定义的全局转化器

在这里插入图片描述

重新启动后,一切都可以了——

在这里插入图片描述

//JacksonObjectMapper
package com.itzq.reggie.common;import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;/*** 对象映射器:基于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框架的消息转换器* @param converters*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {//创建消息转换器对象MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();//设置对象转化器,底层使用jackson将java对象转为jsonmessageConverter.setObjectMapper(new JacksonObjectMapper());//将上面的消息转换器对象追加到mvc框架的转换器集合当中(index设置为0,表示设置在第一个位置,避免被其它转换器接收,从而达不到想要的功能)converters.add(0,messageConverter);}

二、编辑员工信息

@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){log.info("根据id查询员工信息。。。");Employee employee = employeeService.getById(id);if (employee != null){return R.success(employee);}return R.error("没有查询到该员工信息");
}

 修改回显数据后,点击保存,会发送一个update的请求给后端,前面我们已经写了这个update的controller,所以只需要在前端跳转发请求就行;这样就实现了方法的复用,减少了代码两;

Day03分类业务开发

一、公共字段自动填充

(一)需求分析
(二)代码开发

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert]++++++");
        log.info(metaObject.toString());
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", new Long(1));
        metaObject.setValue("updateUser", new Long(1));
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充[update]++++++");
        log.info(metaObject.toString());
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", new Long(1));
        metaObject.setValue("updateUser", new Long(1));

    }
}

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

(三)功能测试
(四)功能完善

(1)目前问题——createTime和UpdateTime是固定值,应该设置为动态值

在这里插入图片描述

(2)解决方案——threadlocal类解决

在这里插入图片描述

(4)什么是Threadlocal——Thread的局部变量

在这里插入图片描述

(5)代码流程

在这里插入图片描述

然后为了动态的获取员工的id,这里我们使用了threadLocal这个局部变量来获取和存储员工id;

创建一个工具类来设置和获取threadLocal中的员工id, 注意:要先把数据设置进threadLocal中,才能获取到

package com.itheima.reggie.common;/*** @author LJM* @create 2022/4/16*  基于ThreadLocal封装工具类,用户保存和获取当前登录用户id*/
public class BaseContext {//用来存储用户idprivate static ThreadLocal<Long> threadLocal = new ThreadLocal<>();/*** 设置值* @param id*/public static void setCurrentId(Long id){threadLocal.set(id);}/*** 获取值* @return*/public static Long getCurrentId(){return threadLocal.get();}
}

 在前面我们写的LongCheckFilter这个过滤器中,把这个地方的代码加上添加和保存id的代码

//4、判断登录状态,如果已登录,则直接放行if(request.getSession().getAttribute("employee") != null){//log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));//把用户id存储到本地的threadLocalLong emId = (Long) request.getSession().getAttribute("employee");BaseContext.setCurrentId(emId);filterChain.doFilter(request,response);return;}
把处理器中的静态id改为动态获取:
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());


在这里插入图片描述

二、新增分类

数据模型:

从资料去复制实体Category类到entity包;

数据库中的表结构:

 

创建mapper:

package com.itheima.reggie.mapper;
 
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Category;
import org.apache.ibatis.annotations.Mapper;
 
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {

package com.itheima.reggie.service;
 
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Category;
 
/**
 * @author LJM
 * @create 2022/4/16
 */
public interface CategoryService extends IService<Category> {

package com.itheima.reggie.service.impl;
 
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Category;
import com.itheima.reggie.mapper.CategoryMapper;
import com.itheima.reggie.service.CategoryService;
import org.springframework.stereotype.Service;
 
/**
 * @author LJM
 * @create 2022/4/16
 */
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
 

编写controller: 

/**
     * 新增套餐分类
     * @param category
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody Category category){
        log.info("{category}" ,category);
        categoryService.save(category);
        return R.success("新增分类成功");
    } 

 三、分类信息分页查询

   /*** 分页查询* @param page* @param pageSize* @return*/@GetMapping("/page")public R<Page> page(int page,int pageSize){//创建一个分页构造器Page<Category> categoryPage = new Page<>(page,pageSize);//创建一个条件构造器  用来排序用的  注意这个条件构造器一定要使用泛型,否则使用条件查询这个方法的时候会报错LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper();//添加排序条件 ,根据sort字段进行排序queryWrapper.orderByAsc(Category::getSort);categoryService.page(categoryPage,queryWrapper);return R.success(categoryPage);}

 四、删除分类

代码实现: 注意这里的删除功能是不完整的,因为可能需要删除的数据是与其他表关联的,所以删除之前要先判断该条数据是否与其他表中的数据关联; 

    /*** 根据id来删除分类的数据* @param id* @return*/@DeleteMapping()public R<String> delete(@RequestParam("ids") Long ids){ //注意这里前端传过来的数据是idscategoryService.removeById(ids);return R.success("分类信息删除成功");}

 创建对应的mapper:

package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Dish;
import org.apache.ibatis.annotations.Mapper;/*** @author LJM* @create 2022/4/16*/
@Mapper
public interface DishMapper extends BaseMapper<Dish> {}
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Setmeal;
import org.apache.ibatis.annotations.Mapper;
/*** @author LJM* @create 2022/4/16*/
@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal> {
}

创建service:

package com.itheima.reggie.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Dish;/*** @author LJM* @create 2022/4/16*/
public interface DishService extends IService<Dish> {
}
package com.itheima.reggie.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Setmeal;public interface SetmealService extends IService<Setmeal> {
}
package com.itheima.reggie.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.mapper.DishMapper;
import com.itheima.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;/*** @author LJM* @create 2022/4/16*/
@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
}
package com.itheima.reggie.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Setmeal;
import com.itheima.reggie.mapper.SetmealMapper;
import com.itheima.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;/*** @author LJM* @create 2022/4/16*/
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
}

添加自定义的service方法:(就是我们需要的业务mybatis没有提供,所以就需要自己另外在service创建新的方法,并且在相关的业务中实现)

//在CategoryService中定义自己需要的方法,直接写就行
void remove(Long id); 

 在CategoryService实现类中重写该方法:
 
自定义异常类,因为这里需要抛异常了:

package com.itheima.reggie.common;/*** 自定义业务异常类*/
public class CustomException extends RuntimeException {public CustomException(String message){super(message);}
}
//然后在外面前面写的GlobalExceptionHandler全局异常捕获器中添加该异常,这样就可以把相关的异常信息显示给前端操作的人员看见/*** 处理自定义的异常,为了让前端展示我们的异常信息,这里需要把异常进行全局捕获,然后返回给前端* @param exception* @return*/@ExceptionHandler(CustomException.class)public R<String> exceptionHandle(CustomException exception){log.error(exception.getMessage()); //报错记得打日志//这里拿到的message是业务类抛出的异常信息,我们把它显示到前端return R.error(exception.getMessage());}
/*** 根据id删除 分类,删除之前需要进行判断是否有关联数据* @param id*/@Overridepublic void remove(Long id) {LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();//添加查询条件dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);//注意:这里使用count方法的时候一定要传入条件查询的对象,否则计数会出现问题,计算出来的是全部的数据的条数int count = dishService.count(dishLambdaQueryWrapper);//查询当前分类是否关联了菜品,如果已经管理,直接抛出一个业务异常if (count > 0){//已经关联了菜品,抛出一个业务异常throw new CustomException("当前分类项关联了菜品,不能删除");}//查询当前分类是否关联了套餐,如果已经管理,直接抛出一个业务异常LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);//注意:这里使用count方法的时候一定要传入条件查询的对象,否则计数会出现问题,计算出来的是全部的数据的条数int setmealCount = setmealService.count(setmealLambdaQueryWrapper);if (setmealCount > 0){//已经关联了套餐,抛出一个业务异常throw new CustomException("当前分类项关联了套餐,不能删除");}//正常删除super.removeById(id);}

然后在controller调用刚刚实现的方法就行:把之前的remove方法给删除就行,重新调用我们自己实现的方法;

    /*** 根据id来删除分类的数据* @param id* @return*/@DeleteMappingpublic R<String> delete(@RequestParam("ids") Long id){ //注意这里前端传过来的数据是idscategoryService.remove(id);return R.success("分类信息删除成功");}

 

五、修改分类 

这里的编辑的数据回显,前端已经帮我们做好了,所以我们就不需要去数据库查询了,这样可以减少对数据库的操作;

    /*** 根据id修改分类* @param category* @return*/@PutMappingpublic R<String> update(@RequestBody Category category){categoryService.updateById(category);return R.success("修改分类信息成功");}

记得在对应的实体类加上公共字段的值设置:前面我们配置了这个,所以这里只需要加注解就行;

    //创建时间@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;

Day04菜品管理业务开发

管理端——
在这里插入图片描述
客户端——
在这里插入图片描述

一、文件上传下载

在这里插入图片描述


参数名有要求的
接收的文件类型一定是 方法名(MultipartFile 前端上传的文件名称)


在这里插入图片描述
在这里插入图片描述
所以后端的接收名字也得改为file
在这里插入图片描述

上传逻辑实现

具体的存储路径写在配置文件里了
在这里插入图片描述
用@Value注入到业务里就可以了
在这里插入图片描述

后端具体代码的实现:

yml配置文件:配置上传图片的存储位置;

reggie:

path: E:\reggie\

package com.itheima.reggie.controller;import com.itheima.reggie.common.R;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;/*** @author LJM* @create 2022/4/16* 文件上传和下载*/
@RestController
@RequestMapping("/common")
public class CommonController {@Value("${reggie.path}")private String basePath;/*** 文件的上传* @param file* @return*/@PostMapping("/upload")public R<String> upload(MultipartFile file){//这个file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除//拿到文件的原始名String originalFilename = file.getOriginalFilename();//拿到文件的后缀名 比如 .png  .jpgString suffix = originalFilename.substring(originalFilename.lastIndexOf("."));//使用uuid生成的作为文件名的一部分,这样可以防止文件名相同造成的文件覆盖String fileName = UUID.randomUUID().toString() + suffix;//创建一个目录对象,看传文件的时候,接收文件的目录存不存在File dir = new File(basePath);if (!dir.exists()){//文件目录不存在,直接创建一个目录dir.mkdirs();}try {//把前端传过来的文件进行转存file.transferTo(new File(basePath + fileName));}catch (IOException e){e.printStackTrace();}return R.success(fileName);}@GetMapping("/download")public void download(String name, HttpServletResponse response){try {//输入流,通过输入流读取文件内容  这里的name是前台用户需要下载的文件的文件名//new File(basePath + name) 是为了从存储图片的地方获取用户需要的图片对象FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));//输出流,通过输出流将文件写回浏览器ServletOutputStream outputStream = response.getOutputStream();//设置写回去的文件类型response.setContentType("image/jpeg");//定义缓存区,准备读写文件int len  = 0 ;byte[] buff = new byte[1024];while ((len = fileInputStream.read(buff)) != -1){outputStream.write(buff,0,len);outputStream.flush();}//关流outputStream.close();fileInputStream.close();}catch (Exception e){e.printStackTrace();}}
}

二、新增菜品

  需求分析:

数据模型:

 

代码开发:

 创建相关的mapper和service层:

package com.itheima.reggie.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.DishFlavor;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
}
package com.itheima.reggie.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.DishFlavor;public interface DishFlavorService extends IService<DishFlavor>  {
}
package com.itheima.reggie.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.DishFlavor;
import com.itheima.reggie.mapper.DishFlavorMapper;
import com.itheima.reggie.service.DishFlavorService;
import org.springframework.stereotype.Service;/*** @author LJM* @create 2022/4/16*/
@Service
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor> implements DishFlavorService {
}

编写controller:

在CategoryController书写查询代码,不过这里的返回值和参数接收值可能和自己想的有点不一样。。。这个的返回值和参数值 值得多思考一下; 这里之所以返回list集合,是因为这个要展示的数据是引用类型的数据集,集合可以存放任意类型的数据;

 /*** 根据条件查询分类数据* @param category* @return*/@GetMapping("/list")//这个接口接收到参数其实就是一个前端传过来的type,这里之所以使用Category这个类来接受前端的数据,是为了以后方便//因为这个Category类里面包含了type这个数据,返回的数据多了,你自己用啥取啥就行private R<List<Category>> list(Category category){//条件构造器LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper();//添加查询条件queryWrapper.eq(category.getType() != null,Category::getType,category.getType());//添加排序条件  使用两个排序条件,如果sort相同的情况下就使用更新时间进行排序queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);List<Category> list = categoryService.list(queryWrapper);return R.success(list);}

测试的返回数据:

接收页面提交的数据(涉及两张表)


点击保存按钮的时候,把前端的json数据提交到后台,后台接收数据,对数据进行处理;要与两张表打交道,一个是dish一个是dish_flavor表;

先用前端页面向后端发一次请求,看看前端具体的请求是什么,我们好写controller;然后再看前端提交携带的参数是什么,我们好选择用什么类型的数据来接收!!!

看下图:这是前端传过来的具体参数,我们需要什么参数类型来接收这些数据就大概知道了;因为这里传过来的参数比较复杂,所以这里有两种方式进行封装,第一:创建与这些数据对应的实体类(dto) ,第二使用map来接收;


这里我们选择使用第一种方式;

package com.itheima.reggie.dto;import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;@Data
public class DishDto extends Dish {private List<DishFlavor> flavors = new ArrayList<>();private String categoryName;  //后面要用的private Integer copies;  //后面要用的
}

后端代码:

在DishService中新增一个方法:

//新增菜品,同时插入菜品对应的口味数据,需要同时操作两张表:dish  dish_flavor
void saveWithFlavor(DishDto dishDto); 

@Autowired
private DishFlavorService dishFlavorService;
/*** 新增菜品同时保存对应的口味数据* @param dishDto*/
@Override
@Transactional //涉及到对多张表的数据进行操作,需要加事务,需要事务生效,需要在启动类加上事务注解生效
public void saveWithFlavor(DishDto dishDto) {//保存菜品的基本信息到菜品表dish中this.save(dishDto);Long dishId = dishDto.getId();//为了把dishId  set进flavors表中//拿到菜品口味List<DishFlavor> flavors = dishDto.getFlavors();//这里对集合进行赋值 可以使用循环或者是stream流flavors = flavors.stream().map((item) ->{//拿到的这个item就是这个DishFlavor集合item.setDishId(dishId);return item; //记得把数据返回去}).collect(Collectors.toList()); //把返回的集合搜集起来,用来被接收//把菜品口味的数据到口味表 dish_flavor  注意dish_flavor只是封装了name value 并没有封装dishId(从前端传过来的数据发现的,然而数据库又需要这个数据)dishFlavorService.saveBatch(dishDto.getFlavors()); //这个方法是批量保存
}

 在启动类开启事务: 加上这个注解就行 @EnableTransactionManagement

controller 层的代码:

package com.itheima.reggie.controller;import com.itheima.reggie.common.R;
import com.itheima.reggie.dto.DishDto;
import com.itheima.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** @author LJM* @create 2022/4/16*/
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {@Autowiredprivate DishService dishService;/*** 新增菜品* @param dishDto* @return*/@PostMappingpublic R<String> save(@RequestBody DishDto dishDto){ //前端提交的是json数据的话,我们在后端就要使用这个注解来接收参数,否则接收到的数据全是nulldishService.saveWithFlavor(dishDto);return R.success("新增菜品成功");}
}

 三、菜品信息分页查询

需求分析:

 图片下载的请求前面已经写好了,前端也写好了相关的请求,所以第二步的图片下载和展示就不需要我们管了;

代码编写:

controller层的代码:不过这里是有bug的,后面会改善;

/*** 菜品信息分页查询* @param page* @param pageSize* @param name* @return*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){//构造一个分页构造器对象Page<Dish> dishPage = new Page<>(page,pageSize);//构造一个条件构造器LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();//添加过滤条件 注意判断是否为空  使用对name的模糊查询queryWrapper.like(name != null,Dish::getName,name);//添加排序条件  根据更新时间降序排queryWrapper.orderByDesc(Dish::getUpdateTime);//去数据库处理分页 和 查询dishService.page(dishPage,queryWrapper);//因为上面处理的数据没有分类的id,这样直接返回R.success(dishPage)虽然不会报错,但是前端展示的时候这个菜品分类这一数据就为空return R.success(dishPage);
}

 功能完善:引入了DishDto

package com.itheima.reggie.dto;import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;@Data
public class DishDto extends Dish {private List<DishFlavor> flavors = new ArrayList<>();private String categoryName;private Integer copies; //后面用的
}
/*** 菜品信息分页查询* @param page* @param pageSize* @param name* @return*/@GetMapping("/page")public R<Page> page(int page,int pageSize,String name){//构造一个分页构造器对象Page<Dish> dishPage = new Page<>(page,pageSize);Page<DishDto> dishDtoPage = new Page<>(page,pageSize);//上面对dish泛型的数据已经赋值了,这里对DishDto我们可以把之前的数据拷贝过来进行赋值//构造一个条件构造器LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();//添加过滤条件 注意判断是否为空  使用对name的模糊查询queryWrapper.like(name != null,Dish::getName,name);//添加排序条件  根据更新时间降序排queryWrapper.orderByDesc(Dish::getUpdateTime);//去数据库处理分页 和 查询dishService.page(dishPage,queryWrapper);//获取到dish的所有数据 records属性是分页插件中表示分页中所有的数据的一个集合List<Dish> records = dishPage.getRecords();List<DishDto> list = records.stream().map((item) ->{//对实体类DishDto进行categoryName的设值DishDto dishDto = new DishDto();//这里的item相当于Dish  对dishDto进行除categoryName属性的拷贝BeanUtils.copyProperties(item,dishDto);//获取分类的idLong categoryId = item.getCategoryId();//通过分类id获取分类对象Category category = categoryService.getById(categoryId);if ( category != null){//设置实体类DishDto的categoryName属性值String categoryName = category.getName();dishDto.setCategoryName(categoryName);}return dishDto;}).collect(Collectors.toList());//对象拷贝  使用框架自带的工具类,第三个参数是不拷贝到属性BeanUtils.copyProperties(dishPage,dishDtoPage,"records");dishDtoPage.setRecords(list);//因为上面处理的数据没有分类的id,这样直接返回R.success(dishPage)虽然不会报错,但是前端展示的时候这个菜品分类这一数据就为空//所以进行了上面的一系列操作return R.success(dishDtoPage);}

records的值: protected List<T> records;

 功能测试:

四. 修改菜品(回显和保存修改都是两张表)

 

菜品信息的回显:

在service添加自己要实现的方法:

//根据id来查询菜品信息对应的口味信息
DishDto getByIdWithFlavor(Long id); 

 方法的 实现: 

    @Autowired
    private DishFlavorService dishFlavorService;
/**
 * 根据id来查询菜品信息和对应的口味信息
 * @param id
 * @return
 */
@Override
public DishDto getByIdWithFlavor(Long id) {
    //查询菜品的基本信息  从dish表查询
    Dish dish = this.getById(id);
 
    //查询当前菜品对应的口味信息,从dish_flavor查询  条件查询
    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(DishFlavor::getDishId,dish.getId());
    List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
 
    //然后把查询出来的flavors数据set进行 DishDto对象
    DishDto dishDto = new DishDto();
    //把dish表中的基本信息copy到dishDto对象,因为才创建的dishDto里面的属性全是空
    BeanUtils.copyProperties(dish,dishDto);
    dishDto.setFlavors(flavors);
 
    return dishDto;

controller 层的编写: 

/**
 * 根据id来查询菜品信息和对应的口味信息
 * @param id
 * @return
 */
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){  //这里返回什么数据是要看前端需要什么数据,不能直接想当然的就返回Dish对象
    DishDto dishDto = dishService.getByIdWithFlavor(id);
    return R.success(dishDto);

保存修改:(重点)

保存修改设计两张表的数据的修改:

DishService中添加自己实现的方法:

//更新菜品信息同时还更新对应的口味信息
void updateWithFlavor(DishDto dishDto); 

 相关的实现:

   @Override
    @Transactional
    public void updateWithFlavor(DishDto dishDto) {
        //更新dish表的基本信息  因为这里的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();
 
        //下面这段流的代码我注释,然后测试,发现一次是报dishId没有默认值(先测),两次可以得到结果(后测,重新编译过,清除缓存过),相隔半个小时
        //因为这里拿到的flavorsz只有name和value(这是在设计数据封装的问题),不过debug测试的时候发现有时候可以拿到全部数据,有时候又不可以...  所以还是加上吧。。。。。
        flavors = flavors.stream().map((item) -> {
            item.setDishId(dishDto.getId());
            return item;
        }).collect(Collectors.toList());
 
        dishFlavorService.saveBatch(flavors);
 
    } 

 Day05套餐管理

1. 新增套餐

分析⭐️

和上一个开发类似

前端提交过来的信息包含套餐基本信息和套餐与菜品关联信息

因此需要设置一个setmealDto,Dto中包含套餐基本信息和套餐与菜品关联信息

后端在setmealController中接收这个Dto,然后新增业务方法去处理Dto

业务方法:

①将dto基本信息传入到套餐基本信息表

②将套餐id这个对象中的list集合中的数据添加到套餐菜品表

③涉及操作两张表,需要加入@transactional注解,要么同时成功,要么同时失败

需求分析:

数据模型:

 

功能实现一:回显添加菜品

根据分类categoryId,来去相应dish表中查询信息,进而回显信息

/*** 根据categoryId,回显对应分类下的菜品信息* @param dish* @return*/
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();//两个eq信息queryWrapper.eq(dish != null,Dish::getCategoryId,dish.getCategoryId());queryWrapper.eq(Dish::getStatus,1);//添加排序条件queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);List<Dish> list = dishService.list(queryWrapper);return R.success(list);
}

功能实现二:

实现添加菜品功能

  • 这里要注意setMealId在前端传过来的数据没有,需要将前端基本信息添加到SetMeal表中,才能得到相应的Id,然后为套餐菜品对象赋值上值
@Service
public class SetMealServiceImpl extends ServiceImpl<SetMealMapper, Setmeal> implements SetMealService {@Autowiredprivate SetMealDishService setMealDishService;/*** 新增菜品套餐* @param setmealDto*/@Overridepublic void saveWithDish(SetmealDto setmealDto) {//1.调用setMeal本有的功能,顺便得到套餐idthis.save(setmealDto);//2.将从数据库得到的套餐id封装回setmealDishes对象中List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();setmealDishes.stream().map((item) -> {item.setSetmealId(setmealDto.getId());return item;}).collect(Collectors.toList());//3.setMealDishService.saveBatch(setmealDishes);}
}
2. 套餐分页查询
  • 功能与day04的菜品信息分类查询相似
  • 在套餐管理界面,套餐分类字段显示的是categoryId对应的中文,但在数据库里查询到的是categoryId,因此需要利用categoryId查询到categoryName,并赋值给数据传输对象SetmealDto
/*** 套餐分页查询* @param page* @param pageSize* @param name* @return*/
@GetMapping("/page")
public R<Page> list(int page, int pageSize, String name){//分页构造器对象Page<Setmeal> pageInfo = new Page<>(page, pageSize);Page<SetmealDto> dtoPage = new Page<>();//构造查询条件对象LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(name != null, Setmeal::getName, name);//操作数据库setMealService.page(pageInfo,queryWrapper);//对象拷贝BeanUtils.copyProperties(pageInfo,dtoPage,"records");List<Setmeal> records = pageInfo.getRecords();List<SetmealDto> list = records.stream().map((item) -> {SetmealDto setmealDto = new SetmealDto();BeanUtils.copyProperties(item, setmealDto);//获取categoryIdLong categoryId = item.getCategoryId();Category category = categoryService.getById(categoryId);if (category != null) {String categoryName = category.getName();setmealDto.setCategoryName(categoryName);}return setmealDto;}).collect(Collectors.toList());dtoPage.setRecords(list);return R.success(dtoPage);
}
 3.删除套餐信息

需求分析

提供一个方法处理删除一个和删除多个请求

代码开发

注意点:

①接受前端ids数据,传过来的数据本身是数组形式,所以加不加注解无所谓,但是List是列表,所以要加注解@RequestParam

②根据id删除套餐,不仅删除套餐,也删除关联套餐表中的信息

业务逻辑:(SetMealServiceImpl)

​ 1.查询套餐状态,确定是否可用删除
​ 2.如果不能删除,抛出一个业务异常,提示在售卖中
​ 3.如果可以删除,先删除套餐表中的数据
​ 4.删除关系表中的数据

/*** 根据ids删除套餐信息* @param ids*/
@Override
@Transactional
public void removeWithDish(List<Long> ids) {//        1.查询套餐状态,确定是否可用删除//SQL语句:select count(*) from setMeal where id in ids and status = 1;LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.in(Setmeal::getId,ids);queryWrapper.eq(Setmeal::getStatus,1);int count = this.count(queryWrapper);//        2.如果不能删除,抛出一个业务异常,提示**在售卖中**if(count > 0){throw new CustomException("商品还在销售,不能删除");}//        3.如果可以删除,先删除套餐表中的数据this.removeByIds(ids);//        4.删除关系表中的数据//根据套餐id去关系表中去查数据,然后匹配删除//delete from setMealDish where setmealId in idsLambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);setMealDishService.remove(lambdaQueryWrapper);}

起售,停售操作(SetMealServiceImpl)

这里采用遍历操作实现批量停售,起售不太好,应该用mp的具体更新方法操作,等学了mp之后再来补吧,希望还记得

/*** 更改售卖状态* @param ids* @param status 1表示启售,0表示停售*/
@Override
public void changeStatus(List<Long> ids, int status) {//改变售卖状态for (int i = 0; i < ids.size(); i++) {Long id = ids.get(i);//根据id得到每个dish菜品。Setmeal setmeal = this.getById(id);setmeal.setStatus(status);this.updateById(setmeal);}}

前台开发(手机端)

账户登陆

短信发送

在这里插入图片描述
在这里插入图片描述
阿里云短信业务教程

代码实现

官方文档地址
导入Maven

<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>1.1.0</version>
</dependency>
 

然后调用api: 

package com.itheima.reggie.utils;
 
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
 
/**
 * 短信发送工具类
 */
public class SMSUtils {
 
    /**
     * 发送短信
     * @param signName 签名
     * @param templateCode 模板
     * @param phoneNumbers 手机号
     * @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();
        }
    }

短信验证码登陆

需求分析:

数据模型:

前后端交互过程:

 代码开发:

 导入user实体类;

创建userMapper:

导入工具类:

发送验证码(给的资料有点残缺,这里修改了)

注意这个资料有点残缺,补全:

在longin.html中找到这个获取验证码的方法,把一行注释,然后添加一行代码一行;

getCode(){this.form.code = ''const regex = /^(13[0-9]{9})|(15[0-9]{9})|(17[0-9]{9})|(18[0-9]{9})|(19[0-9]{9})$/;if (regex.test(this.form.phone)) {this.msgFlag = false//this.form.code = (Math.random()*1000000).toFixed(0)sendMsgApi({phone:this.form.phone})  //添加的}else{this.msgFlag = true}
},

在login.js中添加一个方法:

function sendMsgApi(data){return $axios({'url':'/user/sendMsg','method':'post',data})
}

 在登陆拦截器LongCheckFilter中添加新的白名单:

String[] urls = new String[]{"/employee/login","/employee/logout","/backend/**","/front/**","/common/**","/user/sendMsg", //移动端发送短信"/user/login"  // 移动端登陆
};

并且在里面继续添加一个手机端登陆状态的放行判断:

//4-2判断移动端登录状态,如果已登录,则直接放行if(request.getSession().getAttribute("user") != null){//log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));//把用户id存储到本地的threadLocalLong userId = (Long) request.getSession().getAttribute("user");BaseContext.setCurrentId(userId);filterChain.doFilter(request,response);return;}

 编写controller:

@Autowired
private UserService userService;/*** 发送手机短信验证码* @param user* @return*/
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession session){//获取手机号String phone = user.getPhone();if (StringUtils.isNotEmpty(phone)){//随机生成的4为验证码Integer integerCode = ValidateCodeUtils.generateValidateCode(4);String code = integerCode.toString();log.info("code={}",code);//调用阿里云提供的短信服务api完成发送短信  这里个人用户申请不了阿里云短信服务的签名,所以这里在后台输出了//SMSUtils.sendMessage("","","","");//把验证码存起来  这里使用session来存放验证码,当然也可以存到redissession.setAttribute(phone,code);return R.success("手机验证码发送成功");}return R.error("手机验证码发送失败");
}

 功能测试:访问手机端,输入手机号,看能不能在后台打印验证码;

使用验证码登陆(使用map接收数据)
注意:测试的时候发现前端页面明明填了验证码,发现验证码并没有被携带在前端的请求参数中,所以后端也没有拿到验证码这个数据,一看就是前端发请求的地方的参数携带少了;修改一下参数就行;

async btnLogin(){
    if(this.form.phone && this.form.code){
        this.loading = true
        
        //const res = await loginApi({phone:this.form.phone})  这里是资料给的代码
        const res = await loginApi(this.form)  //这里是自己加的
        
        ....
}

controller层代码: 

/**
 * 移动端用户登录
 * @param map
 * @param session
 * @return
 */
@PostMapping("/login")
public R<User> login(@RequestBody Map map, HttpSession session){
    //log.info(map.toString());
 
    //获取手机号
    String phone = map.get("phone").toString();
 
    //获取验证码
    String code = map.get("code").toString();
 
    //从Session中获取保存的验证码
    Object codeInSession = session.getAttribute(phone);
 
    //进行验证码的比对(页面提交的验证码和Session中保存的验证码比对)
    if(codeInSession != null && codeInSession.equals(code)){
        //如果能够比对成功,说明登录成功
 
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getPhone,phone);
        //根据用户的手机号去用户表获取用户
        User user = userService.getOne(queryWrapper);
        if(user == null){
            //判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
            user = new User();
            user.setPhone(phone);
            user.setStatus(1); //可设置也可不设置,因为数据库我们设置了默认值
            //注册新用户
            userService.save(user);
        }
        //这一行容易漏。。保存用户登录状态
        session.setAttribute("user",user.getId()); //在session中保存用户的登录状态,这样才过滤器的时候就不会被拦截了
        return R.success(user);
    }
    return R.error("登录失败");

功能测试:验证码正确后跳转到手机端;

地址管理

导入用户地址簿

在这里插入图片描述
地址表
在这里插入图片描述
这里直接导入现成的AddressBookController

package com.itheima.reggie.controller;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itheima.reggie.common.BaseContext;
import com.itheima.reggie.common.R;
import com.itheima.reggie.entity.AddressBook;
import com.itheima.reggie.service.AddressBookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;import java.util.List;/*** 地址簿管理*/
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {@Autowiredprivate AddressBookService addressBookService;/*** 新增*/@PostMappingpublic R<AddressBook> save(@RequestBody AddressBook addressBook) {addressBook.setUserId(BaseContext.getCurrentId());log.info("addressBook:{}", addressBook);addressBookService.save(addressBook);return R.success(addressBook);}/*** 设置默认地址*/@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);}/*** 根据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<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);}}/*** 查询指定用户的全部地址*/@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));}
}

手机端展示

菜品展示

 前端重点代码:

//获取所有的菜品分类
function categoryListApi() {return $axios({'url': '/category/list','method': 'get',})}//获取购物车内商品的集合
function cartListApi(data) {return $axios({'url': '/shoppingCart/list','method': 'get',params:{...data}})
}

 


 我们发现前端的展示页面中请求到了category的数据,服务器也响应了数据给前端页面,但是我们也看见了手机端并没有展示相关的套餐数据;这是因为在加载页面的时候,前端一共发了两个list请求,具体的请求看上面的前端代码,请求购物车信息的请求返回404,所以导致category的数据也没有被展示;

这里我们先在其他地方静态的接收购物车的请求,这样就可以先显示category的数据;先用假数据测试一下:修改购物车的请求地址,

//获取购物车内商品的集合
function cartListApi(data) {return $axios({//'url': '/shoppingCart/list','url':'/front/cartData.json','method': 'get',params:{...data}})
}

// 假数据文件
{"code":1,"msg":null,"data":[],"map":{}}

功能测试:

但是我们也发现了bug,就是展示的菜品没有对应的口味信息,比如甜度,辣度。。。我们之前是添加过相关的口味数据的;

这是因为我们在请求获取菜品信息的时候,我们返回的数据是R<List<Dish>>Dish这个类是没有相关的口味信息的,所以即便前端请求了这个口味的信息,但是后端是没有给它返回的,所以体现在前端就是口味信息没有展示出来;所以我们需要对DishController中的list接口进行修改;bug修复:

//方法改造
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){ //会自动映射的//这里可以传categoryId,但是为了代码通用性更强,这里直接使用dish类来接受(因为dish里面是有categoryId的),以后传dish的其他属性这里也可以使用//构造查询条件LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());//添加条件,查询状态为1(起售状态)的菜品queryWrapper.eq(Dish::getStatus,1);//添加排序条件queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);List<Dish> list = dishService.list(queryWrapper);//进行集合的泛型转化List<DishDto> dishDtoList = list.stream().map((item) ->{DishDto dishDto = new DishDto();//为一个新的对象赋值,一定要考虑你为它赋过几个值,否则你自己都不知道就返回了null的数据//为dishDto对象的基本属性拷贝BeanUtils.copyProperties(item,dishDto);Long categoryId = item.getCategoryId();Category category = categoryService.getById(categoryId);if (category != null){String categoryName = category.getName();dishDto.setCategoryName(categoryName);}//为dishdto赋值flavors属性//当前菜品的idLong dishId = item.getId();//创建条件查询对象LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper();lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);//select * from dish_flavor where dish_id = ?//这里之所以使用list来条件查询那是因为同一个dish_id 可以查出不同的口味出来,就是查询的结果不止一个List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);dishDto.setFlavors(dishFlavorList);return dishDto;}).collect(Collectors.toList());return R.success(dishDtoList);
}

 套餐展示

套餐展示的前端请求地址和携带的参数:

 在SetmealController中添加相应的方法来接收前端的请求:

    /*** 根据条件查询套餐数据* @param setmeal* @return*/@GetMapping("/list")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);}

功能测试:

购物车

添加菜品和套餐进购物车

数据模型: 购物车对应的数据表为shopping_cart,具体结构如下:从资料中导入相关的实体类就行

代码开发:

加入购物车功能:

加入购物车的前端请求和携带的参数:

 开发准备工作:

导入实体类ShoppingCart;

创建对应的mapper等

controller

@Autowired
private ShoppingCartService shoppingCartService;/*** 添加购物车* @param shoppingCart* @return*/
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){//先设置用户id,指定当前是哪个用户的购物车数据  因为前端没有传这个id给我们,但是这个id又非常重要(数据库这个字段不能为null),// 所以要想办法获取到,我们在用户登录的时候就已经保存了用户的idLong 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/setmeal_id = ?ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);if (cartServiceOne != null) {//如果已经存在,就在原来的数量基础上加一Integer number = cartServiceOne.getNumber();cartServiceOne.setNumber(number+1);shoppingCartService.updateById(cartServiceOne);}else {//如果不存在,则添加到购物车,数量默认是1shoppingCart.setNumber(1);shoppingCartService.save(shoppingCart);cartServiceOne = shoppingCart;}return R.success(cartServiceOne);
}

查看购物车

把相关的前端请求地址给改掉, 不再请求假数据,至于存放假数据的json文件是否删除,看你自己,删不删都没什么影响;

//获取购物车内商品的集合
function cartListApi(data) {return $axios({'url': '/shoppingCart/list',//'url':'/front/cartData.json','method': 'get',params:{...data}})
}

注意:一定要有用户的概念,不同用户看到的购物车是不一样的!!!

controller:

/*** 查看购物车* @return* 前端没有传数据给我们,这里就不用接收了*/
@GetMapping
public R<List<ShoppingCart>> list(){LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());queryWrapper.orderByAsc(ShoppingCart::getCreateTime);List<ShoppingCart> list = shoppingCartService.list(queryWrapper);return R.success(list);
}

清空购物车

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

减少购物车点菜品或者套餐

前端请求: http://localhost:8080/shoppingCart/sub

请求方式:post

携带参数可能是dish_id 也可能是 setmealId,所以我们需要实体类shoppingCart来接收;

遇到的bug: 就是购物车里面的菜品和套餐的数量可能会减少至负数!!!所以这里我们要在入库前做一次判断;把数据库的该字段设置为无符号字段,所以当num数小于0的时候就会报错(500接口异常),但是左下角的小购物车还是会显示菜品为0,所以在后端的代码也要进行判断操作;

在ShoppingCartController中添加下面的接口方法来接收请求:(这个是修改了一次的代码,思路用的是评论区一个老哥提供的思路)

    /*** 客户端的套餐或者是菜品数量减少设置* 没必要设置返回值* @param shoppingCart*/@PostMapping("/sub")@Transactionalpublic R<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart){Long dishId = shoppingCart.getDishId();LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();//代表数量减少的是菜品数量if (dishId != null){//通过dishId查出购物车对象queryWrapper.eq(ShoppingCart::getDishId,dishId);//这里必须要加两个条件,否则会出现用户互相修改对方与自己购物车中相同套餐或者是菜品的数量queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());ShoppingCart cart1 = shoppingCartService.getOne(queryWrapper);cart1.setNumber(cart1.getNumber()-1);Integer LatestNumber = cart1.getNumber();if (LatestNumber > 0){//对数据进行更新操作shoppingCartService.updateById(cart1);}else if(LatestNumber == 0){//如果购物车的菜品数量减为0,那么就把菜品从购物车删除shoppingCartService.removeById(cart1.getId());}else if (LatestNumber < 0){return R.error("操作异常");}return R.success(cart1);}Long setmealId = shoppingCart.getSetmealId();if (setmealId != null){//代表是套餐数量减少queryWrapper.eq(ShoppingCart::getSetmealId,setmealId).eq(ShoppingCart::getUserId,BaseContext.getCurrentId());ShoppingCart cart2 = shoppingCartService.getOne(queryWrapper);cart2.setNumber(cart2.getNumber()-1);Integer LatestNumber = cart2.getNumber();if (LatestNumber > 0){//对数据进行更新操作shoppingCartService.updateById(cart2);}else if(LatestNumber == 0){//如果购物车的套餐数量减为0,那么就把套餐从购物车删除shoppingCartService.removeById(cart2.getId());}else if (LatestNumber < 0){return R.error("操作异常");}return R.success(cart2);}//如果两个大if判断都进不去return R.error("操作异常");}

用户订单

用户下单功能

需求分析:

注意:这里只是把用户的支付订单保存到数据库,因为真正的支付功能是需要去申请支付资质的,个人用户很难申请到;

数据模型:用户下单业务对应的数据表为order表和order_detail表:

order:订单表;

 order_detail:订单明细表;

代码开发:

前端和服务器的交互过程:

第一次交互:

 然后点击去支付:然后前端就会发生http://localhost:8080/order/submit这个请求;并且携带三个参数:一个是地址,一个是字符方式,一个是客户的备注;

 开发准备工作:

导入实体类:order,order_details

创建mapper等

controller层开发:

前端请求的地址和携带的参数前面已经分析过了;

/*** 用户下单* @param orders* @return*/
@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders){orderService.submit(orders);return R.success("下单成功");
}

 service添加submit方法:

/**
 * 用户下单
 * @param orders
 */
public void submit(Orders orders);

方法的实现: 

@Autowired
private ShoppingCartService shoppingCartService;@Autowired
private UserService userService;@Autowired
private AddressBookService addressBookService;@Autowired
OrderDetailService orderDetailService;/*** 用户下单* @param orders*/
@Override
@Transactional
public void submit(Orders orders) {//前端请求携带的参数是没有用户id的,所以要获取用户idLong userId = BaseContext.getCurrentId();//创建查询条件LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(ShoppingCart::getUserId,userId);//查询当前用户的购物车数据List<ShoppingCart> shoppingCarts = shoppingCartService.list(queryWrapper);if (shoppingCarts == null || shoppingCarts.size()==0){throw new CustomException("购物车为空,不能下单");}//查询用户数据User user = userService.getById(userId);//查询用户地址Long addressBookId = orders.getAddressBookId();AddressBook addressBook = addressBookService.getById(addressBookId);if (addressBook==null){throw new CustomException("地址信息有误,不能下单");}Long orderId = IdWorker.getId();//使用工具生成订单号orders.setId(orderId);//进行购物车的金额数据计算 顺便把订单明细给计算出来AtomicInteger amount = new AtomicInteger(0);//使用原子类来保存计算的金额结果//这个item是集合中的每一个shoppingCarts对象,是在变化的List<OrderDetail> orderDetails = shoppingCarts.stream().map((item)->{//每对item进行一次遍历就产生一个新的orderDetail对象,然后对orderDetail进行设置,然后返回被收集,被封装成一个集合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());//单份的金额//addAndGet进行累加 item.getAmount()单份的金额  multiply乘  item.getNumber()份数amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());return orderDetail;}).collect(Collectors.toList());//向订单插入数据,一条数据  因为前端传过来的数据太少了,所以我们需要对相关的属性进行填值orders.setOrderTime(LocalDateTime.now());orders.setCheckoutTime(LocalDateTime.now());orders.setStatus(2);//Amount是指订单总的金额orders.setAmount(new BigDecimal(amount.get()));//总金额orders.setUserId(userId);orders.setNumber(String.valueOf(orderId));if (user.getName() != null){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);//清空购物车数据  queryWrapper封装了userId我们直接使用这个条件来进行删除就行shoppingCartService.remove(queryWrapper);
}

 用户查看自己订单

/*** 用户订单分页查询* @param page* @param pageSize* @return*/
@GetMapping("/userPage")
public R<Page> page(int page, int pageSize){//分页构造器对象Page<Orders> pageInfo = new Page<>(page,pageSize);//构造条件查询对象LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();//添加排序条件,根据更新时间降序排列queryWrapper.orderByDesc(Orders::getOrderTime);orderService.page(pageInfo,queryWrapper);return R.success(pageInfo);
}

相关文章:

瑞吉外卖项目

目录 Day01业务开发 一、项目总体介绍与展示 二、软件开发整体介绍 &#xff08;一&#xff09;软件开发流程 三、瑞吉外卖项目介绍 &#xff08;一&#xff09;项目介绍 &#xff08;二&#xff09;技术选型功能架构 1.技术选型—— ​编辑2.功能架构—— ​编辑 &a…...

Docker:4、龙晰(Anolis OS 8.8)宝塔面板安装

接上文Docker&#xff1a;1、基于龙晰 &#xff08;Anolis OS 8.8 &#xff09;的基础镜像制作&#xff0c;本节我们介绍&#xff1a;基于Docker的龙晰&#xff08;Anolis OS 8.8 &#xff09;宝塔安装。 在第一节中由于我们对 Docker 容器进行了SSH设置&#xff0c;这为我们这…...

多端项目开发全流程详解 - 从需求分析到多端部署

引言 在当今互联网时代&#xff0c;一个完整的产品常常需要覆盖多个终端&#xff0c;包括小程序、Web端&#xff08;后台管理系统&#xff09;、App端等。本文将详细介绍一个采用前后端分离架构的多端项目开发流程&#xff0c;重点分析各个终端的特点、功能定位及其开发要点。…...

4.5KB原生html+js+css实现图片打印位置的坐标和尺寸获取

一般用于图片打印文字或图片的坐标获取,代码来自AI有改动。 功能&#xff1a;本地图选择后不上传直接可比划线条作为对角线得到矩形&#xff0c;动态显示坐标 按下鼠标开始松开鼠标结束。有细微BUG但不影响坐标获取。 <!DOCTYPE html> <html lang"en">…...

智诊小助手-记录模式选择

记录模式总共有连续记录、硬件触发、软件触发、错误触发四种模式选择&#xff0c;并且在选择完记录模式后还可以设置保留触发点前报文条数、存储时间、录制通道、保存类型 配置过程如下&#xff1a; 点击下面右图中模式选择即可进入到左图中的参数配置界面 如上图选择的配置…...

JDBC: Java数据库连接的桥梁

什么是JDBC&#xff1f; Java数据库连接&#xff08;Java Database Connectivity&#xff0c;简称JDBC&#xff09;是Java提供的一种API&#xff0c;允许Java应用程序与各种数据库进行交互。JDBC提供了一组标准的接口&#xff0c;开发者可以利用这些接口执行SQL语句、处理结果集…...

英伟达GPU算力【自用】

GPU&#xff08;图形处理单元&#xff09;算力的提升是驱动当代科技革命的核心力量之一&#xff0c;尤其在人工智能、深度学习、科学计算和超级计算机领域展现出了前所未有的影响力。2024年的GPU技术发展&#xff0c;不仅体现在游戏和图形处理的传统优势上&#xff0c;更在跨行…...

「C/C++」C++11 之 智能指针

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「C/C」C/C程序设计&#x1f4da;全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasoli…...

算法面试小抄

第一章:算法与数据结构要点速学 1.时间复杂度 (大 O) 首先&#xff0c;我们来谈谈常用操作的时间复杂度&#xff0c;按数据结构/算法划分。然后&#xff0c;我们将讨论给定输入大小的合理复杂性。 数组&#xff08;动态数组/列表&#xff09; 规定 n arr.length, 注意: &am…...

当有违法数据时,浏览器不解析,返回了undefined,导致数据不解析

现象&#xff1a;页面上没有看到数据 排查&#xff1a;断点到线上的源码里&#xff1a;1、协议回调确实没有拿到数据是个undefined 2、network里看服务确实响应了数据 3、控制台没有任何报错。 心情&#xff1a;莫名其妙的现象 我本地有json格式化工具&#xff0c;copy进去后&…...

在MySQL中ORDER BY使用的那种排序算法

在 MySQL 中&#xff0c;ORDER BY 子句的排序算法通常根据场景、数据量和表的索引情况而有所不同。MySQL 常用的排序算法包括&#xff1a; 文件排序&#xff08;File Sort&#xff09;&#xff1a;MySQL 没有使用索引排序的情况下&#xff0c;会进行文件排序&#xff0c;这可以…...

学习threejs,使用粒子实现雨滴特效

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.Points简介1.11 ☘️…...

分布式-单元化架构1

一 两地三中心 1.1 两地三中心* 两地指的是两个城市&#xff1a;同城&#xff0c;异地。三中心指的是三个数据中心&#xff1a;生产中心、同城容灾中心、异地容灾中心。 在同一个城市或者临近的城市建设两个相同的系统&#xff0c;双中心具备相当的业务处理能力&#xff0c;…...

C++模板、STL

目录 一、模板 1、函数模板 (1)、基本语法和使用 (2)、函数模板注意事项 (3)、普通函数与函数模板的区别 (4)、普通函数与函数模板的调用规则 (5)、模板的局限性 2、类模板 (1)、基本语法 (2)、类模板与函数模板区别 (3)、类模板中成员函数创建时机 (4)、类模板对象…...

计算机视觉中的点算子:从零开始构建

Hey小伙伴们&#xff01;今天我们要聊的是一个非常基础但极其重要的计算机视觉技术——点算子&#xff08;Point Operators&#xff09;。点算子主要用于对图像的每个像素进行独立的处理&#xff0c;比如亮度调整、对比度增强、灰度化等。通过这些简单的操作&#xff0c;我们可…...

国际中文教育知识图谱问答

你还在为毕业设计头疼么&#xff1f;想快速搭建一个智能化系统&#xff0c;展示数据又能精准回答问题&#xff1f;那你绝对不能错过这个超实用的 知识图谱问答系统&#xff0c;特别适用于需要整合复杂数据关系、交互性强的项目&#xff01; 这个系统基于 Neo4j图数据库 开发&a…...

酒店大板轻触开关与传统的开关有什么区别

酒店大板轻触开关与传统的开关在功能、设计、使用方式以及安装维护等多个方面都存在显著的差异。以下是对这些差异的详细分析&#xff1a; 功能差异 酒店大板轻触开关&#xff1a; 多功能性&#xff1a;酒店大板轻触开关通常集成了多种功能&#xff0c;如控制照明、窗帘、夜灯、…...

【蓝桥杯选拔赛真题78】python电话号码 第十五届青少年组蓝桥杯python选拔赛真题 算法思维真题解析

目录 python电话号码 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 七、 推荐资料 1、蓝桥杯比赛 2、考级资料 3、其它资料 python电话号码 第十五届蓝桥杯青少年组python比赛选拔赛真题 一、题目要…...

对比两个json串的diff,支持map的深度递归

背景 项目重构&#xff0c;对老接口进行技术改造。动代码后&#xff0c;难免会有些bug&#xff0c;我们需要对比改造前后接口的返回&#xff0c;来判断逻辑是否有问题&#xff0c;这就涉及两个json的对比。 常规的diff文本工具是按行对比&#xff0c;无法处理复杂的map。本文通…...

【我的创作纪念日1024】

我的创作纪念日1024 机缘成就明年的规划 机缘 过去的1024个日子里&#xff0c;我在专业发展、职场和发展、科技创新创业、软件开发、人工智能、虚拟现实、区块链等栏目分享了一些工作和学习的建议和体会。尤其是在2022年&#xff0c;我连续发布100篇的博文&#xff0c;不仅仅是…...

萤石设备视频接入平台EasyCVR私有化视频平台变电站如何实现远程集中监控?

一、方案背景 随着城市经济的发展和电力系统的改造&#xff0c;变电站的数量和规模逐渐增加&#xff0c;对变电站的安全管理和监控需求也越来越高。视频监控系统作为重要的安全管理手段&#xff0c;在变电站中起到了关键的作用。 目前青犀视频研发的萤石设备视频接入平台EasyC…...

什么是多线程?请描述 Java 中实现多线程的基本方式?

今天和大家探讨一下 Java 中的多线程&#xff0c;包括它的基本概念、实现方式以及一些实际开发中的注意事项。 什么是多线程&#xff1f; 多线程是指在一个程序中存在多个执行流&#xff0c;每个执行流都可以独立于其他执行流执行。 在 Java 中&#xff0c;多线程允许开发者…...

Dynamic Sparse No Training: Training-Free Fine-tuning for Sparse LLMs

大语言模型&#xff08;LLM&#xff09;在设备上部署道路上落下了一个令人生畏的障碍。本文关注于大语言模型的剪枝算法。 动态稀疏训练&#xff08;Dynamic Sparse Training&#xff0c;DST&#xff09;是一种近期收到广泛关注的剪枝算法。与之前大部分剪枝方法需要训练整个网…...

解决n+1查询数据库问题

文章目录 1. 问题描述2. 解决方法3. 总结 1. 问题描述 在写项目中&#xff0c;可能会碰到一个问题&#xff1a;通过查询表A得到一个list结果&#xff0c;再对list中的n个元素各查询一次关联的表B。形成对数据库执行n1次查询。这种代码会无形增加数据库的处理负担&#xff0c;影…...

DICOM 基础知识:深入理解DICOM数据结构与标签说明

目录 DICOM 图像概念 DICOM 图像关键特性&#xff1a; DICOM 文件结构 常见数据元素&#xff1a; 数据元素示例详解 DICOM-VR 数据类型说明 DICOM 标准支持的数据集 结语 DICOM 图像概念 DICOM&#xff08;Digital Imaging and Communications in Medicine&…...

Git - 如何删除 push 过一次的文件链路追踪?

&#xff08;以 target 文件夹为例&#xff09;如果你已经在 .gitignore 中添加了 target/ 目录&#xff0c;但 target 文件夹仍然出现在 Git 的变更列表中&#xff0c;可能是因为它之前已经被添加到 Git 仓库中。即使你更新了 .gitignore&#xff0c;Git 仍然会跟踪这些文件。…...

软件测试学习总结

一.软件测试概念和目的 软件测试的概念: 测试模型(V模型) 软件测试就是在软件投入运行前,对软件需求分析、设计规格说明和编码实现的最终审查,它是软件质量保证的关键步骤。 通常对软件测试的定义有两种描述: 定义1:软件测试是为了发现错误而执行程序的过程 定义2:…...

c语言错题——#define对应的查找替换

文章目录 一、题目 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、题目 分析 结构体向最长的char对齐&#xff0c;前两个位段元素一共42位&#xff0c;不足8位&#xff0c;合起来占1字节&#xff0c;最后一个单独1字节&#xff0c;一共3字节。另外…...

Visual Basic介绍及简单例子

Visual Basic(简称 VB)是一种由微软公司开发的包含协助开发环境的事件驱动编程语言。 一、主要特点 易于学习和使用: Visual Basic 具有直观的可视化开发环境,使用户可以通过拖放控件和设置属性的方式快速创建用户界面。对于初学者来说,这种方式非常容易上手,无需深入了…...

Matlab学习01-矩阵

目录 一&#xff0c;矩阵的创建 1&#xff0c;直接输入法创建矩阵 2&#xff0c;利用M文件创建矩阵 3&#xff0c;利用其它文本编辑器创建矩阵 二&#xff0c;矩阵的拼接 1&#xff0c;基本拼接 1&#xff09; 水平方向的拼接 2&#xff09;垂直方向的拼接 3&#xf…...