每年到课程设计季,“微信小程序 + SSM”这种组合总是最抢手的选题方向。健身达人微信小程序就是其中很有代表性的一个:前端用微信小程序承载用户交互,后端用 SSM(Spring + SpringMVC + MyBatis)提供接口服务,再加一套 MySQL 数据库和完整的课程设计文档。这个题目好在功能边界清晰、需求容易讲清楚、代码量适中,既能覆盖全栈开发的关键环节,又能把数据库设计、接口设计、小程序联调这几个课程设计必考的点全部串起来。
如果你正在做类似题目,或者拿到一份这种“文档+源码”项目不知道怎么讲清楚设计和实现,这篇文章应该能帮你少走不少弯路。我会从需求拆分、后端实现、小程序端实现、联调部署、踩坑复盘五个维度,把这类项目的完整脉络梳理一遍,核心目标是让你不但能把项目做出来,还能在答辩和文档里说清楚每一个设计决策的理由。
1. 先从需求说起:健身达人小程序的功能边界
1.1 用户能干什么:核心模块拆解
课程设计的第一步不是写代码,而是把“健身达人”这四个字拆成可落地的功能点。健身类应用放在小程序里,最合理的切入点就是“轻量、记录、激励”。我当时把这个项目拆成四个用户端核心模块:
- 课程浏览:按增肌、减脂、塑形、拉伸等分类展示健身课程,支持课程列表分页、课程详情查看。
- 课程打卡:用户完成训练后记录打卡数据,包括训练日期、时长、消耗卡路里、备注。
- 统计与日历:按月展示打卡日历,统计累计打卡天数、本月训练总时长。
- 个人中心:用户登录态维护、个人资料(身高、体重、性别)编辑、收藏的课程列表。
后端管理端一般再加两个模块:用户管理和课程管理。管理员可以查看用户列表、维护课程上下架。功能模块这样一拆,需求文档里该画的功能结构图、用例图就都有素材了。更重要的是,每个模块都能对应到一张数据库表和几个核心接口,功能边界非常清楚。
很多同学在需求分析阶段喜欢把功能做得很庞大,比如加社交动态、加私教预约、加商城支付。我建议课程设计还是克制一点,一个完整的闭环比十个半成品更拿分。健身达人小程序的核心闭环就是“浏览课程 -> 完成训练 -> 打卡记录 -> 查看统计”,这个闭环打通了,项目的完整度就已经很高了。
1.2 为什么是SSM + 小程序:选型逻辑
这个选题的第二个关键问题是:后端为什么用 SSM,而不是 Spring Boot、JSP Servlet,或者纯 Node.js?
先看前端。微信小程序是一个天然独立的客户端,和后端通过 JSON 接口通信。这意味着后端根本不需要负责页面渲染,它只需要提供数据接口。如果用 JSP 做后端页面,那就和“小程序 + 后端接口”的架构冲突了,后端既要做接口又要渲染 HTML,逻辑会非常别扭。SSM 三个框架在这里的分工很清楚:Spring 管 Bean 和事务,SpringMVC 管接口路由和参数绑定,MyBatis 管数据库访问,每一层都有明确职责。
再看为什么不用 Spring Boot。如果是真实的商业项目,我当然推荐 Spring Boot,因为它把配置简化了一大截,内嵌 Tomcat、自动装配、起步依赖都很省事。但很多高校课程大纲仍然以 SSM 为教学重点,课程设计题目也明确要求使用 SSM。这种时候用 SSM 并不是技术落后,而是“教学训练”本身需要你把 Spring 容器配置、SpringMVC 的 HandlerMapping、MyBatis 的 SqlSessionFactory 这些底层概念亲手搭一遍。等你把 SSM 整个跑通了,再去看 Spring Boot 反而会觉得特别轻松,因为很多自动配置做的事你都手写过了。
前端的选型上,原生小程序是课程设计最稳妥的选择。原生框架语法直白、调试工具成熟、代码结构清晰,用 Vue 语法习惯的人可能需要短暂适应一下,但整体学习成本远低于 uni-app 这类跨端框架。如果项目目标是“一套代码同时发布到小程序、App、H5”,那我会建议 uni-app;但课程设计通常只需要跑通微信端,原生开发的代码量和答辩解释成本都更低。
前后端的技术组合确定之后,整个项目的开发路径就清晰了:设计数据库 -> 搭 SSM 后端 -> 写接口 -> 联调小程序 -> 写文档。下面我按这条主线展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 后端的SSM实现:表结构、接口与分层代码
2.1 五张数据表怎么设计最顺手
健身达人小程序的后端数据不复杂,核心表大致就是用户表、课程表、打卡记录表、收藏表和管理员表。真正需要花心思的是字段类型和唯一约束。
用户表的核心是 openid 字段。微信登录拿到的 openid 是用户唯一标识,必须设置为唯一索引,否则同一个用户可能重复入库。另外身高体重用 DECIMAL(5,2) 而不是 FLOAT,避免浮点数精度问题。打卡记录表里一定要加一个 (user_id, checkin_date) 的唯一索引,因为业务规则是“每个用户每天最多一条训练打卡记录”,数据库层面的约束比代码层面判断更可靠。
这个建表脚本可以直接拿去改:
sql复制CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
openid VARCHAR(64) NOT NULL UNIQUE,
nickname VARCHAR(50),
avatar_url VARCHAR(255),
gender TINYINT DEFAULT 0,
height DECIMAL(5,2),
weight DECIMAL(5,2),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE course (
id INT PRIMARY KEY AUTO_INCREMENT,
course_name VARCHAR(100) NOT NULL,
category VARCHAR(50),
difficulty TINYINT,
duration INT,
cover_url VARCHAR(255),
video_url VARCHAR(255),
description TEXT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE checkin_record (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
course_id INT,
checkin_date DATE NOT NULL,
duration_minutes INT DEFAULT 0,
calories INT DEFAULT 0,
remark VARCHAR(255),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_date (user_id, checkin_date)
);
CREATE TABLE favorite (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
course_id INT NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_course (user_id, course_id)
);
打卡数、累计时长这种统计数据不必单独建表,直接通过 SQL 对 checkin_record 进行聚合查询。课程设计阶段不用提前考虑性能优化,等数据量真的大了,再引入统计表或者缓存也不迟。这个设计取舍在文档里可以写清楚:用最简单的结构满足功能需求,避免过度设计。
2.2 Controller-Service-Mapper的分层习惯
SSM 最核心的代码组织方式就是三层架构:Controller 只负责接收请求和返回结果,Service 负责业务逻辑,Mapper 负责数据库操作。这个分层不是形式主义,它带来的直接好处是:以后改业务逻辑只需要动 Service,改 SQL 只需要动 Mapper,改接口只需要动 Controller,互不干扰。
以课程列表接口为例,Controller 层是这么写的:
java复制@Controller
@RequestMapping("/course")
public class CourseController {
@Autowired
private CourseService courseService;
@RequestMapping("/list")
@ResponseBody
public Result list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String category) {
return Result.ok(courseService.getCourseList(category, page, size));
}
}
这里有两个细节特别容易在课程设计里翻车。第一,@ResponseBody 一定要加上,不加的话 SpringMVC 会把返回值当成视图名称去解析,前端收到的就不是 JSON 而是 406 错误。第二,分页参数直接通过 @RequestParam 接收,默认值写在注解里,比在方法体里手工判断简洁得多。如果你用的 Spring 版本支持 @GetMapping、@PostMapping 这类派生注解,可以换掉 @RequestMapping,代码语义更清晰,不过要注意课程模板里有没有强制要求老写法。
Service 层我习惯先写接口再写实现类,这样控制器只依赖接口,将来更换实现方式时控制器一行都不用改。比如 CourseService 定义分页查询方法,实现类里通过 CourseMapper 查询数据。很多同学嫌接口实现类成对出现太麻烦,课程设计这种规模确实可以只用类,但完整一点的项目结构更贴近企业规范,文档里也更好解释。
Mapper 层用 MyBatis 的 XML 方式管理 SQL,和注解方式相比,最大的优势是支持动态 SQL。分类筛选和分页一起出现时,注解写起来很难看,XML 里的 <where> 和 <if> 标签就非常灵活:
xml复制<select id="selectCourseList" resultType="com.example.entity.Course">
SELECT * FROM course
<where>
<if test="category != null and category != ''">
AND category = #{category}
</if>
</where>
ORDER BY id DESC
LIMIT #{offset}, #{size}
</select>
注意这里的 offset 需要在 Service 层提前算好:offset = (page - 1) * size。不要把这个计算逻辑塞到 Controller 里,因为分页计算属于业务规则,应该归 Service 管。
2.3 统一响应体:避免前端判断逻辑写成一锅粥
后端接口返回的数据格式如果不统一,小程序端每个页面都得单独写一遍判断逻辑,非常痛苦。我在这个项目里定义了一个简单的 Result 类,所有接口都返回统一样式:
java复制public class Result {
private Integer code;
private String message;
private Object data;
public static Result ok(Object data) {
Result result = new Result();
result.code = 200;
result.message = "success";
result.data = data;
return result;
}
public static Result error(String message) {
Result result = new Result();
result.code = 500;
result.message = message;
result.data = null;
return result;
}
}
前端的处理逻辑就变成了:先判断 HTTP 状态码,再判断业务 code,然后取 data。业务异常(比如打卡重复提交)也用 HTTP 200 返回,但是 code 不同,由前端根据 code 提示业务错误。这种约定很常见,但必须从项目一开始就统一,而不是写到一半再补。
还有一个必须说的细节:Java 后端的日期类型默认序列化成时间戳或复杂格式,小程序端解析非常麻烦。我建议在实体类日期字段上加 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),或者通过全局配置统一日期格式,保证前端拿到的日期是一个干净的字符串。
3. 小程序端节奏:目录、请求封装与登录态
3.1 原生小程序目录与全局配置
后端接口就绪后,小程序端就可以开始写了。原生小程序的目录结构非常简单,一个典型的健身达人小程序大概长这样:
code复制├── app.js
├── app.json
├── app.wxss
├── utils/
│ └── request.js
├── pages/
│ ├── index/
│ ├── course/
│ ├── checkin/
│ └── user/
└── static/
app.json 里配置页面路由和底部 TabBar。健身类应用底部一般放三个 Tab:首页、打卡、我的,这样信息架构最简单。首页做课程推荐和分类入口,打卡页做日历和当日打卡表单,我的页面放登录状态和个人资料。每个页面文件夹内部保持固定结构:.wxml 管结构、.wxss 管样式、.js 管逻辑、.json 管页面配置。
环境配置要单独拎出来放在一个文件里,方便切换开发环境和生产环境。课程设计阶段最常见的玩法是后端跑在本机 Tomcat,小程序开发者工具里直接用局域网 IP 访问,这个配置后面讲联调时我会详细展开。
3.2 request.js如何统一管理接口
小程序内置的 wx.request 和浏览器的 fetch 类似,但没有任何拦截器和统一处理能力。页面里直接写 wx.request 会带来一个很现实的问题:几十个页面都在重复写 url、header、success、fail,改一次接口地址要全局搜索替换。我把请求逻辑统一封装到 utils/request.js 里:
javascript复制const BASE_URL = 'http://192.168.1.100:8080/ssm_fitness';
function request(path, method = 'GET', data = {}) {
return new Promise((resolve, reject) => {
wx.request({
url: BASE_URL + path,
method,
data,
header: {
'Content-Type': 'application/json'
},
success(res) {
if (res.statusCode === 200) {
resolve(res.data);
} else {
wx.showToast({ title: '请求失败', icon: 'none' });
reject(res);
}
},
fail(err) {
wx.showToast({ title: '网络异常', icon: 'none' });
reject(err);
}
});
});
}
module.exports = { request, BASE_URL };
封装之后,页面里调用接口就变成一行:
javascript复制const { request } = require('../../utils/request');
request('/course/list', 'GET', { category: '减脂' })
.then(res => {
if (res.code === 200) {
setData({ courseList: res.data.list });
}
});
Promise 化最大的好处是可以配合 async/await 写异步逻辑,比 success 回调嵌套清晰太多。这个封装只做了基础层,后续要加登录态 token 时,只需要在这里统一往 header 里塞,页面代码不用改动。
3.3 用缓存维持登录态,而不是每次启动都重新登录
登录态是这类项目里最容易做得粗糙的部分。标准流程是:小程序端调用 wx.login() 拿到临时 code,把 code 传给后端,后端拿 code 去微信接口换 openid 和 session_key,然后返回一个登录标识给前端。但课程设计里很多同学图省事,直接让用户填写用户名密码,这就失去了微信小程序“一键登录”的体验优势。
如果只是课程设计,后端拿不到或不想配微信支付等高级能力时,推荐这样做:后端 user/login 接口接收 code,如果查不到 openid 就自动创建用户账号,查到了就直接返回用户信息,同时生成一个简单的 token(用 UUID 就行)返回给前端。前端把 token 和用户信息写进本地缓存:
javascript复制wx.login({
success: (res) => {
request('/user/login', 'POST', { code: res.code })
.then((result) => {
if (result.code === 200) {
wx.setStorageSync('token', result.data.token);
wx.setStorageSync('userInfo', result.data.user);
}
});
}
});
下次打开小程序时,先检查缓存里有没有 token,有就跳过登录流程直接进入首页,没有才重新走 wx.login。这里有个需要注意的坑:wx.login 的 code 有效期只有五分钟,如果缓存过期或 token 校验失败,要重新调用 wx.login 而不是直接用旧的 code。
真实生产环境里 token 刷新和 session_key 过期处理会更复杂,但课程设计做到“首次登录自动注册、后续启动免登录”这个程度,已经完全够答辩了。我在文档里通常会把安全校验流程画清楚,然后注明“简化处理”的地方,老师反而会觉得你有工程意识。
4. 联调部署:本地环境、Tomcat与合法域名
4.1 本地调试最容易卡住的两个点
前后端都写完之后,联调阶段才是真正开始踩坑的时候。第一个问题是“request 合法域名校验”。微信小程序默认只允许请求在公众平台配置过的 HTTPS 域名,本地开发时后端跑在 http://192.168.1.100:8080 这种地址,直接请求会被拦截,报错信息大致是“不在以下 request 合法域名列表中”。
解决办法有两个。如果只是用开发者工具调试,在右上角详情 -> 本地设置里勾选“不校验合法域名”即可。但要注意,这个设置只对开发者工具生效,真机预览时必须在小程序公众平台把后端域名配置到 request 合法域名列表里,而且要求 HTTPS。
第二个坑是局域网 IP。开发者工具里用 localhost 能通,但手机预览时 localhost 指向的是手机自己,根本连不到电脑。你需要把启动命令或 BASE_URL 改成电脑在局域网里的 IP,比如 http://192.168.1.100:8080/ssm_fitness。同时还要确保电脑防火墙允许 8080 端口被外部访问,否则手机一样连不上。
这两个问题在课程设计现场演示时非常常见,很多同学卡在这一步弄得满头大汗。我的建议是:在 utils/request.js 里把 BASE_URL 单独定义,不要散落在各个页面里,演示前用开发者工具先确认后端接口通,再用真机连同一 Wi-Fi 测一遍,IP 地址变了只需要改这一处。
4.2 打包部署到Tomcat的注意项
后端在 IDEA 里跑起来只是第一步,课程设计交项目时一般要求能直接在 Tomcat 下部署。SSM 项目打包成 war 包放到 Tomcat 的 webapps 目录,启动后就能被小程序访问。
这里第一个坑是访问路径。如果你的 war 包叫 ssm_fitness.war,部署后所有接口的访问地址都要带项目名,也就是 http://ip:8080/ssm_fitness/course/list,而不是 http://ip:8080/course/list。很多人接口写对了、Controller 也写对了,但忘了路径前面还有一层上下文路径,结果一直 404。这个我在第 5 章会展开讲排查过程。
第二个坑是 MySQL 连接配置。SSM 的 jdbc.properties 里连接串最好加上时区参数:
properties复制jdbc.url=jdbc:mysql://localhost:3306/fitness_weixin?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=123456
不加 serverTimezone 的话,高版本 MySQL 驱动会报时区错误。字符集设置为 utf8 可以避免中文乱码,但更推荐建库时就指定 utf8mb4,因为 utf8mb4 能完整支持 emoji 和更多生僻字,微信用户昵称里这些东西很常见。
4.3 课程设计文档怎么写才能撑起“文档+源码”
标题里既然写了“文档+源码”,说明这份项目除了代码之外,文档也是交付物的一部分。课程设计文档的常规结构大致是:需求分析、总体设计、数据库设计、详细设计、系统测试、总结。
常见误区是文档写成了“我用了什么技术”的说明书。比如花三页介绍 SSM 是什么,却只字不提自己的系统为什么需要 SpringMVC 做接口路由、为什么用 MyBatis 做动态查询。正确的写法应该是:每个功能点对应到具体表、具体接口、具体页面,然后给出设计理由。数据库部分直接把建表 SQL 贴进去,逐表说明关键字段和约束含义;详细设计部分放核心接口的请求/响应示例,配合核心代码片段和解释。
系统测试部分不要只写“运行正常”。我建议针对每个功能模块写测试用例表,列清楚测试步骤、预期结果、实际结果。比如“重复打卡”这条用例,预期是后端返回“今日已打卡”的业务错误码,实际返回也一致。这种具体的测试记录,答辩时老师一看就知道你真的跑过,不是拍脑袋写的。
文档和代码还有一个必须对齐的细节:文档里的接口路径、字段名、状态码必须和源码完全一致。很多项目文档写得漂漂亮亮,代码里却是另一套,老师随手抄一个接口去请求,直接 404,这样的印象分会很难看。
5. 一次404排错复盘:从报错到定位的完整链路
5.1 表面现象:小程序请求直接404
我在调试类似项目时遇到过一个很有代表性的 404 案例,值得拿来做完整复盘。当时的现象是:首页加载课程列表,控制台报 404,页面白屏;后端的 Tomcat 日志里没有任何业务打印,也没有异常堆栈。
第一反应肯定是检查小程序端请求的 URL。打开调试器的 Network 面板,看到请求地址是 http://192.168.1.100:8080/course/list。而后端 Controller 的 @RequestMapping 是 /course/list,看起来好像没错,但后端项目部署时带了上下文路径 /ssm_fitness,所以完整的访问地址应该是 http://192.168.1.100:8080/ssm_fitness/course/list。少了这一段,Tomcat 找不到对应的应用,自然就 404 了。
5.2 逐层排查:路径、注解、扫描配置
路径问题修掉之后,端口换到 http://192.168.1.100:8080/ssm_fitness/course/list,结果还是 404,而且这次前端返回的是后端框架的错误页而不是 Tomcat 的错误页。这就说明请求已经进入 SpringMVC 的 DispatcherServlet 了,问题不在网络层和部署层,而在 Controller 映射或 Bean 注册上。
下一步检查的是 spring-mvc.xml。打开一看,<context:component-scan> 的 base-package 配的是 com.example.controller,Controller 类也确实在这个包下面,理论上没问题。但翻到类定义时发现,这个 Controller 类上只写了 @RequestMapping("/course"),没有加 @Controller 注解。没有 @Controller,Spring 容器就不会把它注册成一个处理请求的 Bean,HandlerMapping 自然找不到 /course/list 对应的处理方法,这个 404 也就说得通了。
还有一个容易忽略的点是 mvc:annotation-driven。如果配置文件里没有启用注解驱动,@RequestParam、@ResponseBody 这些注解都不生效,接口即使匹配到了,也可能出现参数绑定异常或者返回视图名而不是 JSON。我当时检查后发现配置里其实有 <mvc:annotation-driven/>,问题根因就锁定在缺 @Controller 上。
5.3 修复结果与同类问题防范
修复方式很简单:给 Controller 类补上 @Controller 注解,重启 Tomcat,再次请求,接口正常返回了课程列表 JSON。这个案例让我形成了一个习惯:遇到接口 404,不要急着改前端,先按下面这个清单排查一遍。
- 确认 URL 是否完整,包括 IP、端口、项目上下文路径、接口路径。
- 确认 Controller 类是否被 Spring 扫描注册,检查类注解和 component-scan 的包路径。
- 确认配置里有没有
<mvc:annotation-driven/>,版本较高的项目还要确认没有漏配 JSON 转换器。 - 确认
web.xml中 DispatcherServlet 的url-pattern配置,匹配不到接口时要检查映射范围。 - 确认后端日志的 Error 输出,页面 404 和框架 404 是两码事,别混着看。
类似的 404 问题,90% 都能在这个清单里找到答案。剩下的可能就是路径参数拼接错误或者 IDE 没有把资源同步到 Tomcat 部署目录,这种通常重启或 clean 一下就好。
最后再说一点个人体会。健身达人小程序这种项目做下来,真正的收获其实不是“会写 SSM”或者“会调小程序接口”,而是你第一次完整经历了从需求分析、数据库设计、后端开发、前端联调到部署上线、文档撰写的整个软件过程。做课程设计时主动把每一步的选择理由想清楚,比如为什么这样分表、为什么统一返回格式、为什么封装请求层,答辩的时候你会发现自己比那些只背代码的同学从容得多。这个习惯带入工作后,价值比任何框架本身都大。
