From 9142f62de2d06bf83af970192e4494f9076271ac Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 16 Apr 2026 08:25:44 +0800 Subject: [PATCH 01/15] docs: update commit skill --- .agents/skills/commit/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/commit/SKILL.md index e9858756..3300e386 100644 --- a/.agents/skills/commit/SKILL.md +++ b/.agents/skills/commit/SKILL.md @@ -119,4 +119,4 @@ git push **Q: 不小心 commit 了不該 commit 的檔案?** -告訴 AI:「我剛才不小心 commit 了 [檔案名稱],請幫我移除。」 +告訴 AI:「我剛才不小心 commit 了 [檔案名稱],請幫我移除。」 \ No newline at end of file From ebd903c95e9904f910bd6382eac1ff873127fa78 Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 16 Apr 2026 08:29:53 +0800 Subject: [PATCH 02/15] docs: add PRD --- docs/PRD.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/PRD.md diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 00000000..ee29c7e9 --- /dev/null +++ b/docs/PRD.md @@ -0,0 +1,52 @@ +# 產品需求文件 (PRD):食譜管理系統 + +## 1. 專案概述 +- **背景與動機**:現代人生活忙碌,常常不知道要煮什麼,或是想要把好吃的食譜記錄下來以便未來查詢。此系統提供一個簡單、直覺的平台讓使用者存放、尋找,甚至根據手邊現有食材自動尋找推薦的食譜。 +- **目標用戶**:我 (個人愛好者、家庭煮夫/婦、對料理有熱忱的自煮族)。 +- **核心價值主張**:更有效率地管理個人食譜,並解決「今天不知道要吃什麼」的煩惱,輔助使用者最大化利用家中現存食材。 + +## 2. 功能需求 +系統將提供以下主要功能: + +1. **新增食譜** + - *使用者故事*:作為一位熱愛烹飪的使用者,我希望能夠新增食譜(記錄名稱、食材、步驟等),以便儲存我喜歡的菜餚。 +2. **搜尋食譜** + - *使用者故事*:作為一位使用者,我希望能透過關鍵字搜尋食譜名稱,以便快速找到我想做的菜。 +3. **用食材推薦食譜** + - *使用者故事*:作為一位面對冰箱剩餘食材的使用者,我希望能夠輸入手邊現有食材,由系統過濾並推薦能做的食譜,以便不浪費食物。 +4. **編輯與刪除食譜** + - *使用者故事*:作為一位記錄者,我希望可以能夠編輯或刪除已經存入的食譜,以便我的食譜庫隨時保持正確、最新。 +5. **食譜分類與標籤** + - *使用者故事*:作為一位習慣有條理整理食譜的使用者,我希望可以為食譜加上分類或標籤(如:前菜、甜點、全素、低卡等),以便後續依照標籤快速篩選食譜。 + +## 3. 非功能需求 +- **技術限制**: + - 後端框架:Python + Flask + - 模板引擎:Jinja2 + - 資料庫:SQLite +- **效能與安全考量**: + - 確保食譜搜尋可快速回傳結果。 + - 使用適當的安全機制來防範常見的安全漏洞(如:防護 SQL Injection, Jinja2 預設防護 XSS)。 + +## 4. MVP 範圍 + +### Must Have (必備功能 - 初期實作) +- 新增與儲存食譜功能。 +- 食譜總覽與明細頁面。 +- 透過關鍵字搜尋食譜。 + +### Should Have (應有功能 - 中期實作) +- 用食材推薦/篩選食譜功能。 +- 編輯與刪除食譜。 + +### Nice to Have (加分功能 - 未來擴充) +- 食譜分類與標籤。 +- 食譜圖片上傳功能。 + +## 5. 專案成員與分工 + +| 角色 | 姓名 | 負責項目 | +| --- | --- | --- | +| PM | [請填寫] | 需求定義與時程控管 | +| 前端開發 | [請填寫] | 網頁介面設計、Jinja2 模板實作 | +| 後端開發 | [請填寫] | Flask API 與路由設定、SQLite 結構實作 | From a36d46955170c410013d3cfab56490fe86556666 Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 16 Apr 2026 08:30:58 +0800 Subject: [PATCH 03/15] docs: add system architecture --- docs/ARCHITECTURE.md | 84 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 docs/ARCHITECTURE.md diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 00000000..88f96938 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,84 @@ +# 系統架構設計文件:食譜管理系統 + +## 1. 技術架構說明 + +本專案採用輕量級的 Python 網頁框架設計,不採用前後端分離,由後端直接渲染 HTML 頁面返回給使用者的瀏覽器。 + +### 選用技術與原因 +- **後端框架:Python + Flask** + - **原因**:Flask 相較於 Django 更為輕量與彈性,非常適合用來開發這類中小型內容管理系統。其學習曲線平緩且易於快速打造原型。 +- **模板引擎:Jinja2** + - **原因**:與 Flask 高度整合,能直接將後端資料無縫嵌入前端 HTML 中,有效降低開發複雜度,並且內建自動轉義防護 XSS 攻擊。 +- **資料庫:SQLite** + - **原因**:不需額外安裝資料庫伺服器,資料儲存在單一檔案中,易於備份與部署,完全符合 MVP (最小可行性產品) 與個人使用的效能需求。 + +### Flask MVC 模式說明 +雖然 Flask 本身不強制要求 MVC 架構,但我們將依循 MVC (Model-View-Controller) 的概念來組織程式碼: +- **Model (模型)**:負責與 SQLite 資料庫溝通,定義「食譜 (Recipe)」、「標籤 (Tag)」等資料結構與操作。 +- **View (視圖)**:由 Jinja2 模板擔任,負責產生最終的 HTML 介面,呈現資料給使用者。 +- **Controller (控制器)**:由 Flask 的路由 (Routes) 擔任,負責接收使用者請求、調用 Model 獲取或更新資料、接著把資料傳遞給 View 進行渲染。 + +## 2. 專案資料夾結構 + +以下是本專案的資料夾結構與各階層職責說明: + +```text +web_app_development/ +├── app/ # 應用程式主目錄 +│ ├── __init__.py # 初始化 Flask 應用程式與套件配置 +│ ├── models/ # (Model) 資料庫模型定義與操作 +│ │ └── recipe.py # 食譜相關的資料庫邏輯 +│ ├── routes/ # (Controller) Flask 路由處理邏輯 +│ │ ├── __init__.py +│ │ └── recipe.py # 處理食譜的增刪改查與搜尋推薦路由 +│ ├── templates/ # (View) Jinja2 HTML 頁面模板 +│ │ ├── base.html # 共用版型 (標題、導覽列) +│ │ └── recipes/ # 食譜相關頁面 +│ │ ├── index.html # 食譜列表/首頁 +│ │ ├── show.html # 食譜明細頁面 +│ │ └── form.html # 新增與編輯表單 +│ └── static/ # css, js, 圖片等靜態資源 +│ ├── css/ +│ │ └── style.css +│ └── images/ # 存放使用者預設圖片等 +├── instance/ # 存放敏感或變動性的本地端資料 +│ └── database.db # SQLite 資料庫檔案 +├── docs/ # 專案文件 +│ ├── PRD.md # 產品需求文件 +│ └── ARCHITECTURE.md # 系統架構文件 +├── requirements.txt # Python 依賴套件清單 +└── app.py # 應用程式啟動入口 +``` + +## 3. 元件關係圖 + +以下展示使用者與系統互動時,各元件如何協作產生回應: + +```mermaid +sequenceDiagram + participant Browser as 瀏覽器 (使用者) + participant Route as Flask Route (Controller) + participant Model as Model (資料操作) + participant DB as SQLite 資料庫 + participant Template as Jinja2 Template (View) + + Browser->>Route: 1. 發送 HTTP 請求 (例如:搜尋食譜) + Route->>Model: 2. 呼叫邏輯獲取資料 + Model->>DB: 3. 執行 SQL 查詢 + DB-->>Model: 4. 回傳查詢結果 + Model-->>Route: 5. 回傳 Python 結構化資料 + Route->>Template: 6. 將資料傳入模板渲染 + Template-->>Route: 7. 產生最終 HTML 字串 + Route-->>Browser: 8. 回傳 HTTP 回應 (顯示網頁) +``` + +## 4. 關鍵設計決策 + +1. **採用 SSR (伺服器端渲染) 而非 SPA (單頁應用)** + - 作為初期 MVP 專案,使用 Flask + Jinja2 直接渲染頁面是最快看到成果的方式,避免了繁瑣的 API 介接與跨域問題,能讓重點集中於食譜核心邏輯。 +2. **採用 SQLite 單一檔案資料庫** + - 避免引入如 MySQL 或 PostgreSQL 帶來額外的效能與維護成本,SQLite 非常輕量且零設定,對於主要是單人使用的儲存與搜尋已綽綽有餘。 +3. **Model 與 Controller 邏輯分離** + - 雖然初期專案較小,但不把所有的資料庫存取層直接寫在 `app.py` 或具體的 route 中。將資料庫層抽象出 `app/models/`,能確保留下擴展「食譜標籤系統」與「食材推薦演算法」的彈性空間。 +4. **使用模板繼承機制 (共用 Base)** + - 透過 `base.html` 共用 Header、導覽列及自訂的 CSS 資源。日後不論是新增編輯頁面還是推薦食譜頁面,只需專注頁面本體,可達到最好的維護性並維持設計的一致感。 From 52b61c6dfbae203abee25b0f5c3640a145d0f487 Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 16 Apr 2026 08:31:49 +0800 Subject: [PATCH 04/15] docs: add user flowchart --- docs/FLOWCHART.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/FLOWCHART.md diff --git a/docs/FLOWCHART.md b/docs/FLOWCHART.md new file mode 100644 index 00000000..26428f1c --- /dev/null +++ b/docs/FLOWCHART.md @@ -0,0 +1,79 @@ +# 流程圖設計文件:食譜管理系統 + +本文件根據產品需求文件 (PRD) 與系統架構文件,視覺化使用者在食譜管理系統中的操作流程、系統背後的處理步驟,以及功能與路由的對照表。 + +## 1. 使用者流程圖(User Flow) + +以下流程圖說明當使用者開啟網頁後,可以執行的各項功能及頁面轉換路徑: + +```mermaid +flowchart LR + A([使用者開啟網站]) --> B[首頁 - 食譜列表] + + B --> C{選擇欲執行的功能} + + %% 新增食譜路線 + C -->|點擊「新增食譜」| D[填寫新增表單頁面] + D -->|送出表單| B + + %% 搜尋/篩選路線 + C -->|輸入「關鍵字 / 食材」| E[呈現篩選後的列表] + E --> C + + %% 查看與編輯/刪除路線 + C -->|點擊某個「食譜項目」| F[食譜明細頁面] + F --> G{在明細頁中選擇操作} + + G -->|返回| B + G -->|點擊「編輯食譜」| H[填寫編輯表單頁面] + H -->|送出表單| F + G -->|點擊「刪除食譜」| I[確認並刪除] + I -->|刪除成功| B +``` + +## 2. 系統序列圖(Sequence Diagram) + +以下序列圖以核心功能**「新增食譜」**為例,展示從使用者介面送出資料到成功寫入資料庫並重導向的完整過程: + +```mermaid +sequenceDiagram + actor User as 使用者 + participant Browser as 瀏覽器 (模板渲染) + participant Flask Route as 路由 (Controller) + participant Model as 邏輯模型 (Model) + participant DB as SQLite 資料庫 + + User->>Browser: 在表單頁面填妥食譜資訊並點擊送出 + Browser->>Flask Route: 發送 POST /recipes 請求 (攜帶表單資料) + + Note over Flask Route, DB: 開始處理新增邏輯 + + Flask Route->>Model: 呼叫 Recipe.create(data) 傳入解析後的資料 + Model->>DB: 執行 SQL INSERT INTO recipes ... + DB-->>Model: 回傳寫入成功訊息 + Model-->>Flask Route: 回傳新建立的 Recipe 物件 + + Note over Flask Route, Browser: 處理畫面重導向 + + Flask Route-->>Browser: 回傳 302 Redirect 至首頁 (食譜列表) + Browser->>Flask Route: 發送 GET / 請求 + Flask Route->>Model: 取得最新所有食譜列表 + Model->>DB: 執行 SELECT * FROM recipes + DB-->>Model: 回傳新列資料 + Model-->>Flask Route: 列表資料 + Flask Route-->>Browser: 使用最新資料重新渲染 index.html (首頁) +``` + +## 3. 功能清單對照表 + +對應上述流程與 PRD 需求,以下為系統功能對應的 URL 路徑與 HTTP 方法整理,提供後續 API/路由設計的參考: + +| 功能項目說明 | HTTP 方法 | 預計對應的 URL 路徑 | View (Jinja2) | 備註 | +| --- | :---: | --- | --- | --- | +| **首頁 / 所有食譜總覽** | `GET` | `/` 或 `/recipes` | `index.html` | 可結合查詢參數 `?q=關鍵字` 處理搜尋與食材推薦功能。 | +| **進入新增食譜表單頁** | `GET` | `/recipes/new` | `form.html` | 呈現空白的輸入表單。 | +| **提交新增食譜資料** | `POST` | `/recipes` | *(處理無畫面)* | 處理完後 302 重導向回首頁。 | +| **查看單一食譜明細** | `GET` | `/recipes/` | `show.html` | 顯示特定 ID 的食譜完整步驟與內容。 | +| **進入編輯食譜表單頁** | `GET` | `/recipes//edit` | `form.html` | 呈現帶有原始資料的編輯表單。 | +| **提交更新的食譜資料** | `POST` | `/recipes//update` | *(處理無畫面)* | 使用 HTML form 故用 POST,更新完成後重導回明細頁。 | +| **確定刪除食譜** | `POST` | `/recipes//delete` | *(處理無畫面)* | 使用 form 觸發 POST,刪除完成後重導回首頁。 | From 392a284908dd6f000204e2a7acb089bc1142e402 Mon Sep 17 00:00:00 2001 From: t11003 Date: Thu, 16 Apr 2026 15:55:07 +0800 Subject: [PATCH 05/15] Update SKILL.md to include .gitignore check Add section on checking .gitignore before pushing changes. --- .agents/skills/commit/SKILL.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/commit/SKILL.md index 3300e386..a0ab7154 100644 --- a/.agents/skills/commit/SKILL.md +++ b/.agents/skills/commit/SKILL.md @@ -13,6 +13,9 @@ description: 提交並推送程式碼變更。用於每個開發階段完成後 - 要把程式碼推送到 GitHub,讓組員可以取得最新版本 - 遇到 Git 要求設定 `user.name` 或 `user.email` 的提示 +## 檢查 .gitignore +在推送之前檢查 .gitignore,確保虛擬環境、環境設定與非必要檔案有正確被排除在版本控制之外。 + ## ⚠️ 設定 Git 使用者身份 如果 Git 顯示以下提示,需要先設定身份才能 commit: @@ -119,4 +122,4 @@ git push **Q: 不小心 commit 了不該 commit 的檔案?** -告訴 AI:「我剛才不小心 commit 了 [檔案名稱],請幫我移除。」 \ No newline at end of file +告訴 AI:「我剛才不小心 commit 了 [檔案名稱],請幫我移除。」 From 6ddcbdf13719a59a40593f3a334cdc4cc1903b40 Mon Sep 17 00:00:00 2001 From: t11003 Date: Thu, 16 Apr 2026 15:58:16 +0800 Subject: [PATCH 06/15] Update README to include system screenshot section Removed team member section and added a system screenshot section. --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 62cd639b..675ae50e 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,8 @@ --- -## 組員與分工(先寫自己就好) - -**第 X 組** - -| 姓名 | 學號 | 負責部分 | -| ---- | ---- | -------- | -| | | | +## 系統截圖 +請在此貼上系統的截圖畫面, 選擇一個功能邊操作邊截圖 --- From 0430354f0619f70fd15a0aa3e58c54fffd665b41 Mon Sep 17 00:00:00 2001 From: t11003 Date: Thu, 16 Apr 2026 15:58:41 +0800 Subject: [PATCH 07/15] =?UTF-8?q?Replace=20'=E7=B5=84=E5=93=A1=E8=88=87?= =?UTF-8?q?=E5=88=86=E5=B7=A5'=20with=20'=E7=B3=BB=E7=B5=B1=E6=88=AA?= =?UTF-8?q?=E5=9C=96'=20in=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 675ae50e..cc3edf2d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - [系統簡介](#系統簡介) - [技術棧](#技術棧) -- [組員與分工](#組員與分工) +- [系統截圖](#系統截圖) - [個人心得](#個人心得) --- From b8bbd4aa8f288ed642cd53bcbd5ace424962bd11 Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 16 Apr 2026 17:31:26 +0800 Subject: [PATCH 08/15] feat: add database schema and models --- .gitignore | 22 ++++++ app/models/__init__.py | 1 + app/models/recipe.py | 73 +++++++++++++++++++ database/schema.sql | 9 +++ docs/DB_DESIGN.md | 57 +++++++++++++++ ...46\344\275\234\350\252\252\346\230\216.md" | 6 +- 6 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 .gitignore create mode 100644 app/models/__init__.py create mode 100644 app/models/recipe.py create mode 100644 database/schema.sql create mode 100644 docs/DB_DESIGN.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8760da8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Environment +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# SQLite database and instance directory +instance/ +*.db +*.sqlite3 + +# OS generated files +.DS_Store +Thumbs.db diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 00000000..f01acc75 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1 @@ +# initialize app/models module diff --git a/app/models/recipe.py b/app/models/recipe.py new file mode 100644 index 00000000..4102d3fa --- /dev/null +++ b/app/models/recipe.py @@ -0,0 +1,73 @@ +import sqlite3 +import os +from contextlib import contextmanager + +# 取得資料庫檔案存放的目錄 instance/ (位於專案根目錄) +DB_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'instance') +DB_PATH = os.path.join(DB_DIR, 'database.db') + +@contextmanager +def get_db_connection(): + # 若目錄不存在則建立一個 + os.makedirs(DB_DIR, exist_ok=True) + conn = sqlite3.connect(DB_PATH) + # 將回傳結果設為 dict-like 的 Row 物件 + conn.row_factory = sqlite3.Row + try: + yield conn + finally: + conn.commit() + conn.close() + +class Recipe: + @staticmethod + def create(data): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO recipes (title, description, ingredients, steps) + VALUES (?, ?, ?, ?) + ''', (data['title'], data.get('description', ''), data['ingredients'], data['steps'])) + return cursor.lastrowid + + @staticmethod + def get_all(query=None): + with get_db_connection() as conn: + cursor = conn.cursor() + if query: + # 簡單支援對 title 或 ingredients 的 LIKE 搜尋 (搜尋推薦食譜) + search_term = f"%{query}%" + cursor.execute(''' + SELECT * FROM recipes + WHERE title LIKE ? OR ingredients LIKE ? + ORDER BY created_at DESC + ''', (search_term, search_term)) + else: + cursor.execute('SELECT * FROM recipes ORDER BY created_at DESC') + return [dict(row) for row in cursor.fetchall()] + + @staticmethod + def get_by_id(recipe_id): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM recipes WHERE id = ?', (recipe_id,)) + row = cursor.fetchone() + return dict(row) if row else None + + @staticmethod + def update(recipe_id, data): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE recipes + SET title = ?, description = ?, ingredients = ?, steps = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', (data['title'], data.get('description', ''), data['ingredients'], data['steps'], recipe_id)) + return cursor.rowcount > 0 + + @staticmethod + def delete(recipe_id): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute('DELETE FROM recipes WHERE id = ?', (recipe_id,)) + return cursor.rowcount > 0 diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 00000000..e356c0e1 --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS recipes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + ingredients TEXT NOT NULL, + steps TEXT NOT NULL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP +); diff --git a/docs/DB_DESIGN.md b/docs/DB_DESIGN.md new file mode 100644 index 00000000..855ce95d --- /dev/null +++ b/docs/DB_DESIGN.md @@ -0,0 +1,57 @@ +# DB Design Document — 資料庫設計文件 + +## 1. ER 圖 + +```mermaid +erDiagram + RECIPES { + INTEGER id PK + TEXT title "食譜名稱" + TEXT description "簡介" + TEXT ingredients "所需食材" + TEXT steps "烹飪步驟" + TEXT created_at "建立時間" + TEXT updated_at "最後更新時間" + } +``` + +## 2. 資料表詳細說明 + +### `recipes` 資料表 +本系統採用單一資料表設計以符合 MVP 需求,各欄位詳細定義如下: + +| 欄位名稱 | 型別 | 必填 | 說明 | +| --- | --- | --- | --- | +| `id` | `INTEGER` | 是 | Primary Key, 自動遞增 (AUTOINCREMENT)。 | +| `title` | `TEXT` | 是 | 食譜名稱。 | +| `description` | `TEXT` | 否 | 食譜的簡單介紹。 | +| `ingredients` | `TEXT` | 是 | 食譜的所需食材,為求簡便與彈性,儲存為純文字 (可依照換行或逗點分隔)。未來依食材推薦將利用 `LIKE` 查詢運算實作。 | +| `steps` | `TEXT` | 是 | 烹飪步驟,儲存為純文字即可。 | +| `created_at` | `TEXT` | 是 | 建立時間,使用 `CURRENT_TIMESTAMP` 的 ISO 格式儲存。 | +| `updated_at` | `TEXT` | 是 | 最後更新時間,編輯資料時觸發更新。 | + +## 3. SQL 建表語法 + +實作的 SQL 建表存放於 `database/schema.sql` 中: + +```sql +CREATE TABLE IF NOT EXISTS recipes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + ingredients TEXT NOT NULL, + steps TEXT NOT NULL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP +); +``` + +## 4. Python Model + +封裝的檔案位於 `app/models/recipe.py`。 +使用內建的 `sqlite3` 操作 `instance/database.db`,並支援: +- `create(data)` +- `get_all(query=None)`: 實作按標題與食材進行簡單字串 `LIKE` 搜尋。 +- `get_by_id(recipe_id)` +- `update(recipe_id, data)` +- `delete(recipe_id)` diff --git "a/\345\257\246\344\275\234\350\252\252\346\230\216.md" "b/\345\257\246\344\275\234\350\252\252\346\230\216.md" index 83b7a6b9..cc8c9399 100644 --- "a/\345\257\246\344\275\234\350\252\252\346\230\216.md" +++ "b/\345\257\246\344\275\234\350\252\252\346\230\216.md" @@ -195,8 +195,7 @@ commit 訊息:docs: add user flowchart **Step 4.4** — 請 AI 提交並推送: ``` -請幫我 commit 並推送目前的變更。 -commit 訊息:feat: add database schema and models +/commit ``` --- @@ -222,8 +221,7 @@ commit 訊息:feat: add database schema and models **Step 5.4** — 請 AI 提交並推送: ``` -請幫我 commit 並推送目前的變更。 -commit 訊息:feat: add route skeleton and template plan +/commit ``` --- From b1e202286b30a2326e6b95816ccae8fa3f8fb5a1 Mon Sep 17 00:00:00 2001 From: antigravity Date: Thu, 16 Apr 2026 17:35:41 +0800 Subject: [PATCH 09/15] feat: initialize application routes and add recipe blueprint support --- app.py | 16 ++++++++++ app/routes/__init__.py | 2 ++ app/routes/recipe.py | 56 +++++++++++++++++++++++++++++++++ docs/ROUTES.md | 70 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 app.py create mode 100644 app/routes/__init__.py create mode 100644 app/routes/recipe.py create mode 100644 docs/ROUTES.md diff --git a/app.py b/app.py new file mode 100644 index 00000000..41549209 --- /dev/null +++ b/app.py @@ -0,0 +1,16 @@ +from flask import Flask +from app.routes.recipe import recipe_bp + +def create_app(): + # 指定 template 與 static 的存放路徑,確保與專案結構相符 + app = Flask(__name__, template_folder='app/templates', static_folder='app/static') + + # 註冊 Blueprints + app.register_blueprint(recipe_bp) + + return app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True) diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 00000000..d1a57de5 --- /dev/null +++ b/app/routes/__init__.py @@ -0,0 +1,2 @@ +# init routes +from app.routes.recipe import recipe_bp diff --git a/app/routes/recipe.py b/app/routes/recipe.py new file mode 100644 index 00000000..d446bcbc --- /dev/null +++ b/app/routes/recipe.py @@ -0,0 +1,56 @@ +from flask import Blueprint, request, render_template, redirect, url_for, abort + +recipe_bp = Blueprint('recipe', __name__) + +@recipe_bp.route('/') +def index(): + """ + 列出所有食譜。 + 若有 ?q= 參數則篩選標題與食材。 + """ + pass + +@recipe_bp.route('/recipes/new') +def new_recipe(): + """ + 顯示新增食譜表單頁面。 + """ + pass + +@recipe_bp.route('/recipes', methods=['POST']) +def create_recipe(): + """ + 處理新增食譜請求。 + 接收表單資料,寫入 DB,完成後導回首頁。 + """ + pass + +@recipe_bp.route('/recipes/') +def show_recipe(id): + """ + 顯示單一食譜的詳細資料。 + """ + pass + +@recipe_bp.route('/recipes//edit') +def edit_recipe(id): + """ + 顯示修改食譜的表單頁面,將原資料填入表單中。 + """ + pass + +@recipe_bp.route('/recipes//update', methods=['POST']) +def update_recipe(id): + """ + 處裡食譜修改請求並寫入。 + 更新後導回食譜明細頁。 + """ + pass + +@recipe_bp.route('/recipes//delete', methods=['POST']) +def delete_recipe(id): + """ + 刪除指定食譜。 + 刪除後導向回首頁。 + """ + pass diff --git a/docs/ROUTES.md b/docs/ROUTES.md new file mode 100644 index 00000000..2514b340 --- /dev/null +++ b/docs/ROUTES.md @@ -0,0 +1,70 @@ +# 路由設計文件 (API Design) + +## 1. 路由總覽列表 +| 功能 | HTTP 方法 | URL 路徑 | 對應模板 | 說明 | +| --- | --- | --- | --- | --- | +| 食譜列表 (首頁) | `GET` | `/` | `templates/recipes/index.html` | 顯示所有食譜,支援 `?q=` 查詢參數進行名稱/食材搜尋。 | +| 新增食譜頁面 | `GET` | `/recipes/new` | `templates/recipes/form.html` | 顯示空白的新增食譜表單。 | +| 建立食譜 | `POST` | `/recipes` | — | 接收表單資料、存入 DB,成功後重導回首頁。 | +| 食譜明細 | `GET` | `/recipes/` | `templates/recipes/show.html` | 顯示特定食譜的食材與步驟。若查無資料回傳 404。 | +| 編輯食譜頁面 | `GET` | `/recipes//edit` | `templates/recipes/form.html` | 顯示現有食譜資料供修改。 | +| 更新食譜 | `POST` | `/recipes//update` | — | 接收更新表單並寫回 DB,完成後重導至食譜明細。 | +| 刪除食譜 | `POST` | `/recipes//delete` | — | 從 DB 中刪除該筆資料,完成後重導回首頁。 | + +--- + +## 2. 路由詳細說明 + +### `GET /` (食譜列表) +- **輸入**: URL 查詢參數 `?q=keyword` (可選) +- **處理邏輯**: + - 若有 `q` 則呼叫 `Recipe.get_all(q)` 並搜尋。 + - 若無則呼叫 `Recipe.get_all()` 取得最新食譜清單。 +- **輸出**: 渲染 `recipes/index.html`,傳入食譜列表變數 `recipes`。 +- **錯誤處理**: 資料庫如讀取錯誤報 500。 + +### `GET /recipes/new` (新增輸入頁面) +- **輸入**: 無 +- **處理邏輯**: 準備畫面即可。 +- **輸出**: 渲染 `recipes/form.html`。 +- **錯誤處理**: 無特殊錯誤。 + +### `POST /recipes` (建立食譜) +- **輸入**: Form Data 包含 `title`, `description`, `ingredients`, `steps`。 +- **處理邏輯**: 接收 Form,呼叫 `Recipe.create(data)`。 +- **輸出**: Http 302 重新導向至 `/` (首頁)。 +- **錯誤處理**: 若 `title`, `ingredients`, `steps` 等必填欄位缺失,透過 Flash Message 提示並導回 `/recipes/new`。 + +### `GET /recipes/` (食譜明細) +- **輸入**: URL 參數 `id`。 +- **處理邏輯**: 呼叫 `Recipe.get_by_id(id)` 取得指定食譜。 +- **輸出**: 渲染 `recipes/show.html`,傳入 `recipe`。 +- **錯誤處理**: 若 `recipe` 為空 (None),拋出 HTTP 404 (Not Found)。 + +### `GET /recipes//edit` (編輯食譜頁面) +- **輸入**: URL 參數 `id`。 +- **處理邏輯**: 呼叫 `Recipe.get_by_id(id)` 取得指定食譜。 +- **輸出**: 渲染 `recipes/form.html`,傳入 `recipe` 將現有值帶入欄位。 +- **錯誤處理**: 若查無結果,拋出 404。 + +### `POST /recipes//update` (更新食譜) +- **輸入**: URL 參數 `id` 與被更新的表單資料。 +- **處理邏輯**: 呼叫 `Recipe.update(id, data)`。 +- **輸出**: 更新成功後重新導向回詳細頁面 `/recipes/`。 +- **錯誤處理**: 查無食譜 (404) 或欄位缺失等驗證同建立食譜的處理方式。 + +### `POST /recipes//delete` (刪除食譜) +- **輸入**: URL 參數 `id`。 +- **處理邏輯**: 呼叫 `Recipe.delete(id)` 進行刪除。 +- **輸出**: 刪除成功後重新導向至 `/`。 +- **錯誤處理**: 若查無食譜則拋出 404。 + +--- + +## 3. Jinja2 模板清單 + +以下檔案後續將於 `app/templates` 建立: +1. `base.html`: 包含全站共用的 ``、導覽列與外觀樣式。 +2. `recipes/index.html`: 繼承 `base.html`,以卡片或列表結構呈現食譜總覽。 +3. `recipes/show.html`: 繼承 `base.html`,全版詳細呈現食材清單與步驟說明。 +4. `recipes/form.html`: 繼承 `base.html`,一個頁面同時支援新增 (Create) 與編輯 (Update)。依據後端是否丟出已存在的 `recipe` 來判斷是 POST 到 `/recipes` 或 `/recipes//update`。 From 1c3ee19e833ea34db7e4f55dc9aa5a3d3333048f Mon Sep 17 00:00:00 2001 From: antigravity Date: Mon, 27 Apr 2026 14:14:15 +0800 Subject: [PATCH 10/15] docs: add PRD --- docs/PRD.md | 92 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 37 deletions(-) diff --git a/docs/PRD.md b/docs/PRD.md index ee29c7e9..a8dd6bfb 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -1,52 +1,70 @@ -# 產品需求文件 (PRD):食譜管理系統 +# 產品需求文件 (PRD) - 個人記帳簿系統 ## 1. 專案概述 -- **背景與動機**:現代人生活忙碌,常常不知道要煮什麼,或是想要把好吃的食譜記錄下來以便未來查詢。此系統提供一個簡單、直覺的平台讓使用者存放、尋找,甚至根據手邊現有食材自動尋找推薦的食譜。 -- **目標用戶**:我 (個人愛好者、家庭煮夫/婦、對料理有熱忱的自煮族)。 -- **核心價值主張**:更有效率地管理個人食譜,並解決「今天不知道要吃什麼」的煩惱,輔助使用者最大化利用家中現存食材。 + +### 背景與動機 +許多學生在日常生活中常面臨收支管理不善、月底吃土的問題。為了幫助學生建立良好的財務觀念,本系統旨在提供一個簡單易用的個人記帳本,記錄並追蹤日常收入與支出,讓用戶能夠清晰掌握資金動向與餘額狀況。 + +### 目標用戶 +- 學生(高中生、大學生) + +### 核心價值主張 +提供直覺且專注收支記錄的記帳系統,幫助學生輕鬆分類消費、設定固定扣款,並透過統計功能快速掌握目前的財務健康度。 + +--- ## 2. 功能需求 -系統將提供以下主要功能: - -1. **新增食譜** - - *使用者故事*:作為一位熱愛烹飪的使用者,我希望能夠新增食譜(記錄名稱、食材、步驟等),以便儲存我喜歡的菜餚。 -2. **搜尋食譜** - - *使用者故事*:作為一位使用者,我希望能透過關鍵字搜尋食譜名稱,以便快速找到我想做的菜。 -3. **用食材推薦食譜** - - *使用者故事*:作為一位面對冰箱剩餘食材的使用者,我希望能夠輸入手邊現有食材,由系統過濾並推薦能做的食譜,以便不浪費食物。 -4. **編輯與刪除食譜** - - *使用者故事*:作為一位記錄者,我希望可以能夠編輯或刪除已經存入的食譜,以便我的食譜庫隨時保持正確、最新。 -5. **食譜分類與標籤** - - *使用者故事*:作為一位習慣有條理整理食譜的使用者,我希望可以為食譜加上分類或標籤(如:前菜、甜點、全素、低卡等),以便後續依照標籤快速篩選食譜。 + +### 主要功能與使用者故事 + +1. **收入分類** + - *使用者故事*:作為學生,我希望能夠新增並分類我的收入(如:零用錢、打工薪水、獎學金),以便我清楚知道錢從哪裡來。 + +2. **支出分類** + - *使用者故事*:作為學生,我希望能夠記錄支出並將其分類(如:餐飲、交通、娛樂、學習),以便我分析每個月或每週把錢花在什麼地方。 + +3. **每月固定扣款** + - *使用者故事*:作為學生,我希望系統能夠自動記錄或設定每月的固定扣款(如:手機費、定期的訂閱服務費),以便我不需要每個月重複手動輸入這筆必定發生的支出。 + +4. **總餘額統計** + - *使用者故事*:作為學生,我希望能在首頁即時看到目前的總餘額,以便我隨時了解自己目前可動用的剩餘資金。 + +5. **一段時間內的收入和支出查詢** + - *使用者故事*:作為學生,我希望能夠查詢特定時間段(如:本月、上週、特定日期區間)的收入與支出總計,以便我進行短期或長期的預算檢討與規劃。 + +--- ## 3. 非功能需求 -- **技術限制**: - - 後端框架:Python + Flask - - 模板引擎:Jinja2 - - 資料庫:SQLite -- **效能與安全考量**: - - 確保食譜搜尋可快速回傳結果。 - - 使用適當的安全機制來防範常見的安全漏洞(如:防護 SQL Injection, Jinja2 預設防護 XSS)。 -## 4. MVP 範圍 +### 技術限制 +- 前端與後端:Flask + Jinja2 +- 資料庫:SQLite -### Must Have (必備功能 - 初期實作) -- 新增與儲存食譜功能。 -- 食譜總覽與明細頁面。 -- 透過關鍵字搜尋食譜。 +### 效能與安全考量 +- **效能考量**:系統需能在使用者輸入資料後,迅速更新餘額與報表。資料查詢(特別是一段時間的篩選)應具備良好反應速度。 +- **安全考量**:若未來發展至多用戶版本,需實作用戶身份驗證與授權機制,確保個別用戶的財務資料具備隱私性且不被他人存取。系統輸入應防範基本的 SQL Injection 與 XSS 攻擊。 -### Should Have (應有功能 - 中期實作) -- 用食材推薦/篩選食譜功能。 -- 編輯與刪除食譜。 +--- + +## 4. MVP 範圍 (Minimum Viable Product) + +| 功能項目 | MVP 層級 | 說明 | +| --- | --- | --- | +| 收入/支出記錄與分類 | Must Have | 最基本的記帳功能,支援自定義分類 | +| 總餘額即時計算與顯示 | Must Have | 首頁的核心數據呈現 | +| 一段時間的收支查詢 | Must Have | 篩選特定區間並顯示總和 | +| 每月固定扣款設定 | Should Have | 方便自動扣款與減少重複操作,可在第二階段優化 | +| 收支圓餅圖或長條圖顯示 | Nice to Have | 視覺化報表,讓數據更直觀 | +| 匯出報表功能 (如 CSV) | Nice to Have | 供進階梳理需求使用 | -### Nice to Have (加分功能 - 未來擴充) -- 食譜分類與標籤。 -- 食譜圖片上傳功能。 +--- ## 5. 專案成員與分工 | 角色 | 姓名 | 負責項目 | | --- | --- | --- | -| PM | [請填寫] | 需求定義與時程控管 | -| 前端開發 | [請填寫] | 網頁介面設計、Jinja2 模板實作 | -| 後端開發 | [請填寫] | Flask API 與路由設定、SQLite 結構實作 | +| PM / SA | [林呈諺] | 需求分析、PRD 撰寫與更新 | +| UI/UX 設計師 | [林呈諺] | 介面規劃、使用者體驗設計 | +| 前端工程師 | [林呈諺] | Jinja2 模板實作、前端互動 | +| 後端工程師 | [林呈諺] | Flask 路由、SQLite Schema 設計與 API 實作 | +| 測試工程師 | [林呈諺] | 功能驗證與錯誤回報 | From 5bb4ea6de92416115d660306a68965083e2dd905 Mon Sep 17 00:00:00 2001 From: antigravity Date: Mon, 27 Apr 2026 14:48:55 +0800 Subject: [PATCH 11/15] docs: add system architecture --- docs/ARCHITECTURE.md | 114 +++++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 59 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 88f96938..93c9fb06 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,84 +1,80 @@ -# 系統架構設計文件:食譜管理系統 +# 系統架構文件 (Architecture) - 個人記帳簿系統 ## 1. 技術架構說明 -本專案採用輕量級的 Python 網頁框架設計,不採用前後端分離,由後端直接渲染 HTML 頁面返回給使用者的瀏覽器。 +根據 PRD 提出的需求與技術限制,我們選擇以下技術組合: -### 選用技術與原因 - **後端框架:Python + Flask** - - **原因**:Flask 相較於 Django 更為輕量與彈性,非常適合用來開發這類中小型內容管理系統。其學習曲線平緩且易於快速打造原型。 + - **原因**:Flask 相較於 Django 更為輕量與彈性,非常適合用來開發這類功能集中且單純的個人記帳簿系統。 - **模板引擎:Jinja2** - - **原因**:與 Flask 高度整合,能直接將後端資料無縫嵌入前端 HTML 中,有效降低開發複雜度,並且內建自動轉義防護 XSS 攻擊。 + - **原因**:Jinja2 與 Flask 高度整合,能直接將後端資料無縫嵌入前端 HTML 中,有效降低開發複雜度。不需要建置前後端分離架構,所有頁面直接在伺服器渲染回傳。 - **資料庫:SQLite** - - **原因**:不需額外安裝資料庫伺服器,資料儲存在單一檔案中,易於備份與部署,完全符合 MVP (最小可行性產品) 與個人使用的效能需求。 + - **原因**:這是一個「零配置」的關聯式資料庫,所有資料都儲存在單一檔案中,無需額外安裝或管理龐大的資料庫伺服器(如 MySQL 或 PostgreSQL),非常適合單機版或個人的網頁應用程式。 ### Flask MVC 模式說明 -雖然 Flask 本身不強制要求 MVC 架構,但我們將依循 MVC (Model-View-Controller) 的概念來組織程式碼: -- **Model (模型)**:負責與 SQLite 資料庫溝通,定義「食譜 (Recipe)」、「標籤 (Tag)」等資料結構與操作。 -- **View (視圖)**:由 Jinja2 模板擔任,負責產生最終的 HTML 介面,呈現資料給使用者。 -- **Controller (控制器)**:由 Flask 的路由 (Routes) 擔任,負責接收使用者請求、調用 Model 獲取或更新資料、接著把資料傳遞給 View 進行渲染。 +雖然 Flask 不像某些框架有嚴格的 MVC (Model-View-Controller) 規定,但我們依舊會採用類似概念來分類程式碼: +- **Model (資料模型)**:負責與 SQLite 資料庫溝通,定義「收入 (Income)」跟「支出 (Expense)」這些資料表記錄,並撰寫新增、修改、刪除的方法。 +- **View (視圖)**:負責呈現使用者見到的 UI 介面,在系統中就是由 **Jinja2** 組裝並渲染的 HTML 檔案。 +- **Controller (控制器)**:負責接收網頁請求與商業邏輯,在系統中就是 **Flask 的 Routes (路由)**。它會接收來自瀏覽器的操作(如:「新增一筆晚餐支出」),並指揮 Model 將結果儲存,最後選擇對應的 Jinja2 Template (View) 來顯示結果給使用者。 + +--- ## 2. 專案資料夾結構 -以下是本專案的資料夾結構與各階層職責說明: +保持架構簡潔且易於擴充,我們規劃了如下資料夾結構: ```text web_app_development/ -├── app/ # 應用程式主目錄 -│ ├── __init__.py # 初始化 Flask 應用程式與套件配置 -│ ├── models/ # (Model) 資料庫模型定義與操作 -│ │ └── recipe.py # 食譜相關的資料庫邏輯 -│ ├── routes/ # (Controller) Flask 路由處理邏輯 -│ │ ├── __init__.py -│ │ └── recipe.py # 處理食譜的增刪改查與搜尋推薦路由 -│ ├── templates/ # (View) Jinja2 HTML 頁面模板 -│ │ ├── base.html # 共用版型 (標題、導覽列) -│ │ └── recipes/ # 食譜相關頁面 -│ │ ├── index.html # 食譜列表/首頁 -│ │ ├── show.html # 食譜明細頁面 -│ │ └── form.html # 新增與編輯表單 -│ └── static/ # css, js, 圖片等靜態資源 -│ ├── css/ -│ │ └── style.css -│ └── images/ # 存放使用者預設圖片等 -├── instance/ # 存放敏感或變動性的本地端資料 -│ └── database.db # SQLite 資料庫檔案 -├── docs/ # 專案文件 -│ ├── PRD.md # 產品需求文件 -│ └── ARCHITECTURE.md # 系統架構文件 -├── requirements.txt # Python 依賴套件清單 -└── app.py # 應用程式啟動入口 +├── app/ +│ ├── models/ ← (Model) 資料庫模型:定義資料表與資料存取邏輯 +│ ├── routes/ ← (Controller) Flask 路由:處理各個頁面的請求 +│ ├── templates/ ← (View) Jinja2 HTML 模板:畫面呈現 (如 Base 共用版型) +│ └── static/ ← CSS / JS 靜態資源:例如自定義的樣式表或圖表的 JS +├── instance/ +│ └── database.db ← SQLite 資料庫檔案:系統自動產生,存放實際記帳資料 +├── docs/ +│ ├── PRD.md ← 產品需求文件 +│ └── ARCHITECTURE.md ← 系統架構文件 (本檔案) +├── app.py ← 應用程式入口點:負責啟動 Flask 伺服器並註冊各路由 +└── requirements.txt ← 專案依賴套件表:記錄需要的模組清單 ``` +--- + ## 3. 元件關係圖 -以下展示使用者與系統互動時,各元件如何協作產生回應: +以下是用以表示使用者在操作記帳系統時,各元件如何互相配合的流程關係: ```mermaid -sequenceDiagram - participant Browser as 瀏覽器 (使用者) - participant Route as Flask Route (Controller) - participant Model as Model (資料操作) - participant DB as SQLite 資料庫 - participant Template as Jinja2 Template (View) - - Browser->>Route: 1. 發送 HTTP 請求 (例如:搜尋食譜) - Route->>Model: 2. 呼叫邏輯獲取資料 - Model->>DB: 3. 執行 SQL 查詢 - DB-->>Model: 4. 回傳查詢結果 - Model-->>Route: 5. 回傳 Python 結構化資料 - Route->>Template: 6. 將資料傳入模板渲染 - Template-->>Route: 7. 產生最終 HTML 字串 - Route-->>Browser: 8. 回傳 HTTP 回應 (顯示網頁) +flowchart TD + Browser["瀏覽器 \n(Browser)"] + Router["Flask Route \n(Controller)"] + Model["資料模型 \n(Model)"] + DB[("SQLite 資料庫 \n(Database)")] + Template["Jinja2 Template \n(View)"] + + Browser -- "1. 請求 (如新增支出或看報表)" --> Router + Router -- "2. 查詢/寫入資料" --> Model + Model -- "3. 讀寫執行" --> DB + DB -- "4. 回傳資料" --> Model + Model -- "5. 將資料回傳" --> Router + Router -- "6. 傳入模板渲染" --> Template + Template -- "7. 渲染 HTML" --> Router + Router -- "8. 回傳頁面給使用者" --> Browser ``` +--- + ## 4. 關鍵設計決策 -1. **採用 SSR (伺服器端渲染) 而非 SPA (單頁應用)** - - 作為初期 MVP 專案,使用 Flask + Jinja2 直接渲染頁面是最快看到成果的方式,避免了繁瑣的 API 介接與跨域問題,能讓重點集中於食譜核心邏輯。 -2. **採用 SQLite 單一檔案資料庫** - - 避免引入如 MySQL 或 PostgreSQL 帶來額外的效能與維護成本,SQLite 非常輕量且零設定,對於主要是單人使用的儲存與搜尋已綽綽有餘。 -3. **Model 與 Controller 邏輯分離** - - 雖然初期專案較小,但不把所有的資料庫存取層直接寫在 `app.py` 或具體的 route 中。將資料庫層抽象出 `app/models/`,能確保留下擴展「食譜標籤系統」與「食材推薦演算法」的彈性空間。 -4. **使用模板繼承機制 (共用 Base)** - - 透過 `base.html` 共用 Header、導覽列及自訂的 CSS 資源。日後不論是新增編輯頁面還是推薦食譜頁面,只需專注頁面本體,可達到最好的維護性並維持設計的一致感。 +1. **採用單體式與伺服器端渲染 (SSR)** + - *原因*:為了加速開發並符合不需前後端分離的限制,我們選擇將所有的畫面依賴 Jinja2 渲染完成。相較於架設一套 React/Vue 的架構,這種方式不必花時間設計複雜的 JSON API,能讓我們將重點集中在核心財務紀錄功能。 + +2. **目錄模組化設計 (`app/models` 與 `app/routes`)** + - *原因*:雖然這是一個 MVP 小型系統,但提早將路由與資料庫模型進行拆分,不僅能避免 `app.py` 變得過於龐大,如果未來要擴增新的圖表功能 (Nice to Have) 或資料匯出功能,也能保持優良的維護性。 + +3. **每月固定扣款的簡化實作** + - *原因*:PRD 中有提到「每月固定扣款」(Should Have) 功能。初期的 MVP 設計可由後端邏輯判斷是否跨月並「自動在背景新增記錄」,而非導入 Celery 等複雜的背景定期任務佇列,如此一來既能確保功能實作也維持了架構的輕量。 + +4. **採用關聯式資料庫架構** + - *原因*:財務記帳系統的特徵在於有明確且規律的資料屬性,例如:分類、金額、日期等。故比起 NoSQL,使用關聯式體系的 SQLite 能帶來更好的資料一致性限制,若之後系統要向外擴展 (Scale up),轉換至 PostgreSQL 的門檻也會較低。 From 402c352924c2e9c522181dd60ac6c6f135df9c74 Mon Sep 17 00:00:00 2001 From: antigravity Date: Mon, 27 Apr 2026 14:55:01 +0800 Subject: [PATCH 12/15] docs: add user flowchart --- docs/FLOWCHART.md | 102 +++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 55 deletions(-) diff --git a/docs/FLOWCHART.md b/docs/FLOWCHART.md index 26428f1c..12622a3b 100644 --- a/docs/FLOWCHART.md +++ b/docs/FLOWCHART.md @@ -1,79 +1,71 @@ -# 流程圖設計文件:食譜管理系統 +# 流程圖設計文件 (Flowcharts) - 個人記帳簿系統 -本文件根據產品需求文件 (PRD) 與系統架構文件,視覺化使用者在食譜管理系統中的操作流程、系統背後的處理步驟,以及功能與路由的對照表。 +本文件基於 PRD 的需求與架構設計,展示系統的「使用者流程」與「系統序列流程」,以協助視覺化系統運作方式。 -## 1. 使用者流程圖(User Flow) +## 1. 使用者流程圖 (User Flow) -以下流程圖說明當使用者開啟網頁後,可以執行的各項功能及頁面轉換路徑: +描述使用者進入系統後,可能進行的各項操作路徑。 ```mermaid flowchart LR - A([使用者開啟網站]) --> B[首頁 - 食譜列表] + Start([使用者開啟網頁]) --> Home[首頁 - 儀表板\n(顯示目前總餘額)] - B --> C{選擇欲執行的功能} + Home --> Action{要執行什麼操作?} - %% 新增食譜路線 - C -->|點擊「新增食譜」| D[填寫新增表單頁面] - D -->|送出表單| B + Action -->|新增收支| AddSelect[選擇新增收入或支出] + AddSelect -->|輸入金額與分類| FillForm[填寫資料表單] + FillForm --> SubmitAdd[送出儲存] + SubmitAdd --> Home - %% 搜尋/篩選路線 - C -->|輸入「關鍵字 / 食材」| E[呈現篩選後的列表] - E --> C + Action -->|查詢或篩選| Records[進入歷史收支紀錄頁面] + Records -->|選擇時間區間| Filter[套用日期或月份條件] + Filter --> ShowResult[顯示篩選後的收支結果列表] + ShowResult --> Home - %% 查看與編輯/刪除路線 - C -->|點擊某個「食譜項目」| F[食譜明細頁面] - F --> G{在明細頁中選擇操作} - - G -->|返回| B - G -->|點擊「編輯食譜」| H[填寫編輯表單頁面] - H -->|送出表單| F - G -->|點擊「刪除食譜」| I[確認並刪除] - I -->|刪除成功| B + Action -->|管理固定扣款| Fixed[進入固定扣款設定頁] + Fixed -->|新增固定扣款| AddFixed[填寫每月固定扣款項目] + AddFixed --> Home ``` -## 2. 系統序列圖(Sequence Diagram) +--- + +## 2. 系統序列圖 (System Sequence Diagram) -以下序列圖以核心功能**「新增食譜」**為例,展示從使用者介面送出資料到成功寫入資料庫並重導向的完整過程: +以下以「使用者操作新增一筆支出」為例,展示前端瀏覽器、Flask 路由、Model 與 SQLite 資料庫之間的互動流程。 ```mermaid sequenceDiagram actor User as 使用者 - participant Browser as 瀏覽器 (模板渲染) - participant Flask Route as 路由 (Controller) - participant Model as 邏輯模型 (Model) + participant Browser as 瀏覽器 (HTML 介面) + participant Flask as Flask Route (Controller) + participant Model as Expense Model participant DB as SQLite 資料庫 - - User->>Browser: 在表單頁面填妥食譜資訊並點擊送出 - Browser->>Flask Route: 發送 POST /recipes 請求 (攜帶表單資料) - - Note over Flask Route, DB: 開始處理新增邏輯 - Flask Route->>Model: 呼叫 Recipe.create(data) 傳入解析後的資料 - Model->>DB: 執行 SQL INSERT INTO recipes ... - DB-->>Model: 回傳寫入成功訊息 - Model-->>Flask Route: 回傳新建立的 Recipe 物件 - - Note over Flask Route, Browser: 處理畫面重導向 - - Flask Route-->>Browser: 回傳 302 Redirect 至首頁 (食譜列表) - Browser->>Flask Route: 發送 GET / 請求 - Flask Route->>Model: 取得最新所有食譜列表 - Model->>DB: 執行 SELECT * FROM recipes - DB-->>Model: 回傳新列資料 - Model-->>Flask Route: 列表資料 - Flask Route-->>Browser: 使用最新資料重新渲染 index.html (首頁) + User->>Browser: 點擊「新增支出」,填寫金額與分類 + User->>Browser: 點擊「送出」按鈕 + Browser->>Flask: POST /expense/add (傳送表單資料) + Flask->>Flask: 驗證接收到的表單格式與資料 + Flask->>Model: 呼叫 add_expense(amount, category, date) + Model->>DB: 執行 INSERT INTO expenses ... + DB-->>Model: 回傳寫入成功 + Model-->>Flask: 處理完成 + Flask-->>Browser: 重導向 (Redirect) 回首頁 + Browser-->>User: 顯示最新總餘額與該筆支出紀錄 ``` +--- + ## 3. 功能清單對照表 -對應上述流程與 PRD 需求,以下為系統功能對應的 URL 路徑與 HTTP 方法整理,提供後續 API/路由設計的參考: +整理未來需要實作的路由對應表,作為之後 API 設計或模板實作的參考依據。 -| 功能項目說明 | HTTP 方法 | 預計對應的 URL 路徑 | View (Jinja2) | 備註 | -| --- | :---: | --- | --- | --- | -| **首頁 / 所有食譜總覽** | `GET` | `/` 或 `/recipes` | `index.html` | 可結合查詢參數 `?q=關鍵字` 處理搜尋與食材推薦功能。 | -| **進入新增食譜表單頁** | `GET` | `/recipes/new` | `form.html` | 呈現空白的輸入表單。 | -| **提交新增食譜資料** | `POST` | `/recipes` | *(處理無畫面)* | 處理完後 302 重導向回首頁。 | -| **查看單一食譜明細** | `GET` | `/recipes/` | `show.html` | 顯示特定 ID 的食譜完整步驟與內容。 | -| **進入編輯食譜表單頁** | `GET` | `/recipes//edit` | `form.html` | 呈現帶有原始資料的編輯表單。 | -| **提交更新的食譜資料** | `POST` | `/recipes//update` | *(處理無畫面)* | 使用 HTML form 故用 POST,更新完成後重導回明細頁。 | -| **確定刪除食譜** | `POST` | `/recipes//delete` | *(處理無畫面)* | 使用 form 觸發 POST,刪除完成後重導回首頁。 | +| 功能名稱 | 說明 | URL 路徑 | HTTP 動作 | +| --- | --- | --- | --- | +| **首頁與總餘額** | 顯示當前可用餘額與最近幾筆收支紀錄,若有跨月狀況,載入時負責觸發自動建立固定扣款。 | `/` | `GET` | +| **新增收入頁面** | 渲染新增收入的表單畫面。 | `/income/add` | `GET` | +| **處理新增收入** | 處理表單送出的收入資料,寫入後重導至首頁。 | `/income/add` | `POST` | +| **新增支出頁面** | 渲染新增支出的表單畫面。 | `/expense/add` | `GET` | +| **處理新增支出** | 處理表單送出的支出資料,寫入後重導至首頁。 | `/expense/add` | `POST` | +| **收支查詢清單** | 顯示所有紀錄,支援讀取 URL query 參數(如 `?start_date=xxx`)來過濾特定區間。 | `/records` | `GET` | +| **固定扣款設定頁** | 顯示與管理每月的固定扣款項目。 | `/fixed-deduction` | `GET` | +| **新增固定扣款** | 處理表單送出的固定扣款資料。 | `/fixed-deduction` | `POST` | From ac6f8122fd3ed59b43ddbefd5708796da7c6e003 Mon Sep 17 00:00:00 2001 From: antigravity Date: Mon, 27 Apr 2026 14:59:51 +0800 Subject: [PATCH 13/15] feat: add database schema and models --- app/models/db.py | 21 +++++++ app/models/fixed_deduction.py | 38 ++++++++++++ app/models/transaction.py | 54 +++++++++++++++++ database/schema.sql | 22 ++++--- docs/DB_DESIGN.md | 108 ++++++++++++++++++++++------------ 5 files changed, 200 insertions(+), 43 deletions(-) create mode 100644 app/models/db.py create mode 100644 app/models/fixed_deduction.py create mode 100644 app/models/transaction.py diff --git a/app/models/db.py b/app/models/db.py new file mode 100644 index 00000000..b09fcde1 --- /dev/null +++ b/app/models/db.py @@ -0,0 +1,21 @@ +import sqlite3 +import os + +DB_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'instance', 'database.db') + +def get_db_connection(): + """建立並回傳資料庫連線,設定 row_factory 讓結果可透過字典的 key 來存取。""" + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + +def init_db(): + """初始化資料庫實體,執行 schema.sql。""" + schema_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'database', 'schema.sql') + if not os.path.exists(schema_path): + return + with open(schema_path, 'r', encoding='utf-8') as f: + schema = f.read() + with get_db_connection() as conn: + conn.executescript(schema) diff --git a/app/models/fixed_deduction.py b/app/models/fixed_deduction.py new file mode 100644 index 00000000..cc67a1e0 --- /dev/null +++ b/app/models/fixed_deduction.py @@ -0,0 +1,38 @@ +from app.models.db import get_db_connection + +class FixedDeduction: + @staticmethod + def create(amount, category, deduct_day): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute( + "INSERT INTO fixed_deductions (amount, category, deduct_day) VALUES (?, ?, ?)", + (amount, category, deduct_day) + ) + conn.commit() + return cursor.lastrowid + + @staticmethod + def get_all(): + with get_db_connection() as conn: + return conn.execute("SELECT * FROM fixed_deductions ORDER BY deduct_day ASC").fetchall() + + @staticmethod + def get_by_id(deduction_id): + with get_db_connection() as conn: + return conn.execute("SELECT * FROM fixed_deductions WHERE id = ?", (deduction_id,)).fetchone() + + @staticmethod + def update_last_processed(deduction_id, processed_month): + with get_db_connection() as conn: + conn.execute( + "UPDATE fixed_deductions SET last_processed_month = ? WHERE id = ?", + (processed_month, deduction_id) + ) + conn.commit() + + @staticmethod + def delete(deduction_id): + with get_db_connection() as conn: + conn.execute("DELETE FROM fixed_deductions WHERE id = ?", (deduction_id,)) + conn.commit() diff --git a/app/models/transaction.py b/app/models/transaction.py new file mode 100644 index 00000000..59d75428 --- /dev/null +++ b/app/models/transaction.py @@ -0,0 +1,54 @@ +from app.models.db import get_db_connection + +class Transaction: + @staticmethod + def create(tx_type, amount, category, transaction_date): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute( + "INSERT INTO transactions (type, amount, category, transaction_date) VALUES (?, ?, ?, ?)", + (tx_type, amount, category, transaction_date) + ) + conn.commit() + return cursor.lastrowid + + @staticmethod + def get_all(): + with get_db_connection() as conn: + return conn.execute( + "SELECT * FROM transactions ORDER BY transaction_date DESC, created_at DESC" + ).fetchall() + + @staticmethod + def get_by_date_range(start_date, end_date): + with get_db_connection() as conn: + return conn.execute( + "SELECT * FROM transactions WHERE transaction_date >= ? AND transaction_date <= ? ORDER BY transaction_date DESC, created_at DESC", + (start_date, end_date) + ).fetchall() + + @staticmethod + def get_by_id(tx_id): + with get_db_connection() as conn: + return conn.execute("SELECT * FROM transactions WHERE id = ?", (tx_id,)).fetchone() + + @staticmethod + def delete(tx_id): + with get_db_connection() as conn: + conn.execute("DELETE FROM transactions WHERE id = ?", (tx_id,)) + conn.commit() + + @staticmethod + def get_total_balance(): + with get_db_connection() as conn: + # 簡化計算方式,取收入總和與支出總和 + result = conn.execute(""" + SELECT + SUM(CASE WHEN type='INCOME' THEN amount ELSE 0 END) as total_income, + SUM(CASE WHEN type='EXPENSE' THEN amount ELSE 0 END) as total_expense + FROM transactions + """).fetchone() + + income = result['total_income'] if result['total_income'] else 0 + expense = result['total_expense'] if result['total_expense'] else 0 + return income - expense diff --git a/database/schema.sql b/database/schema.sql index e356c0e1..8b393971 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -1,9 +1,17 @@ -CREATE TABLE IF NOT EXISTS recipes ( +CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - description TEXT, - ingredients TEXT NOT NULL, - steps TEXT NOT NULL, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT DEFAULT CURRENT_TIMESTAMP + type TEXT NOT NULL, + amount INTEGER NOT NULL, + category TEXT NOT NULL, + transaction_date TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS fixed_deductions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + amount INTEGER NOT NULL, + category TEXT NOT NULL, + deduct_day INTEGER NOT NULL, + last_processed_month TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); diff --git a/docs/DB_DESIGN.md b/docs/DB_DESIGN.md index 855ce95d..b60bd725 100644 --- a/docs/DB_DESIGN.md +++ b/docs/DB_DESIGN.md @@ -1,57 +1,93 @@ -# DB Design Document — 資料庫設計文件 +# 資料庫設計文件 (DB Design) - 個人記帳簿系統 -## 1. ER 圖 +本文件根據產品需求與流程規劃,定義了系統後端使用 SQLite 的結構,包含實體關係圖、資料表詳細說明以及對應的 Python Model 設計。 + +## 1. ER 圖(實體關係圖) + +我們將收入與支出合併為 `transactions` 資料表,以類別(type)區分,方便計算總餘額與單一列表呈現。針對每月固定扣款項目則建立 `fixed_deductions`。 ```mermaid erDiagram - RECIPES { - INTEGER id PK - TEXT title "食譜名稱" - TEXT description "簡介" - TEXT ingredients "所需食材" - TEXT steps "烹飪步驟" - TEXT created_at "建立時間" - TEXT updated_at "最後更新時間" - } + TRANSACTIONS { + INTEGER id PK + TEXT type "收入('INCOME') 或 支出('EXPENSE')" + INTEGER amount "金額" + TEXT category "分類,如 餐飲/交通" + TEXT transaction_date "發生日期 YYYY-MM-DD" + TEXT created_at "建立時間" + } + + FIXED_DEDUCTIONS { + INTEGER id PK + INTEGER amount "扣款金額" + TEXT category "分類,如 手機費/訂閱" + INTEGER deduct_day "每月固定扣款日 (1-31)" + TEXT last_processed_month "最後自動執行月份 YYYY-MM" + TEXT created_at "設定建立時間" + } + + %% 雖然兩者在邏輯上有關聯,但在資料儲存上,每次處理 fixed_deductions 都會產生一筆 transactions,屬於程式邏輯的寫入操作。 ``` +--- + ## 2. 資料表詳細說明 -### `recipes` 資料表 -本系統採用單一資料表設計以符合 MVP 需求,各欄位詳細定義如下: +### 2.1 `transactions` (收支紀錄表) +負責儲存所有使用者的日常收付款紀錄,是計算總餘額的核心。 | 欄位名稱 | 型別 | 必填 | 說明 | | --- | --- | --- | --- | -| `id` | `INTEGER` | 是 | Primary Key, 自動遞增 (AUTOINCREMENT)。 | -| `title` | `TEXT` | 是 | 食譜名稱。 | -| `description` | `TEXT` | 否 | 食譜的簡單介紹。 | -| `ingredients` | `TEXT` | 是 | 食譜的所需食材,為求簡便與彈性,儲存為純文字 (可依照換行或逗點分隔)。未來依食材推薦將利用 `LIKE` 查詢運算實作。 | -| `steps` | `TEXT` | 是 | 烹飪步驟,儲存為純文字即可。 | -| `created_at` | `TEXT` | 是 | 建立時間,使用 `CURRENT_TIMESTAMP` 的 ISO 格式儲存。 | -| `updated_at` | `TEXT` | 是 | 最後更新時間,編輯資料時觸發更新。 | +| `id` | INTEGER | 是 | Primary Key 自動遞增 | +| `type` | TEXT | 是 | 區分此筆帳為 `'INCOME'` (收入) 或 `'EXPENSE'` (支出) | +| `amount` | INTEGER | 是 | 金額,限正整數 | +| `category` | TEXT | 是 | 使用者設定的收支分類,例如「餐飲」、「零用錢」 | +| `transaction_date`| TEXT | 是 | 付款或收款當天的日期,格式為 `YYYY-MM-DD` | +| `created_at` | TEXT | 否 | 資料庫寫入時間,由 `CURRENT_TIMESTAMP` 自動產生 | + +### 2.2 `fixed_deductions` (每月固定扣款設定表) +負責登錄使用者預先設定好的每月必扣款項,系統會在後端判斷該月份是否已經產生支出。 + +| 欄位名稱 | 型別 | 必填 | 說明 | +| --- | --- | --- | --- | +| `id` | INTEGER | 是 | Primary Key 自動遞增 | +| `amount` | INTEGER | 是 | 預設要扣款的金額 | +| `category` | TEXT | 是 | 預設的支出分類 | +| `deduct_day` | INTEGER | 是 | 預設扣款日,範圍 1 到 31 | +| `last_processed_month` | TEXT | 否 | 系統自動扣款後,會更新此欄為該筆已過帳的年月 (如 `2026-04`),避免重複扣款 | +| `created_at` | TEXT | 否 | 設定項的建立時間 | + +--- ## 3. SQL 建表語法 -實作的 SQL 建表存放於 `database/schema.sql` 中: +請參考本專案下的 `database/schema.sql` 檔案。 ```sql -CREATE TABLE IF NOT EXISTS recipes ( +CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + amount INTEGER NOT NULL, + category TEXT NOT NULL, + transaction_date TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS fixed_deductions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - description TEXT, - ingredients TEXT NOT NULL, - steps TEXT NOT NULL, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT DEFAULT CURRENT_TIMESTAMP + amount INTEGER NOT NULL, + category TEXT NOT NULL, + deduct_day INTEGER NOT NULL, + last_processed_month TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); ``` -## 4. Python Model +--- + +## 4. Python Model 程式碼 -封裝的檔案位於 `app/models/recipe.py`。 -使用內建的 `sqlite3` 操作 `instance/database.db`,並支援: -- `create(data)` -- `get_all(query=None)`: 實作按標題與食材進行簡單字串 `LIKE` 搜尋。 -- `get_by_id(recipe_id)` -- `update(recipe_id, data)` -- `delete(recipe_id)` +本專案使用最輕量的內建 `sqlite3` 提供資料存取層。相關連線與 CRUD 操作均已實作於 `app/models/` 目錄中: +- `app/models/db.py`:負責資料庫連線初始化 +- `app/models/transaction.py`:負責建立、查詢特定時間區段的所有收支紀錄 +- `app/models/fixed_deduction.py`:負責新增、與更新扣款執行狀態 From 7f88ba1db10634e4ff0d6d6c01aec69451353107 Mon Sep 17 00:00:00 2001 From: antigravity Date: Mon, 27 Apr 2026 15:14:09 +0800 Subject: [PATCH 14/15] feat: add route skeleton and template plan --- app/routes/fixed_deduction.py | 46 +++++++++++++++ app/routes/main.py | 17 ++++++ app/routes/transaction.py | 49 ++++++++++++++++ docs/ROUTES.md | 106 +++++++++++++++++----------------- 4 files changed, 164 insertions(+), 54 deletions(-) create mode 100644 app/routes/fixed_deduction.py create mode 100644 app/routes/main.py create mode 100644 app/routes/transaction.py diff --git a/app/routes/fixed_deduction.py b/app/routes/fixed_deduction.py new file mode 100644 index 00000000..f13add3c --- /dev/null +++ b/app/routes/fixed_deduction.py @@ -0,0 +1,46 @@ +from flask import Blueprint, request, redirect, render_template, flash, url_for + +fixed_deduction_bp = Blueprint('fixed_deduction', __name__, url_prefix='/fixed-deductions') + +@fixed_deduction_bp.route('/', methods=['GET']) +def index(): + """ + 檢視所有的每月固定扣款清單 + 輸入:無 + 處理邏輯:呼叫 FixedDeduction.get_all() 獲取資料 + 輸出:渲染 templates/fixed_deductions/index.html + """ + pass + +@fixed_deduction_bp.route('/new', methods=['GET']) +def new_fixed_deduction(): + """ + 顯示新增固定扣款的表單介面 + 輸入:無 + 處理邏輯:無複雜邏輯 + 輸出:渲染 templates/fixed_deductions/form.html + """ + pass + +@fixed_deduction_bp.route('/', methods=['POST']) +def create_fixed_deduction(): + """ + 寫入新的固定扣款設定 + 輸入:表單資料 (amount, category, deduct_day) + 處理邏輯: + 1. 驗證金額與 deduct_day(應介於 1 到 31) + 2. 呼叫 FixedDeduction.create 寫入設定 + 輸出:重新導向至扣款清單頁面 + 錯誤處理:資料不齊或範圍錯誤時,透過 flash 推播提示,重回表單輸入頁 + """ + pass + +@fixed_deduction_bp.route('//delete', methods=['POST']) +def delete_fixed_deduction(deduction_id): + """ + 刪除指定的每月固定扣款 + 輸入:FixedDeduction 對應的 ID (從 URL 解析) + 處理邏輯:呼叫 FixedDeduction.delete(deduction_id) 關閉這筆設定 + 輸出:重新導向至清單首頁 + """ + pass diff --git a/app/routes/main.py b/app/routes/main.py new file mode 100644 index 00000000..9a7e38b8 --- /dev/null +++ b/app/routes/main.py @@ -0,0 +1,17 @@ +from flask import Blueprint, render_template + +main_bp = Blueprint('main', __name__) + +@main_bp.route('/', methods=['GET']) +def index(): + """ + 首頁路由 + 輸入:無 + 處理邏輯: + 1. 檢查並觸發固定扣款背景新增邏輯,確保新扣款已過帳 + 2. 呼叫 Transaction.get_total_balance() 計算總餘額 + 3. 呼叫 Transaction.get_all() 獲取最近幾筆紀錄 + 輸出:渲染 templates/main/index.html,傳入餘額與紀錄清單以呈現儀表板 + 錯誤處理:若是初次使用無資料,顯示餘額為 0 與空白清單即可。 + """ + pass diff --git a/app/routes/transaction.py b/app/routes/transaction.py new file mode 100644 index 00000000..5e083e2c --- /dev/null +++ b/app/routes/transaction.py @@ -0,0 +1,49 @@ +from flask import Blueprint, request, redirect, render_template, flash, url_for + +transaction_bp = Blueprint('transaction', __name__, url_prefix='/transactions') + +@transaction_bp.route('/', methods=['GET']) +def index(): + """ + 收支查詢清單路由 + 輸入:支援 URL query parameters (start_date, end_date) + 處理邏輯: + - 若有帶入合理的日期範圍參數,呼叫 Transaction.get_by_date_range() + - 否則呼叫 Transaction.get_all() 取出所有紀錄 + 輸出:渲染 templates/transactions/index.html + 錯誤處理:日期字串格式錯誤則設定 Flash 錯誤訊息並以無條件載入所有資料 + """ + pass + +@transaction_bp.route('/new', methods=['GET']) +def new_transaction(): + """ + 新增收支表單頁面 + 輸入:URL query param (可帶入 type=INCOME 或 EXPENSE 以載入正確的 UI 狀態) + 處理邏輯:依據類型選擇,顯示對應的視覺提示 + 輸出:渲染 templates/transactions/form.html + """ + pass + +@transaction_bp.route('/', methods=['POST']) +def create_transaction(): + """ + 儲存新的收支紀錄 + 輸入:來自表單的資料 (type, amount, category, transaction_date) + 處理邏輯: + 1. 驗證金額是否為正整數、日期是否合理 + 2. 呼叫 Transaction.create 寫入資料庫 + 輸出:Redirect 重導向至首頁以直接確認更新後的總餘額 + 錯誤處理:若必填欄位空白,存放 Flash 錯誤訊息並重導向回新增頁面 + """ + pass + +@transaction_bp.route('//delete', methods=['POST']) +def delete_transaction(record_id): + """ + 刪除特定收支紀錄 + 輸入:Transaction 紀錄的 ID (從 URL 提供) + 處理邏輯:呼叫 Transaction.delete(record_id) + 輸出:完成後重新導向至原本的收支查詢頁面 + """ + pass diff --git a/docs/ROUTES.md b/docs/ROUTES.md index 2514b340..c7207e21 100644 --- a/docs/ROUTES.md +++ b/docs/ROUTES.md @@ -1,70 +1,68 @@ -# 路由設計文件 (API Design) +# 路由與頁面設計文件 (Routes) - 個人記帳簿系統 + +基於 PRD、架構文件以及資料庫設計,本文件定義了 Flask 的所有端點路由,以供前端頁面進行資料綁定與操作。 + +## 1. 路由總覽表格 -## 1. 路由總覽列表 | 功能 | HTTP 方法 | URL 路徑 | 對應模板 | 說明 | | --- | --- | --- | --- | --- | -| 食譜列表 (首頁) | `GET` | `/` | `templates/recipes/index.html` | 顯示所有食譜,支援 `?q=` 查詢參數進行名稱/食材搜尋。 | -| 新增食譜頁面 | `GET` | `/recipes/new` | `templates/recipes/form.html` | 顯示空白的新增食譜表單。 | -| 建立食譜 | `POST` | `/recipes` | — | 接收表單資料、存入 DB,成功後重導回首頁。 | -| 食譜明細 | `GET` | `/recipes/` | `templates/recipes/show.html` | 顯示特定食譜的食材與步驟。若查無資料回傳 404。 | -| 編輯食譜頁面 | `GET` | `/recipes//edit` | `templates/recipes/form.html` | 顯示現有食譜資料供修改。 | -| 更新食譜 | `POST` | `/recipes//update` | — | 接收更新表單並寫回 DB,完成後重導至食譜明細。 | -| 刪除食譜 | `POST` | `/recipes//delete` | — | 從 DB 中刪除該筆資料,完成後重導回首頁。 | +| 首頁 (總餘額與最新紀錄) | GET | `/` | `templates/main/index.html` | 顯示目前總餘額,以及最近的幾筆收支紀錄。並隱性觸發自動扣款檢查。 | +| 收支查詢清單 | GET | `/transactions` | `templates/transactions/index.html`| 顯示所有紀錄,支援以日期過濾。 | +| 新增收支頁面 | GET | `/transactions/new` | `templates/transactions/form.html` | 顯示新增收入或支出的表單介面。 | +| 建立收支 | POST | `/transactions` | — | 接收使用者送出表單,寫入資料庫後重導至首頁。 | +| 刪除單筆收支 | POST | `/transactions//delete`| — | 刪除指定記錄,避免意外誤刪故採用 POST 實作,完畢後重導至列表頁。 | +| 固定扣款清單 | GET | `/fixed-deductions` | `templates/fixed_deductions/index.html`| 顯示與管理每月設定的固定支出。 | +| 新增固定扣款頁面| GET | `/fixed-deductions/new` | `templates/fixed_deductions/form.html` | 顯示建立新自動扣款的表單。 | +| 建立固定扣款 | POST | `/fixed-deductions` | — | 接收使用者送出表單,寫入設定並重導回清單。 | +| 刪除固定扣款 | POST | `/fixed-deductions//delete`| — | 取消該項目的後續每月自動扣款計算。 | --- -## 2. 路由詳細說明 +## 2. 每個路由的詳細說明 -### `GET /` (食譜列表) -- **輸入**: URL 查詢參數 `?q=keyword` (可選) -- **處理邏輯**: - - 若有 `q` 則呼叫 `Recipe.get_all(q)` 並搜尋。 - - 若無則呼叫 `Recipe.get_all()` 取得最新食譜清單。 -- **輸出**: 渲染 `recipes/index.html`,傳入食譜列表變數 `recipes`。 -- **錯誤處理**: 資料庫如讀取錯誤報 500。 +### 首頁模組 +- `GET /` + - **輸入**:無。 + - **處理邏輯**:觸發 FixedDeduction 自動扣款檢查邏輯;調用 `Transaction.get_total_balance()` 與 `Transaction.get_all()` (限制前幾筆) 來獲取基礎儀表板資料。 + - **輸出**:渲染 `main/index.html`。 + - **錯誤處理**:如果資料庫裡沒任何記錄,無影響,正常回傳預設的 0 或空陣列。 -### `GET /recipes/new` (新增輸入頁面) -- **輸入**: 無 -- **處理邏輯**: 準備畫面即可。 -- **輸出**: 渲染 `recipes/form.html`。 -- **錯誤處理**: 無特殊錯誤。 +### 收支實體 (Transaction) 模組 +- `GET /transactions` + - **輸入**:URL 查詢參數 `start_date`, `end_date` (可選)。 + - **處理邏輯**:判斷是否有參數傳入,決定呼叫 `get_all()` 還是 `get_by_date_range()`。 + - **輸出**:渲染 `transactions/index.html`。 +- `POST /transactions` + - **輸入**:表單參數 `type`, `amount`, `category`, `transaction_date`。 + - **處理邏輯**:驗證 `amount` 是否為正整數、`date` 是否合法後,存入 DB。 + - **輸出**:Redirect 至 `/` (快速查看結果) 或是 `/transactions`。 + - **錯誤處理**:如果必填欄位空白,存回 flash message,重導回表單頁。 -### `POST /recipes` (建立食譜) -- **輸入**: Form Data 包含 `title`, `description`, `ingredients`, `steps`。 -- **處理邏輯**: 接收 Form,呼叫 `Recipe.create(data)`。 -- **輸出**: Http 302 重新導向至 `/` (首頁)。 -- **錯誤處理**: 若 `title`, `ingredients`, `steps` 等必填欄位缺失,透過 Flash Message 提示並導回 `/recipes/new`。 +### 固定扣款 (Fixed Deduction) 模組 +- `POST /fixed-deductions` + - **輸入**:表單參數 `amount`, `category`, `deduct_day`。 + - **處理邏輯**:驗證 `deduct_day` 是否落於 1-31,並存入資料庫。 + - **輸出**:Redirect 至 `/fixed-deductions`。 + - **錯誤處理**:日期超出月份合理範圍時 flash 錯誤提示。 -### `GET /recipes/` (食譜明細) -- **輸入**: URL 參數 `id`。 -- **處理邏輯**: 呼叫 `Recipe.get_by_id(id)` 取得指定食譜。 -- **輸出**: 渲染 `recipes/show.html`,傳入 `recipe`。 -- **錯誤處理**: 若 `recipe` 為空 (None),拋出 HTTP 404 (Not Found)。 +--- -### `GET /recipes//edit` (編輯食譜頁面) -- **輸入**: URL 參數 `id`。 -- **處理邏輯**: 呼叫 `Recipe.get_by_id(id)` 取得指定食譜。 -- **輸出**: 渲染 `recipes/form.html`,傳入 `recipe` 將現有值帶入欄位。 -- **錯誤處理**: 若查無結果,拋出 404。 +## 3. Jinja2 模板清單 -### `POST /recipes//update` (更新食譜) -- **輸入**: URL 參數 `id` 與被更新的表單資料。 -- **處理邏輯**: 呼叫 `Recipe.update(id, data)`。 -- **輸出**: 更新成功後重新導向回詳細頁面 `/recipes/`。 -- **錯誤處理**: 查無食譜 (404) 或欄位缺失等驗證同建立食譜的處理方式。 +所有的網頁模板都會共用 `base.html` 提供的導覽列結構與樣式。 -### `POST /recipes//delete` (刪除食譜) -- **輸入**: URL 參數 `id`。 -- **處理邏輯**: 呼叫 `Recipe.delete(id)` 進行刪除。 -- **輸出**: 刪除成功後重新導向至 `/`。 -- **錯誤處理**: 若查無食譜則拋出 404。 +* **共用版型** + * `templates/base.html`:包含 HTML5 Skeleton、NavBar、全域共用 CSS。 +* **首頁** + * `templates/main/index.html`:儀表板 (Dashboard) 設計,繼承自 `base.html`。 +* **收支頁面** + * `templates/transactions/index.html`:含區間篩選器的歷史表格,繼承自 `base.html`。 + * `templates/transactions/form.html`:包含表單元素的新增頁面,繼承自 `base.html`。 +* **固定扣款頁面** + * `templates/fixed_deductions/index.html`:展示扣款設定清單,繼承自 `base.html`。 + * `templates/fixed_deductions/form.html`:固定扣款特定欄位的新增頁面,繼承自 `base.html`。 --- -## 3. Jinja2 模板清單 - -以下檔案後續將於 `app/templates` 建立: -1. `base.html`: 包含全站共用的 ``、導覽列與外觀樣式。 -2. `recipes/index.html`: 繼承 `base.html`,以卡片或列表結構呈現食譜總覽。 -3. `recipes/show.html`: 繼承 `base.html`,全版詳細呈現食材清單與步驟說明。 -4. `recipes/form.html`: 繼承 `base.html`,一個頁面同時支援新增 (Create) 與編輯 (Update)。依據後端是否丟出已存在的 `recipe` 來判斷是 POST 到 `/recipes` 或 `/recipes//update`。 +## 4. 路由骨架程式碼 +相關定義已實作於 `app/routes/` 下的 `main.py`, `transaction.py`, 與 `fixed_deduction.py` 之中。 From 6ebb55b2bed231055c848879c6282ffd5f33a977 Mon Sep 17 00:00:00 2001 From: antigravity Date: Mon, 27 Apr 2026 15:43:05 +0800 Subject: [PATCH 15/15] feat: implement main application features including routes and templates --- .env.example | 3 + app.py | 24 +++- app/__init__.py | 1 + app/models/fixed_deduction.py | 111 +++++++++++++---- app/models/transaction.py | 143 +++++++++++++++++----- app/routes/fixed_deduction.py | 66 +++++----- app/routes/main.py | 33 +++-- app/routes/transaction.py | 73 ++++++----- app/templates/base.html | 132 ++++++++++++++++++++ app/templates/fixed_deductions/form.html | 40 ++++++ app/templates/fixed_deductions/index.html | 66 ++++++++++ app/templates/main/index.html | 62 ++++++++++ app/templates/transactions/form.html | 52 ++++++++ app/templates/transactions/index.html | 69 +++++++++++ requirements.txt | 2 + 15 files changed, 744 insertions(+), 133 deletions(-) create mode 100644 .env.example create mode 100644 app/__init__.py create mode 100644 app/templates/base.html create mode 100644 app/templates/fixed_deductions/form.html create mode 100644 app/templates/fixed_deductions/index.html create mode 100644 app/templates/main/index.html create mode 100644 app/templates/transactions/form.html create mode 100644 app/templates/transactions/index.html create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..f9dfb12d --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +FLASK_APP=app.py +FLASK_DEBUG=1 +SECRET_KEY=super-secret-key-change-in-production diff --git a/app.py b/app.py index 41549209..2fd50013 100644 --- a/app.py +++ b/app.py @@ -1,12 +1,26 @@ +import os from flask import Flask -from app.routes.recipe import recipe_bp +from dotenv import load_dotenv + +# 載入環境變數 +load_dotenv() def create_app(): - # 指定 template 與 static 的存放路徑,確保與專案結構相符 - app = Flask(__name__, template_folder='app/templates', static_folder='app/static') + app = Flask(__name__, template_folder='app/templates') + app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-dev-key') + + # 載入並註冊 Blueprints + from app.routes.main import main_bp + from app.routes.transaction import transaction_bp + from app.routes.fixed_deduction import fixed_deduction_bp + + app.register_blueprint(main_bp) + app.register_blueprint(transaction_bp) + app.register_blueprint(fixed_deduction_bp) - # 註冊 Blueprints - app.register_blueprint(recipe_bp) + # 初始化資料庫 + from app.models.db import init_db + init_db() return app diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 00000000..32948706 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +# 標記 app 為 Python 套件 diff --git a/app/models/fixed_deduction.py b/app/models/fixed_deduction.py index cc67a1e0..e230eeae 100644 --- a/app/models/fixed_deduction.py +++ b/app/models/fixed_deduction.py @@ -1,38 +1,107 @@ +import sqlite3 from app.models.db import get_db_connection class FixedDeduction: @staticmethod def create(amount, category, deduct_day): - with get_db_connection() as conn: - cursor = conn.cursor() - cursor.execute( - "INSERT INTO fixed_deductions (amount, category, deduct_day) VALUES (?, ?, ?)", - (amount, category, deduct_day) - ) - conn.commit() - return cursor.lastrowid + """ + 新增一筆記錄 + :param amount: 扣款金額 + :param category: 扣款分類 + :param deduct_day: 每月固定扣款日 (1-31) + :return: 新增成功的紀錄 ID,若失敗則回傳 None + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute( + "INSERT INTO fixed_deductions (amount, category, deduct_day) VALUES (?, ?, ?)", + (amount, category, deduct_day) + ) + conn.commit() + return cursor.lastrowid + except sqlite3.Error as e: + print(f"Database error in FixedDeduction.create: {e}") + return None @staticmethod def get_all(): - with get_db_connection() as conn: - return conn.execute("SELECT * FROM fixed_deductions ORDER BY deduct_day ASC").fetchall() + """ + 取得所有記錄 + :return: 紀錄列表 (sqlite3.Row 格式) + """ + try: + with get_db_connection() as conn: + return conn.execute("SELECT * FROM fixed_deductions ORDER BY deduct_day ASC").fetchall() + except sqlite3.Error as e: + print(f"Database error in FixedDeduction.get_all: {e}") + return [] @staticmethod def get_by_id(deduction_id): - with get_db_connection() as conn: - return conn.execute("SELECT * FROM fixed_deductions WHERE id = ?", (deduction_id,)).fetchone() + """ + 取得單筆記錄 + :param deduction_id: FixedDeduction ID + :return: 該筆記錄 (sqlite3.Row) 或 None + """ + try: + with get_db_connection() as conn: + return conn.execute("SELECT * FROM fixed_deductions WHERE id = ?", (deduction_id,)).fetchone() + except sqlite3.Error as e: + print(f"Database error in FixedDeduction.get_by_id: {e}") + return None + + @staticmethod + def update(deduction_id, amount, category, deduct_day): + """ + 更新記錄 + :param deduction_id: FixedDeduction ID + :return: 成功與否的布林值 + """ + try: + with get_db_connection() as conn: + conn.execute( + "UPDATE fixed_deductions SET amount=?, category=?, deduct_day=? WHERE id=?", + (amount, category, deduct_day, deduction_id) + ) + conn.commit() + return True + except sqlite3.Error as e: + print(f"Database error in FixedDeduction.update: {e}") + return False @staticmethod def update_last_processed(deduction_id, processed_month): - with get_db_connection() as conn: - conn.execute( - "UPDATE fixed_deductions SET last_processed_month = ? WHERE id = ?", - (processed_month, deduction_id) - ) - conn.commit() + """ + 更新記錄最後被處理的月份 + :param deduction_id: FixedDeduction ID + :param processed_month: 處理的月份字串 (YYYY-MM) + :return: 成功與否的布林值 + """ + try: + with get_db_connection() as conn: + conn.execute( + "UPDATE fixed_deductions SET last_processed_month = ? WHERE id = ?", + (processed_month, deduction_id) + ) + conn.commit() + return True + except sqlite3.Error as e: + print(f"Database error in FixedDeduction.update_last_processed: {e}") + return False @staticmethod def delete(deduction_id): - with get_db_connection() as conn: - conn.execute("DELETE FROM fixed_deductions WHERE id = ?", (deduction_id,)) - conn.commit() + """ + 刪除記錄 + :param deduction_id: FixedDeduction ID + :return: 成功與否的布林值 + """ + try: + with get_db_connection() as conn: + conn.execute("DELETE FROM fixed_deductions WHERE id = ?", (deduction_id,)) + conn.commit() + return True + except sqlite3.Error as e: + print(f"Database error in FixedDeduction.delete: {e}") + return False diff --git a/app/models/transaction.py b/app/models/transaction.py index 59d75428..a0d21ade 100644 --- a/app/models/transaction.py +++ b/app/models/transaction.py @@ -1,54 +1,129 @@ +import sqlite3 from app.models.db import get_db_connection class Transaction: @staticmethod def create(tx_type, amount, category, transaction_date): - with get_db_connection() as conn: - cursor = conn.cursor() - cursor.execute( - "INSERT INTO transactions (type, amount, category, transaction_date) VALUES (?, ?, ?, ?)", - (tx_type, amount, category, transaction_date) - ) - conn.commit() - return cursor.lastrowid + """ + 新增一筆記錄 + :param tx_type: 'INCOME' 或 'EXPENSE' + :param amount: 金額 + :param category: 分類 + :param transaction_date: 交易日期 (YYYY-MM-DD) + :return: 新增成功的紀錄 ID,若失敗則回傳 None + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute( + "INSERT INTO transactions (type, amount, category, transaction_date) VALUES (?, ?, ?, ?)", + (tx_type, amount, category, transaction_date) + ) + conn.commit() + return cursor.lastrowid + except sqlite3.Error as e: + print(f"Database error in Transaction.create: {e}") + return None @staticmethod def get_all(): - with get_db_connection() as conn: - return conn.execute( - "SELECT * FROM transactions ORDER BY transaction_date DESC, created_at DESC" - ).fetchall() + """ + 取得所有記錄 + :return: 紀錄列表 (sqlite3.Row 格式) + """ + try: + with get_db_connection() as conn: + return conn.execute( + "SELECT * FROM transactions ORDER BY transaction_date DESC, created_at DESC" + ).fetchall() + except sqlite3.Error as e: + print(f"Database error in Transaction.get_all: {e}") + return [] @staticmethod def get_by_date_range(start_date, end_date): - with get_db_connection() as conn: - return conn.execute( - "SELECT * FROM transactions WHERE transaction_date >= ? AND transaction_date <= ? ORDER BY transaction_date DESC, created_at DESC", - (start_date, end_date) - ).fetchall() + """ + 取得指定日期區間的紀錄 + :return: 紀錄列表 + """ + try: + with get_db_connection() as conn: + return conn.execute( + "SELECT * FROM transactions WHERE transaction_date >= ? AND transaction_date <= ? ORDER BY transaction_date DESC, created_at DESC", + (start_date, end_date) + ).fetchall() + except sqlite3.Error as e: + print(f"Database error in Transaction.get_by_date_range: {e}") + return [] @staticmethod def get_by_id(tx_id): - with get_db_connection() as conn: - return conn.execute("SELECT * FROM transactions WHERE id = ?", (tx_id,)).fetchone() + """ + 取得單筆記錄 + :param tx_id: Transaction ID + :return: 該筆記錄 (sqlite3.Row) 或 None + """ + try: + with get_db_connection() as conn: + return conn.execute("SELECT * FROM transactions WHERE id = ?", (tx_id,)).fetchone() + except sqlite3.Error as e: + print(f"Database error in Transaction.get_by_id: {e}") + return None + + @staticmethod + def update(tx_id, tx_type, amount, category, transaction_date): + """ + 更新記錄 + :param tx_id: Transaction ID + :return: 成功與否的布林值 + """ + try: + with get_db_connection() as conn: + conn.execute( + "UPDATE transactions SET type=?, amount=?, category=?, transaction_date=? WHERE id=?", + (tx_type, amount, category, transaction_date, tx_id) + ) + conn.commit() + return True + except sqlite3.Error as e: + print(f"Database error in Transaction.update: {e}") + return False @staticmethod def delete(tx_id): - with get_db_connection() as conn: - conn.execute("DELETE FROM transactions WHERE id = ?", (tx_id,)) - conn.commit() + """ + 刪除記錄 + :param tx_id: Transaction ID + :return: 成功與否的布林值 + """ + try: + with get_db_connection() as conn: + conn.execute("DELETE FROM transactions WHERE id = ?", (tx_id,)) + conn.commit() + return True + except sqlite3.Error as e: + print(f"Database error in Transaction.delete: {e}") + return False @staticmethod def get_total_balance(): - with get_db_connection() as conn: - # 簡化計算方式,取收入總和與支出總和 - result = conn.execute(""" - SELECT - SUM(CASE WHEN type='INCOME' THEN amount ELSE 0 END) as total_income, - SUM(CASE WHEN type='EXPENSE' THEN amount ELSE 0 END) as total_expense - FROM transactions - """).fetchone() - - income = result['total_income'] if result['total_income'] else 0 - expense = result['total_expense'] if result['total_expense'] else 0 - return income - expense + """ + 取得當前總餘額 + :return: 總餘額數值 + """ + try: + with get_db_connection() as conn: + # 簡化計算方式,取收入總和與支出總和 + result = conn.execute(""" + SELECT + SUM(CASE WHEN type='INCOME' THEN amount ELSE 0 END) as total_income, + SUM(CASE WHEN type='EXPENSE' THEN amount ELSE 0 END) as total_expense + FROM transactions + """).fetchone() + + income = result['total_income'] if result['total_income'] else 0 + expense = result['total_expense'] if result['total_expense'] else 0 + return income - expense + except sqlite3.Error as e: + print(f"Database error in Transaction.get_total_balance: {e}") + return 0 diff --git a/app/routes/fixed_deduction.py b/app/routes/fixed_deduction.py index f13add3c..36ae985e 100644 --- a/app/routes/fixed_deduction.py +++ b/app/routes/fixed_deduction.py @@ -1,46 +1,50 @@ from flask import Blueprint, request, redirect, render_template, flash, url_for +from app.models.fixed_deduction import FixedDeduction fixed_deduction_bp = Blueprint('fixed_deduction', __name__, url_prefix='/fixed-deductions') @fixed_deduction_bp.route('/', methods=['GET']) def index(): - """ - 檢視所有的每月固定扣款清單 - 輸入:無 - 處理邏輯:呼叫 FixedDeduction.get_all() 獲取資料 - 輸出:渲染 templates/fixed_deductions/index.html - """ - pass + """檢視所有的每月固定扣款清單""" + deductions = FixedDeduction.get_all() + return render_template('fixed_deductions/index.html', deductions=deductions) @fixed_deduction_bp.route('/new', methods=['GET']) def new_fixed_deduction(): - """ - 顯示新增固定扣款的表單介面 - 輸入:無 - 處理邏輯:無複雜邏輯 - 輸出:渲染 templates/fixed_deductions/form.html - """ - pass + """顯示新增固定扣款的表單介面""" + return render_template('fixed_deductions/form.html') @fixed_deduction_bp.route('/', methods=['POST']) def create_fixed_deduction(): - """ - 寫入新的固定扣款設定 - 輸入:表單資料 (amount, category, deduct_day) - 處理邏輯: - 1. 驗證金額與 deduct_day(應介於 1 到 31) - 2. 呼叫 FixedDeduction.create 寫入設定 - 輸出:重新導向至扣款清單頁面 - 錯誤處理:資料不齊或範圍錯誤時,透過 flash 推播提示,重回表單輸入頁 - """ - pass + """寫入新的固定扣款設定""" + amount_str = request.form.get('amount') + category = request.form.get('category') + deduct_day_str = request.form.get('deduct_day') + + if not amount_str or not category or not deduct_day_str: + flash('請填寫所有欄位') + return redirect(url_for('fixed_deduction.new_fixed_deduction')) + + try: + amount = int(amount_str) + deduct_day = int(deduct_day_str) + if amount <= 0: + flash('金額必須為正整數') + return redirect(url_for('fixed_deduction.new_fixed_deduction')) + if deduct_day < 1 or deduct_day > 31: + flash('扣款日必須在 1 到 31 之間') + return redirect(url_for('fixed_deduction.new_fixed_deduction')) + except ValueError: + flash('金額與日期必須為正整數') + return redirect(url_for('fixed_deduction.new_fixed_deduction')) + + FixedDeduction.create(amount, category, deduct_day) + flash('固定扣款設定新增成功') + return redirect(url_for('fixed_deduction.index')) @fixed_deduction_bp.route('//delete', methods=['POST']) def delete_fixed_deduction(deduction_id): - """ - 刪除指定的每月固定扣款 - 輸入:FixedDeduction 對應的 ID (從 URL 解析) - 處理邏輯:呼叫 FixedDeduction.delete(deduction_id) 關閉這筆設定 - 輸出:重新導向至清單首頁 - """ - pass + """刪除指定的每月固定扣款""" + FixedDeduction.delete(deduction_id) + flash('固定扣款已刪除') + return redirect(url_for('fixed_deduction.index')) diff --git a/app/routes/main.py b/app/routes/main.py index 9a7e38b8..a5eb0b9d 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -1,17 +1,34 @@ from flask import Blueprint, render_template +from app.models.transaction import Transaction +from app.models.fixed_deduction import FixedDeduction +from datetime import datetime main_bp = Blueprint('main', __name__) +def process_fixed_deductions(): + """背景處理固定扣款邏輯,檢查所有設定,若符合本月且到期則自動扣款""" + today = datetime.now() + current_month = today.strftime('%Y-%m') + current_day = today.day + + deductions = FixedDeduction.get_all() + for d in deductions: + if d['last_processed_month'] != current_month and d['deduct_day'] <= current_day: + # 建立支出紀錄 + Transaction.create('EXPENSE', d['amount'], d['category'], today.strftime('%Y-%m-%d')) + # 更新已處理月份 + FixedDeduction.update_last_processed(d['id'], current_month) + @main_bp.route('/', methods=['GET']) def index(): """ 首頁路由 - 輸入:無 - 處理邏輯: - 1. 檢查並觸發固定扣款背景新增邏輯,確保新扣款已過帳 - 2. 呼叫 Transaction.get_total_balance() 計算總餘額 - 3. 呼叫 Transaction.get_all() 獲取最近幾筆紀錄 - 輸出:渲染 templates/main/index.html,傳入餘額與紀錄清單以呈現儀表板 - 錯誤處理:若是初次使用無資料,顯示餘額為 0 與空白清單即可。 """ - pass + # 觸發固定扣款背景處理 + process_fixed_deductions() + + # 取得最新資料 + total_balance = Transaction.get_total_balance() + recent_transactions = Transaction.get_all()[:5] # 取得最近5筆 + + return render_template('main/index.html', total_balance=total_balance, transactions=recent_transactions) diff --git a/app/routes/transaction.py b/app/routes/transaction.py index 5e083e2c..a9ccec8b 100644 --- a/app/routes/transaction.py +++ b/app/routes/transaction.py @@ -1,49 +1,54 @@ from flask import Blueprint, request, redirect, render_template, flash, url_for +from app.models.transaction import Transaction transaction_bp = Blueprint('transaction', __name__, url_prefix='/transactions') @transaction_bp.route('/', methods=['GET']) def index(): - """ - 收支查詢清單路由 - 輸入:支援 URL query parameters (start_date, end_date) - 處理邏輯: - - 若有帶入合理的日期範圍參數,呼叫 Transaction.get_by_date_range() - - 否則呼叫 Transaction.get_all() 取出所有紀錄 - 輸出:渲染 templates/transactions/index.html - 錯誤處理:日期字串格式錯誤則設定 Flash 錯誤訊息並以無條件載入所有資料 - """ - pass + """收支查詢清單路由""" + start_date = request.args.get('start_date') + end_date = request.args.get('end_date') + + if start_date and end_date: + transactions = Transaction.get_by_date_range(start_date, end_date) + else: + transactions = Transaction.get_all() + + return render_template('transactions/index.html', transactions=transactions, start_date=start_date, end_date=end_date) @transaction_bp.route('/new', methods=['GET']) def new_transaction(): - """ - 新增收支表單頁面 - 輸入:URL query param (可帶入 type=INCOME 或 EXPENSE 以載入正確的 UI 狀態) - 處理邏輯:依據類型選擇,顯示對應的視覺提示 - 輸出:渲染 templates/transactions/form.html - """ - pass + """新增收支表單頁面""" + tx_type = request.args.get('type', 'EXPENSE') + return render_template('transactions/form.html', tx_type=tx_type) @transaction_bp.route('/', methods=['POST']) def create_transaction(): - """ - 儲存新的收支紀錄 - 輸入:來自表單的資料 (type, amount, category, transaction_date) - 處理邏輯: - 1. 驗證金額是否為正整數、日期是否合理 - 2. 呼叫 Transaction.create 寫入資料庫 - 輸出:Redirect 重導向至首頁以直接確認更新後的總餘額 - 錯誤處理:若必填欄位空白,存放 Flash 錯誤訊息並重導向回新增頁面 - """ - pass + """儲存新的收支紀錄""" + tx_type = request.form.get('type') + amount_str = request.form.get('amount') + category = request.form.get('category') + transaction_date = request.form.get('transaction_date') + + if not amount_str or not category or not transaction_date or not tx_type: + flash('請填寫所有欄位') + return redirect(url_for('transaction.new_transaction', type=tx_type)) + + try: + amount = int(amount_str) + if amount <= 0: + raise ValueError() + except ValueError: + flash('金額必須為正整數') + return redirect(url_for('transaction.new_transaction', type=tx_type)) + + Transaction.create(tx_type, amount, category, transaction_date) + flash('收支紀錄新增成功') + return redirect(url_for('main.index')) @transaction_bp.route('//delete', methods=['POST']) def delete_transaction(record_id): - """ - 刪除特定收支紀錄 - 輸入:Transaction 紀錄的 ID (從 URL 提供) - 處理邏輯:呼叫 Transaction.delete(record_id) - 輸出:完成後重新導向至原本的收支查詢頁面 - """ - pass + """刪除特定收支紀錄""" + Transaction.delete(record_id) + flash('紀錄已刪除') + return redirect(url_for('transaction.index')) diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 00000000..d17a933c --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,132 @@ + + + + + + 個人記帳簿系統 + + + + + + + + +
+ {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + + + diff --git a/app/templates/fixed_deductions/form.html b/app/templates/fixed_deductions/form.html new file mode 100644 index 00000000..d86c62b5 --- /dev/null +++ b/app/templates/fixed_deductions/form.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

記錄固定支出

+

設定後系統將會在每個月指定的時間自動將其納入總餘額的計算中。

+ +
+ +
+ +
+ $ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + 取消並返回清單 +
+
+
+
+
+{% endblock %} diff --git a/app/templates/fixed_deductions/index.html b/app/templates/fixed_deductions/index.html new file mode 100644 index 00000000..7ddfef85 --- /dev/null +++ b/app/templates/fixed_deductions/index.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

每月固定扣款設定

+ + 新增自動扣款 + +
+ +
+ +
+

什麼是固定扣款?

+

系統會在您每個月登入時,檢查預設扣款日是否已到期;一旦到期將會自動為您建立對應的支出紀錄,讓您不必每個月重新記帳。

+
+
+ +
+ + + + + + + + + + + + {% for item in deductions %} + + + + + + + + {% else %} + + + + {% endfor %} + +
預期扣款日支出分類項目扣款金額上次自動執行月份操作
+ + 每月 {{ item.deduct_day }} 號 + + {{ item.category }}-${{ "{:,}".format(item.amount) }} + {% if item.last_processed_month %} + {{ item.last_processed_month }} + {% else %} + 尚未遇過扣款期 + {% endif %} + +
+ +
+
+ + 您目前沒有設定任何固定扣款項目。 +
+
+
+{% endblock %} diff --git a/app/templates/main/index.html b/app/templates/main/index.html new file mode 100644 index 00000000..a781bb33 --- /dev/null +++ b/app/templates/main/index.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} + +{% block content %} +
+ +
+
+
My Total Balance
+

+ ${{ "{:,}".format(total_balance) }} +

+ +
+
+ + +
+
+
+

最新紀錄

+ 檢視全部 +
+ + {% if transactions %} +
+ + + {% for tx in transactions %} + + + + + + {% endfor %} + +
{{ tx.transaction_date[5:] }} + {% if tx.type == 'INCOME' %} + {{ tx.category }} + {% else %} + {{ tx.category }} + {% endif %} + + {% if tx.type == 'INCOME' %}+{% else %}-{% endif %}${{ "{:,}".format(tx.amount) }} +
+
+ {% else %} +
+ +

乾乾淨淨!開始記下你的第一筆帳吧

+
+ {% endif %} +
+
+
+{% endblock %} diff --git a/app/templates/transactions/form.html b/app/templates/transactions/form.html new file mode 100644 index 00000000..ee4c279b --- /dev/null +++ b/app/templates/transactions/form.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

+ {% if tx_type == 'INCOME' %} + 記錄新收入 + {% else %} + 記錄新支出 + {% endif %} +

+ +
+ + + +
+ +
+ $ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + 取消並返回首頁 +
+
+
+
+
+ + +{% endblock %} diff --git a/app/templates/transactions/index.html b/app/templates/transactions/index.html new file mode 100644 index 00000000..7234a77f --- /dev/null +++ b/app/templates/transactions/index.html @@ -0,0 +1,69 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

所有收支明細

+
+ +
+
+
+ + +
+
+ + +
+
+ + 清除 +
+
+
+ +
+ + + + + + + + + + + + {% for tx in transactions %} + + + + + + + + {% else %} + + + + {% endfor %} + +
日期類型分類項目產生金額操作
{{ tx.transaction_date }} + {% if tx.type == 'INCOME' %} + 收入 + {% else %} + 支出 + {% endif %} + {{ tx.category }} + {% if tx.type == 'INCOME' %}+{% else %}-{% endif %}${{ "{:,}".format(tx.amount) }} + +
+ +
+
+ 目前搜尋區段內無任何歷史紀錄。 +
+
+
+{% endblock %} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..f34604ee --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask +python-dotenv