JavaWeb_LeadNews_Day4-阿里云内容安全, 雪花算法, app文章保存, 自媒体文章审核

JavaWeb_LeadNews_Day4-阿里云内容安全, 雪花算法, app文章保存, 自媒体文章审核

阿里云内容安全

  • 依赖

    xml 复制代码
    <!-- 内容安全 -->
    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-core</artifactId>
    </dependency>
    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-green</artifactId>
    </dependency>
  • 实现

    java 复制代码
    @SpringBootTest(classes = WemediaApplication.class)
    @RunWith(SpringRunner.class)
    public class AliyunTest {
    
        @Autowired
        private GreenTextScan greenTextScan;
    
        @Autowired
        private FileStorageService fileStorageService;
    
        @Autowired
        private GreenImageScan greenImageScan;
    
        /**
         * 测试文本内容审核
         */
        @Test
        public void testScanText() throws Exception {
            Map map = greenTextScan.greeTextScan("我是一个好人, 冰毒");
            System.out.println(map);
        }
    
        /**
         * 测试图片审核
         */
        @Test
        public void testScanImage() throws Exception {
            byte[] bytes = fileStorageService.downLoadFile("http://192.168.174.133:9000/leadnews/2023/07/22/7086dfbae7d1478f9ffc7784351e8708.png");
    
            List<byte[]> list = new ArrayList<>();
            list.add(bytes);
    
            Map map = greenImageScan.imageScan(list);
            System.out.println(map);
        }
    }
  • 总结

    • 工具类太复杂, 云盾内容安全也没开通(要企业认证), 看看测试好了
    • 测试图片审核会报错, 可能是没开通

分布式主键策略-雪花算法

  • 背景

    html 复制代码
    随着业务的增长,文章表可能要占用很大的物理存储空间,为了解决该问题,
    后期使用数据库分片技术。将一个数据库进行拆分,通过数据库中间件连接。
    如果数据库中该表选用ID自增策略,则可能产生重复的ID,
    此时应该使用分布式ID生成策略来生成ID。
  • 技术选型

    方案 优势 劣势
    redis (INCR)生成一个全局连续递增的数字类型主键 增加了一个外部组件的依赖, Redis不可以, 则整个数据库将无法再插入
    UUID 全局唯一, Mysql也有UUID实现 36个字符组成, 占用空间大
    snowflake算法 全局唯一, 数字类型, 存储成本低 机器规模大于1024台无法支持
  • 雪花算法

    html 复制代码
    snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。
    其核心思想是:使用41bit作为毫秒数,
    10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),
    12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生4096个ID),
    最后还有一个符号位,永远是0

app文章保存

具体实现

  • feign远程调用依赖

    xml 复制代码
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  • 文章端远程接口

    java 复制代码
    // 定义接口
    @FeignClient("leadnews-article")
    public interface IArticleClient {
    
        @PostMapping("/api/v1/article/save")
        ResponseResult saveArticle(@RequestBody ArticleDto dto);
    }
    
    // 实现接口
    @RestController
    public class ArticleClient implements IArticleClient {
    
        @Autowired
        private ApArticleService apArticleService;
    
        @PostMapping("/api/v1/article/save")
        @Override
        public ResponseResult saveArticle(@RequestBody ArticleDto dto) {
            return apArticleService.saveArticle(dto);
        }
    }
  • app文章保存

    java 复制代码
    // Dto
    public class ArticleDto  extends ApArticle {
        /**
         * 文章内容
         */
        private String content;
    }
    
    // Service
    public ResponseResult saveArticle(ArticleDto dto) {
        // 1. 检查参数
        if(dto == null){
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        ApArticle apArticle = new ApArticle();
        BeanUtils.copyProperties(dto, apArticle);
        // 2. 保存或修改文章
        if(dto.getId() == null){
            // 2.1 不存在id
            // 保存文章
            save(apArticle);
            // 保存文章配置
            ApArticleConfig apArticleConfig = new ApArticleConfig(apArticle.getId());
            apArticleConfigMapper.insert(apArticleConfig);
            // 保存文章内容
            ApArticleContent apArticleContent = new ApArticleContent();
            apArticleContent.setArticleId(apArticle.getId());
            apArticleContent.setContent(dto.getContent());
            apArticleContentMapper.insert(apArticleContent);
        }else{
            // 2.2 存在id
            // 修改文章
            updateById(apArticle);
            // 修改文章内容
            LambdaQueryWrapper<ApArticleContent> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(ApArticleContent::getArticleId, dto.getId());
            ApArticleContent apArticleContent = apArticleContentMapper.selectOne(queryWrapper);
            apArticleContentMapper.updateById(apArticleContent);
        }
        // 3. 返回文章id
        return ResponseResult.okResult(apArticle.getId());
    }

总结

  • dto中没有配置表的数据, 所以保存时设置配置的默认值, 修改时不用修改文章配置表
  • 返回文章id是需要在wm_news表中设置article_id, 新闻表中有个文章id字段

自媒体文章审核

提取内容和图片

  • 实现思路
  1. 通过id查询文章
  2. 查询文章状态, 需要是提交, 待审核状态
  3. 获取文章内容和图片
  • 具体实现

    java 复制代码
    @Service
    @Slf4j
    @Transactional
    public class WmNewsAutoScanServiceImpl implements WmNewsAutoScanService {
    
        @Autowired
        private WmNewsMapper wmNewsMapper;
        /**
         * 自媒体文章审核
         * @param id
         */
        @Override
        public void autoScanWmNews(Integer id) throws Exception {
            // 查询文章
            WmNews wmNews = wmNewsMapper.selectById(id);
            if(wmNews == null){
                throw new RuntimeException("WmNewsAutoScanServiceImpl-文章不存在");
            }
    
            if(wmNews.getStatus() == WmNews.Status.SUBMIT.getCode()) {
                // 获取图片和文本内容
                Map<String, Object> textAndImages = handleTextAndImages(wmNews);
            }
        }  
    
        /**
         * 获取图片和文本内容
         * @param wmNews
         * @return
         */
        private Map<String, Object> handleTextAndImages(WmNews wmNews) {
            // 图片路径
            List<String> imgList = new ArrayList<>();
            // 文本内容
            StringBuilder builder = new StringBuilder();
    
            // 从自媒体文章的内容中提取文章和图片
            if(StringUtils.isNotBlank(wmNews.getContent())) {
                List<Map> maps = JSONArray.parseArray(wmNews.getContent(), Map.class);
                for (Map map : maps) {
                    if (map.get("type").equals("image")) {
                        imgList.add((String) map.get("value"));
                    } else if (map.get("type").equals("text")) {
                        builder.append(map.get("value"));
                    }
                }
            }
            // 提取文章的封面图片
            if(StringUtils.isNotBlank(wmNews.getImages())) {
                String[] split = wmNews.getImages().split(",");
                imgList.addAll(Arrays.asList(split));
            }
            Map<String, Object> resultMap = new HashMap<>();
            resultMap.put("content", builder.toString());
            resultMap.put("images", imgList);
            return resultMap;
        }
    }
  • 总结

    • Arrays.asListString数组转化为List<String>
    • ListaddAll方法可以将List<>直接全加进来

审核内容和图片

  • 具体实现

    java 复制代码
    @Service
    @Slf4j
    @Transactional
    public class WmNewsAutoScanServiceImpl implements WmNewsAutoScanService {
    
        @Autowired
        private WmNewsMapper wmNewsMapper;
        @Autowired
        private FileStorageService fileStorageService;
        @Autowired
        private GreenTextScan greenTextScan;
        @Autowired
        private GreenImageScan greenImageScan;
        /**
         * 自媒体文章审核
         * @param id
         */
        @Override
        public void autoScanWmNews(Integer id) throws Exception {
            ...
    
            if(wmNews.getStatus() == WmNews.Status.SUBMIT.getCode()) {
                ...
                // 2. 文章文本审核
                boolean isTextScan = handleTextScan((String) textAndImages.get("content"), wmNews);
                if(!isTextScan) return;
                // 3. 文章图片审核
                boolean isImageScan = handleImageScan((List<String>) textAndImages.get("images"), wmNews);
                if(!isImageScan) return;
            }
        }
    
        /**
         * 审核图片
         * @param images
         * @param wmNews
         * @return
         */
        private boolean handleImageScan(List<String> images, WmNews wmNews) {
            boolean flag = true;
    
            if(images == null || images.size() == 0){
                return flag;
            }
    
            List<byte[]> list = new ArrayList<>();
            // 图片去重
            images = images.stream().distinct().collect(Collectors.toList());
    
            for (String image : images) {
                byte[] bytes = fileStorageService.downLoadFile(image);
                list.add(bytes);
            }
    
            try {
                Map map = greenImageScan.imageScan(list);
                if(map != null){
                    // 违规内容
                    if(map.get("suggestion").equals("block")){
                        flag = false;
                        updateWmNews(wmNews, 2, "当前文章中存在违规内容");
                    }
                    // 不确定内容, 需要人工审核
                    if(map.get("suggestion").equals("review")){
                        flag = false;
                        updateWmNews(wmNews, 3, "当前文章中存在不确定内容");
                    }
                }
            } catch (Exception e) {
                flag = false;
                throw new RuntimeException(e);
            }
            return flag;
        }
    
        /**
         * 审核纯文本内容
         * @param content
         * @param wmNews
         * @return
         */
        private boolean handleTextScan(String content, WmNews wmNews) {
            boolean flag = true;
            content += wmNews.getTitle();
            if(StringUtils.isEmpty(content)){
                return flag;
            }
            try {
                Map map = greenTextScan.greeTextScan(content);
                if(map != null){
                    // 违规内容
                    if(map.get("suggestion").equals("block")){
                        flag = false;
                        updateWmNews(wmNews, 2, "当前文章中存在违规内容");
                    }
                    // 不确定内容, 需要人工审核
                    if(map.get("suggestion").equals("review")){
                        flag = false;
                        updateWmNews(wmNews, 3, "当前文章中存在不确定内容");
                    }
                }
            } catch (Exception e) {
                flag = false;
                e.printStackTrace();
            }
            return flag;
        }
    
        /**
         * 修改文章内容
         * @param wmNews
         * @param status
         * @param reason
         */
        private void updateWmNews(WmNews wmNews, int status, String reason) {
            wmNews.setStatus((short) status);
            wmNews.setReason(reason);
            wmNewsMapper.updateById(wmNews);
        }
    }
  • 总结

    • 之前误以为stream流中可以通过return+collect(Collectors.toList)修改原数据内容, 而是stream().map((item)->{...})中的item若是对象, 则可以通过修改对象中的属性来修改原数据内容, 而直接通过给对象赋值来修改对象则不行, 因为这相当于让item指向新的地址, 这没有什么意义, 跟原本的数据无关.
    • idea快捷键: ctrl+alt+m, 抽取方法.

保存app端文章

  • 具体实现
java 复制代码
@Service
@Slf4j
@Transactional
public class WmNewsAutoScanServiceImpl implements WmNewsAutoScanService {

    ...

    /**
     * 自媒体文章审核
     * @param id
     */
    @Override
    public void autoScanWmNews(Integer id) throws Exception {
        ...

        if(wmNews.getStatus() == WmNews.Status.SUBMIT.getCode()) {
            ...
            // 4. 审核通过, 保存app端相关文章
            ResponseResult responseResult = saveAppArticle(wmNews);
            if(!responseResult.getCode().equals(200)){
                throw new RuntimeException("WmNewsAutoScanServiceImpl-文字审核, 保存app端相关文章数据失败");
            }
            // 回填article_id
            wmNews.setArticleId((Long) responseResult.getData());
            updateWmNews(wmNews, 9, "审核成功");
        }
    }

    @Autowired
    private IArticleClient articleClient;
    @Autowired
    private WmChannelMapper wmChannelMapper;
    @Autowired
    private WmUserMapper wmUserMapper;
    /**
     * 保存app端相关的文章数据
     * @param wmNews
     */
    private ResponseResult saveAppArticle(WmNews wmNews) {
        ArticleDto dto = new ArticleDto();
        BeanUtils.copyProperties(wmNews, dto);
        // 文章的布局
        dto.setLayout(wmNews.getType());
        // 频道
        WmChannel wmChannel = wmChannelMapper.selectById(wmNews.getChannelId());
        if(wmChannel != null){
            dto.setChannelName(wmChannel.getName());
        }
        // 作者
        dto.setAuthorId(Long.valueOf(wmNews.getUserId()));
        WmUser wmUser = wmUserMapper.selectById(wmNews.getUserId());
        if(wmNews != null){
            dto.setAuthorName(wmUser.getName());
        }
        // 设置文字id
        if(wmNews.getArticleId() != null){
            dto.setId(wmNews.getArticleId());
        }
        dto.setCreatedTime(new Date());

        ResponseResult responseResult = articleClient.saveArticle(dto);
        return responseResult;
    }
}
  • 总结
    • 保存完, 要将返回的文章id, 保存为news表中的article_id

Feign远程调用降级

  • 介绍

    • 服务降级是服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃
    • 服务降级虽然会导致请求失败,但是不会导致阻塞
  • 编写降级实现类

    java 复制代码
    @Component
    public class IArticleClientFallback implements IArticleClient {
        @Override
        public ResponseResult saveArticle(ArticleDto dto)  {
            return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR,"获取数据失败");
        }
    }
  • 自媒体微服务扫描降级类

    java 复制代码
    @Configuration
    @ComponentScan("com.heima.apis.article.fallback")
    public class InitConfig {
    }
  • feign接口指定降级类

    java 复制代码
    @FeignClient(value = "leadnews-article",fallback = IArticleClientFallback.class)
    public interface IArticleClient {
        ...
    }
  • 配置开启服务降级, 并指定响应时间

    yml 复制代码
    feign:
    # 开启feign对hystrix熔断降级的支持
    hystrix:
        enabled: true
    # 修改调用超时时间
    client:
        config:
        default:
            connectTimeout: 2000
            readTimeout: 2000
  • 在文章微服务延时

    java 复制代码
    public ResponseResult saveArticle(ArticleDto dto) {
        // 测试feign调用降级
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  • 总结: 麻

发布文章异步调用

  1. 在自动审核上加异步注解

    java 复制代码
    @Async
    public void autoScanWmNews(Integer id){
        ...
    }
  2. 文章发布成功后, 调用自动审核

    java 复制代码
    public ResponseResult submitNews(WmNewsDto dto) {
        ...
        wmNewsAutoScanService.autoScanWmNews(wmNews.getId());
    
        return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
    }
  3. 自媒体启动类添加注解

    java 复制代码
    @EnableAsync
    public class WemediaApplication {
        ...
    }

自管理敏感词

  • 方案介绍

    方案 说明
    数据库模糊查询 效率太低
    String.indexOf("")查找 数据库量大的话也是比较慢
    全文检索 分词再匹配
    DFA算法 确定有穷自动机(一种数据结构)
  • 具体实现

    java 复制代码
    private boolean handleSensitiveScan(String content, WmNews wmNews) {
        boolean flag = true;
        // 查询敏感词
        List<WmSensitive> wmSensitiveList = wmSensitiveMapper.selectList(Wrappers.<WmSensitive>lambdaQuery().select(WmSensitive::getSensitives));
        List<String> sensitiveList = wmSensitiveList.stream().map(WmSensitive::getSensitives).collect(Collectors.toList());
        // 初始化敏感词库
        SensitiveWordUtil.initMap(sensitiveList);
        // 查看文章中是否包含敏感词
        Map<String, Integer> wordMap = SensitiveWordUtil.matchWords(content);
        if(wordMap.size() > 0){
            updateWmNews(wmNews, 2, "当前文章中存在违规内容:" + wordMap);
            flag = false;
        }
        return flag;
    }
  • 总结

    • 使用DFA算法实现敏感词检测, 感觉DFA算法和字典树差不多

图片文字识别

入门

  • 什么是OCR

    html 复制代码
    OCR(Optical Character Recognition,光学字符识别)是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,
    通过检测暗、亮的模式确定其形状,然后用字符识别方法将形状翻译成计算机文字的过程
  • 方案

    方案 说明
    百度OCR 收费
    Tesseract-OCR Google维护的开源OCR引擎, 支持Java, Python等语言调用
    Test4j 封装了Tesseract-OCR, 支持Java调用
  • tess4j依赖

    xml 复制代码
    <dependencies>
        <dependency>
            <groupId>net.sourceforge.tess4j</groupId>
            <artifactId>tess4j</artifactId>
            <version>4.1.1</version>
        </dependency>
    </dependencies>
  • 案例

    java 复制代码
    public static void main(String[] args) throws TesseractException {
        // 创建实例
        ITesseract tesseract = new Tesseract();
        // 设置字体库路径
        tesseract.setDatapath("D:\\workspace\\tessdata");
        // 设置语言 简体中文
        tesseract.setLanguage("chi_sim");
        File file = new File("D:\\csdn图库\\微服务.png");
        // 识别图片
        String res = tesseract.doOCR(file).replaceAll("\\r|\\n", "-");
        System.out.println("识别的结果为:"+res);
    }

集成至文字审核

  • 导入依赖, spring工厂加入工具类, 在调用工具类的服务中配置参数

  • 工具类

    java 复制代码
    @Getter
    @Setter
    @Component
    @ConfigurationProperties(prefix = "tess4j")
    public class Tess4jClient {
    
        private String dataPath;
        private String language;
    
        public String doOCR(BufferedImage image) throws TesseractException {
            //创建Tesseract对象
            ITesseract tesseract = new Tesseract();
            //设置字体库路径
            tesseract.setDatapath(dataPath);
            //中文识别
            tesseract.setLanguage(language);
            //执行ocr识别
            String result = tesseract.doOCR(image);
            //替换回车和tal键  使结果为一行
            result = result.replaceAll("\\r|\\n", "-").replaceAll(" ", "");
            return result;
        }
    }
  • 具体实现

    java 复制代码
    // 从byte[]转换为BufferedImage
    ByteArrayInputStream in = new ByteArrayInputStream(bytes);
    BufferedImage imageFile = ImageIO.read(in);
    // 识别图片文字
    String result = tess4jClient.doOCR(imageFile);
    System.out.println("识别内容: " + result);
    // 审核是否包含敏感词
    boolean isSensitive = handleSensitiveScan(result, wmNews);
    if(!isSensitive){
        return isSensitive;
    }

静态文章生成, 异步

java 复制代码
@Service
@Slf4j
@Transactional
public class ArticleFreemarkerServiceImpl implements ArticleFreemarkerService {

    @Autowired
    private Configuration configuration;
    @Autowired
    private FileStorageService fileStorageService;
    @Autowired
    private ApArticleService apArticleService;
    /**
     * 生成静态文件上传到minIO中
     * @param article
     * @param content
     */
    @Override
    @Async
    public void buildArticleToMinIO(ApArticle article, String content) {
        if(StringUtils.isNotBlank(content)) {
            // 2. 文章内容通过freemarker生成html文件
            Template template = null;
            StringWriter out = null;
            try {
                template = configuration.getTemplate("article.ftl");
                //    数据模型
                Map<String, Object> data = new HashMap<>();
                data.put("content", JSONArray.parseArray(content));
                out = new StringWriter();
                //    合成
                template.process(data, out);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            // 3. 把html上传到minio中
            InputStream in = new ByteArrayInputStream(out.toString().getBytes());
            String path = fileStorageService.uploadHtmlFile("", article.getId()+".html", in);
            // 4. 修改ap_article表, 保存static_url字段
            apArticleService.update(Wrappers.<ApArticle>lambdaUpdate().eq(ApArticle::getId, article.getId()).set(ApArticle::getStaticUrl, path));
        }
    }
}

来源

黑马程序员. 黑马头条

相关推荐
七星静香18 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员19 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU19 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie623 分钟前
在IDEA中使用Git
java·git
Elaine20239138 分钟前
06 网络编程基础
java·网络
G丶AEOM39 分钟前
分布式——BASE理论
java·分布式·八股
落落鱼201340 分钟前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀41 分钟前
LRU缓存算法
java·算法·缓存
镰刀出海44 分钟前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试
阿伟*rui3 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel