博主給大家推薦一套全部開源的H5電商專案waynboot-mall,由博主在2020年開發至今,已有三年之久,那時候網上很多的H5商城專案都是半開源版本,要么沒有H5前端代碼,要么需要加群咨詢,屬實惡心,于是博主決定自己開發一套完整的移動端H5商城,包含一個管理后臺、一個前臺H5商城、一套后端介面,專案地址如下:
- H5商城前端代碼:https://github.com/wayn111/waynboot-mobile
- 運營后臺前端代碼:https://github.com/wayn111/waynboot-admin
- 后端介面代碼:https://github.com/wayn111/waynboot-mall
歡迎大家關注這個專案,點個Star讓更多的人了解到這個專案,
一、簡介
waynboot-mall是一套全部開源的微商城專案,實作了一個商城所需的首頁展示、商品分類、商品詳情、sku組合、商品搜索、購物車、結算下單、訂單狀態流轉、商品評論等一系列功能,
技術上基于最新得Spring Boot3.0、Jdk17,整合了Redis、RabbitMQ、ElasticSearch等常用中間件,
貼近生產環境實際經驗開發而來,
二、技術特點
- 訂單金額計算使用BigDeciaml型別,支持小數點后兩位
- 支持微信內JsApi支付、H5網頁支付
- 商城介面代碼清晰、注釋完善、模塊拆分合理
- 使用Spring-Security進行訪問權限控制
- 使用jwt進行介面授權驗證
- ORM層使用Mybatis Plus提升開發效率
- 添加全域例外處理器,統一例外處理
- 使用Spring Boot admin進行服務監控
- 集成七牛云存盤配置,支持上傳檔案至七牛獲取cdn下載鏈接
- 集成常用郵箱配置,方便發送郵件
- 添加策略模式使用示例,優化首頁金剛區跳轉邏輯
- 拆分出通用的資料訪問模塊,統一Redis & Elastic配置與訪問
- 使用Elasticsearch高級客戶端依賴對Elasticsearch進行操作
- 支持商品資料同步Elasticsearch操作以及中文分詞搜索
- RabbitMQ生產者發送訊息采用異步confirm模式,消費者消費訊息時需手動確認確保訊息不丟失
- 下單處理程序引入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)
- 下單時扣減,這個方案屬于實時扣減,當有大量下單請求時,由于訂單數小于請求數,會發生下單失敗,但是無法防止短時間大量惡意請求占用庫存,
造成普通用戶無法下單 - 支付成功扣減,這個方案可以預防惡意請求占用庫存,但是會存在多個請求同時下單后,在支付回呼中扣減庫存失敗,導致訂單還是下單失敗并且還要退還訂單金額(這種請求就是訂單數超過了庫存數,無法發貨,影響用戶體驗)
- 還是下單時扣減,但是對于未支付訂單設定一個超時過期機制,比如下單時庫存減一,生成訂單后,對于未在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 + 今日第幾次下單
- 秒級時間戳:時間遞增保證唯一性
- 加密用戶ID:加密處理,回傳用戶ID6位數字,可以防并發訪問,同一秒用戶不會產生2個訂單
- 今日第幾次下單:便于運營查詢處理用戶當日訂單
/**
* 回傳訂單編號,生成規則:秒級時間戳 + 加密用戶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異步生成訂單,提高系統下單處理能力
- 用戶點擊提交訂單按鈕,后臺生成訂單編號和訂單金額跳轉到訂單支付頁面,并將訂單編號等資訊發送rabbitMQ訊息(生成訂單編號,還未生成訂單)
- 訂單消費者接受到訂單訊息后,獲取訂單編號生成訂單記錄(訂單創建成功,用戶待支付)
- 下單頁面,前端根據訂單編號輪詢訂單介面,訂單已創建則跳轉支付頁面,否則提示下單失敗(訂單創建失敗)
- 支付頁面,用戶點擊支付按鈕時,后臺呼叫微信/支付寶下單介面后,前端喚醒微信/支付寶支付,用戶輸入密碼
- 用戶支付完成后在微信/支付寶下回呼通知里更新訂單狀態為已支付(訂單已支付)
- 用戶支付完成后,回傳支付狀態查看頁面,
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());
}
四、演示圖
商城登陸![]() |
商城注冊![]() |
商城首頁![]() |
商城搜索![]() |
搜索結果展示![]() |
金剛位跳轉![]() |
商品分類![]() |
商品詳情![]() |
商品sku選擇![]() |
購物車查看![]() |
確認下單![]() |
選擇支付方式![]() |
商城我的頁面![]() |
我的訂單串列![]() |
添加商品評論![]() |
查看商品評論![]() |
后臺登陸![]() |
后臺首頁![]() |
后臺會員管理![]() |
后臺評論管理![]() |
后臺地址管理![]() |
后臺添加商品![]() |
后臺商品管理![]() |
后臺banner管理![]() |
后臺訂單管理![]() |
后臺分類管理![]() |
后臺金剛區管理![]() |
后臺欄目管理![]() |
五、在線體驗
演示地址:http://121.4.124.33/mall
最后說兩句waynboot-mall作為博主的開源專案集大成者,對于沒有接觸過商城專案的小伙伴來說是非常具有幫助和學習價值的,看完這個專案你能了解到一個商城專案的基本全貌,提前避坑,
感謝大家閱讀,希望這篇文章能為你提供價值,公眾號【waynblog】每周分享技術干貨、開源專案、實戰經驗、高效開發工具等,您的關注將是我的更新動力??,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/551702.html
標籤:Java
上一篇:Java8 Stream流的合并
下一篇:返回列表




























