feat: 新增食物库模块,含模型、服务、API及初始化数据

This commit is contained in:
richarjiang
2025-08-29 09:06:18 +08:00
parent a1c21d8a23
commit 74faebd73d
10 changed files with 721 additions and 0 deletions

View 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);

View 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`);

View File

@@ -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],

View 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克
- 添加按钮(+
## 扩展功能
未来可以扩展的功能:
- 用户自定义食物
- 收藏食物功能
- 食物图片上传
- 营养成分详细分析
- 食物推荐算法

View 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[];
}

View 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;
}
}

View 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 { }

View 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,
};
}
}

View 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;
}

View 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;
}