SpecialWeekSpecialWeek

功能完善

本文导航按标题快速定位

登录功能

浏览器登录请求 -> 后端执行登录操作访问数据库 -> 用户存在则生成令牌并返回给浏览器 -> 接下来的浏览器请求都会带着这个令牌被拦截器拦截,由拦截器解析请求头中的token标识的令牌。

令牌

在拦截器通过在请求头获得令牌jwtProperties.getAdminTokenName()是我们在jwtProperties中通过注入的方式赋值,在yml配置文件中配置了他的值。

String token = request.getHeader(jwtProperties.getAdminTokenName());

密码加密

MD5的加密方式是不可逆的(只能明文->密文)。除了MD5的加密方式,还有HS256
所以MD5比对方式是对比密文。因为是对比数据库中的密码所以要将数据库中的用户密码改成密文。
md5DigestAsHex(转换), DigestUtils(对比)

"进行d5加密,然后再进行比对"
password = DigestUtils.md5DigestAsHex(password.getBytes()),
if(!password.equals(employee.getPassword())){
"密码错误"
  throw new PasswordErrorException(MessageConstant.PASSWORD ERROR);
}

导入接口文档

可以用Yapi(接口管理平台),也可以用Apifox导入YApi文件
进入到Apifox中后创建新项目-> 项目里点更多功能-> 导入-> 选Yapi

Swagger

使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。类似postman。用Apifox的话就不用一下的配置了。

1.Knife4i(封装了Swagger)是为Java MVC框架集成Swagger生成Api文档的增强解决方案。
如果用的Apifox的话就不用配置这个了。

<dependency>
<groupld>com.github.xiaoymin</goupld>
<artifactld>knife4j-spring-boot-starter</artifactld>
<version>3.0.2</version>
</dependency>

2.还需要完成Knifej的配置,在WebMvcConfigurationg中声名第三方的bean。

@Bean
public Docket docket(){
  log.info =("准备生成接口文档...")
  Apilnfo apilnfo = new ApilnfoBuilder()
    .title(“苍穹外卖项目接口文档”)
    .version(2.0)
    .description("苍穹外卖项目接口文档")
    .build();

  Docket docket = new Docket(DocumentationType.SWAGGER 2)
    .apilnfo(apilnfo)
    .select()//指定生成接口需要扫描的包
    .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
    .paths(PathSelectors.any())
    .build();
  return docket;
}

扫描的包是controller,它会根据controller包中的方法来生成对应的接口文档。
3.设置静态资源映射,(否则接口文档页面无法访问)。

protected void addResourceHandlers(ResourceHandlerRegistry registry) {
  log.info("开始设置静态资源映射…");
  registry.addResourceHandler("/doc.htm!").addResourceLocations("classpath:/META-INF/resources/.")
  registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/ebjars/");
}/WebMvcConfigurationg

当我们请求Hander中的路径是就会把资源(生成的接口文档)映射到Locations中的路径中去。
运行完成后就可以通过localhost:8080/doc.html链接显示接口文档了(此接口文档由Knifej动态生成)。

常用注解

@Api 用在类上,例如Controller,表示对类的说明
@ApiModel 用在属性上,描述属性信息
@ApiModelProperty 用在属性上,
@ApiOperation 用在方法上,例如Controller的方法,说明方法的用途、作用

员工操作

新增员工

(1)设计
1.接口设计
 设计请求方式,请求参数,返回数据等
管理端发出的请求,统一使用/admin作为前缀
用户端发出的请求,统一使用/user作为前缀

2.数据库设计
 如根据需求设计字段的属性,约束等.

补充:DTO
 在创建新用户的时候,我们一般用对应的实体类来接受数据,但因为创建用户的话有些属性可以先不填,此时我们可以用DTO来精确的封装,当然想用实体类的话也可以。比如在前端创建用户是用输入名字和密码,而实体类中的属性有id,名字和密码。这时候我们可以创建一个只有名字和密码的DTO。但是在传给持久层的时候建议使用实体类(在service实现类方法中进行类型转换,然后补全数据),说明:DTO跟实体类字段个数不同的话可以防止数据库字段暴露。

在service中可以使用,对象属性拷贝 能自动为两个类的对象中名字相同的属性赋值。1付给2

BeanUtils.copyProperties(employeeDTO, employee);

整体构造:Controller -> service(补充数据) -> mapper

(2)获取登录员的ID
 在下发令牌的时候我们就将登录员的ID放到了令牌里,在发放令牌后,接下来浏览器的请求头中都会携带这个token,然后在拦截器中解析token,此时我们就可以获取登陆员的ID,那么我们在拦截器中获得了ID,怎么将他传到service中呢?
我们可以用ThreadLocal,他并不是一个Thread(一个请求就是一个线程),而是Thread的局部变量。

 ThreadLocal为每个线程提供单独一份存储空间(那么我们就可以在线程的生命周期内,在所有地方去共享这个空间),具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不
能访问。

在黑马提供的初始代码中我们在BaseContext中设置好了ThreadLocal,我们将拦截器获取的ID存到里面。

ThreadLocal常用方法:
 · public void set(T value) 设置当前线程的线程局部变量的值
 · public T get() 返回当前线程所对应的线程局部变量的值
 · public void remove() 移除当前线程的线程局部变量

service层代码

设计代码展开
public void save(EmployeeDTO employeeDTO) {
    Employee employee = new Employee();
    //这是对象属性拷贝 能自动为名字想相同的属性赋值。
    BeanUtils.copyProperties(employeeDTO, employee);

    employee.setStatus(StatusConstant.ENABLE);

    //设置默认密码123456
    employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));

    employee.setCreateTime(LocalDateTime.now());
    employee.setUpdateTime(LocalDateTime.now());

    //ToDo 后续完善
    employee.setCreateUser(BaseContext.getCurrentId());
    employee.setUpdateUser(BaseContext.getCurrentId());

    employeeMapper.insert(employee);
}

分页查询

1.接口设计
 通过Query形式,就是用地址+?的形式传参;
请求参数为page(页码)、pageSize(每页记录数),返回数据记录数和员工信息
2.代码开发
 设计DTO(name(非必须),page,pageSize),设计PageResult类,将PageResult放到Result中返回就行。记得在Controller方法中的返回值类型写这个Result提前指定。查询类型的请求提前指定,非查询类型的就不用指定了。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {

private long total;//总记录数

private List records;//当前页数据集合

}

3.业务层
 在service中使用pagehelper(需导入插件)可以简化分页查询的SQL语句。根据pagehelper的使用规则我们需要用page来接收查询的结果。底层用到了ThreadLocal,将page和pageSize存到了空间中,在我们编写Sql语句后,他会动态的加上limit语句,并从空间中取用page和pageSize。并且它会自动执行一个SQL语句:select count(0) fron 数据表; 来统计数据量。
controller层

@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {
Log.info("员工分页查询,参数为:{}",employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
return Result. success(pageResult);
}

service层

public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO){
// select * from employee limit 0,10
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());

Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);

long total = page.getTotal();
List<Employee> records = page.getResult();
//这里用到了有参构造。
return new PageResult(total, records);
}

因为name可有可无所以用动态SQL,在service -> yml配置文件中用mapper-locations来扫描Mapper.xml文件。

employeeMapperxml

<select id="pageQuery" resultType="com.sky.entity.Employee">
    select * from employee
    <where>
        <if test="name != null and name != ''">
            and name like concat('%',#{name},'%')
        </if>
    </where>
    order by create_time desc
</select>

优化
我们会发现我们返回的员工信息中的时间格式,前端识别不出来导致前端页面不能正常显示。

时间格式处理

  方式一:采用注解(只能处理一个类的属性)

@JsonFormat (pattern = "yyyy-MM-dd HH:mm: ss")
  private LocalDateTime updateTime;

  方式二:统一处理
  

  protected void extendMessageConverters(List<HttpMessageConverter <? >> converters){
    log.info("开始扩展消息转换器.");

  //创建一个消息转化器对象
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
  //设置对象转换器,可以将Java对象序列化为json数据
    converter.setobjectMapper(new JacksonObjectMapper());

  //将我们自己的转换器放入spring MVC框架的容器中
    converters.add(0,converter);
}

对象转换器黑马提供的初始框架中就有commen -> json -> JacksonObjectMapper

对象转化器可以将java对象转换成json,也可以将解析json成java对象(反序列化)

converters中存放的是工程中所有的消息转换器。

其中add中的零是说优先执行converter消息转换器。

启用禁用员工账号

1.设计接口
我们通过路径参数传入状态,发送方式:post路径:/admin/employee/status/{status}

通过Query发送员工id数据
2.代码设计
动态SQL的一般写法,没新的东西。启用禁用通过status的值来判断。

编辑员工

1.接口设计
 编辑员工设计两个接口。1.根据id查员工信息(Get,路径参数)。2.编辑员工信息(PUT,json形式)。
2.代码开发
 在service层,其他一般的写法。

public void update(EmployeeDTO employeeDTO) {
    Employee employee = new Employee();
    BeanUtils.copyProperties(employeeDTO,employee);
    employee.setUpdateTime(LocalDateTime.now());
    employee.setUpdateUser(BaseContext.getCurrentId());
    employeeMapper.update(employee);
}

因为在前面我们已经通过ThreadLocal设置好了Id拿来直接用就行了。

分类(菜品)管理

设计修改、查询、分页查询、新增,查看分类等操作;在数据库中category表中储存着数据

设计代码展开 1.DishMapper和SetmealMapper中实现查询菜单和setmeal(套餐)分类下的菜品数量;
@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);

2.在categoryMapper中编写insert,pagequery,delete,update等方法。
3.在service层中实现对应的方法。

展开
@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;
    //新增分类
    public void save(CategoryDTO categoryDTO) {
        Category category = new Category();
        //属性拷贝
        BeanUtils.copyProperties(categoryDTO, category);
        //分类状态默认为禁用状态0
        category.setStatus(StatusConstant.DISABLE);
        //设置创建时间、修改时间、创建人、修改人
        category.setCreateTime(LocalDateTime.now());
        category.setUpdateTime(LocalDateTime.now());
        category.setCreateUser(BaseContext.getCurrentId());
        category.setUpdateUser(BaseContext.getCurrentId());

        categoryMapper.insert(category);
    }
    //分页查询
    public PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) {
        PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());
        //下一条sql进行分页,自动加入limit关键字分页
        Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

    public void deleteById(Long id) {
        //查询当前分类是否关联了菜品,如果关联了就抛出业务异常
        Integer count = dishMapper.countByCategoryId(id);
        if(count > 0){
            //当前分类下有菜品,不能删除
            throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH);
        }

        //查询当前分类是否关联了套餐,如果关联了就抛出业务异常
        count = setmealMapper.countByCategoryId(id);
        if(count > 0){
            //当前分类下有菜品,不能删除
            throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL);
        }

        //删除分类数据
        categoryMapper.deleteById(id);
    }
    public void update(CategoryDTO categoryDTO) {
        Category category = new Category();
        BeanUtils.copyProperties(categoryDTO,category);
        //设置修改时间、修改人
        category.setUpdateTime(LocalDateTime.now());
        category.setUpdateUser(BaseContext.getCurrentId());

        categoryMapper.update(category);
    }

    public void startOrStop(Integer status, Long id) {
        Category category = Category.builder()
                .id(id)
                .status(status)
                .updateTime(LocalDateTime.now())
                .updateUser(BaseContext.getCurrentId())
                .build();
        categoryMapper.update(category);
    }
    //根据类型查询分类
    public List<Category> list(Integer type) {
        return categoryMapper.list(type);
    }
}
3.在categoryController中编写对应的方法。最后配置Mapper.xml文件。

公共字段自动填充

 如创建/修改时间,创建/修改者ID等都要我们在service实现类中手动填充,我们要做的是让他们自动填充。
具体方法:
  ● 自定义注解AutoFill(主要是放在mapper上),用于标识需要进行公共字段自动填充的方法

  ● 自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值

  ● 在Mapper的方法上加入AutoFill 注解

技术点:枚举、注解、AOP、反射

自定义注解在server -> annotation -> Autofill

设计代码展开
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    //指定数据库的操作类型。
    OperationType value();
}

设计切面类

设计代码展开
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    //切入点
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    //拦截mapper中的所有方法。但是新增,还有删除的化,就不需要太南充字段,
    //所以使用annotation中指定只在加上了AutoFill注解的方法进行太填充
    public void autoFillPointCut(){}
//    使用前置通知
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("开始进行公共字段自动填冲");
        //获得方法签名的对象,getMethod是MethodSignature特有的方法,所以要转型。
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //获得在方法上的注解对象,目的是看配置的数据库操作类型
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
        OperationType operationType = autoFill.value();
        //获得方法中的参数
        Object[] args = joinPoint.getArgs();
        if(args == null || args.length == 0){
            return;
        }
        //在我们定义的方法中实体类对象一般是放在方法参数的第一位
        Object entity = args[0];

        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();
        //用ser方法进行映射赋值,在双引号中设置方法名的时候用可常量代替。
        if(operationType == OperationType.INSERT){
            try {
                Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
                //通过反射为对象属性赋值
                setCreateTime.invoke(entity, now);
                setCreateUser.invoke(entity, currentId);
            } catch (Exception e) {
               e.printStackTrace();
            }
        }
        try {
            Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);
            Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
            setUpdateTime.invoke(entity, now);
            setUpdateUser.invoke(entity, currentId);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

最后在Mapper方法上加上注解如:更新@AutoFill(value = OperationType.UPDATE),OperationType.UPDATE是我们定义的常量。common -> enumeration;

public enum OperationType {
    UPDATE,
    INSERT
}

配置好后就可以将service实现类中把对应的普补充数据的语法删掉就行了。

文件上传

(1) 设计
业务规则:
· 菜品名称必须是唯一的。
· 菜品必须属于某个分类下,不能单独存在。
· 新增菜品时可以根据情况选择菜品的口味。
· 每个菜品必须对应一张图片。
在上面我们完成了分类查询的功能,现在我们设计文件上传和新增菜品

引入概念:逻辑外键:我们在程序中把该字段看成外键,但是并不把他的约束设置成外键。

上传文件接口设计:post 在请求头中Content-Type配置成multipart/form-data Body是file类型的

(2) 代码开发
设计上传文件的Controller,我们这里命名成CommonController
1.配置属性,我们可以直接在application.yml中填写数据,也可以用引用application-dev.yml的属性,以下是引用的方法。前提是在yml文件中的spring标签中的profils中和dev进行绑定,之就说明我们可以绑定其他的配置文件。(application.yml是主配置文件),这样的作用是环境隔离。

配置文件展开 >
# dev.yml 记得改回自己的密钥
sky:
alioss:
endpoint: 
access-key-id: 
access-key-secret: 
bucket-name: 
# yml
sky:
alioss:
endpoint: ${sky.alioss.endpoint}
access-key-id: ${sky.alioss.access-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${sky.alioss.bucket-name}
2.创建AliossUtil,在登陆的时候也创建了一个JWTUtil用来生成和解析令牌。
设计代码展开
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    //文件上传
    public String upload(byte[] bytes, String objectName) {

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            // 创建PutObject请求。
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder
                .append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(objectName);

        log.info("文件上传到:{}", stringBuilder.toString());

        return stringBuilder.toString();
    }
}
3.想着只属于第三方的Bean,所以我们创建了AliOSS的配置类来交给ioc这个bean,创建令牌的时候我们并没有创建对应的配置类。
设计代码展开
@Configuration
@Slf4j
public class OssConfiguration{
@Bean
@ConditionalOnMissingBean//这个注解保证这个bean是单例。
   public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
   log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
   return new AliOssUtil(aliOssProperties.getEndpoint(),
   aliOssProperties.getAccessKeyId(),
   aliOssProperties.getAccessKeySecret(),
   aliOssProperties.getBucketName());
   }
}
AliOssProperties是我们创建的配置属性类,事实上直接创建AliOssUtil就行了,因为我们已经完成了注入。 我们也可以不写配置类,在AliOssUtil中引用AliOssproperties赋值后,直接交给ioc也行。推荐使用配置类。

4.CommonController

代码展开
先注入AliOssUtil
@PostMapping("/upload")
public Result<String> upload(MultipartFile file){
Log.info("文件上传:{}",file);
 
try {
//原始文件名
    String originalFilename = file.getOriginalFilename();
//截取原始文件名的后缀 dfdfdf.png
    String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
//构造新文件名称
    String objectName = UUID.randomUUID().toString() + extension;
//文件的请求路径
    String filePath = aliOssUtil.upload(file.getBytes(), objectName);
    return Result.success(filePath);
} catch (IOException e) {
    log.error(“文件上传失败:{},e);
}
return Result.error("文件上传失败。");
}
如果不能正常显示图片可以试着将bucket(Alioss中)权限设置为公共读。

菜品操作

 创建DishController,service和mapper,因为我们一个菜品还对应者多个口味,在数据库中我们有菜品表和口味表,在菜品的实体类中我们还有一个存储口味的集合list<口味类>,涉及到多张表所以要用到事务(在service中用注解开启事务)。

新增

准备Dish,DishDTO(其中具有Flavor集合),DishFlavor实体类,DishServiceImpl,Dish Mapper和DishFlavorMapper,及对应xml配置文件

DishController

@GetMapping("/page")
 public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
     log.info("菜品分页查询{}",dishPageQueryDTO);
     PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
     return Result.success(pageResult);
 }
DishServiceImpl
@Service
@Slf4j
public class DishServiceImpl implements DishService {

    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavoerMapper dishFlavoerMapper;

    @Transactional
    public void saveWithFlavor(DishDTO dishDTO) {
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);

        dishMapper.insert(dish);
        //因为id是自动分配的所以在插入前我们不知道id是多少,通过在Mapper.XML的配置就可以返回id。
        //从而获取id的值。赋值给dish_id
        Long dishId = dish.getId();
         List<DishFlavor> flavors = dishDTO.getFlavors();
         if(flavors!=null&&flavors.size()>0){
            //遍历将外键赋值
             flavors.forEach(dishFlavor->{
                 dishFlavor.setDishId(dishId);
             });

            dishFlavoerMapper.insertBatch(flavors);

         }
    }
}

对于DishFlavor因为传入的是集合所以采用批量处理的操作,foreach

DishFlavorMapper

@Mapper
public interface DishFlavoerMapper {

    //批量加如口味
    void insertBatch(List<DishFlavor> flavors);
}

DishFlavorMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishFlavoerMapper">

    <insert id="insertBatch">
          insert into dish_flavor(dish_id, name, value) values
        <foreach collection="flavors" item="df" separator=",">
            (#{df.dishId},#{df.name},#{df.value})
        </foreach>
    </insert>
</mapper>

collection指定集合名称,item为集合元素命名,separator因为集合中的元素是以逗号进行分割的,所以指定逗号分割。

DishMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into dish(name, category_id, price, image, description, create_time, update_time, create_user, update_user,status)
        values
        (#{name},#{categoryId},#{price},#{image},#{description},#{categoryId},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})
    </insert>
</mapper>

useGeneratedKeys是否返回主键。KeyProperty主键映射指定成实体类中主键的命名

DishController

@RestController
@RequestMapping("/admin/dish")
@Slf4j
public class DishController {
    @Autowired
    private DishService dishService;

    @PostMapping
    public Result save(@RequestBody DishDTO dishDTO){
        log.info("新增菜品{}",dishDTO);
        dishService.saveWithFlavor(dishDTO);
        return Result.success();
    }
}

分页查询

接口:get方式,参数类型:Query,返回数据:id,name,price,image,description,status,updateTime,catoryName。因为在dish类中并没有分类名称,只有分类ID,所以我们需要创建一个VO用来返回。整体的话和员工分页查询一样,只不过多了一个VO

VO
展开
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishVO implements Serializable {
    private Long id;
    //菜品名称
    private String name;
    //菜品分类id
    private Long categoryId;
    //菜品价格
    private BigDecimal price;
    //图片
    private String image;
    //描述信息
    private String description;
    //0 停售 1 起售
    private Integer status;
    //更新时间
    private LocalDateTime updateTime;
    //分类名称
    private String categoryName;
    //菜品关联的口味
    private List<DishFlavor> flavors = new ArrayList<>();
    //private Integer copies;
}

service层

public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
       PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
       Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
       return new PageResult(page.getTotal(),page.getResult());
   }

动态SQL采用了多表联查,因为在dish表和category表中的名字字段都是name,所以在映射的时候可能导致DishVO的catgoryname属性赋值不上,所以需要在查询SQL中为category的name字段起别名。

<select id="pageQuery" resultType="com.sky.vo.DishVO">
     select d.*,c.name as categoryName from dish d left outer join category c on d.category_id = c.id
     <where>
         <if test="name != null">
             and d.name like concat('%',#{name},'%')
         </if>
         <if test="categoryId != null">
             and d.category_id = #{categoryId}
         </if>
         <if test="status != null">
             and d.status = #{status}
         </if>
     </where>
     order by d.create_time desc
 </select>

删除菜品

业务规则:

  • 可以一次删除一个菜品,也可以批量删除菜品
  • 起售中的菜品不能删除
  • 被套餐关联的菜品不能删除
  • 删除菜品后,关联的口味数据也需要删除掉

接口:delete,query传参,参数名名称ids,在地址中id之间用逗号隔开(如:ids = 1,2,3)。删除的话数据库设计需要涉及三张表dish,dish_flavor,setmeal_dish(套餐关联表)

对于地址中的ids他其实是字符串类型的,在controller中可以用String来接收,再用逗号分割后处理,当然用集合接收是最简单的

controller

public Result delete(@RequestParam List<Long> ids){
      log.info("菜品删除:{}",ids);
      dishService.deleteBatch(ids);
      return Result.success();
  }

service

展开
public void deleteBatch(List<Long> ids) {
    //判断菜品是否是起售中的菜品
    for(Long id:ids){
        Dish dish = dishMapper.getById(id);
        if(dish.getStatus() == StatusConstant.ENABLE){
            throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
        }
    }
    //是否配套餐关联了
    List<Long> setmealids = setmealDishMapper.getSetmealIdByDishIds(ids);
    if(setmealids!=null&&setmealids.size()>0){
        throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
    }
    //删除dish表中的数据
    for(Long id:ids){
        dishMapper.deleteById(id);
        //删除dish_flavor数据
        dishFlavoerMapper.deleteByDishId(id);
    }
}

for循环可以用批量查询(SQL -> in)来代替,先批量查询,然后拿到结果集,使用java8的stream中的anyMatch来做,替换的换写法和下面的SetmealDish.xml一样

书写DishMapper,DishFlavoerMapper,SetmealDishMapper

SetmealDish.xml

<select id="getSetmealIdByDishIds" resultType="java.lang.Long">
         select * from setmeal_dish where dish_id in
         <foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
             #{dishId}
         </foreach>
 </select>

修改菜品

在修改之前需要进行查询操作,在修改页面将信息回显。在查询菜品的时候需要将口味相关的数据也查询出来。所以要用DishVO来返回

接口设计:查询GET Path参数,修改:PUT,json数据

1.用两步在Dishservice实现类中查询数据,controller和mapper一般写法

public DishVO getByIdWithFlavor(@PathVariable Long id) {
      Dish dish = dishMapper.getById(id);
      List<DishFlavor> dishFlavors = dishFlavoerMapper.getByDishId(id);
 
      DishVO dishVO = new DishVO();
      BeanUtils.copyProperties(dish,dishVO);
      dishVO.setFlavors(dishFlavors);

      return dishVO;
  }

2.修改提交的数据和新增的接收一样用DishDTO,返回的话Result中date是空

展开 1.controller一般写法(别忘了写查询的Mapping方法)
@PutMapping
 public Result update(@RequestBody DishDTO dishDTO){
     log.info("修改菜品{}",dishDTO);
     dishService.updateWithFlavor(dishDTO);
     return Result.success();
 }

2.service的话稍微难一点,因为口味在修改时可以把原来的删除重新添加新的。我们采用先删除再重新插入新数据。所以我们分三步1.修改菜品基本信息。2.删除口味。3.重新插入口味(采用上面的批量插入:saveWithFlavor方法中)。

public void updateWithFlavor(DishDTO dishDTO) {
       Dish dish = new Dish();
       BeanUtils.copyProperties(dishDTO, dish);
       //修改菜品基本信息
       dishMapper.update(dish);
       //删除原本口味
       dishFlavoerMapper.deleteByDishId(dish.getId());
       //重新插入新口味
       List<DishFlavor> flavors = dishDTO.getFlavors();
       if(flavors!=null&&flavors.size()>0){
           flavors.forEach(dishFlavor->{
               dishFlavor.setDishId(dishDTO.getId());
           });
           dishFlavoerMapper.insertBatch(flavors);
       }
   }

插入的时候需要给Flavor中的dish_id赋值。这个功能用到Mapper都是以前写的。

菜品起售禁售

采用post接口,id用Query传,status用path传

1.dishController

@PostMapping("/{status}")
public Result sale(@PathVariable Integer status,Long dishId){
    dishService.updateSale(status,dishId);
    return Result.success();
}

dishServiceimpl

public void updateSale(Integer status,Long dishId) {
    Dish dish = Dish.builder()
            .status(status)
            .id(dishId)
            .build();
    dishMapper.update(dish);
}
DishMapper.xml代码展开
<update id="update">
       update dish
           <set>
               <if test="name != null">
                   name = #{name},
               </if>
               <if test="categoryId != null">
                   category_id = #{categoryId},
               </if>
               <if test="price != null">
                   price = #{price},
               </if>
               <if test="status != null">
                   status = #{status},
               </if>
               <if test="image != null">
                   image = #{image},
               </if>
               <if test="description != null">
                   description = #{description},
               </if>
               <if test="updateTime != null">
                   update_time = #{updateTime},
               </if>
               <if test="updateUser != null">
                   update_user = #{updateUser}
               </if>
           </set>
           where id = #{id}
   </update>

套餐操作

新增套餐

根据需求我们需要设计两个接口1.根据分类Id查询菜品{Get;Query}。2.新增套餐(和新增菜品差不多){Post;json},Query数据接收的时候不用注解,Path参数接收需要注解,因为在套餐中包含菜品,我们需要准备好SetmealDish实体类,还有SetmealDishMapper及xml,方便我们在setmealService中调用setmeal_dish数据库。

根据Id查询菜品,在套餐添加菜品时前端会发送此请求
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<Dish>> list(Long categoryId){
    List<Dish> list = dishService.list(categoryId);
    return Result.success(list);
}
public List<Dish> list(Long categoryId) {
    Dish dish = Dish.builder()
        .categoryId(categoryId)
        .status(StatusConstant.ENABLE)
        .build();
    return dishMapper.list(dish);
}
<select id="list" resultType="Dish" parameterType="Dish">
    select * from dish
    <where>
        <if test="name != null">
            and name like concat('%',#{name},'%')
        </if>
        <if test="categoryId != null">
            and category_id = #{categoryId}
        </if>
        <if test="status != null">
            and status = #{status}
        </if>
    </where>
    order by create_time desc
</select>
套餐新增
public class SetmealController {

    @Autowired
    private SetmealService setmealService;
    @PostMapping
    @ApiOperation("新增套餐")
    public Result save(@RequestBody SetmealDTO setmealDTO) {
        setmealService.saveWithDish(setmealDTO);
        return Result.success();
    }
}
    @Transactional
    public void saveWithDish(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);
        //向套餐表插入数据
        setmealMapper.insert(setmeal);
        //获取生成的套餐id
        Long setmealId = setmeal.getId();
        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });
        //保存套餐和菜品的关联关系
        setmealDishMapper.insertBatch(setmealDishes);
}

正常写SetmealMapper和xml记得返回主键

SetmealDishMapper.xml

<insert id="insertBatch" parameterType="list">
    insert into setmeal_dish
    (setmeal_id,dish_id,name,price,copies)
    values
    <foreach collection="setmealDishes" item="sd" separator=",">
        (#{sd.setmealId},#{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})
    </foreach>
</insert>

修改,删除,分页查询可以参考菜品操作

分页查询setmealMapper.xml代码展开
<select id="pageQuery" resultType="com.sky.vo.SetmealVO">
    select
    	s.*,c.name categoryName
    from
    	setmeal s
    left join
    	category c
    on
    	s.category_id = c.id
    <where>
        <if test="name != null">
            and s.name like concat('%',#{name},'%')
        </if>
        <if test="status != null">
            and s.status = #{status}
        </if>
        <if test="categoryId != null">
            and s.category_id = #{categoryId}
        </if>
    </where>
    order by s.create_time desc
</select>

起售和停售

SetmealController

@PostMapping("/status/{status}")
@ApiOperation("套餐起售停售")
public Result startOrStop(@PathVariable Integer status, Long id) {
    setmealService.startOrStop(status, id);
    return Result.success();
}

SetmealService

void startOrStop(Integer status, Long id);

SetmealServiceImpl

public void startOrStop(Integer status, Long id) {
    //起售套餐时,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含未启售菜品,无法启售"
    if(status == StatusConstant.ENABLE){
        //select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ?
        List<Dish> dishList = dishMapper.getBySetmealId(id);
        if(dishList != null && dishList.size() > 0){
            dishList.forEach(dish -> {
                if(StatusConstant.DISABLE == dish.getStatus()){
                    throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
                }
            });
        }
    }

    Setmeal setmeal = Setmeal.builder()
        .id(id)
        .status(status)
        .build();
    setmealMapper.update(setmeal);
}

DishMapper

@Select("select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}")
List<Dish> getBySetmealId(Long setmealId);

到这里可以看一下:《Redis快速入门》

店铺营业状态设置

需求分析

接口设计:需要查询状态(GET)的接口和修改状态(PUT)的接口。这里我们需要设计两个查询的接口一个用户端和一个管理端。在前面管理端用/admin做前缀,用户端用/user做前缀。

1.设置营业状态。
alt text
2.查询
管理端和用户端就一个前缀不同。
alt text

现在我们想把这个status存到哪里呢,我们创建一个表来存储的话,一张表就这一个数据显然太浪费了。我们可以将这个status用Redis的字符串来存储。

代码实现

代码也是非常的简单,就写一个ShopController就行了。

@Autowired
private RedisTemplate redisTemplate;

@PutMapping("/{status}")
public Result setStatus(@PathVariable Integer status){
    log.info("设置店铺营业状态为{}",status == 1 ? "营业中" : "打烊中");
    redisTemplate.opsForValue().set("SHOP_STATUS",status);
    return  Result.success();
}

@GetMapping("/status")
public Result<Integer> getstatus(){
    Integer status = (Integer)redisTemplate.opsForValue().get("SHOP_STATUS");
    log.info("获取到店铺的营业状态:{}",status == 1 ? "营业中" : "打烊中");
    return null;
}

需要注意一下我们用什么数据传给Redis,取用的时候需要强转回原本的数据类型

对于用户端我们在controller中创建一个新的包,需要注意的是在admin和user中的Conller不能是同名,这些Cotroller都会放到spring容器中,如果重名的话,创建的bean也会重名就会报错,我们可以在Controller类中指定Bean的名称来避免这个问题(RestController指定名称。)

微信小程序开发