苍穹外卖需要注意的地方
公共字段自动填充自定义注解AutoFill公共字段自动填充和反射有很大的关系公共字段填充中自定义注解AutoFill -反射在查找给某个方法进行公共字段填充的时候的标识反射与注解认识注解属性名后面要加在使用的时候把注解写在方法上括号内为属性名赋值特殊情况在注解只有一个属性value的时候在方法上面给value赋值的时候可以不写value注解的原理就是注解本质上是一个接口继承了Annotation方法在给注解中的属性赋值的时候实际上是在实现注解给注解创造实现类对象又因为继承的特殊性实现了子类注解也就实现了Annotation注解元注解Target注解说明注解可以在哪里使用Retention注解说明注解的保留周期注解的解析自定义切面在执行update和insert方法的时候开启公共字段的自动填充通过反射获取方法签名从而获取签名中的对数据库的操作类型通过反射获取方法先获取类再获取方法通过反射获取的不同方法对于不同的方法设置不同的数据/* 自定义切面实现公共字段自动填充处理逻辑 */ //加入切面注解 Aspect //Bean类交给spring容器管理 Component Slf4j public class AutoFillAspect { /* 切入点 */ Pointcut(execution(com.sky.annotation.AutoFill * com.sky.mapper.*.*(..))) public void autoFillPointCut() { } /* 前置通知在通知中进行公共字段的赋值 */ Before(autoFillPointCut()) public void autoFill(JoinPoint joinPoint) { log.info(开始进行公共字段的自动填充); //获取到当前被拦截的方法上的数据库的操作类型 //1.获取方法签名对象 MethodSignature signature (MethodSignature) joinPoint.getSignature(); //2.获取方法上的注解对象 AutoFill autoFill signature.getMethod().getAnnotation(AutoFill.class); //3.获取数据库的操作类型 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.equals(OperationType.INSERT)){ //为四个公共字段赋值 try { Method setCreateTime entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setCreateUser entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateTime entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); //通过反射为对象属性赋值 setCreateTime.invoke(entity,now); setCreateUser.invoke(entity,currentId); setUpdateTime.invoke(entity,now); setUpdateUser.invoke(entity,currentId); } catch (Exception e) { throw new RuntimeException(e); } }else if(operationType.equals(OperationType.UPDATE)){ try { 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) { throw new RuntimeException(e); } } } }查询回显-一对多多表查询—两张表分开查询Service 层核心分开两次查询Service public class CategoryServiceImpl implements CategoryService { Autowired private CategoryMapper categoryMapper; Autowired private SetmealMapper setmealMapper; // 注入套餐Mapper /** * 分开查询分类 套餐一对多 */ Override public CategoryVO getCategoryWithSetmeal(Long categoryId) { // 第一次查询查 主表一的一方 Category category categoryMapper.getById(categoryId); // 第二次查询查 从表多的一方 // 根据分类ID查所有套餐 ListSetmeal setmealList setmealMapper.getByCategoryId(categoryId); // 手动封装成 VO CategoryVO vo new CategoryVO(); BeanUtils.copyProperties(category, vo); vo.setSetmealList(setmealList); return vo; } }—另一种方法XML 核心一对多查询最关键resultMap idCategoryWithSetmealMap typecom.sky.vo.CategoryVO !-- 一的一方分类 -- id columnc_id propertyid/ result columnc_name propertyname/ !-- 多的一方套餐一对多核心 -- collection propertysetmealList ofTypecom.sky.entity.Setmeal id columns_id propertyid/ result columns_name propertyname/ result columns_price propertyprice/ /collection /resultMap !-- 一对多关联查询 SQL -- select idgetCategoryWithSetmeal resultMapCategoryWithSetmealMap SELECT c.id AS c_id, c.name AS c_name, s.id AS s_id, s.name AS s_name, s.price AS s_price FROM category c LEFT JOIN setmeal s ON c.id s.category_id WHERE c.id #{categoryId} /select表套表--修改的复杂情况口味可能被是被删掉了也可能是被修改了不好说调用哪个接口所以采用先删除后上传的方法在新增菜品的时候如果还要新增口味就只能在菜品添加完毕并返回主键ID之后才能添加口味insert idinsert useGeneratedKeystrue keyPropertyid insert into dish(name, category_id, price, image, description, create_time, update_time, create_user, update_user, status) values (#{name},#{categoryId},#{price},#{image},#{description}, #{createTime},#{updateTime},#{createUser},#{updateUser},#{status}) /insertTransactional Override public void saveWithFlavor(DishDTO dishdto) { Dish dish new Dish(); BeanUtils.copyProperties(dishdto,dish); //向菜品表插入1条数据 dishMapper.insert(dish); //获取insert语句生成的主键值 Long dishIddish.getId(); //向口味表插入n条数据,支持批量插入 ListDishFlavor flavors dishdto.getFlavors(); if(flavors!null !flavors.isEmpty()){ for(DishFlavor flavor:flavors){ flavor.setDishId(dishId); } dishFlavorMapper.insertBatch(flavors); } }接口参数规则解析这张表是一个 **【修改套餐状态】** 的接口文档我帮你把参数的含义、位置、以及前后端怎么对接彻底讲清楚。一、接口核心信息请求方式PUT / POST通常是修改状态请求格式JSONBody 体路径参数status在 URL 路径中Query 参数id在 URL 问号后二、参数位置与规则3 个参数全覆盖1. Header请求头参数名值说明Content-Typeapplication/json必须填表示我发送的是 JSON 格式数据缺了这个后端解析不到数据。2. 路径参数Path Param参数名示例备注status1套餐状态1 起售打开售卖0 停售禁止售卖这个参数拼在 URL 路径里。3. Query 参数Query Param参数名是否必须示例备注id是101套餐 ID要修改的那个套餐的 ID比如 101 号套餐这个参数拼在 URL?后面。三、正确的请求 URL拼接规则根据接口文档请求的 URL 要这样写/api/setmeal/[status]?id[套餐id]实际例子修改 101 号套餐为起售PUT http://localhost:8080/api/setmeal/1?id101/1路径参数status1表示起售?id101Query 参数id101表示修改 ID 为 101 的套餐四、后端代码怎么接Java 解析因为参数在路径和Query两个地方后端 Controller 要分开接收。1. Controller 写法核心PutMapping(/setmeal/{status}) // 这里捕获路径参数 status ApiOperation(修改套餐状态) public ResultString updateStatus( // 接收路径参数 PathVariable Integer status, // 接收 Query 参数 RequestParam Long id ) { log.info(修改套餐状态id{}, status{}, id, status); // 调用 Service 处理逻辑 setmealService.updateStatus(id, status); return Result.success(修改成功); }2. DTO / 实体类不需要写 DTO因为参数很少直接用RequestParam和PathVariable接不用写SetmealDTO如果是复杂的新增 / 修改才用 DTO。五、前端请求代码Vue 示例前端必须按照这个规则发送请求URL 格式不能错。// 1. 定义参数 const status 1; // 起售 const id 101; // 套餐ID // 2. 发送请求 // 注意URL 要拼接 /status?idxxx await axios.put(/api/setmeal/${status}?id${id});六、特别注意避坑指南Content-Type必须是application/json虽然这个接口传的参数很少只有 id但因为是 PUT 请求后端通常要求接收 JSON 体。如果后端报错Required request body is missing说明你没加这个 Header或者没传 JSON 体。参数位置不要搞混status写在路径里/1id写在查询参数里?id101不要把id写到路径里严格按照文档来。Path 参数 vs Query 参数我用最通俗的方式把本质区别、使用场景、前后端写法一次性讲透帮你彻底分清。一、本质区别一句话总结维度Path 参数路径参数Query 参数查询参数位置URL路径中/xxx/{id}URL? 后面?idxxxnamexxx作用标识资源本身比如「哪个套餐」「哪个用户」对资源做筛选、分页、条件、附加操作是否必须通常是必填缺了就找不到资源通常是可选不传用默认值格式直接嵌入路径无key只有值keyvalue键值对多参数用分隔缓存友好性路径变了 资源变了适合做缓存同一资源不同参数缓存需特殊处理二、直观对比用你刚才的套餐接口举例1. 路径参数Path ParamURL 示例/admin/setmeal/1含义1是路径参数代表「状态 1起售」直接嵌在 URL 路径里特点是 URL 的一部分缺了这个路径就不存在404适用场景资源的状态、类型、分类比如/setmeal/{status}、/user/{id}2. 查询参数Query ParamURL 示例/admin/setmeal/1?id101含义id101是查询参数在?后面是对路径资源的附加条件特点不是 URL 的核心路径不传也能访问后端做默认值处理适用场景分页、筛选、排序、附加参数比如?page1pageSize10、?status1三、核心使用场景怎么选✅ 什么时候用 Path 参数标识唯一资源/user/{userId}、/order/{orderId}必须传否则找不到资源资源的分类 / 状态/setmeal/{status}、/category/{type}状态是资源的属性嵌在路径里RESTful 风格接口符合「URL 代表资源HTTP 方法代表操作」的设计规范✅ 什么时候用 Query 参数分页查询/setmeal/page?page1pageSize10条件筛选/setmeal?categoryId13status1排序 / 搜索/setmeal?name套餐sortcreateTime可选附加参数非必须不传不影响核心资源访问四、后端代码写法SpringBoot 对比1. Path 参数PathVariable// 路径参数{status} 嵌在URL里 PutMapping(/setmeal/{status}) public Result updateStatus( PathVariable Integer status, // 绑定路径中的{status} RequestParam Long id // 绑定?后面的id ) { // 逻辑... }必须用PathVariable注解参数名要和路径占位符{status}完全一致路径参数是 URL 的一部分缺了会 4042. Query 参数RequestParam/ 自动绑定// Query参数?idxxxnamexxx GetMapping(/setmeal/page) public Result page(SetmealPageQueryDTO dto) { // 自动绑定?后面的参数到DTO } // 或者单独接收 GetMapping(/setmeal) public Result list( RequestParam(required false) String name, RequestParam(defaultValue 1) Integer page ) { // 逻辑... }用RequestParam或直接用 DTO接收支持requiredfalse可选、defaultValue默认值多参数用分隔顺序不影响五、前端请求写法Axios 对比1. Path 参数// 路径参数直接拼在URL里 const status 1; const id 101; axios.put(/admin/setmeal/${status}?id${id});2. Query 参数// Query参数用params对象自动拼接 axios.get(/admin/setmeal/page, { params: { page: 1, pageSize: 10, categoryId: 13, status: 1 } }); // 自动生成URL/admin/setmeal/page?page1pageSize10categoryId13status1六、关键避坑指南1. 不要搞混参数位置路径参数是URL 的一部分不能用?包裹Query 参数必须在?后面多参数用分隔错误示例/setmeal?status1?id101?只能有一个后面全是 Query 参数2. Path 参数的 RESTful 规范路径参数尽量用单数/user/{userId}不要/users/{userId}不要把多个参数都塞路径里/setmeal/{id}/{status}不如/setmeal/{id}?status{status}清晰3. Query 参数的空值处理Query 参数支持requiredfalse不传时后端用默认值路径参数通常是必填不传会 404适合做强校验七、一句话总结面试 / 工作都能用Path 参数是「资源的身份证」用来定位唯一资源Query 参数是「资源的筛选条件」用来对资源做附加操作。遵循 RESTful 风格URL 定位资源Query 描述操作。pagehelper的细节PageHelper 原理一、PageHelper 到底是什么它是一个MyBatis 拦截器Interceptor作用自动帮你拼接分页 SQL不用你自己写LIMIT ?,?二、核心原理一句话PageHelper 在执行你的查询 SQL 之前偷偷拦截自动帮你改成 分页 SQL然后再执行。三、它的工作流程4 步走1. 你写PageHelper.startPage(1, 10);作用把 page1、pageSize10 存到当前线程里ThreadLocal2. 你执行查询ListSetmealVO list mapper.page(dto);3. PageHelper 拦截器工作拦截你的 SQL从 ThreadLocal 取出page1, pageSize10*自动计算offset (1-1)10 0把你的 SQL 改成分页 SQLSELECT * FROM table LIMIT 0,104. 返回分页结果 Page/PageInfo总条数当前页数据总页数四、为什么你之前page 不生效只有 pageSize 生效因为你违反了 PageHelper 最核心的规则 规则 1必须紧跟在查询方法前面中间不能有任何代码java运行// 正确 PageHelper.startPage(1,10); List list mapper.select(); // 错误分页失效 PageHelper.startPage(1,10); 其他代码(); List list mapper.select(); 规则 2查询方法必须返回 List 类型你之前写xmlresultTypePageResult→ 返回的不是 List→ PageHelper无法拦截→ 只能拼出sqlLIMIT 10→page 失效 规则 3page 不能是 0 或 nulljava运行PageHelper.startPage(0,10);→ 生成 SQLsqlLIMIT 10→ 只有 pageSize 生效五、PageHelper 最关键的 3 个知识点1. 基于ThreadLocal存储分页参数2. 基于MyBatis 拦截器改写 SQL3. 只对紧跟的第一条查询生效六、你之前的错误总结XML 返回类型错误返回 PageResult 而不是 VOPageHelper 无法拦截生成错误 SQLLIMIT 10page 不生效只有 pageSize 生效七、正确写法最终版java运行// 1. 开启分页 PageHelper.startPage(pageNum, pageSize); // 2. 立刻查询必须紧跟 ListSetmealVO list setmealMapper.page(dto); // 3. 封装分页 PageInfoSetmealVO pageInfo new PageInfo(list); return new PageResult(pageInfo.getTotal(), pageInfo.getList());select idpage resultTypeSetmealVO ... /selectPageHelper 自动帮你拼 LIMIT 的拦截器必须紧跟查询、必须返回 List、page 不能为 0PageHelper 原理极简版PageHelper.startPage(page, pageSize)把分页参数存到当前线程的ThreadLocal里。执行查询紧接着的第一条List?查询会被 PageHelper 的MyBatis 拦截器截获。自动改 SQL拦截器根据线程里的分页参数自动计算offset (page-1)*pageSize给你的 SQL 加上LIMIT offset, pageSize。封装分页结果查询完返回Page/PageInfo包含总条数、当前页数据。核心记住这 3 条只对紧跟的第一条查询生效必须返回List才能分页基于ThreadLocal MyBatis 拦截器实现以后再出现 “只有 pageSize 生效”你就知道要么page 是 0要么没紧跟查询要么返回不是 List。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2483695.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!