基于SpringBoot及PostgreSQL的国家减肥食谱管理项目(中):食谱与菜单配置搭建

目录

前言

一、项目需求简介

1、需求简介

2、本次实现介绍

二、SpringBoot后端实现

1、Model实现

2、业务层实现

2.1关于返回数量限制

2.2父子表单的后台新增实现

2.3父子表单的后台编辑实现

三、Thymeleaf前端实现

1、食谱管理实现

2、餐别及详情实现

3、父子表单提交

四、总结


前言

在当今社会,健康意识的觉醒促使越来越多的人关注减肥与营养均衡。减肥不仅关乎外在形象,更是关乎内在健康的重要课题。一个科学合理的减肥食谱对于减肥者来说至关重要,它能够帮助人们在减少热量摄入的同时,保证身体获取足够的营养。然而,不同地区的人们由于生活习惯、饮食文化以及可获取食材的差异,对于减肥食谱有着各自独特的需求。基于此,开发一个国家减肥食谱管理系统显得尤为必要,它能够根据不同区域和省份的特点,为用户提供个性化的减肥食谱方案。博文基于SpringBoot及PostgreSQL的国家减肥食谱管理项目(上):区域与省份配置搭建中已经对食谱的区域和省份信息配置进行了详细的说明。

本文作为健康项目的中篇,将详细介绍如何对区域食谱以及食谱对应的菜单详情进行配置,通过案例的讲解,大家可以看到整个的健康管理的全貌,对涉及的相关表有一个具体的认识。

本例还在场景配置中介绍如何基于ruoyi开发父子表格提交的窗口,可以为类似的需求开发做一个技术参考。通过基于 SpringBoot 和 PostgreSQL 的技术实现,我们相信能够构建出一个稳定、高效、用户友好的系统,为全国不同地区的减肥者提供科学、个性化的减肥食谱服务。在接下来的章节中,我们将详细介绍具体实现过程,包括数据库设计、后端服务开发以及前端用户界面设计等方面的内容,让我们一起开启这段精彩的开发之旅吧。

一、项目需求简介

本节将重点介绍国家减肥食谱项目的背景要求,分别从需求简介和本次需要实现的需求两个方面进行介绍,通过本节让大家可以了解一些背景信息,更好的去参与后期的项目建设。

1、需求简介

在本项目的上篇中,我们将重点聚焦于区域和省份的配置搭建。区域和省份配置是整个减肥食谱管理系统的基础,它决定了系统能够覆盖的地理范围以及能够根据不同地区特点提供服务的能力。通过精心设计的区域和省份配置,系统可以针对不同地区的饮食偏好、食材供应情况等因素,生成更具针对性和实用性的减肥食谱。例如,在沿海地区,海鲜资源丰富,减肥食谱可以充分利用这一优势,设计出以海鲜为主的低热量、高蛋白的菜品;而在内陆地区,蔬菜和肉类的搭配则可能成为减肥食谱的重点。

2、本次实现介绍

作为面向数据库的应用开发,数据库的设计直观重要。在之前的博文中对数据库表进行了深入的分析和讨论,详见:面向国家健康食谱指南的PostgreSQL物理表设计及数据存储实践方案。大家可以通过点击查看之前的博文。由于食谱的管理内容较多,设计到食谱区域、区域关联省份、餐次即食谱详情等,在之前的博文中详细介绍了食谱区域以及食谱区域对应的省份信息,本次将继续做好这个基础工作,主要是剩下的食谱、餐别及详情信息,也将为后续的工作提供基础。本次实现的需求如下图中的红框中的数据库表所示:

以上数据库表格是后续功能实现的数据基础,需要重点理解掌握。

二、SpringBoot后端实现

本节将以Ruoyi开发框架为例,重点讲解在SpringBoot后台中如何实现健康食谱的管理以及食谱对应的菜单信息管理。主要包括以下两个层的实现,即Model模型层和业务层。

1、Model实现

与数据库设计一一对应,在SpringBoot中主要包含三个类,即食谱信息类、餐别信息类以及菜单详情信息类。食谱信息主要是按四季的维度组织数据,用于定义不同的季节吃什么样的食物。食谱信息类关键的代码如下:

java 复制代码
package com.yelang.project.extend.earthquake.domain;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
/**
 * - 四季食谱信息表
 * @author 夜郎king
 */
@TableName(value = "biz_four_seasons_recipe")
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@ToString
public class FourSeasonsRecipe implements Serializable {
	private static final long serialVersionUID = -6072349554277340610L;
	@TableId(value = "pk_id")
	private Long pkId;// 主键
	@TableField(value = "food_id")
	private Long foodId;// 区域饮食主键
	@TableField(value = "food_name")
	private String foodName;// 食谱名称
	@TableField(value = "total_energy")
	private Integer totalEnergy;// 总能量,单位kcal
	private Integer season;// 季节标志,1:春季 2:夏季 3:秋季 4:冬季
	@TableField(value = "order_num")
	private Integer orderNum;// 排序号
	private String remark;// 备注
	public FourSeasonsRecipe(Long foodId, String foodName, Integer totalEnergy, Integer season, Integer orderNum,
			String remark) {
		super();
		this.foodId = foodId;
		this.foodName = foodName;
		this.totalEnergy = totalEnergy;
		this.season = season;
		this.orderNum = orderNum;
		this.remark = remark;
	}
}

需要注意的是,这里我们使用1、2、3、4四个数字分别对应一年春夏秋冬四季。有了一年四季的食谱后,接下来就是季节内一日三餐或者需要加餐信息的管理,所以我们增加一个餐别的管理,餐别管理的核心代码如下:

java 复制代码
package com.yelang.project.extend.earthquake.domain;
import java.io.Serializable;
import java.util.List;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
/**
 * - 食谱餐别信息表
 * @author 夜郎king
 */
@TableName(value = "biz_recipe_meal_type")
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@ToString
public class RecipeMealType implements Serializable {
	private static final long serialVersionUID = 4399855059597304978L;
	@TableId(value = "pk_id")
	private Long pkId;// 主键
	@TableField(value = "recipe_id")
	private Long recipeId;// 食谱id
	@TableField(value = "meal_type_name")
	private String mealTypeName;// 餐别名称,如早餐、加餐等
	@TableField(value = "order_num")
	private Integer orderNum;// 排序号
	@TableField(exist = false)
	private List<RecipeDetails> recipeDetailsList;//菜单详情
	public RecipeMealType(Long recipeId, String mealTypeName, Integer orderNum) {
		super();
		this.recipeId = recipeId;
		this.mealTypeName = mealTypeName;
		this.orderNum = orderNum;
	}
}

针对不同类型的餐别,比如早餐、中餐、晚餐等,我们会根据时间来进行不同食物的准备。所以在定义好餐别后,我们可以基于餐别来详细定义不同的餐别的食物,也就是菜单。菜单信息类的核心代码如下:

java 复制代码
package com.yelang.project.extend.earthquake.domain;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
/**
 * - 菜单详情信息表
 * @author 夜郎king
 */
@TableName(value = "biz_recipe_details")
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@ToString
public class RecipeDetails implements Serializable {
	private static final long serialVersionUID = -4003180539390925788L;
	@TableId(value = "pk_id")
	private Long pkId;// 主键
	@TableField(value = "meal_type_id")
	private Long mealTypeId;// 餐别id
	private String name;// 名称
	private String formula;// 配方信息
	@TableField(value = "order_num")
	private Integer orderNum;// 排序号
	public RecipeDetails(Long mealTypeId, String name, String formula, Integer orderNum) {
		super();
		this.mealTypeId = mealTypeId;
		this.name = name;
		this.formula = formula;
		this.orderNum = orderNum;
	}
}

大家可以看到,在餐别信息类中,我们定义个菜单详情的集合,方便的对应的业务层中进行统一保存。与模型层配套的用于数据库读写的是Mapper,由于不包含太复杂的业务,这里不进行赘述。

2、业务层实现

类似于这种业务,对于控制层的实现并不是很复杂,但是业务层还是一个核心,其中包含了一些基本的业务规则的校验和关联等。比如有的时候需要使用MybatisPlus的一些高级语法进行数据的查询。因此本文将重点和目光聚焦在业务层,重点讲讲在业务层可能会遇到什么问题。

2.1关于返回数量限制

如果要在MybatisPlus中执行SQL语句时,使用Limit等子句来进行返回结果集的限制。则可以使用以下代码:

java 复制代码
private FourSeasonsRecipe getFourSeasonsRecipe(FourSeasonsRecipe fourSeasonsRecipe) {
	QueryWrapper<FourSeasonsRecipe> queryWrapper = new QueryWrapper<FourSeasonsRecipe>();
	queryWrapper.eq("food_id", fourSeasonsRecipe.getFoodId());
	if(StringUtils.isNotBlank(fourSeasonsRecipe.getFoodName())){
        queryWrapper.like("food_name", fourSeasonsRecipe.getFoodName());
    }
	//在末尾追加limit1,表示限制返回数量
	queryWrapper.last(" limit 1 ");
	return this.baseMapper.selectOne(queryWrapper);
}

通过这段代码就可以实现Limit子句的添加和执行。

2.2父子表单的后台新增实现

关于有父子表单的实现,这里以餐别和菜单详情的关系,一个餐别入早餐,势必会有多种菜单,也就是一对多的关系。在这种场景下就比较适合实用父子表单来进行业务的管理和实现。相对而言,新增的业务处理场景是比较简单的,因此首先我们来讲解一下父子表单的新增实现。业务层的核心代码如下:

java 复制代码
@Override
@Transactional(propagation=Propagation.REQUIRED,rollbackFor=Exception.class)
public int insertEntity(RecipeMealType recipeMealType) {
	int result = this.baseMapper.insert(recipeMealType);
	Long pkId = recipeMealType.getPkId();
	List<RecipeDetails> recipeDetailsList = recipeMealType.getRecipeDetailsList();
	for(RecipeDetails details : recipeDetailsList) {
		details.setMealTypeId(pkId);
	}
	recipeDetailsService.saveBatch(recipeDetailsList);
	return result;
}

在调用MybatisPlus的新增方法后,新生成一个pkid的字段,需要在批量插入时设置好对应的ID即可。

2.3父子表单的后台编辑实现

编辑的功能实现其实与新增相差不大,这里我们采用比较简单的一种方式,即在使用编辑功能时,先将所有的菜单详情删除,然后再全部采用新增的模式。这种模式比较粗暴,在实际业务中,需要开发人员进行选择。实现代码如下:

三、Thymeleaf前端实现

在完成了SpringBoot的后端程序设计与实现之后,下面我们就需要使用Thymeleaf的方式来进行前端界面的实现。这里依然以我们熟悉的单体架构模式进行集成,如需要分离版本,可以自己设计。

1、食谱管理实现

在食谱的管理中,我们需要支持不同的地区进行统一管理,因此我们需要在打开的每个tab页中记住之前所选择的地区信息。对应的窗口处理Javascript脚本如下:

javascript 复制代码
function recipemealtypeConfig(recipeId,foodName){
	var url = ctx + "/eq/foodarea/recipemealtype/" + recipeId;
	$.modal.openTab("[" + foodName + "]餐别配置", url);
}

不仅如此,食谱的管理还支持按照食谱名称进行查询,列表页面如下:

食谱的新增页面需要按照一年四季进行管理,因此在新增或者编辑页面中也需要支持四季的选择,实现效果如下:

这里将食谱的新增和编辑功能一并讲解。

2、餐别及详情实现

基于一年四季的食谱定义完成之后,接下来就需要对餐别即中餐、加餐、中餐、晚餐等进行统一的管理配置。同时还要支持不同的餐别的菜单配置,到这里才是重点的饮食控制。

餐别的新增和编辑页面实现效果如下:

3、父子表单提交

父子表单域普通表单还是有蛮大的区别的。在这里菜单和详情之间其实就一个父子表单关系,因此我们可以使用这种方式进行数据的提交和管理。下面我们以新增功能为例,讲解如何进行集成。首先是html页面中需要做如下定义:

html 复制代码
<h4 class="form-header h4">菜单详情</h4>
    <div class="row">
         <div class="col-sm-12">
              <button type="button" class="btn btn-white btn-sm" onclick="addRow()"><i class="fa fa-plus"> 增加</i></button>
              <button type="button" class="btn btn-white btn-sm" onclick="sub.delRow()"><i class="fa fa-minus"> 删除</i></button>
               <div class="col-sm-12 select-table table-striped">
					<table id="bootstrap-table"></table>
				/div>
          </div>
     </div>
 </div>

最主要的是下面的JS脚本,用户子表单的新增和功能交互:

javascript 复制代码
$(function() {
    // 初始化数据, 可以由后台传过来
	var data = [
	   {
	    	pkId: "",
	    	name: "",
	    	formula: "",
            orderNum: "1",
	    }];
    var options = {
		data: data,
        pagination: false,
		showSearch: false,
        showRefresh: false,
        showToggle: false,
        showColumns: false,
        sidePagination: "client",
		columns: [{
		      checkbox: true
		},
		{
		    field: 'index',
		    align: 'center',
		    title: "序号",
            formatter: function (value, row, index) {
               var columnIndex = $.common.sprintf("<input type='hidden' name='index' value='%s'>", $.table.serialNumber(index));	
               return columnIndex + $.table.serialNumber(index);
            }
        },
		{
		     field: 'name',
		     align: 'center',
		     title: '菜单名称',
		     formatter: function(value, row, index) {
		          var html = $.common.sprintf("<input class='form-control' type='text' name='recipeDetailsList[%s].name' value='%s'>", index, value);
		        return html;
            }
		},
		{
		            field: 'formula',
		            align: 'center',
		            title: '配方信息',
		            formatter: function(value, row, index) {
		            	var html = $.common.sprintf("<input class='form-control' type='text' name='recipeDetailsList[%s].formula' value='%s'>", index, value);
		        		return html;
                    }
		        },
		        {
		            field: 'orderNum',
		            align: 'center',
		            title: '排序号',
		            formatter: function(value, row, index) {
		            	var html = $.common.sprintf("<input class='form-control' type='text' name='recipeDetailsList[%s].orderNum' value='%s'>", index, value);
		        		return html;
                    }
		        },
		        {
                    title: '操作',
                    align: 'center',
                    formatter: function(value, row, index) {
                    	var value = $.common.isNotEmpty(row.index) ? row.index : $.table.serialNumber(index);
                        return '<a class="btn btn-danger btn-xs" href="javascript:void(0)" onclick="sub.delRowByIndex(\'' + value + '\')"><i class="fa fa-remove"></i>删除</a>';
                    }
                }]
		    };
		    $.table.init(options);
		});
	    
        function addRow() {
        	var count = $("#" + table.options.id).bootstrapTable('getData').length;
        	var row = {
                index: $.table.serialNumber(count),
            	name: "",
            	formula: "",
            	orderNum: "",
            }
        	sub.addRow(row);
        }

使用以上定义基本可以实现父子表单的前端定义,来看一下实际效果:

四、总结

以上就是本文的主要内容,本文作为健康项目的中篇,将详细介绍如何对区域食谱以及食谱对应的菜单详情进行配置,通过案例的讲解,大家可以看到整个的健康管理的全貌,对涉及的相关表有一个具体的认识。本例还在场景配置中介绍如何基于ruoyi开发父子表格提交的窗口,可以为类似的需求开发做一个技术参考。通过基于 SpringBoot 和 PostgreSQL 的技术实现,我们相信能够构建出一个稳定、高效、用户友好的系统,为全国不同地区的减肥者提供科学、个性化的减肥食谱服务。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。