写文章需要 三个接口:
-
获取所有文章类别
-
获取所有标签
-
发布文章
1和2
1. 所有文章分类
1.1 接口说明
接口url:/categorys
请求方式:GET
请求参数:无
返回数据:
{
"success":true,
"code":200,
"msg":"success",
"data":
[
{"id":1,"avatar":"/category/front.png","categoryName":"前端"},
{"id":2,"avatar":"/category/back.png","categoryName":"后端"},
{"id":3,"avatar":"/category/lift.jpg","categoryName":"生活"},
{"id":4,"avatar":"/category/database.png","categoryName":"数据库"},
{"id":5,"avatar":"/category/language.png","categoryName":"编程语言"}
]
}
这个比较简单,代码如下
CategoryController:
package com.example.blog.controller;
import com.example.blog.entity.Category;
import com.example.blog.service.CategoryService;
import com.example.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/categorys")
public class CategoryController
{
@Autowired
private CategoryService categoryService;
@GetMapping
public Result categories()
{
return categoryService.findAll();
}
}
CategoryServiceImpl:
package com.example.blog.service.impl;
import com.example.blog.dao.mapper.CategoryMapper;
import com.example.blog.entity.Category;
import com.example.blog.service.CategoryService;
import com.example.blog.vo.CategoryVo;
import com.example.blog.vo.Result;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class CategoryServiceImpl implements CategoryService
{
@Autowired
private CategoryMapper categoryMapper;
@Override
public Result findAll()
{
List<Category> categories = categoryMapper.selectList(null);
return Result.success(copyList(categories));
}
private List<CategoryVo> copyList(List<Category> categories)
{
List<CategoryVo> categoryVoList = new ArrayList<>();
for(Category category:categories)
{
categoryVoList.add(copy(category));
}
return categoryVoList;
}
private CategoryVo copy(Category category)
{
CategoryVo categoryVo = new CategoryVo();
BeanUtils.copyProperties(category,categoryVo);
return categoryVo;
}
}
所有文章标签:
2.1 接口说明
接口url:/tags
请求方式:GET
请求参数:无
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id": 5,
"tagName": "springboot"
},
{
"id": 6,
"tagName": "spring"
},
{
"id": 7,
"tagName": "springmvc"
},
{
"id": 8,
"tagName": "11"
}
]
}
TagsController:
package com.example.blog.controller;
import com.example.blog.service.TagService;
import com.example.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/tags")
public class TagsController
{
@Autowired
private TagService tagService;
@GetMapping
public Result findAll()
{
return tagService.findAll();
}
}
TagServiceImpl:
package com.example.blog.service.impl;
import com.baomidou.mybatisplus.extension.api.R;
import com.example.blog.dao.mapper.TagMapper;
import com.example.blog.entity.Tag;
import com.example.blog.service.TagService;
import com.example.blog.vo.Result;
import com.example.blog.vo.TagVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Service
public class TagServiceImpl implements TagService
{
@Autowired
private TagMapper tagMapper;
@Override
public Result findAll()
{
List<Tag> tags = tagMapper.selectList(null);
return Result.success(copyList(tags));
}
public List<TagVo> copyList(List<Tag> tags)
{
List<TagVo> tagVoList = new ArrayList<>();
for(Tag tag:tags)
{
tagVoList.add(copy(tag));
}
return tagVoList;
}
public TagVo copy(Tag tag)
{
TagVo tagVo = new TagVo();
BeanUtils.copyProperties(tag,tagVo);
return tagVo;
}
}
发布文章
接口url:/articles/publish
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
title | string | 文章标题 |
id | long | 文章id(编辑有值) |
body | object({content: "ww", contentHtml: "<p>ww</p>↵"}) | 文章内容 |
category | {id: 2, avatar: "/category/back.png", categoryName: "后端"} | 文章类别 |
summary | string | 文章概述 |
tags | [{id: 5}, {id: 6}] | 文章标签 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": {"id":12232323}
}
这里需要封装一个前端参数ArticleParam
package com.example.blog.vo.params;
import com.example.blog.vo.ArticleBodyVo;
import com.example.blog.vo.CategoryVo;
import com.example.blog.vo.TagVo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.util.List;
//发布文章前端所传递的参数
@Data
public class ArticleParam
{
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private ArticleBodyParam body;
private CategoryVo category;
private String summary;
private List<TagVo> tags;/*文章标签*/
private String title;
}
ArticleBodyParam:
package com.example.blog.vo.params;
import lombok.Data;
//发布文章所需要的前端参数 文章内容
//body: {content: "cxz", contentHtml: "<p>cxz</p>↵"}
@Data
public class ArticleBodyParam
{
private String content;
private String contentHtml;
}
返回的数据是文章的id,注意是一个对象
ArticleController:
package com.example.blog.controller;
import com.example.blog.service.ArticleService;
import com.example.blog.vo.Result;
import com.example.blog.vo.params.ArticleParam;
import com.example.blog.vo.params.PageParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RequestMapping("/articles")
@RestController
public class ArticleController
{
@Autowired
private ArticleService articleService;
@PostMapping("/publish")
public Result publish(@RequestBody ArticleParam articleParam)
{
return articleService.publish(articleParam);
}
}
该接口需要加入到登录拦截器中,不然获得到的用户的id可能为空,article的成员AuthorId也就赋值不了。
/*新增登录拦截器*/
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/comments/create/change")
.addPathPatterns("/articles/publish")
.excludePathPatterns("/login")
.excludePathPatterns("/register");
}
ArticleServiceImpl:
代码逻辑:
1.构建Article对象,并添加对应属性 2.作者id 从当前的登录用户获取 (该登录接口需要加入到登录拦截器中,不然获得到的id可能为空) 3.标签 要将标签加入到关联列表中(ms_article_tag),需要先插入文章到数据库中,让数据库生成文章id,再循环遍历插入到数据库表中 4.将文章内容存储到ms_article_body表中,插入之后还需要把BodyId返回给article对象,让article对象更新自己表中的BodyId
package com.example.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.blog.dao.mapper.ArticleBodyMapper;
import com.example.blog.dao.mapper.ArticleMapper;
import com.example.blog.dao.mapper.ArticleTagMapper;
import com.example.blog.dao.mapper.CategoryMapper;
import com.example.blog.dos.Archives;
import com.example.blog.entity.*;
import com.example.blog.service.*;
import com.example.blog.utils.UserThreadLocal;
import com.example.blog.vo.*;
import com.example.blog.vo.params.ArticleBodyParam;
import com.example.blog.vo.params.ArticleParam;
import com.example.blog.vo.params.ArticleTag;
import com.example.blog.vo.params.PageParams;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.sql.Date;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class ArticleServiceImpl implements ArticleService
{
@Autowired
private ArticleMapper articleMapper;
@Autowired
private TagService tagService;
@Autowired
private SysUserService sysUserService;
@Autowired
private ArticleBodyMapper articleBodyMapper;
@Autowired
private CategoryService categoryService;
@Autowired
private ArticleTagMapper articleTagMapper;
/**
* 文章发布
* @param articleParam
* @return
*/
@Override
public Result publish(ArticleParam articleParam)
{
/**
* 1.发布文章 目的是构建Article对象,并添加对应属性
* 2.作者id 从当前的登录用户获取 (该接口需要加入到登录拦截器中,不然获得到的id可能为空)
* 3.标签 要将标签加入到 关联列表中(ms_article_tag),需要先插入文章到数据库中,让数据库生成文章id
* 4.body 文章内容存储,插入之后还需要把articleBodyId返回给article对象
*
*/
//1.
Article article = new Article();
article.setWeight(Article.Article_Common);//是否置顶,设为默认不置顶
article.setViewCounts(0);
article.setTitle(articleParam.getTitle());
article.setSummary(articleParam.getSummary());//摘要
article.setCommentCounts(0);
article.setCreateDate(System.currentTimeMillis());
article.setCategoryId(articleParam.getCategory().getId());
//2.
SysUser sysUser = UserThreadLocal.get();
article.setAuthorId(sysUser.getId());
//3.
//文章插入到数据库之后,就会生成一个文章id
articleMapper.insert(article);
Long articleId = article.getId();
List<TagVo> tags = articleParam.getTags();
if (null != tags)
{
for(TagVo tagVo:tags)
{
ArticleTag articleTag = new ArticleTag();
articleTag.setTagId(tagVo.getId());
articleTag.setArticleId(articleId);
articleTagMapper.insert(articleTag);
}
}
//4.内容存储
ArticleBody articleBody = new ArticleBody();
ArticleBodyParam articlebodyParam = articleParam.getBody();
articleBody.setContent(articlebodyParam.getContent());
articleBody.setContentHtml(articlebodyParam.getContentHtml());
articleBody.setArticleId(articleId);
articleBodyMapper.insert(articleBody);
article.setBodyId(articleBody.getId());
//插入articleBodyId之后,需要进行article在数据库中的更新
articleMapper.updateById(article);
Map<String,String> map = new HashMap<>();
//这里也有精度损失的问题,需要在ArticleVo类中的id设置序列化
map.put("id",articleId.toString());
return Result.success(map);
}
}
AOP记录日志:
首先添加一个注解,加上此注解的方法就是一个切点方法
package com.example.blog.common.aop;
import java.lang.annotation.*;
//ElementType.TYPE代表可以放在类上面;ElementType.METHOD代表可以放在方法上
@Target({ElementType.TYPE, ElementType.METHOD})
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
@Retention(RetentionPolicy.RUNTIME)
//标记注解,用于描述其它类型的注解应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。
@Documented
public @interface LogAnnotation
{
String module() default "";
String operator() default "";
}
添加切面
package com.example.blog.common.aop;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.api.R;
import com.example.blog.utils.HttpContextUtils;
import com.example.blog.utils.IPUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
@Component
@Aspect //切面(通知+切点)
@Slf4j
public class LogAspect
{
//切点就是该注解,这个注解加在哪里,哪里就是切点
@Pointcut("@annotation(com.example.blog.common.aop.LogAnnotation)")
public void pt(){}
//环绕通知
@Around("pt()")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
long beginTime = System.currentTimeMillis();
//执行方法
Object result = joinPoint.proceed();
//执行时常
Long time = System.currentTimeMillis() - beginTime;
//保存在日志里
recordLog(joinPoint,time);
return result;
}
private void recordLog(ProceedingJoinPoint joinPoint, long time) {
//通过连接点可以获得MethodSignature,以及对应切点方法的详情(方法名、参数)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获得该切点方法
Method method = signature.getMethod();
//通过该方法获得该注解详情
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
log.info("=====================log start================================");
log.info("module:{}",logAnnotation.module());
log.info("operation:{}",logAnnotation.operator());
//请求的类名以及方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
log.info("request method:{}",className + "." + methodName + "()");
// //请求的参数
Object[] args = joinPoint.getArgs();
String params = JSON.toJSONString(args[0]);
log.info("params:{}",params);
//获取request 设置IP地址
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
log.info("ip:{}", IPUtils.getIpAddr(request));
log.info("execute time : {} ms",time);
log.info("=====================log end================================");
}
}
记录调用该接口的ip地址
package com.example.blog.utils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
public class HttpContextUtils
{
public static HttpServletRequest getHttpServletRequest()
{
return ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
}
}
package com.example.blog.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
/****
*
* Project Name:spring-boot-seckill
* <p>从http请求中获取ip地址<br>
*
* @ClassName: IPUtils
* @date 2019年1月3日 下午6:30:02
*
* @author youqiang.xiong
* @version 1.0
* @since
* @see
*/
public class IPUtils {
private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
/**
* 获取IP地址
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = null;
try {
ip = request.getHeader("x-forwarded-for");
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} catch (Exception e) {
logger.error("IPUtils ERROR ", e);
}
// 使用代理,则获取第一个IP地址
if (StringUtils.isEmpty(ip) && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
return ip;
}
}
比如我需要查询文章列表的日志:
/**
* 分页查询 article数据库表
* @param pageParams
* @return
*/
@Override
//加上此注解 代表要对此接口记录日志
@LogAnnotation(module="文章",operator="获取文章列表")
public Result listArticle(PageParams pageParams)
{
Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();/*查询器*/
queryWrapper.orderByDesc(Article::getWeight);/*是否置顶进行排序*/
queryWrapper.orderByDesc(Article::getCreateDate);/*根据创建时间进行降序排序 order by create_date desc*/
Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);/*等同于编写一个普通list查询,mybatis-plus自动替你分页*/
List<Article> records = articlePage.getRecords();
List<ArticleVo> articleVoList = copyList(records,true,true);
return Result.success(articleVoList);
}
修正文章归档bug;
ArticleServiceImpl:
/**
* 文章归档
* @return
*/
@Override
public Result listArchives()
{
/**
* 对每年每月的文章进行分组,并统计它们的数量
*/
/* select year(create_date) as year,month(create_date) as month,count(*) as count from ms_article
group by year,month*/
List<Archives> archivesList = articleMapper.listArchives();
return Result.success(archivesList);
}
articleMapper.xml: 后台用的是System.currentTimeMillis()方法设置的时间,从数据库获取需要FROM_UNIXTIME(create_date/1000,'%Y')获取对应年份,月份(毫秒值除以1000,也就是获取它的秒)
<?xml version="1.0" encoding="UTF-8" ?>
<!--MyBatis配置文件-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.blog.dao.mapper.ArticleMapper">
<select id="listArchives" resultType="com.example.blog.dos.Archives">
select FROM_UNIXTIME(create_date/1000,'%Y') as year, FROM_UNIXTIME(create_date/1000,'%m') as month,count(*) as count from ms_article group by year,month
</select>
</mapper>