新增普拉提训练系统的数据库结构和数据导入功能

- 创建普拉提分类和动作数据的SQL导入脚本,支持垫上普拉提和器械普拉提的分类管理
- 实现数据库结构迁移脚本,添加新字段以支持普拉提类型和器械名称
- 更新数据库升级总结文档,详细说明数据库结构变更和数据导入步骤
- 创建训练会话相关表,支持每日训练实例功能
- 引入训练会话管理模块,整合训练计划与实际训练会话的关系
This commit is contained in:
richarjiang
2025-08-15 15:34:11 +08:00
parent bea71af5d3
commit 0edcfdcae9
28 changed files with 2528 additions and 164 deletions

View File

@@ -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', () => {

View File

@@ -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 {};
}
}
}