CH2-SSMP案例解析

web应用图书管理系统介绍

  1. 案例效果演示

image-20220318225151017

  1. 案例实现方案分析

    • 实体类开发————使用Lombok快速制作实体类

    • Dao开发————整合MyBatisPlus,制作数据层测试类

    • Service开发————基于MyBatisPlus进行增量开发,制作业务层测试类

    • Controller开发————基于Restful开发,使用PostMan测试接口功能

    • Controller开发————前后端开发协议制作

    • 页面开发————基于VUE+ElementUI制作,前后端联调,页面数据处理,页面消息处理

      • 列表、新增、修改、删除、分页、查询
    • 项目异常处理

    • 按条件查询————页面功能调整、Controller修正功能、Service修正功能

  2. SSMP案例制作流程解析

    • 先开发基础CRUD功能,做一层测一层

    • 调通页面,确认异步提交成功后,制作所有功能

    • 添加分页功能与查询功能

配置

  1. 勾选SpringMVC与MySQL坐标
  2. 修改配置文件为yml格式
  3. 设置端口为80方便访问

一、实体类开发

  1. 实体类制作

  2. 使用lombok简化开发

    • 导入lombok无需指定版本,由SpringBoot提供版本
    • @Data注解
  • Lombok,一个Java类库,提供了一组注解,简化POJO实体类开发
  • lombok版本由SpringBoot提供,无需指定版本
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
  • 常用注解:@Data
  • 为当前实体类在编译期设置对应的get/set方法,toString方法,hashCode方法,equals方法等
@Data
public class Book {
    private Integer id;
    private String type;
    private String name;
    private String description;
}

二、数据层开发

  • 技术实现方案
  • MyBatisPlus
  • Druid

2.1 开发环境配置

  1. 手工导入starter坐标(2个)
  2. 配置数据源与MyBatisPlus对应的配置
  3. 开发Dao接口(继承BaseMapper)
  4. 制作测试类测试Dao功能是否有效
  5. 使用配置方式开启日志,设置日志输出方式为标准输出
  • 导入MyBatisPlus与Druid对应的starter
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.3</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.6</version>
</dependency>
  • 配置数据源与MyBatisPlus对应的基础配置(id生成策略使用数据库自增策略)
server:
  port: 81

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/new_db?serverTimezone=UTC
      username: root
      password: 2001

mybatis-plus:
  global-config:
    db-config:
      table-prefix: tbl_
      id-type: auto
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  • 继承BaseMapper并指定泛型
@Mapper
public interface BookDao extends BaseMapper<Book> {
}
  • 制作测试类测试结果
@SpringBootTest
public class BookDaoTest {
    @Autowired
    private BookDao bookDao;
    @Test
    void testSave() {
        Book book = new Book();
        book.setName("测试数据");
        book.setType("测试类型");
        book.setDescription("测试描述数据");
        bookDao.insert(book);
    }
    @Test
    void testGetById() {
        System.out.println(bookDao.selectById(13));
    }
    ...
}
  • 为方便调试可以开启MyBatisPlus的日志

    mybatis-plus:
    	configuration:
    		log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    

2.2 分页功能

  1. 使用IPage封装分页数据
  2. 分页操作依赖MyBatisPlus分页拦截器实现功能
  3. 借助MyBatisPlus日志查阅执行SQL语句
  • 分页操作需要设定分页对象IPage
@Test
void testGetPage(){
    IPage page = new Page(1,5);
    bookDao.selectPage(page,null);
}
  • IPage对象中封装了分页操作中的所有数据

    • 数据
    • 当前页码值
    • 每页数据总量
    • 最大页码值
    • 数据总量
  • 分页操作是在MyBatisPlus的常规操作基础上增强得到,内部是动态的拼写SQL语句,因此需要增强对应的功能,使用MyBatisPlus拦截器实现

@Configuration
public class MpConfig {
    @Bean
    public MybatisPlusInterceptor mpInterceptor() {
        //1.定义Mp拦截器
        MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
        //2.添加具体的拦截器
        mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mpInterceptor;
    }
}

2.3 条件查询功能

  1. 使用QueryWrapper对象封装查询条件
  2. 推荐使用LambdaQueryWrapper对象
  3. 所有查询操作封装成方法调用
  4. 查询条件支持动态条件拼装
  • 使用QueryWrapper对象封装查询条件,推荐使用LambdaQueryWrapper对象,所有查询操作封装成方法调用
@Test
void testGetByCondition(){
    IPage page = new Page(1,10);
    LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<Book>();
    lqw.like(Book::getName,"Spring");
    bookDao.selectPage(page,lqw);
}
@Test
void testGetByCondition(){
    QueryWrapper<Book> qw = new QueryWrapper<Book>();
    qw.like("name","Spring");
    bookDao.selectList(qw);
}
  • 支持动态拼写查询条件
@Test
void testGetByCondition(){
    String name = "Spring";
    IPage page = new Page(1,10);
    LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<Book>();
    lqw.like(Strings.isNotEmpty(name),Book::getName,"Spring");
    bookDao.selectPage(page,lqw);
}

三、业务层开发

  1. Service接口名称定义成业务名称,并与Dao接口名称进行区分
  2. 制作测试类测试Service功能是否有效
  • Service层接口定义与数据层接口定义具有较大区别,不要混用

    • selectByUserNameAndPassword(String username,String password);
    • login(String username,String password);
  • 接口定义

public interface BookService {
    boolean save(Book book);
    boolean delete(Integer id);
    boolean update(Book book);
    Book getById(Integer id);
    List<Book> getAll();
    IPage<Book> getByPage(int currentPage,int pageSize);
}
  • 实现类定义
@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;
    public Boolean save(Book book) {
        return bookDao.insert(book) > 0;
    }
    public Boolean delete(Integer id) {
        return bookDao.deleteById(id) > 0;
    }
    public Boolean update(Book book) {
        return bookDao.updateById(book) > 0;
    }
}


@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;
    public Book getById(Integer id) {
        return bookDao.selectById(id);
    }
    public List<Book> getAll() {
        return bookDao.selectList(null);
    }
    public IPage<Book> getByPage(int currentPage, int pageSize) {
        IPage page = new Page<Book>(currentPage,pageSize);
        return bookDao.selectPage(page,null);
    }
}
  • 测试类定义
@SpringBootTest
public class BookServiceTest {
    @Autowired
    private BookService bookService;
    @Test
    void testGetById(){
        bookService.getById(9);
    }
    @Test
    void testGetAll(){
        bookService.getAll();
    }
    @Test
    void testGetByPage(){
        bookService.getByPage(1,5);
    }
    … …
}

3.1 快速开发

  1. 使用通用接口(ISerivce)快速开发Service
  2. 使用通用实现类(ServiceImpl<M,T>)快速开发ServiceImpl
  3. 可以在通用接口基础上做功能重载或功能追加
  4. 注意重载时不要覆盖原始操作,避免原始提供的功能丢失

image-20220318203316499

  • 实现类定义
@Service
public class BookServiceImpl2 extends ServiceImpl<BookDao,Book> implements IBookService {
}
  • 实现类追加功能
@Service
public class BookServiceImpl2 extends ServiceImpl<BookDao,Book> implements IBookService {
}
@Service
public class BookServiceImpl2 extends ServiceImpl<BookDao,Book> implements IBookService {
    @Autowired
    private BookDao bookDao;
    public Boolean insert(Book book) {
        return bookDao.insert(book) > 0;
    }
    public Boolean modify(Book book) {
        return bookDao.updateById(book) > 0;
    }
    public Boolean delete(Integer id) {
        return bookDao.deleteById(id) > 0;
    }
    public Book get(Integer id) {
        return bookDao.selectById(id);
    }
}

四、表现层开发

  1. 基于Restful制作表现层接口
    • 新增:POST
    • 删除:DELETE
    • 修改:PUT
    • 查询:GET
  2. 接收参数
    • 实体数据:@RequestBody
    • 路径变量:@PathVariable
  • 基于Restful进行表现层接口开发

  • 使用Postman测试表现层接口功能

  • 功能测试

    @RestController
    @RequestMapping("/books")
    public class BookController {
        @Autowired
        private IBookService bookService;
        @GetMapping
        public List<Book> getAll(){
            return bookService.list();
        }
    }
    
  • 表现层接口开发

    @RestController
    @RequestMapping("/books")
    public class BookController {
        @Autowired
        private IBookService bookService;
        @PostMapping
        public Boolean save(@RequestBody Book book){
            return bookService.insert(book);
        }
        @PutMapping
        public Boolean update(@RequestBody Book book){
            return bookService.modify(book);
        }
        @DeleteMapping("/{id}")
        public Boolean delete(@PathVariable Integer id){
            return bookService.delete(id);
        }
    }
    
    
    @RestController
    @RequestMapping("/books")
    public class BookController {
        @Autowired
        private IBookService bookService;
        @GetMapping("/{id}")
        public Book getById(@PathVariable Integer id){
            return bookService.getById(id);
        }
        @GetMapping
        public List<Book> getAll(){
            return bookService.list();
        }
        @GetMapping("/{currentPage}/{pageSize}")
        public List<Book> getAll(@PathVariable Integer currentPage,@PathVariable Integer pageSize){
            return bookService.getPage(currentPage,pageSize).getRecords();
        }
    }
    

3.1 消息一致性处理

  1. 设计统一的返回值结果类型便于前端开发读取数据
  2. 返回值结果类型可以根据需求自行设定,没有固定格式
  3. 返回值结果模型类用于后端与前端进行数据格式统一,也称为前后端数据协议
  • 设计表现层返回结果的模型类,用于后端与前端进行数据格式统一,也称为前后端数据协议
@Data
public class R{
    private Boolean flag;
    private Object 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;
    }
}
  • 表现层接口统一返回值类型结果
@RestController
@RequestMapping("/books")
public class BookController {
    @Autowired
    private IBookService bookService;
    @PostMapping
    public R save(@RequestBody Book book){
        Boolean flag = bookService.insert(book);
        return new R(flag);
    }
    @PutMapping
    public R update(@RequestBody Book book){
        Boolean flag = bookService.modify(book);
        return new R(flag);
    }
    @DeleteMapping("/{id}")
    public R delete(@PathVariable Integer id){
        Boolean flag = bookService.delete(id);
        return new R(flag);
    }
    @GetMapping("/{id}")
    public R getById(@PathVariable Integer id){
        Book book = bookService.getById(id);
        return new R(true,book);
    }
    @GetMapping
    public R getAll(){
        List<Book> bookList = bookService.list();
        return new R(true ,bookList);
    }
    @GetMapping("/{currentPage}/{pageSize}")
    public R getAll(@PathVariable Integer currentPage,@PathVariable Integer pageSize){
        IPage<Book> page = bookService.getPage(currentPage, pageSize);
        return new R(true,page);
    }
}

3.2 前后端协议联调

  1. 单体项目中页面放置在resources/static目录下

  2. created钩子函数用于初始化页面时发起调用

  3. 页面使用axios发送异步请求获取数据后确认前后端是否联通

  • 前后端分离结构设计中页面归属前端服务器

  • 单体工程中页面放置在resources目录下的static目录中(建议执行clean)

  • 前端发送异步请求,调用后端接口

//列表
getAll() {
    axios.get("/books").then((res)=>{
        console.log(res.data);
    });
},
  • 列表页
//列表
getAll() {
    axios.get("/books").then((res)=>{
        this.dataList = res.data.data;
    });
},
  • 将查询数据返回到页面,利用前端数据双向绑定进行数据展示

    1. 请求方式使用POST调用后台对应操作
    2. 添加操作结束后动态刷新页面加载数据
    3. 根据操作结果不同,显示对应的提示信息
    4. 弹出添加Div时清除表单数据
  • 弹出添加窗口

//弹出添加窗口
handleCreate() {
    this.dialogFormVisible = true;
},
  • 清除数据
//重置表单
resetForm() {
    this.formData = {};
},
//弹出添加窗口
handleCreate() {
    this.dialogFormVisible = true;
    this.resetForm();
},
  • 添加
//添加
handleAdd () {
    //发送异步请求
    axios.post("/books",this.formData).then((res)=>{
        //如果操作成功,关闭弹层,显示数据
        if(res.data.flag){
            this.dialogFormVisible = false;
            this.$message.success("添加成功");
        }else {
            this.$message.error("添加失败");
        }
    }).finally(()=>{
        this.getAll();
    });
},
  • 取消添加
//取消
cancel(){
    this.dialogFormVisible = false;
    this.$message.info("操作取消");
},
  • 删除
    1. 请求方式使用Delete调用后台对应操作
    2. 删除操作需要传递当前行数据对应的id值到后台
    3. 删除操作结束后动态刷新页面加载数据
    4. 根据操作结果不同,显示对应的提示信息
    5. 删除操作前弹出提示框避免误操作
// 删除
handleDelete(row) {
    axios.delete("/books/"+row.id).then((res)=>{
        if(res.data.flag){
            this.$message.success("删除成功");
        }else{
            this.$message.error("删除失败");
        }
    }).finally(()=>{
        this.getAll();
    });
}


// 删除
handleDelete(row) {
    axios.delete("/books/"+row.id).then((res)=>{
        if(res.data.flag){
            this.$message.success("删除成功");
        }else{
            this.$message.error("删除失败");
        }
    }).finally(()=>{
        this.getAll();
    });
}

  • 弹出修改窗口
    1. 加载要修改数据通过传递当前行数据对应的id值到后台查询数据
    2. 利用前端数据双向绑定将查询到的数据进行回显
//弹出编辑窗口
handleUpdate(row) {
    axios.get("/books/"+row.id).then((res)=>{
        if(res.data.flag){
            //展示弹层,加载数据
            this.formData = res.data.data;
            this.dialogFormVisible4Edit = true;
        }else{
            this.$message.error("数据同步失败,自动刷新");
        }
    });
},
  • 删除消息维护
//删除
handleDelete(row) {
    axios.delete("/books/"+row.id).then((res)=>{
        if(res.data.flag){
            this.$message.success("删除成功");
        }else{
            this.$message.error("数据同步失败,自动刷新");
        }
    }).finally(()=>{
        this.getAll();
    });
}
  • 修改
    1. 请求方式使用PUT调用后台对应操作
    2. 修改操作结束后动态刷新页面加载数据(同新增)
    3. 根据操作结果不同,显示对应的提示信息(同新增)
//修改
handleEdit() {
    axios.put("/books",this.formData).then((res)=>{
        //如果操作成功,关闭弹层并刷新页面
        if(res.data.flag){
            this.dialogFormVisible4Edit = false;
            this.$message.success("修改成功");
        }else {
            this.$message.error("修改失败,请重试");
        }
    }).finally(()=>{
        this.getAll();
    });
},
  • 取消添加和修改
cancel(){
    this.dialogFormVisible = false;
    this.dialogFormVisible4Edit = false;
    this.$message.info("操作取消");
},

3.3 业务消息一致性处理

  1. 使用注解@RestControllerAdvice定义SpringMVC异常处理器用来处理异常的
  2. 异常处理器必须被扫描加载,否则无法生效
  3. 表现层返回结果的模型类中添加消息属性用来传递消息到页面
  • 对异常进行统一处理,出现异常后,返回指定信息
@RestControllerAdvice
public class ProjectExceptionAdvice {
@ExceptionHandler(Exception.class)
public R doOtherException(Exception ex){
//记录日志
//发送消息给运维
//发送邮件给开发人员,ex对象发送给开发人员
ex.printStackTrace();
return new R(false,null,"系统错误,请稍后再试!");
}
}
  • 修改表现层返回结果的模型类,封装出现异常后对应的信息
  • flag:false
  • Data: null
  • 消息(msg): 要显示信息
@Data
public class R{
    private Boolean flag;
    private Object data;
    private String msg;
    public R(Boolean flag,Object data,String msg){
        this.flag = flag;
        this.data = data;
        this.msg = msg;
    }
}
  • 页面消息处理,没有传递消息加载默认消息,传递消息后加载指定消息
//添加
handleAdd () {
    //发送ajax请求
    axios.post("/books",this.formData).then((res)=>{
        //如果操作成功,关闭弹层,显示数据
        if(res.data.flag){
            this.dialogFormVisible = false;
            this.$message.success("添加成功");
        }else {
            this.$message.error(res.data.msg);
        }
    }).finally(()=>{
        this.getAll();
    });
},
  • 可以在表现层Controller中进行消息统一处理
  • 目的:国际化
@PostMapping
public R save(@RequestBody Book book) throws IOException {
    Boolean flag = bookService.insert(book);
    return new R(flag , flag ? "添加成功^_^" : "添加失败-_-!");
}
  • 页面消息处理
//添加
handleAdd () {
    //发送ajax请求
    axios.post("/books",this.formData).then((res)=>{
        if(res.data.flag){
            this.dialogFormVisible = false;
            this.$message.success(res.data.msg);
        }else {
            this.$message.error(res.data.msg);
        }
    }).finally(()=>{
        this.getAll();
    });
},

3.4 分页功能

  1. 使用el分页组件
  2. 定义分页组件绑定的数据模型
  3. 异步调用获取分页数据
  4. 分页数据页面回显
  • 页面使用el分页组件添加分页功能
<div class="pagination-container">
    <el-pagination
                   class="pagiantion"
                   @current-change="handleCurrentChange"
                   :current-page="pagination.currentPage"
                   :page-size="pagination.pageSize"
                   layout="total, prev, pager, next, jumper"
                   :total="pagination.total">
    </el-pagination>
</div>
  • 定义分页组件需要使用的数据并将数据绑定到分页组件
data:{
    pagination: { //分页相关模型数据
        currentPage: 1, //当前页码
        pageSize:10, //每页显示的记录数
        total:0, //总记录数
    }
},
  • 替换查询全部功能为分页功能
getAll() { axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize).then((res) => {});
         },

分页查询

  • 使用路径参数传递分页数据或封装对象传递数据
@GetMapping("/{currentPage}/{pageSize}")
public R getAll(@PathVariable Integer currentPage,@PathVariable Integer pageSize){
    IPage<Book> pageBook = bookService.getPage(currentPage, pageSize);
    return new R(null != pageBook ,pageBook);
}
  • 加载分页数据
getAll() {
    axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize).then((res) => {
        this.pagination.total = res.data.data.total;
        this.pagination.currentPage = res.data.data.current;
        this.pagination.pagesize = res.data.data.size;
        this.dataList = res.data.data.records;
    });
},
  • 分页页码值切换
//切换页码
handleCurrentChange(currentPage) {
    this.pagination.currentPage = currentPage;
    this.getAll();
},

3.5 删除功能维护

  1. 基于业务需求维护删除功能
  • 对查询结果进行校验,如果当前页码值大于最大页码值,使用最大页码值作为当前页码值重新查询
@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage,@PathVariable int pageSize){
    IPage<Book> page = bookService.getPage(currentPage, pageSize);
    //如果当前页码值大于了总页码值,那么重新执行查询操作,使用最大页码值作为当前页码值
    if( currentPage > page.getPages()){
        page = bookService.getPage((int)page.getPages(), pageSize);
    }
    return new R(true, page);
}

3.6 条件查询功能

  1. 定义查询条件数据模型(当前封装到分页数据模型中)
  2. 异步调用分页功能并通过请求参数传递数据到后台
  • 查询条件数据封装
    • 单独封装
    • 与分页操作混合封装
pagination: { //分页相关模型数据
    currentPage: 1, //当前页码
    pageSize:10, //每页显示的记录数
    total:0, //总记录数
    name: "",
    type: "",
    description: ""
}
  • 页面数据模型绑定
<div class="filter-container">
    <el-input placeholder="图书类别" v-model="pagination.type" class="filter-item"/>
    <el-input placeholder="图书名称" v-model="pagination.name" class="filter-item"/>
    <el-input placeholder="图书描述" v-model="pagination.description" class="filter-item"/>
    <el-button @click="getAll()" class="dalfBut">查询</el-button>
    <el-button type="primary" class="butT" @click="handleCreate()">新建</el-button>
</div>
  • 组织数据成为get请求发送的数据
  • 条件参数组织可以通过条件判定书写的更简洁
getAll() {
    //1.获取查询条件,拼接查询条件
    param = "?name="+this.pagination.name;
    param += "&type="+this.pagination.type;
    param += "&description="+this.pagination.description;
    console.log("-----------------"+ param);
    axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize+param)
        .then((res) => {
            this.dataList = res.data.data.records;
        });
},
  • Controller接收参数
@GetMapping("{currentPage}/{pageSize}")
public R getAll(@PathVariable int currentPage,@PathVariable int pageSize,Book book) {
    System.out.println("参数=====>"+book);
    IPage<Book> pageBook = bookService.getPage(currentPage,pageSize);
    return new R(null != pageBook ,pageBook);
}
  • 业务层接口功能开发
public interface IBookService extends IService<Book> {
    IPage<Book> getPage(Integer currentPage,Integer pageSize,Book queryBook);
}
@Service
public class BookServiceImpl2 extends ServiceImpl<BookDao,Book> implements IBookService {
    public IPage<Book> getPage(Integer currentPage,Integer pageSize,Book queryBook){
        IPage page = new Page(currentPage,pageSize);
        LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<Book>();
        lqw.like(Strings.isNotEmpty(queryBook.getName()),Book::getName,queryBook.getName());
        lqw.like(Strings.isNotEmpty(queryBook.getType()),Book::getType,queryBook.getType());
        lqw.like(Strings.isNotEmpty(queryBook.getDescription()),
                 Book::getDescription,queryBook.getDescription());
        return bookDao.selectPage(page,lqw);
    }
}
  • Controller调用业务层分页条件查询接口
@GetMapping("{currentPage}/{pageSize}")
public R getAll(@PathVariable int currentPage,@PathVariable int pageSize,Book book) {
    IPage<Book> pageBook = bookService.getPage(currentPage,pageSize,book);
    return new R(null != pageBook ,pageBook);
}
  • 页面回显数据
getAll() {
    //1.获取查询条件,拼接查询条件
    param = "?name="+this.pagination.name;
    param += "&type="+this.pagination.type;
    param += "&description="+this.pagination.description;
    console.log("-----------------"+ param);
    axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize+param)
        .then((res) => {
            this.pagination.total = res.data.data.total;
            this.pagination.currentPage = res.data.data.current;
            this.pagination.pagesize = res.data.data.size;
            this.dataList = res.data.data.records;
        });
},

五、总结

基于SpringBoot的SSMP整合案例

  1. pom.xml
    配置起步依赖
  2. application.yml
    设置数据源、端口、框架技术相关配置等
  3. dao
    继承BaseMapper、设置@Mapper
  4. dao测试类
  5. service
    调用数据层接口或MyBatis-Plus提供的接口快速开发
  6. service测试类
  7. controller
    基于Restful开发,使用Postman测试跑通功能
  8. 页面
    放置在resources目录下的static目录中
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

绿洲213

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值