关于java:稻草问答

稻草我的项目知识点总结

MyBatisPlus

  • 什么是MyBatisPlus

    就是在MyBatis框架的根底上延长了一些新的性能的框架,应用MyBatisPlus不必再导入Mybatis的依赖了

  • 怎么应用MyBatisPlus

    找到父我的项目的pom.xml文件增加一个版本依赖和依赖治理

    代码如下

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.4.0</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>cn.tedu</groupId>
        <artifactId>straw</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>straw</name>
        <description>Demo project for Spring Boot</description>
    
        <packaging>pom</packaging>
    
        <modules>
                <module>straw-portal</module>
        </modules>
        <properties>
            <java.version>1.8</java.version>
            <mybatis.plus.version>3.3.1</mybatis.plus.version>
        </properties>
    
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                    <version>${mybatis.plus.version}</version>
                </dependency>
                <dependency>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-extension</artifactId>
                    <version>${mybatis.plus.version}</version>
                </dependency>
                <dependency>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-generator</artifactId>
                    <version>${mybatis.plus.version}</version>
                </dependency>
            </dependencies>
        </dependencyManagement>
    </project>

    子项目的pom.xml文件应用这些依赖

    代码如下

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>cn.tedu</groupId>
            <artifactId>straw</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>cn.tedu</groupId>
        <artifactId>straw-portal</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>straw-portal</name>
        <description>Demo project for Spring Boot</description>
    
        <properties>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-freemarker</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-generator</artifactId>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-extension</artifactId>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>      
                </plugin>
            </plugins>
        </build>
    
    </project>

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

不要忘了在配置类中配置扫描@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

父子相认

新建的子项目中增加如下依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-extension</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

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

    /**
     * <p>
     * 读取控制台内容
     * </p>
     */
    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<FileOutConfig> 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;

/**
 * <p>
 * ${table.comment!} Mapper 接口
 * </p>
 *
 * @author ${author}
 * @since ${date}
 */
<#if kotlin>
interface ${table.mapperName} : ${superMapperClass}<${entity}>
<#else>
@Repository
public interface ${table.mapperName} extends ${superMapperClass}<${entity}> {

}
</#if>

运行CodeGenerator类中的main办法,输出all期待办法运行结束,

我的项目中就蕴含这些生成的类了

Spring 平安框架🦄

1.介绍

Spring-Security(Spring平安框架)是Spring提供的平安治理组件

是Spring框架环境下提供的平安治理和权限治理的组件

一个我的项目个别都会有登录性能,咱们之前编写的登录性能十分简陋,不能用于理论开发

Spring-Security提供了业余的实现登录的形式,供咱们应用

### 2.应用Spring-Security实现登录

1.导入依赖

<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Security Test -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

2.配置用户名明码

application.properties文件中增加配置如下

# Spring-Security配置用户名明码
spring.security.user.name=admin
spring.security.user.password=123456
明码加密

咱们应用BCrypt的加密规定

1.新建一个包cn.tedu.straw.portal.security

配置类SecurityConfig,在这个类中注入加密对象

代码如下

//@Configuration示意以后类是配置类,可能向Spring容器中注入对象
@Configuration
public class SecurityConfig {

    //注入一个加密对象
    @Bean
    public PasswordEncoder passwordEncoder(){
        //这个加密对象应用BCrypt加密内容
        return new BCryptPasswordEncoder();
    }
}

2.上面进行测试,测试加密性能和验证性能

代码如下

@SpringBootTest
public class SecurityTest {

    @Autowired
    PasswordEncoder passwordEncoder;

    @Test
    public void encodeTest(){
        /*
            每次运行加密后果不同
            是因为加密对象采纳了"随机加盐"技术,进步安全性
         */
        String pwd=passwordEncoder.encode("123456");
        System.out.println(pwd);
//$2a$10$IHMiKBqpiPFYgRg4P0E0HeU.xdkr1nw0/y1AWKIvHh5TMNwxVuBRW
    }
    @Test
    public void matchTest(){
        /*
        验证咱们输出的明码是不是能匹配生成的密文
         */
        boolean b=passwordEncoder.matches("123456",
                "$2a$10$IHMiKBqpiPFYgRg4P0E0" +
                        "HeU.xdkr1nw0/y1AWKIvHh5TMNwxVuBRW");
        System.out.println(b);
    }
}

批改application.properties文件中配置的明码

# Spring-Security配置用户名明码
spring.security.user.name=admin
spring.security.user.password=$2a$10$IHMiKBqpiPFYgRg4P0E0HeU.xdkr1nw0/y1AWKIvHh5TMNwxVuBRW

将一个Spring内置的算法标记标注在application.properties文件的密文明码前

代码如下

# Spring-Security配置用户名明码
spring.security.user.name=admin
spring.security.user.password={bcrypt}$2a$10$IHMiKBqpiPFYgRg4P0E0HeU.xdkr1nw0/y1AWKIvHh5TMNwxVuBRW

3.连贯数据库实现Spring-Security登录

步骤1(筹备)

对于用户的权限多表联查

数据库中有:

permission表,保留权限

role表,保留角色

role_permission表,保留角色和权限的关系

role是permission多对多关系,多对多关系的表肯定会呈现一张两头表,来保留他们的关系

user表,保留用户信息

user_role表,保留用户和角色的关系

user和role表也是多对多的关系

咱们在登录用户时须要指定用户的权限,依据用户的id查问权限可能须要应用这5张表的连贯查问

除了对权限的查问外,还须要用户的根本信息,应用用户名查问出用户对象即可

在UserMapper接口中增加如下两个查问

@Repository
public interface UserMapper extends BaseMapper<User> {

    //依据用户输出的用户名查问用户信息的办法
    @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<Permission> findUserPermissionsById(Integer id);

}

步骤2

在编写IUserService接口中增加一个取得用户详情的办法

public interface IUserService extends IService<User> {

    //这个办法用法查问取得用户详情对象的业务
    //UserDetails是SpringSecurity验证用户必要的信息
    //String username是SpringSecurity接管的用户输出的用户名
    UserDetails getUserDetails(String username);
}

在impl包下的UserServiceImpl类中实现这个办法

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    UserMapper userMapper;
    @Override
    public UserDetails getUserDetails(String username) {
        //依据用户名取得用户对象
        User user=userMapper.findUserByUsername(username);
        //判断用户对象是否为空
        if(user==null) {
            //如果为空间接返回null
            return null;
        }
        //如果不为空依据用户的id查问这个用户的所有权限
        List<Permission> permissions=
                userMapper.findUserPermissionsById(user.getId());
        //将权限List中的权限转成数组不便赋值
        String[] auths=new String[permissions.size()];
        for(int i=0;i<auths.length;i++){
            auths[i]=permissions.get(i).getName();
        }
        //创立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;
    }
}

步骤3

UserDetailsServiceImpl类中来调用刚刚编写的UserServiceImpl类中的办法

返回UserDetails对象即可

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    //Spring-Security认证信息时
    //会将用户名传递到这个办法中
    //依据这个用户名取得数据库中加密的明码,
    //如果匹配则登录胜利
    @Autowired
    IUserService userService;

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        return userService.getUserDetails(username);
    }
}

### 4.管制受权范畴

网站有些页面须要登录后能力拜访,然而有些间接就能够拜访

咱们设置一下受权范畴,无论是否登录都能够拜访首页

代码如下

//@Configuration示意以后类是配置类,可能向Spring容器中注入对象
@Configuration
//上面的注解示意告诉Spring-Security开启权限治理性能
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.userDetailsService(userDetailsService);
    }
    
    //管制受权代码在这里!!!!!
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()//对以后全副申请进行受权
            .antMatchers(
                    "/index.html",
                    "/img/*",
                    "/js/*",
                    "/css/*",
                    "/bower_components/**"
            )//设置门路
            .permitAll()//容许全副申请拜访下面定义的门路
            //其它门路须要全副进行表单登录验证
            .anyRequest().authenticated().and().formLogin();

    }

续Spring-Security

自定义登录界面

步骤1

登录页面是视图模板引擎生成的,所以须要引入Thymeleaf的依赖

子项目的pom.xml文件

         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

步骤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提供的对用户输出信息进行验证的框架组件

是服务器端验证技术

应用Spring验证框架验证发送到服务器的内容的合法性!

Spring-validation(验证)

2.应用Spring-Validation

步骤1

导入依赖

子项目pom.xml文件增加:

<!-- 验证框架 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

步骤2

定位到要验证信息的实体类

将验证规定依照给定注解来标记即可

要验证注册业务,就找RegisterVo类即可

@Data
public class RegisterVo implements Serializable {
    //只能作用在String上,不能为null,去掉空格之后也不能为""
    @NotBlank(message = "邀请码不能为空")
    private String inviteCode;
    @NotBlank(message = "用户名不能为空")
    //@Pattern()示意上面的属性须要通过指定正则表达式的判断
    @Pattern(regexp="^1\\d{10}$",message ="手机号格局不正确")
    private String phone;
    @NotBlank(message = "昵称不能为空")
    @Pattern(regexp="^.{2,20}$",message ="昵称在2到20位之间")
    private String nickname;
    @NotBlank(message = "明码不能为空")
    @Pattern(regexp="^\\w{6,20}$",message ="明码在6~20位之间")
    private String password;
    @NotBlank(message = "确认明码不能为空")
    private String confirm;

}

步骤3

在控制器从表单或ajax取得实体类对象参数时就能够对这个实体类属性的值进行下面设置的验证了

验证办法非常简单,只须要加一个注解即可!

SystemController注册办法代码批改如下

@PostMapping("/register")
    public R registerStudent(
            //控制器接收的参数前加@Validated
            //示意要按这个类规定的验证规定,验证这个对象属性的值
            @Validated RegisterVo registerVo,
            //固定用法,在验证参数后再跟一个参数:BindingResult
            //这个参数中记录保留下面验证过程中的验证信息和后果
            BindingResult validaResult){
        //在控制器调用业务逻辑前,先判断BindingResult对象中是否有谬误
        if(validaResult.hasErrors()){
            //如果验证后果中蕴含任何错误信息,进入这个if
            //取得其中的一个错误信息显示,个别是按程序的第一个错误信息
            String error=validaResult.getFieldError()
                        .getDefaultMessage();
            return R.unproecsableEntity(error);
        }
        System.out.println(registerVo);
        log.debug("失去信息为:{}",registerVo);
        try{
            userService.registerStudent(registerVo);
            return R.created("注册胜利!");
        }catch (ServiceException e){
            log.error("注册失败",e);
            return R.failed(e);
        }
    }

VUE(根本应用)

1.介绍

也是一个js为根底的前端框架

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

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

个别状况下,程序要联合JQuery的ajax操作和Vue的性能实现前后端信息交互

2.如何应用VUE

应用筹备

Idea增加插件

编写html文件

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

这个页面我的项目中不应用,就是测试用

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title>Title</title>    <!-- 增加VUE的反对 -->    <script src="bower_components/vue/dist/vue.js"></script></head><body><div id="app">    <p v-text="message">VUE演示</p>    <input type="text" v-model="content">    <button type="button" v-on:click="hello">按钮</button></div></body><script>    //应用Vue    let app=new Vue({        el:"#app",        data:{            message:"Vue发送的文字!",            content:"输入框的内容"        },        methods:{            hello:function(){                console.log(this.content);                //this.content=this.content+"!";            }        }    });</script></html>

Vue性能的弱小之处在于信息实时同步的双向绑定

应用VUE+Ajax欠缺稻草问答的注册性能

批改register.html代码

批改static/js/register.js代码

“R”类和自定义业务异样类

1.介绍

实体类能接管表单发送过去的信息,然而咱们控制器解决实现后,想返回Json格局的对象给JS,也须要一个实体类

这个实体类最好可能通用于所有业务

当初行业中风行应用一个”R”类来返回JSON格局信息

这个R类中次要蕴含3个属性

1.状态码

2.状态音讯

3.实体(控制器查问出的任何内容)

2.如何应用

创立R类(代码无需把握,会应用即可)

@Data
@Accessors(chain = true)
public class R<T> 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示意这个办法是用来出解决异样的

控制器产生异样时,会主动匹配适合的异样类型,运行办法

文件上传(上载)

1.介绍

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

http协定规定了一些上传文件时的规范

  1. 表单提交的形式必须是post
  2. 表单提交的编码方式必须批改为 multipart/form-data(二进制)
  3. 要上传的文件应用<input type=”file” name=”xxx”> 来示意
  4. HTTP申请头中必须蕴含 Content-type: multipart/form-data, boundary=AaB03x;

    4中的形容理解即可

  5. 容许上传多个文件

2.文件上传流程

3.测试

编写页面代码

只是为了测试,所以咱们编写在static文件夹中即可

upload.html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form method="post" enctype="multipart/form-data"
        action="/upload/file">
        <input type="file" name="imageFile"><br>
        <input type="submit">
    </form>
</body>
</html>

提交到SystemController控制器中的办法代码如下

须要保障f:盘中有upload文件夹

 //接管表单上传的文件
    @PostMapping("/upload/file")
    public R<String> 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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="bower_components/jquery/dist/jquery.min.js"></script>
</head>
<body>
<form id="demoForm">
    <input type="file" id="imageFile" name="imageFile"><br>
    <input type="submit">
</form>
</body>
<script src="js/utils.js"></script>
<script>
    //在页面加载结束之后运行!
    $(function () {
        //先绑定一个办法,当用户点击提交时,验证是否选中了图片
        $("#demoForm").submit(function(){
            //取得用户抉择的文件(js是取得用户抉择的文件数组)
            let files=document.getElementById("imageFile").files;
            //判断文件数组是不是长度>0
            if(files.length>0){
                //有文件,做上传
                let file=files[0];//将文件从数组中取出
                console.log(file);
                //调用专门上传文件的办法
                uploadImage(file);
            }else{
                //没文件,间接完结
                alert("请抉择文件")
            }
            return false;//阻止表单提交
        })
        //实现文件上传的办法
        function uploadImage(file){
            //构建表单
            let form=new FormData();
            form.append("imageFile",file);
            $.ajax({
                url:"/upload/file",
                method:"post",
                data:form,//发送的是咱们构建的表单中的数据
                //上面有两个非凡参数,须要在文件上传时设置
                contentType:false,
                processData:false,
                success:function(r){
                    if(r.code==OK){
                        console.log(r);
                        alert(r.message);
                    }else{
                        alert(r.message);
                    }
                }
            });
        }
    })
</script>
</html>

无需批改控制器代码

间接提交文件测试,胜利即可

用户注册

每个网站都须要用户注册的性能

页面如下图

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

在用户曾经可能登录显示主页的前提下

  1. 主页页面中编写ajax向控制器发送申请所有标签
  2. 管制接到申请后调用业务逻辑层
  3. 业务逻辑层从tagMapper接口查问所有标签
  4. 业务逻辑层将查问到的信息返回给控制器
  5. 控制器取得所以标签返回JSON格局
  6. ajax中取得JSON对象,利用VUE绑定显示在页面上

2.业务逻辑层Service

咱们能够抉择先编写业务逻辑层

步骤1:

ITagService接口中增加办法

public interface ITagService extends IService<Tag> {
    List<Tag> getTags();
}

步骤2:

实现这个接口

TagServiceImpl类中代码如下

@Service
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements ITagService {

    //CopyOnWriteArrayList<>是线程平安的汇合,适宜在高并发的环境下应用
    private final List<Tag> tags=new CopyOnWriteArrayList<>();

    @Override
    public List<Tag> 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<Tag> 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<List<Tag>> tags(){
        List<Tag> list=tagService.getTags();
        return R.ok(list);
    }
}

4.页面和JS代码

到页面中(index.html)绑定vue须要的变量

页面代码如下

    <div class="nav font-weight-light" id="tagsApp">
    <a href="tag/tag_question.html" class="nav-item nav-link text-info">    <small>全副</small></a>
    <!-- v-for 循环中in左侧是随便起的变量名,会在循环体中应用
            in右侧的变量名,绑定这VUE代码中的变量-->
    <a href="tag/tag_question.html"
       class="nav-item nav-link text-info"
       v-for="tag in tags">
       <small v-text="tag.name">Java根底</small>
    </a>
  </div>

index.html网页的完结地位要引入两个js文件

<script src="js/utils.js"></script>
<script src="js/index.js"></script>
<script src="js/tags_nav.js"></script>

编写js/tags_nav.js代码如下

let tagsApp = new Vue({
    el:'#tagsApp',
    data:{
        tags:[]
    },
    methods:{
        loadTags:function () {
            console.log('执行了 loadTags');
            $.ajax({
                url:'/v1/tags',
                method:'GET',
                success:function (r) {
                    console.log(r);
                    if (r.code === OK){
                        console.log('胜利获取tags');
                        tagsApp.tags = r.data;
                    }
                }
            });
        }
    },
    created:function () {
        this.loadTags();
    }
});

开发问题列表🤡

1.理解开发流程

2.业务逻辑层Service

在业务逻辑层的接口中申明办法

IQuestionService接口给中申明办法

public interface IQuestionService extends IService<Question> {

    //按登录用户查问以后用户问题的办法
    List<Question> 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<QuestionMapper, Question> implements IQuestionService {
    @Autowired
    IUserService userService;
    @Autowired
    UserMapper userMapper;
    @Autowired
    QuestionMapper questionMapper;
    //按登录用户查问以后用户问题的办法
    @Override
    public List<Question> 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<Question> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("user_id",user.getId());
        queryWrapper.eq("delete_status",0);
        queryWrapper.orderByDesc("createtime");
        List<Question> 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<List<Question>> my(){
        log.debug("开始查问以后用户的问题");
        //这里要解决个异样,因为用户可能没有登录
        try{
            List<Question> 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格局信息

<div class="container-fluid" id="questionsApp">
          <h4 class="border-bottom m-2 p-2 font-weight-light"><i class="fa fa-comments-o" aria-hidden="true"></i> 我的问答</h4>
          <div class="row" style="display: none">
            <div class="alert alert-warning w-100" role="alert">
              道歉您还没有发问内容, <a href="question/create.html" class="alert-link">您能够点击此处发问</a>,或者点击标签查看其它问答
            </div>
          </div>
          <div class="media bg-white m-2 p-3" v-for="question in questions" >
            <div class="media-body w-50">
              <div class="row">
                <div class="col-md-12 col-lg-2">
                  <span class="badge badge-pill badge-warning" style="display: none">未回复</span>
                  <span class="badge badge-pill badge-info" style="display: none">已回复</span>
                  <span class="badge badge-pill badge-success">已解决</span>
                </div>
                <div class="col-md-12 col-lg-10">
                  <h5 class="mt-0 mb-1 text-truncate">
                    <a class="text-dark" href="question/detail.html"
                      v-text="question.title">
                      eclipse 如何导入我的项目?
                    </a>
                  </h5>
                </div>
              </div>

              <div class="font-weight-light text-truncate text-wrap text-justify mb-2" style="height: 70px;">
                <p v-html="question.content">
                  eclipse 如何导入我的项目?
                </p>
              </div>
              <div class="row">
                <div class="col-12 mt-1 text-info">
                  <i class="fa fa-tags" aria-hidden="true"></i>
                  <a class="text-info badge badge-pill bg-light" href="tag/tag_question.html"><small >Java根底 &nbsp;</small></a>
                </div>
              </div>
              <div class="row">
                <div class="col-12 text-right">
                  <div class="list-inline mb-1 ">
                    <small class="list-inline-item"
                           v-text="question.userNickName">风持续吹</small>
                    <small class="list-inline-item">
                            <span v-text="question.pageViews">12</span>浏览</small>
                    <small class="list-inline-item" >13分钟前</small>
                  </div>
                </div>
              </div>

            </div>
            <!-- / class="media-body"-->
            <img src="img/tags/example0.jpg"  class="ml-3 border img-fluid rounded" alt="" width="208" height="116">
          </div>
          <div class="row mt-2">
            <div class="col-6 offset-3">
              <nav aria-label="Page navigation example">
                <div class="pagination">
                  <a class="page-item page-link" href="#" >上一页</a>
                  <a class="page-item page-link " href="#" >1</a>
                  <a class="page-item page-link" href="#" >下一页</a>
                </div>
              </nav>
            </div>

          </div>
        </div>

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<questions.length;i++){
                //取得问题中的创立工夫属性(毫秒数)
                let createtime=new Date(questions[i].createtime).getTime();
                //取得以后工夫的毫秒数
                let now=new Date().getTime();
                //计算时间差(秒)
                let durtaion=(now-createtime)/1000;
                if(durtaion<60){
                    // 显示刚刚
                    //duration这个名字能够轻易起,只有保障和页面上取的一样就行
                    questions[i].duration="刚刚";
                }else if(durtaion<60*60){
                    // 显示XX分钟
                    questions[i].duration=
                        (durtaion/60).toFixed(0)+"分钟前";
                }else if (durtaion<60*60*24){
                    //显示XX小时
                    questions[i].duration=
                        (durtaion/60/60).toFixed(0)+"小时前";
                }else{
                    //显示XX天
                    questions[i].duration=
                        (durtaion/60/60/24).toFixed(0)+"天前";
                }

            }

        }
    },
    created:function () {
        console.log("执行了办法");
        this.loadQuestions(1);
    }
});

Index.html页面也须要进行一个批改,让计算出的持续时间显示进去

代码如下

<small class="list-inline-item"
                 v-text="question.duration">13分钟前</small>

4.2显示问题的标签列表

页面中的问题是能够多个标签的

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

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

咱们能够看到,在问题表中咱们放弃了冗余的数据列tag_names,这么做的益处就是缩小查问时的复杂度,理论开发中程序员们也可能用这样的形式

实现过程

实现思路

1.创立一个蕴含全副标签的Map,map的key是标签名称,value是标签对象

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

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

4.将获取的value存入Question实体类中的List<Tag>tags属性

1.在Question实体类中须要定义一个List<Tag> tags

起因是咱们须要可能从一个问题中取得多个标签

    //为问题实体类增加标签汇合
    //@TableField(exist = false)示意数据库中没有这样的列,避免报错
    @TableField(exist = false)
    private List<Tag> tags;

2.失去蕴含所有Tag标签的Map

业务逻辑层ITagService增加办法

public interface ITagService extends IService<Tag> {

    //取得所有标签的办法
    List<Tag> getTags();

    //取得所有标签返回Map的办法
    Map<String,Tag> 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;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author tedu.cn
 * @since 2020-12-09
 */
@Service
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements ITagService {

    //CopyOnWriteArrayList<>是线程平安的汇合,适宜在高并发的环境下应用
    private final List<Tag> tags=new CopyOnWriteArrayList<>();
    //ConcurrentHashMap是线程平安的Map,适宜在高并发的环境下应用
    private final Map<String,Tag> map=new ConcurrentHashMap<>();

    @Override
    public List<Tag> 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<String, Tag> getName2TagMap() {
        //判断如果map是空,证实下面getTags办法没有运行
        if(map.isEmpty()){
            //那么就调用下面的getTags办法
            getTags();
        }
        return map;
    }
}

3.将数据库tag_names列中的内容转换成List<Tag>

在QuestionServiceImpl类中编写代码

//依据Question的tag_names列的值,返回List<Tag>
    private  List<Tag> tagNamesToTags(String tagNames){
        //失去的tag_name拆分字符串
        //tagNames="java根底,javaSE,面试题"
        String[] names=tagNames.split(",");
        //names={"java根底","javaSE","面试题"}
        //申明List以便返回
        List<Tag> list=new ArrayList<>();
        Map<String,Tag> 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<Tag>并赋值

在咱们编写的QuestionServiceImpl类中的getMyQuestions办法中

依据步骤3中编写的办法来取得Question对象中的List<Tag>并赋值

@Service
@Slf4j
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {
    @Autowired
    IUserService userService;
    @Autowired
    UserMapper userMapper;
    @Autowired
    QuestionMapper questionMapper;
    //按登录用户查问以后用户问题的办法
    @Override
    public List<Question> 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<Question> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("user_id",user.getId());
        queryWrapper.eq("delete_status",0);
        queryWrapper.orderByDesc("createtime");
        List<Question> list=questionMapper.selectList(queryWrapper);
        log.debug("以后用户的问题数量为:{}",list.size());
        //遍历以后查问出的所有问题对象
        for(Question q: list){
            //将问题每个对象的对应的Tag都查问进去,并赋值为实体类中的List<Tag>
            List<Tag> tags=tagNamesToTags(q.getTagNames());
            q.setTags(tags);
        }
        return list;
    }

    @Autowired
    ITagService tagService;

    //依据Question的tag_names列的值,返回List<Tag>
    private  List<Tag> tagNamesToTags(String tagNames){
        //失去的tag_name拆分字符串
        //tagNames="java根底,javaSE,面试题"
        String[] names=tagNames.split(",");
        //names={"java根底","javaSE","面试题"}
        //申明List以便返回
        List<Tag> list=new ArrayList<>();
        Map<String,Tag> map=tagService.getName2TagMap();
        //遍历String数组
        for(String name:names) {
            //依据String数组中以后的元素取得Map对应的value
            Tag tag=map.get(name);
            //将这个value保留在list对象中
            list.add(tag);
        }
        return list;
    }
}

5.批改一下html页面内容,来获取问题的标签

<a class="text-info badge badge-pill bg-light"
            href="tag/tag_question.html" v-for="tag in question.tags">
     <small v-text="tag.name" >Java根底 &nbsp;</small>
</a>

4.3显示问题的图片

我的项目中每个问题右侧跟一个图片,这个图片实际上是依据问题的第一个标签的id来决定的

须要在index.js文件中编写显示相干图片的代码

并在适合地位调用

代码如下

 updateTagImage: function () {
            let questions = this.questions;
            for (let i = 0; i < questions.length; i++) {
                let tags = questions[i].tags;
                if (tags) {
                    let tagImage = 'img/tags/' + tags[0].id + '.jpg';
                    questions[i].tagImage = tagImage;
                }
            }
        },

在index.html文件中绑定

<img src="img/tags/example0.jpg"
                 v-bind:src="question.tagImage"
                 class="ml-3 border img-fluid rounded"
                 alt="" width="208" height="116">

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的应用🤡

1.导入依赖

因为PageHelper是Mybatis提供的,没有SpringBoot的默认版本反对

所以像Mybatis一眼咱们要本人治理版本

在Straw父我的项目的pom.xml文件中

<java.version>1.8</java.version>
<mybatis.plus.version>3.3.1</mybatis.plus.version>
<pagehelper.starter.version>1.3.0</pagehelper.starter.version>
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>${pagehelper.starter.version}</version>
 </dependency>
        

子项目pom.xml文件增加代码

<dependency>
       <groupId>com.github.pagehelper</groupId>
       <artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>

留神父子我的项目的pom.xml都须要刷新!

2.接口重构

IQuestionService接口重构

 //按登录用户查问以后用户问题的办法
    PageInfo<Question> getMyQuestions(
            Integer pageNum,Integer pageSize
    );

QuestionServiceImpl重构接口中的办法

 //按登录用户查问以后用户问题的办法
    @Override
    public PageInfo<Question> 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<Question> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("user_id",user.getId());
        queryWrapper.eq("delete_status",0);
        queryWrapper.orderByDesc("createtime");
        //执行查问之前,要设置分页查问信息
        PageHelper.startPage(pageNum,pageSize);
        //紧接着的查问就是依照下面分页配置的分页查问
        List<Question> list=questionMapper.selectList(queryWrapper);
        log.debug("以后用户的问题数量为:{}",list.size());
        //遍历以后查问出的所有问题对象
        for(Question q: list){
            //将问题每个对象的对应的Tag都查问进去,并赋值为实体类中的List<Tag>
            List<Tag> tags=tagNamesToTags(q.getTagNames());
            q.setTags(tags);
        }
        return new PageInfo<Question>(list);
    }

3.测试
@SpringBootTest
public class QuestionTest {
    @Autowired
    IQuestionService questionService;

    @Test
    //@WithMockUser是Spring-Security提供的注解
    //在测试中如果须要从Spring-Security中取得用户信息,那么就能够用这个注解标记
    //指定用户信息,也要留神,这只是个测试,Spring-Security不会对信息验证
    @WithMockUser(username = "st2",password = "123456")
    public void getQuest(){
        PageInfo<Question> 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<PageInfo<Question>> my(Integer pageNum){
        if(pageNum==null){
            pageNum=1;
        }
        int pageSize=8;
        log.debug("开始查问以后用户的问题");
        //这里要解决个异样,因为用户可能没有登录
        try{
            PageInfo<Question> questions=
               questionService.getMyQuestions(pageNum,pageSize);
            return R.ok(questions);
        }catch (ServiceException e){
            log.error("用户查问问题失败!",e);
            return R.failed(e);
        }
    }
}

5.重构index.js页面代码
let questionsApp = new Vue({
    el:'#questionsApp',
    data: {
        questions:[],
        pageInfo:{}
    },
    methods: {
        loadQuestions:function (pageNum) {
            if(!pageNum){ //如果pageNum为空,默认页码为1
                pageNum=1;
            }
            $.ajax({
                url: '/v1/questions/my',
                method: "GET",
                data:{pageNum:pageNum},
                success: function (r) {
                    console.log("胜利加载数据");
                    console.log(r);
                    if(r.code === OK){
                        questionsApp.questions = r.data.list;
                        //调用计算持续时间的办法
                        questionsApp.updateDuration();
                        //调用显示所有按标签出现的图片
                        questionsApp.updateTagImage();
                        questionsApp.pageInfo=r.data;
                    }
                }
            });
        },
       //之后代码未修改,略 
}        

6.配置页面给定的分页导航条

实现翻页,配置页面给定的分页导航条

<div class="pagination">
<a class="page-item page-link" href="#"
    v-on:click.prevent="loadQuestions(pageInfo.prePage)"
    >上一页</a>
<a class="page-item page-link " href="#"
    v-for="n in pageInfo.navigatepageNums"
    v-on:click.prevent="loadQuestions(n)"
    v-bind:class="
    {'bg-secondary text-light':n == pageInfo.pageNum}"
    ><span v-text="n">1</span></a>
<a class="page-item page-link" href="#"
    v-on:click.prevent="loadQuestions(pageInfo.nextPage)"
    >下一页</a>
</div>

参考资料

PageInfo类中的罕用属性

//当前页
private int pageNum;
//每页的数量
private int pageSize;
//当前页的行数量
private int size;
//以后页面第一个元素在数据库中的行号
private int startRow;
//以后页面最初一个元素在数据库中的行号
private int endRow;
//总页数
private int pages;
//前一页页号
private int prePage;
//下一页页号
private int nextPage;
//是否为第一页
private boolean isFirstPage;
//是否为最初一页
private boolean isLastPage;
//是否有前一页
private boolean hasPreviousPage;
//是否有下一页
private boolean hasNextPage;
//导航条中页码个数
private int navigatePages;
//所有导航条中显示的页号
private int[] navigatepageNums;
//导航条上的第一页页号
private int navigateFirstPage;
//导航条上的最初一页号
private int navigateLastPage;

学生发问

学员的问题公布性能

显示页面

将static/question/create.html

复制到

templates/question/create.html

并编写控制器代码显示这个页面

HomeController中代码如下

    //显示学生问题发布页面
    @GetMapping("/question/create.html")
    public ModelAndView createQuestion(){
        //templates/question/create.html
        return new ModelAndView("question/create");
    }

复用标签导航条(index.html中)🤡

1.定义模板

在index.html页面中,将要复用的html区域用特定标签标记

th:fragment=”xxx”

<div class="container-fluid"  th:fragment="tags_nav" >
    <!-- 代码略 -->
</div>

2.套用模板

当初是create.html须要复用代码,所以是这个页面套用模板

th:replace=”xxx”来套用

保障页面反对th:的写法不报错

<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">

套用模板

<div class="container-fluid" th:replace="index::tags_nav" >
  <div class="nav font-weight-light">
  <!-- 代码略 -->
  </div>
</div>

th:replace=”index::tags_nav”的意思是用index.html页面中名为tags_nav的模板中的代码替换掉以后编写套用标记的html标签

最初在代码临完结之前,引入ajax和Vue代码

</body>
<script src="../js/utils.js"></script>
<script src="../js/tags_nav.js"></script>
</html>

富文本编辑器

1.介绍

富文本编辑器实用于那些须要格局甚至是图片的用户输出需要,这样的编辑器都是基于<textarea>标签的

只是在这个标签的根底上增加了很多js代码或相干插件的实现,咱们无需手动开发

市面上有很多性能全面的收费的富文本编辑器工具,其中比拟风行的就是咱们应用的这个

summernote官方网站是:www.summernote.org

2.如何应用

下载它的反对后再页面上编写如下代码引入款式和js文件

<link rel="stylesheet" href="../bower_components/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="../bower_components/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="../bower_components/summernote/dist/summernote-bs4.min.css">

<script src="../bower_components/jquery/dist/jquery.min.js"></script>
<script src="../bower_components/popper.js/dist/umd/popper.min.js"></script>
<script src="../bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="../bower_components/polyfill/dist/polyfill.min.js"></script>
<script src="../bower_components/summernote/dist/summernote-bs4.js"></script>
<script src="../bower_components/summernote/dist/lang/summernote-zh-CN.min.js"></script>

在页面中须要富文本编辑器的地位编写如下代码

<textarea name="content" id="summernote"></textarea>

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

<script>
    $(document).ready(function() {
        $('#summernote').summernote({
            height: 300,  //高度
            tabsize: 2,   //tab大小
            lang: 'zh-CN',//中文反对
            placeholder: '请输出问题的详细描述...'
        });
    });
</script>

个别这个代码在页面最初地位

多选下拉框

1.介绍

网页中的下拉列表框是一个可能多选,并且有选中后款式的性能的控件

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

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

依赖JQuery同时也依赖Vue外围的js

2.如何应用

引入一些依赖

<link rel="stylesheet"   href="../bower_components/vue-select/dist/vue-select.css"><script src="../bower_components/vue/dist/vue.js"></script><script src="../bower_components/vue-select/dist/vue-select.js"></script>

下图示意他们的关联

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

将他们的代码批改为:

 <div class="col-8" id="createQuestionApp">
      <h4 class="border-bottom m-2 p-2 font-weight-light"><i class="fa fa-question-circle-o" aria-hidden="true"></i>
        填写问题</h4>
      <form v-on:submit.prevent="createQuestion">
        <div class="form-group">
          <label for="title">题目:</label>
          <input type="text" class="form-control" id="title" name="title" placeholder="请填写题目3~50字符"
                 pattern="^.{3,50}$" required v-model="title">
        </div>
        <div class="form-group">
          <label >请至多抉择一个标签:</label>
          <v-select multiple required v-bind:options="tags"
          v-model="selectedTags" placeholder="请抉择问题的标签"
          >
          </v-select>
        </div>
        <div class="form-group">
          <label >请抉择老师:</label>
          <v-select multiple required
                    v-bind:options="teachers"
                    v-model="selectedTeachers"
                    placeholder="请抉择答复的老师"
          >
          </v-select>
        </div>
        <div class="form-group">
          <!--富文本编辑器 start-->
          <label for="summernote">问题注释</label>
          <textarea name="content" id="summernote"></textarea>
          <!--富文本编辑器 end-->
        </div>
        <button type="submit" class="btn btn-primary mt-3">提交问题</button>
      </form>

    </div>
<script src="../js/createQuestion.js"></script>

createQuestion.js文件中的内容

动静加载所有标签和老师

动静加载所有标签的实现非常简单,因为咱们间接能够调用现成的控制器办法

createQuestion.js中编写代码如下

动静加载所有老师

实现步骤

步骤1:

增加业务逻辑层接口办法IUserService

 //查问所有老师用户的办法    
 List<User> getMasters();

步骤2:

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

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

步骤3:

编写管制层:UserController中设计门路v1/users/master,返回R<List<User>>即可

 @Autowired
    IUserService userService;

    @GetMapping("/master")
    public R<List<User>> master(){
        List<User> 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<list.length;i++){
                            //push办法示意向这个数组的最初地位增加元素
                            //成果和java中list的add办法统一
                            teachers.push(list[i].nickname);
                        }
                        console.log(teachers);
                        createQuestionApp.teachers=teachers;
                    }
                }
            });
        },

公布问题

咱们先来实现数据提交到控制器的内容

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❓

上面咱们须要实现新增问题的业务逻辑的开发

首先来理解一下咱们须要什么操作能力实现这个业务

举例

1.讲师的信息也是能够保留在换存中来防止屡次拜访数据库来提交运行效率的

所以咱们参照对标签的解决办法,对所有讲师也进行缓存

IUserService中

 //查问所有老师用户的办法
    List<User> getMasters();
 //查问所有老师用户的Map办法
    Map<String,User> getMasterMap();

2.参照TagServiceImpl中对标签的缓存,解决讲师缓存

UserServiceImpl代码如下

  private final List<User> masters=
            new CopyOnWriteArrayList<>();
    private final Map<String,User> 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<User> getMasters() {
        if(masters.isEmpty()){
            synchronized (masters){
                if(masters.isEmpty()){
                    QueryWrapper<User> 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<String, User> 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<String,Tag> 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<String,User> 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是阻止表单提交用的

<form v-on:submit.prevent="createQuestion">

在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 增加依赖

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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<String> 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代码如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script class="lazy" referrerpolicy="no-referrer" data-src="bower_components/jquery/dist/jquery.min.js"></script>
</head>
<body>
<form id="demoForm">
    <input type="file" id="imageFile" name="imageFile"><br>
    <input type="submit">
</form>
<img src="" id="image">
</body>
<script src="js/utils.js"></script>
<script>
    //在页面加载结束之后运行!
    $(function () {
        //先绑定一个办法,当用户点击提交时,验证是否选中了图片
        $("#demoForm").submit(function(){
            //取得用户抉择的文件(js是取得用户抉择的文件数组)
            let files=document.getElementById("imageFile").files;
            //判断文件数组是不是长度>0
            if(files.length>0){
                //有文件,做上传
                let file=files[0];//将文件从数组中取出
                console.log(file);
                //调用专门上传文件的办法
                uploadImage(file);
            }else{
                //没文件,间接完结
                alert("请抉择文件")
            }
            return false;//阻止表单提交
        })
        //实现文件上传的办法
        function uploadImage(file){
            //构建表单
            let form=new FormData();
            form.append("imageFile",file);
            $.ajax({
                url:"/upload/file",
                method:"post",
                data:form,//发送的是咱们构建的表单中的数据
                //上面有两个非凡参数,须要在文件上传时设置
                contentType:false,
                processData:false,
                success:function(r){
                    if(r.code==OK){
                        console.log(r);
                        //alert(r.message);
                        $("#image").attr("src",r.message);
                    }else{
                        alert(r.message);
                    }
                }
            });
        }
    })
</script>
</html>

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

上传流程

重构create.html中的代码

代码如下

<!--底部-->
<footer class="container-fluid  bg-light mt-2 py-3">
  <p class="text-center font-weight-light">达内教育-Java教研部 版权所有<br><a href="http://tedu.cn" rel="nofollow" target="_blank">京ICP备16053980号-3</a>
  </p>
</footer>
<script src="../plugins/summernote/summernote.min.js"></script>
<script src="../plugins/summernote/summernote-zh-CN.min.js"></script>
<script>
  $(document).ready(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);
              }
            }
          });
        }
      }
    });
    $('select').select2({placeholder:'请抉择...'});
  });
</script>
</body>
<script src="../js/utils.js"></script>
<script src="../js/tags_nav.js"></script>
<script src="../js/createQuestion.js"></script>
</html>

显示问题状态

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

<div class="col-md-12 col-lg-2">
    <!-- v-show="[boolean类型表达式]" -->
    <!-- 只有boolean类型表达式值为真,这个元素就会显示
    反之就不会显示-->
    <span class="badge badge-pill badge-warning"
        style="display: none"
        v-show="question.status==0">未回复</span>
    <span class="badge badge-pill badge-info"
        style="display: none"
        v-show="question.status==1">已回复</span>
    <span class="badge badge-pill badge-success"
        v-show="question.status==2">已解决</span>
</div>

用户信息面板

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<Question> 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<UserVo> me(){
        UserVo user=userService.currentUserVo();
        return R.ok(user);
    }

4.页面和JS代码

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

<!--个人信息-->
      <div id="userApp" class="container-fluid font-weight-light">
        <div class="card">
          <h5 class="card-header" v-text="user.nickname">陈某</h5>
          <div class="card-body">
            <div class="list-inline mb-1 ">
                <a class="list-inline-item mx-3 my-1 text-center">
                  <div><strong>10</strong></div>
                  <div>答复</div>
                </a>
                <a class="list-inline-item mx-3 my-1 text-center" href="personal/myQuestion.html">
                  <div>
                    <strong v-text="user.questions">10</strong>
                  </div>
                  <div>发问</div>
                </a>
                <a class="list-inline-item mx-3 my-1 text-center" href="personal/collect.html">
                  <div>
                    <strong v-text="user.collections">10</strong>
                  </div>
                  <div>珍藏</div>
                </a>
                <a class="list-inline-item mx-3 my-1 text-center" href="personal/task.html">
                  <div><strong>10</strong></div>
                  <div>工作</div>
                </a>
            </div>
          </div>
        </div>
      </div>
</body>
<script src="js/utils.js"></script>
<script src="js/index.js"></script>
<script src="js/tags_nav.js"></script>
<script src="js/user_info.js"></script>
</html>

在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模板

<div id="userApp"  th:fragment="user_info"
           class="container-fluid font-weight-light">

步骤2:

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

<div class="col-4 pb-2 ">
  <div th:replace="index::user_info">
  </div>
  <!--热点数据代码略-->
</div>

步骤3:

create.html文件开端引入js的反对

</body>
<script src="../js/utils.js"></script>
<script src="../js/tags_nav.js"></script>
<script src="../js/createQuestion.js"></script>
<script src="../js/user_info.js"></script>
</html>

讲师回复首页

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

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<Role> findUserRolesById(Integer id);

测试

@Autowired
    UserMapper userMapper;
    @Test
    public void roles(){
        List<Role> 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<Permission> permissions=
                userMapper.findUserPermissionsById(user.getId());
        //将权限List中的权限转成数组不便赋值
        String[] auths=new String[permissions.size()];
        for(int i=0;i<auths.length;i++){
            auths[i]=permissions.get(i).getName();
        }
        //读取用户的所有角色
        List<Role> 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的命名空间

<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">

应用模板替换导航栏

<!--引入标签的导航栏-->
<div class="container-fluid" th:replace="index::tags_nav">
    <!--中间代码略-->
</div>

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

<!--个人信息-->
<div th:replace="index::user_info" class="container-fluid font-weight-light">
    <!--中间代码略-->
</div>

引入依赖的js文件

</body>
<script src="js/utils.js"></script>
<script src="js/tags_nav.js"></script>
<script src="js/user_info.js"></script>
</html>

显示讲师问题列表

1.数据拜访层

QuestionMapper中增加新的办法代码如下

@Repository
public interface QuestionMapper extends BaseMapper<Question> {
    @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<Question> findTeachersQuestions(Integer userId);
}

能够测试一下

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

2.业务逻辑层

IQuestionService

//分页查问以后登录的老师问题的办法
    PageInfo<Question> getQuestionsByTeacherName(
            String username,Integer pageNum,Integer pageSize
    );

QuestionServiceImpl

 @Override
    public PageInfo<Question> 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<Question> questions=
                questionMapper.findTeachersQuestions(user.getId());
        //别忘了,要将问题列中的标签字符串转成标签的List
        for(Question q: questions){
            List<Tag> tags=tagNamesToTags(q.getTagNames());
            q.setTags(tags);
        }
        return new PageInfo<Question>(questions);
    }

3.管制层

QuestionController

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

4.页面和js

index_teacher.html页面也能够应用th模板来复用问题列表

定义模板

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

批改代码如下

<div class="container-fluid" id="questionsApp"
            th:fragment="questions_app">

套用模板

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

<div class="container-fluid"
             th:replace="index::questions_app">

最初实现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依赖

</body>
<script src="js/utils.js"></script>
<script src="js/tags_nav.js"></script>
<script src="js/user_info.js"></script>
<script src="js/index_teacher.js"></script>
</html>

问题详情页面

流程介绍

  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标签代码批改为

<a class="text-dark" href="question/detail.html"
           v-bind:href="'/question/detail.html?'+question.id"
           v-text="question.title">
       eclipse 如何导入我的项目?
</a>

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<Tag>
        List<Tag> tags=tagNamesToTags(question.getTagNames());
        //将转换实现的List<Tag>保留到这个Question的tags属性中
        question.setTags(tags);
        return question;
    }

2.管制层

QuestionController

    //显示问题具体的Controller办法
    //为了恪守RESTful的格调这个地位的门路比拟非凡
    //例如:/v1/questions/12
    //下面的门路SpringMvc会主动将12赋值给{id}
    //@PathVariable标记的同名属性的值也会是12
    @GetMapping("/{id}")
    public R<Question> 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代码

次要在以后问题显示详情的为地位

<!-- 素材187行  --->
<div id="questionApp" class="container-fluid bg-light">
<div id="questionApp" class="container-fluid bg-light">
        <div class="row">
          <div class="col-2 px-0  ">
            <div class="container-fluid ">
              <div class="row  mt-4 mx-0 px-0" >
                <a class="btn btn-outline-danger btn-md rounded-lg fa fa-close " style="font-size: x-small">删除</a>
              </div>

              <div class="row mt-4 mx-0 px-0" >
                <a class="btn btn-outline-primary btn-md rounded-lg fa fa-edit " style="font-size: x-small"
                   href="../question/edit.html">编辑</a>
              </div>
              <div class="row mt-4 mx-0 px-0">
                <a class="btn btn-outline-info btn-md rounded-lg fa fa-pencil " style="font-size: x-small"
                   href="#writeAnswer">答复</a>
              </div>
              <div class="row mt-4 mx-0 px-0" id="collectApp">
                <a class="btn btn-outline-secondary btn-md rounded-lg fa fa-star "
                    style="font-size: x-small">珍藏</a>
                <a  class="btn btn-outline-secondary btn-md rounded-lg fa fa-star "
                    style="font-size: x-small;display: none" >已珍藏</a>
              </div>

            </div>

          </div>
          <div class="col-10 px-0">
            <div class="container-fluid ">
              <div class="row px-0 mb-3">
                <div class="col-9 px-0">
                  <a class="badge badge-pill  badge-info mx-1"
                     href="../tag/tag_question.html"
                     v-for="tag in question.tags"
                     v-text="tag.name">Java根底</a>
                </div>
                <div class="col-3 px-0">
                  <div class="row px-0">
                    <div class="col border-right text-right">
                      <p class="font-weight-light mb-0">珍藏</p>
                      <p class="font-weight-bold mt-1">1</p>
                    </div>
                    <div class="col">
                      <p class="font-weight-light mb-0">浏览</p>
                      <p class="font-weight-bold mt-1"
                        v-text="question.pageViews">100</p>
                    </div>
                  </div>
                </div>
              </div>
              <p class=" px-0 text-center font-weight-bold" style="font-size: x-large"
                v-text="question.title">
                Java中办法重载和重写的区别
              </p>
              <div class="px-0 container-fluid question-content"
                v-html="question.content">
                请问的办法中重写和重载的区别都是什么,如何应用
              </div>
              <p class="text-right px-0 mt-5">
                <span class="font-weight-light badge badge-primary"
                  v-text="question.userNickName">张三</span>
                <span class="font-weight-light badge badge-info"
                  v-text="question.duration">3天前</span>
              </p>
            </div>

          </div>
        </div>
      </div>
</body>
<script src="../js/utils.js"></script>
<script src="../js/question_detail.js"></script>
</html>

创立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页面代码中删除学生不能执行的操作

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> {
    //提交讲师回复问题的答案信息
    Answer saveAnswer(AnswerVo answerVo,String username);
}

AnswerServiceImpl类中编写代码如下

@Service
@Slf4j
public class AnswerServiceImpl extends ServiceImpl<AnswerMapper, Answer> 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页面的代码

次要针对富文本编辑器提交的表单范畴

<div class="container-fluid mt-4" id="postAnswerApp">
              <h5 class="text-info mb-2"><i class="fa fa-edit"></i>写答案</h5>
              <form action="#" method="post"
                    v-on:submit.prevent="postAnswer"
                    class="needs-validation" novalidate>
                <div class="form-group">
                  <textarea id="summernote" name="content" required ></textarea>
                  <div class="invalid-feedback"
                    v-bind:class="{'d-block':hasError}">
                    <h5 v-text="message">答复内容不能为空!</h5>
                  </div>
                </div>
                <div class="form-group">
                  <p class="text-right">
                    <button type="submit" class="btn btn-primary">提交答复</button>
                  </p>
                </div>
              </form>
            </div>

为了实现讲师回复时抉择的图片上载,和讲师回复的提交操作

在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<Answer> getAnswersByQuestionId(Integer questionId);

AnswerServiceImpl类中

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

2.管制层

AnswerController中

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

3.页面和js

改写detail.html页面,应用Vue绑定答复相干信息

<div class="row mt-5 ml-2" id="answersApp">
              <div class="col-12">
                <div class="well-sm"><h3>
                  <span v-text="answers.length">3</span>条答复
                </h3></div>
                <div class="card card-default my-5"
                  v-for="answer in answers">
                  <!-- Default panel contents -->
                  <div class="card-header">
                    <div class="row">
                      <div class="col-1">
                        <img style="width: 50px;height: 50px;border-radius: 50%;"
                             src="../img/user.jpg">
                      </div>
                      <div class="col-8 ">
                        <div class="row">
                          <span class="ml-3"
                           v-text="answer.userNickName">张三</span>
                        </div>
                        <div class="row">
                          <span class="ml-3"
                            v-text="answer.duration">2天前</span>
                        </div>
                      </div>
                      <div class="3">
                      </div>
                    </div>
                  </div>
                  <div class="card-body ">
                    <span class="question-content text-monospace"
                          v-html="answer.content">
                      办法的重载是overloading,办法名雷同,参数的类型或个数不同,对权限没有要求
                      办法的重写是overrding 办法名称和参数列表,参数类型,返回值类型全副雷同,然而所实现的内容能够不同,个别产生在继承中
                    </span>
           <!-- 以下代码略  -->
</div>

编写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<questions.length;i++){
               addDuration(questions[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<this.answers.length;i++){
                addDuration(this.answers[i]);
            }
        }
    },
    created:function(){
        this.loadAnswers();
    }
})

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

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

评论答案的性能

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

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<Comment> comments=new ArrayList<>();

2.数据拜访层-简单查问

1.首先在AnswerMapper接口中编写上面的办法

@Repository
    public interface AnswerMapper extends BaseMapper<Answer> {
        //简单映射查问按问题id取得所有答复以及每个答复蕴含的评论
        List<Answer> findAnswersByQuestionId(Integer questionId);
        
    }

2.编写执行简单映射查问的xml文件

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

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

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.tedu.straw.portal.mapper.AnswerMapper">

    <!-- 通用查问映射后果 -->
    <resultMap id="answerCommentMap" type="cn.tedu.straw.portal.model.Answer">
        <id column="id" property="id"/>
        <result column="content" property="content"/>
        <result column="like_count" property="likeCount"/>
        <result column="user_id" property="userId"/>
        <result column="user_nick_name" property="userNickName"/>
        <result column="quest_id" property="questId"/>
        <result column="createtime" property="createtime"/>
        <result column="accept_status" property="acceptStatus"/>
        <collection property="comments" ofType="cn.tedu.straw.portal.model.Comment">
            <id column="comment_id" property="id" />
            <result column="comment_user_id" property="userId" />
            <result column="comment_answer_id" property="answerId" />
            <result column="comment_user_nick_name" property="userNickName" />
            <result column="comment_content" property="content" />
            <result column="comment_createtime" property="createtime" />
        </collection>
    </resultMap>
    <select id="findAnswersByQuestionId" resultMap="answerCommentMap" >
        SELECT
            a.id,
            a.content,
            a.user_id,
            a.user_nick_name,
            a.quest_id,
            a.createtime,
            a.accept_status,
            a.like_count,
            c.id AS comment_id,
            c.user_id AS comment_user_id,
            c.user_nick_name AS comment_user_nick_name,
            c.content AS comment_content,
            c.createtime AS comment_createtime,
            c.answer_id AS comment_answer_id
        FROM answer a
        LEFT JOIN `comment` c
        ON c.answer_id=a.id
        WHERE a.quest_id=#{question_id}
        ORDER BY a.createtime,c.createtime
    </select>
</mapper>

3.业务逻辑层-重构

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

AnswerServiceImpl类中getAnswersByQuestionId办法代码如下

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

4.管制层 页面 和 js

曾经实现过控制器调用按id查问答复的办法

当初间接到页面去批改vue的绑定即可

<p class="text-success">
                      <i class="fa fa-comment"></i>&nbsp;
                      <span v-text="answer.comments.length">1</span>条评论
                    </p>
                    <ul class="list-unstyled mt-3">
                      <li class="media my-2" v-for="comment in answer.comments">
                        <img style="width: 50px;height: 50px;border-radius: 50%;"
                             class="lazy" referrerpolicy="no-referrer" data-src="../img/user.jpg" class="mr-3"
                             alt="...">
                        <div class="media-body">
                          <h6 class="mt-0 mb-1">
                            <span v-text="comment.userNickName">李四</span>:
                          </h6>
                          <p class="text-dark">
                            <span class="text-monospace" v-text="comment.content">
                              明确了,谢谢老师!
                            </span>
                            <span class="font-weight-light text-info"
                                  style="font-size: small"></span>
                            <a class="text-primary ml-2"
                               style="font-size: small" data-toggle="collapse" href="#editCommemt1"
                               role="button"
                               v-bind:href="'#editComment'+comment.id"
                               aria-expanded="false" aria-controls="collapseExample">
                              <i class="fa fa-edit"></i>编辑
                            </a>
                            <!--老师角色或者属于本用户的评论能够删除该评论-->
                            <a class="ml-2  fa fa-close " style="font-size: small"
                               data-toggle="collapse"  role="button"
                               aria-expanded="false" aria-controls="collapseExample">
                              删除
                            </a>

                          </p>
                          <div class="collapse" id="editCommemt1"
                            v-bind:id="'editComment'+comment.id">
                            <div class="card card-body border-light">
                              <form action="" method="post" class="needs-validation" novalidate>
                                <div class="form-group">
                                  <textarea class="form-control"
                                            id="textareaComment1" name="content" rows="4"
                                            required></textarea>
                                  <div class="invalid-feedback">
                                    内容不能为空!
                                  </div>
                                </div>
                                <button type="submit" class="btn btn-primary my-1 float-right">提交批改</button>
                              </form>
                            </div>
                          </div>
                        </div>
                      </li>

                    </ul>

增加评论

开发思路

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

    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<CommentMapper, Comment> 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<Comment> 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文件中”驳回答案”左近代码批改为:

<p class="text-left text-dark">
                      <a class="btn btn-primary mx-2"
                         href="#">驳回答案</a>
                      <a class="btn btn-outline-primary"
                         data-toggle="collapse" href="#collapseExample1"
                         v-bind:href="'#addComment'+answer.id"
                         role="button" aria-expanded="false" aria-controls="collapseExample">
                        <i class="fa fa-edit"></i>增加评论
                      </a>
                    </p>
                    <div class="collapse" id="collapseExample1"
                      v-bind:id="'addComment'+answer.id">
                      <div class="card card-body border-light">
                        <form action="#" method="post"
                              v-on:submit.prevent="postComment(answer.id)"
                              class="needs-validation" novalidate>
                          <div class="form-group">
                            <textarea class="form-control" name="content" rows="3" required></textarea>
                            <div class="invalid-feedback">
                              评论内容不能为空!
                            </div>
                          </div>
                          <button type="submit" class="btn btn-primary my-1 float-right">提交评论</button>
                        </form>
                      </div>
                    </div>

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<answers.length;i++){
                            //判断本次增加的评论是不是属于以后答复的
                            if(answers[i].id == answerId){
                                //把新增的评论保留到以后答复中评论的汇合里
                                answers[i].comments.push(comment);
                                break;
                            }
                        }
                    }else{
                        alert(r.message);
                    }
                }

删除评论

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.编写确认删除的红叉

<!--老师角色或者属于本用户的评论能够删除该评论-->
<a class="ml-2" style="font-size: small"
    data-toggle="collapse"  role="button"
    onclick="$(this).next().toggle(200)"
    aria-expanded="false" aria-controls="collapseExample">
    <i class="fa fa-close"></i><small>删除</small>
</a>
<a class="text-white badge badge-pill badge-danger"
    style="display: none; cursor: pointer">
    <i class="fa fa-close"></i>
</a>

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

触发ajax办法

<a class="text-white badge badge-pill badge-danger"
    style="display: none; cursor: pointer"
    @click="removeComment(comment.id)"
    >
    <i class="fa fa-close"></i>
</a>

@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批改如下

<li class="media my-2" v-for="(comment,index) in answer.comments">

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

(comment,index)中的comment依然代表汇合中的每个元素

而index代表以后循环的索引从0开始

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

代码如下

<a class="text-white badge badge-pill badge-danger"
                              style="display: none; cursor: pointer"
                                                                                         @click="removeComment(comment.id,index,answer.comments)"
                              >
                              <i class="fa fa-close"></i>
                            </a>

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<Comment> 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页面
<div class="collapse" id="editCommemt1"
                            v-bind:id="'editComment'+comment.id">
                            <div class="card card-body border-light">
                              <form action="" method="post"
                                v-on:submit.prevent=
                                  "updateComment(comment.id,answer.id,index,answer.comments)"
                                    class="needs-validation" novalidate>
                                <div class="form-group">
                                  <textarea class="form-control"
                                            id="textareaComment1" name="content" rows="4"
                                            v-text="comment.content"
                                            required></textarea>
                                  <div class="invalid-feedback">
                                    内容不能为空!
                                  </div>
                                </div>
                                <button type="submit" class="btn btn-primary my-1 float-right">提交批改</button>
                              </form>
                            </div>
                          </div>
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页面的驳回答案链接批改为:

<a class="btn btn-primary mx-2 text-white"
                         style="cursor: pointer"
                          onclick="$(this).next().toggle(200)"
                          >驳回答案</a>
                      <a class="text-white badge badge-pill badge-success"
                         style="display: none; cursor: pointer"
                         @click="answerSolved(answer.id,answer)"
                      >
                        <i class="fa fa-check-square"></i>
                      </a>

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("不能反复驳回答案");
        }
    }

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理