Spring Boot笔记(持续更新)

springboot常用注解:

https://github.com/mxg133/learnforSpringBoot

注解解释
@SpringBootApplication来标注一个主程序类,说明这是一个springboot应用。
@RequestMapping(“/hello”)接收来自于浏览器的hello请求,给出外界访问方法的路径,或者说触发路径 ,触发条件。
@RequestBody@RequestBody除了把return的结果变成JSON对象返回,还可以把前端传输过来的json数据自动装配成后端可操作的map对象或自定义对象。
@ResponseBody用@ResponseBody标记Controller类中的方法。把return的结果变成JSON对象返回,把类的方法返回的数据写给浏览器。(如果没有这个注解,这个方法只能返回要跳转的路径即跳转的html/JSP页面。有这个注解,可以不跳转页面,只返回JSON数据)。
@Controller@Controller标识的类,该类代表控制器类(控制层/表现层)。
@RestController是==@Controller@ResponseBody==的结合。
@Repository作用于数据访问层。
@Service作用于业务逻辑层。
@SpringBootConfigurationSpringBoot的配置类,标注在某个类上,表明这是一个SpringBoot的配置类。
@EnableAutoConfiguration开启自动配置。
@AutoConfigurationPackage自动配置包,将主配置类(@SpringBootApplication标注的类)的所在包及所有子包里的所有组件扫描到spring容器里。
@Import()给容器导入组件。
@ConfigurationProperties(prefix=“person”)将配置文件中配置的每一个属性,映射到这个组件中,告诉springboot将本类中的所有属性和配置文件中相关的配置进行绑定;prefix前缀,将配置文件中的"person"下的属性映射进来。
@Component把该组件注入springboot容器。作用于实体类或者工具类。
@PropertySource({“classpath:xxx.properties”})加载指定的properties配置文件。
@Configuration指明当前类是一个配置类,就是用来替代之前的spring配置文件,可以和@Bean搭配使用。
@EnableWebMvc加上这个注解,将使所有SpringMVC的自动配置失效,使用自己的配置。
@Async告诉Spring这是一个异步方法
@RequestMapping@RequestMapping可以指定GET、POST请求方式
@GetMapping@GetMapping等价于@RequestMapping的GET请求方式
@ImportResourcespringboot默认使用Java配置,但也可以使用xml配置,只需通过该注解引入一个xml配置。

@PathVariable

@PathVariable绑定URI模板变量值

@PathVariable是用来获得请求url中的动态参数的

@PathVariable用于将请求URL中的模板变量映射到功能处理方法的参数上。//配置url和方法的一个关系

@RequestMapping(“item/{itemId}”)

@RequestMapping 来映射请求,也就是通过它来指定控制器可以处理哪些URL请求,类似于struts的action请求。

@responsebody 表示该方法的返回结果直接写入HTTP responseBody中一般在异步获取数据时使用,在使用@RequestMapping后,返回值通常解析为跳转路径,加上@responsebody后返回结果不会被解析为跳转路径,而是直接写入HTTP responseBody中。比如异步获取json数据,加上@responsebody后,会直接返回json数据。

@Pathvariable 注解绑定它传过来的值到方法的参数上用于将请求URL中的模板变量映射到功能处理方法的参数上,即取出uri模板中的变量作为参数

@RequestMapping("/zyh/{type}")
  public String zyh(@PathVariable(value = "type") int type) throws UnsupportedEncodingException {
    String url = "http://wx.diyfintech.com/zyhMain/" + type;
    if (type != 1 && type != 2) {
      throw new IllegalArgumentException("参数错误");
    }
    String encodeUrl = URLEncoder.encode(url, "utf-8");
    String redirectUrl = MessageFormat.format(OAUTH_URL, WxConfig.zyhAppId, encodeUrl, "snsapi_userinfo", UUID.randomUUID().toString().replace("-", ""));
    return "redirect:" + redirectUrl;m
  }

在SpringMVC后台控制层获取参数的方式主要有两种:

一种是request.getParameter(“name”),另外一种是用注解@RequestParam直接获取

这里主要讲这个注解 @RequestParam

value:参数名字,即入参的请求参数名字,如username表示请求的参数区中的名字为username的参数的值将传入;

required:是否必须,默认是true,表示请求中一定要有相应的参数,否则将报404错误码;

defaultValue:默认值,表示如果请求中没有同名参数时的默认值,例如:

public List getItemTreeNode(@RequestParam(value=”id”,defaultValue=”0”)long parentId)

@Controller
@RequestMapping("/wx")
public class WxController {
    @Autowired
    private WxService wxService;
    private static final Log log= LogFactory.getLog(WxController.class);
    @RequestMapping(value = "/service",method = RequestMethod.GET)
    public void acceptWxValid(@RequestParam String signature, @RequestParam String timestamp, @RequestParam String nonce,
                              @RequestParam String echostr, HttpServletResponse response) throws IOException {
        PrintWriter out = response.getWriter();
        if (SignUtil.checkSignature(signature, timestamp, nonce)) {
            out.print(echostr);
        }else
            out.print("fail");
        out.flush();
        out.close();
    }
@ConfigurationProperties@Value()
功能批量注入配置文件中的属性一个个指定

@RequestParam

(1)不加@RequestParam前端的参数名需要和后端控制器的变量名保持一致才能生效

(2)不加@RequestParam参数为非必传,加@RequestParam写法参数为必传。但@RequestParam可以通过@RequestParam(required = false)设置为非必传。

(3)@RequestParam可以通过@RequestParam(“userId”)或者@RequestParam(value = “userId”)指定传入的参数名。

(4)@RequestParam可以通过@RequestParam(defaultValue = “0”)指定参数默认值

(5)如果接口除了前端调用还有后端RPC调用,则不能省略@RequestParam,否则RPC会找不到参数报错

(6)Get请求访问时:

  • 不加@RequestParam注解:url可带参数也可不带参数,输入 localhost:8080/list1 以及 localhost:8080/list1?userId=xxx 方法都能执行
  • 加@RequestParam注解:url必须带有参数。也就是说你直接输入localhost:8080/list2 会报错,不会执行方法。只能输入localhost:8080/list2?userId=xxx 才能执行相应的方法

Mysql数据库连接配置:

数据库连接application.properties:

spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

数据库连接application.yml:

spring:
datasource:
url: jdbc:mysql://localhost:3306/rate?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver

jpa:
# 显示sql语句
show-sql: true
properties:
hibernate:
# 格式化sql语句
format_sql: true

server:
port: 8080

Thymeleaf语法

官方文档

依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

thymeleaf 命名空间:

xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"

<html lang="en" xmlns:th="http://www.thymeleaf.org" 
				xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
				xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
${...}:获取变量值;
th:object="${}" 来获取对象信息
*{...}:选择变量表达式,在功能上和${...}是一样的;
补充:配合${...}使用,用*{}代替${}获取的对象
示例:
 <div th:object="${session.user}"> #获取对象user 
    <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p> #通过*{}获取对象user的firstName
    <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
    <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
  </div>
#{...}:获取国际化内容(中英文切换)
@{...}:定义URL链接表达式

访问这个链接时:<!-- Will produce 'http://localhost:8080/gtvg/order/details?orderId=3' (plus rewriting) -->
通过:<a href="details.html" th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>
~{...}:片段引用表达式
#表单里面可以有三种方式来获取对象属性
使用 th:value="*{属性名}"
使用 th:value="${对象.属性名}",对象指的是上面使用 th:object 获取的对象
使用 th:value="${对象.get方法}",对象指的是上面使用 th:object 获取的对象

常用的标签操作

标签功能例子
th:value给属性赋值
th:style设置样式
th:onclick点击事件th:onclick="'getInfo()'"
th:if条件判断th:if="${not #strings.isEmpty(msg)}"
th:href超链接th:href="@{/index.html(l=zh_CN)}"
th:unless条件判断和th:if相反
th:switch配合th:case
th:case配合th:switch
th:src地址引入th:src="@{/img/bootstrap-solid.svg}"
th:action表单提交的地址th:action="@{/user/login}"

整合JDBC

jdbc增删改查

JdbcController:

@RestController
public class JdbcController {
    @Autowired
    JdbcTemplate jdbcTemplate;

    //查询数据库的所有信息 万能Map
    @GetMapping("/userList")
    public List<Map<String,Object>> userList(){
        String sql="select * from user";
        List<Map<String, Object>> List_maps = jdbcTemplate.queryForList(sql);
        return List_maps;
    }
    @GetMapping("/addUser")
        public String addUser(){
        String sql="insert into mybatis.user(id,name,pwd) value (1,'小龙','345678')";
        jdbcTemplate.update(sql);
        return "update-ok";
    }
    @GetMapping("/updateUser/{id}")
    public String updateUser(@PathVariable("id") int id){
        String sql="update mybatis.user set name=?,pwd=? where id="+id;
        //封装
        Object[] objects = new Object[2];
        objects[0]="anny";
        objects[1]="121212";

        jdbcTemplate.update(sql,objects);
        return "update-ok";
    }
    @GetMapping("/deletUser/{id}")
    public String deletUser(@PathVariable("id")int id){
        String sql="delete from mybatis.user where id=?";
        jdbcTemplate.update(sql,id);
        return "deletUser-ok";
    }

application.yml:

Spring:
  datasource:
      username: root
      password: 123456
      url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
      driver-calss-name: com.mysql.jdbc.Driver
      type: com.alibaba.druid.pool.DruidDataSource

整合Druid自定义数据源

是阿里巴巴开源平台上的一个数据库连接池实现,结合了C3p0、DBCP、PROXOOL等DB池的优点,同时加入了日志监控

Druid依赖<1.2.8>:

<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.8</version>
</dependency>

log4j依赖:

<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

application.yml:

#SpringBoot默认是不注入这些的,需要自己绑定
#druid数据源专有配置
Spring:
  datasource:
  	  type: com.alibaba.druid.pool.DruidDataSource #选择数据源
      initialSize: 5
      minIdle: 5
      maxActive: 20
      maxWait: 60000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true

      #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
      #如果允许报错,java.lang.ClassNotFoundException: org.apache.Log4j.Properity
      #则导入log4j 依赖就行
      filters: stat,wall,log4j
      maxPoolPreparedStatementPerConnectionSize: 20
      useGlobalDataSourceStat: true
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

DruidConfig:

@Configuration
public class DruidConfig {
    @ConfigurationProperties(prefix = "spring.datasource")//绑定application.yml中的Spring:  datasource:
    @Bean
    public DataSource druidDataSource() {
        return new DruidDataSource();
    }

    //后台监控
    @Bean
    public ServletRegistrationBean StatViewServlet() {
        ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");

        //后台需要有人登录,账号密码配置
        HashMap<String, String> initParameters = new HashMap<>();

        //增加配置
        initParameters.put("loginUsername", "admin");//登录key 是固定的loginUsername loginPassword
        initParameters.put("loginPassword", "123456");

        //允许谁可以访问
        initParameters.put("allow", "");//所有人可访问
		//initParameters.put("allow", "localhost");//本机可访问

        //禁止谁能访问 initParameters.put("mest","192.168.11.123");

        bean.setInitParameters(initParameters);//设置初始化参数
        return bean;

    }

    //filter过滤
    @Bean
    public FilterRegistrationBean webStatFilter() {
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new WebStatFilter());
        //可以过滤哪些请求
        Map<String, String> initParameters = new HashMap<>();
        //这些东西不进行统计
        initParameters.put("exclusions", "*.js,*.css,/druid/*");

        bean.setInitParameters(initParameters);
        return bean;
    }
}

启动项目后访问http://localhost:8080/druid

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-APwqOhWj-1673610088513)(…/springboot随堂笔记/assets/image-20220822201855299.png)]

整合Mybatis

service层主要做的是业务逻辑处理的工作,dao层里面做的是和数据库连接的工作。

service层调用dao层的功能,也就是将dao层的类作为service层类的私有成员变量。

  1. 导入包

mybatis依赖包:

<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
  1. 配置文件
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  1. Mybatis配置
#整合mybatis
mybatis.type-aliases-package=com.mest.pojo
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
  1. 编写sql
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mest.mapper.UserMapper">
	<select id="queryUserList" resultType="User">
    select * from user
	</select>
    
    <select id="queryUserByid" resultType="User">
    select * from user where id=#{id}
	</select>
    
    <insert id="addUser" parameterType="User">
        insert into user (id,name ,pwd) values (#{id},#{name},#{pwd})
    </insert>

    <update id="updateUser" parameterType="User">
        update user set name =#{name},pwd=#{pwd} where id=#{id}
    </update>

    <delete id="deleteUser" parameterType="int">
        delete from user where id=#{id}
    </delete>
</mapper>
  1. Service调用dao层

  2. Controller调用service层

SpringSecurity(安全框架)

在这里插入图片描述

web开发中常用的安全功能,过滤器,拦截器

SpringSecurity主要目标是认证和授权(访问控制)

security依赖:

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.6.8</version>
</dependency>

securityConfig配置:

@EnableWebSecurity
public class securityConfig extends WebSecurityConfigurerAdapter {
    //授权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //首页所有人可以访问,功能页只有有权限的人才能访问
        //请求授权的规则
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3");

        //没有权限跳到登录页,需要开启登录的页面
        //定制登录页面
        http.formLogin().loginPage("/toLogin").loginProcessingUrl("/login");
		//http.formLogin().loginPage("/toLogin");

        //注销
        //防止网站攻击
        http.csrf().disable();//关闭csrf功能
        http.logout().logoutSuccessUrl("/");

        //开启记住我功能 cookie 默认保存两周,自定义接收前端的参数
        http.rememberMe().rememberMeParameter("remember");
    }

    //认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //这些数据正常应该从数据库中读
        //密码编码错误:There was an unexpected error (type=Internal Server Error, status=500).
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
  .withUser("mest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2")
  .and().withUser("saisai").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
  .and().withUser("honey").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
    }

Shiro

有哪些功能:

Authentication:身份验证、登录、验证用户是否拥有相应的身份。

Authorization:授权,即权限验证。判断用户能进行什么操作。

Session Manager:会话管理,即用户登录就是第一次会话,在没有退出之前,所有信息都保存在会话中。

Remember me:记住我。

整合shiro:

项目结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uooB0RF6-1673610088515)(assets/image-20220824103551304.png)]

  1. pom.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.6.4</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.mest</groupId>
        <artifactId>shiro</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>shiro</name>
        <description>Demo project for Spring Boot</description>
        <properties>
            <java.version>1.8</java.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <!--Subject 用户
                SecurityManager 管理所有用户
                Realm 连接所有数据
                -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
    
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>1.2.17</version>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.12</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.0</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.16.10</version>
            </dependency>
    
            <!--        shiro thymeleaf整合的包-->
            <dependency>
                <groupId>com.github.theborakompanioni</groupId>
                <artifactId>thymeleaf-extras-shiro</artifactId>
                <version>2.0.0</version>
            </dependency>
    
    <!--        shiro整合spring的包-->
            <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <version>1.4.1</version>
            </dependency>
    
            <!--thymeleaf模板-->
            <dependency>
                <groupId>org.thymeleaf</groupId>
                <artifactId>thymeleaf-spring5</artifactId>
            </dependency>
            <dependency>
                <groupId>org.thymeleaf.extras</groupId>
                <artifactId>thymeleaf-extras-java8time</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    
    
  2. ShiroConfiger配置类:

   @Configuration
   public class ShiroConfiger {
   
       //ShiroFilterFactoryBean
       @Bean
       public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
           //设置安全管理器
           ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
           bean.setSecurityManager(defaultWebSecurityManager);
   
           //添加shiro的内置过滤器
           /*
            * anon:无需认证就可以访问
            * authc:必须认证了才能访问
            * user:必须拥有记住我功能才能使用
            * perms:拥有对某个资源的权限才能访问
            * role:拥有某个角色的权限才能访问
            * */
   
   //    拦截
           Map<String, String> filterMap = new LinkedHashMap<>();
   
           //授权 正常情况下没有授权会跳转到未授权页面下
           filterMap.put("/user/add", "perms[user:add]");
           filterMap.put("/user/update", "perms[user:update]");
   
   
   //    filterMap.put("/user/add","authc");
   //    filterMap.put("/user/update","authc");
   
           filterMap.put("/user/*", "authc");
           bean.setFilterChainDefinitionMap(filterMap);
   
           //设置登录的请求
           bean.setLoginUrl("/toLogin");
           //未授权页面
           bean.setUnauthorizedUrl("/noauth");
           return bean;
   
       }
   
       //DefaultWebSecurityManager
       @Bean
       public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
           DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
           //关联UserRealm
           securityManager.setRealm(userRealm);
           return securityManager;
       }
   
       //创建realm对象,需要自定义类
       @Bean
       public UserRealm userRealm() {
           return new UserRealm();
       }
   
       @Bean
       //整合shiroDialect: 用来整合shiro和thymeleaf
       public ShiroDialect getShiroDialect() {
           return new ShiroDialect();
       }
   }
  1. 自定义UserRealm类:
 //自定义realm
 public class UserRealm extends AuthorizingRealm {
 
     @Autowired
     UserService userService;
 
 //    授权
     @Override
     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
 
         SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
 //        info.addStringPermission("user:add");默认会走,开启后全部授权,
 
         //当拿到当前登录的这个对象
         Subject subject= SecurityUtils.getSubject();
         User currentUser = (User) subject.getPrincipal();//拿到user对象
         //设置当前用户的权限
         info.addStringPermission(currentUser.getPerms());
         return info;
     }
 
 //    认证
     @Override
     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
 
 //        //用户名,密码
 //        String username="root";
 //        String password="123456";
         //连接真实数据库
         UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
 //        if(!userToken.getUsername().equals(username)){
 //        return null;//抛出异常
 //        }
         User user = userService.queryUserByName(userToken.getUsername());
 
         if(user==null){//没有这个人
             return null;
         }
 
         Subject currentSubject = SecurityUtils.getSubject();
         Session session = currentSubject.getSession();
         session.setAttribute("loginUser",user);
         return new SimpleAuthenticationInfo(user,user.getPwd(),"");//把user对象放入
     }
 }
 
  1. MyController:
    @Controller
    public class MyController {
        @GetMapping({"/", "/index"})
        public String toIndex(Model model) {
            model.addAttribute("msg", "hello,shiro");
            return "index";
        }
    
        @RequestMapping("/user/add")
        public String add() {
            return "user/add";
        }
    
        @RequestMapping("/user/update")
        public String update() {
            return "user/update";
        }
    
        @RequestMapping("/toLogin")
        public String toLogin() {
            return "login";
        }
    
        @RequestMapping("/login")
        public String login(String username, String password, Model model) {
            //获取当前对象
            Subject subject = SecurityUtils.getSubject();
            //封装用户数据
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    
    
            try {
                subject.login(token);//执行登录的方法 如果没有异常就ok
                return "index";
            } catch (UnknownAccountException e) {
                model.addAttribute("msg", "用户名错误");
                return "login";
            } catch (IncorrectCredentialsException e) {
                model.addAttribute("msg", "密码错误");
                return "login";
            }
        }
    
        @RequestMapping("/noauth")
        @ResponseBody
        public String unauthroized() {
            return "未授权页面";
        }
    }
  1. mapper层:

    @Repository
    @Mapper
    public interface UserMapper {

    public User queryUserByName(String name);
    

    }

  2. 实体类pojo:

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private int id;
        private String name;
        private String pwd;
        private String perms;//权限
    }
    
  3. server层:

        //UserService
        public interface UserService {
        
            public User queryUserByName(String name);
        }
    
  4. UserServiceImpl实现类:

 @Service
   public class UserServiceImpl implements UserService{
   
       @Autowired
       UserMapper userMapper;
   
       @Override
       public User queryUserByName(String name) {
           return userMapper.queryUserByName(name);
       }
   }
  1. UserMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mest.mapper.UserMapper">

    <select id="queryUserByName" parameterType="String" resultType="User">
        select * from mybatis.user where name = #{name}
    </select>
</mapper>
  1. application.yml数据库连接:
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #SpringBoot默认是不注入这些的,需要自己绑定
    #druid数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果允许报错,java.lang.ClassNotFoundException: org.apache.Log4j.Properity
    #则导入log4j 依赖就行
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionoProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
  1. application.properties:
mybatis.type-aliases-package=com.mest.pojo
mybatis.mapper-locations=classpath:mapper/*.xml
  1. 前端页面

发送邮件

  1. 导入发送邮件的依赖
		<!--邮件发送-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.sun.mail/jakarta.mail -->
        <dependency>
            <groupId>com.sun.mail</groupId>
            <artifactId>jakarta.mail</artifactId>
            <version>1.6.5</version>
        </dependency>

  1. 配置application.properties文件
spring.mail.username=3112660335@qq.com
spring.mail.password=wvwidyvmstzadhah # qq邮箱pop3令牌

spring.mail.host=smtp.qq.com
#开启加密验证
spring.mail.properties.mail.smtp.ssl.enabled=true
  1. 实现:
@SpringBootTest
class TaskApplicationTests {

    @Autowired
    JavaMailSenderImpl mailSender;
    @Test
    void contextLoads() {
	//简单的邮件
        SimpleMailMessage simpleMailMessage = new SimpleMailMessage();

        simpleMailMessage.setSubject("通知");//主题
        simpleMailMessage.setText("关于延期开学通知");//文本

        simpleMailMessage.setFrom("3112660335@qq.com");
        simpleMailMessage.setTo("3607947574@qq.com");
        mailSender.send(simpleMailMessage);
    }
    //复杂的邮件
    @Test
    void contextLoads2() throws MessagingException {

        MimeMessage mimeMessage = mailSender.createMimeMessage();
        //组装
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
        //正文
        helper.setSubject("网易夏日摸鱼");
        helper.setText("<p style='color:blue'>网易邮箱摸鱼大片爆笑热映,加班?大饼?\n 漏漏漏!只要我摸得够多,内卷就追不上我!</p>",true);

        //附件
        helper.addAttachment("1.jpg",new File("C:\\Users\\缪晟\\Desktop\\1.jpg"));

        helper.setFrom("3112660335@qq.com");
        helper.setTo("3607947574@qq.com");
        mailSender.send(mimeMessage);
    }

    //封装发送邮件的方法
    /*
    tf:true or false
    subject:主题
    text:正文
    * */
    public void sendMail(Boolean tf,String subject,String text) throws MessagingException {

        MimeMessage mimeMessage = mailSender.createMimeMessage();
        //组装
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,tf);
        //正文
        helper.setSubject(subject);
        helper.setText("<p style='color:blue'>"+text+"</p>",tf);

        //附件
        helper.addAttachment("1.jpg",new File("C:\\Users\\缪晟\\Desktop\\1.jpg"));

        helper.setFrom("3112660335@qq.com");
        helper.setTo("3607947574@qq.com");
        mailSender.send(mimeMessage);
    }
}

定时执行任务

https://www.bejson.com/ 网络,前端,后端,加解密,转换,数据处理,表达式网站

@Service
public class ScheduledService {
    //在一个特定的时间执行
    //cron()表达式
    /*
  //秒 分 时 日 月 星期
    30 15 10 * * ?  每天的10点15分30秒执行
    0 0/5 10,18 * * ?  每天的10点和18点,每隔5分钟执行执行一次
    0 15 10 ? * 1-6    每个月的星期1-6 的10点15分执行一次
    0 15 10 L * ?    每个月的最后一天的10点15分执行一次
    0/2 * * * * ?     每隔两秒执行一次
    * */
    //秒 分 时 日 月 星期
    @Scheduled(cron = "0/2 * * * * ?")
    public void hello(){
        System.out.println("hello,你被执行了");
    }
}

SpringBoot整合Redis

  1. 引入redis:
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  1. 配置application.properties:
#SpringBoot所有的配置类,都有一个自动配置类RedisAutoConfig
#自动配置类都会绑定一个properties 配置文件 RedisProperties
spring.redis.host=127.0.0.1
spring.redis.port=6379
  1. 创建User对象
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
//implements Serializable实现序列化
public class User implements Serializable {
    private String username;
    private Integer age;
}

  1. 测试存值取值
@SpringBootTest
class SpringbootRedisApplicationTests {

    //引入RedisTemplate模板
    @Autowired
    @Qualifier("redisTemplate")
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        //redisTemplate 操作不同的数据类型 api和我们的指令命令是一致的
        //opsForValue 操作字符串 类似String
        //opsForList  操作List 类似List
        //opsForSet
        //opsForHash
        //opsForGeo
        //除了基本的操作 我们常用的方法也可以通过redisTemplate来操作 比如事务 和CRUD

        //获取redis的连接对象
//    RedisConnection connection= redisTemplate.getConnectionFactory().getConnection();
//    connection.flushAll();
//    connection.flushDb();
        redisTemplate.opsForValue().set("mykey", "hellomest");
        System.err.println(redisTemplate.opsForValue().get("mykey"));
    }

    //测试存放user对象(User对象未实例化)
    @Test
    public void testRedis() throws JsonProcessingException {
        User user = new User("redis_zhangsan", 12);
        String jsonUser = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user", jsonUser);//key,value
        System.err.println(redisTemplate.opsForValue().get("user"));
    }
    
    //测试存放user对象(User对象被实例化)
    @Test
    public void testRedis2() throws JsonProcessingException {
        User user = new User("redis_lisi", 11);
        redisTemplate.opsForValue().set("user", user);//key,value
        System.err.println(redisTemplate.opsForValue().get("user"));
    }

    //使用RedisUtils工具类
    @Autowired
    private RedisUtil redisUtil;

    @Test
    public void testRedisUtil() {
        redisUtil.set("username","hello");
        System.out.println(redisUtil.get("username"));
    }
}

RedisTemplate模板

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
 
/**
 * 定义RedisTemplate模板
 */
@Configuration
public class RedisConfig {
 
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
 
        // Json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
 
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
 
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
 
        // String序列化配置
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
 
        // key采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
 
        return redisTemplate;
    }
}

在SpringbootRedisApplicationTests中使用RedisTemplate模板:

    @Autowired
    @Qualifier("redisTemplate")
    private RedisTemplate redisTemplate;

    @Test
    public void testRedis2() throws JsonProcessingException {
        User user = new User("redis_lisi", 11);
        redisTemplate.opsForValue().set("user", user);//key,value
        System.err.println(redisTemplate.opsForValue().get("user"));
    }

RedisUtils工具类

封装RedisUtils工具类,避免使用原生api的方式编写代码,直接使用:

package com.mest.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }


    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }


    // ============================String=============================

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */

    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 普通缓存放入并设置时间
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */

    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 递增
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }


    /**
     * 递减
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }


    // ================================Map=================================

    /**
     * HashGet
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * HashSet 并设置时间
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }


    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }


    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }


    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }


    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     * @param key 键
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */

    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将list放入缓存
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */

    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */

    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }

    }

}

在SpringbootRedisApplicationTests中使用RedisUtils工具类:

    @Autowired
    private RedisUtil redisUtil;

    @Test
    public void testRedisUtil() {
        redisUtil.set("username","hello");
        System.out.println(redisUtil.get("username"));
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值