简介:基于微信小程序与NetCore、layui技术构建的多店铺网上购物商城系统,面向毕业设计选题、小程序电商开发学习者及需要快速搭建可运行商城的中小型项目团队。项目完整覆盖微信小程序端、管理后台、插件管理及WebApi接口层,源码中可见订单处理、商品管理、用户与导出管理等核心业务模块,便于理解多店铺商城的整体架构与实现思路。资源共1915个文件,以C#后端代码(798个cs)、小程序页面(wxml/wxss)、JavaScript脚本、HTML视图及数据库文件为主,压缩包约10.34MB,文件类型覆盖前后端与配置,目录结构清晰。包内提供MySQL数据库脚本、数据连接配置及插件配置示例,并附带安装使用说明,可辅助完成从环境配置到功能验证的全流程。已有3592人学习下载,适合需要完整商城系统参考或作为毕业设计方案的开发者使用。
1. 为什么这套商城源码让你卡在启动而不是卡在功能
看到“基于微信小程序的在线网上购物商城系统”这个标题,很多人的第一反应是:代码都齐了,导入就能跑。但我带过的毕设项目里,真正把时间耗进去的从来不是写功能,而是环境。数据库导不进、接口连不上、图片加载不出来,查到最后,问题全出在 MySQL 版本、小程序白名单和配置文件这几处。这套项目的架构其实不复杂:小程序端负责展示和交互,后端对着几张表做增删改查,数据库把用户、商品、订单串起来。真正值钱的,是那份安装使用说明背后的环境思维。下面我按自己平时的做法,把从零跑通这套商城系统的完整路径讲一遍,也把最容易让人翻车的几个坑点列清楚。适合正在做毕设选题、想快速搭出一个能演示的商城,或者想拿这套代码改造成个人项目的同学。
2. 拆解商城系统的三层结构:先看数据库怎么设计
2.1 小程序端、后端接口、MySQL 三层怎么配合
常见做法就是前后端分离:小程序原生框架负责页面,后端用 Spring Boot 或者 Node.js 提供接口,数据落在 MySQL。小程序端不是一个常规的 Web 应用,它没有 cookie 这套东西,登录状态靠自定义 token。后端只做一件事:接收请求、校验参数、查库、返回 JSON。数据库则是整套系统的地基,表没建好,后面接口怎么写都不顺手。
这套系统按照毕设标准,最少要覆盖六个界面:首页、分类、购物车、订单列表、个人中心、商品详情。后端接口大概十个左右:登录、商品列表、商品详情、加入购物车、购物车列表、修改数量、结算下单、订单列表、取消订单、模拟支付。功能不多,但足够把数据库设计、接口封装、状态机、事务这些点讲全。如果你把它当成商业项目,缺少售后、物流、营销这些模块,但作为毕设或课程设计,结构是完整的。
我先说为什么选 MySQL。毕设场景下,MySQL 安装简单、资料多、Workbench 和命令行都能操作,导出 .sql 文件给导师也方便。后端选 Spring Boot 是因为它自带连接池和事务管理,不用自己造轮子。小程序端用原生 WXML/WXSS/JS,不去碰 uniapp 那种跨端框架。毕设答辩时,你把原生小程序的项目结构讲清楚,比讲框架封装更有说服力。
2.2 按“用户、商品、订单、购物车”四张核心表建库
我一般会把表分成两类:一类是基础数据表,用户、商品、分类;一类是流程数据表,购物车、订单、订单明细。下面这套 SQL 是一个可运行的最小集,关键字段我都做了注释。
-- 用户表:存储微信用户与本地用户信息 CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `openid` varchar(64) NOT NULL COMMENT '微信openid,唯一', `nickname` varchar(50) DEFAULT '' COMMENT '昵称', `avatar` varchar(255) DEFAULT '' COMMENT '头像url', `phone` varchar(20) DEFAULT '' COMMENT '手机号', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';-- 分类表:商品分类,sort_order 控制前端排序 CREATE TABLE `t_category` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '分类名', `sort_order` int(11) DEFAULT 0 COMMENT '排序权重', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类表';-- 商品表:status 控制上下架,price 用 decimal CREATE TABLE `t_product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `category_id` int(11) NOT NULL COMMENT '所属分类', `name` varchar(100) NOT NULL COMMENT '商品名称', `subtitle` varchar(200) DEFAULT '' COMMENT '副标题', `main_image` varchar(255) DEFAULT '' COMMENT '主图路径', `price` decimal(10,2) NOT NULL COMMENT '单价,保留两位小数', `stock` int(11) NOT NULL DEFAULT 0 COMMENT '库存', `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '1上架 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';-- 购物车表:同一用户同一商品只保留一条记录 CREATE TABLE `t_cart` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `product_id` int(11) NOT NULL, `quantity` int(11) NOT NULL DEFAULT 1 COMMENT '数量', `checked` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否选中,1选中 0未选', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_product` (`user_id`,`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表';-- 订单主表:一笔订单对应多个商品明细 CREATE TABLE `t_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号,业务上唯一', `user_id` int(11) NOT NULL, `total_amount` decimal(10,2) NOT NULL COMMENT '商品总额', `pay_amount` decimal(10,2) NOT NULL COMMENT '实付金额', `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0待付款 1已付款 2已发货 3已完成 4已取消', `receiver_name` varchar(50) DEFAULT '', `receiver_phone` varchar(20) DEFAULT '', `receiver_address` varchar(255) DEFAULT '', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `pay_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';-- 订单明细表:商品价格必须做快照,不能实时读商品表 CREATE TABLE `t_order_item` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_id` int(11) NOT NULL, `product_id` int(11) NOT NULL, `product_name` varchar(100) NOT NULL, `product_image` varchar(255) DEFAULT '', `price` decimal(10,2) NOT NULL COMMENT '下单时的快照价格', `quantity` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `idx_order` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';为什么 t_order_item 里要冗余 product_name 和 price?因为商品价格和名称会改,订单明细必须保存下单那一刻的快照,否则对账对不上。关于这个设计,答辩时导师常会追问,你把“快照”两个字说出来,基本不会被问死。
2.3 订单状态流转和字段细节:这是答辩时的加分点
status 我用了 tinyint 而不是 varchar,因为枚举状态在程序里更好处理,默认值 0 表示待付款。推荐状态机:0 待付款 → 1 已付款 → 2 已发货 → 3 已完成;任何状态下都可以有 4 已取消。取消的时机要单独说:待付款时取消直接改状态;已付款想取消,就要走退款逻辑,毕设里一般简化为直接改状态,但你要能说清楚“这里实际项目中会对接退款接口”。
字段类型有四个细节直接影响系统能不能稳定跑起来。第一,价格用 decimal(10,2) 而不是 float 或 double,float 做精度计算会出问题,比如 0.1 加 0.2 不等于 0.3。第二,库存用 int,下单时用乐观锁更新,防止库存变负数,具体 SQL 写在第 4 章。第三,所有表默认 utf8mb4,用 utf8 会在用户填 emoji 时直接写入失败。第四,t_cart 里加唯一索引 uk_user_product,这是很多人翻车的地方。如果不用唯一索引,前端连点两次加入购物车会生成两条重复记录,结算时商品数量永远对不上。
3. 把源码跑起来:数据库导入与项目安装使用说明
3.1 需要准备的环境清单
我按“最小可运行”的原则列一下这份清单:
| 组件 | 推荐版本 | 用途 |
|---|---|---|
| JDK | 8 或 11 | 后端运行环境 |
| Maven | 3.6 以上 | 后端依赖管理 |
| MySQL | 5.7 或 8.0 | 数据库 |
| 微信开发者工具 | 稳定版即可 | 小程序调试 |
| 小程序 AppID | 注册后即可获得 | 联调登录功能 |
如果后端是 Node.js 版本,对应装 Node 16 以上就行。不过毕设选择 Spring Boot 的更多,后面的命令和配置我按 Spring Boot 讲。
3.2 导入数据库与修改后端连接参数
拿到 .sql 文件后,用命令行导入。Windows 上要先确认 MySQL 的 bin 目录加了环境变量,否则命令会提示“mysql 不是内部或外部命令”。注意在 cmd 里执行,PowerShell 对重定向的支持不一致。
mysql -uroot -p < shop.sql导入完成后验证一下表是否齐全。
mysql -uroot -p -e "use shop; show tables;"看到 t_user、t_category、t_product、t_cart、t_order、t_order_item 这六张表,说明数据库导入成功。然后修改后端配置文件,文件名是 application.yml 或 application.properties,按项目说明改这三处就够。
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456这里至少有三个坑。第一,driver-class-name 在 MySQL 5.7 和 8.0 里不一样,8.0 必须写 com.mysql.cj.jdbc.Driver,写老的 com.mysql.jdbc.Driver 启动直接报错。第二,连接串里的 characterEncoding=utf8 是 JDBC 驱动的兼容写法,你不用改成 utf8mb4,表结构用 utf8mb4 就行。第三,serverTimezone 不配的话,日期字段会差 8 个小时,导出来的订单时间全都不对。
3.3 小程序端 AppID、合法域名与请求封装
导入小程序项目到微信开发者工具,先用测试号也能预览页面,但登录功能要测 openid 就必须用自己的 AppID。在 project.config.json 里改:
{ "appid": "你的小程序AppID", "projectname": "shop-mall", "setting": { "urlCheck": false } }本地调试阶段,把 urlCheck 设为 false,这样请求 http://localhost:8080 不会被拦截。这是开发阶段的做法,上线前必须把这项关掉,并在小程序后台配置 https 合法域名。
接口请求我习惯统一封装,新建一个 utils/request.js。下面这段是核心:
const BASE_URL = 'http://localhost:8080/api' function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method: method, data: data, header: { 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') || '' }, success(res) { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data) } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }) reject(res) } }, fail(err) { reject(err) } }) }) } module.exports = { request, BASE_URL }逻辑说明:把公共部分全部收敛到 request 函数里,每个页面只需要传 path、method、data,不需要自己拼 URL。token 从本地存储读取,每次请求自动带上,后端再统一做鉴权。这里和后端约定返回结构是 { code, msg, data },code 为 0 表示成功,这样前端处理逻辑可以统一,不用每个页面写重复的错误提示。
4. 核心功能代码怎么对接:登录、商品、购物车到下单
4.1 微信登录换 openid:本地 session 的完整链路
小程序不能把用户的 openid 直接暴露给前端,正确流程是:前端 wx.login 拿临时 code,传给后端,后端拿 code 换 openid,再生成 token 返回,前端把 token 存缓存。后端 Controller 大致如下:
@RestController @RequestMapping("/api/user") public class UserController { @PostMapping("/login") public Result login(@RequestBody LoginRequest req) { // 1. 调用微信接口,用 code 换取 openid String openid = wxService.code2Session(req.getCode()); // 2. 查库,没有则注册新用户 User user = userMapper.findByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setNickname("微信用户"); userMapper.insert(user); } // 3. 生成 token 并保存,后续请求靠 token 辨认身份 String token = UUID.randomUUID().toString().replace("-", ""); tokenService.save(user.getId(), token); return Result.success(token); } }逻辑说明:code2Session 是后端调用微信接口,不是前端直接调。token 用 UUID 生成,存到一张 token 表或 Redis 里,有效期一般设 7 天。每次请求经过拦截器校验 token,查不到就返回 401,前端收到 401 就跳回登录页重新 wx.login。
4.2 商品列表和首页:图片地址与缓存的细节
商品列表接口是整套系统里最简单的,但有两个细节容易出问题。第一个是图片地址,开发阶段用相对路径,比如 /upload/1.jpg,小程序端再拼上后端地址。第二个是缓存,2019 年后小程序对 request 并发有限制,商品列表这种不常变的数据适合做本地缓存,设置缓存过期时间。
async function getProducts(categoryId) { const cacheKey = 'products_' + categoryId const cached = wx.getStorageSync(cacheKey) if (cached && Date.now() - cached.time < 5 * 60 * 1000) { return cached.data } const data = await request('/product/list?categoryId=' + categoryId) wx.setStorageSync(cacheKey, { time: Date.now(), data: data }) return data }逻辑说明:缓存时间设 5 分钟,避免每次进首页都打后端。换分类时 categoryId 不同,缓存 key 不同,不会串数据。也可以把缓存时间做成参数,不同接口自己控制,比如轮播图缓存 10 分钟,购物车列表不缓存。
4.3 购物车到订单:事务与乐观锁
购物车选中商品后点“去结算”,后端要做三件事:生成订单主表、生成订单明细、扣库存。中间任何一步失败,整个操作必须回滚。用 @Transactional 注解最省事。
@Transactional(rollbackFor = Exception.class) public Long createOrder(OrderCreateDTO dto) { // 1. 生成订单主表,状态为待付款 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setStatus(0); order.setPayAmount(calculateAmount(dto.getItems())); orderMapper.insert(order); // 2. 插入订单明细,价格取商品快照 for (ItemDTO item : dto.getItems()) { orderItemMapper.insert(buildItem(order.getId(), item)); } // 3. 乐观锁扣库存:只有库存充足才更新成功 int rows = productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows == 0) { throw new RuntimeException(item.getProductName() + " 库存不足"); } return order.getId(); }注意最后一步的 deductStock,对应 SQL 是:
UPDATE t_product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}这段 SQL 是防止超卖的关键。先查库存再减库存的写法,在并发下必翻车,两条请求同时查到库存是 1,然后都执行减 1,库存就变 -1 了。用 UPDATE 语句里带 stock >= quantity 条件,数据库行锁会保证只有一条更新成功,影响行数为 0 就说明库存不足。
5. 本地部署避坑记录:这五个问题占了八成交付事故
5.1 请求一直 fail:没有配置合法域名
现象:模拟器里页面能打开,但所有 wx.request 都报 fail,控制台提示 url not in domain list。
原因:小程序正式环境只允许请求配置过的 https 域名,http://localhost 不在白名单里。
解决:开发阶段在微信开发者工具右上角“详情→本地设置”勾选“不校验合法域名”。上线前必须在小程序后台配置服务器域名,而且必须是备案过的 https 域名。我见过有人把 urlCheck 关掉后直接打包上传体验版,结果测试同学手机上一片空白,就是这个原因。
5.2 数据库连接报错:Public Key Retrieval is not allowed
现象:后端启动时报错,提示 Public Key Retrieval is not allowed。
原因:MySQL 8.0 默认使用 caching_sha2_password 认证,JDBC 客户端默认不允许获取公钥。
解决:在 JDBC URL 末尾加 allowPublicKeyRetrieval=true。如果不想改连接串,也可以把用户改成 mysql_native_password 认证方式,部署从简的话直接改连接串更快。注意这个参数只影响连接建立,不影响数据安全。
5.3 中文和 emoji 写不进库
现象:用户昵称带表情时插入报错,提示 Incorrect string value: '\xF0\x9F\x98\x80'。
原因:表或字段是 utf8 编码。utf8 最多存 3 字节,emoji 占 4 字节,直接写入失败。
解决:建表用 utf8mb4,同时注意整库、表和连接的字符集保持一致。已经建好的表执行 ALTER TABLE t_user CONVERT TO CHARACTER SET utf8mb4。这里有个容易漏的点:如果只改表不改连接参数,写入时还是会报错,连接 URL 里 characterEncoding=utf8 是驱动兼容写法,不用改成 utf8mb4。
提示:在答辩现场演示导入数据时临时改字符集非常被动,建表 SQL 一开始就统一写成 utf8mb4。
5.4 订单重复提交,库存变成负数
现象:快速点两次提交订单,生成了两笔订单,库存出现负数。
原因:前端没有做按钮防抖,后端扣库存也只判断了查询出来的库存大于 0,不是原子操作。
解决:前端在提交按钮上做 loading 状态,禁止重复点击。后端扣库存改成 UPDATE ... WHERE stock >= quantity 这种乐观锁写法。另外给 t_order 的 order_no 加唯一索引,即使前端并发请求,也只能生成一笔订单。
5.5 模拟支付跑不通
现象:点击微信支付没反应,后端报 mch_id 不存在。
原因:真实微信支付需要商户号,个人主体小程序没有资格申请。
解决:毕设方案常见的做法是模拟支付。点击支付按钮直接把订单状态改成已付款,同时记录支付时间。答辩时主动说明这里是模拟支付,实际项目会对接微信支付统一下单接口,该接口需要商户号和 API 密钥。千万不要为了过演示硬接真实支付,流程走不通不说,还容易泄露密钥。
6. 上线前和答辩前的验证技巧
我每一套毕设项目交付前都会做一轮“从零还原”测试:删掉本地数据库,重新执行一遍安装使用说明,确认每一步都能复现。因为答辩现场用的机器环境和你开发机不一样,能复现才叫交付完成。
第一,一定要用真机扫码预览,不要只在模拟器里演示。模拟器底部导航栏高度、iPhone 安全区、网络请求行为都和真机不同。特别是顶部导航栏,不同机型胶囊按钮位置不一样,可以用 wx.getMenuButtonBoundingClientRect 动态计算页面顶部占位高度。第二,给后端加一个健康检查接口,启动后先访问 /api/health 确认服务活着再打开小程序页面,能省很多排查时间。第三,提交代码前把日志级别调到 debug,现场出问题能最快定位,答辩完再改回 info。第四,把模拟支付那段逻辑讲清楚,这是导师最关心的“真实性和简化点”,主动说比被问到好。
这套系统做得值不值,我自己的判断标准很简单:建表时顺手把订单明细快照和库存扣减做成规范写法,就已经超过大多数毕设了。很多人跑不起来不是代码不行,是懒得看安装使用说明,连 MySQL 版本都没核对。别做那种人。希望帮到你。
本文还有配套的精品资源,点击获取