一套前后台全部开源的H5商城送给大家

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: 一套前后台全部开源的H5商城送给大家

image.png

博主给大家推荐一套全部开源的H5电商项目waynboot-mall。由博主在2020年开发至今,已有三年之久。那时候网上很多的H5商城项目都是半开源版本,要么没有H5前端代码,要么需要加群咨询,属实恶心。于是博主决定自己开发一套完整的移动端H5商城,包含一个管理后台、一个前台H5商城、一套后端接口。项目地址如下:

欢迎大家关注这个项目,点个Star让更多的人了解到这个项目。

一、简介

waynboot-mall是一套全部开源的微商城项目,实现了一个商城所需的首页展示、商品分类、商品详情、sku组合、商品搜索、购物车、结算下单、订单状态流转、商品评论等一系列功能。 技术上基于最新得Spring Boot3.0、Jdk17,整合了Redis、RabbitMQ、ElasticSearch等常用中间件, 贴近生产环境实际经验开发而来。

二、技术特点

  1. 订单金额计算使用BigDeciaml类型,支持小数点后两位
  2. 支持微信内JsApi支付、H5网页支付
  3. 商城接口代码清晰、注释完善、模块拆分合理
  4. 使用Spring-Security进行访问权限控制
  5. 使用jwt进行接口授权验证
  6. ORM层使用Mybatis Plus提升开发效率
  7. 添加全局异常处理器,统一异常处理
  8. 使用Spring Boot admin进行服务监控
  9. 集成七牛云存储配置,支持上传文件至七牛获取cdn下载链接
  10. 集成常用邮箱配置,方便发送邮件
  11. 添加策略模式使用示例,优化首页金刚区跳转逻辑
  12. 拆分出通用的数据访问模块,统一Redis & Elastic配置与访问
  13. 使用Elasticsearch高级客户端依赖对Elasticsearch进行操作
  14. 支持商品数据同步Elasticsearch操作以及中文分词搜索
  15. RabbitMQ生产者发送消息采用异步confirm模式,消费者消费消息时需手动确认确保消息不丢失
  1. 下单处理过程引入RabbitMQ,异步生成订单记录,提高系统下单处理能力

三、商城设计

文项目目录

|-- waynboot-monitor               // 监控模块
|-- 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父项目依赖,定义子项目依赖版本
|-- ...

技术亮点

2.1 库存扣减

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

  1. 下单时扣减,这个方案属于实时扣减,当有大量下单请求时,由于订单数小于请求数,会发生下单失败,但是无法防止短时间大量恶意请求占用库存, 造成普通用户无法下单
  2. 支付成功扣减,这个方案可以预防恶意请求占用库存,但是会存在多个请求同时下单后,在支付回调中扣减库存失败,导致订单还是下单失败并且还要退还订单金额(这种请求就是订单数超过了库存数,无法发货,影响用户体验)
  • 还是下单时扣减,但是对于未支付订单设置一个超时过期机制,比如下单时库存减一,生成订单后,对于未在15分钟内完成支付的订单, 自动取消超期未支付订单并将库存加一,该方案基本满足了大部分使用场景
  • 针对大流量下单场景,比如一分钟内五十万次下单请求,可以通过设置虚拟库存的方式减少下单接口对数据库的访问。具体来说就是把商品库存缓存到redis中, 下单时配合lua脚本原子的get和decr商品库存数量(这一步就拦截了大部分请求),执行成功后在扣减实际库存

2.2 首页查询

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

// 使用CompletableFuture异步查询
List<CompletableFuture<Void>> list = new ArrayList<>();
CompletableFuture<Void> f1 = CompletableFuture.supplyAsync(() -> iBannerService.list(Wrappers.lambdaQuery(Banner.class).eq(Banner::getStatus, 0).orderByAsc(Banner::getSort)), homeThreadPoolTaskExecutor).thenAccept(data -> {
    String key = "bannerList";
    redisCache.setCacheMapValue(SHOP_HOME_INDEX_HASH, key, data);
    success.add(key, data);
});
CompletableFuture<Void> f2 = CompletableFuture.supplyAsync(() -> iDiamondService.list(Wrappers.lambdaQuery(Diamond.class).orderByAsc(Diamond::getSort).last("limit 10")), homeThreadPoolTaskExecutor).thenAccept(data -> {
    String key = "categoryList";
    redisCache.setCacheMapValue(SHOP_HOME_INDEX_HASH, key, data);
    success.add(key, data);
});
list.add(f1);
list.add(f2);
// 主线程等待子线程执行完毕
CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join();

2.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);

2.4 订单编号

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

  1. 秒级时间戳:时间递增保证唯一性
  2. 加密用户ID:加密处理,返回用户ID6位数字,可以防并发访问,同一秒用户不会产生2个订单
  3. 今日第几次下单:便于运营查询处理用户当日订单
/**
 * 返回订单编号,生成规则:秒级时间戳 + 加密用户ID + 今日第几次下单
 *
 * @param userId 用户ID
 * @return 订单编号
 */
public static String generateOrderSn(Long userId) {
        long now = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
        return now + encryptUserId(String.valueOf(userId), 6) + countByOrderSn(userId);
}
/**
 * 计算该用户今日内第几次下单
 *
 * @param userId 用户ID
 * @return 该用户今日第几次下单
 */
public static int countByOrderSn(Long userId) {
        IOrderService orderService = SpringContextUtil.getBean(IOrderService.class);
        return orderService.count(new QueryWrapper<Order>().eq("user_id", userId)
        .gt("create_time", LocalDate.now())
        .lt("create_time", LocalDate.now().plusDays(1)));
}
/**
 * 加密用户ID,返回num位字符串
 *
 * @param userId 用户ID
 * @param num    长度
 * @return num位加密字符串
 */
private static String encryptUserId(String userId, int num) {
        return String.format("%0" + num + "d", Integer.parseInt(userId) + 1);
}

2.5 异步下单

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

  1. 用户点击提交订单按钮,后台生成订单编号和订单金额跳转到订单支付页面,并将订单编号等信息发送rabbitMQ消息(生成订单编号,还未生成订单)
  2. 订单消费者接受到订单消息后,获取订单编号生成订单记录(订单创建成功,用户待支付)
  3. 下单页面,前端根据订单编号轮询订单接口,订单已创建则跳转支付页面,否则提示下单失败(订单创建失败)
  4. 支付页面,用户点击支付按钮时,后台调用微信/支付宝下单接口后,前端唤醒微信/支付宝支付,用户输入密码
  5. 用户支付完成后在微信/支付宝下回调通知里更新订单状态为已支付(订单已支付)
  6. 用户支付完成后,返回支付状态查看页面。

2.6 设计模式

金刚区跳转使用策略模式进行代码编写

1.定义金刚位跳转策略接口以及跳转枚举类

public interface DiamondJumpType {
    List<Goods> getGoods(Page<Goods> page, Diamond diamond);
    Integer getType();
}
// 金刚位跳转类型枚举
public enum JumpTypeEnum {
    COLUMN(0),
    CATEGORY(1);
    private Integer type;
    JumpTypeEnum(Integer type) {
        this.type = type;
    }
    public Integer getType() {
        return type;
    }
    public JumpTypeEnum setType(Integer type) {
        this.type = type;
        return this;
    }
}

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 final 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.使用,注入DiamondJumpContext对象,调用getInstance方法传入枚举类型

@Autowired
private DiamondJumpContext diamondJumpContext;
@Test
public void test(){
    DiamondJumpType diamondJumpType=diamondJumpContext.getInstance(JumpTypeEnum.COLUMN.getType());
}

四、演示图

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

五、在线体验

演示地址:http://121.4.124.33/mall

最后说两句waynboot-mall作为博主的开源项目集大成者,对于没有接触过商城项目的小伙伴来说是非常具有帮助和学习价值的。看完这个项目你能了解到一个商城项目的基本全貌,提前避坑。



相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
目录
相关文章
|
JSON 小程序 数据可视化
手把手带你开发一款云开发版点餐小程序,微信扫码点餐,用户端和后厨端都有
手把手带你开发一款云开发版点餐小程序,微信扫码点餐,用户端和后厨端都有
424 0
|
5月前
|
传感器 小程序 搜索推荐
(源码)java开发的一套(智慧校园系统源码、电子班牌、原生小程序开发)多端展示:web端、saas端、家长端、教师端
通过电子班牌设备和智慧校园数据平台的统一管理,在电子班牌上,班牌展示、学生上课刷卡考勤、考勤状况汇总展示,课表展示,考场管理,请假管理,成绩查询,考试优秀标兵展示、校园通知展示,班级文化各片展示等多种化展示。
90 0
(源码)java开发的一套(智慧校园系统源码、电子班牌、原生小程序开发)多端展示:web端、saas端、家长端、教师端
|
4月前
|
移动开发 前端开发 应用服务中间件
挂售转卖竞拍商城系统源码/竞拍系统/转拍闪拍系统/后端PHP+前端UNiapp源码
挂售转卖竞拍商城系统源码/竞拍系统/转拍闪拍系统/后端PHP+前端UNiapp源码 亲测可用
126 1
|
5月前
|
PHP
PHP开发的爱情盲盒交友系统网站源码 独立后台
PHP开发的爱情盲盒交友系统网站源码 独立后台
45 4
|
6月前
|
JavaScript Java 测试技术
基于小程序的基于微信小程序的外卖点餐系统
基于小程序的基于微信小程序的外卖点餐系统
61 1
|
6月前
|
存储 前端开发 关系型数据库
105-Django开发多商户询盘上级网站-在线聊天交流通讯
该项目旨在构建一个在线询盘交易系统,采用Python的Django框架,支持SQLite3(开发)或MySQL/PostgreSQL(生产)数据库。功能包括用户注册登录、产品上传(带审核)、搜索过滤、即时通讯、超级用户管理端、数据分析和响应式界面。还将考虑使用WebSocket、Redis缓存、Docker部署和推荐算法。数据库设计涉及用户、产品、订单和聊天记录表。开发流程涵盖需求分析、技术选型、数据库设计、编码、测试和部署,同时关注安全性、性能、可维护性和兼容性。
47 2
|
6月前
|
小程序 JavaScript Java
点餐|外卖订餐小程序|基于微信小程序的外卖订餐系统设计与实现(源码+数据库+文档)
点餐|外卖订餐小程序|基于微信小程序的外卖订餐系统设计与实现(源码+数据库+文档)
173 0
|
消息中间件 存储 XML
【易售小程序项目】私聊功能后端实现 (买家、卖家 沟通商品信息)【后端基于若依管理系统开发】
【易售小程序项目】私聊功能后端实现 (买家、卖家 沟通商品信息)【后端基于若依管理系统开发】
156 0
|
6月前
|
小程序 算法 数据可视化
点餐小程序实战教程07-点餐功能开发
点餐小程序实战教程07-点餐功能开发
|
JSON 小程序 前端开发
微信小程序开发之会议OA首页后台数据交互(内带新知识点分享)
微信小程序开发之会议OA首页后台数据交互(内带新知识点分享)
112 0