资讯动态

商城小程序源码:主链路、事务与幂等的可答辩工程骨架

发布时间:2026/10/30 5:06:09 来源:尧图企业网站定制
简介这份小程序商城源码包面向微信小程序开发初学者、课程设计与毕业设计学生以及需要快速搭建在线购物演示项目的开发者围绕商品展示、购物车、订单结算、用户中心与营销活动等模块提供可运行的前端实现参考。压缩包共68个文件约3.32MB以30张png与4张jpg/jpeg界面图、9个js逻辑脚本、7个wxss样式表、7个wxml页面结构及7个json配置为主并附带说明压缩包便于对照页面结构与代码组织方式。已有651人学习。读者可从中理清小程序商城的目录层级、页面跳转、组件样式与数据交互思路借助现成页面和配置快速复现商品列表、详情、购物车、结算等流程并在此基础上改造为课程设计或毕业设计原型对需要理解微信小程序开发框架与商城业务闭环的读者较有参考价值。1. 商城小程序从能跑到能讲清楚这套源码补的是哪块缺口最常见的翻车现场是这样模板下载下来能打开首页轮播在转商品列表也有数据但翻两页代码就露馅——价格写死在 js 里购物车用全局变量存提交订单时前端自己判断库存够不够服务端只负责 insert。这种东西自己演示没问题老师问一句两个人同时下最后一件库存怎么办就答不上来了。这份商城小程序源码要补的就是这块它不是一堆页面拼出来的样子货而是围绕浏览商品 → 加购物车 → 提交订单 → 支付回调改状态这条主链路组织起来的完整工程客户端五个业务模块服务端一套可跑通的接口订单、库存、支付状态三处都留了可讲的设计点。它适合两类人要交课程设计或毕业设计、需要一套结构经得起追问的项目骨架的同学以及要快速搭一个能演示、能二次开发的商城原型的开发者。源码只是起点真正值钱的是它背后那套组织方式。2. 需求拆解与技术选型先定页面清单再定三处取舍动手改代码之前我一般会先把有哪些页面、每个页面调哪个接口、数据从哪张表来这三件事对齐。很多同学拿到源码就直接改样式改到一半发现购物车和订单的数据结构对不上只能推倒重来这一类折腾完全可以在第一天避免。2.1 角色与页面清单买家端十一个页面怎么排商城类项目的角色可以很复杂也可以很克制。课程设计和毕设这个量级我建议只做两类角色买家端小程序和管理端网页或小程序内嵌的简易后台。管理端不需要做权限体系一个账号密码登录够用功能收敛到商品增删改、订单查看发货、分类维护三件事把精力留给买家端的主链路。买家端的页面数量看着多其实可以按流量页—决策页—交易页—资产页四段来切分段页面路径核心交互主要依赖接口流量页首页pages/index轮播、分类入口、推荐位/api/banner、/api/category流量页分类页pages/category左侧分类、右侧商品/api/category、/api/goods决策页商品列表pages/goods/list下拉刷新、触底加载/api/goods决策页商品详情pages/goods/detailSKU 选择、加购、立即购买/api/goods/:id交易页购物车pages/cart单选全选、改数量、结算/api/cart交易页确认订单pages/order/confirm地址选择、金额核算、提交/api/order交易页支付结果pages/order/result轮询订单状态/api/order/:no资产页订单列表pages/order/list状态筛选、取消、确认收货/api/order资产页订单详情pages/order/detail状态时间轴、物流占位/api/order/:no资产页地址管理pages/address增删改、设默认/api/address资产页个人中心pages/mine登录、订单入口/api/user/profile这张表的价值在于每一个页面都能反查到接口每一个接口都能反查到表。答辩时老师随便指一个页面你都能顺着说下去——页面调什么、接口做什么、表里存什么这条链路讲得顺项目的完整度就立住了。反过来如果某个页面找不到对应接口比如猜你喜欢是前端写死的假数据提前标出来别等被问到才解释。2.2 三个选型取舍原生框架、自建服务端、关系型数据库技术选型这一块能讲出为什么不用另一个比用了什么重要得多。这个项目的场景是课程设计/毕业设计 可演示原型我按三个维度做了取舍维度选项 A选项 B这个项目选谁理由客户端框架小程序原生框架跨端编译框架原生包体小、平台能力完整、无编译层答辩时每一行代码都能追到服务端形态平台云开发自建 Node 服务 MySQL自建库存扣减、事务、幂等这些设计点能真实跑起来讲得出细节数据存储文档型数据库MySQLMySQL订单、库存、支付状态都需要事务和行级锁关系型更稳原生框架这条要展开说一句。跨端方案的好处是一套代码多端跑代价是多一层编译和一套适配规则样式对不上时你得先判断是写错了还是编译层的问题。课程设计阶段这个代价不划算——你需要的是我写的每一行都在干什么清清楚楚而不是它帮我生成了什么。原生框架的 WXML/WXSS/JS 三层结构直白调试工具里断点打下去数据流一眼看到底这对答辩和后续修改都是加分项。自建服务端与云开发之间的选择本质是省事和可讲的权衡。云开发把数据库、云函数、存储都包好了两小时能跑通一个增删改查但库存扣减这种需要事务的操作在云函数里要么用原子操作凑要么绕开讲起来底气不足。自建 Node 服务虽然多配一套环境但一笔订单从校验库存 → 扣减 → 写单 → 回调改状态完整走一遍事务这个过程本身就是毕设里最有含金量的部分。我的建议是如果时间只剩一周云开发如果有一个月以上自建。2.3 六张核心表字段怎么定索引怎么加数据模型决定了后面所有代码的形状这一步偷懒后面全是补丁。商城最小闭环需要七张表用户、商品、SKU、购物车、订单、订单明细、收货地址。下面给出关键几张的建表语句重点看字段类型和索引-- 商品表价格统一用“分”存整数避免浮点误差 CREATE TABLE goods ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, category_id BIGINT UNSIGNED NOT NULL DEFAULT 0, title VARCHAR(120) NOT NULL, cover VARCHAR(255) NOT NULL DEFAULT , price INT UNSIGNED NOT NULL COMMENT 售价单位分, origin_price INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 划线价单位分, stock INT UNSIGNED NOT NULL DEFAULT 0, version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_cate_status (category_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表order_no 唯一索引既做查询键也做支付回调的幂等锚点 CREATE TABLE orders ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id BIGINT UNSIGNED NOT NULL, total_fee INT UNSIGNED NOT NULL COMMENT 订单总额单位分, status VARCHAR(16) NOT NULL DEFAULT UNPAID COMMENT UNPAID/PAID/SHIPPED/DONE/CANCELED, idem_key VARCHAR(64) NOT NULL DEFAULT COMMENT 客户端幂等键, pay_time DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), UNIQUE KEY uk_user_idem (user_id, idem_key), KEY idx_user_status (user_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个字段的设计理由值得在答辩时讲price用INT存分是这一整套代码里最容易被忽略又最容易出事的地方浮点数做金额累加0.1 0.2 会给你 0.30000000000000004凑单满减一算就差分。version字段是给乐观锁留的虽然下面下单示例用的是stock num的条件更新两条路都通留着不吃亏。uk_user_idem这个联合唯一索引是防重复下单的最后一道闸客户端每次进确认页生成一个幂等键重复提交会被数据库直接挡回去比在应用层判断可靠得多。购物车表建议做服务端存一份 本地缓存一份服务端表结构只有user_id / goods_id / sku_id / num / checked五六个字段登录后合并本地数据。订单明细表把下单那一刻的商品标题、价格、图片快照下来商品后来改价改名都不影响历史订单——这个细节很多模板会漏老师一问商品改价了历史订单金额会不会变就尴尬了。3. 总体架构设计四层职责与目录导览架构这件事画图容易讲清难。我的习惯是不急着画方框先把一次点击从页面到数据库经过哪些文件走一遍走通了架构自然就出来了。3.1 四层结构页面、服务、接口、数据的职责边界客户端按四层切页面层pages、组件层components、服务层services/api、工具层utils。页面层只做三件事——拿数据、渲染、响应用户操作不写业务规则组件层放商品卡片、数量步进器、空状态这些复用块服务层把每个接口封成一个函数页面里只调api.getGoodsList(params)不出现裸的wx.request工具层放金额换算、时间格式化、缓存读写这些纯函数。这条边界守住了页面文件不会膨胀到五百行改接口时也只改一个地方。服务端对应四层路由router负责路径与方法映射控制器controller做参数校验和响应组装服务层service承载业务规则——库存够不够、订单能不能取消、状态能不能流转数据访问层dao 或直接 model只跟 SQL 打交道。这套分层不是学院派讲究它的实际收益是业务规则集中在 service写单元测试和答辩讲逻辑都在这一层controller 薄到只有十几行看代码的人三分钟能摸清一个接口在干什么。整条链路长这样小程序页面 → 服务层封装函数 → HTTPS 请求 → 路由匹配 → 控制器校验 → 服务层业务判断 → 数据访问层读写 MySQL → 原路返回 JSON。中间可以插 Redis 做商品缓存、对象存储放图片但这两块在课程设计里属于加分项先把主链路跑稳再考虑。3.2 源码目录导览先读哪五个文件再改代码拿到一份陌生源码最忌讳从头到尾顺着读。我的顺序是先读五个骨架文件把数据流摸清楚再钻细节# 客户端 miniprogram/ ├── app.js # ① 全局登录态、启动参数先看这里 ├── app.json # ② 页面注册、tabBar、分包配置 ├── utils/request.js # ③ 请求封装baseURL、token、错误码 ├── services/api.js # ④ 所有接口函数集中在这里 └── pages/ ├── index/ ├── goods/ │ ├── list.js │ └── detail.js ├── cart/ └── order/ # 服务端 server/ ├── app.js # ⑤ 中间件、路由挂载、错误兜底 ├── router/ │ ├── goods.js │ ├── cart.js │ ├── order.js │ └── pay.js ├── controller/ ├── service/ │ └── order.js # 下单事务、状态机都在这里 ├── model/ └── config/db.js前四个文件解决数据从哪来第五个文件解决请求打到哪。这五个看完剩下的页面文件就是填空题——照着已有页面的结构复制一份改比从零写快得多也不会破坏既有约定。改代码之前还有一步把app.json里注册的页面和实际存在的目录对一遍模板里经常有注册了但目录被删的空壳跑起来报一堆 not found白白浪费半小时。3.3 请求层封装baseURL、token 与错误码统一处理请求封装是客户端里最值得先吃透的一段代码因为它决定了后面所有页面怎么写。一个够用的封装要解决四件事环境切换开发/正式域名、登录态注入、统一错误提示、loading 控制。// utils/request.js const BASE_URL https://api.example.com // 换成自己的服务端域名 function request({ url, method GET, data {}, loading false }) { return new Promise((resolve, reject) { if (loading) wx.showLoading({ title: 加载中, mask: true }) wx.request({ url: BASE_URL url, method, data, header: { content-type: application/json, // token 从本地缓存取登录后写入没有就不带服务端按未登录处理 Authorization: wx.getStorageSync(token) || }, success(res) { const body res.data // 约定code0 成功401 未登录其余为业务错误 if (body.code 0) return resolve(body.data) if (body.code 401) { wx.removeStorageSync(token) wx.navigateTo({ url: /pages/login/login }) return reject(new Error(UNAUTHORIZED)) } wx.showToast({ title: body.msg || 请求失败, icon: none }) reject(new Error(body.msg)) }, fail(err) { // 网络层失败单独提示和业务错误区分开排查时能少走弯路 wx.showToast({ title: 网络异常请稍后重试, icon: none }) reject(err) }, complete() { if (loading) wx.hideLoading() } }) }) } module.exports { request }这段代码里有两个参数值得改BASE_URL在开发阶段指向本地或测试环境上线前换成正式域名建议用一个配置文件按环境导出别硬编码在请求文件里loading默认false只有首屏、提交订单这类需要明确反馈的请求才传true列表加载更多不传否则页面会一直闪 loading 遮罩。错误处理把网络失败和业务失败分开是因为这两类问题的排查路径完全不同——一个是域名、证书、网络一个是参数、权限、库存混在一起提示出问题时你根本不知道从哪查。服务端接口的返回结构必须统一成{ code, msg, data }三件套控制器里所有分支都走同一个响应函数别有的接口直接res.json(list)有的包一层。统一之后客户端那一段判断就能覆盖全部接口新增接口不用改请求层。4. 核心模块实现商品、购物车、下单、回调主链路四个环节每个环节都有一个能跑和能讲的分界线。下面按顺序给出关键代码和设计说明代码是示意性质参数按自己项目改。4.1 商品列表分页、防重复请求与价格格式化商品列表是用户进来看到的第一个数据页它暴露的问题也最典型触底加载时用户手快同一个page连发三次请求列表里出现三份重复数据或者价格直接返回分页面上显示 2990 而不是 29.90。// pages/goods/list.js const { request } require(../../utils/request) Page({ data: { list: [], page: 1, size: 10, hasMore: true, loading: false // 请求锁防止触底连续触发 }, onLoad() { this.fetchList(true) }, onPullDownRefresh() { // 下拉刷新必须重置分页否则新数据和旧数据会拼接错位 this.fetchList(true).then(() wx.stopPullDownRefresh()) }, onReachBottom() { if (!this.data.hasMore || this.data.loading) return this.fetchList(false) }, async fetchList(reset) { if (this.data.loading) return this.setData({ loading: true }) const page reset ? 1 : this.data.page try { const res await request({ url: /api/goods, data: { page, size: this.data.size } }) // 服务端价格单位是“分”只在这一层转成元页面里不再做二次换算 const rows res.list.map(it ({ ...it, priceText: (it.price / 100).toFixed(2) })) this.setData({ list: reset ? rows : this.data.list.concat(rows), page: page 1, hasMore: rows.length this.data.size }) } finally { this.setData({ loading: false }) } } })loading这个锁是这段代码的核心它在请求发出时置真、finally里置假中间任何一次触底都会被return挡掉。hasMore用本页返回条数是否等于 size来判断比让服务端返回总数再算页码简单代价是最后一页刚好满额时会多发一次空请求可以接受。价格转换只在小程序端做一次服务端全程用分这条约定写进团队规范后面购物车、订单、支付三处都按它来账就不会错。服务端对应的查询要注意两点WHERE status 1过滤下架商品ORDER BY id DESC保证分页稳定如果做分类筛选把category_id加到条件里并确保idx_cate_status索引能命中数据量上万条之后这个索引的差别很明显。4.2 购物车本地缓存与服务端同步的合并策略购物车最容易纠结的是数据存哪。纯本地存换设备就丢下单时还得回传一次纯服务端存未登录用户加购直接失败体验很差。我一般用本地优先、登录合并的策略未登录时加购写进wx.setStorageSync(cart, [...])结构就是{ goodsId, skuId, num, checked }的数组登录成功后把本地数组整体 POST 给/api/cart/merge服务端按user_id goods_id sku_id做 upsert数量取两者较大值或者直接累加——累加更符合用户预期但要在页面上提示已合并本地购物车。合并完成后清掉本地缓存后续所有操作走服务端。金额计算单独抽成纯函数方便单测// utils/cart.js // 所有金额以“分”为单位参与整数运算只在展示时转元 function summarize(items) { let totalFen 0 let count 0 items.forEach(it { if (!it.checked) return totalFen it.price * it.num count it.num }) return { totalFen, totalText: (totalFen / 100).toFixed(2), count } } // 全选/反选返回新数组不改原对象避免 setData 后视图不刷新 function toggleAll(items, checked) { return items.map(it ({ ...it, checked })) } module.exports { summarize, toggleAll }toggleAll返回新数组而不是原地改it.checked是因为小程序setData对同一引用对象的深层变化不一定能触发视图更新这个坑踩过一次就会记住。数量步进器要限制num 1减到 1 时按钮置灰别让用户减出 0 或负数服务端也得再校验一次——前端限制是体验服务端校验才是底线。4.3 下单事务幂等键、条件更新与订单号规则下单是整个项目里唯一必须用事务的地方也是答辩最容易被追问的地方。一笔订单要动三张表扣库存、写订单、写明细任何一步失败都得回滚否则就会出现库存扣了订单没生成这种对不上账的情况。// server/service/order.js const db require(../model/db) const { genOrderNo } require(../utils/no) async function createOrder(userId, { goodsId, num, idemKey }) { const conn await db.getConnection() try { await conn.beginTransaction() // 1. 幂等同一用户 同一幂等键只允许一笔订单重复提交直接返回原单 const [exist] await conn.query( SELECT id, order_no FROM orders WHERE user_id ? AND idem_key ?, [userId, idemKey] ) if (exist.length) { await conn.rollback() return { orderNo: exist[0].order_no, repeated: true } } // 2. 条件更新扣库存stock num 才更新成功天然防超卖 const [r] await conn.query( UPDATE goods SET stock stock - ?, version version 1 WHERE id ? AND status 1 AND stock ?, [num, goodsId, num] ) if (r.affectedRows 0) { await conn.rollback() throw new Error(STOCK_NOT_ENOUGH) } // 3. 取商品快照价金额以服务端为准绝不信任前端传来的价格 const [[goods]] await conn.query( SELECT title, cover, price FROM goods WHERE id ?, [goodsId] ) const totalFee goods.price * num const orderNo genOrderNo(userId) // 4. 写订单主表 明细表 const [ins] await conn.query( INSERT INTO orders (order_no, user_id, total_fee, status, idem_key) VALUES (?, ?, ?, UNPAID, ?), [orderNo, userId, totalFee, idemKey] ) await conn.query( INSERT INTO order_items (order_id, goods_id, title, cover, price, num) VALUES (?, ?, ?, ?, ?, ?), [ins.insertId, goodsId, goods.title, goods.cover, goods.price, num] ) await conn.commit() return { orderNo, totalFee } } catch (e) { await conn.rollback() throw e } finally { conn.release() } }几个参数和写法的说明idemKey由客户端在进入确认订单页时生成时间戳 随机串即可同一个页面的多次点击用的是同一个键换一单才换键这样既防连点又不会误挡正常复购。扣库存用stock num写进WHERE靠数据库的行锁保证并发下不会扣成负数这比先查再改可靠得多——两个请求同时查到库存 1然后都去更新后者会覆盖前者。totalFee必须由服务端按商品表当前价格重新计算前端传来的金额只用来展示不参与落库。订单号生成规则最好别用单纯的自增 ID容易暴露单量。常见做法是年月日时分秒 用户 ID 后四位 两位随机数长度控制在 20 到 32 位之间// server/utils/no.js function genOrderNo(userId) { const d new Date() const pad n String(n).padStart(2, 0) const ts ${d.getFullYear()}${pad(d.getMonth() 1)}${pad(d.getDate())} ${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())} const tail String(userId).slice(-4).padStart(4, 0) const rand String(Math.floor(Math.random() * 100)).padStart(2, 0) // 时间到秒 用户尾号 两位随机极端并发下仍可能撞号所以库里还有唯一索引兜底 return ts tail rand }4.4 支付回调状态机与重复通知的处理支付回调是看起来简单、出错最难查的一段。平台的支付结果通知会重发可能三次五次也可能在你已经发货之后才到如果不做状态判断重复通知会把订单改成已支付、重复加积分、重复发消息。先把订单状态机定死只允许下面这几条边存在当前状态允许迁移到触发动作UNPAIDPAID支付回调成功UNPAIDCANCELED用户取消 / 超时关单PAIDSHIPPED后台发货SHIPPEDDONE用户确认收货PAID / SHIPPEDREFUNDING发起退款回调处理的核心就是验签 幂等 状态条件更新三件事缺一不可// server/router/pay.js router.post(/api/pay/notify, async (req, res) { // 1. 验签失败直接拒绝不进入任何业务逻辑 if (!verifySign(req.headers, req.rawBody)) { return res.status(401).send(FAIL) } const { outTradeNo, tradeState } parseNotify(req.body) if (tradeState ! SUCCESS) return res.send(SUCCESS) // 2. 状态条件更新只有 UNPAID 才能变成 PAID // 重复通知时 affectedRows 0直接返回成功不做任何副作用 const [r] await db.query( UPDATE orders SET status PAID, pay_time NOW() WHERE order_no ? AND status UNPAID, [outTradeNo] ) if (r.affectedRows 0) { // 3. 副作用只在这里做一次加销量、清购物车、发通知 await onPaid(outTradeNo) } // 无论是否更新成功都返回 SUCCESS否则平台会持续重发 res.send(SUCCESS) })这段代码里最反直觉的一句是无论更新成功与否都返回 SUCCESS。很多人第一反应是没更新成功说明有问题应该返回失败让平台重试但这里是重复通知本来就不该再处理返回失败只会引来更多重试。真正的异常验签失败、订单不存在在前面就拦掉了。onPaid里的副作用同样要做幂等比如清购物车用DELETE ... WHERE user_id ? AND goods_id IN (...)重复执行也无害。前端支付结果页不要只信wx.requestPayment的 success 回调那个回调只代表用户完成了支付动作不代表服务端已确认到账。正确做法是支付成功后跳结果页轮询/api/order/:no查状态最多轮询五到六次、间隔一秒仍未变成 PAID 就展示支付结果确认中请稍后到订单列表查看。5. 避坑与排查五个最容易翻车的点下面五条是这个项目里出现频率最高的问题每一条我都按现象 → 原因 → 解决写遇到时可以直接对照。5.1 金额差一分钱浮点误差与分的约定不统一现象是购物车合计 99.99下单后订单金额 100.00或者满减凑单怎么算都差 0.01。原因基本是两个一处用元做浮点运算另一处用分做整数中间来回转换或者toFixed(2)用在中间步骤而不是最终展示四舍五入被反复叠加。解决方式是把分作为唯一内部单位写进规范数据库存分、接口传分、计算用分只有渲染到页面的那一刻才除以 100。所有* 0.01、parseFloat出现在业务计算里的地方全部改成整数运算。满减、优惠券这类规则也按分定义门槛别写满 99 减 10写满 9900 分减 1000 分。5.2 同一单提交了两次按钮没锁幂等键也没做现象是订单列表里出现两笔一模一样的订单用户来问是不是重复扣款了。原因有两层前端提交按钮点下去没有置灰网络慢的时候用户连点后端只做了插入订单没有幂等键或唯一索引兜底。解决要两头一起做——前端在submit函数入口立刻setData({ submitting: true })并禁用按钮finally里恢复后端用(user_id, idem_key)联合唯一索引插入冲突时捕获ER_DUP_ENTRY错误查出原订单直接返回。只做前端锁不够接口被直接调用时照样会重复只做后端幂等也不够用户看到没反应会一直点体验很差。5.3 开发者工具正常真机请求全部失败现象是模拟器里列表刷刷地加载扫码到手机上全是网络异常。原因通常不是代码而是三件事之一请求域名没加进小程序的合法域名白名单域名没有配置 HTTPS 或者证书链不完整本地调试用的localhost在手机上根本指不到你的电脑。排查顺序建议从外到内——先用手机浏览器直接访问接口地址能出 JSON 说明域名和证书没问题再检查小程序后台的服务器域名配置注意白名单不含端口和路径最后确认开发阶段是否勾选了不校验合法域名这个选项只在开发版生效体验版和正式版一律校验。上线前必须把正式域名配好别指望这个开关。5.4 首屏白屏两三秒图片没压缩主包也超了现象是首页进去先白一片轮播图和商品图一张张往外蹦弱网下更明显。原因一般是商品图直接上传原图一张几 MB列表一次加载十条就是几十 MB加上主包体积逼近上限启动时要下载的东西太多。解决分两步图片在服务端上传时就做压缩和尺寸裁剪列表用缩略图宽度 300px 左右足够详情页再加载大图格式优先用平台支持的压缩格式工程侧把订单、地址、个人中心这些非首屏页面拆进分包主包里只留首页、分类、商品详情、购物车app.json里配好subpackages首屏加载时间能压下来一截。图片懒加载也要开列表项进入视口再请求。5.5 支付成功但订单还显示待付款现象是用户付完钱回到订单列表状态还是待付款过一会儿刷新才变。原因有三种可能回调地址配错了平台通知根本没打到你服务器回调到了但验签失败被拒回调成功了但前端页面用的是本地缓存数据没有重新拉接口。排查顺序是先看服务端日志有没有收到通知没有就查回调地址和网络可达性有通知但订单没变看验签和status条件是否满足都正常就是前端问题在onShow里重新拉一次订单详情别依赖支付成功回调里带回来的状态。这类问题最忌讳用户说没到账就手动改库一定要先定位到是哪一环断的。6. 验收与答辩怎么证明这套代码你讲得清交之前我会走一遍固定的自查十项过完再打包能挡掉八成现场尴尬检查项通过标准主链路浏览 → 加购 → 下单 → 支付 → 订单状态变更全程无报错金额一致性购物车合计 订单金额 支付金额三处一分不差库存库存 1 时下单成功、再下一单提示不足库存不为负幂等连点提交按钮订单列表只有一笔状态机已取消的订单不能再支付已支付不能重复支付空态购物车空、订单空、搜索无结果都有提示页异常断网、接口 500 时有提示不白屏不卡死数据商品改价后历史订单金额不变快照生效配置正式域名、HTTPS、分包配置齐全体验版能跑代码没有 console.log 残留、没有写死的测试数据答辩讲的时候不要按页面顺序念功能那样十分钟讲完全场记不住任何东西。我的建议是抓三条主线第一条讲架构客户端四层、服务端四层各层职责一句话说完配一张自己画的图第二条讲主链路从加购到支付回调重点讲下单那段事务和条件更新这是最能体现你理解深度的地方第三条讲一个你踩过的坑比如金额浮点误差或者重复下单按现象—排查—怎么改—改完怎么验证讲完整比罗列十个功能有说服力得多。老师追问时别慌答不上来的就说这块我目前是按 X 实现的更严谨的做法可能是 Y还没做诚实比硬编强。我自己的习惯是答辩前一天把主链路从零走三遍第三遍故意把库存改成 1、把网络断掉看系统怎么反应。有一次就是这么试出来的——库存不足时前端弹的是网络异常因为服务端抛错没走统一响应前端落到了 fail 分支。这种问题不亲手点一遍根本发现不了。从那以后我每次交项目前都强制走一遍断网 边界值的组合测试宁可自己先翻车也别在台上翻。希望这套结构和清单能帮到你源码拿去改的时候先把第 2 章那几张表和接口对齐剩下的都是体力活。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑