资讯详情

基于Flask+Vue的商城系统实战:从优惠券核销到可视化看板

📅 2026/10/18 11:03:06 | 华诺云谱 👁 阅读
基于Flask+Vue的商城系统实战:从优惠券核销到可视化看板
1. 脱胎于课程设计的商城项目它到底在干什么先说个背景。我当时拿到这个题目的时候原话是基于Flask的网上商城购物系统带优惠券、可视化、商家端、Vue前端跑在PyCharm里。这种描述看起来啰嗦其实信息量足够这是一个典型的前后端分离的Web应用目标用户有三类——普通消费者、商家、平台管理员。作为一个练手项目它比单纯的CRUD要进阶一点因为牵扯到订单状态流转、券核销、数据聚合、权限隔离这些真实业务场景。很多同学的第一反应是又是一个电商系统但真动手之后才发现难度不是写在增删改查里而是藏在那些需求没说清楚的细节里。1.1 这个系统解决的真实场景如果只做一个商品列表加购物车那和上课的demo没什么区别。这个项目真正要解决的问题是让商家能自己管商品和订单让用户能领券下单让平台能看整体经营数据。你拆开看就三条线用户线注册登录 → 浏览商品 → 加入购物车 → 结算并使用优惠券 → 下单 → 查看订单状态 → 确认收货。商家线商家登录 → 维护商品 → 处理订单发货 → 创建优惠券活动 → 查看自己店铺的销售看板。平台或管理员线审核商家、管理全量数据通常放在后台模块里。我把这套逻辑画成一张心智地图所有功能都围绕交易展开优惠券是交易前的营销工具可视化是交易后的数据沉淀而商家端则是把平台的一部分运营权限下放。搞清楚这个脉络后面建表、写接口就不会乱。1.2 拆解核心角色与功能地图建议第一步先定义角色而不是先写代码。这个项目里角色至少有三种角色核心权限典型操作用户浏览、下单、使用优惠券注册、登录、购物车、结算、订单商家只管理自己的商品和订单商品上架、订单发货、创建优惠券、看统计管理员全平台管理商家审核、分类管理、全局数据看板一个很常见的错误是一开始就用一张user表把所有角色塞进去。我当时试过只加一个is_staff字段结果后面商家接口全都要判断这个字段越写越乱。更合理的做法是用user表存公共信息昵称、账号、密码、创建时间再用profile表存角色信息和关联字段或者用user_type加关联外键。这个项目里我采用的是user表 商家info表的方式用户没有额外字段商家有shop_name、description、audit_status等字段。这样商家相关的查询逻辑非常清晰所有涉及到商家的表只要在SQL里join一下商家info表就可以了。2. 用FlaskVue做商城为什么不是Django全家桶很多人看到标题里有Flask又有django包括pycharm会非常困惑。其实很简单这个系统的后端框架用的是Flask前端是Vue.js而PyCharm只是开发IDE。Django在标题里大概率是另一个被对比过但没被选中的选项很多需求文档会把技术选型里的备选方案也写进题目。我当时也纠结过后来选了Flask完全是基于项目体量和可控性。2.1 两个Python框架的真实差异Django的最大优势是全家桶自带Admin后台、ORM、认证体系、表单校验一套下来几乎能把后台管理功能白拿。但是它的耦合度也高模型一旦跑起来迁移逻辑就绑定了Django的机制。对一个时间有限的课设或练手项目来说Django确实能快速出活但也容易让人陷入什么都能点却不知道原理的状态。Flask则完全不同它只是一个核心骨架数据库要用SQLAlchemy序列化要用Marshmallow登录认证要自己搭JWT每个能力都是选装组件。这种手动拼装的代价是前期配置工作量多一点但好处极其明显你写的每一行代码都清楚自己在解决什么问题。所以如果你希望做完这个项目之后能跟面试官讲清楚优惠券的核销流程是怎么实现的可视化数据是怎么聚合出来的Flask反而是更好的选择。2.2 前后端分离的开发节奏这个项目本身是前后端分离的Flask提供JSON APIVue负责页面渲染。前端通过axios调用后端接口中间涉及到跨域、Token鉴权、状态管理等问题。我当时开发时定的规矩是这样的后端只返回JSON不做任何模板渲染前端用Vue Router管理页面路由用Vuex或者现在的Pinia管理用户登录态和购物车数据。这样两边可以并行开发只要事先约定好接口文档比如POST /api/orders创建订单GET /api/merchant/stats获取统计两边对着文档联调就行。提示如果你不想被跨域折磨开发环境下直接给Vue配置proxy代理转发到Flask端口比在Flask里配CORS更省心。生产环境则建议用Nginx同域反代。3. 数据库建模六个表怎么把商城说清楚商城系统的数据库设计看起来简单但真正要落地的时候字段取舍特别容易犹豫。我当时的目标是能用最少的表把流程跑通同时数据不冗余。最终核心表定成了用户表、商家表、分类表、商品表、订单表、订单项表再加上优惠券表和优惠券领取记录表。优惠券本身是扩展模块可以先忽略先说这六张表。3.1 用户、商家、商品的关系陷阱用户表和商家表一定不能混。我见过有人直接在用户表上加role字段然后商家信息也用这张表存导致商品表的外键不知道该指向谁。正确做法是类似于user表id, username, password_hash, nickname, created_atmerchant表id, user_id (外键), shop_name, description, audit_status这样设计后一个用户最多对应一个商家商家是用户的扩展资料。商品表则通过merchant_id关联商家而不是直接关联user_id这样商品归属非常明确。商品分类表用category_id关联但这里要注意一个商品只属于一个分类属于最简单的单分类模型够用了。如果非要搞多分类那需要中间表复杂度会上升不少。订单表是整张关系网的中心order: id, order_no, user_id, merchant_id, total_amount, discount_amount, pay_amount, status, created_at, paid_at order_item: id, order_id, product_id, product_name, price, quantity, subtotal为什么order里要有merchant_id因为一个用户可能同时购买多个商家的商品如果订单表没有merchant_id商家端就不知道怎么过滤自己店铺的订单。我当时就是一开始没加后来接口写了一半又回去加字段还连带改了序列化器非常难受。订单项里冗余一份product_name和price也是同样的思路宁可少一次联表查询也要保证订单页面能在商品被删除的情况下仍然正常显示。3.2 订单字段与优惠券的关联方式优惠券设计我不想孤立的去讲因为它和订单关系非常深。优惠券表至少需要几类字段基本信息name, type满减券还是折扣券, threshold满多少可用, amount减多少, discount折扣率时间约束start_time, end_time归属约束merchant_id商家券如果是平台券则用0或null用户领券后会生成一张用户券记录表user_coupon里面有coupon_id, user_id, status可用/已用/过期, order_id核销时回填。这样设计的好处是券本身是模板用户券才是每个用户手里的具体资产。下单的时候加入购物车结算用户可以选择一张可用券请求后端结算接口时传递user_coupon_id后端计算出原价total_amount、优惠金额discount_amount最终得到pay_amount。订单创建成功后立刻把user_coupon的状态改成已用并把order_id回填进去。这个操作必须在同一个事务里完成否则就会出现订单创建了券却没标记核销或者券标记了订单却失败了的问题。3.3 给可视化留后路统计字段设计一开始做可视化时最容易想的就是用SQL实时group by但实际跑起来会发现订单量少时没问题一旦数据多了前端图表的响应速度会变得很慢而且像今日销售额这种指标还要区分时区很麻烦。我的做法是除了订单表保留原始数据外再建一张日统计表merchant_daily_stats: id, merchant_id, stat_date, order_count, total_sales, refund_count, refund_amount每天定时任务或者每次订单完成时累加更新。画折线图、柱状图的时候只需要按日期查询这张表查询速度极快而且代码逻辑也简单。对于课设体量的数据来说这属于提前做一点小优化但在答辩说明为什么建这张表时可以体现出你对统计报表性能的思考。4. 优惠券模块从发券到核销的关键细节优惠券是这个项目的一大亮点也是很多人做不完的环节。这里面的坑非常密集我分三步拆开讲数据结构、结算逻辑、并发防线。4.1 优惠券数据结构和状态机优惠券要区分配置和实例。配置是满100减20这种规则实例是用户张三领取了这张券这个行为。我之前见过有人直接在用户表上加一个coupon_ids字符串字段用逗号分割这种做法在应付答辩时也许能糊弄过去但稍微复杂一点的这张券有没有用过查询就变得很痛苦。状态机是我建议在文档里先画出来的部分可用(0) → 已用(1) 可用(0) → 过期(2) 每天凌晨扫描一个用户券记录的状态不能直接跳始终是这三个状态之一。后端接口里判断是否可用时的SQL条件要加上user_coupon.query.filter_by( user_idcurrent_user.id, statusavailable, iduser_coupon_id ).filter( coupon.end_time datetime.now() ).first()这样做的好处是即使前端不小心传了一个过期的券后端也能挡住。4.2 下单结算时优惠券的计算逻辑结算接口的输入参数是选择的商品列表或购物车id列表、用户券id、收货地址。后端要做的事情其实是三步计算商品原价总和。校验优惠券是否满足使用门槛。计算最终应付金额。这个逻辑听起来简单但要注意几个细节。首先是门槛判断满减券必须要求原价总额大于等于threshold而不是应付金额。然后是每单只能用一张券的规则如果有平台券和商家券的区分还需要更复杂一点但课设阶段先做成一单一券更稳妥。这里我贴一个核心计算片段供参考def calc_order_amount(items, user_coupon_id): original_total sum(item[price] * item[quantity] for item in items) discount 0 if user_coupon_id: uc UserCoupon.query.get(user_coupon_id) coupon uc.coupon if uc.status ! available or coupon.end_time datetime.now(): raise APIException(优惠券不可用) if original_total coupon.threshold: raise APIException(f满{coupon.threshold}元才可使用) if coupon.type full_reduction: discount coupon.amount elif coupon.type discount: discount original_total * (1 - coupon.discount) payable round(original_total - discount, 2) return original_total, discount, payable注意这里一定要用round处理浮点数否则可能出现99.999999这种结果。4.3 并发下单的坑超发与重复使用很多课设项目压根不考虑并发导致测试时用两个浏览器同时下单同一张券会出现一张券被用两次。原因很简单前端点了两次提交按钮两次请求都通过了券状态可用的检查然后你都insert了订单。要杜绝这个问题最优雅的方案是在数据库层加唯一约束或者在事务里加行锁。我在订单表里没有直接加约束而是在user_coupon表里设计了order_id字段并且在order_id上建立了唯一索引。当订单事务提交时会去UPDATE user_couponresult db.session.query(UserCoupon).filter( UserCoupon.id uc_id, UserCoupon.status available ).update({ status: used, order_id: new_order.id, used_time: datetime.now() }) if result 0: raise APIException(该优惠券已被使用)由于UPDATE会锁住这一行第二个请求如果也进来它要么看不到要么status已经不是available导致update的结果为0从而知道券已经被用了。这个方案简单可靠比单纯查询后再更新好得多。经验之谈优惠券模块答辩时最容易被问如果用户恶意刷券怎么办或者如果同一张券在两个订单同时使用怎么办。你只要把上述的状态校验行锁更新逻辑讲出来基本就算过关了。5. 商家端可视化别把图表做成摆设可视化是这个项目里最容易被做成花架子的部分。很多人的做法是前端拉一个ECharts随便放两三个图表看起来挺炫但数据是假的或者只是把所有订单堆在一起画图没有任何筛选条件。这个项目既然定位为商家可视化就必须从商家视角出发让商家真的能从中看到自己店铺的经营情况。5.1 商家看板需要哪些指标我最终做的看板分成了四块关键数字卡片今日销售额、今日订单数、待发货订单数、累计销售额。近7天销售趋势折线图按天统计订单金额。商品销量排行Top10条形图按商品销售量排名。近30天优惠券使用情况柱状图发券数、用券数、优惠金额。这些指标不是随便拍的。前三个解决的是我店卖得怎么样第四个解决的是我搞的营销活动有没有效果。课程的难度就在于既要会装图表框架也要会写统计SQL。5.2 后端聚合接口怎么写可视化接口的通用做法是用SQL聚合不需要在Python里一层层循环。比如近7天销售趋势records db.session.query( func.date(Order.paid_at).label(day), func.sum(Order.pay_amount).label(total_sales) ).filter( Order.merchant_id current_merchant.id, Order.status.in_([paid, shipped, completed]), Order.paid_at start_date ).group_by( func.date(Order.paid_at) ).all()这里有几个容易忽略的地方时间字段要用paid_at而不是created_at因为订单创建和被支付可能差几分钟统计销售额应按支付时间算。status要排除掉已取消的订单否则销售额会被虚增。如果某一天没有订单SQL聚合结果不会出现这一天前端会少一个点。建议在后端把7天的数据补全为零再返回给前端不然折线图会断线。补全零值可以用Python循环也可以用前端补我选择在后端处理比如date_list [(datetime.now().date() - timedelta(daysi)) for i in range(6, -1, -1)] result_map {record.day: float(record.total_sales) for record in records} full_data [{date: str(d), total_sales: result_map.get(d, 0)} for d in date_list]这样做出来的折线图每一天都有数据点没有任何空白。5.3 前端用ECharts渲染的注意点前端框架选的是Vue图表库用的ECharts。这里要提醒一下ECharts在Vue里用没有官方专用版也没关系可以直接引入echarts包然后在mounted里初始化import * as echarts from echarts; mounted() { this.chart echarts.init(this.$refs.salesChart); this.loadData(); }, methods: { async loadData() { const res await this.$http.get(/api/merchant/stats/sales_trend); this.chart.setOption({ xAxis: { type: category, data: res.data.map(item item.date) }, yAxis: { type: value }, series: [{ type: line, data: res.data.map(item item.total_sales) }] }); } }两个大坑图表容器必须有高度和宽度。很多人忘了给div写height结果图表渲染出来是空的。页面切换后再次进入要先myChart.dispose()再重新初始化否则会报dom has been removed之类的错误或者出现图表重叠。在做商品排行条形图时注意如果商品名过长xAxis的刻度标签会重叠可以把类目轴设置为数据值轴或者把背景图横向拉长。这些细节虽然小但直接影响演示效果。6. 权限、会话与跨域前后端联调最容易翻车的地方商城系统涉及的接口很多但权限模型并不复杂。难的是在前后端分离的架构下把登录状态和用户身份串起来同时处理好跨域。这一章讲的都是我实际运行时踩到的坑按顺序看能少走很多弯路。6.1 JWT还是Session选择JWT的理由很简单Flask如果做Session默认是依赖cookie和后端session_store的前端Vue跑在另一个端口时cookie的跨域问题会变得很麻烦。JWT的方式是用户登录成功后后端返回一个token字符串前端存在localStorage里之后每个请求都在请求头里带Authorization: Bearer token。用PyJWT在Flask里写个扩展不太复杂关键代码def create_token(user_id): payload { user_id: user_id, exp: datetime.utcnow() timedelta(days7) } return jwt.encode(payload, secret_key, algorithmHS256) def get_current_user(): token request.headers.get(Authorization) if token and token.startswith(Bearer ): payload jwt.decode(token.split( )[1], secret_key, algorithms[HS256]) return User.query.get(payload[user_id]) return None注意生产环境密钥一定要放配置里别写死。更值得注意的是JWT是无状态的颁发之后如果再修改用户角色或封禁用户JWT本身不会立刻失效所以如果只是demo项目就不用搞太复杂。有了这个token后端的登录保护就可以用装饰器def login_required(func): wraps(func) def wrapper(*args, **kwargs): user get_current_user() if not user: return jsonify({error: 未登录}), 401 g.user user return func(*args, **kwargs) return wrapper6.2 Flask-CORS与Vue代理的配合前后端分离开发时Flask跑5000端口Vue跑8080也许你用的是另外的端口。你会发现前端访问http://127.0.0.1:5000/api/...时浏览器会因为跨域限制而拦截响应。解决方法有两条用Flask-CORS扩展给后端的响应头添加Access-Control-Allow-Origin等字段。在Vue的vue.config.js里配置devServer代理devServer: { proxy: { /api: { target: http://127.0.0.1:5000, changeOrigin: true } } }这样前端请求/api/xxx相当于请求了Flask的/api/xxx浏览器以为同源不会拦。实际开发中这两条路我都碰过最终的体会是开发时用代理最省事但也要在Flask里用CORS兜底因为总免不了有时候直接打开一个HTML或Postman测接口。另外提醒一下用代理之后前端项目里的axios baseURL要设为空或者/api而不是写死http://127.0.0.1:5000。如果不小心写死开发代理就白配了。6.3 订单状态流转的接口约定商城系统的订单状态通常有待支付、已支付、待发货、已发货、已完成、已取消、已退款。我这个项目里保留了一条主线待支付(created) → 已支付(paid) → 待发货(paid) → 已发货(shipped) → 已完成(completed) ↓ 已取消(cancelled)商家端操作是发货和取消用户端操作是支付和确认收货。在设计接口时我建议用状态变更接口而不是直接更新字段merchant_bp.put(/orders/int:order_id/ship) merchant_login_required def ship_order(order_id): order Order.query.filter_by(idorder_id, merchant_idg.merchant.id).first() if not order or order.status ! paid: raise APIException(订单状态异常无法发货) order.status shipped order.shipped_at datetime.now() db.session.commit() return jsonify(order.to_dict())所有状态变更都检查当前状态是否等于可进入下一状态的前驱状态这就是状态机落地。如果让人随意改状态后期统计报表一定全乱了。我当时因为图省事直接让商家端可以任意修改订单状态导致一个已取消的订单还能被算进销售额里图表数字忽大忽小查了很久才发现是状态没控制住。7. PyCharm里跑通全流程的实测记录最后这部分是给马上要动手的人看的我记录的是在PyCharm里从头把项目跑起来的实测流程和踩过的坑。项目本身不难难的是环境一致性。7.1 虚拟环境与依赖锁版本我建议你在项目根目录建一个虚拟环境PyCharm里直接用Interpreter Settings创建即可。创建完马上执行pip install flask flask-sqlalchemy flask-marshmallow flask-cors pymysql PyJWT这里有个重要的建议不要用flask-sqlalchemy最新版它跟旧教程里的用法可能不兼容。我踩过的坑是有些教程使用db.relationship但新版本如果没配置backref行为会发生奇怪的变化。如果你跟我一样用的是3.0以下的版本可以正常这么写db SQLAlchemy(app)如果你装到了3.0.5初始化时不再推荐在模型类里直接使用db.Model以外的方式吗其实是兼容的但为了省时间我倾向于直接锁版本在requirements.txt里写死Flask2.2.5 Flask-SQLAlchemy2.5.1这里的版本组合经过了大量项目测试和Vue前端联调足够稳定。7.2 数据库迁移的常见报错如果用的是SQLite就没那么多迁移烦恼如果用的是MySQL要注意字符集和时区问题。我第一次用MySQL建库时的命令是CREATE DATABASE mall DEFAULT CHARACTER SET utf8mb4;如果一个不小心没设置字符集中文商品名存进去就有乱码风险。Flask应用连接MySQL也要配置下连接池和编码参数app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://root:password127.0.0.1:3306/mall?charsetutf8mb4另一个高频报错是Table xxx already exists。原因是你手动在数据库里建了表然后又执行db.create_all()两边定义不一致。我的建议是先做模型后建表全部交给db.create_all()如果运行中途要改字段宁可换掉SQLite数据库重新生成也别手工改表结构尤其是课设项目数据量不大重新初始化的成本很低。7.3 一个小建议先跑通最小闭环我的开发顺序是这样的先创建Flask项目写好app.py和config.py用模型定义用户表然后用db.create_all()生成表接着写一个注册接口和一个登录接口在Postman里验证能返回token再去写Vue的登录页把axios调通让页面能拿到token并保存。这个闭环通了之后再一个模块一个模块加商品管理、购物车、订单、优惠券、可视化。如果你一上来就把所有代码写满再启动报错会非常多分不清是Flask的错还是Vue的错。我的经验是小步快跑比一口气跑完可靠得多任何环境问题都在模块碰撞之前暴露出来后面反而是体力活。最后再说一句这个项目做完之后我最大的感触是优惠券和可视化这两个点非常值得花时间打磨。很多同学做的商城都停留在基本增删改查加一个优惠券模块和一套商家看板之后项目的完整度瞬间就上了一个台阶。答辩时演示可视化大屏再结合订单状态的流转整条业务闭环讲下来评委很难不认可你的工作量。你在实操过程中如果遇到口味独特的问题比如数据库连接失败、Vue路由白屏欢迎按这个思路排查先看后端日志再看网络面板最后检查数据格式大部分问题都能在十分钟内定位。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑