feat:项目完成
This commit is contained in:
26
app/__init__.py
Normal file
26
app/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from flask import Flask, request, g
|
||||
from flask_cors import CORS
|
||||
from .routes import auth_bp, order_bp
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__, static_folder="../static", template_folder="../templates")
|
||||
app.config['SECRET_KEY'] = 'your-secret-key'
|
||||
CORS(app)
|
||||
|
||||
# 添加性能监控中间件
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.start_time = time.time()
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
if hasattr(g, 'start_time'):
|
||||
duration = round((time.time() - g.start_time) * 1000, 2)
|
||||
app.logger.info(f"[{datetime.now()}] {request.method} {request.path} - {duration}ms")
|
||||
return response
|
||||
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(order_bp, url_prefix='/orders')
|
||||
return app
|
BIN
app/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/db.cpython-311.pyc
Normal file
BIN
app/__pycache__/db.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/models.cpython-311.pyc
Normal file
BIN
app/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/routes.cpython-311.pyc
Normal file
BIN
app/__pycache__/routes.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/schemas.cpython-311.pyc
Normal file
BIN
app/__pycache__/schemas.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/utils.cpython-311.pyc
Normal file
BIN
app/__pycache__/utils.cpython-311.pyc
Normal file
Binary file not shown.
35
app/db.py
Normal file
35
app/db.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from sqlmodel import create_engine, SQLModel
|
||||
from sqlalchemy.pool import QueuePool
|
||||
from sqlalchemy import text
|
||||
from app.models import UserRole, OrderStatus
|
||||
import time
|
||||
|
||||
def wait_for_db(max_retries=5, delay=5):
|
||||
"""等待数据库连接可用"""
|
||||
for i in range(max_retries):
|
||||
try:
|
||||
test_engine = create_engine(
|
||||
"mysql+pymysql://root:123456@niit-node3/orders_db",
|
||||
poolclass=QueuePool,
|
||||
pool_size=10,
|
||||
max_overflow=20,
|
||||
pool_timeout=30,
|
||||
pool_recycle=3600,
|
||||
echo=False
|
||||
)
|
||||
with test_engine.connect() as conn:
|
||||
conn.execute(text("SELECT 1"))
|
||||
print(f"数据库连接测试成功 (尝试 {i+1}/{max_retries})")
|
||||
return test_engine
|
||||
except Exception as e:
|
||||
if i == max_retries - 1:
|
||||
raise
|
||||
time.sleep(delay)
|
||||
return None
|
||||
|
||||
# 使用连接池的数据库引擎
|
||||
engine = wait_for_db()
|
||||
|
||||
# 创建数据库表
|
||||
def init_db():
|
||||
SQLModel.metadata.create_all(engine)
|
31
app/models.py
Normal file
31
app/models.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from sqlmodel import SQLModel, Field
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
import datetime
|
||||
|
||||
class UserRole(str, Enum):
|
||||
admin = "admin"
|
||||
user = "user"
|
||||
|
||||
class User(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
username: str = Field(index=True, unique=True)
|
||||
password_hash: str
|
||||
role: UserRole = Field(default=UserRole.user)
|
||||
|
||||
class OrderStatus(str, Enum):
|
||||
pending = "pending"
|
||||
in_progress = "in_progress"
|
||||
completed = "completed"
|
||||
|
||||
class Order(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
created_at: datetime.datetime = Field(default_factory=datetime.datetime.utcnow)
|
||||
status: OrderStatus = Field(default=OrderStatus.pending)
|
||||
|
||||
class TopProductSummary(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
product_name: str = Field(index=True)
|
||||
sales_count: int = Field(default=0)
|
250
app/routes.py
Normal file
250
app/routes.py
Normal file
@@ -0,0 +1,250 @@
|
||||
from flask import Blueprint, request, jsonify, g, render_template, redirect, url_for
|
||||
from sqlmodel import Session, select
|
||||
from sqlalchemy import text
|
||||
from .models import User, Order
|
||||
from .schemas import UserLogin, OrderCreate, OrderUpdate, UserUpdate
|
||||
from .utils import hash_password, verify_password, create_jwt_token, decode_jwt_token, admin_required
|
||||
from .db import engine
|
||||
from .schemas import OrderStatus
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
order_bp = Blueprint('order', __name__)
|
||||
|
||||
def jwt_required(f):
|
||||
def wrapper(*args, **kwargs):
|
||||
auth = request.headers.get('Authorization', None)
|
||||
if not auth or not auth.startswith('Bearer '):
|
||||
return jsonify({'msg': 'Missing or invalid token'}), 401
|
||||
token = auth.split(' ')[1]
|
||||
user_data = decode_jwt_token(token)
|
||||
if not user_data:
|
||||
return jsonify({'msg': 'Token invalid or expired'}), 401
|
||||
g.user_id = user_data['user_id']
|
||||
g.role = user_data['role']
|
||||
return f(*args, **kwargs)
|
||||
wrapper.__name__ = f.__name__
|
||||
return wrapper
|
||||
|
||||
@auth_bp.route('/login', methods=['POST'])
|
||||
def login():
|
||||
data = request.get_json()
|
||||
login_data = UserLogin(**data)
|
||||
with Session(engine) as session:
|
||||
user = session.exec(select(User).where(User.username == login_data.username)).first()
|
||||
if not user or not verify_password(login_data.password, user.password_hash):
|
||||
return jsonify({'msg': 'Invalid credentials'}), 401
|
||||
token = create_jwt_token(user)
|
||||
return jsonify({'token': token})
|
||||
|
||||
@auth_bp.route('/')
|
||||
def index():
|
||||
return redirect(url_for('auth.login_page'))
|
||||
|
||||
@auth_bp.route('/login_page')
|
||||
def login_page():
|
||||
return render_template('login.html')
|
||||
|
||||
# 订单管理接口
|
||||
@order_bp.route('/', methods=['GET'])
|
||||
@jwt_required
|
||||
def list_orders():
|
||||
with Session(engine) as session:
|
||||
orders = session.exec(select(Order)).all()
|
||||
return jsonify([order.dict() for order in orders])
|
||||
|
||||
@order_bp.route('/', methods=['POST'])
|
||||
@jwt_required
|
||||
def create_order():
|
||||
data = request.get_json()
|
||||
order_data = OrderCreate(**data)
|
||||
order = Order(title=order_data.title, description=order_data.description)
|
||||
with Session(engine) as session:
|
||||
session.add(order)
|
||||
session.commit()
|
||||
session.refresh(order)
|
||||
return jsonify(order.dict()), 201
|
||||
|
||||
@order_bp.route('/<int:order_id>', methods=['PUT'])
|
||||
@jwt_required
|
||||
def update_order(order_id):
|
||||
data = request.get_json()
|
||||
with Session(engine) as session:
|
||||
order = session.get(Order, order_id)
|
||||
if not order:
|
||||
return jsonify({'msg': 'Order not found'}), 404
|
||||
update_data = OrderUpdate(**data)
|
||||
for field, value in update_data.dict(exclude_unset=True).items():
|
||||
if field == "status" and value is not None:
|
||||
order.status = value
|
||||
elif field != "status":
|
||||
setattr(order, field, value)
|
||||
session.add(order)
|
||||
session.commit()
|
||||
return jsonify(order.dict())
|
||||
|
||||
@order_bp.route('/<int:order_id>', methods=['DELETE'])
|
||||
@jwt_required
|
||||
def delete_order(order_id):
|
||||
with Session(engine) as session:
|
||||
order = session.get(Order, order_id)
|
||||
if not order:
|
||||
return jsonify({'msg': 'Order not found'}), 404
|
||||
session.delete(order)
|
||||
session.commit()
|
||||
return jsonify({'msg': 'Deleted'})
|
||||
|
||||
@auth_bp.route('/users/<int:user_id>', methods=['PUT'])
|
||||
@jwt_required
|
||||
def update_user(user_id):
|
||||
data = request.get_json()
|
||||
update_data = UserUpdate(**data)
|
||||
with Session(engine) as session:
|
||||
user = session.get(User, user_id)
|
||||
if not user:
|
||||
return jsonify({'msg': 'User not found'}), 404
|
||||
for field, value in update_data.dict(exclude_unset=True).items():
|
||||
setattr(user, field, value)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
return jsonify(user.dict())
|
||||
|
||||
@order_bp.route('/panel')
|
||||
def order_panel():
|
||||
return render_template('orders.html')
|
||||
|
||||
@order_bp.route('/summary')
|
||||
@jwt_required
|
||||
def get_order_summary():
|
||||
with Session(engine) as session:
|
||||
# 查询all_orders_summary表,按status分组统计count总和
|
||||
result = session.execute(
|
||||
text("SELECT status, SUM(count) as total_count "
|
||||
"FROM all_orders_summary "
|
||||
"GROUP BY status")
|
||||
).fetchall()
|
||||
|
||||
# 转换为字典列表格式
|
||||
status_mapping = {
|
||||
"0": "差评",
|
||||
"50": "中评",
|
||||
"100": "好评"
|
||||
}
|
||||
summary = [{"status": status_mapping.get(str(row[0]), str(row[0])), "count": row[1]} for row in result]
|
||||
return jsonify(summary)
|
||||
|
||||
@order_bp.route('/rating_summary')
|
||||
@jwt_required
|
||||
def get_rating_summary():
|
||||
with Session(engine) as session:
|
||||
# 查询each_order_summary表,按order_id和status分组统计
|
||||
result = session.execute(
|
||||
text("SELECT order_id, status, SUM(count) as total_count "
|
||||
"FROM each_order_summary "
|
||||
"GROUP BY order_id, status "
|
||||
"ORDER BY order_id, status")
|
||||
).fetchall()
|
||||
|
||||
# 获取所有种类
|
||||
categories = sorted({row[0] for row in result})
|
||||
status_mapping = {
|
||||
"0": "差评",
|
||||
"50": "中评",
|
||||
"100": "好评"
|
||||
}
|
||||
|
||||
# 初始化数据结构,确保每个种类都有三种评分
|
||||
series_data = {
|
||||
"差评": [0] * len(categories),
|
||||
"中评": [0] * len(categories),
|
||||
"好评": [0] * len(categories)
|
||||
}
|
||||
|
||||
# 填充数据
|
||||
for row in result:
|
||||
category_idx = categories.index(row[0])
|
||||
status = status_mapping[row[1]]
|
||||
series_data[status][category_idx] = row[2]
|
||||
|
||||
return jsonify({
|
||||
"categories": categories,
|
||||
"series": [
|
||||
{"name": "差评", "data": series_data["差评"]},
|
||||
{"name": "中评", "data": series_data["中评"]},
|
||||
{"name": "好评", "data": series_data["好评"]}
|
||||
]
|
||||
})
|
||||
@order_bp.route('/type_summary')
|
||||
@jwt_required
|
||||
def get_type_summary():
|
||||
with Session(engine) as session:
|
||||
# 查询order_type_summary表,按order_type分组统计count总和
|
||||
result = session.execute(
|
||||
text("SELECT order_type, SUM(count) as total_count "
|
||||
"FROM order_type_summary "
|
||||
"GROUP BY order_type")
|
||||
).fetchall()
|
||||
|
||||
# 转换为字典列表格式
|
||||
summary = [{"order_type": row[0], "count": row[1]} for row in result]
|
||||
return jsonify(summary)
|
||||
|
||||
@order_bp.route('/top_products')
|
||||
@jwt_required
|
||||
def get_top_products():
|
||||
with Session(engine) as session:
|
||||
# 查询top_products_summary表,按sales_count降序获取前5条记录
|
||||
result = session.execute(
|
||||
text("SELECT order_type, SUM(count) as total_count "
|
||||
"FROM order_type_summary "
|
||||
"GROUP BY order_type"
|
||||
" ORDER BY total_count DESC "
|
||||
"LIMIT 5")
|
||||
).fetchall()
|
||||
|
||||
# 转换为字典列表格式
|
||||
top_products = [{"product_name": row[0], "sales_count": row[1]} for row in result]
|
||||
return jsonify(top_products)
|
||||
|
||||
|
||||
@auth_bp.route('/users/', methods=['GET'])
|
||||
@jwt_required
|
||||
@admin_required
|
||||
def list_users():
|
||||
with Session(engine) as session:
|
||||
users = session.exec(select(User)).all()
|
||||
return jsonify([user.dict() for user in users])
|
||||
|
||||
@auth_bp.route('/users/panel')
|
||||
def users_panel():
|
||||
return render_template('users.html')
|
||||
|
||||
@auth_bp.route('/users/', methods=['POST'])
|
||||
@jwt_required
|
||||
@admin_required
|
||||
def create_user():
|
||||
data = request.get_json()
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
role = data.get('role', 'user')
|
||||
if not username or not password:
|
||||
return jsonify({'msg': '用户名和密码必填'}), 400
|
||||
with Session(engine) as session:
|
||||
if session.exec(select(User).where(User.username == username)).first():
|
||||
return jsonify({'msg': '用户名已存在'}), 400
|
||||
from .utils import hash_password
|
||||
user = User(username=username, password_hash=hash_password(password), role=role)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
return jsonify(user.dict()), 201
|
||||
|
||||
@auth_bp.route('/users/<int:user_id>', methods=['DELETE'])
|
||||
@jwt_required
|
||||
@admin_required
|
||||
def delete_user(user_id):
|
||||
with Session(engine) as session:
|
||||
user = session.get(User, user_id)
|
||||
if not user:
|
||||
return jsonify({'msg': '用户不存在'}), 404
|
||||
session.delete(user)
|
||||
session.commit()
|
||||
return jsonify({'msg': '已删除'})
|
28
app/schemas.py
Normal file
28
app/schemas.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from pydantic import BaseModel
|
||||
from enum import Enum as PydanticEnum
|
||||
|
||||
class UserRole(str, PydanticEnum):
|
||||
admin = "admin"
|
||||
user = "user"
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
class OrderStatus(str, PydanticEnum):
|
||||
pending = "pending"
|
||||
in_progress = "in_progress"
|
||||
completed = "completed"
|
||||
|
||||
class OrderCreate(BaseModel):
|
||||
title: str
|
||||
description: str = ""
|
||||
|
||||
class OrderUpdate(BaseModel):
|
||||
title: str = None
|
||||
description: str = None
|
||||
status: OrderStatus = None
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
username: str = None
|
||||
role: UserRole = None
|
41
app/utils.py
Normal file
41
app/utils.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import jwt
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from datetime import datetime, timedelta
|
||||
from flask import current_app, request, jsonify
|
||||
|
||||
def hash_password(password):
|
||||
return generate_password_hash(password)
|
||||
|
||||
def verify_password(password, password_hash):
|
||||
return check_password_hash(password_hash, password)
|
||||
|
||||
def create_jwt_token(user):
|
||||
payload = {
|
||||
'user_id': user.id,
|
||||
'role': user.role,
|
||||
'exp': datetime.utcnow() + timedelta(days=1)
|
||||
}
|
||||
token = jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
|
||||
return token
|
||||
|
||||
def decode_jwt_token(token):
|
||||
try:
|
||||
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
|
||||
return {'user_id': payload['user_id'], 'role': payload['role']}
|
||||
except jwt.ExpiredSignatureError:
|
||||
return None
|
||||
except jwt.InvalidTokenError:
|
||||
return None
|
||||
|
||||
def admin_required(f):
|
||||
def wrapper(*args, **kwargs):
|
||||
auth = request.headers.get('Authorization', None)
|
||||
if not auth or not auth.startswith('Bearer '):
|
||||
return jsonify({'msg': 'Missing or invalid token'}), 401
|
||||
token = auth.split(' ')[1]
|
||||
user_data = decode_jwt_token(token)
|
||||
if not user_data or user_data.get('role') != 'admin':
|
||||
return jsonify({'msg': 'Admin access required'}), 403
|
||||
return f(*args, **kwargs)
|
||||
wrapper.__name__ = f.__name__
|
||||
return wrapper
|
Reference in New Issue
Block a user