feat: 新增食物库模块,含模型、服务、API及初始化数据
This commit is contained in:
85
sql-scripts/food-library-sample-data.sql
Normal file
85
sql-scripts/food-library-sample-data.sql
Normal file
@@ -0,0 +1,85 @@
|
||||
-- 插入更多示例食物数据
|
||||
-- 清空现有数据(如果需要重新初始化)
|
||||
-- DELETE FROM `t_food_library`;
|
||||
-- DELETE FROM `t_food_categories`;
|
||||
|
||||
-- 插入食物分类数据
|
||||
INSERT IGNORE INTO `t_food_categories` (`key`, `name`, `sort_order`, `is_system`) VALUES
|
||||
('common', '常见', 1, 1),
|
||||
('fruits_vegetables', '水果蔬菜', 2, 1),
|
||||
('meat_eggs_dairy', '肉蛋奶', 3, 1),
|
||||
('beans_nuts', '豆类坚果', 4, 1),
|
||||
('snacks_drinks', '零食饮料', 5, 1),
|
||||
('staple_food', '主食', 6, 1),
|
||||
('dishes', '菜肴', 7, 1);
|
||||
|
||||
-- 插入常见食物(这些食物会显示在"常见"分类中)
|
||||
INSERT IGNORE INTO `t_food_library` (`name`, `category_key`, `calories_per_100g`, `protein_per_100g`, `carbohydrate_per_100g`, `fat_per_100g`, `fiber_per_100g`, `sugar_per_100g`, `sodium_per_100g`, `is_common`, `sort_order`) VALUES
|
||||
-- 常见食物(会显示在常见分类中)
|
||||
('无糖美式咖啡', 'snacks_drinks', 1, 0.1, 0, 0, 0, 0, 2, 1, 1),
|
||||
('荷包蛋(油煎)', 'meat_eggs_dairy', 195, 13.3, 0.7, 15.3, 0, 0.7, 124, 1, 2),
|
||||
('鸡蛋', 'meat_eggs_dairy', 139, 13.3, 2.8, 8.8, 0, 2.8, 131, 1, 3),
|
||||
('香蕉', 'fruits_vegetables', 93, 1.4, 22.8, 0.2, 1.7, 17.2, 1, 1, 4),
|
||||
('猕猴桃', 'fruits_vegetables', 61, 1.1, 14.7, 0.5, 3, 9, 3, 1, 5),
|
||||
('苹果', 'fruits_vegetables', 53, 0.3, 14.1, 0.2, 2.4, 10.4, 1, 1, 6),
|
||||
('草莓', 'fruits_vegetables', 32, 0.7, 7.7, 0.3, 2, 4.9, 1, 1, 7),
|
||||
('蛋烧麦', 'staple_food', 157, 6.2, 22.2, 5.2, 1.1, 1.8, 230, 1, 8),
|
||||
('米饭', 'staple_food', 116, 2.6, 25.9, 0.3, 0.3, 0.1, 5, 1, 9);
|
||||
|
||||
-- 插入水果蔬菜分类的其他食物
|
||||
INSERT IGNORE INTO `t_food_library` (`name`, `category_key`, `calories_per_100g`, `protein_per_100g`, `carbohydrate_per_100g`, `fat_per_100g`, `fiber_per_100g`, `sugar_per_100g`, `sodium_per_100g`, `is_common`, `sort_order`) VALUES
|
||||
('橙子', 'fruits_vegetables', 47, 0.9, 11.8, 0.1, 2.4, 9.4, 0, 0, 1),
|
||||
('葡萄', 'fruits_vegetables', 69, 0.7, 17.2, 0.2, 0.9, 16.3, 2, 0, 2),
|
||||
('西瓜', 'fruits_vegetables', 30, 0.6, 7.6, 0.2, 0.4, 6.2, 1, 0, 3),
|
||||
('菠菜', 'fruits_vegetables', 23, 2.9, 3.6, 0.4, 2.2, 0.4, 79, 0, 4),
|
||||
('西兰花', 'fruits_vegetables', 34, 2.8, 7, 0.4, 2.6, 1.5, 33, 0, 5),
|
||||
('胡萝卜', 'fruits_vegetables', 41, 0.9, 9.6, 0.2, 2.8, 4.7, 69, 0, 6),
|
||||
('西红柿', 'fruits_vegetables', 18, 0.9, 3.9, 0.2, 1.2, 2.6, 5, 0, 7);
|
||||
|
||||
-- 插入肉蛋奶分类的其他食物
|
||||
INSERT IGNORE INTO `t_food_library` (`name`, `category_key`, `calories_per_100g`, `protein_per_100g`, `carbohydrate_per_100g`, `fat_per_100g`, `fiber_per_100g`, `sugar_per_100g`, `sodium_per_100g`, `is_common`, `sort_order`) VALUES
|
||||
('鸡胸肉', 'meat_eggs_dairy', 165, 31, 0, 3.6, 0, 0, 74, 0, 1),
|
||||
('牛肉', 'meat_eggs_dairy', 250, 26, 0, 15, 0, 0, 72, 0, 2),
|
||||
('猪肉', 'meat_eggs_dairy', 242, 27, 0, 14, 0, 0, 58, 0, 3),
|
||||
('三文鱼', 'meat_eggs_dairy', 208, 25, 0, 12, 0, 0, 44, 0, 4),
|
||||
('牛奶', 'meat_eggs_dairy', 54, 3.4, 5.1, 1.9, 0, 5.1, 44, 0, 5),
|
||||
('酸奶', 'meat_eggs_dairy', 99, 10, 3.6, 5.3, 0, 3.2, 36, 0, 6),
|
||||
('奶酪', 'meat_eggs_dairy', 113, 25, 1.3, 0.2, 0, 1.3, 515, 0, 7);
|
||||
|
||||
-- 插入豆类坚果分类的食物
|
||||
INSERT IGNORE INTO `t_food_library` (`name`, `category_key`, `calories_per_100g`, `protein_per_100g`, `carbohydrate_per_100g`, `fat_per_100g`, `fiber_per_100g`, `sugar_per_100g`, `sodium_per_100g`, `is_common`, `sort_order`) VALUES
|
||||
('黄豆', 'beans_nuts', 446, 36, 30, 20, 15, 7, 2, 0, 1),
|
||||
('黑豆', 'beans_nuts', 341, 21, 63, 1.4, 15, 2.1, 2, 0, 2),
|
||||
('红豆', 'beans_nuts', 309, 20, 63, 0.5, 12, 2.2, 2, 0, 3),
|
||||
('核桃', 'beans_nuts', 654, 15, 14, 65, 6.7, 2.6, 2, 0, 4),
|
||||
('杏仁', 'beans_nuts', 579, 21, 22, 50, 12, 4.4, 1, 0, 5),
|
||||
('花生', 'beans_nuts', 567, 26, 16, 49, 8.5, 4.7, 18, 0, 6),
|
||||
('腰果', 'beans_nuts', 553, 18, 30, 44, 3.3, 5.9, 12, 0, 7);
|
||||
|
||||
-- 插入主食分类的其他食物
|
||||
INSERT IGNORE INTO `t_food_library` (`name`, `category_key`, `calories_per_100g`, `protein_per_100g`, `carbohydrate_per_100g`, `fat_per_100g`, `fiber_per_100g`, `sugar_per_100g`, `sodium_per_100g`, `is_common`, `sort_order`) VALUES
|
||||
('白面包', 'staple_food', 265, 9, 49, 3.2, 2.7, 5.7, 491, 0, 1),
|
||||
('全麦面包', 'staple_food', 247, 13, 41, 4.2, 7, 6, 396, 0, 2),
|
||||
('燕麦', 'staple_food', 389, 17, 66, 6.9, 10, 0.99, 2, 0, 3),
|
||||
('小米', 'staple_food', 378, 11, 73, 4.2, 8.5, 1.7, 5, 0, 4),
|
||||
('玉米', 'staple_food', 365, 9.4, 74, 4.7, 7.3, 6.3, 35, 0, 5),
|
||||
('红薯', 'staple_food', 86, 1.6, 20, 0.1, 3, 4.2, 54, 0, 6),
|
||||
('土豆', 'staple_food', 77, 2, 17, 0.1, 2.2, 0.8, 6, 0, 7);
|
||||
|
||||
-- 插入零食饮料分类的其他食物
|
||||
INSERT IGNORE INTO `t_food_library` (`name`, `category_key`, `calories_per_100g`, `protein_per_100g`, `carbohydrate_per_100g`, `fat_per_100g`, `fiber_per_100g`, `sugar_per_100g`, `sodium_per_100g`, `is_common`, `sort_order`) VALUES
|
||||
('绿茶', 'snacks_drinks', 1, 0, 0, 0, 0, 0, 3, 0, 1),
|
||||
('红茶', 'snacks_drinks', 1, 0, 0.3, 0, 0, 0, 3, 0, 2),
|
||||
('柠檬水', 'snacks_drinks', 22, 0.4, 6.9, 0.2, 1.6, 1.5, 1, 0, 3),
|
||||
('苏打水', 'snacks_drinks', 0, 0, 0, 0, 0, 0, 21, 0, 4),
|
||||
('黑巧克力', 'snacks_drinks', 546, 7.8, 61, 31, 11, 48, 20, 0, 5),
|
||||
('饼干', 'snacks_drinks', 502, 5.9, 68, 23, 2.1, 27, 386, 0, 6);
|
||||
|
||||
-- 插入菜肴分类的食物
|
||||
INSERT IGNORE INTO `t_food_library` (`name`, `category_key`, `calories_per_100g`, `protein_per_100g`, `carbohydrate_per_100g`, `fat_per_100g`, `fiber_per_100g`, `sugar_per_100g`, `sodium_per_100g`, `is_common`, `sort_order`) VALUES
|
||||
('宫保鸡丁', 'dishes', 194, 18, 8, 11, 2, 4, 590, 0, 1),
|
||||
('麻婆豆腐', 'dishes', 164, 11, 6, 12, 2, 3, 680, 0, 2),
|
||||
('红烧肉', 'dishes', 395, 15, 8, 35, 1, 6, 720, 0, 3),
|
||||
('清蒸鱼', 'dishes', 112, 20, 2, 3, 0, 1, 280, 0, 4),
|
||||
('蒸蛋羹', 'dishes', 62, 5.8, 1.2, 4.1, 0, 1, 156, 0, 5),
|
||||
('凉拌黄瓜', 'dishes', 16, 0.7, 3.6, 0.1, 0.5, 1.7, 6, 0, 6);
|
||||
80
sql-scripts/food-library-tables-create.sql
Normal file
80
sql-scripts/food-library-tables-create.sql
Normal file
@@ -0,0 +1,80 @@
|
||||
-- 创建食物分类表
|
||||
-- 该表用于存储食物的分类信息,如常见、水果蔬菜、肉蛋奶等
|
||||
CREATE TABLE IF NOT EXISTS `t_food_categories` (
|
||||
`key` varchar(50) NOT NULL COMMENT '分类唯一键(英文/下划线)',
|
||||
`name` varchar(50) NOT NULL COMMENT '分类中文名称',
|
||||
`icon` varchar(100) DEFAULT NULL COMMENT '分类图标',
|
||||
`sort_order` int NOT NULL DEFAULT '0' COMMENT '排序(升序)',
|
||||
`is_system` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否系统分类(1:系统,0:用户自定义)',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`key`),
|
||||
KEY `idx_sort_order` (`sort_order`),
|
||||
KEY `idx_is_system` (`is_system`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='食物分类表';
|
||||
|
||||
-- 创建食物库表
|
||||
-- 该表用于存储食物的基本信息和营养成分
|
||||
CREATE TABLE IF NOT EXISTS `t_food_library` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`name` varchar(100) NOT NULL COMMENT '食物名称',
|
||||
`description` varchar(500) DEFAULT NULL COMMENT '食物描述',
|
||||
`category_key` varchar(50) NOT NULL COMMENT '分类键',
|
||||
`calories_per_100g` float DEFAULT NULL COMMENT '每100克热量(卡路里)',
|
||||
`protein_per_100g` float DEFAULT NULL COMMENT '每100克蛋白质含量(克)',
|
||||
`carbohydrate_per_100g` float DEFAULT NULL COMMENT '每100克碳水化合物含量(克)',
|
||||
`fat_per_100g` float DEFAULT NULL COMMENT '每100克脂肪含量(克)',
|
||||
`fiber_per_100g` float DEFAULT NULL COMMENT '每100克膳食纤维含量(克)',
|
||||
`sugar_per_100g` float DEFAULT NULL COMMENT '每100克糖分含量(克)',
|
||||
`sodium_per_100g` float DEFAULT NULL COMMENT '每100克钠含量(毫克)',
|
||||
`additional_nutrition` json DEFAULT NULL COMMENT '其他营养信息(维生素、矿物质等)',
|
||||
`is_common` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否常见食物(1:常见,0:不常见)',
|
||||
`image_url` varchar(500) DEFAULT NULL COMMENT '食物图片URL',
|
||||
`sort_order` int NOT NULL DEFAULT '0' COMMENT '排序(分类内)',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_category_key` (`category_key`),
|
||||
KEY `idx_is_common` (`is_common`),
|
||||
KEY `idx_name` (`name`),
|
||||
KEY `idx_category_sort` (`category_key`, `sort_order`),
|
||||
CONSTRAINT `fk_food_category` FOREIGN KEY (`category_key`) REFERENCES `t_food_categories` (`key`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='食物库表';
|
||||
|
||||
-- 插入食物分类数据
|
||||
INSERT INTO `t_food_categories` (`key`, `name`, `sort_order`, `is_system`) VALUES
|
||||
('common', '常见', 1, 1),
|
||||
('fruits_vegetables', '水果蔬菜', 2, 1),
|
||||
('meat_eggs_dairy', '肉蛋奶', 3, 1),
|
||||
('beans_nuts', '豆类坚果', 4, 1),
|
||||
('snacks_drinks', '零食饮料', 5, 1),
|
||||
('staple_food', '主食', 6, 1),
|
||||
('dishes', '菜肴', 7, 1);
|
||||
|
||||
-- 插入示例食物数据
|
||||
INSERT INTO `t_food_library` (`name`, `category_key`, `calories_per_100g`, `protein_per_100g`, `carbohydrate_per_100g`, `fat_per_100g`, `fiber_per_100g`, `sugar_per_100g`, `sodium_per_100g`, `is_common`, `sort_order`) VALUES
|
||||
-- 常见食物
|
||||
('无糖美式咖啡', 'common', 1, 0.1, 0, 0, 0, 0, 2, 1, 1),
|
||||
('荷包蛋(油煎)', 'common', 195, 13.3, 0.7, 15.3, 0, 0.7, 124, 1, 2),
|
||||
('鸡蛋', 'common', 139, 13.3, 2.8, 8.8, 0, 2.8, 131, 1, 3),
|
||||
|
||||
-- 水果蔬菜
|
||||
('香蕉', 'fruits_vegetables', 93, 1.4, 22.8, 0.2, 1.7, 17.2, 1, 1, 1),
|
||||
('猕猴桃', 'fruits_vegetables', 61, 1.1, 14.7, 0.5, 3, 9, 3, 1, 2),
|
||||
('苹果', 'fruits_vegetables', 53, 0.3, 14.1, 0.2, 2.4, 10.4, 1, 1, 3),
|
||||
('草莓', 'fruits_vegetables', 32, 0.7, 7.7, 0.3, 2, 4.9, 1, 1, 4),
|
||||
|
||||
-- 主食
|
||||
('蛋烧麦', 'staple_food', 157, 6.2, 22.2, 5.2, 1.1, 1.8, 230, 1, 1),
|
||||
('米饭', 'staple_food', 116, 2.6, 25.9, 0.3, 0.3, 0.1, 5, 1, 2),
|
||||
|
||||
-- 零食饮料
|
||||
('无糖美式咖啡', 'snacks_drinks', 1, 0.1, 0, 0, 0, 0, 2, 0, 1),
|
||||
|
||||
-- 肉蛋奶
|
||||
('鸡蛋', 'meat_eggs_dairy', 139, 13.3, 2.8, 8.8, 0, 2.8, 131, 0, 1),
|
||||
('荷包蛋(油煎)', 'meat_eggs_dairy', 195, 13.3, 0.7, 15.3, 0, 0.7, 124, 0, 2);
|
||||
|
||||
-- 创建索引以优化查询性能
|
||||
CREATE INDEX `idx_food_common_category` ON `t_food_library` (`is_common`, `category_key`);
|
||||
CREATE INDEX `idx_food_name_search` ON `t_food_library` (`name`);
|
||||
@@ -16,6 +16,7 @@ import { WorkoutsModule } from './workouts/workouts.module';
|
||||
import { MoodCheckinsModule } from './mood-checkins/mood-checkins.module';
|
||||
import { GoalsModule } from './goals/goals.module';
|
||||
import { DietRecordsModule } from './diet-records/diet-records.module';
|
||||
import { FoodLibraryModule } from './food-library/food-library.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -37,6 +38,7 @@ import { DietRecordsModule } from './diet-records/diet-records.module';
|
||||
MoodCheckinsModule,
|
||||
GoalsModule,
|
||||
DietRecordsModule,
|
||||
FoodLibraryModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
94
src/food-library/README.md
Normal file
94
src/food-library/README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 食物库功能
|
||||
|
||||
## 功能概述
|
||||
|
||||
食物库功能提供了一个完整的食物数据库,包含各种食物的营养信息。用户可以通过分类浏览或搜索来查找食物。
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 食物分类表 (t_food_categories)
|
||||
- `key`: 分类唯一键(如:common, fruits_vegetables等)
|
||||
- `name`: 分类中文名称(如:常见、水果蔬菜等)
|
||||
- `icon`: 分类图标(可选)
|
||||
- `sort_order`: 排序顺序
|
||||
- `is_system`: 是否系统分类
|
||||
|
||||
### 食物库表 (t_food_library)
|
||||
- `id`: 食物唯一ID
|
||||
- `name`: 食物名称
|
||||
- `category_key`: 所属分类
|
||||
- `calories_per_100g`: 每100克热量
|
||||
- `protein_per_100g`: 每100克蛋白质含量
|
||||
- `carbohydrate_per_100g`: 每100克碳水化合物含量
|
||||
- `fat_per_100g`: 每100克脂肪含量
|
||||
- `fiber_per_100g`: 每100克膳食纤维含量
|
||||
- `sugar_per_100g`: 每100克糖分含量
|
||||
- `sodium_per_100g`: 每100克钠含量
|
||||
- `additional_nutrition`: 其他营养信息(JSON格式)
|
||||
- `is_common`: 是否常见食物
|
||||
- `image_url`: 食物图片URL
|
||||
- `sort_order`: 排序顺序
|
||||
|
||||
## 特殊逻辑
|
||||
|
||||
### 常见食物分类
|
||||
- 标记为 `is_common = true` 的食物会显示在"常见"分类中
|
||||
- 其他分类只显示 `is_common = false` 的食物
|
||||
- 这样避免了食物在多个分类中重复显示
|
||||
|
||||
## API接口
|
||||
|
||||
### 1. 获取食物库列表
|
||||
```
|
||||
GET /food-library
|
||||
```
|
||||
返回按分类组织的食物列表,常见食物会归类到"常见"分类中。
|
||||
|
||||
### 2. 搜索食物
|
||||
```
|
||||
GET /food-library/search?keyword=关键词
|
||||
```
|
||||
根据关键词搜索食物,常见食物会优先显示。
|
||||
|
||||
### 3. 获取食物详情
|
||||
```
|
||||
GET /food-library/:id
|
||||
```
|
||||
根据ID获取特定食物的详细信息。
|
||||
|
||||
## 数据初始化
|
||||
|
||||
1. 执行表结构创建脚本:
|
||||
```bash
|
||||
mysql -u root -p pilates_db < sql-scripts/food-library-tables-create.sql
|
||||
```
|
||||
|
||||
2. 插入示例数据:
|
||||
```bash
|
||||
mysql -u root -p pilates_db < sql-scripts/food-library-sample-data.sql
|
||||
```
|
||||
|
||||
## 客户端界面对应
|
||||
|
||||
根据提供的客户端界面,食物库包含以下分类:
|
||||
- 常见:显示标记为常见的食物
|
||||
- 水果蔬菜:fruits_vegetables 分类的非常见食物
|
||||
- 肉蛋奶:meat_eggs_dairy 分类的非常见食物
|
||||
- 豆类坚果:beans_nuts 分类的非常见食物
|
||||
- 零食饮料:snacks_drinks 分类的非常见食物
|
||||
- 主食:staple_food 分类的非常见食物
|
||||
- 菜肴:dishes 分类的非常见食物
|
||||
|
||||
每个食物显示:
|
||||
- 食物名称
|
||||
- 营养信息(如:139千卡/100克)
|
||||
- 添加按钮(+)
|
||||
|
||||
## 扩展功能
|
||||
|
||||
未来可以扩展的功能:
|
||||
- 用户自定义食物
|
||||
- 收藏食物功能
|
||||
- 食物图片上传
|
||||
- 营养成分详细分析
|
||||
- 食物推荐算法
|
||||
70
src/food-library/dto/food-library.dto.ts
Normal file
70
src/food-library/dto/food-library.dto.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class FoodItemDto {
|
||||
@ApiProperty({ description: '食物ID' })
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: '食物名称' })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: '食物描述', required: false })
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ description: '每100克热量(卡路里)', required: false })
|
||||
caloriesPer100g?: number;
|
||||
|
||||
@ApiProperty({ description: '每100克蛋白质含量(克)', required: false })
|
||||
proteinPer100g?: number;
|
||||
|
||||
@ApiProperty({ description: '每100克碳水化合物含量(克)', required: false })
|
||||
carbohydratePer100g?: number;
|
||||
|
||||
@ApiProperty({ description: '每100克脂肪含量(克)', required: false })
|
||||
fatPer100g?: number;
|
||||
|
||||
@ApiProperty({ description: '每100克膳食纤维含量(克)', required: false })
|
||||
fiberPer100g?: number;
|
||||
|
||||
@ApiProperty({ description: '每100克糖分含量(克)', required: false })
|
||||
sugarPer100g?: number;
|
||||
|
||||
@ApiProperty({ description: '每100克钠含量(毫克)', required: false })
|
||||
sodiumPer100g?: number;
|
||||
|
||||
@ApiProperty({ description: '其他营养信息', required: false })
|
||||
additionalNutrition?: Record<string, any>;
|
||||
|
||||
@ApiProperty({ description: '是否常见食物' })
|
||||
isCommon: boolean;
|
||||
|
||||
@ApiProperty({ description: '食物图片URL', required: false })
|
||||
imageUrl?: string;
|
||||
|
||||
@ApiProperty({ description: '排序', required: false })
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export class FoodCategoryDto {
|
||||
@ApiProperty({ description: '分类键' })
|
||||
key: string;
|
||||
|
||||
@ApiProperty({ description: '分类名称' })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: '分类图标', required: false })
|
||||
icon?: string;
|
||||
|
||||
@ApiProperty({ description: '排序', required: false })
|
||||
sortOrder?: number;
|
||||
|
||||
@ApiProperty({ description: '是否系统分类' })
|
||||
isSystem: boolean;
|
||||
|
||||
@ApiProperty({ description: '该分类下的食物列表', type: [FoodItemDto] })
|
||||
foods: FoodItemDto[];
|
||||
}
|
||||
|
||||
export class FoodLibraryResponseDto {
|
||||
@ApiProperty({ description: '食物分类列表', type: [FoodCategoryDto] })
|
||||
categories: FoodCategoryDto[];
|
||||
}
|
||||
56
src/food-library/food-library.controller.ts
Normal file
56
src/food-library/food-library.controller.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Controller, Get, Query, Param, ParseIntPipe, NotFoundException } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiQuery, ApiParam } from '@nestjs/swagger';
|
||||
import { FoodLibraryService } from './food-library.service';
|
||||
import { FoodLibraryResponseDto, FoodItemDto } from './dto/food-library.dto';
|
||||
|
||||
@ApiTags('食物库')
|
||||
@Controller('food-library')
|
||||
export class FoodLibraryController {
|
||||
constructor(private readonly foodLibraryService: FoodLibraryService) { }
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取食物库列表' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '成功获取食物库列表',
|
||||
type: FoodLibraryResponseDto,
|
||||
})
|
||||
async getFoodLibrary(): Promise<FoodLibraryResponseDto> {
|
||||
return this.foodLibraryService.getFoodLibrary();
|
||||
}
|
||||
|
||||
@Get('search')
|
||||
@ApiOperation({ summary: '搜索食物' })
|
||||
@ApiQuery({ name: 'keyword', description: '搜索关键词', required: true })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '成功搜索食物',
|
||||
type: [FoodItemDto],
|
||||
})
|
||||
async searchFoods(@Query('keyword') keyword: string): Promise<FoodItemDto[]> {
|
||||
if (!keyword || keyword.trim().length === 0) {
|
||||
return [];
|
||||
}
|
||||
return this.foodLibraryService.searchFoods(keyword.trim());
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '根据ID获取食物详情' })
|
||||
@ApiParam({ name: 'id', description: '食物ID', type: 'number' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '成功获取食物详情',
|
||||
type: FoodItemDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '食物不存在',
|
||||
})
|
||||
async getFoodById(@Param('id', ParseIntPipe) id: number): Promise<FoodItemDto> {
|
||||
const food = await this.foodLibraryService.getFoodById(id);
|
||||
if (!food) {
|
||||
throw new NotFoundException('食物不存在');
|
||||
}
|
||||
return food;
|
||||
}
|
||||
}
|
||||
16
src/food-library/food-library.module.ts
Normal file
16
src/food-library/food-library.module.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SequelizeModule } from '@nestjs/sequelize';
|
||||
import { FoodLibraryController } from './food-library.controller';
|
||||
import { FoodLibraryService } from './food-library.service';
|
||||
import { FoodCategory } from './models/food-category.model';
|
||||
import { FoodLibrary } from './models/food-library.model';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
SequelizeModule.forFeature([FoodCategory, FoodLibrary]),
|
||||
],
|
||||
controllers: [FoodLibraryController],
|
||||
providers: [FoodLibraryService],
|
||||
exports: [FoodLibraryService],
|
||||
})
|
||||
export class FoodLibraryModule { }
|
||||
138
src/food-library/food-library.service.ts
Normal file
138
src/food-library/food-library.service.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/sequelize';
|
||||
import { Op } from 'sequelize';
|
||||
import { FoodCategory } from './models/food-category.model';
|
||||
import { FoodLibrary } from './models/food-library.model';
|
||||
import { FoodCategoryDto, FoodItemDto, FoodLibraryResponseDto } from './dto/food-library.dto';
|
||||
|
||||
@Injectable()
|
||||
export class FoodLibraryService {
|
||||
constructor(
|
||||
@InjectModel(FoodCategory)
|
||||
private readonly foodCategoryModel: typeof FoodCategory,
|
||||
@InjectModel(FoodLibrary)
|
||||
private readonly foodLibraryModel: typeof FoodLibrary,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* 获取食物库列表,按分类组织
|
||||
* 常见食物会被归类到"常见"分类中
|
||||
*/
|
||||
async getFoodLibrary(): Promise<FoodLibraryResponseDto> {
|
||||
// 获取所有分类,按排序顺序
|
||||
const categories = await this.foodCategoryModel.findAll({
|
||||
order: [['sortOrder', 'ASC']],
|
||||
});
|
||||
|
||||
const result: FoodCategoryDto[] = [];
|
||||
|
||||
for (const category of categories) {
|
||||
let foods: FoodLibrary[] = [];
|
||||
|
||||
if (category.key === 'common') {
|
||||
// 常见分类:获取所有标记为常见的食物
|
||||
foods = await this.foodLibraryModel.findAll({
|
||||
where: { isCommon: true },
|
||||
order: [['sortOrder', 'ASC'], ['name', 'ASC']],
|
||||
});
|
||||
} else {
|
||||
// 其他分类:获取该分类下的非常见食物
|
||||
foods = await this.foodLibraryModel.findAll({
|
||||
where: {
|
||||
categoryKey: category.key,
|
||||
isCommon: false
|
||||
},
|
||||
order: [['sortOrder', 'ASC'], ['name', 'ASC']],
|
||||
});
|
||||
}
|
||||
|
||||
const foodDtos: FoodItemDto[] = foods.map(food => ({
|
||||
id: food.id,
|
||||
name: food.name,
|
||||
description: food.description,
|
||||
caloriesPer100g: food.caloriesPer100g,
|
||||
proteinPer100g: food.proteinPer100g,
|
||||
carbohydratePer100g: food.carbohydratePer100g,
|
||||
fatPer100g: food.fatPer100g,
|
||||
fiberPer100g: food.fiberPer100g,
|
||||
sugarPer100g: food.sugarPer100g,
|
||||
sodiumPer100g: food.sodiumPer100g,
|
||||
additionalNutrition: food.additionalNutrition,
|
||||
isCommon: food.isCommon,
|
||||
imageUrl: food.imageUrl,
|
||||
sortOrder: food.sortOrder,
|
||||
}));
|
||||
|
||||
result.push({
|
||||
key: category.key,
|
||||
name: category.name,
|
||||
icon: category.icon,
|
||||
sortOrder: category.sortOrder,
|
||||
isSystem: category.isSystem,
|
||||
foods: foodDtos,
|
||||
});
|
||||
}
|
||||
|
||||
return { categories: result };
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关键词搜索食物
|
||||
*/
|
||||
async searchFoods(keyword: string): Promise<FoodItemDto[]> {
|
||||
const foods = await this.foodLibraryModel.findAll({
|
||||
where: {
|
||||
name: {
|
||||
[Op.like]: `%${keyword}%`
|
||||
}
|
||||
},
|
||||
order: [['isCommon', 'DESC'], ['name', 'ASC']],
|
||||
limit: 50, // 限制搜索结果数量
|
||||
});
|
||||
|
||||
return foods.map(food => ({
|
||||
id: food.id,
|
||||
name: food.name,
|
||||
description: food.description,
|
||||
caloriesPer100g: food.caloriesPer100g,
|
||||
proteinPer100g: food.proteinPer100g,
|
||||
carbohydratePer100g: food.carbohydratePer100g,
|
||||
fatPer100g: food.fatPer100g,
|
||||
fiberPer100g: food.fiberPer100g,
|
||||
sugarPer100g: food.sugarPer100g,
|
||||
sodiumPer100g: food.sodiumPer100g,
|
||||
additionalNutrition: food.additionalNutrition,
|
||||
isCommon: food.isCommon,
|
||||
imageUrl: food.imageUrl,
|
||||
sortOrder: food.sortOrder,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取食物详情
|
||||
*/
|
||||
async getFoodById(id: number): Promise<FoodItemDto | null> {
|
||||
const food = await this.foodLibraryModel.findByPk(id);
|
||||
|
||||
if (!food) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: food.id,
|
||||
name: food.name,
|
||||
description: food.description,
|
||||
caloriesPer100g: food.caloriesPer100g,
|
||||
proteinPer100g: food.proteinPer100g,
|
||||
carbohydratePer100g: food.carbohydratePer100g,
|
||||
fatPer100g: food.fatPer100g,
|
||||
fiberPer100g: food.fiberPer100g,
|
||||
sugarPer100g: food.sugarPer100g,
|
||||
sodiumPer100g: food.sodiumPer100g,
|
||||
additionalNutrition: food.additionalNutrition,
|
||||
isCommon: food.isCommon,
|
||||
imageUrl: food.imageUrl,
|
||||
sortOrder: food.sortOrder,
|
||||
};
|
||||
}
|
||||
}
|
||||
54
src/food-library/models/food-category.model.ts
Normal file
54
src/food-library/models/food-category.model.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Column, DataType, HasMany, Model, Table } from 'sequelize-typescript';
|
||||
import { FoodLibrary } from './food-library.model';
|
||||
|
||||
@Table({
|
||||
tableName: 't_food_categories',
|
||||
underscored: true,
|
||||
})
|
||||
export class FoodCategory extends Model {
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
primaryKey: true,
|
||||
comment: '分类唯一键(英文/下划线)',
|
||||
})
|
||||
declare key: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
comment: '分类中文名称',
|
||||
})
|
||||
declare name: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: true,
|
||||
comment: '分类图标',
|
||||
})
|
||||
declare icon: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '排序(升序)',
|
||||
})
|
||||
declare sortOrder: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '是否系统分类(true:系统,false:用户自定义)',
|
||||
})
|
||||
declare isSystem: boolean;
|
||||
|
||||
@HasMany(() => FoodLibrary, { foreignKey: 'categoryKey', sourceKey: 'key' })
|
||||
declare foods: FoodLibrary[];
|
||||
|
||||
@Column({ type: DataType.DATE, defaultValue: DataType.NOW })
|
||||
declare createdAt: Date;
|
||||
|
||||
@Column({ type: DataType.DATE, defaultValue: DataType.NOW })
|
||||
declare updatedAt: Date;
|
||||
}
|
||||
126
src/food-library/models/food-library.model.ts
Normal file
126
src/food-library/models/food-library.model.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { BelongsTo, Column, DataType, ForeignKey, Model, Table } from 'sequelize-typescript';
|
||||
import { FoodCategory } from './food-category.model';
|
||||
|
||||
@Table({
|
||||
tableName: 't_food_library',
|
||||
underscored: true,
|
||||
})
|
||||
export class FoodLibrary extends Model {
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '主键ID',
|
||||
})
|
||||
declare id: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
comment: '食物名称',
|
||||
})
|
||||
declare name: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: true,
|
||||
comment: '食物描述',
|
||||
})
|
||||
declare description: string;
|
||||
|
||||
@ForeignKey(() => FoodCategory)
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
comment: '分类键',
|
||||
})
|
||||
declare categoryKey: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.FLOAT,
|
||||
allowNull: true,
|
||||
comment: '每100克热量(卡路里)',
|
||||
})
|
||||
declare caloriesPer100g: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.FLOAT,
|
||||
allowNull: true,
|
||||
comment: '每100克蛋白质含量(克)',
|
||||
})
|
||||
declare proteinPer100g: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.FLOAT,
|
||||
allowNull: true,
|
||||
comment: '每100克碳水化合物含量(克)',
|
||||
})
|
||||
declare carbohydratePer100g: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.FLOAT,
|
||||
allowNull: true,
|
||||
comment: '每100克脂肪含量(克)',
|
||||
})
|
||||
declare fatPer100g: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.FLOAT,
|
||||
allowNull: true,
|
||||
comment: '每100克膳食纤维含量(克)',
|
||||
})
|
||||
declare fiberPer100g: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.FLOAT,
|
||||
allowNull: true,
|
||||
comment: '每100克糖分含量(克)',
|
||||
})
|
||||
declare sugarPer100g: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.FLOAT,
|
||||
allowNull: true,
|
||||
comment: '每100克钠含量(毫克)',
|
||||
})
|
||||
declare sodiumPer100g: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.JSON,
|
||||
allowNull: true,
|
||||
comment: '其他营养信息(维生素、矿物质等)',
|
||||
})
|
||||
declare additionalNutrition: Record<string, any>;
|
||||
|
||||
@Column({
|
||||
type: DataType.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
comment: '是否常见食物(true:常见,false:不常见)',
|
||||
})
|
||||
declare isCommon: boolean;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: true,
|
||||
comment: '食物图片URL',
|
||||
})
|
||||
declare imageUrl: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '排序(分类内)',
|
||||
})
|
||||
declare sortOrder: number;
|
||||
|
||||
@BelongsTo(() => FoodCategory, { foreignKey: 'categoryKey', targetKey: 'key' })
|
||||
declare category: FoodCategory;
|
||||
|
||||
@Column({ type: DataType.DATE, defaultValue: DataType.NOW })
|
||||
declare createdAt: Date;
|
||||
|
||||
@Column({ type: DataType.DATE, defaultValue: DataType.NOW })
|
||||
declare updatedAt: Date;
|
||||
}
|
||||
Reference in New Issue
Block a user