以下轉載於https://www.cnblogs.com/cerofang/p/9457875.html 僅供本人學習和研究
商城商業模式:
C2B模式(消費者到企業的商業模式),相類似網站包括:京東,淘寶,海爾商城,尚品宅配等。
商城需求分析
1,用戶部分
2,商品部分
3,購物車部分
4,商品訂單備份
5,用戶支付部分
6,上線程序的配置
用戶部分模塊:
基本功能:用戶注冊,登錄,密碼的重置,第三方登錄
用戶注冊
1,圖片驗證碼
流程分析:
1,前端生成uuid隨機字符串
2,后端生成圖片驗證碼發送給前端,將圖形驗證碼的存入到redis中
2,短信驗證碼
1,檢查圖片的驗證碼
2,檢驗是否是在60s內是否已經發送過
3,生成短信驗證碼
4,保存發送的短信驗證碼
5,發送短信(第三方平台發送:雲通訊)
3,判斷用戶名是否存在
1,用戶輸入用戶名之后ajax局部刷新頁面
2,后台查詢數據庫用戶是否存在
3,返回數據給前端
4,判斷手機號碼是否已經存在
同3
技術點:前后端的域名不相同,涉及到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的使用目的
- http協議本生是一種無狀態的協議,假如用戶每次發送請求過來將用戶名和密碼在后端進行驗證后才可以登錄到某些界面才可以進行操作,當客戶端再次請求服務器時,又要重新進行認證;
- 解決方法:在客戶端設置cookie並在本地設置session存儲用戶的敏感信息,從而來記錄當前用戶登錄的狀態,如果用戶量再次請求服務器,將cookie帶給服務器,服務器查詢session獲取用戶信息,進行下一步操作。
- 客戶端比較多的情況下,seession中默認會存在內存,隨着用戶量的增加服務器的壓力就會變大;
-
傳統的cookies顯示出來的問題
- 在現在的市場各種不同的瀏覽器,對同一個網站進行,用戶的每種設備都需要維護相關的session在服務器端,會造成服務器資源的浪費,相關網站采取單點登錄來記錄用戶的狀態狀態來解決以上傳統cookies帶來的問題 -
單點登錄的概念
- 用戶在不同的設備中進行登錄,服務器端不用維護用戶的相關信息,在每次登錄的過程中都由客戶端將自己的用戶信息發送過來,服務端進行解析來獲取用戶的相關信息
-
token認證的機制
- 用戶攜帶用戶名和密碼來后端進行驗證
- 服務器端驗證通過后對為當前用戶生成token
- 將token返回給前端,記錄用戶的信息
- 用戶再次請求服務器的時候,服務端解析token相關的信息,驗證用戶
- 確定用戶狀態,進行 相關操作
-
備注:
- jwt的組成第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload, 類似於飛機上承載的物品),第三部分是簽證(signature).
- secretkey是存儲在服務器端的,如果secret key
-
用戶登錄JWT認證的的流程源代碼(待繼續理解)
-
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有自己的私鑰才可以打開最外層的包裹信息)