项目背景:
项目业务介绍:
**技术架构图:**
开发配置环境、git等常规配置此处跳过
页面UI分析:
课程id
、课程名称、任务数、创建时间、是否付费、审核状态、类型,操作因为是分页查询所以查询结果中还要包括总记录数、当前页、每页显示记录数
。创建数据库表与PO类:
接口设计分析:
确定协议:
通常协议采用HTTP,查询类接口通常为get或post,查询条件较少的使用get,较多的使用post
分析请求参数:
请求参数为:课程名称、课程审核状态、当前页码、每页显示记录数,根据请求参数定义模型类
分析响应结果:
。 根据前边对数据模型的分析,响应结果为数据列表加一些分页信息
(总记录数、当前页、每页显示记录数)。
。 数据列表中数据的属性包括:课程id、课程名称、任务数、创建时间、审核状态、类型
。 根据分析的响应结果定义模型类,如工作中常见的命名如AjaxResult、ResponseResult类
POST /content/course/list?pageNo=2&pageSize=1
Content-Type: application/json{"auditStatus": "202002","courseName": "","publishStatus":""
}
###成功响应结果
{"items": [{"id": 26,"companyId": 1232141425,"companyName": null,"name": "spring cloud实战","users": "所有人","tags": null,"mt": "1-3","mtName": null,"st": "1-3-2","stName": null,"grade": "200003","teachmode": "201001","description": "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。","pic": "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg","createDate": "2019-09-04 09:56:19","changeDate": "2021-12-26 22:10:38","createPeople": null,"changePeople": null,"auditStatus": "202002","auditMind": null,"auditNums": 0,"auditDate": null,"auditPeople": null,"status": 1,"coursePubId": null,"coursePubDate": null}],"counts": 23,"page": 2,"pageSize": 1
}
分页查询模型类
由于分页查询这一类的接口在项目较多,这里针对分页查询的参数(当前页码、每页显示记录数)单独在xuecheng-plus-base
基础工程中定义。
package com.xuecheng.base.model;import lombok.Data;
import lombok.ToString;
import lombok.extern.java.Log;/*** @description 分页查询通用参数*/
@Data
@ToString
public class PageParams {//当前页码private Long pageNo = 1L;//每页记录数默认值private Long pageSize =10L;public PageParams(){}public PageParams(long pageNo,long pageSize){this.pageNo = pageNo;this.pageSize = pageSize;}}
定义j接收前端的查询结果模型类,即dto
package com.xuecheng.content.model.dto;import lombok.Data;
import lombok.ToString;/*** @description 课程查询参数Dto*/@Data@ToString
public class QueryCourseParamsDto {//审核状态private String auditStatus;//课程名称private String courseName;//发布状态private String publishStatus;}
定义返回给前端的
响应模型类
所有分页查询的返回结果是一个固定的格式,定义一个响应类在base下,方便以后复用
package com.xuecheng.base.model;import lombok.Data;
import lombok.ToString;import java.io.Serializable;
import java.util.List;/*** @description 分页查询结果模型类*/
@Data
@ToString
public class PageResult implements Serializable {// 数据列表private List items;//总记录数private long counts;//当前页码private long page;//每页记录数private long pageSize;public PageResult(List items, long counts, long page, long pageSize) {this.items = items;this.counts = counts;this.page = page;this.pageSize = pageSize;}}
数据这里使用泛型,以后查询返回的是User对象,则泛型用User,查询返回的是Book对象,则填Book
@RestController
public class CourseBaseInfoController {@PostMapping("/course/list")public PageResult list(PageParams pageParams, @RequestBody(required=false) QueryCourseParamsDto queryCourseParams){return null;}
}
说明:pageParams分页参数通过url的key/value传入,queryCourseParams通过json数据传入,使用@RequestBody注解将json转成QueryCourseParamsDto对象。
@RequestBody后添加(required=false)表示此参数不是必填项(required默认为true,即必填) ,不加这个注解,当过滤条件为空,即没有传递json内容时,会导致400错误
dto、po、vo的解释:
DTO用于接口层向业务层之间传输数据
,即controller层的形参常为一个DTO,传给Service层PO用于业务层与持久层之间传输数据
,即service层的形参常为PO,调用mapper层的时候,传给mapper层VO对象用在前端与接口层之间传输数据
,查询到的结果封装VO对象为data,加上code、message后封装成一个AjaxResult结果类,由controller层返回给前端
VO的意义
:
场景:
。 手机查询:查询结果只要课程名称和课程状态
。 PC查询:可以根据课程名称、课程状态、课程审核状态等条件查询,查询显示的结果也比手机查询结果内容多
。 此时,Service业务层尽量提供一个业务接口,即使两个前端接口需要的数据不一样,Service可以提供一个最全查询结果,由Controller进行数据整合。
接口文档生成工具—swapper
com.spring4all swagger-spring-boot-starter
@Api(value = "课程信息编辑接口",tags = "课程信息编辑接口")
@RestController
public class CourseBaseInfoController {@ApiOperation("课程查询接口")@PostMapping("/course/list")public PageResult list(PageParams pageParams, @RequestBody(required=false) QueryCourseParamsDto queryCourseParams){//....}
}
启动服务,工程启动起来,访问http://localhost:63040/content/swagger-ui.html查看接口信息
swagger的常用注解:
@Api:修饰整个类,描述Controller的作用@ApiOperation:描述一个类的一个方法,或者说一个接口@ApiParam:单个参数描述@ApiModel:用对象来接收参数@ApiModelProperty:用对象接收参数时,描述对象的一个字段@ApiResponse:HTTP响应其中1个描述@ApiResponses:HTTP响应整体描述@ApiIgnore:使用该注解忽略这个API@ApiError :发生错误返回的信息@ApiImplicitParam:一个请求参数@ApiImplicitParams:多个请求参数
接口定义出来以后,接下来先mapper层,再写service层:
直接使用插件生成实体类和Mapper接口以及Mapper.xml或者手写Mapper接口,再继承BaseMapper
分页查询
分页插件的原理:
(select * from table where a) 转换为 (select count(*) from table where a)和(select * from table where a limit ,)
测试Mapper
@SpringBootTest
class CourseBaseMapperTests {@AutowiredCourseBaseMapper courseBaseMapper;@Testvoid testCourseBaseMapper() {CourseBase courseBase = courseBaseMapper.selectById(74L);Assertions.assertNotNull(courseBase);//测试查询接口LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();//查询条件QueryCourseParamsDto queryCourseParamsDto = new QueryCourseParamsDto();queryCourseParamsDto.setCourseName("java");queryCourseParamsDto.setAuditStatus("202004");queryCourseParamsDto.setPublishStatus("203001");//拼接查询条件//根据课程名称模糊查询 name like '%名称%'queryWrapper.like(StringUtils.isNotEmpty(queryCourseParamsDto.getCourseName()),CourseBase::getName,queryCourseParamsDto.getCourseName());//根据课程审核状态queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParamsDto.getAuditStatus()),CourseBase::getAuditStatus,queryCourseParamsDto.getAuditStatus());//使用之前定义的分页模型类,分页参数PageParams pageParams = new PageParams();pageParams.setPageNo(1L);//页码pageParams.setPageSize(3L);//每页记录数Page page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());//分页查询E page 分页参数, @Param("ew") Wrapper queryWrapper 查询条件Page pageResult = courseBaseMapper.selectPage(page, queryWrapper);//数据List items = pageResult.getRecords();//总记录数long total = pageResult.getTotal();//准备返回数据 List items, long counts, long page, long pageSizePageResult courseBasePageResult = new PageResult<>(items, total, pageParams.getPageNo(), pageParams.getPageSize());System.out.println(courseBasePageResult);}}
queryWrapper.like(StringUtils.isNotEmpty(queryCourseParamsDto.getCourseName()),CourseBase::getName,queryCourseParamsDto.getCourseName())
;
前面提到,下拉框取值可以为:
如果在课程table中新增一个字段,似乎也可以实现,但当用户需求变更,如想改"审核未通过"为"未通过",每次去改成千上万行数据,可维护性太差。
和审核状态同类的有好多这样的信息,比如:课程状态、课程类型、用户类型等等,这一类数据有一个共同点就是它有一些分类项,且这些分类项较为固定
。针对这些数据,为了提高系统的可扩展性,专门定义数据字典表去维护
。
服务层是controller层来调,所以service层接口定义时,关于方法的返回值类型,思路有:
看controller层的返回值类型,如我们这个练习中的PageResult<>,那service层方法返回同一个类型也好,此时controller直接 return service.method();
即可
或者返回一个VO对象,再调用公司的统一结果类,比如若依的AjaxResult,传入VO对象做为data,此时service层方法返回值类型为VO对应的类型
返回其他类型,此时和controller层类型不一样,在controller层不能直接return,可通过set、get或者其他common类中的方法包装成需要的类型来return
关于形参:
/*** @description 课程基本信息管理业务接口*/public interface CourseBaseInfoService {/** @description 课程查询接口* @param pageParams 分页参数* @param queryCourseParamsDto 条件条件*/PageResult queryCourseBaseList(PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto);}
}
写接口的实现类
:!!!!!!!!!!!!!!!
/*** @description 课程信息管理业务接口实现类*/
@Service
public class CourseBaseInfoServiceImpl implements CourseBaseInfoService {@AutowiredCourseBaseMapper courseBaseMapper;@Overridepublic PageResult queryCourseBaseList(PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto) {//构建查询条件对象LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();//构建查询条件,根据课程名称查询queryWrapper.like(StringUtils.isNotEmpty(queryCourseParamsDto.getCourseName()),CourseBase::getName,queryCourseParamsDto.getCourseName());//构建查询条件,根据课程审核状态查询queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParamsDto.getAuditStatus()),CourseBase::getAuditStatus,queryCourseParamsDto.getAuditStatus());//构建查询条件,根据课程发布状态查询queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParamDto.getPublishStatus()),CourseBase:getPublishStatus,queryCourseParamDto.getPublishStatus());//分页对象Page page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());// 查询数据内容获得结果Page pageResult = courseBaseMapper.selectPage(page, queryWrapper);// 获取数据列表List list = pageResult.getRecords();// 获取数据总数long total = pageResult.getTotal();// 构建结果集PageResult courseBasePageResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());return courseBasePageResult;}}
@SpringBootTest
class CourseBaseInfoServiceTests {@AutowiredCourseBaseInfoService courseBaseInfoService;@Testvoid testCourseBaseInfoService() {//查询条件,相当于前端筛选框输入后点查询传过来的QueryCourseParamsDto queryCourseParamsDto = new QueryCourseParamsDto();queryCourseParamsDto.setCourseName("java");queryCourseParamsDto.setAuditStatus("202004");queryCourseParamsDto.setPublishStatus("203001");//分页参数PageParams pageParams = new PageParams();pageParams.setPageNo(1L);//页码pageParams.setPageSize(3L);//每页记录数PageResult courseBasePageResult = courseBaseInfoService.queryCourseBaseList(pageParams, queryCourseParamsDto);System.out.println(courseBasePageResult);}}
在开发完mapper层和service层后,controller层不再用return null来占位,自动注入service层对象,调用service层方法。
@RestController
public class CourseBaseInfoController {@Autowire
CourseBaseInfoService courseBaseInfoService;@PostMapping("/course/list")public PageResult list(PageParams pageParams, @RequestBody(required=false) QueryCourseParamsDto queryCourseParams){//service层这里返回的本来就是PageResult类型,直接return就行return courseBaseInfoService.queryCourseBaseList(pageParams,queryCourseParams);}
}
到此,接口开发完成,总结下以上的流程
:
安装:
Swagger是一个在线接口文档,虽然使用它也能测试但需要浏览器进入Swagger,最关键的是它并不能保存测试数据,可使用IDEA中的插件HttpClient:
用法:
在controller层接口上点击Generate request in HTTP Client
可以看到自己生成了一个.http结尾的文件
添加测试json
点击运行
优化:
为了统一保存,在项目工程的根目录创建一个目录单独存放.http文件
并以模块为单位创建.http文件,拷贝刚才的http文件内容
为了方便将来和网关集成测试,这里我们把测试主机地址在配置文件http-client.env.json
中配置
{"dev": {"access_token": "","gateway_host": "localhost:63010","content_host": "localhost:63040","system_host": "localhost:63110","media_host": "localhost:63050","search_host": "localhost:63080","auth_host": "localhost:63070","checkcode_host": "localhost:63075","learning_host": "localhost:63020"}
}
此时:再回到xc-content-api.http文件,将http://localhost:63040 用变量代替
实现设计澄清后,前后端开始照着接口文档同时开发前后端,此时前端开发人员会使用mock数据(假数据)进行开发。后端开发完成后,前端工程师将mock数据改为请求后端接口获取,前端代码请求后端服务测试接口是否正常,这个过程是前后端联调
。
使用msi包安装nodejs,安装完成后查看版本
拷贝前端工程,并使用IDEA启动:从前端工程拷贝project-xczx2-portal-vue-ts.zip到代码目录并解压,并使用IDEA或VS Code打开project-xczx2-portal-vue-ts目录
点击show npm Script
打开npm窗口
点击“Edit ‘serve’” setting,下边对启动项目的一些参数进行配置,选择nodejs、npm
右键server,点击run
启动成功
如果存在问题通过以下命令启动:
--------------------
1、cmd进入工程根目录 2、运行以下命令npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm i
npm run serve
在浏览器通过http://localhost:8601/地址访问前端工程,有个接口报错:
即System服务异常,这个接口是前端请求后端获取数据字典数据的接口
进入system模块,找到resources下的application.yml修改数据库连接参数,系统服务的端口是63110。启动系统管理服务,启动成功,在浏览器请求:http://localhost:63110/system/dictionary/all
跨域报错
在浏览器通过http://localhost:8601/地址访问前端工程。
Access to XMLHttpRequest at 'http://localhost:63110/system/dictionary/all'
from origin 'http://localhost:8601'
has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
firefox浏览器报错如下:
已拦截跨源请求:同源策略禁止读取位于
http://localhost:63110/system/dictionary/all 的远程资源。
(原因:CORS 头缺少 'Access-Control-Allow-Origin')。状态码:200。
报错分析:
从http://localhost:8601访问http://localhost:63110/system/dictionary/all被CORS policy阻止,因为没有Access-Control-Allow-Origin 头信息。CORS全称是 cross origin resource share 表示跨域资源共享。
出这个提示的原因是基于浏览器的同源策略
,去判断是否跨域请求,同源策略是浏览器的一种安全机制,从一个地址请求另一个地址,如果协议、主机、端口三者全部一致则不属于跨域
,否则有一个不一致就是跨域请求。
如:
解决思路1:
服务器收到请求判断这个Origin是否允许跨域,如果允许则在响应头中说明允许该来源的跨域请求:
Access-Control-Allow-Origin:http://localhost:8601
如果允许任何域名来源的跨域请求,则响应如下:
Access-Control-Allow-Origin:*
解决思路2:JSONP
解决思路3:Nginx做代理
由于服务端之间没有跨域,浏览器通过nginx去访问跨域地址
解决跨域报错
使用上面的方式一来解决:在内容管理的api工程config包下编写GlobalCorsConfig.java
/*** @description 跨域过虑器*/@Configurationpublic class GlobalCorsConfig {/*** 允许跨域调用的过滤器*/@Beanpublic CorsFilter corsFilter() {CorsConfiguration config = new CorsConfiguration();//允许白名单域名进行跨域调用config.addAllowedOrigin("*");//允许跨越发送cookieconfig.setAllowCredentials(true);//放行全部原始头信息config.addAllowedHeader("*");//允许所有请求方法跨域调用config.addAllowedMethod("*");UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", config);return new CorsFilter(source);}}
此配置类实现了跨域过虑器,在响应头添加Access-Control-Allow-Origin。