博客后台模块续更(三)
四、后台模块-动态路由
实现了这个动态路由功能之后,就能在浏览器web页面登录进博客管理后台了
1. 接口分析
后台系统需要能实现不同的用户权限可以看到不同的功能,即左侧的导航栏
请求方式 | 请求地址 | 请求头 |
GET | /getRouters | 需要token请求头 |
响应格式如下: 前端为了实现动态路由的效果,需要后端有接口能返回用户所能访问的菜单数据。注意: 返回的菜单数据需要体现父子菜单的层级关系
如果用户id为1代表管理员,menus中需要有所有菜单类型为C或者M的,C表示菜单,M表示目录,状态为正常的,未被删除的权限
注意这里不返回F的原因是按钮不属于菜单管理,所以接口不用返回
响应体如下:
{"code":200,"data":{"menus":[{"children":[],"component":"content/article/write/index","createTime":"2022-01-08 11:39:58","icon":"build","id":2023,"menuName":"写博文","menuType":"C","orderNum":"0","parentId":0,"path":"write","perms":"content:article:writer","status":"0","visible":"0"},{"children":[{"children":[],"component":"system/user/index","createTime":"2021-11-12 18:46:19","icon":"user","id":100,"menuName":"用户管理","menuType":"C","orderNum":"1","parentId":1,"path":"user","perms":"system:user:list","status":"0","visible":"0"},{"children":[],"component":"system/role/index","createTime":"2021-11-12 18:46:19","icon":"peoples","id":101,"menuName":"角色管理","menuType":"C","orderNum":"2","parentId":1,"path":"role","perms":"system:role:list","status":"0","visible":"0"},{"children":[],"component":"system/menu/index","createTime":"2021-11-12 18:46:19","icon":"tree-table","id":102,"menuName":"菜单管理","menuType":"C","orderNum":"3","parentId":1,"path":"menu","perms":"system:menu:list","status":"0","visible":"0"}],"createTime":"2021-11-12 18:46:19","icon":"system","id":1,"menuName":"系统管理","menuType":"M","orderNum":"1","parentId":0,"path":"system","perms":"","status":"0","visible":"0"}]},"msg":"操作成功"
}
2. 代码实现
第一步: 在huanf-framework工程的vo目录新建RoutersVo类,写入如下,负责把指定字段返回给前端
package com.keke.domain.vo;import com.keke.domain.entity.Menu;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Date;
import java.util.List;@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoutersVo {List<Menu> menus;
}
第二步: 把keke-framework工程的Menu类修改为如下,增加了children字段(成员变量)、增加了
@Accessors(chain = true)注解
package com.keke.domain.entity;import java.util.Date;
import java.io.Serializable;
import java.util.List;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.experimental.Accessors;/*** 菜单权限表(Menu)表实体类** @author makejava* @since 2023-10-18 20:55:24*/
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@TableName("sys_menu")
public class Menu {//菜单IDprivate Long id;//菜单名称private String menuName;//父菜单IDprivate Long parentId;//显示顺序private Integer orderNum;//路由地址private String path;//组件路径private String component;//是否为外链(0是 1否)private Integer isFrame;//菜单类型(M目录 C菜单 F按钮)private String menuType;//菜单状态(0显示 1隐藏)private String visible;//菜单状态(0正常 1停用)private String status;//权限标识private String perms;//菜单图标private String icon;//创建者private Long createBy;//创建时间private Date createTime;//更新者private Long updateBy;//更新时间private Date updateTime;//备注private String remark;private String delFlag;private List<Menu> children;}
第三步: 把keke-framework工程的MenuService接口修改为如下,增加了查询用户的路由信息(权限菜单)的接口
package com.keke.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.entity.Menu;import java.util.List;/*** 菜单权限表(Menu)表服务接口** @author makejava* @since 2023-10-18 20:55:48*/
public interface MenuService extends IService<Menu> {//查询用户权限信息List<String> selectPermsByUserId(Long userId);//查询用户的路由信息,也就是权限菜单List<Menu> selectRouterMenuTreeByUserId(Long userId);
}
第四步: 把keke-framework工程的MenuServiceImpl类修改为如下,增加了查询用户的路由信息(权限菜单)的具体代码
package com.keke.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.entity.Menu;
import com.keke.mapper.MenuMapper;
import com.keke.service.MenuService;
import com.keke.utils.SecurityUtils;
import org.springframework.stereotype.Service;import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;/*** 菜单权限表(Menu)表服务实现类** @author makejava* @since 2023-10-18 20:55:48*/
@Service("menuService")
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {//根据用户id查询权限关键字@Overridepublic List<String> selectPermsByUserId(Long userId) {//如果用户id为1代表管理员,roles 中只需要有admin,// permissions中需要有所有菜单类型为C(菜单)或者F(按钮)的,状态为正常的,未被删除的权限if(SecurityUtils.isAdmin()) {LambdaQueryWrapper<Menu> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.in(Menu::getMenuType, SystemConstants.MENU, SystemConstants.BUTTON);lambdaQueryWrapper.eq(Menu::getStatus, SystemConstants.STATUS_NORMAL);//由于我们的逻辑删除字段已经配置了,所以无需封装lambdaQueryWrapperList<Menu> menuList = list(lambdaQueryWrapper);//我们需要的是String类型的集合,这里我们要进行数据的处理,这里采用流的方式List<String> permissions = menuList.stream().map(new Function<Menu, String>() {@Overridepublic String apply(Menu menu) {String perms = menu.getPerms();return perms;}}).collect(Collectors.toList());return permissions;}//否则返回这个用户所具有的权限//这里我们需要进行连表查询,因为我们的用户先和角色关联,然后角色才跟权限关联MenuMapper menuMapper = getBaseMapper();//我们期望menuMapper中有一个方法可以直接帮我们去实现这个复杂的逻辑,这里直接返回return menuMapper.selectPermsByUserId(userId);}@Overridepublic List<Menu> selectRouterMenuTreeByUserId(Long userId) {MenuMapper menuMapper = getBaseMapper();List<Menu> menus = null;//如果是管理员,返回所有if(SecurityUtils.isAdmin()){menus = menuMapper.selectAllRoutersMenu();}else {//如果不是管理员,返回对应用户的菜单menus = menuMapper.selectRoutersMenuTreeByUserId(userId);}//因为上面的查询都是从数据库进行查询,所以无法封装children,这里构建TreeList<Menu> menuTree = buildMenuTree(menus,0L);return menuTree;}/*** 构建MenuTree* 思路先找第一层级的菜单,就是找到id于parentId的对应关系,然后把parentId设置为Id的children* @param menus* @return*/private List<Menu> buildMenuTree(List<Menu> menus,Long parentId) {//转化流处理List<Menu> menuTree = menus.stream()//过滤掉除一级菜单之外的菜单.filter(menu -> menu.getParentId().equals(parentId))//然后将获取其子菜单设置到children字段,并返回.map(m -> m.setChildren(gerChildren(m, menus))).collect(Collectors.toList());return menuTree;}//获取当前菜单的子菜单private List<Menu> gerChildren(Menu menu, List<Menu> menus) {//流处理,遍历每一个流对象,筛选出流对象的parentId=menu的id,即过滤List<Menu> children = menus.stream().filter(m -> m.getParentId().equals(menu.getId()))//这里其实不必要写,这一步的逻辑是如果有三级,//可以把流对象中再过筛选出子菜单设置给对应的children并返回.map(m -> m.setChildren(gerChildren(m,menus))).collect(Collectors.toList());return children;}
}
第五步: 把keke-framework工程的MenuMapper接口修改为如下,增加了2个(一个查超级管理员,另一个查普通用户)查询权限菜单的接口
package com.keke.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.keke.domain.entity.Menu;import java.util.List;/*** 菜单权限表(Menu)表数据库访问层** @author makejava* @since 2023-10-18 20:55:48*/
public interface MenuMapper extends BaseMapper<Menu> {//Mapper的实现类对应xml映射文件List<String> selectPermsByUserId(Long userId);List<Menu> selectAllRoutersMenu();List<Menu> selectRoutersMenuTreeByUserId(Long userId);}
第六步: 把keke-framework工程的resources/mapper目录下的MenuMapper.xml文件修改为如下,是查询权限菜单的具体代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.keke.mapper.MenuMapper"><select id="selectPermsByUserId" resultType="java.lang.String">
-- 这里的逻辑是先用userId连表查询roleId,再用roleId连表查询menuId,再根据menuId
-- 查询对应的用户权限selectDISTINCT m.`perms`from `sys_user_role` urleft join `sys_role_menu` rm on ur.`role_id`=rm.`role_id`left join `sys_menu` m on m.`id`=rm.`menu_id`whereur.`user_id`=#{userId} andm.`menu_type` in ('C','F') andm.`status`=0 andm.`del_flag`=0</select><select id="selectAllRoutersMenu" resultType="com.keke.domain.entity.Menu">
-- 这里与上面的sql差不多,只是menu_type有差别,还有查询的字段个数SELECTDISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status,IFNULL(m.perms,'') AS perms, m.is_frame, m.menu_type, m.icon, m.order_num, m.create_timeFROM`sys_menu` mWHERE
-- 查询所有的,所以不需要加userId的条件m.`menu_type` IN ('C','M') ANDm.`status` = 0 ANDm.`del_flag` = 0ORDER BYm.parent_id,m.order_num</select><select id="selectRoutersMenuTreeByUserId" resultType="com.keke.domain.entity.Menu">
-- 这里与上面的sql差不多SELECTDISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status,IFNULL(m.perms,'') AS perms, m.is_frame, m.menu_type, m.icon, m.order_num, m.create_timeFROM`sys_user_role` urLEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`WHEREur.`user_id` = #{userId} ANDm.`menu_type` IN ('C','M') ANDm.`status` = 0 ANDm.`del_flag` = 0ORDER BYm.parent_id,m.order_num</select></mapper>
第七步: 把keke-admin工程的LoginController类修改为如下,增加了查询路由信息(权限菜单)的接口
package com.keke.controller;import com.keke.annotation.KekeSystemLog;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.Menu;
import com.keke.domain.entity.User;
import com.keke.domain.vo.AdminUserInfoVo;
import com.keke.domain.vo.RoutersVo;
import com.keke.domain.vo.UserInfoVo;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.handler.exception.exception.SystemException;
import com.keke.service.MenuService;
import com.keke.service.RoleService;
import com.keke.service.SystemLoginService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.SecurityUtils;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestController
@Api(tags = "后台用户登录相关接口")
public class LoginController {@Autowiredprivate SystemLoginService systemLoginService;@Autowiredprivate MenuService menuService;@Autowiredprivate RoleService roleService;@PostMapping("/user/login")@KekeSystemLog(businessName = "后台用户登录")public ResponseResult login(@RequestBody User user){if(!StringUtils.hasText(user.getUserName())){//提示必须要传用户名throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);}return systemLoginService.login(user);}@GetMapping("getInfo")public ResponseResult<AdminUserInfoVo> getInfo(){//获取当前登录用户,用我们封装的SecurityUtilsLoginUser loginUser = SecurityUtils.getLoginUser();//根据用户id查询权限信息Long userId = loginUser.getUser().getId();List<String> permissions = menuService.selectPermsByUserId(userId);//根据用户id查询角色信息List<String> roles = roleService.selectRoleKeyByUserId(userId);//获取userInfo信息User user = loginUser.getUser();UserInfoVo userInfoVo = BeanCopyUtils.copyBean(user, UserInfoVo.class);//创建Vo,封装返回AdminUserInfoVo adminUserInfoVo = new AdminUserInfoVo(permissions,roles,userInfoVo);return ResponseResult.okResult(adminUserInfoVo);}@GetMapping("/getRouters")public ResponseResult<RoutersVo> getRouters(){//获取用户idLong userId = SecurityUtils.getUserId();//查询menu,结果是tree的形式List<Menu> menus = menuService.selectRouterMenuTreeByUserId(userId);//封装返回RoutersVo routersVo = new RoutersVo(menus);return ResponseResult.okResult(routersVo);}}
第八步: 测试
打开redis,启动前端工程,登录后可以看到左侧菜单路由展示出来了,以及一些权限按钮
这里启动前台工程,测试可能是不通过的,可以参见以下问题
AdminUserInfoVo的变量名是否与要求返回的字段一致
前端工程中store目录下的modules目录下的permission.js文件中处理子路由的时候push写成了psuh,导致路由不能渲染。看看你浏览器控制台有没有报psuh的错误,有的话就是这里的问题。
补充
如果测试失误,可以把token删除,然后重新登录测试
五、后台模块-退出登录
1. 接口分析
删除redis中的用户信息
请求方式 | 请求地址 | 请求头 |
POST | /user/logout | 需要token请求头 |
{"code": 200,"msg": "操作成功"
}
2. 代码实现
第一步: 把keke-framework工程的SystemLoginService接口修改为如下,增加了退出登录的接口
package com.keke.service;import com.keke.domain.ResponseResult;
import com.keke.domain.entity.User;public interface SystemLoginService {//后台用户登录ResponseResult login(User user);//后台用户退出登录ResponseResult logout();}
第二步: 把keke-framework工程的SystemLoginServiceImpl类修改为如下,增加了退出登录的具体代码
package com.keke.service.impl;import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.User;
import com.keke.service.BlogLoginService;
import com.keke.service.SystemLoginService;
import com.keke.utils.JwtUtil;
import com.keke.utils.RedisCache;
import com.keke.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;import java.util.HashMap;
import java.util.Map;
import java.util.Objects;@Service
public class SystemLoginServiceImpl implements SystemLoginService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;@Overridepublic ResponseResult login(User user) {UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);//authenticationManager会默认调用UserDetailsService从内存中进行用户认证,我们实际需求是从数据库,因此我们要重新创建一个UserDetailsService的实现类//判断是否认证通过if(Objects.isNull(authenticate)){throw new RuntimeException("用户名或者密码错误");}//获取Userid,生成tokenLoginUser loginUser = (LoginUser) authenticate.getPrincipal();String userId = loginUser.getUser().getId().toString();String jwt = JwtUtil.createJWT(userId);//把用户信息存入redisredisCache.setCacheObject("login:" + userId,loginUser);//把token和userInfo封装返回,因为响应回去的data有这两个属性,所以要封装VoMap<String,String> systemLoginVo = new HashMap<>();systemLoginVo.put("token",jwt);return ResponseResult.okResult(systemLoginVo);}@Overridepublic ResponseResult logout() {//删除redis中的登录信息Long userId = SecurityUtils.getUserId();redisCache.deleteObject("login:" + userId);return ResponseResult.okResult();}
}
第三步: 那keke-admin工程的LoginController类修改为如下,增加了退出登录的访问接口
package com.keke.controller;import com.keke.annotation.KekeSystemLog;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.Menu;
import com.keke.domain.entity.User;
import com.keke.domain.vo.AdminUserInfoVo;
import com.keke.domain.vo.RoutersVo;
import com.keke.domain.vo.UserInfoVo;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.handler.exception.exception.SystemException;
import com.keke.service.MenuService;
import com.keke.service.RoleService;
import com.keke.service.SystemLoginService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.SecurityUtils;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestController
@Api(tags = "后台用户登录相关接口")
public class LoginController {@Autowiredprivate SystemLoginService systemLoginService;@Autowiredprivate MenuService menuService;@Autowiredprivate RoleService roleService;@PostMapping("/user/login")@KekeSystemLog(businessName = "后台用户登录")public ResponseResult login(@RequestBody User user){if(!StringUtils.hasText(user.getUserName())){//提示必须要传用户名throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);}return systemLoginService.login(user);}@GetMapping("getInfo")public ResponseResult<AdminUserInfoVo> getInfo(){//获取当前登录用户,用我们封装的SecurityUtilsLoginUser loginUser = SecurityUtils.getLoginUser();//根据用户id查询权限信息Long userId = loginUser.getUser().getId();List<String> permissions = menuService.selectPermsByUserId(userId);//根据用户id查询角色信息List<String> roles = roleService.selectRoleKeyByUserId(userId);//获取userInfo信息User user = loginUser.getUser();UserInfoVo userInfoVo = BeanCopyUtils.copyBean(user, UserInfoVo.class);//创建Vo,封装返回AdminUserInfoVo adminUserInfoVo = new AdminUserInfoVo(permissions,roles,userInfoVo);return ResponseResult.okResult(adminUserInfoVo);}@GetMapping("/getRouters")public ResponseResult<RoutersVo> getRouters(){//获取用户idLong userId = SecurityUtils.getUserId();//查询menu,结果是tree的形式List<Menu> menus = menuService.selectRouterMenuTreeByUserId(userId);//封装返回RoutersVo routersVo = new RoutersVo(menus);return ResponseResult.okResult(routersVo);}@PostMapping("/user/logout")public ResponseResult logout(){return systemLoginService.logout();}}
第四步: 测试
本地打开你的redis
本地打开postman
运行后台工程
postman测试
启动前端工程,在前端工程中测试如下
退出登录成功,回到登录页面
六、后台模块-标签列表
1. 查询标签
1.1 标签表的字段
1.2 接口分析
为了方便后期对文章进行管理,需要提供标签的功能,一个文章可以有多个标签。在后台需要分页查询标签功能,要求能根据标签名进行分页查询对应的文章
注意:不能把删除了的标签查询出来。除了可以根据标签名查询文章,后期还要添加根据备注名查询文章
请求方式 | 请求路径 |
Get | content/tag/list |
请求参数
Query格式请求参数:pageNum: 页码pageSize: 每页条数name:标签名remark:备注
响应格式
{"code":200,"data":{"rows":[{"id":4,"name":"Java","remark":"sdad"}],"total":1},"msg":"操作成功"
}
1.3 代码实现
第一步: 在keke-framework工程的src/main/java目录新建com.huanf.dto.TagListDto类,写入如下
package com.keke.domain.dto;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Date;@Data
@NoArgsConstructor
@AllArgsConstructor
public class TagListDto {//标签名private String name;//备注private String remark;
}
第二步, 在keke-framework工程的domain/vo目录下创建TagVo,写入如下,主要作用是响应正确
package com.keke.domain.vo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Date;@Data
@AllArgsConstructor
@NoArgsConstructor
public class TagVo {//idprivate Long id;//标签名private String name;//备注private String remark;
}
第三步: 在keke-framework工程的service目录新建TagService接口,写入如下,用于查询标签
package com.keke.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;/*** 标签(Tag)表服务接口** @author makejava* @since 2023-10-18 10:21:06*/
public interface TagService extends IService<Tag> {ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto);}
第四步: 在huanf-framework工程的service目录新建TagServiceImpl类,写入如下,是查询标签的具体代码,模糊+分页查询标签代码
package com.keke.service.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.keke.domain.ResponseResult;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.domain.vo.TagVo;
import com.keke.mapper.TagMapper;
import com.keke.service.TagService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;import java.util.List;/*** 标签(Tag)表服务实现类** @author makejava* @since 2023-10-18 10:21:07*/
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {@Overridepublic ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto) {//封装lambdaQueryWrapperLambdaQueryWrapper<Tag> lambdaQueryWrapper = new LambdaQueryWrapper<>();//根据标签名(如果有)模糊查询lambdaQueryWrapper.eq(StringUtils.hasText(tagListDto.getName()),Tag::getName,tagListDto.getName());//根据备注(如果有)模糊查询lambdaQueryWrapper.eq(StringUtils.hasText(tagListDto.getRemark()),Tag::getRemark,tagListDto.getRemark());//创建mp分页器,设置参数Page<Tag> page = new Page(pageNum,pageSize);//lambdaQueryWrapper条件和page传入pagepage(page,lambdaQueryWrapper);//得到分页数据List<Tag> tags = page.getRecords();//得到页数据总数long total = page.getTotal();//bean拷贝为标准响应格式List<TagVo> tagVos = BeanCopyUtils.copyBeanList(tags, TagVo.class);//封装PageVoPageVo pageVo = new PageVo(tagVos,total);//响应返回return ResponseResult.okResult(pageVo);}
}
第四步: 在keke-admin工程的controller目录新建TagController类,写入如下,是查询标签的访问接口
package com.keke.controller;import com.keke.domain.ResponseResult;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.service.TagService;
import io.swagger.annotations.Api;
import io.swagger.models.auth.In;
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;import java.util.List;@RestController
@Api(tags = "后台标签相关接口")
public class TagController {@Autowiredprivate TagService tagService;@GetMapping("/test")public ResponseResult test(){List<Tag> list = tagService.list();return ResponseResult.okResult(list);}@GetMapping("/content/tag/list")public ResponseResult<PageVo> list(Integer pageNum, Integer pageSize, TagListDto tagListDto){return tagService.pageTagList(pageNum,pageSize,tagListDto);}}
第五步:测试
本地打开你的redis
本地打开postman
运行后台启动类,先登录,拿token去测接口,如下
前端工程
2. 新增标签
1.1 接口分析
点击标签管理的新增按钮可以实现新增标签的功能
请求方式 | 请求地址 | 请求头 |
POST | /content/tag | 需要token请求头 |
请求体:
{
"name":"标签名",
"remark":"标签的备注名"
}
响应体:
{"code":200,"msg":"操作成功"
}
2.2 代码实现
第一步: 确保你在keke-framework工程handler/mybatisplus有MyMetaObjectHandler类,并且写入了如下,作用是配置是mybatisplus的字段自增(这步我们在前台工程中已经写过)
package com.keke.handler.mybatisplus;import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.keke.utils.SecurityUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;import java.util.Date;@Component
//这个类是用来配置mybatis的字段自动填充。用于'发送评论'功能,由于我们在评论表无法对下面这四个字段进行插入数据(原因是前端在发送评论时,没有在
//请求体提供下面四个参数,所以后端在往数据库插入数据时,下面四个字段是空值),所有就需要这个类来帮助我们往下面这四个字段自动的插入值,
//只要我们更新了评论表的字段,那么无法插入值的字段就自动有值了
public class MyMetaObjectHandler implements MetaObjectHandler {@Override//只要对数据库执行了插入语句,那么就会执行到这个方法public void insertFill(MetaObject metaObject) {Long userId = null;try {//获取用户iduserId = SecurityUtils.getUserId();} catch (Exception e) {e.printStackTrace();userId = -1L;//如果异常了,就说明该用户还没注册,我们就把该用户的userid字段赋值d为-1}//自动把下面四个字段新增了值。this.setFieldValByName("createTime", new Date(), metaObject);this.setFieldValByName("createBy",userId , metaObject);this.setFieldValByName("updateTime", new Date(), metaObject);this.setFieldValByName("updateBy", userId, metaObject);}@Overridepublic void updateFill(MetaObject metaObject) {this.setFieldValByName("updateTime", new Date(), metaObject);this.setFieldValByName(" ", SecurityUtils.getUserId(), metaObject);}
}
第二步: 在keke-framework工程的domain目录修改Tag类,写入如下,注意有四个字段是使用了mybatisplus的字段自增
package com.keke.domain.entity;import java.util.Date;
import java.io.Serializable;import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;/*** 标签(Tag)表实体类** @author makejava* @since 2023-10-18 10:20:44*/
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("ke_tag")
public class Tag {private Long id;//标签名private String name;//由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充//所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值@TableField(fill = FieldFill.INSERT)private Long createBy;//由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充//所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值@TableField(fill = FieldFill.INSERT)private Date createTime;//由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充//所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值@TableField(fill = FieldFill.INSERT_UPDATE)private Long updateBy;//由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充//所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值@TableField(fill = FieldFill.INSERT)private Date updateTime;//删除标志(0代表未删除,1代表已删除)private Integer delFlag;//备注private String remark;}
第三步: 在keke-framework工程新建AddTagDto类,写入如下,用于接收前端传过来的参数
package com.keke.domain.dto;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class AddTagDto {//标签名private String name;//备注private String remark;
}
第四步: 把keke-admin工程的TagController类,添加如下,作用是新增标签功能的访问接口
package com.keke.controller;import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.service.TagService;
import io.swagger.annotations.Api;
import io.swagger.models.auth.In;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.parameters.P;
import org.springframework.web.bind.annotation.*;import java.util.List;@RestController
@Api(tags = "后台标签相关接口")
@RequestMapping("/content/tag")
public class TagController {@Autowiredprivate TagService tagService;@GetMapping("/test")public ResponseResult test(){List<Tag> list = tagService.list();return ResponseResult.okResult(list);}@GetMapping("/list")public ResponseResult<PageVo> list(Integer pageNum, Integer pageSize, TagListDto tagListDto){return tagService.pageTagList(pageNum,pageSize,tagListDto);}@PostMappingpublic ResponseResult addTag(@RequestBody AddTagDto addTagDto){return tagService.addTag(addTagDto);}
}
第五步:在keke-framework的service/TagService中新增如下
package com.keke.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;/*** 标签(Tag)表服务接口** @author makejava* @since 2023-10-18 10:21:06*/
public interface TagService extends IService<Tag> {ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto);ResponseResult addTag(AddTagDto addTagDto);
}
第六步:在keke-framework的service/impl/TagServiceImpl中实现具体逻辑
package com.keke.service.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.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.domain.vo.TagVo;
import com.keke.mapper.TagMapper;
import com.keke.service.TagService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;import java.util.List;/*** 标签(Tag)表服务实现类** @author makejava* @since 2023-10-18 10:21:07*/
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {@Autowiredprivate TagService tagService;@Overridepublic ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto) {//封装lambdaQueryWrapperLambdaQueryWrapper<Tag> lambdaQueryWrapper = new LambdaQueryWrapper<>();//根据标签名(如果有)模糊查询lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getName()),Tag::getName,tagListDto.getName());//根据备注(如果有)模糊查询lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getRemark()),Tag::getRemark,tagListDto.getRemark());//创建mp分页器,设置参数Page<Tag> page = new Page(pageNum,pageSize);//lambdaQueryWrapper条件和page传入pagepage(page,lambdaQueryWrapper);//得到分页数据List<Tag> tags = page.getRecords();//得到页数据总数long total = page.getTotal();//bean拷贝为标准响应格式List<TagVo> tagVos = BeanCopyUtils.copyBeanList(tags, TagVo.class);//封装PageVoPageVo pageVo = new PageVo(tagVos,total);//响应返回return ResponseResult.okResult(pageVo);}@Overridepublic ResponseResult addTag(AddTagDto addTagDto) {Tag tag = BeanCopyUtils.copyBean(addTagDto, Tag.class);tagService.save(tag);return ResponseResult.okResult();}
}
第七步:测试
打开前端工程
新增一条标签点击确定
可以看到新增成功
3. 删除标签
3.1 接口分析
例如content/tag/6 代表删除id为6的标签数据。删除后在列表中是否查看不到该条数据,但是数据库中该条数据还是存在的,只是修改了逻辑删除字段的值
请求方式 | 请求地址 | 请求头 |
DELETE | /content/tag/{id} | 需要token请求头 |
请求方式可以看到是PathVariable形式
响应体:
{"code":200,"msg":"操作成功"
}
3.2 代码实现
第一步:在keke-admin的controller包下TagController新增删除标签的接口
package com.keke.controller;import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.service.TagService;
import io.swagger.annotations.Api;
import io.swagger.models.auth.In;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.parameters.P;
import org.springframework.web.bind.annotation.*;import java.util.List;@RestController
@Api(tags = "后台标签相关接口")
@RequestMapping("/content/tag")
public class TagController {@Autowiredprivate TagService tagService;@GetMapping("/test")public ResponseResult test(){List<Tag> list = tagService.list();return ResponseResult.okResult(list);}@GetMapping("/list")public ResponseResult<PageVo> list(Integer pageNum, Integer pageSize, TagListDto tagListDto){return tagService.pageTagList(pageNum,pageSize,tagListDto);}@PostMappingpublic ResponseResult addTag(@RequestBody AddTagDto addTagDto){return tagService.addTag(addTagDto);}@DeleteMapping("/{id}")public ResponseResult deleteTagById(@PathVariable("id") Long id){return tagService.deleteTagById(id);}
}
第二步:在keke-framework的service包下TagService新增如下
package com.keke.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;/*** 标签(Tag)表服务接口** @author makejava* @since 2023-10-18 10:21:06*/
public interface TagService extends IService<Tag> {ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto);ResponseResult addTag(AddTagDto addTagDto);ResponseResult deleteTagById(Long id);
}
第三步:在keke-framework的service/impl包下TagServiceImpl写具体的删除标签逻辑
package com.keke.service.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.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.domain.vo.TagVo;
import com.keke.mapper.TagMapper;
import com.keke.service.TagService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;import java.util.List;/*** 标签(Tag)表服务实现类** @author makejava* @since 2023-10-18 10:21:07*/
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {@Autowiredprivate TagService tagService;@Overridepublic ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto) {//封装lambdaQueryWrapperLambdaQueryWrapper<Tag> lambdaQueryWrapper = new LambdaQueryWrapper<>();//根据标签名(如果有)模糊查询lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getName()),Tag::getName,tagListDto.getName());//根据备注(如果有)模糊查询lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getRemark()),Tag::getRemark,tagListDto.getRemark());//创建mp分页器,设置参数Page<Tag> page = new Page(pageNum,pageSize);//lambdaQueryWrapper条件和page传入pagepage(page,lambdaQueryWrapper);//得到分页数据List<Tag> tags = page.getRecords();//得到页数据总数long total = page.getTotal();//bean拷贝为标准响应格式List<TagVo> tagVos = BeanCopyUtils.copyBeanList(tags, TagVo.class);//封装PageVoPageVo pageVo = new PageVo(tagVos,total);//响应返回return ResponseResult.okResult(pageVo);}@Overridepublic ResponseResult addTag(AddTagDto addTagDto) {Tag tag = BeanCopyUtils.copyBean(addTagDto, Tag.class);tagService.save(tag);return ResponseResult.okResult();}@Overridepublic ResponseResult deleteTagById(Long id) {tagService.removeById(id);return ResponseResult.okResult();}}
第四步:测试,打开前端工程,删除一个标签,删除成功
4. 修改标签
4.1 接口分析
4.1.1 获取标签信息接口
①根据标签id来获取某一条标签的信息,当用户点击修改按钮时触发,展示在弹框里面。例如:content/tag/6 代表获取id为6的标签数据
请求方式 | 请求地址 | 请求头 |
GET | /content/tag/{id} | 需要token请求头 |
请求方式是PathVariable形式
响应体:
{"code":200,"data":{"id":4,"name":"标签名","remark":"标签的备注名"},"msg":"操作成功"
}
4.1.2 修改标签接口
请求方式 | 请求地址 | 请求头 |
PUT | /content/tag | 需要token请求头 |
请求体:
{
"id":7,"name":"标签名",
"remark":"标签的备注名"
}
响应体:
{"code":200,"msg":"操作成功"
}
4.2 代码实现
第一步:在keke-admin的controller包下的TagController新增两个接口
package com.keke.controller;import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.EditTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.service.TagService;
import io.swagger.annotations.Api;
import io.swagger.models.auth.In;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.parameters.P;
import org.springframework.web.bind.annotation.*;import java.util.List;@RestController
@RequestMapping("/content/tag")
@Api(tags = "后台标签相关接口")
public class TagController {@Autowiredprivate TagService tagService;@GetMapping("/test")public ResponseResult test(){List<Tag> list = tagService.list();return ResponseResult.okResult(list);}@GetMapping("/list")public ResponseResult<PageVo> list(Integer pageNum, Integer pageSize, TagListDto tagListDto){return tagService.pageTagList(pageNum,pageSize,tagListDto);}@PostMappingpublic ResponseResult addTag(@RequestBody AddTagDto addTagDto){return tagService.addTag(addTagDto);}@DeleteMapping("/{id}")public ResponseResult deleteTagById(@PathVariable("id") Long id){return tagService.deleteTagById(id);}@GetMapping("/{id}")public ResponseResult getTagInformation(@PathVariable("id") Long id){return tagService.getTagInformation(id);}@PutMappingpublic ResponseResult editTag(@RequestBody EditTagDto editTagDto){return tagService.editTag(editTagDto);}
}
第二步:在keke-framework的domain/dto下新增editTagDto
package com.keke.domain.dto;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class EditTagDto {//idprivate Long id;//标签名private String name;//备注private String remark;
}
第三步:在keke-framework的service/TagService下新增两个方法
package com.keke.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.EditTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;/*** 标签(Tag)表服务接口** @author makejava* @since 2023-10-18 10:21:06*/
public interface TagService extends IService<Tag> {ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto);ResponseResult addTag(AddTagDto addTagDto);ResponseResult deleteTagById(Long id);ResponseResult getTagInformation(Long id);ResponseResult editTag(EditTagDto editTagDto);
}
第四步:在keke-framework的service/impl/TagServiceImpl下添加具体逻辑
package com.keke.service.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.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.EditTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.domain.vo.TagVo;
import com.keke.mapper.TagMapper;
import com.keke.service.TagService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;import java.util.List;/*** 标签(Tag)表服务实现类** @author makejava* @since 2023-10-18 10:21:07*/
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {@Autowiredprivate TagService tagService;@Overridepublic ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto) {//封装lambdaQueryWrapperLambdaQueryWrapper<Tag> lambdaQueryWrapper = new LambdaQueryWrapper<>();//根据标签名(如果有)模糊查询lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getName()),Tag::getName,tagListDto.getName());//根据备注(如果有)模糊查询lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getRemark()),Tag::getRemark,tagListDto.getRemark());//创建mp分页器,设置参数Page<Tag> page = new Page(pageNum,pageSize);//lambdaQueryWrapper条件和page传入pagepage(page,lambdaQueryWrapper);//得到分页数据List<Tag> tags = page.getRecords();//得到页数据总数long total = page.getTotal();//bean拷贝为标准响应格式List<TagVo> tagVos = BeanCopyUtils.copyBeanList(tags, TagVo.class);//封装PageVoPageVo pageVo = new PageVo(tagVos,total);//响应返回return ResponseResult.okResult(pageVo);}@Overridepublic ResponseResult addTag(AddTagDto addTagDto) {Tag tag = BeanCopyUtils.copyBean(addTagDto, Tag.class);tagService.save(tag);return ResponseResult.okResult();}@Overridepublic ResponseResult deleteTagById(Long id) {tagService.removeById(id);return ResponseResult.okResult();}@Overridepublic ResponseResult getTagInformation(Long id) {Tag tag = tagService.getById(id);TagVo tagVo = BeanCopyUtils.copyBean(tag, TagVo.class);return ResponseResult.okResult(tagVo);}@Overridepublic ResponseResult editTag(EditTagDto editTagDto) {Tag tag = BeanCopyUtils.copyBean(editTagDto, Tag.class);tagService.updateById(tag);return ResponseResult.okResult();}
}
第五步,测试,打开前端工程,点击修改,展示出标签的信息,填入修改后的信息,点击确定,标签修改成功
七、后台模块-发布文章
需要提供写博文的功能,写博文时需要关联分类和标签。可以上传缩略图,也可以在正文中添加图片。文章可以直接发布,也可以保存到草稿箱
分析下来,这个发布文章的页面总共有四个接口
查询所有分类的接口
查询所有标签的接口
图片上传的接口
新增文章的接口
下面我们一一实现
1. 查询分类接口
1.1 接口分析
请求方式如下,注意: 无请求参数
请求方式 | 请求地址 | 请求头 |
GET | /content/category/listAllCategory | 需要token请求头 |
响应体:
{"code":200,"data":[{"description":"wsd","id":1,"name":"java"},{"description":"wsd","id":2,"name":"PHP"}],"msg":"操作成功"
}
2.2 代码实现
第一步: 把keke-framework工程的CategoryService接口修改为如下,增加了分页查询分类列表的接口
package com.keke.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Category;
import org.springframework.web.bind.annotation.RequestMapping;/*** 分类表(Category)表服务接口** @author makejava* @since 2023-10-10 20:42:22*/
public interface CategoryService extends IService<Category> {ResponseResult getCategoryList();//后台接口,查询所有文章分类ResponseResult listAllCategory();}
第二步: 把keke-framework工程的CategoryServiceImpl类修改为如下,增加了分页查询分类列表接口的具体代码实现
package com.keke.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Article;
import com.keke.domain.entity.Category;
import com.keke.domain.vo.AdminCategoryVo;
import com.keke.domain.vo.CategoryVo;
import com.keke.mapper.CategoryMapper;
import com.keke.service.ArticleService;
import com.keke.service.CategoryService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;/*** 分类表(Category)表服务实现类** @author makejava* @since 2023-10-10 20:42:22*/
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {@Autowiredprivate ArticleService articleService;@Overridepublic ResponseResult getCategoryList() {//查询文章表,状态已发布的文章,但是在CategoryService下,查询文章表,就要注入ArticleServiceLambdaQueryWrapper<Article> articleWrapper = new LambdaQueryWrapper<>();articleWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);List<Article> articleList = articleService.list(articleWrapper);//获取文章的分类id,并去重Set<Long> categoryIds = articleList.stream().map(article -> article.getCategoryId())//toSet可以去除重复的id.collect(Collectors.toSet());//查询分类表List<Category> categories = listByIds(categoryIds);//分类表中只获取正常状态非禁用的分类,用stream流过滤categories = categories.stream().filter(category -> SystemConstants.STATUS_NORMAL.equals(category.getStatus())).collect(Collectors.toList());//封装VoList<CategoryVo> categoryVos = BeanCopyUtils.copyBeanList(categories, CategoryVo.class);//封装到响应体中,因为有数据,所以要调用有参okResult(),把参数传进去return ResponseResult.okResult(categoryVos);}@Overridepublic ResponseResult listAllCategory() {LambdaQueryWrapper<Category> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.eq(Category::getStatus,SystemConstants.STATUS_NORMAL);List<Category> categoryList = list(lambdaQueryWrapper);List<AdminCategoryVo> adminCategoryVos = BeanCopyUtils.copyBeanList(categoryList, AdminCategoryVo.class);return ResponseResult.okResult(adminCategoryVos);}
}
第三步: 在keke-framework的domain/vo包下创建AdminCategoryVo,这个是后台中要用到的响应体
package com.keke.domain.vo;import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Date;@Data
@NoArgsConstructor
@AllArgsConstructor
public class AdminCategoryVo {private Long id;//分类名private String name;//描述private String description;}
第四步: 把keke-admin工程的CategoryController类修改为如下,增加了分页查询分类功能的访问接口
package com.keke.controller;import com.keke.domain.ResponseResult;
import com.keke.service.CategoryService;
import io.swagger.annotations.Api;
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("/content/category")
@Api(tags = "后台标签相关接口")
public class CategoryController {@Autowiredprivate CategoryService categoryService;@GetMapping("/listAllCategory")public ResponseResult listAllCategory(){return categoryService.listAllCategory();}
}
这里我们不测试,到4个接口写完统一测试
2. 查询标签接口
2.1 接口分析
请求方式如下,注意: 无请求参数
请求方式 | 请求地址 | 请求头 |
GET | /content/tag/listAllTag | 需要token请求头 |
响应体:
{"code":200,"data":[{"id":1,"name":"Mybatis"},{"id":4,"name":"Java"}],"msg":"操作成功"
}
2.2 代码实现
第一步,在keke-framework的domain/vo包下创建AdminTagVo类,用于专门在后台返回接口关于标签的字段
package com.keke.domain.vo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@AllArgsConstructor
@NoArgsConstructor
public class AdminTagVo {//idprivate Long id;//标签名private String name;
}
第二步:在keke-blog的TagController中新增查询所有标签的接口
package com.keke.controller;import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.EditTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.service.TagService;
import io.swagger.annotations.Api;
import io.swagger.models.auth.In;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.parameters.P;
import org.springframework.web.bind.annotation.*;import java.util.List;@RestController
@RequestMapping("/content/tag")
@Api(tags = "后台标签相关接口")
public class TagController {@Autowiredprivate TagService tagService;@GetMapping("/test")public ResponseResult test(){List<Tag> list = tagService.list();return ResponseResult.okResult(list);}@GetMapping("/list")public ResponseResult<PageVo> list(Integer pageNum, Integer pageSize, TagListDto tagListDto){return tagService.pageTagList(pageNum,pageSize,tagListDto);}@PostMappingpublic ResponseResult addTag(@RequestBody AddTagDto addTagDto){return tagService.addTag(addTagDto);}@DeleteMapping("/{id}")public ResponseResult deleteTagById(@PathVariable("id") Long id){return tagService.deleteTagById(id);}@GetMapping("/{id}")public ResponseResult getTagInformation(@PathVariable("id") Long id){return tagService.getTagInformation(id);}@PutMappingpublic ResponseResult editTag(@RequestBody EditTagDto editTagDto){return tagService.editTag(editTagDto);}//发布文章-查询标签接口@GetMapping("/listAllTag")public ResponseResult selectAllTag(){return tagService.selectAllTag();}
}
第三步:在keke-framework的service/TagService中新增方法
package com.keke.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.EditTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;/*** 标签(Tag)表服务接口** @author makejava* @since 2023-10-18 10:21:06*/
public interface TagService extends IService<Tag> {ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto);ResponseResult addTag(AddTagDto addTagDto);ResponseResult deleteTagById(Long id);ResponseResult getTagInformation(Long id);ResponseResult editTag(EditTagDto editTagDto);//写文章-查询所有标签接口ResponseResult selectAllTag();}
第四步:在keke-framework的service/impl包下TagServiceImpl中写具体逻辑
package com.keke.service.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.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.EditTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.AdminTagVo;
import com.keke.domain.vo.PageVo;
import com.keke.domain.vo.TagVo;
import com.keke.mapper.TagMapper;
import com.keke.service.TagService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;import java.util.List;/*** 标签(Tag)表服务实现类** @author makejava* @since 2023-10-18 10:21:07*/
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {@Autowiredprivate TagService tagService;@Overridepublic ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto) {//封装lambdaQueryWrapperLambdaQueryWrapper<Tag> lambdaQueryWrapper = new LambdaQueryWrapper<>();//根据标签名(如果有)模糊查询lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getName()),Tag::getName,tagListDto.getName());//根据备注(如果有)模糊查询lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getRemark()),Tag::getRemark,tagListDto.getRemark());//创建mp分页器,设置参数Page<Tag> page = new Page(pageNum,pageSize);//lambdaQueryWrapper条件和page传入pagepage(page,lambdaQueryWrapper);//得到分页数据List<Tag> tags = page.getRecords();//得到页数据总数long total = page.getTotal();//bean拷贝为标准响应格式List<TagVo> tagVos = BeanCopyUtils.copyBeanList(tags, TagVo.class);//封装PageVoPageVo pageVo = new PageVo(tagVos,total);//响应返回return ResponseResult.okResult(pageVo);}@Overridepublic ResponseResult addTag(AddTagDto addTagDto) {Tag tag = BeanCopyUtils.copyBean(addTagDto, Tag.class);tagService.save(tag);return ResponseResult.okResult();}@Overridepublic ResponseResult deleteTagById(Long id) {tagService.removeById(id);return ResponseResult.okResult();}@Overridepublic ResponseResult getTagInformation(Long id) {Tag tag = tagService.getById(id);TagVo tagVo = BeanCopyUtils.copyBean(tag, TagVo.class);return ResponseResult.okResult(tagVo);}@Overridepublic ResponseResult editTag(EditTagDto editTagDto) {Tag tag = BeanCopyUtils.copyBean(editTagDto, Tag.class);tagService.updateById(tag);return ResponseResult.okResult();}@Overridepublic ResponseResult selectAllTag() {//这里我们创建专门返回给前端的AdminTagVo,List<Tag> tagList = list();List<AdminTagVo> adminTagVos = BeanCopyUtils.copyBeanList(tagList, AdminTagVo.class);return ResponseResult.okResult(adminTagVos);}
}
3. 图片上传
3.1 接口分析
请求方式如下。请求参数是img,值为要上传的文件
请求方式 | 请求地址 | 请求头 |
POST | /upload | 需要token请求头 |
请求头:
Content-Type :multipart/form-data;
响应体:
{"code": 200,"data": "文件访问链接","msg": "操作成功"
}
3.2 代码实现
第一步: 把keke-admin工程的application.yml修改为如下,增加了OSS的相关配置
server:port: 8989spring:# 数据库连接信息datasource:url: jdbc:mysql://localhost:3306/keke_blog?characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword:driver-class-name: com.mysql.cj.jdbc.Driverservlet:# 文件上传multipart:# 单个上传文件的最大允许大小max-file-size: 20MB# HTTP请求中包含的所有文件的总大小的最大允许值max-request-size: 20MBmybatis-plus:# configuration:# # 日志# log-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:# 逻辑删除的字段logic-delete-field: delFlag# 代表已删除的值logic-delete-value: 1# 代表未删除的值logic-not-delete-value: 0# 主键自增策略,以mysql数据库为准id-type: autoOSS:accessKey: 这里填写你自己的accessKeysecretKey: 这里填写你自己的secretKeybucket: keke-blog
第二步: 在keke-admin工程的controller目录新建UploadController类,写入如下
package com.keke.controller;import com.keke.domain.ResponseResult;
import com.keke.handler.exception.exception.SystemException;
import com.keke.service.OssUploadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;@RestControllerpublic class UploadController {@Autowiredprivate OssUploadService ossUploadService;@PostMapping("/upload")public ResponseResult uploadImg(@RequestParam("img") MultipartFile multipartFile){try {return ossUploadService.uploadImg(multipartFile);}catch (Exception e){e.printStackTrace();throw new RuntimeException("文件上传失败");}}}
4. 新增文章
4.1 接口分析
请求方式如下:
请求方式 | 请求地址 | 请求头 |
POST | /content/article | 需要token请求头 |
请求体:
{"title":"测试新增博文","thumbnail":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/08/21/4ceebc07e7484beba732f12b0d2c43a9.png","isTop":"0","isComment":"0","content":"# 一级标题\n## 二级标题\n\n正文","tags":[1,4],"categoryId":1,"summary":"哈哈","status":"1"
}
响应体:
{"code":200,"msg":"操作成功"
}
4.2 代码实现
第一步,在keke-blog的controller中新建ArticleController写入写文章接口
package com.keke.controller;import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;@RestController
public class ArticleController {@Autowiredprivate ArticleService articleService;@PostMapping("/content/article")public ResponseResult add(@RequestBody AddArticleDto addArticleDto){return addArticleDto.add(addArticleDto);}
}
第二步: 在keke-framework工程的domain/dto目录新建AddArticleDto类,写入如下,用来接受前端传过来的参数,最重要的是tags属性
package com.keke.domain.dto;import com.baomidou.mybatisplus.annotation.TableField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Date;
import java.util.List;@Data
@NoArgsConstructor
@AllArgsConstructor
public class AddArticleDto {//标题private String title;//文章内容private String content;//文章摘要private String summary;//所属分类idprivate Long categoryId;//缩略图private String thumbnail;//是否置顶(0否,1是)private String isTop;//状态(0已发布,1草稿)private String status;//是否允许评论 1是,0否private String isComment;//关联标签id们List<Long> tags;
}
第三步: 在keke-framework工程的domain/entity目录新建ArticleTag类,写入如下,文章表&标签表的中间表,对应的实体类,并建立ke_article_tag表对应的service mapper serviceImpl类
package com.keke.domain.entity;import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;/*** 文章标签关联表(ArticleTag)表实体类** @author makejava* @since 2023-10-20 19:37:32*/
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("ke_article_tag")
public class ArticleTag {//文章idprivate Long articleId;//标签idprivate Long tagId;
}
package com.keke.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.keke.domain.entity.ArticleTag;/*** 文章标签关联表(ArticleTag)表数据库访问层** @author makejava* @since 2023-10-20 20:05:17*/
public interface ArticleTagMapper extends BaseMapper<ArticleTag> {}
package com.keke.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.entity.ArticleTag;/*** 文章标签关联表(ArticleTag)表服务接口** @author makejava* @since 2023-10-20 20:05:17*/
public interface ArticleTagService extends IService<ArticleTag> {}
package com.keke.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.domain.entity.ArticleTag;
import com.keke.mapper.ArticleTagMapper;
import com.keke.service.ArticleTagService;
import org.springframework.stereotype.Service;/*** 文章标签关联表(ArticleTag)表服务实现类** @author makejava* @since 2023-10-20 20:05:17*/
@Service("articleTagService")
public class ArticleTagServiceImpl extends ServiceImpl<ArticleTagMapper, ArticleTag> implements ArticleTagService {}
第四步: 在keke-framework工程的domain/entity的Article类修改为如下,注意有4个字段是使用了mybatisplus属性值自增的注解
package com.keke.domain.entity;import java.util.Date;
import java.io.Serializable;import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;/*** 文章表(Article)表实体类** @author makejava* @since 2023-10-10 10:06:59*/
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("ke_article")
@Accessors(chain = true)
public class Article {private Long id;//标题private String title;//文章内容private String content;//文章摘要private String summary;//所属分类idprivate Long categoryId;//分页查询文章列表时,新增的一个字段,为的是更好的封装,但是数据库中没有该字段,为了避免mp//在查询的时候查询这一列,可以添加如下注解@TableField(exist = false)//意思是这个字段在数据库表中实际上是不存在的private String categoryName;//缩略图private String thumbnail;//是否置顶(0否,1是)private String isTop;//状态(0已发布,1草稿)private String status;//访问量private Long viewCount;//是否允许评论 1是,0否private String isComment;@TableField(fill = FieldFill.INSERT)private Long createBy;//由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充//所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值@TableField(fill = FieldFill.INSERT)private Date createTime;//由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充//所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值@TableField(fill = FieldFill.INSERT_UPDATE)private Long updateBy;//由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充//所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值@TableField(fill = FieldFill.INSERT)private Date updateTime;//删除标志(0代表未删除,1代表已删除)private Integer delFlag;public Article(Long id,Long viewCount){this.id = id;this.viewCount = viewCount;}
}
第五步:在keke-framework的service的ArticleService接口新增方法
package com.keke.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.entity.Article;/*** 文章表(Article)表服务接口** @author makejava* @since 2023-10-10 09:59:37*/
public interface ArticleService extends IService<Article> {ResponseResult hotArticleList();ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId);ResponseResult getArticleDetail(Long id);ResponseResult updateViewCount(Long id);ResponseResult add(AddArticleDto addArticleDto);
}
第五步:在keke-framework的service/impl的ArticleServiceImpl写入具体逻辑代码
package com.keke.service.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.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.entity.Article;
import com.keke.domain.entity.ArticleTag;
import com.keke.domain.entity.Category;
import com.keke.domain.vo.ArticleDetailVo;
import com.keke.domain.vo.ArticleListVo;
import com.keke.domain.vo.HotArticleVo;
import com.keke.domain.vo.PageVo;
import com.keke.mapper.ArticleMapper;
import com.keke.service.ArticleService;
import com.keke.service.ArticleTagService;
import com.keke.service.CategoryService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;/*** 文章表(Article)表服务实现类** @author makejava* @since 2023-10-10 09:59:39*/
@Service("articleService")
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {@Autowiredprivate CategoryService categoryService;@Autowiredprivate ArticleService articleService;//显然redisCache是对redisTemplate的封装@Autowiredprivate RedisCache redisCache;@Autowiredprivate ArticleTagService articleTagService;//查询热门文章@Overridepublic ResponseResult hotArticleList() {//---------------------------------------每调用这个方法就从redis查询文章的浏览量,展示在热门文章列表--------------------------------------------------------------Map<String, Integer> viewCountMap = redisCache.getCacheMap(SystemConstants.REDIS_ARTICLE_KEY);List<Article> articleList = viewCountMap.entrySet().stream().map(entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue())).collect(Collectors.toList());articleService.updateBatchById(articleList);//-----------------------------------------------------------------------------------------------------//查询热门文章 封装成ResponseResult返回LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();//必须是正式文章lambdaQueryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);//按照浏览量进行排序lambdaQueryWrapper.orderByDesc(Article::getViewCount);//最多查询10条,设置mp分页对象的参数分别为1和10Page<Article> page = new Page<>(SystemConstants.ARTICLE_STATUS_CURRENT,SystemConstants.ARTICLE_STATUS_SIZE);//将page对象和lambdaQueryWrapper查询条件封装成pagepage(page,lambdaQueryWrapper);//page.getRecords()获取到所有符合条件的数据(也就是文章)List<Article> articles = page.getRecords();//BeanCopyList<HotArticleVo> hotArticleVos = BeanCopyUtils.copyBeanList(articles, HotArticleVo.class);//返回ResponseResult对象return ResponseResult.okResult(hotArticleVos);}//分页查询文章列表,包含首页和分类页面的文章列表分页查询@Overridepublic ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId) {//查询条件LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();//首先明确categoryId在首页中不传,在分类文章页面中会传,所以我们要判断/*这里是lambdaQueryWrapper.eq三个参数的写法,第一个参数返回值是boolean类型,如果判断为true那么后续就会把后面的判断加入sql语句当中*/lambdaQueryWrapper.eq(Objects.nonNull(categoryId)&&categoryId>0,Article::getCategoryId,categoryId);//状态是正式发布的,置顶的文章要显示在最前面,对isTop进行降序lambdaQueryWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL);lambdaQueryWrapper.orderByDesc(Article::getIsTop);//分页查询Page<Article> page = new Page<>(pageNum,pageSize);page(page,lambdaQueryWrapper);//查询categoryNameList<Article> articles = page.getRecords();//有categoryId,但无categoryName//拿着categoryId去查询categoryName,然后封装到article中/*for (Article article : articles) {Category category = categoryService.getById(article.getCategoryId());article.setCategoryName(category.getName());}*/articles.stream()//setter返回的是对象.map(article -> article.setCategoryName(categoryService.getById(article.getCategoryId()).getName())).collect(Collectors.toList());//封装VoList<ArticleListVo> articleListVos = BeanCopyUtils.copyBeanList(page.getRecords(), ArticleListVo.class);//封装PageVoPageVo pageVo = new PageVo(articleListVos,page.getTotal());return ResponseResult.okResult(pageVo);}//查询文章详情@Overridepublic ResponseResult getArticleDetail(Long id) {//根据id查询文章Article article = getById(id);
// //--------------------------------------从redis中获取viewCount--------------------------------------------------------------
// //获取到的是redis当中的Integer类型的viewCount
// Integer viewCount = redisCache.getCacheMapValue(SystemConstants.REDIS_ARTICLE_KEY, id.toString());
// //设置article的viewCount为从redis中查出来的数据
// article.setViewCount(viewCount.longValue());//转化称VoArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);//根据分类id查询分类名称Long categoryId = articleDetailVo.getCategoryId();Category category = categoryService.getById(categoryId);//如果没有获取到id,就不设置if(categoryId!=null){articleDetailVo.setCategoryName(category.getName());}//封装响应体return ResponseResult.okResult(articleDetailVo);}@Overridepublic ResponseResult updateViewCount(Long id) {//更新浏览量(自增)redisCache.incrementCacheMapValue(SystemConstants.REDIS_ARTICLE_KEY,id.toString(),SystemConstants.REDIS_ARTICLE_VIEW_COUNT_INCREMENT);return ResponseResult.okResult();}@Override//因为这里做了两次数据库插入操作,必须保证两个操作同时失败或者成功public ResponseResult add(AddArticleDto addArticleDto) {//添加到文章表Article article = BeanCopyUtils.copyBean(addArticleDto, Article.class);save(article);//添加到文章标签关联表List<Long> tags = addArticleDto.getTags();//拿到ArticleTag对象集合List<ArticleTag> articleTags = tags.stream().map(new Function<Long, ArticleTag>() {@Overridepublic ArticleTag apply(Long tagId) {ArticleTag articleTag = new ArticleTag(article.getId(), tagId);return articleTag;}}).collect(Collectors.toList());//然后批量插入数据库articleTagService.saveBatch(articleTags);return ResponseResult.okResult();}
}
5. 四接口测试
点击发布,发布成功后跳转至文章管理,但这个页面的接口我们还没有写,所以我们需要去数据库中查看刚才的操作是否成功
文章表新增成功
文章标签表新增成功
相关文章:

博客后台模块续更(三)
四、后台模块-动态路由 实现了这个动态路由功能之后,就能在浏览器web页面登录进博客管理后台了 1. 接口分析 后台系统需要能实现不同的用户权限可以看到不同的功能,即左侧的导航栏 请求方式 请求地址 请求头 GET /getRouters 需要token请求头 …...
第十二届蓝桥杯模拟赛第三期
A填空题 问题描述 请问在 1 到 2020 中,有多少个数与 2020 互质,即有多少个数与 2020 的最大公约数为 1。 参考答案 800 public class Main {public static void main(String[] args) {int ans0;for(int i1;i<2020;i) {if(gcd(2020,i)1) {ans;}}…...

2023年浙大MEM考前80天上岸经验分享
时间过得真快,转眼间已经是十月份了。回想起去年这个时候,我还在为考研而感到焦虑不安。然而,如今我已经在浙大MEM项目学习了一个多月的时间了。在这一个月的学习过程中,我不仅学到了许多专业知识,还结识了很多志同道合…...

增加并行度后,发现Flink窗口不会计算的问题。
文章目录 前言一、现象二、结论三、解决 前言 窗口没有关闭计算的问题,一直困扰了很久,经过多次验证,确定了问题的根源。 一、现象 Flink使用了window,同时使用了watermark ,并且还设置了较高的并行度。生产是设置了…...
使用 JMeter 和 Docker 进行服务存根
用于性能测试的服务存根:简介 随着测试项目的复杂性不断增加,越来越多的被测系统的测试流程受到依赖系统的影响。当我说“依赖系统”时,我指的是: 不受当前开发影响的遗留系统 属于另一个组织的第三方服务 您的组织开发的系统&am…...

《王道计算机考研——操作系统》学习笔记总目录+思维导图
本篇文章是对《王道计算机考研——操作系统》所有知识点的笔记总结归档和计算机网络的思维导图 学习视频:王道计算机考研 操作系统 408四件套【计网、计组、操作系统、数据结构】完整课堂PPT 思维导图 (求Star~):【王道考研】计…...

多模态及图像安全的探索与思考
前言 第六届中国模式识别与计算机视觉大会(The 6th Chinese Conference on Pattern Recognition and Computer Vision, PRCV 2023)已于近期在厦门成功举办。通过参加本次会议,使我有机会接触到许多来自国内外的模式识别和计算机视觉领域的研究…...

基础算法相关笔记
排序 最好情况下: 冒泡排序 最坏时间复杂度 O ( n 2 ) O(n^2) O(n2)。 插入排序 最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2),最优时间复杂度为 O ( n ) O(n) O(n)。 平均情况下: 快速排序 最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2)&…...

Mac电脑无法识别移动硬盘怎么办?
很多人都喜欢在Mac电脑上办公、学习,但有时我们将移动硬盘连接Mac电脑时,却会发现电脑无法识别移动硬盘。那么,Mac电脑无法识别移动硬盘怎么办呢? Mac无法识别移动硬盘的原因 导致Mac不识别移动硬盘的原因有很多,你可…...
14Maven与Tomcat面试题
1MAVEN Maven是一个基于项目对象模型(POM)的项目管理工具,它可以帮助开发者自动化构建、依赖管理、项目报告等。Maven通过一个中央信息片段能够管理项目的构建、报告和文档等步骤,同时也能够管理项目的依赖。Maven的核心概念是POM…...

会议OA小程序【首页布局】
目录 一. Flex布局介绍 1.1 什么是Flex布局 1.2 基本概念 1.3 Flex属性 二. 会议OA首页轮播图的实现 配置 Mock工具 swiper 效果展示 三. 会议OA首页会议信息布局 index.js index.wxml index.wxss 首页整体效果展示 一. Flex布局介绍 布局的传统解决方案&#x…...

高效表达三步
一、高效表达 高效表达定主题搭架子填素材 第一: 1个核心主题,让别人秒懂你的想法 (表达要定主题) 第二: 3种经典框架,帮你快速整理表达思路 第三: 2种表达素材,让发言更具说服力…...
怎样修改ESP32的CPU主频
ESP32的主频默认设置为160mhz,但ESP32最高可以跑到240mhz, 修改方法: idf.py menuconfig --> Component config --> ESP System Settings --> CPU frequency 可以看到三个选项,80,160, 240&…...

《视觉 SLAM 十四讲》V2 第 10 讲 后端优化2 简化BA 【位姿图】
文章目录 第10讲 后端210.1 滑动窗口滤波 和 优化10.1.2 滑动窗口法 10.2 位姿图10.3 实践: 位姿图优化本讲 CMakeLists.txt 10.3.1 g2o 原生位姿图 【Code】10.3.2 李代数上的位姿优化 【Code】 习题10题1 【没推完】 LaTex 第10讲 后端2 滑动窗口优化 位姿图优化…...

【斗破年番】再遭群嘲,美杜莎怀孕之事被魔改,三方联手除萧潇?
【侵权联系删除】【文/郑尔巴金】 斗破苍穹年番第67集已经更新了。和很多人一样,小郑也去看了,只是小郑万万没有想到,我满怀期待的去看这一集,这一集却能魔改成这样。魔改成什么样了呢?下面来分析下吧! 一&…...
字节面试题——计算机网络,附答案
1.TCP 三次握手和四次挥手 相关面试题: 计算机网络常见面试题总结(上) | JavaGuide(Java面试 学习指南) 为什么要三次握手?第 2 次握手传回了 ACK,为什么还要传回 SYN?为什么要四次挥手?为什么不能把服务器发送的 ACK 和 FIN…...

Flask Web 安装bootstrap失败pip install bootstrap
失败原因:网速太慢了 把公共wifi换成手机热点,成功:) 😃 更新:开了手机热点还是报下面的错,但是把科学上网关了,就成功了,反正就是网络问题...

可视化 | python可视化相关库梳理(自用)| pandas | Matplotlib | Seaborn | Pyecharts | Plotly
文章目录 📚Plotly🐇堆叠柱状图🐇环形图🐇散点图🐇漏斗图🐇桑基图🐇金字塔图🐇气泡图🐇面积图⭐️快速作图工具:plotly.express🐇树形图…...

黑豹程序员-架构师学习路线图-百科:Java的第二春Spring框架
文章目录 1、 Spring的发展历史2、为什么Spring能霸屏?2.1、容器的设计2.2、通过四个策略2.3、三种方式 3、学习编程设计的典范 1、 Spring的发展历史 正当SUN公司的EJB在全球开始热炒时,正当程序员纷纷转型EJB开发时,正当程序员为跑通EJB程…...
C#获取指定软件安装路径
作用 每个电脑安装的路径不一致会导致无法动态获取指定软件的安装路径,通过注册表来获取安装路径 代码 RegistryKey registryKeyPro Registry.LocalMachine.OpenSubKey("SOFTWARE\\****"); string installDir (string)(registryKeyPro.GetValue(&quo…...

vue3: bingmap using typescript
项目结构: <template><div class"bing-map-market"><!-- 加载遮罩层 --><div class"loading-overlay" v-show"isLoading || errorMessage"><div class"spinner-container"><div class&qu…...
【C++快读快写】
算法竞赛中用于解决卡常问题 int rd(){int k 0;char c getchar();while(!isdigit(c)){c getchar();}while(isdigit(c)){k (k << 1) (k << 3) (c^0), c getchar();}return k; }void wr(int x) {if (x > 9)wr(x / 10);putchar((x % 10) ^ 0); }用法&#x…...
CppCon 2015 学习:Intro to the C++ Object Model
这段代码展示了使用 make 工具来编译 C 程序的简单过程。 代码和步骤解析: C 代码(intro.cpp):#include <iostream> int main() { std::cout<<"hello world\n"; } 这是一个简单的 C 程序,它包…...
如何区分 “通信网络安全防护” 与 “信息安全” 的考核重点?
“通信网络安全防护” 与 “信息安全” 的考核重点可以从以下几个方面进行区分: 保护对象 通信网络安全防护:重点关注通信网络系统本身,包括网络基础设施,如路由器、交换机、基站等,以及网络通信链路和相关设备。同…...
Linux与Windows切换使用Obsidian,出现 unexplained changes 问题的解决
如果你的Obsidian文档在Linux与Windows间来回切换,可能会涉及到文件的保存换行符问题,但这样的话就容易导致一个问题,那就是内容无差异,Obsidian却提示unexplained changes,Windows系统下的解决方法如下,找…...

从零开始开发纯血鸿蒙应用之网络检测
从零开始开发纯血鸿蒙应用 〇、前言一、认识 connection 模块1、获取默认网络2、获取网络能力信息3、解析网络能力信息3.1、NetCap3.2、NetBearType 二、实现网络检测功能1、申请权限2、获取默认网路的 NetCap 数组 三、总结 〇、前言 在之前的博文里,介绍了如何实…...

前后端交互过程中—各类文件/图片的上传、下载、显示转换
前后端交互过程中—各类文件/图片的上传、下载、显示转换 图片补充:new Blob()URL.createObjectURL()替代方案:FileReader.readAsDataURL()对比: tiff文件TIFF库TIFF转换通过url转换tiff文件为png通过文件选择的方式转换tiff文件为png 下…...

AI IDE 正式上线!通义灵码开箱即用
近期,通义灵码AI IDE正式上线,即日起用户可在通义灵码官网免费下载开箱即用。 作为AI原生的开发环境工具,通义灵码AI IDE深度适配了最新的千问3大模型,并全面集成通义灵码插件能力,具备编程智能体、行间建议预测、行间…...

Transformer-BiGRU多变量时序预测(Matlab完整源码和数据)
Transformer-BiGRU多变量时序预测(Matlab完整源码和数据) 目录 Transformer-BiGRU多变量时序预测(Matlab完整源码和数据)效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现Transformer-BiGRU多变量时间序列预测&…...

视觉分析在人员行为属性检测中的应用
基于视觉分析的人员行为属性检测方案 一、背景与需求分析 在工业生产、建筑施工、公共安全等领域,人员行为属性的合规性检测是保障安全生产的关键环节。例如,工地工人未佩戴安全帽、厨房人员未佩戴手套、作业现场人员使用手机等行为,均可能…...