简介:这是一套基于微信小程序的宠物商城系统毕业设计项目,面向计算机相关专业正在准备毕设、课程设计或期末大作业的学生,也适合需要项目实战练习的开发者。系统包含商品展示、购物车、用户管理等商城核心模块,并接入地图与天气等第三方服务,源码与数据库齐备,经过严格调试可正常运行,能直接作为毕设作品提交或二次扩展。压缩包共包含204个文件,整体约10.56MB。其中35个js文件承担业务逻辑,33个json文件用于页面与项目配置,26个wxml文件搭建页面结构,28个wxss文件负责界面样式,另有74张png图片及数据库、说明文档等,目录结构清晰,便于按模块学习与修改。该资源已有345人浏览学习。配套的项目说明可帮助理解系统设计思路,适合从零上手微信小程序开发,也可作为答辩演示和功能演示的完整参考。
1. 宠物商城是毕业设计里出场率最高的题材之一,但想拿高分反而难
宠物商城是毕业设计里出场率最高的题材之一,但正因人人都在做,想让评审老师眼前一亮反而最难——他们看过的微信小程序宠物商城,可能比你想象的还多。多数演示翻车不在页面好不好看,而在“看宠物 → 加购 → 下单”这条主链路走不通:加购没反应、订单状态不更新、刷新一下购物车空了。这套基于微信小程序的宠物商城系统源码+数据库(高分毕业设计).zip之所以能成为被反复采用的参考模板,靠的不是功能堆砌,而是数据库设计与交易闭环经得起追问。适合两类人:一是正在选毕设题目的计算机相关专业学生,二是想理解小程序前后端协作、快速搭一个全栈练手项目的开发者。需要的基本功只有三样:看得懂SQL、改得动JavaScript、会按接口文档调API。
2. 三件套如何协作:小程序端、接口后端、MySQL 数据层的目录与分工
拿到这类压缩包,解压后一般会看到两类东西:小程序项目目录(包含 pages、utils、app.js 等)和数据库初始化脚本(.sql 文件或 SQLite 的 .db 文件)。理解三者的边界,比急着跑起来更重要——答辩时老师最常问的第一句话就是“数据从哪来、存到哪去”。
2.1 前后端分离的落地结构:为什么原生小程序比 uniapp 更适合毕业答辩
宠物商城这类项目最稳妥的结构是前后端分离:小程序端负责页面渲染和交互,所有数据通过 wx.request 向后端要;后端只返回 JSON,不管页面;MySQL 只存数据,不写业务逻辑。答辩时可以分三层讲:表现层、业务层、数据层,每一层都有对应的代码和表可以指给老师看。
很多人会用 uniapp 开发微信小程序,因为一套代码能同时出 H5 和 App。但毕业设计我一般建议直接用原生小程序。原因有两个:第一,原生小程序的生命周期函数(onLoad、onShow、onHide)是评审老师默认的考点,你用 uniapp 封装了一层,遇到“页面为什么重新加载”这类问题时容易露怯;第二,毕设项目的体量普遍不大,跨端收益根本算不过来,原生写法最直接,调试报错也更容易搜到答案。
后端语言选什么并不关键,Spring Boot、Node.js Express、PHP 都能做,接口契约是一致的。关键在于接口路径要稳定,比如宠物列表固定是 GET /pet/list、下单固定是 POST /order/create。不要一个页面一套 URL 风格,否则答辩演示时切换页面会频繁出现 404,体验非常扣分。
2.2 请求封装:把 baseURL、token 和错误提示收进一个 request.js
毕设里最常见的翻车现场是“真机上很多接口请求失败”。原因多半是 baseURL 散落在每个页面里,改了一处忘了另一处,或者后端启动地址变了但小程序里还写着旧 IP。正确做法是让所有请求走一个统一封装,集中管理地址和公共逻辑:
// utils/request.js const BASE_URL = 'http://192.168.1.100:8080/api'; // 开发时填电脑局域网 IP function request(method, url, data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' // 统一注入登录态 }, success(res) { // 和后端约定:code 为 200 表示业务成功 if (res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res.data); } }, fail(err) { // 网络层错误:后端没启动、IP 不对、域名没配,都会走到这里 wx.showToast({ title: '网络异常,请检查后端是否启动', icon: 'none' }); reject(err); } }); }); } module.exports = { request };这段代码解决三个高频问题:一是所有接口共用 BASE_URL,后端换机器只改一处;二是登录后返回的 token 从缓存统一读取、统一注入请求头,不用每个页面都写一遍;三是成功和失败都做了统一提示,页面里不需要重复处理 toast。
页面里调用的方式要足够简单,让后面写业务时注意力集中在数据上:
const { request } = require('../../utils/request'); request('GET', '/pet/list', { page: 1 }).then(list => { this.setData({ pets: list }); });这里注意业务码 200、401、500 的约定要跟后端对好。401 表示登录态过期,收到后应该清除缓存并跳转登录页;500 通常是后端异常,这类错误不要让用户看到堆栈,统一提示“服务开小差了”。把这套约定写进接口文档,答辩时是一块很加分的工程化亮点。
2.3 登录态与缓存时间:wx.login 换 code 不是摆设
微信小程序的登录流程跟传统用户名密码完全不一样。标准做法是 wx.login 拿到临时 code,后端拿 code 去微信接口换 openid,再签发一个自定义 token 返回给前端。这里有个容易偷懒的点:不少人为省事,直接在本地写死一个用户 ID,结果答辩被问到“openid 怎么来的”就卡住了。
// pages/login/login.js wx.login({ success: async ({ code }) => { // code 是临时凭证,5分钟内有效,且只能使用一次 const res = await request('POST', '/auth/login', { code }); wx.setStorageSync('token', res.token); wx.setStorageSync('expire', Date.now() + 24 * 60 * 60 * 1000); // 有效期24小时 } });登录态的“缓存时间”是评审爱问的细节。wx.setStorageSync 默认是永久存储,不会自己过期,所以要么存一个 expire 时间戳,每次进入小程序检查和当前时间做对比,要么在后端给 token 设有效期,前端收到 401 后清理缓存。更好的做法是在 request.js 里拦截 401,统一跳转登录页。
提示:如果只想让答辩流程顺畅,可以在代码里留一个“演示模式”开关,默认走固定 userId,跳过 wx.login,但注释里要写清楚正式上线时如何切回真实流程。这比硬着头皮现场登录稳妥,也显得你考虑过工程边界。
3. 数据库设计与宠物商品特殊性:七张核心表与活体状态字段
数据库脚本是整个压缩包里最先值得打开的东西。评审老师不一定会一行行看你的 JS,但大概率会打开 Navicat 看一眼表结构。宠物商城表设计得好不好,直接决定“高分”能不能落地。
3.1 七张核心表与字段设计:从用户到订单明细全链路
一个完整的宠物商城最少要有七张表:用户表、宠物商品表、分类表、购物车表、订单表、订单明细表、收藏表。其中用户、宠物、订单是必须重点讲的,购物车和收藏是辅助功能,但它们的表结构同样要规范。
| 表名 | 核心字段 | 说明 |
|---|---|---|
| user | id, nickname, avatar, phone, openid | openid 唯一标识微信用户 |
| pet | id, name, category_id, variety, age, gender, health_status, vaccine, price, stock, cover_url, status | 宠物即商品,字段比普通商品多 |
| category | id, name, sort | 猫、狗、小宠等分类 |
| cart | id, user_id, pet_id, quantity, checked | checked 记录勾选状态 |
| order | id, order_no, user_id, total_amount, status, create_time | status 流转待支付/已支付/已完成 |
| order_item | id, order_id, pet_id, pet_name, price, quantity | 冗余商品快照,防止商品下架后订单失效 |
| favorite | id, user_id, pet_id, create_time | 收藏与取消收藏 |
宠物表是这套设计的核心,跟普通商品表最大的区别是多了健康相关字段。下面是宠物表的建表 SQL,关键字段都加了注释:
CREATE TABLE `pet` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '宠物名字', `category_id` INT NOT NULL COMMENT '分类ID,关联category表', `variety` VARCHAR(60) DEFAULT '' COMMENT '品种,如英短、金毛', `age` DECIMAL(3,1) DEFAULT 0 COMMENT '月龄,0.5表示半个月', `gender` TINYINT DEFAULT 0 COMMENT '0未知 1公 2母', `health_status` TINYINT DEFAULT 1 COMMENT '1健康 2治疗中 3待观察', `vaccine` VARCHAR(100) DEFAULT '' COMMENT '疫苗记录,如“三联+狂犬”', `price` DECIMAL(10,2) NOT NULL COMMENT '售价', `stock` INT NOT NULL DEFAULT 1 COMMENT '剩余可售数量', `cover_url` VARCHAR(255) DEFAULT '' COMMENT '封面图', `detail` TEXT COMMENT '详细介绍、性格描述', `status` TINYINT DEFAULT 1 COMMENT '1上架 0下架', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意几个容易被忽略的设置:价格用 DECIMAL(10,2) 而不是 FLOAT,避免浮点误差;stock 字段虽然语义是“还有几只可选”,但下单扣减时要在 SQL 里加 stock > 0 条件防止变成负数;status 字段用于上下架,前端列表页只查 status = 1 的数据。
3.2 宠物不是普通商品:活体状态与订单状态流转的加分点
宠物商城里卖的是活体,这一点是跟普通电商拉开差距的设计亮点。普通商城的商品表只有价格、库存、图片,宠物还要有健康状态、疫苗记录、性格描述。列表页展示时,很多学生只会把数据库里的字段原样渲染出来,但高分做法是把 health_status 翻译成用户看得懂的内容,比如“健康”“治疗中”“待观察”,并配上不同颜色的标签。
订单状态流转是另一个必答考点。标准链路是“待支付 → 已支付 → 已完成”,带线下自提的宠物商城还可以加“待自提”。订单明细表一定要做商品快照——下单那一刻把宠物名、价格、图片复制一份存进 order_item。这样就算宠物下架或者改价,历史订单依然能完整显示,也方便答辩时解释“为什么订单表不直接关联 pet 表”。
数据库的增删改查接口要跟七张表一一对应。比如宠物模块的列表查询、详情查询、上下架修改,购物车模块的增删改查,订单模块的创建与状态更新。前端每个交互动作,老师问到“这个操作改了哪张表”,你都要能立刻回答。建议答辩前把每张表对应的接口列一张表,背下来,这是最容易被追问的部分。
3.3 字符集与连接池:utf8mb4 和两个必调参数
宠物名字里经常带 emoji,比如“🐱小橘”,MySQL 的 utf8 字符集会直接报错或存成乱码。建库时必须用 utf8mb4:
CREATE DATABASE IF NOT EXISTS pet_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;用 Navicat 导入 .zip 里自带的 .sql 文件时,注意连接字符集也要选 utf8mb4,否则表里明明有数据但页面显示乱码。这个坑属于“现象明显、原因隐蔽”的类型,建议在第一版建库时就直接把字符集写好,省得后面返工。
后端连 MySQL 通常会用到连接池。以 Java 的 Druid 或 Node 的 mysql2 连接池为例,有两个参数值得关注:
const pool = mysql.createPool({ host: '127.0.0.1', user: 'root', password: '123456', database: 'pet_mall', connectionLimit: 10, // 最大连接数,毕设场景 10 足够 charset: 'utf8mb4' // 和数据库字符集保持一致 });connectionLimit 设太大会占资源,设太小并发一高接口就排队超时。毕设并发量很低,10 到 20 之间都合理。charset 必须和库表一致,否则请求层中文乱码。这两个参数在答辩时也常被问到,提前熟悉一下能省去不少尴尬。
4. 从选宠到下单:商品列表、加购、结算的核心链路实现
主链路是评审老师最关注的演示路径:首页看到宠物列表 → 点进详情 → 加入购物车 → 购物车勾选 → 提交订单 → 模拟支付。这条链路走通,项目的基本盘就稳了。下面按顺序拆每一段的实现。
4.1 商品列表与详情:分页加载与分类切换
首页列表最常见的错误是一次性把所有宠物查出来,数据量一大就卡。正确做法是分页加载,小程序滑到底部时自动拉下一页。微信小程序的 onReachBottom 就是干这个的:
Page({ data: { page: 1, pets: [], hasMore: true, categoryId: 0 // 0表示全部 }, onLoad() { this.loadPets(); }, async loadPets() { if (!this.data.hasMore) return; const data = await request('GET', '/pet/list', { page: this.data.page, size: 10, categoryId: this.data.categoryId }); // 用 concat 追加而不是直接覆盖,否则翻页会丢数据 this.setData({ pets: this.data.pets.concat(data.list), hasMore: data.hasMore // 后端根据总数和页码计算 }); }, onReachBottom() { this.setData({ page: this.data.page + 1 }, () => { this.loadPets(); }); }, switchCategory(e) { // 切换分类时重置页码,否则会停留在上一个分类的末尾 const categoryId = e.currentTarget.dataset.id; this.setData({ page: 1, pets: [], hasMore: true, categoryId }, () => { this.loadPets(); }); } });这里有两个容易踩的细节。一是 onReachBottom 里要先 setData 更新 page 再调用 loadPets,否则第一次翻页用的还是旧页码;二是切换分类必须重置 page 和 pets,不然会出现“分类是猫,列表里却有狗”的尴尬。后端接口对应的是分页 SQL,一般用 LIMIT offset, size 实现,并返回 hasMore 标记是否有下一页。
详情页的逻辑相对简单,根据路由参数里的 id 调 GET /pet/detail?id=xxx,把返回的宠物信息绑定到页面。这里要强调:详情页展示的数据必须是数据库里查出来的,不要在前端写死一份 JSON。答辩现场老师会随手改数据库里的价格再刷新页面,如果价格不变,印象分会掉一大截。
4.2 加购与购物车:数量修改与勾选联动
加购的接口设计要注意“重复加购”的问题。用户把同一只宠物加了三次,购物车里是三条记录还是一条数量为 3 的记录?正确做法是后者。后端逻辑先查 cart 表里有没有同一 user_id 和 pet_id 的记录,有就 UPDATE quantity,没有才 INSERT。
// 后端伪代码:POST /cart/add async function addToCart(userId, petId, quantity) { const exist = await db.query( 'SELECT id, quantity FROM cart WHERE user_id = ? AND pet_id = ?', [userId, petId] ); if (exist.length > 0) { await db.query( 'UPDATE cart SET quantity = quantity + ? WHERE id = ?', [quantity, exist[0].id] ); } else { await db.query( 'INSERT INTO cart (user_id, pet_id, quantity, checked) VALUES (?, ?, ?, 1)', [userId, petId, quantity] ); } }购物车的勾选状态是另一个高频翻车点。微信原生的 checkbox 组件绑定的 checked 值必须来自数据层,不能靠 DOM 操作去改。常见做法是在每条购物车数据里维护一个 checked 字段,点击时通过>toggleCheck(e) { const id = e.currentTarget.dataset.id; // 购物车记录id const carts = this.data.carts.map(item => { if (item.id === id) { return { ...item, checked: !item.checked }; } return item; }); this.setData({ carts }); },
全选逻辑也简单:把 allChecked 取反后,循环把所有 item 的 checked 设为同一个值,再单独 setData 一次。不要一个勾选框一个 setData,性能差而且容易出现错乱。结算金额也要实时计算:遍历 checked 为 true 的记录,累加 price × quantity,这个计算放在 setData 之后做,保证界面同步。
4.3 下单与模拟支付:库存扣减与订单创建的事务处理
提交订单是整个项目里逻辑最重的接口,涉及多张表,必须保证要么全部成功、要么全部失败。后端做三件事:校验购物车选中商品、扣减库存、创建订单和订单明细。库存扣减要用一条带条件的 UPDATE:
-- 防止库存变成负数:只扣减 stock > 0 的记录 UPDATE pet SET stock = stock - 1 WHERE id = ? AND stock > 0;如果这条语句影响的行数是 0,说明库存不足,直接返回“该宠物已被买走”。只有库存扣减成功才去创建订单,否则回滚。这一步是答辩时的高分亮点,比“先查库存再扣减”的写法安全得多。
小程序端提交订单的代码要处理两个核心点:按钮防重复点击、下单成功后跳转支付。按钮防重用 loading 状态实现:
async submitOrder() { if (this.data.submitting) return; // 防止连点生成重复订单 this.setData({ submitting: true }); wx.showLoading({ title: '提交中' }); try { const cartIds = this.data.carts.filter(i => i.checked).map(i => i.id); const order = await request('POST', '/order/create', { cartIds }); // 拿到订单号,进入支付引导 wx.redirectTo({ url: '/pages/pay/pay?orderNo=' + order.orderNo }); } finally { wx.hideLoading(); this.setData({ submitting: false }); } }支付环节是“毕业设计专用”的典型场景。真实微信支付需要商户号和微信支付资质,个人开发者基本拿不到,所以毕设普遍做法是模拟支付:前端弹一个支付确认框,用户点确认后调后端 POST /pay/mock 接口,把订单状态从待支付改成已支付。代码里用一个开关标明当前是模拟模式:
const USE_MOCK_PAY = true; // 正式接入微信支付时改为 false,并补充支付参数用真实 wx.requestPayment 而没有商户号,调用会直接报错“商户号参数错误”,现场演示非常难看。模拟支付要注意接口里带上 orderNo,后端更新订单时加一个条件“只允许从待支付改成已支付”,防止重复回调改变状态。订单列表页的状态展示要跟这个流程对应:待支付、已支付、已完成三个 Tab,数据从 order 表按 user_id 和 status 查出来。
5. 毕业设计避坑:真机联调、导航栏适配、重复下单的排查记录
这里按“现象 → 原因 → 解决”的格式写五条踩坑记录,都是这类毕设项目里出现频率最高的真实问题。
5.1 网络联调:真机连不上后端、图片加载不出来
坑一:开发者工具里接口正常,一用真机预览全部失败。
现象:模拟器上列表、详情都通,手机扫码打开后页面空白,请求全部超时。
原因:开发者工具默认勾选了“不校验合法域名”,真机预览没有这个豁免;另一个原因是后端服务监听在 127.0.0.1,真机根本访问不到电脑的 localhost。
解决:后端启动时监听 0.0.0.0,BASE_URL 填电脑的局域网 IP(命令行执行 ipconfig 或 ifconfig 查出来,一般是 192.168.xx.xx),不要写 localhost。真机预览前,在开发者工具“详情 → 本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。要注意这是开发阶段的做法,正式上线必须换备案过的 HTTPS 域名,但答辩演示完全够用。
坑二:图片加载不出来,列表页只有一片灰块。
现象:开发者工具里图片正常,真机上图片全部空白,控制台提示下载失败。
原因:cover_url 存的是相对路径或者指向 127.0.0.1 的图片服务地址,真机无法访问。
解决:后端返回图片时拼上完整 URL,或者前端在渲染前统一拼接 BASE_URL。检查时打开开发者工具的 Network 面板,看 image 请求的返回状态码,404 是路径不对,502 是图片服务没启动。图片路径这个坑看着小,但演示时直接影响观感。
5.2 UI 适配:顶部导航栏高度与原单选框样式
坑三:自定义导航栏在 iPhone 上错位,标题和胶囊按钮重叠。
现象:部分安卓机上正常,iPhone 上标题栏文字顶到状态栏,或者和右侧胶囊按钮叠在一起。
原因:使用了自定义导航栏(navigationStyle: custom)后,不同设备的状态栏高度和胶囊按钮位置不同,写死一个高度必然适配失败。
解决:动态计算导航栏高度,用 wx.getMenuButtonBoundingClientRect 拿到胶囊按钮的位置:
const sysInfo = wx.getSystemInfoSync(); const menuRect = wx.getMenuButtonBoundingClientRect(); const statusBarHeight = sysInfo.statusBarHeight; const navBarHeight = (menuRect.top - statusBarHeight) * 2 + menuRect.height;公式的含义是:胶囊顶部到状态栏的距离乘以 2,再加上胶囊自身高度,就是自定义导航栏应有的总高度。拿到这个值后给自定义标题 view 设置高度,并让标题文字垂直居中。这样无论什么机型,标题都和胶囊按钮在同一水平线。这是热词“微信小程序顶部导航栏高度”的核心答案,值得记住。
5.3 业务逻辑:重复下单与购物车勾选错乱
坑四:连点两下“提交订单”,生成了两条一模一样的订单。
现象:快速点击下单按钮,订单列表里出现两条相同内容的记录,库存被扣了两次。
原因:前端没有禁用按钮,后端也没有做防重校验,请求在几百毫秒内被提交了两次。
解决:前端在 submitOrder 里用 submitting 标志位拦截二次点击,上一节代码已经处理;后端在 order 表给 order_no 字段加唯一索引,重复提交时第二次插入会抛 ER_DUP_ENTRY 异常,捕获后提示“订单已存在,请勿重复提交”。前后端双重防护,是工程上标准的幂等做法。
坑五:购物车勾选状态错乱,全选以后取消单个,其他项也跟着变。
现象:点击某个商品的勾选框,结果整行甚至多行的选中状态都被翻转。
原因:setData 时用了错误的索引,或者把 checkbox 的 checked 属性直接绑成了固定的 true,导致状态没有和当前数据项一一对应。
解决:每条购物车记录都维护独立的 checked 字段,操作时用 style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />