当前位置: 首页 > news >正文

前后端分离的后台管理系统开发模板(带你从零开发一套自己的若依框架)上

前言:

目前,前后端分离开发已经成为当前web开发的主流。目前最流行的技术选型是前端vue3+后端的spring boot3,本次。就基于这两个市面上主流的框架来开发出一套基本的后台管理系统的模板,以便于我们今后的开发。

前端使用vue3+element-plus开发。后端使用spring boot3+spring security作为项目的支撑,使用MySQL8.0.30作数据存储,使用redis作缓存,使用minio作为项目的存储机构。

后台管理系统是比较注重权限的,本项目使用市面上最流行的RBAC模型。建立用户、角色、权限和它们两两之间的对映关系一共五张表,日志管理两张表,一张记录用户的行为、一张记录用户的操作。

搭建基础的环境:

后端:

创建一个spring boot项目,并导入一些基础的maven依赖:

 <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-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId><version>4.3.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.21</version></dependency><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.2</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.4</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.22</version></dependency><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>4.3.0</version></dependency><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.4.7</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.6</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></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></dependencies>

项目使用MybatisPlus进行数据库的操作,使用redis作为缓存。使用minio作为项目的存储机构。

MySQL是我本地的服务,redis和minio服务我放在了Linux服务器上。如果有对minio不熟悉的,可以看一下我之前写过的文章:

springboot整合minio(实现文件的上传和下载超详细入门)_minio下载文件-CSDN博客

根据数据库中的表,先创建出相应的controller、service、mapper和相应的实体类。我直接使用mybatisX插件进行相应数据的生成。一共有七张表,相应的SQL脚本,我会连同前后端的代码一起放在git。

至此。后端项目就暂时搭建出了一个基础的模板,我们接下来开始进行前端项目的部署;

前端:

挑选一个文件夹,运行

 npm create vue@latest

命令来创建一个基础的前端vue3项目,在创建项目时可以进行一些基础配置的选择。我在创建前端项目时,选择的编程语言是js,如果有选择ts的可能需要对数据的类型进行相应的指定。

在创建完前端项目之后,我们可能还需要引入一些相应的包。

npm install element-plus --save
npm install @element-plus/icons-vue
npm install sass
npm install pinia-plugin-persistedstate
npm install axios

在项目的终端运行命令来完成相应的依赖下载。下载完成之后。在package.json文件中相应的依赖如下:

至此,我们的前端vue3项目也已经搭建完成了,接下来,就可以开始我们前后端代码的编写了。

代码编写:

先进行前端代码的编写,一步步向后端靠拢,最终完成我们要实现的功能。

前端代码

在搭建环境时,我们引入了一些包的依赖,我们需要在main.js中进行依赖的声明和引用。


import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' 
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import App from '@/App.vue'
import router from '@/router'
// 1、pinia的持久化插件
import { createPersistedState } from 'pinia-plugin-persistedstate'
// element-plus的图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'const app = createApp(App)
const pinia = createPinia()//2、 接收createPersistedState函数
const piniaPersistedState = createPersistedState()// 3、在pinia中引入持久化插件
pinia.use(piniaPersistedState)// 全局引入图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, component)}app.use(pinia)
app.use(router)
app.use(ElementPlus, {locale: zhCn,
})
app.mount('#app')

删除vue3项目自带的一些vue组件,将整个vue项目恢复成一个纯净的项目。

首先,我们要做的页面是后台管理的登录页面。

在views目录下创建一个Login.vue 页面,这个页面中进行我们后台管理系统的登录操作。

在router路由中进行我们登录页面的配置,要求在运行项目时,首先跳转的就是登录页面(这也符合我们项目的预期,后台管理类的所有项目一定是要先登录,接下来才能进行功能的操作)

import { createRouter, createWebHistory } from 'vue-router'
import  useToeknStore from '@/stores/useToken'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [// 系统用户登录{path: '/',name: 'login',component: () => import('@/views/Login.vue')}]})// 前置守卫
// 全局拦截、除了登录页面,其他页面都必须授权(这里为pinia定义的token不为空)才能访问
router.beforeEach((to, from) => {const useToken=useToeknStore()if (to.name !== 'login' && !useToken.token) {//  alert("没有登录,自动跳转到登录页面")return { name: 'login' }}else{return true}})export default router

在这个路由文件中,不仅定义了登录页面,同时引入了一个路由前置守卫。这个守卫的功能是,如果没有登录,那么就只能访问登录页面。

(判断有没有登录的标识,就是pinia中token有没有值。如果登录成功,那么就会在pipin中部存入token的值,如果退出登录。那么前端也会删除token的值。借此,我们就可以判断出用户有没有登录了。我们知道,存入pipin中的值,其实是存储到了我们浏览器的localstore中,对于稍微懂点前端的人来说,都是很容易获取和改变的。在真实的项目中,肯定是不会使用这么简单的方法的。这个项目相当于我们个人开发的一个简单项目,所以就无所谓了。)

接下来,我们对axios进行一下封装,这样,每次发送请求到后端时就可以大大简化了。

创建util目录,在这个目录下新建一个request.js文件,在这个文件中封装我们axios。

import axios from "axios";
import  useTokenStore from '@/stores/useToken'
import { ElMessage } from 'element-plus';
// 先建一个api,系统
const api = axios.create({baseURL: "http://localhost:8888",timeout: 5000});// 发送请求前拦截
api.interceptors.request.use(config =>{
const useToken = useTokenStore();
// 系统用户的请求头
if(useToken.token){console.log("请求头toekn=====>", useToken.token);// 设置请求头// config.headers['token'] = useToken.token;config.headers.token = useToken.token;
}return config;},
error =>{return Promise.reject(error);
}
)// 响应前拦截
api.interceptors.response.use(response =>{console.log("响应数据====>", response);
if(response.data.code ==200){
return response.data;
}// 响应失败
if(response.data.code !=200){
console.log("响应失败====>", response.data);}return response.data;
},
error =>{return Promise.reject(error);
}
)export  {api}

现在,我们就可以正式的编写我们后台管理系统的登录页面了。

(需要注意的是,我们的系统是一个后台管理类的系统,所以在首页不能让所有用户自行注册,在首页就写一个登录按钮和一个忘记密码的按钮。用户的添加需要有权限的用户在后台管理系统的功能区进行添加才行)

登录页面的内容如下:

<template><!-- style="font-family:kaiti" --><div class="background"  ><!-- 注册表单,一个对话框 -->
<el-dialog v-model="isRegister"   title="用户注册"   width="30%"  draggable=true><el-form label-width="120px" v-model="registerForm"><el-form-item label="用户名"><el-input type="text"   v-model="registerForm.username"   ><template #prefix><el-icon><Avatar /></el-icon></template></el-input></el-form-item><el-form-item label="密码"><el-input  type="password" v-model="registerForm.password" ><template #prefix><el-icon><Lock /></el-icon></template></el-input></el-form-item><el-form-item><el-button type="primary" @click="registerAdd" >提交</el-button><el-button @click="closeRegister">取消</el-button></el-form-item></el-form>
</el-dialog><!-- 登陆框 -->
<div class="login-box">
<el-formlabel-width="100px":model="loginFrom"style="max-width: 460px":rules="Loginrules"ref="ruleFormRef"><div style=" text-align: center;font-weight: bold;">后台管理系统模板</div> <el-form-item label="用户名"  prop="username"><el-input v-model="loginFrom.username"  clearable  ><template #prefix><el-icon><Avatar /></el-icon></template></el-input></el-form-item><el-form-item label="密码" prop="password"><el-input v-model="loginFrom.password"   show-password   clearable  type="password" ><template #prefix><el-icon><Lock /></el-icon></template></el-input></el-form-item><el-form-item label="验证码"  prop="codeValue"><el-input v-model="loginFrom.codeValue"  style="width: 100px;"  clearable  ></el-input><img :src="codeImage" @click="getCode" style="transform: scale(0.9);"/></el-form-item><!-- <el-checkbox v-model="rememberMe.rememberMe"   >记住我</el-checkbox>  --><!-- 跳转一个新页面 --><el-link type="primary"  style="transform: translateX(330px)"  @click="resetPassword()">忘记密码</el-link><br>
<el-button type="success" @click="getLogin(ruleFormRef)" size="small"   class="my-button">登录</el-button><!-- <el-button type="primary" @click="isRegister=true"   class="my-button">注册</el-button> --></el-form><!-- 按下enter键提交登录请求 --><!-- <input @keyup.enter="getLogin(ruleFormRef)"> --></div></div>
</template><script  setup>
import { ref,onMounted,reactive, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import useTokenStore  from '@/stores/useToken'
import {  getCodeApi ,loginApi,registerApi} from '@/api/SysLogin';// <!-- 按下enter键提交登录请求 -->const keyDownHandler = (event) => {if (event.key === 'Enter') {// 执行你想要的操作console.log('Enter键被按下了');getLogin(ruleFormRef.value)}}onMounted(() => {window.addEventListener('keydown', keyDownHandler);});onUnmounted(() => {window.removeEventListener('keydown', keyDownHandler);});// 重置密码
const resetPassword=()=>{router.push({name:'resetPassword',query:{type:"sys"
}})}const ruleFormRef =  ref()// const rememberMe=rememberMeStore()const loginFrom=ref({
username:'',
password:'',
codeKey:'',
codeValue:''
// rememberMe:rememberMe.rememberMe
})const Loginrules=reactive({username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],password: [{ required: true, message: '请输入密码', trigger: 'blur' },{ min: 6, max: 12, message: '长度在 6 到 12 个字符', trigger: 'blur'}],codeValue: [{ required: true, message: '请输入验证码', trigger: 'blur' }]})const registerForm=ref({username:'',password:''
})const codeImage=ref('')const isRegister=ref(false)const tokenStore = useTokenStore();const router = useRouter()// 登录,提交阶段
const getLogin = async(formEl) => {if (!formEl)  returnawait formEl.validate(async (valid, fields) => {if (valid) {console.log('submit!')let data = await loginApi(loginFrom.value)console.log("几点几分上了飞机",data.code);if(data.code===200){ElMessage('登录成功')console.log(data.data);tokenStore.token=data.datarouter.replace({name:'layout'})}else{ElMessage('登录失败')ElMessage('失败原因:'+data.message)}} else {ElMessage('请输入完整信息')return;}})}const getCode=async()=>{let {data}=await getCodeApi()console.log(data);console.log("验证码的值为============>",data);loginFrom.value.codeKey=data.codeKeycodeImage.value=data.codeValue
}// 注册,提交阶段
const registerAdd=async()=>{let data=await registerApi(registerForm.value)if(data.code==200){ElMessage('注册成功')registerForm.value={username:'',password:''}isRegister.value=false
}else{ElMessage('注册失败')registerForm.value={username:'',password:''}isRegister.value=false}}const closeRegister=()=>{registerForm.value={username:'',password:''}isRegister.value=false
}// 页面加载完成获取验证码onMounted(()=>{getCode()})</script><style lang="scss" scoped>
// 登录页面总样式
.background{background: url("@/assets/20.png");width:100%;height:100%;  /**宽高100%是为了图片铺满屏幕 */position: fixed;background-size: 100% 100%;font-family:kaiti
}.login-box{border:1px solid #dccfcf;width: 350px;margin:180px auto;padding: 20px 50px 20px 50px;border-radius: 5px;-webkit-border-radius: 5px;-moz-border-radius: 5px;box-shadow: 0 0 25px #909399;background-color:rgba(255,255,255,0.7);//这里最后那个0.7就是为了防止背景表单颜色太浅
}.my-button {margin-right: 100px;width: 400px;padding: 10px 20px;font-size: 16px;background-color: #4CAF50;color: white;border: none;border-radius: 5px;cursor: pointer;}</style>

具体的展现效果如图:

挂载组件时,会发送一个请求验证码的接口到后端。后端我会使用hutoll工具包生成验证码。并将验证码放在redis中,并设置过期时间为5分钟。再通过base64编码的形式将验证码的图片传到前端直接显示。

在这个登录页面中,用户需要输入用户名、密码和验证码到后端。后端会先验证验证码的值如果正确,再验证用户名和密码。如果都正确,那么后端会返回登录成功的提示,并根据用户id生成一个token返回前端,前端收到token之后,会将这个token存入pinia中。接下来跳转到后台管理系统的功能页面

登录页面有一个“忘记密码”的按钮,我们通过这个按钮应该能实现用户密码的重置。

(密码重置通过用户邮箱发送验证码来进行验证,也可以使用短信验证码的形式进行验证,都是可以的)

重置密码页面:

<template><div class="resetPassword">
<h1>密码重置页面</h1>
请输入邮箱:<input  v-model="emailNumber" class="inputEmail" type=email >  </input>
<el-button type="primary" @click="sendEmail">重置密码</el-button><br>
请输入验证码:<input type="text" v-model="code" class="inputEmail"/><el-button type="success" @click="sendCode">提交</el-button><div>
<!-- 修改密码表单,一个对话框 -->
<el-dialog v-model="isPassword"   title="用户修改密码"   width="30%"  draggable=true><el-form label-width="120px" v-model="passwordFrom"><el-form-item label="密码"><el-input  type="password" v-model="passwordFrom.password" show-password   clearable   ><template #prefix><el-icon><Lock /></el-icon></template></el-input></el-form-item><el-form-item label="确认密码"><el-input  type="password" v-model="passwordFrom.doPassword" show-password   clearable  ><template #prefix><el-icon><Lock /></el-icon></template></el-input></el-form-item><el-form-item><el-button type="primary" @click="sendPassword" >提交</el-button><el-button @click="closeRegister">取消</el-button></el-form-item></el-form>
</el-dialog>
</div>
</div></template><script  setup>
import {resetPassword,sendSysCode,sendSysPassword} from "@/api/ResetPassword"
import { ref } from "vue";
import { useRouter } from 'vue-router';let emailNumber = ref('')let code = ref('')let isPassword = ref(false)let passwordFrom = ref({password:'',doPassword:'',email:""
})const router = useRouter()const sendEmail = async() => {
console.log(emailNumber.value);const data= await resetPassword(emailNumber.value)console.log(data);}const sendCode = async() => {const data= await sendSysCode(code.value,emailNumber.value)if(data.code==200){alert("验证码正确,请修改密码")isPassword.value=true
}else{alert("验证码错误,请重新输入")
}console.log(data);}const sendPassword = async() => {if(passwordFrom.value.password==passwordFrom.value.doPassword){passwordFrom.value.email=emailNumber.valueconst data= await sendSysPassword(passwordFrom.value)console.log(data);if(data.code==200){alert("密码修改成功")isPassword.value=falserouter.replace({name:"login"})}else{alert("密码修改失败====>"+data.message)isPassword.value=false}}else{alert("两次密码不一致,请重新输入")}}const closeRegister = () => {isPassword.value=false
}</script><style lang="scss" scoped>
.inputEmail{width: 300px;height: 40px;
}.resetPassword{max-width: 400px;margin: 0 auto;padding: 20px;background-color: #f0f2f5;border-radius: 8px;box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);text-align: center;}</style>

在路由中添加相应的路径,并在路由守卫中加入重置页面。因为这个页面是不需要登录就能访问的:

import { createRouter, createWebHistory } from 'vue-router'
import  useToeknStore from '@/stores/useToken'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [// 系统用户登录{path: '/',name: 'login',component: () => import('@/views/Login.vue')},// 重置密码页面{path: '/resetPassword',name: 'resetPassword',component: () => import('@/views/ResetPassword.vue')},// 通配符路由,匹配所有无法识别的路径
{path: '/:pathMatch(.*)*',component: () => import('@/error/NotFount.vue')
}]})// 前置守卫
// 全局拦截、除了登录页面,其他页面都必须授权(这里为pinia定义的token不为空)才能访问
router.beforeEach((to, from) => {const useToken=useToeknStore()if (to.name !== 'login' && to.name!=='resetPassword'    && !useToken.token) {//  alert("没有登录,自动跳转到登录页面")return { name: 'login' }}else{return true}})export default router

再写一个错误路由统一处理的页面,并加入到路由中。

接下来就可以编写后端的代码来实现登录功能了;

后端代码:

由于我们后端使用了spring security作为安全框架。所以在controller层编写登录逻辑之前,我们还需要在后端做一些security的处理。

在yml配置文件中进行一些信息的配置:

spring:data:redis:host: 192.168.231.110port: 6379password: 123456database: 0timeout: 1000datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/zhangqiao-admin?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTCusername: rootpassword: 123456mail:host: smtp.qq.comusername: 2996809820@qq.compassword: jbtjqbhxeaerdfdidefault-encoding: UTF-8servlet:multipart:max-file-size:  10MB   # 单个文件上传的最大上限max-request-size:  100MB  # 整个请求体的最大上限
mybatis-plus:global-config:db-config:id-type: autologic-delete-value: 1logic-not-delete-value: 0logic-delete-field: isDeletedconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpllogging:file:path: D:/logs/zhangqiao-admin/spring-admin# PageHelper 分页插件配置
pagehelper:helper-dialect: mysqlreasonable: truesupport-methods-arguments: trueparams: count=countsql
minio:url: http://192.168.231.110:9001username: adminpassword: admin123456bucketName: zhangqiao-adminexclude:syspaths:- /sys/getCaptcha- /sys/user/login- /sys/resetPassword- /sys/sendSysCode- /sys/sendSysPassword
#    - /sys/user/addUserjwt:
#  expiration: 3600000Lsecret: zhangqiao

创建一个Security的配置类。来编写spring security的一些配置。

@Configuration
@EnableWebSecurity
public class MyServiceConfig {@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Resourceprivate ExcludeSysPath excludeSysPath;/** security的过滤器链* */@Resourceprivate   CustomAccessDeniedHandler customAccessDeniedHandler;@Resourceprivate CustomAuthenticationEntryPoint customAuthenticationEntryPoint;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)throws Exception {
http.csrf(AbstractHttpConfigurer::disable);http.authorizeHttpRequests((auth) ->auth.requestMatchers(excludeSysPath.getSyspaths().toArray(new String[0])).permitAll().anyRequest().authenticated());http.exceptionHandling(e -> e.accessDeniedHandler(customAccessDeniedHandler).authenticationEntryPoint(customAuthenticationEntryPoint));http.cors(cors->{cors.configurationSource(corsConfigurationSource());});
//自定义过滤器放在UsernamePasswordAuthenticationFilter过滤器之前http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);return http.build();
}@Autowired
private MyUserDetailServerImpl myUserDetailsService;/*
* 验证管理器
* */@Beanpublic AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder){DaoAuthenticationProvider provider=new DaoAuthenticationProvider();
//将编写的UserDetailsService注入进来provider.setUserDetailsService(myUserDetailsService);
//将使用的密码编译器加入进来provider.setPasswordEncoder(passwordEncoder);
//将provider放置到AuthenticationManager 中ProviderManager providerManager=new ProviderManager(provider);return providerManager;}//跨域配置@Beanpublic CorsConfigurationSource corsConfigurationSource() {CorsConfiguration configuration = new CorsConfiguration();configuration.setAllowedOrigins(Arrays.asList("*"));configuration.setAllowedMethods(Arrays.asList("*"));configuration.setAllowedHeaders(Arrays.asList("*"));UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", configuration);return source;}/** 密码加密器*/
@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}}

这个配置类中编写了密码加密器、跨域相关、验证管理、定义了一个有关排除路径的bean,注入进配置文件。配置了未登录和权限不足时后端返回的一些具体信息,添加了一个JWT的拦截器,放在了security的登录拦截器之前。我们知道security的实现就是基于拦截器链的形式,在登录拦截器之前加入JWT的token拦截器,这样就能实现我们已经登录过的用户再访问其他资源时能正常访问。并且在 JWT拦截器中实现token的刷新;

定义一个MyTUserDetail,实现UserDetails接口,来当security的用户类:

@Data
public class MyTUserDetail implements Serializable, UserDetails {private static final long serialVersionUID = 1L;private User users;//    角色private List<String> roles;//    权限private  List<String> permissions;@JsonIgnore  //json忽略@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {List<GrantedAuthority> list =  new ArrayList<>();//        如果角色不用空,则将角色添加到list中if (!ObjectUtils.isEmpty(roles)){roles.forEach(role->list.add(new SimpleGrantedAuthority(role)));}//                如果权限不用空,则将权限添加到list中if (!ObjectUtils.isEmpty(permissions)){permissions.forEach(permission->list.add(new SimpleGrantedAuthority(permission)));}return list;}@JsonIgnore@Overridepublic String getPassword() {return this.getUsers().getPassword();}@JsonIgnore@Overridepublic String getUsername() {return this.getUsers().getUsername();}@JsonIgnore@Overridepublic boolean isAccountNonExpired() {return this.getUsers().getStatus()==0;}@JsonIgnore@Overridepublic boolean isAccountNonLocked() {return this.getUsers().getStatus()==0;}@JsonIgnore@Overridepublic boolean isCredentialsNonExpired() {return this.getUsers().getStatus()==0;}@JsonIgnore@Overridepublic boolean isEnabled() {return this.getUsers().getStatus()==0;}
}

定义一个MyUserDetailServerImpl类,实现UserDetailsService接口,在这个类中实现用户名和密码的具体查询:(现在我还没有实现权限的查询,按理说在登录时就应该一块进行的,现在,我只进行用户的登录,查询权限等到之后再开发。)

@Service
@Slf4j
public class MyUserDetailServerImpl implements UserDetailsService {@AutowiredUserMapper userService;@Autowired
private RedisTemplate<String,String> redisTemplate;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userService.selectOne(new LambdaQueryWrapper<User>().eq(StringUtils.hasText(username), User::getUsername, username));if (user == null) {throw new UsernameNotFoundException("用户名不存在");}MyTUserDetail myTUserDetail=new MyTUserDetail();myTUserDetail.setUsers(user);return myTUserDetail;}
}

JWT的token拦截器,获取登录的用户信息和用户拥有的权限,放在SecurityContextHolder。在后面要用到用户信息时可以直接在SecurityContextHolder中得到登陆的用户信息。并同时在JWT拦截器中进行过期时间的刷新。

@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisTemplate<String,String> redisTemplate;@Autowiredprivate JwtUtil jwtUtil;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取请求头中的tokenString token = request.getHeader("token");System.out.println("前端的token信息=======>"+token);//如果token为空直接放行,由于用户信息没有存放在SecurityContextHolder.getContext()中所以后面的过滤器依旧认证失败符合要求if(!StringUtils.hasText(token)){filterChain.doFilter(request,response);return;}//        解析Jwt中的用户idInteger userId = jwtUtil.getUsernameFromToken(token);//从redis中获取用户信息String redisUser = redisTemplate.opsForValue().get("UserLogin:"+ userId);if(!StringUtils.hasText(redisUser)){filterChain.doFilter(request,response);return;}//        刷新tokenString newToken = jwtUtil.refreshToken(token);redisTemplate.opsForValue().set("UserLogin:"+userId,redisUser,60, TimeUnit.MINUTES);//        将redis中的用户信息转换成MyTUserDetail对象MyTUserDetail myTUserDetail= JSON.parseObject(redisUser, MyTUserDetail.class);log.info("Jwt过滤器中MyUserDetail的值============>"+myTUserDetail.toString());//将用户信息存放在SecurityContextHolder.getContext(),后面的过滤器就可以获得用户信息了。这表明当前这个用户是登录过的,后续的拦截器就不用再拦截了UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(myTUserDetail,null,myTUserDetail.getAuthorities());SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);filterChain.doFilter(request,response);}}

现在,我们就可以在controller中实现用户的登录功能了。

用户登录controller:
 

  @PostMapping("/login")public Result<String> login(@RequestBody LoginDto loginDto){String token = userService.login(loginDto);return Result.successData(token);}

相应的service实现:

 @Autowired
private RedisTemplate<String,String> redisTemplate;@AutowiredAuthenticationManager authenticationManager;@Overridepublic String login(LoginDto loginDto) {
//        先检验验证码String codeRedis = redisTemplate.opsForValue().get(loginDto.getCodeKey());if (codeRedis==null){throw new ResultException(555,"验证码不存在");}if (!codeRedis.equals(loginDto.getCodeValue().toLowerCase())) {throw new ResultException(555, "验证码错误");}
//        验证码正确,删除redis中的验证码redisTemplate.delete(loginDto.getCodeKey());log.info("用户登录");
//        用户登录UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDto.getUsername(),loginDto.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);if(authenticate==null){throw new  ResultException(400,"用户名或密码错误");}
//        获取返回的用户信息Object principal = authenticate.getPrincipal();MyTUserDetail myTUserDetail=(MyTUserDetail) principal;System.out.println(myTUserDetail);
//        使用Jwt生成token,并将用户的id传入String token = jwtUtil.generateToken(myTUserDetail.getUsers().getId());redisTemplate.opsForValue().set("UserLogin:"+ myTUserDetail.getUsers().getId(),JSON.toJSONString(myTUserDetail),60, TimeUnit.MINUTES);return token;}

至此,我们就完成了用户登录的全过程。要注意我们需要放行的路径,验证码的生成路径,用户登录路径、重置密码的路径等都需要进行放行。

由于数据库中还没有数据,所以我先在test测试中生成一条数据,再在前端进行登录;

前端登录的结果如下:

至此,我们的登录功能就实行了。在下篇文章中,我会实现其他的功能。

相关文章:

前后端分离的后台管理系统开发模板(带你从零开发一套自己的若依框架)上

前言&#xff1a; 目前&#xff0c;前后端分离开发已经成为当前web开发的主流。目前最流行的技术选型是前端vue3后端的spring boot3&#xff0c;本次。就基于这两个市面上主流的框架来开发出一套基本的后台管理系统的模板&#xff0c;以便于我们今后的开发。 前端使用vue3ele…...

【C++ | 委托构造函数】委托构造函数 详解 及 例子源码

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…...

iCloud邮件全攻略:设置与使用终极指南

标题&#xff1a;iCloud邮件全攻略&#xff1a;设置与使用终极指南 摘要 iCloud邮件是Apple提供的一项邮件服务&#xff0c;允许用户在所有Apple设备上访问自己的邮件。本文将详细介绍如何在各种设备和邮件客户端上设置和使用iCloud邮件账户&#xff0c;确保用户能够充分利用…...

【计算机毕业设计】基于微信小程序的电子购物系统的设计与实现【源码+lw+部署文档】

包含论文源码的压缩包较大&#xff0c;请私信或者加我的绿色小软件获取 免责声明&#xff1a;资料部分来源于合法的互联网渠道收集和整理&#xff0c;部分自己学习积累成果&#xff0c;供大家学习参考与交流。收取的费用仅用于收集和整理资料耗费时间的酬劳。 本人尊重原创作者…...

CSS实现动画

CSS实现动画主要有三种方式&#xff1a;transition&#xff0c;transform&#xff0c;和animation1。以下是一些详细的逻辑&#xff0c;实例和注意事项&#xff1a; Transition&#xff1a;transition可以为一个元素在不同状态之间切换的时候定义不同的过渡效果。例如&#xff…...

Python+Pytest+Allure+Yaml+Jenkins+GitLab接口自动化测试框架详解

PythonPytestAllureYaml接口自动化测试框架详解 编撰人&#xff1a;CesareCheung 更新时间&#xff1a;2024.06.20 一、技术栈 PythonPytestAllureYamlJenkinsGitLab 版本要求&#xff1a;Python3.7.0,Pytest7.4.4,Allure2.18.1,PyYaml6.0 二、环境配置 安装python3.7&…...

[OtterCTF 2018]Bit 4 Bit

我们已经发现这个恶意软件是一个勒索软件。查找攻击者的比特币地址。** 勒索软件总喜欢把勒索标志丢在显眼的地方&#xff0c;所以搜索桌面的记录 volatility.exe -f .\OtterCTF.vmem --profileWin7SP1x64 filescan | Select-String “Desktop” 0x000000007d660500 2 0 -W-r-…...

计算机视觉全系列实战教程 (十四):图像金字塔(高斯金字塔、拉普拉斯金字塔)

1.图像金字塔 (1)下采样 从G0 -> G1、G2、G3 step01&#xff1a;对图像Gi进行高斯核卷积操作&#xff08;高斯滤波&#xff09;step02&#xff1a;删除所有的偶数行和列 void cv::pyrDown(cv::Mat &imSrc, //输入图像cv::Mat &imDst, //下采样后的输出图像cv::Si…...

正确重写equals和hashcode方法

1. 重写的原因 如有个User对象如下&#xff1a; public class User {private String name;private Integer age; }如果不重写equals方法和hashcode方法&#xff0c;则&#xff1a; public static void main(String[] args) {User user1 new User("userA", 30);Us…...

数据质量管理-时效性管理

前情提要 根据GB/T 36344-2018《信息技术 数据质量评价指标》的标准文档&#xff0c;当前数据质量评价指标框架中包含6评价指标&#xff0c;在实际的数据治理过程中&#xff0c;存在一个关联性指标。7个指标中存在4个定性指标&#xff0c;3个定量指标&#xff1b; 定性指标&am…...

python 实例002 - 数据转换

题目&#xff1a; 有一组用例数据如下&#xff1a; cases [[case_id, case_title, url, data, excepted],[1, 用例1, www.baudi.com, 001, ok],[4, 用例4, www.baudi.com, 002, ok],[2, 用例2, www.baudi.com, 002, ok],[3, 用例3, www.baudi.com, 002, ok],[5, 用例5, www.ba…...

1.k8s:架构,组件,基础概念

目录 一、k8s了解 1.什么是k8s 2.为什么要k8s &#xff08;1&#xff09;部署方式演变 &#xff08;2&#xff09;k8s作用 &#xff08;3&#xff09;Mesos&#xff0c;Swarm&#xff0c;K8S三大平台对比 二、k8s架构、组件 1.k8s架构 2.k8s基础组件 3.k8s附加组件 …...

动态规划基础练习

我们需要先从数组较大的开始进行处理&#xff0c;每次考察上下左右的&#xff0c;比较当前存储的最大值和转移来的值&#xff0c;哪一个大一点 #define _CRT_SECURE_NO_WARNINGS #include<bits/stdc.h> using namespace std;int n, m; int a[105][105]; int addx[] { 0,…...

基于Java的地方废物回收机构管理系统

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;Java技术&#xff0c;MIS的总体思想&#xff0c;MySQL数据库 工具&#xff1a;Eclipse&#xff0c;…...

Leetcode 450:删除二叉搜索树中的节点

给定一个二叉搜索树的根节点 root 和一个值 key&#xff0c;删除二叉搜索树中的 key 对应的节点&#xff0c;并保证二叉搜索树的性质不变。返回二叉搜索树&#xff08;有可能被更新&#xff09;的根节点的引用。 一般来说&#xff0c;删除节点可分为两个步骤&#xff1a; 首先…...

Go 中使用map时注意的问题

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…...

english-works

前奏&#xff08;prelude&#xff09;&#xff1a; To build up our body, our school sports meeting was held on our play ground last Thursday. All the students in my class took an active part in sports meeting. It began with an opening ceremony on the play g…...

Kubernetes面试整理-如何利用PodSecurityPolicies来提高集群的安全性?

PodSecurityPolicy (PSP) 是 Kubernetes 中用于定义和控制 Pod 安全配置的策略。通过 PSP,可以设置对 Pod 的一些安全约束条件,从而提高集群的安全性。虽然 PSP 从 Kubernetes 1.21 开始已被弃用,并在 1.25 版本中移除,但在一些旧版 Kubernetes 集群中,PSP 仍然是一个重要…...

YOLO网络结构特点收录

YOLO网络结构特点收录 YOLO&#xff08;You Only Look Once&#xff09;网络结构随着版本迭代不断进化&#xff0c;以下是一些关键版本的网络结构特点概述&#xff1a; YOLOv1 输入&#xff1a;将图像调整至固定尺寸&#xff0c;如448x448像素。骨干网络&#xff1a;初期版本…...

人生最有力,最棒的十句话!

人生最有力&#xff0c;最棒的十句话 1、允许一切事发生&#xff0c;所有一切发生的事不是你能阻挡了的&#xff0c;你接受&#xff0c;他也发生&#xff0c;你不接受&#xff0c;他也发生&#xff0c;你还不如坦然面对接受现实。 2、你焦虑的时候千万不要躺着啥也不干&#xf…...

ASUS华硕A豆14笔记本电脑I421EAYB,I421EQYB_ADOL14EA工厂模式原厂Win11系统安装包下载

适用型号&#xff1a;ADOL14EA笔记本I421EAYB、I421EQYB 链接&#xff1a;https://pan.baidu.com/s/1krU8m_lbApyUfZQo5E4cCQ?pwd0ewl 提取码&#xff1a;0ewl 华硕原装WIN11系统工厂安装包&#xff0c;带有MyASUS WinRE RECOVERY恢复功能、自带所有驱动、出厂主题壁纸、系…...

丙酮传感器TGS1820在呼吸气体丙酮含量分析检测中的应用

随着科技的进步&#xff0c;无创检测技术逐渐成为医疗保健领域的新宠。其中&#xff0c;基于呼吸气体的分析检测技术以其独特的优势受到了广泛关注。呼吸气中的挥发性有机化合物&#xff08;VOCs&#xff09;不仅为研究者们提供了关于人体健康状态的宝贵信息&#xff0c;而且它…...

全国今日油价查询-全国今日油价查询接口-API接口

关于油价的信息&#xff0c;以下是详细的分点表示和归纳&#xff1a; 最新油价调整&#xff1a; 时间&#xff1a;2024年6月28日0时 调整内容&#xff1a;汽油价格上调210元/吨&#xff0c;柴油价格上调200元/吨。 涨幅&#xff1a;加油站油价上涨0.16元/升-0.20元/升。 具体油…...

MT1568 学生成绩

题目 有3个学生&#xff0c;每个学生有3门课的成绩&#xff0c;从键盘输入数据&#xff0c;包括学号、姓名、三门课成绩&#xff0c;学号整型&#xff0c;姓名字符型&#xff0c;成绩实型&#xff0c;计算3门课程总平均成绩&#xff0c;以及平均分最高的学生信息。不考虑非法成…...

医院消防设施设备管理系统

医院为人员密集场所&#xff0c;且多为各类病患及其陪护人员&#xff0c;一旦发生火灾&#xff0c;人员疏散逃生困难&#xff0c;容易造成较严重的生命与财产损失。为规范医院的消防设施设备管理&#xff0c;通过凡尔码系统对医院消防设施设备进行信息化管理&#xff0c;提高医…...

[Go 微服务] go-micro + consul 的使用

文章目录 1.go-micro 介绍2.go-micro 的主要功能3.go-micro 安装4.go-micro 的使用4.1 创建服务端4.2 配置服务端 consul4.3 生成客户端 5.goodsinfo 服务5.1 服务端开发5.2 客户端开发 1.go-micro 介绍 Go Micro是一个简化分布式开发 的微服务生态系统&#xff0c;该系统为开…...

嵌入式网页服务实现

嵌入式网页服务的实现方式主要可以归纳为以下几种&#xff1a; 单片机Webchip网关&#xff1a; Webchip&#xff1a;作为专用网络接口芯片&#xff0c;独立于单片机与网关&#xff0c;通过SPI接口与MCU进行指令交互&#xff0c;并通过RS232、USB、Modem等接口与PC作为网关进行通…...

python---OpenCv(二),背景分离方法较有意思

目录 边界矩形 旋转矩形(最小外接矩形): 计算轮廓 找4个点的坐标 把浮点型转为Int 画轮廓 边界矩形--&#xff08;最大外接矩形&#xff09; 转灰度 找轮廓 找顶点 画矩形 显示 背景分离方法&#xff08;这个很好玩&#xff0c;可以识别在动的物体&#xff09; 边…...

java TCP服务器与客户端通信示例

目录 背景: TCP服务代码解析: TCP服务器的代码: 服务器程序的工作流程: TCP客户端代码解析 : TCP客户端代码: 客户端工作流程: 总结: 背景: 如何使用Java编程语言实现一个简单的TCP服务器和客户端。那么TCP是什么?TCP&#xff08;Transmission Control Protocol&…...

【C++】构造函数和析构函数

目录 对象初始化-构造函数构造函数的分类构造函数的调用拷贝构造的应用构造函数调用规则深拷贝和浅拷贝初始化列表类对象作为类成员静态成员 对象释放-析构函数 对象初始化-构造函数 构造函数是类实例化的时候会自动调用的初始化函数&#xff0c;如果用户不写编译器会提供一个…...

Docker Compose:多容器应用的管理利器

在现代应用开发中&#xff0c;微服务架构已成为主流。管理和编排多个容器应用变得至关重要。Docker Compose 是一个强大的工具&#xff0c;通过一个简单的 YAML 文件定义和运行多容器应用。本文将详细介绍 Docker Compose 的基本概念、安装、用法以及一个实际的示例&#xff0c…...

Leetcode - 133双周赛

目录 一&#xff0c;3190. 使所有元素都可以被 3 整除的最少操作数 二&#xff0c;3191. 使二进制数组全部等于 1 的最少操作次数 I 三&#xff0c;3192. 使二进制数组全部等于 1 的最少操作次数 II 四&#xff0c;3193. 统计逆序对的数目 一&#xff0c;3190. 使所有元素都…...

C++总结

...

汽车免拆诊断案例 | 2016 款吉利帝豪EV车无法加速

故障现象 一辆2016款吉利帝豪EV车&#xff0c;累计行驶里程约为28.4万km&#xff0c;车主反映车辆无法加速。 故障诊断 接车后路试&#xff0c;行驶约1 km&#xff0c;踩下加速踏板&#xff0c;无法加速&#xff0c;车速为20 km/h左右&#xff0c;同时组合仪表上的电机及控制…...

前端开发之webpack

安装与入门超详细&#xff01;webpack入门教程(一)-腾讯云开发者社区-腾讯云...

将内容复制到剪贴板?分享 1 段优质 JS 代码片段!

大家好&#xff0c;我是大澈&#xff01; 本文约 600 字&#xff0c;整篇阅读约需 1 分钟。 每日分享一段优质代码片段。 今天分享一段 JS 代码片段&#xff0c;使用 Clipboard API 实现将内容复制到剪贴板。 老规矩&#xff0c;先阅读代码片段并思考&#xff0c;再看代码解析…...

MAS0902量产工具分享,MAS0902A开卡教程,MAS0901量产工具下载

MAS0902和MAS1102都是基于SATA3.2技术开发的DRAM-less SSD控制芯片&#xff0c;简单来说就是SATA协议无缓存主控。下面是我摸索的麦光黑金300 240G SSD开卡修复简易教程&#xff0c;也就是MAS0902量产过程&#xff1a; 注意&#xff1a;开卡转接线必须要用ASM1153E或JMS578主控…...

从我邮毕业啦!!!

引言 时间过的好快&#xff0c;转眼间就要从北邮毕业了&#xff0c;距离上一次月度总结又过去了两个月&#xff0c;故作本次总结。 PS: https://github.com/WeiXiao-Hyy/blog整理了后端开发的知识网络&#xff0c;欢迎Star&#xff01; 毕业&#x1f393; 6月1号完成了自己的…...

gemini 1.5 flash (node项目)

https://www.npmjs.com/package/google/generative-ai https://ai.google.dev/pricing?hlzh-cn https://aistudio.google.com/app/apikey https://ai.google.dev/gemini-api/docs/models/gemini?hlzh-cn#gemini-1.5-flash https://ai.google.dev/gemini-api/docs/get-started…...

在线字节大端序小端序转换器

具体请前往&#xff1a;在线字节大端序小端序转换器...

css_17_背景属性鼠标属性

一.背景属性 -属性值&#xff1a;background-color&#xff08;设置背景颜色&#xff09; 默认背景颜色是 transparent。 -属性值&#xff1a;background-image&#xff08;设置背景图片&#xff09; url&#xff08;图片的地址&#xff09; -属性值&#xff1a;background-re…...

Python hash编码(go hash编码)

id"中国人" 首先&#xff0c;go语言hash: import (mmh3 "murmurhash3") mmh3.Murmurhash3([]byte(id)) 对应到Python hash编码&#xff0c;可以直接使用mmh3 import mmh3 mmh3.hash(id,signedFalse) 其源码可以表示为 def sum32WithSeed(datas, seed…...

004 插入排序(lua)

文章目录 123 1 -- Lua中没有类和方法的概念&#xff0c;所以我们将所有功能都写在一个脚本中 -- 交换数组中两个元素的功能 local function swap(arr, i, j) local temp arr[i] arr[i] arr[j] arr[j] temp end -- 插入排序算法的实现 local function insertionS…...

计算机网络 —— 基本概念

基本概念 1. 通信协议2. 面向连接 v.s. 面向无连接3. 电路交换 v.s. 分组交换4. 单工通信 v.s. 双工通信 1. 通信协议 通信协议就是计算机与计算机之间通过网络实现通信时事先达成的一种“约定”。这种“约定”使那些由不同厂商的设备、不同的CPU 以及不同的操作系统组成的计算…...

高精度除法的实现

高精度除法与高精度加法的定义、前置过程都是大致相同的&#xff0c;如果想了解具体内容&#xff0c;可以移步至我的这篇博客&#xff1a;高精度加法计算的实现 在这里就不再详细讲解&#xff0c;只讲解主体过程qwq 主体过程 高精度除法的原理和小学学习的竖式除法是一样的。 …...

STM32CUBEMX配置USB虚拟串口

STM32CUBEMX配置USB虚拟串口 cubemx上默认配置即可。 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 配置完后生成工程&#xff0c;主要就是要知道串口的收发接口就行了。 发送&#xff1a;CDC_Transmit_FS()&#xff0c;同时记得包含头文件#include “…...

安卓开发中margin和padding的区别

在 Android 开发中&#xff0c;margin 和 padding 都是用来定义视图&#xff08;View&#xff09;的空间属性&#xff0c;但它们的作用和应用场景有所不同&#xff1a; Margin&#xff08;外边距&#xff09;&#xff1a; Margin 是视图与其他视图之间的空间。它定义了视图之间…...

Symfony事件调度系统:掌控应用程序生命周期的钥匙

Symfony事件调度系统&#xff1a;掌控应用程序生命周期的钥匙 引言 Symfony是一个高度灵活的PHP框架&#xff0c;用于构建各种规模的Web应用程序。它的核心特性之一是事件调度系统&#xff0c;该系统允许开发者在应用程序的生命周期中触发和监听事件。这种机制为开发者提供了…...

maven安装jar和pom到本地仓库

举例子我们要将 elastic-job-spring-boot-starter安装到本地的maven仓库&#xff0c;如下&#xff1a; <dependency><groupId>com.github.yinjihuan</groupId><artifactId>elastic-job-spring-boot-starter</artifactId><version>1.0.5&l…...

[leetcode]assign-cookies. 分发饼干

. - 力扣&#xff08;LeetCode&#xff09; class Solution { public:int findContentChildren(vector<int>& g, vector<int>& s) {sort(g.begin(), g.end());sort(s.begin(), s.end());int m g.size(), n s.size();int count 0;for (int i 0, j 0; i…...

ai智能语音机器人在电销里发挥怎样的作用

得益于语音识别技术的的进步&#xff0c;人工智能发展越来越成熟。相信作为企业的管理者&#xff0c;都遇到过这样的事&#xff1a;一个电销新人刚刚入行&#xff0c;需求经过一两个月的学习培训才能成为一名合格的销售人员。在这段学习的期间&#xff0c;企业投入的成本是没有…...

顺序表应用——通讯录

在本篇之前的顺序表专题我们已经学习的顺序表的实现&#xff0c;了解了如何实现顺序表的插入和删除等功能&#xff0c;那么在本篇当中就要学习基于顺序表来实现通讯录&#xff0c;在通讯录当中能实现联系人的增、删、查改等功能&#xff0c;接下来就让我们一起来实现通讯录吧&a…...

猫头虎分享[可灵AI」官方推荐的驯服指南-V1.0

猫头虎分享[可灵AI」官方推荐的驯服指南-V1.0 猫头虎是谁&#xff1f; 大家好&#xff0c;我是 猫头虎&#xff0c;别名猫头虎博主&#xff0c;擅长的技术领域包括云原生、前端、后端、运维和AI。我的博客主要分享技术教程、bug解决思路、开发工具教程、前沿科技资讯、产品评…...

为什么这几年参加PMP考试的人越来越多

参加PMP认证的人越来越多的原因我认为和社会发展、职场竞争、个人提升等等方面有着不小的关系。国际认证与国内认证的性质、发展途径会有一些区别&#xff0c;PMP引进到中国二十余年&#xff0c;报考人数持增长状态也是正常的。 具体可以从下面这几个点来展开论述。 市场竞争…...

【Redis一】Redis配置与优化

目录 一.关系型数据库与非关系型数据库 1.关系型数据库 2.非关系型数据库 3.二者区别 4.非关系型数据库产生背景 5.NoSQL与SQL数据记录对比 关系型数据库 非关系型数据库 二.Redis相关概述 1.简介 2.五大数据类型 3.优缺点 3.1.优点 3.2.缺点 4.使用场景 5.采用…...

原创超然语录

人活一生到底为了啥&#xff0c;其实我也在想这个问题&#xff0c;很多时候我也在思考这个问题&#xff0c;自己穷极一生所追求的东西究竟还有哪一些&#xff0c;如果一个人他没有了自己的追求&#xff0c;失去了自己所有要追求的东西和目标&#xff0c;它自己所追求的&#xf…...

城市通勤神器!奔腾小马2.89万元火爆预售

上下班高峰期的通勤,总是让人头疼。坐公交人挤人,路上的耗时更是无法计算;乘地铁相对省时间,但车厢里依然像是“沙丁鱼罐头”。如果遇到刮风下雨等恶劣天气,就更加令人恼火。在这种情况下,很多人都希望能拥有一辆价格便宜、配置够用的代步小车,虽无奢华体验,但求遮风挡…...

考拉五座北京车展惊艳亮相,黑科技再次成为制胜法宝

在刚刚结束的北京国际车展上,极狐品牌携旗下全系车型强势登场,其中考拉五座更是首次亮相,引起了广泛关注。这款车型的出现,预示着智能汽车领域又迎来了一位实力不俗的新成员。随着考拉五座的加入,新能源汽车市场的竞争无疑将更加激烈和多彩。车展上,观众们纷纷驻足观看,…...

底盘革新+M9同款雷达,问界M7Ultra升级点都在这

20万级的SUV,8个月获得超过18万的用户,这样辉煌的成绩你几乎很难在汽车圈找到第二家,问界M7国民SUV名副其实。???但华为与赛力斯绝不止步于此,5月31日晚,问界M7 Ultra正式上市,次共推出四款车型,售价区间为28.98-32.98万元。与此同时北汽新能源与华为合作的享界S9也正…...

起售13.98万,取消鲢鱼嘴设计,试驾体验第十一代索纳塔!

对于2002年就进入国内市场的索纳塔,相信大多数人并不陌生,定位上是一台中型车,整体的风格就是突出运动基因,而在今年是迎来了家族第十一代车型上市。车辆指导售价在13.98~18.68万之间,作为一台中型车,定价方面还算合理,从低到高,动力上有两种选择,一种是1.5T涡轮增压引…...

python常用文件路径切片及写脚本

常用文件路径切片 因为自己对路径切片这一块不是很熟悉,所以每次用都到处查浪费了不少时间,今天刚好又用到,整理一下: #绝对路径 video_path=r“E:\avfilm\pokes\qw0001.mp4” # 从绝对路径中提取完整文件名 video_name = os.path.basename(video_path) # 文件路径提取,用…...

这个橙子真的香!老司机徒手把玩香橙派Kunpeng Pro事后感言

水果派爱好者 作为一个水果派爱好者&#xff0c;我早在大学时期就购入了树莓派3B以及一堆传感器配件&#xff0c;把玩至今。 后来在工作的时候&#xff0c;由于不满足树莓派3B的性能&#xff0c;又购入了树莓派4B&#xff0c;玩得不亦乐乎&#xff0c;做出了很多因吹斯汀的小玩…...