入門python30:Flask個(gè)人賬本從空目錄運(yùn)行與階段驗(yàn)收)
零基礎(chǔ)入門python30Flask個(gè)人賬本從空目錄運(yùn)行與階段驗(yàn)收一、上一篇課后練習(xí)講解報(bào)表測(cè)試應(yīng)準(zhǔn)備收入和支出兩種記錄斷言income、expense、balance三個(gè)字段無效月份請(qǐng)求應(yīng)返回 400不能被當(dāng)成當(dāng)前月份處理。上一篇課后練習(xí)完整答案上一篇練習(xí)的要求已落實(shí)到下面完整文件先運(yùn)行項(xiàng)目測(cè)試再用 curl 對(duì)照狀態(tài)碼和數(shù)據(jù)庫持久化結(jié)果答案要點(diǎn)pytest 用戶故事從注冊(cè)、登錄走到分類、賬目和報(bào)表并覆蓋重復(fù)郵箱、越權(quán)和事務(wù)回滾失敗路徑。文件tests/test_flow.py完整參考答案文件完整文件tests/test_flow.pydeftest_duplicate_and_isolation(client):assertclient.post(/api/auth/register,json{email:ae.com,password:correct-horse}).status_code201assertclient.post(/api/auth/register,json{email:ae.com,password:correct-horse}).status_code409assertclient.get(/api/entries).status_code401完整參考答案文件本篇對(duì)應(yīng)的交付源碼完整文件flask-ledger/tests/test_app.pydeftest_health(client):assertclient.get(/api/health).get_json(){status:ok}deftest_register_login_and_me(client):responseclient.post(/api/auth/register,json{email:Aexample.com,password:password123})assertresponse.status_code201assertclient.post(/api/auth/login,json{email:aexample.com,password:password123}).status_code200assertclient.get(/api/auth/me).get_json()[email]aexample.comdeftest_ledger_flow(logged_client):categorylogged_client.post(/api/categories,json{name:餐飲}).get_json()createdlogged_client.post(/api/transactions,json{kind:expense,amount:28.50,category_id:category[id],happened_on:2026-08-06,note:午飯,})assertcreated.status_code201assertcreated.get_json()[amount]28.50itemslogged_client.get(/api/transactions).get_json()[items]assertlen(items)1statslogged_client.get(/api/statistics/monthly?month2026-08).get_json()assertstats[expense]28.50assertlogged_client.delete(f/api/transactions/{items[0][id]}).status_code204deftest_users_cannot_share_categories(client):client.post(/api/auth/register,json{email:firstexample.com,password:password123})client.post(/api/auth/login,json{email:firstexample.com,password:password123})categoryclient.post(/api/categories,json{name:工資}).get_json()client.post(/api/auth/logout)client.post(/api/auth/register,json{email:secondexample.com,password:password123})client.post(/api/auth/login,json{email:secondexample.com,password:password123})responseclient.post(/api/transactions,json{kind:income,amount:100,category_id:category[id]})assertresponse.status_code404驗(yàn)收命令python -m pytest -qDjango 項(xiàng)目使用 python manage.py test。預(yù)期測(cè)試通過若失敗先檢查遷移、配置和事務(wù)回滾。二、最終項(xiàng)目結(jié)構(gòu)flask-ledger/ ├─ app/ │ ├─ __init__.py # 應(yīng)用工廠 │ ├─ extensions.py # db/login擴(kuò)展 │ ├─ models.py # User/Category/Transaction │ ├─ auth.py # 注冊(cè)、登錄、退出 │ └─ ledger.py # 分類、賬目、報(bào)表 ├─ tests/ ├─ requirements.txt └─ run.py三、從空目錄啟動(dòng)python-m venv.venv.venv\\Scripts\\activate pip install-r requirements.txt$env:SECRET_KEYchange-this-in-productionpython run.py生產(chǎn)環(huán)境不能使用開發(fā)密鑰數(shù)據(jù)庫地址、端口和日志級(jí)別也應(yīng)通過環(huán)境變量提供。啟動(dòng)后依次驗(yàn)證/health、注冊(cè)、登錄、分類、賬目、報(bào)表和刪除。四、最終驗(yàn)收python-m pytest-q驗(yàn)收標(biāo)準(zhǔn)測(cè)試全部通過用戶只能訪問自己的資源金額使用 Decimal無效輸入有明確狀態(tài)碼項(xiàng)目能夠從空目錄安裝和啟動(dòng)。Flask 階段完成后下一篇進(jìn)入 Django 商城繼續(xù)學(xué)習(xí)相同的模型、權(quán)限和事務(wù)思想在另一框架中的實(shí)現(xiàn)。五、畢業(yè)練習(xí)給 README 增加 API 表格包括方法、路徑、登錄要求、請(qǐng)求 JSON、成功狀態(tài)碼和失敗狀態(tài)碼并用 curl 完成一次完整演示。項(xiàng)目增量從空目錄完成 Flask 交付驗(yàn)收順序是創(chuàng)建虛擬環(huán)境、安裝依賴、復(fù)制環(huán)境變量、執(zhí)行遷移、運(yùn)行測(cè)試、啟動(dòng)服務(wù)和手工請(qǐng)求。任何一步失敗都記錄命令和原因不用“我本地可以”代替。python-m venv.venv.venv\\Scripts\\python.exe-m pip install-r requirements.txt.venv\\Scripts\\python.exe-m pytest-q.venv\\Scripts\\python.exe-m flask--app app run最終檢查健康接口、注冊(cè)登錄、用戶隔離、金額 Decimal、分頁和月度報(bào)表README 寫清數(shù)據(jù)庫初始化和測(cè)試邊界。課后練習(xí)提交一份故障復(fù)盤下一階段把同樣的項(xiàng)目思路遷移到 Django。六、Flask 階段交付檢查表交付前逐項(xiàng)執(zhí)行刪除ledger.db后運(yùn)行遷移新建用戶并驗(yàn)證密碼列是哈希登錄后創(chuàng)建分類和賬目使用第二個(gè)用戶請(qǐng)求第一個(gè)用戶的 id確認(rèn)返回 404插入收入和支出后核對(duì)報(bào)表執(zhí)行python -m pytest -q并保存原始輸出。若任一項(xiàng)只能在“已有數(shù)據(jù)的機(jī)器”上成功說明 README 或遷移仍不完整。項(xiàng)目的價(jià)值不在接口數(shù)量而在邊界清楚配置不進(jìn)代碼密碼不進(jìn)日志金額不使用浮點(diǎn)查詢默認(rèn)帶所有權(quán)失敗事務(wù)可回滾。帶著這五條原則進(jìn)入 Django看到相似需求時(shí)就能判斷哪些代碼可以復(fù)用、哪些必須重新設(shè)計(jì)。再做一次“破壞性驗(yàn)收”把數(shù)據(jù)庫文件改成只讀/ready應(yīng)返回明確的 503把環(huán)境變量SECRET_KEY刪除生產(chǎn)配置應(yīng)拒絕啟動(dòng)把一個(gè)賬目的user_id改成另一個(gè)用戶列表和報(bào)表都不能顯示它。把這些失敗結(jié)果寫入 README后來的讀者才能知道項(xiàng)目不僅能跑通成功路徑也能在依賴異常時(shí)安全失敗。最后檢查文檔中的每個(gè)命令都在項(xiàng)目根目錄可執(zhí)行路徑使用正斜杠或 PowerShell 兼容寫法代碼示例的文件名與倉庫一致。驗(yàn)收者不需要猜“這段代碼放哪里”也不需要依賴作者機(jī)器上的隱藏環(huán)境變量。若使用外部 PostgreSQL只需替換DATABASE_URL并重新執(zhí)行遷移接口和測(cè)試不應(yīng)改變這正是應(yīng)用工廠和 Repository 邊界帶來的可替換性。把驗(yàn)收記錄連同版本號(hào)提交后續(xù)回顧時(shí)可以復(fù)現(xiàn)同一次結(jié)果。這份報(bào)告就是本階段的交付憑證。七、階段性項(xiàng)目復(fù)盤Flask 賬本從/health開始逐步串起配置、藍(lán)圖、認(rèn)證、分類、賬目、分頁、報(bào)表和 pytest。每個(gè)接口都有明確的輸入、成功狀態(tài)碼和失敗狀態(tài)碼每個(gè)數(shù)據(jù)查詢都帶當(dāng)前用戶每個(gè)跨多表寫入都說明事務(wù)邊界。學(xué)習(xí)者可以從app/__init__.py沿著藍(lán)圖追到 service再從測(cè)試反向理解需求而不是只能復(fù)制一個(gè)大文件。建議把驗(yàn)收數(shù)據(jù)寫入docs/flask-acceptance.md記錄命令、時(shí)間、Python 版本、數(shù)據(jù)庫遷移版本和失敗場(chǎng)景。下一階段 Django 會(huì)用同樣的驗(yàn)收表格但把事務(wù)和權(quán)限放到 Django 的 service、QuerySet 和 DRF 層中。驗(yàn)收?qǐng)?bào)告示例環(huán)境Windows 11 / Python 3.11.9 / Flask 3.x 遷移head2025_01_add_entry_constraints 測(cè)試5 passed 冒煙health200, register201, duplicate409, login200, category201, entry201, report200, cross_user404 故障只讀數(shù)據(jù)庫時(shí) ready503恢復(fù)寫權(quán)限后重新遷移并通過測(cè)試報(bào)告中的數(shù)字必須來自實(shí)際終端而不是手工填寫。若以后增加字段或更換數(shù)據(jù)庫先更新驗(yàn)收腳本再修改文檔正文確保學(xué)習(xí)者總能按同一組命令驗(yàn)證結(jié)果。Flask 階段到此形成可交付的個(gè)人賬本而不是若干互不相干的示例接口。本篇結(jié)束完整模塊文件下面是交付項(xiàng)目中真實(shí)存在的完整文件 flask-ledger/tests/test_app.py。它覆蓋本篇新增邏輯以及前文已經(jīng)完成的依賴代碼復(fù)制單個(gè)函數(shù)會(huì)丟失上下文因此這里提供整份文件。deftest_health(client):assertclient.get(/api/health).get_json(){status:ok}deftest_register_login_and_me(client):responseclient.post(/api/auth/register,json{email:Aexample.com,password:password123})assertresponse.status_code201assertclient.post(/api/auth/login,json{email:aexample.com,password:password123}).status_code200assertclient.get(/api/auth/me).get_json()[email]aexample.comdeftest_ledger_flow(logged_client):categorylogged_client.post(/api/categories,json{name:餐飲}).get_json()createdlogged_client.post(/api/transactions,json{kind:expense,amount:28.50,category_id:category[id],happened_on:2026-08-06,note:午飯,})assertcreated.status_code201assertcreated.get_json()[amount]28.50itemslogged_client.get(/api/transactions).get_json()[items]assertlen(items)1statslogged_client.get(/api/statistics/monthly?month2026-08).get_json()assertstats[expense]28.50assertlogged_client.delete(f/api/transactions/{items[0][id]}).status_code204deftest_users_cannot_share_categories(client):client.post(/api/auth/register,json{email:firstexample.com,password:password123})client.post(/api/auth/login,json{email:firstexample.com,password:password123})categoryclient.post(/api/categories,json{name:工資}).get_json()client.post(/api/auth/logout)client.post(/api/auth/register,json{email:secondexample.com,password:password123})client.post(/api/auth/login,json{email:secondexample.com,password:password123})responseclient.post(/api/transactions,json{kind:income,amount:100,category_id:category[id]})assertresponse.status_code404