持续学习&持续更新中…
守破离
【Github】项目源码地址:https://github.com/lpruoyu/JAVAEE_PROJECT_jiakao
后台:
前端:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>programmer.lp.jkbe_v3groupId>
<artifactId>JiaKaoBE_V3artifactId>
<packaging>jarpackaging>
<version>1.0.0version>
<parent>
<artifactId>spring-boot-starter-parentartifactId>
<groupId>org.springframework.bootgroupId>
<version>2.3.4.RELEASEversion>
parent>
<properties>
<druid.version>1.2.1druid.version>
<mybatis.plus.version>3.4.1mybatis.plus.version>
<tinypinyin.version>2.0.3tinypinyin.version>
<mapStruct.version>1.4.1.FinalmapStruct.version>
<captcha.version>1.6.2captcha.version>
<commons.io.version>2.11.0commons.io.version>
<swagger.models.version>1.6.2swagger.models.version>
<swagger.triui.version>1.9.6swagger.triui.version>
<swagger.version>2.9.2swagger.version>
<shiro.version>1.7.0shiro.version>
<springfox.version>3.0.0springfox.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>${druid.version}version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>${mybatis.plus.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>com.github.promeggroupId>
<artifactId>tinypinyinartifactId>
<version>${tinypinyin.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>${swagger.version}version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>${swagger.version}version>
dependency>
<dependency>
<groupId>io.swaggergroupId>
<artifactId>swagger-modelsartifactId>
<version>${swagger.models.version}version>
dependency>
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>swagger-bootstrap-uiartifactId>
<version>${swagger.triui.version}version>
dependency>
<dependency>
<groupId>org.mapstructgroupId>
<artifactId>mapstructartifactId>
<version>${mapStruct.version}version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.mapstructgroupId>
<artifactId>mapstruct-processorartifactId>
<version>${mapStruct.version}version>
<scope>providedscope>
dependency>
<dependency>
<groupId>com.github.whvcsegroupId>
<artifactId>easy-captchaartifactId>
<version>${captcha.version}version>
dependency>
<dependency>
<groupId>org.ehcachegroupId>
<artifactId>ehcacheartifactId>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-spring-boot-web-starterartifactId>
<version>${shiro.version}version>
dependency>
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
<version>${commons.io.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<finalName>jkfinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
以前这种协作模式的问题:
# 错误的返回结果,并且设置HTTP状态码为400/500...
{
"code": 8001,
"msg": "密码错误"
}
# 正确的返回结果
{
"code": 8000,
"msg": "添加成功",
"data": [ ... ]
}
https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS/Errors/CORSMissingAllowOrigin
@RestController
//@CrossOrigin("*") // 所有的源都可以跨域访问该Controller下的请求
//@CrossOrigin({"http://localhost:63343","http://192.168.152.130:8888"})
//@CrossOrigin("http://localhost:63343")
public class UserController {
@GetMapping("/users")
@CrossOrigin("http://localhost:63343")
public List<User> user() {
List<User> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(new User("lp" + i, i));
}
return list;
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// registry.addMapping("/users/*")
registry.addMapping("/**")
// .allowedOrigins("*")
.allowedOrigins("http://localhost:63343")
.allowCredentials(true) // 允许客户端发送Cookie
.allowedMethods("GET", "POST");
}
}
项目中可以这样配置:
@Component
@Data
@ConfigurationProperties("cors")
public class CORSProperties {
String pathPattern; // 允许哪些路径下的API被跨域访问
String[] origins; // 允许跨域请求的源
String[] methods; // 允许跨域请求的方法类型
boolean allowCredentials; // 是否允许Cookie
// 详细信息可以参考org.springframework.web.bind.annotation.CrossOrigin
// String[] allowedHeaders;
// String[] exposedHeaders;
}
cors:
path-pattern: /**
methods:
- GET
- POST
origins:
- http://localhost:63343
- http://192.168.152.130:8888
allow-credentials: true
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private CORSProperties corsProperties;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping(corsProperties.getPathPattern())
.allowedOrigins(corsProperties.getOrigins())
.allowCredentials(corsProperties.isAllowCredentials())
.allowedMethods(corsProperties.getMethods());
}
}
客户端使用:
<button type="button" id="load-btn">加载用户信息button>
<script src="./js/jquery.min.js">script>
<script>
$(() => {
$('#load-btn').click(() => {
$.getJSON('http://localhost:8080/jk/users', (users) => {
const $table = $('')
$(document.body).append($table)
for (const user of users) {
const $tr = $('')
$table.append($tr)
$tr.append(`${user.name} `)
$tr.append(`${user.age} `)
}
})
})
})
script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
Layui
- 官网:http://layuimini.99php.cn/
- 文档:http://layuimini.99php.cn/docs/
- 演示:http://layuimini.99php.cn/onepage/v2/index.html
- Github:https://github.com/zhongshaofa/layuimini/tree/v2-onepage
- 下载:https://codeload.github.com/zhongshaofa/layuimini/zip/refs/heads/v2-onepage


注意:Layui中的相对路径都是相对于index.html来说的。
注意:layuimini的表格要求服务器返回的JSON数据格式如下:
{
"code": 0,
"data": [
{
"id": 1,
"name": "职业",
"value": "job",
"intro": "一份工作"
},
{
"id": 2,
"name": "性格",
"value": "character",
"intro": "人的性格"
}
],
"count": 87
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
MySQL建议


最大IP地址:255.255.255.255
CREATE TABLE user(
age TINYINT UNSIGNED,
ip VARCHAR(15) #需要15个字节
)
CREATE TABLE user(
age TINYINT UNSIGNED,
ip INT UNSIGNED #只需要4个字节
)
INSERT INTO user VALUES(10, INET_ATON('255.255.255.255'))
SELECT INET_NTOA(ip) FROM user
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15

注意:
-
索引只能给非空列进行优化
-
不使用外键有可能导致数据的一致性出现问题,因此需要自己在Java代码的业务层做好业务逻辑控制
-
数据的一致性远远没有数据库的性能重要
-
高并发&分布式下的系统为了性能更优以及更好维护,不能使用外键。一般而言只要自己在应用层面做好数据库之间关系的维护,那么不使用外键是完全没有问题的。
数据字典


使用PowerDesigner:

/*==============================================================*/
/* DBMS name: MySQL 5.0 */
/* Created on: 2022-05-05 23:16:43 */
/*==============================================================*/
drop table if exists dict_item;
drop table if exists dict_type;
/*==============================================================*/
/* Table: dict_item */
/*==============================================================*/
create table dict_item
(
id bigint not null,
name varchar(20) not null,
value varchar(20) not null,
no int not null default 0 comment '用来排序,数字越小,优先级越高,越先展示',
type_id bigint comment '该条目所属的数据字典类型',
status int not null default 1 comment '是否启用该条目,0:不启用,1:启用',
primary key (id),
unique key AK_UK_1 (name, type_id),
unique key AK_UK_2 (value, type_id)
);
alter table dict_item comment '数据字典每一项具体的内容';
/*==============================================================*/
/* Table: dict_type */
/*==============================================================*/
create table dict_type
(
id bigint not null auto_increment,
name varchar(20) not null comment '名称是展示在客户端的,是有可能会发生改变的',
value varchar(20) not null comment '值不会发生改变,编写SQL操作数据时,一般使用value而不是name',
intro varchar(100) comment '防止程序员忘记该数据字典类型的作用、功能(根据项目需求可有可无)',
primary key (id),
unique key AK_UK_1 (name),
unique key AK_UK_2 (value)
);
alter table dict_type comment '数据字典类型';
alter table dict_item add constraint FK_Reference_1 foreign key (type_id)
references dict_type (id) on delete restrict on update restrict;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
使用IDEA自带的数据库工具+不使用外键+数据库使用最优字段类型:

create table jk.dict_type
(
id smallint unsigned auto_increment comment '主键'
primary key,
name varchar(20) default '' not null comment '名称',
value varchar(20) default '' not null comment '值',
intro varchar(100) default '' not null comment '简介',
constraint dict_type_name_uindex
unique (name),
constraint dict_type_value_uindex
unique (value)
)
comment '数据字典类型';
create table jk.dict_item
(
id smallint unsigned auto_increment comment '主键'
primary key,
name varchar(20) default '' not null comment '名称',
value varchar(20) default '' not null comment '值',
type_id smallint unsigned not null comment '类型id',
sn smallint unsigned default 0 not null comment '排序序号:默认为0,值越大,越优先排列展示',
# 其实个人认为enabled这个字段的类型可以设置为bool或者boolean
enabled tinyint unsigned default 1 not null comment '是否启用:0,禁用;1,启用;默认为1',
constraint dict_item_name_type_id_uindex
unique (name, type_id),
constraint dict_item_value_type_id_uindex
unique (value, type_id)
)
comment '数据字典条目';
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
客户端:


服务端:
POJO—Query:
@Data
public class PageQuery {
private static final int MIN_SIZE = 1;
private static final int DEFAULT_SIZE = 10;
private long size; // 一页展示多少条数据
private long page; // 第几页
/**
* 查询出来的数据集
* 由于将来查出来的类型不确定(VO、BO、PO),因此泛型使用类型通配符
*/
private List<?> data;
private long total; // 总条数
private long pages; // 总页数
public long getSize() {
return size < MIN_SIZE ? DEFAULT_SIZE : size;
}
public long getPage() {
return page < MIN_SIZE ? MIN_SIZE : page;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
@EqualsAndHashCode(callSuper = true)
@Data
public class KeywordQuery extends PageQuery {
private String keyword;
}
- 1
- 2
- 3
- 4
- 5
@EqualsAndHashCode(callSuper = true)
@Data
public class DictTypeQuery extends KeywordQuery {
}
- 1
- 2
- 3
- 4
ServiceImpl:
@Service
@Transactional
public class DictTypeServiceImpl extends ServiceImpl<DictTypeMapper, DictType> implements DictTypeService {
@Autowired
private DictTypeMapper mapper;
@Override
@Transactional(readOnly = true)
public void list(DictTypeQuery query) {
final String keyword = query.getKeyword();
LambdaQueryWrapper<DictType> wrapper = new LambdaQueryWrapper<>();
// 按照关键字查询
if (!StringUtils.isEmpty(keyword)) {
wrapper.like(DictType::getName, keyword).or()
.like(DictType::getIntro, keyword).or()
.like(DictType::getValue, keyword);
}
// 按照id降序排序
wrapper.orderByDesc(DictType::getId);
// 分页查询
Page<DictType> page = new Page<>(query.getNo(), query.getSize());
mapper.selectPage(page, wrapper);
// 更新query对象
query.setData(page.getRecords());
query.setPages(page.getPages());
query.setTotal(page.getTotal());
// 如果客户端的查询条件有问题,MyBatisPlus会自动识别并修正,因此可以修改一下query中的查询条件
query.setSize(getSize());
query.setPage(getCurrent());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
Controller:
@RestController
@RequestMapping("/dictTypes")
public class DictTypeController {
@Autowired
private DictTypeService service;
@GetMapping
public Map<String, Object> list(DictTypeQuery query) {
service.list(query);
final Map<String, Object> map = new HashMap<>();
map.put("msg", "");
map.put("data", query.getData());
map.put("count", query.getTotal());
map.put("code", 0);
return map;
}
@PostMapping("/remove")
public Map<String, Object> remove(String id) {
// id = "10"
// id = "1, 20, 23"
final String[] ids = id.split(",");
final Map<String, Object> map = new HashMap<>();
if (service.removeByIds(Arrays.asList(ids))) {
map.put("msg", "删除成功");
map.put("code", 0);
} else {
map.put("msg", "删除失败");
map.put("code", 1);
}
return map;
}
@PostMapping("/save")
public Map<String, Object> save(DictType dictType) {
final Map<String, Object> map = new HashMap<>();
if (service.saveOrUpdate(dictType)) {
map.put("msg", "保存成功");
map.put("code", 0);
} else {
map.put("msg", "保存失败");
map.put("code", 1);
}
return map;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
封装MyBatis-Plus方便查询
以查询DictType(数据字典类型)为例
enhance—MPPage、MPQueryWrapper:
public class MPPage<T> extends Page<T> {
private final PageQuery query;
public MPPage(PageQuery query) {
super(query.getPage(), query.getSize());
this.query = query;
}
public void updateQuery() {
query.setData(getRecords());
query.setPages(getPages());
query.setTotal(getTotal());
// 如果客户端的查询条件有问题,MyBatis会自动识别并修正,因此可以修改一下query中的查询数据
query.setSize(getSize());
query.setPage(getCurrent());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
public class MPQueryWrapper<T> extends LambdaQueryWrapper<T> {
@SafeVarargs
public final MPQueryWrapper<T> like(Object val, SFunction<T, ?>... funcs) {
if (val == null || funcs == null || funcs.length == 0) return this;
final String str = val.toString();
if (str.length() == 0) return this;
return (MPQueryWrapper<T>) nested((wrapper) -> {
for (SFunction<T, ?> func : funcs) {
wrapper.like(func, str).or();
}
});
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
动态代理更新Query对象:
@Configuration
@EnableAspectJAutoProxy
public class SpringConfig {
}
- 1
- 2
- 3
- 4
@Aspect
@Component
public class PageMapperInterceptor {
@Around("execution(public com.baomidou.mybatisplus.core.metadata.IPage com.baomidou.mybatisplus.core.mapper.BaseMapper.selectPage(com.baomidou.mybatisplus.core.metadata.IPage, com.baomidou.mybatisplus.core.conditions.Wrapper))")
public Object updateQuery(ProceedingJoinPoint point) throws Throwable {
Object result = point.proceed();
final Object[] args = point.getArgs();
if (args != null && args.length > 0) {
Object arg = args[0];
if (arg instanceof MPPage) {
((MPPage<?>) arg).updateQuery();
}
}
return result;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
使用—ServiceImpl:
@Transactional
@Service
public class DictTypeServiceImpl extends ServiceImpl<DictTypeMapper, DictType> implements DictTypeService {
@Override
@Transactional(readOnly = true)
public void list(DictTypeQuery query) {
MPQueryWrapper<DictType> wrapper = new MPQueryWrapper<>();
wrapper.like(query.getKeyword(), DictType::getName, DictType::getValue, DictType::getIntro);
wrapper.orderByDesc(DictType::getId);
baseMapper.selectPage(new MPPage<>(query), wrapper);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
Controller:
@RestController
@RequestMapping("/dictTypes")
public class DictTypeController {
@Autowired
private DictTypeService service;
@GetMapping
public Map<String, Object> list(DictTypeQuery query) {
service.list(query);
final Map<String, Object> map = new HashMap<>();
map.put("msg", "");
map.put("data", query.getData());
map.put("count", query.getTotal());
map.put("code", 0);
return map;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
封装给客户端的返回值
public class R extends HashMap<String, Object> {
public static final int CODE_SUCCESS = 0;
private static final String K_CODE = "code";
private static final String K_MSG = "msg";
private static final String K_DATA = "data";
public R setCode(int code) {
return add(K_CODE, code);
}
public R setMsg(String msg) {
return add(K_MSG, msg);
}
public R setData(Object data) {
return add(K_DATA, data);
}
public R add(String key, Object data) {
put(key, data);
return this;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
public final class Rs {
private Rs() {
}
// 事先和前端约定好:成功code:0,失败code:1
public static final int CODE_SUCCESS = 0;
public static final int CODE_ERROR_DEFAULT = 1;
private static R success() {
return new R().setCode(CODE_SUCCESS);
}
public static R success(PageQuery query) {
return success().setData(query.getData());
}
public static R success(String msg) {
return success().setMsg(msg);
}
public static R success(PageQuery query, String msg) {
return success().setData(query.getData()).setMsg(msg);
}
public static R error() {
return new R().setCode(CODE_ERROR_DEFAULT);
}
public static R error(String msg) {
return error().setMsg(msg);
}
public static R error(int code, String msg) {
return new R().setCode(code).setMsg(msg);
}
public static R r(boolean success) {
return new R().setCode(success ? CODE_SUCCESS : CODE_ERROR_DEFAULT);
}
public static R r(boolean success, String msg) {
return r(success).setMsg(msg);
}
public static R r(boolean success, Object data) {
return r(success).setData(data);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
Controller使用:
@RestController
@RequestMapping("/dictTypes")
public class DictTypeController {
@Autowired
private DictTypeService service;
@GetMapping
public R list(DictTypeQuery query) {
service.list(query);
return Rs.success(query).add("count", query.getTotal());
}
@PostMapping("/remove")
public R remove(String id) {
// id = "10"
// id = "1, 20, 23"
final String[] ids = id.split(",");
// if (service.removeByIds(Arrays.asList(ids))) {
// return Rs.success("删除成功");
// } else {
// return Rs.error("删除失败");
// }
final boolean success = service.removeByIds(Arrays.asList(ids));
final String msg = success ? "删除成功" : "删除失败";
return Rs.r(success, msg);
}
@PostMapping("/save")
public R save(DictType dictType) {
if (!service.saveOrUpdate(dictType)) {
throw new RuntimeException("保存失败");
}
return Rs.success("保存成功");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
统一异常处理+HTTP响应状态码
-
如果服务器端操作失败的话,比如删除失败、保存失败,那么给客户端返回的StatusCode就不应该是200,应该是400/500,原因如下:
-
客户端(前端)极大可能是根据HTTP请求的响应状态码来判断某个请求是否成功的,而不是通过服务器返回的JSON数据的某个属性值来判断
-
比如AJAX的回调方法默认就是通过HTTP的响应状态码来判断是否请求成功的。
-
因此如果服务器处理数据失败,应该修改响应状态码200(OK)为其它StatusCode,比如400、500。
public interface JSONable {
default String jsonString() throws Exception {
return JSONs.getMAPPER().writeValueAsString(this);
}
}
- 1
- 2
- 3
- 4
- 5
public class R extends HashMap<String, Object> implements JSONable {
// ...
}
- 1
- 2
- 3
@ControllerAdvice
public class ExceptionInterceptor {
// 默认处理所有的异常
@ExceptionHandler(Throwable.class)
public void exceptionHandlerOther(Throwable throwable, HttpServletResponse response) throws Exception {
response.setCharacterEncoding("UTF-8");
response.setStatus(400);
// response.getWriter().write(Rs.error(getRealCause(throwable).getMessage()).jsonString());
response.getWriter().write(Rs.error(throwable.getMessage()).jsonString());
}
private Throwable getRealCause(Throwable throwable) {
Throwable cause = throwable.getCause();
while (cause != null) {
throwable = cause;
cause = cause.getCause();
}
return throwable;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
@RestController
@RequestMapping("/dictTypes")
public class DictTypeController {
@Autowired
private DictTypeService service;
@PostMapping("/remove")
public R remove(String id) {
final String[] ids = id.split(",");
if (!service.removeByIds(Arrays.asList(ids))) {
throw new RuntimeException("删除失败");
}
return Rs.success("删除成功");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
-
还可以继续封装:
public enum CodeMsg {
BAD_REQUEST(400, "请求出错"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "资源不存在"),
INTERNAL_SERVER_ERROR(500, "服务器内部错误"),
OPERATE_OK(R.CODE_SUCCESS, "操作成功"),
SAVE_OK(R.CODE_SUCCESS, "保存成功"),
REMOVE_OK(R.CODE_SUCCESS, "删除成功"),
OPERATE_ERROR(40001, "操作失败"),
SAVE_ERROR(40002, "保存失败"),
REMOVE_ERROR(40003, "删除失败"),
UPLOAD_IMG_ERROR(40004, "图片上传失败"),
WRONG_USERNAME(50001, "用户名不存在"),
WRONG_PASSWORD(50002, "密码错误"),
USER_LOCKED(50003, "用户被锁定,无法正常登录"),
WRONG_CAPTCHA(50004, "验证码错误"),
NO_TOKEN(60001, "没有Token,请登录"),
TOKEN_EXPIRED(60002, "Token过期,请重新登录"),
NO_PERMISSION(60003, "没有相关的操作权限");
private final int code;
private final String msg;
CodeMsg(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
@EqualsAndHashCode(callSuper = true)
@Data
public class CommonException extends RuntimeException {
private int code;
public CommonException() {
this(CodeMsg.BAD_REQUEST.getCode(), null);
}
public CommonException(String msg) {
this(msg, null);
}
public CommonException(int code, String msg) {
this(code, msg, null);
}
public CommonException(String msg, Throwable cause) {
this(CodeMsg.BAD_REQUEST.getCode(), msg, cause);
}
public CommonException(int code, String msg, Throwable cause) {
super(msg, cause);
this.code = code;
}
public CommonException(CodeMsg codeMsg) {
this(codeMsg, null);
}
public CommonException(CodeMsg codeMsg, Throwable cause) {
this(codeMsg.getCode(), codeMsg.getMsg(), cause);
}
public int getCode() {
return code;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
public final class Rs {
private Rs() {
}
public static final String K_COUNT = "count";
private static final int CODE_SUCCESS = 0;
private static final int CODE_ERROR_DEFAULT = CodeMsg.BAD_REQUEST.getCode();
private static R success() {
return new R().setCode(CODE_SUCCESS);
}
public static R success(PageQuery query) {
return success().setData(query.getData());
}
public static R success(String msg) {
return success().setMsg(msg);
}
public static R success(Object data) {
return success().setData(data);
}
public static R success(PageQuery query, String msg) {
return success().setData(query.getData()).setMsg(msg);
}
public static R success(CodeMsg codeMsg) {
return success().setMsg(codeMsg.getMsg());
}
public static R error() {
return error(CODE_ERROR_DEFAULT);
}
public static R error(int code) {
return new R().setCode(code);
}
public static R error(String msg) {
return error().setMsg(msg);
}
public static R error(int code, String msg) {
return error(code).setMsg(msg);
}
public static R error(Throwable e) {
// R r = error(e.getMessage()); // 开发阶段
R r = error(); // 项目上线 项目上线了就不要把其它异常信息给用户看了
if (e instanceof CommonException) {
r.setCode(((CommonException) e).getCode());
}
return r;
}
public static R r(boolean success) {
return new R().setCode(success ? CODE_SUCCESS : CODE_ERROR_DEFAULT);
}
public static R r(boolean success, String msg) {
return r(success).setMsg(msg);
}
public static R r(boolean success, Object data) {
return r(success).setData(data);
}
public static R exception(String msg) {
throw new CommonException(msg);
}
public static R exception(CodeMsg codeMsg) {
throw new CommonException(codeMsg);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
@ControllerAdvice
public class ExceptionInterceptor {
@ExceptionHandler(Throwable.class)
public void handle(Throwable throwable,
HttpServletResponse response) throws Exception {
// response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE/*"application/json; charset=UTF-8"*/);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
response.setStatus(400);
// response.getWriter().write(Rs.error(getRealCause(throwable)).json());
response.getWriter().write(Rs.error(throwable).json());
}
private Throwable getRealCause(Throwable throwable) {
Throwable cause = throwable.getCause();
while (cause != null) {
throwable = cause;
cause = cause.getCause();
}
return throwable;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
-
其实异常处理器还有更简便的写法:

@RestControllerAdvice
@Slf4j
public class CommonExceptionHandler {
@ExceptionHandler(Throwable.class)
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public JSONResult handle(Throwable throwable) {
// System.out.println(throwable);
log.error("error", throwable);
return JSONResults.exception(throwable);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
-
Controller使用:
@RestController
@RequestMapping("/dictTypes")
public class DictTypeController {
@Autowired
private DictTypeService service;
@GetMapping
public R list(DictTypeQuery query) {
service.list(query);
return Rs.success(query).add(Rs.K_COUNT, query.getTotal());
}
@PostMapping("/remove")
public R remove(String id) {
// id = "10"
// id = "1, 20, 23"
final String[] ids = id.split(",");
if (!service.removeByIds(Arrays.asList(ids))) {
throw new CommonException(CodeMsg.REMOVE_ERROR);
}
return Rs.success(CodeMsg.REMOVE_OK);
}
@PostMapping("/save")
public R save(DictType dictType) {
if (!service.saveOrUpdate(dictType)) {
throw new CommonException(CodeMsg.SAVE_ERROR);
}
return Rs.success(CodeMsg.SAVE_OK.getMsg());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
-
还可以将Controller中的公共代码抽取出来:
public abstract class BaseController<T> {
protected abstract IService<T> service();
@GetMapping("/list")
public R list() {
return Rs.success(service().list());
}
@PostMapping("/remove")
public R remove(String id) {
final String[] ids = id.split(",");
if (!service().removeByIds(Arrays.asList(ids))) {
Rs.exception(CodeMsg.REMOVE_ERROR);
}
// return Rs.success(CodeMsg.REMOVE_OK.getMsg());
return Rs.success(CodeMsg.REMOVE_OK);
}
@PostMapping("/save")
public R save(T entity) {
if (!service().saveOrUpdate(entity)) {
// throw new CommonException(CodeMsg.SAVE_ERROR);
Rs.exception(CodeMsg.SAVE_ERROR);
}
return Rs.success(CodeMsg.SAVE_OK);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
@RestController
@RequestMapping("/dictTypes")
public class DictTypeController extends BaseController<DictType> {
@Autowired
private DictTypeService service;
@GetMapping
public R list(DictTypeQuery query) {
service.list(query);
return Rs.success(query).add(Rs.K_COUNT, query.getTotal());
}
@Override
protected IService<DictType> service() {
return service;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
统一异常处理—配合Shiro
@RestControllerAdvice
只能拦截到Controller抛出的异常



public class ErrorFilter implements Filter {
public static final String ERROR_URI = "/handleError";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(request, response);
} catch (Exception e) {
request.setAttribute(ERROR_URI, e);
request.getRequestDispatcher(ERROR_URI).forward(request, response);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
@Configuration
public class SpringMVCConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean() {
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
// 设置Filter
bean.setFilter(new ErrorFilter());
bean.addUrlPatterns("/*");
// 最高权限
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
@RestController
public class ErrorController {
@RequestMapping(ErrorFilter.ERROR_URI)
public void handle(HttpServletRequest request) throws Exception {
// 抛出异常
throw (Exception) request.getAttribute(ErrorFilter.ERROR_URI);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
@RestControllerAdvice
@Slf4j
public class CommonExceptionHandler {
@ExceptionHandler(Throwable.class)
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public JSONResult handle(Throwable t) {
log.error("handle", t);
// 一些可以直接处理的异常
if (t instanceof CommonException) {
return handle((CommonException) t);
} else if (t instanceof BindException) {
return handle((BindException) t);
} else if (t instanceof ConstraintViolationException) {
return handle((ConstraintViolationException) t);
} else if (t instanceof AuthorizationException) {
return JSONResults.error(CodeMsg.NO_PERMISSION);
}
// 处理cause异常(导致产生t的异常)
Throwable cause = t.getCause();
if (cause != null) {
return handle(cause);
}
// 其他异常(没有cause的异常)
return JSONResults.error();
}
private JSONResult handle(CommonException ce) {
return JSONResults.error(ce.getCode(), ce.getMessage());
}
private JSONResult handle(BindException be) {
List<ObjectError> errors = be.getBindingResult().getAllErrors();
// 函数式编程的方式:stream
List<String> defaultMsgs = Streams.map(errors, ObjectError::getDefaultMessage);
String msg = StringUtils.collectionToDelimitedString(defaultMsgs, ", ");
return JSONResults.error(msg);
}
private JSONResult handle(ConstraintViolationException cve) {
List<String> msgs = Streams.map(cve.getConstraintViolations(), ConstraintViolation::getMessage);
String msg = StringUtils.collectionToDelimitedString(msgs, ", ");
return JSONResults.error(msg);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
@Configuration
public class ShiroConfig {
/**
* ShiroFilterFactoryBean用来告诉Shiro如何进行拦截
* 1.拦截哪些URL
* 2.每个URL需要经过哪些filter
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(Realm realm/*, JkProperties properties*/) {
ShiroFilterFactoryBean filterBean = new ShiroFilterFactoryBean();
// 安全管理器
filterBean.setSecurityManager(new DefaultWebSecurityManager(realm));
// 添加一些自定义Filter
Map<String, Filter> filters = new HashMap<>();
filters.put("token", new TokenFilter());
filterBean.setFilters(filters);
// 设置URL如何拦截
// Filter的顺序很重要,因此需要使用LinkedHashMap
Map<String, String> urlMap = new LinkedHashMap<>();
// ...
// 放行全局Filter的异常处理
urlMap.put(ErrorFilter.ERROR_URI, "anon");
// 其他
urlMap.put("/**", "token");
filterBean.setFilterChainDefinitionMap(urlMap);
return filterBean;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
数据的一致性
外键有优点,同样,也有它的缺点
- 如果在项目中使用了外键,那么表与表之间的数据一致性其实是不用我们操心的,因为有外键自动帮我们约束。
- 因此对于那些小型的、对于数据一致性要求很高的项目,需要使用外键。
- 但是大型的、分布式的互联网项目出于对数据库的性能、备份、迁移、维护等原因的考虑,一般而言在设计数据库时是不使用外键的,那么这时表与表之间的联系,也就是数据一致性问题怎么解决呢?
- 答案就是需要我们自己在应用层做好相应的处理,但做好这个处理并不简单。
假设现在要对某张表的进行数据一致性的处理,有许多非常麻烦的点
- 虽然数据库没有使用外键,但对业务来讲,表与表之间应该是有联系的
- 那么对这张表进行删除、更新、添加等操作都需要考虑数据一致性,具体到代码,就是remove、save、update这种方法有很多,需要都考虑到
- 有可能会有很多表在数据上都关联这张表,因此我们需要清楚每一张表与每一张表之间的关联关系,知道了表与表之间的关联关系后,才能去逐个处理
解决方案:自己写一个保证数据一致性的小框架,这个小框架的特点:
- 注解驱动
- AOP
- 反射
- …
MJ老师编写框架经验:
- 如果你自己想写一个比较好用的框架
- 首先应该从应用的角度出发,先从使用者(自己、其他开发者)的角度出发
- 考虑别人应该怎么用这个框架、这个框架能够怎样简化开发、这个框架怎么样能够使开发变得更爽、更高效、更敏捷
- 然后再考虑减少BUG
- 然后再考虑安全问题
- 然后再考虑性能问题
- 然后再考虑解耦、抽取、可扩展…
拼音库—tinypinyin的使用
<dependency>
<groupId>com.github.promeggroupId>
<artifactId>tinypinyinartifactId>
<version>2.0.3version>
dependency>
- 1
- 2
- 3
- 4
- 5
- 6
// IService.saveOrUpdate方法里面会调用updateById或者save方法,因此需要重写这两个方法对拼音进行处理
@Override
public boolean updateById(PlateRegion entity) {
processPinyin(entity);
return super.updateById(entity);
}
@Override
public boolean save(PlateRegion entity) {
processPinyin(entity);
return super.save(entity);
}
private void processPinyin(PlateRegion entity) {
final String name = entity.getName();
if (StringUtils.isEmpty(name)) return;
entity.setPinyin(Pinyin.toPinyin(name, "_"));
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
MapStruct
作用:对象转换
- po ——> vo
- vo ——> po
<dependency>
<groupId>org.mapstructgroupId>
<artifactId>mapstructartifactId>
<version>${map.struct.version}version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.mapstructgroupId>
<artifactId>mapstruct-processorartifactId>
<version>${map.struct.version}version>
<scope>providedscope>
dependency>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
@Mapper
public interface MapStruct {
MapStruct INSTANCE = Mappers.getMapper(MapStruct.class);
DictItem vo2po(ReqSaveDictItem reqSaveVo);
DictType vo2po(ReqSaveDictType reqSaveVo);
ExamPlace vo2po(ReqSaveExamPlace reqSaveVo);
ExamPlaceCourse vo2po(ReqSaveExamPlaceCourse reqSaveVo);
PlateRegion vo2po(ReqSavePlateRegion reqSaveVo);
RespDictItem po2vo(DictItem po);
RespDictType po2vo(DictType po);
RespExamPlace po2vo(ExamPlace po);
RespExamPlaceCourse po2vo(ExamPlaceCourse po);
RespPlateRegion po2vo(PlateRegion po);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
基本使用:
final DictItem dictItem = MapStruct.INSTANCE.vo2po(new ReqSaveDictItem());
final RespDictItem respDictItem = MapStruct.INSTANCE.po2vo(new DictItem());
- 1
- 2
项目中使用:
public abstract class BaseController<T, ReqSave> {
protected abstract Function<ReqSave, T> function();
// ...
@PostMapping("/save")
public JSONResult save(@Valid ReqSave entity) {
service.saveOrUpdate(function().apply(entity));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
public class DictItemController extends BaseController<DictItem, ReqSaveDictItem> {
// ...
@Override
protected Function<ReqSaveDictItem, DictItem> function() {
return MapStruct.INSTANCE::vo2po;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
public class PlateRegionServiceImpl extends ServiceImpl<PlateRegionMapper, PlateRegion> implements PlateRegionService {
public JSONDataResult<List<RespPlateRegion>> listProvinces() {
// ...
final List<RespPlateRegion> data = baseMapper.selectList(wrapper)
.stream().map(MapStruct.INSTANCE::po2vo)
.collect(Collectors.toList());
return JSONResults.success(data);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
自定义转换规则:
public class MapStructFormatter {
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Date2Millis {}
@Date2Millis
public static Long date2millis(Date date) {
if (date == null) return null;
return date.getTime();
}
/*
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Mills2Date {}
@Mills2Date
public static Date millis2date(Long mills) {
if (mills == null) return null;
return new Date(mills);
}
*/
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
@Data
public class SysUser {
// ...
//最后一次登录的时间
private Date loginTime;
}
- 1
- 2
- 3
- 4
- 5
- 6
@Data
@ApiModel("系统用户")
public class RespSysUser {
// ...
@ApiModelProperty("最后一次登录的时间")
// 前后端分离一般返回UNIX时间戳
// UNIX时间戳:从 1970-1-1 0:0:0 开始到现在走过的毫秒数
private Long loginTime;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
/**
* ReqVo -> Po
* Po -> Vo
*/
@Mapper(uses = {
MapStructFormatter.class
})
public interface MapStructs {
// ...
@Mapping(source = "loginTime",
target = "loginTime",
qualifiedBy = MapStructFormatter.Date2Millis.class)
RespSysUser po2vo(SysUser po);
// ...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
登录—简单登录
<dependency>
<groupId>com.github.whvcsegroupId>
<artifactId>easy-captchaartifactId>
<version>1.6.2version>
dependency>
- 1
- 2
- 3
- 4
- 5
- 6

@RestController
@RequestMapping("/sysUsers")
@Api(tags = "系统用户", description = "SysUser")
public class SysUserController extends BaseController<SysUser, ReqSaveSysUser> {
@Autowired
private SysUserService service;
@GetMapping("/captcha")
@ApiOperation("生成验证码")
public void captcha(HttpServletRequest request,
HttpServletResponse response) throws Exception {
CaptchaUtil.out(request, response);
}
@PostMapping("/login")
@ApiOperation("登录")
public JSONDataResult<RespLogin> login(ReqLogin reqVo, HttpServletRequest request) {
if (CaptchaUtil.ver(reqVo.getCaptcha(), request)) {
return JSONResults.success(service.login(reqVo));
}
JSONResults.exception(CodeMsg.WRONG_CAPTCHA);
return null;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
@Override
public RespLogin login(ReqLogin reqVo) {
// 根据用户名查询用户
MPLambdaQueryWrapper<SysUser> wrapper = new MPLambdaQueryWrapper<>();
wrapper.eq(SysUser::getUsername, reqVo.getUsername());
SysUser po = baseMapper.selectOne(wrapper);
// 用户名不存在
if (po == null) {
return JsonVos.raise(CodeMsg.WRONG_USERNAME);
}
// 密码不正确
if (!po.getPassword().equals(reqVo.getPassword())) {
return JsonVos.raise(CodeMsg.WRONG_PASSWORD);
}
// 账号锁定
if (po.getStatus() == Constants.SysUserStatus.LOCKED) {
return JsonVos.raise(CodeMsg.USER_LOCKED);
}
// 登录成功
// 更新登录时间
po.setLoginTime(new Date());
baseMapper.updateById(po);
// 返回给客户端的具体数据
RespLogin vo = MapStruct.INSTANCE.po2loginVo(po);
return vo;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
前端Ajax登录:
Ajaxs.loadPost({
uri: 'sysUsers/login',
data: data.field,
success: (response) => {
location.href = '../index.html'
},
xhrFields: { // 需要跨域带上cookie
withCredentials: true
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
登录—Token
后端
-
登录
@Data
@ApiModel("登录成功的结果")
public class RespLogin {
// ...
@ApiModelProperty("登录令牌")
private String token;
}
@PostMapping("/login")
@ApiOperation("登录")
public JSONDataResult<RespLogin> login(ReqLogin reqVo, HttpServletRequest request) {
if (CaptchaUtil.ver(reqVo.getCaptcha(), request)) {
return JSONResults.success(service.login(reqVo));
}
JSONResults.exception(CodeMsg.WRONG_CAPTCHA);
return null;
}
@Override
public RespLogin login(ReqLogin reqVo) {
// 根据用户名查询用户
MPLambdaQueryWrapper<SysUser> wrapper = new MPLambdaQueryWrapper<>();
wrapper.eq(SysUser::getUsername, reqVo.getUsername());
SysUser po = baseMapper.selectOne(wrapper);
// 用户名不存在
if (po == null) {
return JsonVos.raise(CodeMsg.WRONG_USERNAME);
}
// 密码不正确
if (!po.getPassword().equals(reqVo.getPassword())) {
return JsonVos.raise(CodeMsg.WRONG_PASSWORD);
}
// 账号锁定
if (po.getStatus() == Constants.SysUserStatus.LOCKED) {
return JsonVos.raise(CodeMsg.USER_LOCKED);
}
/**** 登录成功 ****/
// 更新登录时间
po.setLoginTime(new Date());
baseMapper.updateById(po);
// 生成Token,发送Token给用户
String token = UUID.randomUUID().toString();
// 存储token到缓存中
Caches.putToken(token, po);
// 返回给客户端的具体数据
RespLogin vo = MapStruct.INSTANCE.po2loginVo(po);
vo.setToken(token);
return vo;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
-
退出登录
@PostMapping("/logout")
@ApiOperation("退出登录")
public JSONResult logout(@RequestHeader("Token") String token) {
Caches.removeToken(token);
return JSONResults.success(CodeMsg.LOGOUT_OK);
}
- 1
- 2
- 3
- 4
- 5
- 6
前端
-
登录:
class DataKey {
static USER = 'user'
static TOKEN = 'token'
static TOKEN_HEADER = 'Token'
}
- 1
- 2
- 3
- 4
- 5
Ajaxs.loadPost({
uri: 'sysUsers/login',
data: data.field,
success: (response) => {
Datas.save(DataKey.USER, response.data)
location.href = '../index.html'
},
xhrFields: { // 需要跨域带上cookie
withCredentials: true
}
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
-
需要确保登录后的每次请求都带上Token信息
static get() { // Datas.get
let ret = layui.data(this.TABLE)
for (let i = 0; i < arguments.length; i++) {
if (!ret) return null
ret = ret[arguments[i]]
}
return ret
}
static _addTokenHeader(cfg) {
// 取出token
const token = Datas.get(DataKey.USER, DataKey.TOKEN)
if (token) {
if (!cfg.headers) {
cfg.headers = {}
}
// 将token放到请求头
cfg.headers[DataKey.TOKEN_HEADER] = token
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
// 自己封装的Ajax请求,每一次请求都需要带上Token信息
static ajax(cfg) {
cfg.url = Commons.url(cfg.uri)
Commons._addTokenHeader(cfg)
$.ajax(cfg)
}
- 1
- 2
- 3
- 4
- 5
- 6
// Layui发送请求时也需要带上Token信息
_init() {
const cfg = this._commonCfg()
cfg.url = Commons.url(this._cfg.uri)
$.extend(cfg, this._cfg)
cfg.elem = cfg.selector
Commons._addTokenHeader(cfg)
this._innerTable = this._layuiTable().render(cfg)
this._cfg = cfg
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
-
登出:
$('.login-out').click(() => {
// 发送请求给服务器:退出登录
Ajaxs.loadPost({
uri: 'sysUsers/logout',
success: () => {
// 清除客户端缓存
Datas.remove(DataKey.USER)
// 提示
Layers.msgSuccess('退出登录成功', () => {
location.href = 'page/login.html'
})
}
})
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
权限管理—RBAC
可以登录后台管理系统的员工/系统管理员:比如:sys_user
(表名以sys_
开头)
使用产品的用户/客户(APP、小程序、网页):比如:user


表结构设计:
可以登录后台管理系统的员工/系统管理员:比如:sys_user(表名以sys_开头)
使用产品的用户/客户(APP、小程序、网页):比如:user
create table if not exists jk.sys_resource
(
id tinyint unsigned auto_increment comment '主键'
primary key,
name varchar(15) default '' not null comment '名称',
uri varchar(100) default '' not null comment '链接地址',
permission varchar(100) default '' not null comment '权限标识',
type tinyint unsigned default 0 not null comment '资源类型(0是目录,1是菜单,2是按钮)PS:按钮就是增删改查之类的能点击的',
icon varchar(100) default '' not null comment '图标',
sn tinyint unsigned default 0 not null comment '序号',
parent_id tinyint unsigned default 0 not null comment '父资源id',
constraint sys_resource_parent_id_name_uindex
unique (parent_id, name)
)
comment '资源';
create table if not exists jk.sys_role
(
id tinyint unsigned auto_increment comment '主键'
primary key,
name varchar(15) default '' not null comment '角色名称',
constraint sys_role_name_uindex
unique (name)
)
comment '角色';
create table if not exists jk.sys_role_resource
(
role_id tinyint unsigned default 0 not null comment '角色id',
resource_id tinyint unsigned default 0 not null comment '资源id',
primary key (resource_id, role_id)
)
comment '角色-资源';
create table if not exists jk.sys_user
(
id smallint unsigned auto_increment comment '主键'
primary key,
nickname varchar(15) default '' not null comment '昵称',
username varchar(15) default '' not null comment '登录用的用户名',
password char(32) default '' not null comment '登录用的密码,密码经过MD5加密之后就是32位的字符串',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建的时间',
login_time datetime null comment '最后一次登录的时间',
status tinyint unsigned default 0 not null comment '账号的状态,0是正常,1是锁定',
constraint sys_user_username_uindex
unique (username)
)
comment '用户(可以登录后台系统的)';
create table if not exists jk.sys_user_role
(
role_id tinyint unsigned default 0 not null comment '角色id',
user_id smallint unsigned default 0 not null comment '用户id',
primary key (user_id, role_id)
)
comment '用户-角色';
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
逻辑删除
-
物理删除:真正从数据库中删除了,永久消失。
-
逻辑删除(假删除、软删除):数据还在数据库中,只是对用户来说,数据被删掉了。
-
逻辑删除的实现:在需要实现逻辑删除的表中增加一个字段来标识数据是否被删除。

逻辑删除—MyBatisPlus

# db: test
create table user
(
id int unsigned auto_increment
primary key,
name varchar(15) default '' not null,
deleted tinyint unsigned default 0 not null comment '1是被删除,0是未删除',
constraint user_name_uindex
unique (name)
);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
企业级文件上传
-
方案一:文件数据和表单数据一起提交(之前Java②学过的那种)
-
方案二:专门弄一个文件服务器,用来操作文件(文件上传、下载、删除等)。文件数据先单独提交,从文件服务器返回一个uri,拿着这个uri和表单数据一起提交【前端需要进行用户的行为控制,比较复杂一点】
@RequestBody修饰的请求参数
-
前端/客户端将Content-Type改为:application/json
-
请求体传递符合要求的JSON字符串【JSON优势:灵活、前端处理方便、第三方库也多】
@PostMapping("/save1")
@ApiOperation("添加或更新")
public JsonVo save1(User user) { // 相当于是加了@RequestParam
JsonVo jsonVo = new JsonVo();
jsonVo.setMsg(service.saveOrUpdate(user) ? "保存成功" : "保存失败");
return jsonVo;
}
@PostMapping("/save2")
@ApiOperation("添加或更新")
public JsonVo save2(@RequestBody User user) { // 要求前端客户端传一个User类型的JSON字符串过来,请求体是一个JSON
JsonVo jsonVo = new JsonVo();
jsonVo.setMsg(service.saveOrUpdate(user) ? "保存成功" : "保存失败");
return jsonVo;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
单元测试
Spring单元测试


SpringBoot单元测试

<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
打包部署

打包部署—jar

打包部署—war


注意
配置JackSON将Model转为JSON时,不包含值为null的属性:
-
application.yml:
spring:
jackson:
default-property-inclusion: non_null
- 1
- 2
- 3
-
Java代码:
public final class JSONs {
private JSONs() {
}
private static final ObjectMapper MAPPER = new ObjectMapper();
static {
MAPPER.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
}
public static ObjectMapper getMAPPER() {
return MAPPER;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
-
MySQL配置:
#url=jdbc:mysql://127.0.0.1:3306/lp_resume
#url=jdbc:mysql://localhost:3306/lp_resume
#url=jdbc:mysql:///lp_resume
#UTC:世界同一时间
#url=jdbc:mysql:///lp_resume?serverTimezone=UTC&useSSL=false
#中国时间:serverTimezone=Asia/Shanghai == serverTimezone=GMT+8
#url=jdbc:mysql:///lp_resume?serverTimezone=GMT+8&useSSL=false
url=jdbc:mysql:///lp_resume?serverTimezone=Asia/Shanghai&useSSL=false
############使用IDEA连接数据库############
#使用IDEA连接MySQL数据库时,由于“+”是一个特殊字符,因此需要编码处理为:“%2B”
#例如:jdbc:mysql:///?serverTimezone=GMT%2B8&useSSL=false
#或者:jdbc:mysql:///?serverTimezone=Asia/Shanghai&useSSL=false
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
-
HTML的button标签,默认类型是
,因此button如果是其它类型的话,最好显示声明button的type,比如:
-
客户端向服务器发送请求参数时
- 如果
http://localhost:8080/jk/dictTypes/list?page=1&size=20
,那么服务器获取到的keyword
就是null
- 如果
http://localhost:8080/jk/dictTypes/list?page=1&size=20&keyword=
,那么服务器获取到的keyword
就是""(空字符串)
-
数据库中,表名和字段名建议使用``、字符串建议使用’'(单引号)
-
MySQL数据库,行(记录)从0开始,列(字段)从1开始
-
标准JSON格式:key使用""(双引号):
[
{
"age": 10,
"name": "lp"
},
{
"age": 20,
"name": "ruoyu"
}
]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
{
"string": "value",
"integer": 10,
"bool": true,
"null": null,
"array": [],
"obj": {}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
补充:ChromeJSON插件
https://chrome.google.com/webstore/detail/json-handle/iahnhfdhidomcpggpaimmmahffihkfnj

参考
小码哥-李明杰: Java从0到架构师③进阶互联网架构师.
本文完,感谢您的关注支持!
-
相关阅读:
基于docker 配置hadoop-hive-spark-zeppelin环境进行大数据项目的开发
Timber 架包的使用
Springboot——如何保证服务启动后不自动停止?
.Net Core(.Net6)创建grpc
永磁无刷直流电机(无框力矩电机)电流和速度控制器的设计
温度及pH敏感性聚乙烯醇/羧甲基壳聚糖水凝胶/金银花多糖/薄荷多糖/O-羧甲基壳聚糖水凝胶
Python-表白小程序练习
Ubuntu server 24 (Linux) sudo 免输密码
Netty入门——概述
SYNOPSYS VCS Makefile学习
-
原文地址:https://blog.csdn.net/weixin_44018671/article/details/124588856