谷粒学苑笔记整理

文章目录

谷粒学苑

常见商业模式

B2C: 两个角色,管理员和普通用户

管理员:添加、修改、删除

普通用户:查询

核心模块:课程模块

B2B2C: 商家到商家再到用户,例如:京东

角色: 普通用户、可以买自营、可以买普通商家

项目功能模块

image-20230505090954195

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);
        }
    }

配置字段自动填充

添加一个字段

image-20230505150034752

实体类标注填充的字段

@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);
    }

实现效果

image-20230505150322299

配置乐观锁和分页插件

乐观锁

乐观锁实现原理:

​ 实际上是在表中增加了一个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

image-20230505150543150

测试乐观锁成功的情况

/**
 * 测试乐观锁成功的情况
 */
@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);
}
分页实现

添加配置

image-20230505151229076

分页查询

    @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字段

image-20230509203555696

配置application.yml逻辑删除的属性

image-20230509203654429

实体类中属性做逻辑删除标识

@ApiModelProperty(value = "逻辑删除 1(true)已删除, 0(false)未删除")
@TableLogic
@TableField(fill = FieldFill.INSERT)
private Integer isDeleted;

插入时自动填充配置

image-20230509203809150

测试删除一条记录

测试前

image-20230509204430721

@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单元测试");
    }
}

测试后

image-20230509210729006

异常处理

首先创建一个统一异常处理类

/**
 * 统一异常处理的类
 */
@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: LTAI5t6K1Gf1Qjj7XYSyw8hx
      keysecret: r3UJqjrjFrntDGt3z9X6qAXPdwMcgn
      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单元测试");
    }
}

运行

image-20230509211913022

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);
}

测试

image-20230515154851243

查看

image-20230515154928316

注意:当第一次查询这个接口数据的时候会执行sql语句,当第二次查询数据的时候就不在执行sql语句了,因为数据已经缓存在redis中。

关于设置redis中数据过期时间的问题

可以在redis的配置类config中设置,例如:

image-20230515155304907

最后

@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);
    }
}

测试

image-20230508113022278

注意:可以参考官方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;
}

实现效果

image-20230508135107789

读操作

创建实体

@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();
}

实现效果

image-20230508135131691

Mybatis 多级分类查询

表数据结构

image-20230508180349479

扫描mapper.xml

image-20230508180600721

开启mapper扫描

image-20230508180621058

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);
}

目录结构

image-20230508180711300

测试效果

image-20230508180752921

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修饰;
————————————————
版权声明:本文为CSDN博主「是杨杨呀」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_46645840/article/details/128099618

阿里云视频点播功能

API和SDK的区别:

  • API是阿里云提供了一个固定的地址,需要向这个地址发送固定的参数,实现功能
  • SDK的底层是API,SDK是对API的方式进行了封装,使用了sdk可以更方便的调用功能实现,调用阿里云提供的类或者接口中的方法实现功能就是SDK

配置

application.propertites

#阿里云 vod
#不同的服务器,地址不同
aliyun.vod.file.keyid=LTAI5t6K1Gf1Qjj7XYSyw8hx
aliyun.vod.file.keysecret=r3UJqjrjFrntDGt3z9X6qAXPdwMcgn
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

image-20230512161752073

配置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的数组格式是:

image-20230512161012618

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'
    })
  },
 
}
测试

image-20230512161155766

视频删除

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注解

验证

image-20230512212839188

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中注册的服务名

image-20230512213246122

提供者

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组件提供断路器、资源隔离与自我修复功能。

image-20230513122121465

​ 分布式部署

image-20230513120746967

使用

引入依赖

<!--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:一个项目拆分成了许多子模块分别部署在各自的服务器中,单点登陆就是指在一个业务模块登陆了之后,其他模块都会实现登陆功能。例如:百度有百度贴吧和百度文库,登陆了百度贴吧后百度文库也是登陆了。

image-20230515163621559

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方式,按照自己特定的规则生成一个字符串,颁发给访问者

    image-20230516201641384

OAuth2误解:

  • 不是一个http协议
  • 并不是一个协议只是一个解决方案

实现微信扫码登陆

参考文档:https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316518&token=&lang=zh_CN

获取token时序图

image-20230517084018139

引入依赖

<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;
}

效果

image-20230517084229771

扫码成功用于确认以后开始带着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">&nbsp;</em>
              </a>
              <q class="red-point" style="display: none">&nbsp;</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进行购物车页面的跳转,在购物车页面进行异步获取订单信息,进行渲染

image-20230519140805896

前端

    //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返回给前端进行渲染

image-20230519140828378

前端

  // 根据订单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如下图

image-20230519141810078

image-20230519142055753

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();
      });
    },

​ 结果显示

image-20230520130436637

Canal数据同步工具

应用场景

​ 将远程数据库的内容同步到本地库中,这样的话可以做到程序解耦,效率更高。如下图,本地数据库edusta的user表同步远程数据库educenter的users。

image-20230520132917636

使用

测试前置说明:

  • 一个本地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账户即可

image-20230520193058029

下载Canal

下载地址:

https://github.com/alibaba/canal/releases

下载完传到服务器解压缩

[root@localhost canal]# ls
bin  canal.deployer-1.1.4.tar.gz  conf  lib  logs

修改配置文件

image-20230520193411387

image-20230520193312448

启动Canal

image-20230520193451418

创建一个新模块和目录结构

image-20230520193731729

引入依赖

<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表添加一条数据

image-20230520194029120

查看本地

image-20230520194046026

权限管理

使用递归的方式查询菜单

效果

image-20230521102026734

开始实现

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

目录结构

image-20230522090228464

持续化部署Docker+Jenkins

1. 准备代码,提交到码云Git库

代码中需要包含以下几部分内容:

(1)代码中需要包含Dockerfile文件

image

文件内容:

FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY ./target/demojenkins.jar demojenkins.jar
ENTRYPOINT [“java”,“-jar”,“/demojenkins.jar”, “&”]

(2)在项目pom文件中指定打包类型,包含build部分内容

image

image

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 &

第二步:访问

http://ip:8080

7. 初始化 Jenkins 插件和管理员用户
7.1访问jenkins

http://ip:8080

7.2解锁jenkins

获取管理员密码

image

image

image

注意:配置国内的镜像

官方下载插件慢 更新下载地址

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选择“继续”

image

7.4选择“安装推荐插件”

image

7.5插件安装完成,创建管理员用户

image

7.6保存并完成

image

7.7进入完成页面

image

8. 配置 Jenkins 构建工具

image

8.1全局工具配置

image

8.1.1配置jdk

JAVA_HOME:/usr/local/jdk

image

8.1.2配置maven

MAVEN_HOME:/usr/local/maven

image

8.1.2配置git

查看git安装路径:which git

image

9. 构建作业
9.1点击创建一个新任务,进入创建项目类型选择页面

image

填好信息点击“确认”

9.2配置“General”

image

9.3配置“源码管理”

填写源码的git地址

image

添加git用户,git的用户名与密码

image

选择添加的用户,上面的红色提示信息消失,说明连接成功,如下图

image

9.4构建作业

到源码中找到docker脚本

选择“执行shell”

image

保存上面的构建作业

image

9.5构建

构建作业之后,就可以执行构建过程了。

9.5.1执行构建过程

image

9.5.2构建结构

第一列是 “上次构建状态显示”,是一个圆形图标,一般分为四种:

image

蓝色:构建成功;

image

黄色:不确定,可能构建成功,但包含错误;

image

红色:构建失败;

image

灰色:项目从未构建过,或者被禁用;

如上显示蓝色,表示构建成功。

注意:手动触发构建的时间与自动定时构建的时间互不影响。

9.5.3查看控制台输出

image

日志内容:

image

image

  • 16
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
谷粒学苑是一个教育机构,提供课程培训和学习资源,其中有一个叫做Jenkins.war的项目。 Jenkins是一个流行的开源持续集成和交付工具,它提供了一个可视化的界面,帮助自动化构建、测试和发布软件项目。 Jenkins.war则是Jenkins的可执行文件,以.war(Web Application Archive)格式打包。通过将Jenkins.war部署到Java应用服务器中,可以快速搭建起Jenkins服务器环境。 Jenkins.war具有以下特点和功能: 1. 可扩展性:Jenkins.war支持安装插件以扩展其功能。用户可以根据自己的需求选择并安装各种插件,如Git、Maven、SonarQube等。 2. 自动化构建:Jenkins.war能够监控软件代码库的变化,并在检测到新的提交或推送时自动触发构建任务。这样可以确保代码在每次变更后都能正确构建,减少手动操作和人为错误。 3. 高度可配置:Jenkins.war提供了丰富的配置选项,可以根据项目的需求进行精细调整。用户可以设置构建触发条件、构建步骤、构建环境等,以满足不同项目的特定要求。 4. 可视化界面:Jenkins.war提供了直观友好的web界面,方便用户管理和监控构建任务的执行情况。用户可以通过界面查看构建历史、日志输出、构建报告等信息,以便随时了解项目的状态。 总之,Jenkins.war是谷粒学苑所提供的一个工具,能够帮助开发团队提高软件开发流程的效率和质量。通过使用Jenkins.war,可以实现自动化构建、持续集成和交付,从而加快软件开发周期,减少人力成本,并提升软件质量和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值