作:校園閑置物品換購平臺(tái)實(shí)戰(zhàn)詳解)
看到基于flask-django這種題目的時(shí)候不少同學(xué)第一反應(yīng)是:這倆框架是不是得二選一?或者怕寫成用Django做了個(gè)主頁、用Flask做了個(gè)登錄頁的縫合怪。其實(shí)這類設(shè)計(jì)題目真正想考察的是你能不能理解兩個(gè)Web框架各自的能力邊界并且用一套合理的架構(gòu)讓它們協(xié)作完成一個(gè)完整業(yè)務(wù)。這個(gè)校園個(gè)人閑置物品換購平臺(tái)我當(dāng)初從需求分析到部署上線完整走了一遍坑踩了不少但做完之后對Python Web開發(fā)的理解確實(shí)上了一個(gè)臺(tái)階。這篇文章我會(huì)把整套設(shè)計(jì)思路、核心數(shù)據(jù)表結(jié)構(gòu)、雙框架協(xié)作方式、關(guān)鍵代碼實(shí)現(xiàn)以及部署階段遇到的真實(shí)問題全部梳理出來。不管你是要做課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)還是單純想練手兩個(gè)主流Python框架這篇都值得看完。1. 雙框架并存的核心思路:主業(yè)務(wù)收斂、輔助功能外放先直接說結(jié)論:一個(gè)項(xiàng)目里同時(shí)出現(xiàn)Flask和Django絕不是讓它們做重復(fù)的事而是讓每個(gè)框架去做自己最擅長的事。1.1 Django和Flask的能力邊界Django自帶ORM、Admin后臺(tái)、用戶認(rèn)證體系、表單處理、CSRF防護(hù)這種全家桶特性非常適合承載業(yè)務(wù)主鏈路。對于校園閑置物品換購平臺(tái)來說用戶注冊登錄、物品發(fā)布、訂單交易、后臺(tái)審核這些核心模塊用Django來做能省掉大量重復(fù)造輪子的工作而且數(shù)據(jù)模型和數(shù)據(jù)庫遷移都通過Django ORM統(tǒng)一管理代碼維護(hù)成本低。Flask則是一個(gè)輕量級框架它不替你做任何決定但勝在靈活、啟動(dòng)快、寫小服務(wù)非常順手。在這個(gè)項(xiàng)目里我把推薦接口、數(shù)據(jù)統(tǒng)計(jì)聚合、圖片壓縮處理這類相對獨(dú)立的輔助功能放到了Flask側(cè)。這類功能的特點(diǎn)是:邏輯相對獨(dú)立、調(diào)用頻率可能很高、不影響主交易流程即使某個(gè)接口掛了也不能拖垮用戶下單。1.2 雙框架協(xié)作的三種常見模式我見過不少團(tuán)隊(duì)做這類雙框架項(xiàng)目拆法大致有三種:協(xié)作模式實(shí)現(xiàn)方式適用場景優(yōu)缺點(diǎn)方式A:共享數(shù)據(jù)庫獨(dú)立服務(wù)Django跑主站Flask跑API子服務(wù)兩者連同一個(gè)MySQL通過HTTP接口互相調(diào)用課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)也是本文采用的方案架構(gòu)清晰、容易演示但要注意兩個(gè)框架訪問同一批表時(shí)的模型一致性問題方式B:主進(jìn)程嵌入用werkzeug的DispatcherMiddleware把Flask應(yīng)用作為Django的子應(yīng)用掛載只想演示同時(shí)使用了兩個(gè)框架時(shí)用來交差部署簡單但兩個(gè)框架的session、中間件會(huì)互相干擾后期難維護(hù)方式C:完全微服務(wù)化各自獨(dú)立數(shù)據(jù)庫通過REST API松耦合通信分布式結(jié)課項(xiàng)目演示效果好但數(shù)據(jù)一致性難保證工作量也大我最終選了方式A。理由很現(xiàn)實(shí):課程設(shè)計(jì)和畢設(shè)的評審老師最看重的是業(yè)務(wù)閉環(huán)也就是用戶能發(fā)布物品、能下單、能完成換購。共享一個(gè)MySQL能保證交易的強(qiáng)一致性Flask只負(fù)責(zé)讀多寫少的輔助接口就算Flask掛了Django站點(diǎn)的核心功能也不受影響。這其實(shí)是真實(shí)企業(yè)里主業(yè)務(wù)收斂、輔助功能外放思想的一個(gè)簡化版答辯的時(shí)候這樣講非常加分。1.3 為什么推薦先設(shè)計(jì)架構(gòu)而不是先寫代碼很多同學(xué)拿到題目第一件事就是django-admin startproject然后就開始寫models。我的建議是反過來先花半天時(shí)間把這個(gè)平臺(tái)到底有哪些角色、哪些核心流程、哪些狀態(tài)想清楚。你可以問自己三個(gè)問題:誰是系統(tǒng)用戶?學(xué)生、管理員還可能有什么?最核心的流程是什么?發(fā)布物品、發(fā)起換購、確認(rèn)交換、互相評價(jià)。哪些功能寫進(jìn)Django哪些拆給Flask?根據(jù)依賴關(guān)系劃分而不是按頁面劃分。想清楚這三個(gè)問題后面寫代碼就是照圖施工而不是邊寫邊改。這個(gè)項(xiàng)目里我最深刻的體會(huì)就是:前期架構(gòu)多花的時(shí)間后期至少能省三倍。2. 校園換購平臺(tái)的需求本質(zhì):不是簡單二手交易閑置物品換購聽起來和閑魚、轉(zhuǎn)轉(zhuǎn)差不多但落在校園場景里需求邏輯其實(shí)有明顯區(qū)別。搞清楚這些數(shù)據(jù)庫表設(shè)計(jì)才有的放矢。2.1 校園場景的三個(gè)核心差異第一,用戶身份天然可信。注冊必須綁定學(xué)校域名郵箱或?qū)W號這意味著平臺(tái)內(nèi)交易雙方的違約成本更高不需要像閑魚那樣做復(fù)雜的芝麻信用體系。第二,交易以線下自提為主。大學(xué)生活動(dòng)范圍高度集中宿舍區(qū)、教學(xué)樓、食堂就是天然的交貨點(diǎn)。所以系統(tǒng)不需要物流模塊但要記錄交易地點(diǎn)偏好比如只限本校圖書館自提。第三,換購包含了物物交換和補(bǔ)差價(jià)。這是這個(gè)項(xiàng)目和普通二手交易平臺(tái)最大的區(qū)別。用戶發(fā)布物品時(shí)除了標(biāo)價(jià)還可以寫明想交換什么比如用一臺(tái)Kindle換一副降噪耳機(jī)可以補(bǔ)差價(jià)50元。這意味著訂單表的設(shè)計(jì)不能只有買家、賣家、金額三個(gè)字段。2.2 核心角色與功能邊界我把系統(tǒng)拆成前臺(tái)、后臺(tái)、輔助服務(wù)三塊:角色核心功能所屬框架普通學(xué)生注冊登錄、瀏覽物品、發(fā)布閑置、發(fā)起換購、留言詢問、確認(rèn)收貨、評價(jià)Django系統(tǒng)管理員審核物品、處理舉報(bào)、用戶封禁、數(shù)據(jù)統(tǒng)計(jì)Django Admin Flask統(tǒng)計(jì)接口輔助服務(wù)物品推薦、瀏覽量統(tǒng)計(jì)、圖片壓縮、Excel導(dǎo)出Flask功能邊界定了之后頁面結(jié)構(gòu)就清楚了。前臺(tái)總共沒幾個(gè)頁面:首頁(物品流)、物品詳情、發(fā)布頁、個(gè)人中心、訂單列表、站內(nèi)信。管理后臺(tái)直接用Django Admin改一改就夠用不需要額外寫。2.3 交易狀態(tài)機(jī):整個(gè)業(yè)務(wù)最核心的部分換購流程不是簡單的下單-付款-發(fā)貨-收貨因?yàn)榇嬖陔p方交換物品的情況狀態(tài)機(jī)必須多幾條分支。我自己項(xiàng)目里的狀態(tài)流轉(zhuǎn)是這樣設(shè)計(jì)的:用戶A發(fā)布物品X,狀態(tài)為在售。用戶B發(fā)起購買或換購請求物品X變?yōu)榻灰字型瑫r(shí)生成一條訂單記錄。雙方約定時(shí)間地點(diǎn)線下驗(yàn)貨。雙方都在系統(tǒng)中點(diǎn)擊確認(rèn)交換或確認(rèn)收貨訂單變?yōu)橐淹瓿伞H绻魏我环皆诮灰字谐瑫r(shí)未確認(rèn)可以申請取消訂單變?yōu)橐讶∠锲稾自動(dòng)回到在售。單獨(dú)把換購拎出來說:當(dāng)B提出我想用物品Y換你的X時(shí)系統(tǒng)要同時(shí)鎖定X和Y兩件物品直到雙方確認(rèn)完成或者取消。這個(gè)雙向鎖定邏輯容易漏寫業(yè)務(wù)代碼里一定要同時(shí)處理兩個(gè)物品的狀態(tài)。3. 數(shù)據(jù)庫設(shè)計(jì):換購業(yè)務(wù)的核心表長什么樣選型上我用了MySQL 8.0原因無他校園網(wǎng)環(huán)境里MySQL最通用后面查資料、找問題都容易。Django側(cè)用ORM管理表結(jié)構(gòu)Flask側(cè)通過SQLAlchemy讀取同一批表。3.1 用戶表與擴(kuò)展資料表Django自帶的auth_user不要直接改而是建一張profile表一對一關(guān)聯(lián)。需要存的就是學(xué)號、學(xué)校郵箱、宿舍區(qū)域、信用分、頭像路徑。class Profile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) student_id models.CharField(max_length20, uniqueTrue, verbose_name學(xué)號) campus_email models.EmailField(uniqueTrue, verbose_name校園郵箱) dorm_area models.CharField(max_length50, blankTrue, verbose_name宿舍區(qū)域) credit_score models.IntegerField(default100, verbose_name信用分) avatar models.ImageField(upload_toavatars/, blankTrue)為什么不直接在User上加字段?因?yàn)镈jango的User模型被Admin、認(rèn)證、session到處引用直接改字典字段容易在后續(xù)升級或第三方庫適配時(shí)報(bào)錯(cuò)。一對一的Profile幾乎是所有Django項(xiàng)目的標(biāo)準(zhǔn)做法。3.2 物品表:狀態(tài)和換購意向是靈魂物品表是整個(gè)平臺(tái)的流量核心字段設(shè)計(jì)直接影響后續(xù)的檢索和交易。class Item(models.Model): STATUS_CHOICES [ (on_sale, 在售), (trading, 交易中), (sold, 已售出), (off_shelf, 已下架), ] title models.CharField(max_length100) category models.ForeignKey(Category, on_deletemodels.SET_NULL, nullTrue) description models.TextField() price models.DecimalField(max_digits8, decimal_places2) want_exchange models.CharField(max_length200, blankTrue, verbose_name想換的物品) condition_level models.IntegerField(choices[(i, f{i}成新) for i in range(1, 11)]) image models.ImageField(upload_toitems/) owner models.ForeignKey(User, on_deletemodels.CASCADE, related_nameitems) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaulton_sale) view_count models.IntegerField(default0) created_at models.DateTimeField(auto_now_addTrue)這里有一個(gè)值得說的細(xì)節(jié):condition_level我用了0到10的成新度而不是幾乎全新/輕微使用痕跡這樣的模糊描述。原因很簡單:成新度便于后期用SQL做排序和篩選比如只看九成新以上的物品文本字段就沒法高效查。3.3 訂單表:換購和購買共用一張表我見過很多項(xiàng)目把購買訂單和換購訂單分成兩張表,這是個(gè)坑。因?yàn)閾Q購本質(zhì)上就是雙方互為買家和賣家的交易拆成兩張表后查詢我參與過的所有交易就變得非常痛苦。正確的做法是用一張order表通過判斷exchange_item字段是否為空來區(qū)分交易類型:字段類型說明id主鍵訂單號itemFK主物品buyerFK發(fā)起交易的人sellerFK物品發(fā)布者exchange_itemFK可空若為換購則指向買家提供的物品priceDecimal成交價(jià)或補(bǔ)差價(jià)statusCharFieldpending / confirmed / completed / cancelledcreated_at / updated_atDateTime記錄時(shí)間關(guān)鍵邏輯是:如果exchange_item不為空那么這筆訂單成交時(shí)buyer的exchange_item狀態(tài)必須變?yōu)閟old同時(shí)item狀態(tài)變?yōu)閟old。這兩個(gè)更新必須放在同一個(gè)數(shù)據(jù)庫事務(wù)里否則會(huì)出現(xiàn)A以為換成功了、B的物品還掛著賣的數(shù)據(jù)錯(cuò)亂。3.4 留言、舉報(bào)、評價(jià)表這三張表結(jié)構(gòu)都比較簡單。留言表關(guān)聯(lián)物品和用戶;舉報(bào)表關(guān)聯(lián)被舉報(bào)的物品或留言;評價(jià)表關(guān)聯(lián)訂單買家和賣家各自只能評價(jià)一次用order_id reviewer_id做聯(lián)合唯一約束。這些都屬于快速迭代的表先建起來后續(xù)有需求再加字段就行。數(shù)據(jù)庫設(shè)計(jì)這塊我的實(shí)際經(jīng)驗(yàn)是:不要想著一步到位建出完美表結(jié)構(gòu)先讓核心交易閉環(huán)跑通再根據(jù)實(shí)際頁面反饋補(bǔ)索引和字段。這個(gè)項(xiàng)目里我最開始沒給item.status加索引結(jié)果數(shù)據(jù)量到幾千條時(shí)首頁查詢明顯變慢后來才補(bǔ)上。對課程設(shè)計(jì)級別的數(shù)據(jù)量來說這一步不做也沒事但知道了對答辯有好處。4. Django主站:從注冊登錄到訂單流轉(zhuǎn)的實(shí)現(xiàn)Django側(cè)承載了平臺(tái)的全部寫操作我按用戶-物品-訂單-后臺(tái)四條線來講。4.1 校園身份驗(yàn)證:注冊時(shí)攔截非校園郵箱校園平臺(tái)最大的特色是身份可信所以注冊環(huán)節(jié)要攔住校外人。實(shí)現(xiàn)方式就是在UserCreationForm的子類里重寫clean_email強(qiáng)制郵箱后綴為學(xué)校域名。class CampusUserCreationForm(UserCreationForm): email forms.EmailField(requiredTrue) def clean_email(self): email self.cleaned_data[email].lower() if not email.endswith(your-university.edu.cn): raise forms.ValidationError(請使用校園郵箱注冊) return email這里的your-university.edu.cn替換成你自己的學(xué)校域名即可。另一個(gè)可選的驗(yàn)證是讓用戶填學(xué)號再和管理員維護(hù)的學(xué)號表比對不過課程設(shè)計(jì)階段用郵箱后綴就夠用了。4.2 物品發(fā)布的圖片處理物品發(fā)布頁用ModelForm綁定Item模型圖片上傳這塊有幾個(gè)容易踩的坑。第一個(gè)是默認(rèn)的ImageField一次只支持一張圖想支持多圖就得建一個(gè)ItemImage子表;第二個(gè)是用戶上傳的原圖動(dòng)輒幾兆直接存服務(wù)器既浪費(fèi)空間又拖慢頁面加載。我的做法是在保存時(shí)用Pillow把圖片壓縮兩份:一份是列表頁用的縮略圖(例如400x300裁剪)一份是詳情頁用的原圖(限制最大邊1600px)。上傳邏輯寫在form.save()里重寫:def save(self, commitTrue): item super().save(commitFalse) if self.cleaned_data.get(image): img Image.open(self.cleaned_data[image]) img.thumbnail((1600, 1600)) # 壓縮后保存到 MEDIA_ROOT并重新賦值路徑 if commit: item.save() return item這段代碼的核心意圖是:不要在模板里做任何圖片縮放全部在服務(wù)端生成好物理文件。Django模板層雖然也能做縮略圖但那是在每次請求時(shí)動(dòng)態(tài)處理的高并發(fā)下CPU直接被打滿。4.3 訂單狀態(tài)機(jī)的事務(wù)控制當(dāng)用戶B對物品X發(fā)起換購時(shí)要同時(shí)做三件事:創(chuàng)建訂單、把X的狀態(tài)改為交易中、把B的物品Y的狀態(tài)也改為交易中。這三個(gè)寫操作必須在一個(gè)原子事務(wù)里用Django的transaction.atomic包起來。from django.db import transaction transaction.atomic def create_exchange_order(item_x, item_y, buyer, price_diff): order Order.objects.create( itemitem_x, exchange_itemitem_y, buyerbuyer, selleritem_x.owner, priceprice_diff, statuspending, ) item_x.status trading item_x.save() item_y.status trading item_y.save() return order是否要用select_for_update()加行鎖?如果只是課程設(shè)計(jì)數(shù)據(jù)量小、并發(fā)低不加也能跑。但如果你在答辯時(shí)想展示自己對并發(fā)問題的理解這是絕佳素材。select_for_update()會(huì)在數(shù)據(jù)庫層面鎖定這兩件物品的行防止兩個(gè)用戶同時(shí)發(fā)起換購導(dǎo)致狀態(tài)錯(cuò)亂。item_x Item.objects.select_for_update().get(pkitem_x_pk) item_y Item.objects.select_for_update().get(pkitem_y_pk)這里要提醒一句:select_for_update()必須在事務(wù)內(nèi)使用也就是說調(diào)用這個(gè)查詢的函數(shù)要被transaction.atomic包裹否則鎖會(huì)在查詢后立刻釋放起不到作用。4.4 用Django Admin做管理后臺(tái)管理后臺(tái)我?guī)缀鯖]有額外寫頁面全部靠Django Admin定制。核心配置就是把Item、Order、Report注冊進(jìn)去并定制列表展示字段:admin.register(Item) class ItemAdmin(admin.ModelAdmin): list_display (title, owner, price, status, created_at) list_filter (status, category) search_fields (title, owner__username) actions [force_off_shelf] admin.action(description強(qiáng)制下架選中物品) def force_off_shelf(self, request, queryset): queryset.update(statusoff_shelf)actions自定義操作在處理舉報(bào)時(shí)非常管用:管理員在舉報(bào)列表里看到被舉報(bào)物品勾選、下拉選擇強(qiáng)制下架、執(zhí)行三步搞定。不需要寫任何前端代碼。5. Flask輔助服務(wù):推薦接口和統(tǒng)計(jì)看板怎么落地Django把主要業(yè)務(wù)包圓了那Flask在項(xiàng)目里到底干什么?我這邊定了兩個(gè)明確的活:推薦接口和統(tǒng)計(jì)看板。這兩塊邏輯簡單、讀多寫少、和主流程解耦非常適合用Flask起一個(gè)輕量服務(wù)。5.1 Flask推薦接口:基于瀏覽記錄的簡單協(xié)同過濾推薦這塊不用整復(fù)雜的機(jī)器學(xué)習(xí)模型課程設(shè)計(jì)階段做看過這個(gè)物品的人還在看同類物品就夠了。算法原理很簡單:根據(jù)用戶在ItemViewLog表里的瀏覽記錄找出瀏覽過當(dāng)前物品的所有用戶;再查這些用戶還瀏覽過哪些同分類的物品;按出現(xiàn)次數(shù)倒序取前6個(gè)排除用戶已擁有的物品。app.route(/api/recommend/int:item_id) def recommend(item_id): item db.session.get(Item, item_id) viewers [v.user_id for v in db.session.query(ItemViewLog).filter_by(item_iditem_id)] candidates {} for uid in viewers: for vlog in db.session.query(ItemViewLog).filter_by(user_iduid).all(): if vlog.item_id ! item_id: candidates[vlog.item_id] candidates.get(vlog.item_id, 0) 1 ranked sorted(candidates.items(), keylambda x: x[1], reverseTrue)[:6] return jsonify([{item_id: k} for k, _ in ranked])這段代碼本身沒什么技術(shù)含量但它在架構(gòu)上定義了Flask負(fù)責(zé)計(jì)算、Django負(fù)責(zé)展示的協(xié)作模式。前端頁面通過fetch(/api/recommend/3)拿到推薦列表再渲染物品卡片主站代碼保持干凈。5.2 統(tǒng)計(jì)看板:給管理員看的輕量報(bào)表第二個(gè)Flask接口是統(tǒng)計(jì)看板。我需要幾組數(shù)據(jù):每日發(fā)布物品數(shù)、當(dāng)前在售物品分類占比、熱門物品Top10。直接在Flask里用SQLAlchemy聚合查詢?nèi)缓蠓祷豃SON前端用Chart.js渲染圖表。app.route(/api/stats/category) def stats_category(): rows db.session.query(Item.category_id, func.count(Item.id)).group_by(Item.category_id).all() return jsonify([{category_id: cid, count: cnt} for cid, cnt in rows])這個(gè)統(tǒng)計(jì)接口如果也塞進(jìn)Django里代碼量和路由復(fù)雜度都會(huì)增加。放在Flask這邊整個(gè)Django項(xiàng)目只需要在Admin后臺(tái)里嵌一個(gè)iframe或?qū)懸粋€(gè)fetch調(diào)用就能展示圖表。5.3 雙框架共享同一個(gè)MySQL的模型一致性問題這是整個(gè)項(xiàng)目里最隱蔽的坑。Django的ORM會(huì)自動(dòng)給模型加字段和約束比如多對多關(guān)系會(huì)生成中間表在某些情況下還有局部索引。Flask的SQLAlchemy模型是從零手工寫的兩邊字段定義稍有出入查詢結(jié)果就可能出問題輕則查不到數(shù)據(jù)重則SQLalchemy報(bào)字段不存在。我的經(jīng)驗(yàn)是:以Django的模型為準(zhǔn)Flask側(cè)只做只讀查詢的模型定義字段精簡到只定義要用到的部分并且不允許Flask側(cè)做任何寫操作。比如Item表在Flask側(cè)只需要定義id、title、category_id、price、status、view_count這幾個(gè)字段因?yàn)橥扑]和統(tǒng)計(jì)只用到這些。這樣兩邊模型的字段交集很小出錯(cuò)的概率就低。還有一個(gè)細(xì)節(jié):created_at字段在Flasks側(cè)定義時(shí)必須加上timezoneTrue否則查出來的時(shí)間會(huì)比實(shí)際時(shí)間慢8小時(shí)。這個(gè)問題我排查了很久最后發(fā)現(xiàn)是Django啟用了USE_TZTrueMySQL里存的是UTC時(shí)間SQLAlchemy默認(rèn)按本地時(shí)間解析兩邊時(shí)區(qū)沒有對齊。5.4 Flask服務(wù)的獨(dú)立部署Flask服務(wù)最終是作為一個(gè)獨(dú)立進(jìn)程跑的。開發(fā)環(huán)境直接python app.py就行部署時(shí)我用Gunicorn來啟動(dòng)和Django那邊的uWSGI區(qū)分開。這樣兩個(gè)服務(wù)互不干擾Flask掛了重啟FlaskDjango掛了重啟Django。gunicorn -w 2 -b 127.0.0.1:5001 app:app日志單獨(dú)寫到flask_app.log方便排查問題。6. 開發(fā)到部署階段的真實(shí)踩坑清單這部分是全文含金量最高的地方。以下每一條我都實(shí)際遇到過不給結(jié)論直接給排查思路。6.1 Python版本和Django版本之間的兼容性先檢查服務(wù)器上的Python版本再選Django版本。Django 4.2要求Python 3.10以上如果服務(wù)器是3.8裝新版Django直接報(bào)語法錯(cuò)誤。我一開始在本地Windows上用Python 3.12開發(fā)一切正常部署到Linux服務(wù)器發(fā)現(xiàn)系統(tǒng)自帶的Python是3.8最后鎖定方案是Django 4.1.7 Flask 2.3兼容性最穩(wěn)。提示:做項(xiàng)目前先確定團(tuán)隊(duì)或?qū)嶒?yàn)室服務(wù)器的Python版本python3 --version一看便知不要想當(dāng)然裝最新版。6.2 Pillow擴(kuò)展庫裝不上的問題圖片處理依賴Pillow在純凈系統(tǒng)上經(jīng)常裝失敗。報(bào)錯(cuò)信息通常是RequiredDependencyException: zlib或者jpeg。原因很直白:系統(tǒng)缺底層圖片庫。解決方式:# Ubuntu/Debian sudo apt-get install libjpeg-dev zlib1g-dev pip install Pillow6.3 圖片上傳后頁面404本地開發(fā)時(shí)MEDIA_URL和MEDIA_ROOT配置正確圖片能顯示。部署到Nginx后所有圖片路徑全部404。原因是Django本身不提供靜態(tài)文件服務(wù)Nginx需要把包括/media/和/static/在內(nèi)的路徑直接代理到服務(wù)器的物理目錄。Nginx配置里加一段:location /media/ { alias /var/www/project/media/; }這個(gè)坑幾乎人人都會(huì)踩因?yàn)檫@屬于Nginx配置不屬于Django代碼教程里很少寫清楚。6.4 表結(jié)構(gòu)改來改去的遷移災(zāi)難開發(fā)過程中我多次修改Item模型導(dǎo)致遷移文件堆積數(shù)據(jù)庫狀態(tài)和模型不同步的怪問題時(shí)有出現(xiàn)。后來養(yǎng)成一個(gè)習(xí)慣:每次改模型前先python manage.py makemigrations --dry-run看看即將生成的遷移操作是否符合預(yù)期再真正執(zhí)行。如果已經(jīng)亂套了怎么辦?我的經(jīng)驗(yàn)是別硬修遷移文件直接重置:python manage.py migrate app_name zero python manage.py makemigrations python manage.py migrate但前提是能接受清空該app的數(shù)據(jù)。開發(fā)階段無所謂如果是接近答辯的數(shù)據(jù),就不要這么干。6.5 換購并發(fā)導(dǎo)致的狀態(tài)錯(cuò)亂有次測試時(shí)發(fā)現(xiàn),兩個(gè)同學(xué)同時(shí)點(diǎn)擊換購按鈕,同一件物品生成了兩筆訂單。原因就是查詢物品狀態(tài)和創(chuàng)建訂單不在一個(gè)事務(wù)里,也沒有加行鎖。修復(fù)方案前面已經(jīng)說了:select_for_update()加transaction.atomic。這是一個(gè)非常好的答辯回答題目,面試官問到如何防止超賣時(shí),你拿這個(gè)項(xiàng)目里真實(shí)修過的bug舉例,說服力遠(yuǎn)超背書。6.6 Django時(shí)區(qū)與MySQL時(shí)區(qū)不一致前面提到Flask側(cè)查時(shí)間差8小時(shí),其實(shí)Django側(cè)也遇到過。解決方案是:# settings.py USE_TZ True TIME_ZONE Asia/Shanghai這樣Django在讀寫MySQL時(shí)會(huì)自動(dòng)把UTC時(shí)間轉(zhuǎn)換為東八區(qū)。Flask側(cè)則是手動(dòng)datetime.now(timezone.utc)或者直接查詢時(shí)加上convert_tz。6.7 雙服務(wù)部署時(shí)Nginx的upstream配置兩個(gè)服務(wù)同時(shí)跑,前端要同時(shí)訪問Django的/路徑和Flask的/api/路徑,靠Nginx路由區(qū)分:upstream django_backend { server 127.0.0.1:8000; } upstream flask_backend { server 127.0.0.1:5001; } server { listen 80; server_name your-domain.com; location /api/ { proxy_pass http://flask_backend; } location / { proxy_pass http://django_backend; } }這樣一個(gè)域名同時(shí)跑兩個(gè)Python Web應(yīng)用,演示的時(shí)候不用來回切換端口,交互體驗(yàn)也自然。6.8 頁面模板和靜態(tài)資源的重復(fù)加載最后是一個(gè)容易被忽略的性能問題。Django的模板是每次請求都渲染一遍,如果有高頻頁面(比如首頁物品流),建議加一層緩存。最簡單的做法是Django的cache_page裝飾器緩存首頁60秒:from django.views.decorators.cache import cache_page urlpatterns [ path(, cache_page(60)(views.index), nameindex), ]加完緩存后,首頁壓力大幅下降,Flask推薦接口那邊也輕松不少。7. 答辯演示和后續(xù)擴(kuò)展的建議做完項(xiàng)目到真正演示還有一段距離。我見過實(shí)力不錯(cuò)但演示翻車的,也見過功能一般但演示節(jié)奏極好的?;趯?shí)際經(jīng)驗(yàn)分享幾點(diǎn)。7.1 演示前準(zhǔn)備好種子數(shù)據(jù)和錄屏不要現(xiàn)場發(fā)布物品、現(xiàn)場拍圖。提前把一個(gè)品類下放滿十幾件物品,圖片用真實(shí)拍的照片而非占位圖,這樣評委打開首頁時(shí)視覺效果好很多。同理,推薦接口的演示也別用空數(shù)據(jù)庫去測,先在ItemViewLog里造好一批瀏覽記錄,否則推薦結(jié)果永遠(yuǎn)為空。錄屏軟件開好,萬一現(xiàn)場網(wǎng)絡(luò)不穩(wěn)定或者服務(wù)沒起來,放錄屏照樣能講完。7.2 講清楚框架邊界比講功能更重要答辯時(shí)評委最常問的就是:你為什么要用Flask,Django不能做推薦接口嗎? 這時(shí)候你把第1節(jié)的架構(gòu)圖往白板上一畫,說明寫操作集中在Django、讀密集的輔助服務(wù)放Flask、共享數(shù)據(jù)庫保證一致性,評委立刻知道你理解架構(gòu)設(shè)計(jì),而不是只會(huì)堆功能。7.3 后續(xù)可以擴(kuò)展的三個(gè)方向這個(gè)項(xiàng)目做完后,想進(jìn)一步延伸可以考慮三條線:給物品加全文搜索,用Django的SearchVector或者接入Elasticsearch,解決物品多了之后分類篩選不好用的問題;把推薦服務(wù)升級成基于用戶行為排序的算法,比如根據(jù)分類偏好加權(quán),不改變架構(gòu),只改Flask側(cè)的計(jì)算邏輯;加Redis緩存熱點(diǎn)數(shù)據(jù),首頁物品流和推薦結(jié)果都緩存到Redis,進(jìn)一步減輕數(shù)據(jù)庫壓力。我自己做這個(gè)項(xiàng)目的最大體會(huì)是:技術(shù)難點(diǎn)不在某個(gè)框架的API上,而在如何讓兩個(gè)框架各司其職、協(xié)同不打架。把這個(gè)問題想透,你就能從跟著教程敲代碼進(jìn)階到真正理解Web應(yīng)用怎么搭。希望這篇能幫你少走幾個(gè)彎路。