简介:本资源是一套面向计算机专业本科生的毕业设计与课程大作业实践案例,聚焦微信小程序前端与Java Web后端的全栈开发能力训练,适用于期末项目、毕设选题及SSM框架综合实训。压缩包共856个文件,涵盖120个Java后端业务逻辑与DAO层代码、104个JS/WXML/WXSS小程序页面脚本与样式、96个Vue组件(含管理后台界面)、175张PNG/SVG图标资源及52个JPG菜品图,辅以XML配置、SQL建表语句与BAT一键部署脚本,整体36.84MB。项目采用标准RESTful API对接,完整呈现食谱浏览、搜索、用户投稿与后台审核等核心业务流程,目录结构清晰区分前后端模块,含.bak备份文件便于版本比对与调试学习。已有108人下载学习,可直接运行调试,快速掌握小程序云开发协同、SSM事务管理、MyBatis动态SQL及跨域联调等关键技术点。
1. 项目概述:一个家庭大厨的数字化厨房构想
最近在整理过去的项目资料时,翻到了一个挺有意思的“老物件”——一个名为“家庭大厨”的微信小程序,搭配SSM后端框架的完整源码案例。这个项目虽然技术栈现在看来不算新颖,但它的设计思路和实现细节,对于想入门全栈开发,特别是想了解如何将一个小想法串联成一个可运行产品的朋友来说,依然有很强的参考价值。它不是那种庞大复杂的商城系统,而是聚焦于一个非常具体的场景:帮助家庭烹饪爱好者更好地管理自己的菜谱、规划每日饮食,并分享给家人。你可以把它理解为一个私域、轻量级的“下厨房”或“豆果美食”,但更侧重于家庭内部的协作与记录。
为什么说它值得拿出来聊聊呢?因为很多初学者在学完SSM(Spring+SpringMVC+MyBatis)和微信小程序基础后,面临的第一个难题就是“如何把它们用起来”。网上教程往往只讲单个技术点,比如怎么配置MyBatis,或者小程序某个API怎么调用。但这个案例提供了一个完整的、前后端分离的上下文。你能看到用户在小程序前端点击“收藏菜谱”后,这个请求是如何经过网络、被后端SpringMVC的Controller接收、如何通过MyBatis与数据库交互,最后又把结果JSON返回给小程序页面渲染的完整链条。这种“端到端”的视角,是打通你知识阻塞点的关键。
这个项目适合谁呢?如果你是Java后端初学者,想找一个SSM框架的实战项目来练手,理解MVC分层和数据库操作;或者你是前端开发者,想深入理解如何与Java后端API进行数据交互;亦或是你有一个类似的、围绕特定生活场景构建数字化工具的想法,这个案例都能提供一个扎实的起点和清晰的实现蓝图。接下来,我们就抛开那些枯燥的理论,直接钻进这个“家庭大厨”的厨房里,看看它到底是怎么搭建起来的。
2. 技术栈选型与项目架构拆解
当我们决定要做一个“家庭大厨”应用时,技术选型是第一个要面对的现实问题。这个案例的选择非常典型,也反映了当时(以及现在很多教学项目)的主流搭配:微信小程序 + SSM后端 + MySQL数据库。我们来拆解一下为什么这么选,以及这个架构是如何工作的。
2.1 前端:为什么是微信小程序?
对于“家庭大厨”这个场景,微信小程序几乎是天然的最佳载体。首先,它的使用门槛极低。目标用户是家庭成员,他们可能不太愿意为了做个菜专门下载一个App,但打开微信,扫个码或者搜一下就能用,这个体验就友好得多。其次,小程序提供了丰富的原生组件和能力,比如上传图片(拍下成品照)、微信登录(免去注册麻烦)、消息订阅(提醒家人今晚吃啥)等,能很好地满足“记录与分享”的核心需求。最后,小程序的开发对于前端开发者来说,学习曲线相对平缓,一套代码可适配iOS和Android,省去了很多跨端适配的烦恼。
在这个案例中,小程序端主要承担了视图渲染和用户交互的职责。它的页面结构通常包括:首页(推荐菜谱/今日计划)、菜谱浏览/搜索页、菜谱详情页(含步骤图文)、个人中心(我的收藏、我的发布)等。数据通过调用后端的RESTful API接口进行获取和提交。
2.2 后端:SSM框架的经典组合
后端选择了经典的Java SSM框架组合,这是一个非常务实且教学意义丰富的选择。
- Spring:作为核心的IoC(控制反转)容器,它负责管理整个应用中的所有Bean(如Service、Dao等)的生命周期和依赖关系。简单说,它让各个模块之间不是硬编码在一起,而是通过配置或注解“注入”依赖,使得代码更松耦合、更易测试。在这个项目里,所有业务逻辑类(Service)和数据访问类(Dao)都是由Spring容器创建和组装的。
- SpringMVC:这是处理前端(小程序)请求的“调度中心”。它采用了模型(Model)-视图(View)-控制器(Controller)的设计模式。对于前后端分离的小程序项目来说,“视图”部分已经交给了小程序前端,所以SpringMVC的Controller主要职责就是:
- 接收小程序发来的HTTP请求(比如
GET /api/recipe/list)。 - 调用相应的Service层处理业务逻辑(如查询菜谱列表)。
- 将处理结果(通常是Java对象)序列化成JSON格式。
- 通过HTTP响应将JSON数据返回给小程序。
- 接收小程序发来的HTTP请求(比如
- MyBatis:这是一个优秀的持久层框架,负责与MySQL数据库“对话”。它避免了编写繁琐的JDBC代码,通过XML映射文件或注解,将Java方法调用和SQL语句优雅地绑定在一起。例如,你定义一个
List<Recipe> selectByCategory(String category);的接口方法,在对应的Mapper.xml里写好SELECT * FROM recipe WHERE category = #{category}的SQL,MyBatis就会帮你执行查询并把结果集自动映射成Recipe对象的列表。
为什么是MySQL?对于“家庭大厨”这类数据关系比较明确的应用(用户、菜谱、食材、步骤、收藏关系等),关系型数据库是首选。MySQL开源、免费、生态成熟、性能足够,是中小型项目的标配。
整个架构的数据流可以概括为:微信小程序(V)发起请求 -> SpringMVC(C)接收并路由 -> Spring管理的Service(业务逻辑)处理 -> MyBatis(M)操作MySQL数据库 -> 原路返回JSON数据 -> 小程序渲染展示。这是一个清晰的三层架构(Web层、Service业务层、Dao数据访问层),职责分离,便于维护和扩展。
3. 核心功能模块设计与数据库建模
有了技术栈的蓝图,下一步就是设计这个应用具体能干什么,以及数据如何存储。这是将想法落地的关键一步,设计的好坏直接影响到后续开发的复杂度和用户体验。
3.1 功能模块分解
“家庭大厨”的核心功能围绕“菜谱”这个核心实体展开,可以分解为以下几个模块:
用户模块:
- 微信一键登录:利用小程序的
wx.login()获取code,传给后端。后端用code、appid、secret调用微信接口换取openid和session_key。openid是用户在微信小程序内的唯一标识,我们用这个来标识用户,通常就不再需要传统的用户名密码了。这是小程序项目的标准做法,能极大提升用户体验。 - 用户信息管理:登录后,可以补充或修改昵称、头像(可同步微信头像)、个人简介等。
- 微信一键登录:利用小程序的
菜谱模块(核心):
- 菜谱创建与编辑:这是最复杂的功能点。一个完整的菜谱包含:标题、封面图、简介、难度、耗时、用餐人数、分类(如川菜、烘焙)、标签(如辣、快手菜)、食材清单(食材名、用量)、详细步骤(每一步的图文说明)。前端需要设计一个表单,能够动态添加/删除食材和步骤项。
- 菜谱浏览与搜索:首页瀑布流或列表展示菜谱,支持按分类、标签、难度筛选,以及关键词搜索(标题、食材名)。
- 菜谱详情:完整展示菜谱的所有信息,图文并茂。包含收藏、点赞、评论(如果设计有社交功能)等交互。
- 我的菜谱:用户查看和管理自己发布的所有菜谱。
互动与收藏模块:
- 收藏/取消收藏:建立用户和菜谱之间的多对多关系。用户可以将感兴趣的菜谱收入自己的收藏夹,方便下次快速查找。
- (可选)点赞与评论:增加社区互动性。评论功能涉及子评论、通知等,复杂度会上升,在最小可行产品(MVP)中可以先不做。
计划与购物车模块(进阶):
- 每日饮食计划:用户可以将菜谱添加到某一天的午餐或晚餐计划中。
- 一键生成购物清单:根据计划中的菜谱,自动汇总所需食材及用量,生成购物清单。这是一个非常实用且能体现“智能”的功能点。
3.2 数据库表结构设计
根据以上功能,我们需要设计数据库表。这里给出核心表的设计思路:
用户表 (user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| openid | varchar(100) | 微信用户唯一标识,唯一索引 |
| nickname | varchar(50) | 用户昵称 |
| avatar_url | varchar(500) | 头像URL |
| create_time | datetime | 创建时间 |
注意:
openid必须加唯一索引,这是识别用户的唯一凭证。切勿存储session_key到数据库,它应该存在于服务端的缓存(如Redis)中,并设置较短过期时间。
菜谱表 (recipe)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| user_id | bigint | 发布者ID,外键关联user.id |
| title | varchar(100) | 菜谱标题 |
| cover_image | varchar(500) | 封面图片URL |
| description | text | 简介 |
| difficulty | tinyint | 难度(1-简单,2-中等,3-困难) |
| time_required | int | 所需时间(分钟) |
| category | varchar(20) | 分类 |
| tags | varchar(200) | 标签,用逗号分隔,如“辣,下饭,快手” |
| view_count | int | 浏览量 |
| collect_count | int | 收藏数 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
菜谱步骤表 (recipe_step)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| recipe_id | bigint | 所属菜谱ID,外键 |
| step_number | int | 步骤序号 |
| content | text | 步骤文字说明 |
| image_url | varchar(500) | 步骤图URL(可选) |
菜谱食材表 (recipe_ingredient)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| recipe_id | bigint | 所属菜谱ID,外键 |
| name | varchar(50) | 食材名称 |
| amount | varchar(50) | 用量(如“200g”、“1个”) |
收藏关系表 (user_collect_recipe)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| user_id | bigint | 用户ID,外键 |
| recipe_id | bigint | 菜谱ID,外键 |
| create_time | datetime | 收藏时间 |
设计心得:将菜谱的步骤和食材单独拆表,而不是用一个大JSON字段存在菜谱表里,是更规范的关系型数据库设计。这样做虽然查询时可能需要联表,但好处是:1. 便于对步骤或食材进行独立的查询和统计(例如,找出所有用到“土豆”的菜谱);2. 避免了JSON字段的解析和索引问题;3. 结构更清晰,符合数据库范式。对于“标签”这种简单且查询频率高的字段,可以用逗号分隔的字符串存储在菜谱表内,方便模糊查询(
LIKE '%辣%'),如果标签功能复杂(如可管理、有热度),则需要单独建标签表和关系表。
4. 后端核心代码实现与关键API剖析
理论设计完成后,我们进入实战环节,看看后端代码如何具体实现。这里以几个核心API为例,展示SSM框架是如何协同工作的。
4.1 项目结构与配置
一个标准的SSM项目目录结构通常如下:
family-chef-backend/ ├── src/main/java/ │ └── com.familychef/ │ ├── controller/ # 控制层,接收请求 │ ├── service/ # 业务逻辑层接口 │ ├── service/impl/ # 业务逻辑层实现 │ ├── dao/ # 数据访问层接口(Mapper接口) │ ├── entity/ # 实体类,对应数据库表 │ └── config/ # 配置类(如Spring, MyBatis配置) ├── src/main/resources/ │ ├── mapper/ # MyBatis的XML映射文件 │ ├── application.yml # 主配置文件(数据库、服务器端口等) │ └── ... └── pom.xml # Maven依赖管理关键配置在application.yml中,需要配置数据库连接、MyBatis映射文件位置、服务器端口等。
4.2 用户登录API实现
这是小程序与后端交互的第一个关口。流程如下:
- 小程序调用
wx.login()获取临时凭证code。 - 小程序将
code发送到我们后端的登录接口。 - 后端用
code、小程序appid和secret调用微信服务器接口https://api.weixin.qq.com/sns/jscode2session。 - 微信返回
openid和session_key。 - 后端根据
openid查询用户表,如果不存在则自动创建一条用户记录。 - 后端生成一个自己的会话标识(如一个随机Token),将
openid和session_key的关联关系存入Redis(并设置过期时间,如2小时),然后将Token返回给小程序。 - 小程序后续请求都在Header中携带此Token,后端通过Token从Redis中取出
openid来识别用户。
Controller层代码示例 (UserController.java):
@RestController // 表明这是RESTful风格的Controller,返回JSON数据 @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @PostMapping("/login") public ApiResponse login(@RequestBody LoginRequest request) { // request中包含小程序传来的code String code = request.getCode(); // 调用Service层进行微信登录业务处理 String token = userService.wechatLogin(code); // 将生成的Token返回给前端 return ApiResponse.success(token); } }Service层实现关键点 (UserServiceImpl.java):
@Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; // MyBatis Mapper接口 @Autowired private RedisTemplate<String, String> redisTemplate; @Value("${wechat.appid}") // 从配置文件中注入 private String appid; @Value("${wechat.secret}") private String secret; @Override public String wechatLogin(String code) { // 1. 构造请求URL,调用微信接口 String url = String.format("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", appid, secret, code); // 使用RestTemplate或HttpClient发送GET请求,获取响应 WechatSessionResponse sessionResp = ...; // 解析微信返回的JSON String openid = sessionResp.getOpenid(); String sessionKey = sessionResp.getSession_key(); // 2. 根据openid查找或创建用户 User user = userDao.selectByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); // 可以此时调用微信用户信息接口获取昵称头像,或等用户主动更新 user.setNickname("微信用户_" + System.currentTimeMillis()); userDao.insert(user); } // 3. 生成自定义Token(如UUID) String token = UUID.randomUUID().toString().replace("-", ""); // 4. 将Token与openid、sessionKey的关联存入Redis,有效期2小时 String redisKey = "user:token:" + token; Map<String, String> sessionMap = new HashMap<>(); sessionMap.put("openid", openid); sessionMap.put("sessionKey", sessionKey); // 注意:sessionKey用于后续解密数据,需妥善保管 redisTemplate.opsForHash().putAll(redisKey, sessionMap); redisTemplate.expire(redisKey, 2, TimeUnit.HOURS); return token; } }实操心得:
session_key是敏感信息,绝不能传到客户端。它主要用于解密小程序端调用wx.getUserInfo(旧版)或wx.getUserProfile返回的加密用户信息。现在更推荐使用wx.login和button open-type="getUserInfo"或wx.getUserProfile配合,无需后端解密的情况。另外,Token的存储和验证可以封装成一个拦截器(Interceptor),这样就不用在每个Controller方法里都写一遍校验逻辑了。
4.3 菜谱发布API实现
发布菜谱是一个典型的“事务性”操作,涉及向recipe表插入主记录,同时向recipe_step和recipe_ingredient表插入多条子记录。我们需要保证这些操作要么全部成功,要么全部失败。
Controller层 (RecipeController.java):
@RestController @RequestMapping("/api/recipe") public class RecipeController { @Autowired private RecipeService recipeService; @PostMapping("/create") // @RequestHeader("Authorization") 从请求头获取Token public ApiResponse createRecipe(@RequestHeader("Authorization") String token, @RequestBody RecipeCreateRequest request) { // 1. 通过Token获取当前用户ID (这一步通常在拦截器中完成,这里简化) Long userId = getCurrentUserIdFromToken(token); // 2. 调用Service Long recipeId = recipeService.createRecipe(userId, request); return ApiResponse.success(recipeId); } }Service层与事务管理 (RecipeServiceImpl.java):
@Service public class RecipeServiceImpl implements RecipeService { @Autowired private RecipeDao recipeDao; @Autowired private RecipeStepDao stepDao; @Autowired private RecipeIngredientDao ingredientDao; @Override @Transactional // 声明式事务管理,关键注解! public Long createRecipe(Long userId, RecipeCreateRequest request) { // 1. 构建菜谱主实体 Recipe recipe = new Recipe(); recipe.setUserId(userId); recipe.setTitle(request.getTitle()); recipe.setCoverImage(request.getCoverImage()); // ... 设置其他字段 recipe.setCreateTime(new Date()); // 2. 插入菜谱主表,并获取自增ID recipeDao.insert(recipe); Long recipeId = recipe.getId(); // MyBatis配置useGeneratedKeys后会自动回填 // 3. 批量插入步骤 List<RecipeStep> steps = request.getSteps(); if (steps != null && !steps.isEmpty()) { for (RecipeStep step : steps) { step.setRecipeId(recipeId); } stepDao.batchInsert(steps); // 需要在Mapper.xml中实现批量插入 } // 4. 批量插入食材 List<RecipeIngredient> ingredients = request.getIngredients(); if (ingredients != null && !ingredients.isEmpty()) { for (RecipeIngredient ing : ingredients) { ing.setRecipeId(recipeId); } ingredientDao.batchInsert(ingredients); } return recipeId; } }关键点解析:
@Transactional注解是Spring事务管理的核心。它被添加在Service方法上,意味着该方法中的所有数据库操作(insert)将在同一个数据库事务中执行。如果中间任何一步失败抛出异常,Spring会自动回滚之前的所有操作,确保数据一致性。这是处理此类“主-子表”关联写入的标配做法。batchInsert通过MyBatis的<foreach>标签实现,比循环执行单条insert语句效率高得多。
4.4 分页查询菜谱列表API
首页或列表页需要分页加载数据,这是一个高频且重要的API。
MyBatis Mapper接口与XML (RecipeDao.java & RecipeMapper.xml):
// RecipeDao.java (Mapper接口) public interface RecipeDao { List<Recipe> selectPage(@Param("category") String category, @Param("keyword") String keyword, @Param("offset") Integer offset, @Param("pageSize") Integer pageSize); int countPage(@Param("category") String category, @Param("keyword") String keyword); }<!-- RecipeMapper.xml --> <select id="selectPage" resultType="com.familychef.entity.Recipe"> SELECT id, title, cover_image, description, difficulty, view_count, collect_count, create_time FROM recipe WHERE 1=1 <if test="category != null and category != ''"> AND category = #{category} </if> <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%')) </if> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select> <select id="countPage" resultType="int"> SELECT COUNT(*) FROM recipe WHERE 1=1 <!-- 条件与selectPage保持一致 --> <if test="category != null and category != ''"> AND category = #{category} </if> <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%')) </if> </select>Service层 (RecipeServiceImpl.java):
@Override public PageResult<Recipe> getRecipePage(String category, String keyword, Integer pageNum, Integer pageSize) { // 计算偏移量 int offset = (pageNum - 1) * pageSize; // 查询当前页数据 List<Recipe> list = recipeDao.selectPage(category, keyword, offset, pageSize); // 查询总记录数 int total = recipeDao.countPage(category, keyword); // 计算总页数 int totalPages = (int) Math.ceil((double) total / pageSize); PageResult<Recipe> result = new PageResult<>(); result.setList(list); result.setPageNum(pageNum); result.setPageSize(pageSize); result.setTotal(total); result.setTotalPages(totalPages); return result; }注意事项:1.参数校验:在Controller层要对
pageNum和pageSize做校验,防止传入负数或过大的值。2.SQL注入:MyBatis的#{}是预编译的,能有效防止SQL注入,切勿使用${}拼接用户输入的keyword。3.性能:当数据量极大时,LIMIT offset, pageSize在offset很大时会有性能问题,可考虑使用基于“上次查询最大ID”的分页优化方式。
5. 微信小程序前端关键实现与联调
后端API准备好后,小程序前端的工作就是调用这些接口,并将数据呈现给用户。这里探讨几个关键页面的实现和联调中的常见问题。
5.1 首页菜谱列表实现
首页通常是一个无限滚动的列表。我们使用小程序的scroll-view组件或直接利用页面的滚动,配合onReachBottom生命周期函数实现分页加载。
页面WXML结构:
<!-- pages/index/index.wxml --> <view class="recipe-list"> <block wx:for="{{recipeList}}" wx:key="id"> <view class="recipe-item" bindtap="goToDetail">Page({ data: { recipeList: [], // 列表数据 pageNum: 1, pageSize: 10, hasMore: true, loading: false, category: '', // 当前筛选分类 keyword: '' // 搜索关键词 }, onLoad(options) { this.loadData(); // 页面加载时请求第一页 }, // 加载数据函数 loadData() { if (!this.data.hasMore || this.data.loading) return; this.setData({ loading: true }); const { pageNum, pageSize, category, keyword } = this.data; // 调用封装好的网络请求方法 wx.request({ url: 'https://your-domain.com/api/recipe/list', method: 'GET', data: { pageNum, pageSize, category, keyword }, header: { 'Authorization': wx.getStorageSync('token') // 从本地缓存获取登录Token }, success: (res) => { if (res.statusCode === 200 && res.data.code === 0) { const newList = res.data.data.list; const total = res.data.data.total; const hasMore = this.data.pageNum * this.data.pageSize < total; this.setData({ recipeList: [...this.data.recipeList, ...newList], // 拼接新旧数据 hasMore: hasMore, pageNum: this.data.pageNum + 1 }); } else { wx.showToast({ title: '加载失败', icon: 'none' }); } }, fail: (err) => { wx.showToast({ title: '网络错误', icon: 'none' }); }, complete: () => { this.setData({ loading: false }); } }); }, // 页面上拉触底事件处理函数 onReachBottom() { this.loadData(); }, // 搜索或筛选 onSearchOrFilter(e) { // 重置参数 this.setData({ recipeList: [], pageNum: 1, hasMore: true, category: e.detail.category, keyword: e.detail.keyword }); this.loadData(); }, // 跳转到详情页 goToDetail(e) { const recipeId = e.currentTarget.dataset.id; wx.navigateTo({ url: `/pages/detail/detail?id=${recipeId}` }); } })联调心得:1.域名与配置:小程序请求的后端域名必须在微信公众平台的后台配置到“服务器域名”列表中,否则无法请求。开发阶段可以在微信开发者工具中勾选“不校验合法域名”。2.Token管理:登录成功后获得的Token需要保存在本地(如
wx.setStorageSync),并在每次请求的Header中携带。可以封装一个统一的request函数来处理Token添加和过期刷新。3.加载状态:一定要做好加载中和无更多数据的UI提示,避免用户重复触发请求。
5.2 图片上传与云存储
菜谱的封面和步骤图都需要上传图片。小程序提供了wx.chooseImage和wx.uploadFileAPI。但直接将图片上传到自己的后端服务器,会面临存储、带宽、图片处理(缩略图、水印)等问题。更常见的做法是结合云存储。
一种推荐的做法是:
- 小程序端调用后端API,请求一个上传凭证(比如针对腾讯云COS的临时密钥)。
- 后端生成一个具有限时、限权限的上传凭证返回给小程序。
- 小程序使用该凭证,直接调用云存储服务商(如腾讯云COS、阿里云OSS)的SDK,将图片直传到对象存储。
- 上传成功后,云存储会返回一个文件的永久访问URL。
- 小程序将这个URL随菜谱其他信息一起提交给后端保存。
这样做的好处是:1. 减轻自身服务器带宽压力;2. 利用云存储的CDN加速图片访问;3. 可以利用云存储提供的图片处理能力(如缩放、裁剪)。后端代码只需要负责签发安全的临时凭证即可。
5.3 表单提交与复杂数据结构
发布菜谱的表单比较复杂,包含动态增减的步骤和食材列表。前端需要维护一个数组来管理这些动态项。
WXML片段示例 (动态步骤列表):
<view class="step-list"> <view wx:for="{{steps}}" wx:key="index" wx:for-index="idx"> <text>步骤 {{idx + 1}}</text> <textarea placeholder="描述步骤..." value="{{item.content}}" bindinput="onStepInput">Page({ data: { steps: [{ content: '', image: null }] // 初始一个空步骤 }, addStep() { this.setData({ steps: [...this.data.steps, { content: '', image: null }] }); }, removeStep(e) { const index = e.currentTarget.dataset.index; const newSteps = this.data.steps.filter((_, i) => i !== index); this.setData({ steps: newSteps }); }, onStepInput(e) { const index = e.currentTarget.dataset.index; const value = e.detail.value; const key = `steps[${index}].content`; this.setData({ [key]: value }); }, // 提交表单时,将this.data.steps和this.data.ingredients作为JSON的一部分传给后端 submitForm() { const formData = { title: this.data.title, // ... 其他字段 steps: this.data.steps, ingredients: this.data.ingredients }; // 调用创建菜谱的API } })踩坑提醒:小程序中动态修改数组或对象内某个字段的值,必须使用
this.setData({ 'array[0].text': 'new text' })或this.setData({ 'object.subfield': 'new value' })这种路径写法。直接修改this.data.array[0].text然后调用this.setData({ array: this.data.array })有时在复杂场景下可能无法触发视图更新。
6. 项目部署、优化与扩展思考
一个项目写完代码只是第一步,让它能稳定、高效地跑起来,并思考未来的可能性,才是更考验人的地方。
6.1 基础部署方案
对于学习或小范围使用的项目,一个简单的部署方案如下:
- 后端:将SpringBoot项目打成可执行的JAR包(使用
mvn clean package)。购买一台云服务器(如1核2G的入门配置),安装Java运行环境(JRE 8或11)。将JAR包上传至服务器,使用nohup java -jar your-app.jar &命令在后台运行。更规范的做法是使用 systemd 或 Docker 来管理进程。 - 数据库:在同一台服务器或使用云厂商提供的MySQL数据库服务(如阿里云RDS)。记得修改后端配置中的数据库连接地址、用户名和密码。
- 前端:在小程序开发者工具中上传代码,提交审核发布。注意将后端的API域名配置到小程序后台。
- 域名与HTTPS:小程序要求后端接口必须使用HTTPS。你需要为你的服务器域名申请SSL证书(云厂商通常提供免费证书),并在Nginx或服务器上配置好。
6.2 性能与体验优化点
- 数据库索引优化:在经常用于查询条件的字段上建立索引,能极大提升查询速度。例如,
recipe表的category,user_id,create_time字段,user_collect_recipe表的user_id和recipe_id组合索引。 - API响应优化:
- 分页:务必做好分页,避免一次查询过多数据。
- 图片懒加载:列表页的图片使用小程序原生的
lazy-load属性。 - 接口合并:详情页可能需要菜谱信息、步骤、食材、作者信息。可以考虑设计一个“聚合接口”,一次请求返回所有必要数据,减少网络请求次数,或者让前端并行请求多个接口。
- CDN加速:静态资源(如图片)一定要放在CDN上。
- 缓存策略:使用Redis缓存热点数据。例如,首页的菜谱列表(非实时性要求极高)、用户基本信息等。可以设置合理的过期时间(如5分钟)。
- 小程序端优化:
- 分包加载:随着功能增加,小程序的代码包会变大。可以将一些独立的功能模块(如“我的收藏”、“消息中心”)放到独立的分包中,提升首次启动速度。
- 骨架屏:在数据加载前显示页面结构的灰色骨架图,提升用户感知速度。
- 本地缓存:对于不常变的数据(如菜谱分类),可以请求一次后存储在
wx.setStorageSync中,下次优先使用。
6.3 功能扩展方向
这个基础框架可以朝很多有趣的方向扩展:
- 社交化:增加关注、私信、菜谱评论/问答、点赞排行榜等功能。
- 智能化:
- 食材识别:集成图像识别API,用户拍照上传食材,自动识别并推荐相关菜谱。
- 智能推荐:根据用户的收藏、浏览记录,使用协同过滤等算法进行个性化菜谱推荐。
- 营养计算:接入营养数据库,根据菜谱食材估算热量、蛋白质等营养信息。
- 工具化:
- 购物清单同步:将生成的购物清单同步到系统提醒或第三方待办应用。
- 烹饪计时器:在菜谱详情页集成步骤计时功能。
- 语音播报:接入语音合成,朗读烹饪步骤,实现“语音菜谱”。
- 多端同步:开发Web管理端或App,实现菜谱数据在多设备间同步。
回顾这个“家庭大厨”项目,从技术选型、数据库设计、API实现到前端联调和部署思考,它完整地走通了一个全栈应用的生命周期。虽然用的技术不是最新的,但其中蕴含的分层架构思想、事务处理、API设计、前后端数据交互、性能优化意识,是无论技术栈如何迭代都不过时的核心能力。对于学习者而言,吃透这样一个麻雀虽小五脏俱全的案例,远比泛泛地看十个框架的官方文档更有价值。你可以基于这个源码进行修改、添加功能,把它变成你自己的“家庭食谱管家”,或者“健身餐记录器”,这个过程本身,就是最好的学习。
本文还有配套的精品资源,点击获取