稻草问答-迁移问答功能

稻草问答-迁移问答功能

1 显示当前用户问题列表

1.1 创建straw-faq项目

将与问答有关的功能迁移到一个独立的微服务项目straw-faq,部署时候会部署为一组服务器,这样好处是可以将系统的功能分配给多个服务器承担,提升系统的并发能力和性能。

首先创建straw-faq项目

配置straw-faq项目的pom文件,设置straw-faq父项目是straw:

<?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>
    <artifactId>straw-faq</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>straw-faq</name>
    <description>稻草问答的:问答功能的微服务模块</description>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

在straw项目的pom文件中登记模块项目:

<modules>
    <module>straw-faq</module>
</modules>

在Eureka中注册straw-faq模块,首先在straw-faq的pom文件中引入Eureka客户端包:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

在straw-faq的启动类上开启Eureka客户端@EnableEurekaClient:

@SpringBootApplication
@EnableEurekaClient
public class StrawFaqApplication {

    public static void main(String[] args) {
        SpringApplication.run(StrawFaqApplication.class, args);
    }
}

在straw-faq项目application.properties文件中配置服务名称的端口号:

server.port=8001
spring.application.name=faq-service

为了稳定设定Eureka注册时候的实例名称,此时可以稳定的使用服务器主机名进行Eureka注册,服务名稳定的好处是进行Session共享时候可以稳定的传输http协议头,避免Session共享失败。Eureka客户端都需要做如下设置:

eureka.instance.prefer-ip-address=false
eureka.instance.hostname=localhost
eureka.instance.ip-address=127.0.0.1
eureka.instance.instance-id=${spring.application.name}:${eureka.instance.hostname}:${server.port}

启动straw-eureka,启动straw-faq测试能否注册到Eureka。

1.2 迁移数据层到straw-faq

迁移与问题相关功能与迁移用户相关功能类似,先迁移数据层,再迁移业务层,最后迁移控制器,首先迁移数据层:

迁移后会出现编译错误,需要导入相关的依赖包,straw-commons、MyBatisPlus、MySQL Driver、PageHelper:

<dependency>
    <groupId>cn.tedu</groupId>
    <artifactId>straw-commons</artifactId>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

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

修改实体类的包名。在straw-faq启动类上标注Mapper接口扫描注解︰

@SpringBootApplication
@EnableEurekaClient
@MapperScan("cn.tedu.straw.faq.mapper")
public class StrawFaqApplication {

    public static void main(String[] args) {
        SpringApplication.run(StrawFaqApplication.class, args);
    }
}

在straw-faq项目application.properties中配置数据库连接参数︰

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/straw?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
spring.datasource.username=root
spring.datasource.password=990921

logging.level.cn.tedu.straw.faq=debug

测试......

package cn.tedu.straw.faq;

import cn.tedu.straw.faq.mapper.*;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
@Slf4j
public class MapperTests {

    @Resource
    QuestionMapper questionMapper;

    @Resource
    QuestionTagMapper questionTagMapper;

    @Resource
    TagMapper tagMapper;

    @Resource
    UserQuestionMapper userQuestionMapper;

    @Resource
    AnswerMapper answerMapper;

    @Resource
    CommentMapper commentMapper;

    @Test
    void mappers(){
        log.debug("{}",questionMapper);
        log.debug("{}",questionTagMapper);
        log.debug("{}",tagMapper);
        log.debug("{}",userQuestionMapper);
        log.debug("{}",answerMapper);
        log.debug("{}",commentMapper);
    }
}

1.3 迁移业务层

从straw-portal迁移问题相关业务层到straw-faq:Projectw

迁移后需要在straw-faq模块pom文件中导入:

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

重构业务层:修改import,导入正确的类。

straw-faq的业务层需要获取全部回答问题的老师列表和根据老师名字获取老师信息,这些功能可以利用Ribbon从straw- sys服务模块中获取:

首先在straw-sys服务模块UserController中添加Rest接口方法:

@GetMapping("/master")
public List<User> master(){
    return userService.getMasters();
}

然后在straw-faq项目启动类中声明RestTemplate:

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
    return new RestTemplate();
}

在straw-faq的业务层中利用Ribbon调用straw-sys服务中的接口,获得答疑老师信息:

//利用Ribbon从Straw-sys服务中获取全部回答问题的老师
String url = "http://sys-service/v1/users/masters";
User[]users = restTemplate.getForobject(url,User[].class);
Map<string,User> masterMap = new HashMap<>();
for(User u:users){
    masterMap.put(u.getNickname(), u); 
}

在straw-faq的业务层中利用Ribbon调用straw-sys服务中的接口,获得用户信息:

public User getUser(String username){
    String url="http://sys-service/v1/auth/user?username={1}";
    User user=restTemplate.getForObject(url,User.class, username);
    return user;
}

测试......

@SpringBootTest
@Slf4j
public class ServiceTests {
    
    @Resource
    IQuestionService questionService;
    
    @Test
    void services(){
        PageInfo<Question> pageInfo=
                questionService.getQuestionsByTeacherName("wangkj",1,10);
        pageInfo.getList().forEach(question -> log.debug("{}",question));
    }
}

迁移后的业务层代码参考:

package cn.tedu.straw.faq.service;

import cn.tedu.straw.commons.model.Question;
import cn.tedu.straw.faq.vo.QuestionVo;
import com.baomidou.mybatisplus.extension.service.IService;
import com.github.pagehelper.PageInfo;

/**
 * <p>
 * 服务类
 * </p>
 *
 * @author tedu.cn
 * @since 2021-07-29
 */
public interface IQuestionService extends IService<Question> {
    /**
     * 获取当前用户的全部问题列表
     * @param pageNum 当前页号
     * @return当前用户的全部问题
     * @param pageSize 翻页时候的页面大小
     */
    PageInfo<Question> getMyQuestions(String username,Integer pageNum, Integer pageSize);

    /**
     * 将用户提交的问题保存到数据库中
     * @param questionVo 用户提交到问题表单数据
     */
    void saveQuestion(String username,QuestionVo questionVo);

    /**
     * 根据用户的id统计其发布的问题数量
     * 删除掉不会被统计(删除标记)
     * @param userId 用户的ID
     * @return 发布的问题数量
     */
    Integer countQuestionsByUserId(Integer userId);

    /**
     * 查询老师相关问题
     */
    PageInfo<Question> getQuestionsByTeacherName(String username, Integer pageNum,Integer pageSize);

    /**
     * 根据问题id查询一个问题数据
     */
    Question getQuestionById(Integer id);
}
package cn.tedu.straw.faq.service.Impl;

import cn.tedu.straw.commons.model.*;
import cn.tedu.straw.faq.kafka.KafkaProducer;
import cn.tedu.straw.faq.mapper.QuestionMapper;
import cn.tedu.straw.faq.mapper.QuestionTagMapper;

import cn.tedu.straw.faq.mapper.UserQuestionMapper;
import cn.tedu.straw.faq.service.IQuestionService;
import cn.tedu.straw.faq.service.ITagService;

import cn.tedu.straw.commons.service.ServiceException;
import cn.tedu.straw.faq.vo.QuestionVo;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author tedu.cn
 * @since 2021-07-29
 */
@Service
@Slf4j
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {
    //@Autowired
    //private IUserService userService;

    //@Autowired
    //private UserMapper userMapper;

    @Autowired
    private QuestionMapper questionMapper;

    @Autowired
    private ITagService tagService;

    @Autowired
    private QuestionTagMapper questionTagMapper;

    @Autowired
    private UserQuestionMapper userQuestionMapper;

    @Resource
    private RibbonClient ribbonClient;

    @Resource
    private KafkaProducer kafkaProducer;

    @Override
    public PageInfo<Question> getMyQuestions(
        String username,
        Integer pagerNum, Integer pageSize) {

        if(pagerNum==null || pageSize==null){
            throw new ServiceException("翻页参数错误");
        }

        //String username =userService.currentUsername();
        log.debug("当前用户是{}",username);
        //http://sys-service/v1/auth/user?username={1}
        //String url="http://sys-service/v1/auth/user?username={1}";
        //User user=restTemplate.getForObject(url, User.class,username);
        //User user=userMapper.findUserByUsername(username);
        User user=ribbonClient.getUser(username);
        log.debug("当前登录用户{}",user);
        if(user==null){
            throw ServiceException.notFound("登录用户没有找到");
        }
        QueryWrapper<Question> query = new QueryWrapper<>();
        query.eq("user_id",user.getId());
        query.eq("delete_status",0);//0表示没被删除,1表示已删除
        query.orderByDesc("createtime");

        PageHelper.startPage(pagerNum, pageSize);
        List<Question> questions=questionMapper.selectList(query);
        log.debug("查询得到{}行数据",questions.size());
        for(Question q:questions){
            List<Tag> tags=tagNamesToTags(q.getTagNames());
            q.setTags(tags);
        }
        return new PageInfo<>(questions);
    }

    /**
     * 将标签名列表转换为标签列表集合
     * @param tagNames 标签名列表
     * @return 标签列表集合
     */
    private List<Tag> tagNamesToTags(String tagNames){
        String[] names=tagNames.split(",\\s?");
        Map<String, Tag> name2TagMap=tagService.getName2TagMap();
        List<Tag> tags =new ArrayList<>();

        for(String name:names){
            Tag tag=name2TagMap.get(name);
            tags.add(tag);
        }
        return tags;
    }

    @Override
    @Transactional //声明式事务,当前方法中的SQL作为整体执行,执行失败会回滚到开始状态
    public void saveQuestion(String username,QuestionVo questionVo) {
        log.debug("问题信息{}",questionVo);
        //获取当前用户
        //String username=userService.currentUsername();
        //获取用户全部信息
        //User username=userMapper.findUserByUsername(username);
        //String url="http://sys-service/v1/auth/user?username={1}";
        //User user =restTemplate.getForObject(url,User.class, username);
        User user=ribbonClient.getUser(username);
        log.debug("当前用户{}",user);


        //根据标签名数组创建标签名列表
        StringBuilder buf=new StringBuilder();
        for(String tagName:questionVo.getTagNames()){
            buf.append(tagName).append(",");
        }
        String tagNames=buf.deleteCharAt(buf.length()-1).toString();
        log.debug("标签名列表{}",tagNames);

        Question question=new Question()
            .setTitle(questionVo.getTitle())
            .setContent(questionVo.getContent())
            .setStatus(0)//0表示刚创建的问题还没有老师回复
            .setPublicStatus(0)//0表示只能学员自己查看到问题
            .setDeleteStatus(0)//0表示没有删除,1表示已经删除
            .setPageViews(0)//问题被查看到次数
            .setCreatetime(LocalDateTime.now())//问题创建时候,默认当前时间
            .setModifytime(LocalDateTime.now())//问题修改时间
            .setUserNickName(user.getNickname())//发布问题昵称,就是当前用户
            .setUserId(user.getId())
            .setTagNames(tagNames); //问题相关的标签名列表
        log.debug("问题信息{}",question);
        int rows=questionMapper.insert(question);
        if(rows !=1){
            throw new ServiceException("数据库繁忙,请稍后再试!");
        }
        log.debug("问题信息{}",question);

        //保存问题和标签的关系 questionVo.getTagNames();
        //根据标签名找到标签ID,问题ID和标签ID存储到 questionTag表
        Map<String, Tag> name2TagMap=tagService.getName2TagMap();

        for (String tagName:questionVo.getTagNames()){
            //根据标签名,查找到其对应到标签信息
            Tag tag=name2TagMap.get(tagName);
            if (tag==null){
                throw ServiceException.unprocesabelEntiry("标签名错误!");
            }
            QuestionTag questionTag=new QuestionTag()
                .setQuestionId(question.getId())
                .setTagId(tag.getId());
            //将标签和问题的对应关系插入到数据库
            rows= questionTagMapper.insert(questionTag);
            if(rows !=1){
                throw new ServiceException("数据库繁忙,请稍后再试!");
            }
        }

        //保存问题和答疑老师关系
        //Map<String, User> masterMap=userService.getMasterMap();
        //利用Ribbon从Straw-sys服务中获取全部回答问题的老师
        //已进行重构封装
        User[] users=ribbonClient.masters();
        Map<String,User> masterMap=new HashMap<>();
        for(User u:users){
            masterMap.put(u.getNickname(),u);
        }

        for(String nickname:questionVo.getTeacherNicknames()){
            User master=masterMap.get(nickname);
            if (master==null){
                throw ServiceException.unprocesabelEntiry("讲师名错误!");
            }
            UserQuestion userQuestion=new UserQuestion()
                .setQuestionId(question.getId())
                .setUserId(master.getId())
                .setCreatetime(LocalDateTime.now());
            rows=userQuestionMapper.insert(userQuestion);
            if(rows !=1){
                throw new ServiceException("数据库繁忙,请稍后再试!");
            }
        }

        //问题保存完成,就将问题发送到Kafka
        kafkaProducer.sendQuestion(question);
    }

    @Override
    public Integer countQuestionsByUserId(Integer userId) {
        QueryWrapper<Question> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("user_id",userId);
        queryWrapper.eq("delete_status",0);//未删除的
        //selectCount是MyBatilsPlus提供的统计查询方法,专门用于统计计算数量
        Integer count=questionMapper.selectCount(queryWrapper);
        return count;
    }

    @Override
    public PageInfo<Question> getQuestionsByTeacherName(String username, Integer pageNum, Integer pageSize) {
        if(username==null){
            throw ServiceException.notFound("用户名不能为空");
        }
        if (pageNum==null){
            pageNum=1;
        }
        if(pageSize==null){
            pageSize=8;
        }
        //User user=userMapper.findUserByUsername(username);
        User user=ribbonClient.getUser(username);

        PageHelper.startPage(pageNum, pageSize);
        List<Question> questions=questionMapper.findTeachersQuestions(user.getId());

        //填充标签列表属性 tags
        for (Question question:questions){
            List<Tag> tags=tagNamesToTags(question.getTagNames());
            question.setTags(tags);
        }
        return new PageInfo<>(questions);
    }

    @Override
    public Question getQuestionById(Integer id) {
        Question question=questionMapper.selectById(id);
        //填充tags属性
        List<Tag> tags=tagNamesToTags(question.getTagNames());
        question.setTags(tags);
        return question;
    }
}
package cn.tedu.straw.faq.service;

import cn.tedu.straw.commons.model.Tag;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;
import java.util.Map;

/**
 * <p>
 * 服务类
 * </p>
 *
 * @author tedu.cn
 * @since 2021-07-29
 */
public interface ITagService extends IService<Tag> {
    List<Tag> getTags();

    Map<String, Tag> getName2TagMap();
}
package cn.tedu.straw.faq.service.Impl;

import cn.tedu.straw.faq.mapper.TagMapper;
import cn.tedu.straw.commons.model.Tag;
import cn.tedu.straw.faq.service.ITagService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author tedu.cn
 * @since 2021-07-29
 */
@Service
@Slf4j
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements ITagService {
    /**
     * CopyOnWriteArrayList<>()是线程安全的List,适合并发访问场合
     */
    private final List<Tag> tags=new CopyOnWriteArrayList<>();
    private final Map<String, Tag> name2TagMap=new ConcurrentHashMap<>();
    @Override
    public List<Tag> getTags() {
        if(tags.isEmpty()){
            synchronized (tags){
                if(tags.isEmpty()){
                    //list()是MyBatis Plue提供的方法,在ServiceImpl中定义
                    //也就得继承于ServiceImpl的方法,方法的作用就是返回
                    //数据库中Tag对象
                    tags.addAll(list());
                    tags.forEach(tag -> name2TagMap.put(tag.getName(), tag));
                    log.debug("加载tag列表{}",tags);
                    log.debug("加载了Map{}",name2TagMap);
                }
            }
        }
        return tags;
    }

    @Override
    public Map<String, Tag> getName2TagMap() {
        if(tags.isEmpty()){
            getTags();
        }
        return name2TagMap;
    }
}

1.4 共享Session

由于显示当前用户相关的问题,需要用到当前登录用户信息,这样就需要利用Session共享功能得到当前登录用户信息。与straw-sys共享Session的做法一致。

在straw-faq项目的pom文件中引入redis-session包:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

在straw-faq配置文件中设置session保存方式:

spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.port=6379

在straw-faq启动类上开启@EnableRedisHttpSession:

@SpringBootApplication
@EnableEurekaClient
@MapperScan("cn.tedu.straw.faq.mapper")
@EnableRedisHttpSession
public class StrawFaqApplication {

    public static void main(String[] args) {
        SpringApplication.run(StrawFaqApplication.class, args);

    }
        @Bean
        @LoadBalanced
        public RestTemplate restTemplate(){
            return new RestTemplate();
    }
}

在straw-gateway项目中配置zuul路由,传递敏感头信息:

zuul.routes.faq.path=/faq/**
zuul.routes.faq.service-id=faq-service
zuul.routes.faq.sensitive-headers=Authorization

配置SpringSecurity:
在straw-faq项目pom中导入spring-security包:

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

在straw-faq中添加security包,在包中添加配置类:

package cn.tedu.straw.faq.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests().anyRequest().permitAll();
    }
}

配置了Session共享以后就可以在控制器中使用@AuthenticationPrincipal UserDetailsuserDetails 注入当前登录用户信息。获得当前的登录用户名。

在straw-faq中编写测试控制器:

package cn.tedu.straw.faq.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@RequestMapping("/v1/faq")
public class DemoController {

    @GetMapping("/demo")
    public String demo(@AuthenticationPrincipal UserDetails userDetails){
        log.debug("{}", userDetails);
        return "OK";
    }
}

按照顺序启动服务模块straw-eureka straw-gateway straw-sys srtaw-faq,然后先登录系统,在请求http://localhost:9000/faq/v1/faq/demo,测试是否能够共享会话信息。

1.5 迁移控制器

从straw-portal中迁移控制器TagController到straw-faq中:

/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author tedu.cn
 * @since 2021-07-29
 */
@RestController
@RequestMapping("/v1/tags")

public class TagController {
    @Autowired
    ITagService tagService;

    /**
     * 请求URL:/v1/tags
     * @return
     */

    @GetMapping("")
    public R<List<Tag>> tags(){
        List<Tag> list=tagService.getTags();
        return R.ok(list);
    }
}

从straw-portal中迁移控制器QuestionController到straw-faq中:

/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author tedu.cn
 * @since 2021-07-29
 */
@RestController
@RequestMapping("/v1/questions")
@Slf4j
public class QuestionController {

    @Autowired
    private IQuestionService questionService;
    /**
     * 获取当前用户的全部问题
     * 请求URL:/v1/questions/my
     * @return
     */
    @GetMapping("/my")
    public R<PageInfo<Question>> my(Integer pageNum,
                                    @AuthenticationPrincipal UserDetails userDetails){
        if(pageNum==null){
            pageNum=1;
        }
        Integer pageSize=8;
     try {
         log.debug("开启请求当前用户的全部问题");
         PageInfo<Question> pageInfo = questionService.getMyQuestions(
                userDetails.getUsername(), pageNum,pageSize);
         return R.ok(pageInfo);
     }catch(ServiceException e){
         log.error("失败的加载当前用户的问题",e);
         return R.failed(e);
     }

    }
    /*
    请求URL:POST  /v1/questions
     */
    @PostMapping("")
    public R<String> createQuestion(@Validated QuestionVo questionVo,
                                    BindingResult result,
                                    @AuthenticationPrincipal UserDetails userDetails){
        if(result.hasErrors()){
            String message=result.getFieldError().getDefaultMessage();
            return R.unprocesabelEntity(message);
        }
        if(questionVo.getTagNames().length==0){
            return R.unprocesabelEntity("标签名不能为空");
        }
        if(questionVo.getTeacherNicknames().length==0){
            return R.unprocesabelEntity("没有选择答疑老师");
        }
        log.debug("收到表单信息{}",questionVo);
        questionService.saveQuestion(userDetails.getUsername(),questionVo);
        return R.created("成功保存问题数据");
    }

    /**
     * 请求/v1/questions/teacher 分页返回当前老师有关的问题
     * @param user 当前老师
     * @param pageNum 页号
     * @return 一页数据
     */
    @GetMapping("/teacher")
    @PreAuthorize("hasRole('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);
    }

    /**
     * 请求路径:、v1/questions/151
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<Question> question(@PathVariable  Integer id){
        if (id==null){
            return R.invalidRequest("ID不能为空");
        }
        log.debug("id:{}",id);
        Question question = questionService.getQuestionById(id);
        return R.ok(question);
    }
}

重构straw-gateway项目中的resources/static/js/tags_nav.js:

url:&%62339;/faq/v1/tags&%2339;,

重构straw-gateway项目中的resources/static/js/index.js:

url:&%62339;/faq/v1/questions/my&%2339;,

重构straw-gateway项目中的resources/static/js/index_teacher.js:

url:&%2339;/faq/v1/questions/teacher&%2339;,

启动全部微服务模块进行测试...

2 重构显示当前用户信息

显示当前用户信息的控制器在straw-sys服务模块中,而返回的数据又包含问题的统计信息,可以利用Ribbon从straw-faq项目获得问题统计信息。

首先在straw-faq项目上添加控制器方法,返回问题统计信息:

@GetMapping("/count")
public Integer count(Integer userId){
    Integer count=questionService.countQuestionsByUserId(userId);
    return count;
}

在straw-sys项目上配置Ribbon客户端RestTemplate:

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
    return new RestTemplate();
}

在staw-sys业务层使用Ribbon调用straw-faq项目中统计问题数量的方法:

@Override
public UserVo getCurrentUserVo(String username) {
    //获取用户
    //String username=currentUsername();
    //用户信息
    UserVo userVo=userMapper.getUserVoByUsername(username);
    //统计数量
    //Integer questions=questionService.countQuestionsByUserId(userVo.getId());
    String url="http://faq-service/v1/questions/count?userId={1}";
    Integer questions=restTemplate.getForObject(url, Integer.class, userVo.getId());

    //TODD:以后增加统计收藏的问题数量
    userVo.setQuestions(questions).setCollections(0);
    return userVo;
}

重构控制器方法:

/**
     * 请求URL /sys/v1/users/me
     * @param userDetails
     * @return
     */
@GetMapping("/me")
public R<UserVo> me(@AuthenticationPrincipal UserDetails userDetails){
    String username=userDetails.getUsername();
    UserVo userVo=userService.getCurrentUserVo(username);
    return R.ok(userVo);
}

重构straw-gateway中resource/static/user_info.js:

url:&%2339;/sys/v1/users/me&%2339;,

3 迁移提问功能

3.1 迁移提问界面

在迁移显示当前用户问题列表时候,提问相关的数据层、业务层、控制器已经一并迁移完成。 只需要重构一下straw-gateway项目中的控制器,显示一下提问界面模板,然后重构一下视图模型中的控制器URL就可以完成界面迁移工作。

重构一下straw-gateway项目中的控制器HomeContrller,显示界面模板:

@GetMapping("/question/create.html")
public String create(){
    return "/question/create";
}

在straw-sys模块UserContoller中添加控制器方法:

   /**
     * 请求:URL: /sys/v1/users/masters
     */
@GetMapping("/master")
public R<List<User>> master(){
    return R.ok(userService.getMasters());
}

将异常处理类ExceptionControllerAdvice从straw-portal迁移到straw-faq服务模块,实现统一异常处理。

重构straw-gateway项目中resource/static/js/createQuestion.js的url请求路径,请求sys服务模块和faq服务模块:

获取全部tag:

url:&%2339;/faq/v1/tags&%62339;,

获取全部答疑讲师

url:&%2339;/sys/v1/users/master&%2339;,

保存问题

url:&%2339;/faq/v1/questions&%2339;,

测试......

3.2 迁移文件上载功能

文件上载功能也需要进行迁移,由于文件资源由straw-resource进行管理,所以将文件上载功能迁移到straw-resource服务模块中。

首先迁移控制器:将straw-portal的SystemController中文件上载方法迁移到ImageController中,迁移时
候文件保存路径可以直接从配置文件的spring.resources.static-locations属性值获得,请求路径更新成Rest风格:

package cn.tedu.straw.resource.controller;

import cn.tedu.straw.commons.vo.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;

@RestController
@Slf4j
@RequestMapping("/v1/images")
public class ImageController {
    //读取自定义配置信息
    @Value("${spring.resources.static-locations}")
    private File resourcePath;

    @Value("${straw.resource.host}")
    private String resourceHost;

    @PostMapping
    public R uploadImage(MultipartFile imageFile) throws IOException {
        //创建目标存储目录
        String path= DateTimeFormatter.ofPattern("yyyy/MM/dd").format(LocalDate.now());
        File folder=new File(resourcePath, path);
        folder.mkdirs();
        log.debug("存储文件夹:{}", folder.getAbsolutePath());

        //获取扩展名
        String filename=imageFile.getOriginalFilename();
        String ext=filename.substring(filename.lastIndexOf('.'));
        log.debug("扩展名:{}", ext);

        //生成随机的文件名
        String name= UUID.randomUUID().toString() + ext;
        File file=new File(folder , name);
        log.debug("保存到:{}", file.getAbsolutePath());

        //保存文件
        imageFile.transferTo(file);

        //文件显示URL
        String url=resourceHost +"/"+path+"/"+name;
        log.debug("Image URL:"+url);
        return R.ok(url);
    }
}

还需要在straw-resource服务模块项目中导入依赖:

<dependency>
    <groupId>cn.tedu</groupId>
    <artifactId>straw-commons</artifactId>
</dependency>

配置straw-resource服务模块项目application.properties,添加文件访问URL路径:

straw.resource.host=http://localhost:9000/resource/

其中http://localhost:9000/resource/是文件上载以后的下载访问路径,9000端口,resource目录都是zuul网关中配置的转发path (请参考straw-gateway中的application.properties)

更新straw-gateway项目中resources/static/js/summernote_init.js中的文件上载路径

url: &%2339;/resource/v1/images&%2339; ,

测试......

4 迁移回答与评论

4.1 迁移数据层

将问答功能问题从straw-portal迁移到straw-faq服务模块中,修改导入包名解决编译错误:

重构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.faq.mapper.AnswerMapper">
    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="cn.tedu.straw.commons.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"/>
    </resultMap>

    <resultMap id="answersMap" type="cn.tedu.straw.commons.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 javaType="java.util.List" property="comments"
                    ofType="cn.tedu.straw.commons.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="answersMap">
       select
            a.id,
            a.content,
            a.like_count,
            a.user_nick_name,
            a.quest_id,
            a.createtime,
            a.accept_status,
            c.id as comment_id,
            c.user_id as comment_user_id,
            c.user_nick_name as comment_user_nick_name,
            c.answer_id as comment_answer_id,
            c.content as comment_content,
            c.createtime as comment_createtime
            from answer a
            left join comment c on a.id=c.answer_id
            where a.quest_id=#{questionId}
      order by a.createtime, c.createtime
    </select>
</mapper>

4.2 迁移业务层

从straw-portal迁移回答问题业务层到straw-faq :

迁移后更新业务层层代码中的包,解决各种编译问题。抽取RibbonClient组件,用于封装Ribbon远程调用,方便业务层中可以复用:

package cn.tedu.straw.faq.service.Impl;

import cn.tedu.straw.commons.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@Component
@Slf4j
public class RibbonClient {
    @Resource
    private RestTemplate restTemplate;

    public User getUser(String username){
        String url="http://sys-service/v1/auth/user?username={1}";
        User user=restTemplate.getForObject(url,User.class, username);
        return user;
    }

    public User[] masters(){
        String url="http://sys-service/v1/users/masters";
        User[] users=restTemplate.getForObject(url, User[].class);
        return users;
    }
}

重构AnswerServicelmpl,利用RibbonClient获取用户信息:

package cn.tedu.straw.faq.service.Impl;

import cn.tedu.straw.faq.mapper.AnswerMapper;
import cn.tedu.straw.faq.mapper.QuestionMapper;

import cn.tedu.straw.commons.model.Answer;
import cn.tedu.straw.commons.model.Question;
import cn.tedu.straw.commons.model.User;
import cn.tedu.straw.faq.service.IAnswerService;
import cn.tedu.straw.commons.service.ServiceException;
import cn.tedu.straw.faq.vo.AnswerVo;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author tedu.cn
 * @since 2021-07-29
 */
@Service
public class AnswerServiceImpl extends ServiceImpl<AnswerMapper, Answer> implements IAnswerService {

    @Resource
    private AnswerMapper answerMapper;

    @Resource
    private QuestionMapper questionMapper;

    @Resource
    private RibbonClient ribbonClient;

    @Override
    @Transactional
    public Answer saveAnswer(AnswerVo answerVo, String username) {
        //User user=userMapper.findUserByUsername(username);
        User user=ribbonClient.getUser(username);
        Answer answer=new Answer()
            .setUserNickName(user.getNickname())
            .setUserId(user.getId())
            .setQuestId(answerVo.getQuestionId())
            .setContent(answerVo.getContent())
            .setLikeCount(0)
            .setCreatetime(LocalDateTime.now())
            .setAcceptStatus(0);
        int rows=answerMapper.insert(answer);
        if(rows !=1){
            throw new ServiceException("数据较忙,请稍后再试!");
        }
        return answer;
    }

    @Override
    public List<Answer> getAnswersByQuestionId(Integer questionId) {
        if(questionId==null){
            throw ServiceException.notFound("QuestionId为空");
        }
        List<Answer> answers=answerMapper.findAnswersByQuestionId(questionId);

        return answers;
    }

    @Override
    @Transactional//两个表同时更新,不间断,非常重要
    public boolean accept(Integer answerId) {
        //查询当前的答案
        Answer answer=answerMapper.selectById(answerId);
        if (answer==null){
            throw ServiceException.notFound("没有找到数据");
        }
        //如果被接受过,则不处理
        if (answer.getAcceptStatus().equals(1)){
            return false;
        }
        int rows=answerMapper.updateAcceptStatus(answerId,1);
        if (rows !=1){
            throw ServiceException.notFound("数据错误或找不到!");
        }
        rows=questionMapper.updateStatus(answer.getQuestId(), Question.SOLVED);
        if(rows !=1){
            throw ServiceException.notFound("数据错误或找不到!");
        }
        log.debug("将问题和答案更新为已经解决的状态");
        return true;
    }
}

4.3 迁移控制器,重构界面模型

迁移业务层后需要重构import,重构获取用户信息的方式,利用Ribbon从straw-sys服务模块获取用户信息。重构straw-commons的User类,增加MASTER属性。

迁移控制器,迁移后更新package:

straw-gateway的HomeContrller中添加显示问题详情页面的方法:

@GetMapping("/question/detail.html")
public String datail(@AuthenticationPrincipal UserDetails userDetails){
    if(userDetails.getAuthorities().contains((STUDENT))){
        return "question/detail";
    }else if(userDetails.getAuthorities().contains(TEACHER)){
        return "question/detail_teacher.html";
    }
    throw new ServiceException("需要登录");
}

重构straw-gateway中的视图控制器,/resource/static/js/detail.js:

url: &%62339;/faq/v1/questions/&%2339;+questionId,
url: &%2339;/faq/v1/answers/question/&%2339;+questionId,
url: &%2339;/faq/v1/comments&%62339;,
url: &%862339;/faq/v1/comments/&%2339;+commentId+&%62339;/delete&%2339;,
url: &%2339;/faq/v1/comments/&%2339;+commentId+&%62339;/update&%2339;,
url: &%862339;/faq/v1/answers/&%2339;+answerId+"/solved" ,

重构straw-gateway中的视图控制器,/resource/static/js/post_ answer.js:

url: &%62339;/faq/v1/answers&%2339;,
posted @ 2022-04-14 22:05  指尖上的未来  阅读(12)  评论(0)    收藏  举报