博主给大家推荐一套全部开源的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管理 |
后台订单管理 | 后台分类管理 |
后台金刚区管理 | 后台栏目管理 |
最后说两句waynboot-mall作为博主的开源项目集大成者,对于没有接触过商城项目的小伙伴来说是非常具有帮助和学习价值的。看完这个项目你能了解到一个商城项目的基本全貌,提前避坑。
感谢大家阅读,希望这篇文章能为你提供价值。公众号【waynblog】每周分享技术干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力😘。