From 9f7b4fc261f27e211469baafde5aa72410ba573f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LydiaWang=20=E7=8E=8B=E8=8E=89=E8=8C=B9?= Date: Sun, 14 Sep 2025 13:17:53 +0800 Subject: [PATCH] Initial commit --- .env | 5 + app.py | 397 ++++++++++++++++++++++++++++++++++++++++++++ setting/setting.txt | 5 + 3 files changed, 407 insertions(+) create mode 100644 .env create mode 100644 app.py create mode 100644 setting/setting.txt diff --git a/.env b/.env new file mode 100644 index 0000000..59c255e --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +DB_HOST=mysql.theaken.com +DB_PORT=33306 +DB_DATABASE=db_A024 +DB_USER=A024 +DB_PASSWORD=p1D37ddnIJCN \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..3cea038 --- /dev/null +++ b/app.py @@ -0,0 +1,397 @@ + +import os +import math +from flask import Flask, jsonify, request +from flask_cors import CORS +from dotenv import load_dotenv +import mysql.connector +import random +from mysql.connector import pooling, Error + +# 載入 .env 檔案中的環境變數 +load_dotenv(override=True) + +# --- 偵錯用:印出環境變數 --- +print("--- 偵錯環境變數 ---") +print(f"DB_HOST: {os.getenv('DB_HOST')}") +print(f"DB_PORT: {os.getenv('DB_PORT')}") +print(f"DB_USER: {os.getenv('DB_USER')}") +print("--------------------") +# ------------------------- + +app = Flask(__name__) +# 啟用 CORS,允許來自任何來源的請求,這在開發本機前端時很方便 +CORS(app) + +# --- 資料庫連線設定 --- +try: + # 建立資料庫連線池,這比每次請求都建立新連線更有效率 + connection_pool = pooling.MySQLConnectionPool( + pool_name="pizzapool", + pool_size=5, + pool_reset_session=True, + host=os.getenv('DB_HOST'), + user=os.getenv('DB_USER'), + password=os.getenv('DB_PASSWORD'), + database=os.getenv('DB_DATABASE'), + port=os.getenv('DB_PORT') + ) + print("資料庫連線池建立成功!") +except Error as e: + print(f"建立資料庫連線池時發生錯誤: {e}") + exit() # 如果連線池建立失敗,則直接退出程式 + +def get_db_connection(): + """從連線池取得一個資料庫連線""" + try: + return connection_pool.get_connection() + except Error as e: + print(f"從連線池取得連線時發生錯誤: {e}") + return None + +# --- 標準化回應格式 --- +def success_response(data, message="Success", code=200): + """產生成功的 JSON 回應""" + response = { + "status": "success", + "code": code, + "message": message + } + if data is not None: + response["data"] = data + return jsonify(response), code + +def error_response(message, code=400): + """產生錯誤的 JSON 回應""" + return jsonify({ + "status": "error", + "code": code, + "message": message + }), code + +# --- 資料庫初始化 --- +def init_db(): + """初始化資料庫,建立資料表並插入初始資料""" + print("正在初始化資料庫...") + db_conn = None + cursor = None + try: + db_conn = get_db_connection() + if db_conn is None: + print("無法取得資料庫連線,初始化失敗。") + return + + cursor = db_conn.cursor() + + # 建立 pizza 資料表,新增 id 作為主鍵 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS pizza ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + size VARCHAR(50) NOT NULL, + price DECIMAL(10, 2) NOT NULL + ) + """) + print("`pizza` 資料表已確認存在。") + + # 檢查是否已有範例資料 + cursor.execute("SELECT id FROM pizza WHERE name = %s", ("火山披薩",)) + if cursor.fetchone() is None: + # 插入一筆範例資料 + cursor.execute( + "INSERT INTO pizza (name, size, price) VALUES (%s, %s, %s)", + ("火山披薩", "M", 280.00) + ) + db_conn.commit() + print("已成功插入範例資料:火山披薩。") + else: + print("範例資料已存在。") + + # 檢查總筆數,如果不足11筆,則補齊 + cursor.execute("SELECT COUNT(*) FROM pizza") + total_pizzas = cursor.fetchone()[0] + + if total_pizzas < 11: + print(f"目前資料庫有 {total_pizzas} 筆資料,將產生 {11 - total_pizzas} 筆新的隨機資料。") + + pizza_names = ['夏威夷', '瑪格麗特', 'BBQ雞肉', '海鮮總匯', '超級豪華', '田園蔬菜', '四種起司'] + sizes = ['S', 'M', 'L', 'XL'] + + pizzas_to_add = [] + for _ in range(11 - total_pizzas): + name = random.choice(pizza_names) + "披薩" + size = random.choice(sizes) + price = round(random.uniform(250, 900), 0) # 價格取整數 + pizzas_to_add.append((name, size, price)) + + insert_query = "INSERT INTO pizza (name, size, price) VALUES (%s, %s, %s)" + cursor.executemany(insert_query, pizzas_to_add) + db_conn.commit() + print(f"已成功新增 {len(pizzas_to_add)} 筆隨機披薩資料。") + else: + print("資料庫中已有11筆或更多資料,無需產生新的隨機資料。") + + + except Error as e: + print(f"資料庫初始化時發生錯誤: {e}") + finally: + if cursor: + cursor.close() + if db_conn and db_conn.is_connected(): + db_conn.close() + print("資料庫初始化完成。") + + +# --- API 路由 --- + +@app.route('/v1/pizza', methods=['GET']) +def get_pizzas(): + """取得披薩清單,支援篩選和分頁""" + db_conn = None + cursor = None + try: + db_conn = get_db_connection() + if db_conn is None: + return error_response("無法連線到資料庫", 500) + + cursor = db_conn.cursor(dictionary=True) + + # 處理查詢參數 + min_price = request.args.get('min_price', type=float) + max_price = request.args.get('max_price', type=float) + page = request.args.get('page', 1, type=int) + limit = request.args.get('limit', 10, type=int) + offset = (page - 1) * limit + + # 動態建立查詢語句以避免 SQL 注入 + query_conditions = [] + query_params = [] + + if min_price is not None: + query_conditions.append("price >= %s") + query_params.append(min_price) + if max_price is not None: + query_conditions.append("price <= %s") + query_params.append(max_price) + + where_clause = " AND ".join(query_conditions) + + # 取得總筆數 + count_query = "SELECT COUNT(id) as total FROM pizza" + if where_clause: + count_query += " WHERE " + where_clause + cursor.execute(count_query, tuple(query_params)) + total_records = cursor.fetchone()['total'] + total_pages = math.ceil(total_records / limit) + + # 取得分頁資料 + data_query = "SELECT * FROM pizza" + if where_clause: + data_query += " WHERE " + where_clause + data_query += " ORDER BY id LIMIT %s OFFSET %s" + + cursor.execute(data_query, tuple(query_params + [limit, offset])) + pizzas = cursor.fetchall() + + meta = { + "current_page": page, + "last_page": total_pages, + "per_page": limit, + "total": total_records + } + + return success_response({"pizzas": pizzas, "meta": meta}) + + except Error as e: + return error_response(f"伺服器錯誤: {e}", 500) + finally: + if cursor: + cursor.close() + if db_conn and db_conn.is_connected(): + db_conn.close() + + +@app.route('/v1/pizza/', methods=['GET']) +def get_pizza_by_name(name): + """依據名稱取得單一披薩""" + db_conn = None + cursor = None + try: + db_conn = get_db_connection() + if db_conn is None: + return error_response("無法連線到資料庫", 500) + + cursor = db_conn.cursor(dictionary=True) + cursor.execute("SELECT * FROM pizza WHERE name = %s", (name,)) + pizza = cursor.fetchone() + + if pizza: + return success_response(pizza) + else: + return error_response("找不到指定的披薩", 404) + + except Error as e: + return error_response(f"伺服器錯誤: {e}", 500) + finally: + if cursor: + cursor.close() + if db_conn and db_conn.is_connected(): + db_conn.close() + + +@app.route('/v1/pizza', methods=['POST']) +def create_pizza(): + """建立新的披薩""" + db_conn = None + cursor = None + try: + data = request.get_json() + if not data or not all(k in data for k in ['name', 'size', 'price']): + return error_response("缺少必要欄位:name, size, price") + + name = data['name'] + size = data['size'] + price = data['price'] + + if not isinstance(name, str) or not isinstance(size, str) or not isinstance(price, (int, float)): + return error_response("欄位型別錯誤") + + db_conn = get_db_connection() + if db_conn is None: + return error_response("無法連線到資料庫", 500) + + cursor = db_conn.cursor(dictionary=True) + cursor.execute( + "INSERT INTO pizza (name, size, price) VALUES (%s, %s, %s)", + (name, size, price) + ) + new_id = cursor.lastrowid + db_conn.commit() + + # 查詢並回傳新建立的資料 + cursor.execute("SELECT * FROM pizza WHERE id = %s", (new_id,)) + new_pizza = cursor.fetchone() + + return success_response(new_pizza, "披薩建立成功", 201) + + except Error as e: + return error_response(f"資料庫錯誤: {e}", 500) + except Exception as e: + return error_response(f"伺服器錯誤: {e}", 500) + finally: + if cursor: + cursor.close() + if db_conn and db_conn.is_connected(): + db_conn.close() + + +@app.route('/v1/pizza/', methods=['PATCH']) +def update_pizza(pizza_id): + """更新現有披薩的資訊""" + db_conn = None + cursor = None + try: + data = request.get_json() + if not data: + return error_response("沒有提供要更新的資料") + + db_conn = get_db_connection() + if db_conn is None: + return error_response("無法連線到資料庫", 500) + + cursor = db_conn.cursor(dictionary=True) + + # 檢查披薩是否存在 + cursor.execute("SELECT id FROM pizza WHERE id = %s", (pizza_id,)) + if not cursor.fetchone(): + return error_response("找不到要更新的披薩", 404) + + # 動態建立更新語句 + update_fields = [] + update_values = [] + if 'name' in data: + update_fields.append("name = %s") + update_values.append(data['name']) + if 'size' in data: + update_fields.append("size = %s") + update_values.append(data['size']) + if 'price' in data: + update_fields.append("price = %s") + update_values.append(data['price']) + + if not update_fields: + return error_response("沒有提供可更新的欄位 (name, size, price)") + + update_values.append(pizza_id) + + query = f"UPDATE pizza SET {', '.join(update_fields)} WHERE id = %s" + cursor.execute(query, tuple(update_values)) + db_conn.commit() + + # 查詢並回傳更新後的資料 + cursor.execute("SELECT * FROM pizza WHERE id = %s", (pizza_id,)) + updated_pizza = cursor.fetchone() + + return success_response(updated_pizza, "披薩資訊已更新") + + except Error as e: + return error_response(f"資料庫錯誤: {e}", 500) + except Exception as e: + return error_response(f"伺服器錯誤: {e}", 500) + finally: + if cursor: + cursor.close() + if db_conn and db_conn.is_connected(): + db_conn.close() + + +@app.route('/v1/pizza/', methods=['DELETE']) +def delete_pizza(pizza_id): + """刪除一個披薩""" + db_conn = None + cursor = None + try: + db_conn = get_db_connection() + if db_conn is None: + return error_response("無法連線到資料庫", 500) + + cursor = db_conn.cursor() + + # 檢查披薩是否存在 + cursor.execute("SELECT id FROM pizza WHERE id = %s", (pizza_id,)) + if not cursor.fetchone(): + return error_response("找不到要刪除的披薩", 404) + + cursor.execute("DELETE FROM pizza WHERE id = %s", (pizza_id,)) + db_conn.commit() + + # cursor.rowcount 會回傳影響的行數 + if cursor.rowcount > 0: + return success_response(None, "披薩已成功刪除", 204) + else: + # 雖然前面檢查過,但這是一個保險措施 + return error_response("找不到要刪除的披薩", 404) + + except Error as e: + return error_response(f"資料庫錯誤: {e}", 500) + finally: + if cursor: + cursor.close() + if db_conn and db_conn.is_connected(): + db_conn.close() + +# --- 全域錯誤處理 --- +@app.errorhandler(Exception) +def handle_global_error(e): + """捕捉所有未處理的例外""" + return error_response(f"發生未預期的伺服器錯誤: {e}", 500) + + +if __name__ == '__main__': + # 在啟動伺服器前,先執行資料庫初始化 + init_db() + # 啟動 Flask 伺服器 + # host='0.0.0.0' 讓伺服器可以從外部網路存取 + # debug=True 讓伺服器在程式碼變更後自動重啟 + print("正在啟動 Flask 伺服器...") + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/setting/setting.txt b/setting/setting.txt new file mode 100644 index 0000000..973be4c --- /dev/null +++ b/setting/setting.txt @@ -0,0 +1,5 @@ +Host:mysql.theaken.com +Port:33306 +Database:db_A024 +Username:A024 +Password:p1D37ddnIJCN \ No newline at end of file