Initial commit

This commit is contained in:
2025-09-08 16:23:54 +08:00
commit 0e1ee0820e
6 changed files with 1304 additions and 0 deletions

7
DB_connection.txt Normal file
View File

@@ -0,0 +1,7 @@
資料庫資訊:
DB_HOST = mysql.theaken.com
DB_PORT = 33306
DB_NAME = db_A019
DB_USER = A019
DB_PASSWORD = 9wvKEkxBzVca
pip install -r requirements.txt

85
README.md Normal file
View File

@@ -0,0 +1,85 @@
# Flask API 伺服器
這是一個使用 Flask 和 MySQL 建立的 RESTful API 伺服器,提供使用者資料的 CRUD 操作。
## 功能
- 獲取使用者列表 (支援過濾和分頁)
- 獲取單一使用者資料
- 新增使用者
- 更新使用者資料
- 刪除使用者
## API 端點
### GET /v1/users
獲取使用者列表,支援以下查詢參數:
- `min_age`: 最小年齡過濾
- `max_age`: 最大年齡過濾
- `page`: 頁碼 (預設: 1)
- `limit`: 每頁筆數 (預設: 10)
### GET /v1/users/<id>
獲取指定 ID 的使用者資料
### POST /v1/users
新增使用者,需提供以下 JSON 資料:
```json
{
"name": "使用者名稱",
"email": "使用者信箱",
"age": 使用者年齡
}
```
### PATCH /v1/users/<id>
更新指定 ID 的使用者資料,可提供以下任一欄位:
```json
{
"name": "新名稱",
"email": "新信箱",
"age": 新年齡
}
```
### DELETE /v1/users/<id>
刪除指定 ID 的使用者
## 安裝與執行
1. 安裝相依套件:
```
pip install -r requirements.txt
```
2. 執行伺服器:
```
python app.py
```
伺服器將在 http://127.0.0.1:5000 啟動。
## 回應格式
所有 API 回應皆使用統一格式:
```json
{
"status": "success" 或 "error",
"code": HTTP 狀態碼,
"message": "回應訊息",
"data": 回應資料 (僅在成功時提供)
}
```
## 錯誤處理
錯誤回應格式:
```json
{
"status": "error",
"code": HTTP 錯誤碼,
"message": "錯誤訊息"
}
```

701
app.py Normal file
View File

@@ -0,0 +1,701 @@
from flask import Flask, request, jsonify
from flask_cors import CORS
import mysql.connector
import os
from datetime import datetime
app = Flask(__name__)
CORS(app) # 啟用 CORS允許本機前端存取
# 資料庫連線資訊
DB_CONFIG = {
'host': 'mysql.theaken.com',
'port': 33306,
'user': 'A019',
'password': '9wvKEkxBzVca',
'database': 'db_A019'
}
# 建立資料庫連線
def get_db_connection():
try:
conn = mysql.connector.connect(**DB_CONFIG)
return conn
except mysql.connector.Error as err:
print(f"資料庫連線錯誤: {err}")
return None
# 統一回應格式
def create_response(status, code, message, data=None):
response = {
"status": status,
"code": code,
"message": message
}
if data is not None:
response["data"] = data
return response
# 錯誤處理
def create_error_response(code, message):
return create_response("error", code, message)
# 獲取所有使用者,支援過濾和分頁
@app.route('/v1/users', methods=['GET'])
def get_users():
try:
# 取得查詢參數
min_age = request.args.get('min_age', type=int)
max_age = request.args.get('max_age', type=int)
page = request.args.get('page', 1, type=int)
limit = request.args.get('limit', 10, type=int)
# 計算偏移量
offset = (page - 1) * limit
conn = get_db_connection()
if not conn:
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
cursor = conn.cursor(dictionary=True)
# 建立基本查詢
query = "SELECT * FROM users"
count_query = "SELECT COUNT(*) as total FROM users"
params = []
where_clauses = []
# 加入過濾條件
if min_age is not None:
where_clauses.append("age >= %s")
params.append(min_age)
if max_age is not None:
where_clauses.append("age <= %s")
params.append(max_age)
# 組合 WHERE 子句
if where_clauses:
query += " WHERE " + " AND ".join(where_clauses)
count_query += " WHERE " + " AND ".join(where_clauses)
# 加入分頁
query += " LIMIT %s OFFSET %s"
params.extend([limit, offset])
# 執行查詢
cursor.execute(query, params)
users = cursor.fetchall()
# 獲取總記錄數
cursor.execute(count_query, params[:-2] if params else [])
total = cursor.fetchone()['total']
# 計算總頁數
total_pages = (total + limit - 1) // limit
# 建立 meta 資訊
meta = {
"total": total,
"page": page,
"limit": limit,
"total_pages": total_pages
}
cursor.close()
conn.close()
return jsonify(create_response("success", 200, "成功獲取使用者列表", {"users": users, "meta": meta})), 200
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
# 獲取所有披薩,支援過濾和分頁
@app.route('/v1/pizzas', methods=['GET'])
def get_pizzas():
try:
# 取得查詢參數
min_price = request.args.get('min_price', type=float)
max_price = request.args.get('max_price', type=float)
size = request.args.get('size')
page = request.args.get('page', 1, type=int)
limit = request.args.get('limit', 10, type=int)
# 計算偏移量
offset = (page - 1) * limit
conn = get_db_connection()
if not conn:
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
cursor = conn.cursor(dictionary=True)
# 建立基本查詢
query = "SELECT * FROM pizzas"
count_query = "SELECT COUNT(*) as total FROM pizzas"
params = []
where_clauses = []
# 加入過濾條件
if min_price is not None:
where_clauses.append("price >= %s")
params.append(min_price)
if max_price is not None:
where_clauses.append("price <= %s")
params.append(max_price)
if size is not None:
where_clauses.append("size = %s")
params.append(size)
# 組合 WHERE 子句
if where_clauses:
query += " WHERE " + " AND ".join(where_clauses)
count_query += " WHERE " + " AND ".join(where_clauses)
# 加入分頁
query += " LIMIT %s OFFSET %s"
params.extend([limit, offset])
# 執行查詢
cursor.execute(query, params)
pizzas = cursor.fetchall()
# 獲取總記錄數
cursor.execute(count_query, params[:-2] if params else [])
total = cursor.fetchone()['total']
# 計算總頁數
total_pages = (total + limit - 1) // limit
# 建立 meta 資訊
meta = {
"total": total,
"page": page,
"limit": limit,
"total_pages": total_pages
}
cursor.close()
conn.close()
return jsonify(create_response("success", 200, "成功獲取披薩列表", {"pizzas": pizzas, "meta": meta})), 200
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
# 獲取單一使用者
@app.route('/v1/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
try:
conn = get_db_connection()
if not conn:
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
user = cursor.fetchone()
cursor.close()
conn.close()
if not user:
return jsonify(create_error_response(404, f"找不到 ID 為 {user_id} 的使用者")), 404
return jsonify(create_response("success", 200, "成功獲取使用者資料", {"user": user})), 200
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
# 獲取單一披薩
@app.route('/v1/pizzas/<int:pizza_id>', methods=['GET'])
def get_pizza_by_id(pizza_id):
try:
conn = get_db_connection()
if not conn:
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT * FROM pizzas WHERE id = %s", (pizza_id,))
pizza = cursor.fetchone()
cursor.close()
conn.close()
if not pizza:
return jsonify(create_error_response(404, f"找不到 ID 為 {pizza_id} 的披薩")), 404
return jsonify(create_response("success", 200, "成功獲取披薩資料", {"pizza": pizza})), 200
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
user = cursor.fetchone()
cursor.close()
conn.close()
if not user:
return jsonify(create_error_response(404, f"找不到 ID 為 {user_id} 的使用者")), 404
return jsonify(create_response("success", 200, "成功獲取使用者資料", {"user": user})), 200
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
# 獲取單一披薩
# 已在上方定義 GET /v1/pizzas/<int:pizza_id> 路由
# 新增披薩
@app.route('/v1/pizzas', methods=['POST'])
def create_pizza():
try:
data = request.get_json()
# 驗證必要欄位
required_fields = ['name', 'price', 'size']
for field in required_fields:
if field not in data:
return jsonify(create_error_response(400, f"缺少必要欄位: {field}")), 400
# 驗證資料類型
if not isinstance(data['name'], str):
return jsonify(create_error_response(400, "name 必須是字串")), 400
try:
price = float(data['price'])
if price <= 0:
return jsonify(create_error_response(400, "price 必須大於 0")), 400
except (ValueError, TypeError):
return jsonify(create_error_response(400, "price 必須是有效的數字")), 400
if not isinstance(data['size'], str) or data['size'] not in ['S', 'M', 'L']:
return jsonify(create_error_response(400, "size 必須是 S、M 或 L")), 400
conn = get_db_connection()
if not conn:
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
cursor = conn.cursor(dictionary=True)
# 設定時間
now = datetime.now()
# 插入資料
insert_query = "INSERT INTO pizzas (name, price, size, created_at, updated_at) VALUES (%s, %s, %s, %s, %s)"
cursor.execute(insert_query, (data['name'], price, data['size'], now, now))
pizza_id = cursor.lastrowid
conn.commit()
# 獲取新增的披薩資料
cursor.execute("SELECT * FROM pizzas WHERE id = %s", (pizza_id,))
new_pizza = cursor.fetchone()
cursor.close()
conn.close()
return jsonify(create_response("success", 201, "成功新增披薩", {"pizza": new_pizza})), 201
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
# 更新披薩
@app.route('/v1/pizzas/<int:pizza_id>', methods=['PATCH'])
def update_pizza(pizza_id):
try:
data = request.get_json()
# 至少需要一個欄位
if not data or not any(field in data for field in ['name', 'price', 'size']):
return jsonify(create_error_response(400, "至少需要提供一個欄位: name, price, size")), 400
# 驗證資料類型
if 'name' in data and not isinstance(data['name'], str):
return jsonify(create_error_response(400, "name 必須是字串")), 400
if 'price' in data:
try:
price = float(data['price'])
if price <= 0:
return jsonify(create_error_response(400, "price 必須大於 0")), 400
data['price'] = price
except (ValueError, TypeError):
return jsonify(create_error_response(400, "price 必須是有效的數字")), 400
if 'size' in data and (not isinstance(data['size'], str) or data['size'] not in ['S', 'M', 'L']):
return jsonify(create_error_response(400, "size 必須是 S、M 或 L")), 400
conn = get_db_connection()
if not conn:
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
cursor = conn.cursor(dictionary=True)
# 檢查披薩是否存在
cursor.execute("SELECT * FROM pizzas WHERE id = %s", (pizza_id,))
pizza = cursor.fetchone()
if not pizza:
cursor.close()
conn.close()
return jsonify(create_error_response(404, f"找不到 ID 為 {pizza_id} 的披薩")), 404
# 設定更新時間
now = datetime.now()
data['updated_at'] = now
# 建立更新查詢
update_fields = []
update_values = []
for field in ['name', 'price', 'size', 'updated_at']:
if field in data:
update_fields.append(f"{field} = %s")
update_values.append(data[field])
update_values.append(pizza_id) # WHERE id = %s 的參數
update_query = f"UPDATE pizzas SET {', '.join(update_fields)} WHERE id = %s"
cursor.execute(update_query, update_values)
conn.commit()
# 獲取更新後的披薩資料
cursor.execute("SELECT * FROM pizzas WHERE id = %s", (pizza_id,))
updated_pizza = cursor.fetchone()
cursor.close()
conn.close()
return jsonify(create_response("success", 200, "成功更新披薩", {"pizza": updated_pizza})), 200
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
# 刪除披薩
@app.route('/v1/pizzas/<int:pizza_id>', methods=['DELETE'])
def delete_pizza(pizza_id):
try:
conn = get_db_connection()
if not conn:
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
cursor = conn.cursor()
# 檢查披薩是否存在
cursor.execute("SELECT id FROM pizzas WHERE id = %s", (pizza_id,))
pizza = cursor.fetchone()
if not pizza:
cursor.close()
conn.close()
return jsonify(create_error_response(404, f"找不到 ID 為 {pizza_id} 的披薩")), 404
# 刪除披薩
cursor.execute("DELETE FROM pizzas WHERE id = %s", (pizza_id,))
conn.commit()
cursor.close()
conn.close()
return jsonify(create_response("success", 204, "成功刪除披薩")), 204
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
# 新增使用者
@app.route('/v1/users', methods=['POST'])
def create_user():
try:
data = request.get_json()
# 驗證必要欄位
required_fields = ['name', 'email', 'age']
for field in required_fields:
if field not in data:
return jsonify(create_error_response(400, f"缺少必要欄位: {field}")), 400
# 驗證資料類型
if not isinstance(data['name'], str):
return jsonify(create_error_response(400, "name 必須是字串")), 400
# 驗證資料類型
if not isinstance(data['name'], str):
return jsonify(create_error_response(400, "name 必須是字串")), 400
try:
price = float(data['price'])
if price <= 0:
return jsonify(create_error_response(400, "price 必須大於 0")), 400
except (ValueError, TypeError):
return jsonify(create_error_response(400, "price 必須是有效的數字")), 400
if not isinstance(data['size'], str) or data['size'] not in ['S', 'M', 'L']:
return jsonify(create_error_response(400, "size 必須是 S、M 或 L")), 400
conn = get_db_connection()
if not conn:
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
cursor = conn.cursor()
# 設定時間
now = datetime.now()
# 插入資料
insert_query = "INSERT INTO pizzas (name, price, size, created_at, updated_at) VALUES (%s, %s, %s, %s, %s)"
cursor.execute(insert_query, (data['name'], price, data['size'], now, now))
pizza_id = cursor.lastrowid
conn.commit()
# 獲取新增的披薩資料
cursor.execute("SELECT * FROM pizzas WHERE id = %s", (pizza_id,))
new_pizza = cursor.fetchone()
cursor.close()
conn.close()
return jsonify(create_response("success", 201, "成功新增披薩", {"pizza": new_pizza})), 201
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
if not isinstance(data['email'], str):
return jsonify(create_error_response(400, "email 必須是字串")), 400
if not isinstance(data['age'], int):
return jsonify(create_error_response(400, "age 必須是整數")), 400
# 驗證 email 格式 (簡單驗證)
if '@' not in data['email']:
return jsonify(create_error_response(400, "email 格式不正確")), 400
conn = get_db_connection()
if not conn:
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
cursor = conn.cursor(dictionary=True)
# 使用參數化查詢避免 SQL 注入
query = "INSERT INTO users (name, email, age) VALUES (%s, %s, %s)"
cursor.execute(query, (data['name'], data['email'], data['age']))
# 獲取新增的使用者 ID
user_id = cursor.lastrowid
conn.commit()
# 獲取新增的使用者資料
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
new_user = cursor.fetchone()
cursor.close()
conn.close()
return jsonify(create_response("success", 201, "成功新增使用者", {"user": new_user})), 201
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
# 更新使用者
@app.route('/v1/users/<int:user_id>', methods=['PATCH'])
def update_user(user_id):
try:
data = request.get_json()
# 檢查是否有要更新的欄位
if not data:
return jsonify(create_error_response(400, "沒有提供要更新的資料")), 400
# 驗證資料類型
if 'name' in data and not isinstance(data['name'], str):
return jsonify(create_error_response(400, "name 必須是字串")), 400
if 'email' in data and not isinstance(data['email'], str):
return jsonify(create_error_response(400, "email 必須是字串")), 400
if 'age' in data and not isinstance(data['age'], int):
return jsonify(create_error_response(400, "age 必須是整數")), 400
# 驗證 email 格式 (簡單驗證)
if 'email' in data and '@' not in data['email']:
return jsonify(create_error_response(400, "email 格式不正確")), 400
conn = get_db_connection()
if not conn:
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
cursor = conn.cursor(dictionary=True)
# 檢查使用者是否存在
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
user = cursor.fetchone()
if not user:
cursor.close()
conn.close()
return jsonify(create_error_response(404, f"找不到 ID 為 {user_id} 的使用者")), 404
# 建立更新查詢
update_fields = []
params = []
if 'name' in data:
update_fields.append("name = %s")
params.append(data['name'])
if 'email' in data:
update_fields.append("email = %s")
params.append(data['email'])
if 'age' in data:
update_fields.append("age = %s")
params.append(data['age'])
# 如果沒有要更新的欄位
if not update_fields:
cursor.close()
conn.close()
return jsonify(create_error_response(400, "沒有提供有效的更新欄位")), 400
# 建立更新查詢
query = "UPDATE users SET " + ", ".join(update_fields) + " WHERE id = %s"
params.append(user_id)
# 執行更新
cursor.execute(query, params)
conn.commit()
# 獲取更新後的使用者資料
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
updated_user = cursor.fetchone()
cursor.close()
conn.close()
return jsonify(create_response("success", 200, "成功更新使用者資料", {"user": updated_user})), 200
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
# 刪除使用者
@app.route('/v1/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
try:
conn = get_db_connection()
if not conn:
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
cursor = conn.cursor()
# 檢查使用者是否存在
cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
user = cursor.fetchone()
if not user:
cursor.close()
conn.close()
return jsonify(create_error_response(404, f"找不到 ID 為 {user_id} 的使用者")), 404
# 刪除使用者
cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
conn.commit()
cursor.close()
conn.close()
return jsonify(create_response("success", 204, "成功刪除使用者")), 204
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
# 獲取所有 pizzas支援分頁
# 已在上方定義 GET /v1/pizzas 路由
# 獲取總記錄數
cursor.execute(count_query, params[:-2] if params else [])
total = cursor.fetchone()['total']
# 計算總頁數
total_pages = (total + limit - 1) // limit
# 建立 meta 資訊
meta = {
"total": total,
"page": page,
"limit": limit,
"total_pages": total_pages
}
cursor.close()
conn.close()
return jsonify(create_response("success", 200, "成功獲取披薩列表", {"pizzas": pizzas, "meta": meta})), 200
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
# 獲取單一 pizza
# 注意:此路由已在其他地方定義
def get_pizza(pizza_id):
try:
conn = get_db_connection()
if not conn:
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT * FROM pizzas WHERE id = %s", (pizza_id,))
pizza = cursor.fetchone()
cursor.close()
conn.close()
if not pizza:
return jsonify(create_error_response(404, f"找不到 ID 為 {pizza_id} 的披薩")), 404
return jsonify(create_response("success", 200, "成功獲取披薩資料", {"pizza": pizza})), 200
except Exception as e:
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
# 新增 pizza
# 已在上方定義 POST /v1/pizzas 路由
# 更新 pizza
# 已在上方定義 PATCH /v1/pizzas/<int:pizza_id> 路由
# 刪除 pizza
# 已在上方定義 DELETE /v1/pizzas/<int:pizza_id> 路由
# 主程式入口
if __name__ == '__main__':
# 確保資料表存在
conn = get_db_connection()
if conn:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL,
age INT NOT NULL
)
""")
# 確保 pizzas 資料表存在
cursor.execute("""
CREATE TABLE IF NOT EXISTS pizzas (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
size VARCHAR(2) NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
)
""")
conn.commit()
cursor.close()
conn.close()
app.run(debug=True)

191
create_pizzas_table.py Normal file
View File

@@ -0,0 +1,191 @@
import mysql.connector
import random
from datetime import datetime
# 資料庫連線資訊
DB_CONFIG = {
'host': 'mysql.theaken.com',
'port': 33306,
'user': 'A019',
'password': '9wvKEkxBzVca',
'database': 'db_A019'
}
# 建立資料庫連線
def get_db_connection():
try:
conn = mysql.connector.connect(**DB_CONFIG)
return conn
except mysql.connector.Error as err:
print(f"資料庫連線錯誤: {err}")
return None
# 建立 pizzas 資料表
def create_pizzas_table():
conn = get_db_connection()
if not conn:
print("無法連接到資料庫")
return False
cursor = conn.cursor()
# 建立 pizzas 資料表
create_table_query = """
CREATE TABLE IF NOT EXISTS pizzas (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
size VARCHAR(2) NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
)
"""
try:
cursor.execute(create_table_query)
conn.commit()
print("成功建立 pizzas 資料表")
return True
except mysql.connector.Error as err:
print(f"建立資料表錯誤: {err}")
return False
finally:
cursor.close()
conn.close()
# 產生隨機 pizza 資料
def generate_random_pizzas(count=6):
# Pizza 名稱列表
pizza_names = [
"夏威夷披薩", "瑪格麗特披薩", "蘑菇披薩", "臘腸披薩",
"海鮮披薩", "素食披薩", "四季披薩", "墨西哥辣味披薩",
"起司披薩", "燒烤雞肉披薩", "牛肉披薩", "蔬菜披薩"
]
# 確保名稱不重複,如果 count 大於名稱列表長度,則使用所有可用名稱
if count > len(pizza_names):
count = len(pizza_names)
# 隨機選擇不重複的名稱
selected_names = random.sample(pizza_names, count)
# Pizza 尺寸
pizza_sizes = ["S", "M", "L"]
# 隨機生成 pizza 資料
pizzas = []
for name in selected_names:
size = random.choice(pizza_sizes)
# 根據尺寸設定價格範圍
if size == "S":
price = round(random.uniform(200, 300), 2)
elif size == "M":
price = round(random.uniform(300, 400), 2)
else: # L
price = round(random.uniform(400, 500), 2)
# 設定時間
now = datetime.now()
created_at = now
updated_at = now
pizzas.append((name, price, size, created_at, updated_at))
return pizzas
# 插入隨機 pizza 資料
def insert_random_pizzas(count=6):
conn = get_db_connection()
if not conn:
print("無法連接到資料庫")
return False
cursor = conn.cursor()
# 生成隨機 pizza 資料
pizzas = generate_random_pizzas(count)
# 插入資料
insert_query = """
INSERT INTO pizzas (name, price, size, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s)
"""
try:
cursor.executemany(insert_query, pizzas)
conn.commit()
print(f"成功插入 {cursor.rowcount} 筆 pizza 資料")
return True
except mysql.connector.Error as err:
print(f"插入資料錯誤: {err}")
return False
finally:
cursor.close()
conn.close()
# 顯示所有 pizza 資料
def show_all_pizzas():
conn = get_db_connection()
if not conn:
print("無法連接到資料庫")
return
cursor = conn.cursor(dictionary=True)
try:
cursor.execute("SELECT * FROM pizzas")
pizzas = cursor.fetchall()
if not pizzas:
print("沒有找到任何 pizza 資料")
return
print("\n所有 Pizza 資料:")
print("-" * 80)
print(f"{'ID':<5} {'名稱':<20} {'價格':<10} {'尺寸':<5} {'建立時間':<20} {'更新時間':<20}")
print("-" * 80)
for pizza in pizzas:
print(f"{pizza['id']:<5} {pizza['name']:<20} {pizza['price']:<10} {pizza['size']:<5} {pizza['created_at']} {pizza['updated_at']}")
except mysql.connector.Error as err:
print(f"查詢資料錯誤: {err}")
finally:
cursor.close()
conn.close()
# 清空 pizzas 資料表
def truncate_pizzas_table():
conn = get_db_connection()
if not conn:
print("無法連接到資料庫")
return False
cursor = conn.cursor()
try:
cursor.execute("TRUNCATE TABLE pizzas")
conn.commit()
print("成功清空 pizzas 資料表")
return True
except mysql.connector.Error as err:
print(f"清空資料表錯誤: {err}")
return False
finally:
cursor.close()
conn.close()
# 主程式
def main():
# 建立資料表
if create_pizzas_table():
# 清空資料表
if truncate_pizzas_table():
# 插入隨機資料
if insert_random_pizzas(6):
# 顯示所有資料
show_all_pizzas()
if __name__ == "__main__":
main()

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
flask==2.3.3
flask-cors==4.0.0
mysql-connector-python==8.1.0

317
sales_dashboard.html Normal file
View File

@@ -0,0 +1,317 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sales Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { max-width: 900px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
h1, h2 { color: #0056b3; border-bottom: 2px solid #eee; padding-bottom: 10px; margin-bottom: 20px; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; }
.stat-box { background: #e9f7ff; padding: 15px; border-radius: 5px; text-align: center; }
.stat-box h3 { margin: 0 0 10px 0; color: #007bff; }
.stat-box p { font-size: 1.8em; font-weight: bold; color: #333; }
.chart-container { margin-bottom: 30px; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.chart-container canvas { max-width: 100%; height: auto; }
.navbar {
background-color: #333;
padding: 10px 0;
text-align: center;
margin-bottom: 20px;
border-radius: 8px;
}
.nav-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
justify-content: center;
}
.nav-item {
margin: 0 15px;
}
.nav-link {
color: white;
text-decoration: none;
font-weight: bold;
padding: 8px 15px;
border-radius: 5px;
transition: background-color 0.3s ease;
}
.nav-link:hover {
background-color: #575757;
}
/* New styles for summary boxes */
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.summary-box {
background: #e0f7fa; /* Light blue */
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.summary-box h3 {
margin-top: 0;
color: #00796b; /* Darker blue-green */
}
.summary-box p {
font-size: 2em;
font-weight: bold;
color: #004d40; /* Even darker blue-green */
}
/* Styles for the data table */
.data-table-container {
margin-top: 30px;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.data-table th, .data-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.data-table th {
background-color: #f2f2f2;
font-weight: bold;
}
.data-table tbody tr:nth-child(even) {
background-color: #f9f9f9;
}
.data-table tbody tr:hover {
background-color: #f1f1f1;
}
</style>
</head>
<body>
<nav class="navbar">
<ul class="nav-list">
<li class="nav-item"><a href="#" class="nav-link">Overview</a></li>
<li class="nav-item"><a href="#" class="nav-link">Product Sales</a></li>
<li class="nav-item"><a href="#" class="nav-link">Regional Sales</a></li>
<li class="nav-item"><a href="sales_records.html" class="nav-link">Sales Records</a></li>
</ul>
</nav>
<div class="dashboard-container">
<h1>Sales Dashboard</h1>
<div class="summary-grid">
<div class="summary-box">
<h3>Total Sales</h3>
<p id="totalSales">Loading...</p>
</div>
<div class="summary-box">
<h3>Total Orders</h3>
<p id="totalOrders">Loading...</p>
</div>
<div class="summary-box">
<h3>Product Categories</h3>
<p id="productCategories">Loading...</p>
</div>
<div class="summary-box">
<h3>Sales Regions</h3>
<p id="salesRegions">Loading...</p>
</div>
</div>
<h2>Sales by Product Category</h2>
<div class="chart-container">
<canvas id="categoryBarChart"></canvas>
</div>
<h2>Sales Over Time</h2>
<div class="chart-container">
<canvas id="timeLineChart"></canvas>
</div>
<h2>All Sales Data</h2>
<div class="data-table-container">
<table class="data-table" id="allSalesDataTable">
<thead>
<tr>
<!-- Headers will be dynamically generated -->
</tr>
</thead>
<tbody>
<!-- Data will be dynamically loaded -->
</tbody>
</table>
</div>
</div>
<script>
// Function to fetch and display summary data
async function fetchSummaryData() {
try {
const response = await fetch('http://127.0.0.1:5000/dashboard_summary');
const data = await response.json();
document.getElementById('totalSales').innerText = `${data.total_sales.toFixed(2)}`;
document.getElementById('totalOrders').innerText = data.total_orders;
document.getElementById('productCategories').innerText = data.product_categories;
document.getElementById('salesRegions').innerText = data.sales_regions;
} catch (error) {
console.error('Error fetching summary data:', error);
document.getElementById('totalSales').innerText = 'Error';
document.getElementById('totalOrders').innerText = 'Error';
document.getElementById('productCategories').innerText = 'Error';
document.getElementById('salesRegions').innerText = 'Error';
}
}
// Function to fetch and render charts
async function fetchChartData() {
try {
const response = await fetch('http://127.0.0.1:5000/dashboard_charts');
const data = await response.json();
// Bar Chart: Sales by Product Category
const categoryBarCtx = document.getElementById('categoryBarChart').getContext('2d');
new Chart(categoryBarCtx, {
type: 'bar',
data: {
labels: data.category_sales.labels,
datasets: [{
label: 'Sales Amount',
data: data.category_sales.data,
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Sales Amount'
}
}
},
plugins: {
title: {
display: true,
text: 'Sales by Product Category'
}
}
}
});
// Line Chart: Sales Over Time
const timeLineCtx = document.getElementById('timeLineChart').getContext('2d');
new Chart(timeLineCtx, {
type: 'line',
data: {
labels: data.time_sales.labels,
datasets: [{
label: 'Sales Amount',
data: data.time_sales.data,
backgroundColor: 'rgba(153, 102, 255, 0.6)',
borderColor: 'rgba(153, 102, 255, 1)',
borderWidth: 1,
fill: false
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Sales Amount'
}
}
},
plugins: {
title: {
display: true,
text: 'Sales Over Time'
}
}
}
});
} catch (error) {
console.error('Error fetching chart data:', error);
}
}
// Function to fetch and display all sales data in a table
async function fetchAllSalesData() {
try {
const response = await fetch('http://127.0.0.1:5000/sales_data');
const data = await response.json();
const table = document.getElementById('allSalesDataTable');
const thead = table.querySelector('thead tr');
const tbody = table.querySelector('tbody');
// Clear existing content
thead.innerHTML = '';
tbody.innerHTML = '';
if (data.length > 0) {
// Create headers
const headers = Object.keys(data[0]);
headers.forEach(header => {
const th = document.createElement('th');
th.innerText = header;
thead.appendChild(th);
});
// Populate table rows
data.forEach(record => {
const tr = document.createElement('tr');
headers.forEach(header => {
const td = document.createElement('td');
td.innerText = record[header];
tr.appendChild(td);
});
tbody.appendChild(tr);
});
} else {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 10; // Arbitrary large number to span all columns
td.innerText = 'No sales data available.';
tr.appendChild(td);
tbody.appendChild(tr);
}
} catch (error) {
console.error('Error fetching all sales data:', error);
const table = document.getElementById('allSalesDataTable');
const tbody = table.querySelector('tbody');
tbody.innerHTML = '<tr><td colspan="10">Error loading data.</td></tr>';
}
}
// Run all fetch functions on page load
document.addEventListener('DOMContentLoaded', () => {
fetchSummaryData();
fetchChartData();
fetchAllSalesData();
});
</script>
</body>
</html>