Java项目实战笔记--基于SpringBoot3.0开发仿12306高并发售票系统--(二)项目实现-第二篇-前端模块搭建及单点登录的实现

本文参考自

Springboot3+微服务实战12306高性能售票系统 - 慕课网 (imooc.com)

本文是仿12306项目实战第(二)章——项目实现 的第二篇,详细讲解使用Vue3 + Vue CLI 实现前端模块搭建的过程,同时其中也会涉及一些前后端交互的实现,因此也会开发一些后端接口;搭建好前端页面后,还会实现JWT单点登录功能

一、环境准备

  • 安装nodejs 18 +

    设置镜像

在这里插入图片描述

  • IDEA 配置nodejs

在这里插入图片描述

  • 安装vue cli

    npm install -g @vue/cli@5.0.8
    

在这里插入图片描述

这里报了错误,原因是淘宝镜像过期了,解决办法是:修改镜像地址为https://registry.npmmirror.com

npm config set registry https://registry.npmmirror.com

参考:npm报错:request to https://registry.npm.taobao.org failed, reason certificate has expired-CSDN博客

在这里插入图片描述

二、使用Vue CLI 创建web模块

解决IDEA命令行(powershell)提示vue脚本报错:

IDEA中报错:因为在此系统上禁止运行脚本有关详细信息,请参阅…(图文解释 亲测已解决)_因为在此系统上禁止运行脚本。有关详细信息-CSDN博客

  • 创建web模块

    vue create web
    

在这里插入图片描述

  • 启动

     $ cd web$ npm run serve
    
  • 修改package.json文件,改变启动的默认端口

在这里插入图片描述

在这里插入图片描述

三、集成Ant Design Vue

  • 安装

    npm i ant-design-vue
    

    这里使用npm i ant-design-vue@3.2.15 安装和教程一样的版本

  • 全局引入组件

    main.js

在这里插入图片描述

  • 测试

在这里插入图片描述

  • 引入css样式

    main.js

    import 'ant-design-vue/dist/antd.css';
    
  • 引入Icon

    npm install --save @ant-design/icons-vue
    

    版本课程里是6.1.0

    全局使用图标

    main.js

    import * as Icons from '@ant-design/icons-vue';
    
    const app = createApp(App);
    app.use(Antd).use(store).use(router).mount('#app');//全局使用图标
    const icons = Icons;
    for (const i in icons) {app.component(i,icons[i])
    }
    
  • 测试

在这里插入图片描述

在这里插入图片描述

四、注册登录二合一界面开发

由于本课程项目主要针对后端技术学习,所以前端就不做详细的讲解

  • 加路由

    web/src/router/index.js

      {path: '/login',component: () => import('../views/login.vue')}
    
  • 增加login.vue页面

    web/src/views/login.vue

    <template><a-row class="login"><a-col :span="8" :offset="8" class="login-main"><h1 style="text-align: center"><rocket-two-tone />&nbsp;neilxu 12306售票系统</h1><a-form:model="loginForm"name="basic"autocomplete="off"@finish="onFinish"@finishFailed="onFinishFailed"><a-form-itemlabel=""name="mobile":rules="[{ required: true, message: '请输入手机号!' }]"><a-input v-model:value="loginForm.mobile" placeholder="手机号"/></a-form-item><a-form-itemlabel=""name="code":rules="[{ required: true, message: '请输入验证码!' }]"><a-input v-model:value="loginForm.code"><template #addonAfter><a @click="sendCode">获取验证码</a></template></a-input><!--<a-input v-model:value="loginForm.code" placeholder="验证码"/>--></a-form-item><a-form-item><a-button type="primary" block html-type="submit">登录</a-button></a-form-item></a-form></a-col></a-row>
    </template><script>
    import { defineComponent, reactive } from 'vue';
    export default defineComponent({name: "login-view",setup() {const loginForm = reactive({mobile: '13000000000',code: '',});const onFinish = values => {console.log('Success:', values);};const onFinishFailed = errorInfo => {console.log('Failed:', errorInfo);};return {loginForm,onFinish,onFinishFailed,};},
    });
    </script><style>
    .login-main h1 {font-size: 25px;font-weight: bold;
    }
    .login-main {margin-top: 100px;padding: 30px 30px 20px;border: 2px solid grey;border-radius: 10px;background-color: #fcfcfc;
    }
    </style>
    

    这里注意name用两个单词以上,不然之前安装的ESLint会报错语法不规范

    export default defineComponent({name: "login-view",
    

或者直接去package.json,“eslintConfig"下的"rules”
修改成
"rules": { "vue/multi-word-component-names": 0 }
则可以关闭eslint multi-word的校验

  • 效果

在这里插入图片描述

五、发送短信验证码接口开发

  • 请求实体类

    com.neilxu.train.member.req.MemberSendCodeReq

    @Data
    public class MemberSendCodeReq {@NotBlank(message = "【手机号】不能为空")@Pattern(regexp = "^\\d{10}$",message = "手机号码格式错误")private String mobile;
    }
    
  • service方法

    public void sendCode(MemberSendCodeReq req) {String mobile = req.getMobile();MemberExample memberExample = new MemberExample();memberExample.createCriteria().andMobileEqualTo(mobile);List<Member> list = memberMapper.selectByExample(memberExample);// 如果手机号不存在,则插入一条记录if (CollUtil.isEmpty(list)) {LOG.info("手机号不存在,插入一条记录");Member member = new Member();member.setId(SnowUtil.getSnowflakeNextId());member.setMobile(mobile);memberMapper.insert(member);} else {LOG.info("手机号存在,不插入记录");}// 生成验证码// String code = RandomUtil.randomString(4);String code = "8888";LOG.info("生成短信验证码:{}", code);// 保存短信记录表:手机号,短信验证码,有效期,是否已使用,业务类型,发送时间,使用时间LOG.info("保存短信记录表");// 对接短信通道,发送短信LOG.info("对接短信通道");}
    
  • controller层

    @PostMapping("/send-code")
    public CommonResp<Long> sendCode(@Valid MemberSendCodeReq req) {memberService.sendCode(req);return new CommonResp<>();
    }
    
  • 测试

    POST http://localhost:8000/member/member/send-code
    Content-Type: application/x-www-form-urlencodedmobile=13000000000###
    

在这里插入图片描述

六、短信验证码登录接口开发

  • 更新下hutool依赖

    这里课程讲解到使用BeanUtil类的时候,发现缺少了BeanUtil.copyToList()方法,因此修改下依赖版本

    <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.10</version>
    </dependency>
    

    BeanUtil.copyToList 方法属于浅拷贝,它只会复制对象的引用而不会复制对象本身。换句话说,当使用 BeanUtil.copyToList 方法将源对象列表中的属性复制到目标对象列表中时,如果属性是引用类型(如自定义类对象),则复制的是对象引用而不是新的独立对象。这意味着如果源对象列表中的对象发生了变化,目标对象列表中对应元素的属性也会随之变化。

    如果需要进行深拷贝,即复制对象本身而不是仅复制引用,可以考虑使用Hutool工具类库中的其他深拷贝方法,例如CopyUtil.copyList。深拷贝会创建全新的对象实例,并将原对象的所有属性值都复制到新创建的对象中,这样即使原对象发生变化也不会影响到新的拷贝对象。

    ​ ----------来自ChatGPT的回答

  • 登录请求实体类

    com.neilxu.train.member.req.MemberLoginReq

    @Data
    public class MemberLoginReq {@NotBlank(message = "【手机号】不能为空")@Pattern(regexp = "^1\\d{10}$",message = "手机号码格式错误")private String mobile;@NotBlank(message = "【短信验证码】不能为空")private String code;
    }
    
  • 登录返回结果类
    com.neilxu.train.member.resp.MemberLoginResp

    package com.neilxu.train.member.resp;public class MemberLoginResp {private Long id;private String mobile;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getMobile() {return mobile;}public void setMobile(String mobile) {this.mobile = mobile;}@Overridepublic String toString() {StringBuilder sb = new StringBuilder();sb.append(getClass().getSimpleName());sb.append(" [");sb.append("Hash = ").append(hashCode());sb.append(", id=").append(id);sb.append(", mobile=").append(mobile);sb.append("]");return sb.toString();}
    }
    
  • 新增异常枚举

    MEMBER_MOBILE_NOT_EXIST("请先获取短信验证码"),
    MEMBER_MOBILE_CODE_ERROR("短信验证码错误");
    
  • service方法

    public MemberLoginResp login(MemberLoginReq req) {String mobile = req.getMobile();String code = req.getCode();Member memberDB = selectByMobile(mobile);// 如果手机号不存在,则插入一条记录if (ObjectUtil.isNull(memberDB)) {throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST);}// 校验短信验证码if (!"8888".equals(code)) {throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR);}return BeanUtil.copyProperties(memberDB, MemberLoginResp.class);}
    

    这里将前面的代码块封装了一个方法——通过手机号查找用户

    private Member selectByMobile(String mobile) {MemberExample memberExample = new MemberExample();memberExample.createCriteria().andMobileEqualTo(mobile);List<Member> list = memberMapper.selectByExample(memberExample);if (CollUtil.isEmpty(list)) {return null;} else {return list.get(0);}
    }
    

    正常项目的登录接口还需要校验验证码的有效性(redis),以及对接口做访问频率检查等(防止黑客恶意访问),但本项目重点不在此,所以不做过多的处理

  • controller层

    com.neilxu.train.member.controller.MemberController

    @PostMapping("/login")
    public CommonResp<MemberLoginResp> login(@Valid MemberLoginReq req) {MemberLoginResp resp = memberService.login(req);return new CommonResp<>(resp);
    }
    
  • 测试

在这里插入图片描述

七、集成Axios完成登录功能

  • 安装Axios组件

    npm install axios
    
  • 引入Axios

    web/src/views/login.vue

    import axios from 'axios';
    
    const sendCode = () => {axios.post("http://localhost:8000/member/member/send-code", {mobile: loginForm.mobile}).then(response => {console.log(response);});
    };
    return {loginForm,onFinish,onFinishFailed,sendCode
    };
    

    此时,会有跨域问题(ip相同但是端口不同)

在这里插入图片描述

  • 解决跨域问题

    跨域问题(Cross-Origin Resource Sharing)是指在浏览器环境中,由于浏览器遵循同源策略的原则,导致在跨域访问资源时被阻止或限制的问题。

    同源策略是浏览器的一项安全策略,它要求网页只能从同一源(协议、域名、端口号)的文档加载其他资源或与同一源的服务器进行交互。当浏览器发现当前网页请求的资源不符合同源策略的要求时,会阻止或限制该请求。

    跨域问题通常在前端开发中遇到,例如当浏览器中运行的 JavaScript 代码尝试获取另一个域名下的数据时,就可能触发跨域问题。为了解决这些问题,通常需要在后端进行一些配置或在前端使用一些技术手段来绕过浏览器的限制。

    ​ ----------来自ChatGPT的回答

    解决:

    修改网关模块配置文件

    gateway/src/main/resources/application.properties

    # 允许请求来源(老版本叫allowedOrigin)
    spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedOriginPatterns=*
    # 允许携带的头信息
    spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedHeaders=*
    # 允许的请求方式
    spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedMethods=*
    # 是否允许携带cookie
    spring.cloud.gateway.globalcors.cors-configurations.[/**].allowCredentials=true
    # 跨域检测的有效期,会发起一个OPTION请求
    spring.cloud.gateway.globalcors.cors-configurations.[/**].maxAge=3600
    
  • 解决前后端传参问题

在这里插入图片描述

修改controller参数接收类型

com.neilxu.train.member.controller.MemberController

@PostMapping("/send-code")
public CommonResp<Long> sendCode(@Valid @RequestBody MemberSendCodeReq req) {memberService.sendCode(req);return new CommonResp<>();
}

修改http请求测试

POST http://localhost:8000/member/member/send-code
Content-Type: application/json{"mobile": "13000000001"
}###

测试

在这里插入图片描述

  • 完成登录功能

    com.neilxu.train.member.controller.MemberController

    @PostMapping("/login")
    public CommonResp<MemberLoginResp> login(@Valid @RequestBody MemberLoginReq req) {MemberLoginResp resp = memberService.login(req);return new CommonResp<>(resp);
    }
    

    web/src/views/login.vue

    <a-form-item><a-button type="primary" block @click="login">登录</a-button>
    </a-form-item>
    
    import { notification } from 'ant-design-vue';
    
    const sendCode = () => {axios.post("http://localhost:8000/member/member/send-code", {mobile: loginForm.mobile}).then(response => {console.log(response);let data = response.data;if (data.success) {notification.success({ description: '发送验证码成功!' });loginForm.code = "8888";} else {notification.error({ description: data.message });}});
    };const login = () => {axios.post("http://localhost:8000/member/member/login", loginForm).then(response => {let data = response.data;if (data.success) {notification.success({ description: '登录成功!' });console.log("登录成功:", data.content);} else {notification.error({ description: data.message });}})
    };
    return {loginForm,sendCode,login
    };
    
  • 测试

在这里插入图片描述

八、增加axios拦截器,打印请求参数和返回结果

web/src/main.js

import axios from 'axios';
/*** axios拦截器*/
axios.interceptors.request.use(function (config) {console.log('请求参数:', config);return config;
}, error => {return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {console.log('返回结果:', response);return response;
}, error => {console.log('返回错误:', error);return Promise.reject(error);
});

测试

在这里插入图片描述

九、Vue CLI多环境配置;为axios配置后端域名

  • 新增配置文件

    web/.env.dev

    NODE_ENV=development
    VUE_APP_SERVER=http://localhost:8000
    

    web/.prod.dev

    NODE_ENV=production
    VUE_APP_SERVER=http://train.imooc.com
    
  • 修改main.js

    axios.defaults.baseURL = process.env.VUE_APP_SERVER;
    console.log('环境:', process.env.NODE_ENV);
    console.log('服务端:', process.env.VUE_APP_SERVER);
    
  • 修改package.json

    "serve-dev": "vue-cli-service serve --mode dev --port 9000",
    "serve-prod": "vue-cli-service serve --mode prod --port 9000",
    
  • 修改login.vue

    去掉baseURL

    const sendCode = () => {axios.post("/member/member/send-code", {mobile: loginForm.mobile}).then(response => {let data = response.data;if (data.success) {notification.success({ description: '发送验证码成功!' });loginForm.code = "8888";} else {notification.error({ description: data.message });}});
    };const login = () => {axios.post("/member/member/login", loginForm).then(response => {let data = response.data;if (data.success) {notification.success({ description: '登录成功!' });} else {notification.error({ description: data.message });}})
    };
    
  • 重启测试

在这里插入图片描述

在这里插入图片描述

十、增加web控台主页,登录成功后跳转主页

  • 修改路由文件

    web/src/router/index.js

    import { createRouter, createWebHistory } from 'vue-router'const routes = [{path: '/login',component: () => import('../views/login.vue')},{path: '/',component: () => import('../views/main.vue')}
    ]const router = createRouter({history: createWebHistory(process.env.BASE_URL),routes
    })export default router
    
  • 增加控台主页面

    web/src/views/main.vue

    从ant design vue官网扒代码

在这里插入图片描述

<template><a-layout id="components-layout-demo-top-side-2"><a-layout-header class="header"><div class="logo" /><a-menuv-model:selectedKeys="selectedKeys1"theme="dark"mode="horizontal":style="{ lineHeight: '64px' }"><a-menu-item key="1">nav 1</a-menu-item><a-menu-item key="2">nav 2</a-menu-item><a-menu-item key="3">nav 3</a-menu-item></a-menu></a-layout-header><a-layout><a-layout-sider width="200" style="background: #fff"><a-menuv-model:selectedKeys="selectedKeys2"v-model:openKeys="openKeys"mode="inline":style="{ height: '100%', borderRight: 0 }"><a-sub-menu key="sub1"><template #title><span><user-outlined />subnav 1</span></template><a-menu-item key="1">option1</a-menu-item><a-menu-item key="2">option2</a-menu-item><a-menu-item key="3">option3</a-menu-item><a-menu-item key="4">option4</a-menu-item></a-sub-menu><a-sub-menu key="sub2"><template #title><span><laptop-outlined />subnav 2</span></template><a-menu-item key="5">option5</a-menu-item><a-menu-item key="6">option6</a-menu-item><a-menu-item key="7">option7</a-menu-item><a-menu-item key="8">option8</a-menu-item></a-sub-menu><a-sub-menu key="sub3"><template #title><span><notification-outlined />subnav 3</span></template><a-menu-item key="9">option9</a-menu-item><a-menu-item key="10">option10</a-menu-item><a-menu-item key="11">option11</a-menu-item><a-menu-item key="12">option12</a-menu-item></a-sub-menu></a-menu></a-layout-sider><a-layout style="padding: 0 24px 24px"><a-breadcrumb style="margin: 16px 0"><a-breadcrumb-item>Home</a-breadcrumb-item><a-breadcrumb-item>List</a-breadcrumb-item><a-breadcrumb-item>App</a-breadcrumb-item></a-breadcrumb><a-layout-content:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">Content</a-layout-content></a-layout></a-layout></a-layout>
</template>
<script>
import { UserOutlined, LaptopOutlined, NotificationOutlined } from '@ant-design/icons-vue';
import { defineComponent, ref } from 'vue';
export default defineComponent({name: "main-view",components: {UserOutlined,LaptopOutlined,NotificationOutlined,},setup() {return {selectedKeys1: ref(['2']),selectedKeys2: ref(['1']),collapsed: ref(false),openKeys: ref(['sub1']),};},
});
</script>
<style>
#components-layout-demo-top-side-2 .logo {float: left;width: 120px;height: 31px;margin: 16px 24px 16px 0;background: rgba(255, 255, 255, 0.3);
}.ant-row-rtl #components-layout-demo-top-side-2 .logo {float: right;margin: 16px 0 16px 24px;
}.site-layout-background {background: #fff;
}
</style>

注意

复制过来后可能出现兼容问题,例如这里需要增加id属性,不然logo就看不到了

在这里插入图片描述

还有就是注意这里加个名字,不然还会有ESLint语法报错

export default defineComponent({name: "main-view",
  • 修改login.vue

    import { useRouter } from 'vue-router'export default defineComponent({name: "login-view",setup() {const router = useRouter();const loginForm = reactive({mobile: '13000000000',code: '',});
    
    const login = () => {axios.post("/member/member/login", loginForm).then(response => {let data = response.data;if (data.success) {notification.success({ description: '登录成功!' });// 登录成功,跳到控台主页router.push("/");} else {notification.error({ description: data.message });}})
    };
    
  • 测试效果

十一、制作Vue3公共组件

这里我们将头部header和侧边栏sider提取出来作为组件,使用课程的vue3语法

  • 提取the-header组件

    web/src/views/main.vue

    更新为左边,后面同理

在这里插入图片描述

在这里插入图片描述

<template><a-layout id="components-layout-demo-top-side-2"><the-header-view></the-header-view><a-layout><a-layout-sider width="200" style="background: #fff"><a-menuv-model:selectedKeys="selectedKeys2"v-model:openKeys="openKeys"mode="inline":style="{ height: '100%', borderRight: 0 }"><a-sub-menu key="sub1"><template #title><span><user-outlined />subnav 1</span></template><a-menu-item key="1">option1</a-menu-item><a-menu-item key="2">option2</a-menu-item><a-menu-item key="3">option3</a-menu-item><a-menu-item key="4">option4</a-menu-item></a-sub-menu><a-sub-menu key="sub2"><template #title><span><laptop-outlined />subnav 2</span></template><a-menu-item key="5">option5</a-menu-item><a-menu-item key="6">option6</a-menu-item><a-menu-item key="7">option7</a-menu-item><a-menu-item key="8">option8</a-menu-item></a-sub-menu><a-sub-menu key="sub3"><template #title><span><notification-outlined />subnav 3</span></template><a-menu-item key="9">option9</a-menu-item><a-menu-item key="10">option10</a-menu-item><a-menu-item key="11">option11</a-menu-item><a-menu-item key="12">option12</a-menu-item></a-sub-menu></a-menu></a-layout-sider><a-layout style="padding: 0 24px 24px"><a-breadcrumb style="margin: 16px 0"><a-breadcrumb-item>Home</a-breadcrumb-item><a-breadcrumb-item>List</a-breadcrumb-item><a-breadcrumb-item>App</a-breadcrumb-item></a-breadcrumb><a-layout-content:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">Content</a-layout-content></a-layout></a-layout></a-layout>
</template>
<script>
import { UserOutlined, LaptopOutlined, NotificationOutlined } from '@ant-design/icons-vue';
import { defineComponent, ref } from 'vue';
import TheHeaderView from "@/components/the-header";
export default defineComponent({name: "main-view",components: {TheHeaderView,UserOutlined,LaptopOutlined,NotificationOutlined,},setup() {return {selectedKeys2: ref(['1']),collapsed: ref(false),openKeys: ref(['sub1']),};},
});
</script>
<style>
#components-layout-demo-top-side-2 .logo {float: left;width: 120px;height: 31px;margin: 16px 24px 16px 0;background: rgba(255, 255, 255, 0.3);
}.ant-row-rtl #components-layout-demo-top-side-2 .logo {float: right;margin: 16px 0 16px 24px;
}.site-layout-background {background: #fff;
}
</style>

web/src/components/the-header.vue

<template><a-layout-header class="header"><div class="logo" /><a-menuv-model:selectedKeys="selectedKeys1"theme="dark"mode="horizontal":style="{ lineHeight: '64px' }"><a-menu-item key="1">nav 11</a-menu-item><a-menu-item key="2">nav 2</a-menu-item><a-menu-item key="3">nav 3</a-menu-item></a-menu></a-layout-header>
</template><script>
import {defineComponent, ref} from 'vue';export default defineComponent({name: "the-header-view",setup() {return {selectedKeys1: ref(['2']),};},
});
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
  • 提取the-sider组件

    web/src/views/main.vue

在这里插入图片描述

在这里插入图片描述

这里注意icons删了且the-sider组件里也没加上是因为前面main.js已经全局引用了icon

<template><a-layout id="components-layout-demo-top-side-2"><the-header-view></the-header-view><a-layout><the-sider-view></the-sider-view><a-layout style="padding: 0 24px 24px"><a-breadcrumb style="margin: 16px 0"><a-breadcrumb-item>Home</a-breadcrumb-item><a-breadcrumb-item>List</a-breadcrumb-item><a-breadcrumb-item>App</a-breadcrumb-item></a-breadcrumb><a-layout-content:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">Content</a-layout-content></a-layout></a-layout></a-layout>
</template>
<script>
import { defineComponent } from 'vue';
import TheHeaderView from "@/components/the-header";
import TheSiderView from "@/components/the-sider";
export default defineComponent({name: "main-view",components: {TheSiderView,TheHeaderView,},setup() {return {};},
});
</script>
<style>
#components-layout-demo-top-side-2 .logo {float: left;width: 120px;height: 31px;margin: 16px 24px 16px 0;background: rgba(255, 255, 255, 0.3);
}.ant-row-rtl #components-layout-demo-top-side-2 .logo {float: right;margin: 16px 0 16px 24px;
}.site-layout-background {background: #fff;
}
</style>

web/src/components/the-sider.vue

<template><a-layout-sider width="200" style="background: #fff"><a-menuv-model:selectedKeys="selectedKeys2"v-model:openKeys="openKeys"mode="inline":style="{ height: '100%', borderRight: 0 }"><a-sub-menu key="sub1"><template #title><span><user-outlined />subnav 11</span></template><a-menu-item key="1">option1</a-menu-item><a-menu-item key="2">option2</a-menu-item><a-menu-item key="3">option3</a-menu-item><a-menu-item key="4">option4</a-menu-item></a-sub-menu><a-sub-menu key="sub2"><template #title><span><laptop-outlined />subnav 2</span></template><a-menu-item key="5">option5</a-menu-item><a-menu-item key="6">option6</a-menu-item><a-menu-item key="7">option7</a-menu-item><a-menu-item key="8">option8</a-menu-item></a-sub-menu><a-sub-menu key="sub3"><template #title><span><notification-outlined />subnav 3</span></template><a-menu-item key="9">option9</a-menu-item><a-menu-item key="10">option10</a-menu-item><a-menu-item key="11">option11</a-menu-item><a-menu-item key="12">option12</a-menu-item></a-sub-menu></a-menu></a-layout-sider>
</template><script>
import {defineComponent, ref} from 'vue';
export default defineComponent({name: "the-sider-view",setup() {return {selectedKeys2: ref(['1']),openKeys: ref(['sub1']),};},
});
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
  • 测试效果

在这里插入图片描述

十二、实现JWT单点登录功能

1.单点登录2种方式的介绍

  • redis+token

    生成的token是无意义字符串,每个用户每次登录都随机生成,token作为key,用户信息作为value存储在redis中

    【每次登录后端都随机生成字符串token,返给前端保存,之后请求时候header带上token,后端查redis去校验】

  • jwt

    生成的token是含有用户信息的一段字符串

    【每次登陆后端都由jwt工具包生成token,返给前端保存,之后请求时候header带上token,后端用工具包解密校验token】

本项目采用方式二实现单点登录

2.JWT单点登录原理与存在的问题及解决方案

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。JWT 可以使用 HMAC 算法或 RSA 的公钥/私钥对来签名,以验证发送者的身份以及确保消息的完整性。

JWT 通常由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。其结构如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
header.payload.signature
  • 头部(Header):包含了两部分信息:令牌类型(即JWT)和所使用的签名算法。
  • 载荷(Payload):包含了所要传递的信息,如用户ID、用户名等。
  • 签名(Signature):由头部、载荷以及一个密钥(只有服务器知道的)共同组成,用于验证消息的完整性。

JWT 的优点之一是它的信息是经过签名的,因此接收者可以验证它是否被篡改。此外,由于信息被编码为 JSON 格式,因此它们可以轻松地在不同平台之间传递。

在实际应用中,JWT 经常用于身份验证和授权机制,特别是在 Web 应用程序中。用户登录后,服务器可以颁发一个 JWT,之后用户每次请求时都将该 JWT 发送给服务器,服务器通过验证 JWT 的签名来确认用户的身份和权限。

​ ----------来自ChatGPT的回答

  • 存在的问题

    • token被解密

      解决:加盐值(密钥),每个项目的盐值不能一样

    • token被拿到第三方使用

      例如 ChatGPT 国内很多人就把这个包装了一层变成自己的产品来收费别人,实际用户交费后登录进去用的都是作者自己的ChatGPT账号的token

      解决:目前只能是限流来制止

3.使用Hutool生成JWT单点登录token

  • 修改登录返回结果类,增加token字段

    com.neilxu.train.member.resp.MemberLoginResp

    package com.neilxu.train.member.resp;public class MemberLoginResp {private Long id;private String mobile;private String token;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getMobile() {return mobile;}public void setMobile(String mobile) {this.mobile = mobile;}public String getToken() {return token;}public void setToken(String token) {this.token = token;}@Overridepublic String toString() {final StringBuffer sb = new StringBuffer("MemberLoginResp{");sb.append("id=").append(id);sb.append(", mobile='").append(mobile).append('\'');sb.append(", token='").append(token).append('\'');sb.append('}');return sb.toString();}
    }
    
  • 修改登录service方法

    com.neilxu.train.member.service.MemberService

    public MemberLoginResp login(MemberLoginReq req) {String mobile = req.getMobile();String code = req.getCode();Member memberDB = selectByMobile(mobile);// 如果手机号不存在,则插入一条记录if (ObjectUtil.isNull(memberDB)) {throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST);}// 校验短信验证码if (!"8888".equals(code)) {throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR);}MemberLoginResp memberLoginResp = BeanUtil.copyProperties(memberDB, MemberLoginResp.class);Map<String, Object> map = BeanUtil.beanToMap(memberLoginResp);String key = "neilxu12306";String token = JWTUtil.createToken(map, key.getBytes());memberLoginResp.setToken(token);return memberLoginResp;
    }
    
  • 测试

在这里插入图片描述

  • 优化:封装JWT工具类

    放在common模块下

    com.neilxu.train.common.util.JwtUtil

    package com.neilxu.train.common.util;import cn.hutool.core.date.DateField;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.json.JSONObject;
    import cn.hutool.jwt.JWT;
    import cn.hutool.jwt.JWTPayload;
    import cn.hutool.jwt.JWTUtil;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;import java.util.HashMap;
    import java.util.Map;public class JwtUtil {private static final Logger LOG = LoggerFactory.getLogger(JwtUtil.class);/*** 盐值很重要,不能泄漏,且每个项目都应该不一样,可以放到配置文件中*/private static final String key = "neilxu12306";public static String createToken(Long id, String mobile) {DateTime now = DateTime.now();DateTime expTime = now.offsetNew(DateField.SECOND, 10);Map<String, Object> payload = new HashMap<>();// 签发时间payload.put(JWTPayload.ISSUED_AT, now);// 过期时间payload.put(JWTPayload.EXPIRES_AT, expTime);// 生效时间payload.put(JWTPayload.NOT_BEFORE, now);// 内容payload.put("id", id);payload.put("mobile", mobile);String token = JWTUtil.createToken(payload, key.getBytes());LOG.info("生成JWT token:{}", token);return token;}public static boolean validate(String token) {JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());// validate包含了verifyboolean validate = jwt.validate(0);LOG.info("JWT token校验结果:{}", validate);return validate;}public static JSONObject getJSONObject(String token) {JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());JSONObject payloads = jwt.getPayloads();payloads.remove(JWTPayload.ISSUED_AT);payloads.remove(JWTPayload.EXPIRES_AT);payloads.remove(JWTPayload.NOT_BEFORE);LOG.info("根据token获取原始内容:{}", payloads);return payloads;}public static void main(String[] args) {createToken(1L, "123");String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE2NzY4OTk4MjcsIm1vYmlsZSI6IjEyMyIsImlkIjoxLCJleHAiOjE2NzY4OTk4MzcsImlhdCI6MTY3Njg5OTgyN30.JbFfdeNHhxKhAeag63kifw9pgYhnNXISJM5bL6hM8eU";validate(token);getJSONObject(token);}
    }
    

    注意:payload里放了过期时间相关,若是过期了,token校验会不通过(但是仍然可以解密得到用户信息)

  • 优化后修改service方法

    com.neilxu.train.member.service.MemberService

    public MemberLoginResp login(MemberLoginReq req) {String mobile = req.getMobile();String code = req.getCode();Member memberDB = selectByMobile(mobile);// 如果手机号不存在,则插入一条记录if (ObjectUtil.isNull(memberDB)) {throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST);}// 校验短信验证码if (!"8888".equals(code)) {throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR);}MemberLoginResp memberLoginResp = BeanUtil.copyProperties(memberDB, MemberLoginResp.class);String token = JwtUtil.createToken(memberLoginResp.getId(), memberLoginResp.getMobile());memberLoginResp.setToken(token);return memberLoginResp;
    }
    
  • 测试

在这里插入图片描述

4.使用vuex保存登录信息

  • 修改/store/index.js

    这里是全局变量

    import { createStore } from 'vuex'export default createStore({state: {member: {}},getters: {},mutations: {setMember (state, _member) {state.member = _member;}},actions: {},modules: {}
    })
    
  • 保存登录信息

    web/src/views/login.vue

    import store from "@/store";
    
    const login = () => {axios.post("/member/member/login", loginForm).then(response => {let data = response.data;if (data.success) {notification.success({ description: '登录成功!' });// 登录成功,跳到控台主页router.push("/");// store保存登录信息store.commit("setMember", data.content);} else {notification.error({ description: data.message });}})
    };
    
  • 读取信息并展示

    <template><a-layout-header class="header"><div class="logo" /><div style="float: right; color: white;">您好:{{member.mobile}} &nbsp;&nbsp;<router-link to="/login">退出登录</router-link></div><a-menuv-model:selectedKeys="selectedKeys1"theme="dark"mode="horizontal":style="{ lineHeight: '64px' }"><a-menu-item key="1">nav 11</a-menu-item><a-menu-item key="2">nav 2</a-menu-item><a-menu-item key="3">nav 3</a-menu-item></a-menu></a-layout-header>
    </template><script>
    import {defineComponent, ref} from 'vue';
    import store from "@/store";export default defineComponent({name: "the-header-view",setup() {let member = store.state.member;return {selectedKeys1: ref(['2']),member};},
    });
    </script><!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped></style>
    
  • 测试效果

在这里插入图片描述

5.vuex配合h5 session缓存,解决浏览器刷新丢失数据的问题

上面第4步有个问题:刷新浏览器,登录信息就没了

  • 新增自定义js

    web/public/js/session-storage.js

    SessionStorage = {get: function (key) {var v = sessionStorage.getItem(key);if (v && typeof(v) !== "undefined" && v !== "undefined") {return JSON.parse(v);}},set: function (key, data) {//JSON.stringify() 是 JavaScript 中一个用于将 JavaScript 对象或值转换为 JSON 字符串的方法。sessionStorage.setItem(key, JSON.stringify(data));},remove: function (key) {sessionStorage.removeItem(key);},clearAll: function () {sessionStorage.clear();}
    };
    

    sessionStorage和localStorage区别:

    sessionStoragelocalStorage 是 HTML5 中引入的 Web 存储 API,它们都用于在客户端存储数据,但有一些重要的区别:

    1. 作用域:
      • sessionStorage:存储在 sessionStorage 中的数据只在当前会话期间有效。当用户关闭浏览器标签或窗口时,会话结束,sessionStorage 中的数据也会被清除。
      • localStorage:存储在 localStorage 中的数据是永久性的,除非通过 JavaScript 显式删除,否则会一直保存在浏览器中,即使用户关闭了浏览器窗口或重新启动计算机。
    2. 数据共享:
      • sessionStorage:每个页面的 sessionStorage 是独立的,即使是同一个页面打开了多个标签,它们之间的 sessionStorage 也是互相隔离的,无法共享数据。
      • localStorage:所有同源(相同协议、主机和端口)页面共享相同的 localStorage,这意味着一个页面设置的 localStorage 数据可以被同一域下的其他页面访问和修改。
    3. 容量限制:
      • sessionStoragelocalStorage 都有存储容量限制,但具体限制因浏览器而异。一般来说,localStorage 的容量限制要大于 sessionStorage
    4. 存储期限:
      • sessionStorage:存储在 sessionStorage 中的数据在当前会话结束时被清除,即用户关闭浏览器标签或窗口时。
      • localStorage:存储在 localStorage 中的数据没有过期时间,除非通过 JavaScript 显式删除。
    5. API 使用:
      • 两者的 API 使用方法类似,都是通过 setItem(), getItem(), removeItem() 等方法来操作存储的数据。

    总的来说,sessionStorage 适合临时存储会话相关的数据,而 localStorage 适合长期存储的数据,如用户首选项、本地缓存等。

    ​ -------------来自ChatGPT的回答

  • 引入js

    web/public/index.html

    <script src="<%= BASE_URL %>js/session-storage.js"></script>
    
  • 修改store全局变量

    web/src/store/index.js

    import { createStore } from 'vuex'const MEMBER = "MEMBER";export default createStore({state: {member: window.SessionStorage.get(MEMBER) || {}},getters: {},mutations: {setMember (state, _member) {state.member = _member;window.SessionStorage.set(MEMBER, _member);}},actions: {},modules: {}
    })
    
  • 测试

    刷新后正常

6.演示gateway拦截器的使用

  • Test1Filter

    com.neilxu.train.gateway.config.Test1Filter

    package com.neilxu.train.gateway.config;import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;@Component
    public class Test1Filter implements GlobalFilter, Ordered {private static final Logger LOG = LoggerFactory.getLogger(Test1Filter.class);@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {LOG.info("Test1Filter");return chain.filter(exchange);}@Overridepublic int getOrder() {return 0;}
    }
    
  • Test2Filter

    com.neilxu.train.gateway.config.Test2Filter

    package com.neilxu.train.gateway.config;import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;@Component
    public class Test2Filter implements GlobalFilter, Ordered {private static final Logger LOG = LoggerFactory.getLogger(Test2Filter.class);@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {LOG.info("Test2Filter");return chain.filter(exchange);}@Overridepublic int getOrder() {return 1;}
    }
    
  • 重启测试

在这里插入图片描述

7.为gateway增加登录校验拦截器

自动刷新maven依赖:

在这里插入图片描述

  • 增加依赖

    gateway的 pom文件

    <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId>
    </dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional>
    </dependency>
    
  • 增加拦截器

    package com.neilxu.train.gateway.config;import com.neilxu.train.gateway.util.JwtUtil;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;@Component
    public class LoginMemberFilter implements Ordered, GlobalFilter {private static final Logger LOG = LoggerFactory.getLogger(LoginMemberFilter.class);@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String path = exchange.getRequest().getURI().getPath();// 排除不需要拦截的请求if (path.contains("/admin")|| path.contains("/hello")|| path.contains("/member/member/login")|| path.contains("/member/member/send-code")) {LOG.info("不需要登录验证:{}", path);return chain.filter(exchange);} else {LOG.info("需要登录验证:{}", path);}// 获取header的token参数String token = exchange.getRequest().getHeaders().getFirst("token");LOG.info("会员登录验证开始,token:{}", token);if (token == null || token.isEmpty()) {LOG.info( "token为空,请求被拦截" );exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);return exchange.getResponse().setComplete();}// 校验token是否有效,包括token是否被改过,是否过期boolean validate = JwtUtil.validate(token);if (validate) {LOG.info("token有效,放行该请求");return chain.filter(exchange);} else {LOG.warn( "token无效,请求被拦截" );exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);return exchange.getResponse().setComplete();}}/*** 优先级设置  值越小  优先级越高** @return*/@Overridepublic int getOrder() {return 0;}
    }
    

    这里将JwtUtil从common复制了一个到gateway,不再引入common依赖

    小tips:

    ctrl + alt + t :可以给选中的代码块加try catch

  • 测试

    GET http://localhost:8000/member/member/count
    Accept: application/json
    token: 123###
    

在这里插入图片描述

在这里插入图片描述

8.为axios增加登录相关拦截:请求时带上token,返回时校验返回码是不是401

  • 修改main.js

    web/src/main.js

    /*** axios拦截器*/
    axios.interceptors.request.use(function (config) {console.log('请求参数:', config);const _token = store.state.member.token;if (_token) {config.headers.token = _token;console.log("请求headers增加token:", _token);}return config;
    }, error => {return Promise.reject(error);
    });
    axios.interceptors.response.use(function (response) {console.log('返回结果:', response);return response;
    }, error => {console.log('返回错误:', error);const response = error.response;const status = response.status;if (status === 401) {// 判断状态码是401 跳转到登录页+console.log("未登录或登录超时,跳到登录页");store.commit("setMember", {});notification.error({description: "未登录或登录超时"});router.push('/login');}return Promise.reject(error);
    });
    
  • 修改控台页面

    <template><a-layout id="components-layout-demo-top-side-2"><the-header-view></the-header-view><a-layout><the-sider-view></the-sider-view><a-layout style="padding: 0 24px 24px"><a-breadcrumb style="margin: 16px 0"><a-breadcrumb-item>Home</a-breadcrumb-item><a-breadcrumb-item>List</a-breadcrumb-item><a-breadcrumb-item>App</a-breadcrumb-item></a-breadcrumb><a-layout-content:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">所有会员总数:{{ count }}</a-layout-content></a-layout></a-layout></a-layout>
    </template>
    <script>
    import {defineComponent, ref} from 'vue';
    import TheHeaderView from "@/components/the-header";
    import TheSiderView from "@/components/the-sider";
    import axios from "axios";
    import {notification} from "ant-design-vue";
    import store from "@/store";export default defineComponent({name: "main-view",components: {TheSiderView,TheHeaderView,},setup() {const count = ref(0);axios.get("/member/member/count").then((response) => {let data = response.data;if (data.success) {count.value = data.content;} else {notification.error({description: data.message});}});return {count};},
    });
    </script>
    <style>
    #components-layout-demo-top-side-2 .logo {float: left;width: 120px;height: 31px;margin: 16px 24px 16px 0;background: rgba(255, 255, 255, 0.3);
    }.ant-row-rtl #components-layout-demo-top-side-2 .logo {float: right;margin: 16px 0 16px 24px;
    }.site-layout-background {background: #fff;
    }
    </style>
    
  • 测试

在这里插入图片描述

在这里插入图片描述

注意:

可能由于axios版本问题,这里会出现页面直接把401报错展示到顶层,如图所示

在这里插入图片描述

解决办法:

查看web前端目录下vue.config.js配置文件,如配置文件入下:

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({transpileDependencies: true
})

则增加如下配置即可关闭问题中所示的错误提示界面:

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({transpileDependencies: true,// 其他配置项devServer:{client:{overlay: false}}
})

9.为路由页面增加登录拦截,访问所有的控台页面都需要登录

  • 修改路由js

    import { createRouter, createWebHistory } from 'vue-router'
    import store from "@/store";
    import {notification} from "ant-design-vue";const routes = [{path: '/login',component: () => import('../views/login.vue')},{path: '/',component: () => import('../views/main.vue'),meta: {loginRequire: true},}
    ]const router = createRouter({history: createWebHistory(process.env.BASE_URL),routes
    })// 路由登录拦截
    router.beforeEach((to, from, next) => {// 要不要对meta.loginRequire属性做监控拦截if (to.matched.some(function (item) {console.log(item, "是否需要登录校验:", item.meta.loginRequire || false);return item.meta.loginRequire})) {const _member = store.state.member;console.log("页面登录校验开始:", _member);if (!_member.token) {console.log("用户未登录或登录超时!");notification.error({ description: "未登录或登录超时" });next('/login');} else {next();}} else {next();}
    });export default router
    
  • 测试

    直接访问控台页面——“/"

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/795160.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

分享webgl魔幻星球

界面截图 webgl 是在网页上绘制和渲染三维图形的技术&#xff0c;可以让用户与其进行交互。divcss、canvas 2d 专注于二维图形。 对公司而言&#xff0c;webgl 可以解决他们在三维模型的显示和交互上的问题&#xff1b;对开发者而言&#xff0c;webgl 可以让我们是实现更多、更…

[从零开始学习Redis | 第九篇] 深入了解Redis数据类型

前言&#xff1a; 在现代软件开发中&#xff0c;数据存储和处理是至关重要的一环。为了高效地管理数据&#xff0c;并实现快速的读写操作&#xff0c;各种数据库技术应运而生。其中&#xff0c;Redis作为一种高性能的内存数据库&#xff0c;广泛应用于缓存、会话存储、消息队列…

栈的ADT实现——有空间限制的栈

1、研究有空间限制的栈的原因 当我们使用很多软件时都有类似“undo”功能,比如Web浏览器的回退功能、文本编辑器的撤销编辑功能。这些功能都可以使用Stack实现,但是在现实中浏览器的回退功能也好,编辑器的撤销功能也好,都有一定的数量限制。因此我们需要的不是一个普通的Sta…

齐护机器人方位传感器指南针罗盘陀螺仪

一、方位传感器原理及功能说明 齐护方位传感器是一款集成了三轴磁传感器芯片的方位传感器模块。适用于无人机、机器人、移动和个人手持设备中的罗盘&#xff08;指南针&#xff09;、导航和游戏等高精度应用。模块可以感应XYZ平面角度外&#xff0c;还可实现1至2的水平面角度罗…

【精品教程】护网HVV实战教程资料合集(持续更新,共20节)

以下是资料目录&#xff0c;如需下载&#xff0c;请前往星球获取&#xff1a; 01-HW介绍.zip 02-HTTP&Burp课程资料.zip 03-信息收集_3.zip 04-SQL注入漏洞_2.zip 05-命令执行漏洞.zip 06-XSS漏洞.zip 07-CSRF.zip 08-中间件漏洞.zip 09-SSRF.zip 10-XXE.zip 11-Java反序列…

用栈实现队列-使用两个栈来实现队列,则使我们插入的元素依照先入先出原则即可

一、用栈实现队列 https://leetcode.cn/problems/implement-queue-using-stacks/ &#xff08;一&#xff09;分析题目 &#xff08;二&#xff09;编写代码 typedef char STDataType; typedef struct Stack {STDataType* a; //int top; //相当于数组下标&#xff0c;注意…

idea 中 大于等于,不等于、小于等于等等这些符号发生了改变问题解决方法

1.问题描述 idea 中&#xff01;变为 ≠、 >变成了≥、<变成了 ≤ 等问题的解决办法 展示效果如下截图 解决方法

Spark 部署与应用程序交互简单使用说明

文章目录 前言步骤一&#xff1a;下载安装包Spark的目录和文件 步骤二&#xff1a;使用Scala或PySpark Shell本地 shell 运行 步骤3:理解Spark应用中的概念Spark Application and SparkSessionSpark JobsSpark StagesSpark Tasks 转换、立即执行操作和延迟求值窄变换和宽变换 S…

设计模式总结-外观模式(门面模式)

外观模式 模式动机模式定义模式结构外观模式实例与解析实例一&#xff1a;电源总开关实例二&#xff1a;文件加密 模式动机 引入外观角色之后&#xff0c;用户只需要直接与外观角色交互&#xff0c;用户与子系统之间的复杂关系由外观角色来实现&#xff0c;从而降低了系统的耦…

leetcode.面试题 02.07. 链表相交

题目 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 思路 假a在链表A上移动,b在链表B上移动&#xff0c;a移动完在B上开始&…

mysql中主键索引和联合索引的原理解析

mysql中主键索引和联合索引的原理解析 一、主键索引二、什么是联合索引? 对应的B树是如何生成的?1、建立索引方式2、什么是最左前缀原则?3、回表4、为什么要遵守最左前缀原则才能利用到索引?5、什么是覆盖索引?6、索引扫描底层原理7、order by为什么会导致索引消失&#x…

OA系统:把复杂流程和操作简单化,十分考验设计对业务的理解。

面向企业端的管理系统功能是越来越臃肿&#xff0c;每个厂商都想把功能做的大而全&#xff0c;如果不这么做就会给你留下市场空挡给竞争对手&#xff0c;在这种复杂化不可逆转的情形下&#xff0c;如何用操作简单化呢&#xff0c;本文给出几点建议。 1. 简化流程&#xff1a; …

opencv使用问题记录一二

opencv介绍 opencv是一个计算机视觉处理软件库&#xff0c;拥有强大的功能和高效的性能。 但是由于早期版本的原因&#xff0c;存在一些与目前主流使用不兼容的问题 问题与解决 RGB通道顺序 一般图片处理类库的通道顺序就是RGB&#xff0c;但是opencv的是反过来的&#xf…

RabbitMQ小记

参考书籍&#xff1a;朱忠华的《RabbitMQ实战指南》 一、基础概念 1.Exchange 1.1 创建方法的参数&#xff0c;exchangeDeclare() exchange&#xff1a;交换器的名称type&#xff1a;交换器的类型durable&#xff1a;是否持久化&#xff0c;true代表持久化。&#xff08;持…

【APUE】网络socket编程温度采集智能存储与上报项目技术------多路复用

作者简介&#xff1a; 一个平凡而乐于分享的小比特&#xff0c;中南民族大学通信工程专业研究生在读&#xff0c;研究方向无线联邦学习 擅长领域&#xff1a;驱动开发&#xff0c;嵌入式软件开发&#xff0c;BSP开发 作者主页&#xff1a;一个平凡而乐于分享的小比特的个人主页…

Git命令(1)[删除,恢复与移动]

文章目录 1.删除文件1.1命令----rm <filename>1.2命令----git rm <filename>1.1命令----git rm <filename> -f 2.恢复文件2.1命令----git restore <filename>2.1命令----git restore --staged <filename> 3.重命名文件3.1命令----mv 旧文件 新文…

八股面试速成—Java语法部分

暑期实习面试在即&#xff0c;这几天八股和算法轮扁我>_ 八股部分打算先找学习视屏跟着画下思维导图&#xff0c;然后看详细的面试知识点&#xff0c;最后刷题 其中导图包含的是常考的题&#xff0c;按照思维导图形式整理&#xff0c;会在复盘后更新 细节研究侧重补全&a…

租用阿里云的服务器多少钱?30元、61元、99元、165元、199元

租个阿里云的服务器多少钱&#xff1f;很便宜&#xff0c;云服务器2核2G3M固定带宽99元一年、2核4G服务器30元3个月、199元一年&#xff0c;轻量应用服务器2核2G3M配置61元一年、2核4G4M带宽165元一年&#xff0c;可以在阿里云CLUB中心查看 aliyun.club 当前最新的优惠券和活动…

GitOps - 为 OpenShift GitOps 配置邮件通知

《OpenShift 4.x HOL教程汇总》 说明&#xff1a;本文已经 在OpenShift 4.15 OpenShift GitOps 1.11.2 环境中验证 文章目录 ArgoCD 的 Notification 功能简介启动 OpenShift GitOps 的 Notification 功能配置邮件通知验证参考 说明&#xff1a;先根据《OpenShift 4 之 GitOp…

学习 Git 基础知识 - 日常开发任务手册

欢迎来到我关于 Git 的综合指南&#xff0c;Git 是一种分布式版本控制系统&#xff0c;已经在软件开发中彻底改变了协作和代码管理方式。 无论你是经验丰富的开发者还是刚开始编程之旅的新手&#xff0c;理解 Git 对于正确掌控代码、高效管理项目和与他人合作至关重要。 在本…