Python Django 商城項目總結

如下轉載於http://www.javashuo.com/article/p-zmycnkvk-g.html   僅供本人學習和研究html

商城商業模式:
C2B模式(消費者到企業的商業模式),相相似網站包括:京東,淘寶,海爾商城,尚品宅配等。前端

商城需求分析
1,用戶部分
2,商品部分
3,購物車部分
4,商品訂單備份
5,用戶支付部分
6,上線程序的配置python

用戶部分模塊:

基本功能:用戶註冊,登陸,密碼的重置,第三方登陸
用戶註冊
1,圖片驗證碼
流程分析:
1,前端生成uuid隨機字符串
2,後端生成圖片驗證碼發送給前端,將圖形驗證碼的存入到redis中
2,短信驗證碼
1,檢查圖片的驗證碼
2,檢驗是不是在60s內是否已經發送過
3,生成短信驗證碼
4,保存發送的短信驗證碼
5,發送短信(第三方平臺發送:雲通信)
3,判斷用戶名是否存在
1,用戶輸入用戶名以後ajax局部刷新頁面
2,後臺查詢數據庫用戶是否存在
3,返回數據給前端
4,判斷手機號碼是否已經存在
同3git

技術點:先後端的域名不相同,涉及到csrf跨站請求僞造的問題;

  • Csrf相關概念:
    1,域=協議+域名+端口,在兩個域中,以上三者中任意一個條件不一樣,均涉及到跨域的問題;
    2,瀏覽器的策略
    1,對於簡單的請求,瀏覽器發送請求,可是獲得請求以後會檢驗響應頭中是否有當前的域中,若是沒有則會在瀏覽器中報錯;
    2,對於複雜的請求,瀏覽器會先發送一個option請求,詢問服務器是否支持跨域,若是響應頭中的域名容許,纔會發送相對應的請求來獲取數據,並交給js進行處理。
    3,Python的django中的跨域處理的相關模塊django-cors-headers

技術點:前端用戶將圖片驗證碼發送給後臺以後,第三方平臺發送短信的過程當中會有網絡的阻塞程序繼續往下執行,進而影響用戶體驗效果;

  • 解決方案:採用celery進行短信驗證碼的異步發送;
    Celery概念:分佈式異步任務隊列調度框架:
    1,支持定時任務和異步任務兩種方式
    2,組成:大概分爲四個部分client客戶端發送數據,broker中間件(redis數據庫,消息隊列),worker(任務的執行者),backend(執行worker任務的執行結果)
    3,能夠開啓多進程也能夠是多線程
    4,應用場景:在某一個任務的執行過程當中,會涉及到耗時的操做,可是這個耗時操做並不會影響後續的程序的執行,此時就能夠用celery來異步執行這些任務;

用戶登陸

JWTtoken的相關了解

  • cookies的使用目的ajax

    • http協議本生是一種無狀態的協議,假如用戶每次發送請求過來將用戶名和密碼在後端進行驗證後才能夠登陸到某些界面才能夠進行操做,當客戶端再次請求服務器時,又要從新進行認證;
    • 解決方法:在客戶端設置cookie並在本地設置session存儲用戶的敏感信息,從而來記錄當前用戶登陸的狀態,若是用戶量再次請求服務器,將cookie帶給服務器,服務器查詢session獲取用戶信息,進行下一步操做。
    • 客戶端比較多的狀況下,seession中默認會存在內存,隨着用戶量的增長服務器的壓力就會變大;
  • 傳統的cookies顯示出來的問題
    - 在如今的市場各類不一樣的瀏覽器,對同一個網站進行,用戶的每種設備都須要維護相關的session在服務器端,會形成服務器資源的浪費,相關網站採起單點登陸來記錄用戶的狀態狀態來解決以上傳統cookies帶來的問題redis

  • 單點登陸的概念sql

    • 用戶在不一樣的設備中進行登陸,服務器端不用維護用戶的相關信息,在每次登陸的過程當中都由客戶端將本身的用戶信息發送過來,服務端進行解析來獲取用戶的相關信息
  • token認證的機制docker

    • 用戶攜帶用戶名和密碼來後端進行驗證
    • 服務器端驗證經過後對爲當前用戶生成token
    • 將token返回給前端,記錄用戶的信息
    • 用戶再次請求服務器的時候,服務端解析token相關的信息,驗證用戶
    • 肯定用戶狀態,進行 相關操做
  • 備註:數據庫

    • jwt的組成第一部分咱們稱它爲頭部(header),第二部分咱們稱其爲載荷(payload, 相似於飛機上承載的物品),第三部分是簽證(signature).
    • secretkey是存儲在服務器端的,若是secret key
  • 用戶登陸JWT認證的的流程源代碼(待繼續理解)django

  • qq登陸

  • qq登陸流程oauth2的認證流程分析
    附件0.00KB

    • 用戶向美多網站發送qq註冊的請求
    • 美多網站向用戶返回qq登陸的頁面
    • 用戶在qq登陸的界面向qq的服務器發送qq用戶名和密碼發起登陸的請求
    • qq服務器認證成功以後將用戶引導到回調的網址中,並返回給用戶qq服務器的token值
    • 用戶重定向到美多頁面並攜帶了qq服務器發送的token
    • 後端接收到token後向qq服務器請求access token
    • qq服務器返回access token
    • 美多服務器經過access token來向qq服務器來獲取用戶的openid
    • 經過id來查詢用戶是否已經在美多商城註冊
    • 用戶已經註冊直接返回用戶的access token值
    • 用戶沒有帳號,生成註冊的access token,(載荷openid)從新註冊信息發送給後端
    • 後端接收到數據以後建立對象並將qq用戶的openid和帳號進行綁定
  • 忘記密碼的功能的實現

    • 用戶發送過來請求,攜帶用戶名和圖片驗證碼
    • 後端驗證圖片驗證碼,經過帳號查詢用戶,將用戶的電話號碼部分返回給前端,並生成發送短信的access token(載荷mobile)值
    • 前端填寫手機號碼驗證碼並攜帶access token到後端
    • 後端接收到手機號碼校驗(正確性,發送時間間隔),經過手機號碼站到用戶對象,生成密碼修改的access token(載荷uer obj)值
    • 前端用戶填寫新的密碼以後,攜帶access token到後端重置密碼

技能點

  • djangorestframework中的實現JWT token的模塊itsdangerous的使用

用戶中心

  • 我的信息

  • 我的信息是用戶的私有信息,必須是登陸用戶才能夠訪問,而且值能夠訪問本身的相關信息

    • 用戶我的信息的展現流程
    • 前端在頁面被加載完成以後向後端發送請求用戶數據
    • 後端經過rest_framework.permissions.IsAuthenticated判斷用戶是否登陸,並獲取用戶的user模型
    • 將用戶的詳細信息返回給前端,在前端進行展現
  • 用戶我的中心的信息中有一項是用戶的郵箱是否激活

  • 郵箱驗證的流程

    • 用戶填入郵箱點擊保存後端接收到郵箱後異步發出郵件,連接中包含access token(載荷uer id& email)
    • 郵件中包含token值,在用戶點擊郵件中的連接以後向前端發送激活的請求
    • 後端驗證access token合法性,DRF中的序列化器update的方法,在序列化器中create方法中將用戶的email字段更改成激活狀態
    • 將用戶的對象返回給前端

技術點:

  • django中發送郵件的配置信息
  • 利用celery來實現異步的郵件發送

用戶收貨地址的設置

  • DRF中序列化器的嵌套
from rest_framework import serializers from .models import Area class AreaSerializer(serializers.ModelSerializer): """ 行政區劃信息序列化器 """ class Meta: model = Area fields = ('id', 'name') class SubAreaSerializer(serializers.ModelSerializer): """ 子行政區劃信息序列化器 """ subs = AreaSerializer(many=True, read_only=True) class Meta: model = Area fields = ('id', 'name', 'subs') 
  • DRF中的ReadOnlyModelViewSet中將請求方式與資源狀態進行了綁定,在這裏咱們只須要從數據庫中去數據因此直接就能夠繼承ModelSerializer這個類
  • view視圖中的action=='list'(即前端url不帶參數)說明前端要獲取全部的省份
  • view視圖中的action!='list'(即前端url帶參數)說明前端要獲取全部的省份底下的行政區劃
  • 在這個返回的過程當中若是前端頁面返回的url中返回的帶有參數則返回省份

技術點

  • DRF的擴展類中的選擇以及序列化器的嵌套調用方法
  • 對DRF的擴展集的理解
  • Views django 中的原始的Views
  • APIView繼承類django中的Views,同時提供了用戶認證,權限認證,權限認證,節流認證,分頁,序列化等方法
  • GenericAPIView 繼承了APIView:在這個類中實現了兩個類實行和三個類方法
  • ListModelMixin 實現了list方法與get_queryset(),paginate_queryset,get_serializer
  • ListAPIView 可用的子類GenericAPIView、ListModelMixin 是上面兩種方法的子類
  • ViewSetMixin 實現了view = MyViewSet.as_view({'get': 'list', 'post': 'create'})

訂單模塊:

基本功能:提交訂單,個人訂單,訂單評價

  • 提交訂單

FastDFS分佈式文件系統

  • FastDFS分佈式文件系統,數據冗餘,數據的備份,數據量的存儲擴展
  • tracker server的做用是負載均衡和調度,能夠實現集羣,每一個reacker節點的地位平等,收集storage的狀態;
  • storage server的做用是存儲,不一樣的組內保存的內容是不一樣的,相同的組內保存的內容是相同的,這樣的設計數據會相對比較安全安全;
  • 不管是tracker仍是storage都支持集羣的方式擴展,數據的擴展比較方便
  • 文件上傳的流程
    • storage server定時向tracker server的上傳存儲狀態信息
    • 客戶端上傳連接請求
    • 請求會先到達tracker server,tracker server查詢能夠調用的storage;
    • 返回storage server 的ip和port
    • 上傳文件到指定的storage server中
    • srorage server生成file id,並將文件存入到磁盤中
    • 返回file id給客戶端
    • 存儲文件信息

docker的理解

  • docker是一種虛擬化的技術,咱們能夠將docker視爲一種容器,在容器的內部能夠運行服務,
  • Docker自己是一種C/S架構的程序,Docker客戶端須要向服務器發送請求,服務器完成全部的工做以後返回給客戶端結果;
  • 優勢
    • 加速本地開發和構建的流程,在本地能夠本身輕鬆的構建,運行,分享所配置好的docker環境
    • 可以在不一樣的操做系統的環境中獲取相同的docker容器中的環境,減小了在部署環節中的環境問題待來的麻煩
    • docker能夠建立虛擬化的沙箱環境能夠供測試使用
    • docker可讓開發者在最開始的開發過程當中在測試的環境中運行,而並不是一開始就在生產環境中開發,部署和測試

首頁靜態化的技術

  • 電商類型的網站首頁的訪問評率較大,每次獲取首頁過程當中去對數據庫進行查詢顯然不太合理,除了使用傳統意義上的緩存實現以外,咱們可使用首頁靜態化的技術,即將動態生成的html頁面生成保存成靜態文件,在用戶訪問的過程當中直接將靜態文件發送給用戶

  • 優勢:能夠緩解數據庫壓力,而且能夠提升用戶訪問的網站的速度,提升用戶體驗

  • 帶來的問題是:用戶在頁面中動態生成的部分數據如何處理

    • 在靜態頁面加載完成以後,經過js的代碼將動態的請求發送到後天請求數據,可是大部分數據均是靜態頁面中的數據,
    • 靜態生成的頁面由於並無實時更新,會出現部分商品的靜態化頁面中的數據和數據庫中實時更新的數據有差別
  • 應用場景:常常容易訪問可是,數據的變更並非太大的一些頁面能夠考慮使用靜態化技術

  • 難點GoodsCategory,GoodsChannel兩個表格之間的關係設計以及獲取商城商品分類的菜單

    • 首先是GoodsChannel,將全部的商品的頻道取出按照組號和組內順序排序
    • 排序後將數據以categories[group_id] = {'channels': [], 'sub_cats': []}的形式存入到有個有序的字典中
    • channels的值爲存儲的是頻道的相關信息(例如手機,相機,數碼)
    • sub_cats中存儲的值是該頻道下的GoodsCategory相關信息(例如手機通信,手機配件...相關,根據GoodsChannel數據結構表中頂級類別來查詢子類別)
    • 分別爲頂級類別下的子類別對象添加一個sub_cats的列表,來存儲此類別下的全部GoodsCategory的queryset對象
  • 難點 商品詳情頁面的數據結構

    • 三 獲取當前商品的規格信息

#!/usr/bin/env python """ 功能:手動生成全部SKU的靜態detail html文件 使用方法: ./regenerate_detail_html.py """ import sys sys.path.insert(0, '../') sys.path.insert(0, '../meiduo_mall/apps') import os if not os.getenv('DJANGO_SETTINGS_MODULE'): os.environ['DJANGO_SETTINGS_MODULE'] = 'meiduo_mall.settings.dev' import django django.setup() from django.template import loader from django.conf import settings from goods.utils import get_categories from goods.models import SKU def generate_static_sku_detail_html(sku_id): """ 生成靜態商品詳情頁面 :param sku_id: 商品sku id """ # 商品分類菜單 categories = get_categories() # 獲取當前sku的信息 sku = SKU.objects.get(id=sku_id) sku.images = sku.skuimage_set.all() # 麪包屑導航信息中的頻道 goods = sku.goods goods.channel = goods.category1.goodschannel_set.all()[0] # 構建當前商品的規格鍵 sku_specs = sku.skuspecification_set.order_by('spec_id') sku_key = [] for spec in sku_specs: sku_key.append(spec.option.id) print("當前商品的規格鍵[1,4,7]",sku_key) # 獲取當前商品的全部SKU skus = goods.sku_set.all() print("獲取當前商品的所在的SPU下的全部SKU對象",skus) # 構建不一樣規格參數(選項)的sku字典 # spec_sku_map = { # (規格1參數id, 規格2參數id, 規格3參數id, ...): sku_id, # (規格1參數id, 規格2參數id, 規格3參數id, ...): sku_id, # ... # } spec_sku_map = {} for s in skus: # 獲取sku的規格參數 s_specs = s.skuspecification_set.order_by('spec_id') # 用於造成規格參數-sku字典的鍵 key = [] for spec in s_specs: key.append(spec.option.id) # 向規格參數-sku字典添加記錄 # print("構造出的不一樣規格的參數",key) spec_sku_map[tuple(key)] = s.id print("{(1, 4, 7): 1, (1, 3, 7): 2}構造出的不一樣規格的參數",spec_sku_map) # 獲取當前商品的規格信息 specs = goods.goodsspecification_set.order_by('id') print("當前商品全部的規格選項,屏幕,顏色,,,",specs) # print("sku_key",sku_key) # 若當前sku的規格信息不完整,則再也不繼續 if len(sku_key) < len(specs): return for index, spec in enumerate(specs): # if index == 0: # print("index", index) # print("GoodsSpecification的規格信息對象", spec) # 複製當前sku的規格鍵 key = sku_key[:] print("當前的規格選項",spec.name) # 該規格的選項 options = spec.specificationoption_set.all() print("options規格信息的選項", options) for option in options: # 在規格參數sku字典中查找符合當前規格的sku print("001",key) print("固定不變的數據庫中只有這兩種商品spec_sku_map",spec_sku_map) print("option.id", option.id) key[index] = option.id print("002",key) option.sku_id = spec_sku_map.get(tuple(key)) print(option.sku_id) spec.options = options # 渲染模板,生成靜態html文件 context = { 'categories': categories, 'goods': goods, 'specs': specs, 'sku': sku } template = loader.get_template('detail.html') html_text = template.render(context) file_path = os.path.join(settings.GENERATED_STATIC_HTML_FILES_DIR, 'goods/'+str(sku_id)+'.html') with open(file_path, 'w') as f: f.write(html_text) if __name__ == '__main__': sku = SKU.objects.get(id=1) generate_static_sku_detail_html(sku.id) 
  • 傳入單個對象時執行結果
當前商品的規格鍵[1,4,7] [1, 4, 7] 獲取當前商品的所在的SPU下的全部SKU對象 <QuerySet [<SKU: 1: Apple MacBook Pro 13.3英寸筆記本 銀色>, <SKU: 2: Apple MacBook Pro 13.3英寸筆記本 深灰色>]> {(1, 4, 7): 1, (1, 3, 7): 2}構造出的不一樣規格的參數 {(1, 4, 7): 1, (1, 3, 7): 2} 當前商品全部的規格選項,屏幕,顏色,,, <QuerySet [<GoodsSpecification: Apple MacBook Pro 筆記本: 屏幕尺寸>, <GoodsSpecification: Apple MacBook Pro 筆記本: 顏色>, <GoodsSpecification: Apple MacBook Pro 筆記本: 版本>]> 當前的規格選項 屏幕尺寸 options規格信息的選項 <QuerySet [<SpecificationOption: Apple MacBook Pro 筆記本: 屏幕尺寸 - 13.3英寸>, <SpecificationOption: Apple MacBook Pro 筆記本: 屏幕尺寸 - 15.4英寸>]> 001 [1, 4, 7] 固定不變的數據庫中只有這兩種商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2} option.id 1 002 [1, 4, 7] 1 001 [1, 4, 7] 固定不變的數據庫中只有這兩種商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2} option.id 2 002 [2, 4, 7] None 當前的規格選項 顏色 options規格信息的選項 <QuerySet [<SpecificationOption: Apple MacBook Pro 筆記本: 顏色 - 深灰色>, <SpecificationOption: Apple MacBook Pro 筆記本: 顏色 - 銀色>]> 001 [1, 4, 7] 固定不變的數據庫中只有這兩種商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2} option.id 3 002 [1, 3, 7] 2 001 [1, 3, 7] 固定不變的數據庫中只有這兩種商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2} option.id 4 002 [1, 4, 7] 1 當前的規格選項 版本 options規格信息的選項 <QuerySet [<SpecificationOption: Apple MacBook Pro 筆記本: 版本 - core i5/8G內存/256G存儲>, <SpecificationOption: Apple MacBook Pro 筆記本: 版本 - core i5/8G內存/128G存儲>, <SpecificationOption: Apple MacBook Pro 筆記本: 版本 - core i5/8G內存/512G存儲>]> 001 [1, 4, 7] 固定不變的數據庫中只有這兩種商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2} option.id 5 002 [1, 4, 5] None 001 [1, 4, 5] 固定不變的數據庫中只有這兩種商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2} option.id 6 002 [1, 4, 6] None 001 [1, 4, 6] 固定不變的數據庫中只有這兩種商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2} option.id 7 002 [1, 4, 7] 1 
  • 最後返回的specs的數據結構爲
specs = [
    {
        'name': '屏幕尺寸', 'options': [ {'value': '13.3寸', 'sku_id': xxx}, {'value': '15.4寸', 'sku_id': xxx}, ] }, { 'name': '顏色', 'options': [ {'value': '銀色', 'sku_id': xxx}, {'value': '黑色', 'sku_id': xxx} ] }, ... ] 
  • 經過sku id來取出此sku商品的SPU對應的全部存在的商品組合
  • 循環數據庫中全部的商品選項,將商品的選項與sku id來作對應,返回上面的數據類型
  • 相同的SPU對應的不一樣的SKU,返回的specs是相同的,例如若是同屬於手機這個SPU的Iphone6手機和Iphone7手機,返回的specs是相同的,若假設手機品牌只有屏幕大小不相同,則返回的數據類型以下
specs = [
    {
        'name': '屏幕尺寸', 'options': [ {'value': '13.3寸', 'sku_id': Iphone7的sku_id}, {'value': '15.4寸', 'sku_id': Iphone6的sku_id}, {'value': '15.4寸', 'sku_id': Iphone7的sku_id2}, ] }, { 'name': '顏色', 'options': [ {'value': '銀色', 'sku_id': Iphone7的sku_id}, {'value': '銀色', 'sku_id': Iphone6的sku_id}, {'value': '金色', 'sku_id': Iphone7的sku_id2}, ] }, ... ] 
  • 前端經過傳入的sku id來取值生成的詳情頁面,從同一份數據數據中拿的值,就會避免重複,例如Iphone6的只是取出全部的Iphone6的全部的信息生成靜態頁面,傳入的sku id不一樣獲得的頁面效果不一樣,經過不一樣的id也能夠找到不一樣的商品的詳情頁面

  • 細節完善在運營人員相關修改商品信息,要在後端實現,自動刷新詳情的頁面的數據,自動觸發靜態頁面的生成,利用了django中的ModelAdmin,在數據發生變更以後自動進行更新相關數據

class SKUSpecificationAdmin(admin.ModelAdmin): def save_model(self, request, obj, form, change): obj.save() from celery_tasks.html.tasks import generate_static_sku_detail_html generate_static_sku_detail_html.delay(obj.sku.id) def delete_model(self, request, obj): sku_id = obj.sku.id obj.delete() from celery_tasks.html.tasks import generate_static_sku_detail_html generate_static_sku_detail_html.delay(sku_id) 

獲取熱銷商品

  • 技術點
  • 詳情頁面中的熱銷商品每一個頁面加載完成以後都會來向後端請求數據,可是熱銷商品卻不常常發生變化或者是在一段時間內根據相關字段統計生成返回給前端便可,全部使用緩存的方式存儲熱銷商品是最合理的方式,避免了數據的連接,減小了服務器的壓力,充分的利用了緩存的響應速度也比較快能夠提升用戶的體驗

商品列表頁面的展現

  • 商品數據動態的從後端獲取,其餘部分生成靜態化頁面
  • 技術點:
    • DRF框架中過濾,排序,分頁,序列化器數據的返回
    • 適當使用到了DRF提供的擴展類ListAPIView來簡化代碼

商品搜索的功能實現

  • 技術點
    • Elasticsearch搜索引擎的原理:經過搜索引擎進行搜索數據查詢的過程當中,搜索引擎並非直接去數據庫中去進行數據庫的查詢,而是搜素引擎會對數據庫中全部的數據進行一遍的預處理,單獨的創建一份索引表,在進行數據庫查詢的過程當中,會在索引表中進行查詢,而後返回相應的數據。
    • Elasticsearch 不支持對中文進行分詞創建索引,須要配合擴展elasticsearch-analysis-ik來實現中文分詞處理
    • 使用haystack對接Elasticsearch的流程
      • 安裝
      • 在配置文件中配置haystack使用的搜索引擎後端
      • SKU索引數據模型類
      • 在templates目錄中建立text字段使用的模板文件
      • 手動生成初始索引
      • 建立序列化器
      • 建立視圖
      • 定義路由

購物車模塊

  • 對於未登陸的用戶,購物車
class CartMixin(): def str2dict(self, redis_data): # redis_data從redis中讀取的數據 """ 轉化爲python字典""" redis_dict = pickle.loads(base64.b64decode(redis_data)) return redis_dict def dict2str(self, redis_dict): """ python中dict轉爲能夠存入redis中的數據""" # 將合併後的字典再次存入到redis中 redis_bytes = pickle.dumps(redis_dict) redis_str = base64.b64encode(redis_bytes) return redis_str def write_cart(self, request: Request, response: Response, cart_dict: dict): """ 寫購物車數據""" cart_str = self.dict2str(cart_dict) if request.user.is_authenticated: key = "cart2_%s" % request.user.id get_redis_connection("cart").set(key, cart_str) else: response.set_cookie("cart", cart_str) def read_from_redis(self, user_id): """ 返回一個字典""" key = "cart2_%s" % user_id redis_data = get_redis_connection("cart").get(key) if redis_data is None: return {} return self.str2dict(redis_data) def read_from_cookie(self, request: Request): value = request.COOKIES.get("cart") if value is None: return {} return self.str2dict(value) def read(self, request: Request) -> dict: if request.user.is_authenticated: cart_dict = self.read_from_redis(request.user.id) else: cart_dict = self.read_from_cookie(request) return cart_dict class CartView(CartMixin, APIView): # pagination_class = StandardResultsSerPagination def post(self, request): serializer = CartSerializer(data=request.data) # 檢查前端發送過來數據是否正確 serializer.is_valid(raise_exception=True) # 數據檢驗經過 sku_id = serializer.validated_data['sku_id'] count = serializer.validated_data['count'] selected = serializer.validated_data["selected"] cart_dict = self.read(request) if sku_id in cart_dict: # new_count = cart_dict[sku_id][0] + count cart_dict[sku_id] = [new_count, selected] else: cart_dict[sku_id] = [count, selected] resp = Response(serializer.data, status=status.HTTP_201_CREATED) self.write_cart(request, resp, cart_dict) return resp def get(self, request): cart_dict = self.read(request) skus = SKU.objects.filter(id__in=cart_dict.keys()) for sku in skus: sku.count = cart_dict[sku.id][0] sku.selected = cart_dict[sku.id][1] serializer = CartSKUSerializer(skus, many=True) return Response(serializer.data) def put(self, request): # 檢查前端發送的數據是否正確 serializer = CartSerializer(data=request.data) serializer.is_valid(raise_exception=True) sku_id = serializer.validated_data.get('sku_id') count = serializer.validated_data.get('count') selected = serializer.validated_data.get('selected') cart_dict = self.read(request) cart_dict[sku_id] = [count, selected] resp = Response(serializer.data, status=status.HTTP_201_CREATED) self.write_cart(request, resp, cart_dict) return resp def delete(self, request): # 檢查參數sku_id serializer = CartDeleteSerializer(data=request.data) serializer.is_valid(raise_exception=True) sku_id = serializer.validated_data['sku_id'] cart_dict = self.read(request) if sku_id in cart_dict: del cart_dict[sku_id] resp = Response(serializer.data, status=status.HTTP_204_NO_CONTENT) self.write_cart(request, resp, cart_dict) return resp 
def merge_cart_cookie_to_redis(request, response, user): """ 合併購物車""" cookies_cart = request.COOKIES.get('cart') if cookies_cart is not None: cookies_dict = pickle.loads(base64.b64decode(cookies_cart)) # 取出cookies中的數據 print("cookies_dict000", cookies_dict) redis_conn = get_redis_connection("cart") redis_cart = redis_conn.get("cart2_%s" % user.id) # redis_dict = {} if redis_cart: redis_dict = pickle.loads(base64.b64decode(redis_cart)) # 取出的是redis中的數據 print("redis_dict001", redis_dict) # 過濾購物車中沒有被選中的商品 new_cookies_dict = deepcopy(cookies_dict) for sku_id, value in cookies_dict.items(): if not value[1]: new_cookies_dict.pop(sku_id) print("redis_dict002", new_cookies_dict) # 合併cookies中的值 redis_dict.update(new_cookies_dict) print("redis_dict003", redis_dict) redis_str = base64.b64encode(pickle.dumps(redis_dict)) key = "cart2_%s" % user.id get_redis_connection("cart").set(key, redis_str) # 往redis中寫入數據 # if cart: # pl = redis_conn.pipeline() # pl.hmset("cart_%s" % user.id, cart) # pl.sadd("cart_selected_%s" % user.id, *redis_cart_selected) # pl.execute() # 刪除cookie中的數據 response.delete_cookie("cart") return response return response 
  • 數據類型的設計原則:
    • 儘可能將cookies中的數據類型格式與redis數據庫中數據類型設計成形同的
    • 對redis數據庫的相關操做
    • {sku id :[count,True]}數據中sku id:商品的id,count:購物車中數據的商品的個數;True或者False表明是否被選中
    • 將對數據的操做封裝成一個擴展集,在視圖中繼承擴展類

訂單相關的操做

  • 訂單數據庫表的設計
    • 訂單的字段分析
    • 首先將訂單分爲兩個表格
    • 1,訂單的基本信息表;
    • 2,訂單的商品信息,二者之間的關係是一對多的關係
    • 訂單的基本信息表中主要存儲這筆訂單的相關信息(訂單的id,下單的用戶,用戶的默認地址,郵費的狀態,訂單的支付方式,訂單的狀態)
    • 訂單商品中保存(訂單的id,用戶商品的id,商品的數量,商品的價格)
    • 在前端中展現還須要的字段有這次訂單的總金額,以及該訂單中商品的數量,這兩個字段雖然通過表格之間的關聯能夠查詢出來,在這裏能夠將這兩個字段一塊兒定義在訂單的基本信息的表格中,避免在後續查詢訂單的過程當中對數據庫的操做;

具體字段的定義:

from django.db import models from meiduo_mall.utils.models import BaseModel from users.models import User, Address from goods.models import SKU # Create your models here. class OrderInfo(BaseModel): """ 訂單信息 """ PAY_METHODS_ENUM = { "CASH": 1, "ALIPAY": 2 } PAY_METHOD_CHOICES = ( (1, "貨到付款"), (2, "支付寶"), ) ORDER_STATUS_ENUM = { "UNPAID": 1, "UNSEND": 2, "UNRECEIVED": 3, "UNCOMMENT": 4, "FINISHED": 5 } ORDER_STATUS_CHOICES = ( (1, "待支付"), (2, "待發貨"), (3, "待收貨"), (4, "待評價"), (5, "已完成"), (6, "已取消"), ) order_id = models.CharField(max_length=64, primary_key=True, verbose_name="訂單號") user = models.ForeignKey(User, on_delete=models.PROTECT, verbose_name="下單用戶") address = models.ForeignKey(Address, on_delete=models.PROTECT, verbose_name="收穫地址") total_count = models.IntegerField(default=1, verbose_name="商品總數") total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="商品總金額") freight = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="運費") pay_method = models.SmallIntegerField(choices=PAY_METHOD_CHOICES, default=1, verbose_name="支付方式") status = models.SmallIntegerField(choices=ORDER_STATUS_CHOICES, default=1, verbose_name="訂單狀態") class Meta: db_table = "tb_order_info" verbose_name = '訂單基本信息' verbose_name_plural = verbose_name class OrderGoods(BaseModel): """ 訂單商品 """ SCORE_CHOICES = ( (0, '0分'), (1, '20分'), (2, '40分'), (3, '60分'), (4, '80分'), (5, '100分'), ) order = models.ForeignKey(OrderInfo, related_name='skus', on_delete=models.CASCADE, verbose_name="訂單") sku = models.ForeignKey(SKU, on_delete=models.PROTECT, verbose_name="訂單商品") count = models.IntegerField(default=1, verbose_name="數量") price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="單價") comment = models.TextField(default="", verbose_name="評價信息") score = models.SmallIntegerField(choices=SCORE_CHOICES, default=5, verbose_name='滿意度評分') is_anonymous = models.BooleanField(default=False, verbose_name='是否匿名評價') is_commented = models.BooleanField(default=False, verbose_name='是否評價了') class Meta: db_table = "tb_order_goods" verbose_name = '訂單商品' verbose_name_plural = verbose_name 

獲取購物車商品邏輯

- 用戶必須在登陸的狀態下才能夠進入到購物車商品結算的頁面
- 查詢到當前訂單的用戶;
- 在redis數據庫中將當前用戶的購物車中全部商品查詢出來
- 過濾篩選出用戶選中的商品信息的id
- 查詢出當前訂單的運費的
- 將相關信息(例如運費和查詢出來的商品的skus對象)傳遞給序列化器,序列化器將數據從數據庫中序列化後返回給前端
  • 最終後端返回給前端的數據格式如圖所示
{
    "freight":"10.00", "skus":[ { "id":10, "name":"華爲 HUAWEI P10 Plus 6GB+128GB 鑽雕金 移動聯通電信4G手機 雙卡雙待", "default_image_url":"http://image.meiduo.site:8888/group1/M00/00/02/CtM3BVrRchWAMc8rAARfIK95am88158618", "price":"3788.00", "count":1 }, { "id":16, "name":"華爲 HUAWEI P10 Plus 6GB+128GB 曜石黑 移動聯通電信4G手機 雙卡雙待", "default_image_url":"http://image.meiduo.site:8888/group1/M00/00/02/CtM3BVrRdPeAXNDMAAYJrpessGQ9777651", "price":"3788.00", "count":1 } ] } 

保存訂單

  • id的字段定義,默認狀況下采用sql數據庫中的自增的id做爲主鍵,可是在考慮到id的可讀性和擴展性將主鍵設置爲具備特定格式的CharField字段,本身定義的id的格式的;
  • 保存訂單的邏輯實現
    • 獲取當前下單的用戶
    • 獲取用戶的基本信息(用戶的默認地址,用戶選擇的支付方式)
    • 建立事物,在如下的任何一個操做不成功的狀況下就會返回到當前這個保存點
    • 組織訂單的id
    • 建立一個訂單基本信息的對象,進行保存
    • redis中取出全部的商品,過濾出用戶選中的商品
    • 給訂單的中設計的冗餘的字段賦初值
    • 從數據庫中查詢商品信息,並判斷用戶購買的商品庫存狀態和銷量的狀態
    • 更新數據庫中庫存和銷量的相關信息。在這個地方用樂觀鎖的方式來判斷在事物保存的過程當中是否有其餘用戶來操做過商品的,若是有則從新返回到事物保存點,沒有則繼續
    • 在這裏來查詢用戶的商品信息表中將訂單基本信息的表格中的相關冗餘字段計算出來一塊兒保存進入到數據庫中
class SaveOrderSerializer(serializers.ModelSerializer): """ 用戶支付訂單的建立序列化器""" class Meta: model = OrderInfo fields = ("order_id", "address", "pay_method") read_only_fields = ("order_id",) extra_kwargs = { "address": { "write_only": True, "required": True }, "pay_method": { "write_only": True, "required": True } } def create(self, validated_data): """ 保存訂單序列化器""" # 獲取當前下單用戶 user = self.context['request'].user address = validated_data.get("address") pay_method = validated_data.get("pay_method") # 組織訂單信息20170903153611+user.id order_id = timezone.now().strftime("%Y%m%d%H%M%S") + ("%09d" % user.id) # 開啓事務 with transaction.atomic(): # 建立保存點,記錄當前數據狀態 save_id = transaction.savepoint() try: # 保存訂單基本信息數據 OrderInfo order = OrderInfo.objects.create(**{ "order_id": order_id, 'user': user, 'address': address, 'total_count': 0, "total_amount": Decimal(0), "freight": Decimal(10), "pay_method": pay_method, # 若是用戶選擇現金支付,訂單狀態爲待發貨;反之爲待支付 "status": OrderInfo.ORDER_STATUS_ENUM['UNSEND'] if validated_data['pay_method'] == OrderInfo.PAY_METHODS_ENUM['CASH'] else OrderInfo.ORDER_STATUS_ENUM['UNPAID'] }) redis_conn = get_redis_connection("cart") # 從redis中獲取購物車結算商品數據 selected_sku_id_list = [] redis_cart = redis_conn.get("cart2_%s" % user.id) # redis_dict = pickle.loads(base64.b64decode(redis_cart)) # 取出的是redis中的數據 # 過濾購物車中被選中的商品的id for sku_id, value in redis_dict.items(): if value[1]: selected_sku_id_list.append(sku_id) # 冗餘數據先設置默認值 order.total_amount = Decimal(0) order.total_count = 0 # 查詢商品信息 # skus = SKU.objects.filter(id__in=cart_dict.keys()) # 獲得選中的商品objs # 遍歷結算商品: for sku_id in selected_sku_id_list: while True: sku = SKU.objects.get(id=sku_id) # 要購買的商品的數量 count = redis_dict[sku.id][0] # 判斷庫存量和銷售量 origin_stock = sku.stock origin_sales = sku.sales # 判斷商品庫存是否充足 if count > origin_stock: transaction.savepoint_rollback(save_id) raise serializers.ValidationError({"庫存不足"}) # 更新庫存和銷量信息 new_stock = origin_stock - count new_sales = origin_sales + count # sku.stock = new_stock # sku.sales = new_sales # 返回受影響的行數 ret = SKU.objects.filter(id=sku.id, stock=origin_stock).update(stock=new_stock, sales=new_sales) if ret == 0: continue # 計算order——info中的兩個冗餘字段結果並賦值 order.total_count += count order.total_amount += (sku.price * count) OrderGoods.objects.create(**{ "order": order, "sku": sku, "count": count, "price": sku.price }) break order.save() except serializers.ValidationError: raise except Exception: transaction.savepoint_rollback(save_id) raise # 提交事務 transaction.savepoint_commit(save_id) # 清除購物車中已經結算的商品 redis_conn.delete('cart2_%s' % user.id) return order 
  • 數據庫的事物
    • Django中對於數據庫的操做,默認在每一次的數據庫操做以後都會自動提交
    • 在Django中能夠經過django.db.transaction模塊提供的atomic來定義一個事務,atomic提供兩種用法
  • 使用方法一:
from django.db import transaction @transaction.atomic def viewfunc(request): # 這些代碼會在一個事務中執行 ... 
  • 使用方法二
from django.db import transaction # 建立保存點 save_id = transaction.savepoint() # 回滾到保存點 transaction.savepoint_rollback(save_id) # 提交從保存點到當前狀態的全部數據庫事務操做 transaction.savepoint_commit(save_id)

支付模塊:

 

 

 

import os from alipay import AliPay from django.conf import settings from django.shortcuts import render # Create your views here. from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from orders.models import OrderInfo from payment.models import Payment class PaymentView(APIView): permission_classes = [IsAuthenticated] def get(self, request, order_id): user = request.user # 校驗訂單order_id try: order = OrderInfo.objects.get(order_id=order_id, user=user, status=OrderInfo.ORDER_STATUS_ENUM["UNPAID"]) except OrderInfo.DoesNotExist: return Response({"message": "訂單信息有誤"}, status=status.HTTP_400_BAD_REQUEST) # 根據訂單的數據,向支付寶發起請求,獲取支付連接參數 alipay_client = AliPay( appid=settings.ALIPAY_APPID, app_notify_url=None, # 默認回調url app_private_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/app_private_key.pem"), alipay_public_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/alipay_public_key.pem"), # 支付寶的公鑰,驗證支付寶回傳消息使用,不是你本身的公鑰, sign_type="RSA2", # RSA 或者 RSA2 debug=settings.ALIPAY_DEBUG # 默認False ) order_string = alipay_client.api_alipay_trade_page_pay( out_trade_no=order_id, total_amount=str(order.total_amount), subject="美多商城%s" % order_id, return_url="http://www.meiduo.site:8080/pay_success.html", ) alipay_url = settings.ALIPAY_GATEWAY_URL + '?' + order_string # 須要跳轉到https://openapi.alipay.com/gateway.do? + order_string # 拼接連接返回前端 return Response({'alipay_url': alipay_url}, status=status.HTTP_201_CREATED) class PaymentStatusView(APIView): """ 修改支付結果狀態 """ def put(self, request): # 取出請求的參數 query_dict = request.query_params # 將django中的QueryDict 轉換python的字典 alipay_data_dict = query_dict.dict() sign = alipay_data_dict.pop('sign') # 校驗請求參數是不是支付寶的 alipay_client = AliPay( appid=settings.ALIPAY_APPID, app_notify_url=None, # 默認回調url app_private_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/app_private_key.pem"), alipay_public_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/alipay_public_key.pem"), # 支付寶的公鑰,驗證支付寶回傳消息使用,不是你本身的公鑰, sign_type="RSA2", # RSA 或者 RSA2 debug=settings.ALIPAY_DEBUG # 默認False ) success = alipay_client.verify(alipay_data_dict, sign) if success: order_id = alipay_data_dict.get('out_trade_no') trade_id = alipay_data_dict.get('trade_no') # 支付寶交易流水號 # 保存支付數據 # 修改訂單數據 Payment.objects.create( order_id=order_id, trade_id=trade_id ) OrderInfo.objects.filter(order_id=order_id, status=OrderInfo.ORDER_STATUS_ENUM['UNPAID']).update( status=OrderInfo.ORDER_STATUS_ENUM['UNSEND']) return Response({'trade_id': trade_id}) else: # 參數據不是支付寶的,是非法請求 return Response({'message': '非法請求'}, status=status.HTTP_403_FORBIDDEN) 
  • 數據加密的過程

    • 在雙方進行通訊的過程當中,若A要給B發送消息
    • 互相交換公鑰密碼
    • 在數據包的發送過程當中,A先用本身的私鑰加密(保證數據的安全的,至少不會明文顯示)。
    • 再用B交給A的公鑰進行加密(只有B有本身的私鑰才能夠打開最外層的包裹信息)
      enter description here
相關文章
相關標籤/搜索