前言
shopping-mall是一個企業級別Java電商項目,是有個人前端設計和開發高質量電商平臺,該篇文章主要是多shopping-mall電商系統的開發總結以及對技術的進一步分析和改進。
項目模塊分析
項目主要分爲七大模塊:用戶模塊、商品模塊、訂單模塊、分類模塊、購物車模塊、收貨地址模塊以及支付模塊。
功能分析
核心模塊
高複用的服務響應對應的設計思想和封裝
用戶模塊
解決橫向越權和縱向越權的問題,用戶密碼使用MD5明文加密,guava緩存實現信息的存儲,用戶功能主要包括:註冊、登錄、檢測用戶名是否有效,密碼的重置,更新用戶信息等等...
用戶忘記密碼以及密碼提示問題和答案設計
當用戶忘記密碼需要重置密碼的時候,需要輸入註冊時設置的密碼提示問題和問題答案,比如:你的生日是什麼時候? 1996-08-28,修改密碼的時候需要校驗密保問題和答案時候相一致,shopping-mall_v1.0的時候,我們採用guava cache來存儲一個forgetToken,比設置token的有效期爲30分鐘(可根據項目場景選擇),在重置密碼的時候需要傳遞參數有:username,password,forgetToken三個參數,這時候需要判斷在guava獲取tokenCache裏面的token和傳進來的forgetToken是否一致,滿足一致條件才能重置密碼成功!
自定義guava cache存儲token
public class TokenCache {
//創建logback的logger
private static Logger logger = LoggerFactory.getLogger(TokenCache.class);
//生成token的前綴
public static final String TOKEN_PREFIX = "token_";
//聲明一個靜態的內存塊,guava裏面的本地緩存
public static LoadingCache<String,String> localCache =
//構建本地緩存,調用鏈的方式,initialCapacity是緩存的初始化容量,
// maxSize是緩存設置的最大內存容量,expireAfterAccess設置緩存的有效期爲12個小時
CacheBuilder.newBuilder()
.initialCapacity(1000)
.maximumSize(10000)
.expireAfterAccess(12, TimeUnit.HOURS)
//build裏面要實現一個匿名內部類
.build((new CacheLoader<String, String>() {
//這個方法是默認的數據加載實現,當get的時候,如果key沒有對應的值,就調用這個方法進行加載
@Override
public String load(String key) throws Exception {
//爲什麼要把return的null值寫成字符串,因爲到時候用null去.equal的時候,會報空指針異常
return "null";
}
}));
//添加本地緩存
public static void setKey(String key,String value){
localCache.put(key,value);
}
//得到本地緩存
public static String getValue(String key){
String value =null;
try {
value = localCache.get(key);
if ("null".equals(value)){
return null;
}
return value;
}catch (ExecutionException e){
logger.error("獲取緩存getKey()方法錯誤",e);
}
return null;
}
}
商品模塊
POJO、VO抽象模型的設計,高效的分頁以及動態排序,使用FTP服務對接,富文本上傳商品圖片信息,商品模塊比較重要的就是商品的搜索以及動態排序和商品分頁,比如:根據關鍵字後臺進行模糊查詢,根據價格升序/降序排列,商品的分頁主要集成 pagehelper 分頁插件來完成,改分頁插件實現數據分頁相對高效,實用起來也比較簡單,我們只需要使用PageHelper.startPage(pageNum,pageSize) 即可實現
訂單模塊
安全漏洞解決方案,考慮電商商品訂單號的生成規則,訂單狀態的分析和訂單枚舉常量的設計,訂單的超時關單處理(延時隊列)
創建訂單OrderVO類的封裝,封裝返回創建訂單的所有信息,包括:訂單信息,訂單明細信息(List<OrderItemVO>),地址信息,創建訂單時需要校驗購物車商品的狀態/數量,比如是否已下架,是否庫存不足,校驗成功才允許下單,下單成功之後需要減庫存操作,需要遍歷每一個訂單Item,通過orderItem的productId獲取到對應商品,執行product.getStock() - product.getQuantity();操作;
訂單號的生成規則(時間戳+隨機數的方式):
/**
* orderNo生成方式
*
* @return
*/
private long generateOrderNo() {
//獲取當前時間戳
long currentTime = System.currentTimeMillis();
//時間+[0,1000)之間的數即[0,999]
return currentTime + new Random().nextInt(1000);
}
定時關單,hour小時以內未付款的訂單,進行自動關閉
相關實現邏輯:首先先獲取前hour小時的訂單,查詢出這些訂單裏面爲付款的訂單,查詢到這些訂單之後,拿到即將關閉訂裏的商品和數量,取消訂單時候庫存需要加上商品數量(也就是庫存還原),最後再設置訂單狀態。
@Override
public void closeOrder(int hours) {
//前hour小時的訂單
Date closeDateTime = DateUtils.addHours(new Date(), -hours);
List<Order> orderList = orderMapper.selectOrderStatusByCreateTime(Constant.OrderStatusEnum.NO_PAY.getCode(), dateToStr(closeDateTime));
for (Order order : orderList) {
List<OrderItem> orderItemList = orderItemMapper.getByOrderNo(order.getOrderNo());
for (OrderItem orderItem : orderItemList) {
//拿到即將要被關閉商品的和數量:一定要用主鍵where條件,防止鎖表,同時必須支持MySQL的InnoDB引擎
Integer stock = productMapper.selectStockByProductId(orderItem.getProductId());
//考慮到已生成訂單裏的商品已被刪除,這時候就不必更新了
if (stock == null) {
continue;
}
Product product = new Product();
product.setId(orderItem.getProductId());
product.setStock(stock + orderItem.getQuantity());
productMapper.updateByPrimaryKeySelective(product);
}
orderMapper.closeOrderByOrderId(order.getId());
log.info("關閉訂單orderNo:{}", order.getOrderNo());
}
}
分類模塊
採用遞歸算法,複雜對象的排重,無限層級的樹狀結構的設計
獲取分類子節點(平級):當父及分類parentId傳0的時候,查找的是跟節點的子分類
獲取分類id及遞歸子節點分類id(返回本身以及它下面的子節點,假設0->10->100->1000,0的下一級子孩子節點爲10,10的下一級節點爲100,100的下一級節點爲1000,業績是返回:0(本身)->10->100->100),設計到遞歸查詢算法,算法設計如下:
/**
* 遞歸查詢本節點的id以及孩子節點的id邏輯實現
*
* @param categoryId
* @return
*/
@Override
public ServerResponse<List<Integer>> selectCategoryAndChildrenById(Integer categoryId) {
Set<Category> categorySet = Sets.newHashSet();
this.findChildCategoryRecursive(categorySet, categoryId);
//最後是返回categoryId
List<Integer> categoryIdList = Lists.newArrayList();
if (categoryId != null) {
for (Category categoryItem : categorySet) {
categoryIdList.add(categoryItem.getId());
}
}
return ServerResponse.createBySuccess(categoryIdList);
}
/**
* 遞歸查詢算法,自己調自己,我們使用Set集合做返回,可以排重,這裏要重寫Category的HashCode和equals
* 的兩個方法爲什麼呢?當兩個對象相同,即equals返回true,則他們的hashCode一定相同,但是當兩個對象的
* hashCode相同,兩個對象不一定相同,所以,得出結論當使用Set集合的時候,注意要重寫equals和hashCode
* 兩個方法。
*/
private Set<Category> findChildCategoryRecursive(Set<Category> categorySet, Integer categoryId) {
Category category = categoryMapper.selectByPrimaryKey(categoryId);
if (category != null) {
categorySet.add(category);
}
//遞歸算法,查找子節點
List<Category> categoryList = categoryMapper.selectCategoryChildrenByParentId(categoryId);
for (Category categoryItem : categoryList) {
//自己調用自己
findChildCategoryRecursive(categorySet, categoryItem.getId());
}
return categorySet;
}
//mapper
<select id="selectCategoryChildrenByParentId" parameterType="int" resultMap="BaseResultMap">
select <include refid="Base_Column_List"/> from tb_category
where parent_id = #{parentId}
</select>
購物車模塊
購物車模塊核高複用的邏輯方法的封裝,商品總價的計算複用和封裝,使用BigDecimal類型解決商業運算丟失精度的問題,購物車的單選/反選,全選/全反選功能,一個購物車裏麪包括多個商品,添加購物車時需要校驗購買的每個商品數量是否超過庫存,超過庫存這時候,購買的數量不能設爲用戶選擇的數量,而是選擇庫存數量,計算價格的時候也只能按照現有庫存數量乘以單價的方式計算總額。
收貨地址模塊
數據綁定和對象綁定,越權問題的升級個鞏固,刪除地址的時候爲了防止橫向越權我們不能只傳一個shippingId,因此不能世界使用mybatis生成的deleteByPrimaryKey()的方法來刪除,試想:當用戶處於登陸狀態的時候,如果只需要傳一個shippingId就能實現刪除的時候,那當傳的不是個人的shippingId就會產生越權問題,也就是所謂的橫向越權,爲了防止橫向越權我們需要自定義一個方法將收穫地址和用戶綁定,比如:自定義deleteByUserIdAndShippingId()方法來實現。(地址的更新也是一樣的道理)
支付模塊
支付寶SDK源碼解析,分析了支付寶支付Demo的支付流程,將支付寶集成到項目中,包括:支付二維碼的生成,掃碼支付,支付的邏輯是:
第一步:查看訂單的支付狀態,如果order.getStatus()<=Constant.OrderStatusEnum.PAID.getCode()時,才滿足支付條件
第二步:支付,採用支付寶當面付,需要生成支付二維碼,其實這一步的關鍵就是創建掃描支付的請求biulder,通過builder來設置請求參數,首先會調用需下單的接口,判斷預下單是否生成,如果預下單成功之後,需要將訂單的狀態修改爲已支付,接着下單成功之後,會返回支付二維碼,這是集成支付的比較關鍵的一步,我們需要做的就是保存這個支付二維碼並上傳到ftp服務器,最後返回一個二維碼訪問地址qrUrl給前端展示給用戶,用戶掃描對應的支付二維碼即可支付訂單,支付完成之後需要作支付寶回調處理,更新對應的訂單狀態爲已支付,更新訂單的支付方式。
支付寶官方支付場景過程說明:
1. 用戶掃碼
2.調用alipay.trade.precreate()請求生成二維碼連接 -> 將二維碼連接轉二維碼圖片
3. 輸入支付密碼完成支付 -> 返回支付成功信息 -> 若支付成功,則返回異步信息(商戶返回success)
4.調用alipay.trade.query()查詢訂單狀態 -> 但會查詢結果 -> 若返回支付成功(code=10000),則流程結束
5.若未在指定時間內未完成支付 -> 調用alipay.trade.cancel進行交易關閉
線上部署
雲服務器vsftp、nginx的配置,雲服務器的配置與域名解析,端口的轉發