操作文档
[TOC]
部署文档
依赖环境
IDE编译器:IDEA 2020.1
Redis: 随意版本
Maven: 随意版本
PostgreSQL:PostgreSQL 12(随意版本也行)
Nginx: 随意版本
JDK: 1.8
修改配置文件
只需修改web模块下的application.yml配置文件
spring:
datasource:
## 连接地址
url: jdbc:postgresql://localhost:5432/postgres?currentSchema=public
## 数据库用户名
username: postgres
## 数据库密码
password: postgres
redis:
database: 0
## Redis默认无密码,都是默认配置
port: 6379
host: localhost
server:
## 访问端口
port: 8080
servlet:
## url访问前缀
context-path: /mrc
## 日志
logging:
## 修改日志存储路径,本机运行可以删掉
file:
path: C:/Users/Administrator/Desktop/Git/mrc-log/my.log
level:
root: INFO
Redis缓存
缓存@Cacheable 的 Value集中放在util模块下的com.mrc.SysCacheValue
使用interface进行分类
interface下的成员变量是==全局变量==,以下代码
public interface DICT {
// 字典类型
String TYPE = "dice_type";
// 字典值
String VALUE = "dict_value";
}
等同于,原理请百度去
public interface DICT {
// 字典类型
public static final String TYPE = "dice_type";
// 字典值
public static final String VALUE = "dict_value";
}
@CacheEvict简单讲解,allEntries = true意思是属于这个value的缓存全部删除
beforeInvocation = true是在执行下面的代码前就执行删除缓存
@CacheEvict(value = '', allEntries = true, beforeInvocation = true)
Sql数据还原
确保正确安装PostgreSQL 12
打开PostgresSQL客户端pgAdmin4
默认使用public架构
右键public
选择备份文件
选择文件
点击还原,大多数都能成功还原。
Nginx配置
安装好Nginx并测试,自行百度去
安装完成后,打开Nginx安装文件夹,打开conf,用记事本打开nginx.conf,修改以下文件
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
underscores_in_headers on; # 开启请求头下划线支持
client_max_body_size 30m; # 上传文件最大限制
proxy_connect_timeout 600; # 设置超时时间
proxy_read_timeout 600;
proxy_send_timeout 600;
sendfile on;
keepalive_timeout 65;
server {
listen 8081; # 本地Nginx端口
server_name localhost;
location / {
root C:/Users/Administrator/Desktop/Git/work/html; # 前端页面路径
index index.html index.htm;
}
location ^~ /mrc/ { # 服务端访问前缀
proxy_pass http://localhost:8080; # 后端访问地址
proxy_redirect off;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Cookie $http_cookie;
}
}
}
修改完成后重启或开启nginx,重启命令 nginx.exe -s reload
测试是否能打开目标页面,不行自行==百度==去
服务端规范
主要构成
Spring boot 2.X、 MyBatis 、PostgreSQL 12、Redis、maven、Nginx
Maven多模块介绍
本系统基于Maven多模块搭建,各模块直接的依赖如上图,也可查看对应模块的pom.xml文件查看依赖关系,其中work-sheet是根模块,一切模块都依赖它。
web负责controller,service负责业务,dao负责与数据库打交道,domain是实体类,rbac是系统鉴权中心,conf是配置中心,util是各种工具类
MyBatis-Plus
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
代码生成器
代码生成器在项目中的位置:dao模块中的 com.mrc.CodeGenerator.java
使用方法
在CodeGenerator.java中找到以下代码段,修改参数,支持多表生产
/**
* 填入需要生成的表的表名,支持 String...,填入多个表名
*/
strategy.setInclude("");
填好后运行main程序,自动生成entity、dao、service、serviceImpl、controller
==注意:==再生成代码完成后,手动将dao模块下的com.mrc.mapper.xml中的xml文件==剪切==到dao模块中的resources中的xml文件夹中,不然不能运行自定义的sql语句
单表操作可以不用自定义SQL语句,直接在对应的controller编写业务逻辑,各种业务场景可参考web模块的com.mrc.controller.TestController
实体类(domain)规则
可参考domain模块中的com.mrc.entity.Test
==代码生成器==生成的model,需在class文件上加上@JsonInclude(JsonInclude.Include.NON_NULL),作用是值为Null字段不会是被JSON序列号
在标记为ID的属性上加上@JsonSerialize(using = ToStringSerializer.class),==只适用在ID字段为Long属性的字段==,加上后JSON序列化返回前端时会变成String类型
在标记为ID的属性上再加上@NotNull(message = "ID不能为空",groups = Update.class)
在==新增==时属性不能为空或者Null的属性加上@NotEmpty(message = "",groups = Insert.class),message是自定义信息,不会返回给用户看,只会打印在日志中,方便到时排查问题。
在==修改==时不能为空的属性属性加上@NotEmpty(message = "",groups = Insert.class)或@NotNull(message = "",groups = Insert.class),这些==注解==都是==Hibernate Validator==的注解,详情百度了解Hibernate Validator;
在==代码生成器==生成的createDate、modifyDate、isDelete字段上的@TableField注解中,加上该属性select = false,代表在使用MyBatis-Plus查询构造器时不查询该字段。
在自定义的各种dto,vo中,必须实现序列号接口并自定义序列化UID,必要时需按照第二,第三
implements Serializable
private static final long serialVersionUID=1L;
由于使用了lombok,IDEA需安装插件,File—》Settings—》Plugins—》搜索Lombok,并安装
IDEA插件推荐Rainbow Brackets、MyBatisX、Alibaba Java Coding Guidelines、Translastion
Lombok相关注解请自行百度
Accessors(chain = true)有此注解的实体类变为链式对象,实际应用如下
Test test = new Test()
.setId(123L)
.setName("名字")
.setIsDelete(0)
.setCreateDate(LocalDateTime.now())
.setModifyDate(LocalDateTime.of(2020, Month.JANUARY,20,12,20));
业务规则
在==业务类==或者==控制器类=上加上@Slf4j注解
所有需要@Autowired的统一采用构造器注入,例如下面这样子,不用在引用对象上加@Autowired
TestService service;
public TestController(TestService service) {
this.service = service;
}
在有业务错误时,例如==插入失败==,==更新失败==等业务上的错误,需在错误时打印错误日志,使用log.info(),方便错误回溯。错误时统一返回CommonEnum.INTERNAL_SERVER_ERROR错误信息,例子
@DeleteMapping("/{id}")
ResultBody remove(@PathVariable Long id) {
if (!service.removeById(id)) {
log.info("Test删除失败,失败id:{}",id);
return ResultBody.error(CommonEnum.INTERNAL_SERVER_ERROR);
}
return ResultBody.success();
}
统一RESTful接口风格,获取用GET,新增用POST,更新用PUT,删除用DELETE
命名规则
dao层命名规则
获取单个对象: selectXXXByXXX,例子:selectTestById(Long testId);
获取多个对象:selectXXXListByXXX,例子:selectTestListByName(String name);
分页获取多个对象:selectXXXPage(long page, long limit);
统计值:countXXX,例子:countTest();
插入新增: insert(Test test), batchInsert(List testList)
修改更新:updateByXXX或update,例子:updateById(Test test),update(Test test)
删除:deleteByXXX,例子:deleteById(Long id),deleteByIds(List id);
service接口命名规则
获取单个对象: getXXXByXXX,例子:getTestById(Long testId);
获取多个对象:getXXXListByXXX,例子:getTestListByName(String name);
分页获取多个对象:getXXXPage(long page, long limit);
统计值:countXXX,例子:countTest();
插入新增: save(Test test), batchSave(List testList)
修改更新:modifyByXXX或update,例子:modifyById(Test test),update(Test test)
删除:removeByXXX,例子:removeById(Long id),removeByIds(List id);
领域模型(domain)命名规则
基本对象:XXXX,例子:Test
数据传输对象(一般用于接收用户数据):xxxxDto,例子:TestDto
展示对象(一般用于分页数据表格):xxxVo,例子:TestVo
RBAC模块使用方法
在前端HTML新建好页面后,需要操作数据库的三个表,menu,interface,menu_interface来关联菜单与接口的关联。
---------------------------------------新增菜单--------------------------------------------
insert
into
public.menu(
id,
parent_id,
title,
check_arr,
"name",
icon,
jump,
"order"
)
values(
1260459079697305600,
1259738910347890688,
'测试',
0,
'',
'',
'',
0
);
---------------------------------------新增接口-------------------------------------------
insert
into
public.interface(
id,
title,
interface_alias
)
values(
1260459079718277120,
'',
''
),
(
1260459079743442944,
'',
''
),
(
1260459079764414464,
'鏇存柊',
'dict::update'
),
(
1260459079789580288,
'',
''
);
------------------------------------关联菜单与接口关系------------------------------------
insert
into
public.menu_interface(
id,
menu_id,
interface_id,
create_date
)
values(
1260459079814746112,
1260459079697305600,
1260459079718277120,
now()
),
(
1260459079839911936,
1260459079697305600,
1260459079743442944,
now()
),
(
1260459079865077760,
1260459079697305600,
1260459079764414464,
now()
),
(
1260459079890243584,
1260459079697305600,
1260459079789580288,
now()
);
以上ID为自己手动填入,自己手动一一对应
util模块包里的com.mrc.IdFactory有个main方法可以生产10个ID,
Menu菜单表
parent_id为-1的是一级菜单,必须设置title,icon
parent_id为父菜单ID时,它们就产生了关联,必须填写jump,title
order为排列顺序,最好从主页是1开始,到最后的设置,每一级菜单均需设置或者只设置一级菜单
Interface接口表
title命名规则
新增类型接口:【新增】
修改类型接口:【修改】
读取类型接口:【读取】
删除类型接口:【删除】
interface_alias命名规则
新增类型接口:【xxx::add】,例子 role::add
修改类型接口:【xxx::update】,例子 role::update
读取类型接口:【xxx::get】,例子 role::get
删除类型接口:【xxx::delete】,例子 role::delete
其他类型的自定义:【操作类::操作名称】,例子 user::resetPwd
权限别名常量类
在==interface表==新增了相关接口权限后,需在util模块下的com.mrc.Constant的Authorization下添加相关接口描述,参照已有例子新建;
/**
* 示例权限
*/
DEMO_ADD("demo::add"),
DEMO_GET("demo::get"),
DEMO_DELETE("demo::delete"),
DEMO_UPDATE("demo::update"),
Controller接口鉴权
在每个需要鉴权的接口加上@Authorization()注解
例子:
// 根据ID获取对象
@Authorization(Constant.Authorization.DEMO_GET)
@GetMapping("/{id}")
ResultBody getOne(@PathVariable Long id) {
return ResultBody.success(service.getById(id));
}
注解参数填写刚刚在com.mrc.Constant里面添加的对应常量
==Excel导入==数据工具类使用教程
在util模块下com.mrc.excel是Excel导入工具类
适用于用户适用Excel文件批量导入数据
支持 String,Short,Integer,Long,Date,暂时不支持 Float,Double
使用教程
新建一个Excel模板,并输入数据()这个括号是提示信息,必须是中文的括号,读取时会忽略
新建一个实体类,这里偷懒用到了lombok,@ExcelId是标识数据库为主键的列,@Excel是对应Excel文件的列标题,不用加(),@ExcelId支持String和Long,默认使用雪花ID算法。
单元测试
@SpringBootTest(classes = WebApplication.class)
public class ExcelTest {
@Test
void ExcelRead() {
try {
File file = new File("C:\\Users\\Administrator\\Desktop\\Test.xlsx");
List list = ExcelRead.read(file, T.class);
list.forEach(System.out::println);
} catch (MyExcelException e) {
e.printStackTrace();
}
}
}
打印结果
ExcelRead.read(File file, Class model)是静态方法,直接用
单元测试类放在web模块 —》test—》ExcelTest
==重点:==由于是提供给用户在Web页面上传,上传的文件是先保存到服务器后,再读取服务器上的Excel文件,读取完后默认==删除文件==。所有单元测试后,文件会被删除!修改逻辑请去
com.mrc.excel.ExcelRead中修改逻辑,里面的注释很完整的啦。
==感谢==,读取全靠它,我只负责装配工作
==Redis_Token==简介
在没开发Redis_Token这个基于Token的用户登录验证之前,用的是JWT(JSON WEB TOKEN),但在使用时发现有点问题
登录时间不能续期,过期了只能重新登录获取,也有很多解决方案,但都是要用户端更换Token。
在Token未过期时,用户登出后,Token依然有效。解决方案是加入黑名单。
复制Token,去JWT官网,可以解密,查看JWT存储的信息
所以基于JWT的特性,再结合Redis的特性,开发出一套小而精的登录鉴权
大致原理如下,对象信息是用的Redis的map数据类型存储。
有重复登录处理,逼退旧的,只给登录权限给最新登录的。
前端规范
ID唯一性
因为开启了==标签页功能==,请==务必==注意 ID 的冲突,尤其是在你自己绑定事件的情况。ID 的命令可以遵循以下规则来规避冲突:
LAY-路由-任意名
以消息中心页面为例,假设它的路由为:/app/message/,那么 ID 应该命名为:
删除
ID唯一性同样适用于lay-filter
数据表格页面规范
==最上面必须有==
操作日志其他可参考src\views\demo\testDemo\list.html
JavaScript引用规范
统一采用以下这种方式引用JavaScript文件
layui.use('demo', layui.factory('demo'));
JavaScript文件必须这样定义
layui.define(['form', 'upload', 'table', 'aut'], function (exports) {
var $ = layui.$,
layer = layui.layer,
setter = layui.setter,
view = layui.view,
admin = layui.admin,
form = layui.form,
table = layui.table,
aut = layui.aut;
var ok = setter.response.statusCode.ok; // 200
var fail = setter.response.statusCode.fail; // 400
var error = setter.response.statusCode.error; // 500
var forbidden = setter.response.statusCode.forbidden;// 403
// 下面的demo对应HTML页面的layui.use('demo', layui.factory('demo'));
exports('demo', {});
})
==不得采用传统方式导入JavaScript文件==
数据表格JavaScript规范
数据表格代码
var myTable = table.render({// 生成一个引用对象,方便表格重载
elem: '#LAY-demo-testDemo-table', // 表格唯一ID
url: '/mrc/test', // 获取数据的URL,默认采用GET方式访问
page: true, // 开启分页
loading: true, // 开启分页加载层
title:"示例", // 定义 table 的大标题(在文件导出等地方会用到)
toolbar: true, // 开启工具条,可导出Excel
autoSort: false, // 关闭前端的排序,启动服务端的排序
headers: { // 加上Token请求头
access_token: layui.data('layuiAdmin').access_token
},
cols: [
[{
checkbox: true // 开启多选框,由于权限操作需要,关闭行操作按钮
},
{
field: 'id', // 对应属性名
title: 'ID' // 表头
}, {
field: 'name',
title: '用户名'
}, {
field: 'create_date', // 当需要开启服务端排序时,该字段填写数据库字段名
title: '时间',
align: 'center',
templet: function(d) { // 并采用templet返回该行数据
return util.toDateString(d.createDate)
},
sort: true
}
]
]
});
表格重载方法(封装成函数)
function table_reload() {
myTable.reload({ // 这里的myTable对象是上面数据表格生成的引用对象
url: '/mrc/test' // 重载地址,不用指定重载页数,默认当前页面数
})
}
表格搜索
// LAY-demo-testDemo-table-search-btn为搜索按钮的lay-filter值
form.on('submit(LAY-demo-testDemo-table-search-btn)', function (data) {
var field = data.field;
myTable.reload({ // 这里的myTable对象是上面数据表格生成的引用对象
url: '/mrc/test', // 重载地址,不用指定重载页数,默认当前页面数
where: field // 搜索对象
});
return false; // 当搜索的form为