springboot+jpa+redis+quzartz+elasticsearch实现微信论坛小程序(四)

图片上传

图片存储在七牛云对象存储中,所以首先配置七牛云sdk

pom.xml


    com.qiniu
    qiniu-java-sdk
    [7.2.0, 7.2.99]

application.yml

#七牛云配置
qiniu:
  accessKey: 你的accessKey
  secretKey: 你的secretKey
  bucket: 你的bucket
  path: 你的七牛云地址

七牛云配置类

QiNiuAccountConfig

@Data
@Component
@ConfigurationProperties(prefix = "qiniu")
public class QiNiuAccountConfig {

    private String accessKey;

    private String secretKey;
    /** 创建的存储空间名. */
    private String bucket;
    /** 存储空间的访问域名. */
    private String path;
}

QiniuUploadFileConfig

@Configuration
public class QiniuUploadFileConfig {

    @Autowired
    private QiNiuAccountConfig qiNiuAccountConfig;

    /**
     * 华南机房,配置自己空间所在的区域
     */
    @Bean
    public com.qiniu.storage.Configuration qiniuConfig() {
        return new com.qiniu.storage.Configuration(Zone.zone2());
    }

    /**
     * 构建一个七牛上传工具实例
     */
    @Bean
    public UploadManager uploadManager() {
        return new UploadManager(qiniuConfig());
    }

    /**
     * 认证信息实例
     * @return
     */
    @Bean
    public Auth auth() {
        return Auth.create(qiNiuAccountConfig.getAccessKey(), qiNiuAccountConfig.getSecretKey());
    }

    /**
     * 构建七牛空间管理实例
     */
    @Bean
    public BucketManager bucketManager() {
        return new BucketManager(auth(), qiniuConfig());
    }

    /**
     * Jason转换
     * @return
     */
    @Bean
    public Gson gson() {
        return new Gson();
    }

}

七牛云服务类(图片上传)

写一个服务让帖子模块中的图片上传调用

QiniuServiceImpl

@Service
public class QiniuServiceImpl implements QiniuService {

    @Autowired
    private UploadManager uploadManager;

    @Autowired
    private BucketManager bucketManager;

    @Autowired
    private Gson gson;

    @Autowired
    private Auth auth;

    @Autowired
    private QiNiuAccountConfig qiNiuAccountConfig;


    @Override
    /**
     * 以文件方式上传
     */
    public String uploadFile(File file, String key) throws QiniuException {

        // 上传,若失败重传3次
        Response response = this.uploadManager.put(file, key, getUploadToken());
        int retry = 0;
        while (response.needRetry() && retry < 3) {
            response = this.uploadManager.put(file, null, getUploadToken());
            retry++;
        }
        // 解析上传成功的结果
        DefaultPutRet putRet = gson.fromJson(response.bodyString(), DefaultPutRet.class);
        return qiNiuAccountConfig.getPath() + "/" + putRet.key;
    }

    @Override
    /**
     * 以文件流方式上传
     */
    public String uploadFile(InputStream inputStream, String key) throws QiniuException {

        // 上传,若失败重传3次
        Response response = this.uploadManager.put(inputStream, key, getUploadToken(), null, null);
        int retry = 0;
        while (response.needRetry() && retry < 3) {
            response = this.uploadManager.put(inputStream, null, getUploadToken(), null, null);
            retry++;
        }
        // 解析上传成功的结果
        DefaultPutRet putRet = gson.fromJson(response.bodyString(), DefaultPutRet.class);
        return qiNiuAccountConfig.getPath() + "/" + putRet.key;
    }

    @Override
    /**
     * 删除上传的文件
     */
    public Response delete(String key) throws QiniuException {
        Response response = bucketManager.delete(qiNiuAccountConfig.getBucket(), key);
        int retry = 0;
        while (response.needRetry() && retry++ < 3) {
            response = bucketManager.delete(qiNiuAccountConfig.getBucket(), key);
        }
        return response;
    }

    /**
     * 获取上传凭证
     * @return
     */
    private String getUploadToken() {
        return this.auth.uploadToken(qiNiuAccountConfig.getBucket());
    }

}

帖子模块

帖子模块主要内容在于service,就省略daocontroller的内容了。

图片上传删除

首先是帖子中图片上传与删除方法

@Override
    /**
     * 上传图片
     */
    public String uploadImg(File file) throws QiniuException {
        // 1.创建存储url的字符串
        String imgUrl = new String();
        // 2.判断文件是否存在
        if (!file.exists()){
            return imgUrl;
        }
        // 3.使用KeyUtil生成唯一主键作为key进行上传,返回图片url
        imgUrl = qiniuService.uploadFile(file, "bbs/" + KeyUtil.genUniqueKey() + "-web");
        return imgUrl;
    }

    @Override
    /**
     * 删除图片
     */
    public Response deleteImg(String imgUrl) throws QiniuException {
        String key = imgUrl.substring(25, imgUrl.length() - 4);
        return qiniuService.delete(key);
    }

创建帖子

接收帖子的内容和图片url,并将url拼接到帖子的articleImg字段中,es的存储暂时先不说,后面会提到

@Override
    /**
     * 创建帖子
     */
    @Transactional
    public Article createArticle(String userId, ArticleForm articleForm){
        Article article = new Article();
        if (!articleForm.getImgUrls().isEmpty()){
            // 1.获取图片url列表
            List imgUrls = articleForm.getImgUrls();
            // 2.将字符串拼接存入帖子字段
            StringBuffer stringBuffer = new StringBuffer();
            for (int i = 0 ; i < imgUrls.size(); i++){
                stringBuffer.append(imgUrls.get(i));
                if (i != imgUrls.size() - 1) stringBuffer.append(";");
            }
            // 3.将生成的图片url给帖子的图片字段
            article.setArticleImg(stringBuffer.toString());
        }
        // 4.将帖子其余信息保存
        BeanUtils.copyProperties(articleForm, article);
        article.setArticleId(KeyUtil.genUniqueKey());
        article.setArticleUserId(userId);
        // 5.计算帖子热度
        article = calcHotNum(article);
        // 6.将帖子存入数据库
        article = articleRepository.save(article);
        // 7.将帖子存入es,以便搜索
        article = articleSearchRepository.save(article);
        // 8.创建帖子需要从redis同步一遍帖子数据,以便排序
        updateArticleDatabase();
        // 9.将用户发帖数+1
        User user = userService.findUser(userId);
        user.setUserArticleNum(user.getUserArticleNum() + 1);
        userService.saveUser(user);
        return article;
    }

帖子查找方法

先从redis中找,如果没有就去数据库中找,找到后存入hash类型,设置过期时间

@Override
    /**
     * 从redis中或数据库中查找帖子
     */
    public Article getArticle(String articleId) throws BBSException{
        Article article;
        if (redisTemplate.hasKey("Article::" + articleId)){
            article = EntityUtils.hashToObject(redisTemplate.opsForHash().entries("Article::" + articleId), Article.class);
        }
        else {
            article = articleRepository.findArticle(articleId);
            if (article == null){
                throw new BBSException(ResultEnum.ARTICLE_NOT_EXIT);
            }
            redisTemplate.opsForHash().putAll("Article::" + articleId, EntityUtils.objectToHash(article));
            redisTemplate.expire("Article::" + articleId, 1, TimeUnit.HOURS);
        }
        return article;
    }

帖子浏览

调用getArticle,将帖子浏览数+1,然后再拼装成VO返回

@Override
    /**
     * 浏览帖子
     */
    public ArticleVO findArticle(String articleId, String userId) throws BBSException {
        Article article = getArticle(articleId);
        ArticleVO articleVO;
        // 2.如果存在这篇帖子,将帖子浏览数+1,存入redis中
        redisTemplate.opsForHash().increment("Article::" + articleId, "articleViewNum", 1);
        article.setArticleViewNum(article.getArticleViewNum() + 1);
//        redisTemplate.opsForValue().set("Article::" + articleId, article, 1, TimeUnit.HOURS);

        articleVO = article2articleVO(article, userId);
        return articleVO;
    }

帖子删除

使用软删除,标志位置1,但redis的缓存要删除,删了的帖子没必要在redis中存留

@Override
    /**
     * 删除帖子
     */
    @Transactional
    public void deleteArticle(String articleId) throws BBSException {
        // 1.获取到帖子
        Article article = getArticle(articleId);
        // 2.将删除位置为1
        article.setArticleIsDelete(DeleteEnum.DELETE.getCode());
        // 3.更新数据库及es并删除缓存
        redisTemplate.delete("Article::" + articleId);
        articleRepository.save(article);
        articleSearchRepository.save(article);
        // 4.用户帖子数-1
        User user = userService.findUser(article.getArticleUserId());
        user.setUserArticleNum(user.getUserArticleNum() - 1);
        userService.saveUser(user);
    }

帖子热度计算

根据帖子的浏览量、评论数、点赞数及时间对帖子热度,各自有权重进行计算

    private static final Double VIEW_NUM_WEIGHT = 1.0;
    private static final Double COMMENT_NUM_WEIGHT = 200.0;
    private static final Double LIKE_NUM_WEIGHT = 200.0;
    private static final Double INIT_VALUE =  100.0;
/**
     * 计算帖子热度
     * @param article
     * @return
     */
    private Article calcHotNum(Article article){
        Double hotNum;
        if (article.getArticleCreateTime() != null){
            Double deltaTime = (System.currentTimeMillis() - article.getArticleCreateTime().getTime()) / 86400000.0;
            hotNum = (INIT_VALUE + LIKE_NUM_WEIGHT * article.getArticleLikeNum()
                    + VIEW_NUM_WEIGHT * article.getArticleViewNum()
                    + COMMENT_NUM_WEIGHT * article.getArticleCommentNum()) / Math.pow(E, deltaTime);
        }
        else {
            hotNum = (INIT_VALUE + LIKE_NUM_WEIGHT * article.getArticleLikeNum()
                    + VIEW_NUM_WEIGHT * article.getArticleViewNum()
                    + COMMENT_NUM_WEIGHT * article.getArticleCommentNum()) / Math.pow(E, 0);
        }
        article.setArticleHotNum(hotNum);
        return article;
    }

缓存更新策略

采用定时任务去把缓存同步到数据库,定时任务后面再说:

@Override
    /**
     * 从redis更新帖子数据
     */
    @Transactional
    public void updateArticleDatabase() {
        // 1.找到所有关于帖子的key
        Set articleKeys = redisTemplate.keys("Article::*");
        for (String articleKey : articleKeys){
            // 2.根据每一个key得到帖子
            Article article = EntityUtils.hashToObject(redisTemplate.opsForHash().entries(articleKey), Article.class);
            // 3.更新帖子热度
            article = calcHotNum(article);
            // 4.保存帖子进数据库及es,再更新redis里的数据
            article = articleRepository.save(article);
            articleSearchRepository.save(article);
            redisTemplate.opsForHash().putAll(articleKey, EntityUtils.objectToHash(article));
            redisTemplate.expire(articleKey, redisTemplate.getExpire(articleKey), TimeUnit.SECONDS);
//            redisTemplate.delete(articleKey);
        }
        return;
    }

文章内容拼装

最后是关于VO拼装的部分,分页查询列表之类的就和平时的查询一样,就不列举了:

/**
     * 文章内容拼装
     * @param article
     * @param userId
     * @return
     */
    private ArticleVO article2articleVO(Article article, String userId){
        ArticleVO articleVO = new ArticleVO();
        // 获得帖子信息
        BeanUtils.copyProperties(article, articleVO);
        // 查找作者信息
        User user = userService.findUser(article.getArticleUserId());
        BeanUtils.copyProperties(user, articleVO);
        // 查看作者身份
        articleVO.setUserRole(user.getUserRoleType());
        // 查看是不是当前用户所发帖子
        if (userId != null){
            articleVO.setIsOneself(userId.equals(user.getUserId()));
        }
        // 设定时间
        articleVO.setArticleCreateTime(Date2StringConverter.convert(article.getArticleCreateTime()));
        // 获得图片url
        if (article.getArticleImg() != null){
            articleVO.setArticleImages(Arrays.asList(article.getArticleImg().split(";")));
        }
        // 查找关键词信息
        if (article.getArticleKeywords() != null){
            articleVO.setKeywords(Arrays.asList(article.getArticleKeywords().split("[ ]+")));
        }
        // 查看帖子是否被当前用户点赞
        articleVO.setIsLike(likeService.isArticleLike(article.getArticleId(), userId));
        // 查看帖子是否被当前用户收藏
        articleVO.setIsCollect(collectService.isArticleCollect(article.getArticleId(), userId));
        // 查看帖子是否被删除
        articleVO.setIsDelete(article.getArticleIsDelete().equals(DeleteEnum.DELETE.getCode()));
        return articleVO;
    }

评论部分和帖子部分大同小异,就先不说了。。提一下es

帖子搜索

es的安装网上教程很多,记住版本要对应,ik分词器需要安装一下,同样也是版本要对应

pom.xml


    org.springframework.boot
    spring-boot-starter-data-elasticsearch

说一下注意的点,启动类注解区分jpaes,里面写上包路径,同时esredis都用了netty,可能因为netty版本冲突,所以es.set.netty.runtime.available.processors设置为false

@SpringBootApplication
@EnableCaching
@EnableJpaAuditing
@EnableJpaRepositories("com.ccnu.bbs.repository")
@EnableElasticsearchRepositories("com.ccnu.bbs.searchRepository")
public class BbsApplication {

    public static void main(String[] args) {
        System.setProperty("es.set.netty.runtime.available.processors","false");
        SpringApplication.run(BbsApplication.class, args);
    }

}

然后再需要全文检索的字段上加上注解:

@Field(type = FieldType.Text, analyzer = "ik_max_word")

单独建一个包叫searchRepository,里面写一个接口继承ElasticsearchRepository接口:

public interface ArticleSearchRepository extends ElasticsearchRepository {}

然后就开始写搜索模块:

searchArticle

@Override
    /**
     * 帖子搜索!!
     */
    public Page searchArticle(String searchKey, Pageable pageable) {
        // 1.设置对应字段的权重分值
        // 创建一个FunctionScoreQueryBuilder.FilterFunctionBuilder对象数组
        List filterFunctionBuilders = new ArrayList<>();
        filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("articleKeywords", searchKey),
                ScoreFunctionBuilders.weightFactorFunction(5)));
        filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("articleTitle", searchKey),
                ScoreFunctionBuilders.weightFactorFunction(5)));
        filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("articleContent", searchKey),
                ScoreFunctionBuilders.weightFactorFunction(2)));
        FunctionScoreQueryBuilder.FilterFunctionBuilder[] builders = new FunctionScoreQueryBuilder.FilterFunctionBuilder[filterFunctionBuilders.size()];
        filterFunctionBuilders.toArray(builders);
        // 将FunctionScoreQueryBuilder.FilterFunctionBuilder对象数组作为构造器参数传入
        FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(builders).
                scoreMode(FunctionScoreQuery.ScoreMode.SUM). //设定分值为分数之和
                setMinScore(5); //超过5分才查询
        // 已经删除的帖子不查询
        QueryBuilder queryBuilder = QueryBuilders.termQuery("articleIsDelete", 0);
        // 用boolQuery()构造多重查询条件
        QueryBuilder qb = QueryBuilders.boolQuery().must(functionScoreQueryBuilder).must(queryBuilder);
        // 创建搜索 DSL 查询
        SearchQuery searchQuery = new NativeSearchQueryBuilder().
                withQuery(qb).
                withPageable(pageable).build();
//        log.info("\n searchArticle(): searchContent [" + searchKey + "] \n DSL  = \n " + searchQuery.getQuery().toString());
        // 搜索,获取结果
        Page
articles = articleSearchRepository.search(searchQuery); // 2.对每一篇帖子进行拼装 List articleVOList = articles.stream(). map(e -> article2articleVO(e, null)).collect(Collectors.toList()); return new PageImpl(articleVOList, pageable, articles.getTotalElements()); }

主要的业务模块就差不多了,用户模块比较简单就不写了,下一章完成帖子点赞、定时任务模块。至于收藏和关注模块,逻辑和点赞差不多,就略过了。
上一篇:springboot+jpa+redis+quzartz+elasticsearch实现微信论坛小程序(三)
下一篇:springboot+jpa+redis+quzartz+elasticsearch实现微信论坛小程序(五)

你可能感兴趣的:(springboot+jpa+redis+quzartz+elasticsearch实现微信论坛小程序(四))