个人博客(8、增删改查-2)

153 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情

一、前言

在之前的文章中完成了博客的大体架构、整合了jwt和redis,完成了简单模块的单表增删改查,本章主要完成了ip获取功能,同时对接高德api获取ip所在城市,为后续前端页面展示使用,完成了文章管理和评论管理

二、ip获取与高德api对接

HttpUtils工具类封装用到的依赖

<!--        http依赖-->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version> 4.5.13</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
            <version> 4.4.12</version>
        </dependency>

HttpUtils详情

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * 请求工具类
 *
 * @author ningxuan
 */
public class HttpUtils {

        /**
         * 方法描述: 发送get请求
         *
         * @param url
         * @param params
         * @param header
         * @Return {@link String}
         * @throws
         * @date 2020年07月27日 09:10:10
         */
        public static String sendGet(String url, Map<String, String> params, Map<String, String> header) throws Exception {
            HttpGet httpGet = null;
            String body = "";
            try {
                CloseableHttpClient httpClient = HttpClients.createDefault();
                List<String> mapList = new ArrayList<>();
                if (params != null) {
                    for (Entry<String, String> entry : params.entrySet()) {
                        mapList.add(entry.getKey() + "=" + entry.getValue());
                    }
                }
                if (CollectionUtils.isNotEmpty(mapList)) {
                    url = url + "?";
                    String paramsStr = StringUtils.join(mapList, "&");
                    url = url + paramsStr;
                }
                httpGet = new HttpGet(url);
                httpGet.setHeader("Content-type", "application/json; charset=utf-8");
                httpGet.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
                if (header != null) {
                    for (Entry<String, String> entry : header.entrySet()) {
                        httpGet.setHeader(entry.getKey(), entry.getValue());
                    }
                }
                HttpResponse response = httpClient.execute(httpGet);

                int statusCode = response.getStatusLine().getStatusCode();
                if (statusCode != HttpStatus.SC_OK) {
                    throw new RuntimeException("请求失败");
                } else {
                    body = EntityUtils.toString(response.getEntity(), "UTF-8");
                }
            } catch (Exception e) {
                throw e;
            } finally {
                if (httpGet != null) {
                    httpGet.releaseConnection();
                }
            }
            return body;
        }

        /**
         * 方法描述: 发送post请求-json数据
         *
         * @param url
         * @param json
         * @param header
         * @Return {@link String}
         * @throws
         * @date 2020年07月27日 09:10:54
         */
        public static String sendPostJson(String url, String json, Map<String, String> header) throws Exception {
            HttpPost httpPost = null;
            String body = "";
            try {
                CloseableHttpClient httpClient = HttpClients.createDefault();
                httpPost = new HttpPost(url);
                httpPost.setHeader("Content-type", "application/json; charset=utf-8");
                httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
                if (header != null) {
                    for (Entry<String, String> entry : header.entrySet()) {
                        httpPost.setHeader(entry.getKey(), entry.getValue());
                    }
                }
                StringEntity entity = new StringEntity(json, Charset.forName("UTF-8"));
                entity.setContentEncoding("UTF-8");
                entity.setContentType("application/json");
                httpPost.setEntity(entity);
                HttpResponse response = httpClient.execute(httpPost);

                int statusCode = response.getStatusLine().getStatusCode();
                if (statusCode != HttpStatus.SC_OK) {
                    throw new RuntimeException("请求失败");
                } else {
                    body = EntityUtils.toString(response.getEntity(), "UTF-8");
                }
            } catch (Exception e) {
                throw e;
            } finally {
                if (httpPost != null) {
                    httpPost.releaseConnection();
                }
            }
            return body;
        }

        /**
         * 方法描述: 发送post请求-form表单数据
         *
         * @param url
         * @param params
         * @param header
         * @Return {@link String}
         * @throws
         * @date 2020年07月27日 09:10:54
         */
        public static String sendPostForm(String url, Map<String, String> params, Map<String, String> header) throws Exception {
            HttpPost httpPost = null;
            String body = "";
            try {
                CloseableHttpClient httpClient = HttpClients.createDefault();
                httpPost = new HttpPost(url);
                httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
                httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
                if (header != null) {
                    for (Entry<String, String> entry : header.entrySet()) {
                        httpPost.setHeader(entry.getKey(), entry.getValue());
                    }
                }
                List<NameValuePair> nvps = new ArrayList<>();
                if (params != null) {
                    for (Entry<String, String> entry : params.entrySet()) {
                        nvps.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
                    }
                }
                //设置参数到请求对象中
                httpPost.setEntity(new UrlEncodedFormEntity(nvps, "UTF-8"));
                HttpResponse response = httpClient.execute(httpPost);

                int statusCode = response.getStatusLine().getStatusCode();
                if (statusCode != HttpStatus.SC_OK) {
                    throw new RuntimeException("请求失败");
                } else {
                    body = EntityUtils.toString(response.getEntity(), "UTF-8");
                }
            } catch (Exception e) {
                throw e;
            } finally {
                if (httpPost != null) {
                    httpPost.releaseConnection();
                }
            }
            return body;
        }

}

获取高德api接口

国内可选地图api供应商我知道的有高德,百度,腾讯三家,腾讯这方面不是很出名我就直接忽略了,相比较于百度,高德的注册直接支付宝扫码就可以了更加的方便,最主要的原因是用高德地图比较习惯

登录之后右上角进入控制台, 在我的应用这里创建应用

image.png

新建好了之后直接 添加 - 填入名称 - 选择web - 同意就OK了

image.png

相对应的,在我的应用, 我们新建的test应用这里就会出现key名称为test的, 直接复制后面的字符串就可以了

image.png

创建SendHttpUtils二次封装http请求

import com.alibaba.fastjson.JSONObject;
import com.ningxuan.blog.common.exception.BlogException;
import com.ningxuan.blog.common.exception.ErrorEnum;
import com.ningxuan.blog.model.AccessIp;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

/**
 * 快捷请求
 *
 * @author ningxuan
 */
@Component
@Slf4j
public class SendHttpUtils {

    private String gaodeKey;

    @Value("${gaode.key}")
    public void setGaodeKey(String gaodeKey) {
        this.gaodeKey = gaodeKey;
    }

    /**
     * 根据ip地址获取省份城市等信息
     *
     * @param ipAddr
     * @return
     */
    public AccessIp getIpInfo(String ipAddr) {
        Map<String, String> param = new HashMap<>();
        param.put("key", gaodeKey);
        param.put("ip", ipAddr);
        Map<String, String> header = new HashMap<>();
        String result;
        try {
            result = HttpUtils.sendGet(HttpConstant.GAODE_IP, param, header);
        } catch (Exception e) {
            log.error("第三方接口请求失败");
            throw new BlogException(ErrorEnum.API_ERROR);
        }
        JSONObject jsonObject = JSONObject.parseObject(result);
        AccessIp accessIp = jsonObject.toJavaObject(AccessIp.class);
        return accessIp;

        /*  返回值
            {
                "status":"1",
                "info":"OK",
                "infocode":"10000",
                "province":"北京市",
                "city":"北京市",
                "adcode":"110000",
                "rectangle":"116.0119343,39.66127144;116.7829835,40.2164962"
            }
         */
    }

}

新建常量类HttpConstant保存所有的url地址

/**
 * http请求api地址类
 * @author ningxuan
 */
public interface HttpConstant {
    /**
     * 高德获取ip位置信息url
     */
    String GAODE_IP="https://restapi.amap.com/v3/ip";


}

三、文章管理

service层如下

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ningxuan.blog.model.Article;
import com.ningxuan.blog.model.dto.ArticleDto;
import com.ningxuan.blog.model.dto.ArticleListDto;
import com.ningxuan.blog.model.vo.ArticleVo;

/**
 * <p>
 * 文章信息表 服务类
 * </p>
 *
 * @author NingXuan
 * @since 2022-08-07
 */
public interface IArticleService extends IService<Article> {
    /**
     * 新增文章
     * @param dto
     */
    void insertArticle(ArticleDto dto);

    /**
     * 按要求查找文章列表
     * @param page
     * @return
     */
    Page<ArticleVo> getList(ArticleListDto page);

    /**
     * 查看文章
     * @param id
     * @return
     */
    ArticleVo getArticle(Long id);

    /**
     * 修改文章
     * @param dto
     */
    void updateArticle(ArticleDto dto);
}

impl层如下

里面有一些待完善的地方和自己的想法也都写在里面了

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ningxuan.blog.common.redis.RedisKeys;
import com.ningxuan.blog.common.redis.RedisUtils;
import com.ningxuan.blog.mapper.ArticleMapper;
import com.ningxuan.blog.model.Article;
import com.ningxuan.blog.model.ArticleInfo;
import com.ningxuan.blog.model.ArticleTag;
import com.ningxuan.blog.model.dto.ArticleDto;
import com.ningxuan.blog.model.dto.ArticleListDto;
import com.ningxuan.blog.model.vo.ArticleVo;
import com.ningxuan.blog.service.IArticleInfoService;
import com.ningxuan.blog.service.IArticleService;
import com.ningxuan.blog.service.IArticleTagService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

/**
 * <p>
 * 文章信息表 服务实现类
 * </p>
 *
 * @author NingXuan
 * @since 2022-08-07
 */
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements IArticleService {

    @Resource
    private IArticleInfoService articleInfoService;
    @Resource
    private IArticleTagService tagService;
    @Resource
    private ArticleMapper mapper;
    @Resource
    private RedisUtils redisUtils;

    @Override
    public void insertArticle(ArticleDto dto) {
        //TODO 增加事务
        // 文章信息表
        Article article = new Article();
        BeanUtils.copyProperties(dto,article);
        article.setReadNum(0);
        article.setCommentNum(0);
        article.setThumbUpNum(0);
        article.setCreateTime(LocalDateTime.now());
        article.setUpdateTime(LocalDateTime.now());
        article.setStatus("0");
        save(article);
        // 主键默认在article中
        Long  articleId = article.getBlid();
        // 文章内容表新增
        ArticleInfo articleInfo = new ArticleInfo();
        BeanUtils.copyProperties(dto, articleInfo);
        articleInfo.setCreateTime(LocalDateTime.now());
        articleInfo.setArticleId(articleId);
        articleInfoService.save(articleInfo);
        // 标签中间表新增
        List<ArticleTag> tagList = new ArrayList<>();
        dto.getLabelIdList().forEach(l -> {
            ArticleTag label = new ArticleTag();
            BeanUtils.copyProperties(l, label);
            label.setCreateTime(LocalDateTime.now());
            label.setArticleId(articleId);
            label.setLabelId(l);
            tagList.add(label);
        });
        tagService.saveBatch(tagList);
    }

    @Override
    public Page<ArticleVo> getList(ArticleListDto dto) {
        Page<ArticleVo> page = new Page<>(dto.getPage(), dto.getSize());
        Page<ArticleVo> list = mapper.getList(page,dto);

        return list ;

    }

    @Override
    public ArticleVo getArticle(Long id) {
        // 先查redis 再查数据库
        String articleInfoKey = RedisKeys.getArticleInfoKey(id);
        Object o = redisUtils.get(articleInfoKey);
        ArticleVo article = null;
        // 如果数据不在redis中
        if (o == null){
            article = mapper.getOne(id);
            // 不管数据是否存在于数据库, 都查出来放到redis中, 如果不存在, 设置过期时间为一天
            if (article != null ){
                redisUtils.set(articleInfoKey, article);
            }else{
                redisUtils.set(articleInfoKey, article, 60*60*24);
            }
        }else{
            // 数据存在于redis中则强转
            article = (ArticleVo) o;
        }

        //TODO 可以先把id放入布隆过滤器, 先查布隆过滤器再查数据库, 若数据库没有则存入redis值为null, 存入布隆过滤器
        return article;
    }

    @Override
    public void updateArticle(ArticleDto dto) {
        // 修改文章信息表
        Article article = new Article();
        BeanUtils.copyProperties(dto, article);
        this.updateById(article);
        // 修改标签中间表 删掉旧的, 生成新的
        if (dto.getLabelIdList() != null){
            LambdaQueryWrapper<ArticleTag> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(ArticleTag::getArticleId, dto.getBlid());
            tagService.remove(wrapper);
            List<ArticleTag> list = new ArrayList<>();
            dto.getLabelIdList().forEach( l -> {
                ArticleTag tag = new ArticleTag();
                tag.setArticleId(article.getBlid());
                tag.setLabelId(l);
                list.add(tag);
            });
            tagService.saveBatch(list);
        }
    }
}

mapper层

分页查询和单个查询是用mybatis做的, 还没测试

<resultMap id="articleListMap" type="com.ningxuan.blog.model.vo.ArticleVo">
    <id property="articleId" column="articleId"></id>
    <result property="categoryName" column="categoryName"></result>
    <result property="title" column="title"></result>
    <result property="imageUrl" column="image_url"></result>
    <result property="summary" column="summary"></result>
    <result property="readNum" column="read_num"></result>
    <result property="commentNum" column="comment_num"></result>
    <result property="thumbUpNum" column="thumb_up_num"></result>
    <result property="createTime" column="create_time"></result>
    <result property="isTop" column="is_top"></result>
    <result property="reprintUrl" column="is_reprint_url"></result>
    <result property="isReprint" column="is_reprint"></result>
    <collection property="labelList">
        <id property="blid" column="labelId"></id>
        <result property="tagName" column="tag_name"></result>
    </collection>
</resultMap>


<select id="getList" resultMap="articleListMap">
    select ba.blid articleId, ba.title, ba.image_url, ba.summary, ba.read_num, ba.comment_num, ba.thumb_up_num, ba.create_time, ba.is_top, ba.is_reprint_url, ba.is_reprint,
            bc.name categoryName,
            bl.tag_name tagName,bl.blid LabelId
    from blogs_article ba
    left join blogs_category bc on ba.category_id = bc.blid
    left join blogs_article_tag bat on ba.blid = bat.article_id
    left join blogs_label bl on bl.id = bat.label_id
    <where>
        1=1
        <if test="isTop != null">
           and  ba.is_top = 1
        </if>
        <if test="categoryType != null">
            bc.blid = #{dto.catrgoryType}
        </if>
        <if test="labelType != null">
            bat.label_id = #{dto.labelType}
        </if>
        <if test="dto.keyword != null">
            and (
                ba.name like concat('%', #{dto.keyword}, '%')
                or
                ba.summary like concat('%', #{dto.keyword}, '%')
            )
        </if>
        <if test="dto.startTime != null nad dto.endTime != null">
            and ba.create_time between(#{dto.startTime}, #{dto.endTime})
        </if>
    </where>
    order by ba.create_time
    <if test="isThumbUp != null">
        , ba.thumb_up_num
    </if>
    <if test="isRead != null">
        , ba.read_num
    </if>
    <if test="isComment != null">
        , ba.comment_num
    </if>
</select>

四、评论管理

这个相对比较简单,没啥东西, 就直接写在了Controller层了

Controller层

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ningxuan.blog.model.ArticleComment;
import com.ningxuan.blog.model.base.PageVo;
import com.ningxuan.blog.model.base.ResultVo;
import com.ningxuan.blog.service.IArticleCommentService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;

/**
 * <p>
 * 文章评论表 前端控制器
 * </p>
 *
 * @author NingXuan
 * @since 2022-08-07
 */
@RestController
@RequestMapping("/comment")
@Api(tags = "评论模块")
public class ArticleCommentController {

    @Resource
    private IArticleCommentService commentService;

    @PostMapping("list")
    @ApiOperation("查看列表")
    public ResultVo getList(PageVo page){
        //TODO 简单完成了评论列表, 按照想法来应该是二级列表的
        Long id = Long.getLong(page.getKeyword());
        LambdaQueryWrapper<ArticleComment> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ArticleComment::getArticleId,id);
        List<ArticleComment> list = commentService.list(wrapper);
        return new ResultVo(list);
    }


    @PostMapping
    @ApiOperation("新增")
    public ResultVo insert(@RequestBody ArticleComment comment){
        comment.setCreateTime(LocalDateTime.now());
        comment.setStatus("0");
        comment.setThumbUpNum(0);
        commentService.save(comment);
        return new ResultVo();
    }

    @DeleteMapping
    @ApiOperation("删除")
    public ResultVo delete(Long id){
        commentService.removeById(id);
        return new ResultVo();
    }
}

五、分类模块

分类之前搞了一次,但是想完善一下二级列表,结构直接出bug了,目前还没得解决

mapper层

查看列表的时候, 想要使用mybatisd resultMap进行参数的接收,但是一直会报参数类型不匹配的错误, 还没找到问题出在哪里

<resultMap id="getListMap" type="com.ningxuan.blog.model.vo.ArticleCommonListVo">
    <id property="blid" column="blid"></id>
    <result property="createTime" column="create_time"></result>
    <result property="commentText" column="comment_text"></result>
    <result property="thumbUpNum" column="thumb_up_num"></result>
    <collection property="commonList" javaType="com.ningxuan.blog.model.ArticleComment">
        <id property="blid" column="blid"></id>
        <result property="createTime" column="create_time"></result>
        <result property="commentText" column="comment_text"></result>
        <result property="thumbUpNum" column="thumb_up_num"></result>
    </collection>
</resultMap>


<select id="getList" resultType="com.ningxuan.blog.model.vo.ArticleCommonListVo">
    select blid, create_time,thumb_up_num
    from blogs_article_comment bac1
    left join (select * from )

</select>

model层

新建了一个vo类,想接收的时候直接接受一级分类和二级分类, 但是直接bug安排我

image.png

Error attempting to get column 'parentTypeName' from result set.  Cause: java.sql.SQLDataException: Cannot determine value type from string '后端'

查询SQL如下

select bc2.blid parentBlid, bc2.blid parentId, bc2.type_name parentTypeName,bc2.num parentNum, bc1.blid, bc1.type_name,bc1.num
 FROM blogs_category bc1
 RIGHT JOIN (select *
 FROM blogs_category
 WHERE parent_id is null ) bc2
 ON bc1.parent_id = bc2.blid
 WHERE bc1.parent_id is not null;

表结构如下

CREATE TABLE `blogs_category` (
  `blid` bigint(20) NOT NULL,
  `type_name` varchar(255) DEFAULT NULL COMMENT '类型名称',
  `num` int(11) unsigned zerofill DEFAULT NULL COMMENT '该分类下文章数量',
  `status` varchar(255) DEFAULT '0' COMMENT '状态',
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父类id',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `creator` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`blid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章分类表';

SQL结果如下

image.png

五、总结

目前简略的来讲还差文件模块, 但是实际上后端要完成的东西还有很多

接下来要做的

  • 文件模块对接七牛云
  • 前端项目创建
  • 后台管理页面大体框架
  • 完善后台管理页面
  • 后台管理页面对接后端接口
  • 后端接口测试及修改
  • 后台登录页面
  • 前端展示页面大体框架
  • 完善前端展示页面
  • 后端接口测试及修改