简介:基于Python Flask框架实现的购物平台API源码,面向正在学习Flask后端开发、接口设计或电商系统搭建的开发者,可帮助理解从商品、用户、购物车到订单支付的一条完整业务链路。压缩包共53个文件,以36个Python脚本和5个XML配置为主体,另含PEM证书、txt说明、HTML/Mako页面以及数据库迁移相关文件,整体仅85KB,便于快速下载和本地部署。项目结构清晰,model层定义商品、用户、购物车、订单等数据模型,router层提供对应接口路由,libs封装统一响应与错误码,auth实现登录鉴权,alipay相关文件展示支付对接方式,templates目录作为前端页面入口,alembic迁移脚本可辅助初始化数据库。目前已有576人学习下载。借助该源码可上手Flask蓝图、SQLAlchemy模型映射、鉴权中间件、支付宝公钥私钥配置及API返回格式设计,适合作为课程设计或中小型电商项目的参考基础。
1. 拿到Flask购物平台API源码先看什么
从网上下载一个标着“Flask购物平台API”的源码包,直接扔进编辑器里改代码,大概率会在数据库迁移和支付宝密钥这两步上卡住。我最近拆了一个非常典型的版本,压缩包里一共54个文件,36个Python脚本、5个XML配置、2个PEM证书,外加一套alembic迁移脚本。定位很清楚:给线上购物App或小程序提供JSON接口,业务链路就是“注册登录—浏览商品—加购物车—创建订单—支付宝支付”。
这个源码包适合两类人:一是刚把Flask基础语法过完,想找一个能跑通的完整项目当作SQLAlchemy和蓝图参考的初学者;二是需要在内部快速搭建电商后端Demo,或者学习如何把支付宝支付、JWT鉴权、数据库迁移这些东西整合进Flask开发流程的工程师。源码里没有复杂的前端,只有一个index.html和零散模板,核心价值在API设计本身,而不是页面渲染。
2. 工程结构与数据模型:从目录拆解一个Flask业务系统
拿到源码以后,我先看文件树。这个包的目录结构在同类Flask项目里很有代表性:model层放SQLAlchemy模型,router层放蓝图,libs层放工具函数和响应封装,migrations是alembic迁移目录,config和key放配置与支付证书。下面是一份我整理过的核心文件职责表。
2.1 文件职责速览
| 路径 | 类型 | 职责 |
|---|---|---|
| app.py | 入口 | Flask应用初始化、配置加载、蓝图注册 |
| manage.py | 脚本 | 命令行入口,用于启动服务或执行命令 |
| model/user.py | 模型 | 用户表 |
| model/commodity.py | 模型 | 商品表 |
| model/category.py | 模型 | 商品分类表 |
| model/shopcar.py | 模型 | 购物车表 |
| model/order.py / orderitem.py | 模型 | 订单主表与订单明细表 |
| router/user.py | 蓝图 | 注册、登录、用户信息接口 |
| router/commodity.py | 蓝图 | 商品列表、详情、搜索接口 |
| router/shopcar.py | 蓝图 | 购物车增删改查接口 |
| router/order.py | 蓝图 | 订单创建、支付、取消接口 |
| router/alipay.py | 蓝图 | 支付宝支付回调处理 |
| libs/response.py | 工具 | 统一JSON响应 |
| libs/auth.py | 装饰器 | JWT登录态校验 |
| migrations/ | 脚本 | alembic数据库版本管理 |
| config/key/ | 证书 | 支付宝公钥与商户私钥 |
从表里能看到,这套源码把“路由—模型—业务”拆得非常清楚。实际项目里最怕的是把所有接口写在app.py一个文件里,而这里router目录用一个个py文件隔开,每个模块对应一组资源,后续扩展、多人协作都不容易冲突。grub.py和grub2.py这种以“数据装载”命名的辅助脚本,通常负责把外部商品文本灌入数据库,后面会专门说。
2.2 用户、商品与订单模型设计
model/user.py采用的写法很标准,是基于Flask-SQLAlchemy的。下面是我抽取出来的核心结构:
# model/user.py from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from model import db # db在model/__init__.py中统一实例化 class User(db.Model): __tablename__ = 'user' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, nullable=False, index=True) password_hash = db.Column(db.String(128), nullable=False) phone = db.Column(db.String(20), unique=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) def to_json(self): return { 'id': self.id, 'username': self.username, 'phone': self.phone, 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') }这里有两个细节值得关注。一是密码字段不存明文,用werkzeug.security做哈希,登录时再通过check_password校验;二是created_at用datetime.utcnow而不是datetime.now,避免服务器时区影响时间记录。to_json方法是API项目里非常常见的做法,ORM对象不直接序列化,而是先转成字典再交给jsonify,这样可以控制暴露字段,比如password_hash永远不出现在响应体里。
商品和分类模型之间是外键关系,订单与用户是一对多,订单与商品通过orderitem表形成多对多。源码中orderitem.py应该包含order_id、commodity_id、price、quantity这些字段。price单独存在订单明细里是电商必须的设计,因为商品价格会变,下单那一刻的价格不能被商品表后续改动影响,否则对账时会出现金额对不上的问题。
2.3 数据库迁移与初始化流程
项目里带了一套migrations目录和alembic.ini,这是用Flask-Migrate管理表结构变更的。拿到源码后,只要Python依赖装好,按下面三步就能把库建出来:
pip install -r requirements.txt flask db upgrade python manage.py runserver迁移脚本里已经有了一堆versions下的文件,所以不需要再执行flask db init。那些b87a0779521b_.py之类的文件就是已经生成的迁移版本,数据库会按顺序执行。如果没有执行flask db upgrade就直接跑应用,会出现“No such table”的SQLAlchemy报错。
如果要把默认的SQLite换成MySQL,需要改config/settings.py里的连接串。常见做法是保留一个默认值,再通过环境变量覆盖:
# config/settings.py import os BASE_DIR = os.path.abspath(os.path.dirname(__file__)) class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret' SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_DATABASE_URI = os.environ.get( 'DATABASE_URI', 'sqlite:///' + os.path.join(BASE_DIR, 'shopping.db') )参数说明:SECRET_KEY用于加密session和JWT,生产环境一定不要用默认值;DATABASE_URI通过环境变量注入,例如mysql+pymysql://root:pass@localhost/shopping,就能从SQLite平滑切到MySQL。大部分刚接触Flask的开发者会在这一步被坑——改了连接串但没装pymysql驱动,启动时直接抛ModuleNotFoundError。
3. 路由蓝图与响应规范:API接口层的实现套路
Flask路由层是用户直接接触的部分,这套API的router目录下按资源划分了蓝图,libs里封装了统一响应和错误码。下面从三个角度拆接口层。
3.1 蓝图划分与路由注册
蓝图的好处是同一个应用里可以按业务域拆分路由文件,避免app.py越来越重。从router/下的文件名看,接口前缀大概是这样划分的:
| 蓝图文件 | URL前缀 | 主要接口 |
|---|---|---|
| router/user.py | /api/user | register、login、info |
| router/commodity.py | /api/commodity | list、detail、search |
| router/shopcar.py | /api/cart | add、update、delete、list |
| router/order.py | /api/order | create、pay、cancel、status |
| router/alipay.py | /api/alipay | notify、return |
在入口文件里注册蓝图时,应该像下面这样设置url_prefix,让蓝图内部路由只写相对路径,可读性更强:
# app.py from flask import Flask from router.user import user_bp from router.commodity import commodity_bp app = Flask(__name__) app.config.from_object('config.settings.Config') def register_blueprints(): app.register_blueprint(user_bp, url_prefix='/api/user') app.register_blueprint(commodity_bp, url_prefix='/api/commodity') register_blueprints()url_prefix为/api/user代表蓝图里的@user_bp.route('/login')最终对外暴露的路径是/api/user/login。这种挂载方式让接口文档天然好生成,也方便统一加版本前缀。如果后续要升级到v2,只需要再挂一次url_prefix,原路由不破坏。项目里router/alipay.py单独一个蓝图,也是为了让支付回调的路径不会被业务路由挤占。
3.2 统一响应体与错误码
从libs/response.py的命名看,这套API采用了“业务状态码+数据”的响应规范。接口返回的基本形态是:
# libs/response.py from flask import jsonify def success(data=None, code=0, message='ok'): return jsonify({ 'code': code, 'message': message, 'data': data }) def fail(code=400, message='error', data=None): return jsonify({ 'code': code, 'message': message, 'data': data })使用统一响应后,前端只需要解析code字段就能判断请求结果。我见过很多Flask项目,有人返回字符串,有人返回dict,还有人直接render_template,前端对接非常痛苦。这个源码的做法值得借鉴:每个路由都返回success或fail包装过的JSON,再配合libs/error_code.py里的错误码常量,排查问题直接看code就可以定位到模块。
下面是一份常用错误码分布表:
| code | 含义 | 典型场景 |
|---|---|---|
| 0 | 请求成功 | 正常数据返回 |
| 400 | 参数错误 | 缺少必填参数、参数格式错误 |
| 401 | 未登录或登录失效 | token缺失、token过期 |
| 403 | 无权限 | 普通用户操作管理员接口 |
| 500 | 服务端异常 | 数据库连接失败或未知异常 |
设计错误码时要注意,不要把HTTP状态码和业务code混为一谈。HTTP 200可以带业务code 400,HTTP 401也可以带业务code 401。很多新手习惯把失败响应的HTTP状态码也改成400或500,导致Nginx或网关侧日志无法按真实状态码统计,后期排查会比较麻烦。
3.3 登录态鉴权与token校验
router/user.py里的登录接口通常会签一个token返回给前端,后续请求把token放进Authorization头。libs/auth.py里封装的login_required装饰器,是Flask API项目的标准做法:
# libs/auth.py from functools import wraps from flask import request, g, current_app import jwt def login_required(f): @wraps(f) def wrapper(*args, **kwargs): token = request.headers.get('Authorization', '') if token.startswith('Bearer '): token = token[7:] if not token: return fail(code=401, message='未登录') try: payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) g.user_id = payload['uid'] except jwt.ExpiredSignatureError: return fail(code=401, message='登录过期') except jwt.InvalidTokenError: return fail(code=401, message='无效token') return f(*args, **kwargs) return wrapper参数说明:Authorization头的标准格式是“Bearer + 空格 + token”,token在jwt.decode时使用的密钥必须和登录时一致,算法一般选HS256。g.user_id存到Flask的g对象里,视图函数里可以直接读取。这里最关键的坑是,不要自己去base64解码token再判断用户,jwt是带签名校验的,只解不验容易被伪造。
4. 从加购到支付:购物车、订单和支付宝对接链路
这一章把业务主链路串起来看:商品列表、购物车操作、订单创建、支付对接和数据导入。
4.1 商品查询与购物车数据操作
先看商品列表,router/commodity.py里最常见的写法是分页加过滤参数:
@commodity_bp.route('') def list_commodity(): page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 20, type=int) category_id = request.args.get('category_id', type=int) query = Commodity.query if category_id: query = query.filter_by(category_id=category_id) pagination = query.paginate(page=page, per_page=per_page, error_out=False) data = { 'items': [c.to_json() for c in pagination.items], 'total': pagination.total, 'page': page, 'per_page': per_page } return success(data=data)page和per_page从URL的query string读取,type=int做强制类型转换。error_out=False非常关键,它让页码超出范围时返回空列表而不是抛404,前端拿到空数组后可以自己判断是否显示“没有更多数据”。如果希望接口更健壮,建议把per_page限制到1到100之间,避免有人传入超大数字拖垮数据库。
购物车表一般是user_id、commodity_id、quantity的组合,加购操作要判断商品是否存在、数量是否合法。下面是加购接口的常用逻辑:
@cart_bp.route('/add', methods=['POST']) @login_required def add_to_cart(): commodity_id = request.json.get('commodity_id') quantity = request.json.get('quantity', 1) commodity = Commodity.query.get(commodity_id) if not commodity: return fail(code=400, message='商品不存在') if quantity <= 0 or quantity > 99: return fail(code=400, message='数量必须在1-99之间') cart = ShopCar.query.filter_by( user_id=g.user_id, commodity_id=commodity_id ).first() if cart: cart.quantity += quantity else: cart = ShopCar(user_id=g.user_id, commodity_id=commodity_id, quantity=quantity) db.session.add(cart) db.session.commit() return success(message='已加入购物车')加上@login_required之后,g.user_id就是当前登录用户。同一商品重复加购时,这里没有新增记录而是累加数量,避免购物车出现重复行。如果你在复现时希望前端能直接改数量,应该用update接口而不是把quantity直接覆盖为目标值,否则并发操作容易互相覆盖。
4.2 创建订单时的事务控制
创建订单是这套API里最容易写出脏数据的地方,因为要同时操作订单表、订单明细表、商品库存和购物车记录。源码里order.py的流程大致是:
@order_bp.route('/create', methods=['POST']) @login_required def create_order(): user = User.query.get(g.user_id) cart_items = ShopCar.query.filter_by(user_id=user.id, checked=True).all() if not cart_items: return fail(code=400, message='没有选中的商品') order = Order(user_id=user.id, status='unpaid', total_amount=0) db.session.add(order) db.session.flush() # 只有flush之后 order.id 才会生成 total = 0 order_items = [] for cart in cart_items: commodity = Commodity.query.get(cart.commodity_id) if commodity.stock < cart.quantity: db.session.rollback() return fail(code=400, message=f'{commodity.title} 库存不足') commodity.stock -= cart.quantity total += commodity.price * cart.quantity order_items.append(OrderItem( order_id=order.id, commodity_id=commodity.id, price=commodity.price, quantity=cart.quantity )) db.session.delete(cart) order.total_amount = total db.session.add_all(order_items) db.session.commit() return success(data={'order_id': order.id, 'total_amount': total})这里最容易忽略的是db.session.flush(),没有这一行,order.id还是None,后面订单明细的外键会直接报错。另一个关键点是循环里发现库存不足时调用db.session.rollback(),把整个事务回滚,否则可能出现“订单没创建成功,但前面几件商品库存已经扣减”的脏数据。事务边界就是一次请求里要么全部成功提交,要么一件事都不发生。
订单创建后要维护status字段,这套源码的状态流转一般如下:
| status | 含义 | 可跳转状态 |
|---|---|---|
| unpaid | 待支付 | paid / cancelled |
| paid | 已支付 | shipped / refunding |
| shipped | 已发货 | completed / refunding |
| completed | 已完成 | - |
| cancelled | 已取消 | - |
接口层通常只允许有限的状态迁移,比如只有unpaid状态的订单才能被取消,paid之后必须走退款流程。如果直接把status改成任意值,后续对账和库存回滚都会失控。
4.3 支付宝支付接入与密钥配置
源码包config/key下放了两个PEM文件:app_private_key.pem是商户自己的应用私钥,用于生成签名;alipay_public_key.pem是支付宝公钥,用于验证支付宝异步通知。很多人会把这两个文件放反,导致pay接口能发起,但回调验签始终失败。
对接支付时的常见做法是生成一个支付链接让前端跳转,核心步骤大致如下:
from alipay import AliPay alipay = AliPay( appid=current_app.config['ALIPAY_APP_ID'], app_notify_url=current_app.config['ALIPAY_NOTIFY_URL'], app_private_key_string=open(current_app.config['APP_PRIVATE_KEY_PATH']).read(), alipay_public_key_string=open(current_app.config['ALIPAY_PUBLIC_KEY_PATH']).read(), sign_type='RSA2', ) order_string = alipay.api_alipay_trade_page_pay( out_trade_no=order_no, total_amount=total_amount, subject='购物平台订单', return_url=current_app.config['ALIPAY_RETURN_URL'], ) pay_url = 'https://openapi.alipay.com/gateway.do?' + order_string生产环境网关一定不能写成沙箱地址,沙箱环境要用openapi.alipaydev.com。app_notify_url是服务端异步通知地址,必须公网可访问,否则支付成功后支付宝回调进不来,订单会一直停在等待支付状态。生产环境这里不能用localhost,至少需要一个内网穿透或真实域名。
支付成功以后,回调里要重新校验金额、订单号、签名,全部通过才把订单状态从unpaid改成paid。只依赖前端跳转返回是不可靠的,因为用户可能支付成功后直接关掉了浏览器,异步通知才是最终对账依据。
4.4 用京东商品.txt批量导入初始化数据
源码里京东商品.txt通常是从电商页面采集下来的商品信息,配合grub.py或grub2.py用来初始化数据库。如果手动录入商品,几十个分类会让人崩溃,我一般会写一个不到100行的导入脚本:
# grub2.py from model import db, Commodity, Category def import_from_txt(path): with open(path, encoding='utf-8') as f: for line in f: line = line.strip() if not line: continue parts = line.split('\t') # 按实际文件分隔符调整 if len(parts) < 2: continue title, price = parts[0], float(parts[1]) category_name = parts[2] if len(parts) > 2 else '默认分类' category = Category.query.filter_by(name=category_name).first() if not category: category = Category(name=category_name) db.session.add(category) db.session.flush() db.session.add(Commodity(title=title, price=price, category_id=category.id)) db.session.commit()这个脚本有几个容易踩的细节。文件编码不一定都是UTF-8,很多中文文本是GBK,如果读出来乱码,把encoding改成'gbk'。分隔符也不一定是\t,打开文件看第一行再确认split()的参数。如果已经用alembic建好了表,运行一次脚本,再调商品列表接口就能看到数据。
5. 启动部署避坑:让Flask购物API真正能被调用
前面把模型、路由和业务链路都拆完了,最后聊一下我从源码到跑通服务时遇到的实际问题和验证方法。这套源码的入口是app.py或manage.py,依赖装好后不要直接双击app.py,很多问题出在环境变量和密钥路径上。
第一坑:密钥文件路径。config/settings.py里如果只用相对路径定位PEM证书,在项目根目录启动没问题,但用gunicorn启动且工作目录不对时,open()证书会抛FileNotFoundError。建议在配置里基于BASE_DIR生成绝对路径,启动前先打印确认文件存在。第二坑:数据库表没建出来。必须执行flask db upgrade,只有迁移执行过,后续import model才不会出现NoSuchTableError。第三坑:跨域访问。如果前端是独立域名的Vue项目,需要支持OPTIONS预检和跨域响应头,Flask里可以用flask-cors扩展解决,别自己拼JSON返回。
启动方式上,我推荐先用开发服务器验证业务,再用生产服务器接管:
python manage.py runserver --host 0.0.0.0 --port 5000 flask --app app.py routesflask routes命令会把所有已注册的路由完整输出,包括HTTP方法和endpoint。第一次拿到这套源码时,我会用这个命令确认哪些接口真正挂载成功,再逐一用curl测试:
curl -X POST http://127.0.0.1:5000/api/user/login \ -H "Content-Type: application/json" \ -d '{"username":"demo","password":"123456"}'拿到token以后,后续请求加上Authorization头再测购物车和订单接口。我习惯把每个接口的响应体都统一成success/fail结构,前端只有在code=0时才解析data,其他情况直接弹message,这样对账和排查都会非常省力。
如果要上线,把服务切到Gunicorn,比如gunicorn -w 4 -b 0.0.0.0:5000 manage:app,配合Nginx反代静态文件和API请求。数据库层面给commodity表的category_id和title加索引,商品列表页的查询会快不少。购物车和订单表的数据量大以后记得按用户维度做分表,先把开发环境的接口全部跑通,再考虑这些优化不迟。
本文还有配套的精品资源,点击获取