diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/commit/SKILL.md new file mode 100644 index 000000000..e69de29bb diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..0e0978d47 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +SECRET_KEY=your_development_secret_key diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..35e74996a --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Database +instance/ +*.sqlite3 +*.db + +# VS Code +.vscode/ diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 000000000..288f000dc --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,15 @@ +import os +from flask import Flask + +def create_app(): + """Flask 應用程式工廠函式""" + app = Flask(__name__) + + # 基本設定 + app.config['SECRET_KEY'] = 'dev_secret_key' # 測試用,實務應從環境變數讀取 + + # 註冊路由 Blueprint + from .routes import bp as records_bp + app.register_blueprint(records_bp) + + return app diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 000000000..e829fd823 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,3 @@ +from .record import RecordModel, init_db + +__all__ = ['RecordModel', 'init_db'] diff --git a/app/models/record.py b/app/models/record.py new file mode 100644 index 000000000..7d3ef5f64 --- /dev/null +++ b/app/models/record.py @@ -0,0 +1,163 @@ +import sqlite3 +import os +import logging + +# 預設資料庫路徑 (對應到 instance/database.db) +DB_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'instance', 'database.db') + +# 設定基本的 logging 記錄錯誤 +logging.basicConfig(level=logging.ERROR) +logger = logging.getLogger(__name__) + +def get_db_connection(): + """取得資料庫連線""" + try: + # 確保 instance 資料夾存在 + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row # 讓查詢結果可以像字典一樣存取欄位 + return conn + except sqlite3.Error as e: + logger.error(f"Database connection error: {e}") + raise + +def init_db(): + """初始化資料庫與資料表""" + schema_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'database', 'schema.sql') + if os.path.exists(schema_path): + try: + with open(schema_path, 'r', encoding='utf-8') as f: + schema_sql = f.read() + conn = get_db_connection() + conn.executescript(schema_sql) + conn.commit() + except Exception as e: + logger.error(f"Error initializing database: {e}") + raise + finally: + if 'conn' in locals() and conn: + conn.close() + +class RecordModel: + @staticmethod + def create(record_type, amount, date, category='', description=''): + """新增一筆收支紀錄""" + conn = None + try: + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO records (type, amount, date, category, description) + VALUES (?, ?, ?, ?, ?) + ''', (record_type, amount, date, category, description)) + conn.commit() + new_id = cursor.lastrowid + return new_id + except sqlite3.Error as e: + logger.error(f"Error creating record: {e}") + if conn: + conn.rollback() + return None + finally: + if conn: + conn.close() + + @staticmethod + def get_all(): + """取得所有收支紀錄 (依日期遞減排序)""" + conn = None + try: + conn = get_db_connection() + records = conn.execute(''' + SELECT * FROM records ORDER BY date DESC, id DESC + ''').fetchall() + return [dict(row) for row in records] + except sqlite3.Error as e: + logger.error(f"Error getting all records: {e}") + return [] + finally: + if conn: + conn.close() + + @staticmethod + def get_by_id(record_id): + """根據 ID 取得單筆紀錄""" + conn = None + try: + conn = get_db_connection() + record = conn.execute(''' + SELECT * FROM records WHERE id = ? + ''', (record_id,)).fetchone() + return dict(record) if record else None + except sqlite3.Error as e: + logger.error(f"Error getting record by id: {e}") + return None + finally: + if conn: + conn.close() + + @staticmethod + def update(record_id, record_type, amount, date, category='', description=''): + """更新單筆收支紀錄""" + conn = None + try: + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute(''' + UPDATE records + SET type = ?, amount = ?, date = ?, category = ?, description = ? + WHERE id = ? + ''', (record_type, amount, date, category, description, record_id)) + conn.commit() + updated_rows = cursor.rowcount + return updated_rows > 0 + except sqlite3.Error as e: + logger.error(f"Error updating record: {e}") + if conn: + conn.rollback() + return False + finally: + if conn: + conn.close() + + @staticmethod + def delete(record_id): + """刪除單筆收支紀錄""" + conn = None + try: + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute(''' + DELETE FROM records WHERE id = ? + ''', (record_id,)) + conn.commit() + deleted_rows = cursor.rowcount + return deleted_rows > 0 + except sqlite3.Error as e: + logger.error(f"Error deleting record: {e}") + if conn: + conn.rollback() + return False + finally: + if conn: + conn.close() + + @staticmethod + def get_balance(): + """計算目前總餘額""" + conn = None + try: + conn = get_db_connection() + result = conn.execute(''' + SELECT + SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END) - + SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END) as balance + FROM records + ''').fetchone() + return result['balance'] or 0 + except sqlite3.Error as e: + logger.error(f"Error calculating balance: {e}") + return 0 + finally: + if conn: + conn.close() diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 000000000..253da0dd3 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,116 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, abort +from app.models.record import RecordModel + +# 建立一個 Blueprint 來管理收支紀錄相關路由 +bp = Blueprint('records', __name__) + +@bp.route('/') +def index(): + """ + [GET] 首頁 + 顯示目前總餘額以及近期的收支歷史紀錄 + """ + balance = RecordModel.get_balance() + records = RecordModel.get_all() + return render_template('index.html', balance=balance, records=records) + +@bp.route('/records/new', methods=['GET']) +def new_record(): + """ + [GET] 新增紀錄頁面 + 顯示填寫收支資料的表單 + """ + return render_template('records/new.html') + +@bp.route('/records', methods=['POST']) +def create_record(): + """ + [POST] 送出新增紀錄 + 接收表單資料並寫入資料庫,完成後導回首頁 + """ + record_type = request.form.get('type') + amount = request.form.get('amount') + date = request.form.get('date') + category = request.form.get('category', '') + description = request.form.get('description', '') + + # 基本輸入驗證 + if not record_type or not amount or not date: + flash('收支類型、金額與日期為必填欄位!', 'danger') + return redirect(url_for('records.new_record')) + + try: + amount = int(amount) + except ValueError: + flash('金額必須為有效的數字!', 'danger') + return redirect(url_for('records.new_record')) + + # 寫入資料庫 + new_id = RecordModel.create(record_type, amount, date, category, description) + if new_id: + flash('新增紀錄成功!', 'success') + else: + flash('新增失敗,請稍後再試。', 'danger') + + return redirect(url_for('records.index')) + +@bp.route('/records//edit', methods=['GET']) +def edit_record(record_id): + """ + [GET] 編輯紀錄頁面 + 顯示帶有原始資料的編輯表單頁面 + """ + record = RecordModel.get_by_id(record_id) + if not record: + abort(404) + + return render_template('records/edit.html', record=record) + +@bp.route('/records//update', methods=['POST']) +def update_record(record_id): + """ + [POST] 更新紀錄 + 接收更新資料並寫入資料庫,完成後導回首頁 + """ + # 檢查該筆資料是否存在 + record = RecordModel.get_by_id(record_id) + if not record: + abort(404) + + record_type = request.form.get('type') + amount = request.form.get('amount') + date = request.form.get('date') + category = request.form.get('category', '') + description = request.form.get('description', '') + + if not record_type or not amount or not date: + flash('收支類型、金額與日期為必填欄位!', 'danger') + return redirect(url_for('records.edit_record', record_id=record_id)) + + try: + amount = int(amount) + except ValueError: + flash('金額必須為有效的數字!', 'danger') + return redirect(url_for('records.edit_record', record_id=record_id)) + + success = RecordModel.update(record_id, record_type, amount, date, category, description) + if success: + flash('更新紀錄成功!', 'success') + else: + flash('更新失敗,請稍後再試。', 'danger') + + return redirect(url_for('records.index')) + +@bp.route('/records//delete', methods=['POST']) +def delete_record(record_id): + """ + [POST] 刪除紀錄 + 將指定 ID 的收支紀錄從資料庫中刪除,並導回首頁 + """ + success = RecordModel.delete(record_id) + if success: + flash('已成功刪除紀錄!', 'success') + else: + flash('刪除失敗,找不到該筆紀錄或發生錯誤。', 'danger') + + return redirect(url_for('records.index')) diff --git a/app/static/css/style.css b/app/static/css/style.css new file mode 100644 index 000000000..7ec10a6af --- /dev/null +++ b/app/static/css/style.css @@ -0,0 +1,4 @@ +/* 基礎樣式設定 */ +body { + font-family: Arial, sans-serif; +} diff --git a/app/static/js/main.js b/app/static/js/main.js new file mode 100644 index 000000000..af72639a6 --- /dev/null +++ b/app/static/js/main.js @@ -0,0 +1,2 @@ +// 前端互動邏輯 +console.log("App initialized."); diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 000000000..6fcd5e16c --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,54 @@ + + + + + + 個人記帳簿 + + + + + + + + + +
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% set bs_class = 'danger' if category == 'error' else category %} + + {% endfor %} + {% endif %} + {% endwith %} + + + {% block content %}{% endblock %} +
+ + + + + + + diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 000000000..46f3a6df6 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,68 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
目前總餘額
+

+ $ {{ balance }} +

+
+
+
+
+ +
+
+
近期的收支紀錄
+ 新增紀錄 +
+
+ {% if records %} +
+ + + + + + + + + + + + {% for record in records %} + + + + + + + + {% endfor %} + +
日期分類描述金額操作
{{ record.date }} + {% if record.category %} + {{ record.category }} + {% else %} + - + {% endif %} + {{ record.description }} + {% if record.type == 'income' %}+{% else %}-{% endif %} {{ record.amount }} + + 編輯 +
+ +
+
+
+ {% else %} +
+

目前尚無任何收支紀錄,趕快來記下第一筆吧!

+
+ {% endif %} +
+
+{% endblock %} diff --git a/app/templates/records/edit.html b/app/templates/records/edit.html new file mode 100644 index 000000000..f50c476aa --- /dev/null +++ b/app/templates/records/edit.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
編輯收支紀錄
+
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+ $ + +
+
+
+ + +
+
+ +
+ + +
+ +
+ 取消 + +
+
+
+
+
+
+{% endblock %} diff --git a/app/templates/records/new.html b/app/templates/records/new.html new file mode 100644 index 000000000..82781b371 --- /dev/null +++ b/app/templates/records/new.html @@ -0,0 +1,63 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
新增收支紀錄
+
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+ $ + +
+
+
+ + +
+
+ +
+ + +
+ +
+ 取消 + +
+
+
+
+
+
+ + +{% endblock %} diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 000000000..a9327f05b --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS records ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL CHECK(type IN ('income', 'expense')), + amount INTEGER NOT NULL, + date TEXT NOT NULL, + category TEXT, + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 000000000..d4b0a0cd6 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,76 @@ +# 系統架構文件 (Architecture) + +## 1. 技術架構說明 + +本系統為「個人記帳簿系統」,主要採用以下技術堆疊來建立輕量級的網頁應用程式: + +- **後端框架:Python + Flask** + - **原因**:Flask 是一個輕量且彈性的 Python 網頁框架,非常適合用於開發中小型專案或 MVP。它能快速建立路由並處理 HTTP 請求。 + - **MVC 模式應用**: + - **Controller (路由與邏輯)**:由 Flask 的 `@app.route` 負責接收使用者的請求,驗證資料(如記帳金額、日期),並呼叫 Model 進行處理。 + - **Model (資料庫邏輯)**:負責與 SQLite 資料庫互動,執行新增、查詢、刪除等操作。 + - **View (視圖)**:由 Jinja2 模板引擎負責將資料渲染成 HTML 網頁回傳給使用者。 + +- **模板引擎:Jinja2** + - **原因**:內建於 Flask 中,能無縫整合 Python 變數與 HTML。由於本專案不採用前後端分離,所有頁面皆由後端渲染後直接回傳給瀏覽器。 + +- **資料庫:SQLite** + - **原因**:輕量級的關聯式資料庫,無需額外安裝伺服器軟體,資料以單一檔案儲存,非常適合個人單機或輕度連線使用的記帳系統。 + +## 2. 專案資料夾結構 + +以下為建議的專案目錄與檔案結構: + +```text +web_app_development/ +├── app/ +│ ├── __init__.py # Flask 應用程式工廠與初始化設定 +│ ├── models.py # 資料庫模型 (定義收支紀錄等 Schema) +│ ├── routes.py # 路由與 Controller 邏輯 (處理收支的新增、刪除、列表) +│ ├── templates/ # Jinja2 HTML 模板 (View) +│ │ ├── base.html # 共用版型 (包含導覽列、頁尾等) +│ │ └── index.html # 首頁與記帳列表、餘額顯示 +│ └── static/ # 靜態資源檔案 +│ ├── css/ +│ │ └── style.css # 自訂樣式表 +│ └── js/ +│ └── main.js # 前端互動邏輯 (如有需要) +├── instance/ +│ └── database.db # SQLite 資料庫檔案 (執行時自動產生) +├── docs/ # 專案文件 (包含 PRD, 架構文件等) +├── requirements.txt # Python 依賴套件清單 +└── run.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. 將資料回傳給 Controller + Route->>Template: 6. 將資料傳入 Jinja2 進行渲染 + Template-->>Route: 7. 產生完整的 HTML + Route-->>Browser: 8. 回傳 HTML 畫面給使用者 +``` + +## 4. 關鍵設計決策 + +1. **採用 Server-Side Rendering (SSR) 而非前後端分離** + - **原因**:為了快速開發 MVP 並降低架構複雜度。直接使用 Flask + Jinja2 渲染畫面,免去設定前端框架(如 React/Vue)及跨域請求 (CORS) 的繁瑣步驟,適合核心為 CRUD 的記帳應用。 +2. **使用輕量級 SQLite 資料庫** + - **原因**:專案目標用戶為個人,資料量小且無高併發需求。SQLite 隨開即用,無需建立獨立的資料庫伺服器,部署與備份皆非常容易。 +3. **集中式的路由與模型管理** + - **原因**:由於目前功能較為單一(主要圍繞在收支紀錄),將所有的路由邏輯統一放在 `routes.py`,模型定義在 `models.py`。若未來功能擴充(如加入圖表分析、匯出 CSV),可再考慮重構為 Blueprint 模組化架構。 +4. **防範 SQL Injection 的實作方式** + - **原因**:雖然是個人應用,但仍需考慮安全性與系統穩定性。將透過 ORM (例如 Flask-SQLAlchemy) 或正確的 Parameterized Query 來處理資料庫操作,避免直接字串拼接 SQL 語法,防止異常輸入導致系統崩潰。 diff --git a/docs/DB_DESIGN.md b/docs/DB_DESIGN.md new file mode 100644 index 000000000..44f370eca --- /dev/null +++ b/docs/DB_DESIGN.md @@ -0,0 +1,42 @@ +# 資料庫設計文件 (DB Design) + +## 1. ER 圖(實體關係圖) + +```mermaid +erDiagram + RECORD { + int id PK + string type "收支類型 (income/expense)" + int amount "金額" + string date "日期 (YYYY-MM-DD)" + string category "分類" + string description "描述說明" + datetime created_at "建立時間" + } +``` + +## 2. 資料表詳細說明 + +### `records` (收支紀錄表) + +主要用於儲存使用者的每一筆收入與支出明細。 + +| 欄位名稱 | 型別 | 必填 | 說明 | +| :--- | :--- | :--- | :--- | +| `id` | INTEGER | 是 | Primary Key,自動遞增 | +| `type` | TEXT | 是 | 收支類型,僅允許 `'income'` (收入) 或 `'expense'` (支出) | +| `amount` | INTEGER | 是 | 金額 (為求簡化,此處使用整數儲存) | +| `date` | TEXT | 是 | 發生日期,格式為 `YYYY-MM-DD` | +| `category` | TEXT | 否 | 紀錄分類,例如:飲食、交通、薪水 | +| `description` | TEXT | 否 | 額外的描述與備註 | +| `created_at` | DATETIME| 是 | 系統建立時間,預設為 `CURRENT_TIMESTAMP` | + +## 3. SQL 建表語法 + +建表語法請參考專案根目錄下的 `database/schema.sql` 檔案。 + +## 4. Python Model 程式碼 + +本專案依據架構設計,使用內建的 `sqlite3` 模組搭配 Parameterized Query 實作 CRUD 操作,以確保輕量化且有效防範 SQL Injection。 + +對應的 Python Model 程式碼請參考 `app/models/record.py`。 diff --git a/docs/FLOWCHART.md b/docs/FLOWCHART.md new file mode 100644 index 000000000..c009b6f7b --- /dev/null +++ b/docs/FLOWCHART.md @@ -0,0 +1,74 @@ +# 流程圖文件 (Flowchart) + +根據產品需求文件 (PRD) 與系統架構文件 (Architecture),以下為個人記帳簿系統的使用者流程圖、系統序列圖與功能清單對照表。 + +## 1. 使用者流程圖 (User Flow) + +此流程圖描述使用者從進入網站開始,在各個主要功能之間的操作路徑。 + +```mermaid +flowchart LR + A([使用者開啟網頁]) --> B[首頁 - 總餘額與收支紀錄列表] + B --> C{要執行什麼操作?} + + C -->|新增紀錄| D[點擊新增按鈕] + D --> E[填寫收支表單 (金額、日期、分類、描述)] + E -->|提交| F{資料驗證} + F -->|成功| B + F -->|失敗| E + + C -->|管理紀錄| G[選擇單筆收支紀錄] + G --> H{選擇動作} + H -->|編輯| I[進入編輯模式/表單] + I -->|儲存| B + H -->|刪除| J[確認刪除視窗] + J -->|確認| B +``` + +## 2. 系統序列圖 (Sequence Diagram) + +此序列圖描述「使用者新增一筆收支紀錄」時,系統各元件之間的詳細互動流程。 + +```mermaid +sequenceDiagram + actor User as 使用者 + participant Browser as 瀏覽器 + participant Route as Flask Route + participant Model as Model + participant DB as SQLite + + User->>Browser: 在首頁點擊新增,填寫金額與日期後送出 + Browser->>Route: POST /records (送出表單資料) + Route->>Route: 驗證輸入資料 (例如: 金額是否為數字) + + alt 驗證失敗 + Route-->>Browser: 回傳錯誤訊息,停留在表單頁面 + else 驗證成功 + Route->>Model: 呼叫新增紀錄函式 + Model->>DB: INSERT INTO records ... + DB-->>Model: 寫入成功 + Model-->>Route: 回傳處理成功狀態 + Route-->>Browser: 重導向 (Redirect) 至首頁 + Browser->>Route: GET / + Route->>Model: 查詢最新總餘額與紀錄列表 + Model->>DB: SELECT * FROM records ... + DB-->>Model: 回傳資料結果 + Model-->>Route: 提供資料給 Controller + Route-->>Browser: 渲染首頁 (包含最新餘額與清單) + end +``` + +## 3. 功能清單對照表 + +以下為各項功能對應的 URL 路徑與 HTTP 請求方法規劃: + +| 功能名稱 | URL 路徑 | HTTP 方法 | 說明 | +| :--- | :--- | :--- | :--- | +| **瀏覽首頁 (餘額與列表)** | `/` | `GET` | 顯示目前總餘額以及近期的收支歷史紀錄 | +| **新增收支紀錄頁面** | `/records/new` | `GET` | 顯示填寫收支資料的表單頁面 (如與首頁整合則可省略) | +| **送出新增紀錄** | `/records` | `POST` | 接收表單資料並寫入資料庫,完成後導回首頁 | +| **編輯收支紀錄頁面** | `/records//edit`| `GET` | 顯示帶有原始資料的編輯表單頁面 | +| **更新收支紀錄** | `/records/` | `POST` (或 PUT)| 接收更新資料並寫入資料庫,完成後導回首頁 | +| **刪除收支紀錄** | `/records//delete`| `POST` (或 DELETE)| 將指定 ID 的收支紀錄從資料庫中刪除 | + +> 備註:因為傳統 HTML 表單僅支援 GET 與 POST,編輯與刪除的送出動作常透過 `POST` 方法加上路徑來實作。 diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 000000000..64db34dcc --- /dev/null +++ b/docs/PRD.md @@ -0,0 +1,45 @@ +# 產品需求文件 (PRD):個人記帳簿系統 + +## 1. 專案概述 +- **背景與動機**:現代人常因為缺乏財務管理概念,導致不清楚金錢流向。一個簡單直覺的記帳系統能幫助使用者輕鬆記錄每日收支,進而達成理財的第一步與目標。 +- **目標用戶**:需要追蹤日常財務狀況的個人使用者(如學生、上班族)。 +- **核心價值主張**:提供一個輕量、直覺、無需複雜設定的記帳介面,幫助使用者快速記錄並掌握個人現金流與餘額。 + +## 2. 功能需求 +系統需提供以下主要功能(依核心重要性排序): + +1. **記錄收入** + - *使用者故事*:作為一般使用者,我希望能輸入我的各項收入(如薪資、獎金),以便追蹤我的金錢來源。 +2. **記錄支出** + - *使用者故事*:作為一般使用者,我希望能隨時記錄日常每一筆花費(如餐飲、交通),以便了解金錢去向。 +3. **顯示餘額統計** + - *使用者故事*:作為一般使用者,我希望能在一進系統時看到目前的總結餘,以便隨時掌握自己目前還剩下多少錢可用。 +4. **瀏覽收支歷史紀錄** + - *使用者故事*:作為一般使用者,我希望能瀏覽過往收支明細清單(包含日期、原因與金額),以便回顧過往消費狀況。 +5. **編輯或刪除紀錄** + - *使用者故事*:作為一般使用者,我希望能修改或刪除先前可能記錯的收支紀錄,以便維持整體財務數據的準確性。 + +## 3. 非功能需求 +- **技術限制**:系統以獨立 Web App 形式運作,後端使用 Python Flask 框架,前端畫面透過 Jinja2 模板渲染,資料庫使用 SQLite 進行輕量儲存。 +- **效能考量**:由於為個人單機或輕度連線使用,資料量不大,系統應確保畫面載入與任何操作(新增、寫入)回應時間小於 1 秒。 +- **安全與穩定性考量**:後端實作需能防止基礎的 SQL Injection (例如使用 ORM 或 Prepared Statement),並優雅處理可能的異常輸入(像是金額輸入非數字等錯誤)。確保系統不崩潰。 + +## 4. MVP 範圍 (Minimum Viable Product) +- **Must Have (必須包含)** + - 能夠成功新增收入與支出(需包含:金額、日期、簡單描述)。 + - 在首頁顯示「總餘額」以及近期的「收支紀錄清單」。 +- **Should Have (應該包含)** + - 能夠刪除某筆特定的紀錄。 + - 提供預設的記帳分類(如:飲食、交通、生活用品、薪水)。 +- **Nice to Have (加分項目,未來迭代)** + - 提供簡單的圖表(如圓餅圖)顯示每月各類別花費比例。 + - 將資料匯出成 CSV 檔案的功能。 + +## 5. 專案成員與分工 + +| 角色 | 姓名 | 主要職責 | +| :--- | :--- | :--- | +| PM / 設計 | | 需求確認、Wireframe / UI 設計 | +| 前端開發 | | Jinja2 模板設計與實作、CSS 樣式 | +| 後端開發 | | Flask 路由設計、API / 參數驗證邏輯 | +| 資料庫開發 | | SQLite Schema 規劃與 ORM 串接 | diff --git a/docs/ROUTES.md b/docs/ROUTES.md new file mode 100644 index 000000000..6caf98326 --- /dev/null +++ b/docs/ROUTES.md @@ -0,0 +1,63 @@ +# 路由設計文件 (API Design) + +根據系統架構與流程圖,以下為個人記帳簿系統的路由與頁面規劃。 + +## 1. 路由總覽表格 + +| 功能 | HTTP 方法 | URL 路徑 | 對應模板 | 說明 | +| :--- | :--- | :--- | :--- | :--- | +| 首頁 (餘額與列表) | GET | `/` | `index.html` | 顯示總餘額與歷史紀錄列表 | +| 新增紀錄頁面 | GET | `/records/new` | `records/new.html` | 顯示新增收支的表單 | +| 送出新增紀錄 | POST | `/records` | — | 接收表單資料,寫入 DB,重導向至首頁 | +| 編輯紀錄頁面 | GET | `/records//edit`| `records/edit.html`| 顯示帶有既有資料的編輯表單 | +| 更新紀錄 | POST | `/records//update`| — | 接收表單資料,更新 DB,重導向至首頁 | +| 刪除紀錄 | POST | `/records//delete`| — | 從 DB 刪除該紀錄,重導向至首頁 | + +## 2. 每個路由的詳細說明 + +### 首頁 (`GET /`) +- **輸入**:無 +- **處理邏輯**:呼叫 `RecordModel.get_balance()` 與 `RecordModel.get_all()` +- **輸出**:渲染 `index.html`,傳入 `balance` 與 `records` 變數 +- **錯誤處理**:若資料庫為空,顯示「目前尚無紀錄」的提示 + +### 新增紀錄頁面 (`GET /records/new`) +- **輸入**:無 +- **處理邏輯**:無特殊資料處理 +- **輸出**:渲染 `records/new.html` + +### 送出新增紀錄 (`POST /records`) +- **輸入**:表單欄位 `type`, `amount`, `date`, `category`, `description` +- **處理邏輯**:呼叫 `RecordModel.create(...)` +- **輸出**:成功後重導向 (`redirect`) 到首頁 `/` +- **錯誤處理**:如果必填欄位缺失或格式錯誤(如金額非數字),flash 錯誤訊息並重導向回新增頁面。 + +### 編輯紀錄頁面 (`GET /records//edit`) +- **輸入**:URL 參數 `id` +- **處理邏輯**:呼叫 `RecordModel.get_by_id(id)` +- **輸出**:若找到紀錄,渲染 `records/edit.html` 並傳入 `record`;若找不到,回傳 404 頁面 +- **錯誤處理**:無效的 ID 應直接觸發 404 Not Found + +### 更新紀錄 (`POST /records//update`) +- **輸入**:URL 參數 `id` 與表單欄位 `type`, `amount`, `date`, `category`, `description` +- **處理邏輯**:呼叫 `RecordModel.update(...)` +- **輸出**:成功後重導向至首頁 `/` +- **錯誤處理**:資料驗證失敗則 flash 錯誤訊息,重導向回編輯頁面 + +### 刪除紀錄 (`POST /records//delete`) +- **輸入**:URL 參數 `id` +- **處理邏輯**:呼叫 `RecordModel.delete(id)` +- **輸出**:刪除成功後重導向至首頁 `/` + +## 3. Jinja2 模板清單 + +所有模板皆預計繼承自 `base.html`,以保持網站版型與導覽列風格統一。 + +- **`base.html`**:共用外框(包含導覽列、Flash 訊息區塊、頁尾,以及 CSS 引入)。 +- **`index.html`**:顯示「總餘額卡片」與「歷史收支表格」。 +- **`records/new.html`**:顯示新增紀錄的 HTML 表單。 +- **`records/edit.html`**:顯示修改紀錄的 HTML 表單。 + +## 4. 路由骨架程式碼 + +已在 `app/routes.py` 中建立對應的 Blueprint 路由骨架(目前僅包含定義與註解,未實作邏輯)。 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..f34604eed --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask +python-dotenv diff --git a/run.py b/run.py new file mode 100644 index 000000000..5220d6a00 --- /dev/null +++ b/run.py @@ -0,0 +1,20 @@ +import os +from dotenv import load_dotenv +from app import create_app +from app.models.record import init_db + +# 載入 .env 環境變數 +load_dotenv() + +# 建立 Flask 應用程式 +app = create_app() + +# 設定環境變數中的 SECRET_KEY(若有) +if os.getenv('SECRET_KEY'): + app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') + +if __name__ == '__main__': + # 確保資料庫與資料表已初始化 + init_db() + # 啟動開發伺服器 + app.run(debug=True)