苍穹外卖—万字总结

前言:

      苍穹外卖写完了,作为我的第一个项目,可谓是收获满满。不管是知识上的收获,还是思维上的收获,都是非常多的,所以我现在回头来好好看看这个项目,总结一下我的这个项目。


接下来我会先展示我们这个项目一共用到了什么技术。

技术总览:

注:本图片来自另一位非常优秀的博客作者。我是一盘牛肉-优快云博客

简单介绍:

Nginx:

     是一款开源的高性能 Web 服务器,同时也可以作为反向代理服务器负载均衡器HTTP 缓存邮件代理服务器

SpringBoot

就像样板房:

      快速搭建一个spring应用,不要自己装修(配置),自己接电线(依赖),内置 Tomcat、默认配置,直接“拎包入住”。帮助我们开发,让我们可以专注于业务本身,无需考虑其他。

springMvc

    处理浏览器发送过来的请求,协调Controller层接收请求,然后调用Service层,去处理请求。Mapper层去查数据库。

springTask

    可以为我们的代码定时,比如我们想代码明天执行,那么它就像一个闹钟,到点就执行。

详细可以去:苍穹外卖第九和第十天-优快云博客这里查看。

HttpClient

   可以让我们后端在代码中去发送HTTP请求。

springCache

   缓存依赖,把数据存储到缓存中,如果前端再次请求相同数据,使得后端可以从缓存中拿数据,减少了对数据库的IO操作,降低了后端服务器的压力。

JWT

JWT (JSON Web Token)

   比喻:像演唱会纸质门票
作用:用户登录后,服务器生成一个加密的“票”(Token),后续请求带上它,服务器验票就知道你是谁,不用反复查数据库。

阿里云OSS

比喻:像云端的超级大仓库 + 快递员
作用:专门存图片、视频、文档等文件,并且能快速分发到全国各地(比如用户访问时自动选最近的仓库取文件)。我们就使用这个来存储了我们的菜品图片。

swagger

接口文档管理工具,我们使用这个来测试接口。

Apache POI

通过这个技术可以来用 Java 代码自动读写 Excel、Word 文件(比如导出报表、批量生成合同)。

Websocket

和HTTP一样都是一种通信协议,但是HTTP是短连接,而它是长连接,可以持久连接客户端和服务端,实现双向通信。

Mysql

一种关系型数据库,也是以后在开发中常用的数据库,支持并且快速操作数据的增删改查。

Redis

缓存高频访问的数据,减轻数据库压力,提升读取速度。

Mybatis

可以通过java代码操作数据库。

Spring data Redis

    Spring Data Redis = Redis 的 Spring 式“快捷操作包”,适合想快速集成 Redis、减少重复代码的场景。

pagehelper:

分页查询时用到它,在查询数据库时,自动计算分页参数和总数。方便开发

Git

分布式项目管理工具,适合多人开发,可以记录项目的改动。

maven

Maven 的核心功能是依赖管理,它可以自动下载项目所需的依赖库,避免手动下载和配置的麻烦。

Juint

可以对指定方法进行单元测试,简化开发人员测试流程。

postman

接口测试工具,可以便捷的代替前端对后端发送各种请求,测试接口运行效果。


详细介绍项目技术:

JWT令牌加密技术

JTW(Json Web Token):JWT令牌由三部分组成:Header-头部.Payload-载荷.Signature-签名

其中签名是用来验证令牌的来源和正确性。JWT 是一种高效的身份验证和授权机制,特别适合无状态、分布式的应用场景。

JWT 的工作流程

客户端登录——下发令牌——客户端保存令牌

然后在以后的所有请求当中,在请求中都必须携带令牌,然后JTW会验证令牌是否正确,如果不正确就会进行拦截,所以在有些网页中我们不管点哪个部分,都会叫我们登录,不登录,就无法使用。     

本项目中的应用

在本项目中我们就是应用的JWT来拦截请求,并进行验证。

自定义JWT工具包

生成JWT令牌

进行拦截验证

自定义拦截器

Nginx负载均衡和反向代理

反向代理:

     实际上是由它来接收的客户端的请求,然后它在转发给我们的服务器。

也就是   客户端——》Nginx——》服务端。

负载均衡:就像银行里的排队引导员

    假设银行有5个柜台(服务器),但所有顾客(用户请求)都挤在1号柜台排队,导致1号忙死,其他柜台闲着。引导员(负载均衡器)的作用就是:

  1. 观察哪个柜台人少、处理快。

  2. 分配新顾客去最合适的柜台,不让任何一个柜台过载。

  3. 动态调整——如果某个柜台突然故障(比如电脑死机),立刻把顾客引导到其他柜台。

结果:所有人更快办完业务(响应快),柜台压力均衡(服务器不崩溃),整体效率最大化(系统稳定)。

一句话总结:把大量任务“公平”分配给多个处理者,避免累死一个,闲死一片。

MD5加密登录:

是一种早期常见的用户密码安全处理方式,其核心思路是使用 MD5(Message-Digest Algorithm 5)哈希算法对用户密码进行单向加密存储和验证。然而,由于 MD5 已被证明存在严重的安全缺陷,现代系统已不再推荐使用。

例如:密码 "123456" → MD5 → "e10adc3949ba59abbe56e057f20f883e"

在本项目的应用

Swagger

一套围绕 OpenAPI 规范(原称 Swagger 规范)构建的工具生态系统,用于设计、构建、文档化和测试 RESTful API。它通过标准化的方式描述 API 的结构、参数、返回值及安全要求,帮助开发者、测试人员和用户高效协作。

Swagger 的核心价值

  1. 标准化 API 描述
    统一接口定义格式,避免文档与代码不一致的问题。

  2. 自动化生成
    从 API 定义自动生成文档、客户端和服务端代码,减少重复劳动。

  3. 可视化与调试
    通过 Swagger UI 直接查看和测试 API,提升开发效率。

  4. 协作与共享
    支持团队协作设计 API,并通过版本控制管理变更。

Knife4j 是一个基于 Swagger(OpenAPI 规范)的增强工具,专为 Spring Boot 应用设计,提供更强大的 API 文档生成和管理功能。它通过优化界面、扩展功能和完善交互体验,显著提升了 API 文档的可读性和易用性,尤其适合中文开发者。在这个项目中我们用到的是Knife4j中的

@Api注解

 用于对Controller类进行说明和描述,可以指定Controller的名称、描述、标签等信息。

@ApiOperation

用于对Controller中的方法进行说明和描述,可以指定方法的名称、描述、请求方法(GET、POST等)等信息。

 

在本项目中应用

我们首先在maven中导入Knife4j的坐标。

然后配置Knife4j

设置静态资源映射

然后我们就可以访问:http://localhost:8080/doc.html来测试我们的接口。

ThreadLocal:

ThreadLocal 是 Java 中用于实现 线程隔离变量 的核心类,它允许每个线程拥有自己的变量副本,避免了多线程环境下的共享变量竞争问题。

ThreadLocal 的核心概念

1. 作用
  • 线程隔离:每个线程操作自己的变量副本,互不干扰。

  • 避免传递参数:将线程相关的变量隐式传递到整个调用链,无需显式传参。

2. 实现原理
  • 每个线程(Thread 类)内部维护一个 ThreadLocalMap(类似哈希表)。

  • ThreadLocal 的 set() 和 get() 方法操作的是当前线程的 ThreadLocalMap

  • Key:ThreadLocal 实例本身(弱引用)。
    Value:实际存储的值(强引用)。

3. 内存泄漏问题
  • 原因:若线程长时间运行(如线程池复用线程),且未手动清理 ThreadLocalMap 中的 Entry,会导致 Key(弱引用)被回收,但 Value(强引用)无法回收。

  • 解决方案:使用完 ThreadLocal 后,必须调用 remove() 清理当前线程的 Value。

核心作用:实现线程隔离,避免参数显式传递。

适用场景:数据库连接、用户会话、线程不安全的工具类(如 SimpleDateFormat)。

注意事项:必须手动清理(remove()),防止内存泄漏。

在本项目中的应用

在本项目中,它的应用主要是在token解析的时候,存储这时员工的ID,方便后续其他包对员工ID的调用。

我们将它给封装到了一个类中

在检查令牌时我们就通过它来放入我们的员工id

然后在需要放入员工id的地方在通过它来获取员工id

基于消息转换器对时间进行格式化:

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

我们来看一下我们自己所定义的对象转换器

我们之所以要消息转换器,就是因为前端传过来的时间参数格式是多种多样的,我们可以自己去指定这个时间的格式

但是这个一次它只能格式化,一个时间参数,如果有大量的时间参数,那么就太麻烦了,所以我们自定义了一个

PageHelper

PageHelper 是 MyBatis 生态中广泛使用的分页插件,通过简化 SQL 分页逻辑,帮助开发者高效实现数据库分页查询。它基于 MyBatis 的拦截器机制,自动将分页参数转换为数据库方言(如 MySQL 的 LIMIT、Oracle 的 ROWNUM),并支持复杂场景的分页需求。

PageHelper 的核心功能

  1. 自动分页
    无需手动编写 LIMIT 和 OFFSET,通过简单 API 设置分页参数。

  2. 多数据库支持
    适配 MySQL、Oracle、PostgreSQL 等主流数据库的分页语法。

  3. 物理分页
    基于 SQL 改写实现真正的物理分页(非内存分页),性能高效。

  4. 分页结果封装
    提供 PageInfo 对象,包含分页数据、总记录数、总页数等元信息。

  5. 排序支持
    支持动态排序参数传递,自动拼接 ORDER BY 子句。

在本项目中的应用

首先在maven中添加依赖

在分页查询的时候去调用它来计算分页参数,和查询总数。

使用其中的Page来储存我们查询到的结果

基于注解和AOP的公共字段填充:

之所以我们要在项目中去实现这个方法,就是因为在对数据库进行插入和更新的时候我们要对数据库中修改人,修改时间创造人和创造时间这几个进行赋值,每次进行插入和更新,都要更新这几个值,就导致了冗余操作太多了,所以就设计了一个自动填充的方法。

核心思想就是,通过注解标记出我们需要进行填充的方法,然后利用AOP思想,来具体的执行字段的填充。这种通过注解来当作我们的切入点的思想真的不错。对于AOP的基本介绍今天是苍穹外卖项目第三天-优快云博客在这里,不清楚AOP的话,可以去简单了解一下。

在本项目中的应用

首先我们自定义了一个注解,然后这个OperationType是一个枚举类。

具体的实现方法

AutoFillConstant

插入和更新操作的时候需要更新的数据也不同,通过反射赋值

在执行插入和更新的时候我们就只需要去添加我们的注解就好了

阿里云OSS

阿里云对象存储服务(Object Storage Service,简称OSS)是阿里云提供的一种海量、安全、低成本、高可靠的云存储服务,适用于存储和处理任意类型的非结构化数据(如图片、视频、文档、日志等)。在本项目中我们就是通过它来存储了我们的菜品图片,然后在调用它来上传我们的图片,放到项目中。

在本项目中的应用

我们首先要去application-dev.yml中配置我们的OSS

在application.yml中的配置,application.yml去应用application-dev.yml中的配置之所以我们要有两个配置文件,就是一个项目在上线之前会进行开发环节-测试环境-生产环境。而这三个环境可能并不会通用一套数据库,oss等配置类,如果我们写死了,那么他们每次都要去修改,就会非常麻烦,如果是一个非常大型的项目,这是非常浪费时间的。

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

阿里oss的工具类

有了这个工具类,我们就可以进行对图片上传的一系列操作,然后去编写配置类。

最后我们在来调用我们的工具,这一整个操作就已经做完了。

然后我们采用了UUID作为新的文件名,就是防止文件名重复导致文件被覆盖。

对方法开启事务:

我们在进行菜品和口味插入的时候,由于是插入两张表,所以我们要确保菜品和口味都可以插入成功,而不是一个成功一个不成。

首先我们开启事务,通过在启动类上面添加注解就可以。

然后在需要进行事务绑定的方法上加上@Transactional,这样我们就把两张表的操作给绑定到了一起。一起成功一起失败。

然后我们来详细介绍一下事务

MySQL 的事务(Transaction)是数据库操作的核心机制之一,用于确保一组数据库操作要么全部成功,要么全部失败,从而保证数据的完整性和一致性。

事务的四大特性(ACID)

  1. 原子性(Atomicity)

    • 事务中的操作要么全部完成,要么全部不执行(回滚)。

    • 例如:转账操作中,A 账户扣款和 B 账户收款必须同时成功或失败。

  2. 一致性(Consistency)

    • 事务执行前后,数据库必须从一个一致状态转换到另一个一致状态。

    • 例如:转账后,总金额(A + B)必须保持不变。

  3. 隔离性(Isolation)

    • 多个并发事务的执行互不干扰,每个事务的操作对其他事务不可见,直到事务提交。

    • 通过不同隔离级别(如 READ COMMITTED)控制可见性。

  4. 持久性(Durability)

    • 事务提交后,修改永久保存在数据库中,即使系统崩溃也不丢失。

    • 依赖日志机制(如 redo log)实现。

事务的隔离级别

MySQL 支持四种隔离级别,不同级别解决不同的并发问题:

隔离级别脏读(Dirty Read)不可重复读(Non-Repeatable Read)幻读(Phantom Read)锁机制
READ UNCOMMITTED可能可能可能不加锁,直接读最新数据
READ COMMITTED避免可能可能使用行级锁,读已提交数据
REPEATABLE READ(默认)避免避免可能使用 MVCC(多版本并发控制)
SERIALIZABLE避免避免避免完全串行化,加表级锁
  • 脏读:读到其他事务未提交的数据。

  • 不可重复读:同一事务中多次读取同一数据,结果不一致(被其他事务修改)。

  • 幻读:同一事务中多次查询,结果集数量变化(被其他事务插入/删除)。

事务的实现原理

  1. Redo Log(重做日志)

    • 用于保证 持久性,记录事务对数据的物理修改。

    • 事务提交时,先将修改写入 redo log,再异步刷新到磁盘数据文件。

    • 崩溃恢复时,通过 redo log 重放未落盘的操作。

  2. Undo Log(回滚日志)

    • 用于保证 原子性 和 一致性,记录事务修改前的旧值。

    • 事务回滚时,通过 undo log 恢复数据到原始状态。

    • 也用于实现 MVCC(多版本并发控制)。

  3. 锁机制

    • 共享锁(S Lock):读锁,允许多个事务并发读取同一数据。

    • 排他锁(X Lock):写锁,独占数据,阻止其他事务读写。

    • 间隙锁(Gap Lock):锁定索引范围,防止幻读(仅 InnoDB 支持)。

  4. MVCC(多版本并发控制)

    • 通过 undo log 保存数据的多个版本,实现非锁定读。

    • 每个事务启动时生成一个“快照”(Read View),读取对应版本的数据。

事务的常见问题

  1. 死锁(Deadlock)

    • 场景:事务 A 锁定了资源 X,事务 B 锁定了资源 Y,同时 A 请求 Y、B 请求 X。

    • 解决

      • MySQL 自动检测死锁,强制回滚其中一个事务。

      • 优化 SQL 执行顺序,减少锁竞争。

  2. 不可重复读 vs 幻读

    • 不可重复读:针对同一数据的修改。

    • 幻读:针对数据集的增减(如 INSERT/DELETE)。

  3. 事务嵌套

    • MySQL 不支持嵌套事务,但可通过保存点(Savepoint)模拟部分回滚。

事务的最佳实践

  1. 尽量短小:减少事务持有锁的时间。

  2. 合理选择隔离级别:默认 REPEATABLE READ 在大多数场景下够用,高并发场景可降级为 READ COMMITTED

  3. 避免长事务:监控并优化执行时间过长的事务。

  4. 显式控制事务:避免依赖隐式提交,增强代码可读性。

引入Redis:

Redis(Remote Dictionary Server)是一个开源的、高性能的 内存数据结构存储系统,常被用作数据库、缓存或消息中间件。它支持多种数据结构(如字符串、哈希、列表、集合等),并提供了丰富的功能特性,适用于高并发、低延迟的应用场景。在苍穹外卖项目第五天-优快云博客这里我简单的介绍了一下redis,然后也涉及到了简单的应用,感兴趣可以去看看。

我们之所以用到redis就是查询店铺功能,店铺的状态我们需要返回,然后1表示营业,0表示打烊。对于这种储存信息少,并且是一个高频查询的信息,我们就没必要去查数据库Mysql,这正好是我们redis的特点。

在本项目的应用

java采用的是Spring Data Redis,通过java语言来操作redis。

Spring Data Redis 是 Spring 框架提供的一个模块,旨在简化在 Spring 应用中与 Redis 的交互。它抽象了 Redis 的底层操作,提供统一的 API、模板类(Template)和仓库(Repository)支持,让开发者能够以面向对象的方式操作 Redis,同时无缝集成 Spring 的事务管理、缓存等特性。

首先引入我们Spring Data Redis依赖

然后我们还要去配置redis

 spring:
    redis:
      host: 地址
      port:端口号
      password:密码

随后我们在去配置类编写RedisTemplate

我们之后就会通过RedisTemplate来操作我们的redis数据库。

通过redis优化我们的代码

我们这里的应用就是缓存数据,每次我们的用户去查看商品,点菜的时候都会查询数据库,对于这种非常频繁,重复查询的操作,在高并发环境下我们必须优化,否则我们数据库很容易崩溃。

我们优化思想就是缓存。首先我们会去查询缓存区,有没有我们想要的数据,如果没有就去数据库查询将数据返回给用户并且将数据存入缓存区,下一次查询,我们首先还是去缓存区查询,这次就有了数据我们就可以直接返回,无需数据库。

这就是我们操作的例子,用户查询菜品

然后在这是我们又会遇到一些问题比如

如果我们数据库数据修改了,那之前缓存到redis的数据怎么办?

在这里我们采取的思想就是,只要我们数据库的菜品数据修改,我们就把缓存区的所有数据给删除,下一场访问就先访问数据库在存储到缓存区。

以增加菜品功能为例子

Spring Cache优化缓存操作

Spring Cache 是 Spring 框架提供的一套缓存抽象层,旨在简化缓存逻辑的集成和管理。它通过注解和接口定义的方式,将缓存操作与应用代码解耦,开发者无需直接操作底层缓存实现(如 Redis、Ehcache 等),只需通过简单注解即可实现缓存功能。

 核心特性

  • 声明式缓存:通过 @Cacheable@CacheEvict 等注解控制缓存行为。

  • 多缓存实现支持:兼容 Redis、Ehcache、Caffeine、Guava 等缓存系统。

  • 灵活配置:支持自定义缓存策略、Key 生成规则和条件过滤。

  • 与 Spring 生态无缝集成:与 Spring Boot、Spring Data 等模块深度整合。

通过核心特性我们可以看出,Spring Cache不只是可以优化redis缓存

核心注解

Spring Cache 通过以下注解实现缓存操作:

注解作用
@Cacheable标记方法结果需要缓存,若缓存存在则直接返回结果,不执行方法体。
@CachePut无论缓存是否存在,都会执行方法体,并将结果更新到缓存。
@CacheEvict删除指定缓存(可批量)。
@Caching组合多个缓存操作(如同时使用 @Cacheable 和 @CacheEvict)。
@CacheConfig类级别的注解,统一配置缓存的公共属性(如缓存名称、Key 生成器等)。

在项目中我们的应用

引入依赖

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

直接采用注解就可以对缓存进行操作。(在这里我们的底层缓存实现选择使用redis)

Httpclient:

HttpClient 是一个用于发送HTTP请求的客户端库,支持多种编程语言。通过它我们就可以在我们的idea来发送请求。

内网穿透工具Cpolar

Cpolar 是一款简单易用的内网穿透工具,可将本地局域网服务(如Web服务器、数据库、远程桌面等)暴露到公网,方便开发者在外网环境中访问内网资源。它类似于 Ngrok 和 Frp,但更注重轻量化和便捷性,适合个人开发者和中小型团队使用。

在项目中主要是在微信支付成功之后,微信团队会给我们发送一个支付成功的结果,因为这个项目是我们个人练习,所以我们所在的是局域网,微信团队就访问不到我们,通过这个工具,就可以把我们暴露在公网,这样就可以访问到我们了。

需要注意的是,在实际的项目开发中,我们的项目最后是会上线使用的,而这里是我们作为个人开发者,而该项目也只是简单的练手项目,因此我们才会使用内网穿透工具。

Spring Task:

Spring Task 是 Spring 框架提供的轻量级任务调度模块,用于实现定时任务和异步任务管理。它基于 TaskExecutor 和 TaskScheduler 接口,支持简单的注解配置和复杂的调度策略,适用于单机环境下的定时任务需求。

在本项目中主要是处理异常订单时这个模块用到了它,就跟一个闹钟一样,我们设置时间,到了时间,代码就执行。

 cron表

cron表达式其实就是一个字符串,通过corn表达式可以定义任务触发的时间

构成规则:分为6或者7个域,由空格分开,每个域代表一个含义,每个含义分别是

格式:秒 分 时 日 月 周 年(可选)年可以省略。

然后需要注意的是,日和周只能写一个,比如,如果有了日,那么周我们就要写?,如果有了周,日我们就要写?,其实cron表达式并不需要我们来写,我们可以用生成器,这个有很多,我随便写一个。

Cron - 在线Cron表达式生成器

springTask使用步骤

需要注意的是,这个依赖他自己很小,小到并不会独立作为一个依赖包需要导入,而是属于 spring context 的一个附属依赖

1.导入maven坐标spring-context(已经存在)

2.启动类添加注解@EnableScheduling开启任务调度

3.自定义定时任务类

很简单只需要在我们要定时的代码方法上面加上@Scheduled然后写上,cron表达式就可以了。

引入Websocket

WebSocket 是一种基于 TCP 的网络通信协议,用于在客户端(如浏览器)和服务器之间建立全双工、持久化的双向通信通道。它解决了传统 HTTP 协议在实时通信中的局限性(如频繁轮询、高延迟),广泛应用于实时聊天、在线游戏、股票行情推送等场景。

WebSocket 与 HTTP 对比

特性WebSocketHTTP
连接类型持久化、全双工短连接、半双工
通信模式双向实时通信请求-响应模式
头部开销小(2~14字节/帧)大(数百字节/请求)
适用场景高频实时交互

静态资源获取、REST API

在项目中我们对商家有一个订单提醒,和用户催单功能。

用户端下单或者催单后,发送特定请求到后端,后端再发送请求到商家端,商家端再根据后端的请求判断是催单还是来单提醒。

主要需要解决的就是商家端如何与后端建立一个连接,websocket正好解决了这一问题,它使得商家端和后端建立了一个持久的通信,并且商家端和后端可以实时通信,所以一旦用户下单和催单,商家马上就可以收到消息。

接下来展示一下我们的代码,首先注册一个websocket

然后我们就可以利用它来向商家发生消息

Apache POI

Apache POI 是 Apache 软件基金会开源的 Java 库,用于读写 Microsoft Office 格式文件(如 Excel、Word、PowerPoint)。它广泛应用于报表生成、数据导入导出、文档自动化处理等场景。

主要功能

  • Excel 操作:创建/修改工作表、单元格数据读写、公式计算、样式设置。

  • Word 操作:段落/表格编辑、字体样式、页眉页脚。

  • PowerPoint 操作:幻灯片创建、形状/文本框插入、动画效果。

  • 低内存模式:支持流式读写大文件(如 SXSSFWorkbook 处理 Excel)。

我就展示一下代码

对于它我们会用就可以了,主要还是了解。

随笔

这个项目在四月几号的时候就写完了,然后最大的收获还是对一个项目的认知已经非常清晰了,在之前对于项目还是仅仅停留在了概念部分,通过实际操作之发现,原来一个项目的组成可以分为这么多模块,然后也用到了非常多的技术,没有总结的时候不知道,但是一回头去看,发现原来已经了解了这么多的东西。

我是分不清前端和后端究竟该做哪些的,比如:用户查看菜品,我会想这些菜品哪儿来,我们要怎么做,在经历这个项目之后,我发现,原来就是去查一下数据库啊,然后在把数据给展示处理,对于一些业务功能变成我们真正的代码,在这之前我也是感觉非常抽象,比如那个购物车,我完全没有思路,在听了讲解之后发现,原来就是一个表啊,就是对一个表的增删而已,然后现在我已经开启了我的另一个项目黑马点评,在有了苍穹外卖的基础后,我会发现我在敲代码的时候,我的思路是完全可以跟得上的,不至于在刚学苍穹外卖的时候,有些地方都听不懂。继续加油吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值