简介:面向高校计算机相关专业学生与初学者,这是一套可直接用于毕业设计、课程设计的微信小程序商城完整项目包,重点解决选题落地、页面搭建、数据组织与答辩演示等需求。压缩包共68个文件,约3.31MB,以30张PNG界面与商品素材、9个JS业务逻辑、7个WXML页面结构、7个WXSS样式和7个JSON配置为主,并附数据库脚本、部署说明和较为完整的代码注释,涵盖首页、商品详情、发现、我的、支付、消息等常见商城模块。项目目录清楚,页面、样式、逻辑与配置分层明确,新手可据此理解小程序页面组织、组件复用和数据交互方式,也方便替换商品素材、调整界面风格、扩展功能模块或作为课程作业与毕业设计的可运行原型。已有1243人学习,适合需要现成赛题方案、可运行源码及数据库配套的读者参考。
1. 先看它能干什么:一份带数据库和教程的小程序商城源码
如果你正卡在毕业设计选题上,想要一个能演示商品浏览、加入购物车、提交订单、订单查询的小程序商城,这份带数据库、源码和教程的压缩包就是冲着“能跑起来”去的。它适合两类人:一类是刚接触小程序、需要照着课程资源把环境搭起来的同学;另一类是想拿现成项目改造成自己的答辩作品、但不想从零写登录和订单的开发者。别急着翻页面,先确认它是不是完整的商城链路。
2. 技术栈与目录结构:小程序端、服务端、数据库三块怎么拼
这份资源不是一个小程序插件,也不是只含几个静态页面的演示,它更像一套按“前端页面 + 后端接口 + 数据库脚本”拼起来的完整课程设计。拿到压缩包后,先别急着改页面颜色,先把三块东西对上:小程序端负责展示和交互,服务端负责业务和权限,数据库负责把商品、购物车、订单这些状态存住。三块里任何一块配置错位,页面就会停在“加载中”或者“请求失败”,新手很容易以为是代码写错了。
2.1 典型技术组合与请求链路
这类毕业设计商城常见的技术组合是:前端用原生小程序写 WXML/WXSS/JS,后端用 Spring Boot + MyBatis-Plus,数据库用 MySQL,构建工具用 Maven;也有 Node.js 或 PHP 版本,目录名和启动命令不同,但接口分层和表结构基本同构。下面按 Java 版拆,换成其他语言时重点看 Controller 的接口路径、Service 的业务顺序和 SQL 里的表字段,不要被语言差异带偏。
一个请求从页面到数据库,链路通常是:小程序页面调用wx.request,请求/api/goods/{id},进入GoodsController,再由GoodsService查商品主表和 SKU 表,最后通过GoodsMapper落到 MySQL。排错时先看这条链路在哪一层断了:页面没发请求、请求路径写错、后端没启动、数据库连不上、SQL 查不到数据,表现都是“页面没东西”,但处理方式完全不同。
| 层次 | 常见位置 | 职责 | 排错先看 |
|---|---|---|---|
| 小程序页面 | pages/goods、pages/cart、pages/order | 展示、表单、跳转 | 控制台请求 URL、参数、token |
| 接口层 | controller | 接收参数、返回统一 JSON | 路径、请求方法、状态码 |
| 业务层 | service | 库存、金额、订单状态 | 事务、业务异常、空值 |
| 数据层 | mapper、resources/mapper | SQL 与对象映射 | 表名、字段名、条件 |
| 数据库 | sql/*.sql | 商品、订单、用户数据 | 库名、字符集、初始数据 |
2.2 解压后的目录该从哪里看起
解压后不要把所有文件都打开,先按“教程 → SQL → 配置文件 → 接口 → 页面”的顺序看。教程目录通常会写默认账号、启动顺序和注意事项;SQL 目录决定你本地要建哪些表;配置文件决定服务端连哪个数据库、跑哪个端口;接口和页面才是后面改功能时要动的地方。这个顺序能让你少走很多弯路,尤其是别人给的毕业设计项目,README 里一句话可能比翻十个类更有用。
| 目录/文件 | 先看什么 | 作用 |
|---|---|---|
README或教程目录 | 启动顺序、默认账号 | 避免一上来就漏配置 |
sql/ | 建库语句、初始数据 | 本地数据库的数据来源 |
application.yml/application.properties | 端口、数据库连接 | 服务端能不能起来 |
controller/ | @RequestMapping路径 | 小程序请求地址对不对 |
service/ | 业务方法顺序 | 库存、金额、订单状态 |
pages/ | onLoad、wx.request | 页面数据和跳转 |
utils/ | request.js、config.js | 统一域名和 token |
如果项目里同时有miniprogram和server两个大目录,说明前后端是分开放的;如果只有一个工程目录,后端通常藏在src/main/java下,小程序端可能在另一个子目录。先确认这一点,再决定用哪个开发工具打开,不要把服务端工程直接导进小程序开发者工具,也不要把小程序页面当成 Java 项目编译。
2.3 数据库表与核心实体关系
毕业设计商城不需要把表设计得像真实电商那么复杂,但几条主线必须有:商品、SKU、购物车、订单、订单明细、用户、地址。商品和 SKU 是一对多,订单和订单明细是一对多,购物车是用户和 SKU 的中间表。下面这段 SQL 只保留商品、订单、订单明细三条核心表,目的是让你先看清字段关系,实际项目里的表名可能带前缀,字段也可能多一些。
-- 商品主表:price 用 decimal,别用 float;stock 是扣减库存的基准 CREATE TABLE `product` ( `id` bigint NOT NULL AUTO_INCREMENT, `category_id` bigint NOT NULL, `name` varchar(120) NOT NULL, `cover` varchar(255) DEFAULT '', `price` decimal(10,2) NOT NULL, `stock` int NOT NULL DEFAULT 0, `status` tinyint NOT NULL DEFAULT 1 COMMENT '1上架 0下架', PRIMARY KEY (`id`) ); -- 订单主表:order_no 加唯一索引,防止重复提交产生相同单号 CREATE TABLE `mall_order` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `user_id` bigint NOT NULL, `total_amount` decimal(10,2) NOT NULL, `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已完成 4已关闭', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ); -- 订单明细:价格要落快照,商品改价后历史订单不能跟着变 CREATE TABLE `order_item` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL, `product_id` bigint NOT NULL, `product_name` varchar(120) NOT NULL, `price` decimal(10,2) NOT NULL, `quantity` int NOT NULL, PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) );这段 SQL 的关键不是建表语法,而是字段背后的约束:price用decimal(10,2)是为了避免金额出现99.90000001这种浮点尾差;stock是扣减库存时的判断条件,后面下单会用stock >= quantity做条件更新;order_no加唯一索引,是为了让重复提交在数据库层也被拦住;order_item里的product_name和price是下单时的快照,商品后来改名或改价,历史订单不能跟着变。把这些字段理解清楚,后面看订单模块就不会只盯着页面按钮。
| 表 | 作用 | 关键字段 |
|---|---|---|
user | 用户与登录态 | openid、nickname、token |
category | 商品分类 | name、sort、status |
product | 商品主信息 | price、stock、status |
product_sku | 规格与独立库存 | product_id、sku_name、stock |
cart | 购物车 | user_id、sku_id、quantity、selected |
mall_order | 订单主表 | order_no、total_amount、status |
order_item | 订单明细 | order_id、price、quantity |
3. 本地跑通:从导入 SQL 到小程序首页出商品
跑通顺序不能乱:数据库 → 服务端 → 小程序端。血泪经验是,先让接口在curl里返回 JSON,再去调小程序页面;否则页面报“请求失败”,你根本不知道是域名没配、服务端没起、还是 SQL 查不到数据。小程序端的报错不会告诉你后端到底收没收到请求,接口日志才是黑匣子。
3.1 数据库导入与连接配置
先建库再导入 SQL,字符集用utf8mb4,不然后面商品名、地址里的特殊字符容易乱码。导入命令里的库名要和配置文件里的库名一致,路径按你解压后的sql目录调整,不要直接复制命令就跑。
# 登录 MySQL 后创建数据库,字符集用 utf8mb4 CREATE DATABASE mall CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 导入项目自带的 SQL;路径按你解压后的 sql 目录调整 mysql -u root -p mall < sql/mall.sqlCREATE DATABASE里的mall是库名,后面application.yml的jdbc:mysql://.../mall必须和它一致;utf8mb4比utf8更稳,商品描述里出现特殊符号时不至于报错。mysql -u root -p里的-u是用户名,-p表示回车后输入密码,mall是目标库,< sql/mall.sql是把脚本喂给 MySQL 客户端。导入后先执行show tables;和select count(*) from product;,确认表和数据都在,再去看后端配置。
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driverurl里的127.0.0.1:3306是本地 MySQL 地址和端口,mall是库名,serverTimezone=Asia/Shanghai用来避免时间字段差几个小时;username和password换成你本机的账号密码;driver-class-name按你本地 MySQL 驱动版本改,驱动不匹配时启动会直接报ClassNotFoundException或连接失败。很多人只改密码不改库名,结果启动报Unknown database,回头翻半天代码,其实问题在配置文件第一行。
3.2 服务端启动与接口自测
服务端启动前先确认pom.xml或package.json在哪个目录,不要在小程序页面目录里执行启动命令。Java 版常见启动方式是mvn spring-boot:run,启动后看控制台有没有Tomcat started on port之类的日志;端口不是固定的,以配置文件为准。
# 在服务端根目录执行,确认当前目录有 pom.xml mvn spring-boot:run # 另开终端,先测商品列表接口;返回 JSON 才算服务端通了 curl http://127.0.0.1:8080/api/goods/listmvn spring-boot:run会按pom.xml拉依赖并启动内嵌容器,第一次执行会慢一些;curl里的127.0.0.1:8080要换成你配置文件里的端口,/api/goods/list要换成 Controller 里实际写的路径。如果返回 404,先看@RequestMapping有没有加全局前缀;如果返回 500,先看控制台异常栈的第一行;如果连接被拒绝,说明服务端根本没起来,不要继续调小程序。
3.3 小程序端配置与首次编译
小程序端第一件事是找请求封装,通常在utils/request.js或config.js里。把BASE_URL改成你本地服务端地址,本地调试用127.0.0.1,真机调试要换成电脑的局域网 IP,因为手机访问不到你电脑的127.0.0.1。下面的封装把 token 放进请求头,把code === 0当作业务成功,具体字段名要以你项目里的统一返回结构为准。
// utils/request.js const BASE_URL = 'http://127.0.0.1:8080'; // 本地调试地址,真机要换成局域网 IP function request(options) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.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 { reject(res.data || res); } }, fail: reject }); }); } module.exports = { request, BASE_URL };BASE_URL是后面所有接口的前缀,改一处就能切环境;options.url传相对路径,比如/api/goods/list;header.token从本地存储取,登录后写入,退出时清掉;res.data.code === 0是项目约定的成功码,如果你的项目用200或success,这里要跟着改。fail: reject把网络错误抛给页面,页面才能用catch弹提示,不然失败时页面只会静默空白。
3.4 三端联调的检查点
联调时按检查点走,不要靠猜。第一步,数据库里product表有数据,status是上架状态;第二步,curl商品列表能返回 JSON;第三步,小程序控制台能看到请求 URL 和响应;第四步,图片地址能打开;第五步,登录后 token 能写进 storage。任何一步断了,先停在那一步修,不要继续往下点。
| 检查点 | 预期结果 | 失败先看 |
|---|---|---|
| 数据库 | select count(*) from product有数字 | 库名、SQL 是否导入 |
| 服务端 | curl返回 JSON | 端口、路径、异常日志 |
| 小程序请求 | 控制台有响应 | BASE_URL、合法域名校验 |
| 图片 | 图片能正常显示 | cover字段、静态资源映射 |
| 登录 | storage 里有 token | 登录接口、token 字段名 |
本地开发时,小程序开发者工具里通常会有一个“不校验合法域名”的选项,本地调试可以临时打开,但真机预览和上线前要关掉,并把服务端域名配到小程序后台。这个点很容易翻车:工具里能跑,真机一片空白,最后发现是域名没配。接口日志、小程序控制台、数据库查询三边对着看,比在页面里加十个console.log更快。
4. 核心链路拆解:商品详情、购物车、订单提交的代码落点
商城最值得拆的不是首页轮播图,而是商品详情、购物车金额、订单提交这三段。首页只证明页面能渲染,订单链路才证明这个项目能答辩。下面按“页面请求 → 接口 → 业务 → SQL”的顺序看,代码是常见写法,类名和字段名以你手里的源码为准,重点是理解每一步在防什么问题。
4.1 商品详情接口与小程序渲染
商品详情接口一般接收一个商品 ID,先查商品主表,再查 SKU 列表,最后组装成 VO 返回。Controller 只负责接参数和包统一返回,业务判断放在 Service,这样后面加“下架商品不能看详情”时不用改页面。
@RestController @RequestMapping("/api/goods") public class GoodsController { @Resource private GoodsService goodsService; @GetMapping("/{id}") public Result<GoodsDetailVO> detail(@PathVariable Long id) { // 商品不存在或已下架时,由 Service 抛业务异常,全局异常处理器转成统一 JSON return Result.ok(goodsService.getDetail(id)); } }@RestController表示返回 JSON,@RequestMapping("/api/goods")是这一组接口的公共前缀,@GetMapping("/{id}")表示GET /api/goods/1这样的路径;@PathVariable Long id从 URL 里取 ID,类型用Long和数据库主键对齐;Result.ok(...)是统一返回包装,前端才能用同一个code字段判断成功失败。
@Service public class GoodsService { @Resource private ProductMapper productMapper; @Resource private ProductSkuMapper skuMapper; public GoodsDetailVO getDetail(Long id) { Product product = productMapper.selectById(id); if (product == null || product.getStatus() == 0) { throw new BizException("商品不存在或已下架"); } List<ProductSku> skus = skuMapper.selectByProductId(id); GoodsDetailVO vo = new GoodsDetailVO(); vo.setProduct(product); vo.setSkus(skus); return vo; } }selectById按主键查商品,status == 0是下架判断,真实项目里可能还有删除标记、审核状态;selectByProductId查这个商品下面的规格,SKU 为空时页面要能降级显示商品主图,不要直接白屏;GoodsDetailVO是给前端的视图对象,不要把数据库实体原样丢出去,否则后面加字段会把内部结构暴露得很乱。
Page({ data: { goods: null, selectedSkuId: null }, onLoad(options) { const id = options.id; request({ url: `/api/goods/${id}` }).then(goods => { this.setData({ goods, selectedSkuId: goods.skus.length ? goods.skus[0].id : null }); }).catch(err => { wx.showToast({ title: '商品加载失败', icon: 'none' }); console.error('goods detail error', err); }); } });onLoad(options)里的options.id来自页面跳转路径,比如/pages/goods/detail?id=1;setData把商品对象和默认选中的 SKU 写进页面数据;goods.skus.length先判断再取第一条,避免空数组取[0]报错;catch里弹提示并打印错误,联调时不要只弹“加载失败”,控制台里的响应体才是定位问题的关键。
4.2 购物车勾选与金额计算
购物车金额在前端只做展示,最终提交订单时后端必须按数据库价格重算。前端计算用“分”做整数运算,避免0.1 + 0.2这种浮点尾差;后端用BigDecimal,两边都不要用float或double存金额。
// 购物车金额只做展示,最终以下单接口重算为准 function calcCart(cartItems) { let totalCents = 0; cartItems.forEach(item => { if (item.selected) { // 先把单价转成分,再乘数量,避免浮点相加出现尾差 totalCents += Math.round(item.price * 100) * item.quantity; } }); return (totalCents / 100).toFixed(2); }cartItems是购物车数组,selected表示是否勾选,price是单价,quantity是数量;Math.round(item.price * 100)把元转成分,totalCents / 100再转回元,toFixed(2)保证页面显示两位小数。这个函数只负责展示,提交订单时要把skuId、quantity传给后端,后端重新查价格,不能信任前端传来的totalAmount。
4.3 订单提交与事务边界
订单提交是整套代码里最不能省事务的地方。扣库存、写订单、写明细、清购物车,这四步要么一起成功,要么一起回滚;库存扣减用条件更新,不能先查再扣,否则并发时会把库存扣成负数。事务是库存的后悔药,但前提是异常真的抛出去,别在catch里吞掉。
@Service public class OrderService { @Resource private ProductMapper productMapper; @Resource private MallOrderMapper orderMapper; @Resource private CartMapper cartMapper; @Transactional(rollbackFor = Exception.class) public String submit(Long userId, List<CartItem> items) { BigDecimal total = BigDecimal.ZERO; for (CartItem item : items) { // 条件更新库存:库存不足时 affected=0,直接抛异常回滚 int affected = productMapper.deductStock(item.getProductId(), item.getQuantity()); if (affected == 0) { throw new BizException("库存不足:" + item.getProductId()); } BigDecimal price = productMapper.selectPrice(item.getProductId()); total = total.add(price.multiply(BigDecimal.valueOf(item.getQuantity()))); } String orderNo = "M" + System.currentTimeMillis() + userId; MallOrder order = new MallOrder(); order.setOrderNo(orderNo); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); // 实际项目里还要逐条插入 order_item,这里省略循环 cartMapper.deleteSelected(userId); return orderNo; } }@Transactional(rollbackFor = Exception.class)表示只要抛出异常就回滚,rollbackFor不能省,默认只回滚运行时异常;deductStock返回受影响行数,affected == 0说明库存不足,直接抛业务异常;selectPrice从数据库取当前价格,不用前端传的价格;total用BigDecimal累加,orderNo用时间戳加用户 ID 拼一个临时单号,真实项目里要用更稳的分布式单号或数据库序列。
<update id="deductStock"> UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity} </update>这条 SQL 的关键在AND stock >= #{quantity}:库存够才扣,不够时更新 0 行,Service 里就能根据受影响行数判断失败。#{quantity}和#{productId}是 MyBatis 参数占位,不要用${}拼接,避免 SQL 注入;如果项目里有 SKU 表,扣库存要落到product_sku,商品主表的库存只做汇总展示,别两边都扣。
4.4 订单状态与支付回调的简化处理
订单状态不要散落在代码里写0、1、2,用一个枚举或常量表统一管理,后面加“已取消”“退款中”时不用全项目搜
本文还有配套的精品资源,点击获取