From 8a69f4f1af05234d4318421b56ecd735750da3d8 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Fri, 29 Aug 2025 09:44:35 +0800 Subject: [PATCH] perf --- CLAUDE.md | 125 +++++++++++++ src/food-library/food-library.service.ts | 212 ++++++++++++----------- 2 files changed, 235 insertions(+), 102 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4b222f6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,125 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Core Development +- `yarn start:dev` - Start development server with hot reload +- `yarn start:debug` - Start development server with debugging enabled +- `yarn build` - Build production bundle +- `yarn start:prod` - Start production server from built files + +### Testing +- `yarn test` - Run unit tests +- `yarn test:watch` - Run tests in watch mode +- `yarn test:cov` - Run tests with coverage report +- `yarn test:e2e` - Run end-to-end tests +- `yarn test:debug` - Run tests with debugging + +### Code Quality +- `yarn lint` - Run ESLint with auto-fix +- `yarn format` - Format code with Prettier + +### Production Deployment +- `yarn pm2:start` - Start with PM2 in production mode +- `yarn pm2:start:dev` - Start with PM2 in development mode +- `yarn pm2:status` - Check PM2 process status +- `yarn pm2:logs` - View PM2 logs +- `yarn pm2:restart` - Restart PM2 processes + +### Deployment Scripts +- `./deploy-optimized.sh` - Recommended deployment (builds on server) +- `./deploy.sh` - Full deployment with options (`--help` for usage) +- `./deploy-simple.sh` - Basic deployment script + +## Architecture Overview + +### Core Framework +This is a **NestJS-based fitness and health tracking API** using TypeScript, MySQL with Sequelize ORM, and JWT authentication. The architecture follows NestJS conventions with modular design. + +### Module Structure +The application is organized into domain-specific modules: + +**Health & Fitness Core:** +- `users/` - User management, authentication (Apple Sign-In, guest), payments, subscriptions +- `diet-records/` - Food logging and nutrition tracking integration +- `food-library/` - Food database with categories and nutritional information +- `exercises/` - Exercise library with categories and instructions +- `training-plans/` - Workout plan management and scheduling +- `workouts/` - Workout session tracking and history +- `goals/` - Goal setting with task management system +- `mood-checkins/` - Mental health and mood tracking + +**AI & Intelligence:** +- `ai-coach/` - OpenAI-powered fitness coaching with diet analysis +- `recommendations/` - Personalized content recommendation engine + +**Content & Social:** +- `articles/` - Health and fitness article management +- `checkins/` - User check-in and progress tracking +- `activity-logs/` - User activity and engagement tracking + +### Key Architectural Patterns + +**Database Layer:** +- Sequelize ORM with MySQL +- Models use `@Table` and `@Column` decorators from `sequelize-typescript` +- Database configuration in `database.module.ts` with async factory pattern +- Auto-loading models with `autoLoadModels: true` + +**Authentication & Security:** +- JWT-based authentication with refresh tokens +- Apple Sign-In integration via `apple-auth.service.ts` +- AES-256-GCM encryption service for sensitive data +- Custom `@CurrentUser()` decorator and `JwtAuthGuard` + +**API Design:** +- Controllers use Swagger decorators for API documentation +- DTOs for request/response validation using `class-validator` +- Base DTO pattern in `base.dto.ts` for consistent responses +- Encryption support for sensitive endpoints + +**Logging & Monitoring:** +- Winston logging with daily rotation +- Separate log files: app, error, debug, exceptions, rejections +- PM2 clustering with memory limits (1GB) +- Structured logging with context and metadata + +### Configuration Management +- Environment-based configuration using `@nestjs/config` +- Global configuration module +- Environment variables for database, JWT, Apple auth, encryption keys +- Production/development environment separation + +### External Integrations +- **OpenAI**: AI coaching and diet analysis +- **Apple**: Sign-in and purchase verification +- **RevenueCat**: Subscription management webhooks +- **Tencent Cloud COS**: File storage service + +### Testing Strategy +- Unit tests with Jest (`*.spec.ts` files) +- E2E tests in `test/` directory +- Test configuration in `package.json` jest section +- Encryption service has comprehensive test coverage + +### Development Patterns +- Each module follows NestJS structure: controller → service → model +- Services are injected using `@Injectable()` decorator +- Models are Sequelize entities with TypeScript decorators +- DTOs handle validation and transformation +- Guards handle authentication and authorization + +## Database Schema Patterns +SQL scripts in `sql-scripts/` directory contain table creation scripts organized by feature: +- `*-tables-create.sql` for table definitions +- `*-sample-data.sql` for seed data +- Migration scripts for database upgrades + +## Production Environment +- **Server**: 129.204.155.94 +- **Deployment Path**: `/usr/local/web/pilates-server` +- **Ports**: 3002 (production), 3001 (development) +- **Process Management**: PM2 with cluster mode +- **Logging**: Daily rotated logs in `logs/` directory \ No newline at end of file diff --git a/src/food-library/food-library.service.ts b/src/food-library/food-library.service.ts index ab1f971..f339ff9 100644 --- a/src/food-library/food-library.service.ts +++ b/src/food-library/food-library.service.ts @@ -15,109 +15,9 @@ export class FoodLibraryService { ) { } /** - * 获取食物库列表,按分类组织 - * 常见食物会被归类到"常见"分类中 + * 将食物模型转换为DTO */ - async getFoodLibrary(): Promise { - // 获取所有分类,按排序顺序 - 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 { - 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 { - const food = await this.foodLibraryModel.findByPk(id); - - if (!food) { - return null; - } - + private mapFoodToDto(food: FoodLibrary): FoodItemDto { return { id: food.id, name: food.name, @@ -135,4 +35,112 @@ export class FoodLibraryService { sortOrder: food.sortOrder, }; } + + /** + * 获取食物库列表,按分类组织 + * 常见食物会被归类到"常见"分类中 + */ + async getFoodLibrary(): Promise { + try { + // 一次性获取所有分类和食物数据,避免N+1查询 + const [categories, allFoods, commonFoods] = await Promise.all([ + // 获取所有分类 + this.foodCategoryModel.findAll({ + order: [['sortOrder', 'ASC']], + }), + // 获取所有非常见食物 + this.foodLibraryModel.findAll({ + where: { isCommon: false }, + order: [['sortOrder', 'ASC'], ['name', 'ASC']], + }), + // 获取所有常见食物 + this.foodLibraryModel.findAll({ + where: { isCommon: true }, + order: [['sortOrder', 'ASC'], ['name', 'ASC']], + }), + ]); + + // 将食物按分类分组 + const foodsByCategory = new Map(); + allFoods.forEach(food => { + const categoryKey = food.categoryKey; + if (!foodsByCategory.has(categoryKey)) { + foodsByCategory.set(categoryKey, []); + } + foodsByCategory.get(categoryKey)!.push(food); + }); + + // 构建结果 + const result: FoodCategoryDto[] = categories.map(category => { + let foods: FoodLibrary[] = []; + + if (category.key === 'common') { + // 常见分类:使用常见食物 + foods = commonFoods; + } else { + // 其他分类:使用该分类下的非常见食物 + foods = foodsByCategory.get(category.key) || []; + } + + return { + key: category.key, + name: category.name, + icon: category.icon, + sortOrder: category.sortOrder, + isSystem: category.isSystem, + foods: foods.map(food => this.mapFoodToDto(food)), + }; + }); + + return { categories: result }; + } catch (error) { + throw new Error(`Failed to get food library: ${error.message}`); + } + } + + /** + * 根据关键词搜索食物 + */ + async searchFoods(keyword: string): Promise { + try { + if (!keyword || keyword.trim().length === 0) { + return []; + } + + const foods = await this.foodLibraryModel.findAll({ + where: { + name: { + [Op.like]: `%${keyword.trim()}%` + } + }, + order: [['isCommon', 'DESC'], ['name', 'ASC']], + limit: 50, + }); + + return foods.map(food => this.mapFoodToDto(food)); + } catch (error) { + throw new Error(`Failed to search foods: ${error.message}`); + } + } + + /** + * 根据ID获取食物详情 + */ + async getFoodById(id: number): Promise { + try { + if (!id || id <= 0) { + return null; + } + + const food = await this.foodLibraryModel.findByPk(id); + + if (!food) { + return null; + } + + return this.mapFoodToDto(food); + } catch (error) { + throw new Error(`Failed to get food by id: ${error.message}`); + } + } } \ No newline at end of file