家里最近一直想做个私房菜谱库:周末想吃什么翻一下,老人做过的好菜能留个记录,顺手再分享给亲戚朋友。看到“家庭大厨微信小程序+ssm”这个项目标题时,我就觉得它戳中了这类需求。微信小程序负责把菜谱内容送到用户手机端,SSM后端负责管数据、算推荐、存评论,一套下来正好覆盖了“记录—搜索—展示—互动”的完整链路。如果你是做毕设或者课程设计,手头刚好拿到这种带文档和源码的项目包,那这篇稿子你可以先存下来再看,它不是那种只给你贴一遍目录的说明书,而是把后端表结构、小程序页面、联调部署这些真正卡人的地方全拆开讲。
很多人拿到别人整理好的源码包后,第一反应就是先启动后端,再打开开发者工具,结果页面白屏、请求404、数据库连不上,这几个坑轮着来。原因其实不在代码本身,而在于你还没搞清楚这类项目的数据流:小程序发请求,SpringMVC接请求,MyBatis查MySQL,最后把JSON数据返回给页面。整个链路只要一个环节的路径、字段、编码对不上,就会出各种莫名其妙的问题。下面我按自己整理过的几个同类型项目经验,从头到尾过一遍。
1. 家庭大厨这套需求到底长什么样
1.1 用户表面上在看菜谱,实际上需要的是一条记录链路
家庭大厨这类小程序,功能上很像一个简化版美食社区。用户进来能按分类找菜,搜索“红烧肉”“蒸蛋”这类关键词,点进详情看图文步骤,顺手收藏一个配方;等自己做过一次觉得不错,还能发布一条新的家常菜谱,附上成品图和做法。听起来简单,但它刚好把一个普通用户的完整行为闭环走完了:游客浏览、注册登录、内容消费、内容生产,最后是收藏和评论互动。
理解这个闭环很重要,因为后端接口几乎都是围绕它展开的。如果你只是想跑通程序,那列表页、详情页、登录页、发布页这四个页面足够你演示;如果还要应付答辩,那你至少得把收藏、评论、分类、用户管理这些功能也讲清楚。我见过不少同学最后的演示视频只有“点进来能看菜谱”这一个镜头,老师一问“用户能不能发布内容”就卡壳,这其实是需求设计本身没闭合。
1.2 数据关系决定了后端表结构,页面清单随之而来
把上述需求翻译成数据库关系,核心就是:用户表、菜谱表、分类表、收藏表、评论表。菜谱表必须存发布者ID,用于关联用户;收藏表存用户ID和菜谱ID;评论表存用户、菜谱和评论内容。分类表则可以独立出来,方便首页做筛选列表。
页面清单也跟着这个思路走。小程序端最少需要:首页/分类页(菜谱流)、搜索页(关键词查菜谱)、详情页(图文+评论+收藏按钮)、登录页(微信授权)、发布页(图片上传+表单)、个人中心页(查看我发布的、我收藏的)。有的项目还会加一个管理员后台,用浏览器打开网页来管理菜谱和用户,这部分在毕设设计里常作为加分项,但并不是小程序本身的页面。拿到源码后,你先对照这个清单找页面文件,能快速定位到自己想改的功能。
1.3 为什么这类项目还坚持用SSM,而不是Spring Boot
现在很多人一看到SSM就觉得老,其实把它放进课程设计和毕业设计里看,仍然是主流。SSM是Spring、SpringMVC、MyBatis三件套,本质上是把“对象管理”“请求路由”“数据库操作”分成三个层次。比起Spring Boot的自动配置,SSM的XML配置虽然繁琐,但反而能把每个环节暴露在你面前,这对做技术讲解、回答答辩问题非常有利。
你被问到“SpringMVC处理流程是什么”,可以很自然地讲:请求先进DispatcherServlet,再找Controller,Controller调Service,Service调Mapper,Mapper操作数据库,最后把ModelAndView或JSON写回前端。如果换成Spring Boot,面试效果反而没那么清晰。另外很多学校的教学案例仍然是SSM,项目包里的源码和文档也大多是围绕SSM写的,这时候硬改成Spring Boot不是一个明智的选择。我更建议你保留SSM,把精力放在理解数据流和业务功能上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. SSM后端设计:从数据库表到Controller的一整套顺序
2.1 建表不是靠拍脑袋,按页面写字段就够了
以“菜谱表”为例,我在多个项目里见过类似的结构,核心字段可以这样规划:
- id:主键
- title:菜名
- category_id:关联分类表
- cover_image:封面图片URL
- content:图文做法,可能是富文本或Markdown
- ingredients:用料清单
- user_id:发布者ID
- create_time:发布时间
- view_count、like_count:浏览数和点赞数
用户表则要存openid、昵称、头像、注册时间。openid是微信生态里识别用户身份的关键,同一个用户在同一个微信小程序下是唯一固定的。收藏表最简单的结构就是id加user_id加recipe_id,再加一个create_time。如果你想支持“收藏/取消收藏”,可以在表中加一个status字段或直接删除记录,都能实现,后一种更干净。
数据库里不需要真的写外键约束,尤其单表操作较多的项目,外键反而会影响性能。你只需要在MyBatis的查询SQL里通过JOIN把关联表信息带出来。比如查收藏列表时,用户点开的是菜谱,接口返回的其实是菜谱表字段,关联条件是收藏表里的recipe_id。
2.2 Controller的URL设计,要能一眼看出模块边界
后端不是随便写几个接口就能跑,Controller路径设计直接影响你后面联调的心情。我建议按业务模块统一前缀,例如:
- /api/user/login 登录、获取用户信息
- /api/category/list 分类列表
- /api/recipe/list 按条件分页查菜谱
- /api/recipe/detail 查询菜谱详情
- /api/recipe/add 发布菜谱
- /api/favorite/add、/api/favorite/remove 收藏与取消
- /api/comment/list、/api/comment/add 评论查询与发布
Controller返回的数据结构也要统一。常见做法是定义一个Result类,里面包含code、msg、data三个字段。code为0表示成功,非0表示失败;msg给前端提示信息;data放真正的数据。这么做的好处是,小程序端可以在封装好的request方法里统一判断code,而不是每个页面各写一套成功失败逻辑。如果你拿到的项目没有这样一个包装类,建议加上,改动成本很低但收益很大。
2.3 登录接口前后端如何配合,openid和token千万别做反
SSM项目的登录逻辑,很多新人容易搞混。微信小程序不像网页端有账号密码,它需要先在小程序里调用wx.login拿到一个临时code,然后请求后端自己的登录接口,把这个code传过去。后端拿到code后,再用appid和secret去微信的接口请求openid和session_key。这一步一定要放在后端做,因为小程序secret一旦放到前端代码里,就等于把钥匙交给了别人。
拿到openid之后,常规做法是先查用户表,看这个openid是否存在。如果不存在,就自动注册一个新用户,然后再把用户信息写进一个token结构返回给前端。前端后续每次请求都要带上这个token,后端通过token判断到底是谁在调用。有的毕设项目为了省事,直接把user_id返回给前端,前端再带user_id调接口,虽然能跑通,但安全性比较弱。答办时如果老师问,你要能说出这里的不足和改法,比如把token换成一串随机字符串,存到Redis并设置过期时间。
2.4 列表接口要考虑到分页和动态条件
列表页最常踩的坑是:数据一多就卡,或者分类筛选不生效。原因往往出在后端SQL写得太死。一个合格的菜谱列表接口,至少需要支持分页和可选条件。小程序端通过pageNum、pageSize、categoryId、keyword这几个参数发起请求,后端在Mapper的SQL里用MyBatis的if标签做动态拼接。
拿一个简单示例:
xml复制<select id="selectRecipeList" resultType="com.demo.entity.Recipe">
SELECT * FROM recipe
<where>
<if test="categoryId != null">
AND category_id = #{categoryId}
</if>
<if test="keyword != null and keyword != ''">
AND (title LIKE CONCAT('%', #{keyword}, '%')
OR content LIKE CONCAT('%', #{keyword}, '%'))
</if>
</where>
ORDER BY create_time DESC
LIMIT #{offset}, #{pageSize}
</select>
页面传过来的是第几页,SQL里需要换成offset。通常在Service层做一次计算:offset = (pageNum - 1) * pageSize。同时还要写一个count查询,用于小程序端判断是否还有下一页。如果你发现某个项目只有一个查询接口,没有返回总数,那你滚动加载时就会出现“最后一页还在反复请求”的尴尬局面。
3. 小程序端才是门面,页面逻辑与接口联调细节
3.1 首页请求层一定要封装,否则后面改URL会改到哭
小程序端很多人直接在每个页面的onLoad里写wx.request,请求地址写死成一长串。项目里页面少的时候还好,页面一多,后端改了端口或加了contextPath,你就得全局搜索替换。更规范的做法是先封装一个request方法,把公共的baseURL、header、token带上,然后统一处理成功和失败状态。
我一般会单独建一个utils/request.js,把baseURL写在一个配置项里。比如开发环境后端跑在8080端口,baseURL就是http://localhost:8080。调用时这样写:
javascript复制const request = (url, method = 'GET', data = {}) => {
return new Promise((resolve, reject) => {
wx.request({
url: getApp().globalData.baseUrl + url,
method,
data,
header: {
'Content-Type': 'application/json',
'token': wx.getStorageSync('token')
},
success(res) {
if (res.data.code === 0) {
resolve(res.data.data);
} else {
wx.showToast({ title: res.data.msg, icon: 'none' });
reject(res.data);
}
},
fail(err) {
wx.showToast({ title: '网络请求失败', icon: 'none' });
reject(err);
}
});
});
};
首页拿到后端的列表数据后,用setData更新到data里的recipes数组。需要强调一点:小程序的setData是异步反馈到视图层的,但代码书写上是同步调用,不要在setData后立刻去拿页面节点数据,那样拿不到。滚动加载时,要做防重复请求,常见做法是维护一个loading状态,请求结束前不允许再次触发onReachBottom。
3.2 详情页的富文本内容和图片路径要特别处理
菜谱的做法内容一般比较长,可能包含多张图片和文字步骤。后端存的内容可能是经过HTML标签拼装的富文本字符串,也可能是简单换行的纯文本。小程序里渲染富文本用的是rich-text组件,给它一个nodes属性,直接传后端返回的html字符串就能显示。
坑点在于图片路径。有的数据库里存的是“/upload/xxx.jpg”这样的相对路径,小程序端直接赋值给image的src是没法显示的。你必须先拼接成完整的URL,比如http://后端IP:8080/项目名/upload/xxx.jpg。所以我通常在封装接口时,对后端返回数据做一个字段加工,把所有图片路径统一处理后放在详情页展示。
另外详情页的收藏按钮,要判断当前用户是否已经收藏过这个菜谱。常见做法是后端detail接口返回一个字段favorited,前端根据这个字段渲染红心和灰心状态,点击时再调收藏或取消收藏接口。不要在点击时才临时去查一次收藏状态,那样页面一刷新就乱了。
3.3 发布菜谱,图片上传的顺序能决定一天的工作量
发布页面是很多同学最害怕实现的功能,因为它涉及两步操作:先把图片传到服务器,再把表单数据提交给后端。有的资源包直接用表单里带图片地址的方式解决,有的则提供了一个上传接口。
我建议的前端流程是:用户选择图片后,先不急着提交整个表单。可以等用户点“发布”按钮时,把所有图片通过wx.uploadFile循环或并行发到后端的upload接口,后端返回每个图片的URL。等所有图片都拿到URL之后,再把标题、分类、用料、步骤这些字段连同图片URL数组通过普通的request方法提交给recipe/add接口。
有人图省事,直接把wx.chooseMedia返回的本地临时路径存到数据库里,这种字段看着有效,但服务器一换地址或者用户重新进入页面就失效了,一定不能这么干。合理的方式是后端设置一个本地目录保存图片,比如放在Tomcat的webapps/upload目录,再通过静态资源映射供访问。有些项目会把上传根目录配置在properties文件里,方便换服务器时调整。
3.4 用户授权这块,不能用旧的getUserInfo思路硬套
微信官方对用户头像昵称的获取规则已经改过好几次。以前用wx.getUserInfo能直接拿到用户微信头像,现在很多新版本已经拿不到完整信息,尤其是用户主动点击授权的场景被限制得很严格。更稳的方案是页面里放一个“微信一键登录”按钮,点击后先走wx.login拿code,由后端换取openid完成注册;头像和昵称则引导用户使用官方提供的头像昵称填写能力。
实际操作时,头像可以用button组件的open-type="chooseAvatar",昵称用input组件的type="nickname",这样能合规地获取用户主动填写的头像和昵称。很多旧项目里还保留着直接getUserInfo的代码,如果你部署上线后发现用户信息拿不到,不要怀疑是自己的问题,第一时间检查是不是走了老接口。演示用的小程序如果只是内部测试,也可以先只保留openid登录,昵称默认设成“微信用户”,头像用默认图,这样反而能减少不必要的兼容工作。
4. 从下载资源到本地跑通,这几步最容易卡住
4.1 第一步永远是从数据库开始,别直接启动Tomcat
拿到资源包,我会先解压看整个目录。首先找到数据库SQL脚本,通常放在sql或resource目录下,文件名可能叫family_cook.sql、db_weixin150.sql之类。用Navicat或者命令行执行前,先打开文件看一眼编码是不是UTF-8,里面有没有创建数据库的语句。如果是分号断开的多行脚本,执行时最好选择“运行SQL文件”,而不是在某个库里复制粘贴,否则容易漏掉表。
MySQL版本和字符集也要注意。老项目可能是用MySQL5.7建的,表结构用的utf8;新电脑装的是MySQL8.0,虽然大体兼容,但密码加密方式和连接驱动会有区别。数据库连接配置一般在src/jdbc.properties里,里面包含了url、username、password。你需要把url中的数据库名改成脚本里实际创建的库名,比如jdbc:mysql://localhost:3306/family_cook,后面还需要加上useUnicode=true&characterEncoding=utf8参数,不然中文写入容易乱码。
4.2 后端启动时,改这三处配置基本就能正常跑
第一是数据库账号密码,这个不用多说。第二是Tomcat的端口和发布路径,如果本机8080被占了,就改成8081。第三是项目context-path,这个决定接口访问的前缀。有的项目把context-path设置成了“/”,那Controller接口就通过http://localhost:8080/api/recipe/list访问;如果设置成了“/familyCook”,那就要多带一层路径访问。
用IDEA导入SSM项目时,要注意几个细节。如果项目是Maven工程,导入后右下角会提示加载Maven项目,你需要等依赖下载完成,确认pom.xml没有报红。如果是非Maven工程,依赖包通常放在WEB-INF/lib下,那就要配置到Artifacts里。启动Tomcat的时候,建议先看控制台日志里有没有“Starting Application”这类关键日志,出现不报错不代表启动成功,只有当日志出现“Server startup”或者类似提示,才说明容器起来了。
启动完成后,先在浏览器里直接访问一个接口地址,比如平台的健康检查接口,确认能够返回JSON。如果浏览器能访问,后端基本就没问题了;浏览器访问不到,你不要急着去小程序里排查,那是浪费时间。绝大多数登录接口、列表接口都能像普通网页接口一样直接通过浏览器调试。
4.3 小程序端导入后,先改appid和本地设置
打开微信开发者工具,选择“导入项目”,找到小程序所在目录的project.config.json那一层,也就是包含app.json、pages目录的文件夹。如果你只是本地学习和演示,可以不使用自己的AppID,选择使用测试号,也可以在小程序后台申请一个测试AppID。
导入之后,最容易被忽略的是平台里的“不校验合法域名”选项。开发阶段后端跑在本机,接口地址是http协议,不是https,如果这个选项没打开,所有请求都会被拦截,页面就什么都加载不出来。真机调试时也要勾选这个选项,手机才能访问到你电脑上运行的本地后端。如果你手机和电脑不在同一局域网,还需要把后端的IP改成电脑的局域网IP,不能用localhost。
还有一点:小程序里不同页面跳转,要留意app.json中是否注册了对应页面路径。很多资源包在页面文件拷贝时会漏掉注册,导致点击某个按钮时报“页面路径不存在”。出现这种情况,打开app.json,在pages数组里补上缺失的页面路径,再重新编译就好。
4.4 文档与源码之间出现偏差,十有八九是字段或SQL没对上
带文档的源码包,最怕的不是项目跑不起来,而是文档里的表结构和实际上代码里用的不一致。比如说文档里写着菜谱表有material字段,但代码里的实体类叫ingredients,启动时MyBatis执行SQL就会报“Unknown column”。这时候不要怀疑环境,要找数据库脚本和XML里的SQL逐字段核对。
一个比较实用的排查方法是:启动项目后开启SQL日志,把MyBatis打印出的SQL语句复制到Navicat里执行,看看是哪一列报错。大多数SSM项目在log4j.properties或日志配置里保留了SQL输出,如果没开启,可以临时将mapper的日志级别设为DEBUG。用这种方式定位异常,比盲改Java代码快得多。
5. 联调与改代码时最难缠的几个问题
5.1 网页/小程序请求404:Controller路径不是你以为的那样
后端接口请求404,最常见的原因有两个。第一是类级别的@RequestMapping和项目context-path叠加,导致实际路径和前端写的baseURL不一致。比如前端请求/api/recipe/list,但后端类上写的是@Controller @RequestMapping("/api"),方法上的路径是/recipe/list,那完整路径确实是对的。可如果前端baseURL里已经带了“/api”,又拼接了Controller里的完整路径,就会变成/api/api/recipe/list,自然就404了。
第二是少了必要的注解或组件扫描。SpringMVC的Controller没有生效,通常是因为配置里没有加注解扫描,比如在springmvc.xml中没有写<context:component-scan base-package="com.demo.controller"/>,那Controller永远不会被识别成Bean。新手查接口问题时很容易盯着方法看半天,其实只要打开Tomcat启动日志,看有没有出现“RequestMappingHandlerMapping”相关的映射路径,就知道路径有没有注册成功。
5.2 返回的数据不是JSON,小程序会一直拿不到data
小程序端期望收到JSON,但后端偶尔会返回一个视图页面或字符串,这在Controller里特别容易发生。如果你用的是@Controller而不是@RestController,方法上就必须加@ResponseBody;如果你用的Spring版本较高,其实每个方法都手动标记会很麻烦,建议直接在Controller类上统一加@RestController,或者在方法上只对返回JSON的接口加@ResponseBody。很多老代码里是返回一个ModelAndView或字符串给前端页面跳转的,用在纯小程序接口中就会出问题。
还有一种情况是依赖里少了Jackson。SpringMVC要把对象转成JSON,一般靠Jackson或fastjson,pom.xml里如果没有jackson-databind依赖,接口返回时可能报406错误,或者直接产生一堆字符串。检查依赖时不要只看Spring相关包,序列化包一定不能漏。
5.3 日期字段变成时间戳数组,前端怎么显示都不对
数据库里的create_time是datetime类型,Java实体用Date接收,如果没有配置格式化规则,Jackson默认可能把它序列化成一串数字。小程序端用new Date(时间戳)还能转回去,但不少毕设代码直接把Date toString后的结果返回给了前端,就变成“Mon Mar 20 00:00:00 CST 2023”这种字符串。
最省事的办法是在实体类的时间字段上加一个注解:
java复制@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
如果后端接口里同一时间字段出现的不止一处,可以做一个统一配置的JacksonObjectMapper,在SSM的XML里配置消息转换器,而不是一个一个加注解。时间格式统一后,小程序端列表页和详情页就不用再自己解析字符串了。
5.4 图片上传成功,但页面上就是不显示
图片上传成功,说明已经写入了服务器某个目录;页面不显示,往往是访问路径不对。如果你在本地用的是IDEA内置Tomcat,上传的文件实际写到的是target目录下的临时部署目录里,重新启动可能文件就被清空了。这种情况在演示时很要命,用户刚发布的菜谱图片,自己能看到,重启后就消失。
可行的方案是把上传目录设置在一个Tomcat之外固定位置,再用虚拟目录映射访问。具体做法是在springmvc.xml或一个WebMvcConfigurer里配置addResourceHandlers,把磁盘路径映射成“/upload/**”。这样做之后,重启不会清空文件,部署到服务器时只要把同样路径配置好就行。实在嫌麻烦,至少保证演示过程中不要频繁重启Tomcat,否则你会发现图片真的会“丢”。
5.5 小程序与后端字段大小写、命名习惯不一致,也会很折腾
Java后端习惯用驼峰,比如coverImage;前端如果请求参数用的是cover_image,后端接收对象很可能会得到null。数据库字段用下划线,Java实体用驼峰,这是MyBatis最常见的配置方式,建议在mybatis-config.xml里开启驼峰映射:
xml复制<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
开启之后,数据库的cover_image能自动映射到Java实体的coverImage属性,这样不仅少写很多resultMap,还能避免字段名前后端来回翻译出错。如果你拿到的项目没有开启这个配置,那你写接收参数的JavaBean时就要手动改成数据库物理字段对应的名字,否则一调接口全是null。
最后再分享一个我在处理这类项目时养成的习惯:拿到源码第一件事,不看文档,先按照“数据库脚本—配置文件—后端启动—接口请求—小程序页面”的顺序把东西真正跑起来,然后对着Controller把所有请求路径和字段整理成一张表。这张表一旦出来,整个项目的逻辑脉络就像地图一样清楚,后面不管是改功能、写论文、准备答辩,都不用再翻来翻去找代码了。很多同学问为什么资源包里的功能自己跑起来就不对,其实大多数问题并不在高深的技术点上,而是栽在了路径、编码、字段映射这些看似琐碎却决定成败的细节里。
