手写一个博客平台 ~ 第六天
留给读者
你们可能想不到,其实博客已经最后一天了,因为前面该搭建的都搭建完了,后面只是业务代码,但为了能照顾大部分同学,我决定还是承诺应允大家做到从规范到代码,从代码到运行,从运行到项目部署,从部署到项目上线这几个过程。
一些业务中的规范直接按照项目来就可以了,就可以避免碰到许多坑了。
想入坑的有下面这么几个,大家可以不要看推文自己试试,能不能躺到坑位,嘟嘟噜。
打包不了的如何解决?用那个依赖就可以解决了?
mongodb
的bean
没法依赖,springboot
启动后报空指针异常spring1.0
转2.0
遇到的配置文件信息变动,Redis
、MongoDB
随之变动出现的问题高版本
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
上泄露的用户信息进行攻击,这里提供一个工具,如果你有经常做笔记发布到github
或gitee
的习惯,推荐你使用这一款网页版的监控敏感信息: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
那么第六天的项目就到这里,大家觉得很赞不妨给个关注或者收藏吧,谢谢各位!
手写一个博客平台 ~ 第六天相关推荐
- 手写一个博客平台~第一天
作者:fyupeng 技术专栏:☞ https://github.com/fyupeng 项目地址:☞ https://github.com/fyupeng/distributed-blog-syst ...
- 手写一个博客平台 ~ 第七天
作者:fyupeng 技术专栏:☞ https://github.com/fyupeng 项目地址:☞ https://github.com/fyupeng/distributed-blog-syst ...
- PHP+JS写一个博客系统
文章目录 注册和登录界面的完成 注册界面 登录界面 数据库的创建 导入 连接 使用 数据库的连接 验证码的生成 DOACTION 即登录和注册的验证 发表博客 PHP学习完成,随之的实验是结合数据库, ...
- 前端开发 使用html写一个博客 基本标签的体验 0226
新建一个html文件 写一段文本作为博客的标题 在body中写内容 *号的出现代表,文件有改变 保存文件后消失 快捷键: ctrl + s 效果 当前这个页面已经在一个模拟的服务器上运行了 当代码内容 ...
- 基于Bmob从零开始写一个博客小程序
2019独角兽企业重金招聘Python工程师标准>>> 实现以下技能点 1.集成Bmob小程序SDK作为数据存储 2.wemark解析markdown文本 3.列表页布局与上拉无限加 ...
- 程序员可以选择哪些平台写技术博客?
前言 只有光头才能变强. 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y 很多时候,别人问我怎么学习Java,怎么可以提升自己 ...
- 搭建一个属于自己的博客平台
本文摘要:文章先讲解了如何从零开始搭建属于自己的博客或者网站平台,接着文章列举了搭建过程中遇到的问题及解决方案.文章步骤详细,适合没有任何基础但想要搭建自己的网站的朋友参考. 正文 最近花了几天时间, ...
- 程序员为什么要写技术博客?都在哪些平台呢?
目录 一.程序员为什么要写技术博客? 1.真正掌握技术 2.没有人会那么在意你 3.珍惜时间 4.懒于思考,疏于总结 5.碎片化学习 6.优秀大神 7.更好的求职机会 8.努力的人一直都有 二.程序员 ...
- 3. 你也要写技术博客?这篇博客告诉你平台怎么选
橡皮擦,一个逗趣的互联网高级网虫,为你带来新职场故事,搬来程序员敲门砖. 已完成文章 国内,首套,成体系,技术博客写作专栏发布啦 技术博客只能写技术文章吗?当然是由我们自己来定义. 为"她& ...
- 写技术博客,如何选择博客平台
序言 很多技术开发在工作2-3年的时候,都会选择去写一些技术博客.一方面可以总结用到的一些技术,另一方面可以帮助到别人. 开始写技术博客,首要面临的问题是如何选择一个合适的博客平台? 选择标准 选择技 ...
最新文章
- python基础爬虫的框架以及详细的运行流程
- 修改mysql字符集_mysql 修改字符集
- unzipping/Users/xq/.gradle/wrapper /dists/gradle-3.3-all/55gk2rcmfc6p2dg9u9ohc3hw9/gradle-3.3-all.zi
- rdlc报表 矩形高固定_固定资产条码管理系统特点分析
- opencv 读取、显示、保存视频
- Android_WakeLock使用
- Hadoop伪分布式集群环境搭建
- cad批量页码lisp_源代码:批量改页码(加前缀)及提取属性块
- 福禄克DSX2-8000——支持铜缆、光纤、OTDR测试的多功能网线测试仪
- java实现文件对比
- 单片机原理与c语言程序设计付先成版答案,单片机原理与C语言程序设计
- (十四)A Deep Neural Network for Unsupervised Anomaly Detection and Diagnosis in Multivariate Time Seri
- 【洛谷】P1957 口算练习题
- Oracle EBS系统维护工具
- 贝塞尔曲线及实践案例
- 人工智能之父图灵头像将登上新版50英镑钞票
- 互联网请回答2020
- thymeleaf中三元运算符嵌套写法
- python如何调用pyd_C#调用pyd的方法
- springmvc+vue ssm 医院预约挂号系统#毕业设计
热门文章
- 你还在为不知道怎么给家人庆祝生日而发愁吗?
- java动物继承_java 编码实现动物世界的继承关系:动物(Animal)属性:名称(name)具有行为:吃(eat)、睡觉(sleep)...
- mysql identify_MySQL用户授权
- 免费不限流的内网穿透,外网共享内网文件
- lua 函数 默认值_简明lua教程[转]
- 2020年中国SCADA行业产值、市场规模及竞争格局分析[图]
- Padding Oracle攻击(POODLE)技术分析
- 网易-资深iOS开发工程师
- 怎么从身份证号码批量提取出生年月日?
- Proteus,keil5仿真运行stm32程序,流水灯详细教程