【Flask 实战】简易下单平台

实现效果

登录页面

用户个人主页

新建订单

一、表结构设计

user_order

userinfo

建表

SQL 复制代码
-- ------------------------------
-- 1. 创建用户表 userinfo
-- ------------------------------
CREATE TABLE userinfo (
    -- 主键:自增整数(推荐用 SERIAL 实现自增,确保非空且唯一)
    id SERIAL PRIMARY KEY,
    -- 手机号:varchar(11),非空(11位手机号,如中国大陆手机号)
    mobile VARCHAR(11) NOT NULL,
    -- 密码:varchar(64),非空(通常存储 MD5/SHA256 哈希后的密码,长度足够)
    password VARCHAR(64) NOT NULL,
    -- 真实姓名:varchar(16),非空(最长16个字符,覆盖大多数姓名场景)
    real_name VARCHAR(16) NOT NULL,
    -- 角色:int,非空(建议用枚举含义,如 1=普通用户,2=管理员,3=操作员等)
    role INT NOT NULL,
    -- 可选优化:添加手机号唯一约束(避免重复注册)
    CONSTRAINT uk_userinfo_mobile UNIQUE (mobile)
);

-- ------------------------------
-- 2. 创建订单表 user_order(避免 order 关键字)
-- ------------------------------
CREATE TABLE user_order (
    -- 主键:自增整数,确保订单ID唯一
    id SERIAL PRIMARY KEY,
    -- 订单关联的URL:varchar(255),非空(存储目标URL,长度满足大多数场景)
    url VARCHAR(255) NOT NULL,
    -- 数量:int,非空(如订单商品数量、任务次数等,需根据业务定义正负范围)
    count INT NOT NULL CHECK (count > 0), -- 可选约束:确保数量为正数
    -- 关联用户ID:int,非空(关联 userinfo 表的 id,确保订单归属有效用户)
    user_id INT NOT NULL,
    -- 订单状态:int,非空(建议枚举含义,如 0=待处理,1=已完成,2=已取消等)
    status INT NOT NULL,
    -- 外键约束:确保 user_id 必须在 userinfo 表中存在(避免无效用户订单)
    CONSTRAINT fk_order_userinfo FOREIGN KEY (user_id) 
        REFERENCES userinfo (id)
        -- 可选:用户删除时的订单处理策略(根据业务选择)
        ON DELETE RESTRICT, -- RESTRICT:禁止删除有订单的用户;CASCADE:删除用户时同步删除订单
    -- 可选优化:添加订单查询索引(按用户ID+状态,提升查询效率)
    CONSTRAINT idx_order_user_status UNIQUE (user_id, status)
);

插入示例数据

为了简单,这里密码直接使用明文。

SQL 复制代码
-- 向用户表 userinfo 插入5条示例数据(明文密码)
INSERT INTO userinfo (mobile, password, real_name, role) VALUES
('13800138000', 'zhangsan123', '张三', 1),  -- 普通用户
('13900139000', 'admin456', '李四', 2),     -- 管理员角色
('13700137000', 'wangwu789', '王五', 1),
('13600136000', 'operator000', '赵六', 3),   -- 操作员角色
('13500135000', 'sunqi2023', '孙七', 1);

-- 向订单表 user_order 插入5条示例数据(关联上面的用户ID)
INSERT INTO user_order (url, count, user_id, status) VALUES
('https://example.com/product/1001', 2, 1, 0),  -- 张三的待处理订单
('https://example.com/service/2002', 1, 2, 1),  -- 李四的已完成订单
('https://example.com/goods/3003', 5, 3, 0),    -- 王五的待处理订单
('https://example.com/item/4004', 3, 1, 2),     -- 张三的已取消订单
('https://example.com/order/5005', 10, 5, 1);   -- 孙七的已完成订单

userinfo表

user_order表

二、功能开发

1. 基于flask蓝图创建项目

创建项目

__init__.py

Python 复制代码
from flask import Flask


def create_app():
    app = Flask(__name__)

    return app

app.py

Python 复制代码
from FlaskDemo import create_app

app = create_app()

if __name__ == '__main__':
    app.run()

创建蓝图

根据功能创建2个蓝图

account.py

Python 复制代码
from flask import Blueprint

# 蓝图对象
ac = Blueprint("account", __name__)

@ac.route('/login')
def login():
    return "登录"

@ac.route('/users')
def users():
    return "用户列表"

order.py

Python 复制代码
from flask import Blueprint

# 蓝图对象
od = Blueprint("order", __name__)


@od.route('/order/list')
def order_list():
    return "订单列表"


@od.route('/order/create')
def order_create():
    return "创建订单"

注册蓝图

__init__.py

Python 复制代码
from flask import Flask
from .views import account, order


def create_app():
    app = Flask(__name__)

    # 注册 相当于加入视图函数
    app.register_blueprint(account.ac)
    app.register_blueprint(order.od)

    return app

2. 用户登录

用户输入身份、手机号、密码后点击提交,会从数据库进行校验。

正确则跳转到订单页面。

错误则提示错误信息。

templates/login.html

新建一个模板,作为登录页面。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <h1>用户登录</h1>
    <form method="post" action="/login">
        <select name="role">
            <option value="1">客户</option>
            <option value="2">管理员</option>
        </select>
        <input type="text" name="mobile" placeholder="手机号"/>
        <input type="password" name="pwd" placeholder="密码"/>
        <input type="submit" value="提 交"/>
        <span style="color:red;"> {{ error }} </span>
    </form>
</body>
</html>

views/account.py

Python 复制代码
from flask import Blueprint, render_template, request, redirect
import psycopg2
from dbutils.pooled_db import PooledDB

# 蓝图对象
ac = Blueprint("account", __name__)

POOL = PooledDB(
    # 连接池配置
    creator=psycopg2,   # 用来连接数据库的模块
    maxconnections=10,  # 连接池最大允许的连接数 0和None表示不限制连接数
    mincached=2,        # 初始化时,连接池中最少创建的空闲连接,0表示不创建
    maxcached=3,        # 连接池中最多闲置的的连接,0和None不限制
    blocking=True,      # 连接池中如果没有可用连接,是否阻塞等待
    setsession=[],      # 开始会话前执行的命令列表 如:["set datestyle to ...", "..."]
    ping=0,

    # 数据库配置
    host="127.0.0.1",
    port="5432",
    user="postgres",
    password="123456",
    database="postgres"
)

@ac.route('/login', methods=["GET", "POST"])
def login():
    if request.method == "GET":
        return render_template("login.html")
    role = request.form.get("role")
    mobile = request.form.get("mobile")
    pwd = request.form.get("pwd")
    print(role, mobile, pwd)

    # 去数据库校验
    conn = POOL.connection()  # 通过连接池获取连接
    cursor = conn.cursor()
    cursor.execute("select * from userinfo where role=%s and mobile=%s and password=%s", [role, mobile, pwd])
    user_data = cursor.fetchone()
    cursor.close()
    conn.close()  # 将此连接交还连接池

    # 登录成功,跳转到订单列表
    if user_data:
        return redirect('/order/list')
    # 登录失败,显示错误信息
    return render_template("login.html", error="用户名或密码错误")

@ac.route('/users')
def users():
    return "用户列表"

登录

正确

错误

优化界面

下载

下载jQuery-3.6.0:https://code.jquery.com/jquery-3.6.0.min.js

下载Bootstrap-3.4.1:https://getbootstrap.com/docs/3.4/getting-started/#download

login.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <link rel="stylesheet" href="/static/bootstrap-3.4.1/css/bootstrap.css">
    <style>
        .login-box {
            margin-top: 30px;
            margin-left: auto;
            margin-right: auto;
            width: 450px;
            border: 1px solid #ddd;
            padding: 50px;
        }
        .login-box h2 {
            text-align: center;
        }
    </style>
</head>
<body>
    <div class="login-box">
        <h2>用户登录</h2>
        <form method="post" action="/login">
        <div class="form-group">
            <select name="role" class="form-control">
                <option value="1">客户</option>
                <option value="2">管理员</option>
            </select>
        </div>
        <div class="form-group">
            <label>手机号</label>
            <input type="text" class="form-control" placeholder="手机号" name="mobile"/>
        </div>
        <div class="form-group">
            <label>密码</label>
            <input type="password" class="form-control" placeholder="密码" name="pwd"/>
        </div>
        <input type="submit" class="btn btn-primary" value="登 录">
        <span style="color:red;"> {{ error }} </span>
    </form>
    </div>

</body>
</html>

优化数据库操作

将数据库相关操作隔离出来

db.py

Python 复制代码
import psycopg2
from dbutils.pooled_db import PooledDB
from psycopg2 import extras  # 导入额外的游标类型


POOL = PooledDB(
    # 连接池配置
    creator=psycopg2,   # 用来连接数据库的模块
    maxconnections=10,  # 连接池最大允许的连接数 0和None表示不限制连接数
    mincached=2,        # 初始化时,连接池中最少创建的空闲连接,0表示不创建
    maxcached=3,        # 连接池中最多闲置的的连接,0和None不限制
    blocking=True,      # 连接池中如果没有可用连接,是否阻塞等待
    setsession=[],      # 开始会话前执行的命令列表 如:["set datestyle to ...", "..."]
    ping=0,

    # 数据库配置
    host="127.0.0.1",
    port="5432",
    user="postgres",
    password="123456",
    database="postgres"
)


def fetch_one(sql, params):
    # 去数据库校验
    conn = POOL.connection()  # 通过连接池获取连接
    cursor = conn.cursor(cursor_factory=extras.DictCursor)  # 使游标返回字典类型
    cursor.execute(sql, params)
    result = cursor.fetchone()
    cursor.close()
    conn.close()  # 将此连接交还连接池
    return result

account.py

Python 复制代码
from flask import Blueprint, render_template, request, redirect
from utils import db

# 蓝图对象
ac = Blueprint("account", __name__)


@ac.route('/login', methods=["GET", "POST"])
def login():
    if request.method == "GET":
        return render_template("login.html")
    role = request.form.get("role")
    mobile = request.form.get("mobile")
    pwd = request.form.get("pwd")

    # 执行数据库校验
    sql = "select * from userinfo where role=%s and mobile=%s and password=%s"
    params = [role, mobile, pwd]
    user_data = db.fetch_one(sql, params)

    # 登录成功,跳转到订单列表
    if user_data:
        return redirect('/order/list')
    # 登录失败,显示错误信息
    return render_template("login.html", error="用户名或密码错误")

@ac.route('/users')
def users():
    return "用户列表"

请求拦截

使用session存储用户信息。

判断:

  1. 如果访问登录页面,所有人都可以访问。
  2. 其他业务页面,登录成功的人才能访问。
    已登录,继续访问
    未登录,返回登录页面

__init__.py

增加密钥和拦截器

Python 复制代码
from flask import Flask, request, session, redirect
from .views import account, order


def auth():
    # 访问静态文件,继续向后执行,不拦截 不加这个会导致页面失去效果
    if request.path.startswith("/static"):
        return
    # 访问登录页面,继续向后执行,不拦截
    if request.path == '/login':
        return
    # 用户已经登录过了,继续向后执行,不拦截
    user_info = session.get("user_info")
    if user_info:
        return

    return redirect('/login')


def create_app():
    app = Flask(__name__)
    app.secret_key = 'wsz4bdjny4dy1jyz3qhdxd0rs'

    # 注册 相当于加入视图函数
    app.register_blueprint(account.ac)
    app.register_blueprint(order.od)

    # 拦截器
    app.before_request(auth)

    return app

account.py

增加session

Python 复制代码
from flask import Blueprint, render_template, request, redirect, session
from utils import db

# 蓝图对象
ac = Blueprint("account", __name__)


@ac.route('/login', methods=["GET", "POST"])
def login():
    if request.method == "GET":
        return render_template("login.html")
    role = request.form.get("role")
    mobile = request.form.get("mobile")
    pwd = request.form.get("pwd")

    # 执行数据库校验
    sql = "select * from userinfo where role=%s and mobile=%s and password=%s"
    params = [role, mobile, pwd]
    user_data = db.fetch_one(sql, params)

    # 登录成功,跳转到订单列表
    if user_data:

        session["user_info"] = {
            'role': user_data['role'],
            'real_name': user_data['real_name'],
            'id': user_data['id']
        }

        return redirect('/order/list')
    # 登录失败,显示错误信息
    return render_template("login.html", error="用户名或密码错误")


@ac.route('/users')
def users():
    return "用户列表"

order.py

Python 复制代码
from flask import Blueprint, session, redirect

# 蓝图对象
od = Blueprint("order", __name__)


@od.route('/order/list')
def order_list():

    return "订单列表"


@od.route('/order/create')
def order_create():

    return "创建订单"


@od.route('/order/delete')
def order_delete():

    return "删除订单"

3. 订单列表

读取订单表,展示订单的信息。

  • 管理员:所有订单
  • 客户:当前客户自己的订单

展示当前用户订单

templates/order_list.html

用于展示所有订单

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <table border="1">
        <thead>
            <tr>
                <td>订单ID</td>
                <td>URL</td>
                <td>数量</td>
                <td>状态</td>
                <td>用户ID</td>
            </tr>
        </thead>
        <tbody>
            {% for item in data_list %}
            <tr>
                <td>{{item.id}}</td>
                <td>{{item.url}}</td>
                <td>{{item['count']}}</td>    <!-- count与PostgreSQL方法命名冲突 -->
                <td>{{item.status}}</td>
                <td>{{item.user_id}}</td>
            </tr>
            {% endfor %}
        </tbody>

    </table>
</body>
</html>

db.py

增加fetch_all,方便取所有数据

Python 复制代码
import psycopg2
from dbutils.pooled_db import PooledDB
from psycopg2 import extras  # 导入额外的游标类型


POOL = PooledDB(
    # 连接池配置
    creator=psycopg2,   # 用来连接数据库的模块
    maxconnections=10,  # 连接池最大允许的连接数 0和None表示不限制连接数
    mincached=2,        # 初始化时,连接池中最少创建的空闲连接,0表示不创建
    maxcached=3,        # 连接池中最多闲置的的连接,0和None不限制
    blocking=True,      # 连接池中如果没有可用连接,是否阻塞等待
    setsession=[],      # 开始会话前执行的命令列表 如:["set datestyle to ...", "..."]
    ping=0,

    # 数据库配置
    host="127.0.0.1",
    port="5432",
    user="postgres",
    password="123456",
    database="postgres"
)


def fetch_one(sql, params):
    # 去数据库校验
    conn = POOL.connection()  # 通过连接池获取连接
    cursor = conn.cursor(cursor_factory=extras.DictCursor)  # 使游标返回字典类型
    cursor.execute(sql, params)
    result = cursor.fetchone()
    cursor.close()
    conn.close()  # 将此连接交还连接池
    return result


def fetch_all(sql, params):
    # 去数据库校验
    conn = POOL.connection()  # 通过连接池获取连接
    cursor = conn.cursor(cursor_factory=extras.DictCursor)  # 使游标返回字典类型
    cursor.execute(sql, params)
    result = cursor.fetchall()
    cursor.close()
    conn.close()  # 将此连接交还连接池
    return result

order.py

增加获取当前用户订单的逻辑

Python 复制代码
from flask import Blueprint, session, render_template
from utils import db

# 蓝图对象
od = Blueprint("order", __name__)


@od.route('/order/list')
def order_list():
    user_info = session.get("user_info")
    role = user_info['role']
    if role == 2:
        data_list = db.fetch_all("select * from user_order", [])
    else:
        id = user_info['id']
        data_list = db.fetch_all("select * from user_order where user_id=%s", [id, ])

    return render_template("order_list.html", data_list=data_list)


@od.route('/order/create')
def order_create():

    return "创建订单"


@od.route('/order/delete')
def order_delete():

    return "删除订单"

优化:显示用户名 + 状态

order.py 修改order_list(),增加联表查询

Python 复制代码
@od.route('/order/list')
def order_list():
    user_info = session.get("user_info")
    role = user_info['role']  # 2-管理员
    if role == 2:
        sql = "select * from user_order left join userinfo on user_order.user_id = userinfo.id;"
        data_list = db.fetch_all(sql, [])
    else:
        id = user_info['id']
        sql = "select * from user_order left join userinfo on user_order.user_id = userinfo.id where user_id=%s;"
        data_list = db.fetch_all(sql, [id, ])

    status_dict = {
        0: "正在下单",
        1: "待执行",
        2: "正在执行",
        3: "完成",
        4: "失败"
    }

    return render_template("order_list.html", data_list=data_list, status_dict=status_dict)

order_list.html 修改表体,将状态显示为文字,增加用户名列

html 复制代码
<tbody>
     {% for item in data_list %}
    <tr>
        <td>{{item.id}}</td>
        <td>{{item.url}}</td>
        <td>{{item['count']}}</td>    <!-- count与PostgreSQL方法命名冲突 -->
        <td>{{status_dict[item.status]}}</td>
        <td>{{item.user_id}}</td>
        <td>{{item.real_name}}</td>
    </tr>
    {% endfor %}
</tbody>

前端美化

增加导航栏,美化状态显示。

order.py

Python 复制代码
from flask import Blueprint, session, render_template
from utils import db

# 蓝图对象
od = Blueprint("order", __name__)


@od.route('/order/list')
def order_list():
    user_info = session.get("user_info")
    role = user_info['role']  # 2-管理员
    if role == 2:
        sql = "select * from user_order left join userinfo on user_order.user_id = userinfo.id;"
        data_list = db.fetch_all(sql, [])
    else:
        id = user_info['id']
        sql = "select * from user_order left join userinfo on user_order.user_id = userinfo.id where user_id=%s;"
        data_list = db.fetch_all(sql, [id, ])

    status_dict = {
        0: {"text": "正在下单", "cls": "default"},
        1: {"text": "待执行", "cls": "primary"},
        2: {"text": "正在执行", "cls": "warning"},
        3: {"text": "完成", "cls": "success"},
        4: {"text": "失败", "cls": "danger"}
    }

    return render_template("order_list.html", data_list=data_list, status_dict=status_dict, real_name=user_info['real_name'])


@od.route('/order/create')
def order_create():

    return "创建订单"


@od.route('/order/delete')
def order_delete():

    return "删除订单"

order_list.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="/static/bootstrap-3.4.1/css/bootstrap.css">
</head>
<body>
<nav class="navbar navbar-default" style="border-radius:0">
  <div class="container-fluid">
    <!-- Brand and toggle get grouped for better mobile display -->
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="#">订单平台</a>
    </div>

    <!-- Collect the nav links, forms, and other content for toggling -->
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li>
        <li><a href="#">Link</a></li>
        <li class="dropdown">
          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> {{real_name}} <span class="caret"></span></a>
          <ul class="dropdown-menu">
            <li><a href="#">Action</a></li>
            <li><a href="#">Another action</a></li>
            <li><a href="#">Something else here</a></li>
            <li role="separator" class="divider"></li>
            <li><a href="#">Separated link</a></li>
            <li role="separator" class="divider"></li>
            <li><a href="#">One more separated link</a></li>
          </ul>
        </li>
      </ul>

      <ul class="nav navbar-nav navbar-right">

        <li class="dropdown">
          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
          <ul class="dropdown-menu">
            <li><a href="#">Action</a></li>
            <li><a href="#">Another action</a></li>
            <li><a href="#">Something else here</a></li>
            <li role="separator" class="divider"></li>
            <li><a href="#">Separated link</a></li>
          </ul>
        </li>
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>
<div class="container">
    <table class="table table-bordered">
        <thead>
            <tr>
                <td>订单ID</td>
                <td>URL</td>
                <td>数量</td>
                <td>状态</td>
                <td>用户ID</td>
                <td>用户名</td>
            </tr>
        </thead>
        <tbody>
            {% for item in data_list %}
            <tr>
                <td>{{item.id}}</td>
                <td>{{item.url}}</td>
                <td>{{item['count']}}</td>    <!-- count与PostgreSQL方法命名冲突 -->
                <td><span class="label label-{{status_dict[item.status].cls}}">{{status_dict[item.status].text}}</span></td>
                <td>{{item.user_id}}</td>
                <td>{{item.real_name}}</td>
            </tr>
            {% endfor %}
        </tbody>

    </table>
</div>

</body>
</html>

4. 创建订单

前端模板提取

几个业务页面的主题是一致的,可以提取为一个模板,方便之后维护。

提取出的模板如下:

{% block body %} {% endblock %} 相当于占位符。

layout.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
    <link rel="stylesheet" href="/static/bootstrap-3.4.1/css/bootstrap.css">
</head>
<body>
<nav class="navbar navbar-default" style="border-radius:0">
  <div class="container-fluid">
    <!-- Brand and toggle get grouped for better mobile display -->
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="#">订单平台</a>
    </div>

    <!-- Collect the nav links, forms, and other content for toggling -->
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li>
        <li><a href="#">Link</a></li>
        <li class="dropdown">
          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> {{real_name}} <span class="caret"></span></a>
          <ul class="dropdown-menu">
            <li><a href="#">Action</a></li>
            <li><a href="#">Another action</a></li>
            <li><a href="#">Something else here</a></li>
            <li role="separator" class="divider"></li>
            <li><a href="#">Separated link</a></li>
            <li role="separator" class="divider"></li>
            <li><a href="#">One more separated link</a></li>
          </ul>
        </li>
      </ul>

      <ul class="nav navbar-nav navbar-right">

        <li class="dropdown">
          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
          <ul class="dropdown-menu">
            <li><a href="#">Action</a></li>
            <li><a href="#">Another action</a></li>
            <li><a href="#">Something else here</a></li>
            <li role="separator" class="divider"></li>
            <li><a href="#">Separated link</a></li>
          </ul>
        </li>
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>
<div class="container">
    {% block body %}
    {% endblock %}
</div>
<script src="/static/jquery-3.6.0.min.js"></script>
<script src="/static/bootstrap-3.4.1/js/bootstrap.js"></script>
</body>
</html>

这样有这部分代码的页面就可以更简洁。

order_create.html

html 复制代码
{% extends 'layout.html' %}

{% block body %}
<div class="panel panel-default">
        <div class="panel-heading">
            新建订单
        </div>
         <div class="panel-body">
            <div class="form-group">
                <input type="text" class="form-control" placeholder="url">
                <input type="number" class="form-control" placeholder="数量">
            </div>
            <button type="submit" class="btn btn-success"> 提交订单 </button>
        </div>
    </div>
{% endblock %}

order_list.html

html 复制代码
{% extends 'layout.html' %}

{% block body %}
<div style="margin-bottom:10px">
        <a class="btn btn-success" href="/order/create">
            <span class="glyphicon glyphicon-plus-sign"></span> 添加
        </a>
    </div>


    <table class="table table-bordered">
        <thead>
            <tr>
                <td>订单ID</td>
                <td>URL</td>
                <td>数量</td>
                <td>状态</td>
                <td>用户ID</td>
                <td>用户名</td>
            </tr>
        </thead>
        <tbody>
            {% for item in data_list %}
            <tr>
                <td>{{item.id}}</td>
                <td>{{item.url}}</td>
                <td>{{item['count']}}</td>    <!-- count与PostgreSQL方法命名冲突 -->
                <td><span class="label label-{{status_dict[item.status].cls}}">{{status_dict[item.status].text}}</span></td>
                <td>{{item.user_id}}</td>
                <td>{{item.real_name}}</td>
            </tr>
            {% endfor %}
        </tbody>

    </table>
{% endblock %}

order.py

Python 复制代码
from flask import Blueprint, session, render_template
from utils import db

# 蓝图对象
od = Blueprint("order", __name__)


@od.route('/order/list')
def order_list():
    user_info = session.get("user_info")
    role = user_info['role']  # 2-管理员
    if role == 2:
        sql = "select * from user_order left join userinfo on user_order.user_id = userinfo.id;"
        data_list = db.fetch_all(sql, [])
    else:
        id = user_info['id']
        sql = "select * from user_order left join userinfo on user_order.user_id = userinfo.id where user_id=%s;"
        data_list = db.fetch_all(sql, [id, ])

    status_dict = {
        0: {"text": "正在下单", "cls": "default"},
        1: {"text": "待执行", "cls": "primary"},
        2: {"text": "正在执行", "cls": "warning"},
        3: {"text": "完成", "cls": "success"},
        4: {"text": "失败", "cls": "danger"}
    }

    return render_template("order_list.html", data_list=data_list, status_dict=status_dict, real_name=user_info['real_name'])


@od.route('/order/create')
def order_create():
    user_info = session.get("user_info")
    return render_template("order_create.html", real_name=user_info['real_name'])


@od.route('/order/delete')
def order_delete():

    return "删除订单"

页面效果

优化

每次切换任务界面,都要获取session中的用户名来替换导航栏中的用户名,非常麻烦。

可以使用模板函数,让模板自动获取用户名。

__init__.py 核心代码

Python 复制代码
from flask import session

def get_real_name():
    user_info = session.get("user_info")
    real_name = user_info['real_name']
    return real_name

def create_app():
    app = Flask(__name__)
    ...
    app.template_global()(get_real_name)
    ...
    return app

layout.html核心代码

在需要显示用户名的地方嵌入如下代码即可。

html 复制代码
{{ get_real_name() }}

这样就不必每个页面都获取一次用户名了。

5. 提交订单

点击提交

可以看到新的订单出现了

我这里遇到一个报错。当时示例数据库是用AI生成的,加了外键导致错误,现在删了省的麻烦。

SQL 复制代码
ALTER TABLE user_order 
DROP CONSTRAINT idx_order_user_status;

db.py 新增插入

Python 复制代码
def insert(sql, params):
    conn = POOL.connection()
    cursor = conn.cursor(cursor_factory=extras.DictCursor)
    cursor.execute(sql, params)
    conn.commit()
    result = cursor.fetchone()
    lastrowid = result[0] if result else None
    cursor.close()
    conn.close()
    return lastrowid

utils/cache.py Redis连接与入队

Python 复制代码
import redis

REDIS_POOL = redis.ConnectionPool(
    host='127.0.0.1',
    password='',
    port=6379,
    encoding='utf-8',
    max_connections=100
)
TASK_QUEUE = "task"
RESULT_QUEUE = "task_result"


def push_queue(value):
    conn = redis.Redis(connection_pool=REDIS_POOL)
    conn.lpush(TASK_QUEUE, value)

order.py

Python 复制代码
@od.route('/order/create', methods=["GET", "POST"])
def order_create():
    if request.method == "GET":
        return render_template("order_create.html")
    url = request.form.get('url')
    count = request.form.get('count')

    # 写入数据库
    user_info = session.get("user_info")
    sql = "insert into user_order(url, count, user_id, status) values(%s, %s, %s, 0) RETURNING id"
    params = [url, count, user_info['id']]
    order_id = db.insert(sql, params)
    print(order_id)

    # 写入redis队列
    cache.push_queue(order_id)

    return redirect('/order/list')

6. Worker执行订单

新建一个Worker项目,utils从老项目直接复制过来

db.py

Python 复制代码
import psycopg2
from dbutils.pooled_db import PooledDB
from psycopg2 import extras  # 导入额外的游标类型


POOL = PooledDB(
    # 连接池配置
    creator=psycopg2,   # 用来连接数据库的模块
    maxconnections=10,  # 连接池最大允许的连接数 0和None表示不限制连接数
    mincached=2,        # 初始化时,连接池中最少创建的空闲连接,0表示不创建
    maxcached=3,        # 连接池中最多闲置的的连接,0和None不限制
    blocking=True,      # 连接池中如果没有可用连接,是否阻塞等待
    setsession=[],      # 开始会话前执行的命令列表 如:["set datestyle to ...", "..."]
    ping=0,

    # 数据库配置
    host="127.0.0.1",
    port="5432",
    user="postgres",
    password="123456",
    database="postgres"
)


def fetch_one(sql, params):
    # 去数据库校验
    conn = POOL.connection()  # 通过连接池获取连接
    cursor = conn.cursor(cursor_factory=extras.DictCursor)  # 使游标返回字典类型
    cursor.execute(sql, params)
    result = cursor.fetchone()
    cursor.close()
    conn.close()  # 将此连接交还连接池
    return result


def fetch_all(sql, params):
    # 去数据库校验
    conn = POOL.connection()  # 通过连接池获取连接
    cursor = conn.cursor(cursor_factory=extras.DictCursor)  # 使游标返回字典类型
    cursor.execute(sql, params)
    result = cursor.fetchall()
    cursor.close()
    conn.close()  # 将此连接交还连接池
    return result


def insert(sql, params):
    conn = POOL.connection()
    cursor = conn.cursor(cursor_factory=extras.DictCursor)
    cursor.execute(sql, params)
    conn.commit()
    result = cursor.fetchone()
    lastrowid = result[0] if result else None
    cursor.close()
    conn.close()
    return lastrowid


def update(sql, params):
    conn = POOL.connection()
    cursor = conn.cursor(cursor_factory=extras.DictCursor)
    cursor.execute(sql, params)
    conn.commit()
    cursor.close()
    conn.close()

cache.py

Python 复制代码
import redis
from utils import db


REDIS_POOL = redis.ConnectionPool(
    host='127.0.0.1',
    password='',
    port=6379,
    encoding='utf-8',
    max_connections=100
)
TASK_QUEUE = "task"
RESULT_QUEUE = "task_result"


def push_queue(value):
    conn = redis.Redis(connection_pool=REDIS_POOL)
    conn.lpush(TASK_QUEUE, value)


def pop_queue():
    conn = redis.Redis(connection_pool=REDIS_POOL)
    data = conn.brpop(TASK_QUEUE, timeout=10)
    if not data:
        return
    return data[1].decode('utf-8')


def fetch_total_queue():
    conn = redis.Redis(connection_pool=REDIS_POOL)
    total_count = conn.llen(TASK_QUEUE)
    conn.lrange(name=TASK_QUEUE, start=0, end=total_count)






def db_queue_init():
    """
    1. 去数据库获取待执行的订单ID
    2. 去redis中获取待执行的订单ID
    3. 找到数据库中有 且 redis队列没有的 订单ID → 放入redis队列
    :return:
    """
    # 1. 去数据库获取待执行的订单ID
    sql = "select id from user_order where status=1"
    params = []
    db_list = db.fetch_all(sql, params)
    # db_id_list = [item['id'] for item in db_list]
    db_id_list = { item['id'] for item in db_list } # 集合

    # 2. 去redis中获取待执行的订单ID
    conn = redis.Redis(connection_pool=REDIS_POOL)
    total_count = conn.llen(TASK_QUEUE)
    cache_list = conn.lrange(TASK_QUEUE, start=0, end=total_count)
    # cache_int_list = [int(item.decode('utf-8')) for item in cache_list]
    cache_int_list = { int(item.decode('utf-8')) for item in cache_list}

    # 3. 找到数据库中有 且 redis队列没有的 订单ID → 放入redis队列
    need_push = db_id_list - cache_int_list
    conn.lpush(TASK_QUEUE, *need_push)

Worker.py

Python 复制代码
from concurrent.futures import ThreadPoolExecutor
import time
from utils import db, cache


def update_order(order_id, status):
    sql = "update user_order set status=%s where id=%s"
    params = [status, order_id]
    db.update(sql, params)


def get_order_object(order_id):
    sql = "select * from user_order where id=%s"
    params = [order_id]
    res = db.fetch_one(sql, params)
    return res


def task(info_dict):
    time.sleep(3)


def run():
    # 1. 初始化数据库未在队列的订单
    cache.db_queue_init()

    while True:
        # 2. 去队列中获取订单
        order_id = cache.pop_queue()
        if not order_id:
            continue

        # 3. 查看订单是否存在
        order_dict = get_order_object(order_id)
        if not order_dict:
            continue

        # 4. 更新订单状态 待执行 → 执行中
        update_order(order_id, 2)

        # 5. 执行订单
        print("执行订单任务:", order_id)
        thread_pool = ThreadPoolExecutor(10)
        for i in range(order_dict['count']):
            thread_pool.submit(task, order_dict)
        thread_pool.shutdown()

        # 6. 执行完成
        update_order(order_id, 3)


if __name__ == '__main__':
    run()

参考资料

1\] [【最快速度搞定Flask-框架教程】用5小时讲完的python-flask项目实战全套教程](https://www.bilibili.com/video/BV11PoTYkEE1?spm_id_from=333.788.videopod.episodes&vd_source=d14dcbb430127497f9d223f1e6337035&p=15)