
毕设选题选了中国剪纸微信小程序后端框架用 SSM乍听是老生常谈的组合但真把这两个东西揉在一起做出来坑远比想象的多。最近不少学弟学妹找我要这套源码问的问题集中在几个地方项目导入 IDE 后全是红叉、MySQL 连接报错、小程序端请求后端总是超时、数据库表不知道从哪里开始设计。这篇文章我就把这套“中国剪纸微信小程序 SSM”项目的完整设计与实现过程拆开讲一遍从选型原因到数据库设计从后端接口到小程序页面再到源码拿到手之后怎么跑起来尽量一次讲透让准备做类似毕设的同学少走弯路。1. 为什么是“剪纸”主题为什么是 SSM 组合1.1 剪纸题材在毕设选题里的天然优势很多同学选毕设题目时第一反应是商城、图书管理系统、博客系统这类“烂大街”的题目。不是说不行而是这类题目答辩老师看了太多需求分析写得再好也难出彩。剪纸这个主题天然有优势它属于非物质文化遗产自带文化属性做出来的东西能讲出社会价值开题报告和论文的“研究意义”部分很好写同时剪纸作品的图片资源极其丰富不管是百度、站酷还是各大博物馆的线上展厅都能找到大量高清素材不用像商城那样到处找商品图。更重要的是剪纸项目的功能边界非常清晰。用户端要做的事情无非是浏览作品、按分类筛选、查看详情、收藏、评论管理端是作品上传、分类管理、用户管理。整个业务模型不复杂但又比纯展示型页面多了交互逻辑非常适合作为毕设的复杂度。不会简单到让答辩老师觉得没工作量也不会难到做不完。选题一出来“中国剪纸微信小程序的设计与实现”这个标题在论文查重和答辩场合都显得有内容可讲。1.2 SSM 框架在今天仍然值得选的理由有人会问现在新项目不都用 Spring Boot 吗为什么还要用 SSMSpring SpringMVC MyBatis这个问题我每次都会被问到。我个人的看法是如果这是你自己的商业项目确实没必要用 SSM但这是毕设情况完全不一样。第一很多学校的《Java Web 开发》课程体系还是以 SSM 为主教材、实验、平时的课程设计都用 SSM你用它做毕设遇到问题能请教的人多网上资料也海量。第二SSM 的最大优势是“配置显式化”——你需要在 XML 里手动配置数据源、事务管理器、Mapper 扫描、视图解析器每一步都摆在你面前。这意味着你做完这个项目对 Spring IoC、SpringMVC 请求流程、MyBatis 映射原理的理解会比直接用一个“开箱即用”的 Spring Boot 深刻得多。第三答辩时老师大概率会问你“SpringMVC 从请求到响应的过程是怎样的”“MyBatis 的 #{} 和 ${} 有什么区别”你用 SSM 做一遍这些问题基本都能答上来。1.3 这套组合的整体技术路线这套源码的技术路线非常典型后端用 Maven 管理依赖Web 容器用 Tomcat数据库用 MySQL框架组合为 Spring SpringMVC MyBatis前端是微信小程序原生开发WXML WXSS JavaScript不使用 uni-app 之类的跨端框架图片资源通过后端静态资源映射访问小程序端通过 wx.request 请求后端接口登录走微信的 openid 流程。整体下来前后端数据通过 JSON 交互是一个完整的前后端分离应用——只不过这里的“前端”换成了小程序端。2. 系统功能拆解与数据库设计动手写代码前先画好图纸2.1 用户端和管理端的功能地图拿到项目先别急着写代码把功能拆清楚。这套项目我按两个端口来划分用户端小程序内首页轮播图 剪纸作品瀑布流列表 分类快捷入口分类浏览按剪纸类型单色剪纸、彩色剪纸、刻纸等或按地域流派筛选作品详情展示大图、作品介绍、作者、技法说明支持收藏和评论收藏列表集中展示我收藏过的作品个人中心用户登录状态、头像昵称、我的评论、关于我们管理端Web 后台管理员登录作品管理新增、编辑、下架剪纸作品上传图片分类管理维护分类名称、排序用户管理查看注册用户列表禁用异常账号评论管理删除不合适评论数据统计简单的用户数和作品数统计功能虽然不多但覆盖了常规管理系统的增删改查也覆盖了小程序端最常见的交互场景。确定功能之后再根据功能反推数据库表结构就会非常顺手。2.2 数据库表结构与字段设计这套项目我一共设计了 6 张核心表分别对应用户端和管理端的角色及业务。管理员表admin字段名类型说明idint主键自增usernamevarchar(50)登录账号passwordvarchar(100)密码建议 MD5 或加盐加密存储用户表user字段名类型说明idint主键自增openidvarchar(64)微信唯一标识务必加唯一索引nicknamevarchar(50)用户昵称avatarvarchar(255)头像 URLphonevarchar(20)手机号可空statustinyint状态1 正常0 禁用create_timedatetime注册时间分类表category字段名类型说明idint主键自增namevarchar(50)分类名称descriptionvarchar(255)分类描述sort_orderint排序值越小越靠前剪纸作品表paper_cut字段名类型说明idint主键自增titlevarchar(100)作品名称category_idint所属分类image_urlvarchar(255)作品图片地址descriptiontext作品简介techniquevarchar(50)技法阴刻、阳刻、套色等regionvarchar(50)地域流派authorvarchar(50)剪纸作者view_countint浏览量statustinyint1 上架0 下架create_timedatetime录入时间收藏表collect字段名类型说明idint主键自增user_idint用户 IDpaper_cut_idint作品 IDcreate_timedatetime收藏时间评论表comment字段名类型说明idint主键自增user_idint评论用户 IDpaper_cut_idint作品 IDcontentvarchar(500)评论内容create_timedatetime评论时间2.3 表关系与索引设计心得表关系其实很简单但有几个设计点想单独说一下。user 和 paper_cut 是多对多关系通过 collect 表中转所以 collect 表要建联合唯一索引uk_user_paper(user_id, paper_cut_id)防止用户重复收藏。paper_cut 和 category 是多对一关系category_id 字段上要建普通索引因为首页按分类筛选、后台按分类查询都用得到。另外openid 字段一定记得加唯一索引这是微信登录能正常关联用户的前提。还有一个容易被忽略的点外键不要建在数据库层面。很多教材喜欢用 FOREIGN KEY 物理外键但实际项目里没人这么做因为影响插入删除性能后期改数据也麻烦。表之间的关系在 Service 层去维护数据库表只保留普通索引就够。这样设计在毕设答辩时也能说出个所以然来——老师问“为什么不用外键”这就是一个很好的加分点。3. 后端 SSM 核心实现从配置到接口的完整链路3.1 Maven 项目结构和依赖拿到源码后第一步是看 pom.xml这里决定你本地的依赖能不能下载完整。这套项目用的是标准 Maven 结构核心依赖如下dependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.1.8.RELEASE/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.1.8.RELEASE/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.1.8.RELEASE/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.2/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.47/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.1.19/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.9.9/version /dependency /dependencies版本号在这里特意写老一点不是因为新版本不好而是因为毕设场景下 JDK 1.8 Tomcat 8 的环境非常普遍这些经典版本是经过大量项目验证的稳定组合。JDK 版本太高反而容易出现兼容问题。3.2 Spring 与 MyBatis 的配置落地SSM 的配置是整个项目里最劝退新手的部分但也是最能拿分的地方。Spring 管理业务对象SpringMVC 负责 Web 层的请求分发MyBatis 负责数据访问。三个框架的整合本质是让 Spring 容器统一创建并管理各层对象。applicationContext.xml 中最关键的是数据源、SqlSessionFactory、Mapper 扫描三件套!-- 数据源Druid -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/paper_cut_db?useUnicodetrueamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword value123456/ /bean !-- SqlSessionFactory注入数据源并扫描 Mapper XML -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean !-- Mapper 接口扫描 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.kaic.mapper/ /bean这个配置最值得注意的地方是 mapperLocations 的值它指定 MyBatis 的 XML 映射文件放在 classpath:mapper/ 目录下。如果项目打包后 Mapper XML 没被扫描到就会报 “Invalid bound statement (not found)” 错误。这种情况十有八九是 xml 文件没放到 resources/mapper 目录下或者路径写错。spring-mvc.xml 这边主要配置注解驱动、静态资源映射和视图解析器mvc:annotation-driven/ context:component-scan base-packagecom.kaic.controller/ !-- 静态资源映射存放上传的剪纸图片 -- mvc:resources mapping/upload/** location/WEB-INF/upload//3.3 Controller 层设计与统一返回格式后端接口返回给小程序的数据要统一格式不然前端处理起来非常痛苦。我封装了一个 Result 类所有接口都返回这个结构public class ResultT { private Integer code; // 200 成功500 失败401 未登录 private String message; // 提示信息 private T data; // 业务数据 public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } }Controller 层的写法很直观以作品列表接口为例RestController RequestMapping(/api/paper) public class PaperCutController { Autowired private PaperCutService paperCutService; GetMapping(/list) public ResultListPaperCut list(RequestParam(defaultValue 1) Integer page, RequestParam(required false) Integer categoryId) { ListPaperCut list paperCutService.queryPage(page, categoryId); return Result.success(list); } GetMapping(/detail/{id}) public ResultPaperCut detail(PathVariable Integer id) { paperCutService.increaseViewCount(id); return Result.success(paperCutService.getById(id)); } }统一返回格式之后小程序端只需要判 code 是否为 200不需要关心每个接口的返回结构是否一致。这一点在你同时写好多个模块时会特别省心。3.4 MyBatis 映射文件里的实用技巧SSM 的 MyBatis 部分最常写的是动态 SQL。比如分类筛选和分页select idqueryPage resultTypecom.kaic.entity.PaperCut SELECT * FROM paper_cut where if testcategoryId ! null and categoryId ! 0 AND category_id #{categoryId} /if AND status 1 /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select注意 where 标签会自动去掉多余的 AND如果漏写这个标签当 categoryId 为空时SQL 就会变成WHERE AND status 1直接报语法错误。另外LIMIT 分页的 offset 计算建议放在 Service 层避免在 Mapper 里写算术表达式。模糊搜索用的是LIKE CONCAT(%, #{keyword}, %)不要直接写成LIKE %${keyword}%。前者是预处理参数能防 SQL 注入后者是字符串拼接虽然也能跑通但如果你在答辩时写出这种写法基本就是送命题。3.5 图片上传、静态资源映射与部署路径剪纸项目里图片是核心资源上传功能必须跑通。图片上传的常规做法是后端接收 MultipartFile把文件写入服务器磁盘指定目录然后把访问路径返回给前端。要注意的是路径问题。本地开发时文件写入D:/tmp/upload/访问路径是http://localhost:8080/upload/xxx.jpg部署到 Linux 服务器后路径变成/usr/local/upload/。如果写死在代码里上线后图片必然 404。最好的做法是把上传根目录放到配置文件里用 Spring 的Value注解读取部署时改配置即可。图片访问则通过前面 spring-mvc.xml 里配置的/upload/**静态资源映射将请求转发到磁盘目录。这里有个 Tomcat 的坑要提醒默认情况下IDEA 中的 Tomcat 部署会把项目打包成 war 放在临时目录如果你把图片写到WEB-INF/upload/下重启后文件可能被清掉。所以图片要存到项目外的磁盘路径不要把图片写在 war 包内部。4. 小程序端设计从页面结构到请求封装4.1 小程序目录结构与页面划分小程序端的目录结构是原生开发的标准形式按页面功能划分miniprogram/ ├── app.js # 全局逻辑含登录逻辑 ├── app.json # 全局配置注册页面和底部 TabBar ├── app.wxss # 全局样式 ├── utils/ │ └── request.js # wx.request 封装 ├── pages/ │ ├── index/ # 首页 │ ├── category/ # 分类页 │ ├── detail/ # 作品详情页 │ ├── collect/ # 收藏页 │ └── user/ # 个人中心 └── static/ └── images/ # 本地静态资源app.json 里建议用自定义导航栏还是默认导航栏取决于你对小程序样式的把控能力。剪纸主题偏传统文化适合暖色调建议把全局的navigationBarBackgroundColor设置为偏红色系或者米黄色系能明显提升整体观感。不要在样式上花太多时间但也不要让页面看起来像没写过样式。4.2 首页瀑布流与分类切换的实现思路首页是用户进入小程序后的第一屏体验做不好后面全白搭。这套项目的首页包含顶部轮播图、分类横向滚动条、下方作品瀑布流。轮播图和分类切换本身不难难点在瀑布流。小程序官方没有瀑布流组件实现方案有两种一种是左右两列flex布局把图片轮流放进左右数组另一种是用grid两列等宽布局。剪纸作品的图片长宽比例不固定如果都用等宽方格长图会被严重裁切。我的做法是接口返回图片的同时返回图片的宽高比小程序端根据宽高比动态计算 image 的高度再放到对应的列中。图片懒加载直接给image标签加lazy-load属性这是小程序原生支持的不用自己写监听image src{{item.image_url}} modewidthFix lazy-loadtrue/imagemodewidthFix能保持图片比例自适应宽度是最适合列表场景的图片模式预处理高度都省了不过它会等图片加载完才确定高度所以页面会有轻微跳动。想要更流畅就得用上一步说的宽高比预计算方案。4.3 wx.request 请求封装、鉴权与请求时序在小程序里直接调 wx.request 可以跑通但项目代码会非常散每个页面都重复写 header、baseUrl、错误处理后期想改个接口地址得满项目找。所以我单独封装了一个 request.jsconst BASE_URL http://192.168.1.100:8080; // 本地调试用局域网 IP function request(url, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method || GET, data: 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) { wx.navigateTo({ url: /pages/login/login }); } else { wx.showToast({ title: res.data.message, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络请求失败, icon: none }); reject(err); } }); }); } module.exports { request };登录流程这块很多第一次做小程序的同学容易搞混。正确顺序是小程序端wx.login拿到 code把 code 发给后端后端拿着 code 去微信的 code2Session 接口换 openid 和 session_key后端用 openid 查自己的 user 表查到就返回用户信息查不到就自动注册一条新用户然后后端生成一个自定义 token也可以用 session_key返回给小程序端小程序存到 storage 里后续请求都带上。注意小程序端拿到的 code 是一次性的五分钟内有效只能使用一次。你可以在后端写一个UserController处理这个流程不要在每次请求的时候都调 wx.login只要 token 没过期就一直用 token。4.4 收藏、评论和个人中心的交互设计收藏功能是典型的“我用过很多次但还是想提醒一遍”的交互。收藏按钮不要用文字链接直接用图片按钮或 icon已收藏和未收藏用两套图标区分。关键点在于用户点击收藏后不要等后端返回再改变图标先本地同步改再发请求等后端失败再回滚。这样体感上会快很多也不会出现点击后“卡住没有反应”的感觉。评论列表要按时间倒序每条评论显示用户昵称、头像、评论内容和时间。时间格式化不要在前端做后端用 Java 格式化好返回yyyy-MM-dd HH:mm字符串省去小程序端处理时区的麻烦。个人中心页面要处理用户登录态。判断wx.getStorageSync(token)是否存在不存在就显示一个“点击登录”的按钮点击后走 openid 静默登录存在就展示用户头像和昵称并提供“我的收藏”“我的评论”入口。静默登录能拿到用户身份但拿不到昵称头像如果你想让用户展示头像昵称需要额外调用wx.getUserProfile让用户主动授权。毕设项目建议用wx.getUserProfile引导授权一次获取头像昵称后传给后端保存体验比较完整。5. 前后端联调与上线部署域名、HTTPS 和本地网络5.1 本地调试的第一道坎request 合法域名小程序有一个安全机制wx.request 只能请求在小程序管理后台配置过的合法域名微信要求域名必须备案且支持 HTTPS。本地开发时后端地址是http://localhost:8080这样的请求默认是被拦截的直接表现就是开发者工具里报 “url not in domain list” 或者请求直接 fail。破解办法很简单微信开发者工具的右上角“详情 → 本地设置”勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。这样本地请求就能走通。但要注意这个设置只在开发者工具和“体验版”下有效手机真机预览时如果没配置合法域名请求照样失败。真机调试时后端地址不能用 localhost手机里的 localhost 指的是手机自己。你要改成电脑的局域网 IP比如http://192.168.1.100:8080并保证手机和电脑在同一 WiFi 下。另外Windows 防火墙经常拦截 8080 端口的入站请求如果手机访问不了先去防火墙里放行这个端口。5.2 上线的域名、HTTPS 证书与反向代理如果项目要部署到服务器并且在小程序正式版里跑必须准备域名、HTTPS 证书、备案、小程序后台配置合法域名这四件套。这个流程最花时间的是备案往往要两三周所以想上线的话要提前准备。部署架构我建议用 Nginx Tomcat 的组合。Nginx 监听 443 端口处理 HTTPS配置 SSL 证书然后把/api/开头的请求反向代理到内网的 Tomcat 8080 端口把/upload/开头的图片请求代理到文件目录。这样做的好处是 Tomcat 不需要自己处理 HTTPS也不用暴露端口。以下是一段简化的 nginx 配置server { listen 443 ssl; server_name yourdomain.com; ssl_certificate /etc/nginx/ssl/yourdomain.pem; ssl_certificate_key /etc/nginx/ssl/yourdomain.key; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /upload/ { alias /usr/local/paper-cut-upload/; } }如果你实在没有域名也没有证书小程序发布时可以选“开发版”和“体验版”这两个版本可以用本地设置的跳过域名校验来调试但这只是权宜之计正式版必须要求合法域名。这个知识点答辩时老师常问建议背熟。5.3 联调阶段的高频报错排查表前后端联调阶段我统计了一下学弟学妹问得最多的问题整理成下面这张表报错现象可能原因解决办法小程序请求 404接口路径写错或 controller 没映射后端浏览器直接访问接口地址确认是否能返回 JSON请求 405GET/POST 方法不匹配检查 Controller 的 Mapping 注解是 GetMapping 还是 PostMapping请求 500后端 NPE、SQL 异常看 Tomcat 控制台日志定位堆栈第一行请求超时 cancel域名没配置、IP 不在同一网段开发者工具勾选不校验域名真机改局域网 IP图片加载失败静态资源映射路径不对浏览器直接访问图片 URL确认是否存在CORS 跨域报错后端没有加跨域配置小程序端 wx.request 不受 CORS 限制这个报错多在 H5 端出现Invalid bound statementMapper XML 没被扫描检查 mapperLocations 路径和 Mapper XML 命名空间这些报错里90% 都能通过看后端日志定位。所以我一直建议联调阶段把 Tomcat 控制台单独开一个窗口每次请求报错第一时间看堆栈信息而不是反复改前端代码。6. 拿到源码之后的三步检查法让项目在你的电脑上跑起来6.1 第一步环境版本先对齐拿到源码不要急着导入 IDE先检查环境。这套 SSM 项目对环境的版本要求比较老但稳定。我建议的版本组合是JDK 1.8 Maven 3.6 Tomcat 8.5 MySQL 5.7 微信开发者工具最新稳定版。如果你电脑装的是 JDK 17 或更高版本直接导入可能报错比如IllegalAccessError或者某些反射相关的异常。解决办法不是改代码而是把项目 SDK 级别改成 1.8IDEA 里File → Project Structure → Project → SDK选择 1.8没有就Project Structure → SDKs里添加。Maven 的 Java 版本也要同步改在 pom.xml 里检查 maven.compiler.source 和 target 是否都等于 1.8或者直接在 IDEA 的 Settings → Maven → Importing 里设置 JDK。很多“项目一堆红叉”的问题根源就是这里。MySQL 5.7 和 8.0 在驱动上有个差异要注意MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.DriverURL 还要带serverTimezoneAsia/Shanghai。如果你的本地是 MySQL 8.0直接使用 pom 里的 5.1.47 驱动也能连因为驱动向下兼容但 URL 里不加时区参数会报错。稳妥起见MySQL 8.0 用户可以直接改 pom 里的驱动版本为 8.0.30同时改数据库连接 URL。6.2 第二步配置文件的三处必改环境版本对齐后第二件事是改配置。一套源码从别人电脑跑到你电脑上配置不可能不改这个项目里最常见的三处改动第一处是数据库连接。打开db.properties或applicationContext.xml把数据库地址、用户名、密码改成你自己的。jdbc.urljdbc:mysql://localhost:3306/paper_cut_db?useUnicodetruecharacterEncodingutf8 jdbc.usernameroot jdbc.password你自己数据库的密码第二处是图片上传路径。打开 Controller 或者上传工具类找到Value(${upload.path})对应的配置项改成你电脑上的绝对路径比如D:/project/upload。同时确认 spring-mvc.xml 里的/upload/**映射和这个路径能对应上。第三处是小程序端的 BASE_URL。在utils/request.js里把 BASE_URL 改成你的后端地址。本地调试建议用局域网 IP比如http://192.168.1.100:8080部署后改成https://yourdomain.com。这三处改完大概率项目已经能跑起来了。如果还跑不起来往下看。6.3 第三步数据库初始化和启动顺序数据库初始化是最容易出错的一步。项目里一般会带一个 .sql 文件比如paper_cut_db.sql。用 Navicat 或命令行执行之前先确认 SQL 文件里的库名和配置文件里的库名一致。如果不一致直接改 SQL 文件开头的CREATE DATABASE语句或者在 Navicat 里新建一个同名空库再导入数据表。一张表一张表地导入也行但要注意 SQL 文件里表的创建顺序——如果有collect表外键引用了user和paper_cut请确保 user 表和 paper_cut 表先建好避免外键约束报错。启动顺序也有讲究。先启动 MySQL再启动 Tomcat。Tomcat 启动后后端项目会自动建数据源连接池如果 MySQL 没启动或者账号密码错误Tomcat 可以正常起来但第一次请求接口时才会报连接异常。所以看不出来启动失败但接口全部 500。所有步骤完成后测试一个最简单的接口浏览器访问http://localhost:8080/api/paper/list如果返回一段 JSON 数据说明后端已经跑通。再去微信开发者工具里导入小程序项目勾选“不校验合法域名”首页应当能加载出后端返回的作品列表。整个项目跑通之后我建议你在本地预置一些测试数据不要用空数据库去截图。找 20 到 30 张不同风格的中国剪纸图片分好类写好介绍把每个分类都填满。答辩展示时要演示浏览、筛选、收藏、评论、后台管理这五个核心操作有充分的数据支撑演示效果会好很多。而且这也能让你对这套剪纸小程序的业务流程熟悉度更高老师提问时更有底气。