携手创作,共同成长!这是我参与「掘金日新计划 · 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供应商我知道的有高德,百度,腾讯三家,腾讯这方面不是很出名我就直接忽略了,相比较于百度,高德的注册直接支付宝扫码就可以了更加的方便,最主要的原因是用高德地图比较习惯
登录之后右上角进入控制台, 在我的应用这里创建应用
新建好了之后直接 添加 - 填入名称 - 选择web - 同意就OK了
相对应的,在我的应用, 我们新建的test应用这里就会出现key名称为test的, 直接复制后面的字符串就可以了
创建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安排我
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结果如下
五、总结
目前简略的来讲还差文件模块, 但是实际上后端要完成的东西还有很多
接下来要做的
- 文件模块对接七牛云
- 前端项目创建
- 后台管理页面大体框架
- 完善后台管理页面
- 后台管理页面对接后端接口
- 后端接口测试及修改
- 后台登录页面
- 前端展示页面大体框架
- 完善前端展示页面
- 后端接口测试及修改