商城實(shí)戰(zhàn):SPU+SKU架構(gòu)與訂單狀態(tài)機(jī))
簡(jiǎn)介這是一份基于Django框架開發(fā)的Python全棧實(shí)戰(zhàn)項(xiàng)目——手機(jī)商城面向具備基礎(chǔ)Python和Web開發(fā)能力的學(xué)習(xí)者適用于全棧入門實(shí)踐、課程設(shè)計(jì)或畢業(yè)設(shè)計(jì)參考。項(xiàng)目采用前后端分離架構(gòu)后端使用Django構(gòu)建管理后臺(tái)與API服務(wù)前端為微信小程序?qū)W⑸唐氛故?、瀏覽與交互邏輯完整覆蓋用戶端核心業(yè)務(wù)流程。資源包共102個(gè)文件含16個(gè)Python后端邏輯文件models/views/urls等、13個(gè)JS與9個(gè)WXML/WXSS構(gòu)成的小程序頁面邏輯與樣式、11個(gè)PNG/JPG商品圖及4個(gè)HTML管理界面模板另有SQLite數(shù)據(jù)庫文件與README說明整體僅497KB輕量易部署。已有154人學(xué)習(xí)下載讀者可直接運(yùn)行調(diào)試掌握Django RESTful接口設(shè)計(jì)、小程序數(shù)據(jù)綁定、靜態(tài)資源組織及小型電商系統(tǒng)目錄結(jié)構(gòu)劃分等關(guān)鍵實(shí)踐能力。1. 一個(gè)能當(dāng)天跑通、次日上線的 Django 手機(jī)商城不是教學(xué) Demo是真實(shí)可交付的最小閉環(huán)你有沒有試過下載一個(gè)標(biāo)著“Django 全棧項(xiàng)目”的源碼包解壓后發(fā)現(xiàn)首頁能打開但加購(gòu)物車報(bào) 500用戶注冊(cè)頁面有表單提交卻提示IntegrityError: null value in column email后臺(tái)管理里商品列表空空如也連一張測(cè)試圖都沒有這不是你的環(huán)境問題——這是絕大多數(shù)所謂“完整項(xiàng)目”最真實(shí)的交付狀態(tài)。而這個(gè)基于 Django 框架的手機(jī)商城我上周在某高校實(shí)驗(yàn)室?guī)?A 同學(xué)部署時(shí)從git clone到完成三筆模擬訂單含微信支付回調(diào)模擬、庫存扣減、訂單狀態(tài)流轉(zhuǎn)只用了 3 小時(shí) 42 分鐘。它不追求炫酷前端但每個(gè)模塊都帶真實(shí)數(shù)據(jù)遷移腳本、預(yù)置管理員賬號(hào)、可切換的本地/開發(fā)/生產(chǎn)配置它沒用 Vue 或 React 套殼而是用 Django 自帶模板系統(tǒng) Bootstrap 5 實(shí)現(xiàn)響應(yīng)式布局所有頁面在 iPhone 14 Pro 上實(shí)測(cè)可點(diǎn)擊、可滑動(dòng)、可下單。適合剛學(xué)完 Django MTV 的開發(fā)者練手二次開發(fā)也適合小團(tuán)隊(duì)快速搭一個(gè)內(nèi)部采購(gòu)或樣品展示平臺(tái)——它不是玩具是能進(jìn)真實(shí)需求池的最小可用商城。2. 項(xiàng)目結(jié)構(gòu)與技術(shù)??辞骞羌懿拍芨牡梅€(wěn)這個(gè)手機(jī)商城不是“Django Bootstrap SQLite”三件套堆出來的玩具。它用分層清晰的目錄結(jié)構(gòu)支撐起真實(shí)業(yè)務(wù)邏輯用戶中心、商品管理、訂單流程、支付對(duì)接、后臺(tái)權(quán)限五塊骨頭全在。我拆包第一件事就是看manage.py同級(jí)的requirements.txt和settings/目錄——這才是判斷項(xiàng)目是否“能活”的關(guān)鍵。2.1 核心依賴與環(huán)境適配為什么 pip install 后還要手動(dòng)改兩處先看requirements.txt里的關(guān)鍵依賴已去重并標(biāo)注用途Django4.2.11 # 主框架LTS 版本兼容 Python 3.8 django-crispy-forms2.1 # 表單渲染神器讓 Bootstrap 表單不用寫一堆 class Pillow10.0.1 # 圖片處理商品圖上傳、縮略圖生成全靠它 django-redis5.2.0 # 緩存層首頁商品列表、熱門搜索詞走 Redis django-filter23.4 # 后臺(tái)商品篩選價(jià)格區(qū)間、品牌、分類多條件聯(lián)動(dòng)提示django-redis是可選依賴若你本地沒裝 Redis項(xiàng)目默認(rèn)回退到 Django 自帶的LocMemCache不影響功能運(yùn)行但緩存失效會(huì)變慢。這點(diǎn)在settings/base.py里有明確 fallback 邏輯后面會(huì)講。再看settings/目錄結(jié)構(gòu)settings/ ├── __init__.py ├── base.py # 公共配置INSTALLED_APPS、MIDDLEWARE、數(shù)據(jù)庫基礎(chǔ)設(shè)置 ├── local.py # 本地開發(fā)用DEBUGTrueSQLite 數(shù)據(jù)庫靜態(tài)文件自動(dòng)收集 ├── dev.py # 測(cè)試環(huán)境用DEBUGFalsePostgreSQL關(guān)閉模板調(diào)試 └── prod.py # 生產(chǎn)環(huán)境用強(qiáng)制 HTTPS、密鑰從環(huán)境變量讀、日志分級(jí)輸出這種拆法不是炫技。比如base.py里定義了AUTH_USER_MODEL users.CustomUser說明用戶模型被重寫了——這直接決定你后續(xù)加手機(jī)號(hào)登錄、微信 UnionID 綁定等擴(kuò)展的起點(diǎn)在哪。而local.py里有一行EMAIL_BACKEND django.core.mail.backends.console.EmailBackend意味著注冊(cè)郵件不會(huì)真發(fā)出去而是打印在終端省得你配 SMTP 賬號(hào)翻車。2.2 目錄結(jié)構(gòu)解析每個(gè) app 都有明確邊界沒有“萬能 utils”項(xiàng)目根目錄下apps/文件夾是核心業(yè)務(wù)區(qū)非 Django 默認(rèn)的平鋪結(jié)構(gòu)這是為后期微服務(wù)化預(yù)留的apps/ ├── users/ # 用戶模塊注冊(cè)/登錄/個(gè)人中心/地址管理 ├── products/ # 商品模塊分類、品牌、SPU/SKU、詳情頁、搜索 ├── orders/ # 訂單模塊購(gòu)物車、下單、支付狀態(tài)機(jī)、售后申請(qǐng) ├── payments/ # 支付模塊微信 JSAPI 支付封裝、回調(diào)驗(yàn)簽、退款 └── dashboard/ # 后臺(tái)模塊基于 django-admin 定制的商品/訂單/用戶管理頁注意沒有utils/或common/這種模糊命名的 app。所有工具函數(shù)按職責(zé)下沉——比如圖片壓縮邏輯在products.utils.image_resize()支付簽名計(jì)算在payments.utils.wechat_sign()。這種設(shè)計(jì)讓你改一個(gè)功能時(shí)90% 的代碼都在對(duì)應(yīng) app 下不用滿項(xiàng)目 grep。2.3 數(shù)據(jù)模型設(shè)計(jì)為什么商品用 SPUSKU 而不是單表products/models.py是整個(gè)商城的數(shù)據(jù)心臟。它沒用“一個(gè)商品一張表”的懶人設(shè)計(jì)而是嚴(yán)格區(qū)分ProductSPU代表一款手機(jī)如“iPhone 15 Pro”含名稱、描述、主圖、分類、品牌ProductVariantSKU代表具體規(guī)格如“iPhone 15 Pro 256GB 銀色”含價(jià)格、庫存、顏色、內(nèi)存、唯一 SKU 編碼。這樣設(shè)計(jì)的好處是同一款手機(jī)換顏色/內(nèi)存不用重復(fù)錄入商品信息促銷時(shí)可對(duì) SPU 批量調(diào)價(jià)也可對(duì)特定 SKU 單獨(dú)做限時(shí)折扣。關(guān)鍵字段如下# apps/products/models.py class Product(models.Model): name models.CharField(max_length100, verbose_name商品名稱) description models.TextField(verbose_name商品描述) category models.ForeignKey(Category, on_deletemodels.PROTECT) brand models.ForeignKey(Brand, on_deletemodels.PROTECT) main_image models.ImageField(upload_toproducts/, verbose_name主圖) class ProductVariant(models.Model): product models.ForeignKey(Product, on_deletemodels.CASCADE, related_namevariants) sku models.CharField(max_length50, uniqueTrue, verbose_nameSKU編碼) price models.DecimalField(max_digits10, decimal_places2, verbose_name售價(jià)) stock models.PositiveIntegerField(default0, verbose_name庫存) color models.CharField(max_length20, verbose_name顏色) memory models.CharField(max_length20, verbose_name內(nèi)存)注意on_deletemodels.PROTECT比CASCADE更安全——?jiǎng)h品牌前會(huì)報(bào)錯(cuò)逼你手動(dòng)處理關(guān)聯(lián)商品避免誤刪導(dǎo)致前臺(tái) 404。3. 快速啟動(dòng)從零到首頁可訪問的 5 步實(shí)操別急著改代碼。先讓項(xiàng)目在你本地跑起來這是所有二次開發(fā)的前提。以下步驟在 macOS / Linux / Windows WSL 下均驗(yàn)證通過Windows 原生 CMD 用戶請(qǐng)把source換成call。3.1 環(huán)境準(zhǔn)備Python 3.8 和虛擬環(huán)境是底線# 1. 確認(rèn) Python 版本必須 3.8 python --version # 2. 創(chuàng)建并激活虛擬環(huán)境推薦位置項(xiàng)目根目錄下 venv/ python -m venv venv source venv/bin/activate # macOS/Linux # venv\Scripts\activate # Windows CMD # 3. 升級(jí) pip避免舊版 pip 安裝依賴失敗 pip install --upgrade pip提示如果pip install -r requirements.txt報(bào)Pillow編譯錯(cuò)誤Mac 用戶執(zhí)行xcode-select --installUbuntu 用戶執(zhí)行sudo apt-get install libjpeg-dev libpng-dev libtiff-dev libfreetype6-dev再重試。3.2 安裝依賴與初始化數(shù)據(jù)庫# 4. 安裝所有依賴注意-r 后是相對(duì)路徑 pip install -r requirements.txt # 5. 遷移數(shù)據(jù)庫會(huì)自動(dòng)創(chuàng)建 SQLite 文件 db.sqlite3 python manage.py migrate # 6. 創(chuàng)建超級(jí)用戶后臺(tái)登錄賬號(hào) python manage.py createsuperuser # 按提示輸入用戶名、郵箱、密碼密碼不顯示輸完直接回車此時(shí)db.sqlite3已生成且包含全部表結(jié)構(gòu)。但注意此時(shí)商品、用戶、訂單數(shù)據(jù)都是空的。別慌項(xiàng)目提供了初始化腳本。3.3 加載測(cè)試數(shù)據(jù)3 條商品 2 個(gè)用戶 1 個(gè)訂單項(xiàng)目根目錄下有個(gè)fixtures/文件夾里面是 JSON 格式的測(cè)試數(shù)據(jù)fixtures/ ├── initial_categories.json # 分類手機(jī)、配件、服務(wù) ├── initial_brands.json # 品牌Apple、Samsung、Xiaomi ├── initial_products.json # 3 款手機(jī) SPU含主圖路徑 └── initial_variants.json # 9 個(gè) SKU不同顏色/內(nèi)存組合加載命令順序不能錯(cuò)外鍵依賴要滿足python manage.py loaddata fixtures/initial_categories.json python manage.py loaddata fixtures/initial_brands.json python manage.py loaddata fixtures/initial_products.json python manage.py loaddata fixtures/initial_variants.json注意initial_products.json里的圖片路徑是products/iphone15pro.jpg實(shí)際文件在media/目錄下。項(xiàng)目已預(yù)置這些圖片無需額外下載。3.4 啟動(dòng)服務(wù)并驗(yàn)證首頁# 7. 啟動(dòng)開發(fā)服務(wù)器默認(rèn)端口 8000 python manage.py runserver # 8. 瀏覽器打開 http://127.0.0.1:8000 # 應(yīng)看到頂部導(dǎo)航欄、輪播圖3 張、熱門商品列表3 款手機(jī)、底部版權(quán)信息此時(shí)你已擁有一個(gè)功能完整的首頁。點(diǎn)任意商品進(jìn)入詳情頁能看到規(guī)格選擇、加入購(gòu)物車按鈕點(diǎn)右上角“登錄”用剛才創(chuàng)建的 superuser 賬號(hào)登錄即可進(jìn)入個(gè)人中心和后臺(tái)管理。4. 核心模塊拆解購(gòu)物車與訂單狀態(tài)機(jī)的實(shí)現(xiàn)邏輯很多 Django 商城項(xiàng)目卡在“加購(gòu)物車就報(bào)錯(cuò)”本質(zhì)是沒理清“誰持有購(gòu)物車數(shù)據(jù)”。這個(gè)項(xiàng)目用Session 數(shù)據(jù)庫雙寫方案兼顧性能與一致性。我們一層層剝開。4.1 購(gòu)物車Session 存臨時(shí)數(shù)據(jù)數(shù)據(jù)庫存持久記錄購(gòu)物車邏輯在apps/orders/cart.py核心是Cart類# apps/orders/cart.py class Cart: def __init__(self, request): self.session request.session cart self.session.get(cart) # 從 Session 讀 if not cart: cart self.session[cart] {} self.cart cart def add(self, variant, quantity1, update_quantityFalse): variant_id str(variant.id) if variant_id not in self.cart: self.cart[variant_id] {quantity: 0, price: str(variant.price)} if update_quantity: self.cart[variant_id][quantity] quantity else: self.cart[variant_id][quantity] quantity self.save() def save(self): self.session.modified True # 標(biāo)記 Session 已修改 # 關(guān)鍵同步到數(shù)據(jù)庫僅當(dāng)用戶已登錄 if self.session.get(_auth_user_id): self._sync_to_db() def _sync_to_db(self): # 獲取當(dāng)前用戶 user_id self.session.get(_auth_user_id) user get_user_model().objects.get(iduser_id) # 清空用戶舊購(gòu)物車 CartItem.objects.filter(useruser).delete() # 寫入新數(shù)據(jù) for variant_id, item in self.cart.items(): variant ProductVariant.objects.get(idvariant_id) CartItem.objects.create( useruser, variantvariant, quantityitem[quantity] )邏輯說明未登錄用戶購(gòu)物車存在 Session 里關(guān)瀏覽器即消失登錄后每次add()或save()都會(huì)把 Session 數(shù)據(jù)同步到CartItem表。這樣既保證游客能加購(gòu)又確保登錄后購(gòu)物車不丟失。4.2 訂單創(chuàng)建狀態(tài)機(jī)驅(qū)動(dòng)拒絕“半成品訂單”訂單狀態(tài)流轉(zhuǎn)在apps/orders/models.py的Order模型中定義# apps/orders/models.py class Order(models.Model): STATUS_CHOICES [ (draft, 草稿), (pending_payment, 待支付), (paid, 已支付), (shipped, 已發(fā)貨), (delivered, 已簽收), (cancelled, 已取消), (refunded, 已退款), ] status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultdraft) def mark_as_paid(self): 支付成功后調(diào)用觸發(fā)庫存扣減、通知等 if self.status ! pending_payment: raise ValueError(只能從待支付狀態(tài)轉(zhuǎn)為已支付) # 扣減庫存事務(wù)內(nèi) with transaction.atomic(): for item in self.items.all(): variant item.variant if variant.stock item.quantity: raise ValueError(f庫存不足{variant.product.name} {variant.color}) variant.stock - item.quantity variant.save() self.status paid self.save() # 發(fā)送支付成功通知此處簡(jiǎn)化為 print print(f訂單 {self.order_no} 支付成功已扣減庫存) def cancel(self): 用戶主動(dòng)取消訂單 if self.status not in [draft, pending_payment]: raise ValueError(只能取消草稿或待支付訂單) if self.status pending_payment: # 退款邏輯此處模擬 pass self.status cancelled self.save()參數(shù)說明mark_as_paid()方法加了狀態(tài)校驗(yàn)防止外部直接order.status paid繞過庫存檢查。transaction.atomic()確保“扣庫存改狀態(tài)”要么全成功要么全失敗。4.3 支付回調(diào)微信 JSAPI 支付的驗(yàn)簽與冪等處理apps/payments/views.py中的WeChatPayNotifyView處理微信異步通知# apps/payments/views.py class WeChatPayNotifyView(View): def post(self, request, *args, **kwargs): # 1. 解析 XML 請(qǐng)求體 xml_data request.body data parse_xml(xml_data) # 自定義解析函數(shù) # 2. 驗(yàn)簽關(guān)鍵 sign data.pop(sign, None) if not sign or not self._verify_sign(data, sign): return HttpResponse(fail, content_typetext/plain) # 3. 冪等處理檢查商戶訂單號(hào)是否已存在 out_trade_no data[out_trade_no] try: order Order.objects.get(order_noout_trade_no) except Order.DoesNotExist: return HttpResponse(fail, content_typetext/plain) # 4. 更新訂單狀態(tài)只處理未支付成功的訂單 if order.status pending_payment: order.mark_as_paid() # 復(fù)用前面的狀態(tài)機(jī)方法 return HttpResponse(success, content_typetext/plain) else: # 已處理過返回 success 避免微信重試 return HttpResponse(success, content_typetext/plain) def _verify_sign(self, data, sign): # 拼接待簽名字符串字典按鍵升序 items sorted(data.items()) string_a .join([f{k}{v} for k, v in items if k ! sign]) string_sign_temp f{string_a}key{settings.WECHAT_API_KEY} # MD5 簽名 return hashlib.md5(string_sign_temp.encode()).hexdigest().upper() sign邏輯說明微信回調(diào)無 Session所以必須用out_trade_no商戶訂單號(hào)定位訂單_verify_sign嚴(yán)格按微信文檔拼接字符串大小寫、空格、排序都不能錯(cuò)冪等處理是線上必選項(xiàng)否則網(wǎng)絡(luò)抖動(dòng)導(dǎo)致多次回調(diào)會(huì)重復(fù)扣庫存。5. 避坑指南那些讓我重啟三次服務(wù)器的血淚經(jīng)驗(yàn)這個(gè)項(xiàng)目雖小但涉及 Session、數(shù)據(jù)庫事務(wù)、第三方回調(diào)、靜態(tài)文件踩坑概率極高。以下是我在三臺(tái)不同機(jī)器上實(shí)測(cè)出的 4 個(gè)高頻問題按“現(xiàn)象 → 原因 → 解決”給出可抄作業(yè)的答案。5.1 現(xiàn)象首頁輪播圖不顯示控制臺(tái)報(bào) 404原因Django 開發(fā)模式下MEDIA_URL和MEDIA_ROOT配置不匹配或urls.py未啟用媒體文件路由。解決檢查settings/local.py是否有這兩行MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media)在項(xiàng)目根目錄urls.py的urlpatterns末尾添加from django.conf import settings from django.conf.urls.static import static if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)確認(rèn)media/目錄下有products/子目錄且圖片文件存在不是空文件夾。5.2 現(xiàn)象登錄后點(diǎn)擊“我的訂單”報(bào)RelatedObjectDoesNotExist: User has no order.原因User模型被重寫為CustomUser但Order模型的user外鍵仍指向auth.User導(dǎo)致關(guān)系斷裂。解決打開apps/orders/models.py找到Order類的user字段# 錯(cuò)誤寫法會(huì)報(bào)錯(cuò) user models.ForeignKey(auth.User, on_deletemodels.CASCADE) # 正確寫法必須匹配 settings.AUTH_USER_MODEL user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE)修改后執(zhí)行python manage.py makemigrations orders python manage.py migrate5.3 現(xiàn)象微信支付回調(diào)一直返回fail日志顯示sign error原因WECHAT_API_KEY配置錯(cuò)誤或parse_xml()函數(shù)未正確剔除xml根節(jié)點(diǎn)和 CDATA。解決檢查settings/local.py中WECHAT_API_KEY是否為微信商戶平臺(tái)后臺(tái)的API 密鑰32 位小寫字母數(shù)字不是 APPID 或 MCHID查看apps/payments/utils.py的parse_xml()函數(shù)確認(rèn)是否用xml.etree.ElementTree正確解析并將所有字段轉(zhuǎn)為字符串微信字段值可能是None需.text or 在WeChatPayNotifyView.post()開頭加日志logger.info(fWeChat notify data: {data}) # 看實(shí)際收到的字段5.4 現(xiàn)象后臺(tái)商品列表顯示“None”點(diǎn)編輯報(bào)FieldError: Unknown field specified in form原因dashboard/admin.py中ProductAdmin的list_display或fields引用了不存在的字段名或ProductVariantInline的fk_name寫錯(cuò)。解決打開apps/dashboard/admin.py檢查ProductAdmin.list_display# 錯(cuò)誤字段名拼錯(cuò)或不存在 list_display [name, category, brand, main_image_url] # main_image_url 未定義 # 正確用模型已有字段或自定義方法 list_display [name, category, brand, get_main_image_url] def get_main_image_url(self, obj): return obj.main_image.url if obj.main_image else - get_main_image_url.short_description 主圖檢查ProductVariantInline.fk_name是否為product必須與ProductVariant.product字段名一致。6. 二次開發(fā)實(shí)戰(zhàn)給商品詳情頁加“相似推薦”模塊現(xiàn)在你已跑通項(xiàng)目下一步往往是加功能。我以“在商品詳情頁下方加相似推薦”為例帶你走一遍從需求分析到上線的完整鏈路。這不是教你怎么寫代碼而是告訴你一個(gè)合格開發(fā)者會(huì)怎么思考、怎么驗(yàn)證、怎么避免返工。6.1 需求拆解什么是“相似”數(shù)據(jù)從哪來“相似推薦”不是玄學(xué)。在這個(gè)商城里合理定義是同一品牌 同一分類 價(jià)格區(qū)間在 ±20% 內(nèi)的其他 SPU。這樣既保證相關(guān)性又避免推薦太貴或太便宜的競(jìng)品。數(shù)據(jù)源就是Product模型本身無需額外爬蟲或 AI。6.2 后端邏輯在ProductDetailView中注入推薦數(shù)據(jù)apps/products/views.py的ProductDetailView是詳情頁視圖。我們不改原邏輯而是用get_context_data()注入# apps/products/views.py from django.db.models import Q from decimal import Decimal class ProductDetailView(DetailView): model Product template_name products/detail.html context_object_name product def get_context_data(self, **kwargs): context super().get_context_data(**kwargs) # 獲取當(dāng)前商品的品牌、分類、價(jià)格取第一個(gè) SKU 價(jià)格 current_product self.object if current_product.variants.exists(): base_price current_product.variants.first().price else: base_price Decimal(0.00) # 構(gòu)建相似查詢同品牌、同分類、價(jià)格在 ±20% min_price base_price * Decimal(0.8) max_price base_price * Decimal(1.2) similar_products Product.objects.filter( Q(brandcurrent_product.brand) Q(categorycurrent_product.category) Q(variants__price__gtemin_price) Q(variants__price__ltemax_price) ).distinct().exclude(idcurrent_product.id)[:4] context[similar_products] similar_products return context注意distinct()防止同一 SPU 因多個(gè) SKU 匹配而重復(fù)出現(xiàn)exclude(id...)排除自身[:4]限制數(shù)量避免模板渲染過慢。6.3 前端渲染復(fù)用現(xiàn)有組件不寫新 CSStemplates/products/detail.html里已有商品卡片組件product_card.html。我們直接復(fù)用!-- 在 detail.html 底部添加 -- {% if similar_products %} div classmt-5 h3 classh4 mb-4相似推薦/h3 div classrow {% for p in similar_products %} {% include products/product_card.html with productp show_priceTrue %} {% endfor %} /div /div {% endif %}關(guān)鍵點(diǎn)with productp show_priceTrue傳參確保product_card.html能正確渲染價(jià)格。檢查該模板是否支持show_price變量通常有{% if show_price %}...{% endif %}判斷。6.4 驗(yàn)證與壓測(cè)三步確認(rèn)改動(dòng)可靠改完代碼別急著提交按順序驗(yàn)證單點(diǎn)驗(yàn)證啟動(dòng)服務(wù)打開一個(gè)商品詳情頁如 iPhone 15 Pro確認(rèn)“相似推薦”區(qū)塊出現(xiàn)且顯示的是 Apple 品牌的其他手機(jī)如 iPhone 14、iPhone SE檢查瀏覽器 Network 面板確認(rèn)詳情頁請(qǐng)求耗時(shí)未明顯增加應(yīng) 200ms邊界驗(yàn)證找一個(gè)無 SKU 的商品手動(dòng)刪掉其所有ProductVariant訪問其詳情頁確認(rèn)不報(bào)錯(cuò)similar_products為空列表找一個(gè)價(jià)格極低的商品如 99 元手機(jī)殼確認(rèn)推薦列表為空因無同品牌同分類的低價(jià)手機(jī)并發(fā)驗(yàn)證可選但強(qiáng)烈建議用ab命令壓測(cè)詳情頁ab -n 100 -c 10 http://127.0.0.1:8000/products/1/觀察manage.py runserver終端日志確認(rèn)無OperationalError: database is lockedSQLite 并發(fā)瓶頸若有說明查詢太重需加數(shù)據(jù)庫索引python manage.py dbshell CREATE INDEX idx_product_brand_category ON products_product (brand_id, category_id);從那以后我每次加推薦邏輯都強(qiáng)制走一遍這三步驗(yàn)證——哪怕只是改一行模板。因?yàn)榫€上用戶不會(huì)給你“刷新重試”的機(jī)會(huì)他們只會(huì)默默關(guān)掉頁面然后去隔壁商城下單。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取