
去年做毕设选题目时我在校园二手交易和农产品商城这类烂大街的题目里翻了大半天最后定下 Python微信小程序基于Flask的私人定做订制订单发布与对应商品出售平台。原因很简单这个题目把两件难事凑在了一起——需求侧的用户发起定制和供给侧的商品出售再加上微信小程序和Flask这个固定搭配。做完之后回头看整趟下来踩得最深的坑不在代码而在业务状态设计。这篇文章就把我完整搭建这套系统的过程包括业务拆解、数据库设计、API实现、小程序端交互、以及上线部署时踩过的坑整体盘一遍。适合正在做类似毕设/课设或者想用Python Flask撑起一个微信小程序完整闭环的读者。1. 这到底是个什么平台业务模型先理清楚1.1 私人定制和普通电商的本质区别很多人一上来就写代码结果做着做着发现页面和接口对不上。原因在于没有先想清楚私人定制平台和普通电商平台根本是两种交易模型。普通电商是货找人。商品已经生产好用户浏览分类、搜索关键词、加入购物车、下单支付流程相对固定。私人定制是人找货。用户先描述需求商家再给出方案和报价中间有协商过程而且商品的规格、价格、交付时间都不是一开始就确定的。所以在我的系统里前台需要两条独立的交易链路定制链路用户发布带图文的定制需求商家看到后报价用户选择商家并支付定金商家制作用户确认收货后支付尾款。成品链路商家把已经做好的商品上架到商城用户直接浏览、下单、支付走标准电商流程。如果当初我把这两条链混在同一张订单表里不做区分后面接微信支付、做状态统计时绝对会乱成一锅粥。这也是这个题目最核心的设计分水岭。1.2 三种核心角色与完整交易链路平台一共三类角色普通用户、商家定制师/工坊、管理员。权限上我用了一个role字段直接区分小项目够用不需要引入独立的权限表。角色能做什么不能做什么普通用户发布定制需求、浏览商品、购买成品、对订单发起付款/收货/评价不能上架商品、不能处理他人订单商家报价定制需求、上传交付凭证、上架/下架成品、处理成品订单不能查看平台全量用户数据管理员管理用户状态、审核需求/商品、查看交易统计、处理投诉不能代替用户支付和确认收货一个完整的定制订单状态在我的系统里是这样流转的用户发布需求状态为待报价。商家提交报价需求状态变为已报价用户端能看到报价列表。用户选定一个报价并支付定金生成定制订单状态为待制作。商家开始制作并把过程进度或交付照片更新到订单状态为制作中。商家点击申请交付状态变为待验收。用户确认收货并支付尾款状态变为已完成。这套状态机看起来简单但真实现起来每一步都有可能出幺蛾子。比如用户付了定金但商家一直不接单怎么办用户确认收货前想申请退款怎么处理这些需要在状态设计阶段预留关闭订单退款中已关闭等旁路状态。我在1.0版本里只做了主链路导致后期补丁越打越多这是后话。1.3 为什么选择微信小程序作为落地端题目里虽然写了app但我最终采用微信小程序来承载前端。理由很实在微信小程序扫码即用不需要用户去应用商店下载安装对毕设和中小型项目来说审核速度远快于原生App上架。更关键的是wx.login天然解决了登录问题用户不需要输入手机号密码后端通过微信的code2session接口就能拿到openid。选小程序还有一个隐性好处Flask后端只需要提供JSON API与具体前端形态完全解耦。以后想把它套到原生Android/iOS壳里或者再做一个商户管理后台网页接口都能复用不用重写业务逻辑。2. Flask后端为什么够用技术选型的真实考量2.1 对比Django/Flask/FastAPI之后的选择开发前我其实在Flask和Django之间犹豫过。最终选Flask核心原因是掌控感。维度FlaskDjangoFastAPI学习曲线低一个文件就能跑起服务较高概念多中等异步概念有门槛数据库ORM需搭配SQLAlchemy自带ORM搭配SQLAlchemy可选生态成熟度老牌稳定稳定更新快但沉淀略少适合场景轻量API、中小业务大而全后台、内容管理高并发异步接口这个项目的数据量不大并发量也不高Flask足够。而且Flask的蓝图机制让我能按模块拆代码不会像单文件那样住着住着就变成面条代码。2.2 项目目录结构与核心依赖我的Flask项目目录长这样app/ __init__.py # 创建Flask app注册蓝图 config.py # 配置文件数据库、密钥、微信参数 extensions.py # 初始化SQLAlchemy、JWT、CORS等扩展 blueprints/ auth/ # 登录、注册、token刷新 user/ # 用户信息、地址管理 demand/ # 定制需求发布、报价 order/ # 订单创建、支付、确认收货 product/ # 商品管理、分类检索 admin/ # 后台统计、用户管理 models/ # SQLAlchemy模型 user.py demand.py product.py order.py services/ # 业务逻辑层微信接口、支付逻辑 wechat.py pay.py utils/ response.py # 统一返回格式 decorators.py # login_required角色校验装饰器 run.py # 启动入口依赖方面核心就是这几个Flask2.2.5 Flask-SQLAlchemy3.0.5 Flask-CORS4.0.0 PyJWT2.8.0 requests2.31.0 gunicorn21.2.0 mysqlclient2.2.0这里有个教训不要一上来就pip install flask装最新版。不同Flask版本对Werkzeug版本有隐式依赖装太新容易遇到路由校验报错。建议新建虚拟环境后一条条安装并锁定版本至少把requirements.txt固定下来部署时才能复现。2.3 会话与权限JWT还是Session微信小程序没有Cookie机制Flask自带的session需要适配所以我直接用JWT做登录态。用户通过wx.login拿到临时code请求后端登录接口后端去微信接口换openid再生成一个签名的token返回小程序。小程序每次请求在请求头带上Authorization: Bearer token。JWT的好处是无状态、好扩展坏处是token一旦签发很难主动吊销。对毕设和中小平台来说把token过期时间设置短一点比如2小时再配合后端在每次请求时校验用户状态已经够用。如果要强制用户下线可以攒一个token黑名单表但当时我没做因为项目里没有账号被管理员封禁后立即踢出的强诉求。核心代码很简单import jwt import time SECRET_KEY your-secret-key def generate_token(user_id): payload { user_id: user_id, exp: int(time.time()) 7200 } return jwt.encode(payload, SECRET_KEY, algorithmHS256) def parse_token(token): try: payload jwt.decode(token, SECRET_KEY, algorithms[HS256]) return payload[user_id] except jwt.ExpiredSignatureError: return None2.4 使用蓝图拆分模块Flask蓝图的本质是路由分组。我把注册逻辑统一放在app/__init__.py里from app.blueprints.auth import auth_bp from app.blueprints.demand import demand_bp from app.blueprints.order import order_bp from app.blueprints.product import product_bp app.register_blueprint(auth_bp, url_prefix/api/auth) app.register_blueprint(demand_bp, url_prefix/api/demands) app.register_blueprint(order_bp, url_prefix/api/orders) app.register_blueprint(product_bp, url_prefix/api/products)每个蓝图文件内部只定义路由业务逻辑放在services层。这样做的直接好处是后期加一个管理后台接口时不需要去改已有的商品路由文件新增一个admin_bp即可。对动辄几十个接口的微信小程序项目来说这个拆分习惯能省大量联调时间。3. 需求发布到订单履约核心数据模型设计3.1 用户、商家、管理员三张表用户表是系统的基础最大特点是openid必须唯一。我用role区分用户和商家没有单独建商家表。原因是一个自然人既可以是买家也可以是卖家拆成两张表反而麻烦。CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL UNIQUE, nickname VARCHAR(64), avatar_url VARCHAR(255), phone VARCHAR(20), role TINYINT DEFAULT 0, -- 0普通用户 1商家 2管理员 status TINYINT DEFAULT 1, -- 1正常 0冻结 created_at DATETIME, updated_at DATETIME );商家真正额外需要的资质信息比如经营类目、店铺介绍我放到了一张merchant_profile关联表里避免把大量冗余字段堆在user表中。3.2 定制需求表字段决定业务边界需求表是整个定制链路的起点。它的设计直接影响商家能不能高效报价。CREATE TABLE demand ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(100) NOT NULL, description TEXT, category_id INT, images JSON, -- 存储多张图片URL数组 budget_min DECIMAL(10,2), budget_max DECIMAL(10,2), deadline DATETIME, status TINYINT DEFAULT 0, -- 0待报价 1已报价 2制作中 3已完成 4已关闭 selected_quote_id INT, -- 用户选中的报价单ID created_at DATETIME, updated_at DATETIME );这里几个字段值得单独说images用JSON存URL数组而不是搞一张需求图片关联表。需求图片数量通常不会超过9张JSON最省事。如果以后要做图片维度检索再拆表迁移也不迟。budget_min和budget_max一定要分开存。很多新手喜欢存一个字符串300-500元结果后面做筛选和统计时都要解析字符串非常痛苦。status状态必须由后端统一更新小程序端展示的状态只是后端状态映射后的文本。防止用户通过改接口参数把待报价改成已完成。3.3 商品表与订单表的关联关系商品表相对常规CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, merchant_id INT NOT NULL, title VARCHAR(100), pictures JSON, detail TEXT, price DECIMAL(10,2), stock INT DEFAULT 0, sales INT DEFAULT 0, status TINYINT DEFAULT 1, -- 1上架 0下架 created_at DATETIME );订单表是系统里最重要的表。我特意加了type字段区分定制订单和普通商品订单并在order_no字段上建唯一索引CREATE TABLE order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, merchant_id INT NOT NULL, product_id INT, demand_id INT, type TINYINT, -- 0定制订单 1商品订单 total_amount DECIMAL(10,2), deposit_amount DECIMAL(10,2), -- 定制定金 balance_amount DECIMAL(10,2), -- 定制尾款 status TINYINT, -- 订单状态 pay_status TINYINT, -- 支付状态 delivery_company VARCHAR(50), tracking_no VARCHAR(64), created_at DATETIME, paid_at DATETIME, finished_at DATETIME );把两种订单放同一张表最大的好处是统计营收、订单数时只要一张表不会出现订单表和定做表两个口径对不上。缺点则是部分字段对某种订单类型为空这是可以接受的。4. 微信小程序端的关键页面与交互逻辑4.1 wx.login 登录的细节小程序端登录流程很多人以为只是调一个wx.login就完事实际上还需要注意时序问题。wx.login拿到的code有效期只有5分钟而且只能使用一次。正确流程是wx.login({ success: async (res) { const code res.code; const loginRes await wx.request({ url: https://yourdomain.com/api/auth/login, method: POST, data: { code } }); const { token } loginRes.data.data; wx.setStorageSync(token, token); } })后端拿到code后调用微信接口resp requests.get( https://api.weixin.qq.com/sns/jscode2session, params{ appid: appid, secret: secret, js_code: code, grant_type: authorization_code } ).json() openid resp.get(openid)这里容易踩的坑是code2session接口在开发工具和真机上有细微差别开发工具里可能自带了模拟登录导致你只需要配置一个测试openid一旦切到真机没有正确传appid和secret就会一直报invalid code。建议从一开始就准备两个环境配置开发环境用测试号正式环境用真实小程序appid。4.2 定制需求发布页与图片上传发布定制需求的页面上最大的技术点是图片上传。不能把用户选中的图片转成base64直接塞进接口请求体里那样请求体积会特别大而且Flask默认接收的数据大小有限制。我的做法是用户用wx.chooseMedia选择图片。小程序端先调wx.uploadFile把图片传到后端/api/upload。后端保存图片返回图片URL。小程序把URL数组和其他表单字段一起通过wx.request提交到/api/demands。这个顺序看起来多了两步但能保证需求发布接口不卡顿。上传接口需要限制文件格式和大小我后端只接收jpg/png/webp且单张不超过5MB。超过就直接拒绝否则一个用户传了十几张原图服务器磁盘很快就满了。4.3 列表加载更多与防止重复请求加载更多是微信小程序最常见的交互之一也是最容易写出重复请求的地方。核心思路是三个变量page、pageSize、isLoading。Page({ data: { demands: [], page: 1, pageSize: 10, hasMore: true, isLoading: false }, onReachBottom() { if (this.data.isLoading || !this.data.hasMore) return; this.loadDemands(); }, async loadDemands() { this.setData({ isLoading: true }); const res await wx.request({ url: /api/demands, data: { page: this.data.page, pageSize: this.data.pageSize } }); const list res.data.data.list; this.setData({ demands: this.data.demands.concat(list), page: this.data.page 1, hasMore: list.length this.data.pageSize, isLoading: false }); } })这里的isLoading锁非常关键。onReachBottom触发频率很高如果没有锁用户快速滑到底部会同时发出好几个相同请求数据就重复了。4.4 订单详情与支付状态管理支付是和小程序原生能力绑得最紧的部分。后端用统一下单接口拿到prepay_id后生成签名返回给小程序端小程序再调wx.requestPayment发起支付。我踩过的坑主要不在签名而在支付回调后的状态同步用户支付成功后微信服务器会异步通知后端但这不代表小程序端能立刻刷出已支付状态。最稳的做法是小程序端在wx.requestPayment成功回调中不直接改状态而是调用后端/api/orders/{orderNo}/refresh接口让后端根据微信支付查询接口返回的最终状态更新订单。同时为了保险订单详情页做一个3秒轮询最多轮询3次。如果还是未支付就提示用户稍后刷新。5. 后端API设计与关键实现5.1 RESTful接口约定微信小程序后端的接口设计我遵循最朴素的REST习惯路径清晰比什么都重要。统一返回结构是{ code: 0, msg: success, data: {} }code为0表示成功非0表示业务错误码。前端只需要在拦截器里判断一次code不用每个接口单独处理异常。鉴权使用请求头Authorization: Bearer token。我在Flask里写了一个装饰器def login_required(fn): wraps(fn) def wrapper(*args, **kwargs): auth request.headers.get(Authorization, ) token auth.replace(Bearer , ) user_id parse_token(token) if not user_id: return jsonify(code401, msg登录已过期, data{}), 401 g.user_id user_id return fn(*args, **kwargs) return wrapper5.2 发布定制需求接口实现这个接口的复杂点在于校验一堆表单字段 保存图片列表 初始化需求状态。demand_bp.route(, methods[POST]) login_required def create_demand(): data request.get_json() title data.get(title, ).strip() if not title or len(title) 5: return jsonify(code400, msg标题至少5个字, data{}) images data.get(images, []) if len(images) 9: return jsonify(code400, msg图片最多9张, data{}) demand Demand( user_idg.user_id, titletitle, descriptiondata.get(description), category_iddata.get(category_id), imagesjson.dumps(images, ensure_asciiFalse), budget_mindata.get(budget_min), budget_maxdata.get(budget_max), deadlinedata.get(deadline) ) db.session.add(demand) db.session.commit() return jsonify(code0, msg发布成功, data{demand_id: demand.id})这里我特意强调budget_min和budget_max要在前端和后端都做校验前端提示用户后端防止有人直接构造请求传负数。5.3 订单状态机与库存扣减的事务控制商品订单的创建和扣库存必须放在同一个事务里。很多教程会写先查询库存够则扣减但并发一上来就会超卖。正确做法是直接用一条带条件的UPDATE# 不推荐先查再改 # product Product.query.get(product_id) # if product.stock 0: # product.stock - 1 # 推荐条件更新受影响行数为0说明库存不足 result Product.query.filter_by( idproduct_id, status1 ).filter(Product.stock 1).update({ stock: Product.stock - 1 }) if result 0: return jsonify(code500, msg库存不足, data{}) db.session.commit()这相当于用数据库的行锁实现了乐观锁既不需要select for update也不需要额外加Redis锁非常适合Flask MySQL的中小项目。5.4 微信支付回调的幂等处理支付回调是后端最容易出问题的接口。微信支付成功后微信服务器会POST一份回调数据到我们配置的回调URL如果我们的接口响应超时或返回非SUCCESS微信会多次重试。所以回调处理必须保证幂等。我做了三件事先校验签名确保数据来自微信。通过out_trade_no商户订单号查订单。如果订单状态已经是已支付直接返回SUCCESS不做重复处理。pay_bp.route(/callback, methods[POST]) def pay_callback(): result parse_and_verify(request.data) if not result: return FAIL, 500 order_no result[out_trade_no] order Order.query.filter_by(order_noorder_no).first() if not order: return FAIL, 500 if order.pay_status 1: return SUCCESS order.pay_status 1 order.status paid order.paid_at datetime.now() db.session.commit() return SUCCESS如果这一步做漏了重复回调会导致订单状态被反复修改严重情况下会重复发货。6. 开发过程踩过的坑从环境到线上部署6.1 环境与依赖不要用最新版作死我一开始直接pip install flask装成了当时最新的Flask 2.3。结果在写路由时发现某条路由没法匹配带斜杠的路径查了半天是因为新版本对strict_slashes的行为变化。后来我锁定Flask2.2.5所有问题消失。另一个常见坑是python-mysqlclient在Windows上安装失败。解决办法是装pymysql并在__init__.py里加一句import pymysql pymysql.install_as_MySQLdb()这类环境问题看似和业务无关但很消磨斗志。建议直接用虚拟环境把依赖版本写死。6.2 跨域与小程序请求白名单开发阶段小程序请求http://127.0.0.1:5000会报跨域或不合法域名。我在Flask里挂了Flask-CORS允许本地调试跨域from flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})上线后必须在小程序管理后台把API域名加到request合法域名里而且域名必须支持HTTPS。真机调试时如果还没配置域名可以在开发者工具右上角勾选不校验合法域名但这只是权宜之计正式版必须配好。6.3 图片上传本地目录和OSS的选择开发环境图片直接保存在app/static/uploadsFlask访问静态文件很方便。上线后我用Nginx把/static路径映射到物理目录location /static/ { alias /home/project/app/static/; }如果你的项目图片量大强烈建议接入云OSS否则服务器磁盘吃紧是迟早的事。我的项目截止到答辩图片量不到1000张本地目录足够。不过如果要做二期我会换OSS因为本地文件备份和迁移都太麻烦。6.4 Flask部署Gunicorn Nginx部署时我用Gunicorn替代Flask自带的开发服务器。启动命令gunicorn -w 2 -b 127.0.0.1:8000 run:app然后用Nginx做反向代理location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }这里有个细节run:app中的run是run.py文件名app是里面创建的Flask实例。如果写错Gunicorn会直接起不来。还有-w 2不改一下的话进程管理容易出问题。我同时配了一个systemd服务保证服务器重启后Gunicorn能自动拉起。7. 项目复盘哪些设计值得保留哪些应该重构7.1 做得顺的地方整趟做下来我觉得最值得保留的设计是需求状态机和条件更新扣库存。这两块让我在答辩时能讲出条理也让前后端联调变得顺畅。因为状态机的每一步都有明确触发点和动作前端不会出现按钮点了没反应的模糊情况。条件更新扣库存则避免了我后期反复处理超卖问题代码干净利落。另外把services层独立出来的习惯在写微信支付回调时帮了大忙。如果所有逻辑都堆在蓝图路由函数里回调里既要解析微信xml又要查订单还要改状态一个函数几百行谁看着都头疼。7.2 如果有二期我会怎么改首先要补的是消息通知。现在商家报价后用户只能主动刷新需求详情页才能看到体验很差。我计划引入微信订阅消息用户发布需求时申请授权商家报价后给用户推一条订阅消息。这样的话平台的撮合感会强很多。其次要处理订单超时关闭。当前如果没有定时任务用户下单但长时间不支付订单会一直挂在待支付。我考虑用APScheduler加一个每分钟扫描的定时任务超过30分钟未支付就关单。虽然小项目用定时轮询很土但简单可靠。最后是权限细分。现在用户只要被管理员设置为role1就是商家缺少资质审核环节。二期应该加一张商家入驻申请表用户提交店铺名称、身份证照片、经营类目管理员审核通过后才开通商家权限。这也是从能跑走向敢用的关键一步。如果你正准备做同类型项目我建议你先把业务模型和订单状态机画在纸上再动手写代码。我最初在这上面省的时间后面十倍百倍地还了回去。把数据模型想清楚Flask和小程序端都只是实现细节。