客户说要做一个微信小程序版的电脑配件商城,我第一反应是:这不就是又一个商品列表加购物车的活儿吗?直到翻到需求文档里“组装机配置”那一栏,才意识到事情没这么简单。用户要在手机上像配电脑一样,一个一个选CPU、主板、内存、显卡,系统要实时算兼容性、算总价,还要能一键把整张配置单加入购物车下单。这个玩法,本质上不是卖配件,而是做一套轻量的“DIY装机工具”。
项目定了三件套:后端用 Python,前端用 uniapp,目标端是微信小程序。正好之前一期交付的是微信小程序源码工程(2048小游戏),这一期从休闲游戏跳到交易类商城,踩坑路径完全不一样。这篇文章就把整个项目的实现过程、技术选型逻辑和真实的坑位梳理一遍,给后面要做同类型商城或者“配置器+交易”结合场景的朋友做个参考。
1. 项目骨架:为什么后端选 Python,前端选 uniapp
1.1 后端用 Flask 而不是 Django 的真实理由
我们这个项目后端团队就两个人,一个写接口一个搞数据,交付周期三周。Django 的 admin 后台确实香,但它的 ORM、迁移、认证体系在项目前期反而是负担——我们要的是快速把接口跑起来,而不是一上来就套重型框架。所以后端选了 Flask 加 SQLAlchemy,配 SQLite 起步,后期数据量上来再切 MySQL,切换成本很低。
具体到工程结构,没有搞复杂的微服务,就一个单体应用:
backend/ ├── app.py # Flask 入口,注册蓝图 ├── models.py # SQLAlchemy 模型:配件、规则、订单、用户 ├── api/ │ ├── parts.py # 配件列表、筛选、详情 │ ├── config.py # 组装机配置器的校验与价格计算 │ ├── cart.py # 购物车 │ └── order.py # 订单与支付参数 ├── utils/ │ ├── auth.py # JWT 鉴权 │ └── compatibility.py # 兼容性规则引擎 └── data/ ├── parts_seed.json # 配件种子数据 └── rules.json # 兼容性规则表Flask 的蓝图机制刚好够用,每个业务模块一个文件,不混乱。数据库表也不多:配件表、类目表、兼容性规则表、配置单表、购物车表、订单表、用户表。配件表我特意用了一个 JSON 字段来存参数,比如 CPU 的插槽类型、核心数、TDP,显卡的功率、长度,内存的频率和代数。因为配件参数五花八门,规范化字段会把自己累死,JSON 加索引反而灵活。
1.2 uniapp 解决的不只是“多端复用”
选 uniapp 最直接的理由当然是后续可能上 App 和 H5,一套 Vue 代码不用重写。但实际做下来,我发现它在这个项目里还有一个隐藏好处:开发调试效率。
微信小程序原生开发的编译器、热重载和调试体验,说实话不如 Vue 的开发范式顺手。uniapp 的页面就是 Vue 单文件组件,data、computed、watch都用得上,对于配置器这种实时算价的场景,响应式数据模型比小程序原生的setData舒服太多。你只要不碰document、window这些浏览器 API,老老实实用uni.request、uni.navigateTo,编译到小程序端基本不会出幺蛾子。
顺便说一句,很多人纠结 uniapp 和 uniappx 的区别。你要是只做微信小程序,普通 uniapp 就够了;要是想上安卓/iOS 原生性能,才需要去看 uniappx。我们这项目没有重度计算,普通的就行。
1.3 用户端只交付微信小程序,为什么还要管 App 的事
有一点必须在项目启动时说清楚:虽然这次只交付微信小程序,但 manifest.json 里的 App 配置也要顺手做对。因为 uniapp 的云打包在配置 appid、图标、权限声明时,如果漏了,后面想补会很痛苦。我们的做法是开发期就把manifest.json的mp-weixin节点配齐,包括小程序的 appid、requiredPrivateInfos(如果需要位置等隐私接口)。哪怕现在用不上,也把基础项填了,省得以后翻工。
2. 组装机配置器:整个商城最硬核的部分
2.1 配件数据建模:不能只当商品卖,得让机器“认得”它
普通商城把配件当商品,标题、图、价格、库存就够了。但组装机配置器不一样,系统必须能理解“这颗 CPU 配这块主板合不合理”,所以配件数据要结构化。
我的配件表核心字段长这样:
category:类目标识,cpu / motherboard / gpu / memory / storage / psu / case / coolername:商品名,比如“Intel 酷睿 i5-13600K 盒装”price:售价,单位分,避免浮点误差params:JSON,里面按类目存放关键参数stock:库存status:上下架
params里每类配件有约定俗成的字段,比如:
{ "cpu": { "socket": "LGA1700", "tdp": 125, "cores": 14, "integrated_graphics": true }, "motherboard": { "socket": "LGA1700", "memory_type": "DDR5", "memory_slots": 4, "form_factor": "ATX" }, "gpu": { "tdp": 320, "length_mm": 336, "power_connector": "3x8Pin" }, "case": { "max_gpu_length_mm": 360, "form_factors": ["EATX", "ATX", "M-ATX", "ITX"] } }这里有个细节:同一类目下的配件,字段必须保持统一命名,比如主板内存类型只允许DDR4或DDR5,否则规则引擎没法跑。最开始我图省事,同一个字段一会儿写memory_type一会儿写ram_type,结果规则匹配时得做两套映射,自我折磨。后来直接定死一份字段字典,所有种子数据照字典填,后面省心很多。
2.2 兼容性校验不靠堆 if,靠规则表驱动
写配置器最容易掉进去的坑是:校验逻辑全写在 Python 里,来一个组合加一个 if。CPU 接口匹配一个 if,内存类型匹配一个 if,电源功率不够又一个 if……一开始确实爽,等配件库扩到几百个 SKU 的时候,函数会膨胀到没法维护。
我换成了规则表驱动。核心思路是“校验动作可配置”:每种兼容性检查对应一条规则,规则里有检查类型、涉及的两个配件类目、判定方式。具体到代码,compatibility.py里只留了规则执行器:
def check_compatibility(selected_parts, rules): """ selected_parts: dict,key 为类目名,value 为配件对象 rules: list,每条规则包含 check_type, part_a, part_b 返回所有不通过的校验结果 """ results = [] for rule in rules: checker = COMPATIBILITY_CHECKERS.get(rule["check_type"]) if not checker: continue part_a = selected_parts.get(rule["part_a"]) part_b = selected_parts.get(rule["part_b"]) if not part_a or not part_b: continue ok, message = checker(part_a, part_b) if not ok: results.append({ "message": message, "level": rule.get("level", "error") # error 硬性不兼容,warning 提醒 }) return results然后定义若干 checker 函数:
def check_socket(cpu, motherboard): if cpu["params"]["socket"] != motherboard["params"]["socket"]: return False, f"CPU 插槽 {cpu['params']['socket']} 与主板插槽 {motherboard['params']['socket']} 不匹配" return True, "" def check_memory_type(motherboard, memory): if motherboard["params"]["memory_type"] != memory["params"]["memory_type"]: return False, f"主板支持 {motherboard['params']['memory_type']},内存是 {memory['params']['memory_type']}" return True, "" def check_psu_power(cpu, gpu, psu): required = cpu["params"]["tdp"] + gpu["params"]["tdp"] + 80 if psu["params"]["wattage"] < required: return False, f"电源 {psu['params']['wattage']}W 不足以支撑整机功耗,建议 {required}W 以上" return True, ""规则表存在rules.json里,每条就是一个声明:
[ {"check_type": "socket", "part_a": "cpu", "part_b": "motherboard", "level": "error"}, {"check_type": "memory_type", "part_a": "motherboard", "part_b": "memory", "level": "error"}, {"check_type": "psu_power", "part_a": "psu", "part_b": "gpu", "part_a2": "cpu", "level": "error"}, {"check_type": "case_gpu_length", "part_a": "case", "part_b": "gpu", "level": "warning"} ]以后新增规则,比如电源线材接口不对,只需要加一个 checker 函数和一条规则声明,不用动前端和主流程。我用“错误”和“警告”两级,错误会阻止用户加入配置单,警告只提示不拦截——比如显卡太长塞不进机箱,我会警告但允许用户继续,因为确实有人强行上开放式机架。
2.3 前端交互:选一个刷新一批,响应式数据模型立功
配置器页面我设计成三栏结构,左边是配件类目(CPU、主板、内存……),中间是当前类目的配件列表,底部是常驻的“已选配置单”栏,实时显示总价和兼容性状态。手机屏幕小,左右结构用 tab 切换实现,底部栏用position: fixed。
用户每点一个配件,前端就把已选的所有配件 ID 发给后端/api/config/check接口,后端返回总价、兼容性错误列表、每项配件的状态(正常/缺货/不兼容)。这里必须定一个原则:价格计算和校验以后端为准,前端只负责展示。你不这么做的话,用户改一下前端代码就能把价格改掉,订单金额对不上,财务那边会骂人。
接口数据结构大概是:
{ "total_price": 859900, "parts": { "cpu": {"id": 101, "name": "Intel 酷睿 i5-13600K", "price": 249900, "status": "ok"}, "gpu": {"id": 205, "name": "RTX 4070 SUPER", "price": 489900, "status": "warning", "warnings": ["显卡长度可能超出机箱限长"]} }, "errors": ["CPU 插槽与主板插槽不匹配"] }前端拿到之后,用 Vue 的响应式数据整体替换底部栏状态,价格和红色感叹号马上刷新。uniapp 这边有个小坑:scroll-view里的列表如果数据量超过几百条,渲染会有明显卡顿。配件库 SKU 虽然有上千个,但用户一次只看一个类目,所以我做了分类目分页,一页 20 条,onReachBottom触底加载下一页,性能没出过问题。
3. 商城基础链路:从商品浏览到订单支付
3.1 商品浏览、搜索和筛选的最小闭环
配置器不是全部,商城该有的基础功能一个不能少。配件列表页和配置器中间件共用一套后端接口,只是展示维度和筛选项不同。
筛选项我用 URL query 实现,比如:
/api/parts?category=gpu&brand=NVIDIA&sort=price_asc&page=1&page_size=20后端用 SQLAlchemy 动态拼接查询条件,注意排序字段要用白名单校验,不能直接拼进 SQL 里。brand、socket、memory_type这些筛选项来自前端提交,万一有人传一个order_by=price; drop table之类的参数,拼接进去就是事故。白名单一拦,现场就没了。
搜索功能没有上 Elasticsearch,配件标题本身不长,用LIKE '%关键词%'够用。量大了以后可以换 SQLite FTS5 或者 MySQL 全文索引,初期没必要引入额外组件。
3.2 购物车既要装单品,也要装整张配置单
购物车是这个项目里比较容易想简单的地方。普通商城购物车只装商品,我们的购物车有两种行:单件配件和整张配置单。
我建了一张cart_item表:
iduser_iditem_type:part 或 configpart_id:当 item_type 为 part 时有值config_snapshot:当 item_type 为 config 时,存整个配置单快照(JSON)quantitycheckedcreated_at
关键在设计config_snapshot。用户把一张配置单加入购物车之后,配件价格和兼容状态必须冻结在快照里,不能等用户下单时再去现查配件表。否则用户加购时显卡卖 4899,等三天后下单时已经涨到 5299,你按哪个价格收钱?按现价收,用户投诉;按旧价收,你亏钱。快照方案逻辑最简单:加购即冻结。
配置单快照的 JSON 大概长这样:
{ "parts": [ {"category": "cpu", "id": 101, "name": "Intel 酷睿 i5-13600K", "price": 249900}, {"category": "gpu", "id": 205, "name": "RTX 4070 SUPER", "price": 489900} ], "total_price": 859900, "compatibility_status": "ok" }下单时直接用快照金额,不跟实时价格联动,这才是交易系统应该有的确定性。
3.3 订单状态机与微信支付的认证门槛
订单状态我定了五个:待支付、已支付、备货中、已完成、已取消。后端用一个status字段加一个允许状态流转的映射表,防止接口被乱调用跳状态。比如从“待支付”只能走到“已支付”或“已取消”,不能直接跳到“已完成”。
微信支付这块有个必须先讲清的现实问题:个人主体的小程序不能开通微信支付,必须是企业主体或个体工商户。如果你是自己练手项目,可以用微信支付沙箱环境模拟;如果是给客户做交付,必须提前确权客户的资质,否则做到一半发现支付接口申请不下来,整个项目白做。我们这次客户是有企业主体的,走了正常的商户号申请流程,前后大概一周时间。
支付流程用 uniapp 的uni.requestPayment封装一层,后端order.py里创建订单后调用微信支付统一下单接口拿到paySign,返回给小程序端拉起支付面板。回调地址注意要配到微信商户平台,而且回调处理要幂等:同一个支付结果通知可能推多次,处理前先查订单状态,避免重复更新。
4. 微信小程序端的适配与真机调试坑
4.1 自定义导航栏:胶囊按钮的“毛边效应”
我们这个小程序没有用默认导航栏,而是自定义了顶部导航,为了把“配置器已选数量”这种信息塞进导航栏右侧。自定义导航栏的第一个坑就是:不同机型的胶囊按钮位置不一样。
iPhone 上有灵动岛的机型和老款刘海屏,胶囊按钮的top和height都不一样。官方 API 是uni.getMenuButtonBoundingClientRect(),能拿到胶囊的位置和尺寸,但不能直接把它写死到样式里。我是这样算的:
const menu = uni.getMenuButtonBoundingClientRect() const navBarHeight = (menu.top - statusBarHeight) * 2 + menu.height道理很简单:胶囊按钮垂直居中的位置决定了整个导航栏的视觉中心,用胶囊的 top 减去状态栏高度,再乘以 2 加自身高度,基本就是自定义导航栏的标准总高。两侧留白也要动态算,不能写死 100rpx,因为不同机型胶囊到屏幕右边界的距离不一样。
4.2 手机号登录:新接口必须在用户点击后才能调
配置器依赖用户登录后才能保存配置单,所以登录环节绕不开。微信小程序获取手机号的接口在 2023 年之后改了规则,不能直接wx.getPhoneNumber拿号码了,必须用open-type="getPhoneNumber"的按钮组件,用户主动点击触发,才能拿到code,然后后端用code换手机号。
前端代码大致是:
<button open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">微信一键登录</button>onGetPhoneNumber里拿e.detail.code传给后端,后端拿 code 去微信接口换手机号,再用手机号关联用户。这里有个容易踩的坑:e.detail.code是一次性的,每点一次换一个新 code,后端必须一次性用完,不能缓存。第一次没调对接口参数,code 就废了,用户只能再点一次按钮,体验很裂开。
4.3 uniapp 开发者工具里“日志不见了”的排查方法
开发到中期,我遇到一个特别玄学的问题:uni.request 的 success 回调里明明有数据,但console.log在微信开发者工具的 Console 面板里死活不显示。网上查了一圈,有人说是 uniapp 编译模式问题,有人说是工具缓存问题。试了一圈管用的操作是:把开发者工具的“ES6 转 ES5”选项关掉再重新打开,然后清缓存,重新编译。这里背后原因其实是部分 console 语句被 babel 转换后丢掉了 sourcemap 关联,重新编译能恢复。
另外,真机调试时日志不显示,通常是打开了“过滤无来源日志”之类的选项。开发者工具左上角有个日志过滤按钮,默认会隐藏一堆插件日志,把级别调到 All,再去 Console 面板看,基本就出来了。
5. 打包交付与后端部署要点
5.1 从 HBuilderX 到微信开发者工具:代码上传四步走
uniapp 项目写完之后,交付给客户时一般走这套流程:
- HBuilderX 里选
运行 -> 运行到小程序模拟器 -> 微信开发者工具,会自动拉起微信开发者工具并加载编译后的dist/dev/mp-weixin目录。 - 确认调试没问题后,选
发行 -> 小程序-微信,生成生产环境包。 - 在微信开发者工具里点“上传”按钮,填版本号和备注,把代码传到微信后台。
- 后台提交审核,审核通过后可以发布体验版或正式版。
这里提醒一句:manifest.json里的微信小程序 appid 必须填对,否则上传时会被后台拒绝。很多人开发时用的测试号 appid,到正式上线时忘了替换,浪费一个审核周期。
5.2 后端部署:域名、HTTPS、备案一个都不能少
微信小程序发正式版时,所有请求域名必须是 HTTPS 且已备案,这是硬性要求。我们后端部署在云服务器上,用了 Nginx 加 Let’s Encrypt 免费证书。Nginx 配置里转发了/api/路径到 Flask 的 5000 端口,同时配了 gzip 压缩,对小程序的请求体很有帮助。
还有个安全细节:后端接口要校验请求来源,最简单的方案是加一个自定义 Header,比如X-App-Client: mini-program,Nginx 层拦截没有这个 Header 的请求。防不住高级攻击,但能挡住扫描器和乱爬的脚本。
5.3 这套代码后续还能扩展什么
交付之后客户问能不能加“智能推荐配置单”,其实底层已经具备条件。配置器收集了很多用户选配数据,后端可以统计每个价位段里哪些配件组合最常同时被选中,给新用户推荐几套“套餐配置”。这就是最简单的协同过滤,用 Python 的 pandas 处理历史配置单,跑个关联规则,不复杂但效果很直观。
另一个方向是库存预警。配置单生成之后可以检查所有配件的库存状态,只要有配件缺货,整个配置单就标成“不可下单”。这个逻辑放到快照生成时同步执行,不用等下单再报错。
我个人在实际操作中的体会是:这种“工具+商城”的复合项目,真正拉开差距的不是商品 CRUD,而是配置器这种带业务逻辑的模块。它逼着你把数据模型设计得足够规范,把校验逻辑抽象成可配置的规则,否则配件库一扩,代码全是补丁。如果让我重做一遍,我大概会把兼容性规则从 JSON 换成数据库表,加上后台管理界面,让运营自己就能录入新规则,不用每次改代码发布。这是下一期迭代最值得做的优化。