黑马头条项目(二):APP查看文章

一. 加载文章列表

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 &lt; #{dto.minBehotTime}
            </if>

            <if test="loadType != null and loadType == 2">
                and aa.publish_time &gt; #{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);
        }
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值