一、项目技术选型及开发工具
前后端分离的项目(前端项目+ 后端项目)
前端:Html、CSS、JavaScript、Vue3、Axios、Router、Element Plus
后端:Spring Boot、Spring Security、MyBatis、MyBatis-plus、MySQL、Redis、MinIo、Nginx
相关组件:HiKariCP(Spring Boot默认数据库连接池)、Spring-Data-Redis(Spring整合Redis)、Lombok(代码生成工具)、jwt(Json web token)、EasyExcel(Excel处理类库)、ECharts(前端图表库)
服务器:MySQL、Redis、Linux
项目依赖管理:Maven
项目开发工具:IDEA、Apifox
数据库sql📎dlyk.sql
简单的项目效果可移至b站观看
具体代码可转至我的码云
二、前端组件添加
npm create vite@latest
npm install element-plus --save
npm install axios --save
npm install vue-router --save
npm install @element-plus/icons-vue --save
npm install echarts --save
//语法结构:import ... from ... 结构,从依赖中的vue框架导入createApp()函数
import { createApp } from 'vue'
//从依赖中的element-plus框架导入ElementPlus组件
import ElementPlus from 'element-plus'
//从依赖的element-plus框架导入css样式,导入样式不需要from
import 'element-plus/dist/index.css'
//从element-plus/icons-vue导入所有的图标组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
//从element-plus/dist/locale/zh-cn.mjs导入zhCn中文组件
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
//从router.js这个文件导入router组件
import router from './router/router.js'
//从./App.vue这个页面导入App组件(vue页面本身也叫组件,组件名默认是页面的文件名)
import App from './App.vue'
//使用上面导入的createApp()函数创建vue应用,创建vue应用时需要给它一个vue页面/组件,不能凭空创建
let app = createApp(App);
//在创建的vue应用中使用ElementPlus组件
app.use(ElementPlus, {locale: zhCn});
//循环出所有的图标,注册到vue应用中
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(ElementPlus)
//在创建的vue应用中使用router组件
app.use(router)
app.mount('#app');
三、后端基本组建
<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.peng</groupId>
<artifactId>peng-dlyk-server</artifactId>
<version>1.0-SNAPSHOT</version>
<name>peng-dlyk-server</name>
<description>peng-dlyk-server</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version> <!-- 确保版本号正确 -->
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<!--Page Helper分页插件的依赖-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.7</version>
</dependency>
<!--aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--EasyExcel-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</version> <!-- 最新版本 -->
</dependency>
<!--minio-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.17</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
server:
port: 8080
servlet:
context-path: /
#配置数据库连接相关信息
spring:
datasource:
url: jdbc:mysql://192.168.37.130:3306/dlyk?useUnicode=true&characterEncoding=utf8&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456@Pyc
hikari:
#最大连接数,默认是10
maximum-pool-size: 30
#最小空闲连接,默认是10
minimum-idle: 30
#等待连接池分配连接的最大时长,超过该时长还没有可用连接则发生超时异常(单位毫秒)
connection-timeout: 5000
#空闲连接的最大时长,空闲多久就被释放回收,设置为0不让连接回收
idle-timeout: 0
#一个连接的最大生命时间,超过该时间还没有使用就回收掉(单位毫秒),最好不要超过8小时
max-lifetime: 18000000
#配置redis的连接信息
data:
redis:
host: 192.168.37.130
port: 6379
password: 123456
database: 1
#设置jackson返回json数据时,时区和格式
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
# mybatis-plus的配置
mybatis-plus:
type-aliases-package: com.peng.model
global-config:
db-config:
camel-case: false
configuration:
map-underscore-to-camel-case: false #plus自动开启驼峰,关闭驼峰
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#定义定时任务的调度时间
project:
task:
cron: '* */5 * * * *'
delay: 180000
#minio配置
minio:
endpoint: http://192.168.37.130:9000
accessKey: minioadmin
secretKey: minioadmin
bucket: myfile
注意关注驼峰映射 plus自带驼峰,但是X生成的不一定是驼峰式!!!
(一) 多个工具类+常量类+返回类
1. 前端
import axios from 'axios'
import {getTokenName, messageConfirm, messageTip, removeToken} from "../util/util.js";
const instance = axios.create({
baseURL: 'http://localhost:8080',
timeout: 1000,
headers: {'X-Requested-With': 'XMLHttpRequest'}
});
export function doGet(url, params) {
return instance({
method: "get",
url: url,
params: params, //{name: "对的", age: 22},
dataType:"json"
})
}
export function doPost(url, data) {
return instance({
method: "post",
url: url,
data: data, //{name: "好的呢", age: 22},
dataType: "json"
})
}
export function doPut(url, data) {
return instance({
method: "put",
url: url,
data: data, //{name:"好的呢", age: 22},
dataType: "json"
})
}
export function doDelete(url, params) {
return instance({
method: "delete",
url: url,
params: params, //{name: "对的", age: 22},
dataType:"json"
})
}
// 添加请求拦截器 配置 token 请求头
instance.interceptors.request.use( (config) => {
console.log("触发了请求拦截器")
// 在发送请求之前做些什么,在请求头中放一个token(jwt),传给后端接口
let token = window.sessionStorage.getItem(getTokenName());
if (!token) { //当 会话储存空间 中没取到值 (没有点击 记住我)
token = window.localStorage.getItem(getTokenName());
if (token) {
config.headers['rememberMe'] = true;
}
}
if (token) { //token是存在的
config.headers['Authorization'] = token;
}
return config;
}, (error) => {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器 反应后端返回来的token验证数据
instance.interceptors.response.use( (response) => {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么,拦截token验证的结果,进行相应的提示和页面跳转
if (response.data.code > 900) { //code大于900说明是token验证未通过,900是后端自定义的
//给前端用户提示,并且跳转页面
messageConfirm(response.data.msg + ",是否重新去登录?").then(() => { //用户点击“确定”按钮就会触发then函数
//既然后端验证token未通过,那么前端的token肯定是有问题的,那没必要存储在浏览器中,直接删除一下
removeToken();
//跳到登录页
window.location.href = "/";
}).catch(() => { //用户点击“取消”按钮就会触发catch函数
messageTip("取消去登录", "warning");
})
return ;
}
return response;
}, (error) => {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
});
export default instance;
import {ElMessage, ElMessageBox} from "element-plus";
/**
* 消息提示
*
* @param msg
* @param type
*/
export function messageTip(msg, type) {
ElMessage({
showClose: true, //是否显示关闭按钮
center: true, //文字是否居中
duration: 3000, //显示时间,单位为毫秒
message: msg, //提示的消息内容
type: type, //消息类型:'success' | 'warning' | 'info' | 'error'
})
}
/**
* 获取存储在sessionStorage或者localStorage中的token(jwt)的名字
*
* @returns {string}
*/
export function getTokenName() {
return "dlyk_token";
}
/**
* 删除localStorage和sessionStorage中存储的token
*
*/
export function removeToken() {
window.sessionStorage.removeItem(getTokenName());
window.localStorage.removeItem(getTokenName());
}
/**
* 消息确认提示
*
* @param msg
* @returns {Promise<MessageBoxData>}
*/
export function messageConfirm(msg) {
return ElMessageBox.confirm(
msg, //提示语
'系统提醒', //提示的标题
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
}
/**
* 封装返回函数
*
*/
export function goBack(router) {
router.go(-1);
}
/**
* 获取token
*
* @returns {string}
*/
export function getToken() {
let token = window.sessionStorage.getItem(getTokenName());
if (!token) { //前面加了一个!,表示token不存在,token是空的,token没有值,这个意思
token = window.localStorage.getItem(getTokenName());
}
if (token) { //表示token存在,token不是空的,token有值,这个意思
return token;
} else {
messageConfirm("请求token为空,是否重新去登录?").then(() => { //用户点击“确定”按钮就会触发then函数
//既然后端验证token未通过,那么前端的token肯定是有问题的,那没必要存储在浏览器中,直接删除一下
removeToken();
//跳到登录页
window.location.href = "/";
}).catch(() => { //用户点击“取消”按钮就会触发catch函数
messageTip("取消去登录", "warning");
})
}
}
2. 后端
public class CacheUtils {
/**
* 带有缓存的查询工具方法
*
* @param cacheSelector
* @param databaseSelector
* @param cacheSave
* @return
* @param <T>
*/
public static <T> T getCacheData(Supplier<T> cacheSelector, Supplier<T> databaseSelector, Consumer<T> cacheSave) {
//从redis查询
T data = cacheSelector.get();
//如果redis没查到
if (ObjectUtils.isEmpty(data)) {
//从数据库查
data = databaseSelector.get();
//数据库查到了数据
if (!ObjectUtils.isEmpty(data)) {
//把数据放入redis
cacheSave.accept(data);
}
}
//返回数据
return data;
}
}
public class JSONUtils {
//jackson这个jar包转json
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* 把java对象转成json
*
* @param object
* @return
*/
public static String toJSON(Object object) {
try {
//把java对象转成json
return OBJECT_MAPPER.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* 把json字符串转java对象
*
* @param json
* @param clazz
* @return
* @param <T>
*/
public static <T> T toBean(String json, Class<T> clazz) {
try {
return OBJECT_MAPPER.readValue(json, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
public class JWTUtils {
public static final String SECRET = "dY8300olWQ3345;1d<3w48";
/**
* 生成JWT (token)
*
*/
public static String createJWT(String userJSON) {
//组装头数据
Map<String, Object> header = new HashMap<>();
header.put("alg", "HS256");
header.put("typ", "JWT");
return JWT.create()
//头部
.withHeader(header)
//负载
.withClaim("user", userJSON)
//签名
.sign(Algorithm.HMAC256(SECRET));
}
/**
* 验证JWT
*
* @param jwt 要验证的jwt的字符串
*/
public static Boolean verifyJWT(String jwt) {
try {
// 使用秘钥创建一个JWT验证器对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
//验证JWT,如果没有抛出异常,说明验证通过,否则验证不通过
jwtVerifier.verify(jwt);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 解析JWT的数据
*
*/
public static void parseJWT(String jwt) {
try {
// 使用秘钥创建一个JWT验证器对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
//验证JWT,得到一个解码后的jwt对象
DecodedJWT decodedJWT = jwtVerifier.verify(jwt);
//通过解码后的jwt对象,就可以获取里面的负载数据
Claim nickClaim = decodedJWT.getClaim("nick");
Claim ageClaim = decodedJWT.getClaim("age");
Claim phoneClaim = decodedJWT.getClaim("phone");
Claim birthDayClaim = decodedJWT.getClaim("birthDay");
String nick = nickClaim.asString();
int age = ageClaim.asInt();
String phone = phoneClaim.asString();
Date birthDay = birthDayClaim.asDate();
System.out.println(nick + " -- " + age + " -- " + phone + " -- " + birthDay);
} catch (TokenExpiredException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static TUser parseUserFromJWT(String jwt) {
try {
// 使用秘钥创建一个JWT验证器对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
//验证JWT,得到一个解码后的jwt对象
DecodedJWT decodedJWT = jwtVerifier.verify(jwt);
//通过解码后的jwt对象,就可以获取里面的负载数据
Claim userClaim = decodedJWT.getClaim("user");
String userJSON = userClaim.asString();
return JSONUtils.toBean(userJSON, TUserDetailsImpl.class);
} catch (TokenExpiredException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
public class ResponseUtils {
/**
* 使用response,把结果写出到前端
*
* @param response
* @param result
*/
public static void write(HttpServletResponse response, String result) {
response.setContentType("application/json;charset=UTF-8");
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.write(result);
writer.flush();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (writer != null) {
writer.close();
}
}
}
}
public class Constants {
public static final String LOGIN_URI = "/api/login";
//redis的key的命名规范: 项目名:模块名:功能名:唯一业务参数(比如用户id)
public static final String REDIS_JWT_KEY = "dlyk:user:login:";
//redis中负责人的key
public static final String REDIS_OWNER_KEY = "dlyk:user:owner";
//jwt过期时间7天
public static final Long EXPIRE_TIME = 7 * 24 * 60 * 60L;
//jwt过期时间30分钟
public static final Long DEFAULT_EXPIRE_TIME = 30 * 60L;
//分页时每页显示10条数据
public static final int PAGE_SIZE = 10;
//请求token的名称
public static final String TOKEN_NAME = "Authorization";
public static final String EMPTY = "";
//导出Excel的接口路径
public static final String EXPORT_EXCEL_URI = "/api/exportExcel";
public static final String EXCEL_FILE_NAME = "客户信息数据";
}
@Getter
@RequiredArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public enum CodeEnum {
OK(200, "成功"),
FAIL(500, "失败"),
TOKEN_IS_EMPTY(901, "请求Token参数为空"),
TOKEN_IS_ERROR(902, "请求Token有误"),
TOKEN_IS_EXPIRED(903, "请求Token已过期"),
TOKEN_IS_NONE_MATCH(904, "请求Token不匹配"),
USER_LOGOUT(200, "退出成功"),
DATA_ACCESS_EXCEPTION(500,"数据库操作失败"),
ACCESS_DENIED(500, "权限不足");
//结果码
private int code;
//结果信息
@NonNull
private String msg;
}
3. Redis Server Impl配置
public interface RedisService {
void setValue(String key, Object value);
Object getValue(String key);
Boolean removeValue(String key);
Boolean expire(String key, Long timeOut, TimeUnit timeUnit);
}
@Service
public class RedisServiceImpl implements RedisService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
public void setValue(String key, Object value) {
System.out.println("redis设置了一个Value "+"key="+key+" value="+value);
redisTemplate.opsForValue().set(key, value);
}
@Override
public Object getValue(String key) {
System.out.println("redis请求得到 "+key+" 的value");
return redisTemplate.opsForValue().get(key);
}
@Override
public Boolean removeValue(String key) {
System.out.println("redis请求删除 "+key);
return redisTemplate.delete(key);
}
@Override
public Boolean expire(String key, Long timeOut, TimeUnit timeUnit) {
System.out.println("redis设置 "+key+" 的过期时间");
return redisTemplate.expire(key, timeOut, timeUnit);
}
}
4. Main Spring启动器配置
@MapperScan("com.peng.mapper")
@SpringBootApplication//CommandLineRunner:实现 run() 方法,在 Spring Boot 启动后执行一些初始化操作(如修改 Redis 配置)
public class Main implements CommandLineRunner {
//定义一个静态 HashMap 作为缓存,可能用于存储临时数据,避免频繁访问数据库或 Redis
public static final Map<String, Object> cacheMap = new HashMap<>();
//RedisTemplate 是 Spring 提供的 Redis 访问工具,用于操作 Redis 数据库
@Resource
private RedisTemplate<String, Object> redisTemplate;
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Main.class, args);
String[] beanNamesForType = context.getBeanNamesForType(Executor.class);
//遍历 Spring 容器中所有 Executor(线程池) 的 Bean 名称
for (String s : beanNamesForType) {
System.out.println(s);
}
//applicationTaskExecutor 是 Spring 默认的异步任务执行器,通常是 线程池,用于异步任务执行
Object obj = context.getBean("applicationTaskExecutor");
System.out.println(obj);
}
@Override
public void run(String... args) throws Exception {
//springboot项目启动后,把redisTemplate这个Bean修改一下,修改一下key和value的序列化方式
//设置key序列化 将 key 序列化为字符串
redisTemplate.setKeySerializer(new StringRedisSerializer());
//对象映射工具,Java对象 和 json对象进行相互转化
ObjectMapper mapper = new ObjectMapper();
//设置可见性
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//激活类型
mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.EVERYTHING);
//设置value序列化
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(mapper, Object.class));
//设置hashKey序列化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//设置haskValue序列化
redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<Object>(mapper, Object.class));
}
//配置mybatis-plus插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //分页
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); //乐观锁
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); //防全局修改和删除
return interceptor;
}
}
5. 配置异常处理
@RestControllerAdvice //aop。拦截标注了@RestController的controller中的所有方法
//@ControllerAdvice //aop。拦截标注了@Controller的controller中的所有方法
public class GlobalExceptionHandler {
/**
* 异常处理的方法(controller方法发生了异常,那么就使用该方法来处理)
*
* @return
*/
@ExceptionHandler(value = Exception.class)
public R handException(Exception e) {
e.printStackTrace(); //在控制台打印异常信息
return R.FAIL(e.getMessage());
}
/**
* 异常的精确匹配,先精确匹配,匹配不到了,就找父类的异常处理
*
* @param e
* @return
*/
@ExceptionHandler(value = DataAccessException.class)
public R handException(DataAccessException e) {
e.printStackTrace(); //在控制台打印异常信息
return R.FAIL(CodeEnum.DATA_ACCESS_EXCEPTION);
}
/**
* 权限不足的异常处理
*
* @param e
* @return
*/
@ExceptionHandler(value = AccessDeniedException.class)
public R handException(AccessDeniedException e) {
e.printStackTrace(); //在控制台打印异常信息
return R.FAIL(CodeEnum.ACCESS_DENIED);
}
}
(二) 事务回滚(执行sql时会出现异常)
//凡是执行sql操作都要添加事务回滚
//添加事务注解 一般在Service层
//用mybatis-plus 有时在controller
@Transactional(rollbackFor = Exception.class)
四、Spring Security 实现 带有保护性的连接(不认证成功则无法调用后端api)
(一) 配置User父类以及UserDetailsImpl子类(UserDetails验证类)
@Getter
@Setter
public class TUserDetailsImpl extends TUser implements UserDetails, Serializable {
private static final long serialVersionUID = 1L;
// 由于JSON转换需要无参构造器,所以必须调用父类无参构造函数(在无@Data注解下)
public TUserDetailsImpl() {
super();
}
public TUserDetailsImpl(TUser user) {
super(user);
}
@JsonIgnore
public TUser getUser() {
return new TUser(
super.getId(),
super.getLogin_act(),
super.getLogin_pwd(),
super.getName(),
super.getPhone(),
super.getEmail(),
super.getAccount_no_expired(),
super.getCredentials_no_expired(),
super.getAccount_no_locked(),
super.getAccount_enabled(),
super.getCreate_time(),
super.getCreate_by(),
super.getEdit_time(),
super.getEdit_by(),
super.getLast_login_time()
);
}
//-------------------------实现Spring Security中UserDetails接口的7个方法------------------------------
private List<String> roleList;
private List<String> permissionList;
private List<TPermission> menuPermissionList;
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list = new ArrayList<>();
//角色
if (!ObjectUtils.isEmpty(this.getRoleList())) {
this.getRoleList().forEach(role -> {
list.add(new SimpleGrantedAuthority(role));
});
}
if (!ObjectUtils.isEmpty(this.getPermissionList())) {
//权限标识符
this.getPermissionList().forEach(permission -> {
list.add(new SimpleGrantedAuthority(permission));
});
}
return list;
}
@JsonIgnore
@Override
public String getPassword() {
return super.getLogin_pwd();
}
@JsonIgnore
@Override
public String getUsername() {
return super.getLogin_act();
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return super.getAccount_no_expired() == 1;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return super.getAccount_no_locked() == 1;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return super.getCredentials_no_expired() == 1;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return super.getAccount_enabled() == 1;
}
}
(二) 登录者进行登录时调用数据库认证(无token认证)
1. 数据库验证
// @CrossOrigin Spring Security中可以配置跨域,无需@CrossOrigin注解
@RestController
public class UserController implements UserDetailsService {
@Autowired
private TUserService tUserService;
//spring security 登录验证
@Override
@Transactional(rollbackFor = Exception.class)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<TUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TUser::getLogin_act, username);
TUser tUser = tUserService.getOne(wrapper);
if (tUser == null) {
throw new UsernameNotFoundException("登录账号不存在");
}
TUserDetailsImpl tUserDetailsImpl = new TUserDetailsImpl(tUser);
//查询一下当前用户的角色
List<TRole> tRoleList = tRoleMapper.selectByUserId(tUser.getId());
//字符串的角色列表
List<String> stringRoleList = new ArrayList<>();
tRoleList.forEach(tRole -> {
stringRoleList.add(tRole.getRole());
});
tUserDetailsImpl.setRoleList(stringRoleList); //设置用户的角色
//查询一下该用户有哪些菜单权限
List<TPermission> menuPermissionList = tPermissionMapper.selectMenuPermissionByUserId(tUserDetailsImpl.getId());
tUserDetailsImpl.setMenuPermissionList(menuPermissionList);
//查询一下该用户有哪些功能权限
List<TPermission> buttonPermissionList = tPermissionMapper.selectButtonPermissionByUserId(tUserDetailsImpl.getId());
List<String> stringPermissionList = new ArrayList<>();
buttonPermissionList.forEach(tPermission -> {
stringPermissionList.add(tPermission.getCode());//权限标识符
});
tUserDetailsImpl.setPermissionList(stringPermissionList);//设置用户的权限标识符
return tUserDetailsImpl;
}
}
进行自定义验证(MyAuthenticationSuccessHandler,MyAuthenticationFailureHandler)
Spring Security会在内部检查 返回的 TUserDetailsImpl 中的账号密码是否正确
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private RedisService redisService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//登录成功,执行该方法,在该方法中返回json给前端,就行了
TUserDetailsImpl tUserDetailsImpl = (TUserDetailsImpl) authentication.getPrincipal();
//1、生成jwt
//把 对象 转成json作为负载数据放入jwt
String userJSON = JSONUtils.toJSON(tUserDetailsImpl);
String jwt = JWTUtils.createJWT(userJSON);
//2、写入redis
redisService.setValue(Constants.REDIS_JWT_KEY + tUserDetailsImpl.getId(), jwt);
//3、设置jwt的过期时间(如果选择了记住我,过期时间是7天,否则是30分钟)
String rememberMe = request.getParameter("rememberMe");
if (Boolean.parseBoolean(rememberMe)) {
redisService.expire(Constants.REDIS_JWT_KEY + tUserDetailsImpl.getId(), Constants.EXPIRE_TIME, TimeUnit.SECONDS);
} else {
redisService.expire(Constants.REDIS_JWT_KEY + tUserDetailsImpl.getId(), Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
}
//登录成功的统一结果
R result = R.OK(jwt);
//把R对象转成json
String resultJSON = JSONUtils.toJSON(result);
//把R以json返回给前端
ResponseUtils.write(response, resultJSON);
}
}
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//登录失败,执行该方法,在该方法中返回json给前端,就行了
//登录失败的统一结果
R result = R.FAIL(exception.getMessage());
//把R对象转成json
String resultJSON = JSONUtils.toJSON(result);
//把R以json返回给前端
ResponseUtils.write(response, resultJSON);
}
}
@EnableMethodSecurity //开启方法级别的权限检查
@Configuration
public class SecurityConfig {
@Resource
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Resource
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Resource
private MyLogoutSuccessHandler myLogoutSuccessHandler;
@Resource
private MyAccessDeniedHandler myAccessDeniedHandler;
@Bean //There is no PasswordEncoder mapped for the id "null"
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, CorsConfigurationSource configurationSource) throws Exception {
//禁用跨站请求伪造
return httpSecurity
.formLogin( (formLogin) -> {
formLogin.loginProcessingUrl(Constants.LOGIN_URI) //登录处理地址,不需要写Controller
.usernameParameter("loginAct")//根据前端变量名
.passwordParameter("loginPwd")
//进行账号密码验证
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler);
})
.authorizeHttpRequests( (authorize) -> {
authorize.requestMatchers("/api/login").permitAll()
.anyRequest().authenticated(); //除/api/login外其它任何请求都需要登录后才能访问
})
.csrf(AbstractHttpConfigurer :: disable) //方法引用,禁用跨站请求伪造
//支持跨域请求
.cors( (cors) -> {
cors.configurationSource(configurationSource);
})
.sessionManagement( (session) -> {
//session创建策略
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 无session状态,也就是禁用session
})
//添加自定义的Filter
.addFilterBefore(tokenVerifyFilter, LogoutFilter.class)
//退出登录
.logout((logout) -> {
logout.logoutUrl("/api/logout") //退出提交到该地址,该地址不需要我们写controller的,是框架处理
.logoutSuccessHandler(myLogoutSuccessHandler);
})
//这个是没有权限访问时触发
.exceptionHandling((exceptionHandling) -> {
exceptionHandling.accessDeniedHandler(myAccessDeniedHandler);
})
.build();
}
//配置跨域
@Bean
public CorsConfigurationSource configurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*")); //允许任何来源,http://localhost:8081
configuration.setAllowedMethods(Arrays.asList("*")); //允许任何请求方法,post、get、put、delete
configuration.setAllowedHeaders(Arrays.asList("*")); //允许任何的请求头
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
(三) 调用除/api/login接口外时进行身份认证(验证token是否存在)
利用 token 来进行用户的认证
前端:token存储到 sessionStorage(会话存储空间)或localStorage(本地存储空间)
后端:利用 Redis 为token 进行缓存
1. token存储思路
- 后端在登陆成功时将 TUserDetailsImpl(验证信息类) 转换为 JSON 后再用 JWT 转换为 token
- 在 Redis 中添加 key:项目名:模块名:功能名:唯一业务参数(比如用户id) dlyk:user:login:1
value:token
- 根据 "rememberMe" 进行 Redis 的过期设定
- 前端接受 token 并根据 "rememberMe" 选择储存到sessionStorage还是localStorage当中
2. token验证思路
- 前端启动钩子(/api/login/info)渲染页面时发送信息,并利用 Axios 的请求拦截器进行请求头(headers)的配置,添加 'Authorization' :token
- SecurityConfig 中的 SecurityFilterChain 添加 检查token工具包(TokenVerifyFilter)
- 验证请求头中 'Authorization' 对应的 token 是否存在于 Redis 中
- 后端构建相对应的接口(/api/login/info)为前端进行返回信息
- 前端开启 Axios 的响应拦截器 响应后端返回的JSON数据,如果是验证失败 进行跳转登录页操作,
验证成功则放行
3. token工具包(TokenVerifyFilter)实现
@Resource
private TokenVerifyFilter tokenVerifyFilter;
//添加自定义的Filter
.addFilterBefore(tokenVerifyFilter, LogoutFilter.class)
//验证 token 的工具包
@Component
public class TokenVerifyFilter extends OncePerRequestFilter {
@Resource
private RedisService redisService;
//spring boot框架的ioc容器中已经创建好了该线程池,可以注入直接使用
@Resource
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request.getRequestURI().equals(Constants.LOGIN_URI)) { //如果是登录请求,此时还没有生成jwt,那不需要对登录请求进行jwt验证
//验证jwt通过了 ,让Filter链继续执行,也就是继续执行下一个Filter
filterChain.doFilter(request, response);
} else {
String token = null;
if (request.getRequestURI().equals(Constants.EXPORT_EXCEL_URI)) {
//从请求路径的参数中获取token
token = request.getParameter("Authorization");
} else {
//其他请求都是从请求头中获取token
token = request.getHeader("Authorization");
}
if (!StringUtils.hasText(token)) {
//token验证未通过的统一结果
R result = R.FAIL(CodeEnum.TOKEN_IS_EMPTY);
//把R对象转成json
String resultJSON = JSONUtils.toJSON(result);
//把R以json返回给前端
ResponseUtils.write(response, resultJSON);
return;
}
//验证token有没有被篡改过
if (!JWTUtils.verifyJWT(token)) {
//token验证未通过统一结果
R result = R.FAIL(CodeEnum.TOKEN_IS_ERROR);
//把R对象转成json
String resultJSON = JSONUtils.toJSON(result);
//把R以json返回给前端
ResponseUtils.write(response, resultJSON);
return;
}
//将前端传过来的 token 转换为 java
TUserDetailsImpl tUserDetailsImpl = (TUserDetailsImpl) JWTUtils.parseUserFromJWT(token);
String redisToken = (String) redisService.getValue(Constants.REDIS_JWT_KEY + tUserDetailsImpl.getId());
//验证返回的 redis value 是否为空
if (!StringUtils.hasText(redisToken)) {
//token验证未通过统一结果
R result = R.FAIL(CodeEnum.TOKEN_IS_EXPIRED);
//把R对象转成json
String resultJSON = JSONUtils.toJSON(result);
//把R以json返回给前端
ResponseUtils.write(response, resultJSON);
return;
}
//验证 token 和 value 是否一致
if (!token.equals(redisToken)) {
//token验证未通过的统一结果
R result = R.FAIL(CodeEnum.TOKEN_IS_NONE_MATCH);
//把R对象转成json
String resultJSON = JSONUtils.toJSON(result);
//把R以json返回给前端
ResponseUtils.write(response, resultJSON);
return;
}
//jwt验证通过了,那么在spring security的上下文环境中要设置一下,设置当前这个人是登录过的,你后续不要再拦截他了
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(tUserDetailsImpl, tUserDetailsImpl.getLogin_pwd(), tUserDetailsImpl.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//刷新一下token(异步处理,new一个线程去执行)
/*new Thread(() -> {
//刷新token
String rememberMe = request.getHeader("rememberMe");
if (Boolean.parseBoolean(rememberMe)) {
redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.EXPIRE_TIME, TimeUnit.SECONDS);
} else {
redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
}
}).start();*/
//异步处理(更好的方式,使用线程池去执行)
threadPoolTaskExecutor.execute(() -> {
//刷新token
String rememberMe = request.getHeader("rememberMe");
if (Boolean.parseBoolean(rememberMe)) {
redisService.expire(Constants.REDIS_JWT_KEY + tUserDetailsImpl.getId(), Constants.EXPIRE_TIME, TimeUnit.SECONDS);
} else {
redisService.expire(Constants.REDIS_JWT_KEY + tUserDetailsImpl.getId(), Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
}
});
//验证jwt通过了 ,让Filter链继续执行,也就是继续执行下一个Filter
filterChain.doFilter(request, response);
}
}
}
(四) 设计用户免登录
//用户免登录
freeLogin(){
let token = window.localStorage.getItem(getTokenName());
if(token){
doGet("/api/freeLogin",{}).then(resp => {
if(resp.data.code === 200){
//token 验证通过了,可以进行免登录
window.location.href = "/dashboard";
}
})
}
}
//后端因为 filter 工具包自动验证token,所以当进入这个接口时肯定是 验证成功的
@GetMapping("api/freeLogin")
public R freeLogin() {
return R.OK();
}
(五) 退出登录(删除前后端中的token并退出)
配置 MyLogoutSuccessHandler 直接在Spring Security中调用
//退出登录 退出成功时需要删除token
function logout(){
doGet("/api/logout",{}).then( (resp) => {
if(resp.data.code === 200){
messageTip("退出成功","success");
removeToken();
window.location.href = "/";
} else{
messageConfirm("退出异常,是否要强制退出").then(()=>{
//既然后端验证token未通过,那么前端的token肯定是有问题的,那没必要存储在浏览器中,直接删除一下
removeToken();
//跳到登录页
window.location.href = "/";
}).catch(()=>{
messageTip("取消强制退出", "warning");
})
}
})
}
@Resource
private MyLogoutSuccessHandler myLogoutSuccessHandler;
//退出登录
.logout((logout) -> {
logout.logoutUrl("/api/logout") //退出提交到该地址,该地址不需要我们写controller的,是框架处理
.logoutSuccessHandler(myLogoutSuccessHandler);
})
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Resource
private RedisService redisService;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//退出成功,执行该方法,在该方法中返回json给前端,就行了
TUserDetailsImpl tUserDetailsImpl = (TUserDetailsImpl)authentication.getPrincipal();
//删除 Redis 中的token
redisService.removeValue(Constants.REDIS_JWT_KEY + tUserDetailsImpl.getId());
//退出成功的统一结果
R result = R.OK(CodeEnum.USER_LOGOUT);
//把R对象转成json
String resultJSON = JSONUtils.toJSON(result);
//把R以json返回给前端
ResponseUtils.write(response, resultJSON);
}
}
(六) 进行token的续约
当5分钟我对页面不做任何操作,token的过期时间会减少5分钟
但是当我对页面进行点击后,token的过期时间会还原成初始值
//同样利用Spring Security的Filter(工具包)
//进行一个异步线程来更新token,当调用任何接口时,忽略前面的验证刷新token
//异步处理(更好的方式,使用线程池去执行)
threadPoolTaskExecutor.execute(() -> {
//刷新token
String rememberMe = request.getHeader("rememberMe");
if (Boolean.parseBoolean(rememberMe)) {
redisService.expire(Constants.REDIS_JWT_KEY + tUserDetailsImpl.getId(),
Constants.EXPIRE_TIME, TimeUnit.SECONDS);
} else {
redisService.expire(Constants.REDIS_JWT_KEY + tUserDetailsImpl.getId(),
Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
}
});
五、账号管理功能开发(用户管理)
(一) 当点击用户管理时,以分页模式查找多个用户的信息
//获取用户信息
//进行分页查询,传入当前页数的值 current : current 是一种 k :v 的简写方式
//但ES6 语法过后 可以直接 {current} 进行键值对映射,除非kv不相同才会用以前的方法
function getData(current){
doGet("/api/users", {current:current}).then((resp)=>{
console.log(resp);
if(resp.data.code === 200){
userList.value = resp.data.data.records;
pageSize.value = resp.data.data.size;
total.value = resp.data.data.total;
}
})
}
//Mapper层
public interface TUserMapper extends BaseMapper<TUser> {
@Select("SELECT * FROM t_user")
IPage<TUser> selectUserPage(Page<TUser> page);
}
//Controller直接调用Mapper(懒)
//以分页形式进行多个用户查询
@GetMapping("/api/users") //required = false 表示参数可传不穿,反之必须穿
public R userPage(@RequestParam(value="current",required = false) Integer current) {
//当没传入参数时,初始化为1
if(current == null) {current = 1;}
IPage<TUser> tUserIPage = tUserMapper.selectUserPage(new Page<TUser>(current, Constants.PAGE_SIZE));
return R.OK(tUserIPage);
}
(二) 实现分页
// 使用 v-model 表单绑定 当前页current-page
<el-pagination
background
layout="prev, pager, next"
:page-size="pageSize"
:total="total"
v-model:current-page="currentPage"
/>
//监视器 精准监视 currentPage 是否发生了改变
watch(currentPage,(newPage)=>{
console.log("当前页变化:",newPage);
toPage(newPage);
})
//这个 watch 监视器传来的新值
function toPage(current){
getData(current);
}
(三) 查看账号详情
//详情按钮
function view(id){
console.log("跳转",id,"的详情页面");
router.push("/dashboard/user/" + id);
}
//子路由 路径前面不能以 / 开头
children:[
{
path:'user/:id',
component:()=>import('../view/UserDetailView.vue'),
}
]
//获取指定 id 用户的信息
@GetMapping("/api/user/{id}")
public R userDetail(@PathVariable(value = "id") Integer id) {
TUser tUser = tUserService.getById(id);
return R.OK(tUser);
}
(四) 新增 | 修改一个用户
<el-dialog v-model="userDialogVisible" :title="userQuery.id > 0 ? '编辑用户' : '新增用户'" width="55%" center draggable>
<el-form ref="userRefForm" :model="userQuery" label-width="110px" :rules="userRules">
<el-form-item label="账号" prop="login_act">
<el-input v-model="userQuery.login_act" />
</el-form-item>
<el-form-item label="密码" v-if="userQuery.id > 0"><!--编辑-->
<el-input type="password" v-model="userQuery.login_pwd" />
</el-form-item>
<el-form-item label="密码" prop="loginPwd" v-else><!--新增-->
<el-input type="password" v-model="userQuery.login_pwd" />
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input v-model="userQuery.name" />
</el-form-item>
<el-form-item label="手机" prop="phone">
<el-input v-model="userQuery.phone" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="userQuery.email" />
</el-form-item>
<el-form-item label="账号未过期" prop="account_no_expired">
<el-select v-model="userQuery.account_no_expired" placeholder="请选择">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"/>
</el-select>
</el-form-item>
<el-form-item label="密码未过期" prop="credentials_no_expired">
<el-select v-model="userQuery.credentials_no_expired" placeholder="请选择">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"/>
</el-select>
</el-form-item>
<el-form-item label="账号未锁定" prop="account_no_locked">
<el-select v-model="userQuery.account_no_locked" placeholder="请选择">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"/>
</el-select>
</el-form-item>
<el-form-item label="账号是否启用" prop="account_enabled">
<el-select v-model="userQuery.account_enabled" placeholder="请选择">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="userDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="userSubmit">提 交</el-button>
</span>
</template>
</el-dialog>
//用户的弹窗是否弹出来,true就弹出来,false就不弹出来
let userDialogVisible = ref(false);
//添加用户中的值
let userQuery = ref({});
//指定添加用户的规则
let userRules = {
login_act : [
{required: true, message: '请输入登录账号', trigger: 'blur'}
],
login_pwd : [
{required: true, message: '请输入登录密码', trigger: 'blur'},
{min: 6, max: 16, message: '登录密码长度为6-16位', trigger: 'blur'}
],
name: [
{required: true, message: '请输入姓名', trigger: 'blur'},
{pattern: /^[\u4E00-\u9FA5]{1,5}$/, message: '姓名必须是中文', trigger: 'blur'}
],
phone: [
{required: true, message: '请输入手机号码', trigger: 'blur'},
{pattern: /^1[3-9]\d{9}$/, message: '格式为 1:1 2:[3-9] 11位', trigger: 'blur'}
],
email: [
{required: true, message: '请输入邮箱', trigger: 'blur'},
{pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/, message: '邮箱格式有误', trigger: 'blur'}
],
account_no_expired: [
{required: true, message: '请选择账号是否未过期', trigger: 'blur'},
],
credentials_no_expired: [
{required: true, message: '请选择密码是否未过期', trigger: 'blur'},
],
account_no_locked: [
{required: true, message: '请选择账号是否未未锁定', trigger: 'blur'},
],
account_enabled: [
{required: true, message: '请选择账号是否可用', trigger: 'blur'},
]
}
//下拉选项数组
let options = [
{label : '是', value : 1},
{label : '否', value : 0}
]
//绑定添加用户表单,用于后续验证规则的实现
let userRefForm = ref();
//实现子组件单独的渲染,为子组件添加一个隐藏或者展现的变量值
<el-main>
<!--利用子路由,只改变中间页面的内容-->
<router-view v-if="isRouterAlive"/>
</el-main>
//刷新页面
const reload = async () => {
isRouterAlive.value = false; //右侧内容隐藏
await nextTick();
isRouterAlive.value = true;
}
//父组件推方法|变量
//注意 provide的方法需要在provide之前定义好
provide("reload",reload);
//获取父亲方法|或变量
const reload = inject("reload");
//添加用户
function add(){
userDialogVisible.value = true;
userQuery.value = {};
}
//编辑按钮
function edit(id){
userDialogVisible.value = true;
loadUser(id);
}
//编辑用户时,先获取用户信息
const loadUser = (id) => {
doGet("/api/user/"+id,{}).then((resp)=>{
if(resp.data.code === 200){
userQuery.value = resp.data.data;
userQuery.value.login_pwd = null;
}
})
}
//新用户 | 编辑用户 信息提交
function userSubmit(){
let formData = new FormData();
for(let field in userQuery.value){
formData.append(field, userQuery.value[field]);
}
//实现 表单的验证规则
userRefForm.value.validate( (isValid) => {
if (isValid) {
if(userQuery.value.id > 0){ //进入编辑
doPut("/api/user", formData).then(resp => {/*新增*/
if (resp.data.code === 200) {
messageTip("提交成功", "success");
reload();
} else {
messageTip("提交失败", "error");
}
})
} else{ //进入新增
doPost("/api/user", formData).then(resp => {/*新增*/
if (resp.data.code === 200) {
messageTip("提交成功", "success");
reload();
} else {
messageTip("提交失败", "error");
}
})
}
userDialogVisible = false;
}
})
}
//新增用户
@PostMapping("api/user")
public R addUser(@ModelAttribute UserQuery userQuery, @RequestHeader(value = "Authorization") String token) {
//token 是获取创建人的id
userQuery.setToken(token);
int save = tUserService.saveUser(userQuery);
return save >= 1 ? R.OK() : R.FAIL();
}
@Override
public int saveUser(UserQuery userQuery) {
//将TUser当中符合userQuery属性copy过来
TUser tUser = new TUser();
BeanUtils.copyProperties(userQuery, tUser);
//将密码加密
tUser.setLogin_pwd(passwordEncoder.encode(userQuery.getLogin_pwd()));
//创建时间
tUser.setCreate_time(new Date());
//利用 token 获取创建人信息
//登录人的id
Integer loginUserId = JWTUtils.parseUserFromJWT(userQuery.getToken()).getId();
tUser.setCreate_by(loginUserId); //创建人
return tUserMapper.insert(tUser);
}
//编辑用户
@PutMapping("api/user")
public R updateUser(@ModelAttribute UserQuery userQuery, @RequestHeader(value = "Authorization") String token) {
//token 是获取创建人的id
userQuery.setToken(token);
int update = tUserService.updateUser(userQuery);
return update >= 1 ? R.OK() : R.FAIL();
}
@Override
public int updateUser(UserQuery userQuery) {
//将TUser当中符合userQuery属性copy过来
TUser tUser = new TUser();
BeanUtils.copyProperties(userQuery, tUser);
//有密码则将密码加密,否则不管
//前端传回的是null
if(tUser.getLogin_pwd() != null){
tUser.setLogin_pwd(passwordEncoder.encode(userQuery.getLogin_pwd()));
}
//编辑时间
tUser.setEdit_time(new Date());
//利用 token 获取编辑人信息
//登录人的id
Integer loginUserId = JWTUtils.parseUserFromJWT(userQuery.getToken()).getId();
tUser.setEdit_by(loginUserId); //创建人
LambdaQueryWrapper<TUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TUser::getId,tUser.getId());
return tUserMapper.update(tUser,queryWrapper);
}
(五) 删除一个用户
//传递登录者
provide("loadUser",user);
//接受登录者
const loadUser = inject("loadUser")
//删除按钮
function del(id){
if(id === loadUser.value.id) {
messageTip("不能删除自身用户", "error");
}else{
//做出提示
messageConfirm("您确定要删除该用户吗").then(() => {
doDelete("/api/user/" + id, {}).then((resp) => {
if (resp.data.code === 200) {
messageTip("用户删除成功", "success");
reload();
} else {
messageTip("删除失败,原因:" + resp.data.msg, "error");
}
})
}).catch(() => {
messageTip("取消删除该用户", "warning");
})
}
}
//删除用户
@Transactional(rollbackFor = Exception.class)
@DeleteMapping("api/user/{id}")
public R deleteUser(@PathVariable(value = "id") Integer id) {
Boolean delete = tUserService.removeById(id);
return delete ? R.OK() : R.FAIL();
}
(六) 批量删除
//删除按钮
function del(id){
if(id === loadUser.value.id) {
messageTip("不能删除自身用户", "error");
}else{
//做出提示
messageConfirm("您确定要删除该用户吗").then(() => {
doDelete("/api/user/" + id, {}).then((resp) => {
if (resp.data.code === 200) {
messageTip("用户删除成功", "success");
reload();
} else {
messageTip("删除失败,原因:" + resp.data.msg, "error");
}
})
}).catch(() => {
messageTip("取消删除该用户", "warning");
})
}
}
//批量删除事件
const batchDel = () => {
if(idList.value.length <= 0){
//提示一下
messageTip("请选择要删除的数据", "warning");
return;
}
let ok = true;
idList.value.forEach((data) => {
if(loadUser.value.id === data) {
ok = false;
//forEach 用 return 跳出
return;
}
})
if(ok === false) {
messageTip("存在自身用户,不能删除", "error");
}else{
//做出提示
messageConfirm("您确定批量删除这些用户吗").then(() => {
// idList 原本是一个数组,直接以params传过去如下
// /api/user/?ids=ids[]1,ids[],2 错误的url格式,无法传输
// 所以要转换为String /api/user/?ids=1,2
doDelete("/api/user", {ids : idList.value.join(",")}).then((resp) => {
if (resp.data.code === 200) {
messageTip("用户删除成功", "success");
reload();
} else {
messageTip("删除失败,原因:" + resp.data.msg, "error");
}
})
}).catch(() => {
messageTip("取消批量删除用户", "warning");
})
}
}
//批量删除用户
@Transactional(rollbackFor = Exception.class)
@DeleteMapping(value = "/api/user")
public R batchDelUser(@RequestParam(value = "ids") String ids) {
//ids = "1,3,5,6,7,11,15";
//解析String 转换为 int 数组
List<Integer> idList = Arrays.stream(ids.split(","))
.map(Integer::parseInt)
.collect(Collectors.toList());
boolean batchDel = tUserService.removeByIds(idList);
return batchDel ? R.OK() : R.FAIL();
}
六、添加权限设定展示
(一) 思路
当查询 sql 语句是,观察当前登录人是否为 ("admin管理者"),如果不是管理着,
则通过切面注解的方式为 sql 语句的尾部增加语句,类似与mybatis-plus的分页插件一样,
在尾部添加 limit 语句
(二) 首先获取登录人的角色 | 菜单权限 | 功能权限
//spring security 登录验证
@Override
@Transactional(rollbackFor = Exception.class)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<TUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TUser::getLogin_act, username);
TUser tUser = tUserService.getOne(wrapper);
if (tUser == null) {
throw new UsernameNotFoundException("登录账号不存在");
}
TUserDetailsImpl tUserDetailsImpl = new TUserDetailsImpl(tUser);
//查询一下当前用户的角色
List<TRole> tRoleList = tRoleMapper.selectByUserId(tUser.getId());
//字符串的角色列表
List<String> stringRoleList = new ArrayList<>();
tRoleList.forEach(tRole -> {
stringRoleList.add(tRole.getRole());
});
tUserDetailsImpl.setRoleList(stringRoleList); //设置用户的角色
//查询一下该用户有哪些菜单权限
List<TPermission> menuPermissionList = tPermissionMapper.selectMenuPermissionByUserId(tUserDetailsImpl.getId());
tUserDetailsImpl.setMenuPermissionList(menuPermissionList);
//查询一下该用户有哪些功能权限
List<TPermission> buttonPermissionList = tPermissionMapper.selectButtonPermissionByUserId(tUserDetailsImpl.getId());
List<String> stringPermissionList = new ArrayList<>();
buttonPermissionList.forEach(tPermission -> {
stringPermissionList.add(tPermission.getCode());//权限标识符
});
tUserDetailsImpl.setPermissionList(stringPermissionList);//设置用户的权限标识符
return tUserDetailsImpl;
}
List<TRole> selectByUserId(@Param("userId") Integer userId);
<!--查询用户角色-->
<select id="selectByUserId" parameterType="java.lang.Integer" resultMap="BaseResultMap">
SELECT
tr.*
FROM
t_role tr
LEFT JOIN t_user_role tur ON tr.id = tur.role_id
where tur.user_id = #{userId, jdbcType=INTEGER}
</select>
List<TPermission> selectMenuPermissionByUserId(@Param("userId")Integer userId);
<!--查询菜单权限-->
<select id="selectMenuPermissionByUserId" parameterType="java.lang.Integer" resultMap="PermissionResultMap">
SELECT
tp.*,
childTp.id cid, childTp.name cname, childTp.url curl, childTp.icon cicon
FROM
t_permission tp
LEFT JOIN t_permission childTp ON tp.id = childTp.parent_id
LEFT JOIN t_role_permission trp ON tp.id = trp.permission_id
LEFT JOIN t_role tr ON trp.role_id = tr.id
LEFT JOIN t_user_role tur ON tr.id = tur.role_id
WHERE
tur.user_id = #{userId, jdbcType=INTEGER}
AND tp.type = 'menu'
and childTp.type = 'menu'
</select>
List<TPermission> selectButtonPermissionByUserId(@Param("userId")Integer userId);
<!--功能权限-->
<select id="selectButtonPermissionByUserId" parameterType="java.lang.Integer" resultMap="PermissionResultMap">
SELECT
tp.*
FROM
t_permission tp
LEFT JOIN t_role_permission trp ON tp.id = trp.permission_id
LEFT JOIN t_role tr ON trp.role_id = tr.id
LEFT JOIN t_user_role tur ON tr.id = tur.role_id
WHERE
tur.user_id = #{userId, jdbcType=INTEGER}
AND tp.type = 'button'
</select>
(三) 利用aop切片注解方法
@Target(ElementType.METHOD)//这个注解只能用于方法上
@Retention(RetentionPolicy.RUNTIME)//该注解在运行时可用,适用于AOP拦截处理
@Documented//让 Javadoc 生成文档时包含该注解的信息
public @interface DataScope {
//要在sql语句的末尾添加一个过滤条件
//select * from t_user (管理员)
//select * from t_user tu where tu.id = 2 (普通用户:于嫣)
//select * from t_activity (管理员)
//select * from t_activity ta where ta.owner_id = 2 (普通用户:于嫣)
//表的别名
public String tableAlias() default "";
//表的字段名
public String tableField() default "";
}
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class BaseQuery {
//jwt
private String token;
//数据权限的SQL过滤条件: tu.id = 2 或者 ta.owner_id = 2
private String filterSQL;
}
@Aspect
@Component
public class DataScopeAspect {
//切入点
@Pointcut(value = "@annotation(com.peng.commons.DataScope)")
private void pointCut() {
}
//环绕
@Around(value = "pointCut()")
//连接点
public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
//拿到方法上的注解
DataScope dataScope = methodSignature.getMethod().getDeclaredAnnotation(DataScope.class);
String tableAlias = dataScope.tableAlias();
String tableField = dataScope.tableField();
//在spring web容器中,可以拿到当前请求的request对象
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader(Constants.TOKEN_NAME);
//从token中解析出该用户是管理员还是普通用户
TUserDetailsImpl tUserDetails = (TUserDetailsImpl) JWTUtils.parseUserFromJWT(token);
//拿到用户的角色
List<String> roleList = tUserDetails.getRoleList();
if (!roleList.contains("admin")) {//不包含admin角色,只查当前用户自己的数据,否则查所有数据
//注意规范!!! 拿方法的第一个参数
Object params = joinPoint.getArgs()[0];
if (params instanceof BaseQuery) {//判断拿到的第一个参数是不是BaseQuery类型
BaseQuery query = (BaseQuery)params;
//编辑 sql 语句
//select * from t_user tu where tu.id = 2 (普通用户:于嫣)
query.setFilterSQL(" and " + tableAlias + "." + tableField + " = " + tUserDetails.getId());
}
}
System.out.println("目标方法执行之前....");
Object result = joinPoint.proceed();
System.out.println("目标方法执行之后....");
return result;
}
}
(四) 实现管理员 | 非管理员的分页查询
//以分页形式进行多个用户查询
@Transactional(rollbackFor = Exception.class)
@GetMapping("/api/users") //required = false 表示参数可传不穿,反之必须穿
public R userPage(@RequestParam(value="current",required = false) Integer current) {
//当没传入参数时,初始化为1
if(current == null) {current = 1;}
IPage<TUser> tUserIPage = tUserMapper
.selectUserPage(BaseQuery.builder().build(),
new Page<TUser>(current, Constants.PAGE_SIZE));
return R.OK(tUserIPage);
}
@Mapper
public interface TUserMapper extends BaseMapper<TUser> {
@DataScope(tableAlias = "tu",tableField = "id")
IPage<TUser> selectUserPage(@Param("query") BaseQuery query, Page<TUser> page);
}
<!--@DataScope(tableAlias = "tu",tableField = "id")-->
<!--
当不是管理员时sql 语法 变为
select * from t_user tu where tu.id = ${userId} + 分页插件
-->
<!-- 使用 <where> 可以避免 "and" 的问题-->
<select id="selectUserPage" resultMap="BaseResultMap">
select * from t_user tu
<where>
${query.filterSQL}
</where>
</select>
七、实现带Redis缓存的查询
//获取负责人
@GetMapping(value = "/api/owner")
public R owner() {
List<TUser> ownerList = tUserService.getOwnerList();
return R.OK(ownerList);
}
public class CacheUtils {
//cacheSelector 缓存生产者 (从中间件中获取缓存)
//databaseSelector 数据库生产者(从数据库中获取信息)
//cacheSave 缓存消费者(如何处理缓存)
public static <T> T getCacheData(Supplier<T> cacheSelector,
Supplier<T> databaseSelector,
Consumer<T> cacheSave) {
//从redis查询
T data = cacheSelector.get();
//如果redis没查到
if (ObjectUtils.isEmpty(data)) {
//从数据库查
data = databaseSelector.get();
//数据库查到了数据
if (!ObjectUtils.isEmpty(data)) {
//把数据放入redis
cacheSave.accept(data);
}
}
//返回数据
return data;
}
}
//有多种类型,这里只管理list类型
@Component
public class RedisManager {
@Resource
private RedisTemplate<String, Object> redisTemplate;
// 从 Redis List 数据结构中取出 key 对应的所有值。
// opsForList().range(key, 0, -1):
// opsForList():操作 Redis 的 List 数据结构。
// range(key, 0, -1):获取该 List 所有元素(从索引 0 到 -1 代表最后一个元素)。
public Object getValue(String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
// 将 Collection<T> 里的数据全部存入 Redis List,存储在 key 对应的列表中。
// data.toArray(t):将 Collection<T> 转换为 Object[] 数组。
// opsForList().leftPushAll(key, t):批量将数据从 左侧 推入 Redis List,新数据排在最前面。
public <T> Object setValue(String key, Collection<T> data) {
Object[] t = new Object[data.size()];
data.toArray(t);
return redisTemplate.opsForList().leftPushAll(key, t);
}
}
@Override
public List<TUser> getOwnerList() {
//1、从redis查询
//2、redis查不到,就从数据库查询,并且把数据放入redis(5分钟过期)
return CacheUtils.getCacheData(() -> {
//生产,从缓存redis查询数据
return (List<TUser>)redisManager.getValue(Constants.REDIS_OWNER_KEY);
},
() -> {
//生产,从mysql查询数据
return tUserMapper.selectByOwner();
},
(t) -> {
//消费,把数据放入缓存redis
redisManager.setValue(Constants.REDIS_OWNER_KEY, t);
}
);
}
八、为静态缓存添加信息(cacheMap)
@EnableScheduling //开启定时任务
@Component
public class DataTask {
@Resource
private TDicTypeService tDicTypeService;
@Resource
private TProductService tProductService;
@Resource
private TActivityService tActivityService;
//调度的意思
// project.task.delay 是一个 配置文件中的值
// (通常在 application.yml 或 application.properties 中定义)
// 设置任务的时区,确保任务按照中国标准时间(CST)执行,避免跨时区问题。
// timeUnit = TimeUnit.MILLISECONDS
// 指定 fixedDelayString 和 initialDelay 的单位是 毫秒(MS)。
// initialDelay = 1000
// 任务在应用启动后,延迟 1 秒(1000 毫秒)再开始执行第一
@Scheduled(fixedDelayString = "${project.task.delay}", zone = "Asia/Shanghai", timeUnit = TimeUnit.MILLISECONDS, initialDelay = 1000)
public void task() {
//定时任务要执行的业务逻辑代码
System.out.println("定时任务业务逻辑执行......" + new Date());
List<TDicType> tDicTypeList = tDicTypeService.loadAllDicData();
tDicTypeList.forEach(tDicType -> {
String typeCode = tDicType.getType_code();
List<TDicValue> tDicValueList = tDicType.getDicValueList();
Main.cacheMap.put(typeCode, tDicValueList);
});
//查询所有在售产品
List<TProduct> tProductList = tProductService.getAllOnSaleProduct();
Main.cacheMap.put(DicEnum.PRODUCT.getCode(), tProductList);
//查询所有正在进行的市场活动
List<TActivity> tActivityList = tActivityService.getOngoingActivity();
Main.cacheMap.put(DicEnum.ACTIVITY.getCode(), tActivityList);
}
}
//获得 (List<TDicValue>)
List<TDicValue> tDicValueList =
(List<TDicValue>) Main.cacheMap.get(DicEnum.APPELLATION.getCode());
//获得 (List<TProduct>)
List<TProduct> tProduct =
(List<TProduct>) Main.cacheMap.get(DicEnum.PRODUCT.getCode());
九、使用EasyExcel进行Excel文件导入
(一) 将Excel中的中文字段转换为序号字段(查找数据库)
数据库将这些字段都通过外键约束绑定了其他的表
当字段数据无法从其他表中查到时,就无法添加
查找成功:将中文属性返回为对应表中的 id 字段
查找失败:将字段 赋值 为 -1 ,表示不符合外键约束
1. 实体类配置Excel转换器对象
@TableName(value ="t_clue")
@Data
public class TClue implements Serializable {
private static final long serialVersionUID = 1L;
//无需转换器的字段已省略
@ExcelProperty(value = "称呼", converter = AppellationConverter.class)
@TableField(value = "appellation")
private Integer appellation;
@ExcelProperty(value = "是否贷款", converter = NeedLoanConverter.class)
@TableField(value = "need_loan")
private Integer need_loan;
@ExcelProperty(value = "意向状态", converter = IntentionStateConverter.class)
@TableField(value = "intention_state")
private Integer intention_state;
@ExcelProperty(value = "意向产品", converter = IntentionProductConverter.class)
@TableField(value = "intention_product")
private Integer intention_product;
@ExcelProperty(value = "线索状态", converter = StateConverter.class)
@TableField(value = "state")
private Integer state;
@ExcelProperty(value = "线索来源", converter = SourceConverter.class)
@TableField(value = "source")
private Integer source;
}
2. Converter转换器配置
/**
* 称呼的转换器
*
* Excel中的 “先生” ----> Java类中是 18
* Excel中的 “女士” ----> Java类中是 41
*/
public class AppellationConverter implements Converter<Integer> {
/**
* 把Excel中的数据转换为Java中的数据
* 也就是Excel中的 “先生” ----> Java类中是 18
*
* @param cellData
* @param contentProperty
* @param globalConfiguration
* @return
* @throws Exception
*/
@Override
public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
//cellData是Excel中读取到的数据,是“先生”、“女士”
String cellAppellationName = cellData.getStringValue();
List<TDicValue> tDicValueList = (List<TDicValue>) Main.cacheMap.get(DicEnum.APPELLATION.getCode());
for (TDicValue tDicValue : tDicValueList) {
Integer id = tDicValue.getId();
String name = tDicValue.getType_value();
if (cellAppellationName.equals(name)) {
return id;
}
}
return -1;
}
}
/**
* 意向产品的转换器
*
* Excel中的比亚迪e2 --> java中的 2
* 秦PLUS EV --> 7
*/
public class IntentionProductConverter implements Converter<Integer> {
/**
* 把Excel中的数据转换为Java中的数据
* 也就是Excel中的 “比亚迪e2” ----> Java类中是 2
*
* @param cellData
* @param contentProperty
* @param globalConfiguration
* @return
* @throws Exception
*/
@Override
public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
//cellData是Excel中读取到的数据,是“比亚迪e2”、“秦PLUS EV”
String cellIntentionProductName = cellData.getStringValue();
List<TProduct> tDicValueList = (List<TProduct>) Main.cacheMap.get(DicEnum.PRODUCT.getCode());
for (TProduct tProduct : tDicValueList) {
Integer id = tProduct.getId();
String name = tProduct.getName();
if (cellIntentionProductName.equals(name)) {
return id;
}
}
return -1;
}
}
/**
* 意向状态转换器
*/
public class IntentionStateConverter implements Converter<Integer> {
/**
* 把Excel中的数据转换为Java中的数据
* 也就是Excel中的 “意向不明” ----> Java类中是 48
*
* @param cellData
* @param contentProperty
* @param globalConfiguration
* @return
* @throws Exception
*/
@Override
public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
//cellData是Excel中读取到的数据,是“意向不明”、“有意向”
String cellIntentionStateName = cellData.getStringValue();
List<TDicValue> tDicValueList = (List<TDicValue>) Main.cacheMap.get(DicEnum.INTENTIONSTATE.getCode());
for (TDicValue tDicValue : tDicValueList) {
Integer id = tDicValue.getId();
String name = tDicValue.getType_value();
if (cellIntentionStateName.equals(name)) {
return id;
}
}
return -1;
}
}
/**
* 是否需要贷款的转换器
*
*/
public class NeedLoanConverter implements Converter<Integer> {
/**
* 把Excel中的数据转换为Java中的数据
* 也就是Excel中的 “需要” ----> Java类中是 49
*
* @param cellData
* @param contentProperty
* @param globalConfiguration
* @return
* @throws Exception
*/
@Override
public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
//cellData是Excel中读取到的数据,是“需要”、“不需要”
String cellNeedLoanName = cellData.getStringValue();
List<TDicValue> tDicValueList = (List<TDicValue>) Main.cacheMap.get(DicEnum.NEEDLOAN.getCode());
for (TDicValue tDicValue : tDicValueList) {
Integer id = tDicValue.getId();
String name = tDicValue.getType_value();
if (cellNeedLoanName.equals(name)) {
return id;
}
}
return -1;
}
}
/**
* 线索来源的转换器
*
* Excel中的 “车展会” ----> Java类中是 3
* Excel中的 “网络广告” ----> Java类中是 16
*/
public class SourceConverter implements Converter<Integer> {
/**
* 把Excel中的数据转换为Java中的数据
* 也就是Excel中的 “车展会” ----> Java类中是 3
*
* @param cellData
* @param contentProperty
* @param globalConfiguration
* @return
* @throws Exception
*/
@Override
public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
//cellData是Excel中读取到的数据,是“车展会”、“网络广告”
String cellSourceName = cellData.getStringValue();
List<TDicValue> tDicValueList = (List<TDicValue>) Main.cacheMap.get(DicEnum.SOURCE.getCode());
for (TDicValue tDicValue : tDicValueList) {
Integer id = tDicValue.getId();
String name = tDicValue.getType_value();
if (cellSourceName.equals(name)) {
return id;
}
}
return -1;
}
}
/**
* 线索状态转换器
*
*/
public class StateConverter implements Converter<Integer> {
/**
* 把Excel中的数据转换为Java中的数据
* 也就是Excel中的 “已联系” ----> Java类中是 27
*
* @param cellData
* @param contentProperty
* @param globalConfiguration
* @return
* @throws Exception
*/
@Override
public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
//cellData是Excel中读取到的数据,是“已联系”、“未联系”
String cellStateName = cellData.getStringValue();
List<TDicValue> tDicValueList = (List<TDicValue>) Main.cacheMap.get(DicEnum.STATE.getCode());
for (TDicValue tDicValue : tDicValueList) {
Integer id = tDicValue.getId();
String name = tDicValue.getType_value();
if (cellStateName.equals(name)) {
return id;
}
}
return -1;
}
}
(二) 前端
<!--活动备注记录的弹窗-->
<el-dialog v-model="importExcelDialogVisible" title="导入线索Excel" width="55%" center draggable>
<el-upload
ref="uploadRef"
method="post"
:http-request="uploadFile"
:auto-upload="false">
<template #trigger>
<el-button type="primary">选择Excel文件</el-button>
</template>
仅支持后缀名为.xls或.xlsx的文件
<template #tip>
<div class="fileTip">
重要提示:
<ul>
<li>上传仅支持后缀名为.xls或.xlsx的文件;</li>
<li>给定Excel文件的第一行将视为字段名;</li>
<li>请确认您的文件大小不超过50MB;</li>
<li>日期值以文本形式保存,必须符合yyyy-MM-dd格式;</li>
<li>日期时间以文本形式保存,必须符合yyyy-MM-dd HH:mm:ss的格式;</li>
</ul>
</div>
</template>
</el-upload>
<template #footer>
<span class="dialog-footer">
<el-button @click="importExcelDialogVisible = false">关 闭</el-button>
<el-button type="primary" @click="submitExcel">导 入</el-button>
</span>
</template>
</el-dialog>
//提交
const submitExcel = () => {
uploadRef.value.submit() //submit执行:http-request绑定的uploadFile方法
}
const uploadFile = (param) => {
console.log(param);
let fileObj = param.file // 相当于input里取得的files
let formData = new FormData() // new一个FormData对象
formData.append('file', fileObj)//文件对象,前面file是参数名,后面fileObj是参数值
doPost("/api/importExcel", formData).then(resp => {
console.log(resp);
if (resp.data.code === 200) {
//Excel导入成功,提示一下
messageTip("导入成功", "success");
//清理一下上传的文件
uploadRef.value.clearFiles();
//页面刷新
reload();
} else {
//Excel导入失败
messageTip("导入失败", "error");
}
})
}
(三) 后端api接口实现
//file的名字要和前端formData里面的名字相同,否则接收不到
@PostMapping(value = "/api/importExcel")
public R importExcel(MultipartFile file,
@RequestHeader(value = "Authorization") String token)
throws IOException {
tClueService.importExcel(file.getInputStream(), token);
return R.OK();
}
@Override
public void importExcel(InputStream inputStream, String token) {
//链式编程,3个参数, 第一个参数是要读取的Excel文件
// 第二个参数是Excel模板类
// 第三个参数是文件读取的监听器
EasyExcel.read(inputStream,
TClue.class,
new UploadDataListener(tClueMapper, token))
.sheet()
.doRead();
}
(四) 监听器UploadDataListener的实现
@Slf4j
public class UploadDataListener implements ReadListener<TClue> {
/**
* 每隔100条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;
//缓存List
private List<TClue> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
/**
* 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
*/
private TClueMapper tClueMapper;
private String token;
/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*
* @param tClueMapper
*/
public UploadDataListener(TClueMapper tClueMapper, String token) {
this.tClueMapper = tClueMapper;
this.token = token;
}
/**
* 这个每一条数据解析都会来调用
*
* @param tClue one row value. It is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void invoke(TClue tClue, AnalysisContext context) {
log.info("读取到的每一条数据:{}", JSONUtils.toJSON(tClue));
//给读到的clue对象设置创建时间(导入时间)和创建人(导入人)
tClue.setCreate_time(new Date());
tClue.setCreate_by(JWTUtils.parseUserFromJWT(token).getId());
//每读取一行,就把该数据放入到一个缓存List中
cachedDataList.add(tClue);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (cachedDataList.size() >= BATCH_COUNT) {
//把缓存list中的数据写入到数据库
saveData();
//存储完成清空list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
log.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", cachedDataList.size());
tClueMapper.saveClue(cachedDataList);
log.info("存储数据库成功!");
}
}
(五) 保存至数据库功能实现
<!--void saveClue(List<TClue> cachedDataList);-->
<insert id="saveClue" keyColumn="id" keyProperty="id" parameterType="com.peng.model.TClue" useGeneratedKeys="true">
insert into t_clue (owner_id, activity_id, full_name,
appellation, phone, weixin,
qq, email, age, job,
year_income, address, need_loan,
intention_state, intention_product, `state`,
`source`, description, next_contact_time,
create_time, create_by, edit_time,
edit_by)
values
<foreach collection="cachedDataList" item="tClue" separator="," >
(#{tClue.owner_id,jdbcType=INTEGER}, #{tClue.activity_id,jdbcType=INTEGER}, #{tClue.full_name,jdbcType=VARCHAR},
#{tClue.appellation,jdbcType=INTEGER}, #{tClue.phone,jdbcType=VARCHAR}, #{tClue.weixin,jdbcType=VARCHAR},
#{tClue.qq,jdbcType=VARCHAR}, #{tClue.email,jdbcType=VARCHAR}, #{tClue.age,jdbcType=INTEGER}, #{tClue.job,jdbcType=VARCHAR},
#{tClue.year_income,jdbcType=DECIMAL}, #{tClue.address,jdbcType=VARCHAR}, #{tClue.need_loan,jdbcType=INTEGER},
#{tClue.intention_state,jdbcType=INTEGER}, #{tClue.intention_product,jdbcType=INTEGER}, #{tClue.state,jdbcType=INTEGER},
#{tClue.source,jdbcType=INTEGER}, #{tClue.description,jdbcType=VARCHAR}, #{tClue.next_contact_time,jdbcType=TIMESTAMP},
#{tClue.create_time,jdbcType=TIMESTAMP}, #{tClue.create_by,jdbcType=INTEGER}, #{tClue.edit_time,jdbcType=TIMESTAMP},
#{tClue.edit_by,jdbcType=INTEGER})
</foreach>
</insert>
解释<insert id="saveClue" keyColumn="id" keyProperty="id"
parameterType="com.peng.model.TClue" useGeneratedKeys="true">
keyColumn:指定数据库中自增主键 keyProperty:指定java对象中的主键属性名
useGeneratedKeys:启动数据库的主键自增
解释<foreach collection="cachedDataList" item="tClue" separator="," >
以tClue为对象遍历参数cachedDataList,values中的参数以 “,” 进行分隔
十、使用EasyExcel进行Excel文件导入
(一) 前端
//导出操作 ids 是customer id序号列表
const exportExcel = (ids) => {
let token = getToken();
//1、向后端发送一个请求 (我们来实现)
let iframe = document.createElement("iframe")
if (ids) {
iframe.src = instance.defaults.baseURL + "/api/exportExcel?Authorization="+token + "&ids="+ ids;
} else {
iframe.src = instance.defaults.baseURL + "/api/exportExcel?Authorization="+token
}
iframe.style.display = "none"; //iframe隐藏,页面上不要显示出来
document.body.appendChild(iframe);
//2、后端查询数据库的数据,把数据写入Excel,然后把Excel以IO流的方式输出到前端浏览器(我们来实现)
//3、前端浏览器弹出一个下载框进行文件下载(浏览器本身实现的,不需要我们去实现)
}
(二) 后端验证请求是否正确
String token = null;
if (request.getRequestURI().equals(Constants.EXPORT_EXCEL_URI)) {
//从请求路径的参数中获取token
token = request.getParameter("Authorization");
} else {
//其他请求都是从请求头中获取token
token = request.getHeader("Authorization");
}
(三) 后端api实现
@GetMapping(value = "/api/exportExcel")
public void exportExcel(HttpServletResponse response, @RequestParam(value = "ids", required = false) String ids) throws IOException {
//要想让浏览器弹出下载框,你后端要设置一下响应头信息
response.setContentType("application/octet-stream");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(Constants.EXCEL_FILE_NAME+System.currentTimeMillis(), StandardCharsets.UTF_8) + ".xlsx");
//2、后端查询数据库的数据,把数据写入Excel,然后把Excel以IO流的方式输出到前端浏览器(我们来实现)
List<Integer> idList = StringUtils.hasText(ids)
? Arrays.stream(ids.split(",")).map(Integer::parseInt).collect(Collectors.toList())
: new ArrayList<>();
List<CustomerExcel> dataList = tCustomerService.getCustomerByExcel(idList);
EasyExcel.write(response.getOutputStream(), CustomerExcel.class)
.sheet()
.doWrite(dataList);
}
(四) 构建CustomerExcel实体类表明Excel表头
@Data
public class CustomerExcel {
/**
* Excel表头的字段如下:
*
* 所属人 所属活动 客户姓名 客户称呼 客户手机 客户微信 客户QQ
* 客户邮箱 客户年龄 客户职业 客户年收入
* 客户住址 是否贷款 客户产品 客户来源 客户描述 下次联系时间
*/
@ExcelProperty(value = "所属人")
private String owner_name;
@ExcelProperty(value = "所属活动")
private String activity_name;
@ExcelProperty(value = "客户姓名")
private String full_name;
@ExcelProperty(value = "客户称呼")
private String appellation_name;
@ExcelProperty(value = "客户手机")
private String phone;
@ExcelProperty(value = "客户微信")
private String weixin;
@ExcelProperty(value = "客户QQ")
private String qq;
@ExcelProperty(value = "客户邮箱")
private String email;
@ExcelProperty(value = "客户年龄")
private int age;
@ExcelProperty(value = "客户职业")
private String job;
@ExcelProperty(value = "客户年收入")
private BigDecimal year_income;
@ExcelProperty(value = "客户住址")
private String address;
@ExcelProperty(value = "是否贷款")
private String need_load_name;
@ExcelProperty(value = "客户产品")
private String product_name;
@ExcelProperty(value = "客户来源")
private String source_name;
@ExcelProperty(value = "客户描述")
private String description;
@ExcelProperty(value = "下次联系时间")
private Date next_contact_time;
}
(五) Service方法实现
@Override
public List<CustomerExcel> getCustomerByExcel(List<Integer> idList) {
List<CustomerExcel> customerExcelList = new ArrayList<>();
List<TCustomer> tCustomerList = tCustomerMapper.selectCustomerByExcel(idList);
//把从数据库查询出来的List<TCustomer>数据,转换为 List<CustomerExcel>数据
tCustomerList.forEach(tCustomer -> {
CustomerExcel customerExcel = new CustomerExcel();
//需要一个一个设置,没有办法,因为没法使用BeanUtils复制
customerExcel.setOwner_name(ObjectUtils.isEmpty(tCustomer.getOwnerDO()) ? Constants.EMPTY : tCustomer.getOwnerDO().getName());
customerExcel.setActivity_name(ObjectUtils.isEmpty(tCustomer.getActivityDO()) ? Constants.EMPTY : tCustomer.getActivityDO().getName());
customerExcel.setFull_name(tCustomer.getClueDO().getFull_name());
customerExcel.setAppellation_name(ObjectUtils.isEmpty(tCustomer.getAppellationDO()) ? Constants.EMPTY : tCustomer.getAppellationDO().getType_value());
customerExcel.setPhone(tCustomer.getClueDO().getPhone());
customerExcel.setWeixin(tCustomer.getClueDO().getWeixin());
customerExcel.setQq(tCustomer.getClueDO().getQq());
customerExcel.setEmail(tCustomer.getClueDO().getEmail());
customerExcel.setAge(tCustomer.getClueDO().getAge());
customerExcel.setJob(tCustomer.getClueDO().getJob());
customerExcel.setYear_income(tCustomer.getClueDO().getYear_income());
customerExcel.setAddress(tCustomer.getClueDO().getAddress());
customerExcel.setNeed_load_name(ObjectUtils.isEmpty(tCustomer.getNeedLoanDO()) ? Constants.EMPTY : tCustomer.getNeedLoanDO().getType_value());
customerExcel.setProduct_name(ObjectUtils.isEmpty(tCustomer.getIntentionProductDO()) ? Constants.EMPTY : tCustomer.getIntentionProductDO().getName());
customerExcel.setSource_name(ObjectUtils.isEmpty(tCustomer.getSourceDO()) ? Constants.EMPTY : tCustomer.getSourceDO().getType_value());
customerExcel.setDescription(tCustomer.getDescription());
customerExcel.setNext_contact_time(tCustomer.getNext_contact_time());
customerExcelList.add(customerExcel);
});
return customerExcelList;
}
(六) Mapper接口实现
<!-- List<TCustomer> selectCustomerByExcel(@Param(value="idList")List<Integer> idList);-->
<select id="selectCustomerByExcel" parameterType="java.util.List" resultMap="CustomerResultMap">
select
tct.*,
tc.id clueId, tc.full_name, tc.phone, tc.weixin, tc.qq, tc.email, tc.age, tc.job, tc.year_income, tc.address,
tu1.id ownerId, tu1.name ownerName,
ta.id activityId, ta.name activityName,
tdv.id appellationId, tdv.type_value appellationName,
tdv2.id needLoanId, tdv2.type_value needLoanName,
tdv3.id intentionStateId, tdv3.type_value intentionStateName,
tdv4.id stateId, tdv4.type_value stateName,
tdv5.id sourceId, tdv5.type_value sourceName,
tp.id intentionProductId, tp.name intentionProductName
from t_customer tct left join t_clue tc on tct.clue_id = tc.id
left join t_user tu1 on tc.owner_id = tu1.id
left join t_activity ta on tc.activity_id = ta.id
left join t_dic_value tdv on tc.appellation = tdv.id
left join t_dic_value tdv2 on tc.need_loan = tdv2.id
left join t_dic_value tdv3 on tc.intention_state = tdv3.id
left join t_dic_value tdv4 on tc.state = tdv4.id
left join t_dic_value tdv5 on tc.source = tdv5.id
left join t_product tp on tct.product = tp.id
<where>
<if test="idList != null and idList.size() > 0">
and tct.id in
<foreach collection="idList" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
</where>
</select>
十一、设置权限功能
(一) UserDetails读取菜单权限 | 功能权限
//查询一下当前用户的角色
List<TRole> tRoleList = tRoleMapper.selectByUserId(tUser.getId());
//字符串的角色列表
List<String> stringRoleList = new ArrayList<>();
tRoleList.forEach(tRole -> {
stringRoleList.add(tRole.getRole());
});
tUserDetailsImpl.setRoleList(stringRoleList); //设置用户的角色
//查询一下该用户有哪些菜单权限
List<TPermission> menuPermissionList = tPermissionMapper.selectMenuPermissionByUserId(tUserDetailsImpl.getId());
tUserDetailsImpl.setMenuPermissionList(menuPermissionList);
//查询一下该用户有哪些功能权限
List<TPermission> buttonPermissionList = tPermissionMapper.selectButtonPermissionByUserId(tUserDetailsImpl.getId());
List<String> stringPermissionList = new ArrayList<>();
buttonPermissionList.forEach(tPermission -> {
stringPermissionList.add(tPermission.getCode());//权限标识符
});
tUserDetailsImpl.setPermissionList(stringPermissionList);//设置用户的权限标识符
(二) 实体类TUserDetailsImpl导入权限
//角色List
private List<String> roleList;
//权限标识符List
private List<String> permissionList;
//菜单的List
private List<TPermission> menuPermissionList;
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list = new ArrayList<>();
//角色
if (!ObjectUtils.isEmpty(this.getRoleList())) {
this.getRoleList().forEach(role -> {
list.add(new SimpleGrantedAuthority(role));
});
}
if (!ObjectUtils.isEmpty(this.getPermissionList())) {
//权限标识符
this.getPermissionList().forEach(permission -> {
list.add(new SimpleGrantedAuthority(permission));
});
}
return list;
}
(三) 前端以菜单权限进行展示
TUserDetailsImpl tUserDetailsimpl = (TUserDetailsImpl) authentication.getPrincipal();
user.value = 后端返回的(TUserDetailsImpl)
<el-sub-menu :index="index" v-for="(menuPermission, index) in user.menuPermissionList" :key="menuPermission.id">
<template #title>
<el-icon><component :is="menuPermission.icon"></component></el-icon>
<span> {{menuPermission.name}} </span>
</template>
<el-menu-item v-for="subPermission in menuPermission.subPermissionList" :key="subPermission.id" :index="subPermission.url">
<el-icon><component :is="subPermission.icon"></component></el-icon>
{{subPermission.name}}
</el-menu-item>
</el-sub-menu>
(四) 为后端api方法设置 权限限制
//删除用户
@PreAuthorize(value = "hasAuthority('user:delete')")
@Transactional(rollbackFor = Exception.class)
@DeleteMapping("api/user/{id}")
public R deleteUser(@PathVariable(value = "id") Integer id) {
Boolean delete = tUserService.removeById(id);
return delete ? R.OK() : R.FAIL();
}
十二、利用ECharts(前端图表库)建图
官网:echarts.apache.org/zh/index.ht…
import * as echarts from 'echarts';
(一) 后端将需要的值都返回值前端
(二) 前端利用ECharts建表
1. 为图表提供 id 以及 大小设定
<!-- 销售漏斗图,为 ECharts 准备一个定义了宽高的 DOM -->
<div id="saleFunnelChart" style="width: 48%; height:350px; margin:10px; float: left;">
图渲染在此处
</div>
<!-- 线索来源饼图,为 ECharts 准备一个定义了宽高的 DOM -->
<div id="sourcePieChart" style="width: 48%; height:350px; margin:10px; float: left;">
图渲染在此处
</div>
2. 构建销售漏斗图
const loadSaleFunnelChart = () => {
doGet("/api/saleFunnel/data",{}).then((resp)=>{
console.log(resp);
if(resp.data.code === 200){
let saleFunelData = resp.data.data;
//1、拿到页面上渲染图表的dom元素
var chartDom = document.getElementById('saleFunnelChart');
//2、使用echarts组件对dom进行初始化,得到一个空白的图表对象
var myChart = echarts.init(chartDom);
//3、配置可选项(查看文档-->配置项手册)
var option = {
//标题组件,包含主标题和副标题。
title: {
//主标题文本,支持使用 \n 换行。
text: '销售漏斗图',
//title 组件离容器左侧的距离。
left: 'center',
//title 组件离容器上侧的距离。
top: 'bottom'
},
//提示框组件。
tooltip: {
//触发类型。item:数据项图形触发,主要在散点图,饼图等无类目轴的图表中使用。
trigger: 'item',
//提示框浮层内容格式器,漏斗图: {a}(系列名称),{b}(数据项名称),{c}(数值), {d}(百分比)
formatter: '{a} <br/>{b} : {c}'
},
//工具栏
toolbox: {
//各工具配置项
feature: {
//数据视图工具,可以展现当前图表所用的数据,编辑后可以动态更新。
dataView: {
//是否不可编辑(只读)。
readOnly: false
},
//配置项还原。
restore: {},
//保存为图片。
saveAsImage: {}
}
},
//图例组件
legend: {
data: ['线索', '客户', '交易', '成交']
},
//系列
series: [
{
//系列名称
name: '销售漏斗数据统计',
//图表的类型,funnel代表漏斗图
type: 'funnel',
//漏斗图组件离容器左侧的距离。
left: '10%',
//漏斗图组件离容器上侧的距离。
top: 60,
//漏斗图组件离容器下侧的距离。
bottom: 60,
width: '80%',
min: 0,
max: 100,
minSize: '0%',
maxSize: '100%',
sort: 'descending',
gap: 2,
label: {
show: true,
position: 'inside'
},
labelLine: {
length: 10,
lineStyle: {
width: 1,
type: 'solid'
}
},
itemStyle: {
borderColor: '#fff',
borderWidth: 1
},
emphasis: {
label: {
fontSize: 50
}
},
//数据项(系列中的数据内容数组, 数组项可以为单个数值,也可以是对象值)
/*data: [
{ value: 20, name: '成交' },
{ value: 60, name: '交易' },
{ value: 80, name: '客户' },
{ value: 100, name: '线索' }
]*/
data : saleFunelData
}
]
};
//4、如果配置了可选项,就把可选项设置到空白的图表对象中
option && myChart.setOption(option);
}
})
}
3. 加载线索来源饼图
const loadSourcePieChart = () => {
doGet("/api/sourcePie/data",{}).then((resp)=>{
console.log(resp);
if(resp.data.code === 200){
let sourcePieData = resp.data.data;
//1、拿到页面上渲染图表的dom元素
var chartDom = document.getElementById('sourcePieChart');
//2、使用echarts组件对dom进行初始化,得到一个空白的图表对象
var myChart = echarts.init(chartDom);
//3、配置可选项(查看文档-->配置项手册)
var option = {
title: {
text: '线索来源统计',
//title 组件离容器左侧的距离。
left: 'center',
//title 组件离容器上侧的距离。
top: 'bottom'
},
tooltip: {
trigger: 'item'
},
//图例
legend: {
orient: 'horizontal',
left: 'center'
},
//系列
series: [
{
name: '线索来源统计',
type: 'pie',
//饼图的半径
radius: '80%',
//数据项
/*data: [
{ value: 1048, name: 'Search Engine' },
{ value: 735, name: 'Direct' },
{ value: 580, name: 'Email' },
{ value: 484, name: 'Union Ads' },
{ value: 300, name: 'Video Ads' }
],*/
data : sourcePieData,
//高亮状态的扇区和标签样式。
emphasis: {
itemStyle: {
//图形阴影的模糊大小
shadowBlur: 10,
//阴影水平方向上的偏移距离。
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
//4、如果配置了可选项,就把可选项设置到空白的图表对象中
option && myChart.setOption(option);
}
})
十三、使用Minio完成头像功能
(一) 用户提交头像
//由于该项目是 spring security 进行保护的
//所有由 element-plus 组件构建的url接口需要定义好请求头才能被后端接收
el-upload
ref="uploadImageRef"
class="avatar-uploader"
:action="instance.getUri()+'/api/user/image?id='+id"
// 定义好请求头
:headers="uploadHeaders"
:show-file-list="true"
:auto-upload="false"
name = "file"
>
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
const submitUpload = () => {
if(id == loadUser.value.id) {
uploadImageRef.value.submit();
} else {
messageTip("当前登录者只能为自己提交头像","error");
}
}
(二) 用户获取头像
//显示头像
<el-avatar :src="imageUrl" />
//获取用户头像
const loadLoginImage = () => {
doGet("/api/userImage",{}).then((resp)=>{
console.log(resp);
if(resp.data.code === 200){
imageUrl.value = resp.data.data;
}
})
}
(三) 后端实现
//为用户添加头像
@PostMapping(value="/api/user/image")
public R image(@RequestParam(value = "file") MultipartFile file, @RequestParam(value="id")Integer id ,@RequestHeader(value = "Authorization") String token) throws Exception{
Integer user_id = JWTUtils.parseUserFromJWT(token).getId();
minioClient.putObject(PutObjectArgs.builder()
.bucket(Constants.MINIO_BUCKET_NAME)
.object("dlyk_image_user_id=" + user_id + ".jpg")
.stream(file.getInputStream(),
file.getSize(),
-1)
.build());
TMinio tMinio = new TMinio();
tMinio.setUser_id(user_id);
tMinio.setBucket(Constants.MINIO_BUCKET_NAME);
tMinio.setObject("dlyk_image_user_id=" + user_id + ".jpg");
tMinioMapper.insert(tMinio);
return R.OK();
}
//获得当前用户头像
@GetMapping(value="/api/userImage")
public R userImage(Authentication authentication) throws Exception {
TUserDetailsImpl tUserDetailsImpl = (TUserDetailsImpl)authentication.getPrincipal();
LambdaQueryWrapper<TMinio> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TMinio::getUser_id,tUserDetailsImpl.getId());
TMinio userMinio = tMinioMapper.selectOne(queryWrapper);
String userBucket;
String userObject;
if(!Objects.isNull(userMinio)){
userBucket = userMinio.getBucket();
userObject = userMinio.getObject();
} else {
userBucket = Constants.MINIO_BUCKET_NAME;
userObject = "baseImage.jpg";
}
String url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(userBucket)
.object(userObject)
.build());
System.out.println(url);
return R.OK(url);
}
十四、通过Nginx部署
(一) 注意事项
当后端也部署到 linux 时,注意 前端 httpRequest 请求的URL记得更改
(二) 前端
1. 打包前端项目 dist 为打包后的文件
npm run build
2. 将 dist放到 /usr/local/nginx/conf/html 目录下 并更改为 dlyk 项目名
3. 配置nginx.conf文件 启动静态文件
server {
listen 80;
server_name localhost;
location / {
//加载 html 下的 dlyk 文件
root html/dlyk;
try_files $uri $uri/ /index.html; //这一行不配置可能会报错
index index.html index.htm;
}
}