feat: 优化AI教练聊天逻辑,增加用户聊天次数检查和响应内容
- 在AI教练控制器中添加用户聊天次数检查,若次数用完则返回相应提示信息。 - 更新AI聊天响应DTO,新增用户剩余聊天次数和AI回复文本字段,提升用户体验。 - 修改用户服务,支持初始体重和目标体重字段的更新,增强用户资料的完整性。
This commit is contained in:
@@ -28,15 +28,11 @@ export class AiCoachController {
|
|||||||
@Res({ passthrough: false }) res: Response,
|
@Res({ passthrough: false }) res: Response,
|
||||||
): Promise<StreamableFile | AiChatResponseDto | void> {
|
): Promise<StreamableFile | AiChatResponseDto | void> {
|
||||||
const userId = user.sub;
|
const userId = user.sub;
|
||||||
|
this.logger.log(`chat: ${userId} chat body ${JSON.stringify(body, null, 2)}`);
|
||||||
const stream = body.stream !== false; // 默认流式
|
const stream = body.stream !== false; // 默认流式
|
||||||
const userContent = body.messages?.[body.messages.length - 1]?.content || '';
|
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,并保存用户消息
|
// 创建或沿用会话ID,并保存用户消息
|
||||||
const { conversationId } = await this.aiCoachService.createOrAppendMessages({
|
const { conversationId } = await this.aiCoachService.createOrAppendMessages({
|
||||||
@@ -45,8 +41,20 @@ export class AiCoachController {
|
|||||||
userContent,
|
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({
|
const result = await this.aiCoachService.streamChat({
|
||||||
userId,
|
userId,
|
||||||
@@ -59,6 +67,9 @@ export class AiCoachController {
|
|||||||
|
|
||||||
await this.usersService.deductUserUsageCount(userId);
|
await this.usersService.deductUserUsageCount(userId);
|
||||||
|
|
||||||
|
// 普通流式/非流式响应
|
||||||
|
const readable = result as any;
|
||||||
|
|
||||||
// 检查是否返回结构化数据(如确认选项)
|
// 检查是否返回结构化数据(如确认选项)
|
||||||
// 结构化数据必须使用非流式模式返回
|
// 结构化数据必须使用非流式模式返回
|
||||||
if (typeof result === 'object' && 'type' in result) {
|
if (typeof result === 'object' && 'type' in result) {
|
||||||
@@ -70,8 +81,7 @@ export class AiCoachController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 普通流式/非流式响应
|
|
||||||
const readable = result as any;
|
|
||||||
|
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
// 非流式:聚合后一次性返回文本
|
// 非流式:聚合后一次性返回文本
|
||||||
|
|||||||
@@ -674,7 +674,7 @@ export class AiCoachService {
|
|||||||
const cleanText = restText.trim();
|
const cleanText = restText.trim();
|
||||||
|
|
||||||
// 识别体重记录指令
|
// 识别体重记录指令
|
||||||
if (/^记体重$|^体重$|^称重$/.test(commandPart)) {
|
if (/^记体重$/.test(commandPart)) {
|
||||||
return {
|
return {
|
||||||
isCommand: true,
|
isCommand: true,
|
||||||
command: 'weight',
|
command: 'weight',
|
||||||
@@ -684,7 +684,7 @@ export class AiCoachService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 识别饮食记录指令
|
// 识别饮食记录指令
|
||||||
if (/^记饮食$|^饮食$|^记录饮食$/.test(commandPart)) {
|
if (/^记饮食$/.test(commandPart)) {
|
||||||
return {
|
return {
|
||||||
isCommand: true,
|
isCommand: true,
|
||||||
command: 'diet',
|
command: 'diet',
|
||||||
|
|||||||
@@ -98,6 +98,14 @@ export class AiChatResponseDto {
|
|||||||
@ApiProperty({ type: AiResponseDataDto, description: '响应数据(非流式时返回)', required: false })
|
@ApiProperty({ type: AiResponseDataDto, description: '响应数据(非流式时返回)', required: false })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
data?: AiResponseDataDto;
|
data?: AiResponseDataDto;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户剩余的AI聊天次数', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
usageCount?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'AI回复的文本内容(非流式时返回)', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 营养分析相关的DTO
|
// 营养分析相关的DTO
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ export class UpdateUserDto {
|
|||||||
@ApiProperty({ description: '体重(公斤)', example: 55.5 })
|
@ApiProperty({ description: '体重(公斤)', example: 55.5 })
|
||||||
weight?: number;
|
weight?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({ description: '初始体重(公斤)', example: 60.0 })
|
||||||
|
initialWeight?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({ description: '目标体重(公斤)', example: 50.0 })
|
||||||
|
targetWeight?: number;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ApiProperty({ description: '身高(厘米)', example: 168 })
|
@ApiProperty({ description: '身高(厘米)', example: 168 })
|
||||||
height?: number;
|
height?: number;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export interface UserWithPurchaseStatus {
|
|||||||
maxUsageCount: number;
|
maxUsageCount: number;
|
||||||
favoriteTopicCount: number;
|
favoriteTopicCount: number;
|
||||||
isVip: boolean;
|
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> {
|
export class UserResponseDto implements BaseResponseDto<UserWithPurchaseStatus> {
|
||||||
|
|||||||
@@ -54,6 +54,20 @@ export class UserProfile extends Model {
|
|||||||
})
|
})
|
||||||
declare weight: number | null;
|
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({
|
@Column({
|
||||||
type: DataType.FLOAT,
|
type: DataType.FLOAT,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ export class UsersService {
|
|||||||
dailyCaloriesGoal: profile?.dailyCaloriesGoal,
|
dailyCaloriesGoal: profile?.dailyCaloriesGoal,
|
||||||
pilatesPurposes: profile?.pilatesPurposes,
|
pilatesPurposes: profile?.pilatesPurposes,
|
||||||
weight: profile?.weight,
|
weight: profile?.weight,
|
||||||
|
initialWeight: profile?.initialWeight,
|
||||||
|
targetWeight: profile?.targetWeight,
|
||||||
height: profile?.height,
|
height: profile?.height,
|
||||||
activityLevel: profile?.activityLevel,
|
activityLevel: profile?.activityLevel,
|
||||||
}
|
}
|
||||||
@@ -179,7 +181,7 @@ export class UsersService {
|
|||||||
|
|
||||||
// 更新用户昵称、头像
|
// 更新用户昵称、头像
|
||||||
async updateUser(updateUserDto: UpdateUserDto): Promise<UpdateUserResponseDto> {
|
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)}`);
|
this.logger.log(`updateUser: ${JSON.stringify(updateUserDto, null, 2)}`);
|
||||||
|
|
||||||
@@ -215,7 +217,7 @@ export class UsersService {
|
|||||||
await user.save();
|
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({
|
const [profile] = await this.userProfileModel.findOrCreate({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
defaults: { userId },
|
defaults: { userId },
|
||||||
@@ -232,6 +234,14 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
profileChanges.weight = weight;
|
profileChanges.weight = weight;
|
||||||
}
|
}
|
||||||
|
if (initialWeight !== undefined) {
|
||||||
|
profile.initialWeight = initialWeight;
|
||||||
|
profileChanges.initialWeight = initialWeight;
|
||||||
|
}
|
||||||
|
if (targetWeight !== undefined) {
|
||||||
|
profile.targetWeight = targetWeight;
|
||||||
|
profileChanges.targetWeight = targetWeight;
|
||||||
|
}
|
||||||
if (height !== undefined) {
|
if (height !== undefined) {
|
||||||
profile.height = height;
|
profile.height = height;
|
||||||
profileChanges.height = height;
|
profileChanges.height = height;
|
||||||
@@ -650,6 +660,8 @@ export class UsersService {
|
|||||||
dailyCaloriesGoal: profileForLogin.dailyCaloriesGoal,
|
dailyCaloriesGoal: profileForLogin.dailyCaloriesGoal,
|
||||||
pilatesPurposes: profileForLogin.pilatesPurposes,
|
pilatesPurposes: profileForLogin.pilatesPurposes,
|
||||||
weight: profileForLogin.weight,
|
weight: profileForLogin.weight,
|
||||||
|
initialWeight: profileForLogin.initialWeight,
|
||||||
|
targetWeight: profileForLogin.targetWeight,
|
||||||
height: profileForLogin.height,
|
height: profileForLogin.height,
|
||||||
activityLevel: profileForLogin.activityLevel,
|
activityLevel: profileForLogin.activityLevel,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
@@ -862,6 +874,8 @@ export class UsersService {
|
|||||||
dailyCaloriesGoal: profileForGuest.dailyCaloriesGoal,
|
dailyCaloriesGoal: profileForGuest.dailyCaloriesGoal,
|
||||||
pilatesPurposes: profileForGuest.pilatesPurposes,
|
pilatesPurposes: profileForGuest.pilatesPurposes,
|
||||||
weight: profileForGuest.weight,
|
weight: profileForGuest.weight,
|
||||||
|
initialWeight: profileForGuest.initialWeight,
|
||||||
|
targetWeight: profileForGuest.targetWeight,
|
||||||
height: profileForGuest.height,
|
height: profileForGuest.height,
|
||||||
activityLevel: profileForGuest.activityLevel,
|
activityLevel: profileForGuest.activityLevel,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user