文章发布功能

 /*** 新增文章** @param dto 参数* @return 响应信息*/@Overridepublic ResponseResult submitNews(WmNewsDto dto) {//0.参数检验if (dto == null || dto.getContent() == null){return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);}//1.判断是新增还是修改WmNews wmNews = BeanUtil.copyProperties(dto, WmNews.class);List<String> images = dto.getImages();//将list拆分为以,分割的字符串if (CollUtil.isNotEmpty(images)){String join = StringUtils.join(images, ",");wmNews.setImages(join);}//如果当前封面类型为自动 -1if(dto.getType().equals(WemediaConstants.WM_NEWS_TYPE_AUTO)){wmNews.setType(null);}saveOrUpdateWmNews(wmNews);//2.判断是否为草稿if (dto.getStatus().equals(WmNews.Status.NORMAL.getCode())){return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);}//3.如果不为草稿 保存素材与内容List<String> materials = getPictureInformation(wmNews.getContent());saveRelativeInfoForContent(wmNews.getId(),materials);//4.如果不为草稿 保存素材与封面saveRelativeInfoForCover(wmNews,materials,dto);
//        wmNewsAutoScanService.autoScanWmNews(wmNews.getId());wmNewsTaskService.addNewsToTask(wmNews.getId(),wmNews.getPublishTime());return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);}

该功能先通过bean拷贝的方式,将dto转换为WmNews类型。接着进行新增或者修改的判断,这里通过判断image也就是封面属性是否存在的方式,如果存在,将集合转换为字符串类型,这里通过用StringUtils的join方法,将封面图片转换为字符串并以逗号分割。如果当前是新增 且封面为自动。设置type为null。接着进行状态的修改。文章发布后默认上架。如果id为空,证明当前是新增操作,直接保存。

private void saveOrUpdateWmNews(WmNews wmNews) {//补全属性wmNews.setUserId(WmThreadLocalUtil.getUser().getId());wmNews.setCreatedTime(new Date());wmNews.setSubmitedTime(new Date());//默认上架wmNews.setEnable((short)1);if (wmNews.getId() == null){//新增save(wmNews);}else {//删除素材//修改文章wmNewsMaterialMapper.delete(Wrappers.<WmNewsMaterial>lambdaQuery().eq(WmNewsMaterial::getNewsId,wmNews.getId()));updateById(wmNews);}}

如果当前为草稿,直接返回。

   //2.判断是否为草稿if (dto.getStatus().equals(WmNews.Status.NORMAL.getCode())){return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);}

如果不为草稿,先将图片内容提取出来。这里通过转为JSON串的形式,提取type中的image属性来获取url,并存在数组当中。

  /*** 将文章内容中关于图片部分提取出来* @param content 文章内容* @return 图片url地址*/private List<String> getPictureInformation(String content) {List<String> materials = new ArrayList<>();List<Map> maps = JSON.parseArray(content, Map.class);for (Map map : maps) {if (map.get("type").equals("image")){String imageUrl = (String) map.get("value");materials.add(imageUrl);}}return materials;}

接着调用方法保存素材与文章的关系

先查找素材内容,如果素材有一张不存在或者素材数量不对应,则抛出异常。如果全部找到,则提取出id,并保存到素材文章关系表中。

   /*** 保存素材文章关系* @param materials 素材集合* @param id 文章id* @param wmContentReference 文章类型*/private void saveRelativeInfo(List<String> materials, Integer id, Short wmContentReference) {if (CollUtil.isNotEmpty(materials)){List<WmMaterial> wmMaterials = wmMaterialMapper.selectList(Wrappers.<WmMaterial>lambdaQuery().in(WmMaterial::getUrl, materials));//判断素材是否有效if(CollUtil.isEmpty(wmMaterials)){//手动抛出异常   第一个功能:能够提示调用者素材失效了,第二个功能,进行数据的回滚throw new CustomException(AppHttpCodeEnum.MATERIASL_REFERENCE_FAIL);}if (materials.size() != wmMaterials.size()){throw new CustomException(AppHttpCodeEnum.MATERIASL_REFERENCE_FAIL);}//将素材id提取出来List<Integer> wmMaterialsIds = wmMaterials.stream().map(WmMaterial::getId).collect(Collectors.toList());//进行保存wmNewsMaterialMapper.saveRelations(wmMaterialsIds,id,wmContentReference);}}

接着进行文章封面的提取

根据规则,文章内容图片大于一张小于三张为单封面文章,文章内容图片大于三张的为多封面文章,文章没有图片的为无封面文章进行匹配。并更新文章信息匹配完成后,如果存在封面属性,则将他保存到素材与文章管理表中。

接着就是将该处理后的文章通过异步调用添加到任务中进行自动审核与延时发布。

这里通过远程调用首先将文章转换为task添加到数据库中。并生成taskInfoLog添加到数据库中

*** 将文章添加到延时任务中* 进行异步调用* @param id          当前文章id* @param publishTime 发布时间*/@Override@Asyncpublic void addNewsToTask(Integer id, Date publishTime) {log.info("将任务放到延时队列中 开始");//1.构建Task对象Task task = new Task();task.setPriority(TaskTypeEnum.NEWS_SCAN_TIME.getPriority());task.setTaskType(TaskTypeEnum.NEWS_SCAN_TIME.getTaskType());task.setExecuteTime(publishTime.getTime());WmNews wmNews = new WmNews();wmNews.setId(id);task.setParameters(ProtostuffUtil.serialize(wmNews));scheduleClient.addTask(task);log.info("将任务放到延时队列中 结束");}

这里的数据库采用了乐观锁,防止重复消费的问题。

接着还需要将数据库中的数据引入redis,这里采用list与zset结合的方式,如果当前时间小于发布时间,存入list立即发布。如果当前小于五分钟存入zset延时发布。

 /*** 将数据加入缓存中* @param task 延时任务*/private void addDataToCache(Task task) {String key = task.getTaskType() + ":" + task.getPriority();//获取5分钟之后的时间  毫秒值Calendar calendar = Calendar.getInstance();calendar.add(Calendar.MINUTE, 5);long nextScheduleTime = calendar.getTimeInMillis();if (task.getExecuteTime() <= System.currentTimeMillis()){//2.1如果当前任务是马上执行 存入listcacheService.lLeftPush(ScheduleConstants.TOPIC + key, JSONUtil.toJsonStr(task));}else if (task.getExecuteTime() <= nextScheduleTime){//2.2如果当前任务需要五分钟后执行 存入zSetcacheService.zAdd(ScheduleConstants.FUTURE + key,JSONUtil.toJsonStr(task),task.getExecuteTime());}}

接着需要制定两个定时任务 将数据库内容同步到redis 将zset内容同步到list。这里消费前会将原有的cache清除,为防止重复消费。

   /*** 将数据库中的数据同步到缓存之中*/@PostConstruct@Scheduled(cron = "0 */5 * * * ?")public void reloadData() {//清除原有的缓存clearCache();//获取5分钟之后的时间Calendar calendar = Calendar.getInstance();calendar.add(Calendar.MINUTE, 5);Date calendarTime = calendar.getTime();//在数据库中进行查询LambdaQueryWrapper<Taskinfo> qw = new LambdaQueryWrapper<>();qw.lt(Taskinfo::getExecuteTime,calendarTime);List<Taskinfo> taskinfos = taskinfoMapper.selectList(qw);//如果查询出来的数据不为空 将查询出来的数据同步到redis缓存中if (CollUtil.isNotEmpty(taskinfos)){for (Taskinfo taskinfo : taskinfos) {Task task = BeanUtil.copyProperties(taskinfo, Task.class);task.setExecuteTime(taskinfo.getExecuteTime().getTime());addDataToCache(task);}}log.info("数据库中的任务同步到了redis中");}
   /*** 定时更新数据*/@Scheduled(cron = "0 */1 * * * ?")public void refresh(){String token = cacheService.tryLock("FUTURE:TASK:SYNC", 1000 * 30);if (StrUtil.isNotEmpty(token)){log.info("开始执行未来定时任务 zSet转入list");//获取未来的任务Set<String> futureKeys = cacheService.scan(ScheduleConstants.FUTURE + "*");if (CollUtil.isNotEmpty(futureKeys)){for (String futureKey : futureKeys) {//1.获得小于当前时间的keySet<String> tasks = cacheService.zRangeByScore(futureKey, 0, System.currentTimeMillis());if (CollUtil.isNotEmpty(tasks)){//拼接list集合中的keyString topicKey = ScheduleConstants.TOPIC + futureKey.split(ScheduleConstants.FUTURE)[1];cacheService.refreshWithPipeline(futureKey,topicKey,tasks);log.info("成功的将freshKey" + futureKey + "刷新到了" + topicKey);}}}}}

接着每秒进行文章的拉取,查询出符合条件的文章后进行自动审核。

/*** 进行文章的任务消费 审核文章*/@Override@Scheduled(fixedRate = 1000)public void scanNewsByTask() {ResponseResult responseResult = scheduleClient.pullTask(TaskTypeEnum.NEWS_SCAN_TIME.getTaskType(),TaskTypeEnum.NEWS_SCAN_TIME.getPriority());if (responseResult.getCode().equals(200) && responseResult.getData()!=null){log.info("开始消费文章");Task task = JSON.parseObject(JSON.toJSONString(responseResult.getData()), Task.class);WmNews wmNews = ProtostuffUtil.deserialize(task.getParameters(), WmNews.class);wmNewsAutoScanService.autoScanWmNews(wmNews.getId());log.info("文章消费完成");}}

自动审核调用了阿里云接口与ocr图像识别还管理了一套敏感词来实现。首先,判断当前文章状态是否为提交状态,如果为提交状态,再进行审核。接着,提取出图片中的文字进行敏感词审核。如果过全部成功,则修改状态为审核成功,如果不能审核,则修改状态为需要人工审核。最后,进行app端文章的保存,与自媒体端数据的回填与保存。

 /*** 自媒体文章审核** @param id 自媒体文章id*/@Override@Async@GlobalTransactionalpublic void autoScanWmNews(Integer id) {//提取自媒体文章WmNews wmNews = wmNewsMapper.selectById(id);//如果文章不存在 抛出异常if (wmNews == null){throw new RuntimeException("WmNewsAutoScanServiceImpl--文章不存在");}//如果为提交状态 再进行审核if (wmNews.getStatus().equals(WmNews.Status.SUBMIT.getCode())){Map<String, Object> textAndImages = extractArticleContentPictures(wmNews);//敏感词检查Boolean sensitiveWordFlag = sensitiveWordCheck((String) textAndImages.get("text"),wmNews);if (!sensitiveWordFlag){return;}//todo 调用阿里云文本接口进行检查Boolean textFlag = checkText((String) textAndImages.get("text"),wmNews);if (!textFlag){return;}//todo 调用阿里云图片接口进行检查//将图片进行ocr文字识别Boolean imageFlag = checkImage((List<String>) textAndImages.get("images"),wmNews);if (!imageFlag){return;}//进行保存ResponseResult responseResult = saveAppArticle(wmNews);if (responseResult.getCode() != 200){updateWmNews(wmNews,WmNews.Status.ADMIN_AUTH.getCode(),null);throw new RuntimeException("WmNewsAutoScanServiceImpl--自动审核失败");}//回填数据wmNews.setArticleId((Long) responseResult.getData());updateWmNews(wmNews,WmNews.Status.PUBLISHED.getCode(),"审核通过");}}

文字保存代码如下:首先进行文章的参数检验。接着根据id来判定是新增还是修改,接着通过freemaker模板引擎异步调用自动生成页面url

 /*** 文章保存 远程调用接口** @param articleDto 文章内容* @return id*/@Override@PostMapping("/api/v1/article/save")public ResponseResult saveArticle(@RequestBody ArticleDto articleDto) {
//        try {
//            Thread.sleep(3000);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }//参数校验if (articleDto == null){return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);}ApArticle apArticle = BeanUtil.copyProperties(articleDto, ApArticle.class);//不存在文章 进行新增if (articleDto.getId() == null){//1.新增文章表articleMapper.insert(apArticle);//2.新增文章内容表ApArticleContent apArticleContent = new ApArticleContent(null, apArticle.getId(), articleDto.getContent());articleContentMapper.insert(apArticleContent);//3.新增文章配置表ApArticleConfig apArticleConfig = new ApArticleConfig(apArticle.getId());articleConfigMapper.insert(apArticleConfig);}else {//存在文章进行修改articleMapper.updateById(apArticle);//对文章内容进行修改ApArticleContent apArticleContent = articleContentMapper.selectOne(Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, apArticle.getId()));apArticleContent.setContent(articleDto.getContent());articleContentMapper.updateById(apArticleContent);}//异步将文章静态页面存入articleFreemakerService.buildArticleToMinIO(apArticle,articleDto.getContent());return ResponseResult.okResult(apArticle.getId());}

页面生成代码如下所示。freemaker先找到自己的模板引擎,再将数据推送给引擎实现代码的生成,接着通过minio文件的上传,将生成后的网页上传到minio中最后保存文章信息,并向kafka发送一条消息用于es数据的同步。

 /*** 将生成后的静态文件上传到minio** @param apArticle 当前文章* @param content   当前文章内容*/@Async@Overridepublic void buildArticleToMinIO(ApArticle apArticle, String content) {StringWriter writer = new StringWriter();if (StrUtil.isNotEmpty(content)){try {//2.生成freemaker文件Template template = configuration.getTemplate("article.ftl");Map<String,Object> dataModel = new HashMap<>();dataModel.put("content", JSONUtil.parseArray(content));//生成文件template.process(dataModel,writer);}catch (Exception ex){ex.printStackTrace();}//3.将文件存储到minio中ByteArrayInputStream inputStream = new ByteArrayInputStream(writer.toString().getBytes());String path = fileStorageService.uploadHtmlFile("", apArticle.getId() + ".html", inputStream);//4.将path存入articleContent//4.修改ap_article表,保存static_url字段ApArticle article = new ApArticle();article.setId(apArticle.getId());article.setStaticUrl(path);//发送消息到kafka 用于es的数据同步sendMessage(apArticle,content,path);articleMapper.updateById(article);}}

当es接收到kafka消息时,就往app_info_article索引库以文章id为主键,添加es的索引。

   /*** 增加索引* @param message*/@KafkaListener(topics = ArticleConstants.ARTICLE_ES_SYNC_TOPIC)public void onMessage(String message){if(StringUtils.isNotBlank(message)){log.info("SyncArticleListener,message={}",message);SearchArticleVo searchArticleVo = JSON.parseObject(message, SearchArticleVo.class);IndexRequest indexRequest = new IndexRequest("app_info_article");indexRequest.id(searchArticleVo.getId().toString());indexRequest.source(JSON.toJSONString(searchArticleVo), XContentType.JSON);try {restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();log.error("sync es error={}",e);}}}

至此,文章发布功能全部完成

黑马头条项目总结之文章发布相关推荐

  1. java 黑马头条 day4 自媒体文章发布 自媒体文章列表查询 频道列表展示 自媒体文章-发布、修改、保存草稿 自媒体文章-根据id查询 自媒体文章-删除

    1 自媒体文章列表查询 1.1 需求分析 1.2 表结构和实体类 wm_news 自媒体文章表 需求: 如果有文章标题,按照文章标题模糊查询 如果有频道信息,按照频道ID查询 如果有文章状态,按照状态 ...

  2. 黑马头条项目 一 项目设计及基础搭建

    黑马头条项目之项目设计及基础搭建 一.概述 工程基于Spring-boot 2.1.5.RELEASE 版本构建,工程父项目为heima-leadnews,并通过继承方式集成Spring-boot. ...

  3. 前端基础第五天项目 社交媒体黑马头条项目-文章模块和评论

    七.文章详情 创建组件并配置路由 1.创建 views/article/index.vue 组件 <template><div class="article-contain ...

  4. 新黑马头条项目经验(黑马)

      swagger (1)简介 Swagger 是一个规范和完整的框架,用于生成.描述.调用和可视化 RESTful 风格的 Web 服务(API Documentation & Design ...

  5. 前端基础第四天项目 社交媒体黑马头条项目-登录注册和个人中心

    一.项目初始化 目标 能使用 Vue CLI 创建项目 了解 Vant 组件库的导入方式 掌握制作使用字体图标的方式 掌握如何在 Vue 项目中处理 REM 适配 理解 axios 请求模块的封装 使 ...

  6. 软件测试项目实战32讲,软件测试入门-黑马头条项目实战

    课程简介 本课程以黑马头条实战项目为例,将项目的整个测试流程做了详细的介绍,并带着大家一起进行产品需求评审,项目测试计划编写,测试需求分析,以及测试用例的设计编写和执行操作,通过完成实际的功能业务测试 ...

  7. 【SpringSSM项目】搏击俱乐部 文章发布

    发布文章是项目的主要功能,编写文章的环境很重要,提供各种功能的编辑工具有利于提高编辑效率以及文章效果. 因此我们可以使用开源的编辑器,提高用户体验.我这里选择了 Editor.md 一款 McDown ...

  8. 黑马头条项目 JWT—4.3 头条项目实施方案(生成token接口测试)

    头条项目实施方案 需求 设置有效期,但有效期不宜过长,需要刷新. 如何解决刷新问题? 手机号+验证码(或帐号+密码)验证后颁发接口调用token与refresh_token(刷新token) Toke ...

  9. Vuex——黑马头条项目(vuex体验版)

    1.搭建项目 1.1通过vue-cli脚手架搭建项目:vue create toutiao (选择 vuex/eslint(standard)/pre-cssprocesser(less)) 1.2在 ...

最新文章

  1. printf 命令详解
  2. PHP实现四种基本排序算法
  3. 《大话数据结构》第3章 线性表 3.8.2 单链表的删除
  4. springIllegalArgumentException Can not set field to $Proxy 在spring中使用事物或AOP遇到的错误
  5. java手动提交事务_Mybatis是如何将事务和连接池高效的结合的
  6. 浅谈HTTP中Get与Post的区别_转
  7. mysql monitor用户_Mysql的用户基本操作
  8. js如何将跨域打开的窗口放到最前面_程序员的强迫症-便捷打开常用网站
  9. L2-039 清点代码库 (25 分)-PAT 团体程序设计天梯赛 GPLT
  10. spring 数组中随机取几个_最新redux-spring前端模块化框架
  11. php中级联,php级联
  12. android交友php,android交友约会社交APP完整源码Dating App 3.7(服务端+客户端)
  13. 包含查询match和对时间进行范围查询range的DSL
  14. 迷宫小游戏c语言代码,C语言编写的迷宫小游戏-源代码
  15. Linux系统如何分区
  16. mysql报错信息1067_mysql 1067错误报错!这个是mysqld - -console得到的信息
  17. twitter全自动发推_如何阻止Twitter视频自动播放
  18. 顶刊TPAMI!目标检测中的不均衡问题综述!
  19. 【转】RNN的神奇之处(The Unreasonable Effectiveness of Recurrent Neural Networks)
  20. Python:扑克牌游戏

热门文章

  1. 机器学习吃瓜(西瓜、南瓜书)笔记 task03-第四章决策树
  2. Git步步进阶---参与开源提交PR步骤完全揭秘
  3. C++如何编写属于自己的头文件 ---- 自己动手,丰衣足食
  4. 谷粒商城VUE模板设置
  5. WINCE五笔输入法
  6. 技嘉GIGABYTE AERO 15SA风扇噪音过大加油修理方法
  7. C语言 关于for循环里定义的变量
  8. 研华工控机主板上电自启
  9. 在画电路图时,想问下几种地之间的区别? power-GND singal-GND GND
  10. WinLogon登录管理和GINA简介 (转)