先说结论:这套东西做出来不难,难在组装机配置器的规则设计,以及小程序端和 Python 后端之间的数据衔接。我前后迭代了两个版本,第一版把配置器做成了纯静态表单,结果用户选完 CPU 和主板,根本不关注兼容性,订单里一堆物理上装不到一起的配置,售后直接炸了。第二版才把兼容性校验、电源功耗估算和价格预算联动全部收进后端逻辑里。这篇文章把我整个设计思路、核心代码骨架、踩过的坑和排查过程完整复盘一遍,希望对正在做类似小程序商城的朋友有实际帮助。
1. 整体设计与思路拆解
1.1 为什么选 uniapp + Python,而不是原生小程序或纯前端
先说技术选型。微信小程序原生开发当然能做商城,但如果你只有一套前端代码,后续还想上支付宝小程序、抖音小程序甚至 App,原生开发就意味着每个端重写一遍。uniapp 的 Vue 语法和 uni-app 编译器帮我省掉了这部分重复劳动,一套pages目录、一套组件库,打包到微信小程序只需要在 manifest 里配置好mp-weixin的 appid。缺点是遇到微信特有的 API 时得查兼容表,比如登录、支付、订阅消息,这些在 uniapp 里都有对应的uni.login、uni.requestPayment,但个别参数和原生写法不完全一致,需要留意。
后端用 Python 的原因更直接:生态成熟、写业务接口快,尤其是商城这种 CRUD 密集、规则校验复杂的场景。Flask 和 Django 我都试过,最终用了 Flask + SQLAlchemy。Django 自带 Admin 后台确实省事,但定制化程度高以后反而被框架的 ORM 和中间件绑住手脚。Flask 轻量,路由、蓝图、SQLAlchemy 模型全部自己掌控,配合 JWT 做登录态,写起来非常顺手。Python 3.8 以上版本,虚拟环境配合requirements.txt,部署到云服务器上也没遇到什么坑。
这个项目的核心模块就三块:商城基础功能(商品、分类、购物车、订单)、用户体系(微信登录、手机号绑定、收货地址)、组装机配置器(核心增值模块)。这三个模块之间不是各自孤立的:配置器选完的配件会组合成一个"配置单",配置单可以直接加入购物车生成订单,订单又回写库存。所以后端接口设计必须从一开始就预留好嵌套资源的关系。
1.2 两个关键设计决策:配置器规则后端化,商品数据双表结构
第一个核心决策是把配置器的兼容性规则放到后端,而不是写死在小程序端。很多人做配置器喜欢把所有配件数据拉下来,在前端 JS 里做筛选和校验,看似响应快,但很快就失控:前端代码越来越胖、规则越加越多、发布新配件还得重新发版小程序。我把配件兼容性表、电源功耗表、预算匹配算法全部用 Python 实现,小程序端只负责展示选项和收集用户选择,每次变更配置单就调一次后端接口,返回校验结果和推荐组合。实测下来一次请求也就几十毫秒,用户无明显感知,但维护成本直线下降。
第二个决策是商品表拆成"基础商品表"和"配件参数表"。普通商城商品字段就那些:标题、价格、库存、图片、详情。但电脑配件不一样,CPU 有插槽类型、核心数、功耗,主板有芯片组、内存插槽数、板型,显卡有长度、供电接口。如果全部塞进一个商品表,字段爆炸且数据冗余严重。我用了主表products存通用字段,副表part_params用 JSON 字段存各品类特有规格。这样商城列表页只查主表,配置器详情页再按品类取参数,性能和数据维护都很舒服。
2. 小程序端核心模块拆解
2.1 微信登录、手机号获取与 token 链路
微信小程序登录是商城绕不开的第一关。uniapp 里直接调uni.login拿code,然后传给后端/api/auth/login,后端拿 code 去微信的code2Session接口换openid。这里有个小坑:uni.login拿到的 code 有效期只有 5 分钟,而且只能用一次,所以前端拿到后必须立即传给后端,不能本地存着等用户操作完再发。
手机号获取这块,微信官方现在推荐用button的open-type="getPhoneNumber"来触发授权。uniapp 里写法是:
<button open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">微信一键登录</button>然后在getPhoneNumber回调里拿到e.detail.code,这是手机号换取的临时凭证,后端需要拿这个 code 再配合 access_token 去微信接口换真实手机号。注意:这个 code 和登录的 code 不是同一个东西,不能混用,而且只有在用户点击授权按钮时才会返回,没法主动触发。
登录态的维护我用 JWT。后端登录接口成功后返回token和refreshToken,小程序端存到uni.setStorageSync,每次请求在拦截器里加上Authorization: Bearer <token>。拦截器我放在request.js里统一封装,所有接口请求都走这一个封装,这样 401 时统一跳登录页或者静默刷新,不用每个页面单独处理。
2.2 商品列表、搜索与购物车的事务一致性
商品列表采用分页加载,小程序端用onReachBottom触底加载下一页。后端接口设计成GET /api/products?category_id=&keyword=&page=1&page_size=10,返回{list, total, has_more}。这里强烈建议后端算好has_more,前端不用自己去判断page*size >= total,因为列表过程中商品可能下架,total会变,依赖前端判断容易漏加载或重复加载。
购物车这里有个很多人忽视的坑:购物车里的商品价格和商品详情页当前价格可能不一致。用户加购时 5999,过了两天商家改价 6299,用户在购物车看到的应该是哪个价?我们采用的是加购时锁定价格快照,在购物车展示快照价格,结算下单时后台再校验一次当前价格,如果有差异提示用户"价格已变动,请重新确认"。虽然实现上多一个价格快照字段和校验逻辑,但避免了大量价格纠纷。
加入购物车和下单过程中的库存扣减,我只用了一个容错机制:下单时不是扣减stock字段,而是用 SQL 的原子更新UPDATE products SET stock = stock - 1 WHERE id = ? AND stock > 0,然后检查受影响行数,如果为 0 说明库存不足,直接返回失败。这比"先查库存再扣库存"要安全得多,能有效避免并发超卖。理论上高并发商城需要 Redis 预扣库存,但这种量级的校园/小规模商城,SQL 原子更新已经完全够用。
2.3 组装机配置器的 UI 与交互逻辑
配置器是这个小程序最核心的交互模块。我把页面设计成左侧品类导航(CPU、主板、内存、显卡、存储、电源、机箱),右侧是当前品类下的配件列表,顶部是当前已选配置总价和兼容性状态条。选完一个品类自动跳到下一个品类,用户也可以手动切回修改。整体参考了各大电商 DIY 装机页的交互习惯,用户学习成本低。
配件列表默认按价格排序,但用户也经常需要按"热门""性能"排序。我做了三个排序维度:价格升序、价格降序、销量降序。每个配件的卡片上直接展示关键参数标签,比如 CPU 的"8核16线程"、显卡的"12GB 显存",这样用户不用进详情页也能快速筛掉明显不合适的选项。
配置单的"加入购物车"按钮会把整套配置作为一个组合商品加入购物车。这里前端处理很简单:拼接一个config_id,后端拿到这个config_id根据配置单计算总价并生成订单。这样用户后续在订单详情里能看到完整的配件明细,方便售后和核对。
3. Python 后端设计与核心接口实现
3.1 Flask 蓝图结构与数据库模型设计
后端目录结构我按功能模块拆分成蓝图:
backend/ ├─ app.py # 入口,注册蓝图 ├─ config.py # 配置:数据库、JWT密钥、微信参数 ├─ models/ │ ├─ user.py # 用户表 │ ├─ product.py # 商品表、配件参数表 │ ├─ cart.py # 购物车表 │ └─ order.py # 订单表、订单项表 ├─ apis/ │ ├─ auth.py # 登录/手机号/Token刷新 │ ├─ product.py # 商品列表/详情/搜索 │ ├─ configurator.py # 配置器规则/方案推荐 │ ├─ cart.py # 购物车接口 │ └─ order.py # 订单接口 └─ utils/ ├─ jwt_helper.py # JWT生成与校验 └─ wx_helper.py # 微信接口封装实际模型设计里,products表长这样(省略部分字段):
class Product(db.Model): __tablename__ = 'products' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(128), nullable=False) category = db.Column(db.String(32), nullable=False) # cpu/motherboard/memory/gpu/storage/psu/case price = db.Column(db.Numeric(10, 2), nullable=False) stock = db.Column(db.Integer, nullable=False, default=0) sales = db.Column(db.Integer, nullable=False, default=0) image = db.Column(db.String(256)) detail = db.Column(db.Text) params = db.Column(db.JSON) # 各品类特有参数 status = db.Column(db.SmallInteger, default=1) # 1上架 0下架 created_at = db.Column(db.DateTime, default=datetime.utcnow)params字段用 JSON 存特有规格,比如 CPU 存{"socket":"AM4","cores":8,"threads":16,"tdp":105},主板存{"socket":"AM4","chipset":"B550","memory_type":"DDR4","memory_slots":4,"form_factor":"ATX"}。这样后端校验时直接用params.get('socket')就能比对,非常方便。
用户、购物车、订单表不再赘述,核心关系就是user 1->n cart、user 1->n order、order 1->n order_item。每个order_item关联商品 ID 或配置单 ID,并冗余一份商品名称、价格、参数快照。
3.2 配置器兼容性校验与功耗估算逻辑
这是整个项目里最有含金量的部分。兼容性校验我维护了一套硬规则字典:
COMPAT_RULES = { 'cpu_motherboard': { 'check': lambda cpu, mb: cpu.params.get('socket') == mb.params.get('socket'), 'message': 'CPU 与主板插槽类型不匹配' }, 'motherboard_memory': { 'check': lambda mb, mem: mb.params.get('memory_type') == mem.params.get('memory_type'), 'message': '主板不支持该内存类型' }, 'psu_power': { 'check': lambda parts, psu: sum( int(part.params.get('tdp', part.params.get('power_consumption', 0))) for part in parts ) * 1.5 <= int(psu.params.get('watts', 0)), 'message': '电源功率不足以支撑整机功耗' } }注意功耗估算这里我用了一个经验系数 1.5:所有配件满载功耗之和乘以 1.5,再和电源额定功率比较。为什么是 1.5?因为实际使用中电源长期工作在 60%~80% 负载效率最好,且 CPU 和显卡瞬时功耗可能超出 TDP,留出 50% 余量相对安全。比如 CPU TDP 105W + 显卡 250W + 其他 80W,合计 435W,乘以 1.5 得 652W,那就直接推荐 650W 以上的电源。
兼容性校验接口设计成POST /api/configurator/check,前端把已选配件 ID 列表传过来,后端返回{ok: true}或{ok: false, errors: ["CPU 与主板插槽类型不匹配"]}。用户每次选完一个配件就调一次,即时反馈,交互体验非常流畅。
预算联动这块,我用了一个很朴素的推荐算法:用户输入总预算后,按 CPU 30%、显卡 30%、主板 15%、内存 10%、存储 10%、电源 5%、机箱 5% 的比例分配预算,然后每个品类在预算范围内推荐销量最高且兼容的配件。这个比例不是绝对的,实际跑下来如果 CPU 加显卡占比太高导致电源不够,会启用二轮调整:把电源预算上调,同时从 CPU 或显卡那边砍一点。迭代两次基本能给出一个令人满意的整机方案。
3.3 JWT 认证与微信接口调用封装
JWT 这块我直接用PyJWT库,生成 token 时把user_id和过期时间写进 payload,密钥放在config.py里。小程序端每次请求通过请求头带上 token,后端用工具函数解析并挂到g.user_id上。
def login_required(f): @wraps(f) def wrapper(*args, **kwargs): token = request.headers.get('Authorization', '').replace('Bearer ', '') try: payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) g.user_id = payload['user_id'] except (jwt.ExpiredSignatureError, jwt.InvalidTokenError): return jsonify({'code': 401, 'message': '登录已过期'}), 401 return f(*args, **kwargs) return wrapper微信接口调用封装在utils/wx_helper.py里,包括code2Session和getPhoneNumber两个核心方法。都用了requests库,并且加了超时和异常捕获,避免微信接口偶发超时导致整个请求卡死。
def code2_session(code): url = 'https://api.weixin.qq.com/sns/jscode2session' params = { 'appid': config.WX_APPID, 'secret': config.WX_SECRET, 'js_code': code, 'grant_type': 'authorization_code' } try: resp = requests.get(url, params=params, timeout=5) return resp.json() except requests.RequestException: return None这里有个实际经验:微信接口返回的openid和session_key,session_key千万不要下发到小程序端,它涉及后续解密手机号等敏感操作,留在后端使用完就丢弃或加密存储。安全底线不能破。
4. 实操经验与踩坑记录
4.1 微信登录返回 code 后如何处理 401 和 token 刷新
我在实际调试中最常遇到的问题就是"用户登录后又报 401"。原因通常是 token 过期了,但前端拿到的登录 code 又已经用过,没法直接再换 token。我们的处理方案是后端提供刷新接口POST /api/auth/refresh,前端拦截器遇到 401 时先调刷新接口换新 token,如果刷新也失败才跳登录页。
刷新接口的逻辑:前端存了一个refreshToken(有效期 30 天),后端收到后先校验 refreshToken 是否有效,有效则派发新的 token 和 refreshToken,旧的立即作废。这里注意,make sure 每次刷新后新旧 refreshToken 都要在数据库里做标记,防止旧 token 被恶意重放。
在这个项目里刷新 token 的实现比较简单,直接在 JWT payload 加一个token_type: 'access'或'refresh',access 有效期 2 小时,refresh 有效期 30 天。校验时区分类型,refresh 接口只接收 refresh 类型的 token。
4.2 uniapp 打包小程序时的 source size 超限问题
热词里提到的source size 2612kb exceed max limit 2mb是微信小程序主包 2MB 上限的经典报错。我第一次打包时把静态图片、数据库说明文档、多余的组件全塞在项目里,主包直接超了。解决思路是微信的分包加载:
{ "pages": [ "pages/index/index", "pages/product/detail", "pages/cart/cart" ], "subPackages": [ { "root": "pagesConfigurator", "pages": [ "pages/configurator/configurator" ] } ] }配置器这个核心模块单独拆进分包,因为用户只有进入配置器页面才会加载相关代码。实测主包从 2.6MB 降到 1.4MB,完美通过。其他常用的优化手段包括:小图片转 base64 或改用云存储 URL、删除unpackage目录后重新打包、easycom按需引入组件等。
4.3 开发调试:为什么 uni.request 请求一直报错
另一个高频问题是uni.request在微信开发者工具里请求本地 Flask 服务失败。原因基本都是微信开发者工具默认不校验合法域名,但本地调试需要在开发者工具"详情-本地设置"里勾选"不校验合法域名...”。有时候本地请求显示ERR_CONNECTION_REFUSED,还要检查 Flask 是否监听在0.0.0.0而不是127.0.0.1,因为微信开发者工具模拟器里访问localhost可能指向了开发者工具自身的环境。
小程序上线之前还要在微信公众平台配置 request 合法域名,必须是 HTTPS 且已备案的域名。我们直接把后端接口域名加到白名单,同时配置了 SSL 证书。如果后端接口里还有http://的图片资源,也要一并加进 downloadFile 合法域名,否则图片加载不出来。
4.4 下单流程中容易踩到的库存和幂等问题
订单模块还有一个隐藏坑:重复提交。用户手速快,点了两次"提交订单",后端就会创建两笔订单,库存也扣两次。我做了两层防护:前端按钮加 loading 禁用,后端根据request_id(前端生成的幂等键)加唯一索引,提交订单时如果该request_id已存在就直接返回已创建的订单,不再重复创建。
库存扣减除了前面说的原子更新,还要配合订单状态机:创建订单后 15 分钟未支付自动取消、释放库存。取消订单这一步同样要避免并发问题,所以我用事务包裹"查询订单状态 -> 修改状态 -> 回补库存",并对相关行加with_for_update()行锁。实测并发场景下不会出现超卖或库存错乱。
5. 常见问题排查速查表
| 现象 | 原因 | 解决方案 |
|---|---|---|
source size 2612kb exceed max limit | 主包体积超 2MB | 分包加载、压缩图片、删除无用文件 |
uni.login报invalid code | code 过期或重复使用 | 拿到 code 立即传给后端,确保一个 code 只用一次 |
| 真机预览请求失败 | 未配置合法域名 | 在公众平台配置 request/downloadFile 域名,必须 HTTPS |
| 手机号授权回调没有数据 | button 没有使用 open-type,或用户拒绝 | 使用open-type="getPhoneNumber",引导用户手动点击 |
| 价格不一致引发售后 | 改价后购物车仍显示旧价 | 加购时价格快照,结算时再次校验并提示 |
| 配置器选完主板后 CPU 选项没变化 | 兼容性规则只在前端写死 | 改为后端动态校验,每次选择请求 /check 接口 |
| 小程序包名不匹配 | manifest 里 appid 错误 | 检查 manifest.json 中 mp-weixin 的 appid 是否与公众平台一致 |
| 本地 Flask 接口请求超时 | 开发者工具跨域/监听地址不对 | 勾选"不校验合法域名",Flask 绑定 0.0.0.0 |
再补充一个运维层面的经验:Flask 的 debug 模式开启后虽然会自动重载代码,但并发能力很弱,我上线用 gunicorn 起了 4 个 worker,部署后用curl测了/api/products接口的响应时间在 30ms 以内。数据库用的 MySQL,连接池配置在 SQLAlchemy 的pool_size=10, max_overflow=20,避免高并发下频繁创建连接。
6. 配置器算法与方案推荐的实操逻辑
6.1 预算分配的计算示例
拿一个真实案例演示:用户输入总预算 8000 元想配一台带显示器的主机。按默认比例分配,CPU 预算是 8000 * 0.3 = 2400,显卡预算是 2400,主板 1200,内存 800,存储 800,电源 400,机箱 400。每个品类在预算范围内取销量最高且兼容的配件。
假设 CPU 选出 i5-13490F(约 1400 元),显卡选出 RTX 4060 Ti(约 2800 元)——这里显卡就超预算了。这时候算法启用溢出调整:先把显卡预算上调,从 CPU、机箱的预算里各匀 10%。如果调整后 CPU 的预算只剩 1300,那就只能选 i5-12400F。这个调整过程和选型结果一起返回给前端,前端展示"为您节省了 100 元预算空间"之类的提示。
这个算法虽然简单,但对于非专业用户已经足够贴心:他只需要输入一个总预算,系统自动给出一套兼容、均衡、可下单的配置方案。后续如果要提升,可以接入真实行情价格和用户评测数据做多目标优化,但对于第一版,跑通业务闭环才是重点。
6.2 为什么把功耗估算系数设为 1.5
前面提过功耗估算的 1.5 倍系数,这里展开讲一下推导逻辑。CPU 和 GPU 的 TDP 只是热设计功耗,并不代表实际最大功耗。例如 i9-13900K 的 TDP 标称 125W,但满载 PL2 状态下能冲到 250W 以上。显卡同样如此,RTX 4090 标称功耗 450W,瞬时峰值能到 600W。所以"所有 TDP 求和 + 50% 余量"是为了保证电源在瞬时峰值时不触发保护断电。
但也要注意系数不能设太高,否则会推荐超大功率电源,整机成本显著上升。1.5 这个值是我在对比多个装机论坛配置单之后取的一个经验中位数。如果配的是 i5 + 4060,这套整机 300W 左右,1.5 倍后 450W,推荐 500W 电源,完全合理;如果是 i7 + 4070 Ti 级别,功耗 500W 左右,1.5 倍后 750W,推荐 750W 或 850W,也符合主流配置单的选择。
6.3 配置单持久化和"快速复购"功能
用户选好一套配置后,除了加购下单,我们还支持保存配置单。这个功能用起来很简单:前端调POST /api/configurator/save把整个配置 ID 列表传过来,后端存进config_plans表,用户在"我的配置"里可以看到历史配置单,一键重新加购。
这个功能在运营上意外地成了一个很好的拉回流手段:很多用户会先在预算内配好两三套方案,慢慢对比,过几天再回来下单。所以配置单的保存和恢复一定要设计好,不能只是一个前端变量,而是真正持久化到数据库。表格结构大概是id, user_id, name, parts(JSON), total_price, created_at。
7. 一点实操感悟
这个项目从立项到第一版上线,我大概花了两周半时间。前端 uniapp 的页面开发很顺利,真正耗时间的是兼容性规则梳理和下单流程的边界情况处理。很多细节是写完代码测试时才暴露的:比如加购后改价,比如并发扣库存,比如用户选完配置未登录就点保存。
如果再让我重做一次,我会在一开始就把配置器规则表做成后台可配置的,而不是写在 Python 代码里。现在规则虽然在后端,但新增一个配件类型时还得改代码、重新部署。如果当初设计成数据库表驱动,运营同事自己就能维护规则,开发量会少很多——这是我踩过的最大的架构层面的坑,写出来给大家避雷。
还有一个小技巧分享:小程序端的接口请求封装,在开发阶段可以加一个"mock 模式",当后端未启动时自动返回本地 mock 数据。这样前端开发和后端开发可以完全并行,不用互相等待。我用的方案是在request.js里判断全局变量USE_MOCK,为 true 时走本地模拟数据,调试完直接关掉,非常省心。这个习惯帮我至少节省了两天联调等待时间。