diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/commit/SKILL.md new file mode 100644 index 00000000..efe46e02 --- /dev/null +++ b/.agents/skills/commit/SKILL.md @@ -0,0 +1,125 @@ +--- +name: commit +description: 提交並推送程式碼變更。用於每個開發階段完成後,將成果儲存到 Git 並推送到 GitHub。 +--- + +# Commit Skill — 提交與推送程式碼 + +這個 skill 會引導 AI agent 幫你完成 Git 提交與推送,並在必要時設定 Git 身份識別資訊。 + +## When to use this skill + +- 完成某個開發階段後,想要儲存進度 +- 要把程式碼推送到 GitHub,讓組員可以取得最新版本 +- 遇到 Git 要求設定 `user.name` 或 `user.email` 的提示 + +## 檢查 .gitignore +在推送之前檢查 .gitignore,確保虛擬環境、環境設定與非必要檔案有正確被排除在版本控制之外。 + +## ⚠️ 設定 Git 使用者身份 + +如果 Git 顯示以下提示,需要先設定身份才能 commit: + +``` +Author identity unknown + +*** Please tell me who you are. + +Run + + git config --global user.email "you@example.com" + git config --global user.name "Your Name" +``` + +請使用 `antigravity` 作為 username 與 email: + +```bash +git config --global user.name "antigravity" +git config --global user.email "antigravity" +``` + +> 這樣設定後,之後所有 commit 都不需要再重新設定。 + +## How to use it + +### 基本用法:提交並推送 + +``` +請幫我將目前的變更提交並推送到 GitHub。 + +commit 訊息:[描述這次做了什麼,例如:docs: add PRD] +``` + +AI 會執行以下步驟: +1. 確認 Git 使用者身份是否已設定(若無則設定為 `antigravity`) +2. `git add .` — 加入所有變更 +3. `git commit -m "[你的訊息]"` — 建立 commit +4. `git push` — 推送到 GitHub + +--- + +### 完整提示語(可直接複製使用) + +``` +請幫我提交並推送目前的變更: + +1. 如果尚未設定 Git 身份,請先執行: + git config --global user.name "antigravity" + git config --global user.email "antigravity" + +2. 然後執行: + git add . + git commit -m "[在此填入 commit 訊息]" + git push + +完成後告訴我推送成功,並顯示 commit 的 hash 值。 +``` + +--- + +## Commit 訊息格式建議 + +遵循以下格式,讓歷史紀錄更清楚: + +| 類型 | 使用時機 | 範例 | +| ------ | --------------------- | ---------------------------- | +| `docs` | 新增或修改文件 | `docs: add PRD` | +| `feat` | 新增功能 | `feat: add task create form` | +| `fix` | 修復錯誤 | `fix: form validation error` | +| `chore`| 設定、初始化等雜項 | `chore: init project` | + +--- + +## 各開發階段建議的 commit 訊息 + +| 階段 | 建議 commit 訊息 | +| ---------- | ----------------------------------------- | +| 階段一完成 | `docs: add PRD` | +| 階段二完成 | `docs: add system architecture` | +| 階段三完成 | `docs: add user flowchart` | +| 階段四完成 | `feat: add database schema and models` | +| 階段五完成 | `feat: add route skeleton and template plan` | +| 階段六完成 | `feat: implement [功能名稱]` | + +--- + +## 常見問題 + +**Q: push 時要求輸入帳號密碼?** + +GitHub 已停止支援密碼驗證,請使用 Personal Access Token(PAT): +1. 到 GitHub → Settings → Developer settings → Personal access tokens +2. 產生一個新 token(勾選 `repo` 權限) +3. push 時,「密碼」欄位貼上 token 即可 + +**Q: push 被拒絕(rejected)?** + +可能是遠端有別的組員更新過,先執行: +```bash +git pull --rebase +git push +``` + +**Q: 不小心 commit 了不該 commit 的檔案?** + +告訴 AI:「我剛才不小心 commit 了 [檔案名稱],請幫我移除。」 \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..b4eeff5e --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +FLASK_APP=run.py +FLASK_DEBUG=1 +SECRET_KEY=your_secret_key_here diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c1e69bb0 Binary files /dev/null and b/.gitignore differ diff --git a/__pycache__/run.cpython-314.pyc b/__pycache__/run.cpython-314.pyc new file mode 100644 index 00000000..8854a1ca Binary files /dev/null and b/__pycache__/run.cpython-314.pyc differ diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 00000000..6374c4a5 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,27 @@ +from flask import Flask +from dotenv import load_dotenv +import os + +load_dotenv() + +def create_app(): + app = Flask(__name__) + app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev_default_secret') + + # 確保 instance 資料夾存在,以供 SQLite 使用 + os.makedirs(app.instance_path, exist_ok=True) + + # 註冊 Blueprints + from app.routes.auth import auth_bp + from app.routes.divination import divination_bp + from app.routes.donation import donation_bp + + app.register_blueprint(auth_bp, url_prefix='/auth') + app.register_blueprint(divination_bp) # uses / and /divination + app.register_blueprint(donation_bp, url_prefix='/donation') + + return app + +def init_db(): + from app.models.database import init_db as db_init + db_init() diff --git a/app/__pycache__/__init__.cpython-314.pyc b/app/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 00000000..583ef19f Binary files /dev/null and b/app/__pycache__/__init__.cpython-314.pyc differ diff --git a/app/models/__pycache__/database.cpython-314.pyc b/app/models/__pycache__/database.cpython-314.pyc new file mode 100644 index 00000000..c12af9fc Binary files /dev/null and b/app/models/__pycache__/database.cpython-314.pyc differ diff --git a/app/models/__pycache__/divination.cpython-314.pyc b/app/models/__pycache__/divination.cpython-314.pyc new file mode 100644 index 00000000..8d312903 Binary files /dev/null and b/app/models/__pycache__/divination.cpython-314.pyc differ diff --git a/app/models/__pycache__/donation.cpython-314.pyc b/app/models/__pycache__/donation.cpython-314.pyc new file mode 100644 index 00000000..02293d1e Binary files /dev/null and b/app/models/__pycache__/donation.cpython-314.pyc differ diff --git a/app/models/__pycache__/ingredient.cpython-314.pyc b/app/models/__pycache__/ingredient.cpython-314.pyc new file mode 100644 index 00000000..cf50e610 Binary files /dev/null and b/app/models/__pycache__/ingredient.cpython-314.pyc differ diff --git a/app/models/__pycache__/recipe.cpython-314.pyc b/app/models/__pycache__/recipe.cpython-314.pyc new file mode 100644 index 00000000..3316818a Binary files /dev/null and b/app/models/__pycache__/recipe.cpython-314.pyc differ diff --git a/app/models/__pycache__/user.cpython-314.pyc b/app/models/__pycache__/user.cpython-314.pyc new file mode 100644 index 00000000..3a7e02a2 Binary files /dev/null and b/app/models/__pycache__/user.cpython-314.pyc differ diff --git a/app/models/database.py b/app/models/database.py new file mode 100644 index 00000000..b5467a51 --- /dev/null +++ b/app/models/database.py @@ -0,0 +1,34 @@ +import sqlite3 +import os + +DB_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../../instance/database.db') + +def get_db_connection(): + """ + 建立並回傳一個與 SQLite 資料庫的連線。 + 預設啟用 row_factory 為 sqlite3.Row,以利透過字典鍵值存取資料。 + """ + try: + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + conn.execute('PRAGMA foreign_keys = ON') + return conn + except Exception as e: + print(f"Database connection error: {e}") + raise + +def init_db(): + """ + 初始化資料庫。讀取 schema.sql 並執行建表語句。 + """ + try: + conn = get_db_connection() + schema_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../../database/schema.sql') + if os.path.exists(schema_path): + with open(schema_path, 'r', encoding='utf-8') as f: + conn.executescript(f.read()) + conn.commit() + conn.close() + except Exception as e: + print(f"Database initialization error: {e}") diff --git a/app/models/divination.py b/app/models/divination.py new file mode 100644 index 00000000..0a513c2c --- /dev/null +++ b/app/models/divination.py @@ -0,0 +1,59 @@ +from .database import get_db_connection + +class Divination: + @staticmethod + def create(user_id, div_type, question, result, explanation): + """ + 新增一筆占卜紀錄 + """ + try: + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute( + 'INSERT INTO divinations (user_id, type, question, result, explanation) VALUES (?, ?, ?, ?, ?)', + (user_id, div_type, question, result, explanation) + ) + conn.commit() + div_id = cursor.lastrowid + conn.close() + return div_id + except Exception as e: + print(f"Error creating divination: {e}") + return None + + @staticmethod + def get_by_id(div_id): + """取得單一紀錄""" + try: + conn = get_db_connection() + div = conn.execute('SELECT * FROM divinations WHERE id = ?', (div_id,)).fetchone() + conn.close() + return dict(div) if div else None + except Exception as e: + print(f"Error getting divination: {e}") + return None + + @staticmethod + def get_by_user_id(user_id): + """取得某會員的所有占卜紀錄""" + try: + conn = get_db_connection() + divs = conn.execute('SELECT * FROM divinations WHERE user_id = ? ORDER BY created_at DESC', (user_id,)).fetchall() + conn.close() + return [dict(d) for d in divs] + except Exception as e: + print(f"Error getting divinations for user: {e}") + return [] + + @staticmethod + def delete(div_id): + """刪除占卜紀錄""" + try: + conn = get_db_connection() + conn.execute('DELETE FROM divinations WHERE id = ?', (div_id,)) + conn.commit() + conn.close() + return True + except Exception as e: + print(f"Error deleting divination: {e}") + return False diff --git a/app/models/donation.py b/app/models/donation.py new file mode 100644 index 00000000..45bf9f6a --- /dev/null +++ b/app/models/donation.py @@ -0,0 +1,34 @@ +from .database import get_db_connection + +class Donation: + @staticmethod + def create(user_id, amount, status='completed'): + """ + 新增一筆隨喜捐款紀錄 + """ + try: + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute( + 'INSERT INTO donations (user_id, amount, status) VALUES (?, ?, ?)', + (user_id, amount, status) + ) + conn.commit() + don_id = cursor.lastrowid + conn.close() + return don_id + except Exception as e: + print(f"Error creating donation: {e}") + return None + + @staticmethod + def get_by_user_id(user_id): + """取得某會員的歷史隨喜紀錄""" + try: + conn = get_db_connection() + dons = conn.execute('SELECT * FROM donations WHERE user_id = ? ORDER BY created_at DESC', (user_id,)).fetchall() + conn.close() + return [dict(d) for d in dons] + except Exception as e: + print(f"Error getting donations for user: {e}") + return [] diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 00000000..828ee281 --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,106 @@ +from .database import get_db_connection + +class User: + @staticmethod + def create(username, password_hash, is_admin=0): + """ + 新增一筆使用者記錄。 + 回傳新建使用者的 id,發生例外時回傳 None。 + """ + try: + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute( + 'INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, ?)', + (username, password_hash, is_admin) + ) + conn.commit() + user_id = cursor.lastrowid + conn.close() + return user_id + except Exception as e: + print(f"Error creating user: {e}") + return None + + @staticmethod + def get_by_id(user_id): + """ + 根據 id 取得單筆使用者記錄。 + """ + try: + conn = get_db_connection() + user = conn.execute('SELECT * FROM users WHERE id = ?', (user_id,)).fetchone() + conn.close() + return dict(user) if user else None + except Exception as e: + print(f"Error getting user by id: {e}") + return None + + @staticmethod + def get_by_username(username): + """ + 根據 username 取得單筆使用者記錄。 + """ + try: + conn = get_db_connection() + user = conn.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone() + conn.close() + return dict(user) if user else None + except Exception as e: + print(f"Error getting user by username: {e}") + return None + + @staticmethod + def get_all(): + """ + 取得所有使用者記錄。 + """ + try: + conn = get_db_connection() + users = conn.execute('SELECT * FROM users').fetchall() + conn.close() + return [dict(u) for u in users] + except Exception as e: + print(f"Error getting all users: {e}") + return [] + + @staticmethod + def update(user_id, password_hash=None, is_admin=None): + """ + 更新指定的 user 記錄。 + 只更新傳入且非 None 的參數。 + """ + try: + conn = get_db_connection() + user = User.get_by_id(user_id) + if not user: + return False + + new_password = password_hash if password_hash is not None else user['password_hash'] + new_is_admin = is_admin if is_admin is not None else user['is_admin'] + + conn.execute( + 'UPDATE users SET password_hash = ?, is_admin = ? WHERE id = ?', + (new_password, new_is_admin, user_id) + ) + conn.commit() + conn.close() + return True + except Exception as e: + print(f"Error updating user: {e}") + return False + + @staticmethod + def delete(user_id): + """ + 刪除指定使用者記錄。 + """ + try: + conn = get_db_connection() + conn.execute('DELETE FROM users WHERE id = ?', (user_id,)) + conn.commit() + conn.close() + return True + except Exception as e: + print(f"Error deleting user: {e}") + return False diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 00000000..d48c762f --- /dev/null +++ b/app/routes/__init__.py @@ -0,0 +1 @@ +# Initialize routes package diff --git a/app/routes/__pycache__/__init__.cpython-314.pyc b/app/routes/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 00000000..eeb63b24 Binary files /dev/null and b/app/routes/__pycache__/__init__.cpython-314.pyc differ diff --git a/app/routes/__pycache__/admin.cpython-314.pyc b/app/routes/__pycache__/admin.cpython-314.pyc new file mode 100644 index 00000000..6225089a Binary files /dev/null and b/app/routes/__pycache__/admin.cpython-314.pyc differ diff --git a/app/routes/__pycache__/auth.cpython-314.pyc b/app/routes/__pycache__/auth.cpython-314.pyc new file mode 100644 index 00000000..7e3ecc0d Binary files /dev/null and b/app/routes/__pycache__/auth.cpython-314.pyc differ diff --git a/app/routes/__pycache__/divination.cpython-314.pyc b/app/routes/__pycache__/divination.cpython-314.pyc new file mode 100644 index 00000000..4247b9ea Binary files /dev/null and b/app/routes/__pycache__/divination.cpython-314.pyc differ diff --git a/app/routes/__pycache__/donation.cpython-314.pyc b/app/routes/__pycache__/donation.cpython-314.pyc new file mode 100644 index 00000000..1de3c974 Binary files /dev/null and b/app/routes/__pycache__/donation.cpython-314.pyc differ diff --git a/app/routes/__pycache__/recipe.cpython-314.pyc b/app/routes/__pycache__/recipe.cpython-314.pyc new file mode 100644 index 00000000..0f9b24ce Binary files /dev/null and b/app/routes/__pycache__/recipe.cpython-314.pyc differ diff --git a/app/routes/auth.py b/app/routes/auth.py new file mode 100644 index 00000000..7ea5cb2a --- /dev/null +++ b/app/routes/auth.py @@ -0,0 +1,90 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, session +import bcrypt +from app.models.user import User + +auth_bp = Blueprint('auth', __name__) + +@auth_bp.route('/register', methods=['GET', 'POST']) +def register(): + """ + GET: 顯示註冊表單 (templates/auth/register.html)。 + POST: 接收表單資料,驗證輸入是否合法、信箱是否重複,寫入資料庫並重新導向至登入頁面。 + """ + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + confirm_password = request.form.get('confirm_password') + + # 基礎輸入驗證 + if not username or not password or not confirm_password: + flash('請填寫所有必填欄位!', 'danger') + return redirect(url_for('auth.register')) + + if password != confirm_password: + flash('兩次輸入的密碼不一致!', 'danger') + return redirect(url_for('auth.register')) + + # 檢查帳號是否已被註冊 + existing_user = User.get_by_username(username) + if existing_user: + flash('該使用者帳號 (信箱) 已被註冊!', 'danger') + return redirect(url_for('auth.register')) + + # 密碼雜湊加密 (Bcrypt) + try: + password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + user_id = User.create(username=username, password_hash=password_hash, is_admin=0) + + if user_id: + flash('註冊成功!請登入。', 'success') + return redirect(url_for('auth.login')) + else: + flash('系統錯誤,註冊失敗,請稍後再試。', 'danger') + except Exception as e: + print(f"Bcrypt Error: {e}") + flash('伺服器發生例外錯誤。', 'danger') + + return redirect(url_for('auth.register')) + + return render_template('auth/register.html') + + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + """ + GET: 顯示登入表單 (templates/auth/login.html)。 + POST: 接收表單並比對資料庫密碼,驗證成功後將使用者狀態存入 Session,將用戶導回首頁。 + """ + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + + if not username or not password: + flash('請輸入帳號與密碼!', 'danger') + return redirect(url_for('auth.login')) + + user = User.get_by_username(username) + if user: + # 校驗雜湊密碼 + if bcrypt.checkpw(password.encode('utf-8'), user['password_hash'].encode('utf-8')): + # 設定 Session + session['user_id'] = user['id'] + session['username'] = user['username'] + session['is_admin'] = user['is_admin'] + flash('登入成功!', 'success') + return redirect(url_for('divination.index')) + + flash('帳號或密碼錯誤!', 'danger') + return redirect(url_for('auth.login')) + + return render_template('auth/login.html') + + +@auth_bp.route('/logout', methods=['GET']) +def logout(): + """ + GET: 清除目前的 Session 登入狀態,重導向至首頁。 + """ + session.clear() + flash('您已成功登出。', 'info') + return redirect(url_for('recipe.index')) diff --git a/app/routes/divination.py b/app/routes/divination.py new file mode 100644 index 00000000..89aac418 --- /dev/null +++ b/app/routes/divination.py @@ -0,0 +1,86 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, session +import random +from app.models.divination import Divination + +divination_bp = Blueprint('divination', __name__) + +def login_required(f): + from functools import wraps + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + flash('請先登入以使用專屬占卜與完整紀錄功能!', 'warning') + return redirect(url_for('auth.login')) + return f(*args, **kwargs) + return decorated_function + +@divination_bp.route('/', methods=['GET']) +def index(): + return render_template('divination/index.html') + +@divination_bp.route('/divination/temple', methods=['GET', 'POST']) +@login_required +def temple_draw(): + if request.method == 'POST': + question = request.form.get('question', '未填寫問題') + + # Simulate simple 60-jiazi draw + draw_number = random.randint(1, 60) + result_title = f"觀音靈籤 第 {draw_number} 籤" + explanation = f"針對您的問題:「{question}」。\n這支籤代表著順流而行,切莫急躁。萬事互相效力,時間到了自然會有好的結果展開。建議您保持內心的平靜,多做善事累積福報。神明指示您只需依循本心,凡事不強求。" + + div_id = Divination.create(session['user_id'], 'temple', question, result_title, explanation) + if div_id: + flash('求籤成功!神明已經給予了指引。', 'success') + return redirect(url_for('divination.result', id=div_id)) + else: + flash('系統發生錯誤,無法儲存。', 'danger') + + return render_template('divination/temple_draw.html') + +@divination_bp.route('/divination/tarot', methods=['GET', 'POST']) +@login_required +def tarot_draw(): + if request.method == 'POST': + question = request.form.get('question', '未填寫問題') + + # Simple Major Arcana simulate + tarot_cards = ["愚者 (The Fool)", "魔術師 (The Magician)", "女祭司 (The High Priestess)", "皇后 (The Empress)", "皇帝 (The Emperor)", "教皇 (The Hierophant)", "戀人 (The Lovers)", "戰車 (The Chariot)", "力量 (Strength)", "隱者 (The Hermit)", "命運之輪 (Wheel of Fortune)", "正義 (Justice)", "太陽 (The Sun)", "世界 (The World)"] + card = random.choice(tarot_cards) + position = random.choice(["正位", "逆位"]) + result_title = f"{card} - {position}" + + explanation = f"針對您的問題:「{question}」。\n您抽到了充滿靈性的代表牌。這象徵著您正面臨一個關鍵的轉折點,傾聽自己內心的聲音將是最佳解答。宇宙正在暗中協助您度過難關,請保持正能量。" + + div_id = Divination.create(session['user_id'], 'tarot', question, result_title, explanation) + if div_id: + flash('塔羅抽牌完成!宇宙傳遞了深層的訊息。', 'success') + return redirect(url_for('divination.result', id=div_id)) + else: + flash('系統發生錯誤,無法儲存。', 'danger') + + return render_template('divination/tarot_draw.html') + +@divination_bp.route('/divination/result/', methods=['GET']) +@login_required +def result(id): + div = Divination.get_by_id(id) + if not div or div['user_id'] != session['user_id']: + flash('找不到該紀錄或您沒有權限。', 'danger') + return redirect(url_for('divination.index')) + return render_template('divination/result.html', div=div) + +@divination_bp.route('/divination/history', methods=['GET']) +@login_required +def history(): + divs = Divination.get_by_user_id(session['user_id']) + return render_template('divination/history.html', divs=divs) + +@divination_bp.route('/divination//delete', methods=['POST']) +@login_required +def delete(id): + div = Divination.get_by_id(id) + if div and div['user_id'] == session['user_id']: + Divination.delete(id) + flash('紀錄已成功刪除。', 'info') + return redirect(url_for('divination.history')) diff --git a/app/routes/donation.py b/app/routes/donation.py new file mode 100644 index 00000000..07ac4be6 --- /dev/null +++ b/app/routes/donation.py @@ -0,0 +1,40 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, session +from app.models.donation import Donation + +donation_bp = Blueprint('donation', __name__) + +def login_required(f): + from functools import wraps + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + flash('請先登入會員以完成線上隨喜程序。', 'warning') + return redirect(url_for('auth.login')) + return f(*args, **kwargs) + return decorated_function + +@donation_bp.route('/', methods=['GET']) +def donate(): + return render_template('donation/donate.html') + +@donation_bp.route('/process', methods=['POST']) +@login_required +def process(): + amount = request.form.get('amount', type=int, default=0) + if amount <= 0: + flash('請輸入大於0的金額。', 'danger') + return redirect(url_for('donation.donate')) + + # Simulate payment processing MVP + don_id = Donation.create(session['user_id'], amount, status='completed') + if don_id: + return redirect(url_for('donation.thanks', amount=amount)) + else: + flash('處理發生錯誤。', 'danger') + return redirect(url_for('donation.donate')) + +@donation_bp.route('/thanks', methods=['GET']) +@login_required +def thanks(): + amount = request.args.get('amount', 0) + return render_template('donation/thanks.html', amount=amount) diff --git a/app/static/css/.gitkeep b/app/static/css/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/static/images/.gitkeep b/app/static/images/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/static/js/.gitkeep b/app/static/js/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/templates/auth/.gitkeep b/app/templates/auth/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html new file mode 100644 index 00000000..5994debc --- /dev/null +++ b/app/templates/auth/login.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+

歡迎回來 👋

+

請登入以存取您的專屬料理空間

+
+ +
+
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+

還沒有帳號嗎? + 免費註冊 +

+
+
+
+
+{% endblock %} diff --git a/app/templates/auth/register.html b/app/templates/auth/register.html new file mode 100644 index 00000000..12b10ca2 --- /dev/null +++ b/app/templates/auth/register.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+

建立新帳號 ✨

+

加入我們,開始打造您的個人食譜庫

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+

已經有帳號了? + 立即登入 +

+
+
+
+
+{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 00000000..01c3ab88 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,199 @@ + + + + + + 線上祈福與算命占卜系統 + + + + + + + + + + + + + + + +
+ +
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} +
+
+ + + {% block content %}{% endblock %} +
+ + + + + diff --git a/app/templates/divination/history.html b/app/templates/divination/history.html new file mode 100644 index 00000000..7ef7f66a --- /dev/null +++ b/app/templates/divination/history.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} +{% block content %} +
+

我的專屬紀錄區 📜

+ 返回占卜大廳 +
+ +
+
+
+
+ + + + + + + + + + + + {% for div in divs %} + + + + + + + + {% else %} + + {% endfor %} + +
占卜時間種類所問之事抽得結果操作
{{ div.created_at[:16] }} + {% if div.type == 'temple' %} + 靈籤 + {% else %} + 塔羅 + {% endif %} + {{ div.question }}{{ div.result }} + 詳解 +
+ +
+
目前還沒有任何占卜紀錄。前往大廳尋求指引吧!
+
+
+
+
+{% endblock %} diff --git a/app/templates/divination/index.html b/app/templates/divination/index.html new file mode 100644 index 00000000..e0a3f692 --- /dev/null +++ b/app/templates/divination/index.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

尋求宇宙與神明的指引

+

無論是傳統廟宇的聖意,或是西方神祕學的卡牌,
在此處靜下心來,誠心默念您的疑問。

+
+
+ +
+
+
+
🏮
+

觀世音靈籤

+

源自傳統廟宇信仰。請誠心稟報本名、生辰、住址與所求之事,擲出聖筊即可得籤。

+ 前往求籤 +
+
+
+
+
🔮
+

塔羅牌占卜

+

透過西方神秘學大阿爾克那牌的指引,探索潛意識與未來的共鳴。適合感情與人生抉擇。

+ 抽取專屬卡牌 +
+
+
+ +
+
+

感念恩賜,線上隨喜

+

若您覺得指示對您有所幫助,您可以進行線上香油錢捐獻,這將轉換為系統營運的能量亦累積您的福報。

+ 👉 點我前往捐獻結緣 +
+
+{% endblock %} diff --git a/app/templates/divination/result.html b/app/templates/divination/result.html new file mode 100644 index 00000000..52ca41af --- /dev/null +++ b/app/templates/divination/result.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% block content %} +
+
+
+
+

您所抽到的是

+

{{ div.result }}

+
+ 占卜類型:{% if div.type == 'temple' %}觀音靈籤{% else %}塔羅牌{% endif %} +
+
+ +
+
您的疑惑:
+
+

{{ div.question }}

+
+
+ +
+
神明與宇宙的回應:
+
{{ div.explanation }}
+
+ +
+

紀錄於:{{ div.created_at[:16] }}

+
+ + +
+
+
+{% endblock %} diff --git a/app/templates/divination/tarot_draw.html b/app/templates/divination/tarot_draw.html new file mode 100644 index 00000000..cb829a07 --- /dev/null +++ b/app/templates/divination/tarot_draw.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% block content %} +
+
+
🔮
+

塔羅牌占卜

+

深呼吸三次,將您的心智集中在您想詢問的事情上。準備好後,寫下您的問題並點擊抽取塔羅牌,傾聽潛意識的回音。

+ +
+
+
+ + +
+ +
+ +
+
+
+ +
+ ※ 塔羅占卜結果將自動保存至您的專屬紀錄區。 +
+
+
+{% endblock %} diff --git a/app/templates/divination/temple_draw.html b/app/templates/divination/temple_draw.html new file mode 100644 index 00000000..4ddb395e --- /dev/null +++ b/app/templates/divination/temple_draw.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
⛩️
+

誠心求籤 (觀音靈籤)

+

在心中默念您的姓名、出生年月日、住址,以及您目前遭遇的困難與疑問。想清楚後,填寫您的疑惑並點擊擲筊。

+ +
+
+
+ + +
+ +
+ +
+
+
+ +
+ ※ 系統將模擬擲出聖筊的瞬間,結果將自動保存至您的個人歷史紀錄。 +
+
+
+{% endblock %} diff --git a/app/templates/donation/donate.html b/app/templates/donation/donate.html new file mode 100644 index 00000000..2f08a385 --- /dev/null +++ b/app/templates/donation/donate.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% block content %} +
+
+
💰
+

線上隨喜添香油

+

布施結善緣,每一分捐獻都將轉換為護持系統的能量,並為您累積無邊福報。

+ +
+
+
+ +
+ NT$ + +
+
+ +
+ + +
+ +
+ +
+
+
+
+
+{% endblock %} diff --git a/app/templates/donation/thanks.html b/app/templates/donation/thanks.html new file mode 100644 index 00000000..5d45fe39 --- /dev/null +++ b/app/templates/donation/thanks.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% block content %} +
+
+ +
+
🎇
+
+
+ +

感恩您的隨喜護持!

+

本系統已收到您的香油錢 NT$ {{ amount }} 元。
神明已降下賜福,願您平安喜樂,萬事如意。

+ +
+

線上虛擬感謝狀

+

茲感謝您發心護持「祈福與占卜系統」

+

功德無量

+
+ + 返回占卜大廳 +
+
+ +{% endblock %} diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 00000000..f462f82b --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,32 @@ +PRAGMA foreign_keys = ON; + +-- 使用者 +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + is_admin INTEGER NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 占卜紀錄 +CREATE TABLE IF NOT EXISTS divinations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + type TEXT NOT NULL, + question TEXT NOT NULL, + result TEXT NOT NULL, + explanation TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- 香油錢隨喜紀錄 +CREATE TABLE IF NOT EXISTS donations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + amount INTEGER NOT NULL, + status TEXT DEFAULT 'completed', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE +); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 00000000..54b70c6a --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,94 @@ +# 系統架構設計文件 (ARCHITECTURE) - 食譜收藏夾系統 + +## 1. 技術架構說明 + +本系統基於 Python 的 **Flask** 框架進行開發,並採用伺服器端渲染 (Server-Side Rendering, SSR) 的方式,讓所有的頁面產製與核心邏輯皆在後端統一處理與產出。 +* **後端框架 (Flask)**:輕量、具有高擴展性,能快速搭建應用程式,非常適合開發中小型 Web 專案。 +* **視圖模板 (Jinja2)**:與 Flask 緊密整合的模板語言。負責把後端處理好的資料庫內容,動態結合 HTML 直接發送給瀏覽器。因此,不需要建立複雜的前後端分離 API,大幅降低開發初期的時間成本。 +* **資料庫 (SQLite)**:以檔案的輕量級格式來儲存關聯式資料,符合 MVP(最小可行性產品)階段的需求,本地端開發、備份皆非常簡單。 +* **前端設計 (Vanilla CSS + JS)**:不依賴如 React 或 Vue 的大型框架。透過手寫 CSS 處理介面外觀,並用基本的 JavaScript 強化前端操作體驗(如按鈕互動、表單基本檢查)。 + +### Flask MVC 對應模式 +本專案在實作切分上,遵循 MVC (Model-View-Controller) 架構精神: +* **Model (模型)**:負責定義資料的結構與欄位,以及與資料庫直接進行讀寫操作(包括:使用者資料表、食譜資料表、與多對多的食材關聯)。對應專案的 `models/` 目錄。 +* **View (視圖)**:負責呈現給使用者操作、閱讀的視覺介面。在此專案中對應到 `templates/` 目錄內的 `.html` 檔案 (透過 Jinja2 渲染)。 +* **Controller (控制器)**:負責接收外界網頁發送的 HTTP Request (如點擊搜尋、送出表單),再透過呼叫 Model 層來處理資料,接著傳遞回 View 介面。對應專案中各個 `routes/` 檔案內的 API 與路由 (@app.route)。 + +--- + +## 2. 專案資料夾結構 + +為保持程式碼的條理分明與後續容易延伸,我們設計以下資料夾結構: + +```text +recipe_app/ +│ +├── app/ ← 存放應用程式主體邏輯的目錄 +│ ├── __init__.py ← 初始化 Flask 實體,並在此註冊各種設定與 Blueprints +│ ├── models/ ← 資料庫對應的 Model 內容存放區 +│ │ ├── user.py ← 使用者關聯資料表 +│ │ ├── recipe.py ← 食譜主體、食材清單、多對多關聯等結構 +│ │ └── database.py ← 給 SQLite3 或 SQLAlchemy 共用的連線與管理設定 +│ ├── routes/ ← Flask 各項業務與路由規則 (Controller) +│ │ ├── auth.py ← 會員註冊、登入登出功能模組 +│ │ ├── recipe.py ← 處理食譜新增、查詢與多條件食材搜尋等模組 +│ │ └── admin.py ← 管理員檢視和下架內容等權限模組 +│ ├── templates/ ← Jinja2 網頁模板放置區 (View) +│ │ ├── base.html ← 全站共用的佈局版型 (導覽列與頁尾) +│ │ ├── auth/ ← 登入、註冊表單等登錄畫面 +│ │ ├── recipe/ ← 食譜列表首頁、食譜單一內頁與新增表單 +│ │ └── admin/ ← 後台管理專用介面 +│ └── static/ ← 靜態資源區(瀏覽器直接抓取的檔案) +│ ├── css/ ← 純手寫 CSS 樣式 +│ ├── js/ ← 瀏覽器端用 JavaScript 檔案 +│ └── images/ ← 備用的圖片存放區 (含使用者上傳儲存的內容) +│ +├── instance/ ← 不納入 Git 版控,安全層級較高的運行資料擺放區 +│ └── database.db ← SQLite 實體資料庫檔案 +│ +├── config.py ← 程式全域環境變數及安全 Secret Key 設定檔 +├── run.py ← 整個專案程式啟動點 +└── requirements.txt ← 專案必須要用的 Python 依賴套件表 (Flask 等) +``` + +--- + +## 3. 元件關係圖 + +以下展示專案在執行時,使用者端至後端的處理互動順序: + +```mermaid +flowchart TD + Browser[使用者的瀏覽器Browser] + + subgraph Flask Application [Flask 應用程式] + Router[Flask 路由 Route / Controller] + JinjaTemplate[Jinja2 模板 Template / View] + Model[Database 資料庫模型 Model] + end + + DB[(SQLite 資料庫)] + + Browser -- "1. 送出搜尋多個食材的 Request (GET / POST)" --> Router + Router -- "2. 進行業務判斷與呼叫模型" --> Model + Model -- "3. 實際對資料庫進行檢索 (SQL)" --> DB + DB -- "4. 回傳符合條件的食譜紀錄" --> Model + Model -- "5. 將內容封裝回傳" --> Router + Router -- "6. 結合結果資料叫用 HTML" --> JinjaTemplate + JinjaTemplate -- "7. 渲染生成完整的網頁" --> Browser +``` + +--- + +## 4. 關鍵設計決策 + +以下為為符合 PRD 需求而訂立的重要設計決策方向: + +1. **無前後端分離設計(降低 MVP 開發複雜度)** + * **原因**:在此階段 (MVP) 首重驗證商業邏輯與核心功能的實作可行性。採取 Jinja2 進行一體的 HTML 畫面伺服端渲染,團隊可以省去撰寫各種規格嚴謹之 RESTful API 與處理前端狀態庫之門檻,將專注力放在「會員」與「食材搜尋關聯」上。 +2. **安全性全面防堵(密碼防護與過濾不當輸出)** + * **原因**:用戶若需註冊個人帳號,不能明文存放密碼,為遵循資訊安全最佳實踐必須導入如 `bcrypt` 來進行不可逆的雜湊加鹽處理。此外,對於 PRD 中要求的表單內容輸入防護,採用 Jinja2 本身內建的強大 Escaping 排版與嚴謹的資料庫預句處理 (Prepared Statements 或 ORM),可直接免除大量的 SQL Injection 及 XSS 攻擊的威脅。 +3. **多對多食材關聯設計與結構最佳化** + * **原因**:核心功能需支援「透過多樣食材篩選食譜」。傳統的單一字串模糊查詢會造成極差的匹配效能。於是我們在資料庫設計端會採取 `食譜(Recipe)` 與 `食材(Ingredient)` 分開建立,中間運用 `Recipe_Ingredient_Map` 對應表的「多對多 (Many-to-Many)」關聯。即使需要擴增幾千筆食材組合查詢,也能順利被 SQLite 單單用優化過的語法執行出來。 +4. **採用 Blueprint 路由模組化功能** + * **原因**:因為專案系統裡,明顯區分出了一般使用者認證 (`auth`)、資料核心業務 (`recipe`) 與特殊權限操作 (`admin`) 三條截然不同的線頭。使用 Flask 提供的 `Blueprint` 功能來做架構上的切分,可以降低各路由之間的依賴程度,未來新增需求時不怕會導致整個控制器大亂。 diff --git a/docs/DB_DESIGN.md b/docs/DB_DESIGN.md new file mode 100644 index 00000000..0febab62 --- /dev/null +++ b/docs/DB_DESIGN.md @@ -0,0 +1,63 @@ +# 祈福與算命占卜系統 - 資料庫設計 (Database Design) + +## 1. 實體關聯圖 (ER Diagram) + +```mermaid +erDiagram + users { + int id PK + string username + string password_hash + boolean is_admin + datetime created_at + } + divinations { + int id PK + int user_id FK + string type + string question + string result + text explanation + datetime created_at + } + donations { + int id PK + int user_id FK + int amount + string status + datetime created_at + } + users ||--o{ divinations : "performs" + users ||--o{ donations : "makes" +``` + +## 2. 資料表定義 (Table Definitions) + +### 2.1 users (使用者表) +| 欄位名稱 (Field) | 資料型別 (Type) | 約束條件 (Constraints) | 說明 (Description) | +|---|---|---|---| +| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 使用者唯一識別碼 | +| username | TEXT | UNIQUE, NOT NULL | 登入帳號或信箱 | +| password_hash | TEXT | NOT NULL | 加密後的密碼 | +| is_admin | INTEGER | DEFAULT 0 | 系統管理員標記 | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 註冊時間 | + +### 2.2 divinations (占卜紀錄表) +| 欄位名稱 (Field) | 資料型別 (Type) | 約束條件 (Constraints) | 說明 (Description) | +|---|---|---|---| +| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 紀錄唯一識別碼 | +| user_id | INTEGER | FOREIGN KEY (users.id) | 建立紀錄的使用者 ID | +| type | TEXT | NOT NULL | 占卜類型 (如:'temple', 'tarot') | +| question | TEXT | NOT NULL | 詢問的事項/問題 | +| result | TEXT | NOT NULL | 抽出來的結果(例如:第23籤、力量牌正位) | +| explanation | TEXT | | 系統回傳的籤詩解析或占卜詳解 | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 占卜時間 | + +### 2.3 donations (香油錢隨喜紀錄表) +| 欄位名稱 (Field) | 資料型別 (Type) | 約束條件 (Constraints) | 說明 (Description) | +|---|---|---|---| +| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 捐款紀錄唯一識別碼 | +| user_id | INTEGER | FOREIGN KEY (users.id) | 捐款的使用者 ID | +| amount | INTEGER | NOT NULL | 隨喜金額 | +| status | TEXT | DEFAULT 'completed' | 交易狀態字串 | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 交易時間 | diff --git a/docs/FLOWCHART.md b/docs/FLOWCHART.md new file mode 100644 index 00000000..9afd00e5 --- /dev/null +++ b/docs/FLOWCHART.md @@ -0,0 +1,80 @@ +# 系統與使用者流程圖 (FLOWCHART) - 食譜收藏夾系統 + +本文件依據 [PRD.md](./PRD.md) 和 [ARCHITECTURE.md](./ARCHITECTURE.md) 的規格,繪製「使用者流程圖」與「系統序列圖」,並整理出「功能清單對照表」,以視覺化方式呈現系統藍圖。 + +## 1. 使用者流程圖 (User Flow) + +此流程圖展示使用者進入系統後的各種操作路徑,包含瀏覽、搜尋、註冊登入及食譜管理等行為。 + +```mermaid +flowchart LR + Start([使用者首頁 / 訪客]) --> VisitList[瀏覽公開食譜清單] + Start --> Search[使用搜尋功能] + Start --> Auth{是否擁有會員帳號?} + + Search --> KeywordSearch[輸入關鍵字尋找食譜] + Search --> IngredientSearch[輸入多樣食材組合找靈感] + + Auth -->|否| Register[進入註冊頁面] + Auth -->|是| Login[進入登入頁面] + + Register --> Login + Login --> MemberDash([會員專屬空間]) + + MemberDash --> ViewMyRecipes[檢視自己的食譜收藏] + MemberDash --> AddRecipe[新增食譜與食材] + MemberDash --> EditRecipe[修改/編輯現有食譜] + MemberDash --> DeleteRecipe[刪除不需保留的食譜] + MemberDash --> Logout([登出系統]) +``` + +## 2. 系統序列圖 (Sequence Diagram) + +以下序列圖以「**使用者新增食譜**」這項核心功能為例,展示資料由前端傳遞至後端進行處理並儲存的完整步驟。 + +```mermaid +sequenceDiagram + actor User as 使用者 + participant Browser as 瀏覽器 (HTML/JS) + participant Flask as Flask 路由 (Controller) + participant Model as Recipe 模型 (Model) + participant DB as SQLite 資料庫 + + User->>Browser: 填寫食譜名稱、步驟與多樣食材並點擊送出 + Browser->>Flask: 發送 POST 請求至 /recipe/new + Flask->>Flask: 驗證使用者是否已登入及輸入格式 + alt 驗證失敗 + Flask-->>Browser: 返回錯誤訊息提示 (Flash) + Browser-->>User: 畫面顯示「輸入有誤」 + else 驗證成功 + Flask->>Model: 解析表單,呼叫新增食譜函式並帶入參數 + Model->>DB: INSERT INTO recipes (建立食譜主表) + DB-->>Model: 回傳 recipe_id + Model->>DB: INSERT INTO ingredients (建立新食材) + Model->>DB: INSERT INTO recipe_ingredient_map (建立多對多關聯) + DB-->>Model: 儲存成功 + Model-->>Flask: 回傳建立成功狀態 + Flask-->>Browser: HTTP 302 重導向 (Redirect) 至該食譜單一內頁 + Browser-->>User: 畫面顯示新增好的食譜詳情 + end +``` + +## 3. 功能清單對照表 + +此表格列出本專案核心功能與對應的 URL 設計、HTTP 方法。此設計遵守了 RESTful 精神與本專案的 Blueprint 拆分原則。 + +| 功能區塊 | 功能名稱 | URL 路徑 | HTTP 方法 | 備註說明 | +| :--- | :--- | :--- | :--- | :--- | +| **公開瀏覽** | 首頁 (最新食譜列表) | `/` | GET | 訪客不需登入即可觀看 | +| | 檢視食譜詳細內容 | `/recipe/` | GET | 顯示食譜做法與配料 | +| | 關鍵字搜尋食譜 | `/search` | GET | 帶入查詢參數 `?q=xxx` | +| | 食材組合搜尋推薦 | `/search/ingredients` | GET | 帶入查詢參數 `?items=蛋,番茄` | +| **會員認證** | 註冊新帳號 | `/auth/register` | GET, POST | GET 取表單,POST 動作 | +| | 會員登入 | `/auth/login` | GET, POST | | +| | 會員登出 | `/auth/logout` | GET | 登出並清除 Session | +| **食譜管理** | 檢視個人專屬食譜區 | `/recipe/my` | GET | 需登入,列出該會員所建食譜 | +| | 新增食譜 | `/recipe/new` | GET, POST | 需登入,填寫各種欄位 | +| | 編輯食譜 | `/recipe//edit` | GET, POST | 只能修改自己名下的食譜 | +| | 刪除食譜 | `/recipe//delete`| POST | 同上,透過獨立 POST 接口防誤刪 | +| **系統管理** | 後台資料全覽 | `/admin` | GET | 僅限特定身分存取 (Admin) | +| | 強制下架違規食譜 | `/admin/recipe//delete` | POST | 系統管理員專屬權限 | diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 00000000..ecebb389 --- /dev/null +++ b/docs/PRD.md @@ -0,0 +1,55 @@ +# 產品需求文件 (PRD) - 食譜收藏夾系統 + +## 1. 專案概述 +- **背景與動機**:現代人注重飲食,常在網路上看到喜歡的食譜,但缺乏一個集中的地方進行收藏與整理。有時打開冰箱看著現有的食材,卻沒有靈感不知道能變出什麼菜。本系統旨在提供一個方便儲存、管理並能透過現有食材找尋靈感的食譜管理平台。 +- **目標用戶**: + - **一般人**:需要收藏食譜、找尋料理靈感的使用者。 + - **管理員**:維護平台內容、管理用戶與系統健康度的角色。 +- **核心價值主張**:提供一個直覺易用的個人食譜收藏空間,並支援強大的「食材組合」搜尋功能,解決「不知道今天要煮什麼」的核心痛點。 + +## 2. 功能需求 +系統涵蓋以下核心功能,各項功能的使用者故事 (User Story) 如下: + +1. **儲存食譜** + - 作為 **一般人**,我希望 **能夠將自己喜歡或創作的食譜(包含名稱、食材、步驟)儲存到系統中**,以便 **在日後準備料理時能快速查閱**。 +2. **瀏覽、搜尋食譜** + - 作為 **一般人**,我希望 **能夠透過關鍵字或分類來瀏覽與搜尋食譜**,以便 **快速找到我想做的菜餚**。 +3. **從食材組合搜尋食譜** + - 作為 **一般人**,我希望 **能夠輸入我手邊現有的多樣食材**,以便 **系統能過濾並建議我可以直接用這些食材做出的食譜**。 +4. **使用者註冊與身份管理** + - 作為 **一般人**,我希望 **能夠註冊個人帳號並登入系統**,以便 **我能擁有專屬的食譜收藏空間,不會與他人混淆**。 +5. **後台管理與內容維護** + - 作為 **管理員**,我希望 **能夠檢視全站食譜內容與用戶清單,並得以下架不當內容**,以便 **維持平台內容的品質與秩序**。 + +## 3. 非功能需求 +- **技術限制**: + - 框架與後端:基於 Web 服務,後端須使用 Python `Flask` 框架。 + - 前端與樣式:HTML 模板使用 `Jinja2` 渲染,搭配 Vanilla CSS 與 JavaScript。 + - 資料庫:採用 `SQLite` 關聯式資料庫。 +- **效能與安全考量**: + - **安全性**:必須實作密碼雜湊加密機制(例如 bcrypt),避免明碼儲存;針對表單輸入須防範 SQL Injection 及 XSS 攻擊。 + - **效能**:「食材組合搜尋」可能涉及多對多或多條件複雜查詢,應優化資料庫關聯表與索引規劃(Schema Design),確保在一般資料量下,搜尋回應時間能維持在合理範圍以內。 + +## 4. MVP 範圍 (Minimum Viable Product) +- **Must Have (必須有)**: + - 用戶註冊、登入、登出系統。 + - 基礎食譜 CRUD(新增、檢視、編輯、刪除),包含食譜名稱、所需食材與步驟。 + - 基本關鍵字搜尋食譜功能。 +- **Should Have (應該有)**: + - 食材組合過濾搜尋功能(輸入多個食材即可找出包含這些食材的食譜)。 + - 對食譜做簡單的隱私分類(公開/私有)。 + - 各用戶身分區分(區別一般用戶與管理員),且管理員有權限刪除全站資料。 +- **Nice to Have (加分項)**: + - 支援食譜封面圖片上傳與預覽。 + - 將別人的公開食譜一鍵「收藏」到自己的清單。 + - 食譜評價或留言系統。 + +## 5. 專案成員與分工 + +| 角色 | 姓名 / 負責人 | 負責範圍 | 備註 | +| --- | --- | --- | --- | +| 專案經理 (PM) | | | | +| UI/UX 設計師 | | | | +| 前端工程師 | | | | +| 後端工程師 | | | | +| 測試工程師 (QA) | | | | diff --git a/docs/ROUTES.md b/docs/ROUTES.md new file mode 100644 index 00000000..edec8166 --- /dev/null +++ b/docs/ROUTES.md @@ -0,0 +1,19 @@ +# 祈福與算命占卜系統 - 路由設計 (Routing Design) + +## 1. 路由規劃表 (Route Table) + +| HTTP 方法 | URL 路徑 | Blueprint | 對應功能描述 | 對應模板視圖 (Template) | +|---|---|---|---|---| +| GET | `/` | `divination` | 系統首頁 (展示各類占卜功能卡片) | `divination/index.html` | +| GET / POST | `/auth/register` | `auth` | 會員註冊 | `auth/register.html` | +| GET / POST | `/auth/login` | `auth` | 會員登入 | `auth/login.html` | +| GET | `/auth/logout` | `auth` | 會員登出 | *重導向至首頁* | +| GET | `/divination/temple` | `divination` | 觀音靈籤/傳統廟宇抽籤頁面 | `divination/temple_draw.html` | +| POST | `/divination/temple` | `divination` | 處理抽籤與擲筊結果,儲存至 DB | *重新導向至結果頁* | +| GET | `/divination/tarot` | `divination` | 塔羅牌單張占卜頁面 | `divination/tarot_draw.html` | +| POST | `/divination/tarot` | `divination` | 處理塔羅牌抽牌結果,儲存至 DB | *重新導向至結果頁* | +| GET | `/divination/result/` | `divination` | 查看專屬占卜結果與解析 | `divination/result.html` | +| GET | `/divination/history` | `divination` | 會員個人專屬占卜歷史紀錄 | `divination/history.html` | +| GET | `/donation/` | `donation` | 線上隨喜捐獻/捐香油錢表單 | `donation/donate.html` | +| POST | `/donation/process` | `donation` | 處理金流邏輯 (MVP先寫入DB) | *重導向至捐獻成功感謝頁* | +| GET | `/donation/thanks` | `donation` | 捐獻成功感謝狀展示 | `donation/thanks.html` | diff --git a/docs/commit/SKILL.md b/docs/commit/SKILL.md new file mode 100644 index 00000000..e69de29b diff --git a/instance/.gitkeep b/instance/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/instance/database.db b/instance/database.db new file mode 100644 index 00000000..2e7fb43e Binary files /dev/null and b/instance/database.db differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..c1f961b1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask +python-dotenv +bcrypt diff --git a/run.py b/run.py new file mode 100644 index 00000000..b37535cb --- /dev/null +++ b/run.py @@ -0,0 +1,7 @@ +from app import create_app +import os + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True) 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..0e759744 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" @@ -169,7 +169,7 @@ commit 訊息:docs: add system architecture ``` 請幫我 commit 並推送目前的變更。 -commit 訊息:docs: add user flowchart +commit 訊息:docs: add user flowchar ``` --- diff --git "a/\347\267\232\344\270\212\347\245\210\347\246\217\350\210\207\347\256\227\345\221\275\345\215\240\345\215\234\347\263\273\347\265\261_PRD.md" "b/\347\267\232\344\270\212\347\245\210\347\246\217\350\210\207\347\256\227\345\221\275\345\215\240\345\215\234\347\263\273\347\265\261_PRD.md" new file mode 100644 index 00000000..34c1343e --- /dev/null +++ "b/\347\267\232\344\270\212\347\245\210\347\246\217\350\210\207\347\256\227\345\221\275\345\215\240\345\215\234\347\263\273\347\265\261_PRD.md" @@ -0,0 +1,62 @@ +# 產品需求文件 (PRD):線上祈福與算命占卜系統 + +## 1. 產品概述 +「線上祈福與算命占卜系統」是一款專為現代人設計的數位化心靈寄託平台。本系統將傳統的算命、占卜、抽籤等儀式與現代網頁技術結合,打破時間與空間的限制,讓使用者能隨時隨地尋求心靈指引,並提供儲存紀錄與線上隨喜(捐香油錢)的功能,打造完整的線上參拜與占卜體驗。 + +## 2. 目標用戶 (Target Audience) +* **尋求心靈慰藉與指引者:** 在生活、事業、感情上面臨迷惘,需要尋找解答或方向的人。 +* **對命理占卜感興趣的年輕族群:** 喜歡嘗試塔羅牌、星座塔羅、傳統廟宇抽籤等多元占卜方式的使用者。 +* **生活忙碌的現代人:** 沒有時間親自到訪實體廟宇或命理館,但仍希望進行祈福或算命的上班族或學生。 +* **海外或遠距離信仰者:** 居住在海外或遠方,無法輕易前往特定信仰中心,希望透過線上方式參與祈福與隨喜的民眾。 + +## 3. 核心功能需求 (Core Features) + +### 3.1 會員註冊及登入 (User Authentication) +* **功能描述:** 提供使用者建立個人帳號,以利系統記錄專屬的占卜結果與祈福歷程。 +* **細部需求:** + * 支援一般信箱 (Email) 註冊/登入。 + * (進階)支援第三方社群登入 (如 Google, Facebook, LINE)。 + * 使用者個人資料管理與密碼修改。 + +### 3.2 線上算命、占卜、抽籤 (Divination & Fortune-telling) +* **功能描述:** 提供多樣化的線上命理服務,模擬真實的抽籤與占卜流程。 +* **細部需求:** + * **傳統廟宇抽籤:** 模擬擲筊(允杯、笑杯、陰杯)決定是否賜籤,並抽出對應的籤詩(如六十甲子籤)。 + * **塔羅牌占卜:** 線上洗牌、抽牌,支援單張或多張牌陣(包含正逆位)。 + * 過程需具備互動性(如動畫特效、翻牌效果)以增強使用者的沉浸感。 + +### 3.3 占卜詳解 (Detailed Explanations) +* **功能描述:** 針對使用者抽出的籤詩或占卜結果,提供詳細、白話的解說。 +* **細部需求:** + * 依據不同的問題類別(如:感情、事業、健康、財運)給予針對性的解答。 + * 籤詩需包含:原意、白話翻譯、吉凶等級。 + * 塔羅牌需包含:牌面意義、針對問題的綜合解析。 + * (進階)可整合 AI (如 ChatGPT API) 提供更具彈性、個人化且有溫度的客製化分析。 + +### 3.4 儲存算命結果 (History & Results Saving) +* **功能描述:** 讓會員能夠回顧過去的占卜與抽籤紀錄,持續追蹤自身的運勢或問題發展。 +* **細部需求:** + * 個人專屬的「歷史紀錄」頁面。 + * 紀錄需包含:占卜日期、問事問題(例如:下半年的工作運)、使用的占卜方式、抽到的結果、以及詳解。 + * 允許使用者自行刪除或隱藏紀錄。 + +### 3.5 線上隨喜 / 捐香油錢 (Online Donation System) +* **功能描述:** 提供使用者在線上進行祈福還願或隨喜捐獻金錢的功能,支持系統營運或實體合作廟宇。 +* **細部需求:** + * 串接第三方支付金流(如綠界科技、Line Pay、信用卡等)。 + * 讓使用者自由輸入捐獻金額(香油錢)。 + * 捐獻完成後,提供線上專屬的「感謝狀」或「線上點燈/上香」動畫回饋。 + * 在會員專區保留歷史捐款/香油錢明細。 + +## 4. 使用者流程 (User Flow) +1. **訪客進入首頁:** 瀏覽平台提供的各項占卜與祈福服務簡介。 +2. **註冊/登入:** 系統引導訪客成為會員,提醒登入後方可永久保存占卜結果。 +3. **選擇服務:** 使用者選擇心儀的模式,如「傳統抽籤」或「塔羅占卜」。 +4. **進行儀式:** 輸入心中所想的問題,經過互動過程取得結果(點擊求籤、抽牌)。 +5. **查看詳解:** 系統展示結果與詳細解析,並自動將此紀錄儲存至「專屬紀錄區」。 +6. **祈福隨喜:** 若使用者對結果滿意或希望求心安,可點擊「捐香油錢」按鈕,進入金流支付流程,完成隨喜。 + +## 5. 後續擴充建議 (Future Roadmap) +* **線上點燈/安太歲:** 擴充年度祈福服務(如點光明燈、安太歲)。 +* **每日推播:** 透過 Email 或 LINE 官方帳號推廣每日運勢或一句吉祥話。 +* **專家一對一:** 媒合真實命理師傅進行線上排盤、視訊解盤服務。