博主給大家推薦一套全部開(kāi)源的H5電商項(xiàng)目waynboot-mall。由博主在2020年開(kāi)發(fā)至今,已有三年之久。那時(shí)候網(wǎng)上很多的H5商城項(xiàng)目都是半開(kāi)源版本,要么沒(méi)有H5前端代碼,要么需要加群咨詢(xún),屬實(shí)惡心。于是博主決定自己開(kāi)發(fā)一套完整的移動(dòng)端H5商城,包含一個(gè)管理后臺(tái)、一個(gè)前臺(tái)H5商城、一套后端接口。項(xiàng)目地址如下:
- H5商城前端代碼:https://github.com/wayn111/waynboot-mobile
- 運(yùn)營(yíng)后臺(tái)前端代碼:https://github.com/wayn111/waynboot-admin
- 后端接口代碼:https://github.com/wayn111/waynboot-mall
歡迎大家關(guān)注這個(gè)項(xiàng)目,點(diǎn)個(gè)Star讓更多的人了解到這個(gè)項(xiàng)目。
一、簡(jiǎn)介
waynboot-mall是一套全部開(kāi)源的微商城項(xiàng)目,實(shí)現(xiàn)了一個(gè)商城所需的首頁(yè)展示、商品分類(lèi)、商品詳情、sku組合、商品搜索、購(gòu)物車(chē)、結(jié)算下單、訂單狀態(tài)流轉(zhuǎn)、商品評(píng)論等一系列功能。
技術(shù)上基于最新得Spring Boot3.0、Jdk17,整合了Redis、RabbitMQ、ElasticSearch等常用中間件,
貼近生產(chǎn)環(huán)境實(shí)際經(jīng)驗(yàn)開(kāi)發(fā)而來(lái)。
二、技術(shù)特點(diǎn)
- 訂單金額計(jì)算使用BigDeciaml類(lèi)型,支持小數(shù)點(diǎn)后兩位
- 支持微信內(nèi)JsApi支付、H5網(wǎng)頁(yè)支付
- 商城接口代碼清晰、注釋完善、模塊拆分合理
- 使用Spring-Security進(jìn)行訪問(wèn)權(quán)限控制
- 使用jwt進(jìn)行接口授權(quán)驗(yàn)證
- ORM層使用Mybatis Plus提升開(kāi)發(fā)效率
- 添加全局異常處理器,統(tǒng)一異常處理
- 使用Spring Boot admin進(jìn)行服務(wù)監(jiān)控
- 集成七牛云存儲(chǔ)配置,支持上傳文件至七牛獲取cdn下載鏈接
- 集成常用郵箱配置,方便發(fā)送郵件
- 添加策略模式使用示例,優(yōu)化首頁(yè)金剛區(qū)跳轉(zhuǎn)邏輯
- 拆分出通用的數(shù)據(jù)訪問(wèn)模塊,統(tǒng)一Redis & Elastic配置與訪問(wèn)
- 使用Elasticsearch高級(jí)客戶(hù)端依賴(lài)對(duì)Elasticsearch進(jìn)行操作
- 支持商品數(shù)據(jù)同步Elasticsearch操作以及中文分詞搜索
- RabbitMQ生產(chǎn)者發(fā)送消息采用異步confirm模式,消費(fèi)者消費(fèi)消息時(shí)需手動(dòng)確認(rèn)確保消息不丟失
- 下單處理過(guò)程引入RabbitMQ,異步生成訂單記錄,提高系統(tǒng)下單處理能力
三、商城設(shè)計(jì)
文項(xiàng)目目錄
|-- waynboot-monitor // 監(jiān)控模塊
|-- waynboot-admin-api // 運(yùn)營(yíng)后臺(tái)api模塊,提供后臺(tái)項(xiàng)目api接口
|-- waynboot-common // 通用模塊,包含項(xiàng)目核心基礎(chǔ)類(lèi)
|-- waynboot-data // 數(shù)據(jù)模塊,通用中間件數(shù)據(jù)訪問(wèn)
| |-- waynboot-data-redis // redis訪問(wèn)配置模塊
| |-- waynboot-data-elastic // elastic訪問(wèn)配置模塊
|-- waynboot-generator // 代碼生成模塊
|-- waynboot-message-consumer // 消費(fèi)者模塊,處理訂單消息和郵件消息
|-- waynboot-message-core // 消費(fèi)者核心模塊,隊(duì)列、交換機(jī)配置
|-- waynboot-mobile-api // h5商城api模塊,提供h5商城api接口
|-- pom.xml // maven父項(xiàng)目依賴(lài),定義子項(xiàng)目依賴(lài)版本
|-- ...
技術(shù)亮點(diǎn)
2.1 庫(kù)存扣減
庫(kù)存扣減操作是在下單操作扣減還是在支付成功時(shí)扣減?(ps:扣減庫(kù)存使用樂(lè)觀鎖機(jī)制 where goods_num - num >= 0
)
- 下單時(shí)扣減,這個(gè)方案屬于實(shí)時(shí)扣減,當(dāng)有大量下單請(qǐng)求時(shí),由于訂單數(shù)小于請(qǐng)求數(shù),會(huì)發(fā)生下單失敗,但是無(wú)法防止短時(shí)間大量惡意請(qǐng)求占用庫(kù)存,
造成普通用戶(hù)無(wú)法下單 - 支付成功扣減,這個(gè)方案可以預(yù)防惡意請(qǐng)求占用庫(kù)存,但是會(huì)存在多個(gè)請(qǐng)求同時(shí)下單后,在支付回調(diào)中扣減庫(kù)存失敗,導(dǎo)致訂單還是下單失敗并且還要退還訂單金額(這種請(qǐng)求就是訂單數(shù)超過(guò)了庫(kù)存數(shù),無(wú)法發(fā)貨,影響用戶(hù)體驗(yàn))
- 還是下單時(shí)扣減,但是對(duì)于未支付訂單設(shè)置一個(gè)超時(shí)過(guò)期機(jī)制,比如下單時(shí)庫(kù)存減一,生成訂單后,對(duì)于未在15分鐘內(nèi)完成支付的訂單,
自動(dòng)取消超期未支付訂單并將庫(kù)存加一,該方案基本滿(mǎn)足了大部分使用場(chǎng)景 - 針對(duì)大流量下單場(chǎng)景,比如一分鐘內(nèi)五十萬(wàn)次下單請(qǐng)求,可以通過(guò)設(shè)置虛擬庫(kù)存的方式減少下單接口對(duì)數(shù)據(jù)庫(kù)的訪問(wèn)。具體來(lái)說(shuō)就是把商品庫(kù)存緩存到redis中,
下單時(shí)配合lua腳本原子的get和decr商品庫(kù)存數(shù)量(這一步就攔截了大部分請(qǐng)求),執(zhí)行成功后在扣減實(shí)際庫(kù)存
2.2 首頁(yè)查詢(xún)
首頁(yè)商品展示接口利用多線程技術(shù)進(jìn)行查詢(xún)優(yōu)化,將多個(gè)sql語(yǔ)句的排隊(duì)查詢(xún)變成異步查詢(xún),接口時(shí)長(zhǎng)只跟查詢(xún)時(shí)長(zhǎng)最大的sql查詢(xún)掛鉤
// 使用CompletableFuture異步查詢(xún)
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);
// 主線程等待子線程執(zhí)行完畢
CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join();
2.3 中文分詞搜索
ElasticSearch
搜索查詢(xún),查詢(xún)包含搜索關(guān)鍵字并且是上架中的商品,在根據(jù)指定字段進(jìn)行排序,最后分頁(yè)返回
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));
}
// 按價(jià)格高低排序
if (isPrice) {
searchSourceBuilder.sort(new FieldSortBuilder("retailPrice").order("asc".equals(orderBy) ? SortOrder.ASC : SortOrder.DESC));
}
// 按銷(xiāo)量排序
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 訂單編號(hào)
訂單編號(hào)生成規(guī)則:秒級(jí)時(shí)間戳 + 加密用戶(hù)ID + 今日第幾次下單
- 秒級(jí)時(shí)間戳:時(shí)間遞增保證唯一性
- 加密用戶(hù)ID:加密處理,返回用戶(hù)ID6位數(shù)字,可以防并發(fā)訪問(wèn),同一秒用戶(hù)不會(huì)產(chǎn)生2個(gè)訂單
- 今日第幾次下單:便于運(yùn)營(yíng)查詢(xún)處理用戶(hù)當(dāng)日訂單
/**
* 返回訂單編號(hào),生成規(guī)則:秒級(jí)時(shí)間戳 + 加密用戶(hù)ID + 今日第幾次下單
*
* @param userId 用戶(hù)ID
* @return 訂單編號(hào)
*/
public static String generateOrderSn(Long userId) {
long now = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
return now + encryptUserId(String.valueOf(userId), 6) + countByOrderSn(userId);
}
/**
* 計(jì)算該用戶(hù)今日內(nèi)第幾次下單
*
* @param userId 用戶(hù)ID
* @return 該用戶(hù)今日第幾次下單
*/
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)));
}
/**
* 加密用戶(hù)ID,返回num位字符串
*
* @param userId 用戶(hù)ID
* @param num 長(zhǎng)度
* @return num位加密字符串
*/
private static String encryptUserId(String userId, int num) {
return String.format("%0" + num + "d", Integer.parseInt(userId) + 1);
}
2.5 異步下單
下單流程處理過(guò)程,通過(guò)rabbitMQ異步生成訂單,提高系統(tǒng)下單處理能力
- 用戶(hù)點(diǎn)擊提交訂單按鈕,后臺(tái)生成訂單編號(hào)和訂單金額跳轉(zhuǎn)到訂單支付頁(yè)面,并將訂單編號(hào)等信息發(fā)送rabbitMQ消息(生成訂單編號(hào),還未生成訂單)
- 訂單消費(fèi)者接受到訂單消息后,獲取訂單編號(hào)生成訂單記錄(訂單創(chuàng)建成功,用戶(hù)待支付)
- 下單頁(yè)面,前端根據(jù)訂單編號(hào)輪詢(xún)訂單接口,訂單已創(chuàng)建則跳轉(zhuǎn)支付頁(yè)面,否則提示下單失敗(訂單創(chuàng)建失?。?/li>
- 支付頁(yè)面,用戶(hù)點(diǎn)擊支付按鈕時(shí),后臺(tái)調(diào)用微信/支付寶下單接口后,前端喚醒微信/支付寶支付,用戶(hù)輸入密碼
- 用戶(hù)支付完成后在微信/支付寶下回調(diào)通知里更新訂單狀態(tài)為已支付(訂單已支付)
- 用戶(hù)支付完成后,返回支付狀態(tài)查看頁(yè)面。
2.6 設(shè)計(jì)模式
金剛區(qū)跳轉(zhuǎn)使用策略模式進(jìn)行代碼編寫(xiě)
1.定義金剛位跳轉(zhuǎn)策略接口以及跳轉(zhuǎn)枚舉類(lèi)
public interface DiamondJumpType {
List<Goods> getGoods(Page<Goods> page, Diamond diamond);
Integer getType();
}
// 金剛位跳轉(zhuǎn)類(lèi)型枚舉
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.定義策略實(shí)現(xiàn)類(lèi),并使用@Component注解注入spring
// 分類(lèi)策略實(shí)現(xiàn)
@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();
}
}
// 欄目策略實(shí)現(xiàn)
@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.定義策略上下文,通過(guò)構(gòu)造器注入spring,定義map屬性,通過(guò)key獲取對(duì)應(yīng)策略實(shí)現(xiàn)類(lèi)
@Component
public class DiamondJumpContext {
private final Map<Integer, DiamondJumpType> map = new HashMap<>();
/**
* 由spring自動(dòng)注入DiamondJumpType子類(lèi)
*
* @param diamondJumpTypes 金剛位跳轉(zhuǎn)類(lèi)型集合
*/
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對(duì)象,調(diào)用getInstance方法傳入枚舉類(lèi)型
@Autowired
private DiamondJumpContext diamondJumpContext;
@Test
public void test(){
DiamondJumpType diamondJumpType=diamondJumpContext.getInstance(JumpTypeEnum.COLUMN.getType());
}
四、演示圖
商城登陸 | 商城注冊(cè) |
商城首頁(yè) | 商城搜索 |
搜索結(jié)果展示 | 金剛位跳轉(zhuǎn) |
商品分類(lèi) | 商品詳情 |
商品sku選擇 | 購(gòu)物車(chē)查看 |
確認(rèn)下單 | 選擇支付方式 |
商城我的頁(yè)面 | 我的訂單列表 |
添加商品評(píng)論 | 查看商品評(píng)論 |
后臺(tái)登陸 | 后臺(tái)首頁(yè) |
后臺(tái)會(huì)員管理 | 后臺(tái)評(píng)論管理 |
后臺(tái)地址管理 | 后臺(tái)添加商品 |
后臺(tái)商品管理 | 后臺(tái)banner管理 |
后臺(tái)訂單管理 | 后臺(tái)分類(lèi)管理 |
后臺(tái)金剛區(qū)管理 | 后臺(tái)欄目管理 |
五、在線體驗(yàn)
演示地址:http://121.4.124.33/mall
最后說(shuō)兩句waynboot-mall作為博主的開(kāi)源項(xiàng)目集大成者,對(duì)于沒(méi)有接觸過(guò)商城項(xiàng)目的小伙伴來(lái)說(shuō)是非常具有幫助和學(xué)習(xí)價(jià)值的??赐赀@個(gè)項(xiàng)目你能了解到一個(gè)商城項(xiàng)目的基本全貌,提前避坑。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-434559.html
感謝大家閱讀,希望這篇文章能為你提供價(jià)值。公眾號(hào)【waynblog】每周分享技術(shù)干貨、開(kāi)源項(xiàng)目、實(shí)戰(zhàn)經(jīng)驗(yàn)、高效開(kāi)發(fā)工具等,您的關(guān)注將是我的更新動(dòng)力??。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-434559.html
到了這里,關(guān)于一套前后臺(tái)全部開(kāi)源的H5商城送給大家的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!