feat: 优化AI教练聊天逻辑,增加用户聊天次数检查和响应内容

- 在AI教练控制器中添加用户聊天次数检查,若次数用完则返回相应提示信息。
- 更新AI聊天响应DTO,新增用户剩余聊天次数和AI回复文本字段,提升用户体验。
- 修改用户服务,支持初始体重和目标体重字段的更新,增强用户资料的完整性。
This commit is contained in:
richarjiang
2025-08-27 14:22:25 +08:00
parent 79aa300aa1
commit c3961150ab
7 changed files with 69 additions and 15 deletions

View File

@@ -28,15 +28,11 @@ export class AiCoachController {
@Res({ passthrough: false }) res: Response,
): Promise<StreamableFile | AiChatResponseDto | void> {
const userId = user.sub;
this.logger.log(`chat: ${userId} chat body ${JSON.stringify(body, null, 2)}`);
const stream = body.stream !== false; // 默认流式
const userContent = body.messages?.[body.messages.length - 1]?.content || '';
// 判断用户是否有聊天次数
const usageCount = await this.usersService.getUserUsageCount(userId);
if (usageCount <= 0) {
this.logger.error(`chat: ${userId} has no usage count`);
throw new HttpException('用户没有聊天次数', HttpStatus.FORBIDDEN);
}
// 创建或沿用会话ID并保存用户消息
const { conversationId } = await this.aiCoachService.createOrAppendMessages({
@@ -45,8 +41,20 @@ export class AiCoachController {
userContent,
});
// 体重和饮食指令处理现在已经集成到 streamChat 方法中
// 通过 # 字符开头的指令系统进行统一处理
// 判断用户是否有聊天次数
const usageCount = await this.usersService.getUserUsageCount(userId);
if (usageCount <= 0) {
this.logger.warn(`chat: ${userId} has no usage count`);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.send({
conversationId,
text: '聊天次数用完了,明天再来吧~',
});
return
}
const result = await this.aiCoachService.streamChat({
userId,
@@ -59,6 +67,9 @@ export class AiCoachController {
await this.usersService.deductUserUsageCount(userId);
// 普通流式/非流式响应
const readable = result as any;
// 检查是否返回结构化数据(如确认选项)
// 结构化数据必须使用非流式模式返回
if (typeof result === 'object' && 'type' in result) {
@@ -70,8 +81,7 @@ export class AiCoachController {
return;
}
// 普通流式/非流式响应
const readable = result as any;
if (!stream) {
// 非流式:聚合后一次性返回文本

View File

@@ -674,7 +674,7 @@ export class AiCoachService {
const cleanText = restText.trim();
// 识别体重记录指令
if (/^记体重$|^体重$|^称重$/.test(commandPart)) {
if (/^记体重$/.test(commandPart)) {
return {
isCommand: true,
command: 'weight',
@@ -684,7 +684,7 @@ export class AiCoachService {
}
// 识别饮食记录指令
if (/^记饮食$|^饮食$|^记录饮食$/.test(commandPart)) {
if (/^记饮食$/.test(commandPart)) {
return {
isCommand: true,
command: 'diet',

View File

@@ -98,6 +98,14 @@ export class AiChatResponseDto {
@ApiProperty({ type: AiResponseDataDto, description: '响应数据(非流式时返回)', required: false })
@IsOptional()
data?: AiResponseDataDto;
@ApiProperty({ description: '用户剩余的AI聊天次数', required: false })
@IsOptional()
usageCount?: number;
@ApiProperty({ description: 'AI回复的文本内容非流式时返回', required: false })
@IsOptional()
text?: string;
}
// 营养分析相关的DTO

View File

@@ -48,6 +48,14 @@ export class UpdateUserDto {
@ApiProperty({ description: '体重(公斤)', example: 55.5 })
weight?: number;
@IsOptional()
@ApiProperty({ description: '初始体重(公斤)', example: 60.0 })
initialWeight?: number;
@IsOptional()
@ApiProperty({ description: '目标体重(公斤)', example: 50.0 })
targetWeight?: number;
@IsOptional()
@ApiProperty({ description: '身高(厘米)', example: 168 })
height?: number;

View File

@@ -25,7 +25,7 @@ export interface UserWithPurchaseStatus {
maxUsageCount: number;
favoriteTopicCount: number;
isVip: boolean;
profile?: Pick<UserProfile, 'dailyStepsGoal' | 'dailyCaloriesGoal' | 'pilatesPurposes' | 'weight' | 'height' | 'activityLevel'>;
profile?: Pick<UserProfile, 'dailyStepsGoal' | 'dailyCaloriesGoal' | 'pilatesPurposes' | 'weight' | 'initialWeight' | 'targetWeight' | 'height' | 'activityLevel'>;
}
export class UserResponseDto implements BaseResponseDto<UserWithPurchaseStatus> {

View File

@@ -54,6 +54,20 @@ export class UserProfile extends Model {
})
declare weight: number | null;
@Column({
type: DataType.FLOAT,
allowNull: true,
comment: '初始体重(公斤)',
})
declare initialWeight: number | null;
@Column({
type: DataType.FLOAT,
allowNull: true,
comment: '目标体重(公斤)',
})
declare targetWeight: number | null;
@Column({
type: DataType.FLOAT,
allowNull: true,

View File

@@ -108,6 +108,8 @@ export class UsersService {
dailyCaloriesGoal: profile?.dailyCaloriesGoal,
pilatesPurposes: profile?.pilatesPurposes,
weight: profile?.weight,
initialWeight: profile?.initialWeight,
targetWeight: profile?.targetWeight,
height: profile?.height,
activityLevel: profile?.activityLevel,
}
@@ -179,7 +181,7 @@ export class UsersService {
// 更新用户昵称、头像
async updateUser(updateUserDto: UpdateUserDto): Promise<UpdateUserResponseDto> {
const { userId, name, avatar, gender, birthDate, dailyStepsGoal, dailyCaloriesGoal, pilatesPurposes, weight, height, activityLevel } = updateUserDto;
const { userId, name, avatar, gender, birthDate, dailyStepsGoal, dailyCaloriesGoal, pilatesPurposes, weight, initialWeight, targetWeight, height, activityLevel } = updateUserDto;
this.logger.log(`updateUser: ${JSON.stringify(updateUserDto, null, 2)}`);
@@ -215,7 +217,7 @@ export class UsersService {
await user.save();
// 更新或创建扩展信息
if (dailyStepsGoal !== undefined || dailyCaloriesGoal !== undefined || pilatesPurposes !== undefined || weight !== undefined || height !== undefined || activityLevel !== undefined) {
if (dailyStepsGoal !== undefined || dailyCaloriesGoal !== undefined || pilatesPurposes !== undefined || weight !== undefined || initialWeight !== undefined || targetWeight !== undefined || height !== undefined || activityLevel !== undefined) {
const [profile] = await this.userProfileModel.findOrCreate({
where: { userId },
defaults: { userId },
@@ -232,6 +234,14 @@ export class UsersService {
}
profileChanges.weight = weight;
}
if (initialWeight !== undefined) {
profile.initialWeight = initialWeight;
profileChanges.initialWeight = initialWeight;
}
if (targetWeight !== undefined) {
profile.targetWeight = targetWeight;
profileChanges.targetWeight = targetWeight;
}
if (height !== undefined) {
profile.height = height;
profileChanges.height = height;
@@ -650,6 +660,8 @@ export class UsersService {
dailyCaloriesGoal: profileForLogin.dailyCaloriesGoal,
pilatesPurposes: profileForLogin.pilatesPurposes,
weight: profileForLogin.weight,
initialWeight: profileForLogin.initialWeight,
targetWeight: profileForLogin.targetWeight,
height: profileForLogin.height,
activityLevel: profileForLogin.activityLevel,
} : undefined,
@@ -862,6 +874,8 @@ export class UsersService {
dailyCaloriesGoal: profileForGuest.dailyCaloriesGoal,
pilatesPurposes: profileForGuest.pilatesPurposes,
weight: profileForGuest.weight,
initialWeight: profileForGuest.initialWeight,
targetWeight: profileForGuest.targetWeight,
height: profileForGuest.height,
activityLevel: profileForGuest.activityLevel,
} : undefined,