图片上传
图片存储在七牛云对象存储中,所以首先配置七牛云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,就省略dao和controller的内容了。
图片上传删除
首先是帖子中图片上传与删除方法
@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
说一下注意的点,启动类注解区分jpa和es,里面写上包路径,同时es和redis都用了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实现微信论坛小程序(五)