更新
This commit is contained in:
@@ -31,20 +31,24 @@ class DailyDietAiLogic
|
||||
return ['ok' => false, 'error' => '缺少就诊卡'];
|
||||
}
|
||||
|
||||
$context = self::buildPatientContext($diagnosisId);
|
||||
if (!$context['ok']) {
|
||||
return ['ok' => false, 'error' => $context['error'] ?? '获取档案失败'];
|
||||
}
|
||||
$analysis = self::buildAnalysisPayload($context['data']);
|
||||
$exercise = self::buildExercisePayload($context['data']);
|
||||
|
||||
$cacheKey = self::recommendCacheKey($diagnosisId);
|
||||
if (!$refresh) {
|
||||
$cached = Cache::get($cacheKey);
|
||||
if (is_array($cached) && !empty($cached['breakfast'])) {
|
||||
$cached['cached'] = true;
|
||||
$cached['cached'] = true;
|
||||
$cached['analysis'] = $analysis;
|
||||
$cached['exercise'] = $exercise;
|
||||
return ['ok' => true, 'data' => $cached];
|
||||
}
|
||||
}
|
||||
|
||||
$context = self::buildPatientContext($diagnosisId);
|
||||
if (!$context['ok']) {
|
||||
return ['ok' => false, 'error' => $context['error'] ?? '获取档案失败'];
|
||||
}
|
||||
|
||||
$data = null;
|
||||
if (AiChatService::isEnabled()) {
|
||||
$raw = self::aiDailyRecommend($context['data'], $refresh);
|
||||
@@ -61,6 +65,8 @@ class DailyDietAiLogic
|
||||
$data['date'] = date('Y-m-d');
|
||||
$data['cached'] = false;
|
||||
Cache::set($cacheKey, $data, self::CACHE_TTL);
|
||||
$data['analysis'] = $analysis;
|
||||
$data['exercise'] = $exercise;
|
||||
|
||||
return ['ok' => true, 'data' => $data];
|
||||
}
|
||||
@@ -117,21 +123,28 @@ class DailyDietAiLogic
|
||||
return;
|
||||
}
|
||||
|
||||
$cacheKey = self::recommendCacheKey($diagnosisId);
|
||||
if (!$refresh) {
|
||||
$cached = Cache::get($cacheKey);
|
||||
if (is_array($cached) && !empty($cached['breakfast'])) {
|
||||
$cached['cached'] = true;
|
||||
$emit('done', $cached);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$context = self::buildPatientContext($diagnosisId);
|
||||
if (!$context['ok']) {
|
||||
$emit('error', ['message' => $context['error'] ?? '获取档案失败']);
|
||||
return;
|
||||
}
|
||||
$analysis = self::buildAnalysisPayload($context['data']);
|
||||
$exercise = self::buildExercisePayload($context['data']);
|
||||
// 先把「分析依据」「运动建议」推给前端,提升首屏感知
|
||||
$emit('analysis', ['analysis' => $analysis]);
|
||||
$emit('exercise', ['exercise' => $exercise]);
|
||||
|
||||
$cacheKey = self::recommendCacheKey($diagnosisId);
|
||||
if (!$refresh) {
|
||||
$cached = Cache::get($cacheKey);
|
||||
if (is_array($cached) && !empty($cached['breakfast'])) {
|
||||
$cached['cached'] = true;
|
||||
$cached['analysis'] = $analysis;
|
||||
$cached['exercise'] = $exercise;
|
||||
$emit('done', $cached);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$data = null;
|
||||
$fullText = '';
|
||||
@@ -168,6 +181,8 @@ class DailyDietAiLogic
|
||||
$data['date'] = date('Y-m-d');
|
||||
$data['cached'] = false;
|
||||
Cache::set($cacheKey, $data, self::CACHE_TTL);
|
||||
$data['analysis'] = $analysis;
|
||||
$data['exercise'] = $exercise;
|
||||
$emit('done', $data);
|
||||
}
|
||||
|
||||
@@ -271,20 +286,27 @@ class DailyDietAiLogic
|
||||
$row = $diagnosis->toArray();
|
||||
$gender = (int) ($row['gender'] ?? 0);
|
||||
$genderText = $gender === 1 ? '男' : ($gender === 0 ? '女' : '未知');
|
||||
$age = (int) ($row['age'] ?? 0);
|
||||
|
||||
$since = strtotime('-7 days 00:00:00');
|
||||
// 拉取近 30 天血糖(涵盖近 7 天),统一计算 7/30 天统计
|
||||
$since30 = strtotime('-30 days 00:00:00');
|
||||
$bloodList = BloodRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('record_date', '>=', $since)
|
||||
->where('record_date', '>=', $since30)
|
||||
->where('delete_time', null)
|
||||
->order('record_date', 'desc')
|
||||
->limit(7)
|
||||
->field('record_date,fasting_blood_sugar,postprandial_blood_sugar,other_blood_sugar')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$since7 = strtotime('-7 days 00:00:00');
|
||||
$list7 = array_values(array_filter($bloodList, static function ($b) use ($since7) {
|
||||
return (int) ($b['record_date'] ?? 0) >= $since7;
|
||||
}));
|
||||
|
||||
// 近 7 天逐日明细(喂给 AI 的细节)
|
||||
$bloodSummary = [];
|
||||
foreach ($bloodList as $b) {
|
||||
$date = !empty($b['record_date']) ? date('Y-m-d', (int) $b['record_date']) : '';
|
||||
foreach (array_slice($list7, 0, 7) as $b) {
|
||||
$date = !empty($b['record_date']) ? date('Y-m-d', (int) $b['record_date']) : '';
|
||||
$parts = [];
|
||||
if ($b['fasting_blood_sugar'] !== null && $b['fasting_blood_sugar'] !== '') {
|
||||
$parts[] = '空腹' . $b['fasting_blood_sugar'];
|
||||
@@ -300,18 +322,163 @@ class DailyDietAiLogic
|
||||
}
|
||||
}
|
||||
|
||||
$stats7 = self::aggregateBloodStats($list7, $age, '近7天');
|
||||
$stats30 = self::aggregateBloodStats($bloodList, $age, '近30天');
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'data' => [
|
||||
'patient_name' => (string) ($row['patient_name'] ?? ''),
|
||||
'age' => (int) ($row['age'] ?? 0),
|
||||
'gender_text' => $genderText,
|
||||
'blood_recent' => $bloodSummary,
|
||||
'today' => date('Y-m-d'),
|
||||
'patient_name' => (string) ($row['patient_name'] ?? ''),
|
||||
'age' => $age,
|
||||
'gender_text' => $genderText,
|
||||
'blood_recent' => $bloodSummary,
|
||||
'stats_7d' => $stats7,
|
||||
'stats_30d' => $stats30,
|
||||
'today' => date('Y-m-d'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据年龄返回血糖偏高阈值(与前端 getBloodSugarThresholds 保持一致)
|
||||
*
|
||||
* @return array{fasting:float,postprandial:float}|null
|
||||
*/
|
||||
private static function bloodThresholds(int $age): array
|
||||
{
|
||||
// 年龄未知时用通用糖尿病控制目标兜底,避免「无阈值=全部达标」
|
||||
if ($age <= 0) {
|
||||
return ['fasting' => 7.0, 'postprandial' => 10.0];
|
||||
}
|
||||
return $age < 50
|
||||
? ['fasting' => 7.0, 'postprandial' => 9.0]
|
||||
: ['fasting' => 8.0, 'postprandial' => 10.0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇总一段时间内的血糖统计:有记录天数、偏高天数、达标率、均值
|
||||
*
|
||||
* @param array<int,array<string,mixed>> $rows
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function aggregateBloodStats(array $rows, int $age, string $label): array
|
||||
{
|
||||
$thresholds = self::bloodThresholds($age);
|
||||
|
||||
$byDate = [];
|
||||
$fastingSum = 0.0;
|
||||
$fastingCnt = 0;
|
||||
$postSum = 0.0;
|
||||
$postCnt = 0;
|
||||
|
||||
foreach ($rows as $b) {
|
||||
$ts = (int) ($b['record_date'] ?? 0);
|
||||
if ($ts <= 0) {
|
||||
continue;
|
||||
}
|
||||
$date = date('Y-m-d', $ts);
|
||||
$fasting = ($b['fasting_blood_sugar'] !== null && $b['fasting_blood_sugar'] !== '') ? (float) $b['fasting_blood_sugar'] : null;
|
||||
$post = ($b['postprandial_blood_sugar'] !== null && $b['postprandial_blood_sugar'] !== '') ? (float) $b['postprandial_blood_sugar'] : null;
|
||||
if ($fasting === null && $post === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($byDate[$date])) {
|
||||
$byDate[$date] = ['high' => false];
|
||||
}
|
||||
if ($fasting !== null) {
|
||||
$fastingSum += $fasting;
|
||||
$fastingCnt++;
|
||||
if ($thresholds && $fasting >= $thresholds['fasting']) {
|
||||
$byDate[$date]['high'] = true;
|
||||
}
|
||||
}
|
||||
if ($post !== null) {
|
||||
$postSum += $post;
|
||||
$postCnt++;
|
||||
if ($thresholds && $post >= $thresholds['postprandial']) {
|
||||
$byDate[$date]['high'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$recordDays = count($byDate);
|
||||
$highDays = 0;
|
||||
foreach ($byDate as $d) {
|
||||
if ($d['high']) {
|
||||
$highDays++;
|
||||
}
|
||||
}
|
||||
$normalDays = max(0, $recordDays - $highDays);
|
||||
$compliance = $recordDays > 0 ? (int) round($normalDays / $recordDays * 100) : 0;
|
||||
|
||||
return [
|
||||
'label' => $label,
|
||||
'record_days' => $recordDays,
|
||||
'high_days' => $highDays,
|
||||
'normal_days' => $normalDays,
|
||||
'compliance' => $compliance,
|
||||
'fasting_avg' => $fastingCnt > 0 ? round($fastingSum / $fastingCnt, 1) : null,
|
||||
'postprandial_avg' => $postCnt > 0 ? round($postSum / $postCnt, 1) : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 把统计数据拼成喂给 AI 的一行人类可读文本
|
||||
*
|
||||
* @param array<string,mixed> $stats
|
||||
*/
|
||||
private static function statsToLine(array $stats): string
|
||||
{
|
||||
if (($stats['record_days'] ?? 0) <= 0) {
|
||||
return $stats['label'] . '无血糖记录';
|
||||
}
|
||||
$segs = [
|
||||
$stats['label'] . "有记录{$stats['record_days']}天",
|
||||
"偏高{$stats['high_days']}天",
|
||||
"达标率{$stats['compliance']}%",
|
||||
];
|
||||
if ($stats['fasting_avg'] !== null) {
|
||||
$segs[] = "空腹均值{$stats['fasting_avg']}";
|
||||
}
|
||||
if ($stats['postprandial_avg'] !== null) {
|
||||
$segs[] = "餐后均值{$stats['postprandial_avg']}";
|
||||
}
|
||||
return implode(',', $segs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 给前端展示的「分析依据」结构(近7天 + 近30天)
|
||||
*
|
||||
* @param array<string,mixed> $context
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function buildAnalysisPayload(array $context): array
|
||||
{
|
||||
$out = [];
|
||||
foreach (['stats_7d', 'stats_30d'] as $key) {
|
||||
$s = $context[$key] ?? null;
|
||||
if (is_array($s)) {
|
||||
$out[] = $s;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给前端展示的「运动降糖」建议(按血糖统计调整强度)
|
||||
*
|
||||
* @param array<string,mixed> $context
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function buildExercisePayload(array $context): array
|
||||
{
|
||||
return GiKnowledge::exercisePlan(
|
||||
is_array($context['stats_7d'] ?? null) ? $context['stats_7d'] : [],
|
||||
is_array($context['stats_30d'] ?? null) ? $context['stats_30d'] : []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:string,1:string}
|
||||
*/
|
||||
@@ -320,6 +487,8 @@ class DailyDietAiLogic
|
||||
$bloodLine = empty($context['blood_recent'])
|
||||
? '近7日无血糖记录'
|
||||
: implode(';', $context['blood_recent']);
|
||||
$stat7Line = self::statsToLine($context['stats_7d'] ?? ['label' => '近7天', 'record_days' => 0]);
|
||||
$stat30Line = self::statsToLine($context['stats_30d'] ?? ['label' => '近30天', 'record_days' => 0]);
|
||||
|
||||
$system = <<<SYS
|
||||
你是村里懂糖尿病饮食的大夫助手,依据「霍大夫升糖指数(GI)」给农村老人推荐一日三餐。输出必须严格可执行。
|
||||
@@ -335,12 +504,14 @@ class DailyDietAiLogic
|
||||
SYS;
|
||||
|
||||
$user = sprintf(
|
||||
"患者:%s,%s,%s岁。日期:%s。近7日血糖:%s。\n\nGI知识:\n%s\n\n农家饭参考:\n%s\n\n请输出严格符合硬性规则的三餐 JSON。",
|
||||
"患者:%s,%s,%s岁。日期:%s。\n近7日逐日血糖:%s。\n血糖统计:%s;%s。\n(若近期偏高天数多或达标率低,请收紧主食与高GI食物,多安排蛋白与绿叶菜;若控制平稳可正常推荐。)\n\nGI知识:\n%s\n\n农家饭参考:\n%s\n\n请输出严格符合硬性规则的三餐 JSON。",
|
||||
$context['patient_name'] ?: '大爷大妈',
|
||||
$context['gender_text'] ?? '',
|
||||
$context['age'] ?? 0,
|
||||
$context['today'] ?? date('Y-m-d'),
|
||||
$bloodLine,
|
||||
$stat7Line,
|
||||
$stat30Line,
|
||||
GiKnowledge::summaryText(),
|
||||
GiKnowledge::ruralMealHints()
|
||||
);
|
||||
@@ -362,6 +533,8 @@ SYS;
|
||||
$bloodLine = empty($context['blood_recent'])
|
||||
? '近7日无血糖记录'
|
||||
: implode(';', $context['blood_recent']);
|
||||
$stat7Line = self::statsToLine($context['stats_7d'] ?? ['label' => '近7天', 'record_days' => 0]);
|
||||
$stat30Line = self::statsToLine($context['stats_30d'] ?? ['label' => '近30天', 'record_days' => 0]);
|
||||
|
||||
$system = <<<SYS
|
||||
你是村里懂糖尿病饮食的大夫助手,依据「霍大夫升糖指数(GI)」给农村老人推荐一日三餐。
|
||||
@@ -381,13 +554,15 @@ SYS;
|
||||
SYS;
|
||||
|
||||
$user = sprintf(
|
||||
"患者:%s,%s,%s岁。日期:%s。近7日血糖:%s。\n\nGI摘要:\n%s",
|
||||
"患者:%s,%s,%s岁。日期:%s。\n近7日逐日血糖:%s。\n血糖统计:%s;%s。\n(偏高多或达标率低则收紧主食、多蛋白绿叶菜;平稳则正常推荐。)\n\nGI摘要:\n%s",
|
||||
$context['patient_name'] ?: '大爷大妈',
|
||||
$context['gender_text'] ?? '',
|
||||
$context['age'] ?? 0,
|
||||
$context['today'] ?? date('Y-m-d'),
|
||||
$bloodLine,
|
||||
mb_substr(GiKnowledge::summaryText(), 0, 380)
|
||||
$stat7Line,
|
||||
$stat30Line,
|
||||
mb_substr(GiKnowledge::summaryText(), 0, 320)
|
||||
);
|
||||
|
||||
if ($refresh) {
|
||||
|
||||
@@ -192,6 +192,89 @@ class GiKnowledge
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据血糖统计给出「运动降糖」建议(规则化,按控制好坏调整强度)
|
||||
*
|
||||
* @param array<string,mixed> $stats7 近7天统计
|
||||
* @param array<string,mixed> $stats30 近30天统计
|
||||
* @return array{level:string,level_label:string,headline:string,items:list<string>,intensity:string,note:string}
|
||||
*/
|
||||
public static function exercisePlan(array $stats7 = [], array $stats30 = []): array
|
||||
{
|
||||
// 优先用记录更全的口径判断;都没记录则用近7天
|
||||
$primary = ((int) ($stats30['record_days'] ?? 0) > 0) ? $stats30 : $stats7;
|
||||
$recordDays = (int) ($primary['record_days'] ?? 0);
|
||||
$compliance = (int) ($primary['compliance'] ?? 0);
|
||||
$highDays = (int) ($primary['high_days'] ?? 0);
|
||||
|
||||
if ($recordDays <= 0) {
|
||||
$level = 'unknown';
|
||||
} elseif ($compliance < 50 || $highDays * 2 >= $recordDays) {
|
||||
$level = 'high'; // 偏高多、达标率低 = 控制不佳
|
||||
} elseif ($compliance < 80) {
|
||||
$level = 'medium';
|
||||
} else {
|
||||
$level = 'good';
|
||||
}
|
||||
|
||||
$note = '餐后 1 小时内开始动,别空腹剧烈运动;身上带几块糖,头晕、心慌、出虚汗就马上停下歇着。';
|
||||
|
||||
switch ($level) {
|
||||
case 'high':
|
||||
return [
|
||||
'level' => 'high',
|
||||
'level_label' => '血糖偏高 · 多动',
|
||||
'headline' => '近期血糖偏高,三顿饭后都动一动,最能帮着把糖降下来。',
|
||||
'items' => [
|
||||
'早饭后快走 15 分钟,走到微微出汗',
|
||||
'午饭后快走或原地踏步 20–30 分钟,降餐后血糖最管用',
|
||||
'晚饭后慢走 30 分钟,吃完别马上坐下、躺下',
|
||||
],
|
||||
'intensity' => '中等强度 · 每天累计约 60 分钟',
|
||||
'note' => $note,
|
||||
];
|
||||
case 'medium':
|
||||
return [
|
||||
'level' => 'medium',
|
||||
'level_label' => '基本平稳 · 再稳稳',
|
||||
'headline' => '血糖大体平稳,坚持餐后活动,把它稳住。',
|
||||
'items' => [
|
||||
'三餐后各散步 20 分钟,午饭后可以走快些',
|
||||
'每天加一段八段锦或太极拳,10–15 分钟',
|
||||
'能走楼梯少坐电梯,多干点家务、地里活',
|
||||
],
|
||||
'intensity' => '中低强度 · 每天累计 40–50 分钟',
|
||||
'note' => $note,
|
||||
];
|
||||
case 'good':
|
||||
return [
|
||||
'level' => 'good',
|
||||
'level_label' => '控制不错 · 保持',
|
||||
'headline' => '血糖控制得不错,保持规律活动就行。',
|
||||
'items' => [
|
||||
'餐后散步 15–20 分钟,雷打不动',
|
||||
'每天一段八段锦、太极或柔和拉伸',
|
||||
'天好多到院里、地里走动,别久坐',
|
||||
],
|
||||
'intensity' => '中低强度 · 每天累计 30–40 分钟',
|
||||
'note' => $note,
|
||||
];
|
||||
default:
|
||||
return [
|
||||
'level' => 'unknown',
|
||||
'level_label' => '先记血糖 · 再调运动',
|
||||
'headline' => '血糖记录还少,先按基础来:饭后多走动。',
|
||||
'items' => [
|
||||
'三餐后都散步 15–20 分钟',
|
||||
'每天一段八段锦或太极拳',
|
||||
'少久坐,坐 1 小时就起来活动几分钟',
|
||||
],
|
||||
'intensity' => '中低强度 · 每天累计约 30 分钟',
|
||||
'note' => $note,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{food:string,level:string,level_label:string,advice:string,source:string}
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user