一. 加载文章列表
1. 接口文档:
2. 请求参数封装类ArticleHomeDto:
@Data
public class ArticleHomeDto {
// 最大时间
Date maxBehotTime;
// 最小时间
Date minBehotTime;
// 分页size
Integer size;
// 频道ID(对应article表中的channel_id)
String tag;
}
3. 业务层代码逻辑:
@Autowired
private ApArticleMapper apArticleMapper;
@Override
public ResponseResult load(Short loadType, ArticleHomeDto dto) {
// 当第一次打开客户端界面时,是没有相关参数的,前端传来的参数都会是空值或默认值
// 所以需要手动校验并设置参数
// 分页参数校验
Integer size = dto.getSize();
if (size == null || size == 0) {
size = 10;
}
// 时间校验
if (dto.getMinBehotTime() == null) {
dto.setMinBehotTime(new Date());
}
if (dto.getMaxBehotTime() == null) {
dto.setMaxBehotTime(new Date());
}
// 文章频道校验
if (StringUtils.isEmpty(dto.getTag())) {
dto.setTag(ArticleConstants.DEFAULT_TAG);
}
// 查询并返回数据
List<ApArticle> apArticles = apArticleMapper.loadArticleList(dto,loadType);
return ResponseResult.okResult(apArticles);
}
在这个Service层代码中,包括三层校验。之所以会进行校验,并且设置具体数值,是因为当用户第一次打开App界面时,前端传来的请求参数会为空值或默认值,所以我们需要进行校验,判断用户是否是第一次打开界面,并展示出对应的界面。分页参数中,我们默认展示10条数据,可根据需要进行修改;时间校验中,我们为用户展示当前时间的最新文章;频道校验中,我们对频道类型进行默认值设置。
public class ArticleConstants {
// 加载文章列表-加载类型:加载更多(上划)
public static final Short LOAD_TYPE_MORE = 1;
// 加载文章列表-加载类型:加载更新(下划)
public static final Short LOAD_TYPE_NEW = 2;
// 加载文章列表-频道Tag:默认值
public static final String DEFAULT_TAG = "__all__";
}
4. Mapper层代码逻辑:
List<ApArticle> loadArticleList(ArticleHomeDto dto, Short loadType);
这里我们传递两个参数,除了封装好的DTO之外,我们还传递一个加载类型loadType参数,目的是区分用户进行的是什么操作。我们在Controller层定义了3个方法,而3个方法使用一个接口方法,所以需要loadType来判断具体的执行方式。
@RestController
@RequestMapping("/api/v1/article")
public class ArticleHomeController {
@Autowired
private ApArticleService apArticleService;
// 主页加载
@PostMapping("/load")
public ResponseResult load(@RequestBody ArticleHomeDto dto) {
return apArticleService.load(ArticleConstants.LOAD_TYPE_MORE, dto);
}
// 加载更多(上划)
@PostMapping("/loadmore")
public ResponseResult loadMore(@RequestBody ArticleHomeDto dto) {
return apArticleService.load(ArticleConstants.LOAD_TYPE_MORE, dto);
}
// 更新加载(下划)
@PostMapping("/loadnew")
public ResponseResult loadNew(@RequestBody ArticleHomeDto dto) {
return apArticleService.load(ArticleConstants.LOAD_TYPE_NEW, dto);
}
}
我们来看XML中的动态SQL语句:
<resultMap id="resultMap" type="com.heima.model.article.pojo.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="loadType != null and loadType == 1">
and aa.publish_time < #{dto.minBehotTime}
</if>
<if test="loadType != null and loadType == 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>
我们定义一个resultMap,查询出的数据封装成对象后存入List。
前两个loadType用来判断我们上面提到的内容,第三个判断,如果不是我们设置好的默认值,则根据具体的channel_id来展示对应频道。
二. 查看文章详情
1. 实现方案:页面静态化
每次查看文章内容,只需加载Html静态页面即可,不需要请求服务端加载资源。我们需要提前读取文章内容,使用FreeMarker生成Html页面,将页面保存到MinIO中,当用户想要查看文章详情时,直接从MinIO加载即可,静态化可以帮助我们减轻服务器和数据库的压力。
2. 封装自定义启动器
(1)在yml中配置MinIO的属性:
# 配置MinIO
minio:
# Minio账号(默认)
access-key: minio
# Minio密码(默认)
secret-key: minio123
# Minio地址
endpoint: http://ip:port
# bucket桶名
bucket: leadnews
# 从Minio中加载文件时使用的路径
read-path: http://ip:port
(2)创建配置类绑定属性值:
@Data
// 绑定配置文件(yml文件)中的属性值
@ConfigurationProperties(prefix = "minio")
public class MinIOConfigProperties implements Serializable {
private String accessKey;
private String secretKey;
private String bucket;
private String endpoint;
private String readPath;
}
(3)构建MinIO客户端对象:
@Data
@Configuration
@EnableConfigurationProperties(MinIOConfigProperties.class)
// 条件注解,如果FileStorageService类运行时可以被JVM加载访问(当调用FileStorageService接口方法时),则启用自动配置组件
@ConditionalOnClass(FileStorageService.class)
public class MinIOConfig {
@Autowired
private MinIOConfigProperties minIOConfigProperties;
// 构建了MinIO客户端对象
@Bean
public MinioClient buildMinioClient() {
return MinioClient
.builder()
.credentials(minIOConfigProperties.getAccessKey(), minIOConfigProperties.getSecretKey())
.endpoint(minIOConfigProperties.getEndpoint())
.build();
}
}
(4)创建功能接口的实现类:
@Slf4j
@EnableConfigurationProperties(MinIOConfigProperties.class) // 使应用程序可以读取到MinIOConfigProperties文件中的minio属性值
@Import(MinIOConfig.class) // 使用MinIOConfig文件中的配置来配置组件
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上传文件失败. ",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上传文件失败. ",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删除文件失败, 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下载文件失败, 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();
}
}
3. 创建测试类存储文章至MinIO
在这里只测试一篇文章上传,使用文章的具体id进行查找到数据。
@SpringBootTest
@RunWith(SpringRunner.class)
public class ArticleContentTest {
@Autowired
private ApArticleContentMapper apArticleContentMapper;
@Autowired
private ApArticleMapper apArticleMapper;
@Autowired
private Configuration configuration;
@Autowired
private FileStorageService fileStorageService;
@Test
public void test() throws IOException, TemplateException {
// 需要上传文章的id
Long articleId = 1383827787629252610L;
// 获取文章内容
ApArticleContent content = apArticleContentMapper.selectOne(Wrappers.
<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, articleId));
if (content != null) {
// 根据模板文件article.ftl生成模板对象
Template template = configuration.getTemplate("article.ftl");
// 获取具体的文章内容
Map<String, Object> model = new HashMap<>();
// 数据库中content存储的是JSON数据
model.put("content", JSONArray.parseArray(content.getContent()));
// 上面获取到的模板对象和具体数据相互结合,变成了writer
// writer中存储着包含动态数据的HTML字符串
StringWriter writer = new StringWriter();
template.process(model, writer);
// 将字符串转换成字节数组
InputStream is = new ByteArrayInputStream(writer.toString().getBytes());
// 页面上传到MinIO后会返回URL地址
String url = fileStorageService.uploadHtmlFile("", articleId + ".html", is);
// 把html的url地址保存到数据库的ap_article表里
ApArticle article = new ApArticle();
article.setId(articleId);
article.setStaticUrl(url);
apArticleMapper.updateById(article);
}
}
}