博主给大家推荐一套全部开源的 H5 电商项目waynboot-mall。由博主在 2020 年开发至今,已有三年之久。那时候网上很多的 H5 商城项目都是半开源版本,要么没有 H5 前端代码,要么需要加群咨询,属实恶心。于是博主决定自己开发一套完整的移动端 H5 商城,包含一个管理后台、一个前台 H5 商城、一套后端接口。项目地址如下:
欢迎大家关注这个项目,点个 Star 让更多的人了解到这个项目。
waynboot-mall是一套全部开源的微商城项目,实现了一个商城所需的首页展示、商品分类、商品详情、sku 组合、商品搜索、购物车、结算下单、订单状态流转、商品评论等一系列功能。 技术上基于最新得 Spring Boot3.0 、Jdk17 ,整合了 Redis 、RabbitMQ 、ElasticSearch 等常用中间件, 贴近生产环境实际经验开发而来。
|-- 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 父项目依赖,定义子项目依赖版本 |-- ...
库存扣减操作是在下单操作扣减还是在支付成功时扣减?( ps:扣减库存使用乐观锁机制 where goods_num - num >= 0
)
首页商品展示接口利用多线程技术进行查询优化,将多个 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();
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);
订单编号生成规则:秒级时间戳 + 加密用户 ID + 今日第几次下单
/** * 返回订单编号,生成规则:秒级时间戳 + 加密用户 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); }
下单流程处理过程,通过 rabbitMQ 异步生成订单,提高系统下单处理能力
金刚区跳转使用策略模式进行代码编写
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()); }
商城登陆![]() | 商城注册![]() |
商城首页![]() | 商城搜索![]() |
搜索结果展示![]() | 金刚位跳转![]() |
商品分类![]() | 商品详情![]() |
商品 sku 选择![]() | 购物车查看![]() |
确认下单![]() | 选择支付方式![]() |
商城我的页面![]() | 我的订单列表![]() |
添加商品评论![]() | 查看商品评论![]() |
后台登陆![]() | 后台首页![]() |
后台会员管理![]() | 后台评论管理![]() |
后台地址管理![]() | 后台添加商品![]() |
后台商品管理![]() | 后台 banner 管理![]() |
后台订单管理![]() | 后台分类管理![]() |
后台金刚区管理![]() | 后台栏目管理![]() |
前台演示地址: http://121.4.124.33/mall 后台演示地址: http://121.4.124.33/admin
最后说两句waynboot-mall作为博主的开源项目集大成者,对于没有接触过商城项目的小伙伴来说是非常具有帮助和学习价值的。看完这个项目你能了解到一个商城项目的基本全貌,提前避坑。
![]() | 1 wayn111 OP 有兴趣的可以关注博主公众号 [waynblog] 每周分享技术干货、开源项目、实战经验、高效开发工具等,加博主微信一起讨论。 |
![]() | 2 winglight2016 2023-05-07 12:47:56 +08:00 后台用的到底是 springboot2 还是 3 ?你这里写的是 3 ,github 上又是 2.。。 |
![]() | 3 wayn111 OP @winglight2016 3 ,我改下 github 的 |
![]() | 4 wayn111 OP @winglight2016 github 上有 spingboot 版本对应 git 分支 |
![]() | 5 winglight2016 2023-05-07 14:44:57 +08:00 建议安装部署增加 Docker/k8s 的方法,最好是 docker compose 把数据库、redis 这些都配置好 |
![]() | 6 Mandelo 2023-05-07 14:50:35 +08:00 已 start,学习下 |
![]() | 7 OP @winglight2016 可以加 |
![]() | 9 x500 2023-05-07 19:56:15 +08:00 mark ,已 star |
10 ethan1i 2023-05-08 09:58:00 +08:00 已 start |
![]() | 11 timepast 2023-05-08 10:43:46 +08:00 感谢分享 |