项目之提问页面-显示问题、发表问题(8)

举报
海拥 发表于 2021/11/25 15:42:26 2021/11/25
【摘要】 🌊 作者主页:海拥🌊 简介:🏆CSDN全栈领域优质创作者、🥇HDZ核心组成员、🥈蝉联C站周榜前十🌊 粉丝福利:粉丝群 每周送六本书,不定期送各种小礼品 30. 提问页面-显示问题标签的下拉列表关于js代码:Vue.component('v-select', VueSelect.VueSelect);let createQuestionApp = new Vue({ el: ...

🌊 作者主页:海拥
🌊 简介:🏆CSDN全栈领域优质创作者、🥇HDZ核心组成员、🥈蝉联C站周榜前十
🌊 粉丝福利:粉丝群 每周送六本书,不定期送各种小礼品

30. 提问页面-显示问题标签的下拉列表

关于js代码:

Vue.component('v-select', VueSelect.VueSelect);
let createQuestionApp = new Vue({
    el: '#createQuestionApp',
    data: {
        tags: [
            {label: 'MyBatis Plus', value: 1},
            {label: 'Spring Security', value: 2},
            {label: 'Spring Validation', value: 3},
            {label: 'Lombok', value: 4},
            {label: 'Vue', value: 5}
        ],
        selectedTags: []
    },
    methods: {
        loadTags: function () {
            $.ajax({
                url: '/api/v1/tags',
                type: 'get',
                success: function(json) {
                    let tags = [];
                    for (let i = 0; i < json.data.length; i++) {
                        let op = {
                            label: json.data[i].name,
                            value: json.data[i].id
                        };
                        tags[i] = op;
                    }
                    createQuestionApp.tags = tags;
                }
            });
        }
    },
    created: function () {
        this.loadTags();
    }
});

31. 提问页面-显示老师的下拉列表

查询老师列表的SQL语句:

select id, nickname, gender, phone from user where type=1 order by id;

先创建TeacherVO类:

@Data
public class TeacherVO {

    private Integer id;
    private String nickname;
    private Integer gender;
    private String phone;

}

UserMapper接口中添加:

/**
 * 查询老师的列表
 *
 * @return 老师的列表
 */
List<TeacherVO> findTeachers();

UserMapper.xml中配置映射:

<select id="findTeachers" resultType="cn.tedu.straw.portal.vo.TeacherVO">
    SELECT
        id, nickname, phone, gender
    FROM
        user
    WHERE
        type=1 AND enabled=1 AND locked=0
    ORDER BY
        id
</select>

UserMapperTests中测试:

@Test
void findTeachers() {
    List<TeacherVO> teachers = userMapper.findTeachers();
    log.debug("teacher count = {}", teachers.size());
    for (TeacherVO teacher : teachers) {
        log.debug(">>> {}", teacher);
    }
}

IUserService中添加:

/**
 * 获取缓存的老师的列表,如果列表为空,还会尝试从数据库查询列表数据,避免因为缓存为空导致无法获取到数据
 *
 * @return 缓存的老师的列表
 */
List<TeacherVO> findTeachers();

/**
 * 获取缓存的老师的列表,由于存在清空缓存机制,获取到的数据将不可靠
 *
 * @return 缓存的老师的列表
 */
List<TeacherVO> findCachedTeachers();

UserServiceImpl中实现以上2个方法:

/**
 * 缓存的老师列表
 */
private List<TeacherVO> teachers = new CopyOnWriteArrayList<>();

@Override
public List<TeacherVO> findTeachers() {
    if (teachers.isEmpty()) {
        synchronized (CacheSchedule.LOCK_CACHE) {
            if (teachers.isEmpty()) {
                teachers.addAll(userMapper.findTeachers());
            }
        }
    }
    return teachers;
}

@Override
public List<TeacherVO> findCachedTeachers() {
    return teachers;
}

CacheSchedule的计划任务中,清除Tag数据缓存时,一并清除Teacher数据缓存:

@Component
@EnableScheduling
@Slf4j
public class CacheSchedule {

    @Autowired
    private ITagService tagService;
    @Autowired
    private IUserService userService;
    /**
     * <p>缓存锁,凡是写入(添加、移除)缓存的数据时使用这个锁</p>
     * <p>public:多个类都需要使用到这把锁</p>
     * <p>static:具有唯一的特性,能保证实现互斥</p>
     * <p>final:不允许任何位置修改或重新创建对象</p>
     * <p>Object:不关心锁的类型,只要是对象,都可以当作锁来用</p>
     */
    public static final Object LOCK_CACHE = new Object();

    @Scheduled(initialDelay = 10 * 60 * 1000, fixedRate = 10 * 60 * 1000)
    public void clearCache() {
        synchronized (LOCK_CACHE) {
            tagService.getCachedTags().clear();
            log.debug("clear tags cache ...");
            userService.findCachedTeachers().clear();
            log.debug("clear teacher cache ...");
        }
    }

}

注意:需要修改原TagServiceImpl中处理缓存数据时使用的锁对象。

UserServiceTests中测试:

@Test
void findTeachers() {
    List<TeacherVO> teachers = userService.findTeachers();
    log.debug("teacher count = {}", teachers.size());
    for (TeacherVO teacher : teachers) {
        log.debug(">>> {}", teacher);
    }
}

关于控制器层:

// http://localhost:8080/api/v1/users/teacher/list
@GetMapping("/teacher/list")
public R<List<TeacherVO>> getTeachers() {
    return R.ok(userService.findTeachers());
}

为了使得为null的数据不会出现在服务器端响应的JSON结果中,可以在application.properties中添加配置:

spring.jackson.default-property-inclusion=non_null

在前端页面中,关于显示“老师”的下拉列表,先将原有的<select>替换为:

<v-select :options="teachers" v-model="selectedTeacherIds"
        :reduce="option => option.value"
        :selectable="() => selectedTeacherIds.length < 3"
        multiple required
        placeholder="请选择回答问题的老师(最多可以选择3个)">
</v-select>

然后,在create.js中,在Vue对象的data中添加teachersselectedTeacherIds这2个属性:

data: {
    tags: [],
    selectedTagIds: [],
    teachers: [],
    selectedTeacherIds: []
}

methods中补充添加新的方法,用于加载数据并填充下拉列表:

loadTeacher: function () {
    $.ajax({
        url: '/api/v1/users/teacher/list',
        type: 'get',
        success: function (json) {
            let teachers = [];
            for (let i = 0; i < json.data.length; i++) {
                let teacher = {
                    label: json.data[i].nickname,
                    value: json.data[i].id
                }
                teachers[i] = teacher;
            }
            createQuestionApp.teachers = teachers;
        }
    });
}

最后,在created中补充调用以上方法:

this.loadTeacher();

完整的js代码:

Vue.component('v-select', VueSelect.VueSelect);
let createQuestionApp = new Vue({
    el: '#createQuestionApp',
    data: {
        tags: [],
        selectedTagIds: [],
        teachers: [],
        selectedTeacherIds: []
    },
    methods: {
        loadTags: function () {
            $.ajax({
                url: '/api/v1/tags',
                type: 'get',
                success: function (json) {
                    let tags = [];
                    for (let i = 0; i < json.data.length; i++) {
                        let op = {
                            label: json.data[i].name,
                            value: json.data[i].id
                        };
                        tags[i] = op;
                    }
                    createQuestionApp.tags = tags;
                }
            });
        },
        loadTeacher: function () {
            $.ajax({
                url: '/api/v1/users/teacher/list',
                type: 'get',
                success: function (json) {
                    let teachers = [];
                    for (let i = 0; i < json.data.length; i++) {
                        let teacher = {
                            label: json.data[i].nickname,
                            value: json.data[i].id
                        }
                        teachers[i] = teacher;
                    }
                    createQuestionApp.teachers = teachers;
                }
            });
        }
    },
    created: function () {
        this.loadTags();
        this.loadTeacher();
    }
});

32. 发表问题-持久层

导入question及相关数据表(共3张表),通过代码生成器项目生成基础代码文件。

打开model包中新生成的实体类,在各实体类之前都添加注解:

@Accessors(chain = true)

则后续创建实体类对象就,就可以使用链式语法更快捷的为属性赋值!

本次“发表问题”时,持久层主要处理的就是“向各数据表中插入数据”,插入数据时,各数据应该都是完整的(将由业务层补全数据),由MyBatis Plus自带的insert()方法足以满足插入数据的需求!

另外,一般情况下,在向任何数据表中插入/删除/修改数据之前,都需要考虑“是否需要通过查询,提前进行相关检查”,考虑的问题大多是“允许插入的数据的数量是否达到上限”、“某些字段的值是否允许重复”、“相关数据是否存在”、“是否具有访问这些数据的权限”……本次需要实现的“发表问题”功能暂时没有需要检查的项。

33. 发表问题-业务层

首先,需要创建一个DTO类,表示用于封装客户端将向服务器端提交的数据的类型!所以,应该先创建一个类,类中的属性与客户端将要提交的数据保持一致即可!则在cn.tedu.straw.portal包中创建dto子包,并在这个包中创建QuestionDTO类:

package cn.tedu.straw.portal.dto;

import lombok.Data;

@Data
public class QuestionDTO {
    
    private String title;
    private Integer[] tagIds;
    private Integer[] teacherIds;
    private String content;
    
}

然后,在业务接口IQuestionService中添加抽象方法:

/**
 * 发布问题
 *
 * @param questionDTO  从客户端提交过来的数据
 * @param uid          当前登录的用户id
 * @param userNickname 用户登录的用户昵称
 */
void create(QuestionDTO questionDTO, Integer uid, String userNickname);

在业务实现类QuestionServiceImpl中规划该抽象方法的实现步骤:

@Autowired
private QuestionMapper questionMapper;
@Autowired
private QuestionTagMapper questionTagMapper;
@Autowired
private UserQuestionMapper userQuestionMapper;

@Transactional
public void create(QuestionDTO questionDTO, Integer uid, String userNickname) {
    // 创建当前时间对象:now
    
    // 将questionDTO中的tagIds转换成例如 2,7,9 这种格式的字符串,名为tagIdsStr
    
    // 创建Question对象
    // 向Question对象中补全数据
    // - title / content > questionDTO的title / content
    // - userId / userNickName > 参数
    // - status > 0
    // - hits > 0
    // - isPublic > 1
    // - createdTime / modifiedTime > now
    // - isDelete > 0
    // - tagIds > tagIdsStr
    // 基于以上Question对象,调用questionMapper的insert()方法,向question表中插入数据,获取返回值
    // 判断返回值是否不为1
    // 是:抛出InsertException
    
    // 遍历questionDTO中的tagIds
    // - 创建QuestionTag对象
    // - 补全属性:questionId > 以上插入Question对象的id
    // - 补全属性:tagId > 被遍历到的数据
    // - 基于以上QuestionTag对象,调用questionTagMapper的insert()方法,向question_tag表中插入数据,以记录“问题”与“标签”的对应关系,并需要获取当前调用方法的返回值
    // - 判断返回值是否不为1
    // - 是:抛出InsertException
    
    // 遍历questionDTO中的teacherIds
    // - 创建UserQuestion对象
    // - 补全属性:questionId > 以上插入Question对象的id
    // - 补全属性:userId > 被遍历到的数据
    // - 补全属性:createdTime > now
    // - 基于以上UserQuestion对象,调用userQuestionMapper的insert()方法,向user_question表中插入数据,以记录“问题”与“回答问题的老师”的对应关系,并需要获取当前调用方法的返回值
    // - 判断返回值是否不为1
    // - 是:抛出InsertException
}
  • 当开发某功能时,如果没有思路,直接编写最后一步,然后,需要什么数据,就在之前补全什么数据。
  • 当缺少某个数据时,这个数据要么直接声明为方法的参数,最终将由方法的调用者来决定数据的值,或者,自行编写相关代码得到这个数据的值。
  • 如果创建了对象,需要检查对象的各属性值,如果某些属性是应该由客户端提交的,可以基于参数赋值或不处理,另一些属性不是由客户端提交的数据,必须补全这些属性的值!

实现业务方法:

@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {

    @Autowired
    private QuestionMapper questionMapper;
    @Autowired
    private QuestionTagMapper questionTagMapper;
    @Autowired
    private UserQuestionMapper userQuestionMapper;

    @Override
    @Transactional
    public void create(QuestionDTO questionDTO, Integer userid, String userNickname) {
        // 创建当前时间对象:now
        LocalDateTime now = LocalDateTime.now();

        // 将questionDTO中的tagIds转换成例如 2,7,9 这种格式的字符串,名为tagIdsStr
        String tagIdsStr = Arrays.toString(questionDTO.getTagIds());    // [2, 7, 9]
        tagIdsStr = tagIdsStr.substring(1, tagIdsStr.length() - 1);     // 2, 7, 9

        // 创建Question对象
        // 向Question对象中补全数据
        // - title / content > questionDTO的title / content
        // - userId / userNickName > 参数
        // - status > 0
        // - hits > 0
        // - isPublic > 1
        // - createdTime / modifiedTime > now
        // - isDelete > 0
        // - tagIds > tagIdsStr
        Question question = new Question()
                .setTitle(questionDTO.getTitle())
                .setContent(questionDTO.getContent())
                .setUserId(userid)
                .setUserNickName(userNickname)
                .setStatus(0).setHits(0).setIsPublic(1).setIsDelete(0)
                .setCreatedTime(now).setModifiedTime(now)
                .setTagIds(tagIdsStr);
        // 基于以上Question对象,调用questionMapper的insert()方法,向question表中插入数据,获取返回值
        int rows = questionMapper.insert(question);
        // 判断返回值是否不为1
        if (rows != 1) {
            // 是:抛出InsertException
            throw new InsertException("发布问题失败!当前服务器忙,请稍后再尝试!");
        }

        // 遍历questionDTO中的tagIds
        for (Integer tagId : questionDTO.getTagIds()) {
            // - 创建QuestionTag对象
            // - 补全属性:questionId > 以上插入Question对象的id
            // - 补全属性:tagId > 被遍历到的数据
            QuestionTag questionTag = new QuestionTag()
                    .setQuestionId(question.getId())
                    .setTagId(tagId);
            // - 基于以上QuestionTag对象,调用questionTagMapper的insert()方法,向question_tag表中插入数据,以记录“问题”与“标签”的对应关系,并需要获取当前调用方法的返回值
            rows = questionTagMapper.insert(questionTag);
            // - 判断返回值是否不为1
            if (rows != 1) {
                // 是:抛出InsertException
                throw new InsertException("发布问题失败!当前服务器忙,请稍后再尝试!");
            }
        }

        // 遍历questionDTO中的teacherIds
        for (Integer teacherId : questionDTO.getTeacherIds()) {
            // - 创建UserQuestion对象
            // - 补全属性:questionId > 以上插入Question对象的id
            // - 补全属性:userId > 被遍历到的数据
            // - 补全属性:createdTime > now
            UserQuestion userQuestion = new UserQuestion()
                    .setQuestionId(question.getId())
                    .setUserId(teacherId)
                    .setCreatedTime(now);
            // - 基于以上UserQuestion对象,调用userQuestionMapper的insert()方法,向user_question表中插入数据,以记录“问题”与“回答问题的老师”的对应关系,并需要获取当前调用方法的返回值
            rows = userQuestionMapper.insert(userQuestion);
            // - 判断返回值是否不为1
            if (rows != 1) {
                // 是:抛出InsertException
                throw new InsertException("发布问题失败!当前服务器忙,请稍后再尝试!");
            }
        }
    }

}

src/test/javacn.tedu.straw.portal.service包中创建业务测试类QuestionServiceTests,编写并执行单元测试:

@SpringBootTest
@Slf4j
public class QuestionServiceTests {

    @Autowired
    IQuestionService service;

    @Test
    void create() {
        try {
            QuestionDTO questionDTO = new QuestionDTO();
            questionDTO.setTitle("SpringSecurity验证时记录了用户的ID吗?");
            questionDTO.setContent("SpringSecurity自动完成验证,可以获取用户名,但是,用户ID在哪里获取?");
            questionDTO.setTagIds(new Integer[] { 5, 8, 13 });
            questionDTO.setTeacherIds(new Integer[] { 2, 3 });
            Integer userId = 5;
            String userNickname = "超级码农";
            service.create(questionDTO, userId, userNickname);
            log.debug("create question ok.");
        } catch (ServiceException e) {
            log.debug("create question failure.", e);
        }
    }

}

34. 发表问题-控制器层

QuestionController中添加处理请求的方法,此次处理请求时,路径可以设计为/api/v1/questions/create,请求类型应该是post,客户端将需要提交QuestionDTO类型的参数,另外,还需要通过@AuthenticationPriciple注入当前登录的用户信息,发表问题成功后,响应R表示成功即可。

@RestController
@RequestMapping("/api/v1/questions")
public class QuestionController {

    @Autowired
    private IQuestionService questionService;

    // http://localhost:8080/api/v1/questions/create?title=Java&content=HelloWorld&tagIds=3&tagIds=9&tagIds=15&teacherIds=1&teacherIds=3
    @RequestMapping("/create")
    public R<Void> create(QuestionDTO questionDTO,
                          @AuthenticationPrincipal UserInfo userInfo) {
        questionService.create(questionDTO, userInfo.getId(), userInfo.getNickname());
        return R.ok();
    }

}

34. 发表问题-补全页面功能

关于提交请求并处理结果的函数:

function () {
    let content = $('#summernote').val();
    console.log("标题:" + this.title);
    console.log("选中的标签:" + this.selectedTagIds);
    console.log("选中的老师:" + this.selectedTeacherIds);
    console.log("正文:" + content);
    $.ajax({
        url: '/api/v1/questions/create',
        type: 'post',
        traditional: true,
        data: {
            title: createQuestionApp.title,
            tagIds: createQuestionApp.selectedTagIds,
            teacherIds: createQuestionApp.selectedTeacherIds,
            content: content
        },
        success: function (json) {
            if (json.state == 2000) {
                alert("发表问题成功!!!")
            } else {
                alert(json.message);
            }
        }
    });
}

作业:显示热点问题列表

通过界面添加不少于10个问题!

查询热点问题列表的SQL语句大致是(暂时不解决“x条回答”的数据):

select id, title, status, hits
from question
where is_public=1 and is_delete=0
order by hits desc
limit 0, 10

则应该先创建新的VO类进行封装:

@Data
public class QuestionListItemVO {
    // 声明id, title, status, hits
}

持久层接口:

List<QuestionListItemVO> findMostHits();

配置SQL语句;

创建QuestionMapperTests测试类,并测试以上方法。

业务层接口:

List<QuestionListItemVO> getMostHits();
List<QuestionListItemVO> getCachedMostHits();

利用缓存的做法实现以上方法。

另行设计计划任务,因为更新“热点问题”缓存的间隔时间应该与“标签”、“老师”的不同,更新缓存的频率应该更高(间隔时间更短)。

控制器设计请求路径为http://localhost:8080/api/v1/questions/hits,将返回R<List<QuestionListItemVO>>

js/commons文件夹下创建question_most_hits.js文件,结合create.html文件,将列表数据显示出来。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。