一.项目介绍 1.项目概述 随着智能手机的普及,人们更加习惯于通过手机来看新闻。由于生活节奏的加快,很多人只能利用碎片时间来获取信息,因此,对于移动资讯客户端的需求也越来越高。黑马头条项目正是在这样背景下开发出来。黑马头条项目采用当下火热的微服务+大数据技术架构实现。本项目主要着手于获取最新最热新闻资讯,通过大数据分析用户喜好精确推送咨询新闻。
2.业务说明 功能架构图
3.技术栈
Spring-Cloud-Gateway : 微服务之前架设的网关服务,实现服务注册中的API请求路由,以及控制流速控制和熔断处理都是常用的架构手段,而这些功能Gateway天然支持
运用Spring Boot快速开发框架,构建项目工程;并结合Spring Cloud全家桶技术,实现后端个人中心、自媒体、管理中心等微服务。
运用Spring Cloud Alibaba Nacos作为项目中的注册中心和配置中心
运用mybatis-plus作为持久层提升开发效率
运用Kafka完成内部系统消息通知;与客户端系统消息通知;以及实时数据计算
运用Redis缓存技术,实现热数据的计算,提升系统性能指标
使用Mysql存储用户数据,以保证上层数据查询的高性能
使用Mongo存储用户热数据,以保证用户热数据高扩展和高性能指标
使用FastDFS作为静态资源存储器,在其上实现热静态资源缓存、淘汰等功能
运用Hbase技术,存储系统中的冷数据,保证系统数据的可靠性
运用ES搜索技术,对冷数据、文章数据建立索引,以保证冷数据、文章查询性能
运用AI技术,来完成系统自动化功能,以提升效率及节省成本。比如实名认证自动化
PMD&P3C : 静态代码扫描工具,在项目中扫描项目代码,检查异常点、优化点、代码规范等,为开发团队提供规范统一,提升项目代码质量
技术栈
解决方案
二.环境搭建 1.Linxu环境的搭建 1.1 虚拟机的安装 1.解压分享的虚拟机镜像文件
2.使用VmWare打开.vmx文件
3.配置虚拟机的网络环境
4.开启虚拟机
5.使用FinalShell连接此虚拟机
用户名: root 密码:root IP地址: 192.168.200.130
1.2 Linux软件安装 Linux中开发环境的搭建 | The Blog (qingling.icu)
2.开发环境的配置 2.1 项目依赖的环境
JDK1.8
Intellij Idea
maven-3.6.1
Git
2.2 后端工程的搭建
解压heima-leadnews.zip文件并用IDEA工具打开
编码格式的设置
三.app端功能开发 1.app登录 登录相关的表结构
表名称
说明
ap_user
APP用户信息表
ap_user_fan
APP用户粉丝信息表
ap_user_follow
APP用户关注信息表
ap_user_realname
APP实名认证信息表
ap_user表对应的实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 package com.heima.model.user.pojos;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import lombok.Data;import java.io.Serializable;import java.util.Date;@Data @TableName("ap_user") public class ApUser implements Serializable { private static final long serialVersionUID = 1L ; @TableId(value = "id", type = IdType.AUTO) private Integer id; @TableField("salt") private String salt; @TableField("name") private String name; @TableField("password") private String password; @TableField("phone") private String phone; @TableField("image") private String image; @TableField("sex") private Boolean sex; @TableField("is_certification") private Boolean certification; @TableField("is_identity_authentication") private Boolean identityAuthentication; @TableField("status") private Boolean status; @TableField("flag") private Short flag; @TableField("created_time") private Date createdTime; }
1.1 用户登录逻辑 注册加盐的过程
用户在登录的时候会生成一个随机的字符串(salt),这个随机的字符串会加到密码后面然后连同密码加密存储到数据库。
登录加盐的过程
先根据账号查询是否存在该用户,如果存在的话,根据用户输入的密码和数据库中的salt进行md5加密,并和数据库中的密码比对,一致的话,比对通过,不一样的话不通过。
1.2 用户模块搭建 heima-leadnews-service父工程依赖文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <parent > <artifactId > heima-leadnews</artifactId > <groupId > com.heima</groupId > <version > 1.0-SNAPSHOT</version > </parent > <packaging > pom</packaging > <modules > <module > heima-leadnews-user</module > </modules > <modelVersion > 4.0.0</modelVersion > <artifactId > heima-leadnews-service</artifactId > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > com.heima</groupId > <artifactId > heima-leadnews-model</artifactId > </dependency > <dependency > <groupId > com.heima</groupId > <artifactId > heima-leadnews-common</artifactId > </dependency > <dependency > <groupId > com.heima</groupId > <artifactId > heima-leadnews-feign-api</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > </dependencies > </project >
创建子模块并创建出对应的目录结构
在heima-leadnews-service父工程下创建工程heima-leadnews-user
编写用户模块的配置文件
1 2 3 4 5 6 7 8 9 10 11 12 server: port: 51801 spring: application: name: leadnews-user cloud: nacos: discovery: server-addr: 192.168 .200 .130 :8848 config: server-addr: 192.168 .200 .130 :8848 file-extension: yml
在配置中心中添加数据库等相关的配置
在resources目录下添加日志的配置文件
logback.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <?xml version="1.0" encoding="UTF-8" ?> <configuration > <property name ="LOG_HOME" value ="e:/logs" /> <appender name ="CONSOLE" class ="ch.qos.logback.core.ConsoleAppender" > <encoder > <pattern > %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern > <charset > utf8</charset > </encoder > </appender > <appender name ="FILE" class ="ch.qos.logback.core.rolling.RollingFileAppender" > <rollingPolicy class ="ch.qos.logback.core.rolling.TimeBasedRollingPolicy" > <fileNamePattern > ${LOG_HOME}/leadnews.%d{yyyy-MM-dd}.log</fileNamePattern > </rollingPolicy > <encoder > <pattern > %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern > </encoder > </appender > <appender name ="ASYNC" class ="ch.qos.logback.classic.AsyncAppender" > <discardingThreshold > 0</discardingThreshold > <queueSize > 512</queueSize > <appender-ref ref ="FILE" /> </appender > <logger name ="org.apache.ibatis.cache.decorators.LoggingCache" level ="DEBUG" additivity ="false" > <appender-ref ref ="CONSOLE" /> </logger > <logger name ="org.springframework.boot" level ="debug" /> <root level ="info" > <appender-ref ref ="FILE" /> <appender-ref ref ="CONSOLE" /> </root > </configuration >
遇到的问题:
问题一:引入@EnableDiscoveryClient注解的时候爆红
解决方案:在heima-leadnews-service父工程下加入如下的注解
1 2 3 4 5 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency >
1.3 登录功能实现 1.3.1 接口定义
接口路径
/api/v1/login/login_auth
请求方式
POST
参数
LoginDto
响应结果
ResponseResult
LoginDto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Data public class LoginDto { @ApiModelProperty(value = "手机号",required = true) private String phone; @ApiModelProperty(value = "密码",required = true) private String password; }
统一返回结果类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 package com.heima.model.common.dtos;import com.alibaba.fastjson.JSON;import com.heima.model.common.enums.AppHttpCodeEnum;import java.io.Serializable;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;public class ResponseResult <T> implements Serializable { private String host; private Integer code; private String errorMessage; private T data; public ResponseResult () { this .code = 200 ; } public ResponseResult (Integer code, T data) { this .code = code; this .data = data; } public ResponseResult (Integer code, String msg, T data) { this .code = code; this .errorMessage = msg; this .data = data; } public ResponseResult (Integer code, String msg) { this .code = code; this .errorMessage = msg; } public static ResponseResult errorResult (int code, String msg) { ResponseResult result = new ResponseResult (); return result.error(code, msg); } public static ResponseResult okResult (int code, String msg) { ResponseResult result = new ResponseResult (); return result.ok(code, null , msg); } public static ResponseResult okResult (Object data) { ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getErrorMessage()); if (data!=null ) { result.setData(data); } return result; } public static ResponseResult errorResult (AppHttpCodeEnum enums) { return setAppHttpCodeEnum(enums,enums.getErrorMessage()); } public static ResponseResult errorResult (AppHttpCodeEnum enums, String errorMessage) { return setAppHttpCodeEnum(enums,errorMessage); } public static ResponseResult setAppHttpCodeEnum (AppHttpCodeEnum enums) { return okResult(enums.getCode(),enums.getErrorMessage()); } private static ResponseResult setAppHttpCodeEnum (AppHttpCodeEnum enums, String errorMessage) { return okResult(enums.getCode(),errorMessage); } public ResponseResult<?> error(Integer code, String msg) { this .code = code; this .errorMessage = msg; return this ; } public ResponseResult<?> ok(Integer code, T data) { this .code = code; this .data = data; return this ; } public ResponseResult<?> ok(Integer code, T data, String msg) { this .code = code; this .data = data; this .errorMessage = msg; return this ; } public ResponseResult<?> ok(T data) { this .data = data; return this ; } public Integer getCode () { return code; } public void setCode (Integer code) { this .code = code; } public String getErrorMessage () { return errorMessage; } public void setErrorMessage (String errorMessage) { this .errorMessage = errorMessage; } public T getData () { return data; } public void setData (T data) { this .data = data; } public String getHost () { return host; } public void setHost (String host) { this .host = host; } public static void main (String[] args) { PageResponseResult responseResult = new PageResponseResult (1 ,5 ,50 ); List list = new ArrayList (); list.add("itcast" ); list.add("itheima" ); responseResult.setData(list); System.out.println(JSON.toJSONString(responseResult)); } }
1.3.2 登录思路分析
1.3.3 登录关键代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 package com.heima.user.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.baomidou.mybatisplus.core.toolkit.Wrappers;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.heima.model.common.dtos.ResponseResult;import com.heima.model.common.enums.AppHttpCodeEnum;import com.heima.model.user.dtos.LoginDto;import com.heima.model.user.pojos.ApUser;import com.heima.user.mapper.ApUserMapper;import com.heima.user.service.ApUserService;import com.heima.utils.common.AppJwtUtil;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import org.springframework.util.DigestUtils;import java.util.HashMap;import java.util.Map;@Service @Transactional @Slf4j public class ApUserServiceImpl extends ServiceImpl <ApUserMapper, ApUser> implements ApUserService { @Override public ResponseResult login (LoginDto loginDto) { if (StringUtils.isNotBlank(loginDto.getPhone()) && StringUtils.isNotBlank(loginDto.getPassword())) { ApUser dbUser = getOne(new LambdaQueryWrapper <ApUser>().eq(ApUser::getPhone, loginDto.getPhone())); if (dbUser == null ){ return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户信息不存在" ); } String salt = dbUser.getSalt(); String password = loginDto.getPassword(); String pwd = DigestUtils.md5DigestAsHex((password + salt).getBytes()); if (!pwd.equals(dbUser.getPassword())){ return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR); } String token = AppJwtUtil.getToken(dbUser.getId().longValue()); Map<String,Object> map = new HashMap <>(); map.put("token" ,token); dbUser.setSalt("" ); dbUser.setPassword("" ); map.put("user" ,dbUser); return ResponseResult.okResult(map); }else { String token = AppJwtUtil.getToken(0L ); Map<String,Object> map = new HashMap <>(); map.put("token" ,token); return ResponseResult.okResult(map); } } }
1.3.4 使用接口工具测试 接口测试工具使用教程:https://qingling.icu/posts/35630.html
2. app端网关搭建 网关的概述
项目中搭建的网关
2.1 搭建过程 1.在heima-leadnews-gateway导入以下依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-gateway</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > </dependency > </dependencies >
2.创建网关的模块
3.创建启动类和bootstrap.yml配置文件
1 2 3 4 5 6 7 @EnableDiscoveryClient @SpringBootApplication public class AppGatewayApplication { public static void main (String[] args) { SpringApplication.run(AppGatewayApplication.class,args); } }
1 2 3 4 5 6 7 8 9 10 11 12 server: port: 51601 spring: application: name: leadnews-app-gateway cloud: nacos: discovery: server-addr: 192.168 .200 .130 :8848 config: server-addr: 192.168 .200 .130 :8848 file-extension: yml
4.在nacos中创建app端网关的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 spring: cloud: gateway: globalcors: add-to-simple-url-handler-mapping: true corsConfigurations: '[/**]' : allowedHeaders: "*" allowedOrigins: "*" allowedMethods: - GET - POST - DELETE - PUT - OPTION routes: - id: user uri: lb://leadnews-user predicates: - Path=/user/** filters: - StripPrefix= 1
5.使用Postman测试网关
http://localhost:51601/user/api/v1/login/login_auth
2.2 全局过滤器实现jwt校验 网关的过滤流程
思路分析:
用户进入网关开始登陆,网关过滤器进行判断,如果是登录,则路由到后台管理微服务进行登录
用户登录成功,后台管理微服务签发JWT TOKEN信息返回给用户
用户再次进入网关开始访问,网关过滤器接收用户携带的TOKEN
网关过滤器解析TOKEN ,判断是否有权限,如果有,则放行,如果没有则返回未认证错误
JWT认证的过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 @Component @Slf4j public class AuthorizeFilter implements Ordered , GlobalFilter { @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); if (request.getURI().getPath().contains("/login" )) { return chain.filter(exchange); } String token = request.getHeaders().getFirst("token" ); if (StringUtils.isBlank(token)) { response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } try { Claims claims = AppJwtUtil.getClaimsBody(token); int res = AppJwtUtil.verifyToken(claims); if (res == 1 || res == 2 ) { response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } } catch (Exception e) { e.printStackTrace(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } return chain.filter(exchange); } @Override public int getOrder () { return 0 ; } }
3.app前端项目集成
通过nginx来进行配置,功能如下
通过nginx的反向代理功能访问后台的网关资源
通过nginx的静态服务器功能访问前端静态页面
3.1 Nginx集成前端项目步骤 ①:解压资料文件夹中的压缩包nginx-1.18.0.zip
cmd切换到nginx所有的目录输入nginx启动nginx
②:解压资料文件夹中的前端项目app-web.zip
解压到一个没有中文的文件夹中,后面nginx配置中会指向这个目录
③:配置nginx.conf文件
在nginx安装的conf目录下新建一个文件夹leadnews.conf
,在当前文件夹中新建heima-leadnews-app.conf
文件
heima-leadnews-app.conf配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 upstream heima-app-gateway{ #APP端网关所在的端口 server localhost:51601; } server { listen 8801; location / { root C:/Gong/data/app-web/; index index.html; } location ~/app/(.*) { proxy_pass http://heima-app-gateway/$1; proxy_set_header HOST $host; # 不改变源请求头的值 proxy_pass_request_body on; #开启获取请求体 proxy_pass_request_headers on; #开启获取请求头 proxy_set_header X-Real-IP $remote_addr; # 记录真实发出请求的客户端IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #记录代理信息 } }
nginx.conf 把里面注释的内容和静态资源配置相关删除,引入heima-leadnews-app.conf文件加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #user nobody; worker_processes 1 ; events { worker_connections 1024 ; } http { include mime.types ; default_type application/octet-stream; sendfile on; keepalive_timeout 65 ; # 引入自定义配置文件 include leadnews.conf
④ :启动nginx
在nginx安装包中使用命令提示符打开,输入命令nginx启动项目
可查看进程,检查nginx是否启动
重新加载配置文件:nginx -s reload
⑤:打开前端项目进行测试 – > http://localhost:8801
用谷歌浏览器打开,调试移动端模式进行访问
4.app端文章列表功能 开发前app的首页面
文章的布局展示
4.1 数据库表的创建 文章的相关的数据库
表名称
说明
ap_article
文章信息表,存储已发布的文章
ap_article_config
APP已发布文章配置表
ap_article_content
APP已发布文章内容表
ap_author
APP文章作者信息表
ap_collection
APP收藏信息表
导入资料中的sql文件创建相关的数据库表
关键的数据库表
文章基本信息表
APP已发布文章配置表
APP已发布文章内容表
APP文章作者信息表
APP收藏信息表
垂直分表
将文章相关的表分成文章配置表和文章内容表和文章信息表
垂直分表:将一个表的字段分散到多个表中,每个表存储其中一部分字段
优势:
减少IO争抢,减少锁表的几率,查看文章概述与文章详情互不影响
充分发挥高频数据的操作效率,对文章概述数据操作的高效率不会被操作文章详情数据的低效率所拖累
拆分规则:
1.把不常用的字段单独放在一张表
2.把text,blob等大字段拆分出来单独放在一张表
3.经常组合查询的字段单独放在一张表中
4.2 文章模块搭建 导入资料中的模块
在nacos中添加配置
1 2 3 4 5 6 7 8 9 10 11 spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/leadnews_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: 123456 mybatis-plus: mapper-locations: classpath*:mapper/*.xml type-aliases-package: com.heima.model.article.pojos
踩坑 : 在导入项目的时候提示Caused by: java.io.FileNotFoundException: class path resource [com/heima/apis/article/IArticleClient.class] cannot be opened because it does not exist ,先删除这个项目,手动创建这个项目,然后复制资料里面文件到这个项目即可,直接复制整个项目可能会报这个错!
4.3 首页文章的列表显示 首页上拉和下拉的实现思路
Sql语句实现
1 2 3 4 5 6 7 8 9 10 #按照发布时间倒序查询十条文章 select * from ap_article aa order by aa.publish_time desc limit 10 #频道筛选 select * from ap_article aa where aa.channel_id = 1 order by aa.publish_time desc limit 10 #加载首页 select * from ap_article aa where aa.channel_id = 1 and aa.publish_time < '2063-09-08 10:20:12' order by aa.publish_time desc limit 10 #加载更多 select * from ap_article aa where aa.channel_id = 1 and aa.publish_time < '2020-09-07 22:30:09' order by aa.publish_time desc limit 10 #加载最新 select * from ap_article aa where aa.channel_id = 1 and aa.publish_time > '2020-09-07 22:30:09' order by aa.publish_time desc limit 10
4.2.1 接口定义
加载首页
加载更多
加载最新
接口路径
/api/v1/article/load
/api/v1/article/loadmore
/api/v1/article/loadnew
请求方式
POST
POST
POST
参数
ArticleHomeDto
ArticleHomeDto
ArticleHomeDto
响应结果
ResponseResult
ResponseResult
ResponseResult
ArticleHomeDto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.heima.model.article.dtos;import lombok.Data;import java.util.Date;@Data public class ArticleHomeDto { Date maxBehotTime; Date minBehotTime; Integer size; String tag; }
4.2.2 实现思路 ①:导入heima-leadnews-article微服务,资料在当天的文件夹中
需要在nacos中添加对应的配置
②:定义接口
接口路径、请求方式、入参、出参
③:编写mapper文件
文章表与文章配置表多表查询
④:编写业务层代码
⑤:编写控制器代码
⑥:swagger测试或前后端联调测试
4.2.3 功能的关键代码实现 mapper层的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.heima.article.mapper.ApArticleMapper" > <resultMap id ="resultMap" type ="com.heima.model.article.pojos.ApArticle" > <id column ="id" property ="id" /> <result column ="title" property ="title" /> <result column ="author_id" property ="authorId" /> <result column ="author_name" property ="authorName" /> <result column ="channel_id" property ="channelId" /> <result column ="channel_name" property ="channelName" /> <result column ="layout" property ="layout" /> <result column ="flag" property ="flag" /> <result column ="images" property ="images" /> <result column ="labels" property ="labels" /> <result column ="likes" property ="likes" /> <result column ="collection" property ="collection" /> <result column ="comment" property ="comment" /> <result column ="views" property ="views" /> <result column ="province_id" property ="provinceId" /> <result column ="city_id" property ="cityId" /> <result column ="county_id" property ="countyId" /> <result column ="created_time" property ="createdTime" /> <result column ="publish_time" property ="publishTime" /> <result column ="sync_status" property ="syncStatus" /> <result column ="static_url" property ="staticUrl" /> </resultMap > <select id ="loadArticleList" resultMap ="resultMap" > SELECT aa.* FROM `ap_article` aa LEFT JOIN ap_article_config aac ON aa.id = aac.article_id <where > and aac.is_delete != 1 and aac.is_down != 1 <if test ="type != null and type == 1" > and aa.publish_time <![CDATA[<]]> #{dto.minBehotTime} </if > <if test ="type != null and type == 2" > and aa.publish_time <![CDATA[>]]> #{dto.maxBehotTime} </if > <if test ="dto.tag != '__all__'" > and aa.channel_id = #{dto.tag} </if > </where > order by aa.publish_time desc limit #{dto.size} </select > </mapper >
service层的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 package com.heima.article.service.Impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.heima.article.mapper.ApArticleMapper;import com.heima.article.service.ApArticleService;import com.heima.common.constants.ArticleConstants;import com.heima.model.article.dtos.ArticleHomeDto;import com.heima.model.article.pojos.ApArticle;import com.heima.model.common.dtos.ResponseResult;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang.StringUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.Date;import java.util.List;@Service @Transactional @Slf4j public class ApArticleServiceImpl extends ServiceImpl <ApArticleMapper, ApArticle> implements ApArticleService { @Autowired private ApArticleMapper apArticleMapper; private static final Integer MAX_PAGE_SIZE = 50 ; @Override public ResponseResult load (ArticleHomeDto dto, Short type) { Integer size = dto.getSize(); if (size == null || size == 0 ) { size = 0 ; } size = Math.min(size, 50 ); if (!type.equals(ArticleConstants.LOADTYPE_LOAD_MORE) && !type.equals(ArticleConstants.LOADTYPE_LOAD_NEW)) { type = 1 ; } if (StringUtils.isBlank(dto.getTag())) { dto.setTag(ArticleConstants.DEFAULT_TAG); } if (dto.getMinBehotTime() == null ) dto.setMinBehotTime(new Date ()); if (dto.getMaxBehotTime() == null ) dto.setMaxBehotTime(new Date ()); List<ApArticle> apArticles = apArticleMapper.loadArticleList(dto, type); return ResponseResult.okResult(apArticles); } }
controller层的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.heima.article.controller.v1;import com.heima.article.service.ApArticleService;import com.heima.common.constants.ArticleConstants;import com.heima.model.article.dtos.ArticleHomeDto;import com.heima.model.common.dtos.ResponseResult;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;@RestController @RequestMapping("/api/v1/article") public class ArticleHomeController { @Autowired private ApArticleService apArticleService; @PostMapping("/load") public ResponseResult<Object> load (@RequestBody ArticleHomeDto articleHomeDto) { return apArticleService.load(articleHomeDto, ArticleConstants.LOADTYPE_LOAD_MORE); } @PostMapping("/loadmore") public ResponseResult<Object> loadmore (@RequestBody ArticleHomeDto articleHomeDto) { return apArticleService.load(articleHomeDto, ArticleConstants.LOADTYPE_LOAD_MORE); } @PostMapping("/loadnew") public ResponseResult<Object> loadnew (@RequestBody ArticleHomeDto articleHomeDto) { return apArticleService.load(articleHomeDto, ArticleConstants.LOADTYPE_LOAD_NEW); } }
网关中增加文章模块的路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 spring: cloud: gateway: globalcors: add-to-simple-url-handler-mapping: true corsConfigurations: '[/**]' : allowedHeaders: "*" allowedOrigins: "*" allowedMethods: - GET - POST - DELETE - PUT - OPTION routes: - id: user uri: lb://leadnews-user predicates: - Path=/user/** filters: - StripPrefix= 1 - id: article uri: lb://leadnews-article predicates: - Path=/article/** filters: - StripPrefix= 1
5. app端文章详情功能 5.1 需求分析
5.2 实现方案-静态模板展示
静态模板展示关键技术-Freemarker
Freemarker教程: https://qingling.icu/posts/29367.html
5.3 对象存储服务MinIO MinIO 教程: https://qingling.icu/posts/36397.html
5.4 实现思路以及代码实现
代码实现
1.在文章模块的pom.xml文件中加入以下的依赖
1 2 3 4 5 6 7 8 9 10 11 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-freemarker</artifactId > </dependency > <dependency > <groupId > com.heima</groupId > <artifactId > heima-file-starter</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > </dependencies >
2.在nacos中有关文章模块的配置中添加以下的内容
1 2 3 4 5 6 minio: accessKey: minio secretKey: minio123 bucket: leadnews endpoint: http://192.168.200.130:9000 readPath: http://192.168.200.130:9000
3.将资料中的article.ftl文件拷贝到文章模块的templates目录下,article.ftl文件内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 <!DOCTYPE html > <html > <head > <meta charset ="utf-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" > <title > 黑马头条</title > <link rel ="stylesheet" href ="https://fastly.jsdelivr.net/npm/vant@2.12.20/lib/index.css" > <link rel ="stylesheet" href ="../../../plugins/css/index.css" > </head > <body > <div id ="app" > <div class ="article" > <van-row > <van-col span ="24" class ="article-title" v-html ="title" > </van-col > </van-row > <van-row type ="flex" align ="center" class ="article-header" > <van-col span ="3" > <van-image round class ="article-avatar" src ="https://p3.pstatp.com/thumb/1480/7186611868" > </van-image > </van-col > <van-col span ="16" > <div v-html ="authorName" > </div > <div > {{ publishTime | timestampToDateTime }}</div > </van-col > <van-col span ="5" > <van-button round :icon ="relation.isfollow ? '' : 'plus'" type ="info" class ="article-focus" :text ="relation.isfollow ? '取消关注' : '关注'" :loading ="followLoading" @click ="handleClickArticleFollow" > </van-button > </van-col > </van-row > <van-row class ="article-content" > <#if content??> <#list content as item> <#if item.type='text'> <van-col span ="24" class ="article-text" > ${item.value}</van-col > <#else> <van-col span ="24" class ="article-image" > <van-image width ="100%" src ="${item.value}" > </van-image > </van-col > </#if> </#list> </#if> </van-row > <van-row type ="flex" justify ="center" class ="article-action" > <van-col > <van-button round :icon ="relation.islike ? 'good-job' : 'good-job-o'" class ="article-like" :loading ="likeLoading" :text ="relation.islike ? '取消赞' : '点赞'" @click ="handleClickArticleLike" > </van-button > <van-button round :icon ="relation.isunlike ? 'delete' : 'delete-o'" class ="article-unlike" :loading ="unlikeLoading" @click ="handleClickArticleUnlike" > 不喜欢</van-button > </van-col > </van-row > <van-list v-model ="commentsLoading" :finished ="commentsFinished" finished-text ="没有更多了" @load ="onLoadArticleComments" > <van-row id ="#comment-view" type ="flex" class ="article-comment" v-for ="(item, index) in comments" :key ="index" > <van-col span ="3" > <van-image round src ="https://p3.pstatp.com/thumb/1480/7186611868" class ="article-avatar" > </van-image > </van-col > <van-col span ="21" > <van-row type ="flex" align ="center" justify ="space-between" > <van-col class ="comment-author" v-html ="item.authorName" > </van-col > <van-col > <van-button round :icon ="item.operation === 0 ? 'good-job' : 'good-job-o'" size ="normal" @click ="handleClickCommentLike(item)" > {{ item.likes || '' }} </van-button > </van-col > </van-row > <van-row > <van-col class ="comment-content" v-html ="item.content" > </van-col > </van-row > <van-row type ="flex" align ="center" > <van-col span ="10" class ="comment-time" > {{ item.createdTime | timestampToDateTime }} </van-col > <van-col span ="3" > <van-button round size ="normal" v-html ="item.reply" @click ="showCommentRepliesPopup(item.id)" > 回复 {{ item.reply || '' }} </van-button > </van-col > </van-row > </van-col > </van-row > </van-list > </div > <van-row type ="flex" justify ="space-around" align ="center" class ="article-bottom-bar" > <van-col span ="13" > <van-field v-model ="commentValue" placeholder ="写评论" > <template #button > <van-button icon ="back-top" @click ="handleSaveComment" > </van-button > </template > </van-field > </van-col > <van-col span ="3" > <van-button icon ="comment-o" @click ="handleScrollIntoCommentView" > </van-button > </van-col > <van-col span ="3" > <van-button :icon ="relation.iscollection ? 'star' : 'star-o'" :loading ="collectionLoading" @click ="handleClickArticleCollection" > </van-button > </van-col > <van-col span ="3" > <van-button icon ="share-o" > </van-button > </van-col > </van-row > <van-popup v-model ="showPopup" closeable position ="bottom" :style ="{ width: '750px', height: '60%', left: '50%', 'margin-left': '-375px' }" > <van-list v-model ="commentRepliesLoading" :finished ="commentRepliesFinished" finished-text ="没有更多了" @load ="onLoadCommentReplies" > <van-row id ="#comment-reply-view" type ="flex" class ="article-comment-reply" v-for ="(item, index) in commentReplies" :key ="index" > <van-col span ="3" > <van-image round src ="https://p3.pstatp.com/thumb/1480/7186611868" class ="article-avatar" > </van-image > </van-col > <van-col span ="21" > <van-row type ="flex" align ="center" justify ="space-between" > <van-col class ="comment-author" v-html ="item.authorName" > </van-col > <van-col > <van-button round :icon ="item.operation === 0 ? 'good-job' : 'good-job-o'" size ="normal" @click ="handleClickCommentReplyLike(item)" > {{ item.likes || '' }} </van-button > </van-col > </van-row > <van-row > <van-col class ="comment-content" v-html ="item.content" > </van-col > </van-row > <van-row type ="flex" align ="center" > <van-col span ="10" class ="comment-time" > {{ item.createdTime | timestampToDateTime }} </van-col > </van-row > </van-col > </van-row > </van-list > <van-row type ="flex" justify ="space-around" align ="center" class ="comment-reply-bottom-bar" > <van-col span ="13" > <van-field v-model ="commentReplyValue" placeholder ="写评论" > <template #button > <van-button icon ="back-top" @click ="handleSaveCommentReply" > </van-button > </template > </van-field > </van-col > <van-col span ="3" > <van-button icon ="comment-o" > </van-button > </van-col > <van-col span ="3" > <van-button icon ="star-o" > </van-button > </van-col > <van-col span ="3" > <van-button icon ="share-o" > </van-button > </van-col > </van-row > </van-popup > </div > <script src =" https://fastly.jsdelivr.net/npm/vue/dist/vue.min.js" > </script > <script src ="https://fastly.jsdelivr.net/npm/vant@2.12.20/lib/vant.min.js" > </script > <#--<script src ="https://unpkg.com/axios/dist/axios.min.js" > </script > --> <script src ="../../../plugins/js/axios.min.js" > </script > <script src ="../../../plugins/js/index.js" > </script > </body > </html >
4.手动上传资料中index.js和index.css两个文件到MinIO中
上传index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void testMinIO () { try { FileInputStream inputStream = new FileInputStream ("C:\\Gong\\java\\黑马头条\\day02-app端文章查看,静态化freemarker,分布式文件系统minIO\\资料\\模板文件\\plugins\\js\\index.js" ); MinioClient minioClient = MinioClient.builder() .credentials("minio" , "minio123" ) .endpoint("http://192.168.200.130:9000" ) .build(); PutObjectArgs putObjectArgs = PutObjectArgs.builder() .object("plugins/js/index.js" ) .contentType("text/js" ) .bucket("leadnews" ) .stream(inputStream,inputStream.available(),-1 ) .build(); minioClient.putObject(putObjectArgs); } catch (Exception e) { e.printStackTrace(); } }
上传index.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void testMinIO () { try { FileInputStream inputStream = new FileInputStream ("C:\\Gong\\java\\黑马头条\\day02-app端文章查看,静态化freemarker,分布式文件系统minIO\\资料\\模板文件\\plugins\\css\\index.css" ); MinioClient minioClient = MinioClient.builder() .credentials("minio" , "minio123" ) .endpoint("http://192.168.200.130:9000" ) .build(); PutObjectArgs putObjectArgs = PutObjectArgs.builder() .object("plugins/css/index.css" ) .contentType("text/css" ) .bucket("leadnews" ) .stream(inputStream,inputStream.available(),-1 ) .build(); minioClient.putObject(putObjectArgs); } catch (Exception e) { e.printStackTrace(); } }
5.测试根据文章的内容生成html文件上传到minio中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 package com.heima.article.test;import com.alibaba.fastjson.JSONArray;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.heima.article.ArticleApplication;import com.heima.article.mapper.ApArticleContentMapper;import com.heima.article.service.ApArticleService;import com.heima.file.service.FileStorageService;import com.heima.model.article.pojos.ApArticle;import com.heima.model.article.pojos.ApArticleContent;import freemarker.template.Configuration;import freemarker.template.Template;import freemarker.template.TemplateException;import org.apache.commons.lang.StringUtils;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.io.ByteArrayInputStream;import java.io.IOException;import java.io.InputStream;import java.io.StringWriter;import java.util.HashMap;@SpringBootTest(classes = ArticleApplication.class) @RunWith(SpringRunner.class) public class ArticleFreeMarkerTest { @Autowired private ApArticleContentMapper apArticleContentMapper; @Autowired private Configuration configuration; @Autowired private FileStorageService fileStorageService; @Autowired private ApArticleService apArticleService; @Test public void createStaticUrlTest () throws IOException, TemplateException { ApArticleContent apArticleContent = apArticleContentMapper.selectOne(new LambdaQueryWrapper <ApArticleContent>().eq(ApArticleContent::getArticleId, "1383827995813531650L" )); if (apArticleContent != null && StringUtils.isNotBlank(apArticleContent.getContent())) { Template template = configuration.getTemplate("article.ftl" ); HashMap<String, Object> map = new HashMap <>(); map.put("content" , JSONArray.parseArray(apArticleContent.getContent())); StringWriter out = new StringWriter (); template.process(map, out); InputStream in = new ByteArrayInputStream (out.toString().getBytes()); String path = fileStorageService.uploadHtmlFile("" , apArticleContent.getArticleId() + ".html" , in); System.out.println("文件在minio中的路径:" +path); ApArticle apArticle = new ApArticle (); apArticle.setId(apArticleContent.getArticleId()); apArticle.setStaticUrl(path); boolean isSuccess = apArticleService.updateById(apArticle); System.out.println(isSuccess ? "文件上传成功" : "文件上传失败" ); } } }
6.实现效果
四.自媒体端功能开发 1.后端环境搭建 需要搭建的模块
搭建步骤
1.创建自媒体模块的数据库
2.导入相应的工程文件
3.配置自媒体模块和网关模块在nacos中的配置
自媒体模块的配置
1 2 3 4 5 6 7 8 9 10 11 spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/leadnews_wemedia?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: root mybatis-plus: mapper-locations: classpath*:mapper/*.xml type-aliases-package: com.heima.model.media.pojos
网关模块的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 spring: cloud: gateway: globalcors: cors-configurations: '[/**]' : allowedOrigins: "*" allowedMethods: - GET - POST - PUT - DELETE routes: - id: wemedia uri: lb://leadnews-wemedia predicates: - Path=/wemedia/** filters: - StripPrefix= 1
2.前端环境搭建 搭建思路
搭建步骤
heima-leadnews-wemedia配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 upstream heima-wemedia-gateway{ #APP端网关所在的端口 server localhost:51602; } server { listen 8802; location / { root C:/Gong/data/wemedia-web/; index index.html; } location ~/wemedia/MEDIA/(.*) { proxy_pass http://heima-wemedia-gateway/$1; proxy_set_header HOST $host; # 不改变源请求头的值 proxy_pass_request_body on; #开启获取请求体 proxy_pass_request_headers on; #开启获取请求头 proxy_set_header X-Real-IP $remote_addr; # 记录真实发出请求的客户端IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #记录代理信息 } }
重启nginx访问
3.自媒体素材管理功能 3.1 素材管理-图片上传 图片素材相关的实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package com.heima.model.wemedia.pojos;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import lombok.Data;import java.io.Serializable;import java.util.Date;@Data @TableName("wm_material") public class WmMaterial implements Serializable { private static final long serialVersionUID = 1L ; @TableId(value = "id", type = IdType.AUTO) private Integer id; @TableField("user_id") private Integer userId; @TableField("url") private String url; @TableField("type") private Short type; @TableField("is_collection") private Short isCollection; @TableField("created_time") private Date createdTime; }
3.1.1 解决图片素材实体类中获取图片userId的问题 实现思路
1.token解析为用户存入header
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 package com.heima.wemedia.gateway.filter;import com.heima.wemedia.gateway.util.AppJwtUtil;import io.jsonwebtoken.Claims;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang.StringUtils;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.http.HttpStatus;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.http.server.reactive.ServerHttpResponse;import org.springframework.stereotype.Component;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;@Component @Slf4j public class AuthorizeFilter implements Ordered , GlobalFilter { @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); if (request.getURI().getPath().contains("/login" )){ return chain.filter(exchange); } String token = request.getHeaders().getFirst("token" ); if (StringUtils.isBlank(token)){ response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } try { Claims claimsBody = AppJwtUtil.getClaimsBody(token); int result = AppJwtUtil.verifyToken(claimsBody); if (result == 1 || result == 2 ){ response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } Object userId = claimsBody.get("id" ); ServerHttpRequest serverHttpRequest = request.mutate().headers(httpHeaders -> { httpHeaders.add("userId" , userId + "" ); }).build(); exchange.mutate().request(serverHttpRequest); } catch (Exception e) { e.printStackTrace(); } return chain.filter(exchange); } @Override public int getOrder () { return 0 ; } }
2.创建拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package com.heima.wemedia.interceptor;import com.heima.model.wemedia.pojos.WmUser;import com.heima.utils.thread.WmThreadLocalUtil;import io.swagger.models.auth.In;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.util.Enumeration;public class WmTokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String userId = request.getHeader("userId" ); if (userId != null ){ WmUser wmUser = new WmUser (); wmUser.setId(Integer.valueOf(userId)); WmThreadLocalUtil.setUser(wmUser); } return true ; } @Override public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { WmThreadLocalUtil.clear(); } }
ThreadLocal工具类,实现在线程中存储、获取、清理用户信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.heima.utils.thread;import com.heima.model.wemedia.pojos.WmUser;public class WmThreadLocalUtil { private final static ThreadLocal<WmUser> WM_USER_THREAD_LOCAL = new ThreadLocal <>(); public static void setUser (WmUser wmUser) { WM_USER_THREAD_LOCAL.set(wmUser); } public static WmUser getUser () { return WM_USER_THREAD_LOCAL.get(); } public static void clear () { WM_USER_THREAD_LOCAL.remove(); } }
3.让拦截器生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.heima.wemedia.config;import com.heima.wemedia.interceptor.WmTokenInterceptor;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new WmTokenInterceptor ()).addPathPatterns("/**" ); } }
3.1.2 图片上传接口的定义
说明
接口路径
/api/v1/material/upload_picture
请求方式
POST
参数
MultipartFile
响应结果
ResponseResult
3.1.3 代码实现 1.在pom.xml中引入自定义minio的starter依赖
1 2 3 4 5 <dependency > <groupId > com.heima</groupId > <artifactId > heima-file-starter</artifactId > <version > 1.0-SNAPSHOT</version > </dependency >
2.在项目中添加minio的配置(在nacos中配置)
1 2 3 4 5 6 minio: accessKey: minio secretKey: minio123 bucket: leadnews endpoint: http://192.168.200.130:9000 readPath: http://192.168.200.130:9000
3.上传图片的关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package com.heima.wemedia.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.heima.file.service.FileStorageService;import com.heima.model.common.dtos.ResponseResult;import com.heima.model.common.enums.AppHttpCodeEnum;import com.heima.model.wemedia.pojos.WmMaterial;import com.heima.utils.thread.WmThreadLocalUtil;import com.heima.wemedia.mapper.WmMaterialMapper;import com.heima.wemedia.service.WmMaterialService;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import org.springframework.web.multipart.MultipartFile;import java.io.IOException;import java.util.Date;import java.util.UUID;@Slf4j @Service @Transactional public class WmMaterialServiceImpl extends ServiceImpl <WmMaterialMapper, WmMaterial> implements WmMaterialService { @Autowired private FileStorageService fileStorageService; @Override public ResponseResult uploadPicture (MultipartFile multipartFile) { if (multipartFile == null || multipartFile.getSize() == 0 ){ return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID); } String fileName = UUID.randomUUID().toString().replace("-" , "" ); String originalFilename = multipartFile.getOriginalFilename(); String postfix = originalFilename.substring(originalFilename.lastIndexOf("." )); String fileId = null ; try { fileId = fileStorageService.uploadImgFile("" , fileName + postfix, multipartFile.getInputStream()); log.info("上传图片到Minio中,fileId:{}" ,fileId); } catch (IOException e) { log.info("WmMaterialServiceImpl-上传图片失败" ); e.printStackTrace(); } WmMaterial wmMaterial = new WmMaterial (); wmMaterial.setUserId(WmThreadLocalUtil.getUser().getId()); wmMaterial.setUrl(fileId); wmMaterial.setType((short )0 ); wmMaterial.setIsCollection((short )0 ); wmMaterial.setCreatedTime(new Date ()); save(wmMaterial); return ResponseResult.okResult(wmMaterial); } }
4.测试上传功能
3.2 素材管理-图片列表 接口定义
接口路径
/api //v1/material/list
请求方式
POST
参数
WmMaterialDto
响应结果
ResponseResult
请求参数的DTO
1 2 3 4 5 6 7 8 9 @Data public class WmMaterialDto extends PageRequestDto { private Short isCollection; }
关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override public ResponseResult findList (WmMaterialDto wmMaterialDto) { wmMaterialDto.checkParam(); IPage page = new Page (wmMaterialDto.getPage(),wmMaterialDto.getSize()); LambdaQueryWrapper<WmMaterial> queryWrapper = new LambdaQueryWrapper <>(); if (wmMaterialDto.getIsCollection() != null && wmMaterialDto.getIsCollection() == 1 ){ queryWrapper.eq(WmMaterial::getIsCollection,wmMaterialDto.getIsCollection()); } queryWrapper.eq(WmMaterial::getUserId,WmThreadLocalUtil.getUser().getId()); queryWrapper.orderByDesc(WmMaterial::getCreatedTime); page = page(page,queryWrapper); ResponseResult responseResult = new PageResponseResult (wmMaterialDto.getPage(), wmMaterialDto.getSize(), (int ) page.getTotal()); responseResult.setData(page.getRecords()); return responseResult; }
MP的配置
1 2 3 4 5 6 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor (DbType.MYSQL)); return interceptor; }
实现效果
4.自媒体文章管理功能 4.1 频道列表查询 频道对应的实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 package com.heima.model.wemedia.pojos;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import lombok.Data;import java.io.Serializable;import java.util.Date;@Data @TableName("wm_channel") public class WmChannel implements Serializable { private static final long serialVersionUID = 1L ; @TableId(value = "id", type = IdType.AUTO) private Integer id; @TableField("name") private String name; @TableField("description") private String description; @TableField("is_default") private Boolean isDefault; @TableField("status") private Boolean status; @TableField("ord") private Integer ord; @TableField("created_time") private Date createdTime; }
接口定义
说明
接口路径
/api/v1/channel/channels
请求方式
GET
参数
无
响应结果
ResponseResult
关键代码
1 2 3 4 5 6 7 8 @GetMapping("/channels") public ResponseResult findAllChannels () { List<WmChannel> channels = wmChannelService.list(); return ResponseResult.okResult(channels); }
实现效果
4.2 文章列表加载 文章表对应的实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 package com.heima.model.wemedia.pojos;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import lombok.Data;import org.apache.ibatis.type.Alias;import java.io.Serializable;import java.util.Date;@Data @TableName("wm_news") public class WmNews implements Serializable { private static final long serialVersionUID = 1L ; @TableId(value = "id", type = IdType.AUTO) private Integer id; @TableField("user_id") private Integer userId; @TableField("title") private String title; @TableField("content") private String content; @TableField("type") private Short type; @TableField("channel_id") private Integer channelId; @TableField("labels") private String labels; @TableField("created_time") private Date createdTime; @TableField("submited_time") private Date submitedTime; @TableField("status") private Short status; @TableField("publish_time") private Date publishTime; @TableField("reason") private String reason; @TableField("article_id") private Long articleId; @TableField("images") private String images; @TableField("enable") private Short enable; @Alias("WmNewsStatus") public enum Status { NORMAL((short )0 ),SUBMIT((short )1 ),FAIL((short )2 ),ADMIN_AUTH((short )3 ),ADMIN_SUCCESS((short )4 ),SUCCESS((short )8 ),PUBLISHED((short )9 ); short code; Status(short code){ this .code = code; } public short getCode () { return this .code; } } }
接口定义
接口路径
/api //v1/news/list
请求方式
POST
参数
WmNewsPageReqDto
响应结果
ResponseResult
关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Override public ResponseResult findNewsList (WmNewsPageReqDto wmNewsPageReqDto) { wmNewsPageReqDto.checkParam(); IPage page = new Page (wmNewsPageReqDto.getPage(), wmNewsPageReqDto.getSize()); LambdaQueryWrapper<WmNews> queryWrapper = new LambdaQueryWrapper <>(); if (wmNewsPageReqDto.getStatus() != null ){ queryWrapper.eq(WmNews::getStatus,wmNewsPageReqDto.getStatus()); } if (wmNewsPageReqDto.getBeginPubDate() != null && wmNewsPageReqDto.getEndPubDate()!=null ){ queryWrapper.between(WmNews::getPublishTime,wmNewsPageReqDto.getBeginPubDate(),wmNewsPageReqDto.getEndPubDate()); } if (wmNewsPageReqDto.getChannelId() != null ){ queryWrapper.eq(WmNews::getChannelId,wmNewsPageReqDto.getChannelId()); } if (wmNewsPageReqDto.getKeyword() != null ){ queryWrapper.like(WmNews::getTitle,wmNewsPageReqDto.getKeyword()); } queryWrapper.eq(WmNews::getUserId, WmThreadLocalUtil.getUser().getId()); queryWrapper.orderByDesc(WmNews::getPublishTime); page = page(page,queryWrapper); ResponseResult responseResult = new PageResponseResult (wmNewsPageReqDto.getPage(), wmNewsPageReqDto.getSize(), (int ) page.getTotal()); responseResult.setData(page.getRecords()); return responseResult; }
实现效果
4.3 发布文章功能(核心功能) 文章和素材对应关系表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.heima.model.wemedia.pojos;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import lombok.Data;import java.io.Serializable;@Data @TableName("wm_news_material") public class WmNewsMaterial implements Serializable { private static final long serialVersionUID = 1L ; @TableId(value = "id", type = IdType.AUTO) private Integer id; @TableField("material_id") private Integer materialId; @TableField("news_id") private Integer newsId; @TableField("type") private Short type; @TableField("ord") private Short ord; }
实现流程
接口定义
说明
接口路径
/api/v1/news/submit
请求方式
POST
参数
WmNewsDto
响应结果
ResponseResult
WmNewsDto
接收前端参数的dto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.heima.model.wemedia.dtos;import lombok.Data;import java.util.Date;import java.util.List;@Data public class WmNewsDto { private Integer id; private String title; private Integer channelId; private String labels; private Date publishTime; private String content; private Short type; private Date submitedTime; private Short status; private List<String> images; }
前端传递的json格式数据举例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "title" : "黑马头条项目背景" , "type" : "1" , "labels" : "黑马头条" , "publishTime" : "2020-03-14T11:35:49.000Z" , "channelId" : 1 , "images" : [ "http://192.168.200.130/group1/M00/00/00/wKjIgl5swbGATaSAAAEPfZfx6Iw790.png" ] , "status" : 1 , "content" : "[ { " type":" text", " value":" 随着智能手机的普及,人们更加习惯于通过手机来看新闻。由于生活节奏的加快,很多人只能利用碎片时间来获取信息,因此,对于移动资讯客户端的需求也越来越高。黑马头条项目正是在这样背景下开发出来。黑马头条项目采用当下火热的微服务+大数据技术架构实现。本项目主要着手于获取最新最热新闻资讯,通过大数据分析用户喜好精确推送咨询新闻" }, { " type":" image", " value":" http: } ] " }
保存文章和素材对应关系mapper接口
mapper接口
1 2 3 4 5 6 7 8 9 10 11 12 @Mapper public interface WmNewsMaterialMapper extends BaseMapper <WmNewsMaterial> { void saveRelations (@Param("materialIds") List<Integer> materialIds,@Param("newsId") Integer newsId, @Param("type") Short type) ; }
mapper.xml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.heima.wemedia.mapper.WmNewsMaterialMapper" > <insert id ="saveRelations" > insert into wm_news_material (material_id,news_id,type,ord) values <foreach collection ="materialIds" index ="ord" item ="mid" separator ="," > (#{mid},#{newsId},#{type},#{ord}) </foreach > </insert > </mapper >
使用到的常量类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.heima.common.constants;public class WemediaConstants { public static final Short COLLECT_MATERIAL = 1 ; public static final Short CANCEL_COLLECT_MATERIAL = 0 ; public static final String WM_NEWS_TYPE_IMAGE = "image" ; public static final Short WM_NEWS_NONE_IMAGE = 0 ; public static final Short WM_NEWS_SINGLE_IMAGE = 1 ; public static final Short WM_NEWS_MANY_IMAGE = 3 ; public static final Short WM_NEWS_TYPE_AUTO = -1 ; public static final Short WM_CONTENT_REFERENCE = 0 ; public static final Short WM_COVER_REFERENCE = 1 ; }
发布文章功能的关键代码
存在许多的bug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 package com.heima.wemedia.service.impl;import com.alibaba.fastjson.JSON;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.baomidou.mybatisplus.core.metadata.IPage;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.heima.common.constants.WemediaConstants;import com.heima.common.exception.CustomException;import com.heima.model.common.dtos.PageResponseResult;import com.heima.model.common.dtos.ResponseResult;import com.heima.model.common.enums.AppHttpCodeEnum;import com.heima.model.wemedia.dtos.WmNewsDto;import com.heima.model.wemedia.dtos.WmNewsPageReqDto;import com.heima.model.wemedia.pojos.WmMaterial;import com.heima.model.wemedia.pojos.WmNews;import com.heima.model.wemedia.pojos.WmNewsMaterial;import com.heima.utils.thread.WmThreadLocalUtil;import com.heima.wemedia.mapper.WmMaterialMapper;import com.heima.wemedia.mapper.WmNewsMapper;import com.heima.wemedia.mapper.WmNewsMaterialMapper;import com.heima.wemedia.service.WmNewsService;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang.StringUtils;import org.apache.jute.compiler.JString;import org.springframework.beans.BeanUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.ArrayList;import java.util.Date;import java.util.List;import java.util.Map;import java.util.stream.Collectors;@Service @Slf4j @Transactional public class WmNewsServiceImpl extends ServiceImpl <WmNewsMapper, WmNews> implements WmNewsService { @Autowired private WmNewsMaterialMapper wmNewsMaterialMapper; @Autowired private WmMaterialMapper wmMaterialMapper; @Override public ResponseResult findNewsList (WmNewsPageReqDto wmNewsPageReqDto) { wmNewsPageReqDto.checkParam(); IPage page = new Page (wmNewsPageReqDto.getPage(), wmNewsPageReqDto.getSize()); LambdaQueryWrapper<WmNews> queryWrapper = new LambdaQueryWrapper <>(); if (wmNewsPageReqDto.getStatus() != null ) { queryWrapper.eq(WmNews::getStatus, wmNewsPageReqDto.getStatus()); } if (wmNewsPageReqDto.getBeginPubDate() != null && wmNewsPageReqDto.getEndPubDate() != null ) { queryWrapper.between(WmNews::getPublishTime, wmNewsPageReqDto.getBeginPubDate(), wmNewsPageReqDto.getEndPubDate()); } if (wmNewsPageReqDto.getChannelId() != null ) { queryWrapper.eq(WmNews::getChannelId, wmNewsPageReqDto.getChannelId()); } if (wmNewsPageReqDto.getKeyword() != null ) { queryWrapper.like(WmNews::getTitle, wmNewsPageReqDto.getKeyword()); } queryWrapper.eq(WmNews::getUserId, WmThreadLocalUtil.getUser().getId()); queryWrapper.orderByDesc(WmNews::getPublishTime); page = page(page, queryWrapper); ResponseResult responseResult = new PageResponseResult (wmNewsPageReqDto.getPage(), wmNewsPageReqDto.getSize(), (int ) page.getTotal()); responseResult.setData(page.getRecords()); return responseResult; } @Override public ResponseResult submitNews (WmNewsDto wmNewsDto) { if (wmNewsDto == null || wmNewsDto.getContent() == null ) { return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID); } WmNews wmNews = new WmNews (); BeanUtils.copyProperties(wmNewsDto, wmNews); if (wmNewsDto.getImages() != null && wmNewsDto.getImages().size() > 0 ) { String imageStr = StringUtils.join(wmNewsDto.getImages(), "," ); wmNews.setImages(imageStr); } if (wmNewsDto.getType().equals(WemediaConstants.WM_NEWS_TYPE_AUTO)) { wmNews.setType(null ); } saveOrUpdateWmNews(wmNews); if (wmNewsDto.getStatus().equals(WmNews.Status.NORMAL.getCode())) { return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS); } List<String> materials = getUrlInfo(wmNewsDto.getContent()); saveRelativeInfoContent(materials,wmNews.getId()); saveRelativeInfoForCover(wmNewsDto,wmNews,materials); return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS); } private void saveRelativeInfoForCover (WmNewsDto wmNewsDto, WmNews wmNews, List<String> materials) { List<String> images = wmNewsDto.getImages(); if (wmNewsDto.getType().equals(WemediaConstants.WM_NEWS_TYPE_AUTO)){ if (materials.size() >= 3 ){ wmNews.setType(WemediaConstants.WM_NEWS_MANY_IMAGE); images = materials.stream().limit(3 ).collect(Collectors.toList()); }else if (materials.size() > 1 && materials.size() < 3 ){ wmNews.setType(WemediaConstants.WM_NEWS_SINGLE_IMAGE); images = materials.stream().limit(1 ).collect(Collectors.toList()); }else { wmNews.setType(WemediaConstants.WM_NEWS_NONE_IMAGE); } if (images != null && images.size() > 0 ){ wmNews.setImages(StringUtils.join(images,"," )); } updateById(wmNews); } if (images != null && images.size() > 0 ){ saveRelativeInfo(images,wmNews.getId(),WemediaConstants.WM_COVER_REFERENCE); } } private void saveRelativeInfoContent (List<String> materials, Integer newsId) { saveRelativeInfo(materials,newsId,WemediaConstants.WM_CONTENT_REFERENCE); } private void saveRelativeInfo (List<String> materials, Integer newsId, Short type) { if (materials != null && !materials.isEmpty()){ LambdaQueryWrapper<WmMaterial> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.in(WmMaterial::getUrl,materials); List<WmMaterial> wmMaterials = wmMaterialMapper.selectList(queryWrapper); if (wmMaterials == null || wmMaterials.size() == 0 ){ throw new CustomException (AppHttpCodeEnum.MATERIASL_REFERENCE); } if (materials.size() != wmMaterials.size()){ throw new CustomException (AppHttpCodeEnum.MATERIASL_REFERENCE); } List<Integer> idList = wmMaterials.stream().map(WmMaterial::getId).collect(Collectors.toList()); wmNewsMaterialMapper.saveRelations(idList,newsId,type); } } private List<String> getUrlInfo (String content) { ArrayList<String> materials = new ArrayList <>(); List<Map> maps = JSON.parseArray(content, Map.class); for (Map map : maps) { if (map.get("type" ).equals("image" )){ String imgUrl = (String) map.get("value" ); materials.add(imgUrl); } } return materials; } 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 { LambdaQueryWrapper<WmNewsMaterial> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(WmNewsMaterial::getNewsId, wmNews.getId()); wmNewsMaterialMapper.delete(queryWrapper); updateById(wmNews); } } }
4.4 文章的审核功能(未实现) 4.4.1 文章审核功能介绍 调用第三方的接口(阿里云内容安全审核接口)实现审核功能
功能介绍
审核流程
1 自媒体端发布文章后,开始审核文章
2 审核的主要是审核文章的内容(文本内容和图片)
3 借助第三方提供的接口审核文本
4 借助第三方提供的接口审核图片,由于图片存储到minIO中,需要先下载才能审核
5 如果审核失败,则需要修改自媒体文章的状态,status:2 审核失败 status:3 转到人工审核
6 如果审核成功,则需要在文章微服务中创建app端需要的文章
4.4.2 调用第三方的审核接口 第三方审核接口
1.内容安全接口介绍:
内容安全是识别服务,支持对图片、视频、文本、语音等对象进行多样化场景检测,有效降低内容违规风险。目前很多平台都支持内容检测,如阿里云、腾讯云、百度AI、网易云等国内大型互联网公司都对外提供了API。
2.文件检测和图片检测api文档
文本垃圾内容Java SDK: https://help.aliyun.com/document_detail/53427.html?spm=a2c4g.11186623.6.717.466d7544QbU8Lr
图片垃圾内容Java SDK: https://help.aliyun.com/document_detail/53424.html?spm=a2c4g.11186623.6.715.c8f69b12ey35j4
项目中集成阿里云内容安全接口
1.依赖导入
1 2 3 4 5 6 7 8 <dependency > <groupId > com.aliyun</groupId > <artifactId > aliyun-java-sdk-core</artifactId > </dependency > <dependency > <groupId > com.aliyun</groupId > <artifactId > aliyun-java-sdk-green</artifactId > </dependency >
2.相关的工具类
GreenImageScan(图片审核的工具类)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 package com.heima.common.aliyun;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONArray;import com.alibaba.fastjson.JSONObject;import com.aliyuncs.DefaultAcsClient;import com.aliyuncs.IAcsClient;import com.aliyuncs.green.model.v20180509.ImageSyncScanRequest;import com.aliyuncs.http.FormatType;import com.aliyuncs.http.HttpResponse;import com.aliyuncs.http.MethodType;import com.aliyuncs.http.ProtocolType;import com.aliyuncs.profile.DefaultProfile;import com.aliyuncs.profile.IClientProfile;import com.heima.common.aliyun.util.ClientUploader;import lombok.Getter;import lombok.Setter;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.PropertySource;import org.springframework.stereotype.Component;import java.util.*;@Getter @Setter @Component @ConfigurationProperties(prefix = "aliyun") public class GreenImageScan { private String accessKeyId; private String secret; private String scenes; public Map imageScan (List<byte []> imageList) throws Exception { IClientProfile profile = DefaultProfile .getProfile("cn-shanghai" , accessKeyId, secret); DefaultProfile .addEndpoint("cn-shanghai" , "cn-shanghai" , "Green" , "green.cn-shanghai.aliyuncs.com" ); IAcsClient client = new DefaultAcsClient (profile); ImageSyncScanRequest imageSyncScanRequest = new ImageSyncScanRequest (); imageSyncScanRequest.setAcceptFormat(FormatType.JSON); imageSyncScanRequest.setMethod(MethodType.POST); imageSyncScanRequest.setEncoding("utf-8" ); imageSyncScanRequest.setProtocol(ProtocolType.HTTP); JSONObject httpBody = new JSONObject (); httpBody.put("scenes" , Arrays.asList(scenes.split("," ))); ClientUploader clientUploader = ClientUploader.getImageClientUploader(profile, false ); String url = null ; List<JSONObject> urlList = new ArrayList <JSONObject>(); for (byte [] bytes : imageList) { url = clientUploader.uploadBytes(bytes); JSONObject task = new JSONObject (); task.put("dataId" , UUID.randomUUID().toString()); task.put("url" , url); task.put("time" , new Date ()); urlList.add(task); } httpBody.put("tasks" , urlList); imageSyncScanRequest.setHttpContent(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(httpBody.toJSONString()), "UTF-8" , FormatType.JSON); imageSyncScanRequest.setConnectTimeout(3000 ); imageSyncScanRequest.setReadTimeout(10000 ); HttpResponse httpResponse = null ; try { httpResponse = client.doAction(imageSyncScanRequest); } catch (Exception e) { e.printStackTrace(); } Map<String, String> resultMap = new HashMap <>(); if (httpResponse != null && httpResponse.isSuccess()) { JSONObject scrResponse = JSON.parseObject(org.apache.commons.codec.binary.StringUtils.newStringUtf8(httpResponse.getHttpContent())); System.out.println(JSON.toJSONString(scrResponse, true )); int requestCode = scrResponse.getIntValue("code" ); JSONArray taskResults = scrResponse.getJSONArray("data" ); if (200 == requestCode) { for (Object taskResult : taskResults) { int taskCode = ((JSONObject) taskResult).getIntValue("code" ); JSONArray sceneResults = ((JSONObject) taskResult).getJSONArray("results" ); if (200 == taskCode) { for (Object sceneResult : sceneResults) { String scene = ((JSONObject) sceneResult).getString("scene" ); String label = ((JSONObject) sceneResult).getString("label" ); String suggestion = ((JSONObject) sceneResult).getString("suggestion" ); System.out.println("scene = [" + scene + "]" ); System.out.println("suggestion = [" + suggestion + "]" ); System.out.println("suggestion = [" + label + "]" ); if (!suggestion.equals("pass" )) { resultMap.put("suggestion" , suggestion); resultMap.put("label" , label); return resultMap; } } } else { System.out.println("task process fail. task response:" + JSON.toJSONString(taskResult)); return null ; } } resultMap.put("suggestion" ,"pass" ); return resultMap; } else { System.out.println("the whole image scan request failed. response:" + JSON.toJSONString(scrResponse)); return null ; } } return null ; } }
GreenTextScan(文字审核的工具类)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 package com.heima.common.aliyun;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONArray;import com.alibaba.fastjson.JSONObject;import com.aliyuncs.DefaultAcsClient;import com.aliyuncs.IAcsClient;import com.aliyuncs.exceptions.ClientException;import com.aliyuncs.exceptions.ServerException;import com.aliyuncs.green.model.v20180509.TextScanRequest;import com.aliyuncs.http.FormatType;import com.aliyuncs.http.HttpResponse;import com.aliyuncs.profile.DefaultProfile;import com.aliyuncs.profile.IClientProfile;import lombok.Getter;import lombok.Setter;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.PropertySource;import org.springframework.stereotype.Component;import java.util.*;@Getter @Setter @Component @ConfigurationProperties(prefix = "aliyun") public class GreenTextScan { private String accessKeyId; private String secret; public Map greeTextScan (String content) throws Exception { System.out.println(accessKeyId); IClientProfile profile = DefaultProfile .getProfile("cn-shanghai" , accessKeyId, secret); DefaultProfile.addEndpoint("cn-shanghai" , "cn-shanghai" , "Green" , "green.cn-shanghai.aliyuncs.com" ); IAcsClient client = new DefaultAcsClient (profile); TextScanRequest textScanRequest = new TextScanRequest (); textScanRequest.setAcceptFormat(FormatType.JSON); textScanRequest.setHttpContentType(FormatType.JSON); textScanRequest.setMethod(com.aliyuncs.http.MethodType.POST); textScanRequest.setEncoding("UTF-8" ); textScanRequest.setRegionId("cn-shanghai" ); List<Map<String, Object>> tasks = new ArrayList <Map<String, Object>>(); Map<String, Object> task1 = new LinkedHashMap <String, Object>(); task1.put("dataId" , UUID.randomUUID().toString()); task1.put("content" , content); tasks.add(task1); JSONObject data = new JSONObject (); data.put("scenes" , Arrays.asList("antispam" )); data.put("tasks" , tasks); System.out.println(JSON.toJSONString(data, true )); textScanRequest.setHttpContent(data.toJSONString().getBytes("UTF-8" ), "UTF-8" , FormatType.JSON); textScanRequest.setConnectTimeout(3000 ); textScanRequest.setReadTimeout(6000 ); Map<String, String> resultMap = new HashMap <>(); try { HttpResponse httpResponse = client.doAction(textScanRequest); if (httpResponse.isSuccess()) { JSONObject scrResponse = JSON.parseObject(new String (httpResponse.getHttpContent(), "UTF-8" )); System.out.println(JSON.toJSONString(scrResponse, true )); if (200 == scrResponse.getInteger("code" )) { JSONArray taskResults = scrResponse.getJSONArray("data" ); for (Object taskResult : taskResults) { if (200 == ((JSONObject) taskResult).getInteger("code" )) { JSONArray sceneResults = ((JSONObject) taskResult).getJSONArray("results" ); for (Object sceneResult : sceneResults) { String scene = ((JSONObject) sceneResult).getString("scene" ); String label = ((JSONObject) sceneResult).getString("label" ); String suggestion = ((JSONObject) sceneResult).getString("suggestion" ); System.out.println("suggestion = [" + label + "]" ); if (!suggestion.equals("pass" )) { resultMap.put("suggestion" , suggestion); resultMap.put("label" , label); return resultMap; } } } else { return null ; } } resultMap.put("suggestion" , "pass" ); return resultMap; } else { return null ; } } else { return null ; } } catch (ServerException e) { e.printStackTrace(); } catch (ClientException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } return null ; } }
3.在heima-leadnews-wemedia中的nacos配置中心添加以下配置
1 2 3 4 5 aliyun: accessKeyId: xxxxxxxxxxxxxxxxxxx secret: xxxxxxxxxxxxxxxxxxxxxxxx scenes: terrorism
4.编写测试类测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import com.heima.common.aliyun.GreenImageScan;import com.heima.common.aliyun.GreenTextScan;import com.heima.file.service.FileStorageService;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.util.Arrays;import java.util.Map;@SpringBootTest(classes = WemediaApplication.class) @RunWith(SpringRunner.class) public class AliyunTest { @Autowired private GreenTextScan greenTextScan; @Autowired private GreenImageScan greenImageScan; @Autowired private FileStorageService fileStorageService; @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.200.130:9000/leadnews/2021/04/26/ef3cbe458db249f7bd6fb4339e593e55.jpg" ); Map map = greenImageScan.imageScan(Arrays.asList(bytes)); System.out.println(map); } }
4.4.3 分布式ID的实现 为什么使用分布式ID
分布式ID的技术选型
雪花算法的介绍
mybatis-plus已经集成了雪花算法,完成以下两步即可在项目中集成雪花算法
第一:在实体类中的id上加入如下配置,指定类型为id_worker
1 2 @TableId(value = "id",type = IdType.ID_WORKER) private Long id;
第二:在application.yml文件中配置数据中心id和机器id
1 2 3 4 5 6 7 mybatis-plus: mapper-locations: classpath*:mapper/*.xml type-aliases-package: com.heima.model.article.pojos global-config: datacenter-id: 1 workerId: 1
datacenter-id:数据中心id(取值范围:0-31) workerId:机器id(取值范围:0-31)
4.4.4 审核功能的具体实现 由于没有阿里云相关的ak和sk,所以本部分默认每篇文章的文字和图片都审核通过(中间会注释掉调用第三方审核接口的代码)
service层代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 package com.heima.wemedia.service.impl;import com.alibaba.fastjson.JSON;import com.heima.apis.article.IArticleClient;import com.heima.common.aliyun.GreenImageScan;import com.heima.common.aliyun.GreenTextScan;import com.heima.file.service.FileStorageService;import com.heima.model.article.dtos.ArticleDto;import com.heima.model.common.dtos.ResponseResult;import com.heima.model.wemedia.pojos.WmChannel;import com.heima.model.wemedia.pojos.WmNews;import com.heima.model.wemedia.pojos.WmUser;import com.heima.wemedia.mapper.WmChannelMapper;import com.heima.wemedia.mapper.WmNewsMapper;import com.heima.wemedia.mapper.WmUserMapper;import com.heima.wemedia.service.WmNewsAutoScanService;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang.StringUtils;import org.springframework.beans.BeanUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.*;import java.util.stream.Collectors;@Service @Slf4j @Transactional public class WmNewsAutoScanServiceImpl implements WmNewsAutoScanService { @Value("${check.env}") private String environment; @Autowired private WmNewsMapper wmNewsMapper; @Autowired private GreenTextScan greenTextScan; @Autowired private GreenImageScan greenImageScan; @Autowired private FileStorageService fileStorageService; @Autowired private IArticleClient iArticleClient; @Autowired private WmChannelMapper wmChannelMapper; @Autowired private WmUserMapper wmUserMapper; @Override public void autoScanWmNews (Integer id) { WmNews wmNews = wmNewsMapper.selectById(id); if (wmNews == null ) { throw new RuntimeException ("文章不存在,无法审核!" ); } if (wmNews.getStatus().equals(WmNews.Status.SUBMIT.getCode())) { Map<String, Object> textAndImages = getTextAndImages(wmNews); Boolean isPassText = checkText((String) textAndImages.get("content" ), wmNews); if (!isPassText) return ; Boolean isPassImage = checkImages((List<String>) textAndImages.get("images" ), wmNews); if (!isPassImage) return ; ResponseResult responseResult = saveAppArticle(wmNews); if (!responseResult.getCode().equals(200 )) { throw new RuntimeException ("文章审核-保存app端文章失败" ); } wmNews.setArticleId((Long) responseResult.getData()); updateWmNews(wmNews, (short ) 9 , "审核成功" ); } } 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(wmNews.getUserId().longValue()); WmUser wmUser = wmUserMapper.selectById(wmNews.getUserId()); if (wmUser != null ) { dto.setAuthorName(wmUser.getName()); } if (wmNews.getArticleId() != null ) { dto.setId(wmNews.getArticleId()); } dto.setCreatedTime(new Date ()); return iArticleClient.saveArticle(dto); } private Boolean checkImages (List<String> images, WmNews wmNews) { boolean flag = true ; if (images == null || images.size() == 0 || "test" .equals(environment)) { return true ; } images = images.stream().distinct().collect(Collectors.toList()); ArrayList<byte []> byteList = new ArrayList <>(); for (String image : images) { byte [] bytes = fileStorageService.downLoadFile(image); byteList.add(bytes); } try { Map map = greenImageScan.imageScan(byteList); if (map != null ) { if (map.get("suggestion" ).equals("block" )) { updateWmNews(wmNews, (short ) 2 , "图片存在违规内容!" ); flag = false ; } if (map.get("suggestion" ).equals("review" )) { updateWmNews(wmNews, (short ) 3 , "图片存在违规内容!" ); flag = false ; } } } catch (Exception e) { flag = false ; e.printStackTrace(); throw new RuntimeException ("图片审核时出现异常!" ); } return flag; } private Boolean checkText (String content, WmNews wmNews) { boolean flag = true ; if ((content + wmNews.getTitle()).length() == 0 || "test" .equals(environment)) { return true ; } try { Map map = greenTextScan.greeTextScan(content); if (map != null ) { if (map.get("suggestion" ).equals("block" )) { updateWmNews(wmNews, (short ) 2 , "文章中的文字信息出现违规内容!" ); flag = false ; } if (map.get("suggestion" ).equals("review" )) { updateWmNews(wmNews, (short ) 3 , "文章中的文字信息在审核时有不确定的内容!" ); flag = false ; } } } catch (Exception e) { flag = false ; e.printStackTrace(); throw new RuntimeException ("文字审核时出现异常!" ); } return flag; } public void updateWmNews (WmNews wmNews, short status, String reason) { wmNews.setStatus(status); wmNews.setReason(reason); wmNewsMapper.updateById(wmNews); } private Map<String, Object> getTextAndImages (WmNews wmNews) { StringBuilder stringBuilder = new StringBuilder (); ArrayList<String> images = new ArrayList <>(); if (StringUtils.isNotBlank(wmNews.getContent())) { List<Map> maps = JSON.parseArray(wmNews.getContent(), Map.class); for (Map map : maps) { if (map.get("type" ).equals("text" )) { stringBuilder.append(map.get("value" )); } if (map.get("type" ).equals("image" )) { images.add((String) map.get("values" )); } } } if (StringUtils.isNotBlank(wmNews.getImages())) { String[] split = wmNews.getImages().split("," ); images.addAll(Arrays.asList(split)); } HashMap<String, Object> resultMap = new HashMap <>(); resultMap.put("content" , stringBuilder.toString()); resultMap.put("images" , images); return resultMap; } }
单元测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.heima.wemedia.service;import com.heima.wemedia.WemediaApplication;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;@SpringBootTest(classes = WemediaApplication.class) @RunWith(SpringRunner.class) public class WmNewsAutoScanServiceTest { @Autowired private WmNewsAutoScanService wmNewsAutoScanService; @Test public void autoScanWmNews () { wmNewsAutoScanService.autoScanWmNews(3 ); } }
实现步骤:
①:在heima-leadnews-feign-api编写降级逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.heima.apis.article.fallback;import com.heima.apis.article.IArticleClient;import com.heima.model.article.dtos.ArticleDto;import com.heima.model.common.dtos.ResponseResult;import com.heima.model.common.enums.AppHttpCodeEnum;import org.springframework.stereotype.Component;@Component public class IArticleClientFallback implements IArticleClient { @Override public ResponseResult saveArticle (ArticleDto dto) { return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR,"获取数据失败" ); } }
在自媒体微服务中添加类,扫描降级代码类的包
1 2 3 4 5 6 7 8 9 package com.heima.wemedia.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration @ComponentScan("com.heima.apis.article.fallback") public class InitConfig {}
②:远程接口中指向降级代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.heima.apis.article;import com.heima.apis.article.fallback.IArticleClientFallback;import com.heima.model.article.dtos.ArticleDto;import com.heima.model.common.dtos.ResponseResult;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;@FeignClient(value = "leadnews-article",fallback = IArticleClientFallback.class) public interface IArticleClient { @PostMapping("/api/v1/article/save") public ResponseResult saveArticle (@RequestBody ArticleDto dto) ; }
③:客户端开启降级heima-leadnews-wemedia
在wemedia的nacos配置中心里添加如下内容,开启服务降级,也可以指定服务响应的超时的时间
1 2 3 4 5 6 7 8 9 10 feign: hystrix: enabled: true client: config: default: connectTimeout: 2000 readTimeout: 2000
④:测试
在ApArticleServiceImpl类中saveArticle方法添加代码
1 2 3 4 5 try { Thread.sleep(3000 ); } catch (InterruptedException e) { e.printStackTrace(); }
在自媒体端进行审核测试,会出现服务降级的现象
4.5 app端文章保存功能 实现思路
在文章审核成功以后需要在app的article库中新增文章数据
1.保存文章信息 ap_article
2.保存文章配置信息 ap_article_config
3.保存文章内容 ap_article_content
保存文章的接口
说明
接口路径
/api/v1/article/save
请求方式
POST
参数
ArticleDto
响应结果
ResponseResult
ArticleDto
1 2 3 4 5 6 7 8 9 10 11 12 package com.heima.model.article.dtos;import com.heima.model.article.pojos.ApArticle;import lombok.Data;@Data public class ArticleDto extends ApArticle { private String content; }
成功:
1 2 3 4 5 { "code" : 200 , "errorMessage" : "操作成功" , "data" : "1302864436297442242" }
失败:
1 2 3 4 { "code" : 501 , "errorMessage" : "参数失效" , }
1 2 3 4 { "code" : 501 , "errorMessage" : "文章没有找到" , }
实现步骤
功能实现:
①:在heima-leadnews- feign-api中新增接口
第一:线导入feign的依赖
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency >
第二:定义文章端的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.heima.apis.article; import com.heima.model.article.dtos.ArticleDto; import com.heima.model.common.dtos.ResponseResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import java.io.IOException; @FeignClient(value = "leadnews-article" ) public interface IArticleClient { @PostMapping("/api/v1/article/save" ) public ResponseResult saveArticle(@RequestBody ArticleDto dto) ; }
②:在heima-leadnews-article中实现该方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.heima.article.feign;import com.heima.apis.article.IArticleClient;import com.heima.article.service.ApArticleService;import com.heima.model.article.dtos.ArticleDto;import com.heima.model.common.dtos.ResponseResult;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import java.io.IOException;@RestController public class ArticleClient implements IArticleClient { @Autowired private ApArticleService apArticleService; @Override @PostMapping("/api/v1/article/save") public ResponseResult saveArticle (@RequestBody ArticleDto dto) { return apArticleService.saveArticle(dto); } }
③:拷贝mapper
在资料文件夹中拷贝ApArticleConfigMapper类到mapper文件夹中
同时,修改ApArticleConfig类,添加如下构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 package com.heima.model.article.pojos;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import lombok.Data;import lombok.NoArgsConstructor;import java.io.Serializable;@Data @NoArgsConstructor @TableName("ap_article_config") public class ApArticleConfig implements Serializable { public ApArticleConfig (Long articleId) { this .articleId = articleId; this .isComment = true ; this .isForward = true ; this .isDelete = false ; this .isDown = false ; } @TableId(value = "id",type = IdType.ID_WORKER) private Long id; @TableField("article_id") private Long articleId; @TableField("is_comment") private Boolean isComment; @TableField("is_forward") private Boolean isForward; @TableField("is_down") private Boolean isDown; @TableField("is_delete") private Boolean isDelete; }
④:在ApArticleService中新增方法
1 2 3 4 5 6 ResponseResult saveArticle (ArticleDto dto) ;
实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @Autowired private ApArticleConfigMapper apArticleConfigMapper;@Autowired private ApArticleContentMapper apArticleContentMapper;@Override public ResponseResult saveArticle (ArticleDto dto) { if (dto == null ){ return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID); } ApArticle apArticle = new ApArticle (); BeanUtils.copyProperties(dto,apArticle); if (dto.getId() == null ){ 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 { updateById(apArticle); ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, dto.getId())); apArticleContent.setContent(dto.getContent()); apArticleContentMapper.updateById(apArticleContent); } return ResponseResult.okResult(apArticle.getId()); }
⑤:测试
编写junit单元测试,或使用postman进行测试
1 2 3 4 5 6 7 8 9 { "title" : "黑马头条项目背景" , "authoId" : 1102 , "layout" : 1 , "labels" : "黑马头条" , "publishTime" : "2028-03-14T11:35:49.000Z" , "images" : "http://192.168.200.130:9000/leadnews/2021/04/26/5ddbdb5c68094ce393b08a47860da275.jpg" , "content" : "黑马头条项目背景,黑马头条项目背景,黑马头条项目背景,黑马头条项目背景,黑马头条项目背景" }
4.6 发布文章提交审核集成 4.6.1 同步调用与异步调用
4.6.2 Springboot集成异步线程调用 ①:在自动审核的方法上加上@Async注解(标明要异步调用)
1 2 3 4 5 @Override @Async public void autoScanWmNews (Integer id) { }
②:在文章发布成功后调用审核的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Autowired private WmNewsAutoScanService wmNewsAutoScanService;@Override public ResponseResult submitNews (WmNewsDto dto) { wmNewsAutoScanService.autoScanWmNews(wmNews.getId()); return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS); }
③:在自媒体引导类中使用@EnableAsync注解开启异步调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SpringBootApplication @EnableDiscoveryClient @MapperScan("com.heima.wemedia.mapper") @EnableFeignClients(basePackages = "com.heima.apis") @EnableAsync public class WemediaApplication { public static void main (String[] args) { SpringApplication.run(WemediaApplication.class,args); } @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor (DbType.MYSQL)); return interceptor; } }
4.7 文章审核功能-综合测试 4.7.1 服务启动列表 1,nacos服务端
2,article微服务
3,wemedia微服务
4,启动wemedia网关微服务
5,启动前端系统wemedia
4.7.2 测试情况列表 1,自媒体前端发布一篇正常的文章
审核成功后,app端的article相关数据是否可以正常保存,自媒体文章状态和app端文章id是否回显
2,自媒体前端发布一篇包含敏感词的文章
正常是审核失败, wm_news表中的状态是否改变,成功和失败原因正常保存
3,自媒体前端发布一篇包含敏感图片的文章
正常是审核失败, wm_news表中的状态是否改变,成功和失败原因正常保存
4.8 自管理敏感词过滤 4.8.1 需求
4.8.2 可选方案
方案
说明
数据库模糊查询
效率太低
String.indexOf(“”)查找
数据库量大的话也是比较慢
全文检索
分词再匹配
DFA算法
确定有穷自动机(一种数据结构)
4.8.3 DFA算法
4.8.4 工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 package com.heima.utils.common;import java.util.*;public class SensitiveWordUtil { public static Map<String, Object> dictionaryMap = new HashMap <>(); public static void initMap (Collection<String> words) { if (words == null ) { System.out.println("敏感词列表不能为空" ); return ; } Map<String, Object> map = new HashMap <>(words.size()); Map<String, Object> curMap = null ; Iterator<String> iterator = words.iterator(); while (iterator.hasNext()) { String word = iterator.next(); curMap = map; int len = word.length(); for (int i = 0 ; i < len; i++) { String key = String.valueOf(word.charAt(i)); Map<String, Object> wordMap = (Map<String, Object>) curMap.get(key); if (wordMap == null ) { wordMap = new HashMap <>(2 ); wordMap.put("isEnd" , "0" ); curMap.put(key, wordMap); } curMap = wordMap; if (i == len -1 ) { curMap.put("isEnd" , "1" ); } } } dictionaryMap = map; } private static int checkWord (String text, int beginIndex) { if (dictionaryMap == null ) { throw new RuntimeException ("字典不能为空" ); } boolean isEnd = false ; int wordLength = 0 ; Map<String, Object> curMap = dictionaryMap; int len = text.length(); for (int i = beginIndex; i < len; i++) { String key = String.valueOf(text.charAt(i)); curMap = (Map<String, Object>) curMap.get(key); if (curMap == null ) { break ; } else { wordLength ++; if ("1" .equals(curMap.get("isEnd" ))) { isEnd = true ; } } } if (!isEnd) { wordLength = 0 ; } return wordLength; } public static Map<String, Integer> matchWords (String text) { Map<String, Integer> wordMap = new HashMap <>(); int len = text.length(); for (int i = 0 ; i < len; i++) { int wordLength = checkWord(text, i); if (wordLength > 0 ) { String word = text.substring(i, i + wordLength); if (wordMap.containsKey(word)) { wordMap.put(word, wordMap.get(word) + 1 ); } else { wordMap.put(word, 1 ); } i += wordLength - 1 ; } } return wordMap; } public static void main (String[] args) { List<String> list = new ArrayList <>(); list.add("法轮" ); list.add("法轮功" ); list.add("冰毒" ); initMap(list); String content="我是一个好人,并不会卖冰毒,也不操练法轮功,我真的不卖冰毒" ; Map<String, Integer> map = matchWords(content); System.out.println(map); } }
4.8.5 项目中集成自管理敏感词过滤 ①:创建敏感词表,导入资料中wm_sensitive到leadnews_wemedia库中,并创建对应的实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.heima.model.wemedia.pojos;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import lombok.Data;import java.io.Serializable;import java.util.Date;@Data @TableName("wm_sensitive") public class WmSensitive implements Serializable { private static final long serialVersionUID = 1L ; @TableId(value = "id", type = IdType.AUTO) private Integer id; @TableField("sensitives") private String sensitives; @TableField("created_time") private Date createdTime; }
②:拷贝对应的wm_sensitive的mapper到项目中
1 2 3 4 5 6 7 8 9 10 package com.heima.wemedia.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.heima.model.wemedia.pojos.WmSensitive;import org.apache.ibatis.annotations.Mapper;@Mapper public interface WmSensitiveMapper extends BaseMapper <WmSensitive> {}
③:在文章审核的代码中添加自管理敏感词审核
第一:在WmNewsAutoScanServiceImpl中的autoScanWmNews方法上添加如下代码
1 2 3 4 5 6 7 8 9 boolean isSensitive = handleSensitiveScan((String) textAndImages.get("content" ), wmNews);if (!isSensitive) return ;
新增自管理敏感词审核代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Autowired private WmSensitiveMapper wmSensitiveMapper;private boolean handleSensitiveScan (String content, WmNews wmNews) { boolean flag = true ; List<WmSensitive> wmSensitives = wmSensitiveMapper.selectList(Wrappers.<WmSensitive>lambdaQuery().select(WmSensitive::getSensitives)); List<String> sensitiveList = wmSensitives.stream().map(WmSensitive::getSensitives).collect(Collectors.toList()); SensitiveWordUtil.initMap(sensitiveList); Map<String, Integer> map = SensitiveWordUtil.matchWords(content); if (map.size() >0 ){ updateWmNews(wmNews,(short ) 2 ,"当前文章中存在违规内容" +map); flag = false ; } return flag; }
4.9 图片识别文字审核敏感词 详细教程: https://qingling.icu/posts/58456.html
①:在heima-leadnews-common中创建工具类,简单封装一下tess4j
需要先导入pom
1 2 3 4 5 <dependency > <groupId > net.sourceforge.tess4j</groupId > <artifactId > tess4j</artifactId > <version > 4.1.1</version > </dependency >
工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.heima.common.tess4j;import lombok.Getter;import lombok.Setter;import net.sourceforge.tess4j.ITesseract;import net.sourceforge.tess4j.Tesseract;import net.sourceforge.tess4j.TesseractException;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;import java.awt.image.BufferedImage;@Getter @Setter @Component @ConfigurationProperties(prefix = "tess4j") public class Tess4jClient { private String dataPath; private String language; public String doOCR (BufferedImage image) throws TesseractException { ITesseract tesseract = new Tesseract (); tesseract.setDatapath(dataPath); tesseract.setLanguage(language); String result = tesseract.doOCR(image); result = result.replaceAll("\\r|\\n" , "-" ).replaceAll(" " , "" ); return result; } }
在spring.factories配置中添加该类,完整如下:
1 2 3 4 5 6 7 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.heima.common.exception.ExceptionCatch,\ com.heima.common.swagger.SwaggerConfiguration,\ com.heima.common.swagger.Swagger2Configuration,\ com.heima.common.aliyun.GreenTextScan,\ com.heima.common.aliyun.GreenImageScan,\ com.heima.common.tess4j.Tess4jClient
②:在heima-leadnews-wemedia中的配置中添加两个属性
1 2 3 tess4j: data-path: D:\workspace\tessdata language: chi_sim
③:在WmNewsAutoScanServiceImpl中的handleImageScan方法上添加如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 try { for (String image : images) { byte [] bytes = fileStorageService.downLoadFile(image); ByteArrayInputStream in = new ByteArrayInputStream (bytes); BufferedImage imageFile = ImageIO.read(in); String result = tess4jClient.doOCR(imageFile); boolean isSensitive = handleSensitiveScan(result, wmNews); if (!isSensitive){ return isSensitive; } imageList.add(bytes); } }catch (Exception e){ e.printStackTrace(); }
最后附上文章审核的完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 package com.heima.wemedia.service.impl;import com.alibaba.fastjson.JSONArray;import com.baomidou.mybatisplus.core.toolkit.Wrappers;import com.heima.apis.article.IArticleClient;import com.heima.common.aliyun.GreenImageScan;import com.heima.common.aliyun.GreenTextScan;import com.heima.common.tess4j.Tess4jClient;import com.heima.file.service.FileStorageService;import com.heima.model.article.dtos.ArticleDto;import com.heima.model.common.dtos.ResponseResult;import com.heima.model.wemedia.pojos.WmChannel;import com.heima.model.wemedia.pojos.WmNews;import com.heima.model.wemedia.pojos.WmSensitive;import com.heima.model.wemedia.pojos.WmUser;import com.heima.utils.common.SensitiveWordUtil;import com.heima.wemedia.mapper.WmChannelMapper;import com.heima.wemedia.mapper.WmNewsMapper;import com.heima.wemedia.mapper.WmSensitiveMapper;import com.heima.wemedia.mapper.WmUserMapper;import com.heima.wemedia.service.WmNewsAutoScanService;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.springframework.beans.BeanUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.annotation.Async;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import javax.imageio.ImageIO;import java.awt.image.BufferedImage;import java.io.ByteArrayInputStream;import java.util.*;import java.util.stream.Collectors;@Service @Slf4j @Transactional public class WmNewsAutoScanServiceImpl implements WmNewsAutoScanService { @Autowired private WmNewsMapper wmNewsMapper; @Override @Async 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<String, Object> textAndImages = handleTextAndImages(wmNews); boolean isSensitive = handleSensitiveScan((String) textAndImages.get("content" ), wmNews); if (!isSensitive) return ; boolean isTextScan = handleTextScan((String) textAndImages.get("content" ), wmNews); if (!isTextScan) return ; boolean isImageScan = handleImageScan((List<String>) textAndImages.get("images" ), wmNews); if (!isImageScan) return ; ResponseResult responseResult = saveAppArticle(wmNews); if (!responseResult.getCode().equals(200 )) { throw new RuntimeException ("WmNewsAutoScanServiceImpl-文章审核,保存app端相关文章数据失败" ); } wmNews.setArticleId((Long) responseResult.getData()); updateWmNews(wmNews, (short ) 9 , "审核成功" ); } } @Autowired private WmSensitiveMapper wmSensitiveMapper; private boolean handleSensitiveScan (String content, WmNews wmNews) { boolean flag = true ; List<WmSensitive> wmSensitives = wmSensitiveMapper.selectList(Wrappers.<WmSensitive>lambdaQuery().select(WmSensitive::getSensitives)); List<String> sensitiveList = wmSensitives.stream().map(WmSensitive::getSensitives).collect(Collectors.toList()); SensitiveWordUtil.initMap(sensitiveList); Map<String, Integer> map = SensitiveWordUtil.matchWords(content); if (map.size() >0 ){ updateWmNews(wmNews,(short ) 2 ,"当前文章中存在违规内容" +map); flag = false ; } return flag; } @Autowired private IArticleClient articleClient; @Autowired private WmChannelMapper wmChannelMapper; @Autowired private WmUserMapper wmUserMapper; 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(wmNews.getUserId().longValue()); WmUser wmUser = wmUserMapper.selectById(wmNews.getUserId()); if (wmUser != null ) { dto.setAuthorName(wmUser.getName()); } if (wmNews.getArticleId() != null ) { dto.setId(wmNews.getArticleId()); } dto.setCreatedTime(new Date ()); ResponseResult responseResult = articleClient.saveArticle(dto); return responseResult; } @Autowired private FileStorageService fileStorageService; @Autowired private GreenImageScan greenImageScan; @Autowired private Tess4jClient tess4jClient; private boolean handleImageScan (List<String> images, WmNews wmNews) { boolean flag = true ; if (images == null || images.size() == 0 ) { return flag; } images = images.stream().distinct().collect(Collectors.toList()); List<byte []> imageList = new ArrayList <>(); try { for (String image : images) { byte [] bytes = fileStorageService.downLoadFile(image); ByteArrayInputStream in = new ByteArrayInputStream (bytes); BufferedImage imageFile = ImageIO.read(in); String result = tess4jClient.doOCR(imageFile); boolean isSensitive = handleSensitiveScan(result, wmNews); if (!isSensitive){ return isSensitive; } imageList.add(bytes); } }catch (Exception e){ e.printStackTrace(); } try { Map map = greenImageScan.imageScan(imageList); if (map != null ) { if (map.get("suggestion" ).equals("block" )) { flag = false ; updateWmNews(wmNews, (short ) 2 , "当前文章中存在违规内容" ); } if (map.get("suggestion" ).equals("review" )) { flag = false ; updateWmNews(wmNews, (short ) 3 , "当前文章中存在不确定内容" ); } } } catch (Exception e) { flag = false ; e.printStackTrace(); } return flag; } @Autowired private GreenTextScan greenTextScan; private boolean handleTextScan (String content, WmNews wmNews) { boolean flag = true ; if ((wmNews.getTitle() + "-" + content).length() == 0 ) { return flag; } try { Map map = greenTextScan.greeTextScan((wmNews.getTitle() + "-" + content)); if (map != null ) { if (map.get("suggestion" ).equals("block" )) { flag = false ; updateWmNews(wmNews, (short ) 2 , "当前文章中存在违规内容" ); } if (map.get("suggestion" ).equals("review" )) { flag = false ; updateWmNews(wmNews, (short ) 3 , "当前文章中存在不确定内容" ); } } } catch (Exception e) { flag = false ; e.printStackTrace(); } return flag; } private void updateWmNews (WmNews wmNews, short status, String reason) { wmNews.setStatus(status); wmNews.setReason(reason); wmNewsMapper.updateById(wmNews); } private Map<String, Object> handleTextAndImages (WmNews wmNews) { StringBuilder stringBuilder = new StringBuilder (); List<String> images = new ArrayList <>(); if (StringUtils.isNotBlank(wmNews.getContent())) { List<Map> maps = JSONArray.parseArray(wmNews.getContent(), Map.class); for (Map map : maps) { if (map.get("type" ).equals("text" )) { stringBuilder.append(map.get("value" )); } if (map.get("type" ).equals("image" )) { images.add((String) map.get("value" )); } } } if (StringUtils.isNotBlank(wmNews.getImages())) { String[] split = wmNews.getImages().split("," ); images.addAll(Arrays.asList(split)); } Map<String, Object> resultMap = new HashMap <>(); resultMap.put("content" , stringBuilder.toString()); resultMap.put("images" , images); return resultMap; } }
4.10 文章详情-静态文件生成
实现步骤
1.新建ArticleFreemarkerService ,定义创建静态文件并上传到minIO中方法
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.heima.article.service;import com.heima.model.article.pojos.ApArticle;public interface ArticleFreemarkerService { public void buildArticleToMinIO (ApArticle apArticle,String content) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 package com.heima.article.service.Impl;import com.alibaba.fastjson.JSONArray;import com.heima.article.service.ApArticleService;import com.heima.article.service.ArticleFreemarkerService;import com.heima.file.service.FileStorageService;import com.heima.model.article.pojos.ApArticle;import freemarker.template.Configuration;import freemarker.template.Template;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang.StringUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.annotation.Async;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.io.ByteArrayInputStream;import java.io.InputStream;import java.io.StringWriter;import java.util.HashMap;@Service @Slf4j @Transactional public class ArticleFreemarkerServiceImpl implements ArticleFreemarkerService { @Autowired private Configuration configuration; @Autowired private FileStorageService fileStorageService; @Autowired private ApArticleService apArticleService; @Async @Override public void buildArticleToMinio (ApArticle apArticle, String content) { if (StringUtils.isNotBlank(content)) { StringWriter out = new StringWriter (); try { Template template = configuration.getTemplate("article.ftl" ); HashMap<String, Object> map = new HashMap <>(); map.put("content" , JSONArray.parseArray(content)); out = new StringWriter (); template.process(map, out); } catch (Exception e) { e.printStackTrace(); } InputStream in = new ByteArrayInputStream (out.toString().getBytes()); String path = fileStorageService.uploadHtmlFile("" , apArticle.getId() + ".html" , in); log.info("文件在minio中的路径:" + path); apArticle.setStaticUrl(path); boolean isSuccess = apArticleService.updateById(apArticle); log.info(isSuccess ? "文件上传成功" : "文件上传失败" ); } } }
2.在ApArticleService的saveArticle实现方法中添加调用生成文件的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @Override public ResponseResult saveArticle (ArticleDto dto) { if (dto == null ){ return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID); } ApArticle apArticle = new ApArticle (); BeanUtils.copyProperties(dto,apArticle); if (dto.getId() == null ){ 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 { updateById(apArticle); ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, dto.getId())); apArticleContent.setContent(dto.getContent()); apArticleContentMapper.updateById(apArticleContent); } articleFreemarkerService.buildArticleToMinIO(apArticle,dto.getContent()); return ResponseResult.okResult(apArticle.getId()); }
3.文章微服务开启异步调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.heima.article;import com.baomidou.mybatisplus.annotation.DbType;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.context.annotation.Bean;import org.springframework.scheduling.annotation.EnableAsync;@SpringBootApplication @EnableDiscoveryClient @EnableAsync @MapperScan("com.heima.article.mapper") public class ArticleApplication { public static void main (String[] args) { SpringApplication.run(ArticleApplication.class,args); } @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor (DbType.MYSQL)); return interceptor; } }
测试
4.11 文章定时发布