稻草问答

稻草项目知识点总结

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}> { }

运行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表也是多对多的关系

稻草问答_第1张图片

我们在登录用户时需要指定用户的权限,根据用户的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);

}

image-20210929154145377

步骤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

image-20210929155433309

步骤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);
    }
}

稻草问答_第2张图片

### 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");
    }

方法说明:

  1. csrf().disable():关闭防跨域攻击功能,不关闭容易发生错误
  2. loginPage:指定登录页面路径
  3. loginProcessingUrl:指定表单提交的路径
  4. failureUrl:指定登录失败时的路径
  5. defaultSuccessUrl:指定登录成功时的路径
  6. logout():表示开始配置登出时的内容
  7. logoutUrl:指定出的路径(当页面有这个请求时,Spring-Security去执行用户登出操作)
  8. logoutSuccessUrl:指定登出成功之后显示的页面

Spring验证框架

1.介绍

Spring提供的对用户输入信息进行验证的框架组件

是服务器端验证技术

稻草问答_第3张图片

使用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);
        }
    }

稻草问答_第4张图片

VUE(基本使用)

1.介绍

也是一个js为基础的前端框架

提供了一套前端信息和服务器信息交互的一种方式

这种方式要比以前的信息交互方式简单

一般情况下,程序要结合JQuery的ajax操作和Vue的功能完成前后端信息交互

2.如何使用VUE

使用准备

Idea添加插件

稻草问答_第5张图片

编写html文件

static文件夹下创建一个测试Vue的页面vue.html

这个页面项目中不使用,就是测试用

        Title        

VUE演示

Vue功能的强大之处在于信息实时同步的双向绑定

稻草问答_第6张图片

使用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的声明式事务

什么是事务

事务是数据库管理系统执行过程的一个最小逻辑单位

转账操作对数据库的影响分两步:

  1. 转出账户金额减少
  2. 转入账户金额增加

如果转出账户成功,转入账户失败,那么转出账户金额的减少操作应该撤销

即这两个操作要么都执行要么多不执行

事务的出现就是为了保证数据完整性的

面试常见题:

数据库事务拥有的四个特性

简称ACID

  1. 原子性(Atomicity):事务是执行数据库操作的最小逻辑,不可再分
  2. 一致性(Consistency):事务中对数据库操作的命令状态应该是一致的,

    ​ 即要么都执行要么都不执行

  3. 隔离性(Isolation):一个事务的执行,不影响其他事务
  4. 持久性(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);
    }

}

说明

  1. @RestControllerAdvice表示对控制器方法的异常增强处理
  2. @ExceptionHandler表示这个方法是用来出处理异常的

控制器发生异常时,会自动匹配合适的异常类型,运行方法

稻草问答_第7张图片

文件上传(上载)

1.介绍

在Http协议的标准上,实现将客户端本地文件复制到服务器硬盘中的过程

http协议规定了一些上传文件时的标准

  1. 表单提交的方式必须是post
  2. 表单提交的编码方式必须修改为 multipart/form-data(二进制)
  3. 要上传的文件使用 来表示
  4. HTTP请求头中必须包含 Content-type: multipart/form-data, boundary=AaB03x;

    4中的描述了解即可

  5. 允许上传多个文件

2.文件上传流程

稻草问答_第8张图片

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
    



无需修改控制器代码

直接提交文件测试,成功即可

用户注册

每个网站都需要用户注册的功能

页面如下图

image-20210929114657672

  1. 获得邀请码(开发过程是从数据库获得,运营时向老师索取)
  2. 通过登录页上的注册连接显示注册页面
  3. 向服务器请求注册页并显示到浏览器
  4. 注册页面填写信息并提交表单
  5. 服务器接收到表单信息,控制层调用业务逻辑层执行注册操作
  6. 业务层执行连库操作新增之前验证邀请码
  7. 邀请码验证通过在执行数据库新增操作
  8. 返回新增操作的运行结果
  9. 根据结果反馈到控制层,有异常就报异常
  10. 控制器将注册结果信息使用JSON返回给浏览器
  11. 浏览器中局部刷新页面,将注册结果显示给用户

显示注册页面

步骤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.标签列表显示原理

稻草问答_第9张图片

在用户已经能够登录显示主页的前提下

  1. 主页页面中编写ajax向控制器发送请求所有标签
  2. 控制接到请求后调用业务逻辑层
  3. 业务逻辑层从tagMapper接口查询所有标签
  4. 业务逻辑层将查询到的信息返回给控制器
  5. 控制器获得所以标签返回JSON格式
  6. 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.了解开发流程

稻草问答_第10张图片

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格式信息

我的问答

已解决

eclipse 如何导入项目?

风继续吹 12浏览 13分钟前

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. 刚刚(1分钟之内)
  2. XX分钟前(60分钟以内)
  3. XX小时前(24小时以内)
  4. 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显示问题的标签列表

页面中的问题是可以多个标签的

稻草问答_第11张图片

怎么实现显示多个标签呢?

首先来了解一下标签和问题的对应关系

稻草问答_第12张图片

我们可以看到,在问题表中我们保持了冗余的数据列tag_names,这么做的好处就是减少查询时的复杂度,实际开发中程序员们也可能用这样的方式

实现过程

实现思路

1.创建一个包含全部标签的Map,map的key是标签名称,value是标签对象

2.从问题实体类中获得tag_names属性,利用字符串的split方法,拆分成字符串数组

3.遍历字符串数组,从Map中通过key(标签名称)获得value(标签对象)

4.将获取的value存入Question实体类中的Listtags属性

1.在Question实体类中需要定义一个List tags

原因是我们需要能够从一个问题中获得多个标签

    //为问题实体类添加标签集合
    //@TableField(exist = false)表示数据库中没有这样的列,防止报错
    @TableField(exist = false)
    private List tags;

稻草问答_第13张图片

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;
    }
}

稻草问答_第14张图片

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;
                }
            }
        },

稻草问答_第15张图片

在index.html文件中绑定

4.4实现分页功能

1.介绍

  1. 不会一次显示太多内容,不会产生大量流量,对服务器压力小
  2. 我们需要的信息,往往在前面几条的内容,后面的内容使用率不高
  3. 用户体验强,方便记忆位置

实现分页的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的使用

稻草问答_第16张图片

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}
 
        

稻草问答_第17张图片

子项目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);
    }

稻草问答_第18张图片

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);
        }
    }
}

稻草问答_第19张图片

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;
                    }
                }
            });
        },
       //之后代码未修改,略 
}        

稻草问答_第20张图片

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;

学生提问

稻草问答_第21张图片

学员的问题发布功能

稻草问答_第22张图片

显示页面

将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代码




富文本编辑器

1.介绍

富文本编辑器适用于那些需要格式甚至是图片的用户输入需求,这样的编辑器都是基于

最终通过编写js代码来开启这个编辑器的效果

一般这个代码在页面最后位置

多选下拉框

1.介绍

网页中的下拉列表框是一个能够多选,并且有选中后样式的功能的控件

这个控件是由Vue提供的插件Vue-Select实现的

官方网站是www.vue-select.org

依赖JQuery同时也依赖Vue核心的js

2.如何使用

引入一些依赖

下图表示他们的关联

稻草问答_第23张图片

在create.html的form表单中找到选择标签和老师的下拉框

将他们的代码修改为:

 

填写问题

createQuestion.js文件中的内容

动态加载所有标签和老师

动态加载所有标签的实现非常简单,因为我们直接可以调用现成的控制器方法

createQuestion.js中编写代码如下

动态加载所有老师

实现步骤

步骤1:

添加业务逻辑层接口方法IUserService

 //查询所有老师用户的方法    
 List getMasters();

步骤2:

实现这个业务逻辑层接口的方法UserServiceImpl

@Override
    public List getMasters() {
        QueryWrapper qw = new QueryWrapper<>();
        qw.eq("type",1);
        List list = userMapper.selectList(qw);
        return list;
    }

步骤3:

编写控制层:UserController中设计路径v1/users/master,返回R>即可

 @Autowired
    IUserService userService;

    @GetMapping("/master")
    public R> master(){
        List masters = userService.getMasters();
        return R.ok(masters);
    }

步骤4:

参照绑定所有标签的Vue代码绑定所有老师即可

loadTeachers:function(){
            $.ajax({
                url:"/v1/users/master",
                method:"get",
                success:function(r){
                    console.log(r);
                    if(r.code==OK){
                        let list=r.data;//获得所有讲师数组
                        let teachers=[];
                        for(let i=0;i

发布问题

我们先来完成数据提交到控制器的内容

1.新建Vo类

新建一个Vo类 QuestionVo

@Data
public class QuestionVo implements Serializable {
    @NotBlank(message = "标题不能为空")
    @Pattern(regexp = "^.{3,50}$",message = "标题长度在3~50个字符之间")
    private String title;

    private String[] tagNames={};
    private String[] teacherNickNames={};

    @NotBlank(message = "问题内容不能为空")
    private String content;
}

步骤2❓

下面我们需要完成新增问题的业务逻辑的开发

首先来了解一下我们需要什么操作才能完成这个业务

稻草问答_第24张图片

举例

image-20210930195127868

1.讲师的信息也是可以保存在换存中来避免多次访问数据库来提交运行效率的

所以我们参照对标签的处理方法,对所有讲师也进行缓存

IUserService中

 //查询所有老师用户的方法
    List getMasters();
 //查询所有老师用户的Map方法
    Map getMasterMap();

2.参照TagServiceImpl中对标签的缓存,处理讲师缓存

UserServiceImpl代码如下

  private final List masters=
            new CopyOnWriteArrayList<>();
    private final Map masterMap=
            new ConcurrentHashMap<>();
    private final Timer timer=new Timer();
    //初始化块:在构造方法运行前开始运行
    {
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                synchronized (masters){
                    masters.clear();
                    masterMap.clear();
                }
            }
        },1000*60*30,1000*60*30);
    }
 @Override
    public List getMasters() {
        if(masters.isEmpty()){
            synchronized (masters){
                if(masters.isEmpty()){
                    QueryWrapper query=new QueryWrapper<>();
                    query.eq("type",1);
                    //将所有老师缓存masters集合中
                    masters.addAll(userMapper.selectList(query));
                    for(User u: masters){
                        masterMap.put(u.getNickname(),u);
                    }
                    //脱敏:将敏感信息从数组(集合\map)中移除
                    for(User u: masters){
                        u.setPassword("");
                    }
                }
            }
        }
        return masters;
    }

    @Override
    public Map getMasterMap() {
        if(masterMap.isEmpty()){
            getMasters();
        }
        return masterMap;
    }

3.业务逻辑层Service

IQuestionService接口中发布问题的方法

//保存用户发布信息的方法
void saveQuestion(QuestionVo questionVo);

在QuestionServiceImpl类中实现接口中定义的方法

业务的步骤大概为

// 获取当前登录用户信息(可以验证登录情况)
// 将该问题包含的标签拼接成字符串以","分割 以便添加tag_names列
// 构造Question对象
// 新增Question对象
// 处理新增的Question和对应Tag的关系
// 处理新增的Question和对应User(老师)的关系

代码如下

 @Autowired
    QuestionTagMapper questionTagMapper;

    @Autowired
    UserQuestionMapper userQuestionMapper;

    @Override
    @Transactional
    public void saveQuestion(QuestionVo questionVo) {
        log.debug("收到问题数据{}",questionVo);
        // 获取当前登录用户信息(可以验证登录情况)
        String username=userService.currentUsername();
        User user=userMapper.findUserByUsername(username);
        // 将该问题包含的标签拼接成字符串以","分割 以便添加tag_names列
        StringBuilder bud=new StringBuilder();
        for(String tag : questionVo.getTagNames()){
            bud.append(tag).append(",");
        }
        //删除最后一个","
        bud.deleteCharAt(bud.length()-1);
        String tagNames=bud.toString();

        // 构造Question对象
        Question question=new Question()
                .setTitle(questionVo.getTitle())
                .setContent(questionVo.getContent())
                .setUserId(user.getId())
                .setUserNickName(user.getNickname())
                .setTagNames(tagNames)
                .setCreatetime(LocalDateTime.now())
                .setStatus(0)
                .setPageViews(0)
                .setPublicStatus(0)
                .setDeleteStatus(0);
        // 新增Question对象
        int num=questionMapper.insert(question);
        if(num!=1){
            throw  new ServiceException("服务器忙!");
        }
        log.debug("保存了对象:{}",question);
        // 处理新增的Question和对应Tag的关系
        Map name2TagMap=tagService.getName2TagMap();
        for(String tagName : questionVo.getTagNames()){
            //根据本次循环的标签名称获得对应的标签对象
            Tag tag=name2TagMap.get(tagName);
            //构建QuestionTag实体类对象
            QuestionTag questionTag=new QuestionTag()
                    .setQuestionId(question.getId())
                    .setTagId(tag.getId());
            //执行新增
            num=questionTagMapper.insert(questionTag);
            if(num!=1){
                throw new ServiceException("数据库忙!");
            }
            log.debug("新增了问题和标签的关系:{}",questionTag);
        }


        // 处理新增的Question和对应User(老师)的关系
        Map masterMap=userService.getMasterMap();
        for(String masterName : questionVo.getTeacherNickNames()){
            //根据本次循环的讲师名称获得对应的讲师对象
            User uu=masterMap.get(masterName);
            //构建QuestionTag实体类对象
            UserQuestion userQuestion=new UserQuestion()
                    .setQuestionId(question.getId())
                    .setUserId(uu.getId())
                    .setCreatetime(LocalDateTime.now());
            //执行新增
            num=userQuestionMapper.insert(userQuestion);
            if(num!=1){
                throw new ServiceException("数据库忙!");
            }
            log.debug("新增了问题和讲师的关系:{}",userQuestion);
        }
    }

4.控制层Controller

QuestionController代码如下

//学生发布问题的控制器方法
    @PostMapping
    public R createQuestion(
            @Validated QuestionVo questionVo,
            BindingResult result){
        if(result.hasErrors()){
            String message=result.getFieldError()
                    .getDefaultMessage();
            log.warn(message);
            return R.unproecsableEntity(message);
        }
        if(questionVo.getTagNames().length==0){
            log.warn("必须选择至少一个标签");
            return R.unproecsableEntity("必须选择至少一个标签");
        }
        if(questionVo.getTeacherNickNames().length==0){
            log.warn("必须选择至少一个老师");
            return R.unproecsableEntity("必须选择至少一个老师");
        }
        //这里应该将vo对象交由service层去新增
        log.debug("接收到表单数据{}",questionVo);
        try {
            questionService.saveQuestion(questionVo);
            return R.ok("发布成功!");
        }catch (ServiceException e){
            log.error("发布失败",e);
            return R.failed(e);
        }
    }

5.页面和JS代码

找到create.html的form标签

使用v-on:submit.prevent绑定提交事件 .prevent是阻止表单提交用的

在createQuestion.js

文件中新增createQuestion方法

并在方法中收集要提交的信息,最后使用ajax提交到控制器

 createQuestion:function(){
            let content=$("#summernote").val();
            console.log(content);
            //定义一个data对象,用于ajax提交信息到控制器
            let data={
                title:this.title,
                tagNames:this.selectedTags,
                teacherNickNames:this.selectedTeachers,
                content:content
            }
            console.log(data);
            $.ajax({
                url:"/v1/questions",
                traditional:true,//使用传统数组的编码方式,SpringMvc才能接收
                method:"post",
                data:data,
                success:function(r){
                    console.log(r)
                    if(r.code== OK){
                        console.log(r.message);
                    }else{
                        console.log(r.message);
                    }
                }
            });
        }

富文本编辑器中文件的上传

1.搭建静态资源服务器

使用SpringBoot聚合项目的子项目完成搭建任务即可

1.创建straw-resource子项目

创建过程中不需要选中任何依赖

2.父子相认

3.子项目pom.xml 添加依赖

 
            org.springframework.boot
            spring-boot-starter-web
        

4.创建一个文件夹作为图片服务器的资源路径D:/resource

在这个文件夹中适当保存若干测试图片

在straw-resource项目的application.properties文件中配置如下

server.port=8899
spring.web.resources.static-locations=file:D:/resource

5.测试http://localhost:8899/1.jpg能够看到保存的图片即可,其中1.jpg是我们创建的resource文件夹中真是存在的文件名

2.将图片上传到静态资源服务器

1.我们需要将静态资源服务器的ip地址和端口号以及存放文件的路径名保存到当前配置文件中

straw-portal项目的application.properties配置添加代码如下

straw.resource.path=file:D:/resource
straw.resource.host=http://localhost:8899

2.重构SystemController中上传文件的代码

//下面两个属性值来自application.properties配置文件
    @Value("${straw.resource.path}")
    private File resourcePath;
    @Value("${straw.resource.host}")
    private String resourceHost;

    //接收表单上传的文件
    @PostMapping("/upload/file")
    public R upload(MultipartFile imageFile) throws IOException {
        /*
            我们需要保证任何用户上传的文件的文件名都不能重复
            我们为了尽量避免文件名的重复,采用以下策略
            1.将原有文件名修改为使用UUID生成的字符串
            2.不同的日期创建不同的文件夹
            3.保留文件的扩展名,还能方便文件识别
         */
        //按照当前日期创建文件夹
        String path= DateTimeFormatter.ofPattern("yyyy/MM/dd")
                .format(LocalDate.now());
        //path="2020/12/16"
        File folder=new File(resourcePath,path);
        //folder->F:/resource/2020/12/16
        folder.mkdirs();//创建一串文件夹带s的!!!!
        log.debug("上传的文件夹为:{}",folder.getAbsolutePath());
        //按照上传文件的原始文件名,保留扩展名xx.xx.jpg
        //                              012345678
        String fileName=imageFile.getOriginalFilename();
        String ext=fileName.substring(fileName.lastIndexOf("."));
        //使用UUID生成文件名
        String name= UUID.randomUUID().toString()+ext;
        log.debug("生成的文件名:{}",name);
        //F:/resource/2020/12/16/uuid.jpg
        File file=new File(folder,name);
        //向硬盘写入文件
        imageFile.transferTo(file);
        //直接返回路径方便调用测试
        String url=resourceHost+"/"+path+"/"+name;
        log.debug("访问这个文件的路径为:{}",url);
        return R.ok(url);
    }

如果希望我们上传文件之后能立即显示在页面上

需要在页面上定一个img标签

并在ajax接收到上传图片的url后,将这个ur赋值给img标签的src属性

upload.html代码如下




    
    Title
    



    

3.将富文本编辑器中用户选择的文件上传

上传流程

稻草问答_第25张图片

重构create.html中的代码

代码如下










显示问题状态

index.html中通过v-show来控制span的显示或隐藏即可

已解决

用户信息面板

稻草问答_第26张图片

index.html以及很多页面上都有这个用户信息面板

1.新建一个UserVo类

@Data
//支持连缀书写
@Accessors(chain = true)
public class UserVo {
    private Integer id;
    private String username;
    private String nickname;

    //两个面板中显示的数据
    //问题数量
    private int questions;
    //收藏数量
    private int collections;
}

2.数据访问层

查询这个用户开始

UserMapper接口中添加一个查询User基本信息的方法

@Select("select id,username,nickname from user " +
            " where username=#{username}")
    UserVo findUserVoByUsername(String username);

在IQuestionService中编写一个根据用户id获得问题数量的方法

代码如下

Integer countQuestionsByUserId(Integer userId);

在QuestionServiceImpl类中实现这个方法

 @Override
    public Integer countQuestionsByUserId(Integer userId) {
        QueryWrapper query=new QueryWrapper<>();
        query.eq("user_id",userId);
        query.eq("delete_status",0);
        Integer count=questionMapper.selectCount(query);
        return count;
    }

上面查询的问题数量,实际上是为了让UserVo获得信息的准备工作

而UserVo的创建赋值需要在UserService中

所以完成IUserService接口中方法的编写代码如下

  //查询当前登录用户信息面板的方法
  UserVo currentUserVo();

这个接口方法的实现

UserServiceImpl类代码如下

    @Autowired
    IQuestionService questionService;
    @Override
    public UserVo currentUserVo() {    
        String username=currentUsername();
           UserVo user=userMapper.findUserVoByUsername(username);
        Integer questions=questionService
                .countQuestionsByUserId(user.getId());
        user.setQuestions(questions);    
        return user;
    }

3.控制层

UserController类中调用代码如下

//显示用户信息面板的控制层方法
    @GetMapping("/me")
    public R me(){
        UserVo user=userService.currentUserVo();
        return R.ok(user);
    }

4.页面和JS代码

首先编写index.html页面中vue信息的绑定


      





在static文件夹中的js文件夹中创建user_info.js文件来绑定html文件中的内容

user_info.js代码如下

let userApp = new Vue({
    el: "#userApp",
    data: {
        user: {}
    },
    methods: {
        loadCurrentUser: function () {
            $.ajax({
                url: "/v1/users/me",
                method: "get",
                success: function (r) {
                    console.log(r)
                    if (r.code==OK) {
                        userApp.user=r.data;
                    }else{
                        console.log(r.message);
                    }
                }
            });
        }
    },
    created: function () {
        //页面加载完毕后立即调用loadCurrentUser方法
        this.loadCurrentUser();
    }
});

完成用户信息面板的复用

步骤1:

将我们刚刚编写的index.html页面的用户信息面板的div设置为TH模板

步骤2:

在create.html文件中找到对应的位置,套用模板

步骤3:

create.html文件末尾引入js的支持






讲师回复首页

按登录用户的身份显示不同的主页(讲师首页)

1.数据访问层

在UserMapper中添加按用户id查询这个用户的所有身份的方法以便保存到Spring-Security中

//按用户id查询用户的所有角色
    @Select("select r.id,r.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 " +
            "where u.id=#{userId}")
    List findUserRolesById(Integer id);

测试

@Autowired
    UserMapper userMapper;
    @Test
    public void roles(){
        List list=userMapper.findUserRolesById(1);
        for(Role role:list){
            System.out.println(role);
        }
    }

2.业务逻辑层

在UserServiceImpl类中重构登录用户的权限,添加角色信息

UserServiceImpl的getUserDetails方法

@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 roles=userMapper.findUserRolesById(user.getId());
        int j=auths.length;
        //扩容上面的数组
        auths= Arrays.copyOf(auths,
                auths.length+roles.size());
        //向数组内容中赋值
        for(Role r:roles){
            auths[j]=r.getName();
            j++;
        }

        //创建UserDetails对象,并为他赋值
        UserDetails ud= org.springframework.security.core.userdetails
                .User.builder()
                .username(user.getUsername())
                .password(user.getPassword())
                .accountLocked(user.getLocked()==1)//写==1是判断锁定
                .disabled(user.getEnabled()==0)//写==0是判断不可用
                .authorities(auths).build();
        //最后返回UserDetails对象
        return ud;
    }

运行完上面的代码

Spring-Security的权限管理的字符串包含了权限相关的内容和角色相关的内容

以一个学生登录为例

这个数组的内容是:auths={"/index.html","/question/create","/question/uploadMultipleFile","/question/detail","ROLE_STUDENT"}

3.控制层

HomeController类index方法代码如下

//声明两个常亮以便判断用户的角色
    static final GrantedAuthority STUDENT =
            new SimpleGrantedAuthority("ROLE_STUDENT");
    static final GrantedAuthority TEACHER =
            new SimpleGrantedAuthority("ROLE_TEACHER");

    //显示首页
    @GetMapping("/index.html")
    //@AuthenticationPrincipal 注解后面跟Spring-Security的User类型参数
    //表示需要Spring-Security将当前登录用户的权限信息赋值给User对象
    //以便我们在方法中验证他的权限或身份
    public ModelAndView index(
            @AuthenticationPrincipal User user){
        //  根据Spring-Security提供的用户判断权限,绝对返回哪个页面
        if(user.getAuthorities().contains(STUDENT)){
            return  new ModelAndView("index");
        }else if(user.getAuthorities().contains(TEACHER)){
            return new ModelAndView("index_teacher");
        }
        return null;
    }

4.页面

在index_teacher.html页面中做一些修改

首先Th的命名空间

使用模板替换导航栏


使用模板替换用户信息面板


引入依赖的js文件





显示讲师问题列表

1.数据访问层

QuestionMapper中添加新的方法代码如下

@Repository
public interface QuestionMapper extends BaseMapper {
    @Select("SELECT q.* " +
            " FROM question q" +
            " LEFT JOIN user_question uq " +
            "      ON q.id=uq.question_id" +
            " WHERE uq.user_id=#{userId} OR q.user_id=#{userId}" +
            " ORDER BY q.createtime DESC")
    List findTeachersQuestions(Integer userId);
}

可以测试一下

@Autowired
    QuestionMapper questionMapper;
    @Test
    public void teacherQuestions(){
        List list=
                questionMapper.findTeachersQuestions(3);
        for(Question question:list){
            System.out.println(question);
        }
    }

2.业务逻辑层

IQuestionService

//分页查询当前登录的老师问题的方法
    PageInfo getQuestionsByTeacherName(
            String username,Integer pageNum,Integer pageSize
    );

QuestionServiceImpl

 @Override
    public PageInfo getQuestionsByTeacherName(
            String username, Integer pageNum, Integer pageSize) {
        if(pageNum == null)
            pageNum=1;
        if(pageSize == null)
            pageSize=8;

        //根据用户名查询用户对象
        User user=userMapper.findUserByUsername(username);
        //设置分页查询
        PageHelper.startPage(pageNum,pageSize);
        List questions=
                questionMapper.findTeachersQuestions(user.getId());
        //别忘了,要将问题列中的标签字符串转成标签的List
        for(Question q: questions){
            List tags=tagNamesToTags(q.getTagNames());
            q.setTags(tags);
        }
        return new PageInfo(questions);
    }

3.控制层

QuestionController

     @GetMapping("/teacher")
    @PreAuthorize("hasRole('ROLE_TEACHER')")
    public R> teachers(
            //声明权限是为了获得用户名的
            @AuthenticationPrincipal User user,
            Integer pageNum){
        if(pageNum == null)
            pageNum = 1;
        Integer pageSize=8;
        //调用业务逻辑层的方法
        PageInfo pageInfo=questionService
                .getQuestionsByTeacherName(
                     user.getUsername(),pageNum,pageSize
                );
        return R.ok(pageInfo);
    }

4.页面和js

index_teacher.html页面也可以使用th模板来复用问题列表

定义模板

在index.html页面中找到定义id为QuestionApp的div

修改代码如下

套用模板

在index_teacher.html页面中找到对应的div编写复用代码

最后完成js文件的编写

可以复制index.js修改名称为index_teacher.js

修改ajax的调用路径即可

 $.ajax({
                url: '/v1/questions/teacher',
                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;
                    }
                }
            });

最后引用js依赖






问题详情页面

流程介绍

  1. 显示页面
  2. 异步查询本问题的详细信息显示在当前问题区域
  3. 异步完成讲师回答问题添加到数据的功能
  4. 异步查询当前问题的所有回答
  5. 异步添加指定回答的评论
  6. 异步查询所有回答的评论
  7. 异步实现评论的修改和删除

显示问题详情页

1.显示页面

1.复制static/question/detail.html文件到templates/question/detail.html

2.在HomeController中编写显示这个页面的方法

 //显示问题详情页面
    @GetMapping("/question/detail.html")
    public ModelAndView detail(){
        return new ModelAndView("question/detail");
    }

3.我们现在没有办法知道用户请求的问题的具体id

我们可以采用将问题的id保存在url中的方法来实现页面显示之后使用异步查询问题详情

这个id的保存实现在index.html页面

因为index_teacher.html也是复用index.html页面内容的

index.html标题连接的a标签代码修改为


       eclipse 如何导入项目?

2.业务逻辑层

按id查询Question的方法是有MybatisPlus直接提供的,所以Mapper不用写

IQuestionService接口添加方法

//按id查询问题详情的方法
    Question getQuestionById(Integer id);

QuestionServiceImpl类实现如下

@Override
    public Question getQuestionById(Integer id) {
        //先按id查询出Question
        Question question=questionMapper.selectById(id);
        //再按Question的tag_names列的标签转换为List
        List tags=tagNamesToTags(question.getTagNames());
        //将转换完成的List保存到这个Question的tags属性中
        question.setTags(tags);
        return question;
    }

2.控制层

QuestionController

    //显示问题详细的Controller方法
    //为了遵守RESTful的风格这个位置的路径比较特殊
    //例如:/v1/questions/12
    //上面的路径SpringMvc会自动将12赋值给{id}
    //@PathVariable标记的同名属性的值也会是12
    @GetMapping("/{id}")
    public R question(
            @PathVariable Integer id){
        //判断必须要有id
        if(id==null){
            return R.invalidRequest("ID不能为空");
        }
        Question question=questionService.getQuestionById(id);
        return R.ok(question);

    }

3.页面和js

编写detail.html代码

主要在当前问题显示详情的为位置


收藏

1

浏览

100

Java中方法重载和重写的区别

请问的方法中重写和重载的区别都是什么,如何使用

张三 3天前




创建question_detail.js文件

let questionApp = new Vue({
    el:"#questionApp",
    data:{
        question:{}
    },
    methods:{
        loadQuestion:function(){
            //获取浏览器地址栏中当前url中?之后的内容
            let questionId=location.search;
            console.log("questionId:"+questionId);
            //判断是不是获得了?之后的内容
            if(!questionId){
                //如果没有?则终止
                alert("必须指定问题id");
                return;
            }
            //如果存在?之后的内容,则去掉?    ?354
            questionId=questionId.substring(1);
            //发送异步请求
            $.ajax({
                url:"/v1/questions/"+questionId,//v1/questions/15
                method:"get",
                success:function(r){
                    console.log(r);
                    if(r.code == OK){
                        questionApp.question=r.data;
                        questionApp.updateDuration();
                    }else{
                        alert(r.message);
                    }
                }
            })
        },
        updateDuration:function(){
            //获得问题中的创建时间属性(毫秒数)
            let createtime=new Date(this.question.createtime).getTime();
            //获得当前时间的毫秒数
            let now=new Date().getTime();
            //计算时间差(秒)
            let durtaion=(now-createtime)/1000;
            if(durtaion<60){
                // 显示刚刚
                //duration这个名字可以随便起,只要保证和页面上取的一样就行
                this.question.duration="刚刚";
            }else if(durtaion<60*60){
                // 显示XX分钟
                this.question.duration=
                    (durtaion/60).toFixed(0)+"分钟前";
            }else if (durtaion<60*60*24){
                //显示XX小时
                this.question.duration=
                    (durtaion/60/60).toFixed(0)+"小时前";
            }else{
                //显示XX天
                this.question.duration=
                    (durtaion/60/60/24).toFixed(0)+"天前";
            }
        }
    },
    created:function(){
        this.loadQuestion();
    }
});

4.问题详情页分离

1.赋值当前的detail.html页面,名为detail_teacher.html

2.在detail.html页面代码中删除学生不能执行的操作

稻草问答_第27张图片

稻草问答_第28张图片

3.js代码

将老师和学生业务逻辑不同的js文件分离

只需要将question.detail.js文件中,postAnswer模块的代码分离出来,复制到一个新建的js文件:post_answer.js中

老师的detail_teacher.html中引入question.detail.js和post_answer.js

而学生的detail.html只引入question.detail.js

4.控制层

在HomeController中跳转到detail.html的方法

 //显示问题详情页面
    @GetMapping("/question/detail.html")
    public ModelAndView detail(
            @AuthenticationPrincipal User user){
        if(user.getAuthorities().contains(STUDENT)){
            //如果是学生,跳detail.html
            return new ModelAndView("question/detail");
        }else if(user.getAuthorities().contains(TEACHER)){
            //如果是老师,跳detail_teacher.html
            return new ModelAndView(
                    "question/detail_teacher");
        }
        return null;
    }

讲师回复问题

1.编写值对象类

在vo包中创建AnswerVo类

@Data
@Accessors(chain = true)
public class AnswerVo implements Serializable {
    @NotNull(message = "问题编号不能为空")
    private Integer questionId;

    @NotBlank(message = "回复内容不能为空")
    private String content;
}

2.业务逻辑层

IAnswerService接口中添加方法

public interface IAnswerService extends IService {
    //提交讲师回复问题的答案信息
    Answer saveAnswer(AnswerVo answerVo,String username);
}

AnswerServiceImpl类中编写代码如下

@Service
@Slf4j
public class AnswerServiceImpl extends ServiceImpl implements IAnswerService {

    @Resource
    private AnswerMapper answerMapper;

    @Resource
    private UserMapper userMapper;

    @Override
    @Transactional
    public Answer saveAnswer(AnswerVo answerVo, String username) {
        //收集信息,先获得当前回答问题的讲师的用户信息,结合answerVo
        User user=userMapper.findUserByUsername(username);
        Answer answer=new Answer()
                .setUserId(user.getId())
                .setUserNickName(user.getNickname())
                .setContent(answerVo.getContent())
                .setQuestId(answerVo.getQuestionId())
                .setLikeCount(0)
                .setAcceptStatus(0)
                .setCreatetime(LocalDateTime.now());
        int rows=answerMapper.insert(answer);
        if(rows!=1){
            throw new ServiceException("数据库忙!");
        }
        return answer;
    }
}

3.控制层

重构AnswerController类

//新增回复的控制方法
    @PostMapping("")
    @PreAuthorize("hasRole('ROLE_TEACHER')")
    public R postAnswer(
            @Validated AnswerVo answerVo,
            BindingResult result,
            @AuthenticationPrincipal User user){
        log.debug("收到回复信息{}",answerVo);
        if(result.hasErrors()){
            String message=result.getFieldError().getDefaultMessage();
            log.warn(message);
            return  R.unproecsableEntity(message);
        }
        //这里调用业务逻辑层方法
        answerService.saveAnswer(answerVo,user.getUsername());
        return R.created("回复问题成功!");

    }

3.页面和js

编写detail.html页面的代码

主要针对富文本编辑器提交的表单范围

写答案
回答内容不能为空!

为了完成讲师回复时选择的图片上载,和讲师回复的提交操作

在question_detail.js文件中添加如下代码

$(function(){
    $('#summernote').summernote({
        height: 300,
        lang: 'zh-CN',
        placeholder: '请输入问题的详细描述...',
        callbacks:{
            //在执行指定操作后自动调用下面的方法
            //onImageUpload方法就会在用户选中图片之后立即运行
            onImageUpload:function(files) {
                //参数是一个file数组取出第一个,因为我们只会选中一个
                let file =files[0];
                //构建表单
                let form=new FormData();
                form.append("imageFile",file);
                $.ajax({
                    url:"/upload/file",
                    method:"post",
                    data:form,//发送的是我们构建的表单中的数据
                    //下面有两个特殊参数,需要在文件上传时设置
                    cache:false,
                    contentType:false,
                    processData:false,
                    success:function(r){
                        if(r.code==OK){
                            console.log(r);
                            //将刚刚上传成功的图片显示在summernote富文本编辑器中
                            var img=new Image();//实例化了一个img标签
                            img.src=r.message;//将img标签的src属性赋值为刚上传的图片
                            //summernote方法中提供了插入标签的功能
                            //支持使用"insertNode"表示要向富文本编辑器中添加标签内容
                            $("#summernote").summernote(
                                "insertNode",img
                            )
                        }else{
                            alert(r.message);
                        }
                    }
                });
            }
        }
    });
})

let postAnswerApp=new Vue({
    el:"#postAnswerApp",
    data:{
        message:"",
        hasError:false
    },
    methods:{
        postAnswer:function(){
            postAnswerApp.hasError=false;
            let questionId=location.search;
            if(!questionId){
                this.message="没有问题ID";
                this.hasError=true;
                return;
            }
            //去掉?
            questionId=questionId.substring(1);
            let content=$("#summernote").val();
            if(!content){
                this.message="请填写回复内容";
                this.hasError=true;
                return;
            }
            let data={
                questionId:questionId,
                content:content
            }
            $.ajax({
                url:"/v1/answers",
                method:"post",
                data:data,
                success:function(r){
                    if(r.code==CREATED){
                        postAnswerApp.message=r.message;
                        postAnswerApp.hasError=true;
                    }else{
                        postAnswerApp.message=r.message;
                        postAnswerApp.hasError=true;
                    }
                }
            })
        }
    }
})

显示回答列表

1.业务逻辑层

IAnswerService添加接口方法

 //根据问题id查询这个问题的所有回答的方法
    List getAnswersByQuestionId(Integer questionId);

AnswerServiceImpl类中

@Override
    public List getAnswersByQuestionId(Integer questionId) {
        if(questionId==null){
            throw ServiceException.invalidRequest("问题id不能为空");
        }
        QueryWrapper query=new QueryWrapper<>();
        query.eq("quest_id",questionId);
        query.orderByDesc("createtime");
        List answers=answerMapper.selectList(query);
        return answers;
    }

2.控制层

AnswerController中

//根据问题id获得这个问题的所有回答的方法
    // 例如:/v1/answers/question/12
    @GetMapping("/question/{id}")
    public R> questionAnswers(
            @PathVariable Integer id){
        if(id==null){
            return R.invalidRequest("问题ID不能为空!");
        }
        List answers=answerService
                .getAnswersByQuestionId(id);
        return R.ok(answers);
    }

3.页面和js

改写detail.html页面,使用Vue绑定回答相关信息

3条回答

张三
2天前
方法的重载是overloading,方法名相同,参数的类型或个数不同,对权限没有要求 方法的重写是overrding 方法名称和参数列表,参数类型,返回值类型全部相同,但是所实现的内容可以不同,一般发生在继承中

编写js文件

question_detail.js文件中再添加vue方法

let answersApp=new Vue({
    el:"#answersApp",
    data:{
        message:"",
        hasError:false,
        answers:[]
    },
    methods:{
        loadAnswers:function(){
            let questionId=location.search;
            if(!questionId){
                this.message="必须有问题ID";
                this.hasError=true;
                return;
            }
            questionId=questionId.substring(1);
            $.ajax({
                url:"/v1/answers/question/"+questionId,
                method:"get",
                success:function(r){
                    if(r.code==OK){
                        answersApp.answers=r.data;
                    }else{
                        answersApp.message=r.message;
                        answersApp.hasError=true;
                    }
                }
            })
        }
    },
    created:function(){
        this.loadAnswers();
    }
})

4.优化

1.重构updateDuration方法

在utils.js文件中添加通用的计算持续时间的方法

function addDuration(item){
    //判断参数状态
    if(item==null || item.createtime==null){
        return;
    }
    //获得问题中的创建时间属性(毫秒数)
    let createtime=new Date(item.createtime).getTime();
    //获得当前时间的毫秒数
    let now=new Date().getTime();
    //计算时间差(秒)
    let durtaion=(now-createtime)/1000;
    if(durtaion<60){
        // 显示刚刚
        //duration这个名字可以随便起,只要保证和页面上取的一样就行
        item.duration="刚刚";
    }else if(durtaion<60*60){
        // 显示XX分钟
        item.duration=
            (durtaion/60).toFixed(0)+"分钟前";
    }else if (durtaion<60*60*24){
        //显示XX小时
        item.duration=
            (durtaion/60/60).toFixed(0)+"小时前";
    }else{
        //显示XX天
        item.duration=
            (durtaion/60/60/24).toFixed(0)+"天前";
    }
}

在question_detail.js文件中

            $.ajax({
                url:"/v1/questions/"+questionId,//v1/questions/15
                method:"get",
                success:function(r){
                    console.log(r);
                    if(r.code == OK){
                        questionApp.question=r.data;
                        addDuration(questionApp.question);//修改了这个里!!!!
                    }else{
                        alert(r.message);
                    }
                }
            })
        }
    },

修改index.js和index_teacher.js中updateDuration方法的的内容

代码如下

 updateDuration:function () {
            let questions=this.questions;
            for(let i=0;i

编写回答列表中老师回答问题的持续时间

代码如下

let answersApp=new Vue({
    el:"#answersApp",
    data:{
        message:"",
        hasError:false,
        answers:[]
    },
    methods:{
        loadAnswers:function(){
            let questionId=location.search;
            if(!questionId){
                this.message="必须有问题ID";
                this.hasError=true;
                return;
            }
            questionId=questionId.substring(1);
            $.ajax({
                url:"/v1/answers/question/"+questionId,
                method:"get",
                success:function(r){
                    if(r.code==OK){
                        answersApp.answers=r.data;
                        answersApp.updateDuration();
                    }else{
                        answersApp.message=r.message;
                        answersApp.hasError=true;
                    }
                }
            })
        },
        updateDuration:function(){
            for(let i=0;i

将新回复的答案插入答案列表

在R类中补全一个方法

这个方法是在执行新增操作时,不但返回新增结果信息,还返回新增对象的方法

R类中添加方法如下

/**
     * 新增成功,并且需要获得新增成功对象时使用这个方法
     */
    public static R created(Object data){
        return new R().setCode(CREATED).setMessage("创建成功")
                .setData(data);
    }

重构AnswerController中的postAnswer方法

//新增回复的控制方法
    @PostMapping("")
    @PreAuthorize("hasRole('ROLE_TEACHER')")
    public R postAnswer(
            @Validated AnswerVo answerVo,
            BindingResult result,
            @AuthenticationPrincipal User user){
        log.debug("收到回复信息{}",answerVo);
        if(result.hasErrors()){
            String message=result.getFieldError().getDefaultMessage();
            log.warn(message);
            return  R.unproecsableEntity(message);
        }
        //这里调用业务逻辑层方法
        Answer answer=
            answerService.saveAnswer(answerVo,user.getUsername());
        return R.created(answer);
    }

重构question_detail.js文件中的postAnswerApp区域的方法

代码如下

let postAnswerApp=new Vue({
    el:"#postAnswerApp",
    data:{
        message:"",
        hasError:false
    },
    methods:{
        postAnswer:function(){
            postAnswerApp.hasError=false;
            let questionId=location.search;
            if(!questionId){
                this.message="没有问题ID";
                this.hasError=true;
                return;
            }
            //去掉?
            questionId=questionId.substring(1);
            let content=$("#summernote").val();
            if(!content){
                this.message="请填写回复内容";
                this.hasError=true;
                return;
            }
            let data={
                questionId:questionId,
                content:content
            }
            $.ajax({
                url:"/v1/answers",
                method:"post",
                data:data,
                success:function(r){
                    if(r.code==CREATED){
                        let answer=r.data;//这个r.data就是新增的回答
                        //将这个问题的持续时间计算出来
                        addDuration(answer);
                        //将新增的方法插入到anwsers数组的后面
                        answersApp.answers.push(answer);
                        //回答已经显示,清空富文本编辑器中的内容
                        $("#summernote").summernote("reset");
                        postAnswerApp.message=r.message;
                        postAnswerApp.hasError=true;
                        //2秒中之后信息消失
                        setTimeout(function(){
                            postAnswerApp.hasError=false;
                        },2000);
                    }else{
                        postAnswerApp.message=r.message;
                        postAnswerApp.hasError=true;
                    }
                }
            })
        }
    }
})

评论答案的功能

再次强调<问题>、<回答>、<评论>的关系

image-20210906154638634

question对answer是一对多

answer对comment又是一对多

要想查询一个问题的所有评论,是需要连表查询的

显示评论

显示评论信息需要昵称和评论内容

但是数据表中没有昵称列,这样我们就需要连表查询才能得到

为了简化这样的操作,我们可以添加一个冗余列信息:user_nick_name

保存用户昵称,避免过多的连表操作

怎么样给当前的表添加一个列呢?

1.数据库

为comment表添加user_nick_name列,简化查询

代码如下

ALTER TABLE `comment` ADD COLUMN user_nick_name 
VARCHAR(255) AFTER user_id

UPDATE `comment` c SET user_nick_name =
(SELECT nickname FROM `user` u WHERE u.id=c.user_id )

步骤2:

我们在数据库中添加了昵称

那么也许要在实体类中添加对应的属性

找到model.Comment类,添加新增的列的属性

    /**
     * 用户昵称
     */
    @TableField("user_nick_name")
    private String userNickName;

步骤3:

开始执行查询操作的准备工作

每个回答包含多个评论,所以我们先要在Answer实体类中添加一个Comment类型的集合

model.Answer实体类添加代码如下

    /**
     * 当前回答的所有评论
     */
    @TableField(exist = false)
    private List comments=new ArrayList<>();

2.数据访问层-复杂查询

1.首先在AnswerMapper接口中编写下面的方法

@Repository
    public interface AnswerMapper extends BaseMapper {
        //复杂映射查询按问题id获得所有回答以及每个回答包含的评论
        List findAnswersByQuestionId(Integer questionId);
        
    }

2.编写执行复杂映射查询的xml文件

SpringBoot约定这些xml文件的位置必须在resources/mapper下

编写AnswerMapper.xml文件,在文件中编写代码如下





    
    
        
        
        
        
        
        
        
        
        
            
            
            
            
            
            
        
    
    

3.业务逻辑层-重构

IAnswerService接口中是有一个按问题id查询问题的所有会的的方法的,所以直接去重构这个方法

AnswerServiceImpl类中getAnswersByQuestionId方法代码如下

@Override
    public List getAnswersByQuestionId(Integer questionId) {
        if(questionId==null){
            throw ServiceException.invalidRequest("问题id不能为空");
        }
        List answers=answerMapper.findAnswersByQuestionId(questionId);
        return answers;
    }

4.控制层 页面 和 js

已经完成过控制器调用按id查询回答的方法

现在直接到页面去修改vue的绑定即可

  1条评论

  • ...
    李四:

    明白了,谢谢老师!

    内容不能为空!

添加评论

开发思路

1.创建CommentVo

2.编写控制器代码

3.页面表单的绑定

4.编写js文件代码

5.业务逻辑层和添加操作

1.新建CommentVo类来准备新增评论

@Data
@Accessors(chain=true)
public class CommentVo implements Serializable {

    @NotNull(message = "问题ID不能为空")
    private Integer answerId;

    @NotBlank(message = "评论内容不能为空")
    private String content;
}

2.业务逻辑层

先编写ICommentService

添加方法如下

public interface ICommentService extends IService {

    Comment saveComment(CommentVo commentVo,String username);

}

准备一个服务器忙的异常方法方便调用

ServiceException类中添加方法

//返回服务器忙的异常
    public static ServiceException busy(){
        return new ServiceException("数据库忙",R.INTERNAL_SERVER_ERROR);
    }

再编写CommentServiceImpl

@Service
@Slf4j
public class CommentServiceImpl extends ServiceImpl implements ICommentService {

    @Resource
    private UserMapper userMapper;
    @Resource
    private CommentMapper commentMapper;

    @Override
    public Comment saveComment(CommentVo commentVo, String username) {
        //获得当前登录用户信息
        User user=userMapper.findUserByUsername(username);
        //构建要新增的评论对象
        Comment comment=new Comment()
                .setUserId(user.getId())
                .setUserNickName(user.getNickname())
                .setAnswerId(commentVo.getAnswerId())
                .setContent(commentVo.getContent())
                .setCreatetime(LocalDateTime.now());
        int num=commentMapper.insert(comment);
        if(num!=1){
            throw ServiceException.busy();
        }
        return comment;
    }
}

3.控制层代码

 @PostMapping
    public R postComment(
            @Validated CommentVo commentVo,
            BindingResult result,
            @AuthenticationPrincipal UserDetails userDetails){
        if(result.hasErrors()){
            String message=result.getFieldError().getDefaultMessage();
            return R.unproecsableEntity(message);
        }
        log.debug("收到评论信息{}:",commentVo);
        //这里调用业务逻辑层方法执行评论的新增即可
        Comment comment=commentService.saveComment(
                commentVo,userDetails.getUsername());
        return R.created(comment);

    }

4.页面和js

1.表单绑定

现在页面上的添加评论的按钮会引发所有回答的添加评论的输入框展开,这是不合理的

需要绑定id分别展开控制

detail.html文件中"采纳答案"附近代码修改为:

采纳答案

评论内容不能为空!

2.开发js代码

评论的新增无需自己单独编写一个vue模块,直接借助已经编写好的answersApp模块即可

answersApp模块中添加一个方法postComment即可

代码如下

postComment:function(answerId){
            //现在我们需要获得回答id和评论内容,以新增评论
            let content=$("#addComment"+answerId+" textarea").val()
            if(!content){
                console.log("评论内容不能为空");
                return;
            }
            let data={
                answerId:answerId,
                content:content
            }
            $.ajax({
                url:"/v1/comments",
                method:"post",
                data: data,
                success:function(r){
                    console.log(r);
                    if(r.code==CREATED){
                        alert(r.message);
                    }else{
                        alert(r.message);
                    }
                }
            })
        }

新增评论成功后,立即显示在页面上

只需要修改js代码postComment方法中ajax操作成功的代码即可

            $.ajax({
                url:"/v1/comments",
                method:"post",
                data: data,
                success:function(r){
                    console.log(r);
                    if(r.code==CREATED){
                        //清空textarea的内容
                        $("#addComment"+answerId+" textarea").val("");
                        //获得新增的评论
                        let comment=r.data;
                        //获得当前所有的回答
                        let answers=answersApp.answers;
                        //遍历所有回答
                        for(let i=0;i

删除评论

1.老师可以删除任何人的评论

2.评论的发布者可以删除自己的评论

1.业务逻辑层

ICommentService接口

 // 删除评论
    boolean removeComment(Integer commentId,String username);

CommentServiceImpl类实现

@Override
    public boolean removeComment(Integer commentId, String username) {
        User user=userMapper.findUserByUsername(username);
        //判断身份
        if(user.getType()==1){
            //如果是老师,可以删除
            int num=commentMapper.deleteById(commentId);
            return num == 1;
        }
        //不是老师要删除评论,要判断这个评论是不是当前登录用户发布的
        //那么就获得这个评论的对象
        Comment comment=commentMapper.selectById(commentId);
        //判断要删除的评论的发布者的id是不是当前登录用户的id
        if(comment.getUserId()==user.getId()){
            //是同一用户,可以删除
            int num=commentMapper.deleteById(commentId);
            return num == 1;
        }
        throw ServiceException.invalidRequest("权限不足");
    }

2.控制层

@GetMapping("/{id}/delete")
    public R removeComment(
            @PathVariable Integer id,
            @AuthenticationPrincipal User user){
         boolean isdelete=commentService.removeComment(id,user.getUsername());
        if(isdelete) {
            return R.gone("删除成功");
        }else{
            return R.notFound("没有找到对应记录");
        }
    }

3.页面和js

1.删除的方式

为了防止用户误删除,我们将删除的操作设计为

先点击删除链接,再删除链接右侧出现一个红叉,再点击红叉实现删除效果

所以,我们先要把右侧的红叉编写出来

1.编写确认删除的红叉



2.在红叉上绑定点击事件

触发ajax方法

@click是v-on:click的缩写形式

3.编写删除评论的方法

removeComment:function(commentId){
            if(!commentId){
                return;
            }
            $.ajax({
                //   匹配/v1/comments/{id}/delete
                url:"/v1/comments/"+commentId+"/delete",
                method:"get",
                success:function(r){
                    if(r.code==GONE){
                        alert(r.message);
                    }else{
                        alert(r.message);
                    }
                }
            })
        },
2.将删除结果同步更新到页面

我们删除了评论,但是没删除页面上已经显示出来的那份

所以要编写删除已经显示出来的评论内容的代码

1.更新detail.html的代码

循环li标签的v-for修改如下

  • v-for="(comment,index) in answer.comments"中

    (comment,index)中的comment仍然代表集合中的每个元素

    而index代表当前循环的索引从0开始

    在确认删除的红叉链接上,修改调用的删除代码,添加几个参数

    代码如下

    2.页面中的调用变化了,那么我们的方法的实现也要随之变化

    question_detail.js代码如下

    removeComment:function(commentId,index,comments){
                if(!commentId){
                    return;
                }
                $.ajax({
                    //   匹配/v1/comments/{id}/delete
                    url:"/v1/comments/"+commentId+"/delete",
                    method:"get",
                    success:function(r){
                        if(r.code==GONE){
                            //splice方法是从指定数组中,从index的位置开始删除,删除几个元素
                            //这里写1就表示只删除index位置的一个元素
                            comments.splice(index,1);
                        }else{
                            alert(r.message);
                        }
                    }
                })
            }

    编辑评论

    1.业务逻辑层

    ICommentService

    // 修改评论
        Comment updateComment(Integer commentId,CommentVo commentVo,String username);

    编写业务逻辑层的实现

    CommentServiceImpl的updateComment方法

    @Override
        public Comment updateComment(Integer commentId, 
                                     CommentVo commentVo, String username) {
            //获得登录用户信息
            User user=userMapper.findUserByUsername(username);
            //获得要修改的评论信息
            Comment comment=commentMapper.selectById(commentId);
            //判断修改权限
            if((user.getType()!=null&&user.getType()==1) 
                    || comment.getUserId()==user.getId()){
                //权限允许,开始修改,修改只能改内容
                comment.setContent(commentVo.getContent());
                int num=commentMapper.updateById(comment);
                if(num != 1){
                    throw ServiceException.busy();
                }
                return comment;
            }
            throw ServiceException.invalidRequest("权限不足");
        }

    2.控制层

    CommentController类中编写代码如下

    @PostMapping("/{id}/update")
        public R update(
                @PathVariable Integer id,
                @Validated CommentVo commentVo,BindingResult result,
                @AuthenticationPrincipal User user){
            if(result.hasErrors()){
                String message=result.getFieldError().getDefaultMessage();
                return R.unproecsableEntity(message);
            }
            Comment comment=
                    commentService.updateComment(id,commentVo,user.getUsername());
            return R.ok(comment);
        }

    3.页面和js

    1.detail.html页面
    内容不能为空!
    2.编写js文件

    继续在question_detail.js文件中的answersApp模块中添加方法

    updateComment方法

    代码如下

    updateComment:function(commentId,answerId,index,comments){
                let textarea=$("#editComment"+commentId+" textarea");
                let content=textarea.val();
                if(!content){
                    return;
                }
                let data={
                    answerId:answerId,
                    content:content
                };
                $.ajax({
                    url:"/v1/comments/"+commentId+"/update",
                    method:"post",
                    data:data,
                    success:function(r){
                        console.log(r)
                        if(r.code==OK){
                            //如果是对数组内部的属性值的修改
                            //不会触发Vue的绑定更新
                            //Vue提供了手动绑定更新的方法,能够修改数组中的值
                            // 而且还能触发绑定的更新
                            Vue.set(comments,index,r.data)
                            //将当前显示编辑输入框的div隐藏
                            $("#editComment"+commentId).collapse("hide");
                        }else{
                            alert(r.message);
                        }
                    }
                })
            }

    采纳答案功能

    1.页面和js

    1.完成页面上采纳答案的二次确认效果

    在点击采纳答案后,弹出二次确认的按钮

    detail.html页面的采纳答案链接修改为:

    采纳答案
                          

    2.编写js文件

    在question_detail.js文件的answersApp中编写方法,代码如下

    //问题采纳
            answerSolved: function (answerId, answer) {
                if (!answerId) {
                    return;
                }
                //判断这个问题是否已经被采纳
                if (answer.acceptStatus == 1) {
                    alert("此问题已经被采纳")
                    return;
                }
                $.ajax({
                    url: "/v1/answers/" + answerId + "/solved",
                    method: "get",
                    success: function (r) {
                       console.log(r);
                       if(r.code==ACCEPTED){
                           answer.acceptStatus=1;
                       }else{
                           alert(r.message);
                       }
                    }
                });
    
            }

    3.数据访问层和业务逻辑层

    1.数据库操作

    修改answer表中accept_status 列的值为1

    修改question表中status列的值为2

    一个业务有两个修改操作,需要事务的支持

    2.数据访问层

    AnswerMapper接口中添加修改accept_status列的方法

    @Update("update answer set accept_status=#{status}" +
                    " where id=#{answerId}")
            int updateStatus(@Param("answerId") Integer answerId,
                             @Param("status") Integer acceptStatus);
            

    QuestionMapper中添加修改问题状态的方法

    @Update("update question set status=#{status} " +
                " where id=#{questionId}")
        int updateStatus(@Param("questionId") Integer questionId,
                         @Param("status") Integer status);

    在Question实体类中添加问题状态的常量,以便调用和表示

    public class Question implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        //定义问题状态的常量
        public static final Integer POSTED=0; //已添加/未回复
        public static final Integer SOLVING=1;//正在采纳/已回复
        public static final Integer SOLVED=2; //已经采纳/已解决
         //..其它代码略   
    }    

    3.业务逻辑层

    IAnswerService接口中添加方法

     //采纳答案的方法
     boolean accept(Integer answerId);

    AnswerServiceImpl实现类中方法的代码如下

    @Resource
        private QuestionMapper questionMapper;
        @Override
        @Transactional
        public boolean accept(Integer answerId) {
            //查询当前要采纳的answer对象
            Answer answer=answerMapper.selectById(answerId);
            //判断这个answer是不是已经被采纳
            if(answer.getAcceptStatus()==1){
                //如果已经被采纳返回false
                return false;
            }
            //开始执行采纳业务
            answer.setAcceptStatus(1);
            int num=answerMapper.updateStatus(answerId
                    ,answer.getAcceptStatus());
            if(num!=1){
                throw ServiceException.busy();
            }
            //修改问题状态为已解决
            num=questionMapper.updateStatus(answer.getQuestId(),
                    Question.SOLVED);
            if(num!=1){
                throw ServiceException.busy();
            }
            return true;
        }

    3.控制层

    AnswerController类中添加方法代码如下

    @GetMapping("/{id}/solved")
        public R solved(
                @PathVariable Integer id){
            log.debug("收到参数:{}",id);
            boolean accepted=answerService.accept(id);
            if(accepted) {
                return R.accepted("采纳成功!");
            }else{
                return R.notFound("不能重复采纳答案");
            }
        }