做了大半年植物绿植盆景销售商城从最开始只有一个模糊的想法到最后跑通支付、上线运营我踩过的坑和最终沉淀的方案今天一次性整理出来。这个项目用的是python flask vue这套组合技术栈不算新但胜在轻量、可控、落地快。很多人一上来就纠结要不要上微服务、加消息队列实际做一个垂直品类的商城系统flask 加 vue 完全够用而且维护成本低到让人感动。先交代一下背景这个商城面向的是一二线城市喜欢绿植和盆景的年轻用户核心功能包括商品展示、搜索筛选、购物车、订单管理、后台商品管理、会员系统以及盆景植物特有的养护信息展示。前端是 vue3 element plus后端是 flask sqlite/MySQL整套系统前后端分离本地可跑服务器部署也简单。1. 项目概述与整体设计思路1.1 核心需求拆解植物绿植盆景这个品类和普通电商有个明显的差异点用户下单前非常依赖图片和养护说明。买一盆龟背竹用户想知道这盆有多大、冠幅多少、是不是带盆发货、到手怎么换盆这些都是商城系统里必须承载的信息维度。所以我在设计时把商品模型拆得比普通电商更细商品基础信息名称、分类、价格、库存、主图商品规格盆径、株高、冠幅、带盆/裸盆养护信息光照需求、浇水频率、适养温度、难度等级图文详情多图展示、快递包装说明前台功能围绕浏览-决策-下单-售后这条链路来设计后台则聚焦商品管理-库存扣减-订单处理-用户管理。权限上分普通用户和管理员两种角色用 JWT 做身份认证前端路由守卫控制页面访问。1.2 为什么选 flask 而不是 Django 或 Spring Boot我个人的原则是项目复杂度决定技术选型而不是技术热度。这个商城系统虽然功能看着不少但本质上还是 CRUD 加少量业务逻辑。flask 的轻量在这里体现得淋漓尽致起步快一个 main.py 就能把项目跑起来不需要像 django 那样先建 project 再建 appORM 用 SQLAlchemy 灵活度很高关联查询和字段扩展都不费劲蓝图Blueprint机制可以做到模块化商品、订单、用户各一个模块后期好维护周边生态成熟flask-cors、flask-jwt-extended、flask-sqlalchemy 都能解决实际问题当然 flask 也有短板比如默认不支持异步、模板渲染性能一般但前后端分离之后这些都不是问题。真正遇到性能瓶颈的地方用 nginx 做反向代理加 CDN 缓存图片就能扛过去。1.3 前端为什么选 vue3vue3 的组合式 APIComposition API对商城这种模块复用度高的场景太友好了。购物车状态、用户登录态、商品筛选条件这些逻辑抽取成自定义 hook 之后组件之间几乎不需要互相传参代码可读性和可维护性明显提升。我实际开发中的体会是vue3 的响应式系统比 vue2 更精确组件更新粒度更细页面渲染商品列表的时候不会出现多余的重绘。另外 element plus 组件库成熟度高后台管理界面可以直接拖组件拼出来省了大量手写样式的功夫。2. 技术选型与架构设计2.1 前后端分离的数据流设计整个系统的数据流我画在脑子里长这样前端 vue 发出 axios 请求经过 nginx 代理到 flask 后端flask 用蓝图分发到对应模块处理完通过 SQLAlchemy 操作数据库再把 JSON 返回给前端渲染。关键是所有接口都统一返回格式避免前端处理各种异常结构。我定义的标准返回结构是这样的{ code: 200, message: success, data: {} }错误时 code 返回 400/401/403/500前端 axios 拦截器统一判断 code如果不是 200 就弹 ElMessage 提示。这样写的好处是前端不需要每个接口都写一遍错误处理维护起来很清爽。2.2 项目目录结构项目我分成 backend 和 frontend 两个目录完全独立开发和部署。plant-mall/ ├── backend/ │ ├── app.py # 应用入口Flask 实例创建 │ ├── config.py # 配置文件数据库连接、密钥等 │ ├── models/ # SQLAlchemy 数据模型 │ │ ├── user.py │ │ ├── product.py │ │ ├── order.py │ │ └── cart.py │ ├── api/ # 蓝图模块 │ │ ├── auth.py # 登录注册 │ │ ├── product.py # 商品接口 │ │ ├── cart.py # 购物车接口 │ │ ├── order.py # 订单接口 │ │ └── admin.py # 后台管理接口 │ ├── utils/ # 工具函数JWT 解析、文件上传等 │ └── requirements.txt ├── frontend/ │ ├── src/ │ │ ├── api/ # axios 封装和接口定义 │ │ ├── view/ # 页面组件 │ │ ├── router/ # 路由配置 │ │ ├── store/ # Pinia 状态管理 │ │ ├── utils/ # 通用工具 │ │ └── App.vue │ └── package.json └── README.md这个结构是我反复调整后定下来的。一开始把路由和模型全堆在 app.py 里还是能跑但到后期加功能的时候改动一个文件就要连带看几百行代码排查问题的效率非常低。后来拆成蓝图模块每个功能域一个文件新增接口或者改字段都清晰很多。2.3 技术栈版本选择依赖版本锁得很死方便团队协作复现环境。技术组件版本选择理由Python3.10兼容性好各类依赖都能装Flask2.3.x稳定版本社区资料多SQLAlchemy2.0.xORM 功能完善支持现代语法Vue3.4.x组合式 API 成熟稳定Element Plus2.x组件丰富后台开发效率高Vite5.x开发热更新快构建配置简单比起那些一上来就追求最新版本的做法我更倾向于选稳定版本再加 lock 文件。python 的 requirements.txt 和前端 package.json 都锁定具体版本号确保换个电脑拉下来就能跑不会出现依赖冲突这种问题。3. 数据库设计与核心模型3.1 核心表结构分析商城系统的数据库设计核心在用户、商品、购物车、订单四张表再加上分类表和订单项表六张表构成完整闭环。用户表 user字段设计id主键username用户名password_hash密码哈希email / phonerole用户角色user/adminstatus是否禁用created_at密码字段必须存哈希值我用的是 werkzeug 自带的 generate_password_hash默认 pbkdf2 算法不要自己用 md5 去写。这个坑很多入门教程都没提实际项目中明文存密码是致命错误。商品表 product是信息密度最大的表id、name、category_idprice价格用 Decimal 而非 Floatstock库存main_image主图URLimages轮播图JSON数组description详情富文本light_requirement光照需求water_frequency浇水频率difficulty养护难度pot_diameter / plant_height规格参数价格用 Decimal 是因为 Float 在涉及折扣、满减这些运算的时候会有精度丢失比如 29.9 可能存成 29.899999。虽然商城金额一般不大但账目出错就是事故必须从一开始就用 Decimal。购物车表和订单表也很好理解购物车关联 user_id 和 product_id带上数量和选中状态。订单表则复杂一些需要包含订单号、总金额、状态、收货信息快照。3.2 为什么订单要存收货信息快照这里我想特别说一个设计细节订单表里不光存 user_id还要直接冗余一份收货人、电话、地址的文本快照。原因很朴素——用户下单后可能修改地址如果你只在用户表里关联一个地址 ID历史订单的收货信息就会跟着变。植物盆景这种客单价较高的商品售后纠纷本来就多到时候你连用户当时填的地址是什么都说不清楚。所以我在 order 表里直接存了 address_snapshot 字段下单时把地址 JSON 序列化存进去。多占一点存储换来的是订单记录不可变的历史可追溯性。3.3 SQLAlchemy 模型实现模型定义我贴一下关键代码方便直接参考from datetime import datetime from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash db SQLAlchemy() class User(db.Model): __tablename__ user id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(80), uniqueTrue, nullableFalse) password_hash db.Column(db.String(200), nullableFalse) role db.Column(db.String(20), defaultuser) created_at db.Column(db.DateTime, defaultdatetime.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) class Product(db.Model): __tablename__ product id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(120), nullableFalse) category_id db.Column(db.Integer, db.ForeignKey(category.id)) price db.Column(db.Numeric(10, 2), nullableFalse) stock db.Column(db.Integer, default0) main_image db.Column(db.String(255)) images db.Column(db.Text) # JSON 数组 description db.Column(db.Text) light_requirement db.Column(db.String(50)) water_frequency db.Column(db.String(50)) is_on_sale db.Column(db.Boolean, defaultTrue) created_at db.Column(db.DateTime, defaultdatetime.utcnow)建表时需要注意 MySQL 的字符集如果用的是 sqlite 则不用操心。我本地开发用 sqlite部署到服务器切到 MySQLSQLAlchemy 的抽象层让这个切换几乎是零成本的改一下连接字符串就行。3.4 分类与标签设计植物绿植的分类比较垂直比如观叶植物、多肉植物、盆景、鲜花、园艺资材。但如果只做单级分类用户在找耐阴的大型绿植时会很痛苦。所以我在分类基础上加了一层标签tag商品可以打多个标签首页按标签做推荐。分类表 category 和标签表 tag 都是简单的主键加名称结构商品和标签之间建立多对多关系用关联表维护。这个设计投入成本很低但对前端的筛选体验提升非常大。4. 核心功能实现与实操细节4.1 用户认证与 JWT 持久化用户登录这块我用 flask-jwt-extended 来做思路是用户提交用户名密码后端校验通过后生成 access_token短期和 refresh_token长期前端把 token 保存在 localStorage 或 Pinia 中axios 请求拦截器在 header 里带 Authorization: Bearer后端每个需要登录的接口用 jwt_required() 装饰器保护直接上代码from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity app.post(/api/auth/login) def login(): data request.get_json() user User.query.filter_by(usernamedata.get(username)).first() if not user or not user.check_password(data.get(password)): return {code: 401, message: 用户名或密码错误}, 401 access_token create_access_token(identitystr(user.id)) refresh_token create_refresh_token(identitystr(user.id)) return { code: 200, data: { access_token: access_token, refresh_token: refresh_token, username: user.username, role: user.role } }这里要注意一个点token 过期时间不能设太长。本地开发为了方便设了 7 天上线前必须改成 2 小时以内不然用户 token 泄漏了别人可以持续操作你的账号。Refresh token 设 7 天前端 axios 拦截 401 后自动用 refresh_token 换新 access_token。4.2 商品列表与搜索筛选商品列表是全站访问量最大的接口一定要关注查询效率。前端传 category、keyword、price_range、sort 这些参数后端用 SQLAlchemy 动态拼接查询条件def get_product_list(): query Product.query # 关键词搜索模糊匹配名称和描述 keyword request.args.get(keyword) if keyword: like_pattern f%{keyword}% query query.filter(db.or_( Product.name.like(like_pattern), Product.description.like(like_pattern) )) # 分类筛选 category_id request.args.get(category_id) if category_id: query query.filter(Product.category_id category_id) # 价格区间 min_price request.args.get(min_price) max_price request.args.get(max_price) if min_price: query query.filter(Product.price Decimal(min_price)) if max_price: query query.filter(Product.price Decimal(max_price)) # 排序 sort request.args.get(sort, new) if sort price_asc: query query.order_by(Product.price.asc()) elif sort price_desc: query query.order_by(Product.price.desc()) else: query query.order_by(Product.created_at.desc()) products query.all() return {code: 200, data: [product.to_dict() for product in products]}这类接口性能上要注意的是避免 N1 查询关联分类和标签时要 join 出来不要循环里再查数据库。我实际测试 200 个商品足够覆盖中小型盆景商城场景SQLite 下响应时间在 100ms 左右完全健康。4.3 购物车与订单流程购物车接口是典型的前后端协同逻辑。前端维护购物车列表和选中状态下单时把选中的商品项传给后端后端统一从库里查最新价格和库存。创建订单的接口要拆成两个步骤来保证数据一致性校验库存检查商品是否还有货库存不足直接提示扣减库存创建订单的同时把商品库存减掉这里需要加事务保护try: order Order(...) db.session.add(order) for item in items: product Product.query.filter_by(iditem.product_id).with_for_update().first() if not product or product.stock item.quantity: raise Exception(f商品 {product.name} 库存不足) product.stock - item.quantity order_item OrderItem(...) db.session.add(order_item) db.session.commit() except Exception as e: db.session.rollback() return {code: 400, message: str(e)}, 400with_for_update() 是行级锁防止并发下单导致超卖。虽然商城规模短期到不了这个量级但学会这个写法是必须的等出了问题再补课就迟了。订单状态我用一个状态字段来流转待付款、已付款/待发货、已发货、已完成、已取消。后台管理员可以改状态前端按状态渲染不同的操作按钮。4.4 后台管理模块后台管理我单独做了一套页面和接口路由前缀是 /admin。功能包括商品管理上架、下架、编辑库存价格、批量操作订单管理查看订单列表、更改订单状态用户管理查看注册用户、禁用异常账号数据统计销售额、订单量、热销商品排行前端后台路由统一用 meta.requiresAdmin 做权限标记全局路由守卫里判断用户 role 是否为 admin不是就直接跳首页。后端接口再做一层 jwt 验证和 role 校验双保险。4.5 图片上传与附件路径处理绿植商城图多每一件商品至少 4-6 张实拍图。图片处理是整个项目里最容易出问题的地方我前后调整了三次方案才稳定下来。最终方案是前端用 element plus 的 Upload 组件拖拽上传后端接收图片文件用 uuid 重命名按日期分目录存储返回可访问的 URL 给前端存储入库后端处理的坑主要集中在路径拼接和静态文件映射。flask 默认只能访问 static 目录下的文件我自定义了上传目录后必须在 app.py 添加路由from flask import send_from_directory import os UPLOAD_FOLDER os.path.join(os.path.dirname(__file__), uploads) app.route(/uploads/path:filename) def uploaded_file(filename): return send_from_directory(UPLOAD_FOLDER, filename)部署到服务器后最容易踩的坑是附件路径不对我遇到过 nginx 配置没指向 uploads 目录导致图片 404。排查方式很简单先在本地 python main.py 起来看图片能不能访问能访问说明 flask 层正常不能用才去找 nginx 的原因。逐步缩小排查范围是最高效的方式。5. 前后端联调与部署上线5.1 跨域问题处理前后端分离开发时前端跑在 localhost:5173后端跑在 localhost:5000端口不同就会产生跨域请求浏览器默认拦截。解决方式用 flask-cors 开启全部跨域from flask_cors import CORS CORS(app, resources{r/*: {origins: *}})本地开发这么配没问题部署到生产环境时必须收敛只允许你自己的前端域名跨域。不然任何第三方网站都能向你的接口发请求方便是方便了安全就没了。5.2 Vite 代理与构建配置前端开发环境我推荐用 Vite 代理而不是依赖后端 CORS代理配置写在 vite.config.js 里export default defineConfig({ server: { proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } })这样前端代码里所有请求都写 /api/xxx不用写完整的后端地址。发布构建时 npm run build 生成静态文件到 dist 目录nginx 直接托管 dist 即可。5.3 Nginx 部署配置生产环境我用 nginx 同时托管前端静态文件和转发后端 APIserver { listen 80; server_name plantmall.example.com; # 前端静态文件 root /var/www/plant-mall/dist; index index.html; # 刷新页面时路由重定向到 index.html location / { try_files $uri $uri/ /index.html; } # 后端 API 反向代理 location /api/ { proxy_pass http://127.0.0.1:5000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传的图片 location /uploads/ { alias /var/www/plant-mall/backend/uploads/; } }try_files 是前端路由刷新不报 404 的关键。vue3 的 history 模式刷新页面时浏览器会请求 /product/1 这个路径后端没有这个路由nginx 就会尝试找对应的静态文件找不到就返回 index.html由前端路由接管渲染。5.4 Flask 生产环境运行很多人直接把 flask app.run() 跑在生产环境这是不对的。app.run() 自带的是开发服务器性能和安全性都不适合生产。我的做法是 gunicorn 启动gunicorn -w 4 -b 127.0.0.1:5000 app:app4 个 worker 进程能充分利用多核 CPU。再设置 systemd 服务管理进程这样服务器重启后 flask 服务也能自动起来。6. 常见问题与踩坑记录6.1 数据库连接与编码问题用 SQLite 本地开发完全顺畅但切到 MySQL 后遇到了两个问题一是表的字符集默认不是 utf8mb4插入 emoji 表情或者特殊字符会报错。建表时要明确指定CREATE DATABASE plant_mall CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;二是 MySQL 8.0 默认的认证插件是 caching_sha2_passwordSQLAlchemy 连接需要加指定。我干脆用 pymysql 驱动连接字符串写成SQLALCHEMY_DATABASE_URI mysqlpymysql://username:passwordlocalhost:3306/plant_mall?charsetutf8mb46.2 时间字段的时区问题flask 的 DateTime 默认用 datetime.utcnow存的是 UTC 时间。前端显示订单时间时直接展示会出现 8 小时的时差。解决方案有两种一是在后端输出前统一转换成本地时间二是前端拿到时间字符串后加 JS 时区转换。我更推荐前者因为所有端都统一处理前端显示时不需要知道后端存的是什么时区。写个工具函数在字段序列化时调用 timezone.localtime() 转换输出。6.3 Vue 路由守卫与登录态持久化网上很多项目把登录态存在 localStorage 或 sessionStorage刷新页面还能保持。但我遇到的坑是用户改密码或管理员禁用账号后前端存的 token 仍然有效需要等 token 过期才能拦截。我在 /api/auth/info 接口里每次获取当前登录用户都会查库校验 status如果被禁用就返回 401。前端 axios 收到 401 后清除本地登录态并跳转登录页。这样用户被禁用的当下就能踢下线不用等 token 过期。6.4 flask 静态文件缓存前端构建后的 js/css 文件名带 hashnginx 可以做长缓存location /assets { expires 365d; add_header Cache-Control public, immutable; }这个优化对加载速度提升非常明显因为构建后的资源基本不会变一年缓存完全没问题。但要注意 index.html 不能缓存否则用户总是看到旧页面。我在 nginx 里对 index.html 设置 no-cache。6.5 植物信息展示的富文本与图片懒加载商品详情我用的是 quill 富文本编辑器后台编辑时直接插入图片和排版文字。渲染到前台时要注意 XSS 安全后端处理时对富文本做 sanitize过滤掉 script 标签。绿植商品图片多且大列表页和详情页的图片必须做懒加载。vue3 里我封装了一个 v-lazy 指令基于 IntersectionObserver 实现滚动到可视区域再加载图片。首屏加载的图片数量从 40 张降到 8 张左右页面打开速度直观提升。7. 搜索推荐与用户体验优化7.1 关键词搜索实现首页和商品列表页都接入了搜索框支持按商品名称和描述模糊搜索。用户搜索耐阴植物后端用 LIKE 语句模糊匹配。这个方案在一千条商品数据下性能没问题但数据量上到几万条时 LIKE %关键词% 因为不能走索引查询会明显变慢。后续优化方向是引入全文检索或者倒排索引方案但现阶段用 SQLAlchemy 的 like 查询足够满足小规模商城的实际需求。7.2 智能推荐逻辑因为商城用户基数不大用户行为数据还需要积累一开始就上协同过滤算法会因为没有数据而无法工作。我采用了更实用的基于规则的推荐方案相同分类下销量最高的商品推荐给浏览过该分类的用户相同养护难度如新手友好的商品放在对应商品详情页的推荐搭配区域用户加购多个商品后在购物车页面推荐同品类的关联商品这种方案虽然算法含量不高但好处是冷启动快、解释性强、实现成本低。等积累了足够多的订单和浏览数据后再引入协同过滤算法会更合适。7.3 移动端适配现在逛商城的人一大半是从手机进来的前端在 PC 和移动端都要适配。我的做法分三步全局使用 rem 布局Apple 和 Android 机型下都能自适应Element Plus 的栅格系统做响应式断点PC 端一行 4 个商品手机上自动变 2 列关键按钮加入购物车、立即购买固定到移动端底部方便单手操作移动端测试时可以用浏览器的 device mode 模拟常见机型但建议真机过一遍很多布局问题和滑动体验必须真机才能发现。8. 项目部署后的运维经验与后续扩展8.1 日常运营中的性能观察商城跑起来之后我每天关注三个指标接口响应时间、数据库连接数、图片带宽占用。目前单台 2 核 4G 的服务器扛住这个体量没有任何问题。接口平均响应 100ms 左右数据库查询没有明显瓶颈。图片是主要带宽消耗一张商品图压缩到 200KB 以内之后一晚上整站带宽消耗也就在 5GB 左右。8.2 数据备份策略之前吃过一次亏手动改数据库时把某个商品分类下的图片路径批量写错了导致十来个商品图片全部挂掉。从那以后我固定每天凌晨用 cron 任务备份数据库保留最近 14 天的备份文件。sqlite 直接复制文件MySQL 用 mysqldump操作都很简单关键是要真正执行不能只在文档里写了计划。8.3 后续扩展方向商城跑顺之后下一步我打算加几个功能盆栽养护日历提醒用户购买后按植物类型推送浇水、施肥提醒增加用户粘性批发采购模式照顾线下花店和园艺工作室的批量下单需求微信小程序端复用现有 API只写一套前端整体上这个项目的架构在很长一段时间内都不会过时因为核心的轻量化设计原则不会变。在做这个项目的过程中我自己最大的体会是稳扎稳打比炫技更重要。真实运营一个商城系统稳定性、可维护性和可迭代性永远是第一位的。
阅读完成 · 觉得有帮助?