乐趣区

关于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 的保留实现在 i ndex.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("不能反复驳回答案");
        }
    }
退出移动版