JavaWeb_LeadNews_Day2-文章查询, freemarker, minio
文章列表查询
表的拆分-垂直分表
- 垂直分表: 将一个表的字段分散到多个表中,每个表存储其中一部分字段
- 优势:
- 减少IO争抢,减少锁表的几率,查看文章概述与文章详情互不影响
- 充分发挥高频数据的操作效率,对文章概述数据操作的高效率不会被操作文章详情数据的低效率所拖累
- 拆分规则:
- 把不常用的字段单独放在一张表
- 把text,blob等大字段拆分出来单独放在一张表
- 经常组合查询的字段单独放在一张表中
接口实现
- ArticleHomeDto
public class ArticleHomeDto { // 最大时间 Date maxBehotTime; // 最小时间 Date minBehotTime; // 分页size Integer size; // 频道id String tag; }
- ArticleHomeController
@RestController @RequestMapping("/api/v1/article") public class ArticleHomeController { @Autowired private ApArticleService apArticleService; /** * 加载首页 * @param articleHomeDto * @return */ @PostMapping("/load") public ResponseResult load(@RequestBody ArticleHomeDto articleHomeDto) { return apArticleService.load(articleHomeDto, ArticleConstants.LOADTYPE_LOAD_MORE); } /** * 加载更多 * @param articleHomeDto * @return */ @PostMapping("/loadmore") public ResponseResult loadmore(@RequestBody ArticleHomeDto articleHomeDto) { return apArticleService.load(articleHomeDto, ArticleConstants.LOADTYPE_LOAD_MORE); } /** * 加载最新 * @param articleHomeDto * @return */ @PostMapping("/loadnew") public ResponseResult loadnew(@RequestBody ArticleHomeDto articleHomeDto) { return apArticleService.load(articleHomeDto, ArticleConstants.LOADTYPE_LOAD_NEW); } }
- ApArticleMapper
@Mapper public interface ApArticleMapper extends BaseMapper<ApArticle> { /** * 加载文章列表 * @param dto * @param type 1.加载更多 2.加载最新 * @return */ List<ApArticle> loadArticleList(ArticleHomeDto dto, Short type); } // ApArticleMapper.xml <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 <!-- loadmore --> <if test="type != null and type == 1"> and aa.publish_time <![CDATA[<]]> #{dto.minBehotTime} </if> <if test="type != null and type == 2"> and aa.publish_time <![CDATA[>]]> #{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>
功能实现
- ApArticleService
// ApArticleServiceImpl @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. 检验参数 // 分页条数的检验 Integer size = dto.getSize(); if(size == null || size == 0){ size = 10; } // 分页值不超过50 size = Math.min(size, MAX_PAGE_SIZE); dto.setSize(size); // type检验 if(!type.equals(ArticleConstants.LOADTYPE_LOAD_MORE) && !type.equals(ArticleConstants.LOADTYPE_LOAD_NEW)){ type = ArticleConstants.LOADTYPE_LOAD_MORE; } // 频道参数检验 if(StringUtils.isBlank(dto.getTag())){ dto.setTag(ArticleConstants.DEFAULT_TAG); } // 时间检验 if(dto.getMaxBehotTime() == null){ dto.setMaxBehotTime(new Date()); } if(dto.getMinBehotTime() == null){ dto.setMinBehotTime(new Date()); } // 2. 查询 List<ApArticle> articleList = apArticleMapper.loadArticleList(dto, type); return ResponseResult.okResult(articleList); } } // 常量类 package com.heima.common.constants; 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__"; }
文章详情
实现分析
- 方案1: 用户某一条文章, 根据文章的id去查询文章内容表, 返回渲染页面
- 方案2: 静态模板展示
- 根据文章内容通过模板技术(freemarker)生成静态的html文件
- 将文件存入分布式文件系统(MinIO)中
- 将生成好的html访问路径存入文章表中
- 获取html的url
- 访问静态页面
freemarker
快速入门
- 依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency>
- 配置
server: port: 8881 #服务端口 spring: application: name: freemarker-demo #指定服务名 freemarker: cache: false #关闭模板缓存,方便测试 settings: template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试 suffix: .ftl #指定Freemarker模板文件的后缀名
- 模板文件 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>
- Controller
// entity public class Student { private String name;//姓名 private int age;//年龄 private Date birthday;//生日 private Float money;//钱包 } // controller @Controller public class HelloController { @GetMapping("/hello") public String hello(Model model) { model.addAttribute("name", "freemarker"); Student student = new Student(); student.setName("cen"); student.setAge(100); model.addAttribute("stu", student); return "01-basic"; } }
语法
- 基础语法
// 注释 <#-- freemarker注释 --> // 插值 Hello ${name} // FTL指令 Freemarker会解析标签中的表达式或逻辑 <# >FTL指令</#> // 文本 忽略解析, 直接输出 普通文本
- List, Map
// 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> <#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/> <br/> <a href="###">方式二:通过map.keyname.property</a><br/> 输出stu2的学生信息:<br/> 姓名:${stuMap.stu2.name}<br/> 年龄:${stuMap.stu2.age}<br/> <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> // controller @Controller public class HelloController { @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); //------------------------------------ //创建Map数据 HashMap<String,Student> stuMap = new HashMap<>(); stuMap.put("stu1",stu1); stuMap.put("stu2",stu2); // 3.1 向model中存放Map数据 model.addAttribute("stuMap", stuMap); return "02-list"; } }
- 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>
- freemarker没有刷新的话, 启动类配置On frame deactivation, 勾选Update resources
=
和==
都是判断相等
- 运算符
- 建议gt代替
>
, FreeMarker会把>
解释为标签的结束字符, 也可以使用括号来避免
- 建议gt代替
- 空值处理
// ?? 判断变量是否存在 <#if stus??> ... </#if> // ! 缺失变量默认值 ${name!''} // 如果是嵌套对象建议使用() ${(stu.name)!''}
- 内建函数
// 集合大小 stus集合的大小: ${stus?size} // 日期格式化 现在时间: ${today?datetime} 现在时间: ${today?string("yyyy年MM月")} // 将数字转成字符串 ${point?c} // assign标签是定义变量 // 将json字符串转成对象 <#assign text="{'bank':'工商银行', 'account':'123456789123'}" /> <#assign data=text?eval /> 开户行: ${data.bank} 账号: ${data.account}
静态文件生成
@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("C:\\Users\\cen\\Desktop\\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);
//向model中存放List集合数据
map.put("stus",stus);
//------------------------------------
//创建Map数据
HashMap<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", 123456789123L);
return map;
}
}
MinIO
下载安装
docker pull minio/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
快速入门
- 依赖
<dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>7.1.0</version> </dependency>
- MinIoTest
public class MinIOTest { public static void main(String[] args) { FileInputStream fileInputStream = null; try { fileInputStream = new FileInputStream("C:\\Users\\cen\\Desktop\\list.html");; //1.创建minio链接客户端 MinioClient minioClient = MinioClient.builder().credentials("minio", "minio123").endpoint("http://192.168.174.133: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); System.out.println("http://192.168.174.133:9000/leadnews/list.html"); } catch (Exception ex) { ex.printStackTrace(); } } }
starter
集成
- 依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </dependency> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>7.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies>
- 配置
// MinIOConfig @Data @Configuration @EnableConfigurationProperties({MinIOConfigProperties.class}) //当引入FileStorageService接口时 @ConditionalOnClass(FileStorageService.class) public class MinIOConfig { @Autowired private MinIOConfigProperties minIOConfigProperties; @Bean public MinioClient buildMinioClient() { return MinioClient .builder() .credentials(minIOConfigProperties.getAccessKey(), minIOConfigProperties.getSecretKey()) .endpoint(minIOConfigProperties.getEndpoint()) .build(); } } // MinIOConfigProperties @Data @ConfigurationProperties(prefix = "minio") // 文件上传 配置前缀file.oss public class MinIOConfigProperties implements Serializable { private String accessKey; private String secretKey; private String bucket; private String endpoint; private String readPath; } // spring.factories org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.heima.file.service.impl.MinIOFileStorageService
- 实现类
@Slf4j @EnableConfigurationProperties(MinIOConfigProperties.class) @Import(MinIOConfig.class) public class MinIOFileStorageService implements FileStorageService { @Autowired private MinioClient minioClient; @Autowired private MinIOConfigProperties minIOConfigProperties; private final static String separator = "/"; /** * @param dirPath * @param filename yyyy/mm/dd/file.jpg * @return */ public String builderFilePath(String dirPath,String filename) { StringBuilder stringBuilder = new StringBuilder(50); if(!StringUtils.isEmpty(dirPath)){ stringBuilder.append(dirPath).append(separator); } SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); String todayStr = sdf.format(new Date()); stringBuilder.append(todayStr).append(separator); stringBuilder.append(filename); return stringBuilder.toString(); } /** * 上传图片文件 * @param prefix 文件前缀 * @param filename 文件名 * @param inputStream 文件流 * @return 文件全路径 */ @Override public String uploadImgFile(String prefix, String filename,InputStream inputStream) { String filePath = builderFilePath(prefix, filename); try { PutObjectArgs putObjectArgs = PutObjectArgs.builder() .object(filePath) .contentType("image/jpg") .bucket(minIOConfigProperties.getBucket()).stream(inputStream,inputStream.available(),-1) .build(); minioClient.putObject(putObjectArgs); StringBuilder urlPath = new StringBuilder(minIOConfigProperties.getReadPath()); urlPath.append(separator+minIOConfigProperties.getBucket()); urlPath.append(separator); urlPath.append(filePath); return urlPath.toString(); }catch (Exception ex){ log.error("minio put file error.",ex); throw new RuntimeException("上传文件失败"); } } /** * 上传html文件 * @param prefix 文件前缀 * @param filename 文件名 * @param inputStream 文件流 * @return 文件全路径 */ @Override public String uploadHtmlFile(String prefix, String filename,InputStream inputStream) { String filePath = builderFilePath(prefix, filename); try { PutObjectArgs putObjectArgs = PutObjectArgs.builder() .object(filePath) .contentType("text/html") .bucket(minIOConfigProperties.getBucket()).stream(inputStream,inputStream.available(),-1) .build(); minioClient.putObject(putObjectArgs); StringBuilder urlPath = new StringBuilder(minIOConfigProperties.getReadPath()); urlPath.append(separator+minIOConfigProperties.getBucket()); urlPath.append(separator); urlPath.append(filePath); return urlPath.toString(); }catch (Exception ex){ log.error("minio put file error.",ex); ex.printStackTrace(); throw new RuntimeException("上传文件失败"); } } /** * 删除文件 * @param pathUrl 文件全路径 */ @Override public void delete(String pathUrl) { String key = pathUrl.replace(minIOConfigProperties.getEndpoint()+"/",""); int index = key.indexOf(separator); String bucket = key.substring(0,index); String filePath = key.substring(index+1); // 删除Objects RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket(bucket).object(filePath).build(); try { minioClient.removeObject(removeObjectArgs); } catch (Exception e) { log.error("minio remove file error. pathUrl:{}",pathUrl); e.printStackTrace(); } } /** * 下载文件 * @param pathUrl 文件全路径 * @return 文件流 * */ @Override public byte[] downLoadFile(String pathUrl) { String key = pathUrl.replace(minIOConfigProperties.getEndpoint()+"/",""); int index = key.indexOf(separator); String bucket = key.substring(0,index); String filePath = key.substring(index+1); InputStream inputStream = null; try { inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(minIOConfigProperties.getBucket()).object(filePath).build()); } catch (Exception e) { log.error("minio down file error. pathUrl:{}",pathUrl); e.printStackTrace(); } ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byte[] buff = new byte[100]; int rc = 0; while (true) { try { if (!((rc = inputStream.read(buff, 0, 100)) > 0)) break; } catch (IOException e) { e.printStackTrace(); } byteArrayOutputStream.write(buff, 0, rc); } return byteArrayOutputStream.toByteArray(); } }
使用
- 依赖
<dependency> <groupId>com.heima</groupId> <artifactId>heima-file-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
- 配置
minio: accessKey: minio secretKey: minio123 bucket: leadnews endpoint: http://192.168.174.133:9000 readPath: http://192.168.174.133:9000
- 使用
@SpringBootTest(classes = MinIOApplication.class) @RunWith(SpringRunner.class) public class MinIOTest { @Autowired private FileStorageService fileStorageService; @Test public void test() throws FileNotFoundException { FileInputStream fileInputStream = new FileInputStream("C:\\Users\\cen\\Desktop\\list.html"); String filePath = fileStorageService.uploadHtmlFile("", "list.html", fileInputStream); System.out.println(filePath); } }
文章详情实现
- 依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>com.heima</groupId> <artifactId>heima-file-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>
- 配置
minio: accessKey: minio secretKey: minio123 bucket: leadnews endpoint: http://192.168.174.133:9000 readPath: http://192.168.174.133:9000
- 模板文件
<!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/vant@2.12.20/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> <!-- 评论Popup 弹出层 --> <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"> <!-- TODO: js计算时间差 --> <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> <!-- 引入 Vue 和 Vant 的 JS 文件 --> <script src=" https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"> </script> <script src="https://cdn.jsdelivr.net/npm/vant@2.12.20/lib/vant.min.js"></script> <!-- 引入 Axios 的 JS 文件 --> <#--<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>
- 实现
@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; @Test public void createStaticUrlTest() throws IOException, TemplateException { // 1. 获取文章内容 ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, 1302862387124125698L)); if(apArticleContent != null && StringUtils.isNotBlank(apArticleContent.getContent())) { // 2. 文章内容通过freemarker生成html文件 Template template = configuration.getTemplate("article.ftl"); // 数据模型 Map<String, Object> content = new HashMap<>(); content.put("content", JSONArray.parseArray(apArticleContent.getContent())); StringWriter out = new StringWriter(); // 合成 template.process(content, out); // 3. 把html上传到minio中 InputStream in = new ByteArrayInputStream(out.toString().getBytes()); String path = fileStorageService.uploadHtmlFile("", apArticleContent.getArticleId()+".html", in); // 4. 修改ap_article表, 保存static_url字段 apArticleService.update(Wrappers.<ApArticle>lambdaUpdate().eq(ApArticle::getId, apArticleContent.getArticleId()) .set(ApArticle::getStaticUrl, path)); } } }
来源
黑马程序员. 黑马头条