nginx 的反向代理 前端发送的请求,是如何请求到后端服务的?
nginx 的反向代理,就是将前端发送的动态请求由 nginx 转发到后端服务器 nginx 反向代理的好处: 提高访问速度 进行负载均衡,所谓的负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器
使用MD5信息摘要算法完善登录功能
对数据库中存储的账户密码进行处理,首先修改数据库中的明文密码,改为MD5加密后的密文。 修改Java代码,前端提交的密码进行MD5加密后再跟数据库中的密码对比,包括学校教务系统也是这个逻辑进行 实现的(我猜的,因为每次输入完密码之后点击登录我的密码都会变成一长串)。并且java中自带MD5识别函数:DigestUtils.md5DigestAsHex 代码实现:
1 2 //md5加密对比 password = DigestUtils.md5DigestAsHex(password.getBytes());
导入接口文档 使用 Swagger 你只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。,[链接] 导入接口可以进行更好的测试。Knife4j 是为 Java MVC 框架集成 Swagger 生成 Api 文档的增强解决方案。 Yap i是设计阶段使用的工具,管理和维护接口。Swagger在开发阶段使用的框架,帮助后端开发人员做后端的接口测试。
使用Swagger方法 Swagger作用为进行后端测试的常用工具. 首先导入knife4j的maven坐标,再在配置类中加入knife4j相关配置,然后设置静态资源映射,否则接口文档页面无法访问 在本项目中的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /** * 通过knife4j生成接口文档 * @return */ @Bean public Docket docket() { ApiInfo apiInfo = new ApiInfoBuilder() .title("苍穹外卖项目接口文档") .version("2.0") .description("苍穹外卖项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller")) .paths(PathSelectors.any()) .build(); return docket; } /** * 设置静态资源映射 * @param registry */ protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); }
其中在设置静态资源映射部分,我们可以看到它是从哪个路径来找到生成的接口文档的,即 [链接] 如果没有设置静态资源映射的话我们就会打不开整个接口文档网站,因为其并不会将其当成静态网页处理而是动态控制命令。
常用注解 通过一些注解,我们可以来控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:
1 2 3 4 5 6 7 8 9 @Api :用在类上,例如Controller,表示对类的说明@ApiModel @ApiModel :用在类上,例如entity、DTO、VO @ApiModelProperty @ApiModelProperty :用在属性上,描述属性信息@ApiOperation @ApiOperation :用在方法上,例如Controller的方法,说明方法的用途、作用
员工管理模块
需求分析: 1 2 3 4 5 账号必须是唯一的 手机号为合法的 11 位手机号码 身份证号为合法的 18 位身份证号码 注意到表单项中没有 “密码” ,这里我们约定,新增员工时会默认员工密码为 123456,后续员工可以自行修改密码
设计:
因为我们是前后端分离开发,所以我们必须先设计好接口后,才能进行前后端的开发。在接口设计中,我们需要考虑请求的路径以及请求的
类型;同时,我们需要定义前端需要传给后端的参数,以及后端返回给前端的数据格式。 由于本项目分为管理端和用户端,所以统一规定:
管理端发出的请求,统一使用 /admin 作为前缀
用户端发出的请求,统一使用 /user 作为前缀
代码开发: 根据新增员工接口设计对应的DTO: 注意:当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 @Data public class EmployeeDTO implements Serializable { private Long id; private String username; private String name; private String phone; private String sex; private String idNumber; } 在 EmployeeController 中创建新增员工方法,接收前端提交的参数: /** * 新增员工 * @param employeeDTO * @return */ @PostMapping @ApiOperation("新增员工接口") public Result save(@RequestBody EmployeeDTO employeeDTO) { log.info("新增员工:{}", employeeDTO); employeeService.save(employeeDTO); return Result.success(); } 在 EmployeeService 接口中声明新增员工方法: /** * 新增员工 * @param employeeDTO */ void save(EmployeeDTO employeeDTO); 在 EmployeeServiceImpl 中实现新增员工方法: /** * 新增员工 * * @param employeeDTO */ public void save(EmployeeDTO employeeDTO) { //定义员工对象 Employee employee = new Employee(); //对象属性拷贝: 将employeeDTO的属性拷贝给employee BeanUtils.copyProperties(employeeDTO, employee); //账号状态默认为1, 正常状态 employee.setStatus(StatusConstant.ENABLE); //默认密码为123456 employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes())); //设置当前记录的创建时间和修改时间 employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); //设置当前记录创建人id和修改人id //由于仅凭当前的技术还无法获取这两个id, 所以我们先给它一个固定值 employee.setCreateUser(10L); employee.setUpdateUser(10L); employeeMapper.insert(employee); } 在 EmployeeMapper 中声明 insert 方法: /** * 插入员工数据 * @param employee */ @Insert("insert into employee" + "(name, username, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user) " + "VALUES " + "(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})") void insert(Employee employee);
功能测试 功能测试方式: 通过接口文档测试 , 通过前后端联调测试.
测试完成:
1 2 3 4 代码完善 程序存在的问题: 录入的用户名已存在,抛出异常后没有处理 新增员工时,创建人id和修改人id的设置
针对第一个问题,可以通过全局异常处理器来处理: 添加代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /** * 捕获sql异常 * * @param ex * @return */ @ExceptionHandler public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) { log.info("异常信息:{}", ex.getMessage()); //即 Duplicate entry 'zhangsan' for key 'employee.idx_username' String message = ex.getMessage(); if (message.contains("Duplicate entry")) { String[] split = message.split(" "); String username = split[2]; //获取传进来的用户名 return Result.error(username + MessageConstant.ALREADY_EXITS);//这部分有问题,无法使用MessageConstant.ALREADY_EXITS } return Result.error(MessageConstant.UNKNOWN_ERROR); }
针对第二个问题,我们需要通过某种方式来动态获取当前登录员工的 id: 员工登录成功后会生成 JWT 令牌并响应给前端,后续请求中,前端会携带 JWT 令牌,通过 JWT 令牌可以解析出当前登录员工 id,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 解决方法——使用ThreadLocal ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量。 ThreadLocal 为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。 注意:客户端发送的每次请求,后端的 Tomcat 服务器都会分配一个单独的线程来处理请求,所以这里我们才能使用 ThreadLocal 来处理。 ThreadLocal 常用方法: public void set(T value) :设置当前线程的线程局部变量的值 public T get():返回当前线程所对应的线程局部变量的值 public void remove():移除当前线程的线程局部变量 初始工程中已经封装了 ThreadLocal 操作的工具类,方便我们后续使用 完善代码 在拦截器中解析出当前登录员工 id,并放入线程局部变量中
员工分页查询 需求分析和设计 1 2 3 4 5 业务规则: 根据页码展示员工信息 每页展示 10 条数据 分页查询时可以根据需要,输入员工姓名进行查询
代码开发 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 根据分页查询接口设计对应的 DTO,这个 DTO 在初始工程中已经定义: 请求参数格式为 Query,不是 json 后面所有的分页查询,统一都封装成 PageResult 对象 员工信息分页查询后端返回的对象类型为:Result<PageResult> 根据接口定义创建分页查询方法: /** * 员工分页查询 * * @param employeePageQueryDTO * @return */ @GetMapping("/page") @ApiOperation("员工分页查询") public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) { //不是json格式的数据, 不能加@RequestBody log.info("员工分页查询,参数:{}", employeePageQueryDTO); PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO); return Result.success(pageResult); } 在 EmployeeService 接口中声明 pageQuery 方法: /** * 分页查询 * * @param employeePageQueryDTO * @return */ PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO); 在 EmployeeServiceImpl 中实现 pageQuery 方法: /** * 分页查询 * * @param employeePageQueryDTO * @return */ public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) { //开始分页查询 PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize()); Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO); return new PageResult(page.getTotal(), page.getResult()); } 注意:此处使用了 mybatis 的分页插件 PageHelper 来简化分页代码的开发,底层基于 mybatis 的拦截器实现。 所需依赖已添加
功能测试 可以通过接口文档进行测试,也可以进行前后端联调测试,测试过程中发现最后操作时间字段展示有问题,就是时间的格式好像不太对
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 代码完善 解决方式: 方式一:在属性上加入注解,对日期进行格式化 @JsonFormat (pattern "yyyy-MM-dd HH: mm: ss") private LocalDateTime updateTime; 方式二:在 WebMvcConfiguration 中扩展 SpringMVC 的消息转换器,统一对日期类型进行格式化处理 /** * 扩Spring MVC框架的消息转换器 * * @param converters */ @Override protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { log.info("扩展消息转换器..."); //创建一个消息转换器对象 MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据 //JacksonObjectMapper在com.sky.json中已存在 converter.setObjectMapper(new JacksonObjectMapper()); //将自己的消息转换器加入容器中,并设置为第一位 converters.add(0, converter); }
如果想具体到秒,可以在对象转换器 JacksonObjectMapper 中修改。
启动禁用员工账号 需求分析 1 2 3 4 业务规则: 可以对状态为 “启用” 的员工账号进行 “禁用” 操作 可以对状态为 “禁用” 的员工账号进行 “启用” 操作 状态为 “禁用” 的员工账号不能登录系统
代码开发 根据接口设计中的请求参数形式,对应在 EmployeeController 中创建启用禁用员工账号的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 /** * 启动禁用员工账号 * * @param status * @param id * @return */ @PostMapping("/status/{status}") @ApiOperation("启动禁用员工账号") public Result startOrStop(@PathVariable Integer status, Long id) {//由于变量名相同, 所以注解中不用指定使用哪个路径参数 log.info("启动禁用员工账号:{}, {}", status, id); employeeService.startOrStop(status, id); return Result.success(); } 在 EmployeeService 接口中声明启用禁用员工账号的业务方法: /** * 启动禁用员工账号 * * @param status * @param id */ void startOrStop(Integer status, Long id); 在 EmployeeServiceImpl 中实现启用禁用员工账号的业务方法: /** * 启动禁用员工账号 * * @param status * @param id */ @Override public void startOrStop(Integer status, Long id) { //方式一:可以直接new一个Employee, 再设置id和status //方式二:因为Employee类中已经添加了@Builder注解,所以可以使用builder Employee employee = Employee.builder() .id(id) .status(status) .build(); employeeMapper.update(employee); } 在 EmployeeMapper 接口中声明 update 方法: /** * 根据id修改员工信息 * * @param employee */ void update(Employee employee); 在 EmployeeMapper.xml 中编写 SQL: <!-- 参数类型可以不指定具体包名, 因为在配置文件中我们已经指定了扫描的包为com.sky.entity--> <update id="update" parameterType="Employee"> update employee <set> <if test="username != null">username = #{username},</if> <if test="name != null">name = #{name},</if> <if test="password != null">password = #{password},</if> <if test="phone != null">phone = #{phone},</if> <if test="sex != null">sex = #{sex},</if> <if test="idNumber != null">id_Number = #{idNumber},</if> <if test="updateTime != null">update_Time = #{updateTime},</if> <if test="updateUser != null">update_User = #{updateUser},</if> <if test="status != null">status = #{status}</if> </set> where id = #{id} </update>
编辑员工 需求分析与设计 1 2 3 4 5 接口设计: 编辑员工功能涉及到两个接口: 根据 id 查询员工信息 编辑员工信息
代码开发 根据id查询员工信息接口开发 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 在 EmployeeController 中创建 getById 方法: /** * 根据id查询员工 * * @param id * @return */ @GetMapping("/{id}") @ApiOperation("根据id查询员工") public Result<Employee> getById(@PathVariable Long id) { log.info("根据id查询员工:{}", id); return Result.success(employeeService.getById(id)); } 在 EmployeeService 接口中声明 getById 方法: /** * 根据id查询员工 * * @param id * @return */ Employee getById(Long id); 在 EmployeeServiceImpl 中实现 getById 方法: /** * 根据id查询员工 * * @param id * @return */ @Override public Employee getById(Long id) { Employee employee = employeeMapper.getById(id); employee.setPassword("****"); //设置一下密码, 防止他人通过开发者控制台获取密码 return employee; } 在 EmployeeMapper 接口中声明 getById 方法: /** * 根据id查询员工信息 * * @param id * @return */ @Select("select * from employee where id = #{id}") Employee getById(Long id);
编辑员工信息接口开发 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 在 EmployeeController 中创建 update 方法: /** * 编辑员工信息 * * @param employeeDTO * @return */ @PutMapping @ApiOperation("编辑员工信息") public Result update(@RequestBody EmployeeDTO employeeDTO) { log.info("编辑员工:{}", employeeDTO); employeeService.update(employeeDTO); return Result.success(); } 在 EmployeeService 接口中声明 update 方法: /** * 根据id修改员工信息 * * @param employeeDTO */ void update(EmployeeDTO employeeDTO); 在 EmployeeServiceImpl 中实现 update 方法: /** * 根据id修改员工信息 * * @param employeeDTO */ @Override public void update(EmployeeDTO employeeDTO) { //拷贝对象属性 Employee employee = new Employee(); BeanUtils.copyProperties(employeeDTO, employee); //设置修改人和修改时间 employee.setUpdateUser(BaseContext.getCurrentId()); employee.setUpdateTime(LocalDateTime.now()); //使用前面开发好的update方法 employeeMapper.update(employee); }
导入分类模块功能代码 需求分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 产品原型: 业务规则: 分类名称必须是唯一的 分类按照类型可以分为菜品分类和套餐分类 新添加的分类状态默认为 “禁用” 接口设计: 新增分类 分类分页查询 根据 id 删除分类 修改分类 启用禁用分类 根据类型查询分类 代码导入 从技术层面上来看,分类管理模块和员工管理模块其实是非常类似的。除了操作的表不一样,实际上都是对单个表的增删改查,技术难度是差不多的,所以关于分类管理的代码我们就不再细讲,直接导入即可,小伙伴有不懂的地方也可以在评论区提出来。 在第二天的资料中找到分类管理的代码,导入即可 导入的时候可以按照从后往前的顺序导入,这样代码不会报错 导入完,手动编译一下,导入的类有时候不会自动编译
公共字段自动填充 问题分析 在前面的业务表中,比如表 employee 和 category 中,我们可以发现一些公共字段,这些字段在后面的业务表中也同样存在:这些公共字段在某些操作中都需要设置, 而且设置的代码都是一样的这些公共字段在某些操作中都需要设置,而且设置的代码都是一样的
1 2 3 4 那么就出现了问题:代码冗余、不便于后期维护 工程中存在多段相同的代码 如果后期我们需要对业务表进行修改,涉及到这些公共字段,进行修改也是非常麻烦的
解决思路 我们发现,这些公共字段的设置只会出现在特定的操作中,比如 create_time 和 create_user 只会出现在 insert 操作中。所以,我们的想法是将其统一处理。
1 2 3 4 5 6 7 具体思路: 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值 在 Mapper 中需要处理公共字段的方法上加入 AutoFill 注解 涉及技术点:枚举、注解、AOP、反射
代码开发 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 创建一个专门用来存放自定义注解的包 annotation,在包下创建自定义注解 AutoFill import com.sky.enumeration.OperationType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) //只作用于方法上 @Retention(RetentionPolicy.RUNTIME) //表示被该注解修饰的注解在运行时保留,即可以通过反射机制在运行时获取这些注解的信息 public @interface AutoFill { //定义注解的属性为数据库操作类型: UPDATE INSERT //OperationType在包com.sky.enumeration中 OperationType value(); } 创建一个专门用来存放切面类的包 aspect,在包下创建切面类 AutoFillAspect import com.sky.annotation.AutoFill; import com.sky.constant.AutoFillConstant; import com.sky.context.BaseContext; import com.sky.enumeration.OperationType; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.time.LocalDateTime; /** * 自定义切面,实现公共字段自动填充处理逻辑 */ @Aspect //切面类 @Component //标记为Spring管理的Bean @Slf4j //日志 public class AutoFillAspect { //切入点 /** * execution(* com.sky.mapper.*.*(..)): 切点表达式, 方法返回值为任意, 匹配com.sky.mapper包下所有类和接口, * 参数为任意 * * @annotation(com.sky.annotation.AutoFill): 注解匹配表达式, 拦截带有AutoFill注解的方法 */ @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)") public void autoFillPointCut() { } /** * 前置通知:在通知中进行公共字段的赋值 * * @param joinPoint */ @Before("autoFillPointCut()") public void autoFill(JoinPoint joinPoint) { log.info("公共字段自动填充..."); //获取到当前被拦截的方法上的数据库操作类型 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(); //根据当前不同的操作类型, 为对应的属性通过反射来赋值 if (operationType == OperationType.INSERT) { //当前执行的是insert操作, 为4个字段赋值 try { //获取set方法对象——Method Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); //通过反射调用目标对象的方法 setCreateTime.invoke(entity, now); setUpdateTime.invoke(entity, now); setCreateUser.invoke(entity, currentId); setUpdateUser.invoke(entity, currentId); } catch (Exception e) { log.info("公共字段自动填充失败:{}", e.getMessage()); } } else if (operationType == OperationType.UPDATE) { //当前执行的是update操作, 为2个字段赋值 try { //获取set方法对象——Method Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); //通过反射调用目标对象的方法 setUpdateTime.invoke(entity, now); setUpdateUser.invoke(entity, currentId); } catch (Exception e) { log.info("公共字段自动填充失败:{}", e.getMessage()); } } } } 给 EmployeeMapper 和 CategoryMapper 中数据库操作类型为 insert 和 update 的方法加上自定义的注解 @AutoFill(value = OperationType.INSERT), ;然后,原先代码 Service 层中给这些公共字段赋值的代码就可以注解掉了。
新增菜品 需求分析和设计 1 2 3 4 5 6 7 8 9 10 11 业务规则: 菜品名称必须是唯一的 菜品必须属于某个分类下,不能单独存在 新增菜品时可以根据情况选择菜品的口味 每个菜品必须对应一张图片 接口设计: 根据类型查询分类(已开发) 文件上传 新增菜品
代码开发 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 import com.sky.properties.AliOssProperties; import com.sky.utils.AliOssUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration //配置类 @Slf4j public class OssConfiguration { /** * 通过spring管理对象 * * @param aliOssProperties * @return */ @Bean @ConditionalOnMissingBean //只有当容器中不存在名为aliOssUtil的Bean时才创建该Bean, 防止重复创建 public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) { log.info("开始创建阿里云OSS工具类对象..."); return new AliOssUtil(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName()); } } CommonController 中: import com.sky.constant.MessageConstant; import com.sky.result.Result; import com.sky.utils.AliOssUtil; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; 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.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.util.UUID; /** * 通用接口 */ @RestController @RequestMapping("/admin/common") @Slf4j @Api(tags = "通用接口") public class CommonController { @Autowired private AliOssUtil aliOssUtil; /** * 文件上传 * * @param file * @return */ @PostMapping("/upload") @ApiOperation("文件上传") public Result<String> upload(MultipartFile file) { log.info("文件上传:{}", file.getName()); String originalFilename = file.getOriginalFilename(); //原始文件名 String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); //获取文件后缀 //生成一个独一无二的文件名 String newFileName = UUID.randomUUID().toString() + extension; //将文件上传到阿里云 try { String filePath = aliOssUtil.upload(file.getBytes(), newFileName); return Result.success(filePath); } catch (Exception e) { log.error("文件上传失败:{}", e.getMessage()); } return Result.error(MessageConstant.UPLOAD_FAILED); } }
开发新增菜品接口 根据新增菜品接口设计对应的DTO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 新建 DishController,在其中创建新增菜品方法 import com.sky.dto.DishDTO; import com.sky.result.Result; import com.sky.service.DishService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; 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; @RestController @RequestMapping("/admin/dish") @Api(tags = "菜品相关接口") @Slf4j public class DishController { @Autowired private DishService dishService; /** * 新增菜品 * * @param dishDTO * @return */ @PostMapping @ApiOperation("新增菜品") public Result<String> save(@RequestBody DishDTO dishDTO) { log.info("新增菜品:{}", dishDTO); dishService.saveWithFlavor(dishDTO); return Result.success(); } } 在 DishService 接口中声明新增菜品方法: import com.sky.dto.DishDTO; public interface DishService { /** * 新增菜品和对应的口味 * @param dishDTO */ void saveWithFlavor(DishDTO dishDTO); } 在 DishServiceImpl 中实现新增菜品方法: import com.sky.dto.DishDTO; import com.sky.entity.Dish; import com.sky.entity.DishFlavor; import com.sky.mapper.DishFlavorMapper; import com.sky.mapper.DishMapper; import com.sky.service.DishService; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service public class DishServiceImpl implements DishService { @Autowired private DishMapper dishMapper; @Autowired private DishFlavorMapper dishFlavorMapper; /** * 新增菜品和对应的口味 * * @param dishDTO */ @Transactional //声明方法应该被包含在一个事务中执行 public void saveWithFlavor(DishDTO dishDTO) { Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); //拷贝对象属性 //向菜品表dish中插入1条数据 dishMapper.insert(dish); //获取菜品的主键值 Long dishId = dish.getId(); List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors != null && flavors.size() > 0) { //向口味表dish_flavor插入n条数据 flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishId); }); //批量插入口味数据 dishFlavorMapper.insertBatch(flavors); } } } 在 DishMapper 中声明 insert 方法: /** * 插入菜品数据 * * @param dish */ @AutoFill(OperationType.INSERT) void insert(Dish dish); /** * 插入菜品数据 * * @param dish */ @AutoFill(OperationType.INSERT) void insert(Dish dish); 新建 DishMapper.xml,在其中编写对应的 SQL: <?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"> <!-- useGeneratedKeys="true": 表示获取数据插入后所在的主键值 keyProperty="id": 表示将主键值赋给id属性 --> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user) values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}) </insert> </mapper> 新建 DishFlavorMapper.xml,在其中编写对应的 SQL: <?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.DishFlavorMapper"> <insert id="insertBatch"> insert into dish_flavor(dish_id, name, value) values <foreach collection="flavors" item="dishFlavor" separator=","> (#{dishFlavor.dishId}, #{dishFlavor.name}, #{dishFlavor.value}) </foreach> </insert> </mapper>
功能测试 进行前后端联调测试,点击菜品管理,添加菜品,可以看到图片,说明文件上传接口开发成功
菜品分页查询 需求分析和设计 1 2 3 4 5 业务规则: 根据页码展示菜品信息 每页展示10条数据 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询
代码开发 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 根据接口定义创建 DishController 的 page 分页查询方法: /** * 菜品分页查询 * * @param dishPageQueryDTO * @return */ @GetMapping("/page") @ApiOperation("菜品分页查询") public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) { log.info("菜品分页查询:{}", dishPageQueryDTO); PageResult pageResult = dishService.pageQuery(dishPageQueryDTO); return Result.success(pageResult); } 在 DishService 中扩展分页查询方法: /** * 菜品分页查询 * * @param dishPageQueryDTO * @return */ PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO); 在 DishServiceImpl 中实现分页查询方法: /** * 菜品分页查询 * * @param dishPageQueryDTO * @return */ public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) { PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize()); Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO); return new PageResult(page.getTotal(), page.getResult()); } 在 DishMapper 接口中声明 pageQuery 方法: /** * 菜品分页查询 * * @param dishPageQueryDTO * @return */ Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO); 在 DishMapper.xml 中编写 SQL: <select id="pageQuery" resultType="com.sky.vo.DishVO"> select d.*, c.name as categoryName from dish d left 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>
功能测试 进行前后端联调测试,可以看到所有菜品,也可以输入条件查询,可以切换下一页,成功
删除菜品 需求分析和设计 1 2 3 4 5 6 7 业务规则: 可以一次删除一个菜品,也可以批量删除菜品 起售中的菜品不能删除 被套餐关联的菜品不能删除 删除菜品后,关联的口味数据也需要删除掉
代码开发 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 根据删除菜品的接口定义在 DishController 中创建方法: /** * 菜品批量删除 * * @param ids * @return */ @DeleteMapping @ApiOperation("菜品批量删除") public Result delete(@RequestParam List<Long> ids) { log.info("菜品批量删除:{}", ids); dishService.deleteBatch(ids); return Result.success(); } 在 DishService 接口中声明 deleteBatch 方法: /** * 菜品批量删除 * * @param ids */ void deleteBatch(List<Long> ids); 在 DishServiceImpl 中实现 deleteBatch 方法: /** * 菜品批量删除 * * @param ids */ @Transactional public void deleteBatch(List<Long> ids) { ids.forEach(id -> { Dish dish = dishMapper.getById(id); //判断当前要删除的菜品状态是否为起售 if (dish.getStatus() == StatusConstant.ENABLE) { //抛出自定义异常并返回错误信息给前端 throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE); } }); //判断当前要删除的菜品是否关联了套餐 List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids); //根据菜品id查询关联的所有套餐id if (setmealIds != null && setmealIds.size() > 0) { throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL); } //删除菜品表中的数据 ids.forEach(id -> { dishMapper.deleteById(id); //删除菜品对应的口味数据 dishFlavorMapper.deleteByDishId(id); }); } 在 DishMapper 中声明 getById 方法,并配置 SQL: /** * 根据id查询菜品数据 * * @param id * @return */ @Select("select * from dish where id = #{id}") Dish getById(Long id); 创建 SetmealDishMapper,声明 getSetmealIdsByDishIds 方法,并在 xml 文件中编写 SQL: import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface SetmealDishMapper { /** * 根据菜品id查询关联的套餐id * * @param ids * @return */ List<Long> getSetmealIdsByDishIds(List<Long> ids); } <?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.SetmealDishMapper"> <select id="getSetmealIdsByDishIds" resultType="java.lang.Long"> select setmeal_id from setmeal_dish where dish_id in <foreach collection="ids" separator="," open="(" close=")" item="dishId"> #{dishId} </foreach> </select> </mapper> 在 DishMapper 中声明 deleteById 方法并配置 SQL: /** * 根据id删除菜品 * * @param id */ @Delete("delete from dish where id = #{id}") void deleteById(Long id); 在 DishFlavorMapper 中声明 deleteByDishId 方法并配置 SQL: /** * 根据菜品id删除口味数据 * * @param dishId */ @Delete("delete from dish_flavor where dish_id = #{dishId}") void deleteByDishId(Long dishId);
功能测试 通过Swagger接口文档进行测试,通过后再前后端联调测试即可
修改菜品 需求分析和设计 1 2 3 4 5 6 7 8 根据id查询菜品 根据类型查询分类(已实现) 文件上传(已实现) 修改菜品
代码开发 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 根据 id 查询菜品接口开发 在 DishController 中创建 getById 方法: /** * 根据id查询菜品和关联的口味数据 * * @param id * @return */ @GetMapping("/{id}") @ApiOperation("根据id查询菜品和关联的口味数据") public Result<DishVO> getById(@PathVariable Long id) { log.info("根据id查询菜品和关联的口味数据:{}", id); return Result.success(dishService.getByIdWithFlavor(id)); } 在 DishService 接口中声明 getByIdWithFlavor 方法: /** * 根据id查询菜品和关联的口味数据 * * @param id * @return */ DishVO getByIdWithFlavor(Long id); 在 DishServiceImpl 中实现 getByIdWithFlavor 方法: /** * 根据id查询菜品和关联的口味数据 * * @param id * @return */ public DishVO getByIdWithFlavor(Long id) { //查询菜品表 Dish dish = dishMapper.getById(id); //查询菜品关联的口味 List<DishFlavor> dishFlavorList = dishFlavorMapper.getByDishId(id); //封装成VO DishVO dishVO = new DishVO(); BeanUtils.copyProperties(dish, dishVO); dishVO.setFlavors(dishFlavorList); return dishVO; } 在 DishFlavorMapper 中声明 getByDishId 方法,并配置 SQL: /** * 根据菜品id查询对应的口味 * * @param dishId * @return */ @Select("select * from dish_flavor where dish_id = #{dishId}") List<DishFlavor> getByDishId(Long dishId); 修改菜品接口开发 在 DishController 中创建 update 方法: /** * 修改菜品 * * @param dishDTO * @return */ @PutMapping @ApiOperation("修改菜品") public Result update(@RequestBody DishDTO dishDTO) { log.info("修改菜品:{}", dishDTO); dishService.updateWithFlavor(dishDTO); return Result.success(); } 在 DishService 接口中声明 updateWithFlavor 方法: /** * 根据id修改菜品和关联的口味 * @param dishDTO */ void updateWithFlavor(DishDTO dishDTO); 在 DishServiceImpl 中实现 updateWithFlavor 方法: /** * 根据id修改菜品和关联的口味 * * @param dishDTO */ @Transactional public void updateWithFlavor(DishDTO dishDTO) { Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); //获取所要修改菜品的id Long dishId = dishDTO.getId(); //修改菜品表dish dishMapper.update(dish); //删除当前菜品关联的口味数据 dishFlavorMapper.deleteByDishId(dishId); //插入最新的口味数据 List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors != null && flavors.size() > 0) { flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishId); }); //批量插入口味数据 dishFlavorMapper.insertBatch(flavors); } } 在 DishMapper 中声明 update 方法: /** * 根据菜品id修改菜品信息 * * @param dish */ @AutoFill(OperationType.UPDATE) void update(Dish dish); 在 DishMapper.xml 中配置 SQL: <!-- 根据菜品id修改菜品信息--> <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="image != null"> image = #{image}, </if> <if test="description != null"> description = #{description}, </if> <if test="status != null"> status = #{status}, </if> <if test="updateTime != null"> update_time = #{updateTime}, </if> <if test="updateUser != null"> update_user = #{updateUser} </if> </set> where id = #{id} </update>
功能测试 直接进行前后端联调比较方便,可以给没有图片的菜品修改图片
菜品启售停售 需求分析和设计 1 2 3 4 5 业务规则: 菜品停售,则包含菜品的套餐同时停售 数据库设计: 涉及 dish、setmeal 和 setmeal_dish 三个表
代码开发 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 在 DishController 中创建 startOrStop 方法: /** * 菜品启售停售 * * @param status * @return */ @PostMapping("/status/{status}") @ApiOperation("菜品启售停售") public Result startOrStop(@PathVariable Integer status, Long id) { log.info("菜品启售停售:{}", status); dishService.startOrStop(status, id); return Result.success(); } 在 DishService 中声明 startOrStop 方法: /** * 修改菜品状态 * * @param status * @param id */ void startOrStop(Integer status, Long id); 在 DishServiceImpl 中实现 startOrStop 方法: /** * 修改菜品状态 * * @param status * @param id */ @Transactional //涉及修改的操作加入事务管理比较好 public void startOrStop(Integer status, Long id) { Dish dish = dishMapper.getById(id);//获取菜品对象 dish.setStatus(status);//设置菜品状态 dishMapper.update(dish);//更新菜品信息 //如果是停售操作, 则需要将菜品关联的套餐一并停售 if (status.equals(StatusConstant.DISABLE)) { List<Long> dishIds = new ArrayList<>(); dishIds.add(id); //根据菜品id查询关联的所有套餐id List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds); if (setmealIds != null && setmealIds.size() > 0) { setmealIds.forEach(setmealId -> { Setmeal setmeal = Setmeal.builder() .id(setmealId) .status(StatusConstant.DISABLE) .build(); setmealMapper.update(setmeal); //根据套餐id修改套餐信息 }); } } } 在 SetmealMapper 中声明 update 方法: /** * 根据套餐id修改套餐信息 * * @param setmeal */ @AutoFill(OperationType.UPDATE) void update(Setmeal setmeal); 新建 SetmealMapper.xml,并在其中配置对应的 SQL: <?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.SetmealMapper"> <!-- 根据套餐id修改套餐信息--> <update id="update"> update setmeal <set> <if test="categoryId != null"> category_id = #{categoryId}, </if> <if test="name != null"> name = #{name}, </if> <if test="price != null"> price = #{price}, </if> <if test="status != null"> status = #{status}, </if> <if test="description != null"> description = #{description}, </if> <if test="image != null"> image = #{image}, </if> <if test="updateTime != null"> update_time = #{updateTime}, </if> <if test="updateUser != null"> update_user = #{updateUser} </if> </set> where id = #{id} </update> </mapper>
功能测试 通过swagger接口文档和前后端联调进行功能测试