稻草项目知识点总结
MyBatisPlus
什么是MyBatisPlus
就是在MyBatis框架的基础上延伸了一些新的功能的框架,使用MyBatisPlus不用再导入Mybatis的依赖了
怎么使用MyBatisPlus
找到父项目的pom.xml文件添加一个版本依赖和依赖管理
代码如下
4.0.0 org.springframework.boot spring-boot-starter-parent 2.4.0 cn.tedu straw 0.0.1-SNAPSHOT straw Demo project for Spring Boot pom straw-portal 1.8 3.3.1 com.baomidou mybatis-plus-boot-starter ${mybatis.plus.version} com.baomidou mybatis-plus-extension ${mybatis.plus.version} com.baomidou mybatis-plus-generator ${mybatis.plus.version} 子项目的pom.xml文件使用这些依赖
代码如下
4.0.0 cn.tedu straw 0.0.1-SNAPSHOT cn.tedu straw-portal 0.0.1-SNAPSHOT straw-portal Demo project for Spring Boot 1.8 com.baomidou mybatis-plus-boot-starter org.springframework.boot spring-boot-starter-freemarker org.springframework.boot spring-boot-starter com.baomidou mybatis-plus-generator com.baomidou mybatis-plus-extension mysql mysql-connector-java org.projectlombok lombok org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin
1.简化实体类的mapper接口
创建一个实体类使用MyBatisPlus
创建Tag实体类代码如下
@Data
public class Tag {
private Integer id;
private String name;
private String createBy;
private String createTime;
}
创建这个实体类对应的Mapper接口
TagMapper代码如下
//BaseMapper接口是MyBatisPlus提供的
//其中包含着一些最基本的查询
public interface TagMapper extends BaseMapper {
}
不要忘了在配置类中配置扫描@MapperScan
@SpringBootApplication
@MapperScan("cn.tedu.straw.portal.mapper")
public class StrawPortalApplication {
public static void main(String[] args) {
SpringApplication.run(StrawPortalApplication.class, args);
}
}
2.代码自动生成
按照数据库的内容(表,列等信息)自动生成实体类和实体类相关的其它类
这些功能是由MyBatisPlus的代码生成器提供的
1.导入依赖
上面的课程中已经将代码生成器需要的依赖导入了
2.创建子项目straw-generator
父子相认
新建的子项目中添加如下依赖
com.baomidou
mybatis-plus-boot-starter
org.springframework.boot
spring-boot-starter-freemarker
org.springframework.boot
spring-boot-starter
com.baomidou
mybatis-plus-generator
com.baomidou
mybatis-plus-extension
mysql
mysql-connector-java
org.projectlombok
lombok
3.创建一个类CodeGenerator类中复制从苍老师的网站获得代码生成器的代码:
package cn.tedu.generator;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.FileOutConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.TemplateConfig;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
/**
* @Description: 代码生成类
*/
public class CodeGenerator {
//数据库连接参数
public static String driver = "com.mysql.cj.jdbc.Driver";
public static String url = "jdbc:mysql://localhost:3306/straw?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true";
public static String username="root";
public static String password="root";
//父级别包名称
public static String parentPackage = "cn.tedu.straw";
//代码生成的目标路径
public static String generateTo = "/straw-generator/src/main/java";
//mapper.xml的生成路径
public static String mapperXmlPath = "/straw-generator/src/main/resources/mapper";
//控制器的公共基类,用于抽象控制器的公共方法,null值表示没有父类
public static String baseControllerClassName ;// = "cn.tedu.straw.portal.base.BaseController";
//业务层的公共基类,用于抽象公共方法
public static String baseServiceClassName ; // = "cn.tedu.straw.portal.base.BaseServiceImpl";
//作者名
public static String author = "tedu.cn";
//模块名称,用于组成包名
public static String modelName = "portal";
//Mapper接口的模板文件,不用写后缀 .ftl
public static String mapperTempalte = "/ftl/mapper.java";
/**
*
* 读取控制台内容
*
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
/**
* RUN THIS
*/
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + generateTo);
gc.setAuthor(author);
gc.setOpen(false);
//设置时间类型为Date
gc.setDateType(DateType.TIME_PACK);
//开启swagger
//gc.setSwagger2(true);
//设置mapper.xml的resultMap
gc.setBaseResultMap(true);
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl(url);
// dsc.setSchemaName("public");
dsc.setDriverName(driver);
dsc.setUsername(username);
dsc.setPassword(password);
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setEntity("model");
//pc.setModuleName(scanner("模块名"));
pc.setModuleName(modelName);
pc.setParent(parentPackage);
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
List focList = new ArrayList<>();
focList.add(new FileOutConfig("/templates/mapper.xml.ftl") {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输入文件名称
return projectPath + mapperXmlPath
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
mpg.setTemplate(new TemplateConfig().setXml(null));
mpg.setTemplate(new TemplateConfig().setMapper(mapperTempalte));
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
//字段驼峰命名
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
//设置实体类的lombok
strategy.setEntityLombokModel(true);
//设置controller的父类
if (baseControllerClassName!=null) strategy.setSuperControllerClass(baseControllerClassName);
//设置服务类的父类
if (baseServiceClassName !=null ) strategy.setSuperServiceImplClass(baseServiceClassName);
// strategy.
//设置实体类属性对应表字段的注解
strategy.setEntityTableFieldAnnotationEnable(true);
//设置表名
String tableName = scanner("表名, all全部表");
if(! "all".equalsIgnoreCase(tableName)){
strategy.setInclude(tableName);
}
strategy.setTablePrefix(pc.getModuleName() + "_");
strategy.setRestControllerStyle(true);
mpg.setStrategy(strategy);
// 选择 freemarker 引擎需要指定如下加,注意 pom 依赖必须有!
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
4.在resources中创建ftl文件夹,文件夹中创建mapper.java.ftl文件
代码如下
import ${package.Entity}.${entity};
import ${superMapperClassPackage};
import org.springframework.stereotype.Repository;
/**
*
* ${table.comment!} Mapper 接口
*
*
* @author ${author}
* @since ${date}
*/
<#if kotlin>
interface ${table.mapperName} : ${superMapperClass}<${entity}>
<#else>
@Repository
public interface ${table.mapperName} extends ${superMapperClass}<${entity}> {
}
#if>
运行CodeGenerator类中的main方法,输入all等待方法运行完毕,
项目中就包含这些生成的类了
Spring 安全框架
1.介绍
Spring-Security(Spring安全框架)是Spring提供的安全管理组件
是Spring框架环境下提供的安全管理和权限管理的组件
一个项目一般都会有登录功能,我们之前编写的登录功能非常简陋,不能用于实际开发
Spring-Security提供了专业的实现登录的方式,供我们使用
### 2.使用Spring-Security实现登录
1.导入依赖
org.springframework.boot
spring-boot-starter-security
org.springframework.security
spring-security-test
test
2.配置用户名密码
application.properties文件中添加配置如下
# Spring-Security配置用户名密码
spring.security.user.name=admin
spring.security.user.password=123456
密码加密
我们使用BCrypt的加密规则
1.新建一个包cn.tedu.straw.portal.security
配置类SecurityConfig,在这个类中注入加密对象
代码如下
//@Configuration表示当前类是配置类,可能向Spring容器中注入对象
@Configuration
public class SecurityConfig {
//注入一个加密对象
@Bean
public PasswordEncoder passwordEncoder(){
//这个加密对象使用BCrypt加密内容
return new BCryptPasswordEncoder();
}
}
2.下面进行测试,测试加密功能和验证功能
代码如下
@SpringBootTest
public class SecurityTest {
@Autowired
PasswordEncoder passwordEncoder;
@Test
public void encodeTest(){
/*
每次运行加密结果不同
是因为加密对象采用了"随机加盐"技术,提高安全性
*/
String pwd=passwordEncoder.encode("123456");
System.out.println(pwd);
//$2a$10$IHMiKBqpiPFYgRg4P0E0HeU.xdkr1nw0/y1AWKIvHh5TMNwxVuBRW
}
@Test
public void matchTest(){
/*
验证我们输入的密码是不是能匹配生成的密文
*/
boolean b=passwordEncoder.matches("123456",
"$2a$10$IHMiKBqpiPFYgRg4P0E0" +
"HeU.xdkr1nw0/y1AWKIvHh5TMNwxVuBRW");
System.out.println(b);
}
}
修改application.properties文件中配置的密码
# Spring-Security配置用户名密码
spring.security.user.name=admin
spring.security.user.password=$2a$10$IHMiKBqpiPFYgRg4P0E0HeU.xdkr1nw0/y1AWKIvHh5TMNwxVuBRW
将一个Spring内置的算法标记标注在application.properties文件的密文密码前
代码如下
# Spring-Security配置用户名密码
spring.security.user.name=admin
spring.security.user.password={bcrypt}$2a$10$IHMiKBqpiPFYgRg4P0E0HeU.xdkr1nw0/y1AWKIvHh5TMNwxVuBRW
3.连接数据库实现Spring-Security登录
步骤1(准备)
关于用户的权限多表联查
数据库中有:
permission表,保存权限
role表,保存角色
role_permission表,保存角色和权限的关系
role是permission多对多关系,多对多关系的表一定会出现一张中间表,来保存他们的关系
user表,保存用户信息
user_role表,保存用户和角色的关系
user和role表也是多对多的关系
我们在登录用户时需要指定用户的权限,根据用户的id查询权限可能需要使用这5张表的连接查询
除了对权限的查询外,还需要用户的基本信息,使用用户名查询出用户对象即可
在UserMapper接口中添加如下两个查询
@Repository
public interface UserMapper extends BaseMapper {
//根据用户输入的用户名查询用户信息的方法
@Select("select * from user where username=#{username}")
User findUserByUsername(String username);
//查询指定id的用户的所有权限
@Select("SELECT p.id,p.name" +
" FROM user u" +
" LEFT JOIN user_role ur ON u.id=ur.user_id" +
" LEFT JOIN role r ON r.id=ur.role_id" +
" LEFT JOIN role_permission rp ON r.id=rp.role_id" +
" LEFT JOIN permission p ON p.id=rp.permission_id" +
" WHERE u.id=#{id}")
List findUserPermissionsById(Integer id);
}
步骤2
在编写IUserService接口中添加一个获得用户详情的方法
public interface IUserService extends IService {
//这个方法用法查询获得用户详情对象的业务
//UserDetails是SpringSecurity验证用户必要的信息
//String username是SpringSecurity接收的用户输入的用户名
UserDetails getUserDetails(String username);
}
在impl包下的UserServiceImpl类中实现这个方法
@Service
public class UserServiceImpl extends ServiceImpl implements IUserService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails getUserDetails(String username) {
//根据用户名获得用户对象
User user=userMapper.findUserByUsername(username);
//判断用户对象是否为空
if(user==null) {
//如果为空直接返回null
return null;
}
//如果不为空根据用户的id查询这个用户的所有权限
List permissions=
userMapper.findUserPermissionsById(user.getId());
//将权限List中的权限转成数组方便赋值
String[] auths=new String[permissions.size()];
for(int i=0;i
步骤3
UserDetailsServiceImpl类中来调用刚刚编写的UserServiceImpl类中的方法
返回UserDetails对象即可
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
//Spring-Security认证信息时
//会将用户名传递到这个方法中
//根据这个用户名获得数据库中加密的密码,
//如果匹配则登录成功
@Autowired
IUserService userService;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
return userService.getUserDetails(username);
}
}
### 4.控制授权范围
网站有些页面需要登录后才能访问,但是有些直接就可以访问
我们设置一下授权范围,无论是否登录都可以访问首页
代码如下
//@Configuration表示当前类是配置类,可能向Spring容器中注入对象
@Configuration
//下面的注解表示通知Spring-Security开启权限管理功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends
WebSecurityConfigurerAdapter {
@Autowired
UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService);
}
//控制授权代码在这里!!!!!
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()//对当前全部请求进行授权
.antMatchers(
"/index.html",
"/img/*",
"/js/*",
"/css/*",
"/bower_components/**"
)//设置路径
.permitAll()//允许全部请求访问上面定义的路径
//其它路径需要全部进行表单登录验证
.anyRequest().authenticated().and().formLogin();
}
续Spring-Security
自定义登录界面
步骤1
登录页面是视图模板引擎生成的,所以需要引入Thymeleaf的依赖
子项目的pom.xml文件
org.springframework.boot
spring-boot-starter-thymeleaf
org.thymeleaf.extras
thymeleaf-extras-springsecurity5
步骤2
将static文件夹中的login.html复制到templates文件夹下
需要注意
现在login.html提交的路径是/login
用户名和密码输入框的name是username和password
这两个名字也是Spring-Security约定的不要改!!
步骤3
我们需要写一个控制器来访问显示这个页面
这个控制器不输于任何实体类,新建一个SystemController
@RestController
public class SystemController {
//显示登录页面的方法
@GetMapping("/login.html")
public ModelAndView loginForm(){
//ModelAndView("login");对应的是resources/templates/login.html
return new ModelAndView("login");
}
}
步骤4
要对login.html进行放行,要配置登录时的各种信息,要配置登出时的各种信息
SecurityConfig类中编写
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()//对当前全部请求进行授权
.antMatchers(
"/index.html",
"/img/*",
"/js/*",
"/css/*",
"/bower_components/**",
"/login.html"
)//设置路径
.permitAll()//允许全部请求访问上面定义的路径
//其它路径需要全部进行表单登录验证
.anyRequest().authenticated().and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.failureUrl("/login.html?error")
.defaultSuccessUrl("/index.html")
.and().logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html?logout");
}
方法说明:
- csrf().disable():关闭防跨域攻击功能,不关闭容易发生错误
- loginPage:指定登录页面路径
- loginProcessingUrl:指定表单提交的路径
- failureUrl:指定登录失败时的路径
- defaultSuccessUrl:指定登录成功时的路径
- logout():表示开始配置登出时的内容
- logoutUrl:指定出的路径(当页面有这个请求时,Spring-Security去执行用户登出操作)
- logoutSuccessUrl:指定登出成功之后显示的页面
Spring验证框架
1.介绍
Spring提供的对用户输入信息进行验证的框架组件
是服务器端验证技术
使用Spring验证框架验证发送到服务器的内容的合法性!
Spring-validation(验证)
2.使用Spring-Validation
步骤1
导入依赖
子项目pom.xml文件添加:
org.springframework.boot
spring-boot-starter-validation
步骤2
定位到要验证信息的实体类
将验证规则按照给定注解来标记即可
要验证注册业务,就找RegisterVo类即可
@Data
public class RegisterVo implements Serializable {
//只能作用在String上,不能为null,去掉空格之后也不能为""
@NotBlank(message = "邀请码不能为空")
private String inviteCode;
@NotBlank(message = "用户名不能为空")
//@Pattern()表示下面的属性需要通过指定正则表达式的判断
@Pattern(regexp="^1\\d{10}$",message ="手机号格式不正确")
private String phone;
@NotBlank(message = "昵称不能为空")
@Pattern(regexp="^.{2,20}$",message ="昵称在2到20位之间")
private String nickname;
@NotBlank(message = "密码不能为空")
@Pattern(regexp="^\\w{6,20}$",message ="密码在6~20位之间")
private String password;
@NotBlank(message = "确认密码不能为空")
private String confirm;
}
步骤3
在控制器从表单或ajax获得实体类对象参数时就可以对这个实体类属性的值进行上面设置的验证了
验证方法非常简单,只需要加一个注解即可!
SystemController注册方法代码修改如下
@PostMapping("/register")
public R registerStudent(
//控制器接收的参数前加@Validated
//表示要按这个类规定的验证规则,验证这个对象属性的值
@Validated RegisterVo registerVo,
//固定用法,在验证参数后再跟一个参数:BindingResult
//这个参数中记录保存上面验证过程中的验证信息和结果
BindingResult validaResult){
//在控制器调用业务逻辑前,先判断BindingResult对象中是否有错误
if(validaResult.hasErrors()){
//如果验证结果中包含任何错误信息,进入这个if
//获得其中的一个错误信息显示,一般是按顺序的第一个错误信息
String error=validaResult.getFieldError()
.getDefaultMessage();
return R.unproecsableEntity(error);
}
System.out.println(registerVo);
log.debug("得到信息为:{}",registerVo);
try{
userService.registerStudent(registerVo);
return R.created("注册成功!");
}catch (ServiceException e){
log.error("注册失败",e);
return R.failed(e);
}
}
VUE(基本使用)
1.介绍
也是一个js为基础的前端框架
提供了一套前端信息和服务器信息交互的一种方式
这种方式要比以前的信息交互方式简单
一般情况下,程序要结合JQuery的ajax操作和Vue的功能完成前后端信息交互
2.如何使用VUE
使用准备
Idea添加插件
编写html文件
static文件夹下创建一个测试Vue的页面vue.html
这个页面项目中不使用,就是测试用
Title VUE演示
Vue功能的强大之处在于信息实时同步的双向绑定
使用VUE+Ajax完善稻草问答的注册功能
修改register.html代码
修改static/js/register.js代码
"R"类和自定义业务异常类
1.介绍
实体类能接收表单发送过来的信息,但是我们控制器处理完成后,想返回Json格式的对象给JS,也需要一个实体类
这个实体类最好能够通用于所有业务
现在行业中流行使用一个"R"类来返回JSON格式信息
这个R类中主要包含3个属性
1.状态码
2.状态消息
3.实体(控制器查询出的任何内容)
2.如何使用
创建R类(代码无需掌握,会使用即可)
@Data
@Accessors(chain = true)
public class R implements Serializable {
/** 200 OK - [GET]:服务器成功返回用户请求的数据 */
public static final int OK = 200;
/** 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。 */
public static final int CREATED = 201;
/** 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务) */
public static final int ACCEPTED = 202;
/** 204 NO CONTENT - [DELETE]:用户删除数据成功。 */
public static final int NO_CONTENT = 204;
/** 400 INVALID REQUEST - [POST/PUT/PATCH]:
用户发出的请求有错误,服务器没有进行新建或修改数据的操作。*/
public static final int INVALID_REQUEST = 400;
/** 401 Unauthorized - [*]:
表示用户没有权限(令牌、用户名、密码错误)。 */
public static final int UNAUTHORIZED = 401;
/** 403 Forbidden - [*]
表示用户得到授权(与401错误相对),但是访问是被禁止的。*/
public static final int FORBIDDEN = 403;
/** 404 NOT FOUND - [*]:
用户发出的请求针对的是不存在的记录,服务器没有进行操作。 */
public static final int NOT_FOUND = 404;
/** 410 Gone -[GET]:
用户请求的资源被永久删除,且不会再得到的。*/
public static final int GONE = 410;
/** 422 Unprocesable entity - [POST/PUT/PATCH]
当创建一个对象时,发生一个验证错误。 */
public static final int UNPROCESABLE_ENTITY = 422;
/** 500 INTERNAL SERVER ERROR - [*]:
服务器发生错误,用户将无法判断发出的请求是否成功。 */
public static final int INTERNAL_SERVER_ERROR = 500;
private int code;
private String message;
private T data;
/**
* 服务器成功返回用户请求的数据
* @param message 消息
*/
public static R ok(String message){
return new R().setCode(OK).setMessage(message);
}
/**
* 服务器成功返回用户请求的数据
* @param data 数据
*/
public static R ok(Object data){
return new R().setMessage("OK").setCode(OK).setData(data);
}
/**
* 用户新建或修改数据成功。
*/
public static R created(String message){
return new R().setCode(CREATED).setMessage(message);
}
/**
* 表示一个请求已经进入后台排队(异步任务)
*/
public static R accepted(String message){
return new R().setCode(ACCEPTED).setMessage(message);
}
/**
* 用户删除数据成功
*/
public static R noContent(String message){
return new R().setCode(NO_CONTENT).setMessage(message);
}
/**
* 用户发出的请求有错误,服务器没有进行新建或修改数据的操作。
*/
public static R invalidRequest(String message){
return new R().setCode(INVALID_REQUEST).setMessage(message);
}
/**
* 表示用户没有权限(令牌、用户名、密码错误)
*/
public static R unauthorized(String message){
return new R().setCode(UNAUTHORIZED).setMessage(message);
}
/**
* 登录以后,但是没有足够权限
*/
public static R forbidden(){
return new R().setCode(FORBIDDEN).setMessage("权限不足!");
}
/**
* 用户发出的请求针对的是不存在的记录,服务器没有进行操作。
*/
public static R notFound(String message){
return new R().setCode(NOT_FOUND).setMessage(message);
}
/**
* 用户请求的资源被永久删除,且不会再得到的。
*/
public static R gone(String message){
return new R().setCode(GONE).setMessage(message);
}
/**
* 当创建一个对象时,发生一个验证错误。
*/
public static R unproecsableEntity(String message){
return new R().setCode(UNPROCESABLE_ENTITY)
.setMessage(message);
}
/**
* 将异常消息复制到返回结果中
*/
public static R failed(ServiceException e){
return new R().setCode(e.getCode())
.setMessage(e.getMessage());
}
/**
* 服务器发生错误,用户将无法判断发出的请求是否成功。
*/
public static R failed(Throwable e){
return new R().setCode(INTERNAL_SERVER_ERROR)
.setMessage(e.getMessage());
}
/**
* 新增成功,并且需要获得新增成功对象时使用这个方法
*/
public static R created(Object data){
return new R().setCode(CREATED).setMessage("创建成功")
.setData(data);
}
自定义业务异常类
这个类和R类相同也不需要掌握代码,只需要掌握用法
public class ServiceException extends RuntimeException{
private int code = R.INTERNAL_SERVER_ERROR;
public ServiceException() { }
public ServiceException(String message) {
super(message);
}
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
public ServiceException(Throwable cause) {
super(cause);
}
public ServiceException(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public ServiceException(int code) {
this.code = code;
}
public ServiceException(String message, int code) {
super(message);
this.code = code;
}
public ServiceException(String message, Throwable cause,
int code) {
super(message, cause);
this.code = code;
}
public ServiceException(Throwable cause, int code) {
super(cause);
this.code = code;
}
public ServiceException(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace, int code) {
super(message, cause, enableSuppression, writableStackTrace);
this.code = code;
}
public int getCode() {
return code;
}
/** 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作。*/
public static ServiceException invalidRequest(String message){
return new ServiceException(message, R.INVALID_REQUEST);
}
/** 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作。 */
public static ServiceException notFound(String message){
return new ServiceException(message, R.NOT_FOUND);
}
/** 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。*/
public static ServiceException gone(String message){
return new ServiceException(message, R.GONE);
}
/** 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。 */
public static ServiceException unprocesabelEntity(String message){
return new ServiceException(message, R.UNPROCESABLE_ENTITY);
}
//返回服务器忙的异常
public static ServiceException busy(){
return new ServiceException("数据库忙",R.INTERNAL_SERVER_ERROR);
}
}
声明式事务
如果上面章节中新增过程中发生了异常
那么已经新增到数据库的数据不会删除,还没新增到数据库的数据就不会进入数据库了
就会造成数据的不完整
为了保证事务的完整性我们需要学习Spring的声明式事务
什么是事务
事务是数据库管理系统执行过程的一个最小逻辑单位
转账操作对数据库的影响分两步:
- 转出账户金额减少
- 转入账户金额增加
如果转出账户成功,转入账户失败,那么转出账户金额的减少操作应该撤销
即这两个操作要么都执行要么多不执行
事务的出现就是为了保证数据完整性的
面试常见题:
数据库事务拥有的四个特性
简称ACID
- 原子性(Atomicity):事务是执行数据库操作的最小逻辑,不可再分
一致性(Consistency):事务中对数据库操作的命令状态应该是一致的,
即要么都执行要么都不执行
- 隔离性(Isolation):一个事务的执行,不影响其他事务
- 持久性(Durability):事务操作如果提交,多数据库的影响是持久的
Spring的声明式事务
SpringBoot提供了对事务的支持
相较于我们自己控制JDBC或Mybatis来实现事务,明显SpringBoot实现的方式更简单
只需要在Service层方法上加@Transactional即可
加上这个注解的效果就是:
这个方法对数据库的操作要么都成功,要么都失败
只要发生异常,在发生异常之前的数据库操作会自动撤销!
@Transactional
public void saveXXX(){
//...
}
Spring异常增强
统一处理异常
在控制器方法中多数都需要使用try-catch结构来处理异常,而这个异常的处理又不能省略,每个catch代码又都是相似的,造成了代码冗余
可以使用Spring提供的异常增强处理功能来统一处理控制层方法的异常
处理原理
我们可以定义一个异常增强类
这个异常增强类可以声明为自动处理控制层发生的异常,这样我们就不必每个方法都处理了
在controller包中新建类ExceptionControllerAdvice
代码如下
//@RestControllerAdvice表示对控制器方法的异常增强处理
@RestControllerAdvice
@Slf4j
public class ExceptionControllerAdvice {
//@ExceptionHandler表示这个方法时用来出处理异常的
@ExceptionHandler
public R handlerServiceException(ServiceException e){
log.error("业务异常",e);
return R.failed(e);
}
@ExceptionHandler
public R handlerException(Exception e) {
log.error("其它异常", e);
return R.failed(e);
}
}
说明
- @RestControllerAdvice表示对控制器方法的异常增强处理
- @ExceptionHandler表示这个方法是用来出处理异常的
控制器发生异常时,会自动匹配合适的异常类型,运行方法
文件上传(上载)
1.介绍
在Http协议的标准上,实现将客户端本地文件复制到服务器硬盘中的过程
http协议规定了一些上传文件时的标准
- 表单提交的方式必须是post
- 表单提交的编码方式必须修改为 multipart/form-data(二进制)
- 要上传的文件使用 来表示
HTTP请求头中必须包含 Content-type: multipart/form-data, boundary=AaB03x;
4中的描述了解即可
- 允许上传多个文件
2.文件上传流程
3.测试
编写页面代码
只是为了测试,所以我们编写在static文件夹中即可
upload.html文件
Title
提交到SystemController控制器中的方法代码如下
需要保证f:盘中有upload文件夹
//接收表单上传的文件
@PostMapping("/upload/file")
public R upload(MultipartFile imageFile) throws IOException {
//获得文件名
String name=imageFile.getOriginalFilename();
File f=new File("F:/upload/"+name);
imageFile.transferTo(f);
return R.ok("上载完成!");
}
其中MultipartFile imageFile参数imageFile的名字必须和表单中file控件的name属性一致
getOriginalFilename获得原始文件名
transferTo将这个文件写入到指定的file对象中
Ajax上传文件
我们在实际的开发中,也是使用ajax提交的情况较多
所以我们需要学习ajax如何上传文件
重构upload.html
Title
无需修改控制器代码
直接提交文件测试,成功即可
用户注册
每个网站都需要用户注册的功能
页面如下图
- 获得邀请码(开发过程是从数据库获得,运营时向老师索取)
- 通过登录页上的注册连接显示注册页面
- 向服务器请求注册页并显示到浏览器
- 注册页面填写信息并提交表单
- 服务器接收到表单信息,控制层调用业务逻辑层执行注册操作
- 业务层执行连库操作新增之前验证邀请码
- 邀请码验证通过在执行数据库新增操作
- 返回新增操作的运行结果
- 根据结果反馈到控制层,有异常就报异常
- 控制器将注册结果信息使用JSON返回给浏览器
- 浏览器中局部刷新页面,将注册结果显示给用户
显示注册页面
步骤1:
复制static文件夹中的register.html页面到templates文件夹
步骤2:
编写控制器SystemController类中添加方法
//显示注册页面的方法
@GetMapping("/register.html")
public ModelAndView register(){
return new ModelAndView("register");
}
步骤3:
SecurityConfig类中放行register.html
http.csrf().disable()
.authorizeRequests()//对当前全部请求进行授权
.antMatchers(
"/index.html",
"/img/*",
"/js/*",
"/css/*",
"/bower_components/**",
"/login.html",
"/register.html"//放行在这个!!!!!!
)//设置路径
.permitAll()//允许全部请求访问上面定义的路径
//其它路径需要全部进行表单登录验证
.anyRequest().authenticated().and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.failureUrl("/login.html?error")
.defaultSuccessUrl("/index.html")
.and().logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html?logout");
开发注册业务
1.准备工作
表单提交的5个属性创建一个Vo类接收代码如下
@Data
public class RegisterVo implements Serializable {
private String inviteCode;
private String phone;
private String nickname;
private String password;
private String confirm;
}
1.业务逻辑层Service
注册业务逻辑属于User表
所以在IUserService接口中新建注册方法
//用户注册的方法(现在是针对学生注册) void registerStudent(RegisterVo registerVo);
在IUserService的实现类UserServiceImpl类中重写接口的方法
在方法中排定业务逻辑顺序
@Autowired
ClassroomMapper classroomMapper;
@Autowired
UserRoleMapper userRoleMapper;
BCryptPasswordEncoder passwordEncoder=
new BCryptPasswordEncoder();
@Override
public void registerStudent(RegisterVo registerVo) {
if (registerVo == null) {
throw ServiceException.unprocesabelEntity("表单数据为空");
}
QueryWrapper qw = new QueryWrapper<>();
qw.eq("invite_code", registerVo.getInviteCode());
Classroom classroom = classroomMapper.selectOne(qw);
if (classroom == null) {
throw ServiceException.unprocesabelEntity("邀请码错误!");
}
User u = userMapper.findUserByUserName(registerVo.getPhone());
if (u != null) {
throw ServiceException.unprocesabelEntity("手机号已经被注册");
}
;
User user = new User().setNickname(registerVo.getPhone())
.setUsername(registerVo.getPhone())
.setPhone(registerVo.getPhone())
.setNickname(registerVo.getNickname())
.setClassroomId(classroom.getId())
.setCreatetime(LocalDateTime.now())
.setEnabled(1)
.setLocked(0)
.setPassword("{bcrypt}" + passwordEncoder.encode(registerVo.getPassword()));
int num = userMapper.insert(user);
if(num!=1){
throw new ServiceException("服务器忙,稍后再试");
}
//将新增的用户赋予学生的角色(新增user_role的关系表)
UserRole userRole=new UserRole();
userRole.setUserId(user.getId());
userRole.setRoleId(2);
num=userRoleMapper.insert(userRole);
//验证关系表新增结果
if(num!=1) {
throw new ServiceException("服务器忙,稍后再试");
}
}
测试
@Test void studentRegister(){
RegisterVo registerVo = new RegisterVo();
registerVo.setInviteCode("JSD2001-706246");
registerVo.setNickname("rrr");
registerVo.setPassword("123456");
registerVo.setPhone("11110610361");
registerVo.setConfirm("123456");
userService.registerStudent(registerVo);
}
2.控制器Controller
SystemController类中调用UserServiceImpl类的方法
@PostMapping("/register")
public R registerStudent(
//控制器接收的参数前加@Validated
//表示要按这个类规定的验证规则,验证这个对象属性的值
@Validated RegisterVo registerVo,
//固定用法,在验证参数后再跟一个参数:BindingResult
//这个参数中记录保存上面验证过程中的验证信息和结果
BindingResult validaResult){
//在控制器调用业务逻辑前,先判断BindingResult对象中是否有错误
if(validaResult.hasErrors()){
//如果验证结果中包含任何错误信息,进入这个if
//获得其中的一个错误信息显示,一般是按顺序的第一个错误信息
String error=validaResult.getFieldError()
.getDefaultMessage();
return R.unproecsableEntity(error);
}
System.out.println(registerVo);
log.debug("得到信息为:{}",registerVo);
try{
userService.registerStudent(registerVo);
return R.created("注册成功!");
}catch (ServiceException e){
log.error("注册失败",e);
return R.failed(e);
}
}
配置/register请求的放行
SecurityConfig代码中
学生首页
制作首页的流程
1.制作首页导航栏的tag列表
2.制作学生问题的显示和分页
3.制作学生信息面板
显示首页
1.将static文件中的index.html复制到templates文件夹中
2.创建HomeController类,显示index.html
代码如下
@RestController
@Slf4j
public class HomeController {
//显示首页
@GetMapping("/index.html")
public ModelAndView index(){
return new ModelAndView("index");
}
}
3.撤销在SecurityConfig类中对index.html的放行
达到必须登录才能访问主页的效果
http.csrf().disable()
.authorizeRequests()//对当前全部请求进行授权
.antMatchers(
"/img/*",
"/js/*",
"/css/*",
"/bower_components/**",
"/login.html",
"/register.html",
"/register"
)//设置路径
.permitAll()//允许全部请求访问上面定义的路径
//其它路径需要全部进行表单登录验证
.anyRequest().authenticated().and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.failureUrl("/login.html?error")
.defaultSuccessUrl("/index.html")
.and().logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html?logout");
开发标签列表
1.标签列表显示原理
在用户已经能够登录显示主页的前提下
- 主页页面中编写ajax向控制器发送请求所有标签
- 控制接到请求后调用业务逻辑层
- 业务逻辑层从tagMapper接口查询所有标签
- 业务逻辑层将查询到的信息返回给控制器
- 控制器获得所以标签返回JSON格式
- ajax中获得JSON对象,利用VUE绑定显示在页面上
2.业务逻辑层Service
我们可以选择先编写业务逻辑层
步骤1:
ITagService接口中添加方法
public interface ITagService extends IService {
List getTags();
}
步骤2:
实现这个接口
TagServiceImpl类中代码如下
@Service
public class TagServiceImpl extends ServiceImpl implements ITagService {
//CopyOnWriteArrayList<>是线程安全的集合,适合在高并发的环境下使用
private final List tags=new CopyOnWriteArrayList<>();
@Override
public List getTags() {
//这个if主要是为了保证tags被顺利赋值之后的高效运行
if(tags.isEmpty()) {
synchronized (tags) {
//这个if主要是为了保证不会有两条以上线程为tags重复添加内容
if (tags.isEmpty()) {
//super.list()是父类提供的查询当前指定实体类全部行的代码
tags.addAll(super.list());
}
}
}
return tags;
}
}
步骤3:
测试
@SpringBootTest
public class TagTest {
@Autowired
ITagService tagService;
@Test
public void test() {
List list = tagService.getTags();
for (Tag tag : list)
System.out.println(tag);
}
}
3.控制层Controller
步骤1:
TagController类中编写代码如下
@RestController
//下面的注解表示想访问本控制器中的任何方法需要前缀/v1/tags
//这个v1开头的格式是后期微服务的标准名为RESTful
@RequestMapping("/v1/tags")
public class TagController {
@Autowired
private ITagService tagService;
//查询所有标签@GetMapping("")表示使用类上声明的前缀就可以访问这个方法
@GetMapping("")
public R> tags(){
List list=tagService.getTags();
return R.ok(list);
}
}
4.页面和JS代码
到页面中(index.html)绑定vue需要的变量
页面代码如下
index.html网页的结束位置要引入两个js文件
编写js/tags_nav.js代码如下
let tagsApp = new Vue({
el:'#tagsApp',
data:{
tags:[]
},
methods:{
loadTags:function () {
console.log('执行了 loadTags');
$.ajax({
url:'/v1/tags',
method:'GET',
success:function (r) {
console.log(r);
if (r.code === OK){
console.log('成功获取tags');
tagsApp.tags = r.data;
}
}
});
}
},
created:function () {
this.loadTags();
}
});
开发问题列表
1.了解开发流程
2.业务逻辑层Service
在业务逻辑层的接口中声明方法
IQuestionService接口给中声明方法
public interface IQuestionService extends IService {
//按登录用户查询当前用户问题的方法
List getMyQuestions();
}
要想实现查询当前登录的用户信息,必须使用Spring-Security提供的指定方法
调用这个方法的代码可能在项目后面的业务中也需要
这样写字QuestionService中就不合适了,所以我们先在IUserService中添加一个获得当前登录用户名的方法
IUserService添加代码
//从Spring-Security中获得当前登录用户的用户名的方法
String currentUsername();
在UserServiceImpl类中实现获得当前登录用户名并返回
@Override
public String currentUsername() {
//利用Spring-Security框架获得当前登录用户信息
Authentication authentication=
SecurityContextHolder.getContext()
.getAuthentication();
//判断当前用户有没有登录,如果没有登录抛出异常
if(!(authentication instanceof AnonymousAuthenticationToken)){
//上面代码是判断当前用的抽象权限类型是不是匿名用户
//如果不是匿名用户,就是登录的用户,只有登录的用户才能返回用户名
String username=authentication.getName();
return username;
}
//没运行上面的if证明用户没有登录,抛出异常即可
throw ServiceException.notFound("没有登录");
}
现在就可以在QuestionServiceImpl类中调用上面编写的方法来获得当前登录用户了
在根据这个用户信息(id)查询这个用户的问题
代码如下
@Service
@Slf4j
public class QuestionServiceImpl extends ServiceImpl implements IQuestionService {
@Autowired
IUserService userService;
@Autowired
UserMapper userMapper;
@Autowired
QuestionMapper questionMapper;
//按登录用户查询当前用户问题的方法
@Override
public List getMyQuestions() {
//获得当前登录用户的用户名
String username=userService.currentUsername();
log.debug("当前登录用户为:{}",username);
//如果已经登录,使用之前编写好的findUserByUsername方法
//查询出当前用户的详细信息(实际上主要需要用户的id)
User user=userMapper.findUserByUsername(username);
if(user == null){
throw ServiceException.gone("登录用户不存在");
}
log.debug("开始查询{}用户的问题",user.getId());
QueryWrapper queryWrapper=new QueryWrapper<>();
queryWrapper.eq("user_id",user.getId());
queryWrapper.eq("delete_status",0);
queryWrapper.orderByDesc("createtime");
List list=questionMapper.selectList(queryWrapper);
log.debug("当前用户的问题数量为:{}",list.size());
return list;
}
}
3.控制层Controller
编写完QuestionServiceImpl类中的代码
就可以在控制器中调用了,
控制器调用无需任何参数直接调用即可
第一次打开QuestionController类编写代码如下
@RestController
@RequestMapping("/v1/questions")
@Slf4j
public class QuestionController {
@Autowired
IQuestionService questionService;
//查询返回当前登录用户发布的问题
@GetMapping("/my")
public R> my(){
log.debug("开始查询当前用户的问题");
//这里要处理个异常,因为用户可能没有登录
try{
List questions=
questionService.getMyQuestions();
return R.ok(questions);
}catch (ServiceException e){
log.error("用户查询问题失败!",e);
return R.failed(e);
}
}
}
编写到这里,我们就可以向浏览器编写路径
http://localhost:8080/v1/ques...来看到控制返回的JSON格式信息
4.页面和JS代码
先在index.html页面中编写VUE代码准备绑定JSON格式信息
js/index.js文件修改为
/*
显示当前用户的问题
*/
let questionsApp = new Vue({
el:'#questionsApp',
data: {
questions:[]
},
methods: {
loadQuestions:function () {
$.ajax({
url: '/v1/questions/my',
method: "GET",
success: function (r) {
console.log("成功加载数据");
console.log(r);
if(r.code === OK){
questionsApp.questions = r.data;
}
}
});
}
},
created:function () {
console.log("执行了方法");
this.loadQuestions(1);
}
});
4.1显示问题持续时间
现在流行的处理问题时间的方式不是单纯的显示这个问题的提问时间
而是显示出这个问题出现了多久可能又一下情况
- 刚刚(1分钟之内)
- XX分钟前(60分钟以内)
- XX小时前(24小时以内)
- XX天前
由于时间是数据库中保存好的信息,这个信息已经以JSON格式发送到了ajax中
所以添加这个功能不需要编写后台代码
首先在index.js文件中添加一个计算持续时间的方法
updateDuration,并在ajax中调用
代码如下
/*
显示当前用户的问题
*/
let questionsApp = new Vue({
el:'#questionsApp',
data: {
questions:[]
},
methods: {
loadQuestions:function () {
$.ajax({
url: '/v1/questions/my',
method: "GET",
success: function (r) {
console.log("成功加载数据");
console.log(r);
if(r.code === OK){
questionsApp.questions = r.data;
//调用计算持续时间的方法
questionsApp.updateDuration();
}
}
});
},
updateDuration:function () {
let questions=this.questions;
for(let i=0;i
Index.html页面也需要进行一个修改,让计算出的持续时间显示出来
代码如下
13分钟前
4.2显示问题的标签列表
页面中的问题是可以多个标签的
怎么实现显示多个标签呢?
首先来了解一下标签和问题的对应关系
我们可以看到,在问题表中我们保持了冗余的数据列tag_names,这么做的好处就是减少查询时的复杂度,实际开发中程序员们也可能用这样的方式
实现过程
实现思路
1.创建一个包含全部标签的Map,map的key是标签名称,value是标签对象
2.从问题实体类中获得tag_names属性,利用字符串的split方法,拆分成字符串数组
3.遍历字符串数组,从Map中通过key(标签名称)获得value(标签对象)
4.将获取的value存入Question实体类中的List
1.在Question实体类中需要定义一个List tags
原因是我们需要能够从一个问题中获得多个标签
//为问题实体类添加标签集合
//@TableField(exist = false)表示数据库中没有这样的列,防止报错
@TableField(exist = false)
private List tags;
2.得到包含所有Tag标签的Map
业务逻辑层ITagService添加方法
public interface ITagService extends IService {
//获得所有标签的方法
List getTags();
//获得所有标签返回Map的方法
Map getName2TagMap();
}
实现这个方法
package cn.tedu.straw.portal.service.impl;
import cn.tedu.straw.portal.model.Tag;
import cn.tedu.straw.portal.mapper.TagMapper;
import cn.tedu.straw.portal.service.ITagService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
*
* 服务实现类
*
*
* @author tedu.cn
* @since 2020-12-09
*/
@Service
public class TagServiceImpl extends ServiceImpl implements ITagService {
//CopyOnWriteArrayList<>是线程安全的集合,适合在高并发的环境下使用
private final List tags=new CopyOnWriteArrayList<>();
//ConcurrentHashMap是线程安全的Map,适合在高并发的环境下使用
private final Map map=new ConcurrentHashMap<>();
@Override
public List getTags() {
//这个if主要是为了保证tags被顺利赋值之后的高效运行
if(tags.isEmpty()) {
synchronized (tags) {
//这个if主要是为了保证不会有两条以上线程为tags重复添加内容
if (tags.isEmpty()) {
//super.list()是父类提供的查询当前指定实体类全部行的代码
tags.addAll(super.list());
//为所有标签赋值List类型之后,可以同步给map赋值
for(Tag t: tags){
//将tags中所有标签赋值给map
//而map的key是tag的name,value就是tag
map.put(t.getName(),t);
}
}
}
}
return tags;
}
@Override
public Map getName2TagMap() {
//判断如果map是空,证明上面getTags方法没有运行
if(map.isEmpty()){
//那么就调用上面的getTags方法
getTags();
}
return map;
}
}
3.将数据库tag_names列中的内容转换成List
在QuestionServiceImpl类中编写代码
//根据Question的tag_names列的值,返回List
private List tagNamesToTags(String tagNames){
//得到的tag_name拆分字符串
//tagNames="java基础,javaSE,面试题"
String[] names=tagNames.split(",");
//names={"java基础","javaSE","面试题"}
//声明List以便返回
List list=new ArrayList<>();
Map map=tagService.getName2TagMap();
//遍历String数组
for(String name:names) {
//根据String数组中当前的元素获得Map对应的value
Tag tag=map.get(name);
//将这个value保存在list对象中
list.add(tag);
}
return list;
}
4.获得Question对象中的List并赋值
在我们编写的QuestionServiceImpl类中的getMyQuestions方法中
根据步骤3中编写的方法来获得Question对象中的List
@Service
@Slf4j
public class QuestionServiceImpl extends ServiceImpl implements IQuestionService {
@Autowired
IUserService userService;
@Autowired
UserMapper userMapper;
@Autowired
QuestionMapper questionMapper;
//按登录用户查询当前用户问题的方法
@Override
public List getMyQuestions() {
//获得当前登录用户的用户名
String username=userService.currentUsername();
log.debug("当前登录用户为:{}",username);
//如果已经登录,使用之前编写好的findUserByUsername方法
//查询出当前用户的详细信息(实际上主要需要用户的id)
User user=userMapper.findUserByUsername(username);
if(user == null){
throw ServiceException.gone("登录用户不存在");
}
log.debug("开始查询{}用户的问题",user.getId());
QueryWrapper queryWrapper=new QueryWrapper<>();
queryWrapper.eq("user_id",user.getId());
queryWrapper.eq("delete_status",0);
queryWrapper.orderByDesc("createtime");
List list=questionMapper.selectList(queryWrapper);
log.debug("当前用户的问题数量为:{}",list.size());
//遍历当前查询出的所有问题对象
for(Question q: list){
//将问题每个对象的对应的Tag都查询出来,并赋值为实体类中的List
List tags=tagNamesToTags(q.getTagNames());
q.setTags(tags);
}
return list;
}
@Autowired
ITagService tagService;
//根据Question的tag_names列的值,返回List
private List tagNamesToTags(String tagNames){
//得到的tag_name拆分字符串
//tagNames="java基础,javaSE,面试题"
String[] names=tagNames.split(",");
//names={"java基础","javaSE","面试题"}
//声明List以便返回
List list=new ArrayList<>();
Map map=tagService.getName2TagMap();
//遍历String数组
for(String name:names) {
//根据String数组中当前的元素获得Map对应的value
Tag tag=map.get(name);
//将这个value保存在list对象中
list.add(tag);
}
return list;
}
}
5.修改一下html页面内容,来获取问题的标签
Java基础
4.3显示问题的图片
项目中每个问题右侧跟一个图片,这个图片实际上是根据问题的第一个标签的id来决定的
需要在index.js文件中编写显示相关图片的代码
并在合适位置调用
代码如下
updateTagImage: function () {
let questions = this.questions;
for (let i = 0; i < questions.length; i++) {
let tags = questions[i].tags;
if (tags) {
let tagImage = 'img/tags/' + tags[0].id + '.jpg';
questions[i].tagImage = tagImage;
}
}
},
在index.html文件中绑定

4.4实现分页功能
1.介绍
- 不会一次显示太多内容,不会产生大量流量,对服务器压力小
- 我们需要的信息,往往在前面几条的内容,后面的内容使用率不高
- 用户体验强,方便记忆位置
实现分页的sql语句
主要通过limit关键字实现分页查询
只查询userid为11的学生提问的前8条内容
select id,title from question where user_id=11 order by createtime desc limit 0,8
使用上面的sql语句可以实现分页功能
但是所有信息都需要自己计算,而且计算的方式是固定的,
所以Mybatis提供了一套自动完成计算的翻页组件
PageHelper
PageHelper的使用
1.导入依赖
由于PageHelper是Mybatis提供的,没有SpringBoot的默认版本支持
所以像Mybatis一眼我们要自己管理版本
在Straw父项目的pom.xml文件中
1.8
3.3.1
1.3.0
com.github.pagehelper
pagehelper-spring-boot-starter
${pagehelper.starter.version}
子项目pom.xml文件添加代码
com.github.pagehelper
pagehelper-spring-boot-starter
注意父子项目的pom.xml都需要刷新!
2.接口重构
IQuestionService接口重构
//按登录用户查询当前用户问题的方法
PageInfo getMyQuestions(
Integer pageNum,Integer pageSize
);
QuestionServiceImpl重构接口中的方法
//按登录用户查询当前用户问题的方法
@Override
public PageInfo getMyQuestions(
//传入翻页查询的参数
Integer pageNum,Integer pageSize
) {
//分页查询,决定查询的页数
if(pageNum==null || pageSize==null){
//分页查询信息不全,直接抛异常
throw ServiceException.invalidRequest("参数不能为空");
}
//获得当前登录用户的用户名
String username=userService.currentUsername();
log.debug("当前登录用户为:{}",username);
//如果已经登录,使用之前编写好的findUserByUsername方法
//查询出当前用户的详细信息(实际上主要需要用户的id)
User user=userMapper.findUserByUsername(username);
if(user == null){
throw ServiceException.gone("登录用户不存在");
}
log.debug("开始查询{}用户的问题",user.getId());
QueryWrapper queryWrapper=new QueryWrapper<>();
queryWrapper.eq("user_id",user.getId());
queryWrapper.eq("delete_status",0);
queryWrapper.orderByDesc("createtime");
//执行查询之前,要设置分页查询信息
PageHelper.startPage(pageNum,pageSize);
//紧接着的查询就是按照上面分页配置的分页查询
List list=questionMapper.selectList(queryWrapper);
log.debug("当前用户的问题数量为:{}",list.size());
//遍历当前查询出的所有问题对象
for(Question q: list){
//将问题每个对象的对应的Tag都查询出来,并赋值为实体类中的List
List tags=tagNamesToTags(q.getTagNames());
q.setTags(tags);
}
return new PageInfo(list);
}
3.测试
@SpringBootTest
public class QuestionTest {
@Autowired
IQuestionService questionService;
@Test
//@WithMockUser是Spring-Security提供的注解
//在测试中如果需要从Spring-Security中获得用户信息,那么就可以用这个注解标记
//指定用户信息,也要注意,这只是个测试,Spring-Security不会对信息验证
@WithMockUser(username = "st2",password = "123456")
public void getQuest(){
PageInfo pi=
questionService.getMyQuestions(1,8);
for(Question q:pi.getList()){
System.out.println(q);
}
}
}
4.重构QuestionController
@RestController
@RequestMapping("/v1/questions")
@Slf4j
public class QuestionController {
@Autowired
IQuestionService questionService;
//查询返回当前登录用户发布的问题
@GetMapping("/my")
public R> my(Integer pageNum){
if(pageNum==null){
pageNum=1;
}
int pageSize=8;
log.debug("开始查询当前用户的问题");
//这里要处理个异常,因为用户可能没有登录
try{
PageInfo questions=
questionService.getMyQuestions(pageNum,pageSize);
return R.ok(questions);
}catch (ServiceException e){
log.error("用户查询问题失败!",e);
return R.failed(e);
}
}
}
5.重构index.js页面代码
let questionsApp = new Vue({
el:'#questionsApp',
data: {
questions:[],
pageInfo:{}
},
methods: {
loadQuestions:function (pageNum) {
if(!pageNum){ //如果pageNum为空,默认页码为1
pageNum=1;
}
$.ajax({
url: '/v1/questions/my',
method: "GET",
data:{pageNum:pageNum},
success: function (r) {
console.log("成功加载数据");
console.log(r);
if(r.code === OK){
questionsApp.questions = r.data.list;
//调用计算持续时间的方法
questionsApp.updateDuration();
//调用显示所有按标签呈现的图片
questionsApp.updateTagImage();
questionsApp.pageInfo=r.data;
}
}
});
},
//之后代码未修改,略
}
6.配置页面给定的分页导航条
实现翻页,配置页面给定的分页导航条
参考资料
PageInfo类中的常用属性
//当前页
private int pageNum;
//每页的数量
private int pageSize;
//当前页的行数量
private int size;
//当前页面第一个元素在数据库中的行号
private int startRow;
//当前页面最后一个元素在数据库中的行号
private int endRow;
//总页数
private int pages;
//前一页页号
private int prePage;
//下一页页号
private int nextPage;
//是否为第一页
private boolean isFirstPage;
//是否为最后一页
private boolean isLastPage;
//是否有前一页
private boolean hasPreviousPage;
//是否有下一页
private boolean hasNextPage;
//导航条中页码个数
private int navigatePages;
//所有导航条中显示的页号
private int[] navigatepageNums;
//导航条上的第一页页号
private int navigateFirstPage;
//导航条上的最后一页号
private int navigateLastPage;
学生提问
学员的问题发布功能
显示页面
将static/question/create.html
复制到
templates/question/create.html
并编写控制器代码显示这个页面
HomeController中代码如下
//显示学生问题发布页面
@GetMapping("/question/create.html")
public ModelAndView createQuestion(){
//templates/question/create.html
return new ModelAndView("question/create");
}
复用标签导航条(index.html中)
1.定义模板
在index.html页面中,将要复用的html区域用特定标签标记
th:fragment="xxx"
2.套用模板
现在是create.html需要复用代码,所以是这个页面套用模板
th:replace="xxx"来套用
保证页面支持th:的写法不报错
套用模板
th:replace="index::tags_nav"的意思是用index.html页面中名为tags_nav的模板中的代码替换掉当前编写套用标记的html标签
最后在代码临结束之前,引入ajax和Vue代码