苍穹外卖总结

前言

苍穹外卖也终于是完结了,当时写这个项目是因为刚刚学完JavaSE和Javaweb,急需一个项目来练练手,下面我将介绍一下这个项目的整体结构,总结归纳一下所涉及的技术栈,并分析一下自己的感悟

项目介绍:

​ 这是一款为餐饮类企业定制的软件产品,分为管理端和用户端。实现用户端点单,管理端处理订单的简易外卖软件

主要业务:

在这里插入图片描述

1). 管理端

餐饮企业内部员工使用。 主要功能有:

模块描述
登录/退出内部员工必须登录后,才可以访问系统管理后台
员工管理管理员可以在系统后台对员工信息进行管理,包含查询、新增、编辑、禁用等功能
分类管理主要对当前餐厅经营的 菜品分类 或 套餐分类 进行管理维护, 包含查询、新增、修改、删除等功能
菜品管理主要维护各个分类下的菜品信息,包含查询、新增、修改、删除、启售、停售等功能
套餐管理主要维护当前餐厅中的套餐信息,包含查询、新增、修改、删除、启售、停售等功能
订单管理主要维护用户在移动端下的订单信息,包含查询、取消、派送、完成,以及订单报表下载等功能
数据统计主要完成对餐厅的各类数据统计,如营业额、用户数量、订单等

2). 用户端

移动端应用主要提供给消费者使用。主要功能有:

模块描述
登录/退出用户需要通过微信授权后登录使用小程序进行点餐
点餐-菜单在点餐界面需要展示出菜品分类/套餐分类, 并根据当前选择的分类加载其中的菜品信息, 供用户查询选择
点餐-购物车用户选中的菜品就会加入用户的购物车, 主要包含 查询购物车、加入购物车、删除购物车、清空购物车等功能
订单支付用户选完菜品/套餐后, 可以对购物车菜品进行结算支付, 这时就需要进行订单的支付
个人信息在个人中心页面中会展示当前用户的基本信息, 用户可以管理收货地址, 也可以查询历史订单数据

技术选型:

img

1). 用户层

管理端:H5、Vue.js、ElementUI、apache echarts(展示图表)等技术。

用户端:微信小程序开发。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2). 网关层

Nginx是一个服务器,主要用来作为Http服务器,部署静态资源,访问性能高。在Nginx中还有两个比较重要的作用: 反向代理和负载均衡, 在进行项目部署时,要实现Tomcat的负载均衡,就可以通过Nginx来实现。

3). 应用层

SpringBoot: 快速构建Spring项目, 采用 “约定优于配置” 的思想, 简化Spring项目的配置开发。

SpringMVC:SpringMVC是spring框架的一个模块,springmvc和spring无需通过中间整合层进行整合,可以无缝集成。

Spring Task: 由Spring提供的定时任务框架

httpclient: 主要实现了对http请求的发送

Spring Cache: 由Spring提供的数据缓存框架

JWT: 用于对应用程序上的用户进行身份验证的标记。

阿里云OSS: 对象存储服务,在项目中主要存储文件,如图片等。

Swagger: 可以自动的帮助开发人员生成接口文档,并对接口进行测试

POI: 封装了对Excel表格的常用操作

WebSocket: 一种通信网络协议使客户端和服务器之间的数据交换更加简单,用于项目的来单、催单功能实现。

4). 数据层

MySQL: 关系型数据库, 本项目的核心业务数据都会采用MySQL进行存储。

Redis: 基于key-value格式存储的内存数据库, 访问速度快, 经常使用它做缓存

Mybatis: 本项目持久层将会使用Mybatis开发。

pagehelper: 分页插件。

spring data redis: 简化java代码操作Redis的API。

5). 工具

git: 版本控制工具, 在团队协作中, 使用该工具对项目中的代码进行管理。

maven: 项目构建工具

junit:单元测试工具,开发人员功能实现完毕后,需要通过junit对功能进行单元测试。

postman: 接口测工具,模拟用户发起的各类HTTP请求,获取对应的响应结果。

后端环境搭建

项目结构

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

序号名称说明
1sky-take-outmaven父工程,统一管理依赖版本,聚合其他子模块
2sky-common子模块,存放公共类,例如:工具类、常量类、异常类等
3sky-pojo子模块,存放实体类、VO、DTO等
4sky-server子模块,后端服务,存放配置文件、Controller、Service、Mapper等
  • sky-common: 模块中存放的是一些公共类,可以供其他模块使用

    image-20221107093606590

    分析sky-common模块的每个包的作用:

    名称说明
    constant存放相关常量类
    context存放上下文类
    enumeration项目的枚举类存储
    exception存放自定义异常类
    json处理json转换的类
    properties存放SpringBoot相关的配置属性类
    result返回结果类的封装
    utils常用工具类
  • sky-pojo: 模块中存放的是一些 entity、DTO、VO

    image-20221107094611987

    分析sky-pojo模块的每个包的作用:

    名称说明
    Entity实体,通常和数据库中的表对应
    DTO数据传输对象,通常用于程序中各层之间传递数据
    VO视图对象,为前端展示数据提供的对象
    POJO普通Java对象,只有属性和对应的getter和setter
  • sky-server: 模块中存放的是 配置文件、配置类、拦截器、controller、service、mapper、启动类等

    image-20221107094852361

    分析sky-server模块的每个包的作用:

    名称说明
    config存放配置类
    controller存放controller类
    interceptor存放拦截器类
    mapper存放mapper接口
    service存放service类
    SkyApplication启动类
数据库的设计
序号表名中文名
1employee员工表
2category分类表
3dish菜品表
4dish_flavor菜品口味表
5setmeal套餐表
6setmeal_dish套餐菜品关系表
7user用户表
8address_book地址表
9shopping_cart购物车表
10orders订单表
11order_detail订单明细表

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意:

在这里我们使用的是逻辑外键,没用使用物理外键。所以我们在写SQL的时候一定要多注意!!

我们的表之间的关系不通过物理外键的形式来进行关联,而是在代码层面使用代码逻辑的方式进行关联

项目相关技术

Nginx负载均衡和反向代理

C:\Users\张栩睿\Desktop\编程学习\Java\黑马程序员Java项目《苍穹外卖》企业级开发实战\后端\讲义\day01

反向代理:

nginx反向代理:Nginx是一个服务器,将前端发送的动态请求由nginx转发到后端服务器。主要用来作为Http服务器,部署静态资源,访问性能高。在进行项目部署时,要实现Tomcat的负载均衡,就可以通过Nginx来实现。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

反向代理的好处:

  • 进行负载均衡:

    把大量请求按照我们指定的方式均衡分配给集群的每台服务器

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 保证后端服务安全(后端浏览器不能直接访问到)

配置nginx:

拓展:

有反向代理就有正向代理,而二者的区别很明显:反向代理隐藏服务器,正向代理隐藏客户端

正向代理:

  • 客户端发送请求后通过代理服务器访问目标服务器,代理服务器代表客户端发送请求并将响应返回给客户端。正向代理隐藏了客户端的真实身份和位置信息,为客户端提供代理访问互联网的功能。

反向代理

  • 是位于目标服务器和客户端之间的代理服务器,它代表服务器接收客户端的请求并将请求转发到真正的目标服务器上,并将得到的响应返回给客户端。反向代理隐藏了服务器的真实身份和位置信息,客户端只知道与反向代理进行通信,而不知道真正的服务器
JWT

C:\Users\张栩睿\Desktop\编程学习\Java\JavaWeb-资料\资料\day12-SpringBootWeb登录认证\讲义

介绍

是一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的,用于对应用程序上的用户进行身份验证的标记。

简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。

自包含:指的是jwt令牌,看似是一个随机的字符串,但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如:可以直接在jwt令牌中存储用户的相关信息。

简单来讲,jwt就是将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了。

组成
  • 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{“alg”:“HS256”,“type”:“JWT”}

  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{“id”:“1”,“username”:“Tom”}

  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 签名的目的就是为了防jwt令牌被篡改,而正是因为jwt令牌最后一个部分数字签名的存在,所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡改了,整个令牌在校验的时候都会失败,所以它是非常安全可靠的。

  • 加入密钥也是为了防止JWT被篡改,此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造

  • JWT是如何将原始的JSON格式数据,转变为字符串的呢?

    其实在生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码

  • Header和Payload都是明文的,可以被解析出来

在当前项目的使用场景:
  1. 在浏览器发起请求来执行登录操作,此时会访问登录的接口,如果登录成功之后,我们需要生成一个jwt令牌,将生成的 jwt令牌返回给前端
  2. 前端拿到jwt令牌之后,会将jwt令牌存储起来。在后续的每一次请求中都会将jwt令牌携带到服务端
  3. 服务端统一拦截请求之后,先来判断一下这次请求有没有把令牌带过来,如果没有带过来,直接拒绝访问,如果带过来了,还要校验一下令牌是否是有效。如果有效,就直接放行进行请求的处理。

项目里有两处验证身份的地方,一个是管理的一个是用户端,但是逻辑都是一样的,下面就展示其中管理端。

JWT加密:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

JWT解密:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

配置属性类:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

配置:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

MD5加密:

C:\Users\张栩睿\Desktop\编程学习\Java\黑马程序员Java项目《苍穹外卖》企业级开发实战\后端\讲义\day01

  • 为了防止数据库泄露带来的用户账号密码安全性问题,我们即使是在数据库中也不会进行明文存储密码,而是存储MD5加密方法加密后的一串字符串

  • MD5加密属于不可逆性,用户无法通过某种算法来解密MD5加密后所得到的字符串来获取原始密码,这也在一定程度上降低了数据库泄露所带来的风险

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基于Swagger的Knife4j注解

C:\Users\张栩睿\Desktop\编程学习\Java\黑马程序员Java项目《苍穹外卖》企业级开发实战\后端\讲义\day1

介绍:
  • Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务

  • Knife4j是Swagger的一个增强工具,是基于Swagger构建的一款功能强大的文档工具。它提供了一系列注解,用于增强对API文档的描述和可视化展示

  • 可以自动的帮助开发人员生成接口文档,并对接口进行测试

knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui

使用:

SpringBoot:

(C:\Users\张栩睿\Desktop\编程学习\Java\JavaWeb-资料\资料\day14-SpringBoot原理篇\讲义)

简介

快速构建Spring项目, 采用 “约定优于配置” 的思想, 简化Spring项目的配置开发。

  1. 起步依赖:起步依赖的原理就是Maven的依赖传递
  2. 自动配置:SpringBoot的自动配置就是当Spring容器启动后,一些配置类(配置类也是bean对象哦)、bean对象就自动存入到了IOC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作。我们只需要在启动类添加@Enable注解就可以自动配置
ThreadLocal

C:\Users\张栩睿\Desktop\编程学习\Java\黑马程序员Java项目《苍穹外卖》企业级开发实战\后端\讲义\day02

简介:
  • ThreadLocal是Java中的一个线程本地变量,它可以为每个线程提供一个独立的变量副本。线程本地变量意味着每个线程都拥有自己的变量副本,互不影响。
  • ThreadLocal的主要作用是在多线程环境下提供线程安全的变量访问。它常用于解决线程间数据共享的问题,特别是在并发编程中,当多个线程需要使用同一个变量时,可以使用ThreadLocal确保每个线程访问的都是自己的变量副本,从而避免了线程安全问题
使用:

时间进行格式化

C:\Users\张栩睿\Desktop\编程学习\Java\黑马程序员Java项目《苍穹外卖》企业级开发实战\后端\讲义\day2

引入:

前端给我们传递过来的时间参数的格式我们是无法得知的,因此我们要对前端传递过来的时间参数进行格式化

解决方式:
方式一:

如果时间参数少,我们可以使用 @JsonFormat(pattern=“yyyy-MM-dd HH:mm:ss”)来对某个属性指定格式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但这种方式,需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。

方式二:

在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理

/**
 * 扩展Spring MVC框架的消息转化器
 * @param converters
 */
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("扩展消息转换器...");
    //创建一个消息转换器对象
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
    converter.setObjectMapper(new JacksonObjectMapper());
    //将自己的消息转化器加入容器中
    converters.add(0,converter);
}

原理:

  • 消息转换器在Spring MVC中负责处理请求和响应的数据格式转换,例如将Java对象转换为JSON格式或者把JSON格式转换为Java

  • 先创建一个用来处理Json格式的消息转换器,之后我们再为这个消息转换器指定自定义的对象转换器(JacksonObjectMapper)。而这个对象转换器的作用是指定序列化和反序列化的格式。我们可以看一下指定的这个JacksonObjectMapper。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基于PageHelper的分页查询

C:\Users\张栩睿\Desktop\编程学习\Java\JavaWeb-资料\资料\day10-SpringBootWeb案例\讲义

介绍

PageHelper是基于java的一个开源框架,用于在MyBatis等持久层框架中方便地进行分页查询操作。它提供了一组简单易用的API和拦截器机制,可以帮助开发者快速集成和使用分页功能

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在执行empMapper.list()方法时,就是执行:select * from emp 语句,怎么能够实现分页操作呢?

分页插件帮我们完成了以下操作:

  1. 先获取到要执行的SQL语句:select * from emp
  2. 把SQL语句中的字段列表,变为:count(*)
  3. 执行SQL语句:select count(*) from emp //获取到总记录数
  4. 再对要执行的SQL语句:select * from emp 进行改造,在末尾添加 limit ? , ?
  5. 执行改造后的SQL语句:select * from emp limit ? , ?
使用:

当使用了PageHelper分页插件进行分页,就无需再Mapper中进行手动分页了。 在Mapper中我们只需要进行正常的列表查询即可。在Service层中,调用Mapper的方法之前设置分页参数,在调用Mapper方法执行查询之后,解析分页结果,并将结果封装到PageBean对象中返回。

1、在pom.xml引入依赖

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.2</version>
</dependency>

2、EmpMapper

@Mapper
public interface EmpMapper {
    //获取当前页的结果列表
    @Select("select * from emp")
    public List<Emp> page(Integer start, Integer pageSize);
}

3、EmpServiceImpl

@Override
public PageBean page(Integer page, Integer pageSize) {
    // 设置分页参数
    PageHelper.startPage(page, pageSize); 
    // 执行分页查询
    List<Emp> empList = empMapper.list(name,gender,begin,end); 
    // 获取分页结果
    Page<Emp> p = (Page<Emp>) empList;   
    //封装PageBean
    PageBean pageBean = new PageBean(p.getTotal(), p.getResult()); 
    return pageBean;
}
原理:

PageHelper的底层原理是拦截,拦截需要进行分页查询的SQL请求,读取用户传入参数,自主构造分页SQL语句。而用户传入参数,底层是借助threadlocal设置page变量,数据的隔离,同线程内的共享

开启事务

C:\Users\张栩睿\Desktop\编程学习\Java\JavaWeb-资料\资料\day13-SpringBootWeb AOP\讲义

介绍:

随着业务的增加,修改一张表很有可能会影响到其他表,比如当前项目,我们有逻辑外键,删除一条消息可能会影响其他表,所以要保证他们是原子性的,就要开启一个事务。

使用:
  • 在启动类上方添加@EnableTransactionManagement

  • 开启事务注解之后,我们只需要在需要捆绑成为一个事务的方法上添加@Transactional

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

原理:
  • 其原理就是AOP
阿里云OSS云存储服务:

C:\Users\张栩睿\Desktop\编程学习\Java\JavaWeb-资料\资料\day11-SpringBootWeb案例\讲义

简介

这是阿里巴巴为我们提供了一项云存储服务。我们通过这项技术来存储菜品,套餐,员工的图片。之所以不存到本地,这是因为前端无法回调服务器的本地图片,这也就造成我们只能存图片,无法回显图片的BUG,而我们如果调用阿里云的云存储服务,照片存储到阿里巴巴的云之后,会返送一个URL,我们通过这段URL就可以回调图片

使用:

先配置阿里云的各项配置

# application-dev.yml

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

#application.yml

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

#sky-common

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这段代码的意思是读取阿里云OSS的配置,这意味着当Spring Boot启动时,它会自动将以"sky.alioss"为前缀的配置项绑定到AliOssProperties对象中

工具类对象

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过配置类将其注入IOC容器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

调用工具类:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

而在云存储服务中,还有两个小知识点:

1.使用UUID生成文件名

我们需要使用UUID来为上传到阿里云OSS的图片命名。而阿里云图片命名不允许重复,否则就会覆盖。因此我们使用UUID来生成一串随机数,这样就确保了文件不被新文件覆盖。

2.不要把配置类写死

在阿里云配置中我们可以最直观的看到,我们并不是直接把配置写到application.yml 而是在application-dev.yml 写具体配置,在application.yml中应用application-dev.yml的配置。这是因为一个大型项目落地的时候,需要经过很多的环境:开发环节-测试环境-生产环境。而这三个环境可能并不会通用一套数据库,oss等配置类,如果我们直接把配置写到application.yml中,那在切换环境的时候,就要在代码中逐个修改,这对于大型项目的体量而言,无疑是灾难性的。而我们在开发环境采用application-dev.yml 的配置,在测试环境采用application-tex.yml的配置,以此类比。这样是一种很好的开发习惯。我们在自己的练手项目中也应该这样写。

AOP面向切面编程

C:\Users\张栩睿\Desktop\编程学习\Java\JavaWeb-资料\资料\day13-SpringBootWeb AOP

什么是动态代理?
  • 动态代理是一种编程技术,它允许在运行时创建代理对象并将方法调用转发给真实的目标对象。动态代理可以用于对对象的方法进行拦截、增强或修改,而无需修改原始对象的代码。它基于反射机制,在运行时动态地生成代理类,从而实现对目标对象的间接访问
  • 简单的说就是代理对象可以对真实对象的方法进行加强
什么是AOP?
  • AOP全称为面向切面编程(Aspect-Oriented Programming),是一种软件开发技术和编程范式。

  • Spring的AOP是Spring框架的高级技术,旨在管理bean对象的过程中底层使用动态代理机制,对特定的方法进行编程(功能增强)

使用:

项目的使用:

对于创建时间和修改时间,在新增和修改操作时都需要填充,显得代码很冗余,所以我们可以通过注解的方式标记方法,利用AOP思想创建一个切面,在切面中实现对新增修改时间字段的填充,然后再运行原方法(mapper方法)。这样就实现了在不改动原方法的前提下,实现了对代码的优化升级。

自定义注解:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

完成切面代码(这里使用前置通知)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

获得mapper的参数之后,我们就要对其进行填充并判断当前是新增还是修改。当然,这里要使用反射!!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

SpringMVC:

(C:\Users\张栩睿\Desktop\编程学习\Java\JavaWeb-资料\资料\day14-SpringBoot原理篇\讲义)

SpringMVC不是一个单独的框架,它是spring框架的web开发模块,springmvc和spring无需通过中间整合层进行整合,可以无缝集成。是用来简化原始的Servlet程序开发的

在Spring框架的生态中,对web程序开发提供了很好的支持,如:全局异常处理器拦截器接收请求响应数据这些都是Spring框架中web开发模块所提供的功能

全局异常处理器

C:\Users\张栩睿\Desktop\编程学习\Java\JavaWeb-资料\资料\day12-SpringBootWeb登录认证\讲义

引入

在三层构架项目中,出现了异常,该如何处理?

  • 方案一:在所有Controller的所有方法中进行try…catch处理
    • 缺点:代码臃肿(不推荐)
  • 方案二:全局异常处理器
    • 好处:简单、优雅(推荐)
使用:

我们该怎么样定义全局异常处理器?

  • 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。
  • 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。
@RestControllerAdvice
public class GlobalExceptionHandler {

    //处理异常
    @ExceptionHandler(Exception.class) //指定能够处理的异常类型
    public Result ex(Exception e){
        e.printStackTrace();//打印堆栈中的异常信息

        //捕获到异常之后,响应一个标准的Result
        return Result.error("对不起,操作失败,请联系管理员");
    }
}

@RestControllerAdvice = @ControllerAdvice + @ResponseBody

处理异常的方法返回值会转换为json后再响应给前端

引入Redis:

C:\Users\张栩睿\Desktop\编程学习\Java\黑马程序员Java项目《苍穹外卖》企业级开发实战\后端\讲义\day05

介绍

**Redis(Remote Dictionary Server)**是一个开源的内存存储系统,常用于构建高性能、高可扩展性的应用程序

使用
  1. 先要导入Spring Data Redis的依赖
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 配置redis数据源
 spring:
    redis:
      host: 地址
      port:端口号
      password:密码
  1. 编写配置类,创建RedisTemplate对象:
/*
当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为
JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别(key会出问题),故设置为StringRedisSerializer序列化器。
 */
@Configuration
@Slf4j
public class RedisConfiguration {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory)
    {
        log.info("开始创建redis模板对象...");
        RedisTemplate redisTemplate=new RedisTemplate();
        //设置redis的连接工厂对象
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        //设置redis key的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        return redisTemplate;
    }
}
  1. 使用RedisTemplate 对象操作Redis

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

运用
  • 使用场景:店铺营业状态存入redis

  • Redis是基于键值对进行存储的,键值对这种形式就符合我们对于店铺营业状态数据格式的理想存储状态,Redis也把数据放到缓存中,而不是磁盘,有效缓解了这种高频查询给磁盘带来的压力

Redis的优化:

C:\Users\张栩睿\Desktop\编程学习\Java\黑马程序员Java项目《苍穹外卖》企业级开发实战\后端\讲义\day06

查询

用户一旦点进店铺,店铺就需要向用户展示菜品,套餐等等数据。这种通过少量的操作可以调起大量后端操作的行为,是一个很危险的杠杆操作。而在高并发环境下,这无疑又是在拷打服务器。

而且这种重复查询的请求,正是我们要优化的目标。

我们的思路很简单:缓存请求相应内容,如果小程序又发送相同请求,那么我们就从缓存中直接返回相应内容。这样就减少了直接对后端的数据库的查询

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

插入和删除操作:

数据库的数据与缓存的数据一致,本项目解决Redis缓存的问题非常简单:只要有更新业务或者新建业务就清空对应的缓冲区。

Spring Cache:

C:\Users\张栩睿\Desktop\编程学习\Java\黑马程序员Java项目《苍穹外卖》企业级开发实战\后端\讲义\day17

介绍:

Spring Cache 是 Spring Framework 提供的缓存抽象和实现框架。它为应用程序提供了一种统一的缓存抽象,支持多种缓存技术的集成,并支持 AOP 机制实现基于方法的缓存,从而简化了缓存的使用和管理。只需要简单地加一个注解,就能实现缓存功能。

特点
  1. 缓存技术支持:Spring Cache 支持多种主流的缓存技术,包括 EHCache、Redis、Guava 等。

  2. 基于注解的缓存:Spring Cache 提供了基于注解的缓存,可以在方法上直接使用 @Cacheable、@CachePut、@CacheEvict 等注解,实现对方法结果的自动缓存和更新。

简单的说:它也是一种缓存技术,使得所用工具不局限于Redis相比较于使用Redis的时候需要把相关代码内嵌到方法体种,Spring Cache是一种基于注解方式来达到内嵌代码相同的效果

使用:
  1. 导入依赖

    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    
  2. 加注解(底层缓存实现选择使用redis)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

httpclient:

C:\Users\张栩睿\Desktop\编程学习\Java\黑马程序员Java项目《苍穹外卖》企业级开发实战\后端\讲义\day6

介绍

提供了一个高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包。主要实现了对http请求的发送

使用:
  1. 引入了aliyun-sdk-oss坐标:

    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
    </dependency>
    

    上述依赖的底层已经包含了HttpClient相关依赖

  2. 导入工具类

    /**
     * Http工具类
     */
    public class HttpClientUtil {
    
        static final  int TIMEOUT_MSEC = 5 * 1000;
    
        /**
         * 发送GET方式请求
         * @param url
         * @param paramMap
         * @return
         */
        public static String doGet(String url,Map<String,String> paramMap){
            // 创建Httpclient对象
            CloseableHttpClient httpClient = HttpClients.createDefault();
    
            String result = "";
            CloseableHttpResponse response = null;
    
            try{
                URIBuilder builder = new URIBuilder(url);
                if(paramMap != null){
                    for (String key : paramMap.keySet()) {
                        builder.addParameter(key,paramMap.get(key));
                    }
                }
                URI uri = builder.build();
    
                //创建GET请求
                HttpGet httpGet = new HttpGet(uri);
    
                //发送请求
                response = httpClient.execute(httpGet);
    
                //判断响应状态
                if(response.getStatusLine().getStatusCode() == 200){
                    result = EntityUtils.toString(response.getEntity(),"UTF-8");
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                try {
                    response.close();
                    httpClient.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            return result;
        }
    
        /**
         * 发送POST方式请求
         * @param url
         * @param paramMap
         * @return
         * @throws IOException
         */
        public static String doPost(String url, Map<String, String> paramMap) throws IOException {
            // 创建Httpclient对象
            CloseableHttpClient httpClient = HttpClients.createDefault();
            CloseableHttpResponse response = null;
            String resultString = "";
    
            try {
                // 创建Http Post请求
                HttpPost httpPost = new HttpPost(url);
    
                // 创建参数列表
                if (paramMap != null) {
                    List<NameValuePair> paramList = new ArrayList();
                    for (Map.Entry<String, String> param : paramMap.entrySet()) {
                        paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
                    }
                    // 模拟表单
                    UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
                    httpPost.setEntity(entity);
                }
    
                httpPost.setConfig(builderRequestConfig());
    
                // 执行http请求
                response = httpClient.execute(httpPost);
    
                resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
            } catch (Exception e) {
                throw e;
            } finally {
                try {
                    response.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            return resultString;
        }
    
        /**
         * 发送POST方式请求
         * @param url
         * @param paramMap
         * @return
         * @throws IOException
         */
        public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
            // 创建Httpclient对象
            CloseableHttpClient httpClient = HttpClients.createDefault();
            CloseableHttpResponse response = null;
            String resultString = "";
    
            try {
                // 创建Http Post请求
                HttpPost httpPost = new HttpPost(url);
    
                if (paramMap != null) {
                    //构造json格式数据
                    JSONObject jsonObject = new JSONObject();
                    for (Map.Entry<String, String> param : paramMap.entrySet()) {
                        jsonObject.put(param.getKey(),param.getValue());
                    }
                    StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8");
                    //设置请求编码
                    entity.setContentEncoding("utf-8");
                    //设置数据类型
                    entity.setContentType("application/json");
                    httpPost.setEntity(entity);
                }
    
                httpPost.setConfig(builderRequestConfig());
    
                // 执行http请求
                response = httpClient.execute(httpPost);
    
                resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
            } catch (Exception e) {
                throw e;
            } finally {
                try {
                    response.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            return resultString;
        }
        private static RequestConfig builderRequestConfig() {
            return RequestConfig.custom()
                    .setConnectTimeout(TIMEOUT_MSEC)
                    .setConnectionRequestTimeout(TIMEOUT_MSEC)
                    .setSocketTimeout(TIMEOUT_MSEC).build();
        }
    
    }
    
  3. 使用工具类:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

运用:
  • 项目里面,对于微信支付的时候,我们的服务端需要向微信接口服务发起请求并且响应数据,此时服务端就相当于一个客户端,通过httpclient即可实现这个功能。
  • httpclient是一个很常用的技术,因为很多第三方接口的使用方式就需要我们的后端发送请求到指定资源路径,这样才可以调用相关服务
内网穿透工具Cpolar

C:\Users\张栩睿\Desktop\编程学习\Java\黑马程序员Java项目《苍穹外卖》企业级开发实战\后端\讲义\day08

介绍

内网穿透就是在私有IP地址和公有IP地址之间建立一个临时的映射关系,使得我们的内网服务器暴漏到公网之中

使用:

cpolar软件的使用:

1). 下载与安装

下载地址:https://dashboard.cpolar.com/get-started

image-20221215184407217

在资料中已提供,可无需下载。

image-20221215184446260

安装过程中,一直下一步即可,不再演示。

2). cpolar指定authtoken

复制authtoken:

image-20221215184746092

执行命令:

image-20221215185152869

3). 获取临时域名

执行命令:

image-20221215185749163

获取域名:

image-20221215185833157
运用:

在微信支付接口的流程中,当支付成功之后,微信后台要向我们的服务器后台返送支付结果,但是存在一个问题:我们的IP地址都是私有IP地址,微信后台根本访问不了,这样就接收不到支付结果,因此我们需要一个公有的IP地址。通过内网穿透。我们就可以为微信后台提供一个可以在公网访问的地址,用于接收支付结果

Spring Task:

C:\Users\张栩睿\Desktop\编程学习\Java\黑马程序员Java项目《苍穹外卖》企业级开发实战\后端\讲义\day10

介绍:
  • Spring Task 是Spring框架提供的定时任务框架,可以定时自动执行某段Java代码,只要是需要定时处理的场景都可以使用Spring Task
  • 通过 Spring Task,开发人员可以通过注解或者配置的方式定义需要执行的任务,并设置执行的时间间隔或者执行时间点。Spring Task 提供了灵活的任务调度能力,可以满足各种任务执行的需求,例如定时的数据同步、定时的报表生成、定时的缓存清理等

使用:

  1. 导入依赖
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>版本号</version>
</dependency>
  1. 加注解

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

具体的定时任务很简单,就是简单的业务逻辑实现代码,而重点在这个注解**@Scheduled**

它是用来设置定时的注解,它里面采用的表达式叫做cron表达式,通过这个表达式,我们可以指定任务多久执行一次。比如这里的0 * * * *,表示每一年每个月每一天的每个小时每分钟的第0秒钟执行一次这个任务,也就是每一分钟执行一次。对于cron表达式,网上有很多工具可以帮我们解析的在线Cron表达式生成器

运用:

这项目主要用于处理支付超时订单和一直处于派送中的订单

WebSocket

C:\Users\张栩睿\Desktop\编程学习\Java\黑马程序员Java项目《苍穹外卖》企业级开发实战\后端\讲义\day10

介绍
  • 传统的 HTTP 协议是一种请求-响应模式,客户端需要定期发送请求并等待服务器的响应。但在某些场景下,需要实时地将数据推送给客户端,如聊天应用、实时数据监控等。这时就可以使用 WebSocket 协议

  • WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输(持久的,全双工)

  • 服务器可以主动向客户端推送消息,而无需客户端发送请求

使用
  1. 导入maven坐标
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  1. 定义WebSocket服务端组件
package com.sky.websocket;

import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * WebSocket服务
 */
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

    //存放会话对象
    private static Map<String, Session> sessionMap = new HashMap();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        System.out.println("客户端:" + sid + "建立连接");
        sessionMap.put(sid, session);
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, @PathParam("sid") String sid) {
        System.out.println("收到来自客户端:" + sid + "的信息:" + message);
    }

    /**
     * 连接关闭调用的方法
     *
     * @param sid
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid) {
        System.out.println("连接断开:" + sid);
        sessionMap.remove(sid);
    }

    /**
     * 群发
     *
     * @param message
     */
    public void sendToAllClient(String message) {
        Collection<Session> sessions = sessionMap.values();
        for (Session session : sessions) {
            try {
                //服务器向客户端发送消息
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}
  1. 定义配置类,注册WebSocket的服务端组件(从资料中直接导入即可)
package com.sky.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * WebSocket配置类,用于注册WebSocket的Bean
 */
@Configuration
public class WebSocketConfiguration {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}
  1. 通过websocket通信

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

运用:

当前项目用于实现来单、催单功能实现,让后端给前端发送请求,让前端能够显示来单和催单提醒

拓展

而除了使用webSocket之外,我们可以使用SSE或长轮询技术:

SSE 是一种特殊的 HTTP 机制,它允许服务器向客户端推送数据,以实现服务器和客户端之间的实时通信。SSE 与 WebSocket 不同的是,它是基于 HTTP 协议的,不需要像 WebSocket 那样建立专门的协议和通信通道。

长轮询则是一种模拟实时通信的技术,它通过客户端向服务器发送一个请求并保持长时间的连接,在服务器端有新数据到达时返回响应,并在客户端接收到响应后再次发送请求继续保持连接。长轮询的实现方式类似于 SSE,但是相对于 SSE 而言,因为需要频繁的开启和关闭连接,长轮询会增加服务器的负担,同时也不如 SSE 和 WebSocket 那样实时和高效。

Apache POI:

C:\Users\张栩睿\Desktop\编程学习\Java\黑马程序员Java项目《苍穹外卖》企业级开发实战\后端\讲义\day12

介绍
  • Apache POI(Poor Obfuscation Implementation)是一个用于处理Microsoft Office格式文档的开源Java库。POI提供了一组可以读取、写入和操作各种Office文件的API,包括Word文档(.doc和.docx)、Excel电子表格(.xls和.xlsx)以及PowerPoint演示文稿(.ppt和.pptx)。

  • 通过POI,开发者可以在Java应用程序中读取和编辑Office文档,实现对文档内容、样式、格式和元数据的操作。它提供了向现有文档添加新内容、修改现有内容、删除内容以及进行格式设置和样式调整等功能。

  • 一般情况下,POI 都是用于操作 Excel 文件

使用:
  1. Apache POI的maven坐标:(项目中已导入)

    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>3.16</version>
    </dependency>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>3.16</version>
    </dependency>
    
  2. 建表(当然,在本项目中,我们并不使用ApachePOI建表,这样无疑是在拷打自己。我们的想法是直接就提供一张创建好的模板表,这样我们只需要使用ApachePOI来实现填充数据就好了)

  3. 对表进行操作(注意,这里不是通过controller返回一个值,而是使用原生的HttpServletResponse去返回这个文件)

    /**导出近30天的运营数据报表
     * @param response
     **/
    public void exportBusinessData(HttpServletResponse response) {
        LocalDate begin = LocalDate.now().minusDays(30);
        LocalDate end = LocalDate.now().minusDays(1);
        //查询概览运营数据,提供给Excel模板文件
        BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(begin,LocalTime.MIN), LocalDateTime.of(end, LocalTime.MAX));
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
        try {
            //基于提供好的模板文件创建一个新的Excel表格对象
            XSSFWorkbook excel = new XSSFWorkbook(inputStream);
            //获得Excel文件中的一个Sheet页
            XSSFSheet sheet = excel.getSheet("Sheet1");
    
            sheet.getRow(1).getCell(1).setCellValue(begin + "至" + end);
            //获得第4行
            XSSFRow row = sheet.getRow(3);
            //获取单元格
            row.getCell(2).setCellValue(businessData.getTurnover());
            row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
            row.getCell(6).setCellValue(businessData.getNewUsers());
            row = sheet.getRow(4);
            row.getCell(2).setCellValue(businessData.getValidOrderCount());
            row.getCell(4).setCellValue(businessData.getUnitPrice());
            for (int i = 0; i < 30; i++) {
                LocalDate date = begin.plusDays(i);
               //准备明细数据
                businessData = workspaceService.getBusinessData(LocalDateTime.of(date,LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));
                row = sheet.getRow(7 + i);
                row.getCell(1).setCellValue(date.toString());
                row.getCell(2).setCellValue(businessData.getTurnover());
                row.getCell(3).setCellValue(businessData.getValidOrderCount());
                row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
                row.getCell(5).setCellValue(businessData.getUnitPrice());
                row.getCell(6).setCellValue(businessData.getNewUsers());
            }
            //通过输出流将文件下载到客户端浏览器中
            ServletOutputStream out = response.getOutputStream();
            excel.write(out);
            //关闭资源
            out.flush();
            out.close();
            excel.close();
    
        }catch (IOException e){
            e.printStackTrace();
        }
    }
    
拦截器Interceptor

C:\Users\张栩睿\Desktop\编程学习\Java\JavaWeb-资料\资料\day12-SpringBootWeb登录认证\讲义

属于Spring MVC框架Interceptor只会拦截Spring环境中的资源

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

总结

  1. 对于软件开发流程有了进一步的认识,要完成一个项目的基本步骤是需求分析,比如(规格说明书、产品原型的书写),设计(数据库设计,接口设计等),编码(也就是根据页面原型和接口文档写代码),测试,运维这五个步骤
  2. 分业务模块。刚开始写这个项目时,感觉还是很困难的,因为看上去要实现的功能特别多,但当完成这个项目去回看时,发现这一整个项目其实也没这么难,因为在实现这个项目的时候,我们将他拆分为一个一个模块去完成,每写完一个模块,就要对其进行测试,就出苍穹外卖来说,主要分为了管理端和用户端,而管理端又拆分为员工信息管理 , 分类管理 , 菜品管理等模块;用户端又拆分为 收件人地址管理 , 用户历史订单查询 , 菜品规格查询 , 购物车功能等模块,由此我们就把这个看起来很庞大的项目分为一个模块一个模块的去实现。
  3. 当然,当一个项目分为一个又一个业务模块的时候,每一个模块其实也是有关联的,比如我在写mapper的时候,会疑惑为什么这里不直接传参,而要封装为一个对象或map,然后写动态sql去获取数据,但当我写另一个模块的时候才明白,如果当时我直接传参,在这里就需要重新写一个新的接口;但是如果封装为一个对象或map,然后使用动态sql,就能复用很多代码。所以,对于一个项目,我们既要把他细分抽象为一个又一个业务模块去编写的同时,也要能从宏观的视角去思考他们之间的联系,这样也许就能提供代码的复用性和业务的通用性
  4. 学会去阅读需求文档,分析接口文档,首先,粗粒度地分析每个页面有多少接口,然后,再细粒度地分析每个接口的传入参数,返回值参数,同时明确接口路径及请求方式。当然,我映像最深的是,有些时候有一些接口是在开发的时候才发现需要这个接口的。
  5. 对于数据库的设计有了更全面的认识,比如数据库表里面的一些冗余字段,刚开始并不理解冗余字段的意义,感觉多此一举,但是后面在数据层写sql语句时才发现,引入冗余字段的设计,能够减少表关联,使用SQL查询的时候执行效率更快。当然,冗余字段一般用在几乎不会发生修改的字段上
  6. 体会到框架的强大之处,相比于C++我们自己去调用库,java的框架大大的简化了开发过程,减少了很多重复的代码,比如Spring Cache,实现了基于注解的缓存功能,由此在大多数情况下我们通过注解就可以实现对redis的操作,而不用去调用原生的redisTemplate去操作redis。

ption e){
e.printStackTrace();
}
}




#### 拦截器Interceptor

> C:\Users\张栩睿\Desktop\编程学习\Java\JavaWeb-资料\资料\day12-SpringBootWeb登录认证\讲义

**属于Spring MVC框架**,**Interceptor只会拦截Spring环境中的资源**

[外链图片转存中...(img-N0hbg8pS-1731469717025)]









## 总结

1. 对于**软件开发流程**有了进一步的认识,要完成一个项目的基本步骤是**需求分析**,比如(规格说明书、产品原型的书写),**设计**(数据库设计,接口设计等),**编码**(也就是根据页面原型和接口文档**写代码**),**测试,运维**这五个步骤
2. 分业务模块。刚开始写这个项目时,感觉还是很困难的,因为看上去要实现的功能特别多,但当完成这个项目去回看时,发现这一整个项目其实也没这么难,因为在实现这个项目的时候,我们将他拆分为一个一个模块去完成,每写完一个模块,就要对其进行测试,就出苍穹外卖来说,主要分为了管理端和用户端,而管理端又拆分为**员工信息管理 , 分类管理 , 菜品管理**等模块;用户端又拆分为 收件人地址管理 , 用户历史订单查询 , 菜品规格查询 , 购物车功能等模块,由此我们就把这个看起来很庞大的项目分为一个模块一个模块的去实现。
3. 当然,当一个项目分为一个又一个业务模块的时候,每一个模块其实也是有关联的,比如我在写mapper的时候,会疑惑为什么这里不直接传参,而要封装为一个对象或map,然后写动态sql去获取数据,但当我写另一个模块的时候才明白,如果当时我直接传参,在这里就需要重新写一个新的接口;但是如果封装为一个对象或map,然后使用动态sql,就能复用很多代码。所以,对于一个项目,我们既要把他细分抽象为一个又一个业务模块去编写的同时,也要能从宏观的视角去思考他们之间的联系,这样也许就能提供代码的复用性和业务的通用性
4. 学会去阅读需求文档,分析接口文档,首先,粗粒度地分析每个页面有多少接口,然后,再细粒度地分析每个接口的传入参数,返回值参数,同时明确接口路径及请求方式。当然,我映像最深的是,有些时候有一些接口是在开发的时候才发现需要这个接口的。
5. 对于数据库的设计有了更全面的认识,比如数据库表里面的一些冗余字段,刚开始并不理解冗余字段的意义,感觉多此一举,但是后面在数据层写sql语句时才发现,引入冗余字段的设计,能够减少表关联,使用SQL查询的时候执行效率更快。当然,冗余字段一般用在几乎不会发生修改的字段上
6. 体会到框架的强大之处,相比于C++我们自己去调用库,java的框架大大的简化了开发过程,减少了很多重复的代码,比如Spring Cache,实现了基于注解的缓存功能,由此在大多数情况下我们通过注解就可以实现对redis的操作,而不用去调用原生的redisTemplate去操作redis。





















评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

坤小满

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

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

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

打赏作者

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

抵扣说明:

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

余额充值