Java微信小程序宠物店商城毕设:SSM框架搭建与避坑全解析
简介面向毕业设计场景的Java微信小程序宠物商城项目基于SSM框架与Vue管理后台搭配MySQL数据库和JDK1.8环境涵盖管理员、用户、商家三类角色管理员可管理用户、商家、宠物分类、商品分类、服务项目与订单用户可购买宠物/用品、预约服务、发表日志和管理订单商家可上架宠物及用品、提供服务并处理预约。整套资源包含运行源码、数据库脚本、论文文档、环境工具包及同框架项目的安装教程压缩包共1662个文件总大小约52.19MB以Java源码、Vue页面、JavaScript脚本、微信小程序WXML/WXSS、PNG图片及SQL脚本等为主目录结构覆盖前后端与文档模块便于整体学习和二次开发。资源已有123人浏览学习适合需要快速搭建宠物商城小程序毕业设计或入门SSMVue小程序开发的读者。1. 这个标题到底在交付什么一套能跑通前后端的宠物店商城作业如果你正在为毕业设计选题发愁或者在二手交易平台、开源社区里刷到“java微信小程序宠物店商城小程序设计与实现ssm源码含文档含教程”这个描述建议先别急着付款或下载。这个标题翻译成白话是一套基于 SSMSpring SpringMVC MyBatis框架的 Java 后端配一个微信小程序前端实现宠物店常见的商品浏览、购物车、下单、订单管理等功能并且附带论文和相关文档的毕设项目。它就是典型的 Java 课程设计 / 毕业设计方向面向的是需要交代码、交论文、能演示、能答辩的在校学生。这个项目能解决的问题很聚焦让你不用从零开始搭框架、写接口、设计数据库而是拿到一套已经分层完整的代码按文档配置好环境就能本地跑起来再把小程序端导入微信开发者工具就能联调。适合的人群也很明确——Java 基础刚入门、还没真正做过完整项目的同学想用一套成熟方案快速出一份“能演示、能解释、能改”的毕设成品。但这里有个很容易被忽视的现实这类项目网上很多质量参差不齐有的代码陈旧、用 Maven 构建不了有的数据库脚本和实体类对不上有的小程序端 AppID 写死导致登录功能直接废掉。所以这背后要做的事不只是“下载运行”而是要学会怎么把一套别人的代码变成你自己能讲清楚、能改、能答的项目。接下来这篇文章就按我实际接手这类项目的顺序来拆先搭后端骨架再把小程序端接进来然后打通登录和订单这两个核心链路最后帮你避开那些能让人通宵的坑。内容全部基于我处理这类 SSM 宠物商城项目的真实经验照着走就行。2. 把 SSM 后端骨架搭起来从环境到第一个可访问接口2.1 为什么这个项目选 SSM 而不是 Spring Boot现在出去面试或看招聘要求Spring Boot 才是主流但毕业设计选 SSM 依然有它的合理性。宠物店商城这个业务本身不复杂SSM 的三层结构Controller、Service、Mapper恰好能匹配论文里要写的“设计思路”和“分层架构”答辩时老师问你“为什么这么分层”你能答得很清楚。另外网上大量现成毕设项目就是 SSM 的你拿到手的这套源码大概率也是 SSM相比自己改用 Spring Boot直接在原框架上改工作量和风险都小得多。SSM 的配置过程比 Spring Boot 繁琐但这也是它适合当毕设的原因每一层配置都能写进论文的“系统配置”章节。比如 SpringMVC 负责请求路由MyBatis 负责 SQL 映射Spring 负责管理 Bean你需要手工写 web.xml、spring-mvc.xml、spring-mybatis.xml、jdbc.properties 这些文件每一个都能当作一个技术点来答辩。换句话说SSM 的项目更好“讲”而不是更好“写”。我一般会用 Maven 来管理依赖Java 环境建议 JDK 1.8。这里对新手多提一句动手之前先把 java 环境变量配置好很多同学项目跑不起来不是因为代码而是因为 JAVA_HOME 没配上命令行里 java -version 都报错。配好之后用下面的 pom.xml 片段拉依赖是第一步properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.2.25.RELEASE/version /dependency !-- SpringMVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.2.25.RELEASE/version /dependency !-- MyBatis 与 Spring 整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version1.3.2/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.47/version /dependency /dependencies这段配置的核心是版本对齐。Spring 框架三件套context 和 webmvc版本必须一致mybatis-spring 1.3.2 对应 MyBatis 3.4.x不要手痒乱升版本否则启动时会出现 ClassNotFoundException。用 5.2.x 的 Spring 而不是更新的版本不是因为老的更好而是网上大量教程和现成配置都是基于这套组合你搜报错信息时能搜到更多参考。JDK 1.8 Tomcat 8.5 是这个组合最稳的运行环境不要一上来就配 JDK 17。参数调整的要点如果本地 MySQL 是 8.x需要把 mysql-connector-java 换成 8.0.x并修改 driver-class-name 为 com.mysql.cj.jdbc.Driver否则连接数据库会报 class not found。这一步属于毕设老司机都会提前处理的问题。2.2 配置 spring-mybatis.xml数据库连接与 Mapper 扫描的联动SSM 项目里最容易让新手翻车的就是 spring-mybatis.xml 的配置。这个文件负责把数据源、MyBatis 的 SqlSessionFactory、Mapper 接口扫描这三件事串起来。常见的操作是直接复制网上模板结果数据库连接串里的时区参数有问题或者 Mapper 文件路径写错启动时不停报 “Invalid bound statement (not found)”。正常运行的最小配置长这样beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:contexthttp://www.springframework.org/schema/context xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd !-- 读取数据库连接参数 -- context:property-placeholder locationclasspath:jdbc.properties/ !-- 数据源使用 dbcp2 连接池 -- bean iddataSource classorg.apache.commons.dbcp2.BasicDataSource destroy-methodclose property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean !-- SqlSessionFactory -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.pet.entity/ /bean !-- Mapper 接口扫描 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.pet.mapper/ /bean /beans这份配置里的逻辑可以拆成三层来看数据源仅负责拿连接SqlSessionFactory 把连接变成能执行 SQL 的会话MapperScannerConfigurer 则把接口和 XML 文件绑定到一起。最关键的是 mapperLocations 的路径必须和你实际放 XML 文件的目录一致。我见过一个项目里 mapper 文件夹同时在 src/main/resources 和 src/main/java 下存在结果一直报找不到 SQL排查了半小时才发现是两份文件不一致。jdbc.properties 文件放在 src/main/resources 下内容按自己数据库情况改jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/pet_shop?useUnicodetruecharacterEncodingutf8useSSLfalse jdbc.usernameroot jdbc.password123456这里的 url 参数中 useSSLfalse 必须加MySQL 5.7 及之后版本默认开启 SSL 握手本地开发没有配置证书时不加会看到一串 SSL 警告虽然不影响连接但在答辩演示时如果日志刷屏观感很差。characterEncodingutf8 保证中文写入数据库不乱码宠物名、商品描述这些字段能不能正常显示就看这一项。2.3 编写宠物商品列表接口验证三层架构是通的配置文件的正确性要靠一个能跑通的最小接口来验证。别急着把整个后端代码全搬进去先写一个查询宠物商品列表的接口走完 Controller → Service → Mapper 全链路。这样能尽早暴露配置问题避免后面联调小程序时一次报几十个错根本不知道从哪下手。Mapper 接口与 XML 的写法package com.pet.mapper; import com.pet.entity.Pet; import java.util.List; public interface PetMapper { ListPet selectAllPets(); }对应的 PetMapper.xmlmapper namespacecom.pet.mapper.PetMapper select idselectAllPets resultTypecom.pet.entity.Pet SELECT id, name, category, price, image, stock FROM pet WHERE status 1 ORDER BY id DESC /select /mapperService 接口与实现public interface PetService { ListPet getAllPets(); }Service public class PetServiceImpl implements PetService { Resource private PetMapper petMapper; Override public ListPet getAllPets() { return petMapper.selectAllPets(); } }Controller 层的写法RestController RequestMapping(/api/pet) public class PetController { Resource private PetService petService; RequestMapping(value /list, method RequestMethod.GET) public ResultListPet list() { return Result.success(petService.getAllPets()); } }注意 Controller 用的是 RestController 而不是 Controller如果你用的是 SSM 的 XML 配置方式需要在 spring-mvc.xml 里加 mvc:annotation-driven /否则 RestController 不生效接口会变成找不到页面的 404。这是一个非常隐蔽的坑因为在 Spring Boot 里这些都是自动配置的而在 SSM 里少一行 XML 就完全不一样。ERC 的 Resource 注入是 Java 自带注解比 Autowired 更规范在只有一个实现类时两者效果一样但答辩时老师常会问“用什么方式注入”能说出 Resource 按名称注入、Autowired 按类型注入的区别算一个加分项。写完这三层把 Tomcat 启动起来浏览器访问 http://localhost:8080/api/pet/list如果返回一段 JSON 数据说明后端骨架已经打通。返回值通常包在 Result 对象里格式为 {code:200, message:success, data:{...}}code 字段在小程序端会在请求成功判断时用到。2.4 数据库设计与初始化脚本的检查要点宠物商城这类项目的数据库表一般有用户表、商品表、购物车表、订单表、订单明细表。我拿到源码后第一件事不是看代码而是先打开 SQL 脚本和实体类逐一比对。网上流传的毕设项目最常见的问题就是数据库字段和 Java 实体类对不上比如商品表有个字段叫 pet_status实体类里叫 petStatusMyBatis 开启驼峰映射后能自动转换但如果没开就需要在 XML 里写 resultMap 手动映射。在 spring-mybatis.xml 的 SqlSessionFactory 配置里加一个 settings 项就能解决大部分命名不一致的问题property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ /bean /property这个配置的作用是把数据库的下划线字段自动映射成 Java 的驼峰属性。name 字段对应数据库的 name 属性因为两边都一样所以没影响但 user_name 就会自动映射到 userName 上省去大量 resultMap 的编写工作。加上之后实体类的字段名就可以按 Java 规范写成驼峰而不必迁就数据库的命名方式。初始化数据一般会在 SQL 脚本里插入一批宠物商品数据我会建议把图片链接写成相对路径或本地占位因为很多源码里的图片链接已经失效了小程序端加载时会一直转圈。你可以在后端的 resources 目录下建一个 static/upload 文件夹配合一个虚拟路径映射的配置这样本地上传的图片才可能有稳定的访问地址。3. 小程序端的对接方案目录结构、请求封装与页面拆分3.1 微信小程序项目实例的目录结构别把代码全塞在 app.js微信开发者工具导入项目后第一件事是看目录结构。一个正常的宠物商城小程序pages 下至少要有 index首页、category分类、cart购物车、order订单、mine个人中心这几个页面。常见的“代码一团糟”的毕设小程序会有十几个页面全部堆在一个目录下样式靠全局 app.wxss 硬撑改一个页面影响所有页面这种代码后期很难改。我一般会要求严格按照下面这个结构来组织项目├── pages │ ├── index // 首页商品列表、搜索入口 │ ├── cart // 购物车增删改查、结算入口 │ ├── order // 订单列表待付款、待收货 │ ├── order-detail // 订单详情物流信息、订单状态 │ └── mine // 个人中心用户信息、订单入口 ├── utils │ ├── request.js // 封装 wx.request 的统一请求入口 │ └── auth.js // 登录态管理token 存取 ├── static │ └── images // 本地静态图片 ├── app.json ├── app.js └── app.wxss这个结构最核心的价值是把“页面”和“工具方法”分开。request.js 负责所有网络请求以后要改 baseURL 或统一加请求头只需改一个文件auth.js 负责 token 的存取登录过期时统一处理跳转。很多毕设拿到手跑不通就是因为请求代码散落在每个页面的 onLoad 里改登录逻辑时漏掉了一处导致部分页面一直 401。3.2 封装 request.js解决域名白名单和 baseUrl 的联动问题小程序有一个和浏览器很不一样的限制所有请求地址必须在小程序后台配置域名白名单否则真机预览时直接报 “url not in domain list”。开发阶段可以勾选“不校验合法域名”来绕过但演示答辩时如果评委用的是微信开发者工具问题不大如果是真机扫码预览就可能抓瞎。request.js 的标准封装const BASE_URL http://localhost:8080; function request(url, method GET, data {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token); wx.request({ url: BASE_URL url, method: method, data: data, header: { Content-Type: application/json, token: token || }, success: (res) { // 后端统一返回 { code:200, message:success, data:{} } if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { // token 过期跳转登录 wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(new Error(登录已过期)); } else { wx.showToast({ title: res.data.message || 请求失败, icon: none }); reject(new Error(res.data.message)); } }, fail: (err) { // 网络异常处理 wx.showToast({ title: 网络异常请检查后端服务, icon: none }); reject(err); } }); }); } module.exports { request, BASE_URL };这段封装的核心价值是把各种返回状态的处理统一收敛。后端如果你不按统一的 Result 结构返回数据这里的 code ! 200 判断就会失效。所以后端 Controller 返回时一定要用统一的 Result 类包装在 com.pet.common.Result 里定义 success 和 error 两个静态方法前端整个请求层才会稳定。参数要点BASE_URL 的 localhost 只能在微信开发者工具里访问真机上必须改成电脑的局域网 IP如 http://192.168.1.100:8080。如果真机请求失败优先检查这两处一是开发者工具里“详情 → 域名信息 → 不校验合法域名”是否勾选二是电脑防火墙是否放行了 Tomcat 的 8080 端口。这两个问题占了真机联调 80% 的失败原因。3.3 首页商品列表页面从接口数据到页面渲染的完整路径首页一般分三部分顶部搜索栏、轮播图、宠物商品列表。商品列表是核心代码在 index.js 的 onLoad 里调用封装的 requestPage({ data: { petList: [], loading: true }, onLoad() { this.fetchPetList(); }, fetchPetList() { const api require(../../utils/request); api.request(/api/pet/list, GET) .then(data { this.setData({ petList: data, loading: false }); }) .catch(() { this.setData({ loading: false }); }); } });对应的 index.wxmlview classcontainer block wx:for{{petList}} wx:keyid view classpet-card bindtapgoDetail />并将 BASE_URL 定义在全局 app.js 里。4. 核心链路打通微信登录、购物车与下单的实现细节4.1 微信登录的完整流程wx.login 不能直接拿到用户信息微信小程序的登录和后端传统登录有很大区别。wx.login 拿到的 code 不能直接换用户身份必须把它发到后端由后端调用微信接口的 jscode2session 换取 openid再以这个 openid 作为用户唯一标识。很多毕设项目为了省事在前端直接用 wx.getUserInfo 获取昵称头像然后当作用户数据存储这样不仅拿不到真实 openid还会在登录态校验时出问题。整个登录链路代码横跨小程序端和后端前端部分login() { const that this; wx.login({ success(res) { const api require(../../utils/request); api.request(/api/user/login, POST, { code: res.code }).then(data { // 后端返回 token 和用户信息 wx.setStorageSync(token, data.token); wx.setStorageSync(userInfo, data.userInfo); that.setData({ isLogin: true }); }); } }); }后端接收 code 并完成 session 换取之后会把 openid 和数据库里 user 表的 openid 字段比对如果不存在就新建一条用户记录然后生成 token 返回给前端。token 用 UUID 用户 ID 生成存入数据库或 Redis。由于毕设项目没有 Redis 环境常见做法是把 token 存到用户表的 token 字段里前端每次请求时携带后端在拦截器里校验。这里有一个需要注意的地方微信的 jscode2session 接口要求后端配置 AppID 和 AppSecret这两个值在小程序后台的“开发管理 → 开发设置”里获取。很多同学拿到源码时AppSecret 还是别人的直接沿用会导致登录一直报错。解决方法是注册自己的小程序账号把 app.js 后端配置文件里的 AppID、AppSecret 替换成自己的。4.2 购物车设计的表结构冗余商品快照是聪明做法购物车的数据结构直接决定了下单逻辑的复杂度。低质量的毕设项目通常会把购物车和订单设计成同样的表结构——每次都查商品表最新的价格看似没问题实际上一旦商品价格变动历史订单的金额就会对不上。更稳的做法是在购物车表里冗余一份商品 ID、商品名、单价、图片快照。购物车表的核心字段设计CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT 用户ID, pet_id INT NOT NULL COMMENT 宠物商品ID, pet_name VARCHAR(100) NOT NULL COMMENT 商品名称快照, price DECIMAL(10, 2) NOT NULL COMMENT 下单时单价快照, image VARCHAR(255) COMMENT 商品图片快照, quantity INT NOT NULL DEFAULT 1 COMMENT 购买数量, selected TINYINT(1) NOT NULL DEFAULT 1 COMMENT 是否选中1选中 0未选中, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_id (user_id) );这张表把商品名称和价格都冗余进来下单时直接读取快照不必再联表查 pet 表。这样做的核心价值是避免因商品信息改动影响已加入购物车的数据同时在下单时不用做复杂的多表关联查询性能更好。但代价是需要手动维护冗余字段比如用户修改了购物车数量时要同时更新快照字段。添加购物车的后端 Mapper 插入语句需要加一个 ON DUPLICATE KEY UPDATE 来实现重复添加时数量累加insert idinsertOrUpdateCart parameterTypecom.pet.entity.Cart INSERT INTO cart(user_id, pet_id, pet_name, price, image, quantity) VALUES(#{userId}, #{petId}, #{petName}, #{price}, #{image}, #{quantity}) ON DUPLICATE KEY UPDATE quantity quantity #{quantity} /insertusername 字段和 user_id 字段在 MySQL 中本身没有唯一约束要保证 ON DUPLICATE KEY UPDATE 生效必须在 pet_id 和 user_id 上建立唯一索引。如果没有唯一索引重复添加会直接插入新行而不是累加数量。4.3 下单与订单状态流转事务必须加在这里从购物车下单是系统里最需要保证数据一致性的地方扣库存、生成订单、清空购物车三步必须在一个事务里完成。很多毕设项目在这块偷懒三步各自独立提交就会出现“用户下单成功但库存没减”或“库存减了购物车还在”的脏数据情况。Service 层的下单方法Transactional(rollbackFor Exception.class) public Order createOrder(Long userId, ListInteger cartIds) { // 1. 根据 cartIds 查出购物车中选中的商品 ListCart cartList cartMapper.selectByIds(cartIds); // 2. 校验数据商品是否存在、库存是否充足 for (Cart cart : cartList) { Pet pet petMapper.selectById(cart.getPetId()); if (pet null || pet.getStock() cart.getQuantity()) { throw new BusinessException(宠物商品库存不足: cart.getPetName()); } } // 3. 生成主订单和订单明细 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(calculateTotal(cartList)); order.setStatus(0); // 0待付款 orderMapper.insert(order); for (Cart cart : cartList) { OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setPetId(cart.getPetId()); item.setPetName(cart.getPetName()); item.setPrice(cart.getPrice()); item.setQuantity(cart.getQuantity()); orderItemMapper.insert(item); // 扣减库存加乐观锁防止超卖 int rows petMapper.reduceStock(cart.getPetId(), cart.getQuantity()); if (rows 0) { throw new BusinessException(库存扣减失败: cart.getPetName()); } } // 4. 清空对应的购物车记录 cartMapper.deleteByIds(cartIds); return order; }这段代码有三个核心设计在答辩时会派上用场。首先是 Transactional(rollbackFor Exception.class)它保证中途任何一步抛异常前面所有的数据库操作全部回滚不会出现订单生成了但库存没扣的尴尬情况。然后是 generateOrderNo() 生成订单号格式一般用日期 随机数比如 202501011200001234。最后是 petMapper.reduceStock 这个 SQL 必须带库存足够的条件才能防止多个用户同时下单时超卖UPDATE pet SET stock stock - #{quantity} WHERE id #{petId} AND stock #{quantity}上面 SQL 的影响行数 rows 为 0 说明库存不足或商品已被删除此时直接抛异常整体事务回滚。这是用数据库行锁实现乐观锁的方式在毕设和课程设计的并发场景下完全够用不需要引入 Redis 分布式锁这些重型组件。下单后的订单状态字段 status建议用数字字典而不是直接存中文字符串0 待付款、1 待发货、2 待收货、3 已完成、4 已取消。小程序端展示时再做映射这样后端代码更简洁也方便后续扩展退款状态。5. 这些坑每年都有人踩SSM 宠物商城调试避坑实录5.1 坑项目启动报 ClassNotFoundException 但代码看着没问题现象Tomcat 启动时抛出 org.springframework.web.context.ContextLoaderListener 相关的 ClassNotFoundException代码和依赖看起来都是齐的。原因Tomcat 部署时没有把 Maven 依赖的 jar 包发布到 WEB-INF/lib 目录下。IDE 里右键项目 → Properties → Deployment Assembly 里没有添加 Maven Dependencies导致依赖只在编译时可用运行时找不到类。解决在 Eclipse 的 Deployment Assembly 中添加 Maven Dependencies或在 pom.xml 里显式配置 maven-war-plugin 的 webResources 来把依赖打进去。顺手检查 Tomcat 的发布目录看 WEB-INF/lib 下是否有 spring-webmvc 的 jar 包没有就发布配置有问题。这类问题在 idea 环境下几乎不会遇到因为 idea 的 Maven 集成自动处理了依赖发布但如果你是第一次从网上下的源码用 Eclipse 打开运行就会碰上。5.2 坑小程序请求全部 404但后端在浏览器里能正常访问现象微信开发者工具里所有请求都返回 404但同一个地址在浏览器地址栏直接打开能看到 JSON 数据。原因小程序请求到的路径和后端实际暴露的路径不一致通常是缺少上下文路径。后端部署时访问地址是 http://localhost:8080/pet_shop/api/pet/list但你小程序里 BASE_URL 配的是 http://localhost:8080漏掉了 /pet_shop。解决先在后端的 spring-mvc.xml 中加 mvc:annotation-driven / 并确认 RestController 生效然后在浏览器里访问完整地址确认后端路径确实带上下文。小程序 BASE_URL 按实际路径拼接比如 http://localhost:8080/pet_shop。注意修改后要重新编译并重启后端Tomcat 的 context 路径才会更新这个操作别忘。5.3 坑图片裂开无法显示但数据库里的 URL 看起来是对的现象商品图片在网页里能打开但小程序里始终裂图而且控制台报 “downloadFile:fail” 或 “url not in domain list” 之类的错误。原因小程序对图片资源和请求接口一样有域名白名单限制开发阶段必须在开发者工具里勾选“不校验合法域名”。另一个常见原因是图片地址是相对路径小程序 组件不会自动拼接域名。解决开发阶段在微信开发者工具右上角“详情” → “本地设置” → 勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。代码层面把图片地址统一处理成完整 URL写一个 formatImageUrl 的公共方法如果字段以 / 开头就拼上 BASE_URL。5.4 坑微信登录提示 code 无效或一直登录失败现象点击登录后一直弹“登录失败”后端日志显示调用微信接口时返回 errcode 40029invalid code。原因微信的 code 是一次性的而且有效期只有 5 分钟。前端 wx.login 的 code 发送给后端换取 openid 后立刻失效如果你在同一页面里调用了两次 wx.login第二次拿到的 code 再发给后端肯定报错。另外 AppID 和 AppSecret 不匹配也会报这个错误。解决在小程序端保证每次登录操作只调用一次 wx.login并把发起登录的按钮做防重复点击处理比如加一个 loading 状态。后端的 appid 和 secret 必须从自己的小程序后台复制不要沿用网上源码里的值。开发阶段可以在微信开发者工具的控制台里确认 code 是否只生成了一次。5.5 坑数据库中文乱码从 MySQL 到页面一路是问号现象插入数据库的中文变成 ???或者从数据库查出来在小程序端显示为乱码。原因三层都有可能出现问题——数据库表编码不是 utf8、JDBC 连接串没加 characterEncodingutf8、Tomcat 的 POST 请求没有设置 UTF-8 编码过滤器。最常见的是第三种被忽略。解决在 web.xml 里加一个 Spring 的 CharacterEncodingFilter强制所有请求和响应都使用 UTF-8filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping同时检查数据库建表语句确认 DEFAULT CHARSETutf8mb4。utf8mb4 是 utf8 的超集能存 emoji 表情很多商品名称和用户昵称里会有特殊字符用 utf8mb4 才能正常显示。6. 把项目从“能跑”变成“能答辩”验证清单与进阶方向拿到的源码能在本地跑起来只是满足了毕设的最低要求。想拿高分需要把系统验证完整并能在答辩时讲清楚几个关键设计决策。我建议按以下顺序验证整个系统先用微信开发者工具走一遍“注册登录 → 浏览商品 → 加购物车 → 下单 → 查看订单”的完整流程记录每一步的输入输出然后用不同账号分别测试确认订单数据是隔离的A 用户看不到 B 用户的购物车和订单最后打开数据库表检查购物车在订单生成后是否被清空、库存是否相应减少。这个流程里最容易出问题的支付环节因为微信支付需要企业资质个人做的毕设项目基本接入不了真实支付。常见的替代方案是把“支付”模拟成交付按钮点击后从待付款状态变成待发货状态订单状态这个逻辑要在论文里写清楚。如果坚持要接支付可以用微信小程序的模拟支付但个人主体的小程序大部分功能会被限制得不偿失。答辩时的技术亮点建议放在两个地方事务和乐观锁。下单方法里的 Transactional 和 reduceStock 的条件 UPDATE 语句是能体现和普通课程设计差距的地方。讲清楚“为什么要在 SQL 里加 stock #{quantity} 这个条件”比背十条快速排序的代码有用得多。另一个加分项是 token 登录态方案说明为什么用自定义 token 而不是 session以及如何处理 token 过期。如果还有精力做进阶方向有两个。一是把管理端补齐做一个简单的后台管理员可以上架宠物商品、修改库存、查看订单这也正好对应“商城”这个定位比单靠小程序端填数据更像完整系统。二是把 SSM 换成 Spring Boot如果答完辩还有时间用 Spring Boot 重写一遍后端很多配置自动处理掉你可以更专注在业务逻辑上。这其实也是你后面找实习或准备 java 基础面试时能讲的“真实项目经历”。面试官不会再问“你有没有用过 Redis / 消息队列”而是会问“你的订单系统是如何防止超卖的”“token 存在哪里怎么设计过期策略”“如果用户重复点击下单按钮你如何防止生成重复订单”。这三个问题你全部能从现在这套代码里找到答案。最后给你一个我自己的习惯所有收到的源码项目先跑通一遍核心流程再按自己的理解重新写一遍数据库脚本不要直接沿用原来的建表 SQL。这个重写的过程会让你把表之间的关系彻底吃透你在答辩时才不会被问倒。希望这篇笔记能帮你少熬几个夜顺利把宠物店商城做成交得出手的作品。本文还有配套的精品资源点击获取