文章目录
- **1\. 准备代码,提交到码云Git库**
- 2. 安装JAVA 运行环境
- 3. 安装maven
- 4. 安装git
- 5. 安装docker
- 6. 安装Jenkins
- 7. 初始化 Jenkins 插件和管理员用户
- 9.1点击创建一个新任务,进入创建项目类型选择页面
- 9.2配置“General”
- 9.3配置“源码管理”
- 9.4构建作业
- 9.5构建
- 9.5.1执行构建过程
- 9.5.2构建结构
- 9.5.3查看控制台输出
谷粒学苑
常见商业模式
B2C: 两个角色,管理员和普通用户
管理员:添加、修改、删除
普通用户:查询
核心模块:课程模块
B2B2C: 商家到商家再到用户,例如:京东
角色: 普通用户、可以买自营、可以买普通商家
项目功能模块
Mybatis-plus
前期准备
添加依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </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> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> </dependencies>
设置启动类并加入mapper包扫描
@SpringBootApplication @MapperScan("com.atguigu.springbootmybatisplus.mapper") public class SpringbootMybatisPlusApplication { public static void main(String[] args) { SpringApplication.run(SpringbootMybatisPlusApplication.class, args); } }
建表
CREATE TABLE user ( id BIGINT(20) NOT NULL COMMENT '主键ID', name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', age INT(11) NULL DEFAULT NULL COMMENT '年龄', email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (id) ); INSERT INTO user (id, name, age, email) VALUES (1, 'Jone', 18, 'test1@baomidou.com'), (2, 'Jack', 20, 'test2@baomidou.com'), (3, 'Tom', 28, 'test3@baomidou.com'), (4, 'Sandy', 21, 'test4@baomidou.com'), (5, 'Billie', 24, 'test5@baomidou.com');
写实体类
@Data @Getter @Setter @ToString public class User { private Long id; private String name; private Integer age; private String email; }
写mapper类
public interface UserMapper extends BaseMapper<User> { }
查询测试
@Test void testSelectList(){ List<User> users = userMapper.selectList(null); for (User user : users) { System.out.println(user); } }
配置字段自动填充
添加一个字段
实体类标注填充的字段
@TableField(fill = FieldFill.INSERT) private Date createTime;
配置自动自动填充类MyMetaObjectHandler
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.setFieldValByName("createTime",new Date(),metaObject); } @Override public void updateFill(MetaObject metaObject) { } }
测试
@Test void testInsert(){ User user = new User(); user.setName("小赵"); user.setAge(18); user.setEmail("2676580540@qq.com"); userMapper.insert(user); }
实现效果
配置乐观锁和分页插件
乐观锁
乐观锁实现原理:
实际上是在表中增加了一个version字段作为版本控制,version初值为1,当进行update操作时候,会先根据id查询出这一条记录,然后再进行更新操作,更新的时候判断查询出的version和当前表的version是否相同,如果相同则进行更新并且version+1,不相同则回滚。他人同时进行更新的时候,会拿自己查询出的version和表中version进行比较。如果相同则进行更新并且version+1,不相同则回滚。
写MybatisPlusConfig配置类
@EnableTransactionManagement @Configuration @MapperScan("com.atguigu.springbootmybatisplus.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }
给表添添加一个version字段
ALTER TABLE `user` ADD COLUMN `version` INT
在实体类中标注版本控制的字段
@Version @TableField(fill = FieldFill.INSERT) private Integer version;
设置插入时自动填充version = 1
测试乐观锁成功的情况
/** * 测试乐观锁成功的情况 */ @Test void testOptimisticLocker(){ User user = userMapper.selectById(1654374884886777857L); System.out.println(user); user.setName("xiaozhao"); //user.setVersion(user.getVersion()-1); userMapper.updateById(user); }
测试模拟并发时已经修改的数据
/** * 测试乐观锁成功的情况 */ @Test void testOptimisticLocker(){ User user = userMapper.selectById(1654374884886777857L); System.out.println(user); user.setName("xiaozhao"); user.setVersion(user.getVersion()-1); userMapper.updateById(user); }
分页实现
添加配置
分页查询
@Test void testPage(){ Page<User> page = new Page<>(1,5); userMapper.selectPage(page,null); page.getRecords().forEach(System.out::println); System.out.println("page.getCurrent():"+page.getCurrent()); System.out.println("page.getPages():"+page.getPages()); System.out.println("page.getSize():"+page.getSize()); System.out.println("page.getTotal():"+page.getTotal()); System.out.println("page.hasNext():"+page.hasNext()); System.out.println("page.hasPrevious():"+page.hasPrevious()); }
实现效果
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@719d35e8] User(id=1, name=JoneEdit, age=18, email=test1@baomidou.com, createTime=null, version=1) User(id=2, name=Jack, age=20, email=test2@baomidou.com, createTime=null, version=null) User(id=3, name=Tom, age=28, email=test3@baomidou.com, createTime=null, version=null) User(id=4, name=Sandy, age=21, email=test4@baomidou.com, createTime=null, version=null) User(id=5, name=Billie, age=24, email=test5@baomidou.com, createTime=null, version=null) page.getCurrent():1 page.getPages():2 page.getSize():5 page.getTotal():7 page.hasNext():true page.hasPrevious():false
逻辑删除
添加一个is_deleted字段
配置application.yml逻辑删除的属性
实体类中属性做逻辑删除标识
@ApiModelProperty(value = "逻辑删除 1(true)已删除, 0(false)未删除") @TableLogic @TableField(fill = FieldFill.INSERT) private Integer isDeleted;
插入时自动填充配置
测试删除一条记录
测试前
@RunWith(SpringRunner.class) @SpringBootTest(classes = EduApplication.class) public class MyTest { @Autowired private EduCourseMapper courseMapper; @Test public void testdemo01(){ courseMapper.deleteById("1655898309098749953"); System.out.println("springboot单元测试"); } }
测试后
异常处理
首先创建一个统一异常处理类
/** * 统一异常处理的类 */ @ControllerAdvice public class GlobalExceptionHandler {}
之后分别加入
当出现特定异常的时候则会被特定异常所捕获
//全局异常 @ExceptionHandler(Exception.class) @ResponseBody public R error(Exception e){ e.printStackTrace(); return R.error().message("出现了异常"); }
//特定异常 @ExceptionHandler(ArithmeticException.class) @ResponseBody public R error(ArithmeticException e){ e.printStackTrace(); return R.error().message("出现了特定异常 被除数不能为0"); }
//自定义异常 @ExceptionHandler(GuliException.class) @ResponseBody public R error(GuliException e){ e.printStackTrace(); return R.error().message(e.getMsg()).code(e.getCode()); }
配置自定义异常同时也要配置自定义异常处理类
@Data @AllArgsConstructor @NoArgsConstructor public class GuliException extends RuntimeException{ @ApiModelProperty(value = "状态码") private Integer code; //异常代码 private String msg; //异常信息 }
模拟异常,抛出自定义异常
try{ int i = 1/0; }catch (Exception e){ throw new GuliException(20001,"出现了自定义异常"); }
SpringBoot启动时设置不加载数据库
在启动类上添加
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
SpringBoot @Value用法
yml中声明一些常量
aliyun: oss: file: endpoint: oss-cn-beijing.aliyuncs.com keyid: xxxxx keysecret: xxxxx bucketname: guli-file-xiaozhao
读取yml配置文件
@Component public class ConstantYmlUtils { //读取配置文件中内容 @Value("${aliyun.oss.file.endpoint.endpoint}") private String endpoint; @Value("${aliyun.oss.file.endpoint.keyid}") private String keyid; @Value("${aliyun.oss.file.endpoint.keysecret}") private String keysecret; @Value("${aliyun.oss.file.endpoint.bucketname}") private String bucketname; }
注意:如果不加${}则直接将“”中内容赋值
SpringBoot整合单元测试
关于SpringBoot单元测试找不到Mapper和Service报java.lang.NullPointerException的错误
根据SpringBoot版本引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
设置单元测试
注意添加以下两个注解,EduApplication是主启动类
@RunWith(SpringRunner.class)
@SpringBootTest(classes = EduApplication.class)
import org.junit.Test; @RunWith(SpringRunner.class) @SpringBootTest(classes = EduApplication.class) public class MyTest { @Autowired private EduCourseMapper courseMapper; @Test public void testdemo01(){ courseMapper.deleteById("1655898309098749953"); System.out.println("springboot单元测试"); } }
运行
SpringBoot 整合Redis
下载
windows https://github.com/MicrosoftArchive/redis/releases
linux https://download.redis.io/releases/
linux修改配置文件:
- redis.conf 中 daemonize no 控制是前台运行还是后台运行
- requirepass 123456 修改是否启用密码
- bind 127.0.0.1 开启则代表只允许本地访问
- protected-mode no 关闭redis的保护模式,如果别的机器要调用的时候需要关闭保护模式
启动 (Win10)
服务端
E:\soft\Redis-x64-3.2.100>redis-server.exe redis.windows.conf _._ _.-``__ ''-._ _.-`` `. `_. ''-._ Redis 3.2.100 (00000000/0) 64 bit .-`` .-```. ```\/ _.,_ ''-._ ( ' , .-` | `, ) Running in standalone mode |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379 | `-._ `._ / _.-' | PID: 22452 `-._ `-._ `-./ _.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | http://redis.io `-._ `-._`-.__.-'_.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | `-._ `-._`-.__.-'_.-' _.-' `-._ `-.__.-' _.-' `-._ _.-' `-.__.-' [22452] 15 May 16:02:56.192 # Server started, Redis version 3.2.100 [22452] 15 May 16:02:56.194 * DB loaded from disk: 0.000 seconds [22452] 15 May 16:02:56.195 * The server is now ready to accept connections on port 6379
客户端连接
E:\soft\Redis-x64-3.2.100>redis-cli.exe 127.0.0.1:6379> keys * 1) "famousTeachersAndHotCourse::selectIndexList" 127.0.0.1:6379> a
==============================================================================================================
如果开启了密码
requirepass 123456
重启服务端
客户端连接
E:\soft\Redis-x64-3.2.100>redis-cli.exe 127.0.0.1:6379> keys * (error) NOAUTH Authentication required. 127.0.0.1:6379> auth 123456 OK 127.0.0.1:6379> keys * (empty list or set) 127.0.0.1:6379>
Redis的数据类型
Redis存储的是key-value结构的数据,其中key是字符串类型,value有五种常用的数据类型
- 字符串 : 普通字符串,常用
- 哈希 : 适合存储对象
- 列表 : list按照插入顺序排序,可以有重复元素
- 集合 : set无序集合,没有重复元素
- 有序集合 : sorted set 有序集合,没有重复元素
Redis是当前比较热门的NoSql系统之一,它是一个开源的使用ANSI c语言编写的key-value存储系统。与Memcache类似,但是弥补了Memcache的很多不足之处。与Memcache不同的地方在于,Memcache只能将数据写到内存中,不能实现数据同步到硬盘实现持久化,redis则可以定期的将数据存储到硬盘中,实现数据的持久化。
特点:
- redis读取速度是110000次/s 写的速度是81000次/s
- redis的操作都是原子性的
- 支持多种数据结构:string、list、hash、set、zset
一般将经常查询,不经常修改的数据,不是特别重要的数据放到redis作为缓存
使用
在common模块引入依赖
<!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--spring2.X集成redis所需common-pool2--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.6.0</version> </dependency>
写redis的配置文件 (这里需要注意的是需要在启动类中开启包扫描确定可以扫描到common模块中redis的配置类)
@EnableCaching @Configuration public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); template.setConnectionFactory(factory); //key序列化方式 template.setKeySerializer(redisSerializer); //value序列化 template.setValueSerializer(jackson2JsonRedisSerializer); //value hashmap序列化 template.setHashValueSerializer(jackson2JsonRedisSerializer); return template; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); //解决查询缓存转换异常的问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // 配置序列化(解决乱码的问题),过期时间600秒 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(600)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); return cacheManager; } }
配置application.propertities/ yml
# 配置redis spring.redis.host=localhost spring.redis.port=6379 spring.redis.database= 0 spring.redis.timeout=1800000 spring.redis.lettuce.pool.max-active=20 spring.redis.lettuce.pool.max-wait=-1 #最大阻塞等待时间(负数表示没限制) spring.redis.lettuce.pool.max-idle=5 spring.redis.lettuce.pool.min-idle=0
在service层加入@Cacheable注解
@Cacheable(key = "'selectIndexList'",value = "banner") @Override public List<CrmBanner> getAllList() { LambdaQueryWrapper<CrmBanner> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.orderByAsc(CrmBanner::getSort); List<CrmBanner> list = baseMapper.selectList(null); return list; }
controller
@GetMapping("/getlist") public R getList(){ List<CrmBanner> list = bannerService.getAllList(); return R.ok().data("rows",list); }
测试
查看
注意:当第一次查询这个接口数据的时候会执行sql语句,当第二次查询数据的时候就不在执行sql语句了,因为数据已经缓存在redis中。
关于设置redis中数据过期时间的问题
可以在redis的配置类config中设置,例如:
最后
@CachePut 使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。
@CacheEvict 使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上
@CachePut(value = "banner", allEntries=true) @Override public void saveBanner(CrmBanner banner) { baseMapper.insert(banner); } @CacheEvict(value = "banner", allEntries=true) @Override public void updateBannerById(CrmBanner banner) { baseMapper.updateById(banner); }
RedisTemplate
@Autowired private RedisTemplate<String,String> redisTemplate; //从redis获取验证码,如果获取到直接返回 String code = redisTemplate.opsForValue().get(phone); redisTemplate.opsForValue().set(phone,newCode,5, TimeUnit.MINUTES);
阿里云OSS文件上传
创建一个oss桶guli-file-xiaozhao
1、引入依赖
<!--aliyunOSS--> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.10.2</version> </dependency> <!--格式化时间工具用于获取本地时间 用法:String datePath = new DateTime().toString("yyyy/MM/dd");--> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> </dependency>
2、创建一个工具类用于获取常量的值
@Component public class ConstantYmlUtils implements InitializingBean { //读取配置文件中内容 @Value("${aliyun.oss.file.endpoint}") private String endpoint; @Value("${aliyun.oss.file.keyid}") private String keyid; @Value("${aliyun.oss.file.keysecret}") private String keysecret; @Value("${aliyun.oss.file.bucketname}") private String bucketname; //定义静态常量 public static String ENDPOINT; public static String KEYID; public static String KEYSECRET; public static String BUCKETNAME; @Override public void afterPropertiesSet() throws Exception { ENDPOINT = endpoint; KEYID = keyid; KEYSECRET = keysecret; BUCKETNAME = bucketname; } }
3、写service
package com.atguigu.oss.service; public interface OssService { String uploadFileAvatar(MultipartFile file); }
@Service public class OssServiceImpl implements OssService { @Override public String uploadFileAvatar(MultipartFile file) { String endpoint = ConstantYmlUtils.ENDPOINT; String keyid = ConstantYmlUtils.KEYID; String keysecret = ConstantYmlUtils.KEYSECRET; String bucketname = ConstantYmlUtils.BUCKETNAME; // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, keyid, keysecret); try { InputStream inputStream = null; try { inputStream = file.getInputStream(); } catch (IOException e) { e.printStackTrace(); } // 创建PutObjectRequest对象。 String fileName = file.getOriginalFilename(); //在文件名称中添加一个随机的唯一的一个值 String uuid = UUID.randomUUID().toString().replace("-",""); fileName = uuid + fileName; //把文件按照日期进行分类 String datePath = new DateTime().toString("yyyy/MM/dd"); //拼接 fileName = datePath +"/"+ fileName; PutObjectRequest putObjectRequest = new PutObjectRequest(bucketname, fileName, inputStream); // 设置该属性可以返回response。如果不设置,则返回的response为空。 putObjectRequest.setProcess("true"); // 创建PutObject请求。 PutObjectResult result = ossClient.putObject(putObjectRequest); // 如果上传成功,则返回200。 System.out.println(result.getResponse().getStatusCode()); String url = "https://guli-file-xiaozhao.oss-cn-beijing.aliyuncs.com/"+fileName; return url; } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason."); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network."); System.out.println("Error Message:" + ce.getMessage()); } finally { if (ossClient != null) { ossClient.shutdown(); } } return null; } }
4、写controller
@RestController @RequestMapping("/eduoss/fileoss") @CrossOrigin @Api(value = "用于文件上传到阿里云的接口") public class OssController { @Autowired private OssService ossService; //上传头像的方法 @PostMapping("uploadOssFile") public R uploadOssFile(MultipartFile file){ //获取上传的文件 String url = ossService.uploadFileAvatar(file); return R.ok().data("url",url); } }
测试
注意:可以参考官方sdk文档 https://help.aliyun.com/document_detail/84781.html?spm=a2c4g.32009.0.0.15e6c927P7Kpd4
Nginx配置
修改nginx配置文件
server { # 监听的端口 listen 9001; server_name localhost; location ~ /eduservice { proxy_pass http://localhost:8001; } location ~ /eduoss { proxy_pass http://localhost:8002; } }
nginx监听9001端口,对路径中存在eduservice和eduoss的路径进行转发,注意后端接口中eduservice和eduoss是唯一的
使用EasyExcel工具对Excel读写
EasyExcel是阿里巴巴开源的一个操作excel的工具
引入依赖
<!--xls--> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.1.1</version> </dependency>
写操作
创建实体类
@Data public class DemoData { //设置excel表头的名称 @ExcelProperty(value = "学号",index = 0) private Integer sno; @ExcelProperty(value = "姓名",index = 1) private String sname; }
调用Excel方法写文件
public static void main(String[] args) { //实现excel的写操作 //设置写入的文件路径 String filename = "E:\\MyProject\\javaProject\\guli_parent\\write01.xlsx"; //调用excel的写方法进行操作 EasyExcel.write(filename,DemoData.class).sheet("学生列表").doWrite(getData()); } //创建一个方法,返回一个list集合 public static List<DemoData> getData(){ List<DemoData> data = new ArrayList<>(); for (int i = 0; i < 10; i++) { DemoData demoData = new DemoData(); demoData.setSno(i); demoData.setSname("sname"+i); data.add(demoData); } return data; }
实现效果
读操作
创建实体
@Data public class DemoData { //设置excel表头的名称,第一个参数是列名称,第二个参数是第几列 @ExcelProperty(value = "学号",index = 0) private Integer sno; @ExcelProperty(value = "姓名",index = 1) private String sname; }
创建excel监听器
public class ExcelListener extends AnalysisEventListener<DemoData> { //一行一行的读取excel中的内容 @Override public void invoke(DemoData data, AnalysisContext analysisContext) { System.out.println("********"+data); } //读取表头的方法 public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { System.out.println("*******表头:"+headMap); } //读取完成之后要做的事情 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { } }
调用方法
@Test public void readTest(){ String filename = "E:\\MyProject\\javaProject\\guli_parent\\write.xlsx"; EasyExcel.read(filename,DemoData.class,new ExcelListener()).sheet().doRead(); }
实现效果
Mybatis 多级分类查询
表数据结构
扫描mapper.xml
开启mapper扫描
vo
@Data public class EduOneSubjectVO { @ApiModelProperty(value = "课程类别ID") private String id; @ApiModelProperty(value = "类别名称") private String label; @ApiModelProperty(value = "二级分类list内容") private List<EduOneSubjectVO> children = new ArrayList<>(); }
mapper
<mapper namespace="com.atguigu.eduservice.mapper.EduSubjectMapper"> <resultMap id="queryAllSubjectMap" type="com.atguigu.eduservice.entity.vo.EduOneSubjectVO"> <id property="id" column="oneId" /> <result property="label" column="oneTitle"/> <collection property="children" ofType="com.atguigu.eduservice.entity.vo.EduOneSubjectVO"> <id property="id" column="twoId"/> <result property="label" column="twoTitle"/> </collection> </resultMap> <select id="queryAllSubject" resultType="list" resultMap="queryAllSubjectMap"> SELECT o.id as oneId,o.title as oneTitle,t.id as twoId,t.title as twoTitle FROM edu_subject o LEFT JOIN edu_subject t on o.id = t.parent_id WHERE o.parent_id = 0 </select>
service
List<EduOneSubjectVO> queryAllSubject();
impl
@Override public List<EduOneSubjectVO> queryAllSubject() { List<EduOneSubjectVO> eduOneSubjectVOS = subjectMapper.queryAllSubject(); Collections.sort(eduOneSubjectVOS, new Comparator<EduOneSubjectVO>() { @Override public int compare(EduOneSubjectVO o1, EduOneSubjectVO o2) { return o1.getChildren().size() - o2.getChildren().size() >= 0? -1:1; } }); return eduOneSubjectVOS; }
controller
@GetMapping("getAllSubject") @ApiOperation(value = "获取课程分类") public R getAllSubject(){ List<EduOneSubjectVO> list = subjectService.queryAllSubject(); return R.ok().data("list",list); }
目录结构
测试效果
mybatis使用内部类处理一对多类型数据2
当一对多关系时,需要把多的那个数据传入到一个
例如: 需要获取用户id和模板id,一个租户id 可以创建多个模板,所以租户和模板是一对多的关系,为了减少创建实体类,使用内部类存储模板id和模板名称,然后存储到list集合中。
实体类
@Data public class TenantAndTemplateId { private String tenantId; private String tenantName; private List<TemplateId> templateIds; //注意需要用static修饰 @Data static class TemplateId { private String templateId; private String templateName; }
sql语句 租户表为主表,左连接模板表,此时会有查询到同一个租户有多个模板,我们将多的模板信息,封装到 List templateIds 这个集合中;
select template_id, template_name, tenant.tenant_id, tenant_name from tenant left join template on tenant.tenant_id = template.tenant_id
使用mybatis 的xml 整理映射关系是,我们使用 collection 标签映射内部类 ,propertyd对应参数 templateIds,javaType 对应 templateIds参数的类型为list, 此处需要注意 ofType 对应的是TenantAndTemplateId 实体类的路径,后面使用$连接内部类名称即可实现内部类关系的映射
测试接口返回结果
注意点
01:resultType后面的内部类用$符号连接;
02:内部类必须有无参构造函数;
03:内部类必须为静态类有static修饰;
阿里云视频点播功能
API和SDK的区别:
- API是阿里云提供了一个固定的地址,需要向这个地址发送固定的参数,实现功能
- SDK的底层是API,SDK是对API的方式进行了封装,使用了sdk可以更方便的调用功能实现,调用阿里云提供的类或者接口中的方法实现功能就是SDK
配置
application.propertites
#阿里云 vod #不同的服务器,地址不同 aliyun.vod.file.keyid=xxxx aliyun.vod.file.keysecret=xxxx aliyun.vod.file.regionId = cn-shanghai # 最大上传单个文件大小:默认1M spring.servlet.multipart.max-file-size=1024MB # 最大置总上传的数据大小 :默认10M spring.servlet.multipart.max-request-size=1024MB
ConstantVodUtils
@Component public class ConstantVodUtils implements InitializingBean { @Value("${aliyun.vod.file.keyid}") private String keyId; @Value("${aliyun.vod.file.keysecret}") private String keySecret; @Value("${aliyun.vod.file.regionId}") private String regionId; public static String ACCESS_KEY_SECRET; public static String ACCESS_KEY_ID; public static String REGION_ID; @Override public void afterPropertiesSet() throws Exception { ACCESS_KEY_ID = keyId; ACCESS_KEY_SECRET = keySecret; REGION_ID = regionId; } }
InitVodClient
public class InitVodClient { //填入AccessKey信息 public static DefaultAcsClient initVodClient(String regionId,String accessKeyId, String accessKeySecret) throws ClientException { DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret); DefaultAcsClient client = new DefaultAcsClient(profile); return client; } }
转发nginx
配置nginx
http { # 客户端上传文件最大容量 client_max_body_size 1024m; }
location ~ /eduvod { proxy_pass http://localhost:8003; }
视频上传
后端接口
service
String uploadVideoAliy(MultipartFile file); void removeVideo(String videoId);
@Override public String uploadVideoAliy(MultipartFile file) { String videoId = null; try { //上传后显示的名称 String title = file.getOriginalFilename(); //上传文件的原始名称 String fileName = file.getOriginalFilename(); //fileName = fileName.substring(0,fileName.lastIndexOf('.')); System.out.println(fileName); InputStream inputStream = file.getInputStream(); UploadStreamRequest request = new UploadStreamRequest(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET, title, fileName, inputStream); request.setApiRegionId("cn-shanghai"); UploadVideoImpl uploader = new UploadVideoImpl(); UploadStreamResponse response = uploader.uploadStream(request); System.out.print("RequestId=" + response.getRequestId() + "\n"); //请求视频点播服务的请求ID if (response.isSuccess()) { System.out.print("VideoId=" + response.getVideoId() + "\n"); videoId = response.getVideoId(); } else { //如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因 System.out.print("VideoId=" + response.getVideoId() + "\n"); System.out.print("ErrorCode=" + response.getCode() + "\n"); System.out.print("ErrorMessage=" + response.getMessage() + "\n"); videoId = response.getVideoId(); } }catch (Exception e){ e.printStackTrace(); } return videoId; } @Override public void removeVideo(String videoId) { //删除云端视频 try{ DefaultAcsClient client = InitVodClient.initVodClient(ConstantVodUtils.REGION_ID,ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET); DeleteVideoRequest request = new DeleteVideoRequest(); request.setVideoIds(videoId); DeleteVideoResponse response = client.getAcsResponse(request); System.out.print("RequestId = " + response.getRequestId() + "\n"); }catch (com.aliyuncs.exceptions.ClientException e){ throw new GuliException(20001, "视频删除失败"); } }
controller
@Autowired private VodService vodService; //上传视频到阿里云 @PostMapping("uploadAliyunVideo") public R uploadAliyunVideo(MultipartFile file){ String videoId = vodService.uploadVideoAliy(file); return R.ok().data("videoId",videoId); } /** * 删除阿里云视频根据videoId */ @DeleteMapping("removeAlyVideo/{id}") public R removeAlyVideo(@PathVariable String id){ vodService.removeVideo(id); return R.ok().message("视频删除成功喽"); }
前端
<el-form-item label="上传视频"> <el-upload :on-success="handleVodUploadSuccess" :on-remove="handleVodRemove" :before-remove="beforeVodRemove" :on-exceed="handleUploadExceed" :file-list="fileList" :action="BASE_API + '/eduvod/video/uploadAliyunVideo'" :limit="1" class="upload-demo" > <el-button size="small" type="primary">上传视频</el-button> <el-tooltip placement="right-end"> <div slot="content"> 最大支持1G,<br /> 支持3GP、ASF、AVI、DAT、DV、FLV、F4V、<br /> GIF、M2T、M4V、MJ2、MJPEG、MKV、MOV、MP4、<br /> MPE、MPG、MPEG、MTS、OGG、QT、RM、RMVB、<br /> SWF、TS、VOB、WMV、WEBM 等视频格式上传 </div> <i class="el-icon-question" /> </el-tooltip> </el-upload> </el-form-item>
video: { id: "", chapterId: "", courseId: "", title: "", sort: 0, isFree: 0, videoSourceId: "", videoOriginalName: "", //视频名称 }, formLabelWidth: "120px", fileList: [], //上传文件列表 BASE_API: process.env.BASE_API, // 接口API地址
此处注意fileList的数组格式是:
method
// ========================上传视频============================== beforeVodRemove(file, fileList) { return this.$confirm(`确定移除 ${file.name}?`); }, //删除视频 handleVodRemove(file, fileList) { console.log(file); vod.removeAlyVideo(this.video.videoSourceId).then((response) => { this.$message({ type: "success", message: response.message, }); this.fileList = [] this.video.videoSourceId = '' this.video.videoOriginalName = '' }); }, //成功回调 handleVodUploadSuccess(response, file, fileList) { this.video.videoSourceId = response.data.videoId; this.video.videoOriginalName = file.name; this.fileList = fileList }, //如果上传多于一个视频 handleUploadExceed(files, fileList) { this.$message.warning("想要重新上传视频,请先删除已上传的视频"); },
import request from '@/utils/request' const api_name = '/eduvod/video' //根据videoid 删除云端视频 export default { removeAlyVideo(videoId) { return request({ url: `${api_name}/removeAlyVideo/`+videoId, method: 'delete' }) }, }
测试
视频删除
servie
/** * 删除云端video videoId是指云端中视频的id * @param videoId */ void removeVideo(String videoId);
@Override public void removeVideo(String videoId) { //删除云端视频 try{ DefaultAcsClient client = InitVodClient.initVodClient(ConstantVodUtils.REGION_ID,ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET); DeleteVideoRequest request = new DeleteVideoRequest(); request.setVideoIds(videoId); DeleteVideoResponse response = client.getAcsResponse(request); System.out.print("RequestId = " + response.getRequestId() + "\n"); }catch (com.aliyuncs.exceptions.ClientException e){ throw new GuliException(20001, "视频删除失败"); } }
controller
/** * 删除阿里云视频根据videoId */ @DeleteMapping("removeAlyVideo/{id}") public R removeAlyVideo(@PathVariable String id){ vodService.removeVideo(id); return R.ok().message("视频删除成功喽"); }
视频点播
controller
@GetMapping("get-play-auth/{videoId}") public R getVideoPlayAuth(@PathVariable("videoId") String videoId) throws Exception { //获取阿里云存储相关常量 String accessKeyId = ConstantVodUtils.ACCESS_KEY_ID; String accessKeySecret = ConstantVodUtils.ACCESS_KEY_SECRET; String regionId = ConstantVodUtils.REGION_ID; //初始化 DefaultAcsClient client = AliyunVodSDKUtils.initVodClient(regionId,accessKeyId, accessKeySecret); //请求 GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest(); request.setVideoId(videoId); //响应 GetVideoPlayAuthResponse response = client.getAcsResponse(request); //得到播放凭证 String playAuth = response.getPlayAuth(); //返回结果 return R.ok().message("获取凭证成功").data("playAuth", playAuth); }
前端
<template> <body> <div id="J_prismPlayer"/> </body> </template> <script> import vod from '@/api/vod' //引入前先安装依赖 npm i aliyun-aliplayer import Aliplayer from 'aliyun-aliplayer' export default { /** * 页面渲染完成时:此时js脚本已加载,Aliplayer已定义,可以使用 * 如果在created生命周期函数中使用,Aliplayer is not defined错误 */ mounted() { new Aliplayer({ id: 'J_prismPlayer', vid: this.vid, // 视频id playauth: this.playAuth, // 播放凭证 encryptType: '1', // 如果播放加密视频,则需设置encryptType=1,非加密视频无需设置此项 width: '100%', height: '500px' }, function(player) { console.log('播放器创建成功') }) }, layout: 'video', // 应用video布局 asyncData({ params, error }) { return vod.getPlayAuth(params.vid).then(response => { // console.log(response.data.data) return { vid: params.vid, playAuth: response.data.data.playAuth } }) } } </script>
// 以下可选设置
cover: ‘http://guli.shop/photo/banner/1525939573202.jpg’, // 封面
qualitySort: ‘asc’, // 清晰度排序
mediaType: ‘video’, // 返回音频还是视频
autoplay: false, // 自动播放
isLive: false, // 直播
rePlay: false, // 循环播放
preload: true,
controlBarVisibility: ‘hover’, // 控制条的显示方式:鼠标悬停
useH5Prism: true, // 播放器类型:html5
SpringCloud Nacos使用
下载
下载地址:https://github.com/alibaba/nacos/releases
下载版本:nacos-server-1.1.4.tar.gz或nacos-server-1.1.4.zip,解压任意目录即可
启动
- Linux/Unix/Mac
启动命令(standalone代表着单机模式运行,非集群模式)
启动命令:sh startup.sh -m standalone
- Windows
启动命令:cmd startup.cmd 或者双击startup.cmd运行文件。
访问:http://localhost:8848/nacos
用户名密码:nacos/nacos
使用
在夫工程中引入依赖
<!--服务注册--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
在子工程中配置application.yml 将服务注册到nacos(其他模块相同的配置)
cloud: nacos: discovery: server-addr: 127.0.0.1:8848
启动类中加入@EnableDiscoveryClient注解
验证
Nacos替换config
引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
创建bootstrap.properties配置文件
#配置中心地址 spring.cloud.nacos.config.server-addr=127.0.0.1:8848 #spring.profiles.active=dev # 该配置影响统一配置中心中的dataId spring.application.name=service-statistics
把项目之前的application.properties内容注释,启动项目查看效果
补充:springboot配置文件加载顺序
其实yml和properties文件是一样的原理,且一个项目上要么yml或者properties,二选一的存在。推荐使用yml,更简洁。
bootstrap与application
(1)加载顺序这里主要是说明application和bootstrap的加载顺序。
bootstrap.yml(bootstrap.properties)先加载
application.yml(application.properties)后加载
bootstrap.yml 用于应用程序上下文的引导阶段。
bootstrap.yml 由父Spring ApplicationContext加载。
父ApplicationContext 被加载到使用 application.yml 的之前。
(2)配置区别bootstrap.yml 和application.yml 都可以用来配置参数。
bootstrap.yml 可以理解成系统级别的一些参数配置,这些参数一般是不会变动的。
application.yml 可以用来定义应用级别的。
名称空间切换环境
在实际开发中,通常有多套不同的环境(默认只有public),那么这个时候可以根据指定的环境来创建不同的 namespce,例如,开发、测试和生产三个不同的环境,那么使用一套 nacos 集群可以分别建以下三个不同的 namespace。以此来实现多环境的隔离。
1、创建命名空间
**
默认只有public,新建了dev、test和prod命名空间
**
2、克隆配置
**
(1)切换到配置列表:
可以发现有四个名称空间:public(默认)以及我们自己添加的3个名称空间(prod、dev、test),可以点击查看每个名称空间下的配置文件,当然现在只有public下有一个配置。
默认情况下,项目会到public下找 服务名.properties文件。
接下来,在dev名称空间中也添加一个nacos-provider.properties配置。这时有两种方式:
第一,切换到dev名称空间,添加一个新的配置文件。缺点:每个环境都要重复配置类似的项目
第二,直接通过clone方式添加配置,并修改即可。推荐
点击编辑:修改配置内容,端口号改为8013以作区分
在项目模块中,修改bootstrap.properties添加如下配置
**
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.profiles.active=dev
# 该配置影响统一配置中心中的dataId,之前已经配置过
spring.application.name=service-statistics
spring.cloud.nacos.config.namespace=13b5c197-de5b-47e7-9903-ec0538c9db01
**
namespace的值为:
**
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X9e9nbD9-1684758493933)(null)]
重启服务提供方服务,测试修改之后是否生效
多配置文件加载
在一些情况下需要加载多个配置文件。假如现在dev名称空间下有三个配置文件:service-statistics.properties、redis.properties、jdbc.properties
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qvvHiH53-1684758494291)(null)]
**
添加配置,加载多个配置文件
**
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.profiles.active=dev
# 该配置影响统一配置中心中的dataId,之前已经配置过
spring.application.name=service-statistics
spring.cloud.nacos.config.namespace=13b5c197-de5b-47e7-9903-ec0538c9db01
spring.cloud.nacos.config.ext-config[0].data-id=redis.properties
# 开启动态刷新配置,否则配置文件修改,工程无法感知
spring.cloud.nacos.config.ext-config[0].refresh=true
spring.cloud.nacos.config.ext-config[1].data-id=jdbc.properties
spring.cloud.nacos.config.ext-config[1].refresh=true
SpringCloud Feign使用
父工程中引入依赖
<!--服务调用--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
消费者
在消费者服务模块主启动类中开启Feign
@EnableFeignClients
创建一个VodClient接口
@FeignClient("service-vod") // 提供者的nacos中注册的服务名 @Component public interface VodClient { /** * videoId是指云端视频id,对应video数据库中的video_source_id * @param videoId * @return */ @DeleteMapping(value = "/eduvod/video/removeAlyVideo/{videoId}") public R removeVideo(@PathVariable("videoId") String videoId); }
注意:@FeignClient(“service-vod”) // 提供者的nacos中注册的服务名
提供者
controller
@RestController @RequestMapping("/eduvod/video") @Api(value = "用于视频上传的接口") @CrossOrigin public class VodController { @Autowired private VodService vodService; /** * 删除阿里云视频根据videoId */ @DeleteMapping("removeAlyVideo/{id}") public R removeAlyVideo(@PathVariable String id){ vodService.removeVideo(id); return R.ok().message("视频删除成功喽"); } }
Feign调用
消费者调用接口向提供者发送请求
@Override public void deleteVideoById(String videoId) { //删除云端视频 EduVideo video = baseMapper.selectById(videoId); String videoSourceId = video.getVideoSourceId(); if (StringUtils.isNotEmpty(videoSourceId))vodClient.removeVideo(videoSourceId); //删除数据库中小节信息 baseMapper.deleteById(videoId); }
SpringCloud Hytrix
Hytrix是一个供分布式系统使用,提供延迟和容错功能。保证复杂的分布系统在面临不可避免的失败的时候,仍能使其有弹性
比如:系统中有很多服务,当某些服务不稳定的时候,使用这些服务的用户线程会阻塞,如果没有隔离机制,系统随时就有可能会挂掉,从而带来很大的风险。springcloud使用Hystix组件提供断路器、资源隔离与自我修复功能。
分布式部署
使用
引入依赖
<!--hystrix依赖,主要是用 @HystrixCommand --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
# 开启熔断机制 feign: hystrix: enabled: true # 设置hystrix的超时时间,默认是1000毫秒 hystrix: metrics: polling-interval-ms: 6000
@FeignClient(name = "service-vod",fallback = VodFileDegradeFeignClient.class) @Component public interface VodClient { /** * videoId是指云端视频id,对应video数据库中的video_source_id * @param videoId * @return */ @DeleteMapping(value = "/eduvod/video/removeAlyVideo/{videoId}") public R removeVideo(@PathVariable("videoId") String videoId); }
@Component public class VodFileDegradeFeignClient implements VodClient{ @Override public R removeVideo(String videoId) { System.out.println("调用了熔断器的执行方法"); return R.error().message("time out"); } }
SpringCloud GateWay 使用
介绍
在客户端和服务端的一面墙,可以起到:请求转发、负载均衡、权限控制等等。
- 路由:路由是网关最基础的部分,路由信息有这些部分组成,一个ID、一个目的URL、一组断言和一组Filter组成。如果断言为真,则说明请求的URL和配置匹配。
使用
引入依赖
<dependencies> <dependency> <groupId>com.atguigu</groupId> <artifactId>common_utils</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!--gson--> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> </dependency> <!--服务调用--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies>
配置启动类
@SpringBootApplication @EnableDiscoveryClient public class ApiGatewayApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayApplication.class, args); } }
写配置类
注意: 路由的id可以是任意的,但是uri必须是nacos中注册的服务名,predicates断言是该服务模块的controller的接口的第一个路径
# 服务端口 server.port=8222 # 服务名 spring.application.name=service-gateway # nacos服务地址 spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 #使用服务发现路由 spring.cloud.gateway.discovery.locator.enabled=true #设置路由id spring.cloud.gateway.routes[0].id=service-acl #设置路由的uri spring.cloud.gateway.routes[0].uri=lb://service-acl #设置路由断言,代理servicerId为auth-service的/auth/路径 spring.cloud.gateway.routes[0].predicates= Path=/*/acl/**
这样的话我们就可以通过网关访问nacos中注册的服务模块了。
此外,全局配置类也可以放到gateway模块中,这样就不用每个服务的接口出添加@CrossOrigin
@Configuration public class CorsConfig { @Bean public CorsWebFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedMethod("*"); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); } }
前端 NUXT框架
简介
nuxt是一个基于vue的前端框架,可以帮助我们使用vue更快速的搭建前端的环境
nuxt目录
- .nuxt是前端编译过后的js文件,相当于java编译后的.class文件
- assets 是用于存放前端静态资源的,如:htm、css、js
- component 项目中用到的相关的组件
- layouts 在default.vue中设置网页布局的方式
- middleware 下载后的组件
- node_module 下载的依赖
- pages项目的具体的页面都放在pages内
- nuxt.config.js nuxt框架的核心配置文件
前端封装request接口
安装axios
npm install axios@version
创建utils目录,新建request.js
import axios from 'axios' // 创建axios实例 const service = axios.create({ baseURL: 'http://localhost:9001', // api的base_url timeout: 20000 // 请求超时时间 }) export default service
创建api目录下创建index.js调用request
import request from '@/utils/request' export default { getList() { return request({ url: `/eduservice/index/index`, method: 'get' }) } }
在 index.vue中调用index中的函数
import index from '@/api/index.js' index.getList().then(response => { this.teacherList = response.data.data.teacherList this.courseList = response.data.data.courseList }) }
用户登陆业务介绍
单一服务器模式:
- 使用session对象实现
- 登陆成功之后,把用户数据放到session中 session.setAttribute(“user”,user)
- 判断是否登陆,从session获取数据,可以获取到登陆 session.getAttribute(“user”)
- 缺点:单点性能压力,无法扩展
集群(分布式)部署模式:
- 单点登陆SSO:一个项目拆分成了许多子模块分别部署在各自的服务器中,单点登陆就是指在一个业务模块登陆了之后,其他模块都会实现登陆功能。例如:百度有百度贴吧和百度文库,登陆了百度贴吧后百度文库也是登陆了。
SSO(Single sign on )模式就是单点登陆模式,三种常见的方式
- session广播机制
- 本质上是session复制
- 缺点:资源浪费、数据冗余
- 使用cookie+redis
- 因为浏览器每次发送请求都会带着cookie去发送,因此在用户登陆成功后
- redis:在key中存放唯一随机的值:由ip、用户id等等随机生成,在value中存放用户的数据
- cookie:把redis里面生成的key放到cookie中
- 获取cooki的值,将这个值到redis中查询,查询出来就是登陆,查询不到就是没登陆
- 使用token(令牌)实现:token是指按照一定的规则生成的字符串,字符串可以包含用户信息
- 在项目某个模块登陆后,按照规则生成字符串,把登陆之后的用于包含到这个字符串中,把字符串返回
- 将token存到浏览器中,每次请求可以带着token到服务器端
- 也可以将token放到地址栏内,返回到服务器
- 服务器收到token进行解码,提取信息,判断是否登陆
JWT令牌
一种已定的规则,用于生成token令牌,里面包含用户信息
jwt生成的字符串包含三部分
- jwt头信息
- 有效载荷,包含主体信息
- 签名哈希:字符串的一个防伪标志
使用方法
引入依赖
<!-- JWT--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency>
导入jwt工具类JwtUtils
MD5加密
md5是一种加密技术,它是不可逆的,只能加密不能解密
登陆实现流程
后端
service
@Service public class UcenterMemberServiceImpl extends ServiceImpl<UcenterMemberMapper, UcenterMember> implements UcenterMemberService { @Autowired private RedisTemplate<String,String> redisTemplate; public static final String DEFAULT_AVATOR = "https://typora-images-1307135242.cos.ap-beijing.myqcloud.com/images/image-20230516110501589.png"; @Override public String login(LoginVO loginVO) { //获取登陆的手机号和密码 String mobile = loginVO.getMobile(); String password = MD5.encrypt(loginVO.getPassword()); if (StringUtils.isEmpty(mobile)|| StringUtils.isEmpty(password))throw new GuliException(20001,"登陆失败"); //判断手机号是否正确 LambdaQueryWrapper<UcenterMember> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(UcenterMember::getMobile,mobile); UcenterMember dbMember = baseMapper.selectOne(lambdaQueryWrapper); if (dbMember == null) throw new GuliException(20001,"改手机号还未注册,请先注册"); if (dbMember.getIsDisabled() == 1)throw new GuliException(20001,"账户被禁用不能的登陆"); if (!password.equals(dbMember.getPassword()))throw new GuliException(20001,"密码不正确"); //登陆成功 String token = JwtUtils.getJwtToken(dbMember.getId(), dbMember.getMobile()); return token; }
controller
@Autowired private UcenterMemberService memberService; //登陆 @PostMapping("login") public R loginUser(@RequestBody LoginVO loginVO){ //调用service方法实现登陆 //返回一个token使用jwt生成 String token = memberService.login(loginVO); return R.ok().data("token",token); }
前端
安装
npm install --save js-cookie
配置request 给每个请求加入过滤器,也就是说每次前端向后端发送请求必须携带token,没有token就是没有登陆,可以将其跳转到登陆界面
import axios from 'axios' import cookie from 'js-cookie' // 创建axios实例 const service = axios.create({ baseURL: 'http://localhost:9001', // api的base_url timeout: 20000 // 请求超时时间 }) // http request 拦截器 service.interceptors.request.use( config => { // debugger if (cookie.get('guli_token')) { config.headers['token'] = cookie.get('guli_token') } return config }, err => { return Promise.reject(err) })
login.js
import request from '@/utils/request' export default { // 登录 submitLogin(userInfo) { return request({ url: `/ucenterservice/ucenterMember/login`, method: 'post', data: userInfo }) }, // 根据token获取用户信息 getLoginInfo() { return request({ url: `/ucenterservice/ucenterMember/getMemberInfo`, method: 'get' // headers: { 'token': cookie.get('guli_token') } }) // headers: {'token': cookie.get('guli_token')} } }
调用
submitLogin() { loginApi.submitLogin(this.user).then(response => { if (response.data.success) { // 把token存在cookie中、也可以放在localStorage中 cookie.set('guli_token', response.data.data.token, { domain: 'localhost' }) // 登录成功根据token获取用户信息 loginApi.getLoginInfo().then(response => { this.loginInfo = response.data.data.userInfo // 将用户信息记录cookie cookie.set('guli_ucenter', JSON.stringify(this.loginInfo), { domain: 'localhost' }) // 跳转页面 window.location.href = '/' }) } }) },
注册实现流程
service
@Override public void register(RegisterVO registerVO) { //获取注册信息,进行校验 String nickname = registerVO.getNickname(); String mobile = registerVO.getMobile(); String password = registerVO.getPassword(); String code = registerVO.getCode(); //校验参数 if(StringUtils.isEmpty(mobile) || StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password) || StringUtils.isEmpty(code)) { throw new GuliException(20001,"error"); } //校验验证码 String mobleCode = redisTemplate.opsForValue().get(mobile); if(!code.equals(mobleCode)) { throw new GuliException(20001,"error"); } //查询数据库中是否存在相同的手机号码 Integer count = baseMapper.selectCount(new QueryWrapper<UcenterMember>().eq("mobile", mobile)); if(count.intValue() > 0) { throw new GuliException(20001,"error"); } UcenterMember user = new UcenterMember(); BeanUtils.copyProperties(registerVO,user); //设置用户默认头像 user.setAvatar(UcenterMemberServiceImpl.DEFAULT_AVATOR); //密码加密 user.setPassword(MD5.encrypt(password)); baseMapper.insert(user); }
退出功能
logout() { cookie.set('guli_ucenter', '', { domain: 'localhost' }) cookie.set('guli_token', '', { domain: 'localhost' }) // 跳转页面 window.location.href = '/' }
OAuth2
OAuth2是一种特定问题的解决方案,主要解决两个问题(令牌机制,按照一定规则生成字符串,字符串包含用户的信息)
- 开放系统之间的授权问题
- 场景:lucy需要打印百度网盘的照片,所以打印招聘需要百度网盘的权限,给予权限的方式:
- lucy直接将账户名和密码给打印照片,打印照片服务去找百度网盘拿照片,缺点是不安全
- lucy给一个通用开发者key,打印照片拿着key,但是这种仅仅适用在合作伙伴之间
- 办法令牌,接近OAuthe2方式,按照自己特定的规则生成一个字符串,颁发给访问者
OAuth2误解:
- 不是一个http协议
- 并不是一个协议只是一个解决方案
实现微信扫码登陆
获取token时序图
引入依赖
<dependencies> <!--httpclient--> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <!--commons-io--> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> </dependency> <!--gson--> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> </dependency> </dependencies>
application.properties
# 微信开放平台 appid wx.open.app_id=wxed9954c01bb89b47 # 微信开放平台 appsecret wx.open.app_secret=a7482517235173ddb4083788de60b90e # 微信开放平台 重定向url wx.open.redirect_url=http://localhost:8160/ucenterservice/api/ucenter/wx/callback
controller(请求用户确认)
@GetMapping("login") public String genQrConnect(HttpSession session) { // 微信开放平台授权baseUrl String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" + "?appid=%s" + "&redirect_uri=%s" + "&response_type=code" + "&scope=snsapi_login" + "&state=%s" + "#wechat_redirect"; // 回调地址 String redirectUrl = ConstantPropertiesUtil.WX_OPEN_REDIRECT_URL; //获取业务服务器重定向地址 try { redirectUrl = URLEncoder.encode(redirectUrl, "UTF-8"); //url编码 } catch (UnsupportedEncodingException e) { throw new GuliException(20001, e.getMessage()); } // 防止csrf攻击(跨站请求伪造攻击) //String state = UUID.randomUUID().toString().replaceAll("-", "");//一般情况下会使用一个随机数 String state = "atguigu";//为了让大家能够使用我搭建的外网的微信回调跳转服务器,这里填写你在ngrok的前置域名 System.out.println("state = " + state); // 采用redis等进行缓存state 使用sessionId为key 30分钟后过期,可配置 //键:"wechar-open-state-" + httpServletRequest.getSession().getId() //值:satte //过期时间:30分钟 //生成qrcodeUrl String qrcodeUrl = String.format( baseUrl, ConstantPropertiesUtil.WX_OPEN_APP_ID, redirectUrl, state); return "redirect:" + qrcodeUrl; }
效果
扫码成功用于确认以后开始带着code和state执行回调url获取token和openid,根据openid查询数据库看用户是否微信扫码注册过,如果没有则向wx发送获取用户信息的请求最后向本地数据插入信息
@GetMapping("callback") public String callback(String code,String state){ System.out.println("code:"+code); System.out.println("state:"+state); try { //向认证服务器发送请求换取access_token String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" + "?appid=%s" + "&secret=%s" + "&code=%s" + "&grant_type=authorization_code"; String accessTokenUrl = String.format( baseAccessTokenUrl, ConstantPropertiesUtil.WX_OPEN_APP_ID, ConstantPropertiesUtil.WX_OPEN_APP_SECRET, code ); //请求这个拼接好的地址,得到access_token 和openid //使用httpClient发送请求,获取结果 String accessTokenInfo = HttpClientUtils.get(accessTokenUrl); System.out.println("accessTokenInfo:"+accessTokenInfo); //解析json字符串 Gson gson = new Gson(); HashMap map = gson.fromJson(accessTokenInfo, HashMap.class); String accessToken = (String)map.get("access_token"); String openid = (String)map.get("openid"); //查询数据库当前用户是否使用微信登陆过 UcenterMember member = memberService.getById(openid); if (member == null){ System.out.println("新用户开始注册"); String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" + "?access_token=%s" + "&openid=%s"; String userInfoUrl = String.format(baseUserInfoUrl, accessToken, openid); String resultUserInfo = HttpClientUtils.get(userInfoUrl); System.out.println("resultUserInfo==========" + resultUserInfo); //解析json HashMap<String, Object> mapUserInfo = gson.fromJson(resultUserInfo, HashMap.class); String nickname = (String)mapUserInfo.get("nickname"); String headimgurl = (String)mapUserInfo.get("headimgurl"); //向数据库中插入一条记录 member = new UcenterMember(); member.setNickname(nickname); member.setOpenid(openid); member.setAvatar(headimgurl); memberService.save(member); //使用jwt根据member对象生成token字符串 String token = JwtUtils.getJwtToken(member.getId(), member.getNickname()); //最后返回首页,通过路径传递token字符串(因为cookie不能跨域) return "redirect:http://localhost:3000?token="+token; } }catch (Exception e){ e.printStackTrace(); } return "redirect:http://localhost:3000"; }
前端
data() { return { token: '', loginInfo: { id: '', age: '', avatar: '', mobile: '', nickname: '', sex: '' } } }, created() { this.token = this.$route.query.token if (this.token) { this.wxLogin() } this.showInfo() }, methods: { showInfo() { // debugger var jsonStr = cookie.get('guli_ucenter') // alert(jsonStr) if (jsonStr) { this.loginInfo = JSON.parse(jsonStr) } console.log(this.loginInfo.id) }, wxLogin() { if (this.token === '') return // 把token存在cookie中、也可以放在localStorage中 cookie.set('guli_token', this.token, { domain: 'localhost' }) cookie.set('guli_ucenter', '', { domain: 'localhost' }) // 登录成功根据token获取用户信息 userApi.getLoginInfo().then(response => { this.loginInfo = response.data.data.userInfo // 将用户信息记录cookie cookie.set('guli_ucenter', JSON.stringify(this.loginInfo), { domain: 'localhost' }) }) }, logout() { cookie.set('guli_ucenter', '', { domain: 'localhost' }) cookie.set('guli_token', '', { domain: 'localhost' }) // 跳转页面 window.location.href = '/' } } }
展示
<!-- / nav --> <ul class="h-r-login"> <li v-if="!loginInfo.id" id="no-login"> <a href="/login" title="登录"> <em class="icon18 login-icon"/> <span class="vam ml5">登录</span> </a> <a href="/register" title="注册"> <span class="vam ml5">注册</span> </a> </li> <li v-if="loginInfo.id" id="is-login-one" class="mr10"> <a id="headerMsgCountId" href="#" title="消息"> <em class="icon18 news-icon"> </em> </a> <q class="red-point" style="display: none"> </q> </li> <li v-if="loginInfo.id" id="is-login-two" class="h-r-user"> <a href="/ucenter" title> <img :src="loginInfo.avatar" width="30" height="30" class="vam picImg" alt > <span id="userName" class="vam disIb">{{ loginInfo.nickname }}</span> </a> <a href="javascript:void(0)" title="退出" class="ml5" @click="logout();">退出</a> </li> </ul> <!-- /未登录显示第1 li;登录后显示第2,3 li -->
前端Cookie的使用
安装
npm install --save js-cookie
引入
import cookie from 'js-cookie'
保存cookie,如果是对象需要序列化
loginInfo: {} //保存 cookie.set('guli_ucenter', JSON.stringify(this.loginInfo), { domain: 'localhost' })
获取
var jsonStr = cookie.get('guli_ucenter') // alert(jsonStr) if (jsonStr) { this.loginInfo = JSON.parse(jsonStr) }
微信支付流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LHWwtMyZ-1684758485780)(https://typora-images-1307135242.cos.ap-beijing.myqcloud.com/images/03%20%E8%AF%BE%E7%A8%8B%E6%94%AF%E4%BB%98%E9%9C%80%E6%B1%82%E5%88%86%E6%9E%90.png)]
生成vx支付二维码
准备工作:
需要:
微信支付id、商户号、商户key、回调地址
# 微信支付所需固定参数 weixin.pay.appid=wx74862e0dfcf69954 weixin.pay.partner=1558950191 weixin.pay.partnerkey=T6m9iK73b0kn9g5v426MKfHQH7X8rKwb weixin.pay.notifyurl=http://localhost/api/order/weixinPay/weixinNotify
支付流程
创建订单
前端发送课程号和请求头中带有用户信息的token后端使用jwt进行解析用户id,之后根据courseID和userId创建订单信息,最后将订单id发送给前端,前端带着订单id进行购物车页面的跳转,在购物车页面进行异步获取订单信息,进行渲染
前端
//course详情页面点击购买 createOrder() { orders.createOrder(this.courseId).then(res => { // 获取订单号 // res.data.data.orderNo // 生成订单之后跳转到订单的显示页面 this.$router.push({ path: '/orders/' + res.data.data.orderNo }) }) } //购物车order页面异步渲染 asyncData({ params, error }) { return orders.getById(params.orderId).then(res => { console.log(res.data) return { order: res.data.data.item } }) },
后端
controller
//生成订单的方法 @GetMapping("createOrder/{courseId}") public R saveOrder(@PathVariable String courseId, HttpServletRequest request){ String userId = JwtUtils.getMemberIdByJwtToken(request); //创建订单返回订单号 String orderNo = orderService.createOrders(courseId,userId); return R.ok().data("orderNo",orderNo); } //根据订单id查询订单信息 @GetMapping("/getOrderInfo/{orderId}") public R getOrderInfo(@PathVariable String orderId){ LambdaQueryWrapper<Order> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(Order::getOrderNo,orderId); Order order = orderService.getOne(lambdaQueryWrapper); return R.ok().data("item",order); }
service生成订单的业务逻辑,通过feign远程调用edu模块和ucenter模块根据courseid和userid获取课程信息、用户信息、教师信息
//生成订单的方法 @Override public String createOrders(String courseId, String userId) { //通过远程调用获取课程信息 CourseInfoVO courseInfo = getCourseInfoById.getCourseInfo(courseId); //通过远程调用获取用户信息 UcenterMemberOrder userInfo = getUserInfoById.getUcenterMember(userId); //通过远程调用获取老师信息 TeacherVO teacherInfo = getTeacherInfoById.getTeacherInfo(courseInfo.getTeacherId()); Order order = new Order(); order.setOrderNo(OrderNoUtil.getOrderNo()); order.setCourseId(courseId); order.setCourseTitle(courseInfo.getTitle()); order.setCourseCover(courseInfo.getCover()); order.setTeacherName(teacherInfo.getName()); order.setTotalFee(courseInfo.getPrice()); order.setMemberId(userId); order.setMobile(userInfo.getMobile()); order.setNickname(userInfo.getNickname()); order.setStatus(0); order.setPayType(1); baseMapper.insert(order); return order.getOrderNo(); }
购物车order页面渲染订单信息后,用户确认立即支付带着订单号跳转到支付pay页面,支付页面根据订单号异步向后端发起请求获取支付的微信付款码和订单基本信息例如金额,订单号等,封装成map返回给前端进行渲染
前端
// 根据订单id生成微信支付二维码 asyncData({ params, error }) { return orderApi.createNative(params.pid).then(response => { return { payObj: response.data.data.map } }) },
controller
@Autowired private PayLogService payLogService; //生成微信支付二维码的接口 @GetMapping("/createNative/{orderNo}") public R createNative(@PathVariable String orderNo){ //返回相关的一些信息,包含二维码的地址和其他信息 Map map = payLogService.createNative(orderNo); return R.ok().data("map",map); }
service
@Autowired private OrderService orderService; @Override public Map createNative(String orderNo) { try { //根据订单id查询订单信息 LambdaQueryWrapper<Order> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(Order::getOrderNo,orderNo); Order order = orderService.getOne(lambdaQueryWrapper); //设置参数 HashMap m = new HashMap(); m.put("appid",ConstantUtils.APPID); //partner m.put("mch_id", ConstantUtils.PARTNER); m.put("nonce_str", WXPayUtil.generateNonceStr()); m.put("body", order.getCourseTitle()); m.put("out_trade_no", orderNo); m.put("total_fee", order.getTotalFee().multiply(new BigDecimal("1")).longValue()+""); m.put("spbill_create_ip", "127.0.0.1"); m.put("notify_url", ConstantUtils.NOTIFYURL); m.put("trade_type", "NATIVE"); //2、HTTPClient来根据URL访问第三方接口并且传递参数 HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder"); //client设置参数 client.setXmlParam(WXPayUtil.generateSignedXml(m, ConstantUtils.PARTNERKEY)); client.setHttps(true); client.post(); //3、返回第三方的数据 String xml = client.getContent(); Map<String, String> resultMap = WXPayUtil.xmlToMap(xml); //4、封装返回结果集 Map map = new HashMap<>(); map.put("out_trade_no", orderNo); map.put("course_id", order.getCourseId()); map.put("total_fee", order.getTotalFee()); map.put("result_code", resultMap.get("result_code")); //返回二维码操作码 map.put("code_url", resultMap.get("code_url")); //返回二维码图片地址 //微信支付二维码2小时过期,可采取2小时未支付取消订单 //redisTemplate.opsForValue().set(orderNo, map, 120, TimeUnit.MINUTES); return map; }catch (Exception e){ throw new GuliException(20001,"生成二维码失败"); } }
此时前端会不停的每隔三秒向后端发送用户是否确认支付的请求,并且在axios的response中做校验
mounted() { // 在页面渲染之后执行 // 每隔三秒,去查询一次支付状态 this.timer1 = setInterval(() => { this.queryPayStatus(this.payObj.out_trade_no) }, 3000) }, methods: { // 查询支付状态的方法 queryPayStatus(out_trade_no) { orderApi.queryPayStatus(out_trade_no).then(response => { console.log(response) if (response.data.success) { // 如果支付成功,清除定时器 clearInterval(this.timer1) this.$message({ type: 'success', message: '支付成功!' }) // 跳转到课程详情页面观看视频 this.$router.push({ path: '/course/' + this.payObj.course_id }) } }) } }
response拦截器,如果返回code是28004则用户为登陆进行登陆跳转,如果code是25000则提示用户未支付等待用户支付,如果code是20000则表名后端校验用户已经付款成功,response拦截器放行,执行用户支付成功友好提示,跳转支付后的course页面
import axios from 'axios' import cookie from 'js-cookie' // 创建axios实例 const service = axios.create({ baseURL: 'http://localhost:9001', // api的base_url timeout: 20000 // 请求超时时间 }) // http request 拦截器 service.interceptors.request.use( config => { // debugger if (cookie.get('guli_token')) { config.headers['token'] = cookie.get('guli_token') } return config }, err => { return Promise.reject(err) }) // http response 拦截器 service.interceptors.response.use( response => { // debugger if (response.data.code === 28004) { console.log('response.data.resultCode是28004') // 返回 错误代码-1 清除ticket信息并跳转到登录页面 // debugger window.location.href = '/login' return } else { if (response.data.code !== 20000) { // 25000:订单支付中,不做任何提示 if (response.data.code !== 25000) { // Message({ // message: response.data.message || 'error', // type: 'error', // duration: 5 * 1000 // }) alert('订单未支付') } } else { return response } } }, error => { return Promise.reject(error.response) // 返回接口返回的错误信息 }) export default service
那么后端是如何进行校验的呢?可以看到当前端发来查询订单状态的订单id时,service业务层会带着订单id向微信支付官方接口发送http请求https://api.mch.weixin.qq.com/pay/orderquery,来获取当前订单的状态信息,订单当前在微信官方的状态信息map如下图
controller,我们就是依据trade_state这个字段进行判断的,当未支付时会返回NOTPAY,当支付成功时会返回,SUCCESS,支付成功则向前端返回20000的code,前端获取20000code则停止监听,并执行支付成功后的业务逻辑。需要注意的是当trade_state判断为SUCCESS后还需更新order表中对应订单的状态信息,将status修改为1,表示已支付。
//查询支付状态信息 @GetMapping("/queryPayStatus/{orderNo}") public R queryPayStatus(@PathVariable String orderNo){ Map<String,String> map = payLogService.queryPayStatus(orderNo); if (map == null)return R.error().message("支付出错了"); //如果map不为空,那么获取订单的状态 if (map.get("trade_state").equals("SUCCESS")){ //向支付表中添加信息,更新订单表 payLogService.updateOrderStatus(map); return R.ok().message("支付成功"); } return R.ok().code(25000).message("支付中"); }
service,
/** * 查询订单的支付状态 * @param orderNo * @return */ @Override public Map<String, String> queryPayStatus(String orderNo) { try { //1、封装参数 Map m = new HashMap<>(); m.put("appid", ConstantUtils.APPID); m.put("mch_id", ConstantUtils.PARTNER); m.put("out_trade_no", orderNo); m.put("nonce_str", WXPayUtil.generateNonceStr()); //2、设置请求 HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery"); client.setXmlParam(WXPayUtil.generateSignedXml(m, ConstantUtils.PARTNERKEY)); client.setHttps(true); client.post(); //3、返回第三方的数据 String xml = client.getContent(); Map<String, String> resultMap = WXPayUtil.xmlToMap(xml); //6、转成Map //7、返回 return resultMap; } catch (Exception e) { e.printStackTrace(); } return null; } /** * 添加支付信息和更新订单状态 * @param map */ @Override public void updateOrderStatus(Map<String, String> map) { //获取订单id String orderNo = map.get("out_trade_no"); //根据订单id查询订单信息 QueryWrapper<Order> wrapper = new QueryWrapper<>(); wrapper.eq("order_no",orderNo); Order order = orderService.getOne(wrapper); if(order.getStatus().intValue() == 1) return; order.setStatus(1); orderService.updateById(order); //记录支付日志 PayLog payLog=new PayLog(); payLog.setOrderNo(order.getOrderNo());//支付订单号 payLog.setPayTime(new Date()); payLog.setPayType(1);//支付类型 payLog.setTotalFee(order.getTotalFee());//总金额(分) payLog.setTradeState(map.get("trade_state"));//支付状态 payLog.setTransactionId(map.get("transaction_id")); payLog.setAttr(JSONObject.toJSONString(map)); baseMapper.insert(payLog);//插入到支付日志表 }
至此,微信支付的前后端执行逻辑就完成了。
统计分析模块
需求:统计在线教育项目中,每天有多少注册人数,把统计出的注册人数使用图表展现出来
SpringBoot整合Cron定时任务
需求:在固定时间自动执行程序,比如闹钟。
场景:在每天晚上凌晨一点自动生成前天的数据
使用:
开启定时任务
@EnableScheduling public class StaApplication { public static void main(String[] args) { SpringApplication.run(StaApplication.class, args); } }
创建一个定时任务的类
@Component public class ScheduledTask { /** * 测试 * 每天七点到二十三点每五秒执行一次 例如: 0/5 * * * * ? 表示每隔五秒执行一次这个方法 * */ @Scheduled(cron = "0/5 * * * * ?") public void task1(){ System.out.println("*******task1执行了....."); } }
Cron表达式(七子表达式)
自动生成cron: 在线Cron表达式生成器 (qqe2.com)
需要注意的是:在springboot整合cron中,默认是六位cron,最后一位年是默认为当前年。
常用cron表达式例子 (1)0/2 * * * * ? 表示每2秒 执行任务 (1)0 0/2 * * * ? 表示每2分钟 执行任务 (1)0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务 (2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业 (3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作 (4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 (5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时 (6)0 0 12 ? * WED 表示每个星期三中午12点 (7)0 0 12 * * ? 每天中午12点触发 (8)0 15 10 ? * * 每天上午10:15触发 (9)0 15 10 * * ? 每天上午10:15触发 (10)0 15 10 * * ? 每天上午10:15触发 (11)0 15 10 * * ? 2005 2005年的每天上午10:15触发 (12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发 (13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发 (14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 (15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发 (16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发 (17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发 (18)0 15 10 15 * ? 每月15日上午10:15触发 (19)0 15 10 L * ? 每月最后一日的上午10:15触发 (20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发 (21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发 (22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
Echarts
一个基于 JavaScript 的开源可视化图表库,快速入门](https://echarts.apache.org/handbook/zh/get-started)所有示例
之前是百度旗下的,后来捐给了apache机构
我们可以通过Echarts快速的构建可视化图表
使用:
安装
npm install --save echarts
引入
import * as echarts from ‘echarts’;
import ‘echarts-gl’;
数据渲染
methods: { showChart() { this.initChartData(); //this.setChart() }, // 准备图表数据 initChartData() { daily.showChart(this.searchObj).then((response) => { console.log(response.data.map); // 数据 this.yData = response.data.map.numDataList; // 横轴时间 this.xData = response.data.map.date_calculated_list; // 当前统计类别 switch (this.searchObj.type) { case "register_num": this.title = "学员注册数统计"; break; case "login_num": this.title = "学员登录数统计"; break; case "video_view_num": this.title = "课程播放数统计"; break; case "course_num": this.title = "每日课程数统计"; break; } this.setChart(); }); },
结果显示
Canal数据同步工具
应用场景
将远程数据库的内容同步到本地库中,这样的话可以做到程序解耦,效率更高。如下图,本地数据库edusta的user表同步远程数据库educenter的users。
使用
测试前置说明:
- 一个本地mysql 端口号3306
- 一个远程mysql 端口号3308
- 版本:mysql8.0
C:\Users\26765>mysql -V mysql Ver 8.0.30 for Win64 on x86_64 (MySQL Community Server - GPL)
[root@localhost ~]# docker ps | grep mysql 73ba6154f568 mysql:8.0.28 "docker-entrypoint.s…" 5 hours ago Up 4 hours 33060/tcp, 0.0.0.0:3308->3306/tcp, :::3308->3306/tcp mysql8.0.28
在远程服务器中搭建Canal环境:
检查binlog功能是否有开启
mysql> show variables like 'log_bin'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | log_bin | OFF | +---------------+-------+ 1 row in set (0.00 sec)
如果显示状态为OFF表示该功能未开启,开启binlog功能
1,修改 mysql 的配置文件 my.cnf vi /etc/my.cnf 追加内容: log-bin=mysql-bin #binlog文件名 binlog_format=ROW #选择row模式 server_id=1 #mysql实例id,不能和canal的slaveId重复 2,重启 mysql: service mysql restart 3,登录 mysql 客户端,查看 log_bin 变量 mysql> show variables like 'log_bin'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | log_bin | ON| +---------------+-------+ 1 row in set (0.00 sec) ———————————————— 如果显示状态为ON表示该功能已开启
在mysql里面添加以下的相关用户和权限
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal'; GRANT SHOW VIEW, SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;
或者查看mysql自带的root是否具有远程操作权限,如果符合以下则也可以使用mysql root账户,本次使用root账户即可
下载Canal
下载地址:
https://github.com/alibaba/canal/releases
下载完传到服务器解压缩
[root@localhost canal]# ls bin canal.deployer-1.1.4.tar.gz conf lib logs
修改配置文件
启动Canal
创建一个新模块和目录结构
引入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.alibaba.otter</groupId> <artifactId>canal.client</artifactId> </dependency> </dependencies>
写配置文件,连接本地数据库
# 服务端口 server.port=10001 # 服务名 spring.application.name=canal-client # 环境设置:dev、test、prod spring.profiles.active=dev # mysql数据库连接 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8 spring.datasource.username=root spring.datasource.password=796321@zy
新建CanalClient
@Component public class CanalClient { //sql队列 private Queue<String> SQL_QUEUE = new ConcurrentLinkedQueue<>(); @Resource private DataSource dataSource; /** * canal入库方法 */ public void run() { CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.100.10", 11111), "example", "", ""); int batchSize = 1000; try { connector.connect(); connector.subscribe(".*\\..*"); connector.rollback(); try { while (true) { //尝试从master那边拉去数据batchSize条记录,有多少取多少 Message message = connector.getWithoutAck(batchSize); long batchId = message.getId(); int size = message.getEntries().size(); if (batchId == -1 || size == 0) { Thread.sleep(1000); } else { dataHandle(message.getEntries()); } connector.ack(batchId); //当队列里面堆积的sql大于一定数值的时候就模拟执行 if (SQL_QUEUE.size() >= 1) { executeQueueSql(); } } } catch (InterruptedException e) { e.printStackTrace(); } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } finally { connector.disconnect(); } } /** * 模拟执行队列里面的sql语句 */ public void executeQueueSql() { int size = SQL_QUEUE.size(); for (int i = 0; i < size; i++) { String sql = SQL_QUEUE.poll(); System.out.println("[sql]----> " + sql); this.execute(sql.toString()); } } /** * 数据处理 * * @param entrys */ private void dataHandle(List<Entry> entrys) throws InvalidProtocolBufferException { for (Entry entry : entrys) { if (EntryType.ROWDATA == entry.getEntryType()) { RowChange rowChange = RowChange.parseFrom(entry.getStoreValue()); EventType eventType = rowChange.getEventType(); if (eventType == EventType.DELETE) { saveDeleteSql(entry); } else if (eventType == EventType.UPDATE) { saveUpdateSql(entry); } else if (eventType == EventType.INSERT) { saveInsertSql(entry); } } } } /** * 保存更新语句 * * @param entry */ private void saveUpdateSql(Entry entry) { try { RowChange rowChange = RowChange.parseFrom(entry.getStoreValue()); List<RowData> rowDatasList = rowChange.getRowDatasList(); for (RowData rowData : rowDatasList) { List<Column> newColumnList = rowData.getAfterColumnsList(); StringBuffer sql = new StringBuffer("update " + entry.getHeader().getTableName() + " set "); for (int i = 0; i < newColumnList.size(); i++) { sql.append(" " + newColumnList.get(i).getName() + " = '" + newColumnList.get(i).getValue() + "'"); if (i != newColumnList.size() - 1) { sql.append(","); } } sql.append(" where "); List<Column> oldColumnList = rowData.getBeforeColumnsList(); for (Column column : oldColumnList) { if (column.getIsKey()) { //暂时只支持单一主键 sql.append(column.getName() + "=" + column.getValue()); break; } } SQL_QUEUE.add(sql.toString()); } } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } /** * 保存删除语句 * * @param entry */ private void saveDeleteSql(Entry entry) { try { RowChange rowChange = RowChange.parseFrom(entry.getStoreValue()); List<RowData> rowDatasList = rowChange.getRowDatasList(); for (RowData rowData : rowDatasList) { List<Column> columnList = rowData.getBeforeColumnsList(); StringBuffer sql = new StringBuffer("delete from " + entry.getHeader().getTableName() + " where "); for (Column column : columnList) { if (column.getIsKey()) { //暂时只支持单一主键 sql.append(column.getName() + "=" + column.getValue()); break; } } SQL_QUEUE.add(sql.toString()); } } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } /** * 保存插入语句 * * @param entry */ private void saveInsertSql(Entry entry) { try { RowChange rowChange = RowChange.parseFrom(entry.getStoreValue()); List<RowData> rowDatasList = rowChange.getRowDatasList(); for (RowData rowData : rowDatasList) { List<Column> columnList = rowData.getAfterColumnsList(); StringBuffer sql = new StringBuffer("insert into " + entry.getHeader().getTableName() + " ("); for (int i = 0; i < columnList.size(); i++) { sql.append(columnList.get(i).getName()); if (i != columnList.size() - 1) { sql.append(","); } } sql.append(") VALUES ("); for (int i = 0; i < columnList.size(); i++) { sql.append("'" + columnList.get(i).getValue() + "'"); if (i != columnList.size() - 1) { sql.append(","); } } sql.append(")"); SQL_QUEUE.add(sql.toString()); } } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } /** * 入库 * @param sql */ public void execute(String sql) { Connection con = null; try { if(null == sql) return; con = dataSource.getConnection(); QueryRunner qr = new QueryRunner(); int row = qr.execute(con, sql); System.out.println("update: "+ row); } catch (SQLException e) { e.printStackTrace(); } finally { DbUtils.closeQuietly(con); } } }
创建启动类
@SpringBootApplication public class CanalApplication implements CommandLineRunner { @Autowired private CanalClient canalClient; public static void main(String[] args) { SpringApplication.run(CanalApplication.class, args); } @Override public void run(String... args) throws Exception { //项目启动,执行canal客户端监听 canalClient.run(); } }
测试
远程mysql member表添加一条数据
查看本地
权限管理
使用递归的方式查询菜单
效果
开始实现
service
@Override public List<Permission> queryAllMenuGuli() { //查询菜单表中所有数据 QueryWrapper<Permission> permissionQueryWrapper = new QueryWrapper<>(); permissionQueryWrapper.orderByDesc("id"); List<Permission> permissionList = baseMapper.selectList(permissionQueryWrapper); //把查询到的所有菜单list集合按照要求进行封装 List<Permission> resultList = buildPermission(permissionList); return resultList; } //把返回所有菜单list集合进行封装的方法 public static List<Permission> buildPermission(List<Permission> permissionList) { //创建一个list集合,用于数据的封装 List<Permission> finalNode = new ArrayList<>(); //把所有菜单的list集合遍历,得到顶层菜单 pid= 0的菜单,设置level是1 for (Permission permissionNode : permissionList) { if ("0".equals(permissionNode.getPid())){ permissionNode.setLevel(1); //根据顶层菜单,向里面继续查询子菜单,封装到finalNode中 finalNode.add(selectChilren(permissionNode,permissionList)); } } return finalNode; } private static Permission selectChilren(Permission permissionNode, List<Permission> permissionList) { //因为向一级菜单放入二级菜单,二级菜单放三级菜单,因此初始化一个list数组 permissionNode.setChildren(new ArrayList<Permission>()); //遍历所有菜单的list集合进行判断id和pid的值是否相同 for (Permission it : permissionList) { //判断一级菜单的id和二级菜单的pid是否相同 if (permissionNode.getId().equals(it.getPid())){ //把夫菜单的level值+1 int level = permissionNode.getLevel() +1 ; it.setLevel(level); //把查询出的子菜单放到夫菜单中 permissionNode.getChildren().add(selectChilren(it,permissionList)); } } return permissionNode; }
使用递归的方式删除菜单
//递归删除菜单 @Override public void removeChildByIdGuli(String id) { //创建list集合,用于封装所有删除菜单的id值 List<String> idList = new ArrayList<>(); //向idList集合设置删除菜单id this.selectPermissionChildById(id,idList); //把当前id封装到id里面 idList.add(id); baseMapper.deleteBatchIds(idList); } //根据当前菜单id,查询菜单里面子菜单id,封装到list集合中 private void selectPermissionChildById(String id,List<String> idList) { //查询菜单里的子id QueryWrapper<Permission> wrapper = new QueryWrapper<>(); wrapper.eq("pid",id); wrapper.select("id"); List<Permission> childIdList = baseMapper.selectList(wrapper); //childIdList里面菜单id值获取出来,封装到idList里面,并且要做递归的操作 childIdList.stream().forEach(item -> { //封装idList里面去 idList.add(item.getId()); //递归查询 this.selectPermissionChildById(item.getId(), idList); }); }
给角色分配权限
//给角色分配菜单(权限) @Override public void saveRolePermissionRealtionShipGuli(String roleId, String[] permissionId) { //创建一个list集合用于最后封装添加数据 List<RolePermission> rolePermissionList = new ArrayList<>(); //遍历所有的菜单数组 for (String perId : permissionId) { RolePermission rolePermission = new RolePermission(); rolePermission.setRoleId(roleId); rolePermission.setPermissionId(perId); //封装到list集合 rolePermissionList.add(rolePermission); } //添加到角色菜单关系表 rolePermissionService.saveBatch(rolePermissionList); }
Spring Security
介绍
- 用户认证:在进行用户登陆的时候输入用户名和密码查询数据库,看输入的账户和密码是否正确
- 用户授权:登陆了系统,登陆用户可能是不同的角色,比如说现在登陆的角色是管理员,则具有管理员的权限
spring security就是一组过滤器,对请求的路径进行过滤:
- 如果是基于session,那么spring security会对cookie里的sessionid进行解析,找到服务器存储的session信息,然后判断当前用户是否如何请求的要求
- 如果是基于token,则解析出token,然后将当前请求加入到spring-security管理的权限信息中去
认证与授权的实现思路:
如果系统的模块众多,每个模块都需要进行授权和认证,所以我们选择基于token的形式进行授权和认证,用户根据用户名和密码授权成功,然后获取当前用户角色的一系列权限值,并以用户名为key,权限列表为value的形式存到redis中,根据用户名相关信息生成token返回到前端,浏览器将token存到cookie中,之后每次发送请求在header中都带上token,spring-security解析header头获取token信息,拿到用户名,根据用户名获取redis中对应的value权限,这样spring-security就知道该用户是否有权限访问了。
整合spring-security
目录结构
持续化部署Docker+Jenkins
1. 准备代码,提交到码云Git库
代码中需要包含以下几部分内容:
(1)代码中需要包含Dockerfile文件
文件内容:
FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY ./target/demojenkins.jar demojenkins.jar
ENTRYPOINT [“java”,“-jar”,“/demojenkins.jar”, “&”]
(2)在项目pom文件中指定打包类型,包含build部分内容
2. 安装JAVA 运行环境
第一步:上传或下载安装包
cd/usr/local
jdk-8u121-linux-x64.tar.gz
第二步:解压安装包
tar -zxvf jdk-8u121-linux-x64.tar.gz
第三步:建立软连接
ln -s /usr/local/jdk1.8.0_121/ /usr/local/jdk
第四步:修改环境变量
vim /etc/profile
export JAVA_HOME=/usr/local/jdk
export JRE_HOME=$JAVA_HOME/jre
export CLASSPATH=.:$CLASSPATH:$JAVA_HOME/lib:$JRE_HOME/lib
export PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin
通过命令source /etc/profile让profile文件立即生效
source /etc/profile
第五步、测试是否安装成功
②、使用java -version,出现版本
3. 安装maven
第一步:上传或下载安装包
cd/usr/local
apache-maven-3.6.1-bin.tar.gz
第二步:解压安装包
tar -zxvf apache-maven-3.6.1-bin.tar.gz
第三步:建立软连接
ln -s /usr/local/apache-maven-3.6.1/ /usr/local/maven
第四步:修改环境变量
vim /etc/profile
export MAVEN_HOME=/usr/local/maven
export PATH=$PATH:$MAVEN_HOME/bin
通过命令source /etc/profile让profile文件立即生效
source /etc/profile
第五步、测试是否安装成功
mvn –v
4. 安装git
yum -y install git
5. 安装docker
参考文档:
https://help.aliyun.com/document_detail/60742.html?spm=a2c4g.11174283.6.548.24c14541ssYFIZ
第一步:安装必要的一些系统工具
yum install -y yum-utils device-mapper-persistent-data lvm2
第二步:添加软件源信息
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
第三步:更新并安装Docker-CE
yum makecache fast
yum -y install docker-ce
第四步:开启Docker服务
service docker start
第五步、测试是否安装成功
docker -v
6. 安装Jenkins
第一步:上传或下载安装包
cd/usr/local/jenkins
jenkins.war
第二步:启动
nohup java -jar /usr/local/jenkins/jenkins.war >/usr/local/jenkins/jenkins.out &
第二步:访问
7. 初始化 Jenkins 插件和管理员用户
7.1访问jenkins
7.2解锁jenkins
获取管理员密码
注意:配置国内的镜像
官方下载插件慢 更新下载地址
cd {你的Jenkins工作目录}/updates #进入更新配置位置
sed -i ‘s/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g’ default.json && sed -i ‘s/http:\/\/www.google.com/https:\/\/www.baidu.com/g’ default.json
这是直接修改的配置文件,如果前边Jenkins用sudo启动的话,那么这里的两个sed前均需要加上sudo
重启Jenkins,安装插件
7.3选择“继续”
7.4选择“安装推荐插件”
7.5插件安装完成,创建管理员用户
7.6保存并完成
7.7进入完成页面
8. 配置 Jenkins 构建工具
8.1全局工具配置
8.1.1配置jdk
JAVA_HOME:/usr/local/jdk
8.1.2配置maven
MAVEN_HOME:/usr/local/maven
8.1.2配置git
查看git安装路径:which git
9. 构建作业
9.1点击创建一个新任务,进入创建项目类型选择页面
填好信息点击“确认”
9.2配置“General”
9.3配置“源码管理”
填写源码的git地址
添加git用户,git的用户名与密码
选择添加的用户,上面的红色提示信息消失,说明连接成功,如下图
9.4构建作业
到源码中找到docker脚本
选择“执行shell”
保存上面的构建作业
9.5构建
构建作业之后,就可以执行构建过程了。
9.5.1执行构建过程
9.5.2构建结构
第一列是 “上次构建状态显示”,是一个圆形图标,一般分为四种:
蓝色:构建成功;
黄色:不确定,可能构建成功,但包含错误;
红色:构建失败;
灰色:项目从未构建过,或者被禁用;
如上显示蓝色,表示构建成功。
注意:手动触发构建的时间与自动定时构建的时间互不影响。
9.5.3查看控制台输出
日志内容: