news 2026/10/27 10:35:14

微信小程序商城毕业设计:源码+数据库脚本+教程全链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微信小程序商城毕业设计:源码+数据库脚本+教程全链路

简介:面向微信小程序课程设计与毕业设计场景的商城项目源码包,适合需要完成商城类小程序作业、答辩演示或二次开发练习的学生与初学者。内容围绕商品展示、分类发现、购物车、支付、消息与个人中心等常见模块组织,页面结构完整,代码附带注释,便于理解小程序端的页面、样式与逻辑协作方式。压缩包共68个文件,约3.31MB,主要由png、jpg等界面素材,js业务脚本,wxss样式,wxml结构与json配置组成,另含txt使用说明,便于按目录快速定位页面和资源。已有1243人学习,说明该选题与实现方式具有一定参考价值。读者可据此梳理商城小程序的目录组织、页面跳转、数据绑定与静态资源管理,并结合数据库脚本、部署说明完成环境搭建和运行调试,也可作为课程设计模板进行功能扩展与界面调整。

1. 从一份能跑通的商城说起:这套方案要解决的三个交付问题

微信小程序商城的毕业设计,最常翻车的不是功能写不出来,而是收尾时发现手里只有一堆页面:数据库建表脚本没有,后台接口是写死的假数据,教程只有一句「自己看代码」。这套基于微信小程序的商城方案,把源码、数据库脚本和一份能跟着操作的教程放在一起,解决的就是「跑得起来、说得清楚、答得上来」这三件事。它适合正在做小程序方向毕业设计或课程资源整理的学生,也适合想拿一套完整电商闭环做参考的开发者——商品、购物车、订单、支付、后台管理这条主线是贯通的,不是只有一个首页好看的空壳。下面按「需求怎么定、架构怎么切、模块怎么写、哪里会炸、怎么验收」的顺序,把这份方案拆开讲一遍。

2. 需求拆解与技术选型:三类角色、两个端、一条主线

做商城类毕业设计,第一个要做的动作不是打开编辑器建工程,而是把功能清单按角色切开、按优先级排一遍。切不清角色的后果是:页面写了三十个,答辩时老师说「你这个后台在哪」,你只能说「后台没做,重点在前端」。

2.1 三类角色的功能边界:买家端做闭环,管理端做管控,数据端做客诉

买家端的验收标准只有一条——能从头走到尾。首页轮播与推荐位 → 分类页 → 商品列表 → 商品详情 → 加入购物车 → 确认订单(选地址、算运费、用备注)→ 支付 → 订单列表 → 确认收货 → 评价。这条主线上任何一环断了,演示就得靠切页面硬撑过去,很容易被追问到答不上来。支线功能包括搜索、收藏、收货地址管理、个人中心、售后申请,这些属于「有更好、没有也能及格」的部分。

管理端不是摆设,它承担了整个系统里唯一能改动数据状态的入口。商品的新增、编辑、上下架、改库存,订单的查看、发货、备注,分类与轮播图的维护,再加一个简单的数据看板(今日订单数、成交额、待发货数量)。管理端通常用一套 Web 页面实现,不需要做得漂亮,但必须能改数据——答辩时老师让你「把这个商品下架看看前台什么样」,你得当场点得出来。

数据端说的是数据库这一层要能自证。表结构、字段注释、索引、初始化数据,这四样在交付物里必须是独立的、可执行的脚本。我一般会把建表语句和十几条测试数据放在同一个 SQL 文件里,导入就能跑,省得答辩现场还在那儿一条条 insert。

功能取舍上有一条血泪经验:八到十二周的周期里,砍掉优惠券、拼团、秒杀、分销这类营销模块,把订单状态机和库存扣减做扎实,比堆十个花哨功能更容易拿分。营销模块的逻辑往往是「算一笔账」,讲起来热闹,但真正的工程难度在并发下的数据一致性,而后者才是能撑起答辩内容的东西。

2.2 技术栈取舍:原生小程序配自建服务端,还是云开发一把梭

小程序端有四种常见选择,各自的代价差别很大,选之前先想清楚你要在答辩上「讲什么」。

方案上手成本后端工作量可讲述性真机踩坑密度适合的场景
原生小程序 + 自建服务端中高,事务、索引、接口分层都能讲中想拿中高分、愿意花时间写后端
原生小程序 + 云开发低低,后端被平台藏起来了低周期紧、只求跑通
跨端框架 + 自建服务端中高高高,编译产物出问题难排查需要同时出 H5 或 App
纯静态 + 假数据极低无极低不建议,答辩很难过关

这份方案走的是第一条路:小程序端用原生 WXML/WXSS/JS,服务端用一套常规的 MVC 后端(Java 系或 Node 系都行,结构是通用的),数据库用 MySQL。理由很直接——毕业设计的评分表里,「工作量」和「技术深度」是两项硬指标,云开发把后端藏进平台里了,老师问「库存是怎么扣的、并发下会不会超卖」,你只能回答「平台帮我处理了」,这句话在答辩现场基本等同于没做。

小程序端有几个绕不开的点,选型阶段就要认下来。主包体积有上限,商品详情页的富文本和图片多,通常要做分包,把订单、个人中心这类二级页面拆到子包里。图片不能放本地,得走对象存储或者图床,而且域名要配到小程序的合法域名列表里,否则真机上一个图都加载不出来。网络请求要统一封装,不能每个页面写一遍wx.request,否则后面加 token、加错误提示、加 loading 就是三十个文件挨个改。

服务端这边,接口按「控制器 → 服务 → 数据访问」三层组织就够了,不需要上微服务那套。毕业设计的体量,一个人维护的代码,分层太细反而是负担。真正需要认真对待的是三件事:统一响应格式、事务边界、以及幂等处理,这三样后面会单独展开。

2.3 数据表与状态机设计:订单状态是全系统的主线

商城系统的表不多,核心八张:用户、分类、商品、购物车、收货地址、订单、订单明细、轮播图。其中订单相关的两张表是重点,因为几乎所有跨表操作最后都汇聚到订单状态上。

先看商品表。这里最容易犯的错是用FLOAT或DOUBLE存金额,算一次运费就多出个 0.0000001,最后对不上账。金额字段一律用DECIMAL(10,2),这是硬规矩。

-- 商品表:展示字段与库存字段放在一起,后台改价改库存后前台立即可见 CREATE TABLE `goods` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品主键', `cat_id` BIGINT NOT NULL COMMENT '所属分类 ID', `title` VARCHAR(120) NOT NULL COMMENT '商品标题', `cover` VARCHAR(255) NOT NULL COMMENT '封面图 URL,必须是 https', `price` DECIMAL(10,2) NOT NULL COMMENT '售价,禁用 FLOAT', `origin_price` DECIMAL(10,2) DEFAULT NULL COMMENT '划线价,可为空', `stock` INT NOT NULL DEFAULT 0 COMMENT '可售库存', `sales` INT NOT NULL DEFAULT 0 COMMENT '销量,仅用于展示排序', `detail` MEDIUMTEXT COMMENT '富文本详情,存 HTML', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1 上架 0 下架', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_cat_status` (`cat_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

索引idx_cat_status对应的是「按分类查上架商品」这个最高频的查询,商品列表页每翻一页都要走它。不加这个索引,几百条数据看不出差别,数据量一上去就明显卡顿——虽然毕业设计的数据量通常不至于,但答辩时被问到「你建了哪些索引、为什么建」,能答出这条就够用了。

订单表的关键是状态字段。我一般把状态定义成固定的几个值,并且写死在代码里的枚举中,绝不在业务代码里到处写数字:

状态值含义允许的下一步
0待付款1 待发货、4 已取消
1待发货2 待收货、5 退款中
2待收货3 已完成、5 退款中
3已完成无(终态)
4已取消无(终态)
5退款中6 已退款
6已退款无(终态)
-- 订单主表:状态推进必须带「前置状态」条件更新,防止状态倒退 CREATE TABLE `orders` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '业务订单号,对外暴露用这个', `user_id` BIGINT NOT NULL, `total_amount` DECIMAL(10,2) NOT NULL COMMENT '服务端重算后的应付金额', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '见订单状态表', `receiver_name` VARCHAR(30) NOT NULL COMMENT '收货人,下单时快照', `receiver_phone` VARCHAR(20) NOT NULL, `receiver_addr` VARCHAR(200) NOT NULL, `idempotent_key` VARCHAR(64) NOT NULL COMMENT '前端生成的幂等键', `pay_time` DATETIME DEFAULT 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`, `idempotent_key`), KEY `idx_user_status` (`user_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';

这里有两个设计点值得单独说。一个是收货信息做快照:用户改了自己默认地址之后,历史订单里的收货地址不能跟着变,所以订单表里存一份拷贝,而不是存地址表的 ID 去关联。另一个是uk_user_idem这个唯一索引,它是防重复下单的最后一道防线,业务层查不到重复时,数据库还能兜住。

订单明细表存每个商品的快照——商品标题、下单时的单价、数量。不要只存 goods_id 然后查询时去 join 商品表,因为商品可能被改价甚至被删除,历史订单必须能独立呈现。

提示:所有涉及状态变更的 UPDATE,都把旧状态写进 WHERE 条件里,UPDATE orders SET status = 1 WHERE order_no = ? AND status = 0。影响行数为 0 就说明状态已经被别的请求改过了,这时候不要重试,直接返回。

3. 总体架构与模块划分:页面层、接口层、数据层怎么切

架构这一章在大纲里看着虚,落到交付物上其实很具体:前端目录怎么放、后端分几层、接口长什么样、一次下单请求要穿过哪些模块。这些东西定下来之后,剩下就是往格子里填代码,不会出现「写到一半发现要重构」的情况。

3.1 目录结构与模块边界:前端按页面拆,后端按域拆

小程序端的目录,我习惯按「页面 + 公共能力」两分。pages目录按业务域再分子目录,components放商品卡片、数量步进器、空状态这类可复用组件,utils放请求封装、金额格式化、时间格式化,api目录单独一层,把每个接口封成函数,页面里只调函数不写 URL。这一层的价值在于:接口地址改了、加了公共参数、换了鉴权方式,只改api目录和utils/request.js两个地方。

// utils/request.js —— 全局唯一的请求出口 const BASE_URL = 'https://your-domain.com/api'; function request(path, { method = 'GET', data = {}, loading = true } = {}) { if (loading) wx.showLoading({ title: '加载中', mask: true }); return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method, data, header: { 'content-type': 'application/json', // token 从本地存储取,登录后写入,过期由服务端返回 401 触发重登 'Authorization': wx.getStorageSync('token') || '' }, success(res) { const body = res.data; if (body.code === 0) { resolve(body.data); } else if (body.code === 401) { wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject(body); } else { wx.showToast({ title: body.msg || '请求失败', icon: 'none' }); reject(body); } }, fail(err) { // 真机断网、域名未配置合法域名都会走到这里 wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); }, complete() { if (loading) wx.hideLoading(); } }); }); } module.exports = { request };

这段封装里,code === 0是成功约定,401 单独拎出来做登录态失效处理,其他错误统一 toast。loading参数的作用是让某些静默请求(比如轮询订单状态)不弹遮罩,不然页面上会一直转圈。最后一个细节在fail回调——真机上图片裂了、接口 404、域名没加白名单,全都走这里,给它一句明确的提示,比让用户对着白屏发呆强。

服务端的模块按业务域划分:用户域(登录、授权、地址)、商品域(分类、商品、搜索)、交易域(购物车、订单、支付回调)、管理域(后台的商品与订单操作)。每个域内部再分控制器、服务、数据访问三层。这样切的好处是,订单相关的所有逻辑都在交易域里,不会出现「库存扣减写在商品服务里、订单状态改在控制器里」这种散落各处的情况。

3.2 接口约定:统一响应体、分页参数与错误码

接口不规范是毕设项目里最常见的隐性债务。前后端一个人写的时候感觉不到,等你写完三十个页面回头改一个字段名,就知道痛了。约定三件事就够了:响应体统一、分页参数统一、错误码分段。

{ "code": 0, "msg": "ok", "data": { "list": [], "total": 128, "page": 1, "pageSize": 10 } }

code的分段我一般这样定:0 成功;1xxx 参数错误;2xxx 业务规则不通过(库存不足、订单状态不允许);4xxx 鉴权失败(401 未登录、403 无权限);5xxx 服务端异常。分页请求统一用page和pageSize两个参数,服务端返回total让前端判断有没有下一页。这套东西定下来之后,列表页的代码几乎是复制粘贴改个路径,写起来非常快。

接口文档不需要多正式,一份 Markdown 表格就够:路径、方法、入参、出参示例、备注。答辩时把它打进附录,比口头描述有说服力得多。

3.3 关键链路拆解:从加购到收货的六次状态变更

把主链路画成文字版的时序,写代码前先在纸上过一遍,能省掉大量返工。一次完整的购物流程,数据在四个地方流转:小程序的本地存储、服务端的购物车表、订单表、商品表。

第一次状态变更发生在加购。如果用户没登录,购物车数据先写本地存储;登录后触发一次合并,把本地条目同步到服务端购物车表。这一步的坑在于「同一个商品加了两次」,合并时按 goods_id 累加数量,而不是插两条。

第二次是进入确认订单页。前端把选中的购物车条目 ID 传给服务端,服务端重新查一遍商品的价格和库存,算出租金、优惠(如果有)、总计,返回给前端做展示。这一步的金额只是「预览值」,不落库。

第三次是提交订单,也就是下单。服务端在一个事务里做四件事:校验幂等键、逐条扣库存、写订单主表、写订单明细。任何一步失败整个事务回滚,库存也不会被扣掉。

第四次是支付回调。微信支付成功后,支付平台会回调你的服务端,服务端把订单从「待付款」推到「待发货」。这一步是整个系统里最容易出问题的地方,后面单独讲。

第五次是后台点发货,订单从「待发货」到「待收货」。第六次是用户点确认收货,订单到「已完成」,同时可以把商品销量加上去。

触发动作前置状态后置状态触发方是否需要事务
提交订单无待付款小程序是(扣库存 + 写两表)
支付成功回执待付款待发货支付平台是(更新状态 + 记流水)
超时未付待付款已取消定时任务是(回滚库存)
后台发货待发货待收货管理端否
确认收货待收货已完成小程序是(更新状态 + 加销量)
申请退款待发货/待收货退款中小程序否

这张表建议直接放进论文的「系统设计」章节,配上文字说明,老师一眼就能看出你把状态管住了。

4. 核心模块落地实现:商品、购物车、订单、支付四个抓手

到这一层,思路已经清楚了,缺的是能直接抄的代码骨架。下面四块是这套方案里最值得细看的地方,每一块都给出关键片段和设计说明,参数怎么改、失败时看什么,都写在说明里。

4.1 商品列表与详情:分页加载、图片懒加载与富文本兜底

列表页的第一个问题是分页。小程序没有内置的分页组件,onReachBottom触发加载下一页时,最容易出现的 bug 是「用户在上一页还没返回时又滑到底」,导致同一页请求发两次、列表里出现重复数据。

// pages/goods/list.js const { request } = require('../../utils/request'); Page({ data: { list: [], page: 1, pageSize: 10, total: 0, loading: false // 请求锁,防止重复触发 }, onLoad(query) { this.catId = query.catId || 0; this.loadPage(1); }, onReachBottom() { const { list, total, page } = this.data; // 已加载数量 >= 总数,或者正在请求,都不再触发 if (list.length >= total || this.data.loading) return; this.loadPage(page + 1); }, async loadPage(page) { this.setData({ loading: true }); try { const res = await request('/goods/list', { data: { catId: this.catId, page, pageSize: this.data.pageSize } }); this.setData({ // 用 concat 而不是 push,配合页码去重更安全 list: page === 1 ? res.list : this.data.list.concat(res.list), total: res.total, page }); } finally { this.setData({ loading: false }); } } });

loading这个字段就是请求锁,看着简单但很有必要。onReachBottom在快速滑动时会连续触发好几次,没有锁的话就是三个相同的请求同时在飞。另外page === 1时用覆盖而不是拼接,这样下拉刷新和切换分类可以复用同一个方法,不用写两套。

商品详情页要处理富文本。后台用富文本编辑器录入的商品详情是一段 HTML,小程序里用rich-text组件渲染。这里有个限制:rich-text支持的标签是白名单的,video、iframe、部分style属性都不生效。我的处理方式是在后台录入时就限制可用标签,前端再加一层兜底——如果详情为空,显示一段默认文案,避免详情页下面一大片空白。

图片方面,列表页的封面图用mode="aspectFill"保证不拉伸变形,详情页的大图加lazy-load。图片地址必须走 https,而且域名要提前配置到小程序后台的「downloadFile 合法域名」里,否则真机上全是裂图,开发者工具里却一切正常——这个坑后面还会提。

4.2 购物车:本地态与数据库态的双写策略

购物车是整套系统里状态最分裂的模块,因为用户可能在未登录状态下加购。我的做法是双轨:未登录写本地,登录后合并到服务端,之后所有操作都以服务端为准。

// utils/cart.js —— 未登录时用本地存储,登录后走接口 const KEY = 'cart_local'; function addLocal(goods) { const cart = wx.getStorageSync(KEY) || []; const idx = cart.findIndex(i => i.goodsId === goods.id); if (idx > -1) { cart[idx].num += goods.num; // 同一商品累加,不新增一条 } else { cart.push({ goodsId: goods.id, title: goods.title, cover: goods.cover, price: goods.price, // 价格仅用于展示,结算以服务端为准 num: goods.num, checked: true }); } wx.setStorageSync(KEY, cart); } function mergeToServer() { const cart = wx.getStorageSync(KEY) || []; if (!cart.length) return Promise.resolve(); // 只上传 goodsId 和 num,价格一律由服务端重查 const payload = cart.map(i => ({ goodsId: i.goodsId, num: i.num })); return request('/cart/merge', { method: 'POST', data: { items: payload }, loading: false }) .then(() => wx.removeStorageSync(KEY)); // 合并成功后清本地,避免重复合并 }

这段代码有三个设计点。第一,本地存的price只用来在购物车页面显示,任何时候都不参与结算计算,结算金额必须服务端重新算——否则用户改一下本地存储就能一块钱买手机。第二,合并接口传的只有商品 ID 和数量,服务端要校验商品是否还在售、库存够不够,不合格的直接丢弃并返回提示。第三,合并成功后立刻清本地存储,否则下次登录会再合并一遍,数量翻倍。

服务端的购物车表用user_id + goods_id做唯一索引,插入用INSERT ... ON DUPLICATE KEY UPDATE num = num + ?,一条 SQL 搞定「有就累加、没有就新增」,不用先查再判断。

注意:购物车页面显示的价格,要在每次进入页面时重新从服务端拉一次。商品降价了但购物车里还是旧价,用户下单时看到金额变了会以为系统出错——这个体验问题在实际项目里被投诉的概率很高。

4.3 订单创建:事务边界、库存扣减与幂等键

下单是整个系统的心脏。这一段代码写对了,答辩时最容易被问的技术点你都能接住;写错了,超卖、重复订单、金额对不上,一个都跑不掉。

先看库存扣减的 SQL。关键在于把判断条件写进 WHERE 里,让数据库来保证原子性,而不是先 SELECT 查库存再 UPDATE。

<!-- GoodsMapper.xml:带条件的原子扣减 --> <update id="deductStock"> UPDATE goods SET stock = stock - #{num}, sales = sales + #{num} WHERE id = #{goodsId} AND status = 1 AND stock >= #{num} </update>

这条 UPDATE 返回的是影响行数。返回 1 说明扣成功,返回 0 说明要么商品下架了,要么库存不够了。整个判断在数据库的一次行锁里完成,不存在两个请求同时读到「还剩 1 件」然后都扣成功的可能。

@Service public class OrderService { @Resource private GoodsMapper goodsMapper; @Resource private OrderMapper orderMapper; @Resource private OrderItemMapper itemMapper; /** * @param idempotentKey 前端在进入确认页时生成的 UUID,一次提交一个 * @param items 只信任 goodsId 和 num,金额由服务端重算 */ @Transactional(rollbackFor = Exception.class) public String createOrder(Long userId, String idempotentKey, List<OrderItemDTO> items, AddressDTO addr) { // 1. 幂等前置检查:同一个用户 + 同一个键,直接返回上次的订单号 Order exist = orderMapper.selectByIdempotentKey(userId, idempotentKey); if (exist != null) { return exist.getOrderNo(); } // 2. 逐条扣库存,任何一条失败整个事务回滚 BigDecimal total = BigDecimal.ZERO; for (OrderItemDTO item : items) { int affected = goodsMapper.deductStock(item.getGoodsId(), item.getNum()); if (affected == 0) { throw new BizException("商品已下架或库存不足"); } Goods g = goodsMapper.selectById(item.getGoodsId()); // 金额用 BigDecimal 累加,保留两位,禁止用 double total = total.add(g.getPrice() .multiply(BigDecimal.valueOf(item.getNum())) .setScale(2, RoundingMode.HALF_UP)); } // 3. 写订单主表 + 明细表,收货信息做快照 Order order = new Order(); order.setOrderNo(generateOrderNo()); // 时间戳 + 用户尾号 + 随机数 order.setUserId(userId); order.setTotalAmount(total); order.setStatus(OrderStatus.WAIT_PAY.getCode()); order.setIdempotentKey(idempotentKey); order.setReceiverName(addr.getName()); // 快照,不存地址表 ID order.setReceiverPhone(addr.getPhone()); order.setReceiverAddr(addr.getFullAddress()); orderMapper.insert(order); for (OrderItemDTO item : items) { itemMapper.insert(buildItem(order.getId(), item)); } return order.getOrderNo(); } }

关于事务边界,有一点很容易被忽略:@Transactional只对 RuntimeException 生效,业务里抛的BizException如果是受检异常,事务不会回滚。所以要么让BizException继承RuntimeException,要么在注解里显式写rollbackFor = Exception.class。上面这段用的是后一种,保险。

幂等键的生成时机也要注意。不能在下单按钮的点击事件里生成,否则用户连点五次就是五个不同的键,防不住。正确做法是在进入确认订单页时生成一次,存在页面的 data 里,整个页面生命周期内复用。

4.4 支付回调与补单:状态机推进的唯一入口

微信支付需要企业主体和商户号,个人开发者拿不到。毕业设计里通常的替代方案是「模拟支付」:前端点支付按钮,调一个/pay/mock接口,服务端直接把订单推到待发货。这套方案能演示,但答辩时容易被问「真实支付怎么对接」,所以我在实现的时候会把模拟支付和真实回调写成同一套接口签名,模拟接口只做一件事——伪造一次回调调用。这样后面接入真实支付时,业务代码一行都不用改。

/** * 支付结果处理的唯一入口,真实回调与模拟支付都走这里 */ @Transactional(rollbackFor = Exception.class) public void handlePaySuccess(String orderNo, String tradeNo) { Order order = orderMapper.selectByOrderNo(orderNo); if (order == null) { return; // 订单不存在,直接忽略,不抛异常 } // 只允许「待付款 -> 待发货」,已经推进过的直接返回,天然幂等 int affected = orderMapper.updateStatus(orderNo, OrderStatus.WAIT_PAY.getCode(), OrderStatus.WAIT_SHIP.getCode()); if (affected == 0) { return; // 并发回调被别的请求抢先处理了 } // 只有真正推进成功的那一次,才记录支付流水 payLogMapper.insert(orderNo, tradeNo, new Date()); }

这段代码的核心思想是「用状态机做幂等」。支付平台的回调可能重复发好几次(网络超时重试是常态),如果每次回调都去加销量、加积分、发消息,数据就乱了。把「前置状态」写进 UPDATE 的 WHERE 条件里,只有第一次能把 0 改成 1,第二次影响行数为 0 直接返回,什么都不做。这比在代码里写if (order.getStatus() == 待付款)可靠得多,因为那个判断在读和写之间有时间窗口。

还有一件必须做的事:补单。用户支付成功了但回调没到你服务端(网络抖动、服务重启),订单会一直挂在待付款。通常的做法是订单列表页点进去时,前端带订单号调一次「查询支付结果」的接口,服务端主动去支付平台查一遍,查到已支付就手动触发一次handlePaySuccess。这套兜底逻辑不复杂,但它是「这系统做得挺完整」和「这系统只能演示」的分水岭。

超时未支付也要处理。定时任务每五分钟扫一次「创建超过 30 分钟且状态为待付款」的订单,把状态改成已取消,同时把库存加回去。加库存用stock = stock + num,不需要条件判断,因为回滚是纯粹的加法。

5. 避坑与排查:五处最容易在答辩前一晚炸掉的地方

下面这五条,每一条都是我见过或者亲自踩过的。共同特征是:开发者工具里一切正常,真机上、演示时、并发下才出问题,而且排查起来很费时间。

5.1 真机白屏、图片全裂、接口全 404:合法域名没配

现象:模拟器上跑得好好的,扫码到真机上打开,页面白屏或者图片全是灰块,控制台报「不在以下 request 合法域名列表中」。

原因:小程序的网络请求和下载文件都有域名白名单限制,开发者工具里可以勾选「不校验合法域名」绕过,真机没有这个开关。另外域名必须是备案过的 https,端口只能是 443。

解决:在小程序管理后台的「开发设置 → 服务器域名」里,把接口域名加到 request 域名,把图片和文件域名加到 downloadFile 域名。本地开发阶段如果拿不到备案域名,就在开发者工具里勾上「不校验合法域名」做调试,但演示前一定要换成真域名并配好白名单,同时把工具里的勾去掉再测一遍——这一步不做,答辩现场大概率翻车。

5.2 登录态丢失:code 只能用一次,session_key 会过期

现象:用户第一次进小程序能正常下单,过一会儿或者杀掉进程重进,就变成未登录,订单列表空了。

原因:wx.login拿到的 code 是一次性的,用过就失效;换来的session_key也有有效期,而且用户长时间不操作会被平台回收。很多实现是把 openid 当 token 直接用,前端存起来再也不更新,一旦服务端换了会话就得重新登录。

解决:走「code 换 token」的标准流程。前端wx.login拿 code,传给服务端,服务端调接口换 openid 和 session_key,自己生成一个有过期时间的 token(比如 7 天)返回给前端,前端存本地并在request的 header 里带上。服务端返回 401 时,前端清掉本地 token 并静默重新登录一次,而不是直接跳登录页——用户正在结算时被弹到登录页,体验很差。

5.3 金额算错、库存超卖:类型选错与判断位置选错

现象:订单金额偶尔差几分钱;两个人同时下单,库存 1 件的商品卖出去了 2 件。

原因:金额用 float 或 double,浮点累加必然产生误差;库存是先 SELECT 查出来判断够不够,再 UPDATE 扣减,两条语句之间有空隙,两个请求都能通过检查。

解决:金额字段和 Java 里的变量都用 DECIMAL / BigDecimal,运算时显式指定setScale(2, RoundingMode.HALF_UP)。库存扣减必须用一条带条件的 UPDATE,靠影响行数判断成败,不要先查后改。如果业务上允许少量超卖(比如秒杀),再另说,但普通商城不允许。

5.4 支付回调重复、订单状态倒退

现象:一次支付,订单的销量被加了三次;或者订单先变成待发货,过一会儿又变回待付款。

原因:支付平台回调是「至少一次」语义,网络超时就会重发;状态更新没带前置状态条件,任何一次回调都能改状态,包括延迟到达的旧回调。

解决:所有状态变更都带前置状态条件,影响行数为 0 就认为已经被处理过,直接返回成功(注意是返回成功,不是返回失败,否则平台会一直重发)。回调处理逻辑要写成幂等的,重复执行结果一致。

5.5 上拉加载出现重复数据、下单按钮连点生成多单

现象:列表滑到底部,同一个商品出现两三次;用户手快点两下,生成两个一模一样的订单。

原因:分页请求没有并发锁,快速滑动时同一页请求发了多次;下单按钮没有防抖,也没有幂等键。

解决:分页加loading请求锁,同时在拼接数据时按主键去重。下单按钮点击后立刻setData({ submitting: true })并把按钮置灰,请求结束再恢复;服务端用幂等键 + 唯一索引做最后兜底。前端防抖是体验,服务端幂等是正确性,两个都要有。

6. 验收与答辩:用一份清单和 20 分钟演示把系统讲圆

交付前一周,我会把系统按下面这张清单完整过一遍。清单的好处是它把「我觉得应该没问题」变成「我一条条点过了」。

验收项检查方式通过标准
数据库可重建删库,重新执行建表脚本 + 初始化数据无报错,前台能正常显示商品
主链路闭环用一个全新账号下单到确认收货六个状态全部走通,订单列表同步
库存一致性下单前后对比商品表 stock差值等于订单数量,取消后回滚
金额准确性下单金额与商品单价 × 数量手工核对分毫不差
管理端可用后台改价、下架、发货各操作一次前台立即反映变化
真机运行关掉开发者工具的域名校验豁免图片、接口全部正常
异常兜底断网、重复点击、未登录访问订单页有明确提示,不白屏、不崩溃

演示脚本按「一条主线 + 三个亮点」组织。主线就是刚说的下单全流程,用一台手机投屏,从首页点进去,八分钟走完。三个亮点挑你投入最多的部分:库存扣减的原子性、支付回调的幂等处理、管理端改数据后前端的实时反映。每讲一个,先说「如果不这么做会出什么问题」,再说「我怎么做的」,最后说「效果是什么」。这个结构比平铺直叙地念功能列表有效得多,因为它证明的是你思考过,而不只是你敲过代码。

老师常问的三个问题,提前准备好答案:一是「库存超卖你怎么防的」,答带条件的 UPDATE 加影响行数判断;二是「支付回调重发怎么办」,答状态机幂等加唯一索引;三是「订单状态能不能被随意改」,答所有变更都带前置状态条件,前台没有直接改状态的接口。这三个问题答得出来,技术分基本稳了。

提示:答辩前一晚,把数据库导出成一份.sql文件,和源码、教程一起放到同一个目录下。现场如果电脑出问题,换一台机器导入脚本就能跑,这比任何讲解都有说服力。

从那以后我每次带项目,验收前都强制走一遍「删库重建 + 真机跑主链路」这两步——因为真正会让你在最后一刻手忙脚乱的,从来不是没写完的功能,而是那些你以为已经好了的东西。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 10:34:20

陈卫军《持续成交》核心观点:成交不是运气,是系统

如果说《赚钱思维》解决的是"客人从哪来"&#xff0c;那陈卫军的第二本书《持续成交》解决的就是"客人来了怎么买、买完怎么再来"。这篇文章带你读懂这本书的核心观点。 陈卫军有一个判断&#xff0c;很多老板听完会沉默&#xff1a;一次成交靠嘴&#xff…

作者头像 李华
网站建设 2026/10/27 10:33:18

用 Codex 做历史解说视频工作流,Skill 要接素材接口

用 Codex 做历史解说&#xff0c;片子是按工作流往下走的。解说稿先按句拆开&#xff0c;每一句配一段空镜&#xff0c;再配旁白&#xff0c;最后导出到剪映里调整。配空镜这一步做成 Skill。句子是「中国古代宫殿与军队列阵历史纪录片场景」时&#xff0c;Skill 调用视频素材接…

作者头像 李华
网站建设 2026/10/27 10:32:39

从零理解差分升级 JojoDiff + Uzlib:一份补丁,怎样还原成新固件?

从零理解差分升级 JojoDiff Uzlib&#xff1a;一份补丁&#xff0c;怎样还原成新固件&#xff1f; 我们已经能在电脑上生成升级、回退差分包&#xff0c;也能用上位机验证还原。但真正准备移植时&#xff0c;仍可能卡在这些问题上&#xff1a;旧程序地址填在哪里&#xff1f;谁…

作者头像 李华
网站建设 2026/10/27 10:32:24

Linux与Shell总结

一、Linux 与 Shell 基础AI 工程岗位常用 Linux 运行程序、查看日志和排查服务问题。先掌握这些命令&#xff1a;1. 文件与目录pwd # 显示当前目录 ls -la # 列出文件&#xff0c;包括隐藏文件 cd /path/to/project # 切换目录 mkdir data …

作者头像 李华
网站建设 2026/10/27 10:32:23

【落雨】三星Note8.0 N5100刷机指南GT-N5100刷LineageOS 16.0

GT-N5100 刷 LineageOS 16.0 操作清单 生成时间&#xff1a;2026-10-25 设备&#xff1a;Samsung Galaxy Note 8.0 (GT-N5100 / codename n5100 / 国行 ZC 固件 / Exynos 4412 / 2GB RAM) 需要先备份并且刷入TWRP TWRP 官方下载链接页面&#xff1a;https://dl.twrp.me/n5100/ …

作者头像 李华