feat: 优化AI教练聊天逻辑,增加用户聊天次数检查和响应内容
- 在AI教练控制器中添加用户聊天次数检查,若次数用完则返回相应提示信息。 - 更新AI聊天响应DTO,新增用户剩余聊天次数和AI回复文本字段,提升用户体验。 - 修改用户服务,支持初始体重和目标体重字段的更新,增强用户资料的完整性。
This commit is contained in:
@@ -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) {
|
||||
// 非流式:聚合后一次性返回文本
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user