From e166ec615bcc70fc40b5c5b044a5bdf20cbc8a57 Mon Sep 17 00:00:00 2001 From: kakune55 Date: Tue, 17 Jun 2025 19:55:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E9=A1=B9=E7=9B=AE=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + Dockerfile | 20 + app/__init__.py | 26 ++ app/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 2086 bytes app/__pycache__/db.cpython-311.pyc | Bin 0 -> 2055 bytes app/__pycache__/models.cpython-311.pyc | Bin 0 -> 2755 bytes app/__pycache__/routes.cpython-311.pyc | Bin 0 -> 18966 bytes app/__pycache__/schemas.cpython-311.pyc | Bin 0 -> 2072 bytes app/__pycache__/utils.cpython-311.pyc | Bin 0 -> 2961 bytes app/db.py | 35 ++ app/models.py | 31 ++ app/routes.py | 250 +++++++++++ app/schemas.py | 28 ++ app/utils.py | 41 ++ main.py | 20 + order_ms.db | Bin 0 -> 16384 bytes requirements.txt | 8 + templates/login.html | 170 ++++++++ templates/order_detail.html | 187 ++++++++ templates/orders.html | 526 +++++++++++++++++++++++ templates/users.html | 205 +++++++++ 21 files changed, 1548 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-311.pyc create mode 100644 app/__pycache__/db.cpython-311.pyc create mode 100644 app/__pycache__/models.cpython-311.pyc create mode 100644 app/__pycache__/routes.cpython-311.pyc create mode 100644 app/__pycache__/schemas.cpython-311.pyc create mode 100644 app/__pycache__/utils.cpython-311.pyc create mode 100644 app/db.py create mode 100644 app/models.py create mode 100644 app/routes.py create mode 100644 app/schemas.py create mode 100644 app/utils.py create mode 100644 main.py create mode 100644 order_ms.db create mode 100644 requirements.txt create mode 100644 templates/login.html create mode 100644 templates/order_detail.html create mode 100644 templates/orders.html create mode 100644 templates/users.html 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 0000000000000000000000000000000000000000..a780b5d7589af8d70310e94ddb7fc8f584ab864f GIT binary patch literal 2086 zcmZ`)T}&fY6ux(U+Uc}V1+|1_wQD7t8fo=uH-v03fC?JWg%|73?9iD)VWv~>+-9W< zv73-sHfz*m$;P-OnymN(CO&B5<38wnYns?h!jmzPw;EmyPo6vdV|L;8_VnCy&iy&x z{qE_vJv}ml#tpx<{3?&oUu@8BTuV866O_A1LmD=aiBs6*Qk==Bc+6~^A(&!H1fACe z!)N+aep57Mic@&op+46DT5nnM>d)4t1{eyyTQwJ}_ zu_H{(U7bI~EP`|VYJn~*i+M+~h^78!FxIff?en`3yT{_)c8k-5EpDX~rHNx;r~Z~3ezv$>SZ}YhCI-uj(DrM-O|%Px zy|w{uFh4t!+sa%90|e4TF?fsn%GZuL`Ad7dqeu5WW6opUIhwQ|*oAENIvKI^0{HT5 zF_1_kZK~32x)=hL>So>mKyARgr)gI*mH}`(;kK`3D2CUp0vWOOG|}nEs=nsP^HUR7 zrxujgr{4Bl2HUnDzDMIAyjQUTmS*#Uaq^0k&apKFhsLwO7J=P3VTd|19FtIm8NNqH zY_a2uNn%+vnYJ`N+2o38E7$eKq?*qs6(yU?QbkGR*PPx(J!27FX_2n@EaU>&D-f3$ zGpw{~*jEzXm>$@`X4#a`-;r~7);_;+=f)@acBl*=!tCh|D_{_WubcV{VBN;vU6jltvdco-gT1CM_lgOqjx4|95*U2|ZTJPwiBe<0mM#(-n|PLX>rdW@q* zJ~|S0IMa@Y9RIRvtCW%^7erzeavEXWcV|03!&+Jb@(S4;Tr@kE!{@B)guPYAXX

z(i|bL(q$l(M07iEO_>X)QN|O((y%`Q z@gGno*th-CXGS#`s|8~X#03`dL1bu0yO;f9_1@}_FYmumefsjRlhyO%4=z+AleNfX z`L%i|e0%28xm$CUp=;kr_d{Rz-tPq!`Y#uMPBgXOFa0*w)G7<_wv?f3)sR{Xsg;mg z4@b&jmz_m_^F`QDI^_Z}lswTh*$?9Z!jg0(MaikAt|*Q#ZRIlACBiBad6J1?7SKoZ zrL4^WSTqWHo@8^BFj|lRh&bPkB+r4Nh1_@(Ik^iJc^o!L!h0QVgFVxW-ganrO34^X^Yq{ZjKN52*tFEP?MUBhb{tyy&88-7olTm%(S$E zp9y>u9)gTCbyE$^F#P`HHtUgnk@uphty!#Qy>Q CPtH{U literal 0 HcmV?d00001 diff --git a/app/__pycache__/db.cpython-311.pyc b/app/__pycache__/db.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef9b97624b9c29ceff706bfce54c0748692e8b88 GIT binary patch literal 2055 zcmZ`)-ER{|5Z^uD+2^n191&C!oP;LOn2(qg=mQ{Bh0;oeh9;&Du~b>k-9k*yKC`#3 zlgO!}ib|oSZCZ+WptO-HQYzZQODkHbAEf_)jVxiEl&30zyd4S;QJy+`j`KyC+x^|_ z+|KOmcxL>4Fz7=--|gxfdP79$U%sf3u)-`30doy$NW(fx<2X(WaRKwSpu5syTy%Js zE~VXZx5JCNCoRY2v^VZ`){^dv`!uiSxrpO_0TKT+SiD_32gL)Ld=bTiniNA3-vXCM z1S_agl4K-7GUrkmV#%?-Q*Rj>(IeRM_T@;9ylohKAu=+|V88s1Noc>J6U%>^YJ|pE zlI6_a1qsZlwYzZJ{KhVri&L22iviZCfTm$}dsXrpdiRep!HXE*zd{;j5UCnzg67gh zP14*ydZs;~3$sdNu7Yc7onB3j(8k*zLSteC)VvAI)FAJOAm=bH87C{MR@0pWU4M{LZ7VZjJ6wkC+$qJ=qcGeK;DWhQSVZ zzkJ}}p;unbq*82O2J+@W)ZkfRCbYo?o&gJ6IJ5wy6u8gp5x3>dLPn{2(lo1V!c2`4 zE09hOCye(AJ*yjeE5P?8SSn4799vmYqK>GLcOp@Tj8Qpu;?#-bXWF`R9bkNu%k#f{ z^7zix`H9K7kH2l}oV$AiR&5JB-%sE1N>QJ7%mPa)*8KN@FLxzc( zV}!c7Sjy5=hFRVd!z#(Ll#zi_0KY8B)CtMLef->WN*GNM(~_Wck|P#QS$;-LRx2x! z+qN(6vY6uQYA1gj=1lqte)9*UQayPY}RU z4ihKs2GkIK80vTs>L`VFl|#F1Bm`slR!2qIaxL&>;KrG$-KFrpa(G`!=_)H-MWt(2 zX)JE-yVrN`!f!EPVElcg`UWg1{bi-UsPx+sw^OxN#V3yCrw*={KaXEpT6*3m+l{ug z76UESu{2x%f{k2Wh(HZ0^~&VQsU2UOeo)_DtZ%O<^6uS9#c3HODgJwECFu>kNhKC!3HJ5%T4dHmlDaxX(j#5Gj;rP6Ta#7`%LjSh z<>;D}=QWi%Z0EpJoklot|LATG{BXzlW%(#*<#ck4c7u$E&-@y|c43SwXm9cVMg{FG tZX6ZVT--RGuj?Oo9$Re0ij6kHe&lPMXucG_93FSuZj2)~dKMh-{{zZA10MhY literal 0 HcmV?d00001 diff --git a/app/__pycache__/models.cpython-311.pyc b/app/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa9680028aef093f8c33e0becb9eec13e3c209dc GIT binary patch literal 2755 zcmbtW&2JM&6rWws+Uu_*kc5~3MKp<$RX@^1RfTBMN>u_hp`qY#@nyAm2QXcG&Foq< zhpJnNgAYCUP>HCg94ZPTe*k~RZso&jZdFgcB}z`5`rbIk*vSFZ-REE4%zHCC^L}sM z)q#XPO%2Q$!TX9HLDJfO8q$=1}okW$EsMHgVR!x?Y zQm0O;lv2qxVrx%`on$&oKBI)ZgkR?=rKL3mR-Gw!dN(s*&q#X~?70(0XQed{)}a$e z=cIiY>?0@I^ENFKc=I#)cu!ODH>Tcqh z3T(o!^OPvlSn_PQNm$CJOVE=3D)?&y{yIy8PZE8kmg;)5%)@MVnqB=Fg!yGR)TESS z85osDUt56yX9UFnZ*0C$MapLPtcK%@Y@JuC7T+>|U|T}1*g&sVsoCsDk!;i|j~Yzq zNCL)KE;@MNK|UR7&c9-R&#v| zbHy`Fjxi7k&*vQ$kUf4LVF*Endjz%9T|f`-XMi3@0f`G2wzVDm6%7l^QDM1VSPs5v z>aEdV^C4XZiRf}XcDA*;L!WPkV~f$)VtZ^cxZW&>bP*(?i}C20kfKENOgui>y0dfn zkL7TDAsSz3k1qr(&AE^+fJ72O>8uTxwFw!o`PN;B^(94@hJ#7)=_N%k<4c-6RGP@% z#M~@GP#T^{)mhE1)b5LHrDoQ-d!IAU6X~*Btvk$T_J)xh4D=LyfKMaLAPhwAL+nGq zIC>WVdXbBVCW346>5IX7JaR7h7T?ak(7$gtNG3i9AwX|8@1NpOoKm*dNy$1EYpn9(*%z4n|gJl7o#+J_;jb z)&i#$A1YDSW?q?BWOW0khmX#UdCw~QF5eQFGG`VLV_H5x2ZKe@lS~>TgFImjdk2Ce z75oE)iwFjQ&~2E?ufS1DHvDqUeat^Xo8(Z^{4q8M7@oyGgmL(J+W+LJI!|7r)U2IPmgSD349=R0KB1lAw z@#MwrndjN|+?V0xQZ%{Lo?Hs9HrGSC1QJOErITE^)RU9z30VG5a&_t_RL}l8_*V6l z9;suMGQ*MsmGac4TmPQJMN{^RjVcz%x0AzA_ynv~8q!&iNFpd5`fvrtkkGwHj=V#80y<>17fFAs4z;UO@F|1;0yy5O z{1XJZFL~F>VlH!9rmoyq|6X6~OPx#pEB*zH#ailp4Y-$36eT9p;Ykvcb78;4WIXJb zm<$K<-J_~WLy9M literal 0 HcmV?d00001 diff --git a/app/__pycache__/routes.cpython-311.pyc b/app/__pycache__/routes.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fe0dbb2e8e1183dc91884a8efb99d9d3e7210970 GIT binary patch literal 18966 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 D;Z_A+ literal 0 HcmV?d00001 diff --git a/app/__pycache__/schemas.cpython-311.pyc b/app/__pycache__/schemas.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af9ed5ea8e5c7775f931ed3aa4e4a8d5abf8415e GIT binary patch literal 2072 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..057841225ed460876bd2afff0509cbe4c3e40186 GIT binary patch literal 16384 zcmeI&%TC)s6b4|MAksuC?gn|$%?Pnb)Pj7!WYeZ#l{6uNG_<7~H1#BJFBk|_4(veK25?(*lm{qe)@mgL>Ukj4mSKa)q^FE9wMsKjYrg^RxS&kEG%yA=c z%v{UWY|CK2Zu!1O8FRQ#HQhEsllrFac-~kSSCQ%ZR-_pYwoQhYP)QW81XXP^M0$EnBCFNTAde6METnIn_0uX=z z1Rwwb2tWV=5P$##ZWE}Lfd>_}R__0Ae*eFcl&jkkFb)9-KmY;|fB*y_009U<00Izz rz?~Fmia#PKO8c>{8=7ISYtFjv5>0OzUQ2T~98>dbbIKDICm+58o$=L! literal 0 HcmV?d00001 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用户名角色操作
+
+
+
+ + +