崙?zhàn):從后端設(shè)計(jì)到前端聯(lián)調(diào))
如果你正打算做一個(gè)電商類的購物小程序又不想把前后端拆得太開、來回折騰那 Python Flask uni-app 這套組合我實(shí)測下來很適合。我最近完整做了一個(gè)商城購物平臺(tái)的電商小程序從數(shù)據(jù)庫設(shè)計(jì)、后端接口到前端頁面全部自己寫。標(biāo)題里那幾個(gè)關(guān)鍵詞——flask、uniapp、vue、pycharm、django其實(shí)是我當(dāng)初做技術(shù)選型時(shí)反復(fù)對(duì)比過的內(nèi)容。這篇文章把整個(gè)設(shè)計(jì)和實(shí)現(xiàn)過程拆開講包括為什么最后選了 Flask在 PyCharm 里怎么搭環(huán)境uni-app 里的 Vue 語法怎么和小程序頁面綁定以及聯(lián)調(diào)上線后踩到的各種坑。這項(xiàng)目不是玩具 Demo而是能跑完“登錄-逛商品-加購物車-下單-模擬支付-訂單查詢”完整閉環(huán)的真實(shí)產(chǎn)物適合正在寫畢業(yè)設(shè)計(jì)、個(gè)人作品集或者第一款獨(dú)立小程序的人參考。1. 項(xiàng)目整體設(shè)計(jì)與技術(shù)選型1.1 為什么選了 Flask 而不是 Django這個(gè)項(xiàng)目的標(biāo)題里同時(shí)出現(xiàn)了 flask 和 django說明很多人跟我一樣在選擇 Python 后端框架時(shí)糾結(jié)過。Django 確實(shí)自帶 Admin 后臺(tái)、ORM、遷移工具看起來“開箱即用”但對(duì)于一個(gè)電商小程序后端來說它的問題也很明顯太重了。Django 的項(xiàng)目結(jié)構(gòu)默認(rèn)幫你分好了 app你還沒寫業(yè)務(wù)代碼就要先理解 settings、urls、models、admin、middleware 這些概念。而 Flask 只做一件事用路由接收請(qǐng)求返回 JSON。剩下的數(shù)據(jù)庫、登錄鑒權(quán)、參數(shù)校驗(yàn)全部可以通過第三方擴(kuò)展按需組合。商城購物平臺(tái)的核心是 API不是后臺(tái)管理頁面。小程序端不需要后端渲染 HTML只需要后端穩(wěn)定地吐出 JSON。Flask 配合 Flask-SQLAlchemy、Flask-JWT-Extended、Flask-CORS 這幾個(gè)庫已經(jīng)能覆蓋項(xiàng)目 95% 的需求。開發(fā)時(shí)用 PyCharm 建一個(gè)虛擬環(huán)境裝依賴、啟動(dòng)服務(wù)、打斷點(diǎn)調(diào)試整個(gè)流程非常輕量。如果用 Django前期要處理的東西至少多一倍而實(shí)際用到的那部分核心能力Flask 完全夠用。我并不是說 Django 不好而是“項(xiàng)目適合什么”更重要。如果你要做一個(gè)大型內(nèi)容管理系統(tǒng)后臺(tái)功能復(fù)雜那 Django 是更好的選擇但如果目標(biāo)明確是給小程序提供 APIFlask 的學(xué)習(xí)成本和交付速度都有明顯優(yōu)勢。我在這套項(xiàng)目里用 Flask 寫了接近四十個(gè)接口從用戶登錄、商品列表到訂單提交代碼結(jié)構(gòu)依然很清爽。如果你的項(xiàng)目也需要快速迭代Flask 更合適。1.2 uni-app 和 Vue 的關(guān)系以及為什么能一套代碼出小程序uni-app 本身是基于 Vue 語法的跨端框架項(xiàng)目里寫的export default { data() { return {} }, methods: {} }和 Vue 單文件組件的寫法幾乎一致。所以標(biāo)題里出現(xiàn)“vue”并不奇怪因?yàn)樵谛〕绦蚨藢?uni-app本質(zhì)上就是在用 Vue 的語法組織頁面邏輯。唯一不同的是模板部分uni-app 把 Vue 的div換成了小程序和 App 都能識(shí)別的內(nèi)置組件比如view、text、image、scroll-view數(shù)據(jù)綁定仍然用的是{{ }}和v-for。選擇 uni-app 的主要原因是可以只寫一套代碼同時(shí)發(fā)布到微信小程序、支付寶小程序、H5 甚至原生 App。電商項(xiàng)目最怕的是單獨(dú)維護(hù)一個(gè)小程序端再維護(hù)一個(gè) H5 端如果你還想要 App工作量直接翻三倍。uni-app 把這一層跨端差異封裝掉了普通頁面的開發(fā)體驗(yàn)和寫 Vue 幾乎一致。對(duì)于我這種以技術(shù)實(shí)現(xiàn)為主的人來說不需要去記小程序原生那套繁瑣的setData規(guī)則開發(fā)效率提高很多。而且在 PyCharm 里做后端再用 HBuilderX 或 VS Code 寫 uni-app正好符合標(biāo)題里“pycharm”這個(gè)關(guān)鍵詞的使用場景PyCharm 管 Python 后端前端編輯器管 Vue 文件。兩個(gè)窗口并行調(diào)試后端斷點(diǎn)打進(jìn)去看請(qǐng)求數(shù)據(jù)前端控制臺(tái)看渲染結(jié)果排查問題非常直觀。2. 商城核心需求拆解與數(shù)據(jù)庫設(shè)計(jì)2.1 四大核心模塊的數(shù)據(jù)表結(jié)構(gòu)電商小程序看著功能多拆開以后核心就是用戶、商品、購物車、訂單四大模塊。我設(shè)計(jì)表結(jié)構(gòu)時(shí)沒有過度設(shè)計(jì)能拆的表拆掉能省的字段省掉。用戶表存id、openid、nickname、avatar、phone其中openid是登錄憑證用唯一索引防止重復(fù)綁定。商品表存id、title、subtitle、cover_url、price、stock、category_id、status、sales價(jià)格字段用Numeric(10, 2)絕對(duì)不用浮點(diǎn)數(shù)不然算金額會(huì)出現(xiàn)小數(shù)點(diǎn)后面莫名其妙的誤差。分類表就兩個(gè)字段id和name再加一個(gè)sort控制排序。購物車表比較關(guān)鍵它關(guān)聯(lián)的是用戶和商品user_id、goods_id、quantity、selected另外加了一個(gè)唯一約束(user_id, goods_id)保證同一個(gè)用戶同一件商品只會(huì)有一行記錄再次加入購物車就做數(shù)量累加。訂單表字段最多order_no、user_id、total_amount、status、address_snapshot、create_time、pay_time。訂單項(xiàng)表則存每個(gè)訂單里的商品快照order_id、goods_id、title、cover_url、price、quantity。這里要特別強(qiáng)調(diào)訂單項(xiàng)必須做“快照”。用戶下單之后商品標(biāo)題和價(jià)格可能會(huì)被后臺(tái)修改如果訂單詳情直接去關(guān)聯(lián)商品表那用戶看到的過去訂單信息就會(huì)變。把標(biāo)題、圖片、單價(jià)原樣復(fù)制到訂單項(xiàng)表里才是電商系統(tǒng)該有的做法。地址也一樣用戶地址可以隨便改但每筆訂單要保留下單那一刻的地址完整字段不然售后對(duì)賬會(huì)非常麻煩。2.2 用戶登錄與 JWT 無狀態(tài)鑒權(quán)小程序端的登錄不能直接用賬號(hào)密碼因?yàn)橛脩舸蜷_小程序后要的是“一鍵登錄”。我的方案是前端調(diào)用小程序提供的uni.login獲取臨時(shí) code把 code 傳給后端后端拿著 code 去交換 openid然后創(chuàng)建或更新用戶記錄最后簽發(fā)一個(gè) JWT token 返回給前端。這個(gè)流程聽起來繞但它是小程序生態(tài)的標(biāo)準(zhǔn)做法核心目的就是讓服務(wù)端拿到的不是用戶自己填的密碼而是平臺(tái)校驗(yàn)過的身份憑證。JWT 的好處是不用存儲(chǔ) session服務(wù)端不需要維護(hù)一張會(huì)話表。flask-jwt-extended生成 token 后前端把它存在uni.setStorageSync里之后每次請(qǐng)求在 header 中帶上Authorization: Bearer token后端通過jwt_required()裝飾器就能拿到當(dāng)前用戶 ID。比如購物車、訂單這些接口都依賴用戶身份我只需要在函數(shù)里寫get_jwt_identity()就能獲取user_id干凈利落。使用 JWT 也有一個(gè)必須注意的點(diǎn)token 一旦簽發(fā)在有效期內(nèi)是吊銷不了的。所以我把有效期設(shè)置成了 7 天同時(shí)在用戶表中維護(hù)了一個(gè)token_version字段當(dāng)用戶修改密碼或者被管理員禁用時(shí)token_version加一重新簽發(fā)的 token 里包含新的版本號(hào)后端在每次請(qǐng)求時(shí)比較版本號(hào)是否一致不通過就直接返回 401。這種方法既保留 JWT 無狀態(tài)的優(yōu)勢又解決了“用戶被踢下線”的實(shí)際需求。3. Flask 后端 API 的實(shí)現(xiàn)細(xì)節(jié)3.1 項(xiàng)目目錄結(jié)構(gòu)與藍(lán)圖劃分很多新手寫 Flask 喜歡把所有路由寫在一個(gè)app.py里接口少的時(shí)候還行電商這種規(guī)模的項(xiàng)目一上去文件就失控了。我實(shí)際用的是包結(jié)構(gòu)把不同模塊用藍(lán)圖Blueprint拆開。項(xiàng)目目錄長這樣shop_api/ ├── app.py # 入口文件注冊(cè)所有藍(lán)圖 ├── config.py # 配置項(xiàng) ├── extensions.py # 擴(kuò)展對(duì)象初始化 ├── models/ │ ├── __init__.py │ ├── user.py │ ├── goods.py │ ├── cart.py │ └── order.py ├── apis/ │ ├── __init__.py │ ├── auth.py # 登錄相關(guān) │ ├── goods.py # 商品、分類 │ ├── cart.py # 購物車 │ └── order.py # 訂單、支付模擬 ├── utils/ │ ├── response.py # 統(tǒng)一返回格式 │ └── decorators.py # 自定義裝飾器 └── requirements.txt入口文件只負(fù)責(zé)創(chuàng)建app、加載配置、初始化擴(kuò)展、注冊(cè)藍(lán)圖。藍(lán)圖里每個(gè)模塊的 URL 前綴都獨(dú)立比如購物車藍(lán)圖統(tǒng)一掛在/api/cart下面接口路徑就自然形成了/api/cart/list、/api/cart/add。這樣后期加模塊不會(huì)相互影響團(tuán)隊(duì)協(xié)作時(shí)各寫各的藍(lán)圖文件沖突也少。統(tǒng)一響應(yīng)格式也很有必要。我用的結(jié)構(gòu)是{code: 0, message: success, data: {}}。成功時(shí) code 為 0失敗時(shí) code 為具體錯(cuò)誤碼比如 1001 表示 token 失效1002 表示商品庫存不足。前端封裝了請(qǐng)求攔截器后只需要判斷 code 是不是 0不用去解析 HTTP 狀態(tài)碼和自定義 message 的混合體異常提示也能直接透傳給用戶。3.2 商品列表、搜索與詳情接口設(shè)計(jì)商品列表接口不要一上來就把所有字段都返回用戶在小程序端只需要卡片所需的標(biāo)題、圖片、價(jià)格、銷量。所以我用 Flask-SQLAlchemy 的查詢按分類、關(guān)鍵詞、銷量、價(jià)格排序輸出的時(shí)候手工構(gòu)造字典而不是直接序列化模型對(duì)象。這樣能避免把stock、status這些內(nèi)部字段也暴露給前端減少安全性問題。商品搜索我用的是LIKE模糊匹配。電商小程序的并發(fā)量不會(huì)太大這個(gè)方案是夠用的。如果以后數(shù)據(jù)量大了再上全文檢索引擎也不遲。接口設(shè)計(jì)上支持page和size參數(shù)返回total、items、has_more小程序端滾動(dòng)到底部加載下一頁用戶體驗(yàn)會(huì)好很多。商品詳情接口需要注意瀏覽數(shù)和銷量展示。我在商品表里設(shè)計(jì)了sales字段只有訂單支付成功后才累加。商品詳情頁的瀏覽量我選擇不加字段記錄因?yàn)轭l繁更新瀏覽量會(huì)拖垮數(shù)據(jù)庫。如果后續(xù)真的需要我會(huì)用緩存做異步累加而不是每次訪問都去 UPDATE 一次。3.3 下單與庫存扣減事務(wù)和條件更新的正確姿勢下單是整個(gè)系統(tǒng)最核心、最容易出錯(cuò)的環(huán)節(jié)。我的實(shí)現(xiàn)嚴(yán)格按下面幾步執(zhí)行校驗(yàn)商品是否存在、是否上架。根據(jù)購物車?yán)锏墓催x商品計(jì)算出訂單總金額。按商品維度依次扣減庫存??蹨p成功則創(chuàng)建訂單和訂單項(xiàng)扣減失敗則整體回滾。清空對(duì)應(yīng)購物車記錄返回訂單號(hào)??蹘齑娌荒芟炔?stock判斷大于等于購買數(shù)量后再 UPDATE因?yàn)椴l(fā)請(qǐng)求下兩個(gè)用戶可能同時(shí)查到庫存為 1然后同時(shí)通過判斷最終把庫存扣成負(fù)數(shù)。我用的是一個(gè)帶條件的 UPDATE在數(shù)據(jù)庫層面保證原子性result db.session.execute( db.update(Goods) .where(Goods.id goods_id, Goods.stock quantity) .values(stockGoods.stock - quantity) ) if result.rowcount 0: raise StockNotEnough(goods_id)rowcount如果等于 0說明要么商品不存在要么庫存不足這里直接拋出異常再由外層的事務(wù)裝飾器回滾。把關(guān)鍵判斷交給數(shù)據(jù)庫而不是 Python 進(jìn)程這是防止超賣的基本原則。同時(shí)我還要說明下單不一定會(huì)支付所以下單時(shí)扣減的是“可售庫存”支付后如果超時(shí)未支付需要定時(shí)任務(wù)把庫存加回去。這個(gè)項(xiàng)目里我用了一個(gè)簡單的定時(shí)清理邏輯每隔一段時(shí)間把超過 30 分鐘未支付且未取消的訂單取消并回補(bǔ)庫存。整個(gè)下單流程用db.session.commit()一次性提交任何一步異常都調(diào)用db.session.rollback()。我建議在開發(fā)時(shí)寫一個(gè)測試腳本開 20 個(gè)線程同時(shí)搶購最后一件商品跑完庫存剛好為 0且只有 1 筆訂單創(chuàng)建成功這是驗(yàn)證并發(fā)安全最直接的方式。4. uni-app 前端開發(fā)實(shí)戰(zhàn)4.1 初始化工程與 pages 目錄規(guī)劃用 HBuilderX 創(chuàng)建 uni-app 項(xiàng)目時(shí)我習(xí)慣選擇“默認(rèn)模板”然后手動(dòng)刪除多余的東西。項(xiàng)目結(jié)構(gòu)里最重要的就是pages.json和pages目錄。pages.json負(fù)責(zé)注冊(cè)頁面路徑、設(shè)置 tabBar 和窗口樣式。因?yàn)槭巧坛琴徫锲脚_(tái)我設(shè)了四個(gè)底部 tab首頁、分類、購物車、我的對(duì)應(yīng)四個(gè)頁面目錄。其余頁面按照業(yè)務(wù)模塊拆pages/ ├── index/ # 首頁 ├── category/ # 分類頁 ├── goods/ # 商品詳情 ├── cart/ # 購物車 ├── order/ # 訂單列表、確認(rèn)訂單 ├── pay/ # 模擬支付 └── user/ # 個(gè)人中心、登錄這樣規(guī)劃的好處是頁面路由和目錄一一對(duì)應(yīng)找文件時(shí)不需要猜。uni-app 的項(xiàng)目里每個(gè)頁面就是一組.vue文件我在pages.json中給每個(gè)頁面配置了navigationBarTitleText確保小程序頂部標(biāo)題不會(huì)被默認(rèn)值占位。我習(xí)慣把公共樣式放到uni.scss比如主題色、價(jià)格顏色、邊框變量。小程序里不能直接用var()做主題切換提前把顏色變量統(tǒng)一起來后面改主色調(diào)就不用全項(xiàng)目搜索替換了。這個(gè)習(xí)慣幫我在老板突然說“換個(gè)品牌色”時(shí)省了不少時(shí)間。4.2 封裝請(qǐng)求工具與 token 注入小程序不像瀏覽器沒有 axios但uni.request已經(jīng)是一個(gè)封裝好的 API。我在此基礎(chǔ)上再包了一層request.js統(tǒng)一處理 baseURL、超時(shí)、token 注入、錯(cuò)誤提示。核心代碼如下const BASE_URL http://localhost:5000/api export function request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: Bearer uni.getStorageSync(token) }, success(res) { if (res.data.code 0) { resolve(res.data.data) } else { uni.showToast({ title: res.data.message, icon: none }) reject(res.data) } }, fail(err) { uni.showToast({ title: 網(wǎng)絡(luò)異常, icon: none }) reject(err) } }) }) }這里有個(gè)問題容易踩開發(fā)階段后端跑在 PyCharm 的本地 Flask 服務(wù)上瀏覽器調(diào)試 H5 端可以但真機(jī)預(yù)覽小程序時(shí)localhost指向的是手機(jī)自己根本訪問不到電腦上的服務(wù)。我的辦法是開發(fā)時(shí)使用電腦的局域網(wǎng) IP比如http://192.168.1.100:5000/api。同時(shí)在小程序開發(fā)者工具中勾選“不校驗(yàn)合法域名”等到上線前再改寫成正式的 HTTPS 域名。token 失效的處理也很重要。當(dāng)接口返回 code 為 1001 時(shí)request.js需要清掉本地 token然后跳轉(zhuǎn)到登錄頁面。我一開始沒寫這個(gè)邏輯結(jié)果 token 過期后用戶一直停留在失敗頁面體驗(yàn)特別差。加上這個(gè)處理之后用戶只要重新走一次登錄流程就能繼續(xù)使用。4.3 首頁、商品詳情與購物車的聯(lián)動(dòng)首頁我用了搜索框、分類側(cè)邊欄、商品卡片流結(jié)合的方式。頂部搜索關(guān)鍵詞變化時(shí)調(diào)用商品搜索接口分類切換時(shí)重新請(qǐng)求當(dāng)前分類的商品列表。為了減少請(qǐng)求次數(shù)我用的是觸底分頁加載每次加載 10 條滾動(dòng)到底部觸發(fā)下一頁。商品詳情頁的關(guān)鍵是“立即購買”和“加入購物車”兩個(gè)操作。點(diǎn)擊加入購物車時(shí)前端只做兩件事把商品 ID 和數(shù)量傳給后端購物車接口如果成功就彈出一個(gè)小 toast并且更新底部 tabBar 的購物車角標(biāo)。角標(biāo)數(shù)量邏輯我通過uni.$emit(cartUpdated)全局通知其他頁面監(jiān)聽這個(gè)事件后重新獲取購物車數(shù)量避免出現(xiàn)加購成功后首頁角標(biāo)還是舊的這種尷尬。購物車頁交互是最繁瑣的。每行有一個(gè)復(fù)選框控制選中狀態(tài)底部欄顯示已選商品總價(jià)和結(jié)算按鈕。我維護(hù)一個(gè)selectedMap對(duì)象來記錄每件商品的選中狀態(tài)選中變化時(shí)重新計(jì)算總價(jià)。后端也支持批量刪除和修改數(shù)量所有操作都返回最新的列表和總價(jià)前端直接用返回?cái)?shù)據(jù)刷新視圖不自己做本地計(jì)算。這樣可以避免本地?cái)?shù)據(jù)和后端不一致的問題。下單入口我做了區(qū)分從購物車結(jié)算提交的是購物車選中的商品從詳情頁立即購買提交的是單件商品。所以確認(rèn)訂單頁需要接受兩種來源我統(tǒng)一在頁面參數(shù)中帶上from字段值為cart或buy。后端下單接口也接收這個(gè)來源如果是buy就直接用請(qǐng)求里的商品列表如果是cart就去讀取購物車選中項(xiàng)。這個(gè)設(shè)計(jì)看起來簡單但能把兩個(gè)業(yè)務(wù)場景統(tǒng)一成一套接口不用復(fù)制代碼。5. PyCharm 開發(fā)環(huán)境與前后端聯(lián)調(diào)5.1 用 PyCharm 管理 Flask 虛擬環(huán)境與依賴導(dǎo)出在 PyCharm 中新建 Flask 項(xiàng)目時(shí)我建議先創(chuàng)建一個(gè)獨(dú)立的虛擬環(huán)境不要用系統(tǒng) Python 的全局解釋器。因?yàn)椴煌?xiàng)目依賴版本經(jīng)常沖突尤其是 Flask 和 SQLAlchemy 的版本組合全局安裝容易出現(xiàn)“A 項(xiàng)目能跑B 項(xiàng)目跑不起來”的情況。PyCharm 在File - Settings - Project - Python Interpreter里可以選擇虛擬環(huán)境路徑創(chuàng)建后左下角會(huì)顯示當(dāng)前使用的解釋器。依賴管理我推薦兩條命令pip install flask flask-sqlalchemy flask-jwt-extended flask-cors pymysql pip freeze requirements.txtfreeze出來的文件在上服務(wù)器時(shí)直接執(zhí)行pip install -r requirements.txt就能復(fù)現(xiàn)環(huán)境。不過我習(xí)慣再手動(dòng)檢查一遍requirements.txt把那些和項(xiàng)目無關(guān)的傳遞依賴清理掉保證環(huán)境干凈。PyCharm 里調(diào)試 Flask 非常順手。給入口文件app.py配置一個(gè) Run Configuration設(shè)置環(huán)境變量FLASK_ENVdevelopment啟動(dòng)后接口地址是http://127.0.0.1:5000。在視圖函數(shù)里打上斷點(diǎn)再用小程序的模擬器去請(qǐng)求接口請(qǐng)求會(huì)在斷點(diǎn)處停住你可以看到 Flask 接收到的 JSON 數(shù)據(jù)和數(shù)據(jù)庫查詢結(jié)果。這種調(diào)試方式比靠print猜來猜去高效得多。5.2 前端聯(lián)調(diào)域名白名單、真機(jī)調(diào)試與跨域聯(lián)調(diào)過程中最頭疼的問題不是代碼邏輯而是網(wǎng)絡(luò)環(huán)境。小程序生產(chǎn)環(huán)境要求所有請(qǐng)求域名必須是 HTTPS并且在后臺(tái)配置白名單。開發(fā)階段自然沒這個(gè)條件解決方法是在微信開發(fā)者工具的“詳情-本地設(shè)置”中勾選“不校驗(yàn)合法域名、web-view業(yè)務(wù)域名、TLS 版本以及 HTTPS 證書”。這樣就能用本地 Flask 的 HTTP 服務(wù)調(diào)試。但注意這個(gè)設(shè)置只對(duì)開發(fā)者工具生效真機(jī)預(yù)覽時(shí)還是要用局域網(wǎng) IP并且手機(jī)和電腦處于同一個(gè) WiFi 下。如果你用 HBuilderX 運(yùn)行到微信開發(fā)者工具HBuilderX 會(huì)生成一個(gè)臨時(shí)小程序項(xiàng)目路徑。這里有一個(gè)容易忽略的地方每次修改代碼后要在 HBuilderX 里重新運(yùn)行或點(diǎn)擊刷新開發(fā)者工具才能拿到最新代碼。我一開始改了.vue文件發(fā)現(xiàn)小程序沒變化還以為沒保存成功后來才發(fā)現(xiàn)是 HBuilderX 沒有重新編譯到微信開發(fā)者工具目錄。跨域問題主要出現(xiàn)在 H5 端。瀏覽器里uni.request請(qǐng)求 Flask 時(shí)Fe服務(wù)器和前端不是同一個(gè)源瀏覽器會(huì)攔截響應(yīng)。解決辦法是在 Flask 中啟用 CORS我使用flask-corsfrom flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})加了這一行后H5 端的聯(lián)調(diào)就正常了。小程序端本身不存在瀏覽器跨域限制所以不需要關(guān)心 CORS但也因此更需要注意請(qǐng)求異常時(shí)的錯(cuò)誤處理因?yàn)樾〕绦虻腻e(cuò)誤提示不像瀏覽器控制臺(tái)那么詳細(xì)。6. 部署上線與常見問題排查6.1 服務(wù)器部署 Flask 的注意點(diǎn)本地開發(fā)用 Flask 自帶服務(wù)器上線可不能繼續(xù)用它。我用的是 Gunicorn 作為 WSGI 服務(wù)Ubuntu 服務(wù)器上配合 Nginx 反向代理。啟動(dòng)命令大概是gunicorn -w 4 -b 0.0.0.0:8000 app:app-w 4表示 4 個(gè) worker 進(jìn)程。在 PyCharm 中寫的入口app:app前一個(gè)是文件名后一個(gè)是被創(chuàng)建的Flask實(shí)例變量名。Gunicorn 不能跑在 Windows所以部署時(shí)切到 Linux 環(huán)境。如果你用的是 Windows 服務(wù)器可以考慮 Waitress但我個(gè)人還是推薦 Linux 加 Nginx。Nginx 做的事情有兩件一是把 80/443 端口的請(qǐng)求轉(zhuǎn)發(fā)給 Gunicorn 的 8000 端口二是有需要時(shí)直接托管靜態(tài)資源。在小程序場景中圖片和商品圖我建議單獨(dú)用對(duì)象存儲(chǔ)服務(wù)不放在應(yīng)用服務(wù)器里否則帶寬和存儲(chǔ)都會(huì)成為瓶頸。數(shù)據(jù)庫我單獨(dú)使用一個(gè)云數(shù)據(jù)庫實(shí)例不會(huì)和應(yīng)用搶內(nèi)存。部署后一定要檢查數(shù)據(jù)庫連接池。Flask-SQLAlchemy 默認(rèn)連接池不算大高并發(fā)下容易出現(xiàn)TimeoutError。我設(shè)置SQLALCHEMY_POOL_PRE_PINGTrue可以讓連接失效時(shí)自動(dòng)重連這個(gè)參數(shù)解決了我曾經(jīng)在長時(shí)間運(yùn)行后接口假死的問題。6.2 高頻報(bào)錯(cuò)排查實(shí)錄我在整個(gè)開發(fā)過程中整理了下面幾個(gè)高頻報(bào)錯(cuò)它們幾乎每個(gè)都是新手必踩的坑。第一個(gè)是“數(shù)據(jù)庫時(shí)間比本地時(shí)間少 8 小時(shí)”。Flask 里的datetime.now()默認(rèn)讀取服務(wù)器時(shí)區(qū)如果服務(wù)器是 UTC后端返回的create_time就是 UTC 時(shí)間。前端直接把時(shí)間字符串展示出來就會(huì)出現(xiàn)訂單時(shí)間比用戶看到的時(shí)間少 8 小時(shí)。我的解決辦法是在后端統(tǒng)一使用本地時(shí)區(qū)比如在config.py中設(shè)置TIMEZONE Asia/Shanghai并且在返回時(shí)間字段前統(tǒng)一做時(shí)區(qū)轉(zhuǎn)換讓前端拿到的直接就是東八區(qū)的時(shí)間字符串不做二次轉(zhuǎn)換。第二個(gè)是“JSON 序列化 Decimal 失敗”。價(jià)格字段是NumericFlask 的默認(rèn) JSON encoder 不認(rèn)識(shí)Decimal對(duì)象直接用jsonify返回會(huì)報(bào)TypeError。我的解決方法是寫一個(gè)自定義 JSON Encoder把Decimal轉(zhuǎn)換成字符串確保精度不丟失。前端計(jì)算金額時(shí)也不要用浮點(diǎn)數(shù)我是在后端返回字符串后由前端通過parseInt和parseFloat在顯示層處理核心金額計(jì)算始終交給后端。from flask.json import JSONEncoder from decimal import Decimal class CustomJSONEncoder(JSONEncoder): def default(self, obj): if isinstance(obj, Decimal): return str(obj) return super().default(obj) app.json_encoder CustomJSONEncoder第三個(gè)是“前端加了購物車但角標(biāo)不更新”。原因是最先寫的代碼只是在加購成功后uni.showToast沒有通知其他頁面刷新。后來我引入了uni.$emit(cartUpdated)在首頁和購物車頁用uni.$on(cartUpdated, function() { ... })接收事件問題就解決了。不過要注意頁面卸載時(shí)要用uni.$off解綁事件否則會(huì)重復(fù)監(jiān)聽導(dǎo)致多次請(qǐng)求。第四個(gè)是“并發(fā)下單導(dǎo)致庫存扣成負(fù)數(shù)”。我一開始用的是先查詢?cè)俑潞髞砀某蓷l件 UPDATE 后問題徹底消失。這里再強(qiáng)調(diào)一遍高性能場景下的并發(fā)安全不能靠 Python 代碼判斷要充分利用數(shù)據(jù)庫的行鎖和原子操作。6.3 上線前的安全自檢清單電商項(xiàng)目只要上線就涉及資金和用戶數(shù)據(jù)安全。我給自己列了幾條鐵律你可以直接抄走。第一小程序前端絕對(duì)不能放密鑰。任何 AppSecret、支付秘鑰、數(shù)據(jù)庫連接串都只能保存在后端環(huán)境變量里。小程序代碼可以被反編譯只要前端出現(xiàn)明文密鑰基本等于把后臺(tái)拱手送人。第二Flask 必須關(guān)閉debug。我在config.py里區(qū)分生產(chǎn)環(huán)境和開發(fā)環(huán)境生產(chǎn)環(huán)境下DEBUGFalse同時(shí)設(shè)置SECRET_KEY為環(huán)境變量讀取的值不寫死在代碼里。第三所有涉及金額的接口后端必須重新算價(jià)。前端傳過來的商品 ID 和數(shù)量可以信但價(jià)格不能信。下單接口要從數(shù)據(jù)庫讀取當(dāng)前商品價(jià)格乘以數(shù)量得到總金額然后再創(chuàng)建訂單。如果拿前端傳過來的價(jià)格直接入庫用戶改一下請(qǐng)求里的字段就能用 0.01 元買到商城里的商品。這是最典型也最致命的漏洞。第四用戶輸入不能直接拼接進(jìn) SQL。Flask-SQLAlchemy 的 ORM 默認(rèn)參數(shù)化查詢相對(duì)安全但是使用原生 SQL 時(shí)要格外小心。商品搜索里的關(guān)鍵詞參數(shù)一律通過text()綁定參數(shù)的方式傳值不直接字符串拼接。第五做支付回調(diào)時(shí)一定要驗(yàn)簽。這個(gè)項(xiàng)目雖然只是模擬支付但我也按真實(shí)標(biāo)準(zhǔn)預(yù)留了回調(diào)接口。如果接真實(shí)支付回調(diào)通知里哪怕只有一個(gè)字段對(duì)不上簽名也要直接拒收。任何人只要知道你的回調(diào)地址就能偽造支付成功通知。6.4 做完這個(gè)項(xiàng)目后的幾點(diǎn)經(jīng)驗(yàn)沉淀這是我前后端獨(dú)立完成的第一個(gè)完整電商系統(tǒng)最大的體會(huì)是選型不是越火越好而是越貼合場景越好。Flask 搭配 uni-app讓我用最少的心智負(fù)擔(dān)完成了一個(gè)可以跑通完整閉環(huán)的小程序。另一個(gè)感觸是很多問題看起來是技術(shù)問題實(shí)際上是沒有先想清楚流程。比如庫存扣減為什么要用條件更新訂單金額為什么必須后端算地址為什么必須做快照這些都不是代碼本身而是業(yè)務(wù)規(guī)則約束下的必然選擇。最后分享一個(gè)小技巧開發(fā)時(shí)一定要把后端返回的 JSON 結(jié)構(gòu)固定下來。我在項(xiàng)目開始前先寫了一個(gè)接口文檔規(guī)定好 code、message、data 的格式前端和后端完全按這個(gè)文檔并行開發(fā)后面聯(lián)調(diào)幾乎沒有扯皮。如果你也想做類似的項(xiàng)目建議把下單、庫存、支付這三個(gè)環(huán)節(jié)先按最笨的方式實(shí)現(xiàn)跑通后再優(yōu)化千萬不要一上來就追求“分布式”“高并發(fā)”先把單體閉環(huán)做扎實(shí)比什么都強(qiáng)。