留给读者

你们可能想不到,其实博客已经最后一天了,因为前面该搭建的都搭建完了,后面只是业务代码,但为了能照顾大部分同学,我决定还是承诺应允大家做到从规范到代码,从代码到运行,从运行到项目部署,从部署到项目上线这几个过程。

一些业务中的规范直接按照项目来就可以了,就可以避免碰到许多坑了。

想入坑的有下面这么几个,大家可以不要看推文自己试试,能不能躺到坑位,嘟嘟噜。

  • 打包不了的如何解决?用那个依赖就可以解决了?

  • mongodbbean没法依赖,springboot启动后报空指针异常

  • spring1.02.0遇到的配置文件信息变动,RedisMongoDB随之变动出现的问题

  • 高版本swagger2出现的页面不显示问题

所有分布式服务开启后,可以清晰地从Nacos注册中心看到

swagger2展示用户的接口

有一点电脑性能建议的,开启6个进程很消耗内存,我直接飙到了12G,电脑内存16G,不知道你们能不能扛得住,CPU要求比较低,我只有双内核,也就是4个处理器。

重要的事说三遍,不要在项目直接配置数据库或注册中心,为了提供更好的调试和开发体验,将rpc需要配置的信息以及博客自定义配置信息支持到Jar启动外置中配置,一般推荐项目中resource资源下配置信息只配置与用户名和数据密码敏感无关信息,这在支持外部配置的application.properties文件中会重写覆盖,目前rpc-netty-framework只支持配置文件整体覆盖,暂时没有实现局部key-value信息覆盖。

在摸索数据库安全路上碰壁留下的许多经验,就送给大家吧,只要大家能支持我,我满足了。

先说安全可做的方面:

  • 系统账户权限

由于数据库存储类软件,设计到存储功能权限,是需要系统赋予一个存储数据的权限的,所有需要申请账户,有别于系统的root账户

默认会赋予存储的权限。

所以我们可以通过从以下几个方面去加强安全性

  • nologin设置

怎么nologin设置?

如果你玩过linux或者云服务器,你会发现home目录下会有几个用户目录,如下图

而如果想切换到对应的用户,例如使用命令su mongodb,想要实现下面这种做法

只需要一行命令

usermod -s /sbin/nologin mysql
su mysql
This account is currently not available.

这样做的好处就是,如果数据库被攻击导致泄露用户信息,那数据库中涉及到系统的存储权限无法直接使用shell方式登录,那系统中其他信息就相当安全了,不会因为数据库的问题而引发其他问题。

  • 访问源地址限制

这一般在防火墙中设置,而如果使用云服务器设置就相对简单多了,就对阿里云来讲,进入到云控制台中,打开你的实例服务器的安全组,对某个端口设置源地址限制即可。

你可以通过这种方式设置数据库开放端口给内部其他服务器,而不直接开放到外网,这样即便暴露了数据库端口和用户登录信息,对方也无法登录。

当然如果你已经泄露过了用户信息,你不妨看看数据库之前是否开启过日志,或者从系统日志中入手,看看是哪个ip对你的数据库进行了攻击,将其拉入黑名单即可。

  • 端口设置

一般不设置默认端口,因为大多数情况下,数据库攻击手段是通过默认端口和默认用户登录信息进行攻击的,也有些脚本是获取公网中如GitHub上泄露的用户信息进行攻击,这里提供一个工具,如果你有经常做笔记发布到githubgitee的习惯,推荐你使用这一款网页版的监控敏感信息:https://www.gitguardian.com/,有敏感信息会通过邮箱告知你,自己完全不用操心,只需在监控到敏感信息后,自己手动处理即可。

  • 使用代理

使用代理是将设计到数据存储的服务器地址和端口隐藏不对外开发,而通过另一台服务器开发端口,由它来做代理服务。

做完这方面,可以看看架构上的设计,是如何巧妙通过代理、分布式、缓冲层、鉴权、反射调用来一步步加强安全和联系的。

1. 软件架构

软件架构说明

  • 项目分布式前后端代理架构设计:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NgpFI5BV-1663662434198)(https://yupeng-tuchuang.oss-cn-shenzhen.aliyuncs.com/分布式前后分离架构.png)]

  • 项目RPC架构设计:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HXhKs9di-1663662434202)(https://yupeng-tuchuang.oss-cn-shenzhen.aliyuncs.com/分布式博客微服务架构.png)]

下面就着手开发吧,相信各位都能以我的项目作为地基,二次开发出一个更有高度的项目!

2. 编写代码

代理服务器编写控制层代码,编写真实服务的接口,而不去实现接口

主要分为五个模块来编写代码,为什么呢,从设计模式的角度出发,能够解耦,也就是耦合性更低

  • 控制层结构:

  • 服务接口:

  • mapper层:

保留,没有用到,已经分离了,一开始测试时会用到,后面就删了

  • pojo类:

  • 公用包:

堆积这么多张图?是水文???

其实是想让大家看看进度,有没有跟不上进度或遗漏的地方

2.1 配置文件

  • application.properties
############################################################
#
# Server Port
#
############################################################
server.port=8080############################################################
# Server - tomcat
############################################################
# Tomcat URI EncodingiJ1eK1gN4pE0pG1f
server.tomcat.uri-encoding=UTF-8pagehelper.helperDialect=mysql
pagehelper.supportMethodsArguments=true
pagehelper.params=count=countSql# springboot 1.5
#spring.http.multipart.maxFileSize=150Mb
#spring.http.multipart.maxRequestSize=1000Mb
# springboot 2.0
spring.servlet.multipart.max-file-size=150MB
spring.servlet.multipart.max-request-size=1000MB############################################################
#
# Redis
#
############################################################# Redis default use dataBase
#spring.redis.database=0## Redis Host
spring.redis.host=localhost
## Redis Port
spring.redis.port=6379# Redis password
spring.redis.password=your_redis_password#spring.redis.pool.max-active=300
spring.redis.jedis.pool.max-active=300
#spring.redis.pool.max-wait=10000
spring.redis.jedis.pool.max-wait=10000
#spring.redis.pool.maxIdle=300
spring.redis.jedis.pool.max-idle=300
#spring.redis.pool.minIdle=6
spring.redis.jedis.pool.min-idle=6
spring.redis.timeout=0
  • logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration><appender name="console" class="ch.qos.logback.core.ConsoleAppender"><encoder><!--%date{HH:mm:ss.SSS} %c --><pattern>%date{HH:mm:ss.SSS} %c [%t] - %m%n</pattern></encoder></appender><!--<logger name="org.springframework.security.web.FilterChainProxy" level="DEBUG" additivity="false"><appender-ref ref="STDOUT"/></logger><logger name="org.springframework.security.web.access.intercept.FilterSecurityInterceptor" level="DEBUG" additivity="false"><appender-ref ref="STDOUT"/></logger>--><!--<logger name="org.springframework.security.web" level="DEBUG" additivity="false"><appender-ref ref="STDOUT"/></logger>-->
<!--    <logger name="c" level="info" additivity="false">-->
<!--        <appender-ref ref="STDOUT"/>-->
<!--    </logger>--><root level="info"><appender-ref ref="console"/></root>
</configuration>
  • resource.properties
cn.fyupeng.nacos.register-addr=localhost:8848

2.2 启动器和文档接口配置

  • 启动器
package cn.fyupeng;import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;@Slf4j
@SpringBootApplication
@ComponentScan(basePackages = {"cn.fyupeng", "org.n3r.idworker"})
public class FyupengBlogApplication {public static void main(String[] args) {SpringApplication.run(FyupengBlogApplication.class, args);}}
  • 文件接口配置
package cn.fyupeng;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;@Configuration
@EnableSwagger2
public class Swagger2 {@Beanpublic Docket createWebApi(){return new Docket(DocumentationType.SWAGGER_2).groupName("userApi").apiInfo(webApiInfo()).select().apis(RequestHandlerSelectors.basePackage("cn.fyupeng.controller.user"))//.paths(Predicates.and(PathSelectors.regex("/.*"))).paths(PathSelectors.any()).build();}@Beanpublic Docket createAdminApi(){return new Docket(DocumentationType.SWAGGER_2).groupName("adminApi").apiInfo(adminApiInfo()).select().apis(RequestHandlerSelectors.basePackage("cn.fyupeng.controller.admin"))// .paths(Predicates.and(PathSelectors.regex("/admin/.*"))).paths(PathSelectors.any()).build();}private ApiInfo webApiInfo(){return new ApiInfoBuilder()//设置页面标题.title("使用swagger2构建RPC分布式博客管理平台后端user-api接口文档").contact(new Contact("distributed-blog-api - 仓库","git@github.com:fyupeng/distributed-blog-api/blob/main/README.md","fyp010311@163.com")).description("欢迎访问RPC分布式博客管理平台接口文档,本文档描述了博客服务接口定义").version("1.0.1").build();}private ApiInfo adminApiInfo(){return new ApiInfoBuilder()//设置页面标题.title("使用swagger2构建RPC分布式博客管理平台后端admin-api接口文档").contact(new Contact("distributed-blog-api - 仓库","git@github.com:fyupeng/distributed-blog-api/blob/main/README.md","fyp010311@163.com")).description("欢迎访问RPC分布式博客管理平台接口文档,本文档描述了博客服务接口定义").version("1.0.1").build();}}

2.3 拦截器

package cn.fyupeng.interceptor;import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import cn.fyupeng.controller.BasicController;
import cn.fyupeng.annotion.PassToken;
import cn.fyupeng.annotion.UserLoginToken;
import cn.fyupeng.pojo.User;
import cn.fyupeng.service.UserService;
import cn.fyupeng.utils.RedisUtils;
import cn.fyupeng.utils.TokenUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;/*** @Auther: fyp* @Date: 2022/8/18* @Description:* @Package: cn.fyupeng.controller.interceptor* @Version: 1.0*/
@Component
public class LoginInterceptor extends BasicController implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("token");// 从 http 请求头中取出 token// 如果不是映射到方法直接通过if (!(handler instanceof HandlerMethod)) {return true;}HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();//检查方法是否有passtoken注解,有则跳过认证,直接通过if (method.isAnnotationPresent(PassToken.class)) {PassToken passToken = method.getAnnotation(PassToken.class);if (passToken.required()) {return true;}}//检查有没有需要用户权限的注解if (method.isAnnotationPresent(UserLoginToken.class)) {UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);if (userLoginToken.required()) {// 执行认证if (token == null) {throw new RuntimeException("无token,请重新登录");}// 获取 token 中的 user idString userId;String userName;String password;try {userId = JWT.decode(token).getClaim("userId").asString();userName = JWT.decode(token).getClaim("username").asString();password = JWT.decode(token).getClaim("password").asString();} catch (JWTDecodeException j) {throw new RuntimeException("The token is incorrect, please do not create the token by illegal means");}//查询数据库,看看是否存在此用户,方法要自己写UserService userServiceProxy = rpcClientProxy.getProxy(UserService.class);// password 为 MD5 加密密文User user = userServiceProxy.queryUserForLogin(userName, password);if (user == null) {throw new RuntimeException("User does not exist, please log in again");}// 验证 tokenif (TokenUtils.verify(token)) {String userRedisSession = RedisUtils.getUserRedisSession(userId);if(redis.get(userRedisSession) != null)return true;return false;} else {throw new RuntimeException("Token expired or incorrect, please log in again");}}}throw new RuntimeException("Annotation without permission will not pass");}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}

2.4 过滤器

  • Sensitiveilter过滤类
package cn.fyupeng.filter;import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;/*** @Auther: fyp* @Date: 2022/4/13* @Description:* @Package: com.crop.interceptor* @Version: 1.0*/
//敏感词过滤器:利用DFA算法  进行敏感词过滤
public class SensitiveFilter {private SensitiveWordInit sensitiveWordInit;//敏感词过滤器:利用DFA算法  进行敏感词过滤private Map sensitiveWordMap = null;// 最小匹配规则public static int minMatchType = 1;// 最大匹配规则public static int maxMatchType = 2;// 单例private static SensitiveFilter instance = null;// 构造函数,初始化敏感词库private SensitiveFilter() throws IOException {sensitiveWordMap = new SensitiveWordInit().initKeyWord();}// 获取单例public static SensitiveFilter getInstance() throws IOException {if (null == instance) {instance = new SensitiveFilter();}return instance;}// 获取文字中的敏感词public Set<String> getSensitiveWord(String txt, int matchType) {Set<String> sensitiveWordList = new HashSet<String>();for (int i = 0; i < txt.length(); i++) {// 判断是否包含敏感字符int length = CheckSensitiveWord(txt, i, matchType);// 存在,加入list中if (length > 0) {sensitiveWordList.add(txt.substring(i, i + length));// 减1的原因,是因为for会自增i = i + length - 1;}}return sensitiveWordList;}// 替换敏感字字符public String replaceSensitiveWord(String txt, int matchType,String replaceChar) {String resultTxt = txt;// 获取所有的敏感词Set<String> set = getSensitiveWord(txt, matchType);Iterator<String> iterator = set.iterator();String word = null;String replaceString = null;while (iterator.hasNext()) {word = iterator.next();replaceString = getReplaceChars(replaceChar, word.length());resultTxt = resultTxt.replaceAll(word, replaceString);}return resultTxt;}/*** 获取替换字符串** @param replaceChar* @param length* @return*/private String getReplaceChars(String replaceChar, int length) {String resultReplace = replaceChar;for (int i = 1; i < length; i++) {resultReplace += replaceChar;}return resultReplace;}/*** 检查文字中是否包含敏感字符,检查规则如下:<br>* 如果存在,则返回敏感词字符的长度,不存在返回0* @param txt* @param beginIndex* @param matchType* @return*/public int CheckSensitiveWord(String txt, int beginIndex, int matchType) {// 敏感词结束标识位:用于敏感词只有1位的情况boolean flag = false;// 匹配标识数默认为0int matchFlag = 0;Map nowMap = sensitiveWordMap;for (int i = beginIndex; i < txt.length(); i++) {char word = txt.charAt(i);// 获取指定keynowMap = (Map) nowMap.get(word);// 存在,则判断是否为最后一个if (nowMap != null) {// 找到相应key,匹配标识+1matchFlag++;// 如果为最后一个匹配规则,结束循环,返回匹配标识数if ("1".equals(nowMap.get("isEnd"))) {// 结束标志位为trueflag = true;// 最小规则,直接返回,最大规则还需继续查找if (SensitiveFilter.minMatchType == matchType) {break;}}}// 不存在,直接返回else {break;}}if (SensitiveFilter.maxMatchType == matchType){if(matchFlag < 2 || !flag){        //长度必须大于等于1,为词matchFlag = 0;}}if (SensitiveFilter.minMatchType == matchType){if(matchFlag < 2 && !flag){        //长度必须大于等于1,为词matchFlag = 0;}}return matchFlag;}
}
  • SensitiveWordInit单词缓存类
package cn.fyupeng.filter;import org.springframework.core.io.ClassPathResource;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;/*** @Auther: fyp* @Date: 2022/4/13* @Description:* @Package: com.crop.interceptor* @Version: 1.0*/
//屏蔽敏感词初始化
@SuppressWarnings({ "rawtypes", "unchecked" })
public class SensitiveWordInit {// 字符编码private String ENCODING = "UTF-8";// 初始化敏感字库public Map initKeyWord() throws IOException {// 读取敏感词库 ,存入Set中Set<String> wordSet = readSensitiveWordFile();// 将敏感词库加入到HashMap中//确定有穷自动机DFAreturn addSensitiveWordToHashMap(wordSet);}// 读取敏感词库 ,存入HashMap中private Set<String> readSensitiveWordFile() throws IOException {Set<String> wordSet = null;ClassPathResource classPathResource = new ClassPathResource("static/SensitiveWordList.txt");InputStream inputStream = classPathResource.getInputStream();//敏感词库try {// 读取文件输入流InputStreamReader read = new InputStreamReader(inputStream, ENCODING);// 文件是否是文件 和 是否存在wordSet = new HashSet<String>();// StringBuffer sb = new StringBuffer();// BufferedReader是包装类,先把字符读到缓存里,到缓存满了,再读入内存,提高了读的效率。BufferedReader br = new BufferedReader(read);String txt = null;// 读取文件,将文件内容放入到set中while ((txt = br.readLine()) != null) {wordSet.add(txt);}br.close();// 关闭文件流read.close();} catch (Exception e) {e.printStackTrace();}return wordSet;}// 将HashSet中的敏感词,存入HashMap中private Map addSensitiveWordToHashMap(Set<String> wordSet) {// 初始化敏感词容器,减少扩容操作Map wordMap = new HashMap(wordSet.size());for (String word : wordSet) {Map nowMap = wordMap;for (int i = 0; i < word.length(); i++) {// 转换成char型char keyChar = word.charAt(i);// 获取Object tempMap = nowMap.get(keyChar);// 如果存在该key,直接赋值if (tempMap != null) {nowMap = (Map) tempMap;}// 不存在则,则构建一个map,同时将isEnd设置为0,因为他不是最后一个else {// 设置标志位Map<String, String> newMap = new HashMap<>();newMap.put("isEnd", "0");// 添加到集合nowMap.put(keyChar, newMap);nowMap = newMap;}// 最后一个if (i == word.length() - 1) {nowMap.put("isEnd", "1");}}}return wordMap;}
}

2.5 配置类

  • InterceptorConfig拦截器配置类
package cn.fyupeng.config;import cn.fyupeng.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** @Auther: fyp* @Date: 2022/8/18* @Description: 拦截器* @Package: cn.fyupeng.controller.config* @Version: 1.0*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {String[] addPathPatterns= {"/user/**","/admin/**"};String[] excludePathPatterns={"/user/login","/user/regist"};registry.addInterceptor(loginInterceptor).addPathPatterns(addPathPatterns).excludePathPatterns(excludePathPatterns);}
}
  • WebMvc配置类

用于图片的url映射获取

package cn.fyupeng.config;import cn.fyupeng.controller.BasicController;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** @Auther: fyp* @Date: 2022/8/29* @Description:* @Package: cn.fyupeng.config* @Version: 1.0*/
@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler(BasicController.URL_SPACE).addResourceLocations("file:" + BasicController.FILE_SPACE);WebMvcConfigurer.super.addResourceHandlers(registry);}
}

2.6 自定义注解

  • PassToken免校验注解
package cn.fyupeng.annotion;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @Auther: fyp* @Date: 2022/8/18* @Description: 通过Token注解* @Package: cn.fyupeng.controller.annotion* @Version: 1.0*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {boolean required() default true;
}
  • UserLoginToken用户校验注解
package cn.fyupeng.annotion;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @Auther: fyp* @Date: 2022/8/18* @Description: 用户登录Token注解* @Package: cn.fyupeng.controller.annotion* @Version: 1.0*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {boolean required() default true;
}

2.7 控制器

  • 基类控制器
package cn.fyupeng.controller;import cn.fyupeng.loadbalancer.RoundRobinLoadBalancer;
import cn.fyupeng.net.netty.client.NettyClient;
import cn.fyupeng.proxy.RpcClientProxy;
import cn.fyupeng.serializer.CommonSerializer;
import cn.fyupeng.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;import java.io.File;@RestController
public class BasicController {private static final RoundRobinLoadBalancer roundRobinLoadBalancer = new RoundRobinLoadBalancer();private static final NettyClient nettyClient = new NettyClient(roundRobinLoadBalancer, CommonSerializer.KRYO_SERIALIZER);protected RpcClientProxy rpcClientProxy = new RpcClientProxy(nettyClient);@Autowiredpublic RedisOperator redis;public  static final String DATA_NAME = "distributed-blog-data";//文件保存的命名空间public static final String FILE_SPACE =System.getProperties().getProperty("user.home") + File.separator + "webapps" + File.separator + DATA_NAME + File.separator;public static final String URL_SPACE ="/" + DATA_NAME + "/" + "**";//每页分页的记录数public static final Integer ARTICLE_PAGE_SIZE = 6;// 评论分页public static final Integer COMMENT_PAGE_SIZE = 10;// 图片分页public static final Integer PICTURE_PAGE_SIZE = 10;public static final Integer SEARCH_SIZE = 10;public static final Long ONE_WEEK = 604800000L;public static final Long TWO_WEEK = 1209600000L;public static final Long ONE_MONTH = 2592000000L; // 30天public static final Long ONE_YEAR = 31536000000L;}
  • 用户文章控制器
package cn.fyupeng.controller.user;import cn.fyupeng.controller.BasicController;
import cn.fyupeng.annotion.PassToken;
import cn.fyupeng.annotion.UserLoginToken;
import cn.fyupeng.filter.SensitiveFilter;
import cn.fyupeng.pojo.Article;
import cn.fyupeng.pojo.Classfication;
import cn.fyupeng.pojo.vo.ArticleVO;
import cn.fyupeng.service.ArticleService;
import cn.fyupeng.service.ClassficationService;
import cn.fyupeng.service.TagService;
import cn.fyupeng.service.UserService;
import cn.fyupeng.utils.BlogJSONResult;
import cn.fyupeng.utils.PagedResult;
import cn.fyupeng.utils.RedisUtils;
import io.swagger.annotations.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.Cursor;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.*;/*** @Auther: fyp* @Date: 2022/4/2* @Description: 文章 Controller* @Package: com.crop.controller* @Version: 1.0*/@CrossOrigin
@Slf4j
@RestController
@Api(value = "文章相关业务的接口", tags = {"文章相关业务的controller"})
@RequestMapping(value = "/user/article")
public class UserArticleController extends BasicController {private TagService tagServiceProxy = rpcClientProxy.getProxy(TagService.class);private UserService userServiceProxy = rpcClientProxy.getProxy(UserService.class);private ArticleService articleServiceProxy = rpcClientProxy.getProxy(ArticleService.class);private ClassficationService classficationServiceProxy = rpcClientProxy.getProxy(ClassficationService.class);@PassToken@PostMapping(value = "/getAllArticles")@ApiOperation(value = "查找文章信息", notes = "查找文章信息的接口")@ApiImplicitParams({@ApiImplicitParam(name = "searchKey", value = "搜索关键字", required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "page", value = "当前页", required = false, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "pageSize", value = "页大小", required = false, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")})public BlogJSONResult getAllArticles(String searchKey, Integer page, Integer pageSize, String userId) {if (StringUtils.isBlank(userId) || StringUtils.isBlank(searchKey)) {return BlogJSONResult.errorMsg("用户id和搜索关键字不能为空");}boolean userIsExist = userServiceProxy.queryUserIdIsExist(userId);if (!userIsExist) {return BlogJSONResult.errorMsg("用户id不存在");}//非法敏感词汇判断SensitiveFilter filter = null;try {filter = SensitiveFilter.getInstance();} catch (IOException e) {e.printStackTrace();}int n = filter.CheckSensitiveWord(searchKey,0,1);if(n > 0){ //存在非法字符log.info("用户[{}]使用非法字符[{}]进行检索--",userId, searchKey);Set<String> sensitiveWord = filter.getSensitiveWord(searchKey, 1);return BlogJSONResult.errorMsg("捕捉敏感关键字: " + sensitiveWord);}// 进行 热度 维护incrementArticleScore(searchKey);boolean addTrue = addSearchHistory(userId, searchKey);if (!addTrue) {log.info("已存在key:{}",searchKey);}//前端不传该参时会初始化if(page == null){page = 1;}//前端不传该参时会初始化if(pageSize == null){pageSize = ARTICLE_PAGE_SIZE;}List<Article> articleList = null;Classfication classfication = new Classfication();classfication.setName(searchKey);Classfication cf = classficationServiceProxy.queryClassfication(classfication);Article article = new Article();if (cf != null) {article.setClassId(cf.getId());} else {article.setTitle(searchKey);article.setSummary(searchKey);article.setContent(searchKey);}// 优先 匹配 分类 id// 第二 优先 匹配 标题// 第三 匹配 摘要// 第四 优先 匹配 内容PagedResult pageResult = articleServiceProxy.queryArticleSelective(article, page,pageSize);return BlogJSONResult.ok(pageResult);}/*** 策略:*      1. 查询范围:最近一周 数据*      2. 排序方式:时间 最近排序* @param page* @param pageSize* @return*/@PassToken@PostMapping(value = "/getRecentArticles")@ApiOperation(value = "获取最近文章", notes = "获取最近文章的接口")@ApiImplicitParams({@ApiImplicitParam(name = "page", value = "当前页", dataType = "String", paramType = "query"),@ApiImplicitParam(name = "pageSize", value = "页大小", dataType = "String", paramType = "query"),})public BlogJSONResult getRecentArticles(Integer page, Integer pageSize) {//前端不传该参时会初始化if(page == null){page = 1;}//前端不传该参时会初始化if(pageSize == null){pageSize = ARTICLE_PAGE_SIZE;}// 最大 匹配 一 礼拜前的 文章Long timeDifference = ONE_WEEK; // 一周PagedResult pageResult = articleServiceProxy.queryArticleByTime(timeDifference,page,pageSize);return BlogJSONResult.ok(pageResult);}/*** 策略:*      1. 更新频率: 每小时*      2. 更新策略: 用户搜索率最高(过滤敏感信息)* @param page* @param pageSize* @return*/@PassToken@PostMapping(value = "/getHotArticles")@ApiOperation(value = "获取推荐文章", notes = "获取推荐文章的接口")@ApiImplicitParams({@ApiImplicitParam(name = "page", value = "当前页", dataType = "String", paramType = "query"),@ApiImplicitParam(name = "pageSize", value = "页大小", dataType = "String", paramType = "query")})public BlogJSONResult getHotArticles(Integer page, Integer pageSize) {List<String> hotSearchKeyList = getHotSearchKey(null, null); 随机从十条数据中拿出一条来搜索//Random random = new Random();//int index = random.nextInt(hotSearchKeyList.size());/*** 保证 1小时 之内查询的数据一致,1小时更新一次*/int seed = Calendar.HOUR_OF_DAY;// 保证 不越过 数组长度if (hotSearchKeyList.size() == 0) {return BlogJSONResult.ok("缓存数据库中无缓存");}seed %= hotSearchKeyList.size();String searchKey = hotSearchKeyList.get(seed);//前端不传该参时会初始化if(page == null){page = 1;}//前端不传该参时会初始化if(pageSize == null){pageSize = ARTICLE_PAGE_SIZE;}List<Article> articleList = null;Classfication classfication = new Classfication();classfication.setName(searchKey);Classfication cf = classficationServiceProxy.queryClassfication(classfication);Article article = new Article();if (cf != null) {article.setClassId(cf.getId());} else {article.setTitle(searchKey);article.setSummary(searchKey);article.setContent(searchKey);}// 优先 匹配 分类 id// 第二 优先 匹配 标题// 第三 匹配 摘要// 第四 优先 匹配 内容PagedResult pageResult = articleServiceProxy.queryArticleSelective(article, page,pageSize);return BlogJSONResult.ok(pageResult);}@UserLoginToken@PostMapping(value = "/getSearchHistory")@ApiOperation(value = "获取搜索历史记录", notes = "获取搜索历史记录的接口")@ApiImplicitParam(name = "userId",  value = "用户id",required = true, dataType = "String", paramType = "query")public BlogJSONResult getSearchHistory(String userId) {List<String> searchHistoryList = querySearchHistory(userId);return BlogJSONResult.ok(searchHistoryList);}@UserLoginToken@PostMapping(value = "/removeHistory")@ApiOperation(value = "删除搜索历史记录", notes = "删除搜索历史记录的接口")@ApiImplicitParams({@ApiImplicitParam(name = "userId",  value = "用户id",required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "searchKey",  value = "搜索关键字",required = true, dataType = "String", paramType = "query")})public BlogJSONResult removeHistory(String userId, String searchKey) {if (StringUtils.isBlank(userId) || StringUtils.isBlank(searchKey)) {return BlogJSONResult.errorMsg("用户id和搜索关键字不能为空");}Long history = delSearchHistory(userId, searchKey);return BlogJSONResult.ok(history);}/*** 添加个人 历史数据* @param userId* @param searchKey* @return false: 已存在 - true : 添加成功*/private boolean addSearchHistory(String userId, String searchKey) {String searchHistoryKey = RedisUtils.getSearchHistoryKey(userId);boolean keyIsExist = redis.hasKey(searchHistoryKey);if (keyIsExist) {String hk = redis.hget(searchHistoryKey, searchKey);// 关键字 key 存在if (hk != null) {return false;// 关键 key 不存在} else {redis.hset(searchHistoryKey, searchKey, "1");}// 首次 会 创建 包含 userId 的 key} else {redis.hset(searchHistoryKey, searchKey, "1");}return true;}/*** 获取个人 历史数据* @param userId* @return null : 没有数据 - List<String> ${ketList}*/private List<String> querySearchHistory(String userId) {String searchHistoryKey = RedisUtils.getSearchHistoryKey(userId);boolean keyExist = redis.hasKey(searchHistoryKey);if (keyExist) {Cursor<Map.Entry<Object, Object>> cursor = redis.hscan(searchHistoryKey);List<String> keyList = new ArrayList<>();while (cursor.hasNext()) {Map.Entry<Object, Object> current = cursor.next();String key = current.getKey().toString();keyList.add(key);}return keyList;}return null;}/*** 删除个人 历史数据* @param userId* @param searchKey* @return*/private Long delSearchHistory(String userId, String searchKey) {String searchHistoryKey = RedisUtils.getSearchHistoryKey(userId);return redis.hdel(searchHistoryKey, searchKey);}private void incrementArticleScore(String searchKey) {/*** 规则: key: search-score*       value: java-1, linux-2 python-3*/String searchScoreKey = RedisUtils.getSearchScoreKey();Long now = System.currentTimeMillis();// 只需第一次设置 时间戳if (redis.zScore(searchScoreKey, searchKey) == null) {// 统计 时间戳/*** 规则: key: search-score:java*       value: 时间戳*/String keyWithSearchKey = RedisUtils.getSearchScoreKeyWithSearchKey(searchKey);redis.set(keyWithSearchKey, String.valueOf(now));}// 统计 点击量redis.zIncrementScore(searchScoreKey, searchKey, 1);}@PassToken@PostMapping(value = "/getHotSearchKey")@ApiOperation(value = "获取关键字热度", notes = "获取关键字热度的接口")@ApiImplicitParams({@ApiImplicitParam(name = "searchKey", value = "搜索关键字-缺省则匹配热度", dataType = "String", paramType = "query"),@ApiImplicitParam(name = "size", value = "检索数-缺省为10", dataType = "String", paramType = "query")})public BlogJSONResult getHotPot(String searchKey, Integer size) {if (StringUtils.isNotBlank(searchKey)) {//非法敏感词汇判断SensitiveFilter filter = null;try {filter = SensitiveFilter.getInstance();} catch (IOException e) {e.printStackTrace();}int n = filter.CheckSensitiveWord(searchKey,0,1);if(n > 0){ //存在非法字符log.info("使用非法字符[{}]进行检索热度--",searchKey);Set<String> sensitiveWord = filter.getSensitiveWord(searchKey, 1);return BlogJSONResult.ok(sensitiveWord);}}List<String> hotSearchKeyList = getHotSearchKey(searchKey, size);return BlogJSONResult.ok(hotSearchKeyList);}/*** 搜索引擎* @param searchKey* @param size* @return*/public List<String> getHotSearchKey(String searchKey, Integer size) {if (size == null) {size = SEARCH_SIZE;}Long now = System.currentTimeMillis();String searchScoreKey = RedisUtils.getSearchScoreKey();Set<String> allKeys = redis.zRevRangeByScore(searchScoreKey, 0, Double.MAX_VALUE);List<String> resultList = new ArrayList<>();if (!StringUtils.isBlank(searchKey)) {for (String key : allKeys) {// 在所有的 key 中包含 用户输入的 ${searchKey}if (StringUtils.containsIgnoreCase(key, searchKey)) {// 记录数 已达 期待数,停止检索if (resultList.size() >= size) {break;}// 规则:与 新增 关键字 时做的 时间戳 key 值相对应String searchScoreKeyWithSearchKey = RedisUtils.getSearchScoreKeyWithSearchKey(key);String timeStamp = redis.get(searchScoreKeyWithSearchKey);if (timeStamp != null) {Long time = Long.valueOf(timeStamp);// 查询 最近一个礼拜的 数据if ((now - time) < 604800000L) {resultList.add(key);} else {redis.zset(searchScoreKey, key, 0);}}}}} else {for (String key : allKeys) {// 在所有的 key 中包含 用户输入的 ${searchKey}// 记录数 已达 期待数,停止检索if (resultList.size() >= size) {break;}// 规则:与 新增 关键字 时做的 时间戳 key 值相对应String searchScoreKeyWithSearchKey = RedisUtils.getSearchScoreKeyWithSearchKey(key);String timeStamp = redis.get(searchScoreKeyWithSearchKey);if (timeStamp != null) {Long time = Long.valueOf(timeStamp);// 查询 最近一个礼拜的 数据if ((now - time) < 604800000L) {resultList.add(key);} else {redis.zset(searchScoreKey, key, 0);}}}}return resultList;}@PassToken@PostMapping(value = "/getArticleDetail")@ApiOperation(value = "获取文章详细信息", notes = "获取文章详细信息的接口")@ApiImplicitParams({@ApiImplicitParam(name = "articleId", value = "文章id", required = true, dataType = "String", paramType = "query"),//@ApiImplicitParam(name = "request",value = "请求", dataType = "HttpServletRequest", readOnly = true)})public BlogJSONResult getArticleDetail(String articleId, @ApiParam(hidden = true)HttpServletRequest request) {if (StringUtils.isBlank(articleId)) {return BlogJSONResult.errorMsg("文章id不能为空");}Article article = new Article();article.setId(articleId);ArticleVO articleVO = articleServiceProxy.queryArticleDetail(articleId);// 文章 id 不存在if (articleVO == null) {return BlogJSONResult.ok("articleId不存在");}String articleView = RedisUtils.getIdView(articleVO.getId(), request);String articleViewCount = RedisUtils.getIdViewCount(articleVO.getId());//用户短时间 已 访问,redis.get(userView) 返回值是 对象,不能 通过 ""判断if (redis.get(articleView) != null) {return BlogJSONResult.ok(articleVO);}/*** 统计量 一小时 内 同一个 ip 地址 只能算 1 次 阅读*/redis.set(articleView, "1", 60 * 60);/*** articleViewCount 如果 未 初始化,redis 会 初始化为 0*/redis.incr(articleViewCount,1);return BlogJSONResult.ok(articleVO);}@UserLoginToken@PostMapping(value = "/saveArticle")@ApiOperation(value = "保存文章信息 - id 字段请忽略", notes = "保存文章信息的接口")@ApiImplicitParam(name = "article", value = "文章", required = true, dataType = "Article", paramType = "body")public BlogJSONResult saveArticle(@RequestBody Article article) {if(StringUtils.isBlank(article.getUserId()) || StringUtils.isBlank(article.getTitle())|| StringUtils.isBlank(article.getSummary() ) || StringUtils.isBlank(article.getClassId())|| StringUtils.isBlank(article.getContent())){return BlogJSONResult.errorMsg("用户id、标题、摘要、分类id和内容不能为空");}boolean userIdIsExist = userServiceProxy.queryUserIdIsExist(article.getUserId());if (!userIdIsExist) {return BlogJSONResult.errorMsg("用户id不存在");}boolean classficationIsExist = classficationServiceProxy.queryClassficationIdIsExist(article.getClassId());if (!classficationIsExist) {return BlogJSONResult.errorMsg("分类id不存在");}boolean saveIsTrue = articleServiceProxy.save(article);return saveIsTrue ? BlogJSONResult.ok() : BlogJSONResult.errorMsg("内部错误导致保存失败");}@UserLoginToken@PostMapping(value = "/updateArticle")@ApiOperation(value = "更新文章信息", notes = "更新文章信息的接口")@ApiImplicitParam(name = "article", value = "文章", required = true, dataType = "Article", paramType = "body")public BlogJSONResult updateArticle(@RequestBody  Article article) {if (StringUtils.isBlank(article.getId()) || StringUtils.isBlank(article.getUserId())) {return BlogJSONResult.errorMsg("articleId 和 userId 不能为空");}Article articleWithIdAndUserId =  new Article();articleWithIdAndUserId.setId(article.getId());articleWithIdAndUserId.setUserId(article.getUserId());// 文章id 属于 用户idboolean articleIsUser = articleServiceProxy.queryArticleIsUser(articleWithIdAndUserId);if (articleIsUser) {Article ac = new Article();ac.setId(article.getId());ac.setUserId(article.getUserId());// 文章 标题 要更新if (StringUtils.isNotBlank(article.getTitle())) {ac.setTitle(article.getTitle());}// 文章 摘要 要更新if (StringUtils.isNotBlank(article.getSummary())) {ac.setSummary(article.getSummary());}// 文章 内容 要更新if (StringUtils.isNotBlank(article.getContent())) {ac.setContent(article.getContent());}// 文章 分类 要更新if (StringUtils.isNotBlank(article.getClassId())) {boolean classficationIdIsExist = classficationServiceProxy.queryClassficationIdIsExist(article.getClassId());if (!classficationIdIsExist)return BlogJSONResult.errorMsg("分类id不存在");ac.setClassId(article.getClassId());}boolean updateIsTrue = articleServiceProxy.saveWithIdAndUserId(ac);return updateIsTrue ? BlogJSONResult.ok() : BlogJSONResult.errorMsg("内部错误更新失败");}return BlogJSONResult.errorMsg("用户 id 与 articleId 不存在或匹配失败");}@UserLoginToken@PostMapping(value = "/removeArticle")@ApiOperation(value = "删除文章", notes = "删除文章的接口")@ApiImplicitParams({@ApiImplicitParam(name = "articleId", value = "文章id", required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")})public BlogJSONResult removeArticle(String articleId, String userId) {if (StringUtils.isBlank(articleId) || StringUtils.isBlank(userId)) {return BlogJSONResult.errorMsg("articleId或userId不能为空");}Article article = new Article();article.setId(articleId);article.setUserId(userId);boolean articleIsUser = articleServiceProxy.queryArticleIsUser(article);if (!articleIsUser) {return BlogJSONResult.errorMsg("articleId不存在或者userId与commentId约束的userId不同");}/*** 删除文章 以及删除 与 文章 id 关联的 其他表数据*/articleServiceProxy.removeArticle(articleId);return BlogJSONResult.ok();}@PassToken@PostMapping(value = "/getAllClassfications")@ApiOperation(value = "获取文章分类信息", notes = "获取文章分类信息的接口")public BlogJSONResult getAllClassfications() {List<Classfication> classficationList = classficationServiceProxy.queryAllClassfications();return BlogJSONResult.ok(classficationList);}}
  • 用户控制器
package cn.fyupeng.controller.user;import cn.fyupeng.controller.BasicController;
import cn.fyupeng.annotion.PassToken;
import cn.fyupeng.annotion.UserLoginToken;
import cn.fyupeng.pojo.User;
import cn.fyupeng.pojo.UserInfo;
import cn.fyupeng.pojo.vo.UserForUpdateVO;
import cn.fyupeng.pojo.vo.UserInfoVO;
import cn.fyupeng.service.UserService;
import cn.fyupeng.utils.BlogJSONResult;
import cn.fyupeng.utils.MD5Utils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;@CrossOrigin
@RestController
@RequestMapping(value = "/user")
@Api(value = "用户相关业务的接口", tags = {"用户相关业务的controller"})
public class UserController extends BasicController {UserService userServiceProxy = rpcClientProxy.getProxy(UserService.class);@PassToken@GetMapping(value = "/pingNetWork")@ApiOperation(value = "测试网络环境", notes = "测试网络环境的接口")public BlogJSONResult pingNetWork() {return BlogJSONResult.build(200, "ping successful!", null);}@PassToken@ApiOperation(value = "查询用户信息", notes = "查询用户信息的接口")@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")@PostMapping(value = "/query")public BlogJSONResult query(String userId) {if(StringUtils.isBlank(userId)){return BlogJSONResult.errorMsg("用户id不能为空");}UserInfo userInfo = userServiceProxy.queryUserInfo(userId);UserInfoVO userInfoVO = new UserInfoVO();if (userInfo != null) {BeanUtils.copyProperties(userInfo,userInfoVO);User user = userServiceProxy.queryUser(userId);userInfoVO.setPermission(user.getPermission());}return BlogJSONResult.ok(userInfoVO);}@UserLoginToken@ApiOperation(value = "完善个人信息 - id字段请忽略", notes = "完善个人信息的接口")@ApiImplicitParam(name = "userInfo", value = "用户详情", required = true, dataType = "UserInfo", paramType = "body")@PostMapping(value = "/completeUserInfo")public BlogJSONResult completeUserInfo(@RequestBody UserInfo userInfo) {if (StringUtils.isBlank(userInfo.getUserId())) {return BlogJSONResult.errorMsg("用户id不能为空");}UserInfo userInfoByUserId = userServiceProxy.queryUserInfo(userInfo.getUserId());if (userInfoByUserId == null) {return BlogJSONResult.errorMsg("用户id不存在");}// id 是唯一标识符,不可更改userInfo.setId(userInfoByUserId.getId());userServiceProxy.updateUserInfo(userInfo);return BlogJSONResult.ok();}@UserLoginToken@ApiOperation(value = "用户上传头像", notes = "用户上传头像的接口")@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "form")@PostMapping(value = "/uploadFace", headers = "content-type=multipart/form-data")public BlogJSONResult uploadFace(@RequestParam(value = "userId") String userId,@RequestParam(value = "file") /* 这两个注解不能搭配使用,会导致 文件上传按钮失效*//*@ApiParam(value = "头像")*/ MultipartFile file) throws Exception{if(StringUtils.isBlank(userId)){return BlogJSONResult.errorMsg("用户id不能为空");}if (!userServiceProxy.queryUserIdIsExist(userId)) {return BlogJSONResult.errorMsg("用户id不存在");}//保存到数据库中的相对路径String uploadPathDB = "/" + userId + "/face";FileOutputStream fileOutputStream = null;InputStream inputStream = null;try {if(file != null){String fileName = file.getOriginalFilename();if (StringUtils.isNotBlank(fileName)) {//文件上传的最终保存路径String finalFacePath = FILE_SPACE + uploadPathDB + "/" + fileName;//设置数据库保存的路径uploadPathDB += ("/" + fileName);File outFile = new File(finalFacePath);//创建用户文件夹if (outFile.getParentFile() != null && !outFile.getParentFile().isDirectory()) {//创建父文件夹outFile.getParentFile().mkdirs();}fileOutputStream = new FileOutputStream(outFile);inputStream = file.getInputStream();IOUtils.copy(inputStream, fileOutputStream);//把输入流赋值给输出流,就是把图片复制到输出流对应的路径下}}else {return BlogJSONResult.errorMsg("上传出错1....");}} catch (IOException e) {e.printStackTrace();return BlogJSONResult.errorMsg("上传出错2....");}finally {if(fileOutputStream != null){fileOutputStream.flush();fileOutputStream.close();}}/*** User 与 UserInfo 是 一一对应的关系,UserInfo 有两个候选键 id 和 userId*/UserInfo userInfo = new UserInfo();userInfo.setUserId(userId);userInfo.setAvatar(uploadPathDB);userServiceProxy.updateUserInfo(userInfo);return BlogJSONResult.ok(uploadPathDB);}@UserLoginToken@ApiOperation(value = "用户修改密码", notes = "用户修改密码的接口")@ApiImplicitParam(name = "userVO", value = "用户id", required = true, dataType = "UserForUpdateVO", paramType = "body")@PostMapping(value = "/updatePassword")public BlogJSONResult updatePassword(@RequestBody UserForUpdateVO user) throws Exception {if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getOldPassword())) {return BlogJSONResult.errorMsg("用户名和原密码不能为空");}if (StringUtils.isBlank(user.getNewPassword())) {return BlogJSONResult.errorMsg("新密码不能为空");}User userResult = userServiceProxy.queryUserForLogin(user.getUsername(), MD5Utils.getMD5Str(user.getOldPassword()));if (userResult == null) {BlogJSONResult.errorMsg("用户名或原密码不正确");}User userForUpdate = new User();userForUpdate.setId(userResult.getId());userForUpdate.setUsername(user.getUsername());userForUpdate.setPassword(MD5Utils.getMD5Str(user.getNewPassword()));userForUpdate.setPermission(userResult.getPermission());boolean updateTrue = userServiceProxy.updateUser(userForUpdate);userForUpdate.setPassword("");return updateTrue ? BlogJSONResult.ok(userForUpdate) : BlogJSONResult.errorMsg("修改失败");}}
  • 用户标签控制器
package cn.fyupeng.controller.user;import cn.fyupeng.annotion.UserLoginToken;
import cn.fyupeng.pojo.Articles2tags;
import cn.fyupeng.pojo.Tag;
import cn.fyupeng.pojo.vo.ArticleVO;
import cn.fyupeng.pojo.vo.Articles2tagsVO;
import cn.fyupeng.pojo.vo.TagVO;
import cn.fyupeng.service.*;
import cn.fyupeng.controller.BasicController;
import cn.fyupeng.utils.BlogJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;import java.util.List;/*** @Auther: fyp* @Date: 2022/4/8* @Description:* @Package: com.crop.user.controller* @Version: 1.0*/@CrossOrigin
@Slf4j
@RestController
@Api(value = "标签相关业务的接口", tags = {"标签相关业务的controller"})
@RequestMapping(value = "/user/tag")
public class UserTagController extends BasicController {private TagService tagServiceProxy = rpcClientProxy.getProxy(TagService.class);private UserService userServiceProxy = rpcClientProxy.getProxy(UserService.class);private ArticleService articleServiceProxy = rpcClientProxy.getProxy(ArticleService.class);@UserLoginToken@PostMapping(value = "/getTag")@ApiOperation(value = "获取标签", notes = "获取标签的接口")@ApiImplicitParam(name = "tagId", value = "标签id", required = true, dataType = "String", paramType = "query")public BlogJSONResult getTag(String tagId) {if (StringUtils.isBlank(tagId)) {return BlogJSONResult.errorMsg("标签id不能为空");}Tag tagList = tagServiceProxy.queryTag(tagId);return BlogJSONResult.ok(tagList);}@UserLoginToken@PostMapping(value = "/getAllTags")@ApiOperation(value = "获取所有标签", notes = "获取所有标签的接口")@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")public BlogJSONResult getAllTags(String userId) {Tag tagWithUserId = new Tag();tagWithUserId.setUserId(userId);List<TagVO> tagVOList = tagServiceProxy.queryAllTags(tagWithUserId);return BlogJSONResult.ok(tagVOList);}@UserLoginToken@PostMapping(value = "/saveTag")@ApiOperation(value = "保存标签 - id字段请忽略", notes = "保存标签的接口")@ApiImplicitParam(name = "tag", value = "标签", required = true, dataType = "Tag", paramType = "body")public BlogJSONResult saveTag(@RequestBody Tag tag) {if (StringUtils.isBlank(tag.getName()) || StringUtils.isBlank(tag.getUserId())) {return BlogJSONResult.errorMsg("标签名或用户id不能为空");}tag.setId(null);/*** id 是候选键 - 标签名 + userId 也是候选键,能唯一 识别 标签*/if (tagServiceProxy.queryTagIsExist(tag)) {return BlogJSONResult.errorMsg("标签名已存在");}boolean saveIsTrue = tagServiceProxy.saveTag(tag);return saveIsTrue ? BlogJSONResult.ok(): BlogJSONResult.errorMsg("内部错误导致保存失败");}@UserLoginToken@PostMapping(value = "/updateTag")@ApiOperation(value = "更新标签 - userId字段请忽略", notes = "更新标签的接口")@ApiImplicitParam(name = "tag", value = "标签", required = true, dataType = "Tag", paramType = "body")public BlogJSONResult updateTag(@RequestBody Tag tag) {if (StringUtils.isBlank(tag.getId()) || StringUtils.isBlank(tag.getName())) {return BlogJSONResult.errorMsg("标签id或标签名不能为空");}// 用來 防止 其他 用户 对非本地 标签的 更改if (StringUtils.isBlank(tag.getUserId())) {return BlogJSONResult.errorMsg("用户id不能为空");}Tag tagWithIdAndUserId = new Tag();tagWithIdAndUserId.setId(tag.getId());tagWithIdAndUserId.setUserId(tag.getUserId());if (!tagServiceProxy.queryTagIsExist(tagWithIdAndUserId)) {return BlogJSONResult.errorMsg("该用户没有该标签");}boolean updateIsTrue = tagServiceProxy.updateTag(tag);return updateIsTrue ? BlogJSONResult.ok() : BlogJSONResult.errorMsg("内部错误");}@UserLoginToken@PostMapping(value = "/removeTag")@ApiOperation(value = "删除标签 - 连同已标记的文章标签一并删除", notes = "删除标签签的接口")@ApiImplicitParam(name = "tagId", value = "标签id", required = true, dataType = "String", paramType = "query")public BlogJSONResult removeTag(String tagId) {if (StringUtils.isBlank(tagId)) {return BlogJSONResult.errorMsg("tagId不能为空");}// 1. 移除 Tag// 2. 移除 Articles2Tagsboolean delTagIsTrue = tagServiceProxy.deleteTagAndArticleTagWithTagId(tagId);if (!delTagIsTrue) {return BlogJSONResult.errorMsg("标签id不存在或内部错误导致删除失败");}return BlogJSONResult.ok();}@UserLoginToken@PostMapping(value = "/getArticleWithNoneTag")@ApiOperation(value = "获取无标签文章", notes = "获取无标签文章的接口")@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")public BlogJSONResult getArticleWithNoneTag(String userId) {if (StringUtils.isBlank(userId)) {return BlogJSONResult.errorMsg("用户id不能为空");}if (!userServiceProxy.queryUserIdIsExist(userId)) {return BlogJSONResult.errorMsg("用户id不存在");}List<ArticleVO> articleVOList = articleServiceProxy.queryArticleWithNoneTagByUser(userId);return BlogJSONResult.ok(articleVOList);}@UserLoginToken@PostMapping(value = "/getArticleTag")@ApiOperation(value = "获取标签文章", notes = "获取标签文章的接口")@ApiImplicitParam(name = "tagId", value = "标签id", required = true, dataType = "String", paramType = "query")public BlogJSONResult getArticleTag(String tagId) {if (StringUtils.isBlank(tagId)) {return BlogJSONResult.errorMsg("标签id不能为空");}Articles2tags articles2tagsWithTagId = new Articles2tags();articles2tagsWithTagId.setTagId(tagId);List<Articles2tagsVO> articles2tagsVOList = tagServiceProxy.queryArticleTag(articles2tagsWithTagId);return BlogJSONResult.ok(articles2tagsVOList);}@UserLoginToken@PostMapping(value = "/markArticleTag")@ApiOperation(value = "标记文章标签 - id字段请忽略", notes = "标记文章标签的接口")@ApiImplicitParam(name = "articles2tags", value = "文章标签关联", required = true, dataType = "Articles2tags", paramType = "body")public BlogJSONResult markArticleTag(@RequestBody Articles2tags articles2tags) {if (StringUtils.isBlank(articles2tags.getArticleId()) || StringUtils.isBlank(articles2tags.getTagId())) {return BlogJSONResult.errorMsg("文章id或标签id不能为空");}boolean articleIsExist = articleServiceProxy.queryArticleIsExist(articles2tags.getArticleId());Tag tagWithId = new Tag();tagWithId.setId(articles2tags.getTagId());boolean tagIsExist = tagServiceProxy.queryTagIsExist(tagWithId);if (!articleIsExist || !tagIsExist) {return BlogJSONResult.errorMsg("文章id或标签id不存在");}articles2tags.setId(null);// 已标记 的 不能 重复标记 - 文章id和标签id 也是一组 候选键if (tagServiceProxy.queryArticleTagIsExist(articles2tags)) {return BlogJSONResult.errorMsg("关联id已存在,不可重复标记");}// 标记 Articles2Tagsboolean saveIsTrue = tagServiceProxy.saveArticleTag(articles2tags);return saveIsTrue ? BlogJSONResult.ok() : BlogJSONResult.errorMsg("内部错误导致保存失败");}@UserLoginToken@PostMapping(value = "/reMarkArticleTag")@ApiOperation(value = "重新标记文章标签", notes = "重新标记文章标签的接口")@ApiImplicitParam(name = "articles2tags", value = "文章标签关联", required = true, dataType = "Articles2tags", paramType = "body")public BlogJSONResult reMarkArticleTag(@RequestBody Articles2tags articles2tags) {if (StringUtils.isBlank(articles2tags.getId())) {return BlogJSONResult.errorMsg("文章标签关联id不能为空");}// 重新标记 Articles2Tagsif (StringUtils.isBlank(articles2tags.getArticleId()) || StringUtils.isBlank(articles2tags.getTagId())) {return BlogJSONResult.errorMsg("文章id或标签id不能为空");}boolean articleIsExist = articleServiceProxy.queryArticleIsExist(articles2tags.getArticleId());Tag tagWithId = new Tag();tagWithId.setId(articles2tags.getTagId());boolean tagIsExist = tagServiceProxy.queryTagIsExist(tagWithId);if (!articleIsExist || !tagIsExist) {return BlogJSONResult.errorMsg("文章id或标签id不存在");}// 已标记 的 才可以更新if (!tagServiceProxy.queryArticleTagIsExist(articles2tags.getId())) {return BlogJSONResult.errorMsg("关联id不存在");}// 标记 Articles2Tagsboolean saveIsTrue = tagServiceProxy.updateArticleTag(articles2tags);return saveIsTrue ? BlogJSONResult.ok() : BlogJSONResult.errorMsg("内部错误导致保存失败");}}
  • 用户评论控制器
package cn.fyupeng.controller.user;import cn.fyupeng.annotion.PassToken;
import cn.fyupeng.annotion.UserLoginToken;
import cn.fyupeng.enums.CommentStatus;
import cn.fyupeng.pojo.Comment;
import cn.fyupeng.pojo.User;
import cn.fyupeng.service.*;
import cn.fyupeng.utils.BlogJSONResult;
import cn.fyupeng.utils.PagedResult;
import cn.fyupeng.controller.BasicController;
import cn.fyupeng.service.ArticleService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;/*** @Auther: fyp* @Date: 2022/4/8* @Description:* @Package: com.crop.user.controller* @Version: 1.0*/@CrossOrigin
@Slf4j
@RestController
@Api(value = "评论相关业务的接口", tags = {"评论相关业务的controller"})
@RequestMapping(value = "/user/comment")
public class UserCommentController extends BasicController {private TagService tagServiceProxyProxy = rpcClientProxy.getProxy(TagService.class);private UserService userServiceProxy = rpcClientProxy.getProxy(UserService.class);private ArticleService articleServiceProxy = rpcClientProxy.getProxy(ArticleService.class);private ClassficationService classficationServiceProxy = rpcClientProxy.getProxy(ClassficationService.class);private CommentService commentServiceProxy = rpcClientProxy.getProxy(CommentService.class);@PassToken@PostMapping(value = "/getAllComments")@ApiOperation(value = "获取文章所有评论", notes = "获取文章所有评论的接口")@ApiImplicitParams({@ApiImplicitParam(name = "articleId", value = "文章id", required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "page", value = "当前页", dataType = "Integer", paramType = "query"),@ApiImplicitParam(name = "pageSize", value = "页数", dataType = "Integer", paramType = "query"),@ApiImplicitParam(name = "sort", value = "排序-1-缺省[正序时间]-2[倒序时间]", dataType = "Integer", paramType = "query")})public BlogJSONResult getAllComments(String articleId, Integer page, Integer pageSize, Integer sort) {if (StringUtils.isBlank(articleId)) {return BlogJSONResult.errorMsg("文章id不能为空");}//前端不传该参时会初始化if(page == null){page = 1;}//前端不传该参时会初始化if(pageSize == null){pageSize = COMMENT_PAGE_SIZE;}if (sort == null) {sort = 1;}PagedResult pageResult = commentServiceProxy.queryAllComments(articleId, page, pageSize, sort);return BlogJSONResult.ok(pageResult);}@UserLoginToken@PostMapping(value = "saveComment")@ApiOperation(value = "发表文章评论", notes = "发表文章评论的接口")@ApiImplicitParam(name = "comment", value = "评论", required = true, dataType = "Comment", paramType = "body")public BlogJSONResult saveComment(@RequestBody Comment comment) {if (StringUtils.isBlank(comment.getArticleId())) {return BlogJSONResult.errorMsg("articleId不能为空");}if (StringUtils.isBlank(comment.getFromUserId())) {return BlogJSONResult.errorMsg("留言者userId不能为空");}if (StringUtils.isBlank(comment.getComment())) {return BlogJSONResult.errorMsg("评论内容comment不能为空");}// fateherCommentId 是可以为 null 但是 不能是 空串 或 空白串if (comment.getFatherCommentId() != null && StringUtils.isBlank(comment.getFatherCommentId())) {return BlogJSONResult.errorMsg("不允许fatherCommentId为空串");}// toUserId 是可以为 null 但是 不能是 空串 或 空白串if (comment.getToUserId() != null && StringUtils.isBlank(comment.getToUserId())) {return BlogJSONResult.errorMsg("不允许toUserId为空串");}if (StringUtils.isNotBlank(comment.getToUserId()) && comment.getToUserId().equals(comment.getFromUserId())) {return BlogJSONResult.errorMsg("不能回复toUserId为fromUserId");}boolean articleIsExist = articleServiceProxy.queryArticleIsExist(comment.getArticleId());boolean userIdIsExist = userServiceProxy.queryUserIdIsExist(comment.getFromUserId());if (!articleIsExist || !userIdIsExist) {return BlogJSONResult.errorMsg("文章id不存在或留言者id不存在");}// 父 评论 验证if (StringUtils.isNotBlank(comment.getFatherCommentId())) {boolean fatherCommentIdIsExist = commentServiceProxy.queryCommentIsExist(comment.getFatherCommentId());if (!fatherCommentIdIsExist) {return BlogJSONResult.errorMsg("父评论id不存在");}}// 被 回复用户验证if (StringUtils.isNotBlank(comment.getToUserId())) {boolean toUserIsExist = userServiceProxy.queryUserIdIsExist(comment.getToUserId());if (!toUserIsExist) {return BlogJSONResult.errorMsg("被回复用户id不存在");}}boolean saveIsTrue = commentServiceProxy.saveComment(comment);return saveIsTrue ? BlogJSONResult.ok() : BlogJSONResult.errorMsg("内部错误导致保存失败");}@UserLoginToken@PostMapping(value = "/updateMyComment")@ApiOperation(value = "更新评论", notes = "更新评论的接口")@ApiImplicitParams({@ApiImplicitParam(name = "commentId", value = "评论id", required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "content", value = "更新内容", required = true, dataType = "String", paramType = "query")})public BlogJSONResult updateMyComment(String commentId, String userId, String content) {if (StringUtils.isBlank(commentId) || StringUtils.isBlank(userId)) {return BlogJSONResult.errorMsg("commentId或userId不能为空");}Comment comment = commentServiceProxy.queryComment(commentId);if (comment == null || !comment.getFromUserId().equals(userId)) {return BlogJSONResult.errorMsg("commentId不存在或者userId与commentId约束的userId不同");}comment.setComment(content);boolean commentIsUpdate = commentServiceProxy.updateComment(comment);return commentIsUpdate ? BlogJSONResult.ok() : BlogJSONResult.errorMsg("内部错误导致更新失败");}/*** 该方法 已被 弃用 建议使用 public com.crop.utils.BlogJSONResult rollbackMyComment(String commentId, String userId)* @param commentId* @param userId* @return*/@Deprecated@UserLoginToken@PostMapping(value = "/removeMyComment")@ApiOperation(value = "删除评论 - 已废弃", notes = "删除评论的接口")@ApiImplicitParams({@ApiImplicitParam(name = "commentId", value = "评论id", required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")})public BlogJSONResult removeMyComment(String commentId, String userId) {if (StringUtils.isBlank(commentId) || StringUtils.isBlank(userId)) {return BlogJSONResult.errorMsg("commentId或userId不能为空");}Comment comment = commentServiceProxy.queryComment(commentId);if (comment == null || !comment.getFromUserId().equals(userId)) {return BlogJSONResult.errorMsg("commentId不存在或者userId与commentId约束的userId不同");}// 如果评论 已经被 追评,则不可撤销boolean commentWithFatherCommentIsExist = commentServiceProxy.queryCommentWithFatherCommentIsExist(commentId);if (commentWithFatherCommentIsExist) {return BlogJSONResult.errorMsg("有子评论约束,普通用户无权限");}commentServiceProxy.removeCommentById(commentId);return BlogJSONResult.ok();}@UserLoginToken@PostMapping(value = "/rollbackMyComment")@ApiOperation(value = "撤回评论", notes = "撤回评论的接口")@ApiImplicitParams({@ApiImplicitParam(name = "commentId", value = "评论id", required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")})public BlogJSONResult rollbackMyComment(String commentId, String userId) {if (StringUtils.isBlank(commentId)) {return BlogJSONResult.errorMsg("评论id不能为空");}if (StringUtils.isBlank(userId)) {return BlogJSONResult.errorMsg("用户id不能为空");}Comment comment = commentServiceProxy.queryComment(commentId);if (comment == null) {return BlogJSONResult.errorMsg("评论id不存在");}User user = userServiceProxy.queryUser(userId);if (user == null) {return BlogJSONResult.errorMsg("用户id不存在");}if (!comment.getFromUserId().equals(user.getId())) {return BlogJSONResult.errorMsg("非本用户评论无权撤回");}commentServiceProxy.setCommentStatusWithFatherId(comment, CommentStatus.BLOCKED);return BlogJSONResult.ok();}}
  • 用户注册与登录控制器
package cn.fyupeng.controller.user;import cn.fyupeng.annotion.PassToken;
import cn.fyupeng.controller.BasicController;
import cn.fyupeng.annotion.UserLoginToken;
import cn.fyupeng.pojo.User;
import cn.fyupeng.pojo.vo.UserVO;
import cn.fyupeng.service.UserService;
import cn.fyupeng.utils.BlogJSONResult;
import cn.fyupeng.utils.MD5Utils;
import cn.fyupeng.utils.RedisUtils;
import cn.fyupeng.utils.TokenUtils;
import com.auth0.jwt.JWT;
import com.sun.corba.se.spi.servicecontext.UEInfoServiceContext;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import jdk.nashorn.internal.parser.Token;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;/*** @Auther: fyp* @Date: 2022/8/17* @Description: 注册和登录 控制器* @Package: cn.fyupeng.controller* @Version: 1.0*/@CrossOrigin
@RestController
@RequestMapping(value = "/user")
@Api(value = "用户注册登录的接口", tags = {"注册和登录的controller"})
public class UserRegisterAndLoginController extends BasicController {private UserService userServiceProxy = rpcClientProxy.getProxy(UserService.class);/*** 拦截器不拦截注册,所以不需要注解 @PassToken* @param user* @return* @throws Exception*/@ApiOperation(value = "用户注册", notes = "用户注册的接口")@ApiImplicitParam(name = "user", value = "用户", required = true, dataType = "User", paramType = "body")@PostMapping(value = "/regist")public BlogJSONResult regist(@RequestBody User user) throws Exception {//1. 判断用户名和密码必须不为空if(StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())){return BlogJSONResult.errorMsg("用户名和密码不能为空");}//2. 判断用户是否存在boolean usernameIsExist = userServiceProxy.queryUsernameIsExist(user.getUsername());//3. 保存用户,注册信息if(!usernameIsExist){user.setPassword(MD5Utils.getMD5Str(user.getPassword()));// 防止 被 注入user.setPermission(2);userServiceProxy.saveUser(user);}else {return BlogJSONResult.errorMsg("用户名已存在");}user.setPassword("");//UserVO userVO = setUserRedisSessionToken(user);return BlogJSONResult.ok(user);}public UserVO setUserRedisSessionToken(User userModel) throws Exception {//String uniqueToken = UUID.randomUUID().toString();String token = TokenUtils.token(userModel.getId(), userModel.getUsername(), userModel.getPassword());String userRedisSession = RedisUtils.getUserRedisSession(userModel.getId());redis.set(userRedisSession, token,  60 * 5);UserVO usersVO = new UserVO();BeanUtils.copyProperties(userModel, usersVO);usersVO.setUserToken(token);return usersVO;}/*** 拦截器不拦截登录,所以不需要注解 @PassToken* @param user* @return* @throws Exception*/@ApiOperation(value = "用户登录", notes = "用户登录的接口")@ApiImplicitParam(name = "user", value = "用户", required = true, dataType = "User", paramType = "body")@PostMapping(value = "/login")public BlogJSONResult login(@RequestBody User user) throws Exception {String username = user.getUsername();String password = user.getPassword();if(StringUtils.isBlank(username) || StringUtils.isBlank(password)){return BlogJSONResult.errorMsg("用户名或密码不能为空....");}User userResult = userServiceProxy.queryUserForLogin(username, MD5Utils.getMD5Str(user.getPassword()));if(userResult == null || userResult.getPermission() != 2){return BlogJSONResult.errorMsg("用户名密码不正确,或为非用户登录");}else {UserVO usersVO = setUserRedisSessionToken(userResult);usersVO.setPassword("");return BlogJSONResult.ok(usersVO);}}@UserLoginToken@ApiOperation(value = "更新秘钥", notes = "用户更新秘钥的接口")@ApiImplicitParam(name = "token", value = "秘钥", required = true, dataType = "String", paramType = "query")@PostMapping(value = "/updateToken")public BlogJSONResult updateToken(String token) throws Exception {String userId;String username;String password;System.out.println(token);userId = JWT.decode(token).getClaim("userId").asString();username = JWT.decode(token).getClaim("username").asString();password = JWT.decode(token).getClaim("password").asString();if(StringUtils.isBlank(username) || StringUtils.isBlank(password)){return BlogJSONResult.errorMsg("用户名或密码不能为空....");}User userResult = userServiceProxy.queryUserForLogin(username, password);if(userResult == null){return BlogJSONResult.errorMsg("Illegal key, update failed");}else {User tokenUser = new User();tokenUser.setId(userId);tokenUser.setUsername(username);tokenUser.setPassword(password);UserVO usersVO = setUserRedisSessionToken(tokenUser);usersVO.setPassword("");return BlogJSONResult.ok(usersVO);}}@UserLoginToken@ApiOperation(value = "用户注销", notes = "用户注销的接口")@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")@PostMapping(value = "/logout")public BlogJSONResult logout(String userId) throws Exception {String userRedisSession = RedisUtils.getUserRedisSession(userId);redis.del(userRedisSession);return BlogJSONResult.ok("注销成功");}}
  • 管理注册与登录控制器
package cn.fyupeng.controller.admin;import cn.fyupeng.annotion.PassToken;
import cn.fyupeng.annotion.UserLoginToken;
import cn.fyupeng.controller.BasicController;
import cn.fyupeng.pojo.User;
import cn.fyupeng.pojo.vo.UserVO;
import cn.fyupeng.service.UserService;
import cn.fyupeng.utils.BlogJSONResult;
import cn.fyupeng.utils.MD5Utils;
import cn.fyupeng.utils.RedisUtils;
import cn.fyupeng.utils.TokenUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;import java.util.UUID;@CrossOrigin
@SuppressWarnings("all")
@RestController
@RequestMapping(value = "/admin")
@Api(value = "管理员注册登录的接口", tags = {"管理员注册和登录的controller"})
public class AdminRegistLoginController extends BasicController {private UserService userServiceProxy = rpcClientProxy.getProxy(UserService.class);@UserLoginToken@ApiOperation(value = "管理员注册", notes = "用管理员注册的接口")@ApiImplicitParam(name = "user", value = "用户", required = true, dataType = "User", paramType = "body")@PostMapping(value = "/regist")public BlogJSONResult regist(@RequestBody User user) throws Exception{//1. 判断用户名和密码必须不为空if(StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())){return BlogJSONResult.errorMsg("用户名和密码不能为空");}//2. 判断用户是否存在boolean usernameIsExist = userServiceProxy.queryUsernameIsExist(user.getUsername());//3. 保存用户,注册信息if(!usernameIsExist){user.setPassword(MD5Utils.getMD5Str(user.getPassword()));// 防止 被 注入user.setPermission(3);userServiceProxy.saveUser(user);}else {return BlogJSONResult.errorMsg("用户名已存在");}user.setPassword("");//String uniqueToken = UUID.randomUUID().toString();//redis.set(USER_REDIS_SESSION + ":" + user.getId(), uniqueToken, 1000 * 60 * 30);////UsersVO usersVO = new UsersVO();//BeanUtils.copyProperties(user, usersVO);//usersVO.setUserToken(uniqueToken);//UserVO userVO = setUserRedisSessionToken(user);user.setPassword("");return BlogJSONResult.ok(user);}public UserVO setUserRedisSessionToken(User userModel) throws Exception {//String uniqueToken = UUID.randomUUID().toString();//userModel.getPassword() 已经是加密的了String token = TokenUtils.token(userModel.getId(), userModel.getUsername(), userModel.getPassword());String userRedisSession = RedisUtils.getUserRedisSession(userModel.getId());redis.set(userRedisSession, token,  60 * 5);UserVO usersVO = new UserVO();BeanUtils.copyProperties(userModel, usersVO);usersVO.setUserToken(token);return usersVO;}@PassToken@ApiOperation(value = "管理员登录", notes = "管理员登录的接口")@ApiImplicitParam(name = "user", value = "用户", required = true, dataType = "User", paramType = "body")@PostMapping(value = "/login")public BlogJSONResult login(@RequestBody User user) throws Exception {String username = user.getUsername();String password = user.getPassword();if(StringUtils.isBlank(username) || StringUtils.isBlank(password)){return BlogJSONResult.ok("用户名或密码不能为空....");}User userResult = userServiceProxy.queryUserForLogin(username, MD5Utils.getMD5Str(user.getPassword()));if(userResult == null || userResult.getPermission() != 3){return BlogJSONResult.errorMsg("用户名密码不正确,或为非管理员登录");}else {UserVO usersVO = setUserRedisSessionToken(userResult);userResult.setPassword("");return BlogJSONResult.ok(usersVO);}}@ApiOperation(value = "管理员注销", notes = "管理员注销的接口")@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")@PostMapping(value = "/logout")public BlogJSONResult logout(String userId) throws Exception {String userRedisSession = RedisUtils.getAdminRedisSession(userId);redis.del(userRedisSession);return BlogJSONResult.ok("注销成功");}}
  • 管理员文章控制器
package cn.fyupeng.controller.admin;import cn.fyupeng.annotion.UserLoginToken;
import cn.fyupeng.controller.BasicController;
import cn.fyupeng.pojo.Article;
import cn.fyupeng.pojo.Classfication;
import cn.fyupeng.pojo.User;
import cn.fyupeng.service.ArticleService;
import cn.fyupeng.service.ClassficationService;
import cn.fyupeng.service.UserService;
import cn.fyupeng.service.TagService;
import cn.fyupeng.utils.BlogJSONResult;
import cn.fyupeng.utils.PagedResult;
import cn.fyupeng.utils.RedisUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;/*** @Auther: fyp* @Date: 2022/4/5* @Description:* @Package: com.crop.admin.controller* @Version: 1.0*/@CrossOrigin
@Slf4j
@RestController
@RequestMapping(value = "/admin/article")
@Api(value = "文章相关业务的接口", tags = {"文章相关业务的controller"})
public class AdminArticleController extends BasicController {private TagService tagServiceProxy = rpcClientProxy.getProxy(TagService.class);private UserService userServiceProxy = rpcClientProxy.getProxy(UserService.class);private ArticleService articleServiceProxy = rpcClientProxy.getProxy(ArticleService.class);private ClassficationService classficationServiceProxy = rpcClientProxy.getProxy(ClassficationService.class);private static ScheduledExecutorService executor;@UserLoginToken@PostMapping(value = "/removeClassfication")@ApiOperation(value = "删除文章分类 - 注意: 文章分类为所有用户公共的分类,方便查询,私有分类情使用标签", notes = "删除文章分类的接口")@ApiImplicitParams({@ApiImplicitParam(name = "classficationId", value = "文章分类id", required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")})public BlogJSONResult removeClassfication(String classficationId, String userId) {if (StringUtils.isBlank(userId)) {return BlogJSONResult.errorMsg("用户id不能为空");}User user = userServiceProxy.queryUser(userId);// 用户不存在 或 无权 删除if (user == null || user.getPermission() != 3) {return BlogJSONResult.errorMsg("用户不存在或你无权执行该操作");}Article article = new Article();article.setClassId(classficationId);PagedResult pagedResult = articleServiceProxy.queryArticleSelective(article, 1, 1);if (pagedResult.getRecords() != 0) {return BlogJSONResult.errorMsg("删除失败!存在文章绑定了分类id: " + classficationId);}boolean deleteClassficationIsTrue = classficationServiceProxy.deleteClassfication(classficationId);return deleteClassficationIsTrue ? BlogJSONResult.ok() : BlogJSONResult.errorMsg("分类id不存在或内部错误");}@UserLoginToken@PostMapping(value = "/updateClassfication")@ApiOperation(value = "更新文章分类", notes = "更新文章分类的接口")@ApiImplicitParams({@ApiImplicitParam(name = "classfication", value = "文章分类", required = true, dataType = "Classfication", paramType = "body"),@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")})public BlogJSONResult updateClassfication(@RequestBody Classfication classfication, String userId) {if (StringUtils.isBlank(userId)) {return BlogJSONResult.errorMsg("用户id不能为空");}User user = userServiceProxy.queryUser(userId);// 用户不存在 或 无权 删除if (user == null || user.getPermission() != 3) {return BlogJSONResult.errorMsg("用户不存在或你无权执行该操作");}boolean updateClassficationIsTrue = classficationServiceProxy.updateClassfication(classfication);return updateClassficationIsTrue ? BlogJSONResult.ok() : BlogJSONResult.errorMsg("分类id不存在或内部错误导致更新失败");}@UserLoginToken@PostMapping(value = "/saveClassfication")@ApiOperation(value = "新建文章分类", notes = "新建文章分类的接口")@ApiImplicitParams({@ApiImplicitParam(name = "classficationName", value = "分类名", required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")})public BlogJSONResult saveClassFication(String classficationName, String userId) {if (StringUtils.isBlank(classficationName)) {return BlogJSONResult.errorMsg("分类名不能为空");}User user = userServiceProxy.queryUser(userId);// 用户不存在 或 无权 删除if (user == null || user.getPermission() != 3) {return BlogJSONResult.errorMsg("用户不存在或你无权执行该操作");}Classfication classfication = new Classfication();classfication.setName(classficationName);Classfication classficationIsExist = classficationServiceProxy.queryClassfication(classfication);if (classficationIsExist != null) {return BlogJSONResult.errorMsg("分类名已存在");}boolean saveIsTrue = classficationServiceProxy.saveClassfication(classfication);return saveIsTrue ? BlogJSONResult.ok() : BlogJSONResult.errorMsg("内部错误导致保存失败");}@UserLoginToken@PostMapping(value = "/startTimeTask")@ApiOperation(value = "开启任务 - 自动更新阅读量", notes = "开启任务的接口")@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")public BlogJSONResult startTimeTask(String userId) {if (StringUtils.isBlank(userId)) {return BlogJSONResult.errorMsg("用户Id不能为空");}User user = userServiceProxy.queryUser(userId);if (user == null || user.getPermission() != 3) {return BlogJSONResult.errorMsg("用户Id不存在或无权限");}if (executor != null && !executor.isShutdown()) {return BlogJSONResult.errorMsg("任务已启动,无需重启");}executor = Executors.newScheduledThreadPool(2);log.info("正在开启任务线程池...");long initialDelay = 1 * 1000;long fiveMinute = 5 * 60 * 1000;executor.scheduleAtFixedRate(() -> {/*** 定时任务 - 更新 阅读量*/// 获取 所有用户 对Id的 阅读量String viewCount = RedisUtils.getViewCount();List<String> keys = redis.getKeysByPrefix(viewCount);/*** key格式: "crop:viewCount:2204057942HA6Z7C"* 获取 articleId : 2204057942HA6Z7C*/List<String> articleIdKeys = new ArrayList<>();Map<String, String> articleMap = new HashMap<>();for (String k : keys) {// 匹配 最后一个 : 到结束String tempArticleId = k.substring(k.lastIndexOf(":") + 1);articleIdKeys.add(tempArticleId);}List<String> articleIdCounts = redis.multiGet(keys);for (int i = 0; i < articleIdKeys.size(); i++) {articleMap.put(articleIdKeys.get(i), articleIdCounts.get(i));}articleServiceProxy.multiUpdateArticleReadCounts(articleIdKeys, articleMap);log.info("完成一次周期任务 - 任务正常");}, initialDelay, fiveMinute, TimeUnit.MILLISECONDS);return BlogJSONResult.ok("任务启动成功");}@UserLoginToken@PostMapping(value = "/stopTimeTask")@ApiOperation(value = "关闭任务", notes = "关闭任务的接口")@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")public BlogJSONResult stopTimeTask(String userId) {if (StringUtils.isBlank(userId)) {return BlogJSONResult.errorMsg("用户Id不能为空");}User user = userServiceProxy.queryUser(userId);if (user == null || user.getPermission() != 3) {return BlogJSONResult.errorMsg("用户Id不存在或无权限");}if (executor == null || executor.isTerminated()) {return BlogJSONResult.errorMsg("任务未启动");}executor.shutdown();try {executor.awaitTermination(10, TimeUnit.SECONDS);} catch (InterruptedException e) {log.error("任务线程池关闭失败: ",e);executor.shutdownNow();//e.printStackTrace();}log.info("任务线程池已成功关闭");return BlogJSONResult.ok("任务关闭成功");}}
  • 管理员评论控制器
package cn.fyupeng.controller.admin;import cn.fyupeng.annotion.UserLoginToken;
import cn.fyupeng.controller.BasicController;
import cn.fyupeng.enums.CommentStatus;
import cn.fyupeng.pojo.Comment;
import cn.fyupeng.pojo.User;
import cn.fyupeng.pojo.vo.CommentVO;
import cn.fyupeng.service.*;
import cn.fyupeng.utils.BlogJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;/*** @Auther: fyp* @Date: 2022/4/8* @Description:* @Package: com.crop.admin.controller* @Version: 1.0*/@CrossOrigin
@Slf4j
@RestController
@RequestMapping(value = "/admin/comment")
@Api(value = "评论相关业务的接口", tags = {"评论相关业务的controller"})
public class AdminCommentController extends BasicController {private UserService userServiceProxy = rpcClientProxy.getProxy(UserService.class);private CommentService commentServiceProxy = rpcClientProxy.getProxy(CommentService.class);@UserLoginToken@PostMapping(value = "removeComment")@ApiOperation(value = "强制删除评论", notes = "强制删除评论的接口")@ApiImplicitParams({@ApiImplicitParam(name = "commentId", value = "评论id", required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String", paramType = "query")})public BlogJSONResult removeComment(String commentId, String userId) {if (StringUtils.isBlank(commentId) || StringUtils.isBlank(userId)) {return BlogJSONResult.errorMsg("commentId或userId不能为空");}boolean commentIsExist = commentServiceProxy.queryCommentIsExist(commentId);if (!commentIsExist) {return BlogJSONResult.errorMsg("commentId不存在");}User identifyUser = userServiceProxy.queryUser(userId);if (identifyUser == null || identifyUser.getPermission() != 3) {return BlogJSONResult.errorMsg("用户Id不存在或无权限");}commentServiceProxy.removeCommentById(commentId);commentServiceProxy.removeCommentWithFatherCommentId(commentId);return BlogJSONResult.ok();}@UserLoginToken@PostMapping(value = "/filterComments")@ApiOperation(value = "过滤查询评论", notes = "过滤查询评论的接口")@ApiImplicitParams({@ApiImplicitParam(name = "aPattern", value = "文章匹配", dataType = "String", paramType = "query"),@ApiImplicitParam(name = "cPattern", value = "评论匹配", dataType = "String", paramType = "query"),@ApiImplicitParam(name = "userId", value = "用户id", dataType = "String", paramType = "query"),@ApiImplicitParam(name = "startTime", value = "评论开始时间 - yyyy-MM-dd HH:mm:ss", required = true, dataType = "String", paramType = "query"),@ApiImplicitParam(name = "endTime", value = "评论结束时间 - yyyy-MM-dd HH:mm:ss", required = true, dataType = "String", paramType = "query")})public BlogJSONResult filterComments(String aPattern, String cPattern, String userId, String startTime, String endTime) {if (StringUtils.isBlank(aPattern) && StringUtils.isBlank(cPattern)) {return BlogJSONResult.errorMsg("至少指定文章匹配或评论匹配");}if (!(StringUtils.isNotBlank(startTime) && StringUtils.isNotBlank(endTime))) {return BlogJSONResult.errorMsg("必须指定时间跨度");}SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Date startDate = null;Date endDate = null;try {startDate = sdf.parse(startTime);endDate = sdf.parse(endTime);} catch (ParseException e) {e.printStackTrace();return BlogJSONResult.errorMsg("时间格式不正确");}List<CommentVO> result = null;if (StringUtils.isBlank(userId)) {result = commentServiceProxy.queryAllComments(aPattern, cPattern, null, startDate, endDate);} else {result = commentServiceProxy.queryAllComments(aPattern, cPattern, userId, startDate, endDate);}return BlogJSONResult.ok(result);}@UserLoginToken@PostMapping(value = "/setCommentStatus")@ApiOperation(value = "屏蔽评论 - 1 正常 - 2 屏蔽", notes = "屏蔽评论的接口")@ApiImplicitParams({@ApiImplicitParam(name = "commentId", value = "评论id", dataType = "Integer", paramType = "query"),@ApiImplicitParam(name = "status", value = "评论状态", dataType = "String", paramType = "query"),@ApiImplicitParam(name = "userId", value = "用户id", dataType = "String", paramType = "query"),})public BlogJSONResult setCommentStatus(String commentId, Integer status, String userId) {if (StringUtils.isBlank(commentId) || status == null) {return BlogJSONResult.errorMsg("评论id或状态不能为空");}if (StringUtils.isBlank(userId)) {return BlogJSONResult.errorMsg("用户id不能为空");}Comment comment = commentServiceProxy.queryComment(commentId);if (comment == null) {return BlogJSONResult.errorMsg("评论id不存在");}User user = userServiceProxy.queryUser(userId);if (user == null || user.getPermission() == 2) {return BlogJSONResult.errorMsg("用户id不存在或无权限访问");}if (status == 1) {commentServiceProxy.setCommentStatusWithFatherId(comment, CommentStatus.NORMAL);} else if(status == 2) {commentServiceProxy.setCommentStatusWithFatherId(comment, CommentStatus.BLOCKED);} else {return BlogJSONResult.errorMsg("无其他状态可操作");}return BlogJSONResult.ok();}}

2.8 服务接口

  • 文章服务
package cn.fyupeng.service;import cn.fyupeng.pojo.Article;
import cn.fyupeng.pojo.vo.ArticleVO;
import cn.fyupeng.utils.PagedResult;import java.util.List;
import java.util.Map;/*** @Auther: fyp* @Date: 2022/4/2* @Description:* @Package: com.crop.service* @Version: 1.0*/
public interface ArticleService {boolean queryArticleIsExist(String articleId);boolean queryArticleIsUser(Article article);boolean save(Article article);PagedResult queryArticleSelective(Article article, Integer page, Integer pageSize);ArticleVO queryArticleDetail(String articleId);boolean saveWithIdAndUserId(Article article);void multiUpdateArticleReadCounts(List<String> articleIdKeys, Map<String, String> articleMap);void removeArticle(String articleId);PagedResult queryArticleByTime(Long timeDifference, Integer page, Integer pageSize);List<ArticleVO> queryArticleWithNoneTagByUser(String userId);
}
  • 评论服务
package cn.fyupeng.service;import cn.fyupeng.enums.CommentStatus;
import cn.fyupeng.pojo.Comment;
import cn.fyupeng.pojo.vo.CommentVO;
import cn.fyupeng.utils.PagedResult;import java.util.Date;
import java.util.List;/*** @Auther: fyp* @Date: 2022/4/3* @Description:* @Package: com.crop.service* @Version: 1.0*/
public interface CommentService {boolean saveComment(Comment comment);void removeCommentById(String commentId);Comment queryComment(String commentId);boolean queryCommentIsExist(String commentId);boolean updateComment(Comment comment);PagedResult queryAllComments(String articleId, Integer page, Integer pageSize, Integer sort);boolean queryCommentWithFatherCommentIsExist(String commentId);void removeCommentWithFatherCommentId(String fatherCommentId);List<CommentVO> queryAllComments(String aPattern, String cPattern, String userId, Date startTime, Date endTime);void setCommentStatusWithFatherId(Comment comment, CommentStatus commentStatus);
}
  • 用户服务
package cn.fyupeng.service;import cn.fyupeng.pojo.User;
import cn.fyupeng.pojo.UserInfo;/*** @Auther: fyp* @Date: 2022/8/17* @Description: 用户业务服务接口* @Package: cn.fyupeng.service* @Version: 1.0*/
public interface UserService {/*** @Description: 判断用户名是否存在* @param username* @return*/public boolean queryUsernameIsExist(String username);/*** @Description: 用户登录,根据用户名和密码查询用户* @param username* @param password* @return*/public User queryUserForLogin(String username, String password);/*** 查询用户信息* @param userId* @return*/User queryUser(String userId);/*** 查询用户详细信息* @param userId* @return*/public UserInfo queryUserInfo(String userId);/*** 查询用户是否存在* @param userId* @return*/boolean queryUserIdIsExist(String userId);/*** @Description: 用户修改信息* @param user*/public boolean updateUser(User user);/*** @Description: 用户修改详细信息* @param user*//*** @Description: 保存用户(注册用户)* @param user*/public void saveUser(User user);public void updateUserInfo(UserInfo user);}
  • 标签服务
package cn.fyupeng.service;import cn.fyupeng.pojo.Articles2tags;
import cn.fyupeng.pojo.Tag;
import cn.fyupeng.pojo.vo.Articles2tagsVO;
import cn.fyupeng.pojo.vo.TagVO;import java.util.List;/*** @Auther: fyp* @Date: 2022/4/8* @Description:* @Package: com.crop.service* @Version: 1.0*/
public interface TagService {boolean queryTagIsExist(Tag tag);boolean queryArticleTagIsExist(String id);boolean queryArticleTagIsExist(Articles2tags articles2tags);List<TagVO> queryAllTags(Tag tag);boolean saveTag(Tag tag);boolean updateTag(Tag tag);boolean deleteTag(String tagId);void delArticleTag(String tagId);boolean deleteTagAndArticleTagWithTagId(String tagId);Tag queryTag(String tagId);List<Articles2tagsVO> queryArticleTag(Articles2tags articles2tags);boolean saveArticleTag(Articles2tags articles2tags);boolean updateArticleTag(Articles2tags articles2tags);boolean deleteTagAndArticleTagWithArticleId(String articleId);
}
  • 分类服务
package cn.fyupeng.service;import cn.fyupeng.pojo.Classfication;import java.util.List;/*** @Auther: fyp* @Date: 2022/4/3* @Description:* @Package: com.crop.service* @Version: 1.0*/
public interface ClassficationService {boolean queryClassficationIdIsExist(String classId);boolean saveClassfication(Classfication classfication);Classfication queryClassfication(Classfication classfication);List<Classfication> queryAllClassfications();boolean deleteClassfication(String classficationId);boolean updateClassfication(Classfication clssfication);
}

项目链接地址:https://github.com/fyupeng/distributed-blog-system-api

那么第六天的项目就到这里,大家觉得很赞不妨给个关注或者收藏吧,谢谢各位!

手写一个博客平台 ~ 第六天相关推荐

  1. 手写一个博客平台~第一天

    作者:fyupeng 技术专栏:☞ https://github.com/fyupeng 项目地址:☞ https://github.com/fyupeng/distributed-blog-syst ...

  2. 手写一个博客平台 ~ 第七天

    作者:fyupeng 技术专栏:☞ https://github.com/fyupeng 项目地址:☞ https://github.com/fyupeng/distributed-blog-syst ...

  3. PHP+JS写一个博客系统

    文章目录 注册和登录界面的完成 注册界面 登录界面 数据库的创建 导入 连接 使用 数据库的连接 验证码的生成 DOACTION 即登录和注册的验证 发表博客 PHP学习完成,随之的实验是结合数据库, ...

  4. 前端开发 使用html写一个博客 基本标签的体验 0226

    新建一个html文件 写一段文本作为博客的标题 在body中写内容 *号的出现代表,文件有改变 保存文件后消失 快捷键: ctrl + s 效果 当前这个页面已经在一个模拟的服务器上运行了 当代码内容 ...

  5. 基于Bmob从零开始写一个博客小程序

    2019独角兽企业重金招聘Python工程师标准>>> 实现以下技能点 1.集成Bmob小程序SDK作为数据存储 2.wemark解析markdown文本 3.列表页布局与上拉无限加 ...

  6. 程序员可以选择哪些平台写技术博客?

    前言 只有光头才能变强. 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y 很多时候,别人问我怎么学习Java,怎么可以提升自己 ...

  7. 搭建一个属于自己的博客平台

    本文摘要:文章先讲解了如何从零开始搭建属于自己的博客或者网站平台,接着文章列举了搭建过程中遇到的问题及解决方案.文章步骤详细,适合没有任何基础但想要搭建自己的网站的朋友参考. 正文 最近花了几天时间, ...

  8. 程序员为什么要写技术博客?都在哪些平台呢?

    目录 一.程序员为什么要写技术博客? 1.真正掌握技术 2.没有人会那么在意你 3.珍惜时间 4.懒于思考,疏于总结 5.碎片化学习 6.优秀大神 7.更好的求职机会 8.努力的人一直都有 二.程序员 ...

  9. 3. 你也要写技术博客?这篇博客告诉你平台怎么选

    橡皮擦,一个逗趣的互联网高级网虫,为你带来新职场故事,搬来程序员敲门砖. 已完成文章 国内,首套,成体系,技术博客写作专栏发布啦 技术博客只能写技术文章吗?当然是由我们自己来定义. 为"她& ...

  10. 写技术博客,如何选择博客平台

    序言 很多技术开发在工作2-3年的时候,都会选择去写一些技术博客.一方面可以总结用到的一些技术,另一方面可以帮助到别人. 开始写技术博客,首要面临的问题是如何选择一个合适的博客平台? 选择标准 选择技 ...

最新文章

  1. python基础爬虫的框架以及详细的运行流程
  2. 修改mysql字符集_mysql 修改字符集
  3. unzipping/Users/xq/.gradle/wrapper /dists/gradle-3.3-all/55gk2rcmfc6p2dg9u9ohc3hw9/gradle-3.3-all.zi
  4. rdlc报表 矩形高固定_固定资产条码管理系统特点分析
  5. opencv 读取、显示、保存视频
  6. Android_WakeLock使用
  7. Hadoop伪分布式集群环境搭建
  8. cad批量页码lisp_源代码:批量改页码(加前缀)及提取属性块
  9. 福禄克DSX2-8000——支持铜缆、光纤、OTDR测试的多功能网线测试仪
  10. java实现文件对比
  11. 单片机原理与c语言程序设计付先成版答案,单片机原理与C语言程序设计
  12. (十四)A Deep Neural Network for Unsupervised Anomaly Detection and Diagnosis in Multivariate Time Seri
  13. 【洛谷】P1957 口算练习题
  14. Oracle EBS系统维护工具
  15. 贝塞尔曲线及实践案例
  16. 人工智能之父图灵头像将登上新版50英镑钞票
  17. 互联网请回答2020
  18. thymeleaf中三元运算符嵌套写法
  19. python如何调用pyd_C#调用pyd的方法
  20. springmvc+vue ssm 医院预约挂号系统#毕业设计

热门文章

  1. 你还在为不知道怎么给家人庆祝生日而发愁吗?
  2. java动物继承_java 编码实现动物世界的继承关系:动物(Animal)属性:名称(name)具有行为:吃(eat)、睡觉(sleep)...
  3. mysql identify_MySQL用户授权
  4. 免费不限流的内网穿透,外网共享内网文件
  5. lua 函数 默认值_简明lua教程[转]
  6. 2020年中国SCADA行业产值、市场规模及竞争格局分析[图]
  7. Padding Oracle攻击(POODLE)技术分析
  8. 网易-资深iOS开发工程师
  9. 怎么从身份证号码批量提取出生年月日?
  10. Proteus,keil5仿真运行stm32程序,流水灯详细教程