新增普拉提训练系统的数据库结构和数据导入功能
- 创建普拉提分类和动作数据的SQL导入脚本,支持垫上普拉提和器械普拉提的分类管理 - 实现数据库结构迁移脚本,添加新字段以支持普拉提类型和器械名称 - 更新数据库升级总结文档,详细说明数据库结构变更和数据导入步骤 - 创建训练会话相关表,支持每日训练实例功能 - 引入训练会话管理模块,整合训练计划与实际训练会话的关系
This commit is contained in:
@@ -24,31 +24,56 @@ export class AiCoachController {
|
||||
): Promise<StreamableFile | AiChatResponseDto | void> {
|
||||
const userId = user.sub;
|
||||
const stream = body.stream !== false; // 默认流式
|
||||
const userContent = body.messages?.[body.messages.length - 1]?.content || '';
|
||||
|
||||
// 创建或沿用会话ID,并保存用户消息
|
||||
const { conversationId } = await this.aiCoachService.createOrAppendMessages({
|
||||
userId,
|
||||
conversationId: body.conversationId,
|
||||
userContent: body.messages?.[body.messages.length - 1]?.content || '',
|
||||
userContent,
|
||||
});
|
||||
|
||||
// 智能体重识别:若疑似“记体重”且传入图片,则优先识别并更新体重
|
||||
let weightInfo: { weightKg?: number } = {};
|
||||
let weightInfo: { weightKg?: number; systemNotice?: string } = {};
|
||||
|
||||
// 体重识别逻辑优化:
|
||||
// 1. 如果有图片URL,使用原有的图片识别逻辑
|
||||
// 2. 如果没有图片URL,但文本中包含体重信息,使用新的文本识别逻辑
|
||||
try {
|
||||
weightInfo = await this.aiCoachService.maybeExtractAndUpdateWeight(
|
||||
userId,
|
||||
body.imageUrl,
|
||||
body.messages?.[body.messages.length - 1]?.content,
|
||||
);
|
||||
} catch { }
|
||||
if (body.imageUrl) {
|
||||
// 原有逻辑:从图片识别体重
|
||||
const imageWeightInfo = await this.aiCoachService.maybeExtractAndUpdateWeight(
|
||||
userId,
|
||||
body.imageUrl,
|
||||
userContent,
|
||||
);
|
||||
if (imageWeightInfo.weightKg) {
|
||||
weightInfo = {
|
||||
weightKg: imageWeightInfo.weightKg,
|
||||
systemNotice: `系统提示:已从图片识别体重为${imageWeightInfo.weightKg}kg,并已为你更新到个人资料。`
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// 新逻辑:从文本识别体重,并获取历史对比信息
|
||||
const textWeightInfo = await this.aiCoachService.processWeightFromText(userId, userContent);
|
||||
if (textWeightInfo.weightKg && textWeightInfo.systemNotice) {
|
||||
weightInfo = {
|
||||
weightKg: textWeightInfo.weightKg,
|
||||
systemNotice: textWeightInfo.systemNotice
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 体重识别失败不影响正常对话
|
||||
console.error('体重识别失败:', error);
|
||||
}
|
||||
|
||||
if (!stream) {
|
||||
// 非流式:聚合后一次性返回文本
|
||||
const readable = await this.aiCoachService.streamChat({
|
||||
userId,
|
||||
conversationId,
|
||||
userContent: body.messages?.[body.messages.length - 1]?.content || '',
|
||||
systemNotice: weightInfo.weightKg ? `系统提示:已从图片识别体重为${weightInfo.weightKg}kg,并已为你更新到个人资料。` : undefined,
|
||||
userContent,
|
||||
systemNotice: weightInfo.systemNotice,
|
||||
});
|
||||
let text = '';
|
||||
for await (const chunk of readable) {
|
||||
@@ -67,13 +92,11 @@ export class AiCoachController {
|
||||
const readable = await this.aiCoachService.streamChat({
|
||||
userId,
|
||||
conversationId,
|
||||
userContent: body.messages?.[body.messages.length - 1]?.content || '',
|
||||
systemNotice: weightInfo.weightKg ? `系统提示:已从图片识别体重为${weightInfo.weightKg}kg,并已为你更新到个人资料。` : undefined,
|
||||
userContent,
|
||||
systemNotice: weightInfo.systemNotice,
|
||||
});
|
||||
|
||||
readable.on('data', (chunk) => {
|
||||
// 流水首段可提示体重已更新
|
||||
// 简化处理:服务端不额外注入推送段,直接靠 systemNotice
|
||||
res.write(chunk);
|
||||
});
|
||||
readable.on('end', () => {
|
||||
|
||||
@@ -306,6 +306,168 @@ export class AiCoachService {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从用户文本中识别体重信息
|
||||
* 支持多种格式:65kg、65公斤、65.5kg、体重65等
|
||||
*/
|
||||
private extractWeightFromText(text: string | undefined): number | null {
|
||||
if (!text) return null;
|
||||
|
||||
const t = text.toLowerCase();
|
||||
|
||||
// 匹配各种体重格式的正则表达式
|
||||
const patterns = [
|
||||
/(?:体重|称重|秤|重量|weight).*?(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)/i,
|
||||
/(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)/i,
|
||||
/(?:体重|称重|秤|重量|weight).*?(\d+(?:\.\d+)?)/i,
|
||||
/我(?:现在|今天)?(?:体重|重量|称重)?(?:是|为|有)?(\d+(?:\.\d+)?)/i,
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = t.match(pattern);
|
||||
if (match) {
|
||||
const weight = parseFloat(match[1]);
|
||||
// 合理的体重范围检查 (20-400kg)
|
||||
if (weight >= 20 && weight <= 400) {
|
||||
return weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户体重历史记录
|
||||
*/
|
||||
async getUserWeightHistory(userId: string, limit: number = 10): Promise<{
|
||||
currentWeight?: number;
|
||||
history: Array<{ weight: number; source: string; createdAt: Date }>;
|
||||
}> {
|
||||
try {
|
||||
// 获取当前体重
|
||||
const profile = await UserProfile.findOne({ where: { userId } });
|
||||
const currentWeight = profile?.weight;
|
||||
|
||||
// 获取体重历史
|
||||
const history = await this.usersService.getWeightHistory(userId, { limit });
|
||||
|
||||
return {
|
||||
currentWeight: currentWeight || undefined,
|
||||
history
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`获取用户体重历史失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return { history: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建体重更新的系统提示信息
|
||||
*/
|
||||
private buildWeightUpdateSystemNotice(
|
||||
newWeight: number,
|
||||
currentWeight?: number,
|
||||
history: Array<{ weight: number; source: string; createdAt: Date }> = []
|
||||
): string {
|
||||
let notice = `系统提示:已为你更新体重为${newWeight}kg。`;
|
||||
|
||||
if (currentWeight && currentWeight !== newWeight) {
|
||||
const diff = newWeight - currentWeight;
|
||||
const diffText = diff > 0 ? `增加了${diff.toFixed(1)}kg` : `减少了${Math.abs(diff).toFixed(1)}kg`;
|
||||
notice += `相比之前的${currentWeight}kg,你${diffText}。`;
|
||||
}
|
||||
|
||||
// 添加历史对比信息
|
||||
if (history.length > 0) {
|
||||
const recentWeights = history.slice(0, 3);
|
||||
if (recentWeights.length > 1) {
|
||||
const trend = this.analyzeWeightTrend(recentWeights, newWeight);
|
||||
notice += trend;
|
||||
}
|
||||
}
|
||||
|
||||
return notice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析体重趋势
|
||||
*/
|
||||
private analyzeWeightTrend(
|
||||
recentWeights: Array<{ weight: number; createdAt: Date }>,
|
||||
newWeight: number
|
||||
): string {
|
||||
if (recentWeights.length < 2) return '';
|
||||
|
||||
const weights = [newWeight, ...recentWeights.map(w => w.weight)];
|
||||
let trend = '';
|
||||
|
||||
// 计算最近几次的平均变化
|
||||
let totalChange = 0;
|
||||
for (let i = 0; i < weights.length - 1; i++) {
|
||||
totalChange += weights[i] - weights[i + 1];
|
||||
}
|
||||
const avgChange = totalChange / (weights.length - 1);
|
||||
|
||||
if (Math.abs(avgChange) < 0.5) {
|
||||
trend = '你的体重保持相对稳定,继续保持良好的生活习惯!';
|
||||
} else if (avgChange > 0) {
|
||||
trend = `最近体重呈上升趋势,建议加强运动和注意饮食控制。`;
|
||||
} else {
|
||||
trend = `最近体重呈下降趋势,很棒的进步!继续坚持健康的生活方式。`;
|
||||
}
|
||||
|
||||
return trend;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理体重记录和更新(无图片版本)
|
||||
* 从用户文本中识别体重,更新记录,并返回相关信息
|
||||
*/
|
||||
async processWeightFromText(userId: string, userText?: string): Promise<{
|
||||
weightKg?: number;
|
||||
systemNotice?: string;
|
||||
shouldSkipChat?: boolean;
|
||||
}> {
|
||||
if (!userText) return {};
|
||||
|
||||
// 检查是否是体重记录意图
|
||||
if (!this.isLikelyWeightLogIntent(userText)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
// 从文本中提取体重
|
||||
const extractedWeight = this.extractWeightFromText(userText);
|
||||
if (!extractedWeight) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// 获取用户体重历史
|
||||
const { currentWeight, history } = await this.getUserWeightHistory(userId);
|
||||
|
||||
// 更新体重到数据库
|
||||
await this.usersService.addWeightByVision(userId, extractedWeight);
|
||||
|
||||
// 构建系统提示
|
||||
const systemNotice = this.buildWeightUpdateSystemNotice(
|
||||
extractedWeight,
|
||||
currentWeight || undefined,
|
||||
history
|
||||
);
|
||||
|
||||
return {
|
||||
weightKg: extractedWeight,
|
||||
systemNotice,
|
||||
shouldSkipChat: false // 仍然需要与AI聊天,让AI给出激励回复
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`处理文本体重记录失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user