commit e166ec615bcc70fc40b5c5b044a5bdf20cbc8a57 Author: kakune55 Date: Tue Jun 17 19:55:10 2025 +0800 feat:项目完成 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d17dae --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.venv diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d7e976e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# 使用官方 Python 运行时作为父镜像 +FROM python:3.11-slim + +# 设置工作目录 +WORKDIR /app + +# 复制依赖文件 +COPY requirements.txt . + +# 安装依赖 +RUN pip install --no-cache-dir -r requirements.txt + +# 复制项目文件 +COPY . . + +# 暴露端口(假设你的应用运行在8000端口) +EXPOSE 8000 + +# 启动命令(假设用uvicorn启动FastAPI,如果是Flask请替换为flask run) +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..ecbb5e1 --- /dev/null +++ b/app/__init__.py @@ -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 diff --git a/app/__pycache__/__init__.cpython-311.pyc b/app/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a780b5d Binary files /dev/null and b/app/__pycache__/__init__.cpython-311.pyc differ diff --git a/app/__pycache__/db.cpython-311.pyc b/app/__pycache__/db.cpython-311.pyc new file mode 100644 index 0000000..ef9b976 Binary files /dev/null and b/app/__pycache__/db.cpython-311.pyc differ diff --git a/app/__pycache__/models.cpython-311.pyc b/app/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000..fa96800 Binary files /dev/null and b/app/__pycache__/models.cpython-311.pyc differ diff --git a/app/__pycache__/routes.cpython-311.pyc b/app/__pycache__/routes.cpython-311.pyc new file mode 100644 index 0000000..fe0dbb2 Binary files /dev/null and b/app/__pycache__/routes.cpython-311.pyc differ diff --git a/app/__pycache__/schemas.cpython-311.pyc b/app/__pycache__/schemas.cpython-311.pyc new file mode 100644 index 0000000..af9ed5e Binary files /dev/null and b/app/__pycache__/schemas.cpython-311.pyc differ diff --git a/app/__pycache__/utils.cpython-311.pyc b/app/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..c49b51a Binary files /dev/null and b/app/__pycache__/utils.cpython-311.pyc differ diff --git a/app/db.py b/app/db.py new file mode 100644 index 0000000..736345b --- /dev/null +++ b/app/db.py @@ -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) diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..a85577e --- /dev/null +++ b/app/models.py @@ -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) diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..c8bee57 --- /dev/null +++ b/app/routes.py @@ -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('/', 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('/', 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/', 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/', 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': '已删除'}) diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000..a14dd5d --- /dev/null +++ b/app/schemas.py @@ -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 diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..fe570e3 --- /dev/null +++ b/app/utils.py @@ -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 diff --git a/main.py b/main.py new file mode 100644 index 0000000..a402ac8 --- /dev/null +++ b/main.py @@ -0,0 +1,20 @@ +from app import create_app +from app.models import SQLModel, User +from app.db import engine +from app.utils import hash_password + +def init_db(): + SQLModel.metadata.create_all(engine) + # 创建初始管理员 + from sqlmodel import Session, select + with Session(engine) as session: + admin = session.exec(select(User).where(User.username == "admin")).first() + if not admin: + admin = User(username="admin", password_hash=hash_password("admin123"),role="admin") + session.add(admin) + session.commit() + +if __name__ == '__main__': + init_db() + app = create_app() + app.run(debug=True,host='0.0.0.0',port=5000) diff --git a/order_ms.db b/order_ms.db new file mode 100644 index 0000000..0578412 Binary files /dev/null and b/order_ms.db differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b92beb8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +flask +sqlmodel +pydantic +pyjwt +werkzeug +flask-cors +pymysql +pyecharts \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..c58ba80 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,170 @@ + + + + + + 登录 - 订单管理后台 + + + + + + + + + + + + diff --git a/templates/order_detail.html b/templates/order_detail.html new file mode 100644 index 0000000..c947c30 --- /dev/null +++ b/templates/order_detail.html @@ -0,0 +1,187 @@ + + + + + 订单详情 - 订单管理后台 + + + + +
+

订单详情

+ +
+
+
+ + + + + + diff --git a/templates/orders.html b/templates/orders.html new file mode 100644 index 0000000..4000fce --- /dev/null +++ b/templates/orders.html @@ -0,0 +1,526 @@ + + + + + 订单管理后台 + + + + + + +
+

订单管理

+
+ + +
+ + +
+
+

订单状态统计

+
+
+ 刷新频率(秒): + + +
+
+ +
+

热门商品Top5

+
+
+ +
+

订单评分统计

+
+
+
+ +
+

订单类型统计

+
+
+
+
+

新建订单

+
+
+ +
+
+ +
+ +
+
+

订单列表

+
+ + + + + + + + + + + +
ID标题描述状态操作
+
+ + + + diff --git a/templates/users.html b/templates/users.html new file mode 100644 index 0000000..8eabd2d --- /dev/null +++ b/templates/users.html @@ -0,0 +1,205 @@ + + + + + 用户管理 - 订单管理后台 + + + + + +
+

用户管理

+ + +
+
+

新增用户

+
+ + + + +
+
+
+
+ + + + + +
ID用户名角色操作
+
+
+
+ + +