基于SpringBoot的哈士奇博客项目(二)

前面的部分请看基于SpringBoot的哈士奇博客项目(一)-CSDN博客

2、前台博客

2.8、前台登录功能实现

2.8.6、登录接口代码实现

BlogLoginController

import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.User;
import com.hashiqi.service.BlogLoginService;
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 BlogLoginController {

    @Autowired
    private BlogLoginService blogLoginService;

    // 登录
    @PostMapping("/login")
    public ResponseResult login(@RequestBody User user) {
        return blogLoginService.login(user);
    }
}

service中BlogLoginService

import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.User;

public interface BlogLoginService {

    // 登录
    ResponseResult login(User user);
}

serviceImpl中BlogLoginServiceImpl

import com.hashiqi.constants.RedisKeyConstants;
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.LoginUser;
import com.hashiqi.domain.entity.User;
import com.hashiqi.domain.vo.BlogUserLoginVO;
import com.hashiqi.domain.vo.UserInfoVO;
import com.hashiqi.service.BlogLoginService;
import com.hashiqi.utils.BeanCopyUtils;
import com.hashiqi.utils.JwtUtil;
import com.hashiqi.utils.RedisCache;
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.stereotype.Service;

import java.util.Objects;

@Service
public class BlogLoginServiceImpl implements BlogLoginService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    /**
     * 登录
     * @param user 用户
     * @return 返回结果集
     */
    @Override
    public ResponseResult login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken
                =  new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        // 判断是否认证通过
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("用户名或密码错误");
        }
        // 获取userId生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        // 把用户信息存入redis中
        redisCache.setCacheObject(RedisKeyConstants.BLOG_LOGIN_ID + userId, loginUser);
        // 把User转换成UserInfoVO
        UserInfoVO userInfoVO = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVO.class);
        // 把token和userInfo封装并返回
        BlogUserLoginVO vo = new BlogUserLoginVO(jwt, userInfoVO);
        return ResponseResult.okResult(vo);
    }
}

serviceImpl中UserDetailServiceImpl

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hashiqi.domain.entity.LoginUser;
import com.hashiqi.domain.entity.User;
import com.hashiqi.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Objects;

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据用户名查询用户信息
        User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUserName, username));
        // 判断是否存在该用户信息,如果否,则抛出异常
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户不存在");
        }
        // 如果是,返回该用户信息

        //TODO 查询权限信息封装

        return new LoginUser(user);
    }
}

entity包中LoginUser

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * 用户登录实体
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    // 权限集合
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

vo包中的BlogUserLoginVO

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class BlogUserLoginVO {

    private String token;
    private UserInfoVO userInfo;
}

vo包中的UserInfoVO

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class UserInfoVO {
    /**
     * 主键
     */
    private Long id;

    /**
     * 昵称
     */
    private String nickName;

    /**
     * 头像
     */
    private String avatar;

    private String sex;

    private String email;
}

constants包中的RedisKeyConstants

/**
 * redis的key前缀
 */
public class RedisKeyConstants {

    /**
     * 用户登录key前缀
     */
    public static final String BLOG_LOGIN_ID = "bloglogin:";
}

前台博客模块中的config包

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf,因为如果是分布式系统,则不适合session
                .csrf().disable()
                // 不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口,允许匿名访问【即未登录状态下可以访问 .permitAll()代表都可以访问】
                .antMatchers("/login").anonymous()
                // 出上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();

        http.logout().disable();
        // 允许跨域
        http.cors();
    }
}

2.8.7、登录校验过滤器代码实现

思路

  • 定义jwt认证过滤器

    • 获取token

    • 解析token,获取其中的userId

    • 从redis中获取登录用户信息

    • 存入SecurityContextHolder中

前台博客模块中的filter包

 

import com.alibaba.fastjson.JSON;
import com.hashiqi.constants.RedisKeyConstants;
import com.hashiqi.constants.SystemConstants;
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.LoginUser;
import com.hashiqi.enums.AppHttpCodeEnum;
import com.hashiqi.utils.JwtUtil;
import com.hashiqi.utils.RedisCache;
import com.hashiqi.utils.WebUtils;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取请求头中的token
        String token = request.getHeader(SystemConstants.TOKEN);
        if (!StringUtils.hasText(token)) {
            // 说明该接口无需token,直接放行即可
            filterChain.doFilter(request, response);
            return;
        }
        // 解析获取userId
        Claims claims = null;
        try {
            claims = JwtUtil.parseJWT(token);
        } catch (Exception e) {
            // token超时
            // token非法请求
            e.printStackTrace();
            // 响应前端,需重新进行登录
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
            WebUtils.renderString(response, JSON.toJSONString(result));
            return;
        }
        String userId = claims.getSubject();
        // 从redis中获取用户信息
        LoginUser loginUser = redisCache.getCacheObject(RedisKeyConstants.BLOG_LOGIN_ID + userId);
        // redis过期
        if (Objects.isNull(loginUser)) {
            // 说明redis过期,提示重新登录
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
            WebUtils.renderString(response, JSON.toJSONString(result));
            return;
        }
        // 存入SecurityContextHolder
        // TODO 现阶段未有权限信息
        UsernamePasswordAuthenticationToken authenticationToken
                = new UsernamePasswordAuthenticationToken(loginUser, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}
import com.hashiqi.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf,因为如果是分布式系统,则不适合session
                .csrf().disable()
                // 不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口,允许匿名访问【即未登录状态下可以访问 .permitAll()代表都可以访问】
                .antMatchers("/login").anonymous()
                .antMatchers("/link/getAllLink").authenticated()
                // 出上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();

        http.logout().disable();
        // 允许跨域
        http.cors();

        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

2.9、认证授权失败处理

目前项目在认证失败或者权限不足的时候返回前端的响应体是Security进行处理的,但是可能不会符合该项目的接口规范,所以需要自定义异常处理。

这两个处理器放在公共子模块中的handler包中。

AuthenticationEntryPoint 认证失败处理器

import com.alibaba.fastjson.JSON;
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.enums.AppHttpCodeEnum;
import com.hashiqi.utils.WebUtils;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

    // 认证失败处理
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
        // 打印异常信息
        authenticationException.printStackTrace();
        // InsufficientAuthenticationException
        ResponseResult result = null;
        // BadCredentialsException
        if (authenticationException instanceof BadCredentialsException) {
             result = ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_ERROR.getCode(), authenticationException.getMessage());
        } else if (authenticationException instanceof InsufficientAuthenticationException) {
            result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
        } else {
            result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "认证或授权失败");
        }
        // 响应回前端
        WebUtils.renderString(response, JSON.toJSONString(result));
    }
}

AccessDeniedHandler 授权失败处理器

import com.alibaba.fastjson.JSON;
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.enums.AppHttpCodeEnum;
import com.hashiqi.utils.WebUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

    // 授权失败处理
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException authenticationException) throws IOException, ServletException {
        // 打印异常信息
        authenticationException.printStackTrace();
        // 响应回前端
        ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);
        WebUtils.renderString(response, JSON.toJSONString(result));
    }
}

将这两个处理器注入到前台博客模块里的SecurityConfig配置类中

import com.hashiqi.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf,因为如果是分布式系统,则不适合session
                .csrf().disable()
                // 不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口,允许匿名访问【即未登录状态下可以访问 .permitAll()代表都可以访问】
                .antMatchers("/login").anonymous()
                .antMatchers("/link/getAllLink").authenticated()
                // 出上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();

        http.logout().disable();
        // 允许跨域
        http.cors();
        // 将token检验过滤器放在用户名和密码校验之前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 配置认证和授权失败处理器
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
    }
}

2.10、统一异常处理

实际开发过程中需要做很多的判断校验,如果出现了非法情况是期望响应对应的提示的。但是如果每次都是手动处理就显得十分麻烦,所以可以选择直接抛出异常的方式,接着对异常进行统一处理,换言之,把异常中的信息封装成统一返回结果集响应给前端。

在公共子模块中的exception包

import com.hashiqi.enums.AppHttpCodeEnum;

/**
 * 统一异常处理
 */
public class SystemException extends RuntimeException {

    private int code;

    private String msg;

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

    public SystemException(AppHttpCodeEnum httpCodeEnum) {
        super(httpCodeEnum.getMsg());
        this.code = httpCodeEnum.getCode();
        this.msg = httpCodeEnum.getMsg();
    }
}

在公共子模块中的handler包的handler包

import com.hashiqi.domain.ResponseResult;
import com.hashiqi.enums.AppHttpCodeEnum;
import com.hashiqi.exception.SystemException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(SystemException.class)
    public ResponseResult systemExceptionHandler(SystemException e){
        //打印异常信息
        log.error("出现了异常! {}",e.toString());
        //从异常对象中获取提示信息封装返回
        return ResponseResult.errorResult(e.getCode(),e.getMsg());
    }

    @ExceptionHandler(Exception.class)
    public ResponseResult exceptionHandler(Exception e){
        //打印异常信息
        log.error("出现了异常! {}",e.toString());
        //从异常对象中获取提示信息封装返回
        return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage());
    }
}

2.11、退出登录接口

2.11.1、接口设计

请求方式请求地址请求头
POST/logout需要token请求头

响应格式:

{
    "code": 200,
    "msg": "操作成功"
}

2.11.2、代码实现

要实现的操作:删除redis中的用户信息

BlogLoginController

// 退出登录
@PostMapping("/logout")
public ResponseResult logout() {
    return blogLoginService.logout();
}

BlogLoginService

// 退出登录
ResponseResult logout();

BlogLoginServiceImpl

/**
 * 退出登录
 * @return 返回结果集
 */
@Override
public ResponseResult logout() {
    // 获取token,解析获取userId
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    // 获取userId
    Long userId = loginUser.getUser().getId();
    // 删除redis中的用户信息
    redisCache.deleteObject(RedisKeyConstants.BLOG_LOGIN_ID + userId);
    return ResponseResult.okResult();
}

在SecurityConfig需要再次操作

要关闭默认的退出登录功能。并且要配置我们的退出登录接口需要认证才能访问

import com.hashiqi.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf,因为如果是分布式系统,则不适合session
                .csrf().disable()
                // 不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口,允许匿名访问【即未登录状态下可以访问 .permitAll()代表都可以访问】
                .antMatchers("/login").anonymous()
                // 注销接口需要认证才能访问
                .antMatchers("/logout").authenticated()
                // 出上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();
        
        // 关闭默认的注销功能
        http.logout().disable();
        // 允许跨域
        http.cors();
        // 将token检验过滤器放在用户名和密码校验之前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 配置认证和授权失败处理器
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
    }
}

2.12、查询评论列表接口

2.12.1、需求分析

文章详情页面要展示这篇文章下的评论列表。

2.12.2、评论表分析

通过需求去分析需要有哪些字段。

DROP TABLE IF EXISTS `tb_comment`;
CREATE TABLE `tb_comment`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '评论类型(0代表文章评论,1代表友链评论)',
  `article_id` bigint(20) NULL DEFAULT NULL COMMENT '文章id',
  `root_id` bigint(20) NULL DEFAULT -1 COMMENT '根评论id',
  `content` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '评论内容',
  `to_comment_user_id` bigint(20) NULL DEFAULT -1 COMMENT '所回复的目标评论的userid',
  `to_comment_id` bigint(20) NULL DEFAULT -1 COMMENT '回复目标评论id',
  `create_by` bigint(20) NULL DEFAULT NULL,
  `create_time` datetime(0) NULL DEFAULT NULL,
  `update_by` bigint(20) NULL DEFAULT NULL,
  `update_time` datetime(0) NULL DEFAULT NULL,
  `is_deleted` int(1) NULL DEFAULT 0 COMMENT '删除标志(0代表未删除,1代表已删除)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '评论表' ROW_FORMAT = Dynamic;

2.12.3、接口设计

请求方式请求地址请求头
GET/comment/commentList不需要token请求头

Query格式请求参数:

articleId:文章id
pageNum: 页码
pageSize: 每页条数

响应格式:

{
    "code": 200,
    "data": {
        "rows": [
            {
                "articleId": "1",
                "children": [
                    {
                        "articleId": "1",
                        "content": "hhhashiqi",
                        "createBy": "1",
                        "createTime": "2023-09-30 10:06:21",
                        "id": "20",
                        "rootId": "1",
                        "toCommentId": "1",
                        "toCommentUserId": "1",
                        "toCommentUserName": "xxx",
                        "username": "xxx"
                    }
                ],
                "content": "hashiqi",
                "createBy": "1",
                "createTime": "2023-09-29 07:59:22",
                "id": "1",
                "rootId": "-1",
                "toCommentId": "-1",
                "toCommentUserId": "-1",
                "username": "xxx"
            }
        ],
        "total": "15"
    },
    "msg": "操作成功"
}

2.12.4、代码实现

Comment实体类

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.util.Date;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_comment")
public class Comment implements Serializable {

    private static final long serialVersionUID=1L;

      @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 评论类型(0代表文章评论,1代表友链评论)
     */
    private String type;

    /**
     * 文章id
     */
    private Long articleId;

    /**
     * 根评论id
     */
    private Long rootId;

    /**
     * 评论内容
     */
    private String content;

    /**
     * 所回复的目标评论的userid
     */
    private Long toCommentUserId;

    /**
     * 回复目标评论id
     */
    private Long toCommentId;

    private Long createBy;

    private Date createTime;

    private Long updateBy;

    private Date updateTime;

    /**
     * 删除标志(0代表未删除,1代表已删除)
     */
    private Integer isDeleted;
}

2.12.4.1、暂时省略子评论

CommentController

import com.hashiqi.domain.ResponseResult;
import com.hashiqi.service.CommentService;
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("/comment")
public class CommentController {

    @Autowired
    private CommentService commentService;

    // 获取所有文章相关的评论
    @GetMapping("/commentList")
    public ResponseResult commentList(
            Long articleId,
            Integer pageNum,
            Integer pageSize) {
        return commentService.commentList(articleId, pageNum, pageSize);
    }
}

CommentService

import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.Comment;
import com.baomidou.mybatisplus.extension.service.IService;

public interface CommentService extends IService<Comment> {

    // 获取所有文章相关的评论
    ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize);
}

CommentServiceImpl

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hashiqi.constants.SystemConstants;
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.Comment;
import com.hashiqi.domain.vo.CommentVO;
import com.hashiqi.domain.vo.PageVO;
import com.hashiqi.mapper.CommentMapper;
import com.hashiqi.service.CommentService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hashiqi.service.UserService;
import com.hashiqi.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {

    @Autowired
    private UserService userService;

    /**
     * 获取所有文章相关的评论
     * @param articleId 文章id
     * @param pageNum 当前页
     * @param pageSize 页大小
     * @return 返回所有评论
     */
    @Override
    public ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) {
        // 查询对应文章id的根评论
        Page<Comment> page = this.page(new Page<>(pageNum, pageSize),
                new LambdaQueryWrapper<Comment>().eq(Comment::getArticleId, articleId).eq(Comment::getRootId, SystemConstants.ARTICLE_COMMENT_ROOT));
        // 将Comment转换成CommentVO
        List<CommentVO> commentVOs = toCommentList(page.getRecords());
        return ResponseResult.okResult(new PageVO(commentVOs, page.getTotal()));
    }

    /**
     * 对获取的评论VO进行属性赋值封装
     * @param list 要封装的评论对象集合
     * @return 返回封装后的评论对象集合
     */
    private List<CommentVO> toCommentList(List<Comment> list) {
        List<CommentVO> commentVOs = BeanCopyUtils.copyBeanList(list, CommentVO.class);
        commentVOs.forEach(commentVO -> {
            // 通过creatBy查询用户的昵称并赋值
            commentVO.setUsername(userService.getById(commentVO.getCreateBy()).getNickName());
            // 通过toCommentId查询用户的昵称并赋值
            if (!commentVO.getToCommentUserId().equals(SystemConstants.ARTICLE_TO_COMMENT_USER_ID)) {
                commentVO.setToCommentUserName(userService.getById(commentVO.getToCommentUserId()).getNickName());
            }
        });
        return commentVOs;
    }
}

CommentVO

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommentVO {

    private Long id;

    /**
     * 文章id
     */
    private Long articleId;

    /**
     * 根评论id
     */
    private Long rootId;

    /**
     * 评论内容
     */
    private String content;

    /**
     * 所回复的目标评论的userid
     */
    private Long toCommentUserId;
    private String toCommentUserName;

    /**
     * 回复目标评论id
     */
    private Long toCommentId;

    /**
     * 评论人
     */
    private String username;

    private Long createBy;

    private Date createTime;
}

SystemConstants

/**
 * 文章的根评论
 */
public static final String ARTICLE_COMMENT_ROOT = "-1";
/**
 * 根评论回复ID为-1
 */
public static final Long ARTICLE_TO_COMMENT_USER_ID = -1L;

2.12.4.2、查询子评论

CommentVO在之前的基础上增加了 private List<CommentVO> children;

CommentServiceImpl

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hashiqi.constants.SystemConstants;
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.Comment;
import com.hashiqi.domain.vo.CommentVO;
import com.hashiqi.domain.vo.PageVO;
import com.hashiqi.mapper.CommentMapper;
import com.hashiqi.service.CommentService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hashiqi.service.UserService;
import com.hashiqi.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {

    @Autowired
    private UserService userService;

    /**
     * 获取所有文章相关的评论
     * @param articleId 文章id
     * @param pageNum 当前页
     * @param pageSize 页大小
     * @return 返回所有评论
     */
    @Override
    public ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) {
        // 查询对应文章id的根评论
        Page<Comment> page = this.page(new Page<>(pageNum, pageSize),
                new LambdaQueryWrapper<Comment>().eq(Comment::getArticleId, articleId).eq(Comment::getRootId, SystemConstants.ARTICLE_COMMENT_ROOT));
        // 将Comment转换成CommentVO
        List<CommentVO> commentVOs = toCommentList(page.getRecords());

        // 查询所有根评论对应的子评论集合,并且赋值给对应的属性
        commentVOs.forEach(commentVO -> {
            // 查询对应的子评论集合
            List<CommentVO> children = getChildren(commentVO.getId());
            // 赋值
            commentVO.setChildren(children);
        });
        return ResponseResult.okResult(new PageVO(commentVOs, page.getTotal()));
    }

    /**
     * 根据根评论id查询所对应的子评论集合
     * @param id 根评论id
     * @return 返回CommentVO集合
     */
    private List<CommentVO> getChildren(Long id) {
        List<Comment> comments = baseMapper.selectList(new LambdaQueryWrapper<Comment>()
                .eq(Comment::getRootId, id)
                .orderByDesc(Comment::getCreateTime));
        return toCommentList(comments);
    }

    /**
     * 对获取的评论VO进行属性赋值封装
     * @param list 要封装的评论对象集合
     * @return 返回封装后的评论对象集合
     */
    private List<CommentVO> toCommentList(List<Comment> list) {
        List<CommentVO> commentVOs = BeanCopyUtils.copyBeanList(list, CommentVO.class);
        commentVOs.forEach(commentVO -> {
            // 通过creatBy查询用户的昵称并赋值
            commentVO.setUsername(userService.getById(commentVO.getCreateBy()).getNickName());
            // 通过toCommentId查询用户的昵称并赋值
            if (!commentVO.getToCommentUserId().equals(SystemConstants.ARTICLE_TO_COMMENT_USER_ID)) {
                commentVO.setToCommentUserName(userService.getById(commentVO.getToCommentUserId()).getNickName());
            }
        });
        return commentVOs;
    }
}

2.13、发表评论接口

2.13.1、需求分析

用户登录后可以对文章发表评论,也可以对评论进行回复。

用户登录后也可以在友链页面进行评论。

2.13.2、接口设计

请求方式请求地址请求头
POST/comment需要token头

请求体:

回复了文章:

{
    "articleId":1,
    "type":0,
    "rootId":-1,
    "toCommentId":-1,
    "toCommentUserId":-1,
    "content":"评论了文章"
}

回复了某条评论:

{
    "articleId":1,
    "type":0,
    "rootId":"3",
    "toCommentId":"3",
    "toCommentUserId":"1",
    "content":"回复了某条评论"
}

回复网链评论:

{
    "articleId":1,
    "type":1,
    "rootId":"3",
    "toCommentId":"3",
    "toCommentUserId":"1",
    "content":"回复了某条网链评论"
}

响应格式:

{
    "code":200,
    "msg":"操作成功"
}

2.13.3、代码实现

CommentController

// 添加评论
@PostMapping
public ResponseResult addComment(@RequestBody Comment comment) {
    return commentService.addComment(comment);
}

CommentService

// 添加评论
ResponseResult addComment(Comment comment);

CommentServiceImpl

/**
 * 添加评论
 * @param comment 评论对象
 * @return 返回结果集
 */
@Override
public ResponseResult addComment(Comment comment) {
    // 评论内容不能为空
    if (!StringUtils.hasText(comment.getContent())) {
        throw new SystemException(AppHttpCodeEnum.CONTENT_NOT_NULL);
    }
    save(comment);
    return ResponseResult.okResult();
}

公共子模块中utils包

import com.hashiqi.domain.entity.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

public class SecurityUtils {

    /**
     * 获取用户
     **/
    public static LoginUser getLoginUser() {
        return (LoginUser) getAuthentication().getPrincipal();
    }

    /**
     * 获取Authentication
     */
    public static Authentication getAuthentication() {
        return SecurityContextHolder.getContext().getAuthentication();
    }

    public static Boolean isAdmin(){
        Long id = getLoginUser().getUser().getId();
        return id != null && 1L == id;
    }

    public static Long getUserId() {
        return getLoginUser().getUser().getId();
    }
}

公共子模块中的handler包

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.hashiqi.utils.SecurityUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * 配置MP字段自动填充
 */
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        Long userId = null;
        try {
            userId = SecurityUtils.getUserId();
        } catch (Exception e) {
            e.printStackTrace();
            userId = -1L;//表示是自己创建
        }
        this.setFieldValByName("createTime", new Date(), metaObject);
        this.setFieldValByName("createBy",userId , metaObject);
        this.setFieldValByName("updateTime", new Date(), metaObject);
        this.setFieldValByName("updateBy", userId, metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.setFieldValByName("updateTime", new Date(), metaObject);
        this.setFieldValByName(" ", SecurityUtils.getUserId(), metaObject);
    }
}

在entity包中的实体类进行添加,或者嫌添加每一个实体类太麻烦,可以创建一个实体基类,将这些字段写入其中,再让其他实体类去继承该实体基类。

/**
* 创建人的用户id
*/
@TableField(fill = FieldFill.INSERT)
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;

2.14、网链评论列表

2.14.1、需求分析

网链页面也需要查询对应的评论列表。

2.14.2、接口设计

请求方式请求地址请求头
GET/comment/linkCommentList不需要token请求头

Query格式请求参数:

pageNum: 页码
pageSize: 每页条数

响应格式:

{
    "code": 200,
    "data": {
        "rows": [
            {
                "articleId": "1",
                "children": [
                    {
                        "articleId": "1",
                        "content": "回复友链评论3",
                        "createBy": "1",
                        "createTime": "2023-09-29 10:08:50",
                        "id": "23",
                        "rootId": "22",
                        "toCommentId": "22",
                        "toCommentUserId": "1",
                        "toCommentUserName": "xxx",
                        "username": "xxx"
                    }
                ],
                "content": "友链评论2",
                "createBy": "1",
                "createTime": "2023-09-29 10:08:28",
                "id": "22",
                "rootId": "-1",
                "toCommentId": "-1",
                "toCommentUserId": "-1",
                "username": "xxx"
            }
        ],
        "total": "1"
    },
    "msg": "操作成功"
}

2.14.3、代码实现

SystemConstants增加两个常量

/**
 * 评论类型为:文章评论
 */
public static final String ARTICLE_COMMENT = "0";
/**
 * 评论类型为:网链评论
 */
public static final String LINK_COMMENT = "1";

CommentController

对之前文章评论列表接口进行修改,并添加新的网链评论接口。

@Autowired
private CommentService commentService;

// 获取所有文章相关的评论
@GetMapping("/commentList")
public ResponseResult commentList(
        Long articleId,
        Integer pageNum,
        Integer pageSize) {
    return commentService.commentList(SystemConstants.ARTICLE_COMMENT, articleId, pageNum, pageSize);
}

// 获取所有网链相关的评论
@GetMapping("/linkCommentList")
public ResponseResult linkCommentList(
        Integer pageNum,
        Integer pageSize) {
    return commentService.commentList(SystemConstants.LINK_COMMENT, null, pageNum, pageSize);
}

CommentService

同样,既然控制层方法里参数进行改动,那么就要对业务层及业务实现层的方法参数进行改动。

在CommentService中修改commentList方法,新增一个参数为字符串类型的commentType。

// 获取所有文章/网链相关的评论
ResponseResult commentList(String commentType, Long articleId, Integer pageNum, Integer pageSize);

CommentServiceImpl

/**
 * 获取所有文章相关的评论
 * @param commentType 评论类型
 * @param articleId 文章id
 * @param pageNum 当前页
 * @param pageSize 页大小
 * @return 返回所有评论
 */
@Override
public ResponseResult commentList(String commentType, Long articleId, Integer pageNum, Integer pageSize) {
    // 查询对应文章id的根评论
    Page<Comment> page = this.page(new Page<>(pageNum, pageSize),
            new LambdaQueryWrapper<Comment>()
                    .eq(SystemConstants.ARTICLE_COMMENT.equals(commentType), Comment::getArticleId, articleId)
                    .eq(Comment::getRootId, SystemConstants.ARTICLE_COMMENT_ROOT)
                    .eq(Comment::getType, commentType)
                    .orderByDesc(Comment::getCreateTime));
    // 将Comment转换成CommentVO
    List<CommentVO> commentVOs = toCommentList(page.getRecords());

    // 查询所有根评论对应的子评论集合,并且赋值给对应的属性
    commentVOs.forEach(commentVO -> {
        // 查询对应的子评论集合
        List<CommentVO> children = getChildren(commentVO.getId());
        // 赋值
        commentVO.setChildren(children);
    });
    return ResponseResult.okResult(new PageVO(commentVOs, page.getTotal()));
}

2.15、个人信息查询接口

2.15.1、需求分析

进入个人中心的时候需要能够查看当前用户信息。

2.15.2、接口设计

请求方式请求地址请求头
GET/user/userInfo需要token请求头

不需要参数

响应格式:

{
    "code":200,
    "data":{
        "avatar":"https://xxx/image_search/src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2F3bf9c263bc0f2ac5c3a7feb9e218d07475573ec8.gi",
        "email":"23412332@qq.com",
        "id":"1",
        "nickName":"xxx",
        "sex":"1"
    },
    "msg":"操作成功"
}

2.15.3、代码实现

UserController

import com.hashiqi.domain.ResponseResult;
import com.hashiqi.service.UserService;
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("/user")
public class UserController {

    @Autowired
    private UserService userService;

    // 获取个人用户信息
    @GetMapping("/userInfo")
    public ResponseResult userInfo() {
        return userService.userInfo();
    }
}

UserService

import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.User;
import com.baomidou.mybatisplus.extension.service.IService;

public interface UserService extends IService<User> {

    // 获取个人用户信息
    ResponseResult userInfo();
}

UserServiceImpl

import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.User;
import com.hashiqi.domain.vo.UserInfoVO;
import com.hashiqi.mapper.UserMapper;
import com.hashiqi.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hashiqi.utils.BeanCopyUtils;
import com.hashiqi.utils.SecurityUtils;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    /**
     * 获取个人用户信息
     * @return 返回用户信息
     */
    @Override
    public ResponseResult userInfo() {
        // 根据用户id查询用户信息
        User user = this.getById(SecurityUtils.getUserId());
        // User封装成UserInfoVO并返回
        return ResponseResult.okResult(BeanCopyUtils.copyBean(user, UserInfoVO.class));
    }
}

SecurityConfig

配置该接口必须认证后才能访问。

// 个人信息接口必须登录后才可访问
.antMatchers("/user/userInfo").authenticated()

2.16、头像上传接口

2.16.1、需求

在个人中心点击编辑的时候可以上传头像图片。上传完头像后,可以用于更新个人信息接口。

2.16.2、OSS

2.16.2.1、OSS的作用

因为如果把图片视频等文件上传到自己的应用的Web服务器,在读取图片的时候会占用比较多的资源。影响应用服务器的性能。

所以一般使用OSS(Object Storage Service对象存储服务)存储图片或视频。

2.16.2.2、七牛云代码测试

当然也可以使用腾讯云和阿里云的,或者去创建minio。

引入依赖

<dependency>
  <groupId>com.qiniu</groupId>
  <artifactId>qiniu-java-sdk</artifactId>
  <version>[7.13.0, 7.13.99]</version>
</dependency>

在前台博客模块中配置yml文件

这里需要自己去申请放入钥匙和秘钥以及创建桶进行对象存储。

oss:
  accessKey: "xxx"
  secretKey: "xxx"
  bucket: "yourname-bucket"

复制参考文档的代码进行修改

import com.google.gson.Gson;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;

@SpringBootTest
@ConfigurationProperties(prefix = "oss")
public class OSSTest {

    @Value("${oss.accessKey}")
    private String accessKey;
    @Value("${oss.secretKey}")
    private String secretKey;
    @Value("${oss.bucket}")
    private String bucket;
    
    @Test
    void test() {
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.autoRegion());
        cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;// 指定分片上传版本
        //...其他参数参考类注释

        UploadManager uploadManager = new UploadManager(cfg);
        //...生成上传凭证,然后准备上传
        // String accessKey = "xxx";
        // String secretKey = "xxx";
        // String bucket = "yourname-bucket";

        //默认不指定key的情况下,以文件内容的hash值作为文件名
        String key = null;

        try {
            // 默认文档
//            byte[] uploadBytes = "hello qiniu cloud".getBytes("utf-8");
//            ByteArrayInputStream byteInputStream=new ByteArrayInputStream(uploadBytes);

            // 自定义更改
            InputStream is = new FileInputStream("F:\\yezhu.png");

            Auth auth = Auth.create(accessKey, secretKey);
            String upToken = auth.uploadToken(bucket);

            try {
                Response response = uploadManager.put(is,key,upToken,null, null);
                //解析上传成功的结果
                DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
                System.out.println(putRet.key);
                System.out.println(putRet.hash);
            } catch (QiniuException ex) {
                ex.printStackTrace();
                if (ex.response != null) {
                    System.err.println(ex.response);

                    try {
                        String body = ex.response.toString();
                        System.err.println(body);
                    } catch (Exception ignored) {
                    }
                }
            }
        } catch (Exception ex) {
            //ignore
            ex.printStackTrace();
        }
    }
}

2.16.3、接口设计

请求方式请求地址请求头
POST/upload需要token

参数:img,值为要上传的文件

请求头:Content-Type :multipart/form-data;

响应格式:

{
    "code": 200,
    "data": "文件访问链接",
    "msg": "操作成功"
}

2.16.4、代码实现

UploadController

import com.hashiqi.domain.ResponseResult;
import com.hashiqi.service.UploadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class UploadController {

    @Autowired
    private UploadService uploadService;

    // 上传文件
    @PostMapping("/upload")
    public ResponseResult uploadImg(MultipartFile img) {
        return uploadService.uploadImg(img);
    }
}

UploadService

import com.hashiqi.domain.ResponseResult;
import org.springframework.web.multipart.MultipartFile;

public interface UploadService {

    // 上传文件
    ResponseResult uploadImg(MultipartFile img);
}

OssUploadService

import com.google.gson.Gson;
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.enums.AppHttpCodeEnum;
import com.hashiqi.exception.SystemException;
import com.hashiqi.service.UploadService;
import com.hashiqi.utils.PathUtils;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;

@Service
@Data
@ConfigurationProperties(prefix = "oss")
public class OssUploadService implements UploadService {

    @Value("${oss.accessKey}")
    private String accessKey;
    @Value("${oss.secretKey}")
    private String secretKey;
    @Value("${oss.bucket}")
    private String bucket;

    /**
     * 上传文件
     * @param img 文件
     * @return 返回结果集
     */
    @Override
    public ResponseResult uploadImg(MultipartFile img) {
        // 判断文件类型或者文件大小
        // 获取原始文件名
        String originalFilename = img.getOriginalFilename();
        // 对原始名进行判断
        if (!originalFilename.endsWith(".png")) {
            throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR);
        }
        // 判断通过,设置文件路径规则【xxxx/xx/xx】,然后上传文件到OSS
        String url =  uploadOss(img, PathUtils.generateFilePath(originalFilename));
        return ResponseResult.okResult(url);
    }

    /**
     * 七牛云OSS
     */
    private String uploadOss(MultipartFile imgFile, String filePath) {
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.autoRegion());
        cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;// 指定分片上传版本
        //...其他参数参考类注释

        UploadManager uploadManager = new UploadManager(cfg);
        //...生成上传凭证,然后准备上传

        //默认不指定key的情况下,以文件内容的hash值作为文件名
//        String key = null;

        try {
            // 自定义更改
            InputStream is = imgFile.getInputStream();

            Auth auth = Auth.create(accessKey, secretKey);
            String upToken = auth.uploadToken(bucket);

            try {
                Response response = uploadManager.put(is,filePath,upToken,null, null);
                //解析上传成功的结果
                DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
                System.out.println(putRet.key);
                System.out.println(putRet.hash);
                return "http://xxxxxxx.bkt.clouddn.com/" + filePath;
            } catch (QiniuException ex) {
                ex.printStackTrace();
                if (ex.response != null) {
                    System.err.println(ex.response);

                    try {
                        String body = ex.response.toString();
                        System.err.println(body);
                    } catch (Exception ignored) {
                    }
                }
            }
        } catch (Exception ex) {
            //ignore
            ex.printStackTrace();
        }
        return "操作成功";
    }
}

公共子模块的utils包中的PathUtils

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;

/**
 * 规定文件路径
 */
public class PathUtils {

    public static String generateFilePath(String fileName) {
        // 根据日期生成路径【xxxx/xx/xx】
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd");
        String datePath = simpleDateFormat.format(new Date());
        // uuid作为文件名
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        // 后缀和文件后缀一致
        int index = fileName.lastIndexOf(".");
        String fileType = fileName.substring(index);
        // 这里为什么不使用直接拼接,是因为这样处理性能比较好【但是不是安全线程】
        return new StringBuilder().append(datePath).append(uuid).append(fileType).toString();
    }
}

2.17、更新个人信息接口

2.17.1、需求分析

在编辑完个人资料后点击保存会对个人资料进行更新。

2.17.2、接口设计

请求方式请求地址请求头
PUT/user/userInfo需要token请求头

请求体中json格式数据:

{
    "avatar":"https://xxx.aliyuncs.com/2023/09/29/948597e164614902ab1662ba8452e106.png",
    "email":"548415168@qq.com",
    "id":"1",
    "nickName":"xxx",
    "sex":"1"
}

响应格式:

{
    "code":200,
    "msg":"操作成功"
}

2.17.3、代码实现

UserController

// 更新个人用户信息
@PutMapping("userInfo")
public ResponseResult updateUserInfo(@RequestBody User user) {
    return userService.updateUserInfo(user);
}

UserServiceImpl

/**
* 更新个人用户信息【指定字段】
* @param user 要更新的信息
* @return 返回结果集
*/
@Override
public ResponseResult updateUserInfo(User user) {
    this.update(user, new LambdaUpdateWrapper<User>()
                .eq(User::getId, user.getId())
                .set(User::getAvatar, user.getAvatar())
                .set(User::getNickName, user.getNickName())
                .set(User::getSex, user.getSex()));
    return ResponseResult.okResult();
}

2.18、用户注册

2.18.1、需求分析

要求用户能够在注册界面完成用户的注册。要求用户名,昵称,邮箱不能和数据库中原有的数据重复。如果某项重复了注册失败并且要有对应的提示。并且要求用户名,密码,昵称,邮箱都不能为空。

注意:密码必须密文存储到数据库中。

2.18.2、接口设计

请求方式请求地址请求头
POST/user/register不需要token请求头

请求体中json格式数据:

{
  "email": "string",
  "nickName": "string",
  "password": "string",
  "userName": "string"
}

响应格式:

{
    "code":200,
    "msg":"操作成功"
}

2.18.3、代码实现

UserController

// 注册个人用户信息
@PostMapping("/register")
public ResponseResult register(@RequestBody User user) {
    return userService.register(user);
}

UserService

// 注册个人用户信息
ResponseResult register(User user);

UserServiceImpl

/**
 * 注册个人用户信息
 * @param user 用户信息
 * @return 返回结果集
 */
@Override
public ResponseResult register(User user) {
    // 对从前端传回来的数据进行非空判断
    String userName = user.getUserName();
    String password = user.getPassword();
    String nickName = user.getNickName();
    String email = user.getEmail();
    if (!StringUtils.hasText(userName)) {
        throw new SystemException(AppHttpCodeEnum.USERNAME_NOT_NULL);
    }
    if (!StringUtils.hasText(password)) {
        throw new SystemException(AppHttpCodeEnum.PASSWORD_NOT_NULL);
    }
    if (!StringUtils.hasText(email)) {
        throw new SystemException(AppHttpCodeEnum.EMAIL_NOT_NULL);
    }
    if(!StringUtils.hasText(nickName)){
        throw new SystemException(AppHttpCodeEnum.NICKNAME_NOT_NULL);
    }
    // 是否存在数据库同样数据
    if(userNameExist(userName)){
        throw new SystemException(AppHttpCodeEnum.USERNAME_EXIST);
    }
    if(nickNameExist(nickName)){
        throw new SystemException(AppHttpCodeEnum.NICKNAME_EXIST);
    }
    if(emailExist(email)) {
        throw new SystemException(AppHttpCodeEnum.EMAIL_EXIST);
    }
    // 对密码进行加密
    String encodePassword = passwordEncoder.encode(user.getPassword());
    user.setPassword(encodePassword);
    // 将数据存入数据库中
    save(user);
    return ResponseResult.okResult();
}

/**
 * 判断邮箱是否已存在
 * @param email 注册的邮箱
 * @return true:存在;false:不存在
 */
private boolean emailExist(String email) {
    return this.count(new LambdaQueryWrapper<User>().eq(User::getEmail, email)) > 0;
}

/**
 * 判断昵称是否已存在
 * @param nickName 注册或要更新的昵称
 * @return true:存在;false:不存在
 */
private boolean nickNameExist(String nickName) {
    return this.count(new LambdaQueryWrapper<User>().eq(User::getNickName, nickName)) > 0;
}

/**
 * 判断用户名是否已存在
 * @param userName 注册或要更新的用户名
 * @return true:存在;false:不存在
 */
private boolean userNameExist(String userName) {
    return this.count(new LambdaQueryWrapper<User>().eq(User::getUserName, userName)) > 0;
}

AppHttpCodeEnum

USERNAME_NOT_NULL(508, "用户名不能为空"),
NICKNAME_NOT_NULL(509, "昵称不能为空"),
PASSWORD_NOT_NULL(510, "密码不能为空"),
EMAIL_NOT_NULL(511, "邮箱不能为空"),
NICKNAME_EXIST(512, "昵称已存在");

这部分到这里就结束了,后续还会继续更新~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值