黑马头条项目[修改版]-第八天

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

上一篇:分布式任务调度-xxl-job


下一篇:xxl-job-core源码分析