feat(ai): 优化健康日报生成,集成用户健康统计数据并增强视觉提示
This commit is contained in:
@@ -59,6 +59,17 @@ user?: {
|
||||
challenges: {
|
||||
activeChallengeCount: number;
|
||||
};
|
||||
// 来自 UserDailyHealth 的健康统计数据
|
||||
healthStats: {
|
||||
exerciseMinutes?: number; // 锻炼分钟数
|
||||
caloriesBurned?: number; // 消耗卡路里
|
||||
standingMinutes?: number; // 站立时间
|
||||
basalMetabolism?: number; // 基础代谢
|
||||
sleepMinutes?: number; // 睡眠分钟数
|
||||
bloodOxygen?: number; // 血氧饱和度
|
||||
stressLevel?: number; // 压力值
|
||||
steps?: number; // 步数
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -300,7 +311,7 @@ export class AiReportService {
|
||||
const dayStart = dayjs(date).startOf('day').toISOString();
|
||||
const dayEnd = dayjs(date).endOf('day').toISOString();
|
||||
|
||||
const [userProfile, medicationStats, dietHistory, waterStats, moodStats, activeChallengeCount] = await Promise.all([
|
||||
const [userProfile, medicationStats, dietHistory, waterStats, moodStats, activeChallengeCount, dailyHealth] = await Promise.all([
|
||||
this.usersService.getProfile({ sub: userId, email: '', isGuest: false } as any).then(p => p.data).catch(() => null),
|
||||
this.medicationStatsService.getDailyStats(userId, date).catch(() => null),
|
||||
// 获取当日饮食记录并聚合
|
||||
@@ -309,6 +320,8 @@ export class AiReportService {
|
||||
this.moodCheckinsService.getDaily(userId, date).catch(() => null),
|
||||
// 获取用户当前参与的活跃挑战数量
|
||||
this.challengesService.getActiveParticipatingChallengeCount(userId).catch(() => 0),
|
||||
// 获取用户每日健康统计数据
|
||||
this.usersService.getDailyHealth(userId, date).catch(() => null),
|
||||
]);
|
||||
|
||||
// 处理饮食数据聚合
|
||||
@@ -371,12 +384,24 @@ export class AiReportService {
|
||||
challenges: {
|
||||
activeChallengeCount: activeChallengeCount,
|
||||
},
|
||||
// 健康统计数据
|
||||
healthStats: dailyHealth ? {
|
||||
exerciseMinutes: dailyHealth.exerciseMinutes ?? undefined,
|
||||
caloriesBurned: dailyHealth.caloriesBurned ?? undefined,
|
||||
standingMinutes: dailyHealth.standingMinutes ?? undefined,
|
||||
basalMetabolism: dailyHealth.basalMetabolism ?? undefined,
|
||||
sleepMinutes: dailyHealth.sleepMinutes ?? undefined,
|
||||
bloodOxygen: dailyHealth.bloodOxygen ?? undefined,
|
||||
stressLevel: dailyHealth.stressLevel ?? undefined,
|
||||
steps: dailyHealth.steps ?? undefined,
|
||||
} : {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. 生成固定格式的 prompt,适用于 Nano Banana Pro 模型
|
||||
* 优化:支持多语言,根据用户语言偏好生成对应的文本
|
||||
* 包含健康统计数据:步数、睡眠、运动、卡路里消耗、血氧、压力等
|
||||
*/
|
||||
private async generateImagePrompt(data: DailyHealthData, language: string): Promise<string> {
|
||||
const isEnglish = language.toLowerCase().startsWith('en');
|
||||
@@ -384,72 +409,115 @@ export class AiReportService {
|
||||
// 格式化日期
|
||||
const dateStr = isEnglish
|
||||
? dayjs(data.date).format('MMM DD') // "Dec 01"
|
||||
: dayjs(data.date).format('MM月DD日'); // "12-01"
|
||||
: dayjs(data.date).format('MM月DD日'); // "12月01日"
|
||||
|
||||
// 准备数据文本
|
||||
const moodText = this.translateMood(data.mood.primaryMood, language);
|
||||
const medRate = Math.round(data.medications.completionRate); // 取整
|
||||
const medRate = Math.round(data.medications.completionRate);
|
||||
const calories = Math.round(data.diet.totalCalories);
|
||||
const water = Math.round(data.water.totalAmount);
|
||||
const challengeCount = data.challenges.activeChallengeCount;
|
||||
|
||||
// 根据性别调整角色描述
|
||||
let characterDesc = 'A happy cute character or animal mascot';
|
||||
// 健康统计数据
|
||||
const { healthStats } = data;
|
||||
const steps = healthStats.steps;
|
||||
const sleepHours = healthStats.sleepMinutes ? Math.round(healthStats.sleepMinutes / 60 * 10) / 10 : undefined;
|
||||
const exerciseMinutes = healthStats.exerciseMinutes;
|
||||
const caloriesBurned = healthStats.caloriesBurned ? Math.round(healthStats.caloriesBurned) : undefined;
|
||||
const bloodOxygen = healthStats.bloodOxygen ? Math.round(healthStats.bloodOxygen) : undefined;
|
||||
const stressLevel = healthStats.stressLevel;
|
||||
|
||||
// 根据性别调整角色描述 - 优化版本,更具体的形象描述
|
||||
let characterDesc: string;
|
||||
let characterPose: string;
|
||||
if (data.user?.gender === 'male') {
|
||||
characterDesc = 'A happy cute boy character in casual sportswear';
|
||||
characterDesc = 'A cheerful young man with short hair, wearing a comfortable athletic t-shirt and shorts, fit and healthy looking';
|
||||
characterPose = 'standing confidently with a thumbs up or stretching pose';
|
||||
} else if (data.user?.gender === 'female') {
|
||||
characterDesc = 'A happy cute girl character in yoga outfit';
|
||||
characterDesc = 'A cheerful young woman with ponytail hair, wearing a stylish yoga top and leggings, fit and energetic looking';
|
||||
characterPose = 'doing a gentle yoga pose or stretching gracefully';
|
||||
} else {
|
||||
characterDesc = 'A cute friendly mascot character (like a happy cat or bunny) wearing a small fitness headband';
|
||||
characterPose = 'jumping happily or giving a cheerful wave';
|
||||
}
|
||||
|
||||
// 构建多语言文本内容
|
||||
const textContent = isEnglish ? {
|
||||
languageInstruction: 'Please render the following specific text in English correctly:',
|
||||
title: `${dateStr} Health Report`,
|
||||
medication: `Medication: ${medRate}%`,
|
||||
diet: `Calories: ${calories} kcal`,
|
||||
water: `Water: ${water}ml`,
|
||||
medication: medRate > 0 ? `Medication: ${medRate}%` : null,
|
||||
diet: calories > 0 ? `Calories In: ${calories} kcal` : null,
|
||||
water: water > 0 ? `Water: ${water}ml` : null,
|
||||
mood: `Mood: ${moodText}`,
|
||||
challenges: challengeCount > 0 ? `Challenges: ${challengeCount}` : null,
|
||||
// 健康统计
|
||||
steps: steps !== undefined ? `Steps: ${steps.toLocaleString()}` : null,
|
||||
sleep: sleepHours !== undefined ? `Sleep: ${sleepHours}h` : null,
|
||||
exercise: exerciseMinutes !== undefined ? `Exercise: ${exerciseMinutes}min` : null,
|
||||
caloriesBurned: caloriesBurned !== undefined ? `Burned: ${caloriesBurned} kcal` : null,
|
||||
bloodOxygen: bloodOxygen !== undefined ? `SpO2: ${bloodOxygen}%` : null,
|
||||
stress: stressLevel !== undefined ? `Stress: ${stressLevel}` : null,
|
||||
} : {
|
||||
languageInstruction: 'Please render the following specific text in Chinese correctly:',
|
||||
title: `${dateStr} 健康日报`,
|
||||
medication: `用药: ${medRate}%`,
|
||||
diet: `热量: ${calories}千卡`,
|
||||
water: `饮水: ${water}ml`,
|
||||
medication: medRate > 0 ? `用药: ${medRate}%` : null,
|
||||
diet: calories > 0 ? `摄入: ${calories}千卡` : null,
|
||||
water: water > 0 ? `饮水: ${water}ml` : null,
|
||||
mood: `心情: ${moodText}`,
|
||||
challenges: challengeCount > 0 ? `挑战: ${challengeCount}个` : null,
|
||||
// 健康统计
|
||||
steps: steps !== undefined ? `步数: ${steps.toLocaleString()}` : null,
|
||||
sleep: sleepHours !== undefined ? `睡眠: ${sleepHours}小时` : null,
|
||||
exercise: exerciseMinutes !== undefined ? `运动: ${exerciseMinutes}分钟` : null,
|
||||
caloriesBurned: caloriesBurned !== undefined ? `消耗: ${caloriesBurned}千卡` : null,
|
||||
bloodOxygen: bloodOxygen !== undefined ? `血氧: ${bloodOxygen}%` : null,
|
||||
stress: stressLevel !== undefined ? `压力: ${stressLevel}` : null,
|
||||
};
|
||||
|
||||
// 构建挑战相关的 prompt 部分
|
||||
const challengeTextSection = textContent.challenges
|
||||
? `- Challenge section text: "${textContent.challenges}"\n`
|
||||
: '';
|
||||
const challengeIconSection = challengeCount > 0
|
||||
? `- Icon for challenges (trophy or flag icon representing ${challengeCount} active ${challengeCount === 1 ? 'challenge' : 'challenges'}).\n`
|
||||
: '';
|
||||
// 构建文本部分 - 只包含有数据的项
|
||||
const textSections: string[] = [];
|
||||
textSections.push(`- Title text: "${textContent.title}"`);
|
||||
if (textContent.medication) textSections.push(`- Medication section text: "${textContent.medication}"`);
|
||||
if (textContent.diet) textSections.push(`- Diet section text: "${textContent.diet}"`);
|
||||
if (textContent.water) textSections.push(`- Water section text: "${textContent.water}"`);
|
||||
textSections.push(`- Mood section text: "${textContent.mood}"`);
|
||||
if (textContent.challenges) textSections.push(`- Challenge section text: "${textContent.challenges}"`);
|
||||
// 健康统计文本
|
||||
if (textContent.steps) textSections.push(`- Steps section text: "${textContent.steps}"`);
|
||||
if (textContent.sleep) textSections.push(`- Sleep section text: "${textContent.sleep}"`);
|
||||
if (textContent.exercise) textSections.push(`- Exercise section text: "${textContent.exercise}"`);
|
||||
if (textContent.caloriesBurned) textSections.push(`- Calories burned section text: "${textContent.caloriesBurned}"`);
|
||||
if (textContent.bloodOxygen) textSections.push(`- Blood oxygen section text: "${textContent.bloodOxygen}"`);
|
||||
if (textContent.stress) textSections.push(`- Stress section text: "${textContent.stress}"`);
|
||||
|
||||
// 构建图标部分 - 只包含有数据的项
|
||||
const iconSections: string[] = [];
|
||||
iconSections.push(`- ${characterDesc}, ${characterPose}, representing the user.`);
|
||||
if (textContent.medication) iconSections.push('- Icon for medication (pill bottle or pills).');
|
||||
if (textContent.diet) iconSections.push('- Icon for diet (healthy food bowl or apple).');
|
||||
if (textContent.water) iconSections.push('- Icon for water (water glass or water drop).');
|
||||
iconSections.push(`- Icon for mood (a ${moodText.toLowerCase()} face emoji).`);
|
||||
if (textContent.challenges) iconSections.push(`- Icon for challenges (trophy or flag icon representing ${challengeCount} active ${challengeCount === 1 ? 'challenge' : 'challenges'}).`);
|
||||
// 健康统计图标
|
||||
if (textContent.steps) iconSections.push('- Icon for steps (footprints or walking figure).');
|
||||
if (textContent.sleep) iconSections.push('- Icon for sleep (moon and stars or sleeping face).');
|
||||
if (textContent.exercise) iconSections.push('- Icon for exercise (running figure or dumbbell).');
|
||||
if (textContent.caloriesBurned) iconSections.push('- Icon for calories burned (flame or fire icon).');
|
||||
if (textContent.bloodOxygen) iconSections.push('- Icon for blood oxygen (heart with pulse or O2 symbol).');
|
||||
if (textContent.stress) iconSections.push('- Icon for stress level (brain or meditation icon).');
|
||||
|
||||
// 构建 Prompt
|
||||
// 格式:[风格描述] + [主体内容] + [文本渲染指令] + [细节描述]
|
||||
const prompt = `
|
||||
A cute, hand-drawn style health journal page illustration, kawaii aesthetic, soft pastel colors, warm lighting. Vertical 9:16 aspect ratio. High quality, 1k resolution.
|
||||
A cute, hand-drawn style health journal page illustration, kawaii aesthetic, soft pastel colors (pink, mint, lavender, peach), warm lighting. Vertical 9:16 aspect ratio. High quality, 1k resolution.
|
||||
|
||||
The image features a cute layout with icons and text boxes.
|
||||
The image features a cute organized layout with icons and text boxes arranged in a grid or card style.
|
||||
${textContent.languageInstruction}
|
||||
- Title text: "${textContent.title}"
|
||||
- Medication section text: "${textContent.medication}"
|
||||
- Diet section text: "${textContent.diet}"
|
||||
- Water section text: "${textContent.water}"
|
||||
- Mood section text: "${textContent.mood}"
|
||||
${challengeTextSection}
|
||||
${textSections.join('\n')}
|
||||
|
||||
Visual elements:
|
||||
- ${characterDesc} representing the user.
|
||||
- Icon for medication (pill bottle or pills).
|
||||
- Icon for diet (healthy food bowl or apple).
|
||||
- Icon for water (water glass or drop).
|
||||
- Icon for exercise (sneakers or dumbbell).
|
||||
- Icon for mood (a smiley face representing ${moodText}).
|
||||
${challengeIconSection}
|
||||
Composition: Clean, organized, magazine layout style, decorative stickers and washi tape effects.
|
||||
${iconSections.join('\n')}
|
||||
|
||||
Composition: Clean, organized, magazine layout style with rounded corners on each section card, decorative stickers and washi tape effects, small sparkles and stars as decorations. The character should be prominently featured at the top or center of the design.
|
||||
`.trim();
|
||||
|
||||
return prompt;
|
||||
|
||||
24
src/main.ts
24
src/main.ts
@@ -27,15 +27,35 @@ async function bootstrap() {
|
||||
app.use((req, res, next) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// 捕获响应体
|
||||
const originalSend = res.send;
|
||||
let responseBody: any;
|
||||
res.send = function (body) {
|
||||
responseBody = body;
|
||||
return originalSend.call(this, body);
|
||||
};
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - startTime;
|
||||
const appVersion = req.headers['x-app-version'] || 'unknown';
|
||||
const logMessage = `${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms [v${appVersion}]`;
|
||||
|
||||
// 解析响应体
|
||||
let responseStr = '';
|
||||
try {
|
||||
if (typeof responseBody === 'string') {
|
||||
responseStr = responseBody;
|
||||
} else if (responseBody) {
|
||||
responseStr = JSON.stringify(responseBody);
|
||||
}
|
||||
} catch {
|
||||
responseStr = '[Unable to stringify response]';
|
||||
}
|
||||
|
||||
if (res.statusCode >= 400) {
|
||||
logger.error(`${logMessage} - Body: ${JSON.stringify(req.body)}`);
|
||||
logger.error(`${logMessage} - Body: ${JSON.stringify(req.body)} - Response: ${responseStr}`);
|
||||
} else {
|
||||
logger.log(`${logMessage} - Body: ${JSON.stringify(req.body)}`);
|
||||
logger.log(`${logMessage} - Body: ${JSON.stringify(req.body)} - Response: ${responseStr}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3006,6 +3006,23 @@ export class UsersService {
|
||||
|
||||
// ==================== 每日健康数据相关方法 ====================
|
||||
|
||||
/**
|
||||
* 获取用户指定日期的健康数据
|
||||
* @param userId 用户ID
|
||||
* @param date 日期,格式 YYYY-MM-DD,默认为今天
|
||||
* @returns 健康数据记录,如果不存在则返回 null
|
||||
*/
|
||||
async getDailyHealth(userId: string, date?: string): Promise<UserDailyHealth | null> {
|
||||
const recordDate = date || dayjs().format('YYYY-MM-DD');
|
||||
this.logger.log(`获取每日健康数据 - 用户ID: ${userId}, 日期: ${recordDate}`);
|
||||
|
||||
const record = await this.userDailyHealthModel.findOne({
|
||||
where: { userId, recordDate },
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户每日健康数据
|
||||
* 每日每个用户只会生成一条数据,如果已存在则更新
|
||||
|
||||
Reference in New Issue
Block a user