0.黑马头条-学习目标
- 能够理解什么是分布式任务调度
- 能够掌握xxl-job的基本使用
- 能够使用xxl-job解决黑马头条项目中定时任务的功能
- 能够完成自媒体文章人工审核功能
- 能够完成自媒体端文章上下架同步的问题
1.分布式任务调度
1.1.什么是任务调度
我们可以先思考一下业务场景的解决方案:
- 某电商系统需要在每天上午10点,下午3点,晚上8点发放一批优惠券。
- 某银行系统需要在信用卡到期还款日的前三天进行短信提醒。
- 某财务系统需要在每天凌晨0:10结算前一天的财务数据,统计汇总。
- 12306会根据车次的不同,设置某几个时间点进行分批放票。
以上业务场景的解决方案就是任务调度。
任务调度是指系统为了自动完成特定任务,在约定的特定时刻去执行任务的过程。有了任务调度即可解放更多的人力,而是由系统自动去执行任务。
如何实现任务调度?
- 多线程方式,结合sleep
- JDK提供的API,例如:Timer、ScheduledExecutor
- 框架,例如Quartz ,它是一个功能强大的任务调度框架,可以满足更多更复杂的调度需求
- spring task
1.2.入门案例
spring框架中默认就支持了一个任务调度,spring-task
1.创建一个工程:spring-task-demo
pom文件
<!-- 继承Spring boot工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
2.引导类
@SpringBootApplication
@EnableScheduling
public class TaskApplication {
public static void main(String[] args) {
SpringApplication.run(TaskApplication.class,args);
}
}
3.编写案例
@Component
public class HelloJob {
@Scheduled(cron = "0/5 * * * * ?")
public void eat() {
System.out.println("5秒中吃一次饭,我想成为一个胖子" + new Date());
}
}
4.测试:启动项目,每隔5秒中会执行一次eat方法
1.3.cron表达式
cron表达式是一个字符串, 用来设置定时规则, 由七部分组成, 每部分中间用空格隔开, 每部分的含义如下表所示:
表现格式:0/5 * * * * ?
另外, cron表达式还可以包含一些特殊符号来设置更加灵活的定时规则, 如下表所示:
为了让大家更熟悉cron表达式的用法, 接下来我们给大家列举了一些例子, 如下表所示:
1.4.什么是分布式任务调度
当前软件的架构已经开始向分布式架构转变,将单体结构拆分为若干服务,服务之间通过网络交互来完成业务处理。在分布式架构下,一个服务往往会部署多个实例来运行我们的业务,如果在这种分布式系统环境下运行任务调度,我们称之为分布式任务调度。
将任务调度程序分布式构建,这样就可以具有分布式系统的特点,并且提高任务的调度处理能力:
1、并行任务调度
并行任务调度实现靠多线程,如果有大量任务需要调度,此时光靠多线程就会有瓶颈了,因为一台计算机CPU的处理能力是有限的。
如果将任务调度程序分布式部署,每个结点还可以部署为集群,这样就可以让多台计算机共同去完成任务调度,我们可以将任务分割为若干个分片,由不同的实例并行执行,来提高任务调度的处理效率。
2、高可用
若某一个实例宕机,不影响其他实例来执行任务。
3、弹性扩容
当集群中增加实例就可以提高并执行任务的处理效率。
4、任务管理与监测
对系统中存在的所有定时任务进行统一的管理及监测。让开发人员及运维人员能够时刻了解任务执行情况,从而做出快速的应急处理响应。
分布式任务调度面临的问题:
当任务调度以集群方式部署,同一个任务调度可能会执行多次,例如:电商系统定期发放优惠券,就可能重复发放优惠券,对公司造成损失,信用卡还款提醒就会重复执行多次,给用户造成烦恼,所以我们需要控制相同的任务在多个运行实例上只执行一次。常见解决方案:
- 分布式锁,多个实例在任务执行前首先需要获取锁,如果获取失败那么就证明有其他服务已经在运行,如果获取成功那么证明没有服务在运行定时任务,那么就可以执行。
- ZooKeeper选举,利用ZooKeeper对Leader实例执行定时任务,执行定时任务的时候判断自己是否是Leader,如果不是则不执行,如果是则执行业务逻辑,这样也能达到目的。
2.XXL-Job
2.1.XXL-Job简介
针对分布式任务调度的需求,市场上出现了很多的产品:
1) TBSchedule:淘宝推出的一款非常优秀的高性能分布式调度框架,目前被应用于阿里、京东、支付宝、国美等很多互联网企业的流程调度系统中。但是已经多年未更新,文档缺失严重,缺少维护。
2) XXL-Job:大众点评的分布式任务调度平台,是一个轻量级分布式任务调度平台, 其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
3)Elastic-job:当当网借鉴TBSchedule并基于quartz 二次开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,具有任务高可用以及分片功能。
4)Saturn: 唯品会开源的一个分布式任务调度平台,基于Elastic-job,可以全域统一配置,统一监
控,具有任务高可用以及分片功能。
XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
源码地址:https://gitee.com/xuxueli0323/xxl-job
文档地址:https://www.xuxueli.com/xxl-job/
特性
-
简单灵活
提供Web页面对任务进行管理,管理系统支持用户管理、权限控制;
支持容器部署;
支持通过通用HTTP提供跨平台任务调度; -
丰富的任务管理功能
支持页面对任务CRUD操作;
支持在页面编写脚本任务、命令行任务、Java代码任务并执行;
支持任务级联编排,父任务执行结束后触发子任务执行;
支持设置指定任务执行节点路由策略,包括轮询、随机、广播、故障转移、忙碌转移等;
支持Cron方式、任务依赖、调度中心API接口方式触发任务执行 -
高性能
任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰; -
高可用
任务调度中心、任务执行节点均 集群部署,支持动态扩展、故障转移
支持任务配置路由故障转移策略,执行器节点不可用是自动转移到其他节点执行
支持任务超时控制、失败重试配置
支持任务处理阻塞策略:调度当任务执行节点忙碌时来不及执行任务的处理策略,包括:串行、抛弃、覆盖策略 -
易于监控运维
支持设置任务失败邮件告警,预留接口支持短信、钉钉告警;
支持实时查看任务执行运行数据统计图表、任务进度监控数据、任务完整执行日志;
2.2.XXL-Job快速入门-环境搭建
在分布式架构下,通过XXL-Job实现定时任务
调度中心:负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。
任务执行器:负责接收调度请求并执行任务逻辑。
任务:专注于任务的处理。
调度中心会发出调度请求,任务执行器接收到请求之后会去执行任务,任务则专注于任务业务的处理。
1.调度中心环境要求
- Maven3+
- Jdk1.8+
- Mysql5.7+
2.源码仓库地址
源码仓库地址 | Release Download |
---|---|
https://github.com/xuxueli/xxl-job | Download |
http://gitee.com/xuxueli0323/xxl-job | Download |
也可以使用资料文件夹中的源码。
3.初始化“调度数据库”
请下载项目源码并解压,获取 “调度数据库初始化SQL脚本” 并执行即可。源码结构如下:
使用tables_xxl_job.sql构建数据库表,共8张表
- xxl_job_lock:任务调度锁表;
- xxl_job_group:执行器信息表,维护任务执行器信息;
- xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等;
- xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;
- xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;
- xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;
- xxl_job_user:系统用户表;
调度中心支持集群部署,集群情况下各节点务必连接同一个mysql实例;
如果mysql做主从,调度中心集群节点务必强制走主库;
4.配置部署“调度中心”
调度中心项目:xxl-job-admin
作用:统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。
步骤一:调度中心配置
修改调度中心配置文件application.properties,数据库的连接信息修改为自己的数据库。
### xxl-job, datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?Unicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
步骤二:部署项目
如果已经正确进行上述配置,可将项目编译打包部署。
启动方式一:
这是一个springboot项目,可以在idea中直接启动引导类。
启动方式二:
-
执行maven打包命令:package
-
打完包以后,从项目的target目录中找到jar包拷贝到不带空格和中文的目录下
-
执行以下命令,启动项目
java -jar xxl-job-admin-2.2.0-SNAPSHOT.jar
调度中心访问地址:http://localhost:8888/xxl-job-admin (该地址执行器将会使用到,作为回调地址)
默认登录账号 “admin/123456”, 登录后运行界面如下图所示。
注意:使用老师资料中提供的jar运行的时候,访问路径为:http://localhost:8888/xxl-job-admin/
2.3.XXL-Job快速入门-创建执行器和任务
1.新建执行器,原则上一个项目一个执行器,方便管理
2.新建任务
2.4.XXL-Job快速入门-案例编写-与springboot集成
1.将示例代码中的xxl-job-core安装到Maven本地仓库
2.创建xxl-job-demo的Maven项目,pom.xml文件中引入依赖
<!-- 继承Spring boot工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- xxl-job -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.2.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.编写application.properties和logback.xml文件
application.properties
# web port
server.port=8082
# no web
#spring.main.web-environment=false
# log config
logging.config=classpath:logback.xml
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### xxl-job, access token
xxl.job.accessToken=
### xxl-job executor appname
xxl.job.executor.appname=xxl-job-executor-sample2
### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null
xxl.job.executor.address=
### xxl-job executor server-info
xxl.job.executor.ip=
xxl.job.executor.port=9999
### xxl-job executor log-path
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### xxl-job executor log-retention-days
xxl.job.executor.logretentiondays=30
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="1 seconds">
<contextName>logback</contextName>
<property name="log.path" value="/data/applogs/xxl-job/xxl-job-executor-sample-springboot.log"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}.%d{yyyy-MM-dd}.zip</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n
</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>
</configuration>
4.编写引导类
@SpringBootApplication
public class XxlJobDemoApplication {
public static void main(String[] args) {
SpringApplication.run(XxlJobDemoApplication.class,args);
}
}
5.编写配置类
/**
* xxl-job config
*
* @author xuxueli 2017-04-28
*/
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appName;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppName(appName);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
/**
* 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
*
* 1、引入依赖:
* <dependency>
* <groupId>org.springframework.cloud</groupId>
* <artifactId>spring-cloud-commons</artifactId>
* <version>${version}</version>
* </dependency>
*
* 2、配置文件,或者容器启动变量
* spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
*
* 3、获取IP
* String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
*/
}
6.创建定时任务
@Component
public class HelloJob {
/**
* 1、简单任务示例(Bean模式)
*/
@XxlJob("HelloJob2")
public ReturnT<String> demoJobHandler(String param) throws Exception {
System.out.println("当前任务执行了...."+new Date());
return ReturnT.SUCCESS;
}
}
7.启动测试
2.5.XXL-Job-快速入门-案例编写-与springboot集成测试
1.修改application.properties中的配置
# web port
server.port=${port:8082}
# no web
#spring.main.web-environment=false
# log config
logging.config=classpath:logback.xml
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### xxl-job, access token
xxl.job.accessToken=
### xxl-job executor appname
xxl.job.executor.appname=xxl-job-executor-sample2
### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null
xxl.job.executor.address=
### xxl-job executor server-info
xxl.job.executor.ip=
xxl.job.executor.port=${executor.port:9999}
### xxl-job executor log-path
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### xxl-job executor log-retention-days
xxl.job.executor.logretentiondays=30
2.修改定时执行任务的代码
@Component
public class HelloJob {
@Value("${server.port}")
private String port;
/**
* 1、简单任务示例(Bean模式)
*/
@XxlJob("HelloJob2") //参数:任务名称,需要与xxl-job-admin中配置的一致
public ReturnT<String> demoJobHandler(String param) throws Exception {
System.out.println("当前任务执行了...."+new Date()+"端口号为:"+port);
return ReturnT.SUCCESS;
}
}
3.启动测试
先启动项目,端口为8082
之后复制修改启动参数
之后启动,端口为8083,测试,会发现定时执行的任务是轮询的形式。
3.文章审核
3.1.文章审核-定时任务扫描待发布文章-数据准备
1.需求分析
- 前期回顾:在自媒体文章审核的时候,审核通过后,判断了文章的发布时间大于当前时间,这个时候并没有真正的发布文章,而是把文章的状态设置为了8(审核通过待发布)
- 定时任务的作用就是每分钟去扫描这些待发布的文章,如果当前文章的状态为8,并且发布时间小于当前时间的,立刻发布当前文章
2.数据准备
1.在heima-leadnews-apis中的WmNewsControllerApi
public interface WmNewsControllerApi {
........
/**
* 查询待发布的文章
* @return
*/
public List<Integer> findRelease();
}
2.在heima-leadnews-wemedia中的WmNewsService中新增方法
public interface WmNewsService extends IService<WmNews> {
......
/**
* 查询待发布的文章
* @return
*/
public List<Integer> findRelease();
}
3.在heima-leadnews-wemedia中的WmNewsServiceImpl中实现方法
/**
* 查询待发布的文章
* @return
*/
@Override
public List<Integer> findRelease() {
//文章状态为8 发布时间要小于等于当前时间
List<WmNews> list = list(Wrappers.<WmNews>lambdaQuery().eq(WmNews::getStatus, 8).le(WmNews::getPublishTime, new Date()));
List<Integer> idList = list.stream().map(WmNews::getId).collect(Collectors.toList());
return idList;
}
4.在heima-leadnews-wemedia中的WmNewsController中实现接口的方法
/**
* 查询待发布的文章
* @return
*/
@GetMapping("/findRelease")
@Override
public List<Integer> findRelease() {
return wmNewsService.findRelease();
}
5.启动项目测试
6.在heima-leadnews-admin的WemediaFeign中新增远程访问接口
@FeignClient("leadnews-wemedia")
public interface WemediaFeign {
......
/**
* 查询待发布的文章
* @return
*/
@GetMapping("/api/v1/news/findRelease")
public List<Integer> findRelease();
}
3.2.文章审核-定时任务扫描待发布文章-创建任务
1.新建执行器,原则上一个项目一个执行器,方便管理
2.创建任务
3.在heima-leadnews-common中pom.xml文件中引入依赖
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
</dependency>
4.在heima-leadnews-admin的application.yml文件中进行配置
xxljob:
admin:
addresses: http://localhost:8888/xxl-job-admin
executor:
appname: leadnews-admin-executor
port: 9999
5.在heima-leadnews-admin中创建配置类XxlJobConfig
@Log4j2
@Configuration
public class XxlJobConfig {
@Value("${xxljob.admin.addresses}")
private String adminAddresses;
@Value("${xxljob.executor.appname}")
private String appName;
@Value("${xxljob.executor.port}")
private int port;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppName(appName);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setLogRetentionDays(30);
return xxlJobSpringExecutor;
}
}
6.在heima-leadnews-admin中创建定时任务类
@Component
@Log4j2
public class WeMediaNewsAutoScanJob {
@Autowired
private WemediaFeign wemediaFeign;
@Autowired
private WemediaNewsAutoScanService wemediaNewsAutoScanService;
@XxlJob("wemediaAutoScanJob")
public ReturnT newsAutoScanJob(String param){
log.info("自媒体文章审核调度任何开始....");
//1.查询符合条件的文章信息
List<Integer> idList = wemediaFeign.findRelease();
if(idList != null && !idList.isEmpty()){
for (Integer id : idList) {
wemediaNewsAutoScanService.autoScanByMediaNewsId(id);
}
}
//2.调用自动审核
log.info("自媒体文章审核调度任何结束....");
return ReturnT.SUCCESS;
}
}
7.启动heima-leadnews-wemedia和heima-leadnews-admin进行测试
3.3.文章审核-定时任务扫描待发布文章-测试
修改XXL-Job的执行器的注册录入方式
重启启动项目,测试
4.人工审核文章
4.1.需求说明
自媒体文章如果没有自动审核成功,而是到了人工审核(自媒体文章状态为3),需要在admin端人工处理文章的审核。
平台管理员可以查看待人工审核的文章信息,可以通过(状态改为4)或驳回(状态改为2)
也可以通过点击查看按钮,查看文章详细信息,查看详情后可以根据内容判断是否需要通过审核
4.2.自媒体端
4.2.1.接口定义及mapper
- 需要分页查询自媒体文章信息,可以根据标题模糊查询
- 需要根据文章id查看文章的详情
- 修改文章的状态,已实现
1.在heima-leadnews-apis的WmNewsControllerApi中增加接口方法
public interface WmNewsControllerApi {
.....
/**
* 根据标题模糊分页查询文章信息
* @param dto
* @return
*/
public PageResponseResult findList(NewsAuthDto dto);
/**
* 查询文章详情
* @param id
* @return
*/
public WmNewsVo findWmNewsVo(Integer id);
}
在heima-leadnews-model中创建NewsAuthDto
@Data
public class NewsAuthDto extends PageRequestDto {
/**
* 标题
*/
private String title;
}
从原型图中可以看出,返回的文章信心中包含了作者信息,但是作者名称并不能在文章表中体现,只有一个用户id,目前需要通过用户id关联查询用户表或许完整数据
vo:value object 值对象 / view object 表现层对象,主要对应页面显示(web页面)的数据对象、
需要先封装一个vo类,来存储文章信息和作者名称
在heima-leadnews-model中创建WmNewsVo
@Data
public class WmNewsVo extends WmNews {
/**
* 文章作者名称
*/
private String authorName;
}
通过刚才分析,不管是查看文章列表或者是查询文章详情,都需要返回带作者的文章信息,需要关联查询获取数据,而mybatis-plus暂时不支持多表查询,需要手动定义mapper文件实现。
2.在heima-leadnews-wemedia的WmNewsMapper中创建方法,并构建对应的mapper映射文件。
@Mapper
public interface WmNewsMapper extends BaseMapper<WmNews> {
public List<WmNewsVo> findListAndPage(@Param("dto") NewsAuthDto dto);
public int findListCount(@Param("dto") NewsAuthDto dto);
}
<?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.WmNewsMapper">
<select id="findListAndPage" resultType="com.heima.model.wemedia.vo.WmNewsVo" parameterType="com.heima.model.admin.dtos.NewsAuthDto">
SELECT
wn.*, wu.`name` authorName
FROM
wm_news wn
LEFT JOIN wm_user wu ON wn.user_id = wu.id
<where>
<if test="dto.title != null and dto.title != ''">
and wn.title like #{dto.title}
</if>
</where>
LIMIT #{dto.page},#{dto.size}
</select>
<select id="findListCount" resultType="int" parameterType="com.heima.model.admin.dtos.NewsAuthDto">
SELECT
count(1)
FROM
wm_news wn
LEFT JOIN wm_user wu ON wn.user_id = wu.id
<where>
<if test="dto.title != null and dto.title != ''">
and wn.title like #{dto.title}
</if>
</where>
</select>
</mapper>
4.2.2.业务层实现及测试
3.在heima-leadnews-wemedia的WmNewsService中新增方法
public interface WmNewsService extends IService<WmNews> {
.....
/**
* 根据标题模糊分页查询文章信息
* @param dto
* @return
*/
public PageResponseResult findList(NewsAuthDto dto);
/**
* 查询文章详情
* @param id
* @return
*/
public WmNewsVo findWmNewsVo(Integer id);
}
4.在heima-leadnews-wemedia的WmNewsServiceImpl中实现方法
@Service
public class WmNewsServiceImpl extends ServiceImpl<WmNewsMapper, WmNews> implements WmNewsService {
@Autowired
private WmNewsMapper wmNewsMapper;
@Override
public PageResponseResult findList(NewsAuthDto dto) {
//1.检查分页参数
dto.checkParam();
//2.设置分页条件
dto.setPage((dto.getPage()-1)*dto.getSize());
if(StringUtils.isNotBlank(dto.getTitle())){
dto.setTitle("%"+dto.getTitle()+"%");
}
//3.分页查询
List<WmNewsVo> list = wmNewsMapper.findListAndPage(dto);
//统计数据
int count = wmNewsMapper.findListCount(dto);
//4.结果返回
PageResponseResult pageResponseResult = new PageResponseResult(dto.getPage(),dto.getSize(),count);
pageResponseResult.setData(list);
return pageResponseResult;
}
@Autowired
private WmUserMapper wmUserMapper;
@Override
public WmNewsVo findWmNewsVo(Integer id) {
//1.查询文章信息
WmNews wmNews = getById(id);
//2.查询作者
WmUser wmUser = null;
if(wmNews!=null && wmNews.getUserId() != null){
wmUser = wmUserMapper.selectById(wmNews.getUserId());
}
//3.封装vo信息返回
WmNewsVo wmNewsVo = new WmNewsVo();
BeanUtils.copyProperties(wmNews,wmNewsVo);
if(wmUser != null){
wmNewsVo.setAuthorName(wmUser.getName());
}
return wmNewsVo;
}
}
5.在heima-leadnews-wemedia的WmNewsController实现接口方法
@RestController
@RequestMapping("/api/v1/news")
public class WmNewsController implements WmNewsControllerApi {
@Autowired
private WmNewsService wmNewsService;
........
/**
* 根据标题模糊分页查询文章信息
* @param dto
* @return
*/
@PostMapping("/findList")
@Override
public PageResponseResult findList(@RequestBody NewsAuthDto dto) {
return wmNewsService.findList(dto);
}
/**
* 查询文章详情
* @param id
* @return
*/
@GetMapping("/find_news_vo/{id}")
@Override
public WmNewsVo findWmNewsVo(@PathVariable("id") Integer id) {
return wmNewsService.findWmNewsVo(id);
}
}
6.启动heima-leadnews-wemedia进行测试
根据标题模糊分页查询文章信息
查询文章详情
4.3.admin端
4.3.1.接口定义
- 需要分页查询自媒体文章信息,可以根据标题模糊查询
- 当审核通过后,修改文章状态为4
- 当审核驳回后,修改文章状态为3,并且需要说明原因
- 需要根据文章id查看文章的详情
1.在heima-leadnews-apis中创建NewsAuthControllerApi
public interface NewsAuthControllerApi {
/**
* 查询自媒体文章列表
* @param dto
* @return
*/
public ResponseResult findNews(NewsAuthDto dto);
/**
* 查询详情
* @param id
* @return
*/
public ResponseResult findOne(Integer id);
/**
* 文章审核成功
* @param dto
* @return
*/
public ResponseResult authPass(NewsAuthDto dto);
/**
* 文章审核失败
* @param dto
* @return
*/
public ResponseResult authFail(NewsAuthDto dto);
}
在heima-leadnews-model的NewsAuthDto中增加字段
@Data
public class NewsAuthDto extends PageRequestDto {
/**
* 标题
*/
private String title;
private Integer id;
/**
* 审核失败原因
*/
private String msg;
}
2.在heima-leadnews-admin中的WemediaFeign中新增方法
@FeignClient("leadnews-wemedia")
public interface WemediaFeign {
......
/**
* 根据标题模糊分页查询文章信息
* @param dto
* @return
*/
@PostMapping("/api/v1/news/findList")
public PageResponseResult findList(@RequestBody NewsAuthDto dto);
/**
* 查询文章详情
* @param id
* @return
*/
@GetMapping("/api/v1/news/find_news_vo/{id}")
public WmNewsVo findWmNewsVo(@PathVariable("id") Integer id);
}
4.3.2.业务层实现
1.在heima-leadnews-admin中的WemediaNewsAutoScanService新增方法
public interface WemediaNewsAutoScanService {
.........
/**
* 查询自媒体文章列表
* @param dto
* @return
*/
public PageResponseResult findNews(NewsAuthDto dto);
/**
* 查询详情
* @param id
* @return
*/
public ResponseResult findOne(Integer id);
/**
* 审核通过或驳回
* @param dto
* @param type 0 为驳回 1为通过
* @return
*/
public ResponseResult updateStatus(NewsAuthDto dto,Integer type);
}
2.在heima-leadnews-admin中的WemediaNewsAutoScanServiceImpl中实现方法
@Service
@Log4j2
public class WemediaNewsAutoScanServiceImpl implements WemediaNewsAutoScanService {
........
/**
* 查询自媒体文章列表
* @param dto
* @return
*/
@Override
public PageResponseResult findNews(NewsAuthDto dto) {
//分页查询
PageResponseResult responseResult = wemediaFeign.findList(dto);
//返回的数据中有图片需要显示,需要回显一个fasfdfs服务器的地址
responseResult.setHost(fileServerUrl);
return responseResult;
}
/**
* 查询详情
* @param id
* @return
*/
@Override
public ResponseResult findOne(Integer id) {
//1.参数检查
if(id == null){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
//2.查询数据
WmNewsVo wmNewsVo = wemediaFeign.findWmNewsVo(id);
//3.结果封装
ResponseResult responseResult = ResponseResult.okResult(wmNewsVo);
responseResult.setHost(fileServerUrl);
return responseResult;
}
/**
* 审核通过或驳回
* @param dto
* @param type 0 为驳回 1为通过
* @return
*/
@Override
public ResponseResult updateStatus(NewsAuthDto dto, Integer type) {
//1.参数检查
if(dto == null || dto.getId() == null){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
//2.查询文章信息
WmNews wmNews = wemediaFeign.findById(dto.getId());
if(wmNews == null){
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
}
//3.审核失败
if(type.equals(0)){
updateWmNews(wmNews,(short)2,dto.getMsg());
}else if (type.equals(1)){
//4.审核成功
updateWmNews(wmNews,(short)4,"人工审核通过");
}
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
}
3.在heima-leadnews-admin中创建NewsAuthController
@RestController
@RequestMapping("/api/v1/news_auth")
public class NewsAuthController implements NewsAuthControllerApi {
@Autowired
private WemediaNewsAutoScanService wemediaNewsAutoScanService;
/**
* 查询自媒体文章列表
* @param dto
* @return
*/
@PostMapping("/list")
@Override
public ResponseResult findNews(@RequestBody NewsAuthDto dto) {
return wemediaNewsAutoScanService.findNews(dto);
}
/**
* 查询详情
* @param id
* @return
*/
@GetMapping("/one/{id}")
@Override
public ResponseResult findOne(@PathVariable("id") Integer id) {
return wemediaNewsAutoScanService.findOne(id);
}
/**
* 文章审核成功
* @param dto
* @return
*/
@PostMapping("/auth_pass")
@Override
public ResponseResult authPass(@RequestBody NewsAuthDto dto) {
return wemediaNewsAutoScanService.updateStatus(dto,1);
}
/**
* 文章审核失败
* @param dto
* @return
*/
@PostMapping("/auth_fail")
@Override
public ResponseResult authFail(@RequestBody NewsAuthDto dto) {
return wemediaNewsAutoScanService.updateStatus(dto,0);
}
}
4.3.3.综合测试
需要启动的项目
heima-leadnews-article
heima-leadnews-user
heima-leadnews-wemedia
heima-leadnews-admin
heima-leadnews-admin-gateway
seata
kafka
nacos
XXL-Job
前端项目
5.自媒体端-文章上下架
5.1.思路分析
在自媒体文章管理中有文章上下架的操作,上下架是文章已经审核通过发布之后的文章,目前自动审核文章和人工审核文章都已完成,可以把之前代码补充,使用异步的方式,修改app端文章的配置信息即可。
5.2.功能实现
1.在heima-leadnews-wemedia中的WmNewsServiceImpl的downOrUp()方法
@Override
public ResponseResult downOrUp(WmNewsDto dto) {
.......
//4.修改文章状态,同步到app端(后期做)TODO
if(dto.getEnable() != null && dto.getEnable() > -1 && dto.getEnable() < 2){
if(wmNews.getArticleId() != null){
Map<String,Object> map= new HashMap<>();
map.put("enable",dto.getEnable());
map.put("articleId",wmNews.getArticleId());
kafkaTemplate.send(WmNewsMessageConstants.WM_NEWS_UP_OR_DOWN_TOPIC,JSON.toJSONString(map));
}
update(Wrappers.<WmNews>lambdaUpdate().eq(WmNews::getId,dto.getId()).set(WmNews::getEnable,dto.getEnable()));
}
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
public class WmNewsMessageConstants {
public static final String WM_NEWS_UP_OR_DOWN_TOPIC="wm.news.up.or.down.topic";
}
2.在heima-leadnews-article中application.yml文件中配置kafka消费者的配置
spring:
......
kafka:
bootstrap-servers: 192.168.200.130:9092
consumer:
group-id: ${spring.application.name}-kafka-group
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserialize
3.在heima-leadnews-article中创建ArticleIsDownListener
@Component
public class ArticleIsDownListener {
@Autowired
private ApArticleConfigService apArticleConfigService;
@KafkaListener(topics = WmNewsMessageConstants.WM_NEWS_UP_OR_DOWN_TOPIC)
public void receiveMessage(ConsumerRecord<?,?> record){
Optional<? extends ConsumerRecord<?, ?>> optional = Optional.ofNullable(record);
if(optional.isPresent()){
String value = (String) record.value();
Map map = JSON.parseObject(value, Map.class);
apArticleConfigService.update(Wrappers.<ApArticleConfig>lambdaUpdate()
.eq(ApArticleConfig::getArticleId,map.get("articleId"))
.set(ApArticleConfig::getIsDown,map.get("enable")));
}
}
}
4.启动测试
heima-leadnews-wemedia
heima-leadnews-wemedia-gateway
heima-leadnews-admin
heima-leadnews-article
nacos
kafka
seata