waynboot商城发布啦,整合了Redis、RabbitMQ、ElasticSearch等常用中间件, 根据生产环境开发经验而来不断完善、优化、改进中

waynboot-mall项目

觉得有用的铁子们给个star就行了,求求你们啦😘😍 

waynboot-mall是一套全部开源的微商城项目,包含一个运营后台、h5商城和后台接口。 实现了一个商城所需的首页展示、商品分类、商品详情、sku详情、商品搜索、加入购物车、结算下单、订单状态流转、商品评论等一系列功能。 技术上基于Springboot2.0,整合了Redis、RabbitMQ、ElasticSearch等常用中间件, 贴近生产环境实际经验开发而来不断完善、优化、改进中。

后台接口项目 运营后台项目 h5商城项目

waynboot-mall接口项目

  1. 商城接口代码清晰、注释完善、模块拆分合理

  2. 使用Spring-Security进行访问权限控制

  3. 使用jwt进行接口授权验证

  4. ORM层使用Mybatis Plus提升开发效率

  5. 添加全局异常处理器,统一异常处理

  6. 添加https配置代码,支持https访问

  7. 集成七牛云存储配置,上传文件至七牛

  8. 集成常用邮箱配置,方便发送邮件

  9. 集成druid连接池,进行sql监控

  10. 集成swagger,管理接口文档

  11. 添加策略模式使用示例,优化首页金刚区跳转逻辑

  12. 拆分出通用的数据访问模块,统一redis & elastic配置与访问

  13. 使用elasticsearch-rest-high-level-client客户端对elasticsearch进行操作

  14. 支持商品数据同步elasticsearch操作以及elasticsearch商品搜索

  15. RabbitMQ生产者发送消息采用异步confirm模式,消费者消费消息时需手动确认

  16. 下单处理过程引入rabbitMQ,异步生成订单记录,提高系统下单处理能力

  17. ...

商城难点整理

1. 库存扣减操作是在下单操作扣减还是在支付成功时扣减?(ps:扣减库存使用乐观锁机制 where goods_num - num >= 0

  1. 下单时扣减,这个方案属于实时扣减,当有大量下单请求时,由于订单数小于请求数,会发生下单失败,但是无法防止短时间大量恶意请求占用库存, 造成普通用户无法下单

  2. 支付成功扣减,这个方案可以预防恶意请求占用库存,但是会存在多个请求同时下单后,在支付回调中扣减库存失败,导致订单还是下单失败并且还要退还订单金额(这种请求就是订单数超过了库存数,无法发货,影响用户体验)

  3. 还是下单时扣减,但是对于未支付订单设置一个超时过期机制,比如下单时库存减一,生成订单后,对于未在15分钟内完成支付的订单, 自动取消超期未支付订单并将库存加一,该方案基本满足了大部分使用场景

  4. 针对大流量下单场景,比如一分钟内五十万次下单请求,可以通过设置虚拟库存的方式减少下单接口对数据库的访问。具体来说就是把商品实际库存保存到redis中, 下单时配合lua脚本原子的get和decr商品库存数量(这一步就拦截了大部分请求),执行成功后在扣减实际库存

2. 首页商品展示接口利用多线程技术进行查询优化,将多个sql语句的排队查询变成异步查询,接口时长只跟查询时长最大的sql查询挂钩

# 1. 通过创建子线程继承Callable接口
Callable<List<Banner>> bannerCall = () -> iBannerService.list(new QueryWrapper<Banner>().eq("status", 0).orderByAsc("sort"));
# 2. 传入Callable的任务给FutureTask
FutureTask<List<Banner>> bannerTask = new FutureTask<>(bannerCall);
# 3. 放入线程池执行
threadPoolTaskExecutor.submit(bannerTask);
# 4. 最后可以在外部通过FutureTask的get方法异步获取执行结果 
List<Banner> list = bannerTask.get()

3. ElasticSearch查询操作,查询包含搜索关键字并且是上架中的商品,在根据指定字段进行排序,最后分页返回

SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
MatchQueryBuilder matchFiler = QueryBuilders.matchQuery("isOnSale", true);
MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("name", keyword);
MatchPhraseQueryBuilder matchPhraseQueryBuilder = QueryBuilders.matchPhraseQuery("keyword", keyword);
boolQueryBuilder.filter(matchFiler).should(matchQuery).should(matchPhraseQueryBuilder).minimumShouldMatch(1);
searchSourceBuilder.timeout(new TimeValue(10, TimeUnit.SECONDS));
// 按是否新品排序
if (isNew) { 
    searchSourceBuilder.sort(new FieldSortBuilder("isNew").order(SortOrder.DESC));
}
// 按是否热品排序
if (isHot) {
    searchSourceBuilder.sort(new FieldSortBuilder("isHot").order(SortOrder.DESC));
}
// 按价格高低排序
if (isPrice) {
    searchSourceBuilder.sort(new FieldSortBuilder("retailPrice").order("asc".equals(orderBy) ? SortOrder.ASC : SortOrder.DESC));
}
// 按销量排序
if (isSales) {
    searchSourceBuilder.sort(new FieldSortBuilder("sales").order(SortOrder.DESC));
}
// 筛选新品
if (filterNew) {
    MatchQueryBuilder filterQuery = QueryBuilders.matchQuery("isNew", true);
    boolQueryBuilder.filter(filterQuery);
}
// 筛选热品
if (filterHot) {
    MatchQueryBuilder filterQuery = QueryBuilders.matchQuery("isHot", true);
    boolQueryBuilder.filter(filterQuery);
}
​
searchSourceBuilder.query(boolQueryBuilder);
searchSourceBuilder.from((int) (page.getCurrent() - 1) * (int) page.getSize());
searchSourceBuilder.size((int) page.getSize());
List<JSONObject> list = elasticDocument.search("goods", searchSourceBuilder, JSONObject.class);

4. 订单编号生成规则:秒级时间戳 + 加密用户ID + 今日第几次下单

  1. 秒级时间戳:时间递增保证唯一性

  2. 加密用户ID:加密处理,返回用户ID6位数字,可以防并发访问,同一秒用户不会产生2个订单

  3. 今日第几次下单:便于运营查询处理用户当日订单

5. 下单流程处理过程,通过rabbitMQ异步生成订单,提高系统下单处理能力

  1. 用户点击提交订单按钮,后台生成订单编号和订单金额跳转到订单支付页面,并发送rabbitMQ消息(包含订单编号等信息)

  2. 订单消费者接受到订单消息后生成订单记录(未支付)

  3. 用户点击支付按钮时,前端根据订单编号轮询订单信息查询接口,如果订单编号记录已经入库则进行后续支付操作,如果订单编号未入库则返回错误信息(订单异常)

  4. 用户支付完成后在回调通知里更新订单状态为已支付

6. 金刚区跳转使用策略模式

# 1. 定义金刚位跳转策略接口
public interface DiamondJumpType {
​
    List<Goods> getGoods(Page<Goods> page, Diamond diamond);
​
    Integer getType();
}
​
# 2. 定义策略实现类,并使用@Component注解注入spring
@Component
public class CategoryStrategy implements DiamondJumpType {
​
    @Autowired
    private GoodsMapper goodsMapper;
​
    @Override
    public List<Goods> getGoods(Page<Goods> page, Diamond diamond) {
        List<Long> cateList = Arrays.asList(diamond.getValueId());
        return goodsMapper.selectGoodsListPageByl2CateId(page, cateList).getRecords();
    }
​
    @Override
    public Integer getType() {
        return JumpTypeEnum.CATEGORY.getType();
    }
}
@Component
public class ColumnStrategy implements DiamondJumpType {
​
    @Autowired
    private IColumnGoodsRelationService iColumnGoodsRelationService;
​
    @Autowired
    private IGoodsService iGoodsService;
​
    @Override
    public List<Goods> getGoods(Page<Goods> page, Diamond diamond) {
        List<ColumnGoodsRelation> goodsRelationList = iColumnGoodsRelationService.list(new QueryWrapper<ColumnGoodsRelation>()
                .eq("column_id", diamond.getValueId()));
        List<Long> goodsIdList = goodsRelationList.stream().map(ColumnGoodsRelation::getGoodsId).collect(Collectors.toList());
        Page<Goods> goodsPage = iGoodsService.page(page, new QueryWrapper<Goods>().in("id", goodsIdList).eq("is_on_sale", true));
        return goodsPage.getRecords();
    }
​
    @Override
    public Integer getType() {
        return JumpTypeEnum.COLUMN.getType();
    }
}
​
# 3. 定义策略上下文,通过构造器注入spring,定义map属性,通过key获取对应策略实现类
@Component
public class DiamondJumpContext {
​
    private Map<Integer, DiamondJumpType> map = new HashMap<>();
​
    /**
     * 由spring自动注入DiamondJumpType子类
     *
     * @param diamondJumpTypes 金刚位跳转类型集合
     */
    public DiamondJumpContext(List<DiamondJumpType> diamondJumpTypes) {
        for (DiamondJumpType diamondJumpType : diamondJumpTypes) {
            map.put(diamondJumpType.getType(), diamondJumpType);
        }
    }
​
    public DiamondJumpType getInstance(Integer jumpType) {
        return map.get(jumpType);
    }
}
​
# 4.使用
@Autowired
private DiamondJumpContext diamondJumpContext;
​
@Test
public void test(){
    DiamondJumpType diamondJumpType = diamondJumpContext.getInstance(1);
}
​
  • todo

文件目录

|-- waynboot-admin-api             // 运营后台api模块,提供后台项目api接口
|-- waynboot-common                // 通用模块,包含项目核心基础类
|-- waynboot-data                  // 数据模块,通用中间件数据访问
|   |-- waynboot-data-redis        // redis访问配置模块
|   |-- waynboot-data-elastic      // elastic访问配置模块
|-- waynboot-generator             // 代码生成模块
|-- waynboot-message-consumer      // 消费者模块,处理订单消息和邮件消息
|-- waynboot-message-core          // 消费者核心模块,队列、交换机配置
|-- waynboot-mobile-api            // h5商城api模块,提供h5商城api接口
|-- pom.xml                        // maven父项目依赖,定义子项目依赖版本
|-- ...

开发部署

# 1. 克隆项目
git clone git@github.com:wayn111/waynboot-mall.git
​
# 2. 导入项目依赖
将waynboot-mall目录用idea打开,导入maven依赖
​
# 3. 安装Mysql8.0+、Redis3.0+、RabbitMQ3.0+、ElasticSearch7.0+到本地
​
# 4. 导入sql文件
在项目根目录下,找到`wayn_shop_*.sql`文件,新建mysql数据库wayn_shop,导入其中
​
# 5. 修改Mysql、Redis、RabbitMQ、Elasticsearch连接配置
修改`application-dev.yml`以及`application.yml`文件中数据连接配置相关信息
​
# 6. 启动项目
后台api:
    进入waynboot-admin-api子项目,找到AdminApplication文件,右键`run AdminApplication`,启动后台项目
h5商城api:
    进入waynboot-mobile-api子项目,找到MobileApplication文件,右键`run MobileApplication`,启动h5商城项目

在线体验

  • 注册一个账号

  • 然后登陆

演示地址:http://www.wayn.ltd

演示图

waynboot-mall项目

觉得有用的铁子们给个star就行了,求求你们啦😘😍 

waynboot-mall是一套全部开源的微商城项目,包含一个运营后台、h5商城和后台接口。 实现了一个商城所需的首页展示、商品分类、商品详情、sku详情、商品搜索、加入购物车、结算下单、订单状态流转、商品评论等一系列功能。 技术上基于Springboot2.0,整合了Redis、RabbitMQ、ElasticSearch等常用中间件, 贴近生产环境实际经验开发而来不断完善、优化、改进中。

后台接口项目 运营后台项目 h5商城项目

waynboot-mall接口项目

  1. 商城接口代码清晰、注释完善、模块拆分合理

  2. 使用Spring-Security进行访问权限控制

  3. 使用jwt进行接口授权验证

  4. ORM层使用Mybatis Plus提升开发效率

  5. 添加全局异常处理器,统一异常处理

  6. 添加https配置代码,支持https访问

  7. 集成七牛云存储配置,上传文件至七牛

  8. 集成常用邮箱配置,方便发送邮件

  9. 集成druid连接池,进行sql监控

  10. 集成swagger,管理接口文档

  11. 添加策略模式使用示例,优化首页金刚区跳转逻辑

  12. 拆分出通用的数据访问模块,统一redis & elastic配置与访问

  13. 使用elasticsearch-rest-high-level-client客户端对elasticsearch进行操作

  14. 支持商品数据同步elasticsearch操作以及elasticsearch商品搜索

  15. RabbitMQ生产者发送消息采用异步confirm模式,消费者消费消息时需手动确认

  16. 下单处理过程引入rabbitMQ,异步生成订单记录,提高系统下单处理能力

  17. ...

商城难点整理

1. 库存扣减操作是在下单操作扣减还是在支付成功时扣减?(ps:扣减库存使用乐观锁机制 where goods_num - num >= 0

  1. 下单时扣减,这个方案属于实时扣减,当有大量下单请求时,由于订单数小于请求数,会发生下单失败,但是无法防止短时间大量恶意请求占用库存, 造成普通用户无法下单

  2. 支付成功扣减,这个方案可以预防恶意请求占用库存,但是会存在多个请求同时下单后,在支付回调中扣减库存失败,导致订单还是下单失败并且还要退还订单金额(这种请求就是订单数超过了库存数,无法发货,影响用户体验)

  3. 还是下单时扣减,但是对于未支付订单设置一个超时过期机制,比如下单时库存减一,生成订单后,对于未在15分钟内完成支付的订单, 自动取消超期未支付订单并将库存加一,该方案基本满足了大部分使用场景

  4. 针对大流量下单场景,比如一分钟内五十万次下单请求,可以通过设置虚拟库存的方式减少下单接口对数据库的访问。具体来说就是把商品实际库存保存到redis中, 下单时配合lua脚本原子的get和decr商品库存数量(这一步就拦截了大部分请求),执行成功后在扣减实际库存

2. 首页商品展示接口利用多线程技术进行查询优化,将多个sql语句的排队查询变成异步查询,接口时长只跟查询时长最大的sql查询挂钩

# 1. 通过创建子线程继承Callable接口
Callable<List<Banner>> bannerCall = () -> iBannerService.list(new QueryWrapper<Banner>().eq("status", 0).orderByAsc("sort"));
# 2. 传入Callable的任务给FutureTask
FutureTask<List<Banner>> bannerTask = new FutureTask<>(bannerCall);
# 3. 放入线程池执行
threadPoolTaskExecutor.submit(bannerTask);
# 4. 最后可以在外部通过FutureTask的get方法异步获取执行结果 
List<Banner> list = bannerTask.get()

3. ElasticSearch查询操作,查询包含搜索关键字并且是上架中的商品,在根据指定字段进行排序,最后分页返回

SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
MatchQueryBuilder matchFiler = QueryBuilders.matchQuery("isOnSale", true);
MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("name", keyword);
MatchPhraseQueryBuilder matchPhraseQueryBuilder = QueryBuilders.matchPhraseQuery("keyword", keyword);
boolQueryBuilder.filter(matchFiler).should(matchQuery).should(matchPhraseQueryBuilder).minimumShouldMatch(1);
searchSourceBuilder.timeout(new TimeValue(10, TimeUnit.SECONDS));
// 按是否新品排序
if (isNew) { 
    searchSourceBuilder.sort(new FieldSortBuilder("isNew").order(SortOrder.DESC));
}
// 按是否热品排序
if (isHot) {
    searchSourceBuilder.sort(new FieldSortBuilder("isHot").order(SortOrder.DESC));
}
// 按价格高低排序
if (isPrice) {
    searchSourceBuilder.sort(new FieldSortBuilder("retailPrice").order("asc".equals(orderBy) ? SortOrder.ASC : SortOrder.DESC));
}
// 按销量排序
if (isSales) {
    searchSourceBuilder.sort(new FieldSortBuilder("sales").order(SortOrder.DESC));
}
// 筛选新品
if (filterNew) {
    MatchQueryBuilder filterQuery = QueryBuilders.matchQuery("isNew", true);
    boolQueryBuilder.filter(filterQuery);
}
// 筛选热品
if (filterHot) {
    MatchQueryBuilder filterQuery = QueryBuilders.matchQuery("isHot", true);
    boolQueryBuilder.filter(filterQuery);
}
​
searchSourceBuilder.query(boolQueryBuilder);
searchSourceBuilder.from((int) (page.getCurrent() - 1) * (int) page.getSize());
searchSourceBuilder.size((int) page.getSize());
List<JSONObject> list = elasticDocument.search("goods", searchSourceBuilder, JSONObject.class);

4. 订单编号生成规则:秒级时间戳 + 加密用户ID + 今日第几次下单

  1. 秒级时间戳:时间递增保证唯一性

  2. 加密用户ID:加密处理,返回用户ID6位数字,可以防并发访问,同一秒用户不会产生2个订单

  3. 今日第几次下单:便于运营查询处理用户当日订单

5. 下单流程处理过程,通过rabbitMQ异步生成订单,提高系统下单处理能力

  1. 用户点击提交订单按钮,后台生成订单编号和订单金额跳转到订单支付页面,并发送rabbitMQ消息(包含订单编号等信息)

  2. 订单消费者接受到订单消息后生成订单记录(未支付)

  3. 用户点击支付按钮时,前端根据订单编号轮询订单信息查询接口,如果订单编号记录已经入库则进行后续支付操作,如果订单编号未入库则返回错误信息(订单异常)

  4. 用户支付完成后在回调通知里更新订单状态为已支付

6. 金刚区跳转使用策略模式

# 1. 定义金刚位跳转策略接口
public interface DiamondJumpType {
​
    List<Goods> getGoods(Page<Goods> page, Diamond diamond);
​
    Integer getType();
}
​
# 2. 定义策略实现类,并使用@Component注解注入spring
@Component
public class CategoryStrategy implements DiamondJumpType {
​
    @Autowired
    private GoodsMapper goodsMapper;
​
    @Override
    public List<Goods> getGoods(Page<Goods> page, Diamond diamond) {
        List<Long> cateList = Arrays.asList(diamond.getValueId());
        return goodsMapper.selectGoodsListPageByl2CateId(page, cateList).getRecords();
    }
​
    @Override
    public Integer getType() {
        return JumpTypeEnum.CATEGORY.getType();
    }
}
@Component
public class ColumnStrategy implements DiamondJumpType {
​
    @Autowired
    private IColumnGoodsRelationService iColumnGoodsRelationService;
​
    @Autowired
    private IGoodsService iGoodsService;
​
    @Override
    public List<Goods> getGoods(Page<Goods> page, Diamond diamond) {
        List<ColumnGoodsRelation> goodsRelationList = iColumnGoodsRelationService.list(new QueryWrapper<ColumnGoodsRelation>()
                .eq("column_id", diamond.getValueId()));
        List<Long> goodsIdList = goodsRelationList.stream().map(ColumnGoodsRelation::getGoodsId).collect(Collectors.toList());
        Page<Goods> goodsPage = iGoodsService.page(page, new QueryWrapper<Goods>().in("id", goodsIdList).eq("is_on_sale", true));
        return goodsPage.getRecords();
    }
​
    @Override
    public Integer getType() {
        return JumpTypeEnum.COLUMN.getType();
    }
}
​
# 3. 定义策略上下文,通过构造器注入spring,定义map属性,通过key获取对应策略实现类
@Component
public class DiamondJumpContext {
​
    private Map<Integer, DiamondJumpType> map = new HashMap<>();
​
    /**
     * 由spring自动注入DiamondJumpType子类
     *
     * @param diamondJumpTypes 金刚位跳转类型集合
     */
    public DiamondJumpContext(List<DiamondJumpType> diamondJumpTypes) {
        for (DiamondJumpType diamondJumpType : diamondJumpTypes) {
            map.put(diamondJumpType.getType(), diamondJumpType);
        }
    }
​
    public DiamondJumpType getInstance(Integer jumpType) {
        return map.get(jumpType);
    }
}
​
# 4.使用
@Autowired
private DiamondJumpContext diamondJumpContext;
​
@Test
public void test(){
    DiamondJumpType diamondJumpType = diamondJumpContext.getInstance(1);
}
​
  • todo

文件目录

|-- waynboot-admin-api             // 运营后台api模块,提供后台项目api接口
|-- waynboot-common                // 通用模块,包含项目核心基础类
|-- waynboot-data                  // 数据模块,通用中间件数据访问
|   |-- waynboot-data-redis        // redis访问配置模块
|   |-- waynboot-data-elastic      // elastic访问配置模块
|-- waynboot-generator             // 代码生成模块
|-- waynboot-message-consumer      // 消费者模块,处理订单消息和邮件消息
|-- waynboot-message-core          // 消费者核心模块,队列、交换机配置
|-- waynboot-mobile-api            // h5商城api模块,提供h5商城api接口
|-- pom.xml                        // maven父项目依赖,定义子项目依赖版本
|-- ...

开发部署

# 1. 克隆项目
git clone git@github.com:wayn111/waynboot-mall.git
​
# 2. 导入项目依赖
将waynboot-mall目录用idea打开,导入maven依赖
​
# 3. 安装Mysql8.0+、Redis3.0+、RabbitMQ3.0+、ElasticSearch7.0+到本地
​
# 4. 导入sql文件
在项目根目录下,找到`wayn_shop_*.sql`文件,新建mysql数据库wayn_shop,导入其中
​
# 5. 修改Mysql、Redis、RabbitMQ、Elasticsearch连接配置
修改`application-dev.yml`以及`application.yml`文件中数据连接配置相关信息
​
# 6. 启动项目
后台api:
    进入waynboot-admin-api子项目,找到AdminApplication文件,右键`run AdminApplication`,启动后台项目
h5商城api:
    进入waynboot-mobile-api子项目,找到MobileApplication文件,右键`run MobileApplication`,启动h5商城项目

在线体验

  • 注册一个账号

  • 然后登陆

演示地址:http://www.wayn.ltd

演示图

商城登陆商城注册
商城首页商城搜索
搜索结果展示金刚位跳转
商品分类商品详情
商品sku选择购物车查看
确认下单选择支付方式
商城我的页面我的订单列表
添加商品评论查看商品评论
后台登陆后台首页
后台会员管理后台评论管理
后台地址管理后台添加商品
后台商品管理后台banner管理
后台订单管理后台分类管理
后台金刚区管理后台栏目管理

 

waynboot-mall交流群

QQ群:waynboot-mall交流群 有问题可以先提issue😁

todo

  • 支持多店铺
  • 订单详情页面
  • 秒杀专区
  • 优惠卷使用
  • 团购下单
  • ...

感谢

 
  
  
  
  
  
  
  
  
  
  
  
  
  

 

waynboot-mall交流群

QQ群:waynboot-mall交流群 有问题可以先提issue😁

todo

  • 支持多店铺
  • 订单详情页面
  • 秒杀专区
  • 优惠卷使用
  • 团购下单
  • ...

感谢

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
常用中间件种类及其对应的系统环境如下: 1. Web服务器中间件:用于处理HTTP请求和响应,常见的Web服务器中间件有Apache、Nginx和IIS等,可以在Windows、Linux等操作系统上运行。 2. 数据库中间件:用于连接和管理多个数据库,常见的数据库中间件有MySQL Proxy、MyCAT、Trafodion等,可以运行在Windows、Linux等操作系统上。 3. 消息队列中间件:用于解耦消息的发送和接收,常见的消息队列中间件RabbitMQ、ActiveMQ、Kafka等,可以运行在Windows、Linux等操作系统上。 4. 缓存中间件:用于加速数据访问,常见的缓存中间件Redis、Memcached等,可以运行在Windows、Linux等操作系统上。 5. 容器中间件:用于快速部署和管理应用程序,常见的容器中间件有Docker、Kubernetes等,可以运行在Windows、Linux等操作系统上。 6. RPC中间件:用于实现远程过程调用,常见的RPC中间件有Dubbo、gRPC等,可以运行在Windows、Linux等操作系统上。 7. 搜索引擎中间件:用于全文检索和分析,常见的搜索引擎中间件Elasticsearch、Solr等,可以运行在Windows、Linux等操作系统上。 8. 日志中间件:用于收集和管理日志,常见的日志中间件有Fluentd、Logstash等,可以运行在Windows、Linux等操作系统上。 需要注意的是,以上中间件并不是全部,还有许多其他的中间件,不同的中间件有不同的使用场景和优缺点。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值