文章发布功能
/**
* 新增文章
*
* @param dto 参数
* @return 响应信息
*/
@Override
public 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 images = dto.getImages();
//将list拆分为以,分割的字符串
if (CollUtil.isNotEmpty(images)){
String join = StringUtils.join(images, ",");
wmNews.setImages(join);
}
//如果当前封面类型为自动 -1
if(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 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.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 getPictureInformation(String content) {
List materials = new ArrayList<>();
List
接着调用方法保存素材与文章的关系
先查找素材内容,如果素材有一张不存在或者素材数量不对应,则抛出异常。如果全部找到,则提取出id,并保存到素材文章关系表中。
/**
* 保存素材文章关系
* @param materials 素材集合
* @param id 文章id
* @param wmContentReference 文章类型
*/
private void saveRelativeInfo(List materials, Integer id, Short wmContentReference) {
if (CollUtil.isNotEmpty(materials)){
List wmMaterials = wmMaterialMapper.selectList(Wrappers.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 wmMaterialsIds = wmMaterials.stream().
map(WmMaterial::getId).collect(Collectors.toList());
//进行保存
wmNewsMaterialMapper.saveRelations(wmMaterialsIds,id,wmContentReference);
}
}
接着进行文章封面的提取
根据规则,文章内容图片大于一张小于三张为单封面文章,文章内容图片大于三张的为多封面文章,文章没有图片的为无封面文章进行匹配。并更新文章信息匹配完成后,如果存在封面属性,则将他保存到素材与文章管理表中。
接着就是将该处理后的文章通过异步调用添加到任务中进行自动审核与延时发布。
这里通过远程调用首先将文章转换为task添加到数据库中。并生成taskInfoLog添加到数据库中
**
* 将文章添加到延时任务中
* 进行异步调用
* @param id 当前文章id
* @param publishTime 发布时间
*/
@Override
@Async
public 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如果当前任务是马上执行 存入list
cacheService.lLeftPush(ScheduleConstants.TOPIC + key, JSONUtil.toJsonStr(task));
}else if (task.getExecuteTime() <= nextScheduleTime){
//2.2如果当前任务需要五分钟后执行 存入zSet
cacheService.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 qw = new LambdaQueryWrapper<>();
qw.lt(Taskinfo::getExecuteTime,calendarTime);
List 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 futureKeys = cacheService.scan(ScheduleConstants.FUTURE + "*");
if (CollUtil.isNotEmpty(futureKeys)){
for (String futureKey : futureKeys) {
//1.获得小于当前时间的key
Set tasks = cacheService.zRangeByScore(futureKey, 0, System.currentTimeMillis());
if (CollUtil.isNotEmpty(tasks)){
//拼接list集合中的key
String 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
@GlobalTransactional
public 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 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) 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.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
@Override
public void buildArticleToMinIO(ApArticle apArticle, String content) {
StringWriter writer = new StringWriter();
if (StrUtil.isNotEmpty(content)){
try {
//2.生成freemaker文件
Template template = configuration.getTemplate("article.ftl");
Map 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);
}
}
}
至此,文章发布功能全部完成