哈喽小伙伴们~,2020年还剩最后15天

file

今天我们来一个年终福利文章。

社会层面:

file

    2020年不过是一个普通的年份,却经历了与往常大不相同的12月,366天,8784小时,5207040分,31622400秒,世界人民一起经历了比2003年非典严重狡猾的病毒,直到现在世界疫情依然很严重!从任何方面,2020都是坎坷的一年,不论是是个人,或者是国家。通过这次疫情我更加热爱我的祖国,我们祖国的疫情全面得到控制,而国外却愈演愈烈,他们面对疫情不知所措乱成一锅粥。
今年国庆我很荣幸到了天安门广场,说不出来的一种自豪和骄傲。我们中国人民真的站起来了,我为我是中国人而骄傲!

file

    己亥末,庚子春,荆楚大疫,染者数万计,众惶恐,举国防,皆闭户,南山镇守江南都,率白衣郎中数万抗之,且九州一心,月余,疫尽去,国泰民安。-摘自网络。

生活层面:

    小伙伴,一直催我更新技术文章,最近实在是太忙,不瞒大家说,我开始带团队了,已经不是一个人在战斗了。一个人的时候可以没事的时候写写博客,那个时候感觉有很多很多时间也有很多很多精力去给大家更新博客,而现在多多少少有些力不从心了。只要我更新出来的东西,必须给读者负责,前一段时间我一个朋友在某博客学习某一个技术点,结果就是不出结果,最后一看写的博客少一行代码。我们既然敢写,就是敢负责到底。                                                

福利分享:

今天给大家分享由我呢吧完全自主搭建一个完整技术工程,上才艺~

file

Java技术栈

  • Spring Boot
  • MyBatis Plus(含逆向工程)
  • MySql
  • Redis
  • WebSocket
  • Quartz
  • Influx DB时序数据库
  • Shiro+JWT
  • P6Spy+SL4J
  • fastDfs文件上传
  • Swagger接口文档
  • Hutool工具类
  • 自定义注解
  • 自定义异常

一、猿码优创工程简单使用:

1、swagger-ui使用:

file

1.1访问页面地址:http://127.0.0.1:12001/swagger-ui.html
1.2、使用方法:

file

接口注释:
 @ApiOperation(value = "首页", notes = "访问测试", httpMethod = "GET")
参数注释:
 @ApiParam(name = "名字", value = "名字", required = true) String name
1.3、配置方法:

file

 private ApiInfo apiInfo() {
    return new ApiInfo(
            "猿码优创管理API文档",
            "猿码优创管理API文档",
            "2.0",
            null,
            new Contact("kingyifan", "https://cnbuilder.cn/", "itw@tom.com"),
            "Apache 2.0", "https://www.apache.org/licenses/LICENSE-2.0.html", Collections.emptyList());
    }
2、权限使用(流程:用户登录===》获取token===》请求头携带token===》请求接口===》校验权限===》返回结果):
2.0、Token配置文件:

file

2.1、登录:

file 请求参数:

form-data:
  account=admin
  password=admin

返回参数:

{
    "msg": "登录成功(Login Success.)",
    "code": 200,
    "data": {
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjdXJyZW50VGltZU1pbGxpcyI6IjE2MDY0NDg3MDM1NzciLCJleHAiOjE2MDY0OTE5MDMsImFjY291bnQiOiJhZG1pbiJ9.nI_J4K7UtuHISwM3weGYKd65p3VhD7LSqcuCnqKa8A0"
    }
}

登录以后会自动加请求头响应参数: file

登录代码:

  /**
     * 登录授权
     *
     * @param account
     * @param password
     * @author 猿码优创 website:https://cnbuilder.cn/ Email:itw@tom.com
     */
    @PostMapping("/login")
    @ApiOperation(value = "用户", notes = "登录", httpMethod = "GET")
    public R login(
            @ApiParam(name = "account", value = "账号", required = true) @RequestParam(value = "account", required = true) String account,
            @ApiParam(name = "password", value = "密码", required = true) @RequestParam(value = "password", required = true) String password,
            HttpServletResponse httpServletResponse) {

        Map reMap = new HashMap<>();
        // 查询数据库中的帐号信息account
        User userTemp = userService.getOne(new QueryWrapper<User>().eq("account", account));
        if (userTemp == null) {
            return R.error(500, "该帐号不存在(The account does not exist.)");
        }
        // 密码进行AES解密
        String key = AesCipherUtil.deCrypto(userTemp.getPassword());
        // 因为密码加密是以帐号+密码的形式进行加密的,所以解密后的对比是帐号+密码
        if (key.equals(account + password)) {
            // 清除可能存在的Shiro权限信息缓存
            if (JedisUtil.exists(Constant.PREFIX_SHIRO_CACHE + account)) {
                JedisUtil.delKey(Constant.PREFIX_SHIRO_CACHE + account);
            }
            //判斷redis是否存在 存在直接用相同的。这个可以避免同样的账号两个人登录 把另一个人给踢出 TODO:如果一个账号只能一个登录可以去掉这段代码~
            String rtoken = (String) JedisUtil.getObject(Constant.EXISTENCE_TOKEN + account);
            if (rtoken != null) {
                httpServletResponse.setHeader("Authorization", rtoken);
                httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
                reMap.put("token", rtoken);
                return R.ok(HttpStatus.OK.value(), "登录成功(Login Success.)", reMap);
            }

            // 设置RefreshToken,时间戳为当前时间戳,直接设置即可(不用先删后设,会覆盖已有的RefreshToken)
            String currentTimeMillis = String.valueOf(System.currentTimeMillis());
            JedisUtil.setObject(Constant.PREFIX_SHIRO_REFRESH_TOKEN + account, currentTimeMillis, Integer.parseInt(refreshTokenExpireTime));

            // 从Header中Authorization返回AccessToken,时间戳为当前时间戳
            String token = JwtUtil.sign(account, currentTimeMillis);
            //存放redis
            JedisUtil.setObject(Constant.EXISTENCE_TOKEN + account, token, Integer.parseInt(accessTokenExpireTime));

            reMap.put("token", token);
            httpServletResponse.setHeader("Authorization", token);
            httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
            return R.ok("登录成功(Login Success.)").data(reMap);
        }
        return R.error(500, "帐号或密码错误(Account or Password Error.)");
    }

游客模式(不带Token也可以访问接口): file 需要Token放行接口: file

2.2、请求API接口:

不传Token: file 传输错误Token(项目封装了异常捕获统一返回,如果不喜欢这样提示这样可以自行更改哈。): file 传输正确Token 没有权限: file DB赋值权限(目前没做admin全部权限,有兴趣大家可以自己做一下哈): file 有权限访问: file 代码:

加一行注解即可:自己匹配权限
@RequiresPermissions(logical = Logical.AND, value = {"user:menus"})

代码样例:
@GetMapping(value = "/menus")
@ApiOperation(value = "获取菜单", notes = "获取菜单", httpMethod = "GET")
@SystemLogAnnotation(businessType = "用户", methodDetail = "获取菜单")
@RequiresPermissions(logical = Logical.AND, value = {"user:menus"})
public R menus() {
    QueryWrapper<SysMenu> qr = new QueryWrapper<>();
    //根据用户查询菜单权限
    List<SysMenu> list = iSysMenuService.list(qr);
    //配置
    TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
    // 自定义属性名 都要默认值的
    treeNodeConfig.setWeightKey("sort");
    treeNodeConfig.setIdKey("id");
    // 最大递归深度
    treeNodeConfig.setDeep(3);
    //转换器
    List<Tree<String>> treeNodes = TreeUtil.build(list, "-1", treeNodeConfig,
            (treeNode, tree) -> {
                tree.setId(treeNode.getId().toString());
                tree.setParentId(treeNode.getParentid().toString());
                tree.setWeight(treeNode.getSort());
                tree.setName(treeNode.getName());
                // 扩展属性 ...
                tree.putExtra("isJump", treeNode.getIsJump() == null || treeNode.getIsJump() == 0 ? false : true);
                tree.putExtra("icon", treeNode.getIcon() == null ? "" : treeNode.getIcon());
                tree.putExtra("url", treeNode.getUrl());
            });
    return R.ok().data(treeNodes);
}
2.3、获取用户信息(必须过jwt拦截器传Token,才可以获取。):

file 根据Token自己解析用户信息

代码:
final注入:
 private final UserUtil userUtil;这种final方式注入需要在controller或者service加@RequiredArgsConstructor
普通注入:
 @Autowired
    private UserUtil userUtil;
直接使用:
   User user = userUtil.getUser();

ps:权限框架集成参考:https://gitee.com/dolyw/ShiroJwt

3、influxDB:
3.0、influxDB简介和配置:
简介:

file

配置:

file

3.1、influxDB使用:

新增数据:没有database(数据库)和measurement(可以理解成表)自动会创建 file

 //新增时序数据库数据
Map<String, String> tagsMap = new HashMap<>();//默认自己加索引
Map<String, Object> fieldsMap = new HashMap<>();//默认不加索引

tagsMap.put("name", "猿码优创");
tagsMap.put("website", "www.cnbuilder.cn");
tagsMap.put("Email", "itw@tom.com");
fieldsMap.put("author", "kingyifan");
influxDBConnect.insert("user", tagsMap, fieldsMap);
保存:
/**
 * 插入
 *
 * @param measurement 表
 * @param tags        标签
 * @param fields      字段
 */
public void insert(String measurement, Map<String, String> tags, Map<String, Object> fields) {
    Point.Builder builder = Point.measurement(measurement);
    // 纳秒时会出现异常信息:partial write: points beyond retention policy dropped=1
    // builder.time(System.nanoTime(), TimeUnit.NANOSECONDS);
    builder.time(System.currentTimeMillis() / 1000, TimeUnit.SECONDS);
    builder.tag(tags);
    builder.fields(fields);
    log.info("influxDB insert data:[{}]", builder.build().toString());
    influxDB.write(database, "", builder.build());
}

插入成功: file

查询数据 最后加tz('Asia/Shanghai')指定时区 file

PS:influxDB和mysql数据库不一样。
mysql返回的数据:
    key:value 
influxDb返回数据:
    columns=[time, Email, author, name, website], 
    values=[[2020-12-17T10:16:39Z, itw@tom.com, kingyifan, 猿码优创, www.cnbuilder.cn]]]

返回数据: file

解析代码: file

代码:
/**
 * 解析influx返回数据
 *
 * @param queryResult
 * @return
 */
public static List<Map<String, Object>> convertQueryResult(QueryResult queryResult) {
    List<Map<String, Object>> maps = new LinkedList<>();
    if (queryResult.getResults().get(0).getSeries() == null || queryResult.getResults().get(0).getSeries().size() == 0) {
        return null;
    }
    for (QueryResult.Series series : queryResult.getResults().get(0).getSeries()) {
        List<String> columns = series.getColumns();
        List<List<Object>> values = series.getValues();
        for (List<Object> objects : values) {
            Map map = new HashMap();
            for (int i = 0; i < columns.size(); i++) {
                String c = columns.get(i);
                if (c.equals("time")) {
                    map.put(c, objects.get(i).toString().replace("+08:00", ""));
                    continue;
                }
                map.put(c, objects.get(i));
            }
            if (series.getTags() != null) {
                for (String key : series.getTags().keySet()) {
                    map.put(key, series.getTags().get(key));
                }
            }
            maps.add(map);
        }
    }
    return maps;
}
4、定时任务Quartz:
4.1、使用:

file

注解:
  @Scheduled(cron = "0/1 * * * * ? ")

ps:七子表达式在线生成地址:https://cron.qqe2.com/

5、登录拦截日志(已经实现)
5.1、使用:

拦截用户操作日志:谁在什么时间干了什么什么事最后结果是什么?没干成原因是什么?: file

CREATE TABLE `sys_user_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `business_type` varchar(20) DEFAULT NULL COMMENT '业务类型',
  `method_detail` varchar(50) DEFAULT NULL COMMENT '接口备注',
  `ip` varchar(20) DEFAULT NULL COMMENT '请求ip',
  `method_name` varchar(200) DEFAULT NULL COMMENT '接口方法',
  `url` varchar(200) DEFAULT NULL COMMENT '接口url',
  `params` text COMMENT '请求参数',
  `sys_user_id` int(11) DEFAULT NULL COMMENT '用户id',
  `sys_user_name` varchar(50) DEFAULT NULL COMMENT '操作人名称',
  `excute_time` bigint(20) DEFAULT NULL COMMENT '执行耗时',
  `create_time` datetime DEFAULT NULL COMMENT '操作时间',
  `return_result` text COMMENT '返回结果',
  `error_detail` text COMMENT '错误原因',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;

用到了Spring的Aop环绕拦截(后期可以加mq)。

package cn.cnbuilder.manage.common.aspect;

import cn.cnbuilder.manage.busi.service.ISysUserLogService;
import cn.cnbuilder.manage.common.entity.R;
import cn.cnbuilder.manage.shiro.util.UserUtil;
import cn.cnbuilder.manage.busi.entity.SysUserLog;
import cn.cnbuilder.manage.common.annotation.SystemLogAnnotation;
import cn.cnbuilder.manage.common.utils.IpUtil;
import cn.cnbuilder.manage.shiro.model.entity.User;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Date;

/**
 * 日志切面类
 *
 * @author 猿码优创 website:https://cnbuilder.cn/ Email:itw@tom.com
 */
@Aspect
@Component
public class LogAspect {

    @Autowired
    private UserUtil userUtil;

    @Autowired
    private ISysUserLogService sysUserLogService;


    /**
     * 定义一个切入点.
     * 解释下:
     * <p>
     * ~ 第一个 * 代表任意修饰符及任意返回值.
     * ~ 第二个 * 定义在web包或者子包
     * ~ 第三个 * 任意方法
     * ~ .. 匹配任意数量的参数.
     */
    @Pointcut("execution(* cn.cnbuilder.manage.*.controller..*(..))")
    public void logPointcut() {
    }

    @Pointcut("@annotation(cn.cnbuilder.manage.common.annotation.SystemLogAnnotation)")
    public void executePointCut() {
    }

    @org.aspectj.lang.annotation.Around("logPointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        //获取请求
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        SysUserLog sysUserLog = new SysUserLog();
        long start = System.currentTimeMillis();
        Object result = null;
        //  操作模块和方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        SystemLogAnnotation annotation = signature.getMethod().getAnnotation(SystemLogAnnotation.class);
        try {
            //返回结果
            result = joinPoint.proceed();
        } catch (Throwable e) {
            //保存
            sysUserLog.setErrorDetail(e.getMessage());
            e.printStackTrace();
            //返回错误信息
            result = R.error(500, e.getMessage());
        } finally {
            try {
                //结束时间
                long end = System.currentTimeMillis();
                //执行时间
                sysUserLog.setExcuteTime(end - start);
                //y业务类型
                sysUserLog.setBusinessType(annotation == null ? "" : annotation.businessType());
                //操作时间
                sysUserLog.setCreateTime(new Date());
                //ip地址
                sysUserLog.setIp(IpUtil.getIpAddr(request));
                //接口方法
                sysUserLog.setMethodName(joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
                //请求参数
                sysUserLog.setParams(Arrays.toString(joinPoint.getArgs()));
                //返回结果
                sysUserLog.setReturnResult(result == null ? "" : result.toString());
                User user = userUtil.aroundGetUser();
                //用户id
                sysUserLog.setSysUserId(user.getId());
                //用戶名
                sysUserLog.setSysUserName(user.getUsername());
                //请求url
                sysUserLog.setUrl(request.getRequestURI());
                //方法备注
                sysUserLog.setMethodDetail(annotation == null ? "" : annotation.methodDetail());
                sysUserLogService.save(sysUserLog);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //不管保存不保存日志都需要返回结果
                return result;
            }
        }

    }

}

使用了自定义注解:

file

添加使用注解:
 @SystemLogAnnotation(businessType = "用户", methodDetail = "获取菜单")
自定义注解代码(可以自行添加别的参数):
package cn.cnbuilder.manage.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解
 *
 * @author 猿码优创 website:https://cnbuilder.cn/ Email:itw@tom.com
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SystemLogAnnotation {

    //  业务类型
    String businessType() default "";

    //方法名称
    String methodDetail() default "";
}

6、Redis使用(目前用的redis连接池,后期可以更换成redisTemplate):
6.1、配置文件:

file

存值:
    JedisUtil.setObject("name", "wangyifan");
取值:
    JedisUtil.getObject("name").toString();
 更多使用详见JedisUtil工具类
7、WebSocket使用

file

正常发送消息:

file

群发消息:

file

具体配置信息我原来写过文章,请移步:https://blog.cnbuilder.cn/archives/SpringBoot_websocket
8、MyBatisPlus逆向工程

修改成自己的连接等配置,执行main方法,就会生成我们需要的controller、service、mapper、xml等文件

8.1、修改数据库配置:
// ======================数据源配置========================
    DataSourceConfig dsc = new DataSourceConfig();
    dsc.setDbType(DbType.MYSQL);   //设置数据库类型,
    dsc.setUrl("jdbc:mysql://xxx.xxx.xxx.xxx:xx/ymyc_manage?serverTimezone=GMT%2B8");  //指定数据库
    dsc.setDriverName("com.mysql.cj.jdbc.Driver");
    dsc.setUsername("root");
    dsc.setPassword("root");
    autoGenerator.setDataSource(dsc);

8.2、修改创建的表

//设置生成的表名,这里使用exClude来进行排除,写入null,表示生成所有表。
strategyConfig.setExclude(null);
//设置表    可变个数的形参    String数组
strategyConfig.setInclude("t_user",""xxx");

8.3、作者:

 gc.setAuthor("猿码优创");// 作者

8.4、包的路径:

pc.setParent("cn.cnbuilder.manage.busi");          

终、、以上奉献给你们的年终福利~
码云地址:https://gitee.com/it-wangyifan/ymyc_manage

鼓励作者写出更好的技术文档,就请我喝一瓶哇哈哈哈哈哈哈哈。。你们的赞助决定我更新的速度哦!

微信:

支付宝:


感谢一路支持我的人。。。。。

Love me and hold me
QQ:69673804(16年老号)
EMAIL:itw@tom.com
友链交换
如果有兴趣和本博客交换友链的话,请按照下面的格式在评论区进行评论,我会尽快添加上你的链接。

网站名称:猿码优创
网站地址:http://blog.cnbuilder.cn
网站描述:年少是你未醒的梦话,风华是燃烬的彼岸花。
网站Logo/头像: [头像地址](https://blog.cnbuilder.cn/upload/2018/7/avatar20180720144536200.jpg)

欢迎关注猿码优创(联系小优优进内部群哦,新鲜技术优先更新):

file