
做毕设选了这个题目的人十有八九都在搜索引擎里翻过这几行字微信小程序、优购电商、SSM。为什么这个组合那么热因为它是一个性价比极高的全栈闭环——前端是微信生态里最主流的小程序形态后端是Java后台里最经典实用的SSM框架两者一拼就把从数据库建模、接口设计到移动端体验的一条完整链路全部练到了。这篇文章把我做“微信小程序SSM优购电商”的实际过程、踩坑记录和可以“抄作业”的方案完整写出来适合正在做相关毕设的本科生、想快速搭一套可演示电商系统的开发者以及准备毕业答辩但心里还发虚的同学。我会把注意力放在三个地方一是这个题目为什么要选、怎么设计才不会被导师追问到哑口无言二是后端接口和小程序页面里那些“真会用到”的细节比如登录态、购物车、分页加载、订单事务三是开发过程中最容易翻车、但网上很少有人说透的问题。基本都是我用真实代码踩出来的经验不是教科书搬过来的概念。1. 项目整体设计与技术选型1.1 为什么客户端选微信小程序而不是App或H5优购电商的目标用户是C端消费者做毕设时最怕的是“功能还没写环境先劝退”。原生App要处理应用商店审核、不同手机适配、证书打包对一个学生团队来说周期和成本都不可控。H5虽然开发快但支付能力、消息通知能力都要靠浏览器环境体验上差不少。微信小程序相当于“微信里的门店”用户扫一扫码就能进去逛免安装、免注册流程。更重要的是它直接把微信生态里的能力开放出来wx.login() 做静默登录、wx.requestPayment 唤起微信支付、wx.requestSubscribeMessage 做订阅消息通知。这些能力对一个电商系统来说正好是“闭环”的最后几块拼图。毕设答辩时老师问“为什么选小程序”把这些点说出来比一句“因为流行”要站得住脚得多。另外还有一个很现实的原因微信开发者工具的调试体验对新手非常友好。模拟器、真机预览、Network面板都是内置的遇到问题定位起来比纯前端H5项目快很多。我实测下来从零开始写一个用户端小程序页面熟练后一天能出三到五个页面效率比想象中高。1.2 为什么后端选SSM而不直接上Spring BootSSM是Spring、SpringMVC、MyBatis三个框架的组合国内教学和毕设里它有很长一段时间的统治地位。虽然现在Spring Boot已经成了企业开发的主流但对这个体量的电商系统来说SSM的轻量和可控性反而更适合学习和答辩展示。原因有三点。第一SSM的请求链路是显式的请求进来找Controller、Controller调Service、Service调Mapper、Mapper里写SQL操作数据库每一条线都清晰可见你在答辩时说“我用了三层架构”是能指给老师看的Spring Boot把很多东西自动化了反而说不清楚底层发生了什么。第二SSM需要手动维护配置文件比如Spring的applicationContext.xml、SpringMVC的spring-mvc.xml、MyBatis的mybatis-config.xml这一套配置写下来本身就是工作量也很容易在论文里画架构图。第三经典框架的提问点非常集中IoC是什么、AOP用在哪儿、SpringMVC的处理流程、MyBatis的#{}和${}有什么区别这些问题网上资料一大堆准备起来稳。总共来说这个项目体量用SSM是完全撑得住的还能帮你把框架底子打扎实。1.3 系统模块划分与整体架构整个系统我拆成了三层小程序客户端、SSM后端服务、MySQL数据库。用户端功能包括首页商品展示、分类浏览、商品搜索、商品详情、购物车管理、订单提交与支付、个人中心、地址管理。管理员端不需要再做一个小程序用一个轻量Web管理页面就够了负责商品上架/下架、库存修改、订单状态更新、用户列表查看。角色权限我采用了最简单的方案用户表里加一个role字段0表示普通用户1表示管理员。管理员直接通过数据库预置或一个小接口创建不必引入Spring Security这种重型权限框架。做毕设时把“权限控制”讲清楚即可不需要把企业级权限模型搬进来。架构上前后端通过RESTful风格的JSON接口通信所有接口返回统一的数据结构{ code: 0, msg: success, data: {} }。这个统一返回值的设计非常重要后面会详细说。数据库表设计了六张核心表用户表、分类表、商品表、购物车表、地址表、订单表外加订单项表。2. 数据库设计与接口规范2.1 核心表结构与设计思路数据库是整套系统的地基。我一开始为了赶进度随手建了一张“万能商品表”所有信息塞一起结果写到订单功能时发现历史订单数据根本没法保存又回头重构。先给出我最终使用的核心表结构这是踩完坑之后沉淀下来的版本。用户表user字段类型说明idint主键自增openidvarchar(64)微信openid唯一索引nicknamevarchar(64)昵称avatarvarchar(255)头像URLphonevarchar(20)手机号roletinyint0普通用户1管理员create_timedatetime创建时间商品表product字段类型说明idint主键自增category_idint分类ID关联category表namevarchar(128)商品名main_imagevarchar(255)主图URLdetail_imagestext详情图URLJSON数组存pricedecimal(10,2)售价original_pricedecimal(10,2)原价用于展示折扣stockint库存salesint销量用于排序statustinyint0下架1上架descriptiontext商品描述create_timedatetime创建时间订单表order和订单项表order_item是整个系统里最需要用心设计的部分。订单表只存订单维度的信息比如订单号、总价、收货人信息、支付状态订单项表存这个订单买了哪些商品每个商品买了几个、当时单价多少、商品快照是什么。为什么订单项里要冗余一份商品名称、商品图片和价格而不是直接通过product_id关联商品表因为商品是会变的商家可能修改价格、下架商品、更换主图。如果订单项里不留快照三个月后查一个历史订单显示的商品名和价格可能跟用户当时买的不一致这在电商系统里是严重的体验事故。所有正规电商系统都会在订单里做商品快照这个细节论文里写出来是加分项。地址表address也建议单独建不要在用户表里塞一个单地址字段。用户下单时可以选择一个默认地址也可以临时换一个收件地址单独一张address表关联user_id就能支持“多个收货地址”这个常见需求。2.2 统一接口返回格式与状态码前后端分离开发接口规范必须先定好不然小程序端对接的时候能折腾到你怀疑人生。我定的统一返回类是Result核心字段就三个code、msg、data。public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.msg success; r.data data; return r; } public static T ResultT error(Integer code, String msg) { ResultT r new Result(); r.code code; r.msg msg; return r; } }状态码我不用HTTP状态码那一套复杂的体系就自定义了几个够用的200表示成功400表示参数错误401表示登录态失效500表示服务端异常。小程序端封装一个request方法拦截所有响应统一处理401跳转登录页代码能省一大部分。这个小设计在答辩时也要讲“我通过统一返回体把异常处理收敛到了前端请求封装层业务代码里不用每个接口都写try-catch。”这就展示了架构意识。2.3 分页参数的约定商品列表、订单列表这类数据量大又不确定上限的接口必须做分页。我统一用pageNum页码从1开始和pageSize每页条数两个参数。后端返回的数据结构里除了列表本身还要带total总条数或hasMore是否还有下一页。我建议返回total小程序端用它算“有没有更多”更准确。{ code: 200, msg: success, data: { list: [], total: 57, pageNum: 1, pageSize: 10 } }有了total就能判断pageNum * pageSize total时显示“没有更多了”这个逻辑在后面的“加载更多”部分会用到。2.4 订单状态流转设计订单状态是整个业务逻辑的三驾马车设计成数字状态字段status我用四个状态0待付款、1待发货、2待收货、3已完成另外加一个负值 -1 表示已取消。状态流转是单向的待付款可以取消或支付成待发货待发货由管理员发货为待收货待收货用户确认收货变成已完成。不要设计成任意状态可以互跳那样会产生一堆脏数据。3. 后端SSM核心实现与常用注解3.1 SSM常用注解及作用网上那么多SSM常用注解的总结真正写项目时高频使用的其实就这几个我把它们按出现的位置归类注解使用位置作用备注Controller / RestControllerController类标记为请求处理器配合RequestMapping绑定URLRequestMapping类或方法映射请求路径可指定method如GET/POSTAutowired类字段依赖注入把Service或Mapper注入进来ServiceService实现类声明业务组件交给Spring管理Repository / MapperMapper接口标记数据访问组件让MyBatis扫描生成代理实现TransactionalService方法声明式事务下单等写多表操作必须加ResponseBodyController方法返回对象转JSON配合RestController可省略重点关注Transactional和Autowired。Autowired的原理是Spring容器按类型自动注入对于只有一个实现类的Service直接用字段注入最省事答辩时可能会被问到“Autowired按什么装配”答“先按类型byType找不到再按名称byName”即可。Transactional用于声明事务边界默认情况下遇到RuntimeException会自动回滚这点后面下单流程会具体说。3.2 三层架构的职责边界SSM项目和其它Java Web项目一样分层是骨架。Controller层只做参数接收和结果返回不写业务逻辑Service层封装业务规则和事务边界Mapper层只写SQL操作和结果映射。我在写这个项目时给自己定的一条规则是Controller里的方法不能超过十行Service里的方法尽量不要超过五十行。凡是要跨多个Mapper操作的逻辑比如“下单要查购物车、验库存、扣库存、生成订单、删购物车”一定放在Service的一个方法里并且加上 Transactional。这样一来排查问题时定位非常清晰页面报错先看Controller参数对不对再在Service里打断点看业务逻辑最后看Mapper的SQL是不是写错了。很多同学的代码喜欢把SQL写在Controller里刚开始能跑后期加需求时直接变成一团乱麻。3.3 微信小程序登录与session管理小程序登录是后端第一个要对接的接口也是整个系统的入口。微信小程序的登录流程是这样的小程序端调用wx.login()获取一个临时登录凭证code这个code只能用一次有效期五分钟。小程序把code发给后端。后端拿着code调用微信的jscode2session接口传入appid、secret、code换取openid用户唯一标识和session_key会话密钥。后端用openid查用户表不存在则自动注册一个新用户然后生成一个自定义的token返回给前端。小程序把token存在storage里后续所有请求在header里带上token。后端生成token我用的最简单的方案UUID 存Redis或用数据库token表保存。毕设项目如果不想引入Redis可以提前说明“生产环境建议用Redis并设置过期时间本系统用数据库存储token通过设置过期时间字段实现会话管理”这个话术在答辩时很实用。wx.login拿到code后后端关键代码如下RestController RequestMapping(/api/auth) public class AuthController { Autowired private AuthService authService; PostMapping(/login) public Result login(RequestBody MapString, String params) { String code params.get(code); if (StringUtils.isEmpty(code)) { return Result.error(400, code不能为空); } return authService.login(code); } }在AuthService里调用微信接口的部分用RestTemplate或HttpClient实现。核心逻辑是请求这个URLhttps://api.weixin.qq.com/sns/jscode2session?appidAPPIDsecretSECRETjs_codeCODEgrant_typeauthorization_code微信会返回一段JSON其中openid字段就是用户在微信体系内的唯一标识。这里有一个非常容易踩的坑不要直接拿openid当前端登录凭证。如果直接把openid传给前端任何人抓包看到别人的openid就可以伪造请求头伪装成那个用户登录这是严重的安全漏洞。正确做法是后端自己生成一个不透明的token把openid映射关系保存在服务端。3.4 商品分页查询Mapper写法和PageHelper的取舍商品列表接口是最典型的列表接口也最适合展示SSM的开发规范。我一开始用的是手写分页SQL逻辑很简单SELECT * FROM product WHERE status 1 ORDER BY sales DESC LIMIT #{pageNum}, #{pageSize}LIMIT第一个参数是偏移量计算公式是(pageNum - 1) * pageSize这个要放在SQL参数里提前算好。手写分页的优点是逻辑完全可控对小项目来说性能也够。后来为了演示效果我在项目中使用了PageHelper插件它能让你不用关心方言直接PageHelper.startPage(pageNum, pageSize)然后查询List返回结果会自动带上分页信息。这里有个使用细节必须提醒PageHelper.startPage()只对接下来的一条SQL查询生效。如果连续执行两条查询第二条会查出全表数据。所以使用PageHelper时startPage必须紧跟需要分页的那条Mapper查询中间不要插入任何其它查询或操作。3.5 下单与库存扣减的事务实现电商系统最核心的业务逻辑就是下单。先用文字梳理完整流程再说明为什么必须加事务。下单接口的入参是用户ID从token解析、收货地址ID、购物车选中条目ID列表。后端处理逻辑根据购物车ID列表查出商品信息校验商品状态为上架且库存充足。计算总价。生成订单主表记录状态为待付款。批量生成订单项明细。批量扣减商品库存。删除对应的购物车记录。这六个步骤必须在一个事务里完成。中途任何一步失败比如库存不足、地址无效、购物车记录被删除之前所有操作都要回滚否则会出现“订单生成了但库存没扣”或者“库存扣了但订单没生成”的数据不一致问题。在SSM里事务只需要在Service方法上加上Transactional注解Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private OrderItemMapper orderItemMapper; Autowired private ProductMapper productMapper; Autowired private CartMapper cartMapper; Override Transactional(rollbackFor Exception.class) public String createOrder(Long userId, Long addressId, ListLong cartIds) { // 1. 查购物车和商品校验库存 // 2. 计算总价生成订单号 // 3. 插入订单记录 // 4. 插入订单项 // 5. 更新库存 stock stock - quantity // 6. 删除购物车记录 // 返回订单号 } }库存扣减这里有一个并发情况下很经典的问题两个用户同时抢购同一个商品假设库存只有1件两个请求同时读到库存1都判断“库存充足”然后各自扣减。如果用上面的SQL写法可能出现库存扣成负数。解决问题的两种常见方案答辩时问到这个题目可以直接给出来方案一使用乐观锁扣减库存时带上条件。SQL写成UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}这行SQL返回影响行数如果影响行数为0说明库存不够或商品被并发操作改了再回滚事务报“库存不足”。这个方案简单高效也是我现在推荐的最优做法。方案二使用悲观锁查询商品时加FOR UPDATE。在MySQL的InnoDB引擎下SELECT ... FOR UPDATE会对这一行加锁其它请求必须等当前事务提交后才能查这行。这个方案写起来更简单但并发量大时会产生锁等待性能差一些。毕设项目选方案一就够了还能在论文里解释清楚“乐观锁”和“CAS思想”展示你对并发控制有概念。3.6 微信支付的简化与衔接真实微信支付需要企业资质申请商户号还要配置商户证书、回调地址、API v3密钥学生毕设很难走完整流程。我的处理是后端先完整实现“生成预支付单并保存订单待支付状态”的接口小程序端点击支付时通过wx.requestPayment发起真实支付。如果商户号没有申请下来就在前端做一个“模拟支付”按钮点击后直接调用后端接口把订单状态改成已支付。在论文里一定要把真实支付流程写清楚小程序端wx.requestPayment唤起收银台支付成功后微信服务器异步回调后端接口/api/pay/notify后端验证签名、修改订单状态、更新支付时间。这个回调设计很值得在答辩时讲因为微信支付的关键不是前端跳转而是后端的异步通知回调。4. 微信小程序前端实现的关键细节4.1 rpx适配与顶部导航栏高度写小程序页面绕不开rpx这个单位。rpx的全称是responsive pixel设计理念是“以750rpx对应屏幕宽度”也就是说不管手机屏幕是320px还是375px宽只要用rpx写都会按比例换算。写页面时宽度直接用750rpx统一标准即可。真正麻烦的是顶部导航栏高度。默认导航栏在小程序里占了屏幕顶部一块区域iPhone X之后的机型有刘海和底部安全区同样一段CSS样式在iPhone 8和iPhone 15上的表现完全不同。如果项目页面需要沉浸式大图或者自定义导航栏必须处理两个参数状态栏高度微信官方叫statusBarHeight和胶囊按钮位置。获取方式有两种// 方式一获取系统信息 const systemInfo wx.getSystemInfoSync(); const statusBarHeight systemInfo.statusBarHeight; // 方式二获取胶囊按钮位置 const menuButton wx.getMenuButtonBoundingClientRect(); const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.height;第二种方式里navBarHeight算出来的是自定义导航栏总高度公式的原理是“导航栏上下空隙相等所以胶囊到状态栏的距离乘2加上胶囊本身高度”。我在不同机型上实测过这个公式算出的高度适配度很高。还有几个实际坑Android和iOS的胶囊尺寸不同Android上胶囊大概高32pxiOS上高32px或34px状态栏高度在刘海屏和非刘海屏之间差异很大。因此标准做法是拿到数据后用inline style动态设置导航栏高度不要写死在CSS里。4.2 商品列表的“加载更多”与上拉刷新商品列表是电商页面里最典型的长列表场景。前端用scroll-view或者页面自带的滚动都行我推荐用页面自带的滚动因为小程序提供的onReachBottom生命周期钩子可以直接监听到滚动到底部不用手动计算滚动距离。加载更多的基本思路是维护pageNum、pageSize、list、hasMore等数据每次触底请求下一页把新数据append到列表后面。关键代码data: { pageNum: 1, pageSize: 10, goodsList: [], hasMore: true, isFetching: false }, onReachBottom() { if (this.data.isFetching || !this.data.hasMore) return; this.loadGoods(); }, loadGoods() { this.setData({ isFetching: true }); wx.request({ url: ${baseUrl}/api/goods/list, data: { pageNum: this.data.pageNum, pageSize: this.data.pageSize }, success: (res) { const { data } res.data; this.setData({ goodsList: this.data.goodsList.concat(data.list), hasMore: this.data.pageNum * this.data.pageSize data.total, pageNum: this.data.pageNum 1 }); }, complete: () { this.setData({ isFetching: false }); } }); }这里一定要用isFetching标志位防止重复请求。如果不加这个判断用户快速滚动时onReachBottom会在短时间内触发多次每次都会发一个请求页面就会出多条重复数据。上拉刷新有两种实现方式页面配置里开启enablePullDownRefresh: true然后监听onPullDownRefresh回调或者用scroll-view的refresher-enabled属性和bindrefresherrefresh事件。我用的是后者因为在自定义导航栏的页面里页面级下拉刷新有时会被导航栏的交互影响scroll-view下拉更可控。4.3 wx.login完整示例与请求封装登录这块把完整代码贴出来毕设里基本可以直接套用。先在app.js的onLaunch里调用登录逻辑App({ onLaunch() { const token wx.getStorageSync(token); if (token) { this.globalData.token token; return; } this.login(); }, login() { wx.login({ success: (res) { const code res.code; wx.request({ url: ${baseUrl}/api/auth/login, method: POST, data: { code }, success: (response) { const { data } response.data; wx.setStorageSync(token, data.token); this.globalData.token data.token; }, fail: () { // 登录失败延时重试 setTimeout(() this.login(), 3000); } }); } }); } });封装一个统一的请求方法request.js所有页面都用它发出请求const request (options) { return new Promise((resolve, reject) { wx.request({ url: ${baseUrl}${options.url}, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, token: wx.getStorageSync(token) || }, success: (res) { if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { // 登录态失效重新登录 getApp().login(); reject(res.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: reject }); }); };注意一个容易犯的错误wx.request的success回调里不能用普通函数方式访问外层this因为回调函数有自己的this指向。上面代码用了箭头函数arrow function箭头函数没有自己的this所以可以安全使用外层this。如果写成function(res) {...}里面用this.setData就会报错。这个问题在微信小程序里非常常见用const that this或者箭头函数都能解决推荐直接用箭头函数。4.4 购物车服务端存储与本地状态同步购物车我建议存服务端不要只存在小程序本地。因为用户换一台手机登录同一个微信号购物车如果存在本地就全丢了且购买逻辑基于服务端购物车校验更可靠。用户登录后购物车接口跟着token走数据天然和用户绑定。页面上购物车的数据结构是一个数组cartItems: [ { id: 1, productId: 10, name: 商品A, price: 99.00, image: ..., quantity: 2, checked: true }, { id: 2, productId: 12, name: 商品B, price: 59.00, image: ..., quantity: 1, checked: false } ]页面操作有加号减号、勾选/取消勾选、批量删除。每一项操作都先做本地setData更新界面保证交互流畅再异步调接口同步服务端数据。如果接口失败再回滚本地状态并提示。用一句话概括就是“本地先改服务端兜底”。计算合计金额时有一个血泪教训JavaScript浮点数精度问题。0.1 0.2在JS里等于0.30000000000000004商品价格一多就会出诡异的总价。解决方案是金额一律用“分”做整数计算比如99元存储为9900前端展示时再除以100转成元。这个细节写进论文里技术含量瞬间提升一个档次。4.5 订阅消息授权弹框的实现与坑订单支付或者发货后要通知用户小程序里用订阅消息。用户必须主动同意接收且一次同意只能发一条。代码示例wx.requestSubscribeMessage({ tmplIds: [模板ID], success: (res) { if (res[模板ID] accept) { // 用户同意了可以发送一条订阅消息 } else { // 拒绝授权不要反复弹窗骚扰用户 } } });常见坑有两个一是用户拒绝后不能每次进页面都弹授权框体验极差需要做状态记录比如只在支付成功页弹一次二是长期订阅需要申请专门的长期模板个人小程序基本申请不到所以项目里用一次性订阅是最现实的方案。另外要注意订阅消息的下发必须由后端调用微信接口触发前端只是收集用户的授权记录真正的发送逻辑在服务端完成。5. 调试、打包与上线部署5.1 接口调试开发者工具Network面板与本地抓包小程序开发最常见的调试需求是看请求发出了多少、响应是什么状态。微信开发者工具自带Network面板可以查看每个请求的Headers、RequestBody、ResponseBody对于绝大部分定位工作已经足够。如果要在真机上调试可以用开发者工具的真机调试功能配合“调试”模式中的Network信息。如果遇到一些比较隐蔽的接口问题比如HTTPS证书错误、请求被拦截、响应数据被异常截断可以用本地抓包工具做更底层的分析。原理是让手机和电脑处于同一个局域网电脑上运行抓包服务手机端对该服务开启信任后小程序的完整请求都能在电脑上展示出来。这样能看到微信开发者工具模拟器里“看不见”的内容比如真实网络环境下请求头的完整信息、TLS握手状态等。用的时候注意抓包工具看到的都是明文流量数据自己调试本地开发环境没问题不要乱抓陌生网络。更常见的做法是让后端把接口文档维护好前端和后端各搭各的联调时再用开发者工具的Network对齐字段。这套流程才是真正高效的比在模拟器里反复点按钮看效果省一半时间。5.2 小程序包体积超限问题与分包加载这是几乎每个人都会撞到的报错source size 2612kb exceed max limit 2mb。微信小程序主包大小限制2MB超过就无法上传。如果用的是uni-app框架发布到微信小程序时经常碰到这个爆包问题。从根本逻辑来说单个包体超限意味着要把体积“拆开”。拆包有几种手段按优先级来第一压缩图片。小程序包里放一张大图就可能占几百KB商品图等非关键资源全部放到服务器或对象存储用URL引用不要放进包代码。第二删除无用代码和依赖。检查miniprogram_npm目录是否有引入但没用到的库把console.log和注释多的代码清理干净。第三使用分包加载。在小程序app.json里配置subPackages{ subPackages: [ { root: pagesOrderPackage, pages: [ pages/order-list/order-list, pages/order-detail/order-detail ] } ] }分包加载的规则是小程序启动时只加载主包用户进入分包页面时才加载对应分包。所以把主包里的公共页面首页、登录、商品详情放主包把订单、个人中心这种二级页面放进分包包体积能降一个量级。主包总大小建议控制在1.5MB以内留一点余量给后续修改。实测中还有一个细节分包里的页面要做到“真分包”不能只是分发目录但代码里还是通过主包的路由跳转。配置了subPackages后跳转路径如果从根写分包需要带上root前缀路径否则可能跳不到分包页面。5.3 真机预览、体验版与多人试用开发完成后要让别人试用不能直接把代码发过去让人家装开发者工具。正确的流程是在微信开发者工具右上角点击“上传”填写版本号和备注代码会进入微信公众平台。然后在“版本管理”里找到开发版本点击“设为体验版本”再把体验者的微信添加为体验成员。体验成员扫码打开小程序就是体验版。如果需要收集试用反馈不要只是口头让大家去试。体验版有“体验数据”隔离的特性体验成员看到的页面数据取决于后台API用的是生产环境还是测试环境。如果要收集真实反馈建议把体验版配置的请求域名指到测试服务器并开放一个“测试模式”按钮方便体验者一键生成测试数据。体验版本还有一个易混淆点体验版和正式版是两套独立的审核和管理流程。改动代码后必须重新上传并设为体验版原来那条体验版的二维码链接本身不会更新内容。多人同时体验时每个人扫同一个码看到的都是同一份体验版本代码反馈是统一的这对收集集中测试意见很友好。5.4 服务器部署与HTTPS证书小程序正式版本要求所有请求域名必须是HTTPS且需要在微信公众平台后台配置request合法域名。域名要求是备案过的并且证书有效。部署方案准备一台云服务器装好JDK 8、MySQL 8、Tomcat或直接跑打包好的Spring Boot jarSSM项目一般用Tomcat部署war包。初始化数据库把SQL文件导入。打包后端项目上传到服务器启动。在域名服务商处申请SSL证书。国内主流云服务商都有免费证书下载并配置到Nginx或直接配置到Tomcat的server.xml。在小程序后台的“开发设置”里配置服务器域名。这里有一个项目结构上的建议如果后端是SSM的项目结构尽量打包成war部署到Tomcat如果是Spring Boot结构直接java -jar跑更简单。毕设项目如果没有对外真实运营需求本地演示时可以让开发者工具勾选“不校验合法域名”用本地或局域网的IP地址联调这样省去了申请域名和证书的很多麻烦。正式答辩前把本地环境跑通、把演示数据准备好比盲目折腾服务器稳妥得多。6. 常见问题排查与避坑指南把我在整个开发过程中遇到的、以及在同学们的项目里反复出现的典型问题集中整理成一张速查表方便定位。现象可能原因解决方法页面请求一直失败域名未配置到小程序后台合法域名列表登录微信公众平台把HTTPS域名加进request合法域名登录成功但所有接口报401token过期或未正确保存在storage检查请求封装里header是否带上了token检查后端token有效期判断商品图片显示空白图片是http地址或相对路径小程序禁止使用https图床或后端返回完整的https图片URL商品列表下拉不加载页面高度不够没有触发滚动到最底部检查页面是否有enablePullDownRefresh、onReachBottom是否在页面根层不要放到自定义组件里列表出现重复数据触底事件重复触发没有去重标志加上isFetching标志位在请求完成前阻止再次发起支付成功但订单状态不变支付回调通知未打到后端接口检查支付回调URL配置测试后端接口能独立收到微信请求库存扣成负数没有做条件更新或事务使用stock quantity的条件更新SQL并加事务订单状态混乱状态机设计不严谨明确状态流转方向不要任意状态互相跳转小程序包上传超2MB图片、依赖库太大压缩图片、分包加载、移除无用依赖自定义导航栏在不同机型错位没有动态获取胶囊位置计算高度用wx.getSystemInfoSync().statusBarHeight和wx.getMenuButtonBoundingClientRect()动态计算浮点价格计算错误JavaScript浮点数精度问题金额以分为单位存储和计算展示时再除以100除了表格里这些问题还有几个不一定报错但会被导师盯上的细节。第一个是SQL注入。MyBatis里写SQL时传参用#{}而不是${}。#{}会被预编译成占位符?由JDBC驱动做参数绑定天然防注入${}是字符串拼接直接拼到SQL里有注入风险。以“按商品名模糊搜索”为例select idsearchByName resultTypecom.example.entity.Product SELECT * FROM product WHERE name LIKE CONCAT(%, #{keyword}, %) /select这个写法安全且能正常模糊匹配。如果写成LIKE %${keyword}%测试时好使一旦被人传个; DROP TABLE product; --之类参数数据库就危险了。这是个一票否决的安全问题。第二个是日志。SSM项目要养成交代日志的习惯特别是在Service层的方法入口、数据库操作的异常处。用LoggerFactory.getLogger打印日志比自己写System.out.println强一百倍。答辩时被问“线上报错怎么排查”回答“看日志定位异常堆栈”是一个标准答案。第三个是密码和密钥管理。小程序的appid和secret不要硬编码在前端代码里secret只能存在后端。有的同学为了省事把secret写在wx.request的URL参数里这等于把钥匙插在门上被人抓包就泄露了。正确做法是把secret放在后端配置文件里由后端统一调用微信接口。7. 从开发到答辩的完整建议7.1 论文写作要点与整体节奏毕设论文的常见结构是绪论背景与意义、需求分析、系统设计、系统实现、系统测试、总结展望。结合这个项目每一章的核心输出我总结一下第一章绪论里“国内研究现状”可以写微信小程序的生态发展、电商小程序的应用趋势但不要空喊“随着移动互联网的发展”要落到具体数据和技术细节上。第二章需求分析先画用例图游客/用户/管理员三个角色各能干什么每个用例对应到功能模块。第三章系统设计画架构图、E-R图、主要用例时序图。第四章系统实现按“登录模块、商品模块、购物车模块、订单模块”拆章节每个模块给出核心代码片段和界面截图重点解释关键技术。第五章测试写功能测试用例表最后加一点性能测试或安全测试的描述。论文节奏上我强烈建议先写“系统设计”再写“需求分析”因为很多人在写需求分析时根本不知道系统会做成什么样空想出一堆需求代码实现时全变了。先搭好表结构、定好接口需求分析一章等于“照着已有功能写使用说明”真实且高效。7.2 答辩常见提问与应对答辩老师问得最多的几个问题我提前把标准回答备好了问SSM框架的请求流程是怎样的 答请求进入SpringMVC的前端控制器DispatcherServlet通过HandlerMapping找到对应的Controller方法方法内部调用ServiceService调用Mapper接口Mapper在MyBatis中通过XML或注解找到SQL并执行返回结果逐层向上传最终由HandlerAdapter封装成JSON响应给前端。问数据库表之间的关联关系 答用户和订单是一对多订单和订单项是一对多商品和分类是多对一用户和购物车是一对多购物车和商品是多对一。问为什么订单表要冗余商品信息 答商品可能改价、下架、删除历史订单需要保留用户购买时的商品信息和价格快照这是电商平台的通用做法。问事务怎么实现的 答通过Spring的声明式事务TransactionalSpring AOP在方法前后织入事务开启、提交和回滚逻辑。默认遇到RuntimeException自动回滚通过rollbackFor配置也可以指定回滚的异常类型。问如果用户下单后不支付订单怎么办 答设计了订单状态机待付款订单超过一定时间未支付可以自动取消或由用户手动取消。论文里可以写一个定时任务扫单接口或者在小程序端做倒计时提醒但核心是说明这个状态流转是有规划的。能把这几个问题从容答出来答辩就已经成功了一大半。最怕的是对着PPT念代码老师一追问就卡壳。保持“我先说流程再演示结果”的节奏就不会出现太尴尬的场面。7.3 如何让项目比同类选题更有亮点“微信小程序电商系统”是毕设里的常青树很多同学做的都差不多但只要在几个点上下点功夫就能让项目明显拉开差距。第一个亮点是搜索功能。不做数据库全表扫描而是用一个keyword参数配合SQL的模糊查询同时在前端做一个搜索历史记录这个功能看起来简单但交互完整很容易在演示时出效果。第二个亮点是前端体验细节。比如商品列表的骨架屏、加载中的loading动画、空状态的插画、页面切换的动画这些都是低成本高感知的部分。评委老师打开小程序第一眼看到的是界面不是后端接口。把首页做得干净、加载速度快印象分会高很多。第三个亮点是接口安全。登录接口做频率限制防止恶意刷验证码所有接口统一校验token为后续扩展鉴权框架预留接口。这两个点看似轻描淡写但体现的是工程意识非常加分。第四个亮点是数据统计。管理员页面加一个简单的可视化统计比如按天的订单量柱状图、商品销量排名。后端写一个SELECT DATE(create_time), COUNT(*) FROM order GROUP BY DATE(create_time)前端用ec-canvas画个柱状图五分钟就能做完但整个项目的完整度立刻上了一个台阶。说到底毕设项目不是跟别人比谁功能多而是比谁更能把“为什么这么设计”讲得清楚。做小程序电商这个选题只要把登录、商品、购物车、订单、支付这几条核心链路彻底打通把SSM每一层的作用说明白在技术上就已经是一个完整且合理的学生项目了。真要说个人体会做这类全栈项目时最忌讳一上来就写代码。先把数据库字段设计好把接口列表列清楚把订单状态流转画明白哪怕多花两三天后期开发的顺畅程度完全不一样。数据设计对了后面所有接口都好写数据设计乱了越写越乱最后不是加需求是填坑。另一个建议是提前把微信开发者工具里的“上传时自动压缩图片”和缓存设置研究明白目录能省很多体积也能少遇到一点莫名其妙的缓存问题。我到现在还记得第一次把小程序版本传到手机真机上、扫码打开、下单、支付、后台订单状态同步改变时的那种成就感。这条路走下来所有踩过的坑最终都变成了答辩时的底气。你也一样按部就班把每一步做扎实这个小系统一定能撑起一篇合格的毕业论文。