一:审核流程
1.需求分析
在上一篇文章中我们提到了创作者创作完成之后可以选择提交草稿或者提交审核,当创作者选择提交审核时候后台就需要对文章内容及封面等进行审核,以避免出现违规内容。在我项目中我使用的自动审核工具为腾讯云的文本内容安全和图片内容安全,新用户可以免费试用一个月。至于项目如何整合内容安全我这里就不再赘述了,可以参考我这篇文章:使用腾讯云进行安全检测
2.实现流程
编辑
当创作者点击提交文章审核之后,就需要异步调用自动审核方法实现对文章文本及图片的审核,我们要先从文章中提取出文本信息(包括标题)及图片信息(包括封面),然后再分别调用不同的方法分别对文本及图片进行审核。审核结果有三种,可以分为审核通过、审核不通过、审核不确定。当审核通过之后可以直接调用文章微服务中的方法来将文章进行保存并发布,当审核不通过时候则修改文章状态为“审核不通过”,当审核结果为不确定时候这时候就需要人工进行审核,这一部分功能我们后面再实现,现在只需要修改文章状态即可。
二:移动端保存文章接口
1.分布式ID
随着业务的增长,文章表可能要占用很大的物理存储空间,为了解决该问题,后期使用数据库分片技术。将一个数据库进行拆分,通过数据库中间件连接。如果数据库中该表选用ID自增策略,则可能产生重复的ID,此时应该使用分布式ID生成策略来生成ID。
编辑 snowflake(雪花算法)是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0
编辑
文章端相关的表都使用雪花算法生成id,包括ap_article、 ap_article_config、 ap_article_content
mybatis-plus已经集成了雪花算法,完成以下两步即可在项目中集成雪花算法
第一:在实体类中的id上加入如下配置,指定类型为AUTO
@TableId(value = "id",type = IdType.ASSIGN_ID) private Long id;
第二:在application.yml文件中配置数据中心id和机器id
mybatis-plus: mapper-locations: classpath*:mapper/*.xml # 设置别名包扫描路径,通过该属性可以给包中的类注册别名 type-aliases-package: com.my.model.article.pojos global-config: datacenter-id: 1 workerId: 1
datacenter-id:数据中心id(取值范围:0-31)
workerId:机器id(取值范围:0-31)
2.思路分析
编辑 还记得前面我们说过为了减少数据库压力我们在移动端文章表设置了三张表,分别为文章信息表,文章配置表,文章内容表。当文章完成审核将文章信息传到文章微服务进行保存时候同样也需要先根据文章是否存在id来判断是修改文章还是保存文章,假如文章存在id则直接修改文章信息即可。假如不存在id则需要保存文章信息、文章配置、文章内容。
3.Feign接口
(1)接口说明
说明 | |
接口路径 | /api/v1/article/save |
请求方式 | POST |
参数 | ArticleDto |
响应结果 | ResponseResult |
ArticleDto
package com.my.model.article.dtos; import com.my.model.article.pojos.ApArticle; import lombok.Data; @Data public class ArticleDto extends ApArticle { /** * 文章内容 */ private String content; }
(2)代码实现
在tbug-headlines-feign-api中新增接口:
①导入feign依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
② 定义文章端接口
编辑
package com.my.apis.article; import com.my.model.article.dtos.ArticleDto; import com.my.model.common.dtos.ResponseResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; @FeignClient(value = "headlines-article") public interface IArticleClient { /** * 保存文章至App端 * @param dto * @return */ @PostMapping("/api/v1/article/save") ResponseResult saveArticle(@RequestBody ArticleDto dto) ; }
在tbug-headlines-article中实现方法
编辑
package com.my.article.feign; import com.my.apis.article.IArticleClient; import com.my.article.service.ApArticleService; import com.my.model.article.dtos.ArticleDto; import com.my.model.common.dtos.ResponseResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController public class ArticleClient implements IArticleClient { @Autowired private ApArticleService apArticleService; @Override @PostMapping("/api/v1/article/save") public ResponseResult saveArticle(ArticleDto dto) { return apArticleService.saveArticle(dto); } }
在ApArticleService中新增方法:
/** * 保存app端文章 * @param dto * @return */ ResponseResult saveArticle(ArticleDto dto);
实现类:
@Autowired private ApArticleConfigMapper apArticleConfigMapper; @Autowired private ApArticleContentMapper apArticleContentMapper; @Autowired private ArticleFreemarkerService articleFreemarkerService; /** * 保存app端文章 * @param dto * @return */ @Override public ResponseResult saveArticle(ArticleDto dto) { log.info("开始保存或修改app端文章..."); //1.参数校验 if(dto == null) { return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID); } //2.拷贝属性 ApArticle apArticle = new ApArticle(); BeanUtils.copyProperties(dto,apArticle); //3.文章id为空,直接保存 if(dto.getId() == null) { log.info("保存文章..."); //3.1保存文章 save(apArticle); //3.2保存文章配置 ApArticleConfig apArticleConfig = new ApArticleConfig(apArticle.getId()); apArticleConfigMapper.insert(apArticleConfig); //3.3保存文章内容 ApArticleContent apArticleContent = new ApArticleContent(); apArticleContent.setArticleId(apArticle.getId()); apArticleContent.setContent(dto.getContent()); apArticleContentMapper.insert(apArticleContent); } else { log.info("修改文章..."); //4.文章存在id,修改文章内容 //4.1更新文章信息 updateById(apArticle); //4.2更新文章内容 String content = dto.getContent(); ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers. <ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, dto.getId())); apArticleContent.setContent(content); apArticleContentMapper.updateById(apArticleContent); //4.3修改文章状态 ApArticleConfig apArticleConfig = apArticleConfigMapper.selectOne(Wrappers.<ApArticleConfig>lambdaQuery(). eq(ApArticleConfig::getArticleId, dto.getId())); if(apArticleConfig != null && apArticleConfig.getIsDown()) { apArticleConfig.setIsDown(false); apArticleConfigMapper.updateById(apArticleConfig); } } //5.异步调用生成静态页面上传值MinIO articleFreemarkerService.buildArticleToMinIO(apArticle,dto.getContent()); return ResponseResult.okResult(apArticle.getId()); }
上传静态页面到MinIO方法:
/** * 异步生成静态页面到Minio * @param apArticle * @param apArticleContent */ @Override @Async public void buildArticleToMinIO(ApArticle apArticle, String apArticleContent) { StringWriter out = new StringWriter(); if(StringUtils.isNotBlank(apArticleContent)){ try { if(apArticle.getStaticUrl() != null) { //删除原来Url fileStorageService.delete(apArticle.getStaticUrl()); log.info("成功删除原来的html静态文件"); } log.info("开始生成新的静态html文件..."); //1.文章内容通过freemarker生成html文件 Template template = configuration.getTemplate("article.ftl"); Map<String, Object> params = new HashMap<>(); params.put("content", JSONArray.parseArray(apArticleContent)); template.process(params, out); } catch (Exception e) { e.printStackTrace(); } //2.把html文件上传到minio中 InputStream is = new ByteArrayInputStream(out.toString().getBytes()); String path = fileStorageService.uploadHtmlFile("", apArticle.getId() + ".html", is); log.info("将html文件上传到minio中:{}",path); //3.修改ap_article表,保存static_url字段 ApArticle article = new ApArticle(); article.setId(apArticle.getId()); article.setStaticUrl(path); apArticleMapper.updateById(article); //4.创建索引,发送消息 createArticleESIndex(article,apArticleContent,path); } }
关于异步调用,要在相应的启动类上面添加@EnableAsync注解