SpringCloud微服务项目实战 - 3.App端文章

经历了新冠疫情始末,之后,多出门走走,看看山,看看海,吃好吃的

SpringCloud微服务项目实战 - 3.App端文章_第1张图片



系列文章目录

  1. 项目搭建
  2. App登录及网关
  3. App文章
  4. 自媒体平台(博主后台)
  5. 自媒体文章审核
  6. 延迟任务
  7. kafka及文章上下架
  8. App端文章搜索
  9. 后台系统管理
  10. Long类型精度丢失问题
  11. 定时计算热点文章(xxl-Job)
  12. 热点文章-实时计算(kafkaStream)
  13. 项目部署_持续集成(Jenkins)

文章目录

  • 系列文章目录
  • 一、文章列表
    • 1. 数据库
      • ⑴. 导入文章数据库
      • ⑵. 表结构分析
        • ①. ap_article 文章基本信息表
        • ②. ap_article_config 文章配置表
        • ③. ap_article_content 文章内容表
      • ⑶. 拆表(垂直分表规则)
      • ⑷. 实体类
        • ①. 文章基本信息
        • ②. 文章配置
        • ③. 文章内容
    • 2. 接口实现
      • ⑴. 需求分析
      • ⑵. 接口定义
      • ⑶. 功能实现
        • ①. 文章微服务
          • Ⅰ. 引入项目包
          • Ⅱ. 配置
          • Ⅲ. Maven
          • Ⅳ. Nacos配置
        • ②. 接口定义
          • Ⅰ. DTO
          • Ⅱ. Controller
        • ③. Mapper
          • Ⅰ. mapper接口
          • Ⅱ. xml配置(mapper映射文件)
        • ④. Service
          • Ⅰ. 公共常量
          • Ⅱ. service接口
          • Ⅲ. service实现类
        • ⑤. 控制器
        • ⑥. 测试
          • Ⅰ. nacos配置
          • Ⅱ. 页面测试
  • 二、Freemarker
    • 1. 介绍
    • 2. 创建测试工程
      • ⑴. 新增模块
      • ⑵. POM依赖
      • ⑶. 配置
    • 3. 入门案例
      • ⑴. 创建模板
      • ⑵. 启动类
      • ⑶. 模板类
      • ⑷. Controller
      • ⑸. 测试
    • 4. 指令语法
      • ⑴. 基础语法种类
      • ⑵. 集合指令(List和Map)
        • ①. 测试模板
          • Ⅰ. 新增模板
          • Ⅱ. Controller
          • Ⅲ. 测试
        • ②. 遍历List
          • Ⅰ. Controller
          • Ⅱ. 模板
          • Ⅲ. 测试
        • ③. 遍历Map
          • Ⅰ. Controller
          • Ⅱ. 模板
          • Ⅲ. 测试
      • ⑶. if指令
        • ①. 模板
        • ②. 测试
      • ⑷. 运算符
        • ①. 算数运算符
        • ②. 比较运算符
        • ③. 逻辑运算符
      • ⑸. 空值处理
        • ①. 数据为空
          • Ⅰ. Controller
          • Ⅱ. 测试
        • ②. 变量为空
          • Ⅰ. 模板
          • Ⅱ. 测试
        • ③. 变量缺失
          • Ⅰ. Controller
          • Ⅱ. 模板
          • Ⅲ. 测试
          • Ⅳ. 模板
          • Ⅴ. 测试
        • ④. 嵌套对象
      • ⑹. 内建函数
        • ①. 集合的大小
        • ②. 日期格式化
          • Ⅰ. Controller
          • Ⅱ. 模板
          • Ⅲ. 测试
        • ③. 内建函数c
          • Ⅰ. Controller
          • Ⅱ. 模板
          • Ⅲ. 测试
        • ④. assign标签
    • 5. 输出静态化文件
  • 三、MinIO
    • 1. 介绍
    • 2. 环境搭建
      • ⑴. FinalShell
      • ⑵. Minio管理系统
      • ⑶. 基本概念
    • 3. 入门案例
      • ⑴. 新建模块
      • ⑵. 配置
      • ⑶. 启动类
      • ⑷. 测试类
      • ⑸. 测试
    • 4. 封装MinIO为starter
      • ⑴. 引入模块
      • ⑵. 管理依赖
      • ⑶. 配置文件
      • ⑷. 测试类
      • ⑸. 测试
    • 5. 静态页面上传至minIO
      • ⑴. 管理依赖
      • ⑵. Nacos配置
      • ⑶. 模板文件
      • ⑷. 数据JSON文件
      • ⑸. css 和 js 上传MinIO
        • ①. 本地文件
        • ②. 上传至minio
      • ⑹. mapper
      • ⑺. 测试类
      • ⑻. 测试
    • 6. App端测试


一、文章列表

SpringCloud微服务项目实战 - 3.App端文章_第2张图片

1. 数据库

⑴. 导入文章数据库

sql链接: https://pan.baidu.com/s/17LSC_ldZ3C75-bZ_Uezs-A?pwd=abcd
在这里插入图片描述
SpringCloud微服务项目实战 - 3.App端文章_第3张图片


⑵. 表结构分析

ap_article 文章基本信息表 和 ap_article_config 文章配置表 是一对一的关系
ap_article 文章基本信息表 和 ap_article_content 文章内容表 是一对一的关系

表名称 说明
ap_article 文章信息表,存储已发布的文章
ap_article_config APP已发布文章配置表
ap_article_content APP已发布文章内容表
ap_author APP文章作者信息表
ap_collection APP收藏信息表

①. ap_article 文章基本信息表

CREATE TABLE `ap_article` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '标题',
  `author_id` int(11) unsigned DEFAULT NULL COMMENT '文章作者的ID',
  `author_name` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '作者昵称',
  `channel_id` int(10) unsigned DEFAULT NULL COMMENT '文章所属频道ID',
  `channel_name` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '频道名称',
  `layout` tinyint(1) unsigned DEFAULT NULL COMMENT '文章布局\r\n            0 无图文章\r\n            1 单图文章\r\n            2 多图文章',
  `flag` tinyint(3) unsigned DEFAULT NULL COMMENT '文章标记\r\n            0 普通文章\r\n            1 热点文章\r\n            2 置顶文章\r\n            3 精品文章\r\n            4 大V 文章',
  `images` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文章图片\r\n            多张逗号分隔',
  `labels` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文章标签最多3个 逗号分隔',
  `likes` int(5) unsigned DEFAULT NULL COMMENT '点赞数量',
  `collection` int(5) unsigned DEFAULT NULL COMMENT '收藏数量',
  `comment` int(5) unsigned DEFAULT NULL COMMENT '评论数量',
  `views` int(5) unsigned DEFAULT NULL COMMENT '阅读数量',
  `province_id` int(11) unsigned DEFAULT NULL COMMENT '省市',
  `city_id` int(11) unsigned DEFAULT NULL COMMENT '市区',
  `county_id` int(11) unsigned DEFAULT NULL COMMENT '区县',
  `created_time` datetime DEFAULT NULL COMMENT '创建时间',
  `publish_time` datetime DEFAULT NULL COMMENT '发布时间',
  `sync_status` tinyint(1) DEFAULT '0' COMMENT '同步状态',
  `origin` tinyint(1) unsigned DEFAULT '0' COMMENT '来源',
  `static_url` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1383828014629179394 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='文章信息表,存储已发布的文章';

②. ap_article_config 文章配置表

CREATE TABLE `ap_article_config` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `article_id` bigint(20) unsigned DEFAULT NULL COMMENT '文章ID',
  `is_comment` tinyint(1) unsigned DEFAULT NULL COMMENT '是否可评论',
  `is_forward` tinyint(1) unsigned DEFAULT NULL COMMENT '是否转发',
  `is_down` tinyint(1) unsigned DEFAULT NULL COMMENT '是否下架',
  `is_delete` tinyint(1) unsigned DEFAULT NULL COMMENT '是否已删除',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_article_id` (`article_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1383828014645956610 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP已发布文章配置表';

③. ap_article_content 文章内容表

CREATE TABLE `ap_article_content` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `article_id` bigint(20) unsigned DEFAULT NULL COMMENT '文章ID',
  `content` longtext COLLATE utf8mb4_unicode_ci COMMENT '文章内容',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_article_id` (`article_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1383828014650150915 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP已发布文章内容表';

⑶. 拆表(垂直分表规则)

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

优势:

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

拆分规则:

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

⑷. 实体类

①. 文章基本信息

新建 heima-leadnews-model/src/main/java/com/heima/model/article/pojos/ApArticle.java 实体类:

/**
 * 

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

* * @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; }

②. 文章配置

新建 heima-leadnews-model/src/main/java/com/heima/model/article/pojos/ApArticleConfig.java 实体类:

/**
 * 

* APP已发布文章配置表 *

* * @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; }

③. 文章内容

新建 heima-leadnews-model/src/main/java/com/heima/model/article/pojos/ApArticleContent.java 实体类:

@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;
}


2. 接口实现

⑴. 需求分析

  1. 默认频道展示10条文章信息
  2. 可以切换频道查看不同种类文章
  3. 当用户下拉可以加载最新的文章(分页)本页文章列表中发布时间为最大的时间为依据
  4. 当用户上拉可以加载更多的文章信息(按照发布时间)本页文章列表中发布时间最小的时间为依据
  5. 如果是当前频道的首页,前端传递默认参数
    • maxBehotTime:0(毫秒)
    • minBehotTime:20000000000000(毫秒)—>2063年

⑵. 接口定义

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

ArticleHomeDto:

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

⑶. 功能实现

①. 文章微服务

Ⅰ. 引入项目包

资料链接: https://pan.baidu.com/s/1S3T-ssECQILH0IsCHk-HgQ?pwd=abcd
在这里插入图片描述
解压至 heima-leadnews-service 目录下

Ⅱ. 配置

编辑 heima-leadnews-service/pom.xml 文件:

    <modules>
        <module>heima-leadnews-usermodule>
        
        <module>heima-leadnews-articlemodule>
    modules>
Ⅲ. Maven

刷新 Maven,再在maven中 + 添加 heima-leadnews-article 的pom文件
SpringCloud微服务项目实战 - 3.App端文章_第4张图片

Ⅳ. Nacos配置

SpringCloud微服务项目实战 - 3.App端文章_第5张图片

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/leadnews_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
    username: root
    password: 123456
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  # 设置别名包扫描路径,通过该属性可以给包中的类注册别名
  type-aliases-package: com.heima.model.article.pojos

②. 接口定义

Ⅰ. DTO

新建 heima-leadnews-model/src/main/java/com/heima/model/article/dtos/ArticleHomeDto.java 文件:

@Data
public class ArticleHomeDto {

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

新建 heima-leadnews-service/heima-leadnews-article/src/main/java/com/heima/article/controller/v1/articleHomeController.java 文件:

@RestController
@RequestMapping("/api/v1/article")
public class articleHomeController {

    /**
     * 加载首页
     * @param dto
     * @return
     */
    @PostMapping("/load")
    public ResponseResult load(@RequestBody ArticleHomeDto dto) {
        return null;
    }

    /**
     * 加载更多
     * @param dto
     * @return
     */
    @PostMapping("/loadmore")
    public ResponseResult loadmore(@RequestBody ArticleHomeDto dto) {
        return null;
    }

    /**
     * 加载最新
     * @param dto
     * @return
     */
    @PostMapping("/loadnew")
    public ResponseResult loadnew(@RequestBody ArticleHomeDto dto) {
        return null;
    }
}

③. Mapper

Ⅰ. mapper接口

新建 heima-leadnews-service/heima-leadnews-article/src/main/java/com/heima/article/mapper/ApArticleMapper.java 文件:

public interface ApArticleMapper extends BaseMapper<ApArticle> {

    /**
     * 加载文章列表
     * @param dto
     * @param type 1 加载更多 2 加载最新
     * @return
     */
    public List<ApArticle> loadArticleList(ArticleHomeDto dto, short type);
}
Ⅱ. xml配置(mapper映射文件)

新建 heima-leadnews-service/heima-leadnews-article/src/main/resources/mapper/ApArticleMapper.xml 文件:


DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heima.article.mapper.ApArticleMapper">

    <resultMap id="resultMap" type="com.heima.model.article.pojos.ApArticle">
        <id column="id" property="id"/>
        <result column="title" property="title"/>
        <result column="author_id" property="authorId"/>
        <result column="author_name" property="authorName"/>
        <result column="channel_id" property="channelId"/>
        <result column="channel_name" property="channelName"/>
        <result column="layout" property="layout"/>
        <result column="flag" property="flag"/>
        <result column="images" property="images"/>
        <result column="labels" property="labels"/>
        <result column="likes" property="likes"/>
        <result column="collection" property="collection"/>
        <result column="comment" property="comment"/>
        <result column="views" property="views"/>
        <result column="province_id" property="provinceId"/>
        <result column="city_id" property="cityId"/>
        <result column="county_id" property="countyId"/>
        <result column="created_time" property="createdTime"/>
        <result column="publish_time" property="publishTime"/>
        <result column="sync_status" property="syncStatus"/>
        <result column="static_url" property="staticUrl"/>
    resultMap>
    <select id="loadArticleList" resultMap="resultMap">
        SELECT
        aa.*
        FROM
        `ap_article` aa
        LEFT JOIN ap_article_config aac ON aa.id = aac.article_id
        <where>
            and aac.is_delete != 1 -- 不能删除
            and aac.is_down != 1 -- 不能下载
            
            <if test="type != null and type == 1"> -- 加载更多
                and aa.publish_time  #{dto.minBehotTime}
            if>
            <if test="type != null and type == 2"> -- 加载最新
                and aa.publish_time ]]> #{dto.maxBehotTime}
            if>
            <if test="dto.tag != '__all__'"> -- 推荐栏(无频道)
                and aa.channel_id = #{dto.tag}
            if>
        where>
        order by aa.publish_time desc -- 发布时间倒序
        limit #{dto.size} -- 限制条数
    select>

mapper>

④. Service

Ⅰ. 公共常量

新建 heima-leadnews-common/src/main/java/com/heima/common/constants/ArticleConstants.java 文件:

public class ArticleConstants {
    public static final Short LOADTYPE_LOAD_MORE = 1;
    public static final Short LOADTYPE_LOAD_NEW = 2;
    public static final String DEFAULT_TAG = "__all__";
}
Ⅱ. service接口

新建 heima-leadnews-service/heima-leadnews-article/src/main/java/com/heima/article/service/ApArticleService.java 文件:

public interface ApArticleService extends IService<ApArticle> {

    /**
     * 加载文章列表
     * @param dto
     * @param type 1 加载更多 2 加载最新
     * @return
     */
    public ResponseResult load(ArticleHomeDto dto, Short type);
}
Ⅲ. service实现类

新建 heima-leadnews-service/heima-leadnews-article/src/main/java/com/heima/article/service/impl/ApArticleServiceImpl.java 文件:

@Service
@Transactional
@Slf4j
public class ApArticleServiceImpl extends ServiceImpl<ApArticleMapper, ApArticle> implements ApArticleService {

    @Autowired
    private ApArticleMapper apArticleMapper;

    private final static short Max_PAGE_SIZE = 50;

    /**
     * 加载文章列表
     * @param dto
     * @param type 1 加载更多 2 加载最新
     * @return
     */
    @Override
    public ResponseResult load(ArticleHomeDto dto, Short type) {

        // 1.参数校验
        // 1.1 分页参数校验
        Integer size = dto.getSize();
        if( size == null || size == 0) {
            size = 10;
        }
        // 分页的值不超过50
        size = Math.min(size, Max_PAGE_SIZE);

        // 1.2 类型参数校验(更多/最新)
        if(!type.equals(ArticleConstants.LOADTYPE_LOAD_MORE) && !type.equals(ArticleConstants.LOADTYPE_LOAD_NEW)) {
            type = ArticleConstants.LOADTYPE_LOAD_MORE;
        }

        // 1.3 频道参数校验
        if(StringUtils.isBlank(dto.getTag())) {
            dto.setTag(ArticleConstants.DEFAULT_TAG);
        }

        // 1.4 时间参数校验
        if(dto.getMaxBehotTime() == null) dto.setMaxBehotTime(new Date());
        if(dto.getMinBehotTime() == null) dto.setMinBehotTime(new Date());

        // 2. 查询
        List<ApArticle> articleList = apArticleMapper.loadArticleList(dto, type);

        // 3. 结果返回
        return ResponseResult.okResult(articleList);
    }
}

⑤. 控制器

编辑 heima-leadnews-service/heima-leadnews-article/src/main/java/com/heima/article/controller/v1/articleHomeController.java 文件:

@RestController
@RequestMapping("/api/v1/article")
public class articleHomeController {

    @Autowired
    private ApArticleService apArticleService;

    /**
     * 加载首页
     * @param dto
     * @return
     */
    @PostMapping("/load")
    public ResponseResult load(@RequestBody ArticleHomeDto dto) {
        return apArticleService.load(dto, ArticleConstants.LOADTYPE_LOAD_MORE);
    }

    /**
     * 加载更多
     * @param dto
     * @return
     */
    @PostMapping("/loadmore")
    public ResponseResult loadmore(@RequestBody ArticleHomeDto dto) {
        return apArticleService.load(dto, ArticleConstants.LOADTYPE_LOAD_MORE);
    }

    /**
     * 加载最新
     * @param dto
     * @return
     */
    @PostMapping("/loadnew")
    public ResponseResult loadnew(@RequestBody ArticleHomeDto dto) {
        return apArticleService.load(dto, ArticleConstants.LOADTYPE_LOAD_NEW);
    }
}

⑥. 测试

Ⅰ. nacos配置

在app网关的微服务的nacos的配置中心添加文章微服务的路由
SpringCloud微服务项目实战 - 3.App端文章_第6张图片

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
      routes:
        # 用户微服务
        - id: user
          uri: lb://leadnews-user
          predicates:
            - Path=/user/**
          filters:
            - StripPrefix= 1
        # 文章微服务
        - id: article
          uri: lb://leadnews-article
          predicates:
            - Path=/article/**
          filters:
            - StripPrefix= 1
Ⅱ. 页面测试

页面地址: http://localhost:8801/#/login
SpringCloud微服务项目实战 - 3.App端文章_第7张图片
SpringCloud微服务项目实战 - 3.App端文章_第8张图片





二、Freemarker

SpringCloud微服务项目实战 - 3.App端文章_第9张图片

1. 介绍

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

模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。

SpringCloud微服务项目实战 - 3.App端文章_第10张图片

2. 创建测试工程

freemarker 作为springmvc一种视图格式,默认情况下SpringMVC支持freemarker视图格式。

⑴. 新增模块

SpringCloud微服务项目实战 - 3.App端文章_第11张图片

⑵. POM依赖

编辑 heima-leadnews-test/freemarker-demo/pom.xml 文件:

...

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-freemarkerartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
        dependency>

        
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-ioartifactId>
            <version>1.3.2version>
        dependency>
    dependencies>

⑶. 配置

新建 heima-leadnews-test/freemarker-demo/src/main/resources/application.yml 文件:

server:
  port: 8881 #服务端口
spring:
  application:
    name: freemarker-demo #指定服务名
  freemarker:
    cache: false  #关闭模板缓存,方便测试
    settings:
      template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试
    suffix: .ftl               #指定Freemarker模板文件的后缀名


3. 入门案例

⑴. 创建模板

新建 heima-leadnews-test/freemarker-demo/src/main/resources/templates/01-basic.ftl 文件:

DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Hello World!title>
head>
<body>
<b>普通文本 String 展示:b><br><br>
Hello ${name} <br>
<hr>
<b>对象Student中的数据展示:b><br/>
姓名:${stu.name}<br/>
年龄:${stu.age}
<hr>
body>
html>

⑵. 启动类

新建 heima-leadnews-test/freemarker-demo/src/main/java/com/heima/freemarker/FreemarkerDemoApplication.java 文件:

@SpringBootApplication
public class FreemarkerDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(FreemarkerDemoApplication.class, args);
    }
}

⑶. 模板类

新建 heima-leadnews-test/freemarker-demo/src/main/java/com/heima/freemarker/entity/Student.java 文件:

@Data
public class Student {
    private String name;//姓名
    private int age;//年龄
    private Date birthday;//生日
    private Float money;//钱包
}

⑷. Controller

新建 heima-leadnews-test/freemarker-demo/src/main/java/com/heima/freemarker/controller/HelloController.java 文件:

@Controller
public class HelloController {

    @GetMapping("/basic")
    public String hello(Model model) {

        // name
        model.addAttribute("name", "freemarker");

        // stu
        Student student = new Student();
        student.setName("lily");
        student.setAge(18);
        model.addAttribute("stu", student);

        return "01-basic";
    }
}

⑸. 测试

模板地址: http://localhost:8881/basic
SpringCloud微服务项目实战 - 3.App端文章_第12张图片



4. 指令语法

⑴. 基础语法种类

1、注释,即<#-- -->,介于其之间的内容会被freemarker忽略

<#--我是一个freemarker注释-->

2、插值(Interpolation):${..} 部分,freemarker会用真实的值代替**${..}**

Hello ${name}

3、FTL指令: 和HTML标记类似,名字前加#予以区分,Freemarker会解析标签中的表达式或逻辑。

<# >FTL指令 

4、文本,仅文本信息,这些不是freemarker的注释、插值、FTL指令的内容会被freemarker忽略解析,直接输出内容。

<#--freemarker中的普通文本-->
我是一个普通的文本

⑵. 集合指令(List和Map)

①. 测试模板

Ⅰ. 新增模板

新建 heima-leadnews-test/freemarker-demo/src/main/resources/templates/02-list.ftl 文件:

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

<#-- list 数据的展示 -->
<b>展示list中的stu数据:b>
<br>
<br>
<table>
    <tr>
        <td>序号td>
        <td>姓名td>
        <td>年龄td>
        <td>钱包td>
    tr>
table>
<hr>

<#-- Map 数据的展示 -->
<b>map数据的展示:b>
<br/><br/>
<a href="###">方式一:通过map['keyname'].propertya><br/>
输出stu1的学生信息:<br/>
姓名:<br/>
年龄:<br/>
<br/>
<a href="###">方式二:通过map.keyname.propertya><br/>
输出stu2的学生信息:<br/>
姓名:<br/>
年龄:<br/>

<br/>
<a href="###">遍历map中两个学生信息:a><br/>
<table>
    <tr>
        <td>序号td>
        <td>姓名td>
        <td>年龄td>
        <td>钱包td>
    tr>
table>
<hr>

body>
html>

Ⅱ. Controller

编辑 heima-leadnews-test/freemarker-demo/src/main/java/com/heima/freemarker/controller/HelloController.java 文件:

    @GetMapping("/list")
    public String list(Model model) {
        
        return "02-list";
    }

Ⅲ. 测试

地址: http://localhost:8881/list
SpringCloud微服务项目实战 - 3.App端文章_第13张图片

②. 遍历List

Ⅰ. Controller

编辑 heima-leadnews-test/freemarker-demo/src/main/java/com/heima/freemarker/controller/HelloController.java 文件:

    @GetMapping("/list")
    public String list(Model model) {

        //------------------------------------
        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> stus = new ArrayList<>();
        stus.add(stu1);
        stus.add(stu2);

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

        return "02-list";
    }
Ⅱ. 模板

编辑 heima-leadnews-test/freemarker-demo/src/main/resources/templates/02-list.ftl 文件:

<#-- list 数据的展示 -->
<b>展示list中的stu数据: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>
Ⅲ. 测试

地址: http://localhost:8881/list
SpringCloud微服务项目实战 - 3.App端文章_第14张图片

③. 遍历Map

# 获取map中的值
map['keyname'].property
http://map.keyname.property/

# 遍历map
<#list userMap?keys as key>
	key:${key}--value:${userMap["${key}"]}
</#list>
Ⅰ. Controller

编辑 heima-leadnews-test/freemarker-demo/src/main/java/com/heima/freemarker/controller/HelloController.java 文件:

        //------------------------------------

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

        model.addAttribute("stuMap", stuMap);

Ⅱ. 模板

编辑 heima-leadnews-test/freemarker-demo/src/main/resources/templates/02-list.ftl 文件:

<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>

Ⅲ. 测试

地址: http://localhost:8881/list
SpringCloud微服务项目实战 - 3.App端文章_第15张图片

⑶. if指令

# 语法
<#if expression>
	<#else>
</#if>

①. 模板

编辑 heima-leadnews-test/freemarker-demo/src/main/resources/templates/02-list.ftl 文件:

<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>-->

        <#--if指令-->
        <#--判断名称为小红的数据字体显示为红色-->
        <#if stu.name = "小红">
            <tr style="color:red">
                <td>${stu_index + 1}td>
                <td>${stu.name}td>
                <td>${stu.age}td>
                <td>${stu.money}td>
            tr>
            <#else >
            <tr>
                <td>${stu_index + 1}td>
                <td>${stu.name}td>
                <td>${stu.age}td>
                <td>${stu.money}td>
            tr>
        #if>
    #list>
table>

②. 测试

地址: http://localhost:8881/list
SpringCloud微服务项目实战 - 3.App端文章_第16张图片


⑷. 运算符

①. 算数运算符

  • 加法: +
  • 减法: -
  • 乘法: *
  • 除法: /
  • 求模 (求余): %
100+5 运算:  ${100 + 5 }<br/>
100 - 5 * 5运算:${100 - 5 * 5}<br/>
5 / 2运算:${5 / 2}<br/>
12 % 10运算:${12 % 10}<br/>

②. 比较运算符

Column 1 Column 2
=或者== 判断两个值是否相等
!= 判断两个值是否不等
>或者gt 判断左边值是否大于右边值
>=或者gte 判断左边值是否大于等于右边值
<或者lt 判断左边值是否小于右边值
<=或者lte 判断左边值是否小于等于右边值
  • =和!=可以用于字符串、数值和日期来比较是否相等
  • =和!=两边必须是相同类型的值,否则会产生错误
  • 字符串 “x” 、"x " 、"X"比较是不等的.因为FreeMarker是精确比较
  • gt代替>, FreeMarker会把>解释成FTL标签的结束字符,可使用括号避免这种情况,如:<#if (x>y)>

③. 逻辑运算符

  • 逻辑与:&&
  • 逻辑或:||
  • 逻辑非:!
<#if (10 lt 12 )&&( 10  gt  5 )  >
	(10 lt 12 )&&( 10  gt  5 )  显示为 true
#if>
<br/>
<br/>
<#if !false>
	false 取反为true
#if>

⑸. 空值处理

①. 数据为空

Ⅰ. Controller

编辑 heima-leadnews-test/freemarker-demo/src/main/java/com/heima/freemarker/controller/HelloController.java 文件:

// model.addAttribute("stus",stus);  // 测试数据为空时报错

Ⅱ. 测试

地址: http://localhost:8881/list
SpringCloud微服务项目实战 - 3.App端文章_第17张图片


②. 变量为空

Ⅰ. 模板

编辑 heima-leadnews-test/freemarker-demo/src/main/resources/templates/02-list.ftl 文件:

    <#--空值处理-->
    <#if stus??>
        <#list stus as stu>
            <#--<tr>
                <td>${stu_index + 1}td>
                <td>${stu.name}td>
                <td>${stu.age}td>
                <td>${stu.money}td>
            tr>-->

            <#--if指令-->
            <#--判断名称为小红的数据字体显示为红色-->
            <#if stu.name = "小红">
                <#--= 和 == 是一样的-->
                <tr style="color:red">
                    <td>${stu_index + 1}td>
                    <td>${stu.name}td>
                    <td>${stu.age}td>
                    <td>${stu.money}td>
                tr>
                <#else >
                <tr>
                    <td>${stu_index + 1}td>
                    <td>${stu.name}td>
                    <td>${stu.age}td>
                    <td>${stu.money}td>
                tr>
            #if>
        #list>
    #if>

Ⅱ. 测试

地址: http://localhost:8881/list
SpringCloud微服务项目实战 - 3.App端文章_第18张图片

③. 变量缺失

Ⅰ. Controller

编辑 heima-leadnews-test/freemarker-demo/src/main/java/com/heima/freemarker/controller/HelloController.java 文件:

// model.addAttribute("stus",stus);  // 测试数据为空时报错

Ⅱ. 模板

编辑 heima-leadnews-test/freemarker-demo/src/main/resources/templates/02-list.ftl 文件:

                    <#--测试变量缺失-->
                    <#--<td>${stu.name}td>-->
                    <td>${stu.name2}td>

Ⅲ. 测试

地址: http://localhost:8881/list
SpringCloud微服务项目实战 - 3.App端文章_第19张图片

Ⅳ. 模板

编辑 heima-leadnews-test/freemarker-demo/src/main/resources/templates/02-list.ftl 文件:

                    <#--测试变量缺失-->
                    <#--<td>${stu.name}td>-->
                    <#--<td>${stu.name2}td>-->
                    <td>${stu.name2!'-'}td>

Ⅴ. 测试

地址: http://localhost:8881/list
SpringCloud微服务项目实战 - 3.App端文章_第20张图片


④. 嵌套对象

如果是嵌套对象则建议使用()括起来
例:${(stu.name)!‘’}表示,如果stu或name为空默认显示空字符串。

⑹. 内建函数

# 语法: 变量 + ? + 函数名称
${stus?size}

①. 集合的大小

# 语法:
${集合名?size}

编辑 heima-leadnews-test/freemarker-demo/src/main/resources/templates/02-list.ftl 文件:

...

<#-- 内建函数 -->
<b>内建函数:b>
<br/><br/>

<#--集合大小-->
stus集合大小: ${stus?size}
<br/><br/>

②. 日期格式化

  • 显示年月日: ${today?date}
  • 显示时分秒:${today?time}
  • 显示日期+时间:${today?datetime}
  • 自定义格式化:${today?string(“yyyy年MM月”)}
Ⅰ. Controller

编辑 heima-leadnews-test/freemarker-demo/src/main/java/com/heima/freemarker/controller/HelloController.java 文件:

        model.addAttribute("stuMap", stuMap);
        // 内建函数 - 日期
        model.addAttribute("today", new Date());

Ⅱ. 模板

编辑 heima-leadnews-test/freemarker-demo/src/main/resources/templates/02-list.ftl 文件:

<#--日期格式化-->
显示年月日: ${today?date}
<br>
显示时分秒:${today?time}
<br>
显示日期+时间:${today?datetime}
<br>
自定义格式化:${today?string("yyyy年MM月")}

Ⅲ. 测试

地址: http://localhost:8881/list
SpringCloud微服务项目实战 - 3.App端文章_第21张图片

③. 内建函数c

Ⅰ. Controller

编辑 heima-leadnews-test/freemarker-demo/src/main/java/com/heima/freemarker/controller/HelloController.java 文件:

        // 内建函数 - c
        model.addAttribute("point", 234523452345L);

Ⅱ. 模板

编辑 heima-leadnews-test/freemarker-demo/src/main/resources/templates/02-list.ftl 文件:

<#--日期格式化-->
初始数值: ${point}
<br>
初始数值: ${point?c}

Ⅲ. 测试

地址: http://localhost:8881/list
SpringCloud微服务项目实战 - 3.App端文章_第22张图片

④. assign标签

将json字符串转成对象

<#assign text="{'bank':'工商银行','account':'10101920201920212'}" />
<#assign data=text?eval />
开户行:${data.bank}  账号:${data.account}


5. 输出静态化文件

新建 heima-leadnews-test/freemarker-demo/src/test/java/com/heima/freemarker/test/FreemarkerTest.java 测试类:

@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");

        /**
         * 合成方法
         * 两个数据: 模型数据 / 输出流
         */
        template.process(getData(), new FileWriter("d:/list.html"));
    }

    public 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> stus = new ArrayList<>();
        stus.add(stu1);
        stus.add(stu2);

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

        //------------------------------------

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

        map.put("stuMap", stuMap);
        map.put("today", new Date());
        map.put("point", 234523452345L);

        return map;
    }
}

执行后, 打开D盘下 list.html 文件
SpringCloud微服务项目实战 - 3.App端文章_第23张图片




三、MinIO

1. 介绍

MinIO基于Apache License v2.0开源协议的对象存储服务,可以做为云存储的解决方案用来保存海量的图片,视频,文档。由于采用Golang实现,服务端可以工作在Windows,Linux, OS X和FreeBSD上。配置简单,基本是复制可执行程序,单行命令可以运行起来。

MinIO兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。

MinIO官网文档:http://docs.minio.org.cn/docs/

MinIO特点:

  • 数据保护: Minio使用Minio Erasure Code(纠删码)来防止硬件故障。即便损坏一半以上的driver,但是仍然可以从中恢复。
  • 高性能: 作为高性能对象存储,在标准硬件条件下它能达到55GB/s的读、35GB/s的写速率
  • 可扩容: 不同MinIO集群可以组成联邦,并形成一个全局的命名空间,并跨越多个数据中心
  • SDK支持: 基于Minio轻量的特点,它得到类似Java、Python或Go等语言的sdk支持
  • 有操作页面: 面向用户友好的简单操作界面,非常方便的管理Bucket及里面的文件资源
  • 功能简单: 这一设计原则让MinIO不容易出错、更快启动
  • 丰富的API: 支持文件资源的分享连接及分享链接的过期策略、存储桶操作、文件列表访问及文件上传下载的基本功能等。
  • 文件变化主动通知: 存储桶(Bucket)如果发生改变,比如上传对象和删除对象,可以使用存储桶事件通知机制进行监控,并通过以下方式发布出去:AMQP、MQTT、Elasticsearch、Redis、NATS、MySQL、Kafka、Webhooks等。

2. 环境搭建

⑴. FinalShell

# 搜索minio(第一项)
docker search minio

# 列出本地镜像(本地已下载)
docker images

# 安装 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

# 列出容器
docker ps

# 查看日志(启动)
docker logs -f 容器ID

⑵. Minio管理系统

访问minio系统: http://192.168.200.130:9000/

账号: minio密码: minio123
SpringCloud微服务项目实战 - 3.App端文章_第24张图片

⑶. 基本概念

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

SpringCloud微服务项目实战 - 3.App端文章_第25张图片

3. 入门案例

⑴. 新建模块

SpringCloud微服务项目实战 - 3.App端文章_第26张图片

⑵. 配置

编辑 heima-leadnews-test/minio-demo/pom.xml 文件:

    <dependencies>
        <dependency>
            <groupId>io.miniogroupId>
            <artifactId>minioartifactId>
            <version>7.1.0version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
        dependency>
    dependencies>

⑶. 启动类

新建 heima-leadnews-test/minio-demo/src/main/java/com/heima/minio/MinIOApplication.java 文件:

@SpringBootApplication
public class MinIOApplication {

    public static void main(String[] args) {
        SpringApplication.run(MinIOApplication.class,args);
    }
}

⑷. 测试类

新建 heima-leadnews-test/minio-demo/src/test/java/com/heima/minio/test/MinIOTest.java 文件:

public class MinIOTest {

    /**
     * 把 list.html 文件上传到minio中, 并且可以在浏览器中访问
     * @param args
     */
    public static void main(String[] args) {

        try {
            FileInputStream fileInputStream = new FileInputStream("d:/list.html");

            // 1. 获取minio的链接信息 创建一个minio客户端
            MinioClient minioClient = MinioClient.builder().credentials("minio", "minio123").endpoint("http://192.168.200.130:9000").build();

            // 2. 上传
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .object("list.html") // 文件名
                    .contentType("text/html") // 文件类型
                    .bucket("leadnews") // 桶名称, 与minio管理界面创建的桶一致
                    .stream(fileInputStream, fileInputStream.available(), -1).build();
            minioClient.putObject(putObjectArgs);

            // 3. 访问路径
            System.out.println("http://192.168.200.130:9000/leadnews/list.html");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

⑸. 测试

执行测试类,点击测试链接: http://192.168.200.130:9000/leadnews/list.html
SpringCloud微服务项目实战 - 3.App端文章_第27张图片

minio地址: http://192.168.200.130:9000/minio/leadnews/

minio管理界面会新增 list.html 文件
SpringCloud微服务项目实战 - 3.App端文章_第28张图片


4. 封装MinIO为starter

⑴. 引入模块

模块资源链接: https://pan.baidu.com/s/1wpx-XxADF7qJIymKhWzD4Q?pwd=abcd
在这里插入图片描述
解压至 heima-leadnews 目录下,编辑 pom.xml 配置文件:

    <modules>
        <module>heima-leadnews-commonmodule>
        <module>heima-leadnews-utilsmodule>
        <module>heima-leadnews-modelmodule>
        <module>heima-leadnews-feign-apimodule>
        <module>heima-leadnews-servicemodule>
        <module>heima-leadnews-gatewaymodule>
        <module>heima-leadnews-testmodule>
        <module>heima-leadnews-basicmodule>
    modules>

重新加载Maven,查看到 heima-leadnews-basic 图标改变即可

⑵. 管理依赖

编辑 heima-leadnews-test/minio-demo/pom.xml 文件:

    <dependencies>
        <dependency>
            <groupId>io.miniogroupId>
            <artifactId>minioartifactId>
            <version>7.1.0version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
        dependency>

        <dependency>
            <groupId>com.heimagroupId>
            <artifactId>heima-file-starterartifactId>
            <version>1.0-SNAPSHOTversion>
        dependency>
    dependencies>

⑶. 配置文件

新建 heima-leadnews-test/minio-demo/src/main/resources/application.yml 文件:

minio:
  accessKey: minio
  secretKey: minio123
  bucket: leadnews
  endpoint: http://192.168.200.130:9000
  readPath: http://192.168.200.130:9000

⑷. 测试类

编辑 heima-leadnews-test/minio-demo/src/test/java/com/heima/minio/test/MinIOTest.java 文件:

@SpringBootTest(classes = MinIOApplication.class)
@RunWith(SpringRunner.class)
public class MinIOTest {

/*    *//**
     * 把 list.html 文件上传到minio中, 并且可以在浏览器中访问
     * @param args
     *//*
    public static void main(String[] args) {

        try {
            FileInputStream fileInputStream = new FileInputStream("d:/list.html");

            // 1. 获取minio的链接信息 创建一个minio客户端
            MinioClient minioClient = MinioClient.builder().credentials("minio", "minio123").endpoint("http://192.168.200.130:9000").build();

            // 2. 上传
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .object("list.html") // 文件名
                    .contentType("text/html") // 文件类型
                    .bucket("leadnews") // 桶名称, 与minio管理界面创建的桶一致
                    .stream(fileInputStream, fileInputStream.available(), -1).build();
            minioClient.putObject(putObjectArgs);

            // 3. 访问路径
            System.out.println("http://192.168.200.130:9000/leadnews/list.html");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }*/


    @Autowired
    private FileStorageService fileStorageService;

    // 把 list.html 文件上传到minio中, 并且可以在浏览器中访问

    @Test
    public void test() throws FileNotFoundException {
        FileInputStream fileInputStream = new FileInputStream("d:/list.html");
        String path = fileStorageService.uploadHtmlFile("", "list.html", fileInputStream);
        System.out.println(path);
    }
}

⑸. 测试

执行测试类,点击测试链接
SpringCloud微服务项目实战 - 3.App端文章_第29张图片

minio地址: http://192.168.200.130:9000/minio/leadnews/

minio管理界面会新增 上传的文件,根据上传日期分类
SpringCloud微服务项目实战 - 3.App端文章_第30张图片

5. 静态页面上传至minIO

⑴. 管理依赖

编辑 heima-leadnews-service/heima-leadnews-article/pom.xml 文件:

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-freemarkerartifactId>
        dependency>
        <dependency>
            <groupId>com.heimagroupId>
            <artifactId>heima-file-starterartifactId>
            <version>1.0-SNAPSHOTversion>
        dependency>
    dependencies>

⑵. Nacos配置

编辑 leadnews-article 配置:

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/leadnews_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
    username: root
    password: 123456
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  # 设置别名包扫描路径,通过该属性可以给包中的类注册别名
  type-aliases-package: com.heima.model.article.pojos
 
minio:
  accessKey: minio
  secretKey: minio123
  bucket: leadnews
  endpoint: http://192.168.200.130:9000
  readPath: http://192.168.200.130:9000

⑶. 模板文件

新建 heima-leadnews-service/heima-leadnews-article/src/main/resources/templates/article.ftl 文件:

DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport"
          content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover">
    <title>黑马头条title>
    
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.css">
    
    <link rel="stylesheet" href="../../../plugins/css/index.css">
head>

<body>
<div id="app">
    <div class="article">
        <van-row>
            <van-col span="24" class="article-title" v-html="title">van-col>
        van-row>

        <van-row type="flex" align="center" class="article-header">
            <van-col span="3">
                <van-image round class="article-avatar" src="https://p3.pstatp.com/thumb/1480/7186611868">van-image>
            van-col>
            <van-col span="16">
                <div v-html="authorName">div>
                <div>{{ publishTime | timestampToDateTime }}div>
            van-col>
            <van-col span="5">
                <van-button round :icon="relation.isfollow ? '' : 'plus'" type="info" class="article-focus"
                            :text="relation.isfollow ? '取消关注' : '关注'" :loading="followLoading" @click="handleClickArticleFollow">
                van-button>
            van-col>
        van-row>

        <van-row class="article-content">
            <#if content??>
                <#list content as item>
                    <#if item.type='text'>
                        <van-col span="24" class="article-text">${item.value}van-col>
                    <#else>
                        <van-col span="24" class="article-image">
                            <van-image width="100%" src="${item.value}">van-image>
                        van-col>
                    #if>
                #list>
            #if>
        van-row>

        <van-row type="flex" justify="center" class="article-action">
            <van-col>
                <van-button round :icon="relation.islike ? 'good-job' : 'good-job-o'" class="article-like"
                            :loading="likeLoading" :text="relation.islike ? '取消赞' : '点赞'" @click="handleClickArticleLike">van-button>
                <van-button round :icon="relation.isunlike ? 'delete' : 'delete-o'" class="article-unlike"
                            :loading="unlikeLoading" @click="handleClickArticleUnlike">不喜欢van-button>
            van-col>
        van-row>

        
        <van-list v-model="commentsLoading" :finished="commentsFinished" finished-text="没有更多了"
                  @load="onLoadArticleComments">
            <van-row id="#comment-view" type="flex" class="article-comment" v-for="(item, index) in comments" :key="index">
                <van-col span="3">
                    <van-image round src="https://p3.pstatp.com/thumb/1480/7186611868" class="article-avatar">van-image>
                van-col>
                <van-col span="21">
                    <van-row type="flex" align="center" justify="space-between">
                        <van-col class="comment-author" v-html="item.authorName">van-col>
                        <van-col>
                            <van-button round :icon="item.operation === 0 ? 'good-job' : 'good-job-o'" size="normal"
                                        @click="handleClickCommentLike(item)">{{ item.likes || '' }}
                            van-button>
                        van-col>
                    van-row>

                    <van-row>
                        <van-col class="comment-content" v-html="item.content">van-col>
                    van-row>
                    <van-row type="flex" align="center">
                        <van-col span="10" class="comment-time">
                            {{ item.createdTime | timestampToDateTime }}
                        van-col>
                        <van-col span="3">
                            <van-button round size="normal" v-html="item.reply" @click="showCommentRepliesPopup(item.id)">回复 {{
                                item.reply || '' }}
                            van-button>
                        van-col>
                    van-row>
                van-col>
            van-row>
        van-list>
    div>
    
    <van-row type="flex" justify="space-around" align="center" class="article-bottom-bar">
        <van-col span="13">
            <van-field v-model="commentValue" placeholder="写评论">
                <template #button>
                    <van-button icon="back-top" @click="handleSaveComment">van-button>
                template>
            van-field>
        van-col>
        <van-col span="3">
            <van-button icon="comment-o" @click="handleScrollIntoCommentView">van-button>
        van-col>
        <van-col span="3">
            <van-button :icon="relation.iscollection ? 'star' : 'star-o'" :loading="collectionLoading"
                        @click="handleClickArticleCollection">van-button>
        van-col>
        <van-col span="3">
            <van-button icon="share-o">van-button>
        van-col>
    van-row>

    
    <van-popup v-model="showPopup" closeable position="bottom"
               :style="{ width: '750px', height: '60%', left: '50%', 'margin-left': '-375px' }">
        
        <van-list v-model="commentRepliesLoading" :finished="commentRepliesFinished" finished-text="没有更多了"
                  @load="onLoadCommentReplies">
            <van-row id="#comment-reply-view" type="flex" class="article-comment-reply"
                     v-for="(item, index) in commentReplies" :key="index">
                <van-col span="3">
                    <van-image round src="https://p3.pstatp.com/thumb/1480/7186611868" class="article-avatar">van-image>
                van-col>
                <van-col span="21">
                    <van-row type="flex" align="center" justify="space-between">
                        <van-col class="comment-author" v-html="item.authorName">van-col>
                        <van-col>
                            <van-button round :icon="item.operation === 0 ? 'good-job' : 'good-job-o'" size="normal"
                                        @click="handleClickCommentReplyLike(item)">{{ item.likes || '' }}
                            van-button>
                        van-col>
                    van-row>

                    <van-row>
                        <van-col class="comment-content" v-html="item.content">van-col>
                    van-row>
                    <van-row type="flex" align="center">
                        
                        <van-col span="10" class="comment-time">
                            {{ item.createdTime | timestampToDateTime }}
                        van-col>
                    van-row>
                van-col>
            van-row>
        van-list>
        
        <van-row type="flex" justify="space-around" align="center" class="comment-reply-bottom-bar">
            <van-col span="13">
                <van-field v-model="commentReplyValue" placeholder="写评论">
                    <template #button>
                        <van-button icon="back-top" @click="handleSaveCommentReply">van-button>
                    template>
                van-field>
            van-col>
            <van-col span="3">
                <van-button icon="comment-o">van-button>
            van-col>
            <van-col span="3">
                <van-button icon="star-o">van-button>
            van-col>
            <van-col span="3">
                <van-button icon="share-o">van-button>
            van-col>
        van-row>
    van-popup>
div>


<script src=" https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js">
script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/vant.min.js">script>

<#--<script src="https://unpkg.com/axios/dist/axios.min.js">script>-->
<script src="../../../plugins/js/axios.min.js">script>

<script src="../../../plugins/js/index.js">script>
body>

html>

⑷. 数据JSON文件

新建 heima-leadnews-service/heima-leadnews-article/src/main/resources/a.json 文件:

[
  {
    "type": "text",
    "value": "Java虚拟机——JVM\n\n- JVM(Java Virtual Machine ):Java虚拟机,简称JVM,是运行所有Java程序的假想计算机,是Java程序的运行环境,是Java 最具吸引力的特性之一。我们编写的Java代码,都运行在JVM 之上。\n- 跨平台:任何软件的运行,都必须要运行在操作系统之上,而我们用Java编写的软件可以运行在任何的操作系统上,这个特性称为Java语言的跨平台特性。该特性是由JVM实现的,我们编写的程序运行在JVM上,而JVM运行在操作系统上。\n"
  },
  {
    "type": "image",
    "value": "http://192.168.200.130/group1/M00/00/00/wKjIgl9V2n6AArZsAAGMmaPdt7w502.png"
  },
  {
    "type": "text",
    "value": "如图所示,Java的虚拟机本身不具备跨平台功能的,每个操作系统下都有不同版本的虚拟机。\n\n问题1: Java 是如何实现跨平台的呢?\n\n- 答:因为在不同操作系统中都安装了对应版本的 JVM 虚拟机\n- 注意: Java程序想要运行, 必须依赖于JVM虚拟机.\n\n问题2: JVM 本身是否允许跨平台呢?\n\n- 答:不允许,允许跨平台的是 Java 程序,而不是虚拟机。\n"
  }
]

⑸. css 和 js 上传MinIO

①. 本地文件

新增 D:\tem\css\index.css 文件:

html {
    overflow-x: hidden;
}

#app {
    position: relative;
    width: 750px;
    margin: 0 auto;
    color: #333;
    background-color: #f8f8f8;
}

.article {
    padding: 0 40px 120px;
}

.article-title {
    margin-top: 48px;
    font-size: 40px;
    font-weight: bold;
    color: #3A3A3A;
    line-height: 65px;
}

.article-header {
    margin-top: 57px;
}

.article-content {
    margin-top: 39px;
}

.article-avatar {
    width: 70px;
    height: 70px;
}

.article-author {
    font-size: 28px;
    font-weight: 400;
    color: #3A3A3A;
}

.article-publish-time {
    font-size: 24px;
    font-weight: 400;
    color: #B4B4B4;
}

.article-focus {
    width: 170px;
    height: 58px;
    font-size: 28px;
    font-weight: 400;
    color: #FFFFFF;
}

.article-text {
    font-size: 32px;
    font-weight: 400;
    color: #3A3A3A;
    line-height: 56px;
    text-align: justify;
}

.article-action {
    margin-top: 59px;
}

.article-like {
    width: 156px;
    height: 58px;
    font-size: 25px;
    font-weight: 400;
    color: #777777;
}

.article-unlike {
    width: 156px;
    height: 58px;
    margin-left: 42px;
    font-size: 25px;
    font-weight: 400;
    color: #E22829;
}

.article-comment {
    margin-top: 69px;
}

.comment-author {
    font-size: 24px;
    font-weight: 400;
    color: #777777;
    line-height: 49px;
}

.comment-content {
    font-size: 32px;
    font-weight: 400;
    color: #3A3A3A;
    line-height: 49px;
}

.comment-time {
    font-size: 24px;
    font-weight: 400;
    color: #B4B4B4;
    line-height: 49px;
}

.article-comment-reply {
    padding: 40px;
}

.article-bottom-bar, .comment-reply-bottom-bar {
    position: fixed;
    bottom: 0;
    width: 750px;
    height: 99px;
    background: #F4F5F6;
}

.article-bottom-bar .van-field, .comment-reply-bottom-bar .van-field {
    width: 399px;
    height: 64px;
    background: #FFFFFF;
    border: 2px solid #EEEEEE;
    border-radius: 32px;
    font-size: 25px;
    font-weight: 400;
    color: #777777;
}

.article-bottom-bar .van-button, .comment-reply-bottom-bar .van-button {
    background-color: transparent;
    border-color: transparent;
    font-size: 25px;
    font-weight: 400;
    color: #777777;
}

新增 D:\tem\js\index.js 文件:

// 初始化 Vue 实例
new Vue({
  el: '#app',
  data() {
    return {
      // Minio模板应该写真实接口地址
      baseUrl: 'http://192.168.200.150:51601', //'http://172.16.17.191:5001',
      token: '',
      equipmentId: '',
      articleId: '',
      title: '',
      authorId: 0,
      authorName: '',
      publishTime: '',
      relation: {
        islike: false,
        isunlike: false,
        iscollection: false,
        isfollow: false,
        isforward: false
      },
      followLoading: false,
      likeLoading: false,
      unlikeLoading: false,
      collectionLoading: false,
      // 评论
      comments: [],
      commentsLoading: false,
      commentsFinished: false,
      commentValue: '',
      currentCommentId: '',
      // 评论回复
      commentReplies: [],
      commentRepliesLoading: false,
      commentRepliesFinished: false,
      commentReplyValue: '',
      showPopup: false
    }
  },
  filters: {
    // TODO: js计算时间差
    timestampToDateTime: function (value) {
      if (!value) return ''

      const date = new Date(value)
      const Y = date.getFullYear() + '-'
      const M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'
      const D = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate()) + ' '
      const h = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ':'
      const m = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) + ':'
      const s = (date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds())

      return Y + M + D + h + m + s
    }
  },
  created() {
    this.token = this.getQueryVariable('token')
    this.equipmentId = this.getQueryVariable('equipmentId')
    this.articleId = this.getQueryVariable('articleId')
    this.title = this.getQueryVariable('title')
    const authorId = this.getQueryVariable('authorId')
    if (authorId) {
      this.authorId = parseInt(authorId, 10)
    }
    this.authorName = this.getQueryVariable('authorName')
    const publishTime = this.getQueryVariable('publishTime')
    if (publishTime) {
      this.publishTime = parseInt(publishTime, 10)
    }
    this.loadArticleBehavior()
	this.readArticleBehavior()
  },
  
  methods: {
    // 加载文章评论
    async loadArticleComments(index = 1, minDate = 20000000000000) {
      const url = `${this.baseUrl}/comment/api/v1/comment/load`
      const data = { articleId: this.articleId, index: index, minDate: minDate }
      const config = { headers: { 'token': this.token } }

      try {
        const { status, data: { code, errorMessage, data: comments } } = await axios.post(url, data, config)
        if (status !== 200) {
          vant.Toast.fail('当前系统正在维护,请稍后重试')
          return
        }
        if (code !== 200) {
          vant.Toast.fail(errorMessage)
          return
        }
        if (comments.length) {
          this.comments = this.comments.concat(comments)
        }

        // 加载状态结束
        this.commentsLoading = false;

        // 数据全部加载完成
        if (!comments.length) {
          this.commentsFinished = true
        }
      } catch (err) {
        this.commentsLoading = false
        this.commentsFinished = true
        console.log('err: ' + err)
      }
    },
    // 滚动加载文章评论
    onLoadArticleComments() {
      let index = undefined
      let minDate = undefined
      if (this.comments.length) {
        index = 2
        minDate = this.comments[this.comments.length - 1].createdTime
      }
      this.loadArticleComments(index, minDate)
    },
    // 加载文章行为
    async loadArticleBehavior() {
      const url = `${this.baseUrl}/article/api/v1/article/load_article_behavior/`
      const data = { equipmentId: this.equipmentId, articleId: this.articleId, authorId: this.authorId }
      const config = { headers: { 'token': this.token } }

      try {
        const { status, data: { code, errorMessage, data: relation } } = await axios.post(url, data, config)
        if (status !== 200) {
          vant.Toast.fail('当前系统正在维护,请稍后重试')
          return
        }
        if (code !== 200) {
          vant.Toast.fail(errorMessage)
          return
        }
        this.relation = relation
      } catch (err) {
        console.log('err: ' + err)
      }
    },
	//阅读文章行为
	async readArticleBehavior(){
		const url = `${this.baseUrl}/behavior/api/v1/read_behavior`
		const data = {equipmentId:this.equipmentId,articleId:this.articleId,count:1,readDuration:0,percentage:0,loadDuration:0}
		const config = {headers:{'token':this.token}}
		
		try{
			const {status,data:{code,errorMessage}} = await axios.post(url,data,config)
			if(status !== 200){
				vant.Toast.fail("当前系统正在维护,请稍后重试")
				return
			}
			if(code !== 0){
				vant.Toast.fail(errorMessage)
				return
			}
		}catch (err){
			console.log('err: '+ err)
		}
	},
    // 关注/取消关注
    async handleClickArticleFollow() {
      const url = `${this.baseUrl}/user/api/v1/user/user_follow/`
      const data = { authorId: this.authorId, operation: this.relation.isfollow ? 1 : 0, articleId: this.articleId }
      const config = { headers: { 'token': this.token } }

      this.followLoading = true
      try {
        const { status, data: { code, errorMessage } } = await axios.post(url, data, config)
        if (status !== 200) {
          vant.Toast.fail('当前系统正在维护,请稍后重试')
          return
        }
        if (code !== 200) {
          vant.Toast.fail(errorMessage)
          return
        }
        this.relation.isfollow = !this.relation.isfollow
        vant.Toast.success(this.relation.isfollow ? '成功关注' : '成功取消关注')
      } catch (err) {
        console.log('err: ' + err)
      }
      this.followLoading = false
    },
    // 点赞/取消赞
    async handleClickArticleLike() {
      const url = `${this.baseUrl}/behavior/api/v1/likes_behavior/`
      const data = { equipmentId: this.equipmentId, articleId: this.articleId, type: 0, operation: this.relation.islike ? 1 : 0 }
      const config = { headers: { 'token': this.token } }

      this.likeLoading = true
      try {
        const { status, data: { code, errorMessage } } = await axios.post(url, data, config)
        if (status !== 200) {
          vant.Toast.fail('当前系统正在维护,请稍后重试')
          return
        }
        if (code !== 200) {
          vant.Toast.fail(errorMessage)
          return
        }
        this.relation.islike = !this.relation.islike
        vant.Toast.success(this.relation.islike ? '点赞操作成功' : '取消点赞操作成功')
      } catch (err) {
        console.log('err: ' + err)
      }
      this.likeLoading = false
    },
    // 不喜欢/取消不喜欢
    async handleClickArticleUnlike() {
      const url = `${this.baseUrl}/behavior/api/v1/un_likes_behavior/`
      const data = { equipmentId: this.equipmentId, articleId: this.articleId, type: this.relation.isunlike ? 1 : 0 }
      const config = { headers: { 'token': this.token } }

      this.unlikeLoading = true
      try {
        const { status, data: { code, errorMessage } } = await axios.post(url, data, config)
        if (status !== 200) {
          vant.Toast.fail('当前系统正在维护,请稍后重试')
          return
        }
        if (code !== 200) {
          vant.Toast.fail(errorMessage)
          return
        }
        this.relation.isunlike = !this.relation.isunlike
        vant.Toast.success(this.relation.isunlike ? '不喜欢操作成功' : '取消不喜欢操作成功')
      } catch (err) {
        console.log('err: ' + err)
      }
      this.unlikeLoading = false
    },
    // 提交评论
    async handleSaveComment() {
      if (!this.commentValue) {
        vant.Toast.fail('评论内容不能为空')
        return
      }
      if (this.commentValue.length > 140) {
        vant.Toast.fail('评论字数不能超过140字')
        return
      }
      const url = `${this.baseUrl}/comment/api/v1/comment/save`
      const data = { articleId: this.articleId, content: this.commentValue }
      const config = { headers: { 'token': this.token } }

      try {
        const { status, data: {  code, errorMessage } } = await axios.post(url, data, config)
        if (status !== 200) {
          vant.Toast.fail('当前系统正在维护,请稍后重试')
          return
        }
        if (code !== 200) {
          vant.Toast.fail(errorMessage)
          return
        }
        vant.Toast.success('评论成功')
        this.commentValue = ''
		this.comments = []
        this.loadArticleComments()
		this.commentsFinished = false;
      } catch (err) {
        console.log('err: ' + err)
      }
    },
    // 页面滚动到评论区
    handleScrollIntoCommentView() {
      document.getElementById('#comment-view').scrollIntoView({ behavior: 'smooth' })
    },
    // 收藏/取消收藏
    async handleClickArticleCollection() {
      const url = `${this.baseUrl}/article/api/v1/collection_behavior/`
      const data = { equipmentId: this.equipmentId, entryId: this.articleId, publishedTime: this.publishTime, type: 0, operation: this.relation.iscollection ? 1 :0 }
      const config = { headers: { 'token': this.token } }

      this.collectionLoading = true
      try {
        const { status, data: { code, errorMessage } } = await axios.post(url, data, config)
        if (status !== 200) {
          vant.Toast.fail('当前系统正在维护,请稍后重试')
          return
        }
        if (code !== 200) {
          vant.Toast.fail(errorMessage)
          return
        }
        this.relation.iscollection = !this.relation.iscollection
        vant.Toast.success(this.relation.iscollection ? '收藏操作成功' : '取消收藏操作成功')
      } catch (err) {
        console.log('err: ' + err)
      }
      this.collectionLoading = false
    },
    // 评论点赞
    async handleClickCommentLike(comment) {
      const commentId = comment.id
      const operation = comment.operation === 0 ? 1 : 0

      const url = `${this.baseUrl}/comment/api/v1/comment/like`
      const data = { commentId: comment.id, operation: operation }
      const config = { headers: { 'token': this.token } }

      try {
        const { status, data: { code, errorMessage, data: { likes } } } = await axios.post(url, data, config)
        if (status !== 200) {
          vant.Toast.fail('当前系统正在维护,请稍后重试')
          return
        }
        if (code !== 200) {
          vant.Toast.fail(errorMessage)
          return
        }
        const item = this.comments.find((item) => {
          return item.id === commentId
        })
        item.operation = operation
        item.likes = likes
        vant.Toast.success((operation === 0 ? '点赞' : '取消点赞') + '操作成功!')
      } catch (err) {
        console.log('err: ' + err)
      }
    },
    // 弹出评论回复Popup
    showCommentRepliesPopup(commentId) {
      this.showPopup = true;
      this.currentCommentId = commentId
      this.commentReplies = []
      this.commentRepliesFinished = false
    },
    // 加载评论回复
    async loadCommentReplies(minDate = 20000000000000) {
      const url = `${this.baseUrl}/comment/api/v1/comment_repay/load`
      const data = { commentId: this.currentCommentId, 'minDate':  minDate}
      const config = { headers: { 'token': this.token } }

      try {
        const { status, data: { code, errorMessage, data: commentReplies } } = await axios.post(url, data, config)
        if (status !== 200) {
          vant.Toast.fail('当前系统正在维护,请稍后重试')
          return
        }
        if (code !== 200) {
          vant.Toast.fail(errorMessage)
          return
        }
        if (commentReplies.length) {
          this.commentReplies = this.commentReplies.concat(commentReplies)
        }

        // 加载状态结束
        this.commentRepliesLoading = false;

        // 数据全部加载完成
        if (!commentReplies.length) {
          this.commentRepliesFinished = true
        }
      } catch (err) {
        this.commentRepliesLoading = false
        this.commentRepliesFinished = true
        console.log('err: ' + err)
      }
    },
    // 滚动加载评论回复
    onLoadCommentReplies() {
      let minDate = undefined
      if (this.commentReplies.length) {
        minDate = this.commentReplies[this.commentReplies.length - 1].createdTime
      }
      this.loadCommentReplies(minDate)
    },
    // 提交评论回复
    async handleSaveCommentReply() {
      if (!this.commentReplyValue) {
        vant.Toast.fail('评论内容不能为空')
        return
      }
      if (this.commentReplyValue.length > 140) {
        vant.Toast.fail('评论字数不能超过140字')
        return
      }
      const url = `${this.baseUrl}/comment/api/v1/comment_repay/save`
      const data = { commentId: this.currentCommentId, content: this.commentReplyValue }
      const config = { headers: { 'token': this.token } }

      try {
        const { status, data: {  code, errorMessage } } = await axios.post(url, data, config)
        if (status !== 200) {
          vant.Toast.fail('当前系统正在维护,请稍后重试')
          return
        }
        if (code !== 200) {
          vant.Toast.fail(errorMessage)
          return
        }
        vant.Toast.success('评论成功')
        this.commentReplyValue = ''
		this.commentReplies = []
		this.comments = []
        // 刷新评论回复列表
        this.loadCommentReplies()
        // 刷新文章评论列表
        this.loadArticleComments()
      } catch (err) {
        console.log('err: ' + err)
      }
    },
    // 评论回复点赞
    async handleClickCommentReplyLike(commentReply) {
      const commentReplyId = commentReply.id
      const operation = commentReply.operation === 0 ? 1 : 0

      const url = `${this.baseUrl}/comment/api/v1/comment_repay/like`
      const data = { commentRepayId: commentReplyId, 'operation': operation }
      const config = { headers: { 'token': this.token } }

      try {
        const { status, data: { code, errorMessage, data: { likes } } } = await axios.post(url, data, config)
        if (status !== 200) {
          vant.Toast.fail('当前系统正在维护,请稍后重试')
          return
        }
        if (code !== 200) {
          vant.Toast.fail(errorMessage)
          return
        }
        const item = this.commentReplies.find((item) => {
          return item.id === commentReplyId
        })
        item.operation = operation
        item.likes = likes
        vant.Toast.success((operation === 0 ? '点赞' : '取消点赞') + '操作成功!')
      } catch (err) {
        console.log('err: ' + err)
      }
    },
    getQueryVariable(aVariable) {
      const query = decodeURI(window.location.search).substring(1)
      const array = query.split('&')
      for (let i = 0; i < array.length; i++) {
        const pair = array[i].split('=')
        if (pair[0] == aVariable) {
          return pair[1]
        }
      }
      return undefined
    },
    // onSelect(option) {
    //   vant.Toast(option.name);
    //   this.showShare = false;
    // }
  }
})

新增 D:\tem\js\axios.min.js 文件:

/* axios v0.21.1 | (c) 2020 by Matt Zabriskie */
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";function r(e){var t=new i(e),n=s(i.prototype.request,t);return o.extend(n,i.prototype,t),o.extend(n,t),n}var o=n(2),s=n(3),i=n(4),a=n(22),u=n(10),c=r(u);c.Axios=i,c.create=function(e){return r(a(c.defaults,e))},c.Cancel=n(23),c.CancelToken=n(24),c.isCancel=n(9),c.all=function(e){return Promise.all(e)},c.spread=n(25),c.isAxiosError=n(26),e.exports=c,e.exports.default=c},function(e,t,n){"use strict";function r(e){return"[object Array]"===R.call(e)}function o(e){return"undefined"==typeof e}function s(e){return null!==e&&!o(e)&&null!==e.constructor&&!o(e.constructor)&&"function"==typeof e.constructor.isBuffer&&e.constructor.isBuffer(e)}function i(e){return"[object ArrayBuffer]"===R.call(e)}function a(e){return"undefined"!=typeof FormData&&e instanceof FormData}function u(e){var t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer}function c(e){return"string"==typeof e}function f(e){return"number"==typeof e}function p(e){return null!==e&&"object"==typeof e}function d(e){if("[object Object]"!==R.call(e))return!1;var t=Object.getPrototypeOf(e);return null===t||t===Object.prototype}function l(e){return"[object Date]"===R.call(e)}function h(e){return"[object File]"===R.call(e)}function m(e){return"[object Blob]"===R.call(e)}function y(e){return"[object Function]"===R.call(e)}function g(e){return p(e)&&y(e.pipe)}function v(e){return"undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams}function x(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function w(){return("undefined"==typeof navigator||"ReactNative"!==navigator.product&&"NativeScript"!==navigator.product&&"NS"!==navigator.product)&&("undefined"!=typeof window&&"undefined"!=typeof document)}function b(e,t){if(null!==e&&"undefined"!=typeof e)if("object"!=typeof e&&(e=[e]),r(e))for(var n=0,o=e.length;n<o;n++)t.call(null,e[n],n,e);else for(var s in e)Object.prototype.hasOwnProperty.call(e,s)&&t.call(null,e[s],s,e)}function E(){function e(e,n){d(t[n])&&d(e)?t[n]=E(t[n],e):d(e)?t[n]=E({},e):r(e)?t[n]=e.slice():t[n]=e}for(var t={},n=0,o=arguments.length;n<o;n++)b(arguments[n],e);return t}function j(e,t,n){return b(t,function(t,r){n&&"function"==typeof t?e[r]=S(t,n):e[r]=t}),e}function C(e){return 65279===e.charCodeAt(0)&&(e=e.slice(1)),e}var S=n(3),R=Object.prototype.toString;e.exports={isArray:r,isArrayBuffer:i,isBuffer:s,isFormData:a,isArrayBufferView:u,isString:c,isNumber:f,isObject:p,isPlainObject:d,isUndefined:o,isDate:l,isFile:h,isBlob:m,isFunction:y,isStream:g,isURLSearchParams:v,isStandardBrowserEnv:w,forEach:b,merge:E,extend:j,trim:x,stripBOM:C}},function(e,t){"use strict";e.exports=function(e,t){return function(){for(var n=new Array(arguments.length),r=0;r<n.length;r++)n[r]=arguments[r];return e.apply(t,n)}}},function(e,t,n){"use strict";function r(e){this.defaults=e,this.interceptors={request:new i,response:new i}}var o=n(2),s=n(5),i=n(6),a=n(7),u=n(22);r.prototype.request=function(e){"string"==typeof e?(e=arguments[1]||{},e.url=arguments[0]):e=e||{},e=u(this.defaults,e),e.method?e.method=e.method.toLowerCase():this.defaults.method?e.method=this.defaults.method.toLowerCase():e.method="get";var t=[a,void 0],n=Promise.resolve(e);for(this.interceptors.request.forEach(function(e){t.unshift(e.fulfilled,e.rejected)}),this.interceptors.response.forEach(function(e){t.push(e.fulfilled,e.rejected)});t.length;)n=n.then(t.shift(),t.shift());return n},r.prototype.getUri=function(e){return e=u(this.defaults,e),s(e.url,e.params,e.paramsSerializer).replace(/^\?/,"")},o.forEach(["delete","get","head","options"],function(e){r.prototype[e]=function(t,n){return this.request(u(n||{},{method:e,url:t,data:(n||{}).data}))}}),o.forEach(["post","put","patch"],function(e){r.prototype[e]=function(t,n,r){return this.request(u(r||{},{method:e,url:t,data:n}))}}),e.exports=r},function(e,t,n){"use strict";function r(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}var o=n(2);e.exports=function(e,t,n){if(!t)return e;var s;if(n)s=n(t);else if(o.isURLSearchParams(t))s=t.toString();else{var i=[];o.forEach(t,function(e,t){null!==e&&"undefined"!=typeof e&&(o.isArray(e)?t+="[]":e=[e],o.forEach(e,function(e){o.isDate(e)?e=e.toISOString():o.isObject(e)&&(e=JSON.stringify(e)),i.push(r(t)+"="+r(e))}))}),s=i.join("&")}if(s){var a=e.indexOf("#");a!==-1&&(e=e.slice(0,a)),e+=(e.indexOf("?")===-1?"?":"&")+s}return e}},function(e,t,n){"use strict";function r(){this.handlers=[]}var o=n(2);r.prototype.use=function(e,t){return this.handlers.push({fulfilled:e,rejected:t}),this.handlers.length-1},r.prototype.eject=function(e){this.handlers[e]&&(this.handlers[e]=null)},r.prototype.forEach=function(e){o.forEach(this.handlers,function(t){null!==t&&e(t)})},e.exports=r},function(e,t,n){"use strict";function r(e){e.cancelToken&&e.cancelToken.throwIfRequested()}var o=n(2),s=n(8),i=n(9),a=n(10);e.exports=function(e){r(e),e.headers=e.headers||{},e.data=s(e.data,e.headers,e.transformRequest),e.headers=o.merge(e.headers.common||{},e.headers[e.method]||{},e.headers),o.forEach(["delete","get","head","post","put","patch","common"],function(t){delete e.headers[t]});var t=e.adapter||a.adapter;return t(e).then(function(t){return r(e),t.data=s(t.data,t.headers,e.transformResponse),t},function(t){return i(t)||(r(e),t&&t.response&&(t.response.data=s(t.response.data,t.response.headers,e.transformResponse))),Promise.reject(t)})}},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t,n){return r.forEach(n,function(n){e=n(e,t)}),e}},function(e,t){"use strict";e.exports=function(e){return!(!e||!e.__CANCEL__)}},function(e,t,n){"use strict";function r(e,t){!s.isUndefined(e)&&s.isUndefined(e["Content-Type"])&&(e["Content-Type"]=t)}function o(){var e;return"undefined"!=typeof XMLHttpRequest?e=n(12):"undefined"!=typeof process&&"[object process]"===Object.prototype.toString.call(process)&&(e=n(12)),e}var s=n(2),i=n(11),a={"Content-Type":"application/x-www-form-urlencoded"},u={adapter:o(),transformRequest:[function(e,t){return i(t,"Accept"),i(t,"Content-Type"),s.isFormData(e)||s.isArrayBuffer(e)||s.isBuffer(e)||s.isStream(e)||s.isFile(e)||s.isBlob(e)?e:s.isArrayBufferView(e)?e.buffer:s.isURLSearchParams(e)?(r(t,"application/x-www-form-urlencoded;charset=utf-8"),e.toString()):s.isObject(e)?(r(t,"application/json;charset=utf-8"),JSON.stringify(e)):e}],transformResponse:[function(e){if("string"==typeof e)try{e=JSON.parse(e)}catch(e){}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,validateStatus:function(e){return e>=200&&e<300}};u.headers={common:{Accept:"application/json, text/plain, */*"}},s.forEach(["delete","get","head"],function(e){u.headers[e]={}}),s.forEach(["post","put","patch"],function(e){u.headers[e]=s.merge(a)}),e.exports=u},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){r.forEach(e,function(n,r){r!==t&&r.toUpperCase()===t.toUpperCase()&&(e[t]=n,delete e[r])})}},function(e,t,n){"use strict";var r=n(2),o=n(13),s=n(16),i=n(5),a=n(17),u=n(20),c=n(21),f=n(14);e.exports=function(e){return new Promise(function(t,n){var p=e.data,d=e.headers;r.isFormData(p)&&delete d["Content-Type"];var l=new XMLHttpRequest;if(e.auth){var h=e.auth.username||"",m=e.auth.password?unescape(encodeURIComponent(e.auth.password)):"";d.Authorization="Basic "+btoa(h+":"+m)}var y=a(e.baseURL,e.url);if(l.open(e.method.toUpperCase(),i(y,e.params,e.paramsSerializer),!0),l.timeout=e.timeout,l.onreadystatechange=function(){if(l&&4===l.readyState&&(0!==l.status||l.responseURL&&0===l.responseURL.indexOf("file:"))){var r="getAllResponseHeaders"in l?u(l.getAllResponseHeaders()):null,s=e.responseType&&"text"!==e.responseType?l.response:l.responseText,i={data:s,status:l.status,statusText:l.statusText,headers:r,config:e,request:l};o(t,n,i),l=null}},l.onabort=function(){l&&(n(f("Request aborted",e,"ECONNABORTED",l)),l=null)},l.onerror=function(){n(f("Network Error",e,null,l)),l=null},l.ontimeout=function(){var t="timeout of "+e.timeout+"ms exceeded";e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),n(f(t,e,"ECONNABORTED",l)),l=null},r.isStandardBrowserEnv()){var g=(e.withCredentials||c(y))&&e.xsrfCookieName?s.read(e.xsrfCookieName):void 0;g&&(d[e.xsrfHeaderName]=g)}if("setRequestHeader"in l&&r.forEach(d,function(e,t){"undefined"==typeof p&&"content-type"===t.toLowerCase()?delete d[t]:l.setRequestHeader(t,e)}),r.isUndefined(e.withCredentials)||(l.withCredentials=!!e.withCredentials),e.responseType)try{l.responseType=e.responseType}catch(t){if("json"!==e.responseType)throw t}"function"==typeof e.onDownloadProgress&&l.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&l.upload&&l.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then(function(e){l&&(l.abort(),n(e),l=null)}),p||(p=null),l.send(p)})}},function(e,t,n){"use strict";var r=n(14);e.exports=function(e,t,n){var o=n.config.validateStatus;n.status&&o&&!o(n.status)?t(r("Request failed with status code "+n.status,n.config,null,n.request,n)):e(n)}},function(e,t,n){"use strict";var r=n(15);e.exports=function(e,t,n,o,s){var i=new Error(e);return r(i,t,n,o,s)}},function(e,t){"use strict";e.exports=function(e,t,n,r,o){return e.config=t,n&&(e.code=n),e.request=r,e.response=o,e.isAxiosError=!0,e.toJSON=function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code}},e}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){return{write:function(e,t,n,o,s,i){var a=[];a.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&a.push("expires="+new Date(n).toGMTString()),r.isString(o)&&a.push("path="+o),r.isString(s)&&a.push("domain="+s),i===!0&&a.push("secure"),document.cookie=a.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},function(e,t,n){"use strict";var r=n(18),o=n(19);e.exports=function(e,t){return e&&!r(t)?o(e,t):t}},function(e,t){"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},function(e,t){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},function(e,t,n){"use strict";var r=n(2),o=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var t,n,s,i={};return e?(r.forEach(e.split("\n"),function(e){if(s=e.indexOf(":"),t=r.trim(e.substr(0,s)).toLowerCase(),n=r.trim(e.substr(s+1)),t){if(i[t]&&o.indexOf(t)>=0)return;"set-cookie"===t?i[t]=(i[t]?i[t]:[]).concat([n]):i[t]=i[t]?i[t]+", "+n:n}}),i):i}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){function e(e){var t=e;return n&&(o.setAttribute("href",t),t=o.href),o.setAttribute("href",t),{href:o.href,protocol:o.protocol?o.protocol.replace(/:$/,""):"",host:o.host,search:o.search?o.search.replace(/^\?/,""):"",hash:o.hash?o.hash.replace(/^#/,""):"",hostname:o.hostname,port:o.port,pathname:"/"===o.pathname.charAt(0)?o.pathname:"/"+o.pathname}}var t,n=/(msie|trident)/i.test(navigator.userAgent),o=document.createElement("a");return t=e(window.location.href),function(n){var o=r.isString(n)?e(n):n;return o.protocol===t.protocol&&o.host===t.host}}():function(){return function(){return!0}}()},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){function n(e,t){return r.isPlainObject(e)&&r.isPlainObject(t)?r.merge(e,t):r.isPlainObject(t)?r.merge({},t):r.isArray(t)?t.slice():t}function o(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(s[o]=n(void 0,e[o])):s[o]=n(e[o],t[o])}t=t||{};var s={},i=["url","method","data"],a=["headers","auth","proxy","params"],u=["baseURL","transformRequest","transformResponse","paramsSerializer","timeout","timeoutMessage","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","decompress","maxContentLength","maxBodyLength","maxRedirects","transport","httpAgent","httpsAgent","cancelToken","socketPath","responseEncoding"],c=["validateStatus"];r.forEach(i,function(e){r.isUndefined(t[e])||(s[e]=n(void 0,t[e]))}),r.forEach(a,o),r.forEach(u,function(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(s[o]=n(void 0,e[o])):s[o]=n(void 0,t[o])}),r.forEach(c,function(r){r in t?s[r]=n(e[r],t[r]):r in e&&(s[r]=n(void 0,e[r]))});var f=i.concat(a).concat(u).concat(c),p=Object.keys(e).concat(Object.keys(t)).filter(function(e){return f.indexOf(e)===-1});return r.forEach(p,o),s}},function(e,t){"use strict";function n(e){this.message=e}n.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},n.prototype.__CANCEL__=!0,e.exports=n},function(e,t,n){"use strict";function r(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise(function(e){t=e});var n=this;e(function(e){n.reason||(n.reason=new o(e),t(n.reason))})}var o=n(23);r.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},r.source=function(){var e,t=new r(function(t){e=t});return{token:t,cancel:e}},e.exports=r},function(e,t){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}},function(e,t){"use strict";e.exports=function(e){return"object"==typeof e&&e.isAxiosError===!0}}])});
//# sourceMappingURL=axios.min.map

②. 上传至minio

编辑 heima-leadnews-test/minio-demo/src/test/java/com/heima/minio/test/MinIOTest.java 测试类上传CSS:

    // 上传css
    public static void main(String[] args) {
        try {
            FileInputStream fileInputStream = new FileInputStream("d:/tem/css/index.css");

            // 1. 获取minio的链接信息 创建一个minio客户端
            MinioClient minioClient = MinioClient.builder().credentials("minio", "minio123").endpoint("http://192.168.200.130:9000").build();

            // 2. 上传
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .object("plugins/css/index.css") // 文件名
                    .contentType("text/html") // 文件类型
                    .bucket("leadnews") // 桶名称, 与minio管理界面创建的桶一致
                    .stream(fileInputStream, fileInputStream.available(), -1).build();
            minioClient.putObject(putObjectArgs);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

编辑 heima-leadnews-test/minio-demo/src/test/java/com/heima/minio/test/MinIOTest.java 测试类上传JS:

    // 上传js
    public static void main(String[] args) {
        try {
            FileInputStream fileInputStream = new FileInputStream("d:/tem/js/index.js");

            // 1. 获取minio的链接信息 创建一个minio客户端
            MinioClient minioClient = MinioClient.builder().credentials("minio", "minio123").endpoint("http://192.168.200.130:9000").build();

            // 2. 上传
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .object("plugins/js/index.js") // 文件名
                    .contentType("text/js") // 文件类型
                    .bucket("leadnews") // 桶名称, 与minio管理界面创建的桶一致
                    .stream(fileInputStream, fileInputStream.available(), -1).build();
            minioClient.putObject(putObjectArgs);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

编辑 heima-leadnews-test/minio-demo/src/test/java/com/heima/minio/test/MinIOTest.java 测试类上传JS:

    // 上传axios.min
    public static void main(String[] args) {
        try {
            FileInputStream fileInputStream = new FileInputStream("d:/tem/js/axios.min.js");

            // 1. 获取minio的链接信息 创建一个minio客户端
            MinioClient minioClient = MinioClient.builder().credentials("minio", "minio123").endpoint("http://192.168.200.130:9000").build();

            // 2. 上传
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .object("plugins/js/axios.min.js") // 文件名
                    .contentType("text/js") // 文件类型
                    .bucket("leadnews") // 桶名称, 与minio管理界面创建的桶一致
                    .stream(fileInputStream, fileInputStream.available(), -1).build();
            minioClient.putObject(putObjectArgs);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

⑹. mapper

新建 heima-leadnews-service/heima-leadnews-article/src/main/java/com/heima/article/mapper/ApArticleContentMapper.java 文件:

@Mapper
public interface ApArticleContentMapper extends BaseMapper<ApArticleContent> {
}

⑺. 测试类

新建 heima-leadnews-service/heima-leadnews-article/src/test/java/com/heima/article/test/ArticleFreemarkerTest.java 文件:

@SpringBootTest(classes = ArticleApplication.class)
@RunWith(SpringRunner.class)
public class ArticleFreemarkerTest {

    @Autowired
    private ApArticleContentMapper apArticleContentMapper;

    @Autowired
    private Configuration configuration;

    @Autowired
    private FileStorageService fileStorageService;

    @Autowired
    private ApArticleService apArticleService;

    @Autowired
    private ApArticleMapper apArticleMapper;

    @Test
    public void createStaticUrlTest() throws Exception {

        //1.获取文章内容
        ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, 1303156149041758210L));

        // 2.判断文章内容不为空
        if(apArticleContent != null && StringUtils.isNotBlank(apArticleContent.getContent())){

            //3.文章内容通过freemarker生成html文件
            Template template = configuration.getTemplate("article.ftl");
            // 数据模型
            Map<String, Object> context = new HashMap<>();
            context.put("content", JSONArray.parseArray(apArticleContent.getContent()));
            StringWriter out = new StringWriter();
            // 合成
            template.process(context, out);

            //4.把html文件上传到minio中
            InputStream in = new ByteArrayInputStream(out.toString().getBytes());
            String path = fileStorageService.uploadHtmlFile("", apArticleContent.getArticleId() + ".html", in);

            //5.修改ap_article表,保存static_url字段
            apArticleService.update(Wrappers.<ApArticle>lambdaUpdate().eq(ApArticle::getId, apArticleContent.getArticleId())
                    .set(ApArticle::getStaticUrl, path));

//            ApArticle article = new ApArticle();
//            article.setId(apArticleContent.getArticleId());
//            article.setStaticUrl(path);
//            apArticleMapper.updateById(article);
        }
    }
}

⑻. 测试

执行测试类,查看ID对应的 static_url 属性
SpringCloud微服务项目实战 - 3.App端文章_第31张图片

将 url 粘贴至浏览器预览
SpringCloud微服务项目实战 - 3.App端文章_第32张图片

minio地址: http://192.168.200.130:9000/minio/leadnews/

minio管理界面会上传文章详情 的静态文件
SpringCloud微服务项目实战 - 3.App端文章_第33张图片

6. App端测试

启动 ArticleApplication(文章)UserApplication(用户)AppGatewayApplication(网关) 服务,访问App端: http://localhost:8801/#/login

SpringCloud微服务项目实战 - 3.App端文章_第34张图片



你可能感兴趣的:(Java,java,spring,spring,boot,spring,cloud)