web端電商購物項目shopping-mall_v1.0構架設計

前言

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的配置,雲服務器的配置與域名解析,端口的轉發

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章