SpringBoot学习(黑马),基础篇

1.SpringBoot 项目创建

1.1.IDEA的SpringBoot 项目创建

 这里我们的jdk,安装的是8,所以版本选成8.

如果是mvc项目,这里要用web。选这个。 

写个入门案例

package com.itheima.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Just Ching
 * @create 2022-02-09 6:34
 */
//rest开发模式
@RestController
@RequestMapping("/books")

public class BookController {
    @GetMapping
    public String getById(){
        System.out.println("sb is running");
        return "spring boot is running";
    }
}

1.1.1 运行环境

1.2.官网创建项目模块

 https://start.spring.io/

 


点击下方generate生成。

下载这个文件,放进IDEA工作空间。就可以了。 


1.3.阿里云网址创建项目

这个创建速度十分快。

c

1.4 不联网创建项目,制作。

创建一个全新的模块,maven的pom,copy springboot的pom就行

<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.3</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>


<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</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>

然后new一个全新的类。

 ,然后就可以用了。

1.5 隐藏,不用修改也不想看的文件

README.md;mvnw.cmd;.gitignore;HELP.md;mvnw;.mvn;*.iml;

1.6 导入依赖的parent。

现在在pom里,导包不用加版本号,它自己选择最适合的。避免冲突。

 里面放的都是坐标版本。

点开一看。

 里面的依赖管理。

1.7 starter

我们建立项目选的web类型,自动给导入starter。

导入了starter,starter有web要用的包。

1.8 启动的引导类

 这句话就是启动spring容器。

 这个能扫描到它所在包的bean。别的包扫描不到。

1.9 内置tomcat

内嵌的tomcat核心。 

将tomcat执行过程抽取出来,变成一个对象。然后把这个对象交给spring容器去管。

 1.9.1 jetty

不用tomcat,用jetty。排除这个,导入jetty。

2.REST开发

 

发的值少,用路径参数@pathvariable。

值多,用json。 

 2.1 restful开发案例

不同的提交类型对应不同的方法。

 这样可以取到值给自己的方法。

 2.2 restful 简化开发

1.直接去掉value,自动识别了属于是。

路径提到类上。

@ResponseBody,也提出来,放在类上,这个类里所有的方法都带有这个注解

 

上面俩注解可以合并

2.能不能去掉method?

2.boot的配置

2.1 复制工程

复制一份,模块文件。然后修改新工程里的pom文件。

别的文件删了,就剩这俩。

 

idea里导入全新的模块 

 

然后导入的时候选maven工程。

在IDEA打开后,再打开pom文件。删掉这两句。

或者修改<name>,里的值,改成和模块名一样。 

 

 
2.2 基础属性配置

里面能配置,是因为pom里导入的spring相关的包。

2.2.1修服务器

必须得有stater web包才能配置。

boot使用的配置文件。 properties,改个key和value。

 

改成这样,我们启动项目的时候。访问路径就会变化。

前面加前缀,访问。 

 2.2.2修改下面的图案

banner。导入starter包就能用。

改成自己的图片。

2.2.3日志信息查看

改成debug调试的时候可以看到boot启动的每一步。

 改成error,出错才有日志。默认是info级别。

2.3 配置文件类型

格式写的不一样罢了。 

2.3.1配置文件加载优先级。

yaml里写配置信息,没有提示。是因为boot现在不认这是配置文件。

 

所以我们要改的让它认识这是配置文件。

 

 然后点击加进去就行。

 

然后成功后的样子。

 2.3.2 配置文件的小bug

如果添加的时候不识别,那就先建properties文件。再添加。 

2.3.3 yaml 详解

容易阅读。

语法规则

属性前面有空格。

同样的名称只允许出现一次。 

数组表示,用-,分开一个数组对象。

2.4 读取配置文件(yml)

controller里读取。用spring的value注入。

读取user的属性。

 2.4.1 数据引用问题

引用别的路径

转义字符的使用

 这里就转义字符\t,起作用了。制表符。

使用引号包裹的字符串,里面转义的字符可以生效

 2.4.2 解决数据一条一条读出来的问题

1.封装全部数据

整一个,所有数据的变量对象。自动装进。

然后用这个对象的getProperty方法获取值。

2.封装自己需要的数据。

比如我们只想要datasource。

创建类,用于封装下面的数据,这个类定义成bean。注意:类里的属性名,与配置文件属性名必须一样

让spring 帮我们去加载数据到对象中,一定 要告诉spring加载这组信息

 用@ConfigurationProperties将配置文件的属性绑定。

后面生成对象,会把属性里的值赋值给对象。

3.boot整合第三方技术

目的学会思想:拿到任何第三方技术的时候,怎么开展工作。

两个步骤。

3.1 boot整合Junit

test里的这个类就是整合Junit的类。 

随便建一个boot工程,Junit是自带的。pom里这个是自动导入的。

1.建一个dao类,里面有方法,测试里面的方法。

 2.在test里的那个类里测试。

@SpringBootTest

这个注解定义了这个类是测试类。注入对象,就可以用这个对象的方法。

3.1.1 纯手工做一个boot工程时

3.1.2 测试类的注意事项

如果当前的测试类,在引导类所在的包及其子包下。不需要修改什么,这个测试类就可以启动。

 那如果当前的测试类,不在引导类所在的包及其子包下。需要在注解后加一句话。

写上引导类的类名。

 

3.2 boot整合mybatis

3.2.1 简单整合案例

 新建一个项目,SQL那里选上Mybatis Framework。和MySQL Driver

 

pom文件里发生了一些变化。

配上datasource。

mapper层写好数据库查询方法。

 然后在测试类里运行。

3.2.2 整合myBatis过程可能遇到的:错误一

 配时区。

解决方法1:url后面加上。

3.2.3 整合myBatis过程可能遇到的:错误二

driver废弃,建议换新的driver。

 

 这里修改成这样即可。

3.3 boot整合mybatis plus

3.3.1正常建立mybatis plus工程

  正常建立boot项目,spring里没有引进mybatis plus。只导入mysQL的驱动。

然后去mvn里查,mybatis plus。

找到要用的版本定义的坐标。导入pom。 

 3.3.2   用aliyun创建。

3.3.3 小案例

在mapper的接口里。继承BaseMapper,把要操作的对象的泛型传进去。

里面有很多写好了的方法。国人开发好的技术

但后面会遇到一个问题与数据库的表设定有关系。

也就是它自己默认搜的表,没和我们数据库的表对应上。

实体类的名和数据库表名不一样。 

方法一:

我们可以在domain里的实体类上加一个注解。@TableName("数据库里的表名。")

方法二:在yml文件里,加上前缀或后缀(与数据库里表的不同)。

3.4 整合Druid 数据源

        数据源是一种用来提高数据库连接性能的常规手段,数据源会负责维持一个数据库连接池,当程序创建数据源实例时,系统会一次性地创建多个数据库连接,并把这些数据库连接保存在连接池中。当程序需要进行数据库访问时,无须重新获得数据库连接,而是从连接池中取出一个空闲的数据库连接,当程序使用数据库连接访问结束后,无须关闭数据库连接,而是将数据库连接归还给连接池即可。通过这种方式,就可比避免频繁地获取数据库连接,关闭数据库连接所导致的性能下降。

数据源是给mybatis用的。

方式一:

导入druid坐标。

在yml文件里配上

 方式二:没导druid的starter。

 4. 基于SpringBoot的SSMP整合案例。

4.1 项目搭环境

建一个项目,导入MP得坐标。

4.2 实体类和数据库表

自己建一个数据库表

 用lombok,必须得按lombok插件

boot里收录了,lombok得坐标。 

 

实体类加@data注解,除了构造方法。例如tostring,get set,hashcode。

都写好了。

 4.3 数据层开发、

 4.3.1 先导入pom,然后修改配置。

application.yml

server:
  port: 80
spring:
  datasource:
    druid:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ssmp?serverTimezone=UTC
    username: root
    password: a839846976


mybatis-plus:
  global-config:
    db-config:
      table-prefix: tbl_

4.3.2 dao接口(跟mapper一样,叫法不一样)

接口dao

package com.book.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.book.domain.Book;
import org.apache.ibatis.annotations.Mapper;


@Mapper
public interface BookDao extends BaseMapper<Book> {

}

4.3.3 增加对象会出现的问题(insert)

加数据的时候, 参数类型对应失败。

这里id,没有像数据库里那样自动生成。

Exception: nested exception is org.apache.ibatis.reflection.ReflectionException:

Could not set property 'id' of 'class com.book.domain.Book' with value '1492005888272625666' Cause: java.lang.IllegalArgumentException: argument type mismatch。

 

4.3.4 MP的运行日志

这里就选标准的形式。再运行就得到整个运行过程。

4.3.5 分页

在方法里写好分页的方法

 如果不用拦截器,结果是,SQL语句里没有limit语句。

这个limit限制,必须得自己配置。

写一个配置类,用拦截器的方法,来加上limit语句。。

Spring的@Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中。

SpringIOC 容器管理一个或者多个bean,这些bean都需要在@Configuration注解下进行创建,在一个方法上使用@Bean注解就表明这个方法需要交给Spring进行管理。

@configuration,标记为配置类,这样可以读取里面的配置信息。

然后@bean注入MP的拦截器。创建MP的拦截器,添加内部拦截器。

添加一个分页的拦截器,PagenationInnerInterceptor。

return出来就行。

就能实现limit分页的功能了。

结果,getRecords,就是得到当前第二页的所有数据。

getTotal就是获取所有数据的条数。

getPages,就是获得这些数据总共分了多少页。

分页查询的代码:

MGConfig.java


package com.book.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MPConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor (){
        MybatisPlusInterceptor interceptor=new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;

    }


}

bookService里

 IPage<Book> getPage(int currentPage,int pageSize);

bookServiceimpl里

@Override
    public IPage<Book> getPage(int currentPage, int pageSize) {
        IPage page=new Page(currentPage,pageSize);

        return bookDao.selectPage(page,null);

 4.3.6 按条件查询

 里面封装了一个查询对象。可以通过修改这个对象的属性,更改条件。

就比如   like属性

定义一个查询类型。

用like方法,匹配查询,含有name属性含有,spring,的book实体。

类似这句SQL。

结果,看日志里。

这个方法的改进方法,这样写属性的时候,name属性就不会自己写错了。 

 这里给name当成字符串了。

解决方法:里面判断一下。是不是null

4.3.7 按条件的分页查询

给前面整的按条件查询的条件代入。

4.4 业务层的开发

MP里的方法,返回值都为int类型,影响几行。

在test建立的service包下测试。

在业务层不像数据库层那样能看详细日志。

调试的时候还是打印来看。

bookService 接口

package com.book.Service;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.book.domain.Book;

import java.util.List;


public interface BookService {
    Boolean save(Book book);
    Boolean update(Book book);
    Boolean delete(Integer id);
    Book getById(Integer id);
    List<Book> getAll();
    IPage<Book> getPage(int currentPage,int pageSize);



}

 bookServiceimpl

package com.book.Service.impl;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.book.Service.BookService;
import com.book.dao.BookDao;
import com.book.domain.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author Just Ching
 * @create 2022-02-11 18:39
 */
@Service
public class BookServiceimpl implements BookService {

    @Autowired
    private BookDao bookDao;



    @Override
    public Boolean save(Book book) {
       return bookDao.insert(book) >0 ;
    }

    @Override
    public Boolean update(Book book) {
        return bookDao.updateById(book)>0;
    }

    @Override
    public Boolean delete(Integer id) {
        return bookDao.deleteById(id)>0;
    }

    @Override
    public Book getById(Integer id) {
        return bookDao.selectById(id);
    }

    @Override
    public List<Book> getAll() {
        return bookDao.selectList(null);
    }

    @Override
    public IPage<Book> getPage(int currentPage, int pageSize) {
        IPage page=new Page(currentPage,pageSize);

        return bookDao.selectPage(page,null);
        /*或者bookDao.selectPage(page,null);
        return page,都是一个东西。都返回一个page*/


    }
}

4.4.1 业务层的快速开发

mybatis甚至帮我做了业务层的通用接口和它的实现类。........

但是很蠢,都是写死的方法。

这个类继承IService

  然后在Service实现类里,用写好的方法。继承ServiceImpl<dao,对象类型>。

人家没写的方法你就自己写一下。

或者自己重写。

4.5 表现层开发、

对象数据,用@requestBody,接收传过来的JSON数据

 BookController.java

package com.book.controller;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.book.Service.BookService;
import com.book.domain.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * @author Just Ching
 * @create 2022-02-11 22:48
 */
@RestController
@RequestMapping("/books")
public class BookController {

    @Autowired
    private BookService bookService;

@GetMapping
    public List<Book> getAll(){
        return bookService.getAll();
    }

    @PostMapping
    public Boolean save(@RequestBody Book book)
    {
        return bookService.save(book);
    }

    @PutMapping
    public Boolean update(@RequestBody Book book)
    {
        return bookService.update(book);
    }

    @DeleteMapping("{id}")
    public Boolean delete(@PathVariable Integer id){
    return bookService.delete(id);
    }


    @GetMapping("{id}")
    public Book getById(@PathVariable Integer id){
    return bookService.getById(id);
    }

    /*http://localhost/books/1/10*/
    @GetMapping("{currentPage}/{pageSize}")
    public IPage<Book> getPage(@PathVariable int currentPage,int pageSize){

    return bookService.getPage(currentPage, pageSize);

    }




}

 4.5.1 表现层消息一致性处理

主要是让前端看到操作成功没有

只要前后端协议好了就行。 

前后端数据协议。

 

 R.java

package com.book.controller.utils;

import lombok.Data;

@Data
public class R {
    private Boolean flag;
    private Object data;
    public R(){

    }
    public R(Boolean flag){
        this.flag=flag;
    }
    public R(Boolean flag,Object data){
        this.flag=flag;
        this.data=data;
    }

}

BookController2.java

package com.book.controller;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.book.Service.BookService;
import com.book.controller.utils.R;
import com.book.domain.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;


@RestController
@RequestMapping("/books")
public class BookController2 {

    @Autowired
    private BookService bookService;

@GetMapping
    public R getAll(){

    return new R(true,bookService.getAll());
    }

    @PostMapping
    public R save(@RequestBody Book book)
    {
        return new R(bookService.save(book));

    }

    @PutMapping
    public R update(@RequestBody Book book)

    {
        return new R(bookService.update(book));
    }

    @DeleteMapping("{id}")
    public R delete(@PathVariable Integer id){

        return new R(bookService.delete(id));
    }


    @GetMapping("{id}")
    public R getById(@PathVariable Integer id)
    {
        return new R(true,bookService.getById(id));
      /*这里直接flag是true*/
    }

    /*http://localhost/books/1/10*/
    @GetMapping("{currentPage}/{pageSize}")
    public R getPage(@PathVariable int currentPage,int pageSize){

        return new R(true,bookService.getPage(currentPage,pageSize));

    }




}

4.5 前端的开发

不然会有idea的小bug。 

 

4.5.1 刷页面的时候数据显示到页面。

 然后在控制台,看显示的信息。

 这里发的是get请求,如果是post请求。.post 

我们下一步要让res.data展示出来。要展示的表的数据来源“dataList”。

我们只需要让dataList里面有数据,就能展示出来。

EL UI,的双向数据绑定。

res的data里,有flag和data。所以要再写一个。

 4.5.2 新增数据

 

触发handleCreate()函数。 然后把新增的界面展示出来

 变为true就行。

重启项目,就能弹出。这是个弹出页面。点击之后,

在handle方法里,把弹出属性设置为true,就弹出了。

然后写完数据,点确定,我们要把数据发到后台。它关联一个handleAdd(),方法。

 model是formData。

我们把这个formData,给post过去

flag为true了。

添加成功或失败的功前端源码:

  //添加
            handleAdd () {
                axios.post("/books",this.formData).then((res)=>{
                    //判断当前操作是否成功
                    if(res.data.flag){
                        /*1.成功关闭弹层*/
                        this.dialogFormVisible= false;
                        this.$message.success("添加成功");
                    }else {
                        this.$message.error("添加失败");
                    }

         /*不管成功失败,都要查询一遍结果*/
                }).finally(()=>{
                    this.getAll();

                });
            },

但随之而来的小问题,随着我们新建一个图书,里面的值还在。所以我们需要,每次弹层的时候,

清空里面的数据。

 

给里面的formData清空。

 

取消按钮的功能关闭弹层

 

 

 

4.5.3 删除功能

ELMENT UI里面把行封装成对象。row指的是这一行的数据。

 我们随便点一个删除,把操作打成consloe.log(row),控制台里看row的信息。

 

,如果手抖删错了也不行,得在删除前做一个提醒。

然后点确定才进行删除。写一下确定按钮对应的功能。catch操作的是取消按钮,没有catch就是执行确定的按钮。

 我们之前写好的删除功能如下:

把它和删除前的确认结合即可。

 handleDelete(row) {
                axios.delete("/books/"+row.id).then((res)=>{

                    if(res.data.flag){
                        /*1.成功关闭弹层*/
                        this.$message.success("删除成功");
                        this.dialogFormVisible= false;
                    }else {
                        this.$message.error("删除失败");
                    }

                }).finally(()=>{
                    this.getAll();

                });

            },

 

结合后的代码:

 // 删除

            handleDelete(row) {

               this.$confirm("此操作将永久删除当前信息,是否继续?","提示",{type:"info"}).then(()=>{
                   axios.delete("/books/"+row.id).then((res)=>{
                       if(res.data.flag){

                           this.$message.success("删除成功");

                       }else {
                           this.$message.error("删除失败");
                       }

                   }).finally(()=>{
                       this.getAll();

                   });

               }).catch(()=>{
                   this.$message.info("取消操作");

               });

            },

4.5.4 修改功能(编辑功能)

点开修改功能,类似添加页面,里面有回显的数据。然后修改这些数据。

 先弹出编辑窗口

 //弹出编辑窗口
            handleUpdate(row) {
              /*后端 update功能是get方法。*/

                axios.get("/books/"+row.id).then((res)=> {
                 if(res.data.flag && res.data.data != null)
                 {
                     //编辑用的弹出窗口。
                     this.dialogFormVisible4Edit = true;
                     this.formData = res.data.data;
                 }else {
                     this.$message.error("数据同步失败,自动刷新")
                 }

                }).finally(()=>{
                    this.getAll();

                });

            },

然后在编辑窗口里修改。看看弹层里对应的方法,写那个方法。 

这个handleEdit功能给表里的数据传进去。跟添加功能一样,给弹出的那个页面的数据,传进数据库。不过这个弹层的

点取消去cancel方法。

4.5.5 异常消息处理原理

抛一个数据库IO异常。 

 

正常返回给前台,不是true就是false,你这里抛出的异常给前端,别人不好处理。

我们要保证的,除了异常,也要给前端他们读的懂的数据。

SpringMVC里有异常处理器,在表现层里处理就好,因为异常会一层一层往上抛出去。

这里拦截异常,然后把它处理成R对象传给前台。

这个注解里可以放,你想拦截的异常类型。

 

 这里用@RestControllerAdvice,是因为要用response。可以点开注解进去看看。

 

 

 

我们在controller抛出一个异常,检查一下。

@PostMapping
    public R save(@RequestBody Book book) throws IOException {
        if(book.getName().equals("123")) throw new IOException();

        return new R(bookService.save(book));

    }

在前台接收这个msg。 

 

4.5.6 异常处理最终版本

R里整个构造器。

 

 

 

4.5.7 分页功能(改进后的getALL方法,分页查询所有)

页面上的布局 LayOut  

 

vue里定义的三个初始数据。前端定义的分页的值

 

我们需要给从前端,把值传给后台让它做一个分页处理。然后把值返回给前台。

 

这里@PathVariable。注解,一个参数后面要跟一个注解。这里必须两个

 

然后我们点击要跳到的页面的数字。

 这里会返回一个currentPage。点下2,currentPage=2。我们在红线的方法里处理。

 

有两个BUG,

1是id的问题,每次都是从1开始。

解决:这里绑定id即可。

 

2.是如果当前页数据删完了,想跳到第三页,第三页却已经不存在。

解决:如果当前的页码值,大于总页码值数,使用最大页码值作为当前页码值。

 

 

 

4.5.8 条件查询

 

这三条数据没有绑定。而且点了这个查询按钮,跳到getALL方法,说明条件查询做在getAll方法里。

定义这一下这三个数据模型。

 

 

完成绑定。

 

然后我们接收查询里传来的信息

通过param参数传到后端。

 getAll() {
                param ="?type="+this.pagination.type;
                param +="&name="+this.pagination.name;
                param +="&description" +this.pagination.description;

                
                //发送异步请求,用箭头函数,把里面的数据取出来。
                axios.get("/books/"+ this.pagination.currentPage + "/"+this.pagination.pageSize + param).then((res)=>{

                    this.pagination.currentPage=res.data.data.current;
                    this.pagination.total=res.data.data.total;
                   this.dataList=res.data.data.records;

                });


            },

在controller层里用book对象接收。 

输出可看到。 

 

把book放进方法里,接口里没有这个方法。点击refactor,添加这个方法。

 

service层改成这样,然后去实现类里改。

 实现类里重写的方法。实现了模糊查询。

@Override
    public IPage<Book> getPage(int currentPage, int pageSize, Book book) {

        LambdaQueryWrapper <Book> lqw =new LambdaQueryWrapper<Book>();
        lqw.like(Strings.isEmpty(book.getType()),Book::getType,book.getType());
        lqw.like(Strings.isEmpty(book.getName()),Book::getName,book.getName());
        lqw.like(Strings.isEmpty(book.getDescription()),Book::getDescription,book.getDescription());

        IPage page=new Page(currentPage,pageSize);

        return bookDao.selectPage(page,lqw);
        /*或者bookDao.selectPage(page,null);
        return page,都是一个东西。都返回一个page*/


    }

    @Override
    public IPage<Book> getPage(int currentPage, int pageSize) {
        IPage page=new Page(currentPage,pageSize);

        return bookDao.selectPage(page,null);
        /*或者bookDao.selectPage(page,null);
        return page,都是一个东西。都返回一个page*/
    }

4.6 ssmp案例 总结与后面展望。

 

 

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值