其他分享
首页 > 其他分享> > 第一章 项目介绍和工程搭建

第一章 项目介绍和工程搭建

作者:互联网

第一章 项目介绍和工程搭建

学习目标

1. 项目介绍

1.1项目背景

随着智能手机的普及,人们更加习惯于通过手机来看新闻。由于生活节奏的加快,很多人只能利用碎片时间来获取信息,因此,对于移动资讯客户端的需求也越来越高。黑马头条项目正是在这样背景下开发出来。黑马头条项目采用当下火热的微服务+大数据技术架构实现。本项目主要着手于获取最新最热新闻资讯,通过大数据分析用户喜好精确推送咨询新闻

1.2 项目概述

黑马头条项目是对在线教育平台业务进行大数据统计分析的系统。碎片化、切换频繁、社交化和个性化现如今成为人们阅读行为的标签。黑马头条对海量信息进行搜集,通过系统计算分类,分析用户的兴趣进行推送从而满足用户的需求。

1.3 项目术语定义

2. 项目需求

功能需求项目模块结构

2.1 APP主要功能大纲

2.2 APP用例图(主要功能)

2.3 WEMEDIA功能大纲

2.4 WEMEDIA用例图(主要功能)

2.5 ADMIN功能大纲

2.6 ADMIN用例图(主要功能)

2.7 其它需求

2.8 交互需求

3. 项目技术介绍

3.1 技术栈-基础六层技术

基础六层中包括前端(Weex、Vue、Echarts、WS)、网关(GateWay)、DevOps(单元测试、代码规范)等重难点技术

3.2 技术栈-服务四层技术

服务四层中包括中间件(Kafka、Mycat)、计算(Spark、Neo4j、Hive)、索引、微服务、大数据存储等重难点技术

3.1 技术栈-分布

4. 数据库设计

4.1 ER图设计

er图设计划分出了9个库,各个库主要解决的是某一个特定的业务。

数据库设计规范,详见资料文件夹下《黑马头条-数据库规范设计说明书.md》文件。

PowerDesinger工具使用,详见资料文件夹下'powerdesinger的基本使用'文件夹里的《powerdesinger的基本使用》文件。

4.2 分库设计

​ 黑马头条项目采用的分库分表设计,因为业务比较复杂,后期的访问量巨大,为了分摊数据库的压力,整个项目用的不只是一个数据库。其中核心库有5个,每一个数据库解决的是一个业务点,非常接近与实际项目设计。

4.3 核心数据流转图

黑马项目中的文章采用了多库设计的方式,以减少高并发情况下核心数据库表压力,共计设计为三个库表:

cl_news和wm_news中的数据审核通过之后发布到ap_article中。

4.4 冗余设计

​ 黑马头条项目全部采用逻辑关联,没有采用主外键约束。也是方便数据源冗余,尽可能少的使用多表关联查询。冗余是为了效率,减少join。单表查询比关联查询速度要快。某个访问频繁的字段可以冗余存放在两张表里,不用关联了。

​ 如查询一个订单表需要查询该条订单的用户名称,就必须join另外用户表,如果业务表很大,那么就会查询的很慢,这个时候我们就可以使用冗余来解决这个问题,在新建订单的同时不仅仅需要把用户ID存储,同时也需要存储用户的名称,这样我们在查询订单表的时候就不需要去join另外用户表,也能查询出该条订单的用户名称。这样的冗余可以直接的提高查询效率,单表更快。

4.5 导入数据库

当天资料文件夹下:数据库脚本

5. 后端工程结构

5.1 后端工程说明

后端工程基于Spring-boot 2.1.5.RELEASE 版本构建,工程父项目为heima-leadnews,并通过继承方式集成Spring-boot。

【父项目下分4个公共子项目】:

【多个微服务】:

5.2 后端通用工程搭建

5.2.1 开发环境说明

项目依赖环境(需提前安装好):

5.2.2 IDEA开发工具配置

5.2.3 后端初始项目导入

在当天资料中解压heima-leadnews.zip文件,拷贝到一个没有中文和空格的目录,使用idea打开即可

6 后端开发-通用说明及开发规范

6.1 后端接口开发规范

6.1.1 开发原则

6.1.2 开发步骤

6.1.2 接口版本规范说明

随着业务的复杂,同一个接口可能出现多个版本,为了方便后期切换和AB测试,需要定义接口的版本号

在某一个微服务下访问controller的时候在包名下加一个版本号,如下

com.heima.article.controller.v1

在访问具体的接口方法的url映射的时候也应该加上版本说明,如下:

@RequestMapping("/api/v1/article")

6.2.3 接口通用规范

ID混淆 请求和响应的连续增长的ID需要经过混淆加密
Date数化 请求和响应的的时间字段,统一转换成13位时间戳
字符编码 请求和响应的内容字符集为UTF-8
支持多格式 响应结果支持JSON和XML,可通过Header Accept设置
URL格式 Url为全小写字符,多个单词用下划线分隔
token 请求头中存放当前用户的请求token(JWT格式)
t 请求头中存放当前请求的时间,用于基本的请求时效判断
md 请求头中存放当前请求的参数验签字符串(查询串排序MD5加密)
响应格式 响应格式只接受ResponseResult,code码需定义在AppHttpCodeEnum

6.2 工具类说明

6.3 接口通用请求和响应

dto(Data Transfer Object):数据传输对象,用于展示层与服务层之间的数据传输对象

6.3.1 通用的响应对象:

不分页:com.heima.model.common.dtos.ResponseResult

/**
 * 通用的结果返回类
 * @param <T>
 */
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;
    }
}

分页通用返回:com.heima.model.common.dtos.PageResponseResult

public class PageResponseResult extends ResponseResult {
    private Integer currentPage;
    private Integer size;
    private Integer total;

    public PageResponseResult(Integer currentPage, Integer size, Integer total) {
        this.currentPage = currentPage;
        this.size = size;
        this.total = total;
    }

    public int getCurrentPage() {
        return currentPage;
    }

    public void setCurrentPage(int currentPage) {
        this.currentPage = currentPage;
    }

    public int getSize() {
        return size;
    }

    public void setSize(int size) {
        this.size = size;
    }

    public int getTotal() {
        return total;
    }

    public void setTotal(int total) {
        this.total = total;
    }
}

6.3.2 通用的请求dtos

com.heima.model.common.dtos.PageRequestDto

@Data
@Slf4j
public class PageRequestDto {

    protected Integer size;
    protected Integer page;

    public void checkParam() {
        if (this.page == null || this.page < 0) {
            setPage(1);
        }
        if (this.size == null || this.size < 0 || this.size > 100) {
            setSize(10);
        }
    }
}

6.3.3 通用的异常枚举

com.heima.model.common.enums.AppHttpCodeEnum

public enum AppHttpCodeEnum {

    // 成功段0
    SUCCESS(0,"操作成功"),
    // 登录段1~50
    NEED_LOGIN(1,"需要登录后操作"),
    LOGIN_PASSWORD_ERROR(2,"密码错误"),
    // TOKEN50~100
    TOKEN_INVALID(50,"无效的TOKEN"),
    TOKEN_EXPIRE(51,"TOKEN已过期"),
    TOKEN_REQUIRE(52,"TOKEN是必须的"),
    // SIGN验签 100~120
    SIGN_INVALID(100,"无效的SIGN"),
    SIG_TIMEOUT(101,"SIGN已过期"),
    // 参数错误 500~1000
    PARAM_REQUIRE(500,"缺少参数"),
    PARAM_INVALID(501,"无效参数"),
    PARAM_IMAGE_FORMAT_ERROR(502,"图片格式有误"),
    SERVER_ERROR(503,"服务器内部错误"),
    // 数据错误 1000~2000
    DATA_EXIST(1000,"数据已经存在"),
    AP_USER_DATA_NOT_EXIST(1001,"ApUser数据不存在"),
    DATA_NOT_EXIST(1002,"数据不存在"),
    // 数据错误 3000~3500
    NO_OPERATOR_AUTH(3000,"无权限操作");

    int code;
    String errorMessage;

    AppHttpCodeEnum(int code, String errorMessage){
        this.code = code;
        this.errorMessage = errorMessage;
    }

    public int getCode() {
        return code;
    }

    public String getErrorMessage() {
        return errorMessage;
    }
}

6.4 jackson封装解决字段序列化及反序列化

主要针对一些数据进行过滤设置,使用jackson来实现,比如一些自增的id值或者一些混淆的属性,在互联网传输的过程中最好不要轻易暴露在外面,这样安全性就比较低,应该对这些自增的值进行加密混淆。

日期处理:在网络上进行传输的时候不要直接传输日期的格式,传输的格式为13位时间戳,如果在每个日期字段都手动设置的话是比较费时费力的,所以也可以做成自动转换,用的也是jackson来完成

主要的类包括:

6.5 通用环境说明

6.5.1 多环境切换

在每一个微服务的工程中的根目录下创建三个文件,方便各个环境的切换

(1)maven_dev.properties

​ 定义开发环境的配置

(2)maven_prod.properties

​ 定义生产环境的配置

(3)maven_test.properties

​ 定义测试环境的配置,开发阶段使用这个测试环境

默认加载的环境为test,在打包的过程中也可以指定参数打包 package -P test/prod/dev

具体配置,请查看父工程下的maven插件的profiles配置

<profiles>
    <profile>
        <id>dev</id>
        <build>
            <filters>
                <filter>maven_dev.properties</filter>
            </filters>
        </build>
    </profile>
    <profile>
        <id>test</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <build>
            <filters>
                <filter>maven_test.properties</filter>
            </filters>
        </build>
    </profile>
    <profile>
        <id>prod</id>
        <build>
            <filters>
                <filter>maven_prod.properties</filter>
            </filters>
        </build>
    </profile>
</profiles>

6.5.2 mysql的环境配置

在heima-leadnews-common设置配置文件mysql-core-jdbc.properties

### ======================= 核心数据库连接配置 =========================
# 数据库连接字符串
mysql.core.jdbc-url=${mysql.core.jdbc.url}
# 数据库连接名称
mysql.core.jdbc-user-name=${mysql.core.jdbc.username}
# 数据库连接密码,密码需要反转
mysql.core.jdbc-password=${mysql.core.jdbc.password}
# 数据库连接驱动
mysql.core.jdbc-driver=${mysql.core.jdbc.driver}
# mybatis mapper.xml存放在classpath下的根文件夹名称
mysql.core.root-mapper=${mysql.core.root.mapper}
# mybatis pojo对象别名扫描包
mysql.core.aliases-package=${mysql.core.aliases.package}
# 事务扫描包自动代理扫描包
mysql.core.tx-scan-package=${mysql.core.tx.scan.package}

这里面的内容统统都是从maven_test.properties读取

mysql.core.jdbc.url=jdbc:mysql://localhost:3306/heima-leadnews?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
mysql.core.jdbc.username=root
mysql.core.jdbc.password=toor

mysql.core.jdbc.driver=com.mysql.jdbc.Driver
mysql.core.root.mapper=mappers
mysql.core.aliases.package=com.heima.model.**
mysql.core.tx.scan.package=execution(* com.heima..service.*.*(..))

自动化配置核心数据库的连接配置com.heima.common.mysql.core.MysqlCoreConfig

/**
 * 自动化配置核心数据库的连接配置
 */
@Setter
@Getter
@Configuration
@ConfigurationProperties(prefix = "mysql.core")
@PropertySource("classpath:mysql-core-jdbc.properties")
@MapperScan(basePackages = "com.heima.model.mappers", sqlSessionFactoryRef = "mysqlCoreSqlSessionFactory")
public class MysqlCoreConfig {
    String jdbcUrl;
    String jdbcUserName;
    String jdbcPassword;
    String jdbcDriver;
    String rootMapper;//mapper文件在classpath下存放的根路径
    String aliasesPackage;//别名包

    /**
     * 这是最快的数据库连接池
     *
     * @return
     */
    @Bean
    public DataSource mysqlCoreDataSource() {
        HikariDataSource hikariDataSource = new HikariDataSource();
        hikariDataSource.setUsername(this.getJdbcUserName());
        hikariDataSource.setPassword(this.getRealPassword());
        hikariDataSource.setJdbcUrl(this.getJdbcUrl());
        //最大连接数
        hikariDataSource.setMaximumPoolSize(50);
        //最小连接数
        hikariDataSource.setMinimumIdle(5);
        hikariDataSource.setDriverClassName(this.getJdbcDriver());
        return hikariDataSource;
    }

    /**
     * 这是Mybatis的Session
     *
     * @return
     * @throws IOException
     */
    @Bean
    public SqlSessionFactoryBean mysqlCoreSqlSessionFactory(@Qualifier("mysqlCoreDataSource") DataSource mysqlCoreDataSource) throws IOException {
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        //创建sqlSessionFactory工厂对象
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        //数据源
        sessionFactory.setDataSource(mysqlCoreDataSource);
        //mapper文件的路径
        sessionFactory.setMapperLocations(resolver.getResources(this.getMapperFilePath()));
        //别名
        sessionFactory.setTypeAliasesPackage(this.getAliasesPackage());
        //开启自动驼峰标识转换
        org.apache.ibatis.session.Configuration mybatisConf = new org.apache.ibatis.session.Configuration();
        mybatisConf.setMapUnderscoreToCamelCase(true);
        sessionFactory.setConfiguration(mybatisConf);
        return sessionFactory;
    }

    /**
     * 密码反转,简单示意密码在配置文件中的加密处理
     *
     * @return
     */
    public String getRealPassword() {
        return StringUtils.reverse(this.getJdbcPassword());
    }

    /**
     * 拼接Mapper.xml文件的存放路径
     *
     * @return
     */
    public String getMapperFilePath() {
        return new StringBuffer().append("classpath:").append(this.getRootMapper()).append("/**/*.xml").toString();
    }


}

通用事务管理配置类com.heima.common.mysql.core.TransactionConfig

@Setter
@Getter
@Aspect
@EnableAspectJAutoProxy
@EnableTransactionManagement
@Configuration
@ConfigurationProperties(prefix="mysql.core")
@PropertySource("classpath:mysql-core-jdbc.properties")
public class TransactionConfig {

    String txScanPackage;

    /**
     * 初始化事务管理器
     * @param dataSource
     * @return
     */
    @Bean
    public DataSourceTransactionManager mysqlCoreDataSourceTransactionManager(@Qualifier("mysqlCoreDataSource") DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }

    /**
     * 设置事务拦截器
     * @param dataSourceTransactionManager
     * @return
     */
    @Bean
    public TransactionInterceptor mysqlCoreDataSourceTxAdvice(@Qualifier("mysqlCoreDataSourceTransactionManager") DataSourceTransactionManager dataSourceTransactionManager) {
        // 默认事务
        DefaultTransactionAttribute defAttr = new DefaultTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED);
        // 查询只读事务
        DefaultTransactionAttribute queryAttr = new DefaultTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED);
        queryAttr.setReadOnly(true);
        // 设置拦截的方法
        NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
        source.addTransactionalMethod("save*", defAttr);
        source.addTransactionalMethod("insert*", defAttr);
        source.addTransactionalMethod("delete*", defAttr);
        source.addTransactionalMethod("update*", defAttr);
        source.addTransactionalMethod("exec*", defAttr);
        source.addTransactionalMethod("set*", defAttr);
        source.addTransactionalMethod("add*", defAttr);
        source.addTransactionalMethod("get*", queryAttr);
        source.addTransactionalMethod("query*", queryAttr);
        source.addTransactionalMethod("find*", queryAttr);
        source.addTransactionalMethod("list*", queryAttr);
        source.addTransactionalMethod("count*", queryAttr);
        source.addTransactionalMethod("is*", queryAttr);

        return new TransactionInterceptor(dataSourceTransactionManager, source);
    }

    @Bean
    public Advisor txAdviceAdvisor(@Qualifier("mysqlCoreDataSourceTxAdvice") TransactionInterceptor mysqlCoreDataSourceTxAdvice) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(txScanPackage);
        return new DefaultPointcutAdvisor(pointcut, mysqlCoreDataSourceTxAdvice);
    }

}

6.4.3 实体类及mapper

所有实体类都是按业务模板划分,mapper接口单独在一个包下,如下图

com.heima.model.mappers 定义mapper接口类

resources/mapper mapper映射文件的定义

7 app端-文章列表后端开发

7.1 文章列表需求分析

文章首页为用户进入应用之后的第一个页面,在这里我我们需要根据用户是否登录的状态加载不同的文章数据;用户登录则根据其选择的频道进行加载,否则根据系统默认频道推荐,流程图如下;在首页我们加载数据之后需要考虑用户在首页的其他动作的相应,如:用户上拉刷新、用户下拉刷新、用户进入文章详情等用户动作

相关表结构分析

ap_article文章信息表

文章信息表,存储已发布的文章

字段名称 类型 说明
id int(11) 主键
title varchar(50) 标题
author_id int(11) 文章作者的ID
author_name varchar(20) 作者昵称
channel_id int(10) 文章所属频道ID
channel_name varchar(10) 频道名称
layout tinyint(1) 文章布局 0 无图文章 1 单图文章 2 多图文章
flag tinyint(3) 文章标记 0 普通文章 1 热点文章 2 置顶文章 3 精品文章 4 大V 文章
images varchar(1000) 文章图片 多张逗号分隔
labels varchar(500) 文章标签最多3个 逗号分隔
likes int(5) 点赞数量
collection int(5) 收藏数量
comment int(5) 评论数量
views int(5) 阅读数量
province_id int(11) 省市
city_id int(11) 市区
county_id int(11) 区县
created_time datetime 创建时间
publish_time datetime 发布时间
sync_status tinyint(1) 同步状态
origin tinyint(1) 来源

ap_user_article_list APP用户文章列表

字段名称 类型 说明
id int(11) 主键
user_id int(11) 用户ID
channel_id int(11) 频道ID
article_id int(11) 文章ID
is_show tinyint(1) 是否展示
recommend_time datetime 推荐时间
is_read tinyint(1) 是否阅读
strategy_id int(5) 推荐算法

ap_show_behaviorAPP文章展现行为表

字段 类型 描述
id int(11) 主键
entry_id int(11) 实体ID
article_id int(11) 文章ID
is_click tinyint(1) 是否点击
show_time datetime 文章加载时间
created_time datetime 登录时间

ap_behavior_entry app行为实体表

APP行为实体表,一个行为实体可能是用户或者设备,或者其它

字段 类型 描述
id int(11) 主键
type tinyint(1) 实体类型 0终端设备 1用户
entry_id int(11) 实体ID
created_time datetime 创建时间
burst varchar(40) 分片

7.2 文章列表相关接口定义

7.2.1 接口定义分析

由需求分析可知用户在首页的是可能触发的行为有加载文章列表,刷新(上拉刷新、下拉刷新)等动作,这也就是我们后端需要对应的几个数据接口,其中还包含一个隐含的用户行为接口用户记录用户是否阅读某一篇文章的行为接口,则我们可以分析出后端需要的接口有:

(1)load接口

load接口,分两种情况,一个是登录,一个是未登录,加载多条数据(有条数的限制,size),用户可以选择频道进行数据的切换

(2)load_more接口 && load_new 接口

当用户进行刷新是,在我们的系统中定义了两种操作,第一种是上拉刷新也就是load_more,第二种是下拉刷新load_new接口,这两个接口的区别在于加载的内容的时间不同

用户进入系统的时间TimeA浏览了一会首次加载的数据之后到了TimeB时间,这时候如果用户继续上拉看后面的内容则我们调用load_more接口并把TimeB时间传递到后端,后端根据TimeB时间查找当前时间之前发布的内容;

用户下拉刷新则说明用户需要当前最新的内容则调用load_new接口,将TimeA时间传到后端后端查找TimeA时间之后发布的内容。其请求参数接口设计和load接口相同。

(4)behavior 行为接口

记录用户操作行为

7.3 项目开发

在原有的工程中创建一个普通的maven工程模块,选择其作为heima-leadnews作为父工程,并给当前模块命名为heima-leadnews-article

配置我们需要的jar的maven坐标,以及我们项目中模块的项目依赖 注意我们的数据库相关的实体以及Mapper接口和配置文件都是存放在Model 模块。

7.3.1 定义pom文件中的依赖信息

<?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>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>heima-leadnews-article</artifactId>

    <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-apis</artifactId>
        </dependency>
        <!-- Spring boot starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!-- 排除默认的logback日志,使用log4j-->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>ch.qos.logback</groupId>
                    <artifactId>logback-access</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-cbor</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
        </dependency>
    </dependencies>
</project>

7.3.2 Article模块工程结构

注意这里可以只创建基础的包结构即可,后续会讲解每个具体的类的作用以及实现;此处可能你又疑问为什么我们的控制器加了个v1,这里我们的做法是为了兼容不同的终端版本所设立的版本号

项目根路径添加文件:maven_dev.properties

# log4j
log.level=DEBUG
log.pattern=%d{DEFAULT}^|%sn^|%level^|%t^|%c^|%M^|%msg%n

项目根路径添加文件:maven_prod.properties

# log4j
log.level=DEBUG
log.pattern=%d{DEFAULT}^|%sn^|%level^|%t^|%c^|%M^|%msg%n

项目根路径添加文件:maven_test.properties

#log4j
log.level=DEBUG
log.pattern=%d{DEFAULT}^|%sn^|%level^|%t^|%c^|%M^|%msg%n

在resource目录下创建application.properties和log4j2.xml

application.properties

server.port=${port.article}
spring.application.name=${sn.article}

log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <properties>
        <property name="CONSOLE_PATTERN">${log.pattern}</property>
        <property name="FILE_NAME">${project.build.finalName}</property>
    </properties>
    <!--先定义所有的appender-->
    <appenders>
        <!--这个输出控制台的配置-->
        <console name="Console" target="SYSTEM_OUT">
            <!--输出日志的格式-->
            <PatternLayout pattern="${CONSOLE_PATTERN}"/>
        </console>
        <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileInfo" fileName="${sys:user.home}/logs/${FILE_NAME}.log"
                     filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/${FILE_NAME}-%d{yyyy-MM-dd}-%i.log">
            <PatternLayout pattern="${CONSOLE_PATTERN}"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
        </RollingFile>
    </appenders>
    <loggers>
        <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
        <logger name="org.springframework" level="INFO"></logger>
        <logger name="org.mybatis" level="INFO"></logger>
        <logger name="org.apache.http" level="INFO"></logger>
        <logger name="org.apache.kafka" level="INFO"></logger>
        <logger name="com.netflix.discovery" level="INFO"></logger>
        <logger name="org.hibernate" level="INFO"></logger>
        <root level="${log.level}">
            <appender-ref ref="Console"/>
            <appender-ref ref="RollingFileInfo"/>
        </root>
    </loggers>
</configuration>

mysql初始化扫描配置类com.heima.article.config.MysqlConfig

@Configuration
@ComponentScan("com.heima.common.mysql.core")
public class MysqlConfig {

}

7.3.3 article服务功能开发

(0)定义dto

@Data
public class ArticleHomeDto {

    // 省市
    Integer provinceId;
    // 市区
    Integer cityId;
    // 区县
    Integer countyId;
    // 最大时间
    Date maxBehotTime;
    // 最小时间
    Date minBehotTime;
    // 分页size
    Integer size;
    // 数据范围,比如频道ID
    String tag;

}

(1)接口定义,在apis模块中我们建立包com.heima.article.apis并定义接口ArticleHomeControllerApi

/**
 * 首頁文章
 */
public interface ArticleHomeControllerApi {
    /**
     * 加載首頁文章
     * @param dto 封装参数对象
     * @return 文章列表数据
     */
    ResponseResult load(ArticleHomeDto dto);

    /**
     * 加载更多
     * @param dto 封装参数对象
     * @return 文章列表数据
     */
    ResponseResult loadMore(ArticleHomeDto dto);

    /**
     * 加载最新的数据
     * @param dto 封装参数对象
     * @return 文章列表
     */
    ResponseResult loadNew(ArticleHomeDto dto);

}

(2)在定义完数据接口之后,我们需要做的就是去article模块定义我们的控制器, 如果你是拷贝的下面代码你可能发现你的代码中ArticleIndexService没有定义,报错了这里不用慌我们后面就是service层的编写,后面service到dao层也是同样的

@RestController
@RequestMapping("/api/v1/article")
public class ArticleHomeController implements ArticleHomeControllerApi {

    @Autowired
    private AppArticleService appArticleService;



    @Override
    @GetMapping("/load")
    public ResponseResult load(ArticleHomeDto dto) {
        return appArticleService.load( ArticleConstans.LOADTYPE_LOAD_MORE, dto);
    }

    @Override
    @GetMapping("/loadmore")
    public ResponseResult loadMore(ArticleHomeDto dto) {
        return appArticleService.load( ArticleConstans.LOADTYPE_LOAD_MORE, dto);
    }

    @Override
    @GetMapping("/loadnew")
    public ResponseResult loadNew(ArticleHomeDto dto) {
        return appArticleService.load( ArticleConstans.LOADTYPE_LOAD_NEW, dto);
    }

}

定义常量com.heima.common.article.constans.ArticleConstans

public class ArticleConstans{
    public static final Short LOADTYPE_LOAD_MORE = 1;
    public static final Short LOADTYPE_LOAD_NEW = 2;
    public static final String DEFAULT_TAG = "__all__"
    
}

(3)文章service接口定义

public interface AppArticleService {

    /**
     *
     * @param type 1 加载更多  2 加载更新
     * @param dto 封装数据
     * @return 数据列表
     */
    public ResponseResult load(Short type, ArticleHomeDto dto);
}

(4)AppArticleServiceImpl实现类

@Service
public class AppArticleServiceImpl implements AppArticleService {

    // 单页最大加载的数字
    private final  static short MAX_PAGE_SIZE = 50;


    @Autowired
    private ApArticleMapper apArticleMapper;
    @Autowired
    private ApUserArticleListMapper apUserArticleListMapper;


    /**
     *
     * @param time 时间节点
     * @param type 1 加载更多  2 加载更新
     * @param size 每次返回数据量
     * @return 数据列表
     */
    public ResponseResult load(Short type, ArticleHomeDto dto) {
        ApUser user = AppThreadLocalUtils.getUser();
        Integer size = dto.getSize();
        String tag = dto.getTag();
        // 分页参数校验
        if (size == null || size <= 0) {
            size = 20;
        }
        size = Math.min(size,MAX_PAGE_SIZE);
        dto.setSize(size);
        //  类型参数校验
        if (!type.equals(ArticleConstans.LOADTYPE_LOAD_MORE) && !type.equals(ArticleConstans.LOADTYPE_LOAD_NEW))
            type = ArticleConstans.LOADTYPE_LOAD_MORE;
        // 文章频道参数验证
        if (StringUtils.isEmpty(tag)) {
            dto.setTag(ArticleConstans.DEFAULT_TAG);
        }
        // 最大时间处理
        if(dto.getMaxBehotTime()==null){
            dto.setMaxBehotTime(new Date());
        }
        // 最小时间处理
        if(dto.getMinBehotTime()==null){
            dto.setMinBehotTime(new Date());
        }
        // 数据加载
        if(user!=null){
            return ResponseResult.okResult(getUserArticle(user,dto,type));
        }else{
            return ResponseResult.okResult(getDefaultArticle(dto,type));
        }
    }

    /**
     * 先从用户的推荐表中查找文章,如果没有再从大文章列表中获取
     * @param user
     * @param dto
     * @param type
     * @return
     */
    private List<ApArticle> getUserArticle(ApUser user,ArticleHomeDto dto,Short type){
        List<ApUserArticleList> list = apUserArticleListMapper.loadArticleIdListByUser(user,dto,type);
        if(!list.isEmpty()){
            List<ApArticle> temp = apArticleMapper.loadArticleListByIdList(list);
            return temp;
        }else{
            return getDefaultArticle(dto,type);
        }
    }

    /**
     * 从默认的大文章列表中获取文章
     * @param dto
     * @param type
     * @return
     */
    private List<ApArticle> getDefaultArticle(ArticleHomeDto dto,Short type){
        return apArticleMapper.loadArticleListByLocation(dto,type);
    }

}

(5)ApArticleMapper

public interface ApArticleMapper {


    /**
     * 照用户地理位置,加载文章
     * @param dto   参数封装对象
     * @param type  加载方向
     * @return
     */
    List<ApArticle> loadArticleListByLocation(@Param("dto") ArticleHomeDto dto, @Param("type") short type);

    /**
     * 依据文章IDS来获取文章详细内容
     * @param list   文章ID
     * @return
     */
    List<ApArticle> loadArticleListByIdList(@Param("list") List<ApUserArticleList> list);


}

(6)ApArticleMapper 对应xml

<?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.model.mappers.app.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"/>
    </resultMap>
    <sql id="Base_Column_List">
    id, title, author_id, author_name, channel_id, channel_name, layout, flag, images,
    labels, likes, collection, comment, views, province_id, city_id, county_id, created_time, 
    publish_time,sync_status
  </sql>

    <!-- 依据地理位置获取 -->
    <select id="loadArticleListByLocation" resultMap="resultMap">
        select * from ap_article a
        <where>
            <if test="dto.provinceId!=null">
                and a.province_id=#{dto.provinceId}
            </if>
            <if test="dto.cityId!=null">
                and a.city_id=#{dto.cityId}
            </if>
            <if test="dto.countyId!=null">
                and a.county_id=#{dto.countyId}
            </if>
            <!-- loadmore -->
            <if test="type != null and type == 1">
                and a.publish_time <![CDATA[<]]> #{dto.minBehotTime}
            </if>
            <if test="type != null and type == 2">
                and a.publish_time <![CDATA[>]]> #{dto.maxBehotTime}
            </if>
            <if test="dto.tag != '__all__'">
                and a.channel_id = #{dto.tag}
            </if>
        </where>
        limit #{dto.size}
    </select>

    <!-- 以及文章IDS列表获取文章数据 -->
    <select id="loadArticleListByIdList" resultMap="resultMap">
        select * from ap_article where id in(
        <trim prefix="" suffixOverrides=",">
            <foreach item="item" collection="list" separator=",">
                #{item.articleId},
            </foreach>
        </trim>
        )
    </select>
</mapper>

(7)ApUserArticleListMapper

package com.heima.article.mysql.core.model.mappers.app;

import com.heima.article.mysql.core.model.dtos.ArticleHomeDto;
import com.heima.article.mysql.core.model.pojos.app.ApUser;
import com.heima.article.mysql.core.model.pojos.app.ApUserArticleList;
import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface ApUserArticleListMapper {
    /**
     * 按照用户属性阅读习惯,加载文章id
     * @param user  当前登录的用户
     * @param dto   参数封装对象
     * @param type  加载方向
     * @return
     */
    List<ApUserArticleList> loadArticleIdListByUser(@Param("user") ApUser user, @Param("dto") ArticleHomeDto dto, @Param("type") short type);

}

(8)ApUserArticleListMapper 对应xml

<?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.mysql.core.model.mappers.app.ApUserArticleListMapper">
<resultMap id="BaseResultMap" type="com.heima.model.user.pojos.ApUserArticleList">
        <id column="id" property="id"/>
        <result column="user_id" property="userId"/>
        <result column="channel_id" property="channelId"/>
        <result column="article_id" property="articleId"/>
        <result column="is_show" property="isShow" javaType="java.lang.Boolean" jdbcType="BIT" />
        <result column="recommend_time" property="recommendTime" javaType="java.util.Date" jdbcType="TIMESTAMP" />
        <result column="is_read" property="isRead" javaType="java.lang.Boolean" jdbcType="BIT" />
        <result column="strategy_id" property="strategyId"/>
    </resultMap>
  <sql id="Base_Column_List">

    id, user_id, channel_id, article_id, is_show, recommend_time, is_read, strategy_id
  </sql>
  <select id="loadArticleIdListByUser" parameterType="map" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from ap_user_article_list
    <where>
      user_id=#{user.id} and is_show=0 and is_read=0
      <!-- loadmore -->
      <if test="type != null and type == 1">
          and recommend_time <![CDATA[<]]> #{dto.minBehotTime}
      </if>
      <if test="type != null and type == 2">
          and recommend_time <![CDATA[>]]> #{dto.maxBehotTime}
      </if>
      <if test="dto.tag != '__all__'">
          and channel_id = #{dto.tag}
      </if>
  </where>
    limit #{dto.size}
  </select>
</mapper>

(9)单元测试

/**
 * 测试文章列表相关接口
 */
@SpringBootTest(classes = ArticleJarApplication.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class ArticleTest {

    @Autowired
    private AppArticleService appArticleService;

    /**
     * 测试load
     */
    @Test
    public void testLoad() {
        ApUser apUser = new ApUser();
        apUser.setId(1l);
        AppThreadLocalUtils.setUser(apUser);
        ArticleHomeDto dto = new ArticleHomeDto();
        ResponseResult data = appArticleService.load( ArticleConstans.LOADTYPE_LOAD_MORE, dto);
        System.out.println(data.getData());
    }

}

7.3.4 行为相关功能开发

思路分析

导入heima-leadnews-behavior工程

dto的定义:

com.heima.model.behavior.dtos.ShowBehaviorDto

@Data
public class ShowBehaviorDto {
    // 设备ID
    @IdEncrypt
    Integer equipmentId;
    List<ApArticle> articleIds;
}

定义控制器以及控制器接口

(1)接口定义,com.heima.article.apis.BehaviorControllerApi

/**
 * 行为
 */
public interface BehaviorControllerApi {

    ResponseResult saveShowBehavior(ShowBehaviorDto dto);

}

(2)定义控制器:com.heima.behavior.controller.v1.BehaviorController

@RestController
@RequestMapping("/api/v1/behavior")
public class BehaviorController implements BehaviorControllerApi {
    @Autowired
    private AppShowBehaviorService appShowBehaviorService;

    @Override
    @PostMapping("/show_behavior")
    public ResponseResult saveShowBehavior(@RequestBody ShowBehaviorDto dto) {
        return appShowBehaviorService.saveShowBehavior(dto);
    }

}

(3)行为服务层接口:com.heima.behavior.service.AppShowBehaviorService

public interface AppShowBehaviorService {

    /**
     * 存储行为数据
     * @param dto
     * @return
     */
    public ResponseResult saveShowBehavior(ShowBehaviorDto dto);

}

(4)AppShowBehaviorService 实现

@Service
@SuppressWarnings("all")
public class AppShowBehaviorServiceImpl implements AppShowBehaviorService {

    @Autowired
    private ApShowBehaviorMapper apShowBehaviorMapper;
    @Autowired
    private ApBehaviorEntryMapper apBehaviorEntryMapper;

    @Override
    public ResponseResult saveShowBehavior(ShowBehaviorDto dto){
        ApUser user = AppThreadLocalUtils.getUser();
        // 用户和设备不能同时为空
        if(user==null&& (dto.getArticleIds()==null||dto.getArticleIds().isEmpty())){
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_REQUIRE);
        }
        Long userId = null;
        if(user!=null){
            userId = user.getId();
        }
        ApBehaviorEntry apBehaviorEntry = apBehaviorEntryMapper.selectByUserIdOrEquipment(userId, dto.getEquipmentId());
        // 行为实体找以及注册了,逻辑上这里是必定有值得,除非参数错误
        if(apBehaviorEntry==null){
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        // 过滤新数据
        Integer[] temp = new Integer[dto.getArticleIds().size()];
        for (int i = 0; i < temp.length; i++) {
            temp[i]=dto.getArticleIds().get(i).getId();
        }
        List<ApShowBehavior> list = apShowBehaviorMapper.selectListByEntryIdAndArticleIds(apBehaviorEntry.getId(), temp);
        List<Integer> stringList = new ArrayList(Arrays.asList(temp));
        if(!list.isEmpty()){
            list.forEach(item->{
                stringList.remove(item.getArticleId());
            });
        }
        // 插入新数据
        if(!stringList.isEmpty()) {
            temp = new Integer[stringList.size()];
            stringList.toArray(temp);
            apShowBehaviorMapper.saveBehaviors(apBehaviorEntry.getId(), temp);
        }
        return ResponseResult.okResult(0);
    }
}

(5)heima-leadnews-model中定义行为mapper接口:com.heima.article.mysql.core.model.mappers.app.ApShowBehaviorMapper

public interface ApShowBehaviorMapper {
    /**
     * 获取以及存在的用户数据
     * @param entryId
     * @param articleIds
     * @return
     */
    List<ApShowBehavior> selectListByEntryIdAndArticleIds(@Param("entryId") Integer entryId, @Param("articleIds") Integer[] articleIds);

    /**
     * 保存用户展现行为数据
     * @param articleIds  文章IDS
     * @param entryId 实体ID
     */
    void saveBehaviors(@Param("entryId") Integer entryId, @Param("articleIds") Integer[] articleIds);
}

(6)heima-leadnews-model中定义行为mapper文件:mappers/app/ApShowBehaviorMapper.xml

<?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.mysql.core.model.mappers.app.ApShowBehaviorMapper" >
  <resultMap id="BaseResultMap" type="com.heima.article.mysql.core.model.pojos.app.ApShowBehavior" >
      <id column="id" property="id" />
      <result column="entry_id" property="entryId" />
      <result column="article_id" property="articleId" />
      <result column="is_click" property="isClick"/>
      <result column="show_time" property="showTime" />
      <result column="created_time" property="createdTime" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, entry_id, article, is_click, show_time, created_time
  </sql>


  <!-- 选择用户的行为对象,优先按用户选择 -->
  <select id="selectListByEntryIdAndArticleIds" resultMap="BaseResultMap" >
    select * from ap_show_behavior a where a.entry_id=#{entryId} and article_id in(
    <foreach item="item" collection="articleIds" separator=",">
      #{item}
    </foreach>
    )
  </select>

  <insert id="saveBehaviors">
  /*!mycat:catlet=io.mycat.route.sequence.BatchInsertSequence */
  insert into ap_show_behavior ( entry_id, article_id,is_click, show_time, created_time) values
    <foreach item="item" collection="articleIds" separator=",">
      (#{entryId}, #{item},0, now(),now())
    </foreach>
  </insert>
</mapper>

(7)ApBehaviorEntryMapper

public interface ApBehaviorEntryMapper {

    ApBehaviorEntry selectByUserIdOrEquipment(@Param("userId") Integer userId, @Param("equipmentId") Integer equipmentId);

}

(8)ApBehaviorEntryMapper 对应映射配置

<?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.mysql.core.model.mappers.app.ApBehaviorEntryMapper" >
  <resultMap id="BaseResultMap" type="com.heima.article.mysql.core.model.pojos.app.ApBehaviorEntry" >
      <id column="id" property="id" />
      <result column="type" property="type"/>
      <result column="entry_id" property="entryId" />
      <result column="created_time" property="createdTime" />
      <result column="burst" property="burst"/>
  </resultMap>
  <sql id="Base_Column_List" >
    id, type, entry_id, created_time
  </sql>
  <!-- 选择用户的行为对象,优先按用户选择 -->
  <select id="selectByUserIdOrEquipment" resultMap="BaseResultMap" >
    select * from ap_behavior_entry a
    <where>
      <if test="userId!=null">
        and a.entry_id=#{userId} and type=1
      </if>
      <if test="userId==null and equipmentId!=null">
        and a.entry_id=#{equipmentId} and type=0
      </if>
    </where>
    limit 1
  </select>
</mapper>


行为接口测试

/**
 * 测试文章列表相关接口
 */
@SpringBootTest(classes = BehaviorJarApplication.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class BehaviorTest {


    @Autowired
    private AppShowBehaviorService showBehaviorService;


    @Test
    public void testSaveBehavior() {
        ApUser apUser = new ApUser();
        apUser.setId(1l);
        AppThreadLocalUtils.setUser(apUser);
        ShowBehaviorDto dto = new ShowBehaviorDto();
        List<ApArticle> articles = new ArrayList<>();
        ApArticle apArticle = new ApArticle();
        apArticle.setId(1);
        articles.add(apArticle);
        showBehaviorService.saveShowBehavior(dto);
        //articleIndexService.saveBehaviors(data);
    }
}

标签:heima,dto,工程,第一章,文章,id,return,public,搭建
来源: https://www.cnblogs.com/nayou/p/14223163.html