Java篇-项目实战-天机学堂(从0到1)-day1

张开发
2026/4/13 8:36:02 15 分钟阅读

分享文章

Java篇-项目实战-天机学堂(从0到1)-day1
java篇 1.基础地基 2.设计原理 3.项目实战学习建议先看视频跟着学再看文档自己敲;不会地方到处问问完总结记笔记。最重要的一点就是永远相信自己永不放弃本章开始进入正片我会按照视频的天数来更新方便大家一同学习保姆级教程希望大家多多支持一.表的设计实体类设计注解配置后MyBatis-Plus 会自动插入默认写入0未删除查询自动追加WHERE deleted 0删除执行UPDATE table SET deleted 1而非物理删除package com.tianji.learning.enums; import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import com.tianji.common.enums.BaseEnum; import lombok.Getter; Getter public enum LessonStatus implements BaseEnum { NOT_BEGIN(0, 未学习), LEARNING(1, 学习中), FINISHED(2, 已学完), EXPIRED(3, 已过期), ; JsonValue EnumValue int value; String desc; LessonStatus(int value, String desc) { this.value value; this.desc desc; } JsonCreator(mode JsonCreator.Mode.DELEGATING) public static LessonStatus of(Integer value){ if (value null) { return null; } for (LessonStatus status : values()) { if (status.equalsValue(value)) { return status; } } return null; } }JsonValue(Jackson)作用序列化时将枚举转换成什么值返回给前端位置标注在value字段上效果当前端接收这个枚举时只看到数字看不到描述EnumValue(MyBatis-Plus)作用标识枚举在数据库中的存储值位置标注在value字段上效果保存到数据库时存数字查询时自动映射回枚举JsonCreator(Jackson)作用反序列化时如何从前端传值转换回枚举对象mode JsonCreator.Mode.DELEGATING- 委托模式使用单个参数构造效果前端传数字后端自动转成对应的枚举总结一下JsonValue后端发给前端枚举前端看到的是数字。EnumValue后端发给数据库数据库存的是数字。数据库给后端映射成枚举。JsonCreator反序列化前端传数字给后端转回枚举。初始化脚本每次执行都先删除旧数据。查看表的字段结构二.基础搭建EqualsAndHashCode(callSuper false)注解详解自动生成两个方法equals()- 比较两个对象是否相等hashCode()- 生成对象的哈希码用于HashMap、HashSet等集合callSuper参数详解这是最关键的部分决定是否调用父类的equals/hashCode。既继承又实现这是 MyBatis-Plus 的标准写法。1. 继承是为了获得基础能力ServiceImpl是 MyBatis-Plus 提供的通用Service实现类已经帮你实现了save()- 保存实体removeById()- 根据ID删除getById()- 根据ID查询list()- 查询列表page()- 分页查询...等几十个通用CRUD方法2. 实现是为了自定义接口ILearningLessonService是你定义的业务接口可以声明特殊业务方法比如learnLesson()、getLearningProgress()超越通用CRUD的复杂查询这里没有Mapper是因为启动类当中有MapperScan(mapper的路径)总结这里的mapper接口和service接口以及其实现都继承了mp的抽象mapper是不是对应实体类那里面的参数就是实体类。那service接口它要返回的是不是也是实体类那当中的参数也就是实体类。那service接口实现是不是得借助mapper类呀那它最终返回的是不是还是实体类所以参数为mapper类和实体类。CourseClient和CourseController涉及到微服务架构中的服务调用CourseClient的目的是方便给其他人调用CourseClient具体是调用哪个服务的哪个接口三.实现功能添加课程到课表MQ:第一步就是建立XXXListener监听器可以直接用.类连带着包一起创建ctlp看当中有哪些参数①加上Compenent注解标记为组件②先写个方法搭个框架③写RabbitListener()注解这里用的是bindings当然也可以用其他的可以拿黑马点评对比一下。那绑定里面又有三个参数value(队列) exchange(交换机) key(路由键)Value: 用Queue ,队列的名字自己起一般都是模块.业务.方法.queue,然后设置持久化默认也是trueexchange:用Exchange(),这里面就不能乱起了你看里面都是静态变量提前设置的type不确定是啥点前面的静态变量看看这个跟Rabbit客户端发送消息带的参数得一一对应的那key也是同理得一一对应的。④就是开始实现这个方法了那mq和learning其他的例如service是在同一个屋檐下的那就用service方法实现。那当然在这之前还得进行健壮性处理神经敏感一样看到得到的是包装类就想搞下。并且进行日志追踪下面是各级别日志你看情况用级别用途生产环境示例场景ERROR错误信息程序无法正常运行✅ 开启数据库连接失败、调用第三方接口超时、空指针异常WARN警告信息程序能运行但有问题✅ 开启参数校验失败、配置缺失使用默认值、降级触发INFO关键业务流程✅ 开启服务启动、登录成功、订单创建、定时任务执行DEBUG调试信息开发排查问题❌ 关闭方法入参、中间变量、SQL参数、循环内信息TRACE更细粒度的调试❌ 关闭复杂算法的每一步、方法调用链路追踪之后就用lessonService去办事了那它可能没有这个方法那你就去创建一个.addUserLessons()方法AltEnter 构建service接口和具体impl实现具体实现记得加Override之后就是根据业务流程表去实现具体的业务逻辑思考所需的参数该怎么获得。在注释中把每步清晰表示处理然后挨个实现那这里第一步呢查询课程有效期就得调用其他服务的接口才能获得调用哪个看模块对应的API这里还是敏感神经看到得到的是包装类我就很敏感我就得健壮一下。后面就是正常的逻辑没啥好说的最后批量新增小细节保险起见在方法上加上Transactional注解跟数据库交互了那改的那张表就是这个实体对应的表驼峰命名法懂在之后你在IDE中启动这个模块进行测试。你看它就创建了你刚才命名的队列。再看交互机点击去是不是也完成绑定了开始功能测试了点击然后打开你创建的队列你就会看到这就是消息投进去消费掉了。对应的后台也会记录日志。oK快速构建对象加个.var 然后enter三.实现功能分页查询我的课表分析登录用户传递流程authHeaders.get(0)的作用是从 Authorization 头的值列表中取出第一个值。绝大多数情况下Authorization头只会有一个值格式通常是所以get(0)就是取出这个唯一的 token 字符串。如果Token有效用户已登录使用exchange.mutate()创建请求的副本添加新请求头USER_HEADER如X-User-Id值为用户ID.build()生成修改后的请求这里把拦截器放在的auth模块下不是common当中gateway用于网关resource用于微服务Spring MVC拦截器它会在所有controller前执行2.判断为空也放行了因为有些请求不需要登录一.从Controller到ServiceImpl首先创建Controller层①添加REST风格的请求路径搭建方法框架明确传入的参数是啥返回的结构类型是啥。这里传入的参数是PageQuery(对页码页大小是否排序排序字段进行了封装)。返回的结果是PageDTOLearningLessonVO,(对总条数总页码数当前页数据进行了封装这里的页数据是LearningLessonVO那其他查询就可能是其他的)然后这里请求参数没有加任何注解因为是get方法②调用Service层接口实现方法名从controller层到service的接口以及impl都保持一致在方法名上按altenter进入就有进入service接口鼠标移动到这按altenter进入impl进行实现③添加Api注解,tags标签Swagger页面通过模块下的功能放一起便于swagger调试二.ServiceImpl具体实现从头开始分析我们最终得到的是PageDTO它要啥呢它要 1.总行数 2.总页数 3.LearningLessonVO列表那现在我们有PageQuery ,有 1.页码 2.页大小 3.是否升序 4.按什么排序那总行数和总页数可以从数据库查询得到那LearningLessonVO列表呢我们先来看下可以看到VO与用户和课程的关系表中有些字段是相同的但有些po当中又没有所以得另找那怎么找呢就得根据关系表po当中的course_id调用相关的微服务找到找到之后在把两个拼在一起封装就得到了VO。思路确定后先开始查数据库那查我的课程那肯定得有我的userId把so那拿到userId后开始分页查询先写sql语句然后用java代码实现查询的结果得用PagepolambdaQuery()MyBatis-Plus 提供的 Lambda 查询构造器.eq(LearningLesson::getUserId, userId)比较,左右两边是否一样.page(query.toMpPage(latest_learn_time, false))传统写法类似这样但是query进行了封装里面设置了默认值。根据需要传入参数这里传入了根据什么字段排怎么排写法说明lambdaQuery().eq(...).page(...)当前写法Lambda风格query().eq(user_id, userId).page(...)传统字符串写法原生SQLSELECT * FROM learning_lesson WHERE user_id ? ORDER BY latest_learn_time DESC LIMIT ?, ?.page前是查询条件.page是真正开始去查了查到之后我们只需要里面的records集合也就是po集合总行数不要总页数不要。这里进行了健壮性检查CollUtils.isEmpty()封装了 null 和.isEmpty()的情况 PageDTO.empty(page)也进行了封装返回一个空集合我没有课程有什么办法呢没有就是没有那到现在我们有了总行数和总页数那开始搞VO首先得根据po拿到courseId,然后再去course微服务去查完整信息通过流式的方法去映射po当中的CourseId字段然后收集起来调用微服务如果没找到就有问题得抛异常我这表里有的但课程表里没有就有问题了。那接下来就去封装了先搞个VO的集合list然后遍历每个当中生成一个VO拼接两部分加到list当中。开始对po循环遍历先将基础属性从po copy到vo,用BeanUtils.copyBean(,),前面是要拷的对象后面是要把这个对象拷成什么类。接下就是去获取没有的课程信息那它是根据courseId去找的那拿着courseId去cInfoList当中一个个判断遍历吗显然不合适那这时候就想到了用Map,键是courseId值就是cInfo本身。那难道又要for遍历一遍去构造么当然不是我们用stream流的方式前面也提到了stream(),那这里就稍微总结一下放到本章末尾关键点不调用终端操作中间操作不会执行惰性求值一个Stream只能有一个终端操作调用后流就关闭了构建完后我们就可以方便的取cInfo了在将里面课程信息填充到vo,封装完后添加到list最后返回本来是需要new PageDTO装入三部分的。但是现在又有封装总行数和总页数page就有那就传page跟list就行了。之后我们就启动IDE上learning服务对了别忘了把当中的配置改为local然后把远程的learning服务关了,具体情况看nacos然后进行测试。先用swagger测然后页面上测四.实现功能删除课表中课程主动删除和退款异步删除删除课表中的课程有两种场景用户直接删除已失效的课程用户退款后触发课表自动删除对业务分析可以知道第一种是用户直接删那就需要在Controller层当中定义接口退款后触发课表删除这跟购买后添加课表类似因为一个在trade模块一个在learning 模块得用MQ进行异步处理。那按照之前的套路先干listener①这里补充一点就是这是在listener类当中那这里的方法就是针对监听器这里用的是listenLessonRefund不要跟具体Service层方法混淆了。②还是看交换机和路由键是哪个背下格式。③当中order.getCourseIds()用的是CollUtils.isEmpty()判断不用但用null判断因为它是集合既要判断null也要判断。isEmpty()④记得打日志记录一下后面就深入ServiceImpl实现了这里得先判断下userId是否为null,如果为null说明要删的是自己的。那如果原先userId就是有参数的方便其他方法删除指定某个用户的某个方法。接下来先写下删除sql语句mp已经实现了remove()的方法需要Wrapper参数那Wrapper里面的参数肯定得根据什么进行配置那还不是userId和courseId么划红线处得加具体的po不然下面的LearningLesson::getUserId会爆红然后得写.lambda(),不然后面的.eq()会爆红因为QueryWrapper不是LambdaQueryWrapper之后就补下Controller层注解里面记得加,然后背格式就完事了这里默认传的null五.实现功能检查课程是否有效意思是tj-media这个服务需要调用tj-learning当中的校验接口先写Controller层这里的请求和返回Long类型和上面接口类型保持一致记得return 有返回值前面的delete 是void所以不需要return拿用户id写sqlbuildUserIdAndCourseIdWrapper(userId, courseId)构建查询条件WHERE user_id ? AND course_id ?getOne()MyBatis-Plus 提供的 IService 方法查询一条记录返回LearningLesson实体对象如果存在否则返回nullgetOne()的特点注释当中原代码没有就是对过期时间的判断。LocalDateTime now LocalDateTime.now()isBefore()推荐isAfter()compareTo()六.实现功能查询指定课程信息这里的LearningLessonVO返回的字段比需要的多冗余但是前端可以处理这样设计也方便其他接口复用。七.实现功能统计课程的学习人数简单的单表查询用lambdaQuery()就好为什么lambdaQuery()不需要写泛型因为lambdaQuery()是ServiceImpl的方法它已经知道当前 Service 操作的是哪个实体写法如何知道操作哪个表new LambdaQueryWrapperLearningLesson()显式指定泛型lambdaQuery()继承自ServiceImpl的泛型扩展:写法性能说明COUNT(*)⚡ 最快不取值直接统计行数COUNT(1)⚡ 几乎一样快取常量1也是统计行数COUNT(主键)⚡ 很快走主键索引COUNT(普通字段) 慢需要判断是否为 NULL八.实现功能查询正在学习的课程这就是要返回的数据框起来的都是需要另外填充的红色代表要调课程微服务接口蓝色调学习小结微服务接口绿色要查找。所以可以大致分这几步先查找PO再去填充VO,然后依次去干红色、绿色、蓝色。.last(limit 1)的作用MyBatis-Plus 默认没有.limit()方法用.last()可以在 SQL 末尾追加自定义内容.one()vs.list()的区别方法返回类型说明.one()单个实体只取第一条如果没查到返回 null.list()List 集合返回所有符合条件的记录九.模块功能完成git提交查看变更代码输入提交消息点击提交到功能分支feature-lessons,之后右键dev分支点击签出checkout,切换分支在功能分支上右键merge合并到dev分支这时候可能会出现冲突需要修复但是这里原本dev就没有learning模块所以不会有冲突。在之后右键dev点击推送到远端。然后Jenkins启动learning服务就可以测试了如果feature-lessons没用了就可以删掉了测试结果如下通过这里呢做个小小的测试方便理解在IDE和在Jenkins上启动的区别。配置忘了看day0。一开始呢,nacos上是没有course-service的现在我直接启动IDE上的course-service服务刷新nacos这时候就看到并且是远程的然后我改成local重新运行就看到变成本地了注意得重新运行不然还是远程的接着我在Jenkins上启动这个服务这时候就有两个了然后我在把前面设置的local去掉然后重新启动就又剩一个了十.补充.Stream 常用链式方法分类:Stream 的方法按功能分为三类中间操作返回Stream可链式调用、终端操作返回结果结束链式调用。1.中间操作Intermediate Operations筛选与过滤方法说明示例filter(Predicate)过滤符合条件的元素.filter(u - u.getAge() 18)distinct()去重根据equals/hashCode.distinct()limit(long n)截取前n个元素.limit(10)skip(long n)跳过前n个元素.skip(5)映射与转换方法说明示例map(Function)元素转换为另一种类型.map(User::getId)flatMap(Function)扁平化将多个流合并为一个.flatMap(list - list.stream())mapToInt/ToLong/ToDouble转为基本类型流.mapToInt(User::getAge)排序方法说明示例sorted()自然排序需实现Comparable.sorted()sorted(Comparator)自定义排序.sorted(Comparator.comparing(User::getAge))调试与查看方法说明示例peek(Consumer)查看中间结果常用于调试.peek(System.out::println)2.终端操作Terminal Operations聚合与统计方法说明示例count()返回元素个数.count()max(Comparator)最大值.max(Comparator.comparing(User::getAge))min(Comparator)最小值.min(Comparator.comparing(User::getAge))sum()/average()求和/平均基本类型流.mapToInt(User::getAge).sum()收集最常用方法说明示例collect(Collectors.toList())收集为List.collect(Collectors.toList())collect(Collectors.toSet())收集为Set.collect(Collectors.toSet())collect(Collectors.toMap(k,v))收集为Map.collect(Collectors.toMap(User::getId, u-u))collect(Collectors.joining())拼接字符串.collect(Collectors.joining(,))collect(Collectors.groupingBy())分组.collect(Collectors.groupingBy(User::getRole))匹配与查找方法说明示例findFirst()返回第一个元素.findFirst()findAny()返回任意一个.findAny()anyMatch(Predicate)任一匹配返回true.anyMatch(u - u.getAge() 60)allMatch(Predicate)全部匹配返回true.allMatch(u - u.getAge() 0)noneMatch(Predicate)无一匹配返回true.noneMatch(u - u.getName() null)归约方法说明示例reduce(BinaryOperator)累加/累乘等.reduce((a,b) - a b)toArray()转为数组.toArray(String[]::new)至此呢day1也是结束了因为我也是刚接触学习这个项目前面有些内容总结的有点乱还请见谅后面几天的内容会规范很多希望大家多多支持点赞评论收藏热爱可抵一切

更多文章