黑马头条(2)

表结构分析

ap_article 文章基本信息表

image-20210419151839634

ap_article_config 文章配置表

image-20210419151854868

ap_article_content 文章内容表

image-20210419151912063

三张表关系分析

image-20210419151938103

  • 从业务角度分析如何分表
  • 滚屏分页的逻辑

表的拆分-垂直分表

垂直分表:将一个表的字段分散到多个表中,每个表存储其中一部分字段。

优势

  1. 减少IO争抢,减少锁表的几率,查看文章概述与文章详情互不影响
  2. 充分发挥高频数据的操作效率,对文章概述数据的操作的高效率不会被操作文章详情数据的低效率所拖累

拆分规则

  1. 不常用的字段单独放在一张表
  2. 把text、blob等大字段拆分出来单独放在一张表
  3. 经常组合查询的字段单独放在一张表中

导入对应实体类

ApArticle

package com.heima.model.article.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * <p>
 * 文章信息表,存储已发布的文章
 * </p>
 *
 * @author itheima
 */

@Data
@TableName("ap_article")
public class ApArticle implements Serializable {

    @TableId(value = "id",type = IdType.ID_WORKER)
    private Long id;


    /**
     * 标题
     */
    private String title;

    /**
     * 作者id
     */
    @TableField("author_id")
    private Long authorId;

    /**
     * 作者名称
     */
    @TableField("author_name")
    private String authorName;

    /**
     * 频道id
     */
    @TableField("channel_id")
    private Integer channelId;

    /**
     * 频道名称
     */
    @TableField("channel_name")
    private String channelName;

    /**
     * 文章布局  0 无图文章   1 单图文章    2 多图文章
     */
    private Short layout;

    /**
     * 文章标记  0 普通文章   1 热点文章   2 置顶文章   3 精品文章   4 大V 文章
     */
    private Byte flag;

    /**
     * 文章封面图片 多张逗号分隔
     */
    private String images;

    /**
     * 标签
     */
    private String labels;

    /**
     * 点赞数量
     */
    private Integer likes;

    /**
     * 收藏数量
     */
    private Integer collection;

    /**
     * 评论数量
     */
    private Integer comment;

    /**
     * 阅读数量
     */
    private Integer views;

    /**
     * 省市
     */
    @TableField("province_id")
    private Integer provinceId;

    /**
     * 市区
     */
    @TableField("city_id")
    private Integer cityId;

    /**
     * 区县
     */
    @TableField("county_id")
    private Integer countyId;

    /**
     * 创建时间
     */
    @TableField("created_time")
    private Date createdTime;

    /**
     * 发布时间
     */
    @TableField("publish_time")
    private Date publishTime;

    /**
     * 同步状态
     */
    @TableField("sync_status")
    private Boolean syncStatus;

    /**
     * 来源
     */
    private Boolean origin;

    /**
     * 静态页面地址
     */
    @TableField("static_url")
    private String staticUrl;
}

ap_article_config文章配置对应实体类

package com.heima.model.article.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;

/**
 * <p>
 * APP已发布文章配置表
 * </p>
 *
 * @author itheima
 */

@Data
@TableName("ap_article_config")
public class ApArticleConfig implements Serializable {

    @TableId(value = "id",type = IdType.ID_WORKER)
    private Long id;

    /**
     * 文章id
     */
    @TableField("article_id")
    private Long articleId;

    /**
     * 是否可评论
     * true: 可以评论   1
     * false: 不可评论  0
     */
    @TableField("is_comment")
    private Boolean isComment;

    /**
     * 是否转发
     * true: 可以转发   1
     * false: 不可转发  0
     */
    @TableField("is_forward")
    private Boolean isForward;

    /**
     * 是否下架
     * true: 下架   1
     * false: 没有下架  0
     */
    @TableField("is_down")
    private Boolean isDown;

    /**
     * 是否已删除
     * true: 删除   1
     * false: 没有删除  0
     */
    @TableField("is_delete")
    private Boolean isDelete;
}

ap_article_content 文章内容对应的实体类

package com.heima.model.article.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;

@Data
@TableName("ap_article_content")
public class ApArticleContent implements Serializable {

    @TableId(value = "id",type = IdType.ID_WORKER)
    private Long id;

    /**
     * 文章id
     */
    @TableField("article_id")
    private Long articleId;

    /**
     * 文章内容
     */
    private String content;
}

接口定义

加载首页 加载更多 加载最新
接口路径 /api/v1/article/load /api/v1/article/loadmore /api/v1/article/loadnew
请求方式 POST POST POST
参数 ArticleHomeDto ArticleHomeDto ArticleHomeDto
响应结果 ResponseResult ResponseResult ResponseResult

ArticleHomeDto

package com.heima.model.article.dtos;

import lombok.Data;

import java.util.Date;

@Data
public class ArticleHomeDto {

    // 最大时间
    Date maxBehotTime;
    // 最小时间
    Date minBehotTime;
    // 分页size
    Integer size;
    // 频道ID
    String tag;
}

导入heima-leadnews-article微服务,资料在当天的文件夹中

文章详情

大文本静态化方案:

  • freemarker
  • minio

FreeMarker

image-20241129225816684

FreeMarker是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页,电子邮件,配置文件,原代码等)的通用工具。它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入所开发产品的组件。

依赖导入:

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- apache 对 java io 的封装工具库 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>

controller中指定对应的数据:

@Controller
public class HelloController {
    @GetMapping("/basic")
    public String hello(Model model){
        // name
        model.addAttribute("name", "freemarker");
        Student student = new Student();
        student.setName("Kennem");
        student.setAge(21);
        student.setBirthday(new Date(2003, 1, 15));

        model.addAttribute("stu", student);

        return "01-basic";
    }
}

template中指定对应的样式:01-basic.ftl

image-20241130155700342

最终样式:

image-20241130160048122
Freemarker指令语法

  1. 注释,即<#– –>,介于其之间的内容会被freemarker忽略
<#--这里是注释-->
  1. 插值(Interpolation):即${..}部分,freemarker会用真实的值代替${..}
Hello ${name}
  1. FTL指令:和HTML标记类似,名字前加#予以区分,Freemarker会解析标签中的表达式或逻辑。
<# >FTL指令</#>
  1. 文本,仅文本信息,这些不是freemarker的注释、插值、FTL指令的内容会被freemarker忽略
<#-- freemarker中的普通文本 -->
这是普通文本

集合指令-List

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Hello World!</title>
</head>
<body>

<#--List 数据展示-->
<b>List 数据展示:</b> <br><br>
<table>
    <tr>
        <td>序号</td>
        <td>姓名</td>
        <td>年龄</td>
        <td>钱包</td>
    </tr>
    <#list stus as stu>
        <tr>
            <td>${stu_index + 1}</td>
            <td>${stu.name}</td>
            <td>${stu.age}</td>
            <td>${stu.money}</td>
        </tr>
    </#list>
</table>
<hr>

<#--Map 数据展示-->
<b>Map 数据展示:</b><br><br>
<a href="###">方式一:通过map['keyname'].property</a><br/>
输出stu1的学生信息:<br/>
姓名:${stuMap['stu1'].name}<br/>
年龄:${stuMap['stu1'].age}<br/>
钱包:${stuMap['stu1'].money}<br/>
<hr>

<br>
<a href="###">方式二:通过map.keyname.property</a><br/>
输出stu2的学生信息:<br/>
姓名:${stuMap.stu2.name}<br/>
年龄:${stuMap.stu2.age}<br/>
钱包:${stuMap.stu2.money}<br/>
<hr>

<br/>
<a href="###">遍历map中两个学生信息:</a><br/>
<table>
    <tr>
        <td>序号</td>
        <td>姓名</td>
        <td>年龄</td>
        <td>钱包</td>
    </tr>
    <#list stuMap?keys as key>
        <tr>
            <td>${key_index + 1}</td>
            <td>${stuMap[key].name}</td>
            <td>${stuMap[key].age}</td>
            <td>${stuMap[key].money}</td>
        </tr>
    </#list>
</table>
<hr>

</body>
</html>

if指令

<table>
    <tr>
        <td>序号</td>
        <td>姓名</td>
        <td>年龄</td>
        <td>钱包</td>
    </tr>
    <#list stus as stu>
        <tr>
            <#if stu.name='小红'>
                <td>${stu_index + 1}</td>
                <td><font color="red">${stu.name}</font></td>
                <td>${stu.age}</td>
                <td>${stu.money}</td>
            <#else>
                <td>${stu_index + 1}</td>
                <td>${stu.name}</td>
                <td>${stu.age}</td>
                <td>${stu.money}</td>
            </#if>
        </tr>
    </#list>
</table>

运算符

算术运算符

FreeMarker表达式中完全支持算术运算,FreeMarker支持的算术运算符包括:

  • 加法:+
  • 减法:-
  • 乘法:*
  • 除法:/
  • 求模(求余):%

比较运算符

  • =或者==

  • !=

  • > 或者 gt

  • >= 或者 gte

  • <或者lt

  • <=或者lte

逻辑运算符

  1. 逻辑与:&&
  2. 逻辑或:||
  3. 逻辑非:

空值处理

  1. 判断某变量是否存在使用??

用法为:variable??,如果该变量存在,返回true,否则返回false

    <#if stus??>
        <#list stus as stu>
            <tr>
                <#if stu.name='小红'>
                    <td>${stu_index + 1}</td>
                    <td><font color="red">${stu.name}</font></td>
                    <td>${stu.age}</td>
                    <td>${stu.money}</td>
                <#else>
                    <td>${stu_index + 1}</td>
                    <td>${stu.name}</td>
                    <td>${stu.age}</td>
                    <td>${stu.money}</td>
                </#if>
            </tr>
        </#list>
    </#if>
  1. 缺失变量默认值使用!
    • 使用!要以指定一个默认值,当变量为空时显示默认值
      • 例如:${name!''}表示如果name为空显示空字符串
    • 如果是嵌套对象则建议使用()括起来
      • 例如:${(stu.name)''}表示,如果stuname为空默认显示空字符串

内建函数

内建函数语法格式:变量 + ?+ 函数名称

1.集合的大小

${集合名?size}

2.日期格式化

当前的日期:${today?date}<br/>
当前的日期:${today?time}<br/>
当前的日期:${today?datetime}<br/>
当前的日期:${today?string("yyyy-MM-mm")}<br/>

3.内建函数c

        //长数值类型
        model.addAttribute("point", 6549255147894651651L);

默认每三位使用逗号分隔

如果不想显示为每三位分隔的数字,可以使用c函数将数字型转成字符串输出

${point?c}

4.将json字符串转成对象

一个例子:

其中用到了assign标签,assign的作用是定义一个变量

输出静态化文件

代码补全时注意选择哪一个包

@SpringBootTest(classes = FreemarkerDemoApplication.class)
@RunWith(SpringRunner.class)
public class FreemarkerTest {
    @Autowired
    private Configuration configuration;

    @Test
    public void test() throws IOException, TemplateException {
        Template template = configuration.getTemplate("02-list.ftl");

        /**
         * 合成方法
         *
         * 参数1:数据模型
         * 参数2:输出流
         */
        Map map = getData();
        template.process(map, new FileWriter("d:/test/list.html"));
    }

    private Map getData() {
        Map<String, Object> map = new HashMap<>();
        //------------------------------------
        Student stu1 = new Student();
        stu1.setName("小强");
        stu1.setAge(18);
        stu1.setMoney(1000.86f);
        stu1.setBirthday(new Date());

        //小红对象模型数据
        Student stu2 = new Student();
        stu2.setName("小红");
        stu2.setMoney(200.1f);
        stu2.setAge(19);

        //将两个对象模型数据存放到List集合中
        List<Student> list = new ArrayList<>();
        list.add(stu1);
        list.add(stu2);

        //向model中存放List集合数据
        map.put("stus", list);

        //------------------------------------
        //创建Map数据
        Map<String, Student> stuMap = new HashMap<>();
        stuMap.put("stu1", stu1);
        stuMap.put("stu2", stu2);


        // 3.1 向model中存放Map数据
        map.put("stuMap", stuMap);
        map.put("today", new Date());
        //长数值类型
        map.put("point", 6549255147894651651L);

        return map;
    }
}

输出对应含有数据的html文件

image-20241130193923416

MinIO

对象存储的方式对比:

存储方式 优点 缺点
FastDFS 1. 主备服务,高可用
2. 支持主从文件,支持自定义扩展名
3. 支持动态扩容
1. 没有完善的官方文档,近几年没有更新
2. 环境搭建较为麻烦
MinIO 1. 性能高,准硬件条件下它能达到55GB/s的读、35GB/s的写速率
2. 部署自带管理界面
3. MinIO Inc运营的开源项目,社区活跃度高
4. 提供了所有主流开发语言的SDK
1. 不支持动态增加节点

docker部署

docker search minio
docker run -p 9000:9000 --name minio -d --restart=always -e "MINIO_ACCESS_KEY=minio" -e "MINIO_SECRET_KEY=minio123" -v /home/data:/data -v /home/config:/root/.minio minio/minio server /data

基本概念

  • bucket-类比于文件系统的目录
  • Object-类比文件系统的文件
  • Keys-类比文件名

上传文件进行静态访问

public class MinIOTest {
    public static void main(String[] args){
        try {
            FileInputStream fileInputStream = new FileInputStream("D:\\test\\list.html");
            // 获取minio的链接信息, 创建一个minio的客户端
            MinioClient minioClient = MinioClient.builder().credentials("minio", "minio123").endpoint("http://192.168.200.130:9000/").build();

            // 上传
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .object("list.html")
                    .contentType("text/html")
                    .bucket("leadnews")
                    .stream(fileInputStream, fileInputStream.available(), -1).build();

            minioClient.putObject(putObjectArgs);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

封装MinIO为starter

MinIO为starter

image-20241201211856551

错误:

@Autowire不成功

image-20241201220045180

解决方法:

检查启动类是否有@SpringBootApplication

image-20241201220122711

模块内导入依赖:

image-20241201230942863