修改图片路径

This commit is contained in:
fxb 2018-10-07 17:53:22 +08:00
parent bb64ddf34b
commit c344f1be83
45 changed files with 1527 additions and 934 deletions

View File

@ -6,7 +6,7 @@
横切关注点可以被描述为影响应用多处的功能,切面能够帮助我们模块化横切关注点。下图直观呈现了横切关注点的概念: 横切关注点可以被描述为影响应用多处的功能,切面能够帮助我们模块化横切关注点。下图直观呈现了横切关注点的概念:
![横切关注点](./3.面向切面的Spring/切面示例.png) ![横切关注点](./picFolder/切面示例.png)
途中CourseServiceStudentServiceMiscService都需要类似安全、事务这样的辅助功能这些辅助功能就被称为横切关注点。 途中CourseServiceStudentServiceMiscService都需要类似安全、事务这样的辅助功能这些辅助功能就被称为横切关注点。
@ -211,7 +211,7 @@ public class Audience{
  既然可以用AOP为对象拥有的方法添加新功能那为什么不能为对象增加新的方法呢利用被称为**引入**的AOP概念切面可以为Spring Bean添加新的方法示例图如下   既然可以用AOP为对象拥有的方法添加新功能那为什么不能为对象增加新的方法呢利用被称为**引入**的AOP概念切面可以为Spring Bean添加新的方法示例图如下
![引入](.\3.面向切面的Spring\引入新功能.png) ![引入](./picFolder/引入新功能.png)
当引入接口的方法被调用时代理将此调用委托给实现了新接口的某个其他对象。实际上Bean的实现被拆分到了多个类。 当引入接口的方法被调用时代理将此调用委托给实现了新接口的某个其他对象。实际上Bean的实现被拆分到了多个类。

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,48 +0,0 @@
  前面的博客有说到spring boot搭建见另一篇博文其实那篇博文还没写现在来填个坑。我们使用spring initializr来构建idea和eclipse都支持这种方式构建过程类似这里以idea为例详细记录构建过程。
###1.选择spring initializr
![1532967570728](D:\笔记\markdown\springboot系列\springboot搭建.assets\1532967570728.png)
next
#### 2.设置参数
![1532967772110](D:\笔记\markdown\springboot系列\springboot搭建.assets\1532967772110.png)
next
#### 3.选择依赖
  在这里选择spring boot版本和web依赖忽略sql的依赖如有需要[点击这里](f),单独将mybatis的整合)后面也可手动编辑pom文件修改增加删除依赖
![1532967938985](D:\笔记\markdown\springboot系列\springboot搭建.assets\1532967938985.png)
这里我们选择web搭建一个简单的REST风格demo。然后next。
####4.设置项目存放地址
![1532968024509](D:\笔记\markdown\springboot系列\springboot搭建.assets\1532968024509.png)
这样就成功构建了一个springboot项目。
#### 5.测试
  现在新建一个controller包包下新建一个HelloController,创建之后项目目录结构如下:
![1532969025023](D:\笔记\markdown\springboot系列\springboot搭建.assets\1532969025023.png)
HelloController代码如下
```java
@RestController
@RequestMapping("/home")
public class HelloController{
@GetMapping("/hello")
public String sayHello(){
return "hello";
}
}
```
然后运行项目访问localhost:8080/home/hello即可看到hello字符串。

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -2,13 +2,13 @@
###1.选择spring initializr ###1.选择spring initializr
![1532967570728](.\springboot搭建.assets\1532967570728.png) ![1532967570728](./picFolder/1532967570728.png)
next next
#### 2.设置参数 #### 2.设置参数
![1532967772110](D:\笔记\markdown\springboot系列\springboot搭建.assets\1532967772110.png) ![1532967772110](./picFolder/1532967772110.png)
next next
@ -16,13 +16,13 @@ next
  在这里选择spring boot版本和web依赖忽略sql的依赖如有需要[点击这里](f),单独将mybatis的整合)后面也可手动编辑pom文件修改增加删除依赖   在这里选择spring boot版本和web依赖忽略sql的依赖如有需要[点击这里](f),单独将mybatis的整合)后面也可手动编辑pom文件修改增加删除依赖
![1532967938985](D:\笔记\markdown\springboot系列\springboot搭建.assets\1532967938985.png) ![1532967938985](./picFolder/1532967938985.png)
这里我们选择web搭建一个简单的REST风格demo。然后next。 这里我们选择web搭建一个简单的REST风格demo。然后next。
####4.设置项目存放地址 ####4.设置项目存放地址
![1532968024509](D:\笔记\markdown\springboot系列\springboot搭建.assets\1532968024509.png) ![1532968024509](./picFolder/1532968024509.png)
这样就成功构建了一个springboot项目。 这样就成功构建了一个springboot项目。
@ -30,7 +30,7 @@ next
  现在新建一个controller包包下新建一个HelloController,创建之后项目目录结构如下:   现在新建一个controller包包下新建一个HelloController,创建之后项目目录结构如下:
![1532969025023](D:\笔记\markdown\springboot系列\springboot搭建.assets\1532969025023.png) ![1532969025023](./picFolder/1532969025023.png)
HelloController代码如下 HelloController代码如下

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -33,7 +33,7 @@
然后重启数据库,使用`show master status;`语句查看主库状态,如下所示: 然后重启数据库,使用`show master status;`语句查看主库状态,如下所示:
![主库状态](.\读写分离配置\pic1.png) ![主库状态](./picFolder/pic1.png)
- 从库配置 - 从库配置
@ -59,7 +59,7 @@
接着运行`start slave;`开启备份,正常情况如下图所示Slave_IO_Running和Slave_SQL_Running都为yes。 接着运行`start slave;`开启备份,正常情况如下图所示Slave_IO_Running和Slave_SQL_Running都为yes。
![1536223020742](.\读写分离配置\pic2.png) ![1536223020742](./picFolder/pic2.png)
可以用这个步骤开启多个从库。 可以用这个步骤开启多个从库。
@ -306,6 +306,6 @@ public class ReadOnlyInterceptor implements Ordered {
  编写好代码来试试结果如何,下面是运行截图:   编写好代码来试试结果如何,下面是运行截图:
![1536312274474](.\读写分离配置\pic3.png) ![1536312274474](./picFolder/pic3.png)
  断断续续写了好几天终于是写完了如果有帮助到你欢迎star哦这里是完整代码地址[点击跳转](https://github.com/FleyX/demo-project/tree/master/dxfl)   断断续续写了好几天终于是写完了如果有帮助到你欢迎star哦这里是完整代码地址[点击跳转](https://github.com/FleyX/demo-project/tree/master/dxfl)

View File

@ -0,0 +1,17 @@
  项目组最近加了一个新功能到代码中使用flyway记录数据库版本变更该工具会记录每次数据库结构的修改并生成sql文件存在指定目录上当然必须用它来变更数据库外部的变更它是无法感知的然后每次启动时flyway会检查使用的数据库和当前项目代码中的sql变更版本是否一致一致正常启动不一致中如果是数据库落后将会更新数据库这样能够保证代码在任何地方运行数据库都是一致的),否则就报错了。数据库中有一张表记录版本信息,如下图:
![版本记录](./picFolder/版本记录.PNG),同时本地代码中也有一个文件夹保存每次操作的sql语句如下图
![版本sql](./picFolder/版本sql.PNG)
通过对比checksum值来判断当前sql语句和生成数据库的执行语句是否一致checksum值由CRC32计算后处理得出。
  然后问题就来了组中的其他人搭建好flyway后项目文件生成了两个sql文件我用git拉下来后启动报错checkupsum值对不上然后我又不懂这个flyway完全不知道咋回事然后就根据报错的位置一点点找到checkup值生成的代码发现是CRC32计算的就这么搞了一两个小时才发现是文件不一致了但是都是从git拉的怎么就我不一致呢想到可能是文件换行符的问题遂把那几个sql文件的文件换行符全换成了crlfwindows中的换行符然后居然就能够运行。。。关于为啥都从git拉取的文件换行符会不一样原因是他们都用的那个小乌龟的可视化我用的命令行。可视化工具自动配置了文件换行符的自动转换这是git的一个智能功能上传时将文件换行符替换为lf拉取时再替换为crlf这样保证中心仓库使用UNIX风格的换行符本地能够根据运行环境使用相对应的换行符风格但是命令行并没有配置。
  解决办法也很简单开启git 的自动转换。
```
git config --global core.autocrlf true //开启换行符自动转换
git config --global core.safecrlf true //禁止混用换行符
```

View File

@ -0,0 +1,177 @@
## 一、背景
  最近在java上做了一个EXCEL的导出功能写了一个通用类在这里分享分享该类支持多sheet且无需手动进行复杂的类型转换只需提供三个参数即可
- `fileName`
excel文件名
- `HasMap<String,List<?>> data`
具体的数据每个List代表一张表的数据表示可为任意的自定义对象
- `LinkedHashMap<String,String[][]> headers`
`Stirng`代表sheet名。每个`String[][] `代表一个sheet的定义举个例子如下
```java
String[][] header = {
{"field1","参数1"}
{"field2","参数2"}
{"field3","参数3"}
}
```
其中的field1field2field3为对象中的属性名参数1参数2参数3为列名实际上这个指定了列的名称和这个列用到数据对象的哪个属性。
## 二、怎么用
&emsp;&emsp;以一个例子来说明怎么用假设有两个类A和B定义如下
```java
public class A{
private String name;
private String address;
}
public class B{
private int id;
private double sum;
private String cat;
}
```
现在我们通过查询数据库获得了A和B的两个列表
```java
List<A> dataA = .....;
List<B> dataB = .....;
```
我们将这两个导出到excel中首先需要定义sheet
```java
String[][] sheetA = {
{"name","姓名"}
,{"address","住址"}
}
String[][] sheetB = {
{"id","ID"}
,{"sum","余额"}
,{"cat","猫的名字"}
}
```
然后将数据汇总构造一个ExcelUtil
```java
String fileName = "测试Excel";
HashMap<String,List<?>> data = new HashMap<>();
//ASheet为表名后面headers里的key要跟这里一致
data.put("ASheet",dataA);
data.put("BSheet",dataB);
LinkedHashMap<String,String[][]> headers = new LinkedHashMap<>();
headers.put("ASheet",sheetA);
headers.put("BSheet",sheetB);
ExcelUtil excelUtil = new ExcelUtil(fileName,data,headers);
//获取表格对象
HSSFWorkbook workbook = excelUtil.createExcel();
//这里内置了一个写到response的方法判断浏览器类型设置合适的参数如果想写到文件也是类似的
workbook.writeToResponse(workbook,request,response);
```
当然通常数据是通过数据库查询的,这里为了演示方便没有从数据库查找。
## 三、实现原理
&emsp;&emsp;这里简单说明下实现过程,从调用`createExcel()`这里开始
####1、遍历headers创建sheet
```java
public HSSFWorkbook createExcel() throws Exception {
try {
HSSFWorkbook workbook = new HSSFWorkbook();
//遍历headers创建表格
for (String key : headers.keySet()) {
this.createSheet(workbook, key, headers.get(key), this.data.get(key));
}
return workbook;
} catch (Exception e) {
log.error("创建表格失败:{}", e.getMessage());
throw e;
}
}
```
将workbooksheet名表头数据行数据传入crateSheet方法中创建sheet。
#### 2、创建表头
&emsp;&emsp;表头也就是一个表格的第一行,通常用来对列进行说明
```java
HSSFSheet sheet = workbook.createSheet(sheetName);
// 列数
int cellNum = header.length;
// 单元行,单元格
HSSFRow row;
HSSFCell cell;
// 表头单元格样式
HSSFCellStyle columnTopStyle = this.getColumnTopStyle(workbook);
// 设置表头
row = sheet.createRow(0);
for (int i = 0; i < cellNum; i++) {
cell = row.createCell(i);
cell.setCellStyle(columnTopStyle);
String str = header[i][1];
cell.setCellValue(str);
// 设置列宽为表头的文字宽度+6个半角符号宽度
sheet.setColumnWidth(i, (str.getBytes("utf-8").length + 6) * 256);
}
```
#### 3、插入行数据
&emsp;&emsp;这里是最重要的部分首先通过数据的类对象获取它的反射属性Field类然后将属性名和Field做一个hash映射避免循环查找提高插入速度接着通过一个switch语句根据属性类别设值主要代码如下
```java
/**
* 设置单元格,根据fieldName获取对应的Field类使用反射得到值
*
* @param cell 单元格实例
* @param obj 存有属性的对象实例
* @param fieldMap 属性名与Field的映射
* @param fieldName 属性名
*/
private void setCell(HSSFCell cell, Object obj, Map<String, Field> fieldMap, String fieldName) throws Exception {
//获取该属性的Field对象
Field field = fieldMap.get(fieldName);
//通过反射获取属性的值,由于不能确定该值的类型,用下面的判断语句进行合适的转型
Object value = field.get(obj);
if (value == null) {
cell.setCellValue("");
} else {
switch (field.getGenericType().getTypeName()) {
case "java.lang.String":
cell.setCellValue((String) value);
break;
case "java.lang.Integer":
case "int":
cell.setCellValue((int) value);
break;
case "java.lang.Double":
case "double":
cell.setCellValue((double) value);
break;
case "java.util.Date":
cell.setCellValue(this.dateFormat.format((Date) value));
break;
default:
cell.setCellValue(obj.toString());
}
}
}
```
完整代码可以到github上查看下载这里就不列出来了。
github地址[点击跳转]()

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -0,0 +1,48 @@
##一、背景
&emsp;&emsp;今天领导让我写几个正则表达式来对密码做强度验证,听到写正则表达式内心是这样的感觉(哈哈,三分钟搞定,今天又可以打鱼了)。需求如下:密码组成只能是**数字****字母****英文可见半角符号**然后需要如下4个表达式:
- 长度6位及以上
- 长度6位及以上包含数字包含字母
- 长度6位及以上包含数字包含字母包含半角符号
- 长度六位及以上,包含数字,包含大写字母,包含小写字母,包含半角符号
&emsp;&emsp;看完需求我就有点懵了,包含数字或者字母我会写,但是同时存在还要在一个表达式中就有点懵了。
##二、解决方法
&emsp;&emsp;以第三种为例,这个可以分解为如下需求:
- 存在数字
- 存在字母
- 存在半角符号
- 长度六位及以上
关键是如何同时满足前三个条件,在我有限的知识里并不知道怎么搞,然后只好求助于万能的百度了,最终在找了几个小时后发现如下几个关键词,来源[菜鸟教程](http://www.runoob.com/java/java-regular-expressions.html)
- (?=*pattern*) :正向预测先行搜索
名字看着高大上不明所以看完示例大概明白什么意思这个表达式匹配从这个表达式起始的字符串我也不知道咋解释就是假设这样一个表达式abc(?=[abc]) ,用它来匹配abc123字符串(?=[abc])只会对作用于后面的123这个显然是不匹配的后整个就不匹配了然后关键来了名字里有**预测**两个字,这两个字表名了这个表达式的特性:不占用字符,匹配后如果匹配成功就继续匹配了好像从来不存在这个东西一样,匹配失败就立即返回失败了。利用这个特性我们就可以给正则加限制条件了。
- (?!*pattern*) :反向预测先行搜索
概念和上面一样但是效果是相反的abc(?![abc]),对于abc123是匹配成功的对于abca匹配失败如下所示
```javascript
reg = /abc(?![abc])/;
reg.test("abc123")
//返回true
reg.test("abca")
//返回false
```
&emsp;&emsp;有了上面的知识就能搞定需求啦。
## 三、结果
&emsp;&emsp;对于存在字母我们可以用这样的表达式`(?=.\*?[a-zA-Z]+.\*?),来检查是否存在至少一个字母最后对于需求3的表达式如下(半角字符我用的ASCII码里的16进制表示的)
```javascript
^(?=.*?\d+.*?)(?=.*?[a-zA-Z]+.*?)(?=.*?[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+.*?)[\da-zA-Z\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]{6,}$
```

452
其他各种/毕设.md Normal file
View File

@ -0,0 +1,452 @@
&emsp;&emsp;毕业才刚刚两个多月而已,现在想想大学生活是那么的遥不可及,感觉已经过了好久好久,社会了两个月才明白学校的好啊。。。额,扯远了,自从毕业开始就想找个时间写下毕设的记录总结,结果找了好久好久到今天才开始动笔。
&emsp;&emsp;我的毕业设计题目是:教学辅助系统的设计与实现,,是不是很俗。。。至于为啥是这个题目呢,完全是被导师坑了。。。。。
## 1、需求分析
&emsp;&emsp;拿到这个题目想着这个可能被做了无数次了,就像着哪里能够做出点创新,,最后强行创新出了一个个性化组题(根据学生水平出题)和徽章激励(达到某个要求给予一个徽章)。最后就产生了如下需求,系统有学生端和管理端:
学生端:
- 个人资料设置
- 徽章激励机制
- 查看课程信息,下载课程资料
- 知识点检测及针对性训练
- 在线作业,考试
- 在线答疑,向老师或者学生提问
管理端:
- 课程管理,用户管理(需要管理员权限)
- 课程信息管理
- 课程公告管理
- 题库管理,支持单选,多选,填空,编程题,支持题目编组
- 发布作业,包括个性组题和手动组题
- 发布考试,包括随机出题和手动出题
- 自动判题,支持编程题判重
- 在线答疑,给学生解答
- 统计分析,包含测试统计和课程统计
洋洋洒洒需求列了一大堆,后面才发现是给自己挖坑,,答辩老师一看这类的题目就不感兴趣了,不论你做的咋样(况且我的演讲能力真的很一般),最后累死累活写了一大堆功能也没太高的分,,不过倒是让我的系统设计能力和代码能力有了不少的提高。
## 2、架构选择
&emsp;&emsp;大三的时候了解到Node.js这个比较“奇葩"的异步语言再加上在公司实习了三个月也是用的node开发对node已经比较熟悉了于是就用它做了后台前端用最近比较火的vue.js做单页应用。当时还想着负载均衡啥的就没有用传统的sessioncookie机制转而用jwt做的基于token的身份认证同时后台接口也是类Restful风格的因为纯正的Rest接口太难设计了
总的来说后台用了以下技术和框架:
&emsp;&emsp;总的来说后台用了以下技术和框架:
- 语言Node.js
- web框架kOA
- 前后台传输协议jwt
- 缓存redis
- 数据库mysql
- 编程题判题核心:[青岛大学OJ判题核心](https://github.com/QingdaoU/JudgeServer)
- 代码判重:[SIM](https://dickgrune.com/Programs/similarity_tester/)
前台技术如下:
- 框架Vue.js
- UI框架Element-UI
- 图表组件G2
## 3、系统基础框架搭建
&emsp;&emsp;本系统是前后端分离的,下面分别介绍前后端的实现基础。
### 1、后台
&emsp;&emsp;一个web后台最重要的无非那么几个部分路由权限验证数据持久化。
#### a、路由
KOA作为一个web框架其实它本身并没有提供路由功能需要配合使用koa-router来实现路由koa-router以类似下面这样的风格来进行路由
&emsp;&emsp;KOA作为一个web框架其实它本身并没有提供路由功能需要配合使用koa-router来实现路由koa-router以类似下面这样的风格来进行路由
```javascript
const app = require("koa");
const router = require("koa-router");
router.get("/hello",koa=>{
koa.response="hello";
});
app.use(router.routes())
```
显然这样在项目中是很不方便的如果每个路由都要手动进行挂载很难将每个文件中的路由都挂载到一个router中。因此在参考网上的实现后我写了一个方法在启动时自动扫描某个文件夹下所有的路由文件并挂载到router中代码如下
```javascript
const fs = require('fs');
const path = require('path');
const koaBody = require('koa-body');
const config = require('../config/config.js');
function addMapping(router, filePath) {
let mapping = require(filePath);
for (let url in mapping) {
if (url.startsWith('GET ')) {
let temp = url.substring(4);
router.get(temp, mapping[url]);
console.log(`----GET${temp}`);
} else if (url.startsWith('POST ')) {
let temp = url.substring(5);
router.post(temp, mapping[url]);
console.log(`----POST${temp}`);
} else if (url.startsWith('PUT ')) {
let temp = url.substring(4);
router.put(temp, mapping[url]);
console.log(`----PUT${temp}`)
} else if (url.startsWith('DELETE ')) {
let temp = url.substring(7);
router.delete(temp, mapping[url]);
console.log(`----DELETE: ${temp}`);
} else {
console.log(`xxxxx无效路径${url}`);
}
}
}
function addControllers(router, filePath) {
let files = fs.readdirSync(filePath);
files.forEach(element => {
let temp = path.join(filePath, element);
let state = fs.statSync(temp);
if (state.isDirectory()) {
addControllers(router, temp);
} else {
if (!temp.endsWith('Helper.js')) {
console.log('\n--开始处理: ' + element + "路由");
addMapping(router, temp);
}
}
});
}
function engine(router, folder) {
addControllers(router, folder);
return router.routes();
}
module.exports = engine;
```
然后在index.js中use此方法
```
const RouterMW = require("./middleware/controllerEngine.js");
app.use(RouterMW(router,path.join(config.rootPath, 'api')));
```
然后路由文件以下面的形式编写:
```javascript
const knowledgePointDao = require('../dao/knowledgePointDao.js');
/**
* 返回某门课的全部知识点,按章节分类
*/
exports["GET /course/:c_id/knowledge_point"] = async (ctx, next) => {
let res = await knowledgePointDao.getPontsOrderBySection(ctx.params.c_id);
ctx.onSuccess(res);
}
//返回某位学生知识点答题情况
exports["GET /user/:u_id/course/:c_id/knowledge_point/condition"]=async(ctx,next)=>{
let {u_id,c_id}=ctx.params;
let res = await knowledgePointDao.getStudentCondition(u_id,c_id);
ctx.onSuccess(res);
}
```
#### b、权限验证
&emsp;&emsp;权限管理是一个系统最重要的部分之一,目前主流的方式为**基于角色的权限管理** 一个用户对应多个角色每个角色对应多个权限本系统中每个用户对应一个身份每个身份对应多个角色。我们的系统如何实现的呢先从登录开始说起本系统抛弃了传统的cookiesession模式使用json web tokenJWT来做身份认证用户登录后返回一个token给客户端代码如下所示
```javascript
//生成随机盐值
let str = StringHelper.getRandomString(0, 10);
//使用该盐值生成token
let token = jwt.sign({
u_id: userInfo.u_id,
isRememberMe
}, str, {
expiresIn: isRememberMe ? config.longTokenExpiration:config.shortTokenExpiration
});
//token-盐值存入redis如想让该token过期redis中清楚该token键值对即可
await RedisHelper.setString(token, str, 30 * 24 * 60 * 60);
res.code = 1;
res.info = '登录成功';
res.data = {
u_type: userInfo.u_type,
u_id: userInfo.u_id,
token
};
```
以后每次客户端请求都要在header中设置该token然后每次服务端收到请求都先验证是否拥有权限验证代码使用`router.use(auth)`,挂载到koa-router中这样每次在进入具体的路由前都要先执行auth方法进行权限验证,主要验证代码逻辑如下:
```javascript
/**
* 1 验证成功
* 2 登录信息无效 401
* 3 已登录,无操作权限 403
* 4 token已过期
*/
let verify = async (ctx) => {
let token = ctx.headers.authorization;
if (typeof (token) != 'string') {
return 2;
}
let yan = await redisHelper.getString(token);
if (yan == null) {
return 2;
}
let data;
try {
data = jwt.verify(token, yan);
} catch (e) {
return 2;
}
if (data.exp * 1000 < Date.now()) {
return 4;
}
//判断是否需要刷新token如需要刷新将新token写入响应头
if (!data.isRememberMe && (data.exp * 1000 - Date.now()) < 30 * 60 * 1000) {
//token有效期不足半小时重新签发新token给客户端
let newYan = StringHelper.getRandomString(0, 10);
let newToken = jwt.sign({
u_id: data.u_id,
isRememberMe:false
}, newYan, {
expiresIn: config.shortTokenExpiration
});
// await redisHelper.deleteKey(token);
await redisHelper.setString(newToken, newYan,config.shortTokenExpiration);
ctx.response.set('new-token', newToken);
ctx.response.set('Access-Control-Expose-Headers','new-token');
}
//获取用户信息
let userInfoKey = data.u_id + '_userInfo';
let userInfo = await redisHelper.getString(userInfoKey);
if (userInfo == null || Object.keys(userInfo).length != 3) {
userInfo = await mysqlHelper.first(`select u_id,u_type,j_id from user where u_id=?`, data.u_id);
await redisHelper.setString(userInfoKey, JSON.stringify(userInfo), 24 * 60 * 60);
}else{
userInfo = JSON.parse(userInfo);
}
ctx.userInfo = userInfo;
//更新用户上次访问时间
mysqlHelper.execute(`update user set last_login_time=? where u_id=?`,Date.now(),userInfo.u_id);
//管理员拥有全部权限
if (userInfo.u_type == 0) {
return 1;
}
//获取该用户类型权限
let authKey = userInfo.j_id + '_authority';
let urls = await redisHelper.getObject(authKey);
// let urls = null;
if (urls == null) {
urls = await mysqlHelper.row(`
select b.r_id,b.url,b.method from jurisdiction_resource a inner join resource b on a.r_id = b.r_id where a.j_id=?
`, userInfo.j_id);
let temp = {};
urls.forEach(item => {
temp[item.url + item.method] = true;
})
await redisHelper.setObject(authKey, temp);
urls = temp;
}
//判断是否拥有权限
if (urls.hasOwnProperty(ctx._matchedRoute.replace(config.url_prefix, '') + ctx.method)) {
return 1;
} else {
return 3;
}
}
```
根据用户id获取用户身份id根据用户身份id从redis中获取拥有的权限如为null从mysql数据库中拉取并存入redis中然后判断是否拥有要访问的url权限。
#### c、数据持久化
&emsp;&emsp;本系统中使用mysql存储数据redis做缓存由于当时操作库不支持promise故对它两做了个promise封装方便代码中调用参见[MysqlHelper](https://github.com/FleyX/teach_system/tree/master/teachSystem/util/MysqlHelper.js),[RedisHelper.js](https://github.com/FleyX/teach_system/tree/master/teachSystem/util/RedisHelper.js)。
### 2、前端
&emsp;&emsp;前端使用vue-cli构建vue项目主要用到了vue-router,element-ui,axios这三个组件。
#### a、路由组织
&emsp;&emsp;单页应用需要前端自己组织路由。本系统将路由分成了三个部分公共管理端学生端。index.js如下
```javascript
export default new Router({
mode: 'history',
base: '/app/',
routes: [{
path: '',
name: 'indexPage',
component: IndexPage
},
{
path: '/about',
name: 'about',
component: About
},
Admin,
Client,
Public,
{
path: '*',
name: "NotFound",
component: NotFound
}
]
})
```
其中的AdminClientPublic分别为各部分的路由以子路由的形式一级级组织。如下所示
```javascript
export default {
path: "/client",
component: Client,
beforeEnter: (to, from, next) => {
if (getClientUserInfo() == null) {
next({
path: '/public/client_login',
replace: true,
})
} else {
next();
}
},
children: [{
//学生端主页
path: '',
name: "ClientMain",
component: ClientHome
}, {
//学生个人资料页面
path: 'person/student_info',
name: "StudentInfo",
component: StudentInfo
}, {
//公告页面
path: 'course/:c_id/announcement',
name: 'Main',
component: Announcement
}, {
//课程基本信息
path: 'course/:c_id/base',
component: ClientMain,
children: [{
path: 'course_intro',
name: "ClientCourseIntro",
component: CourseIntro
}, {
path: 'exam_type',
name: "ClientExamType",
component: ExamType
}
......
```
其中的beforEnter为钩子函数每次进入路由时执行该函数用于判断用户是否登录。这里涉及到了一个前端鉴权的概念由于前后端分离了前端也必须做鉴权以免用户进入到了无权限的页面这里我只是简单的做了登录判断更详细的url鉴权也可实现只需在对应的钩子函数中进行鉴权操作更多关于钩子函数信息[点击这里](https://router.vuejs.org/zh/guide/advanced/navigation-guards.html)。
#### b、请求封装
&emsp;&emsp;前端还有一个比较重要的部分是ajax请求的处理请求处理还保护错误处理有些错误只需要统一处理而有些又需要独立的处理这样一来就需要根据业务需求进行一下请求封装了对结果进行处理后再返回给调用者。我的实现思路是发起请求收到响应后先对错误进行一个同意弹窗提示然后再将错误继续向后传递调用者可选择性的捕获错误进行针对性处理主要代码如下
```javascript
request = (url, method, params, form, isFormData, type) => {
let token;
if (type == 'admin')
token = getToken();
else
token = getClientToken();
let headers = {
'Authorization': token
};
if (isFormData) {
headers['Content-Type'] = "multipart/form-data";
}
return new Promise((resolve, reject) => {
axios({
url,
method,
params,
data: form,
headers,
// timeout:2000
}).then(res => {
resolve(res.data);
//检查是否有更新token
// console.log(res);
if (res.headers['new-token'] != undefined) {
console.log('set new token');
if (vm.$route.path.startsWith('/admin')){
localStorage.setItem("token",res.headers['new-token']);
window.token = undefined;
}else if(vm.$route.path.startsWith('/client')){
localStorage.setItem("clientToken",res.headers['new-token']);
window.clientToken = undefined;
}
}
}).catch(err => {
reject(err);
if (err.code == 'ECONNABORTED') {
alertNotify("错误", "请求超时", "error");
return;
}
if (err.message == 'Network Error') {
alertNotify("错误", "无法连接服务器", 'error');
return;
}
if (err.response != undefined) {
switch (err.response.status) {
case 401:
if (window.isGoToLogin) {
return;
}
//使用该变量表示是否已经弹窗提示了,避免大量未登录弹窗堆积。
window.isGoToLogin = true;
vm.$alert(err.response.data, "警告", {
type: "warning",
showClose: false
}).then(res => {
window.isGoToLogin = false;
if (vm.$route.path.startsWith('/admin/')) {
clearInfo();
vm.$router.replace("/public/admin_login");
} else {
clearClientInfo();
vm.$router.replace("/public/client_login");
}
});
break;
case 403:
alertNotify("Error:403", '拒绝执行:' + err.response.data, "error");
break;
case 404:
alertNotify("Error:404", "找不到资源:" + url.substr(0, url.indexOf('?')), 'error');
break;
case 400:
alertNotify("Error:400", "请求参数错误:" + err.response.data, 'error');
break;
case 500:
alertNotify("Error:500", "服务器内部错误:" + err.response.data, 'error');
default:
console.log('存在错误未处理:' + err);
}
} else {
console.log(err);
}
})
})
}
```
&emsp;&emsp;到这里就算是简单介绍完了,,想要更加深入了解的可以去github查看源代码地址如下[https://github.com/FleyX/teach_system](https://github.com/FleyX/teach_system)记得star哦

View File

@ -0,0 +1,57 @@
## 1、wmware设置
&emsp;&emsp;这篇记录下nat网络模式下虚拟机与主机的相互ping通。首先使用wmware建立一个ubuntu虚拟机网络模式选择nat模式。然后点击虚拟网络编辑
![1536153781962](./picFolder/pic1.png)
接下来点击nat设置
![1536153954193](./picFolder/pic2.png)
看到如下:
![pic3](./picFolder/pic3.png)
上面红框是关键记录这个值下面虚拟机设置静态ip要用到。
## 2、window网络设置
&emsp;&emsp;打开网络适配器页面选择VMnet,右键->属性->Internet协议版本 4TCP/IPV4->属性设置ip地址为上面上面网关地址最后一个数改成1比如192.168.128.2就要设置为192.168.128.1同时设置子网掩码为255.255.255.0,默认网关不要填。我的如下:
![pic4](./picFolder/pic4.png)
**如果想让虚拟机能够访问主机需要关闭主机的防火墙**
## 3、ubuntu设置
&emsp;&emsp;编辑/etc/network/interfaces
```bash
vim /etc/network/interfaces
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).
source /etc/network/interfaces.d/*
# The loopback network interface
auto lo
iface lo inet loopback
# The primary network interface
auto ens33
# dhcp 改成static然后设置下面的address,netmask,gateway
iface ens33 inet static
address 192.168.128.129
netmask 255.255.255.0
gateway 192.168.128.2
# 设置dns
dns-nameservers 192.168.128.2
```
然后执行`/etc/init.d/networking restart`,或者重启虚拟机以启用网络设置。
## 3、验证
&emsp;&emsp;现在虚拟机中`ping 192.168.128.1`可以ping通主机中`ping 192.168.128.129`也可ping通。

BIN
网络/picFolder/pic1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
网络/picFolder/pic2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
网络/picFolder/pic3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
网络/picFolder/pic4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB