perf
This commit is contained in:
125
CLAUDE.md
Normal file
125
CLAUDE.md
Normal file
@@ -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
|
||||||
@@ -15,109 +15,9 @@ export class FoodLibraryService {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取食物库列表,按分类组织
|
* 将食物模型转换为DTO
|
||||||
* 常见食物会被归类到"常见"分类中
|
|
||||||
*/
|
*/
|
||||||
async getFoodLibrary(): Promise<FoodLibraryResponseDto> {
|
private mapFoodToDto(food: FoodLibrary): FoodItemDto {
|
||||||
// 获取所有分类,按排序顺序
|
|
||||||
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 {
|
return {
|
||||||
id: food.id,
|
id: food.id,
|
||||||
name: food.name,
|
name: food.name,
|
||||||
@@ -135,4 +35,112 @@ export class FoodLibraryService {
|
|||||||
sortOrder: food.sortOrder,
|
sortOrder: food.sortOrder,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取食物库列表,按分类组织
|
||||||
|
* 常见食物会被归类到"常见"分类中
|
||||||
|
*/
|
||||||
|
async getFoodLibrary(): Promise<FoodLibraryResponseDto> {
|
||||||
|
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<string, FoodLibrary[]>();
|
||||||
|
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<FoodItemDto[]> {
|
||||||
|
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<FoodItemDto | null> {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user