where('user_id', $userId) ->where('diagnosis_id', $diagnosisId) ->where('delete_time', null) ->find(); if (!$owned) { return self::setError('无权分享该诊单') ? false : false; } $diagnosis = Diagnosis::where('id', $diagnosisId)->where('delete_time', null)->find(); if (!$diagnosis) { return self::setError('诊单不存在') ? false : false; } if ((int) ($diagnosis['show_card'] ?? 1) !== 1) { return self::setError('该就诊卡已在统计端隐藏') ? false : false; } $inviteDate = date('Y-m-d'); $code = self::generateUniqueCode(); DailyShareInvite::create([ 'invite_code' => $code, 'diagnosis_id' => $diagnosisId, 'user_id' => $userId, 'invite_date' => $inviteDate, 'create_time' => time(), ]); return [ 'invite_code' => $code, 'invite_date' => $inviteDate, 'expires_hint' => '邀请码仅今日有效,明日将无法查看', 'share_path' => '/tongji/pages/index?from=share&invite_code=' . $code, ]; } /** * 凭邀请码查看分享战报(含近 7 日血糖/血压记录,仅当日邀请码有效) */ public static function previewByInviteCode(string $inviteCode, string $viewerKey = ''): array|false { self::$error = ''; $inviteCode = strtoupper(trim($inviteCode)); if ($inviteCode === '') { return self::setError('邀请码不能为空') ? false : false; } $row = DailyShareInvite::where('invite_code', $inviteCode)->find(); if (!$row) { return self::setError('邀请码无效或已失效') ? false : false; } $today = date('Y-m-d'); if ((string) $row['invite_date'] !== $today) { return self::setError('邀请码已过期,仅可在分享当天查看') ? false : false; } $diagnosisId = (int) $row['diagnosis_id']; $diagnosis = Diagnosis::where('id', $diagnosisId)->where('delete_time', null)->find(); if (!$diagnosis) { return self::setError('诊单不存在') ? false : false; } $age = (int) ($diagnosis['age'] ?? 0); $stats = self::buildDesensitizedWeekStats($diagnosisId, $age); $name = trim((string) ($diagnosis['patient_name'] ?? '')); $label = self::maskPatientName($name); $likeMeta = DailyFamilyLikeLogic::metaForInvite($inviteCode, $viewerKey); return array_merge($stats, $likeMeta, [ 'invite_code' => $inviteCode, 'invite_date' => $today, 'expires_hint' => '本邀请码仅今日有效,明日将无法查看', 'viewer_notice' => '您正在查看家人分享的近7日血糖记录', 'patient_label' => $label, ]); } protected static function generateUniqueCode(): string { for ($i = 0; $i < 8; $i++) { $code = strtoupper(substr(bin2hex(random_bytes(4)), 0, 8)); if (!DailyShareInvite::where('invite_code', $code)->find()) { return $code; } } return strtoupper(substr(uniqid('', true), -8)); } protected static function maskPatientName(string $name): string { $name = trim($name); if ($name === '') { return '家人'; } $len = mb_strlen($name, 'UTF-8'); if ($len <= 1) { return $name . '*'; } if ($len === 2) { return mb_substr($name, 0, 1, 'UTF-8') . '*'; } return mb_substr($name, 0, 1, 'UTF-8') . '*' . mb_substr($name, -1, 1, 'UTF-8'); } /** * 近 7 天习惯统计 + 每日血糖/血压记录(供家人分享页展示) */ public static function buildDesensitizedWeekStats(int $diagnosisId, int $age = 0): array { $today = new \DateTime('today'); $dayKeys = []; for ($i = 6; $i >= 0; $i--) { $d = clone $today; $d->modify("-{$i} days"); $dayKeys[] = $d->format('Y-m-d'); } $startTs = strtotime($dayKeys[0] . ' 00:00:00'); $endTs = strtotime($dayKeys[6] . ' 23:59:59'); $rows = BloodRecord::where('diagnosis_id', $diagnosisId) ->where('record_date', '>=', $startTs) ->where('record_date', '<=', $endTs) ->field('record_date,fasting_blood_sugar,postprandial_blood_sugar,other_blood_sugar,systolic_pressure,diastolic_pressure') ->select() ->toArray(); $byDate = []; foreach ($rows as $r) { $key = date('Y-m-d', (int) $r['record_date']); if (!isset($byDate[$key])) { $byDate[$key] = $r; } else { foreach (['fasting_blood_sugar', 'postprandial_blood_sugar', 'other_blood_sugar', 'systolic_pressure', 'diastolic_pressure'] as $f) { if (self::hasValue($r[$f]) && !self::hasValue($byDate[$key][$f])) { $byDate[$key][$f] = $r[$f]; } } } } $recordDays = 0; $completeDays = 0; foreach ($dayKeys as $key) { $b = $byDate[$key] ?? null; if (!$b || !self::dayHasBlood($b)) { continue; } $recordDays++; if (self::hasValue($b['fasting_blood_sugar']) && self::hasValue($b['postprandial_blood_sugar'])) { $completeDays++; } } $streakDays = self::calcStreakDays($diagnosisId); $tree = self::treeMeta($recordDays); $quote = self::buildQuote($recordDays, $completeDays); $weekdayLabels = ['日', '一', '二', '三', '四', '五', '六']; $dailyRecords = []; foreach ($dayKeys as $key) { $b = $byDate[$key] ?? null; $dt = new \DateTime($key); $w = (int) $dt->format('w'); $has = $b && self::dayHasBlood($b); $fasting = $has ? self::formatSugarValue($b['fasting_blood_sugar'] ?? null) : null; $post = $has ? self::formatSugarValue($b['postprandial_blood_sugar'] ?? null) : null; $other = $has ? self::formatSugarValue($b['other_blood_sugar'] ?? null) : null; $sys = $has ? self::formatSugarValue($b['systolic_pressure'] ?? null) : null; $dia = $has ? self::formatSugarValue($b['diastolic_pressure'] ?? null) : null; $dailyRecords[] = [ 'date' => $key, 'date_label' => $dt->format('n') . '/' . $dt->format('j'), 'weekday' => '周' . $weekdayLabels[$w], 'has_record' => $has, 'fasting' => $fasting, 'fasting_high' => self::isHighFasting($fasting, $age), 'postprandial' => $post, 'postprandial_high' => self::isHighPostprandial($post, $age), 'other' => $other, 'other_high' => self::isHighPostprandial($other, $age), 'systolic' => $sys, 'diastolic' => $dia, 'bp_high' => self::isHighBp($sys, $dia), 'bp_text' => self::formatBpText($sys, $dia), ]; } return [ 'week_label' => $today->format('Y') . '年 第' . self::weekOfYear($today) . '周', 'record_days' => $recordDays, 'complete_days' => $completeDays, 'streak_days' => $streakDays, 'quote' => $quote, 'tree_emoji' => $tree['emoji'], 'tree_title' => $tree['title'], 'tree_desc' => $tree['desc'], 'tree_level' => $tree['level'], 'daily_records' => $dailyRecords, ]; } protected static function formatSugarValue($v): ?float { if (!self::hasValue($v)) { return null; } return round((float) $v, 1); } protected static function getBloodSugarThresholds(int $age): ?array { if ($age <= 0) { return null; } if ($age < 50) { return ['fasting' => 7.0, 'postprandial' => 9.0]; } return ['fasting' => 8.0, 'postprandial' => 10.0]; } protected static function isHighFasting(?float $v, int $age): bool { $t = self::getBloodSugarThresholds($age); return $v !== null && $t !== null && $v >= $t['fasting']; } protected static function isHighPostprandial(?float $v, int $age): bool { $t = self::getBloodSugarThresholds($age); return $v !== null && $t !== null && $v >= $t['postprandial']; } protected static function isHighBp(?float $systolic, ?float $diastolic): bool { return ($systolic !== null && $systolic > 140) || ($diastolic !== null && $diastolic > 90); } protected static function formatBpText(?float $systolic, ?float $diastolic): string { if ($systolic === null && $diastolic === null) { return ''; } $s = $systolic !== null ? (string) (int) round($systolic) : '—'; $d = $diastolic !== null ? (string) (int) round($diastolic) : '—'; return $s . '/' . $d; } protected static function hasValue($v): bool { if ($v === null || $v === '' || $v === '0' || $v === 0) { return false; } return is_numeric($v) && (float) $v > 0; } protected static function dayHasBlood(array $b): bool { return self::hasValue($b['fasting_blood_sugar'] ?? null) || self::hasValue($b['postprandial_blood_sugar'] ?? null) || self::hasValue($b['other_blood_sugar'] ?? null) || self::hasValue($b['systolic_pressure'] ?? null) || self::hasValue($b['diastolic_pressure'] ?? null); } protected static function calcStreakDays(int $diagnosisId): int { $today = new \DateTime('today'); $count = 0; for ($i = 0; $i < 90; $i++) { $d = clone $today; $d->modify("-{$i} days"); $key = $d->format('Y-m-d'); $start = strtotime($key . ' 00:00:00'); $end = strtotime($key . ' 23:59:59'); $exists = BloodRecord::where('diagnosis_id', $diagnosisId) ->where('record_date', '>=', $start) ->where('record_date', '<=', $end) ->whereRaw('(fasting_blood_sugar > 0 OR postprandial_blood_sugar > 0 OR other_blood_sugar > 0 OR systolic_pressure > 0 OR diastolic_pressure > 0)') ->find(); if ($exists) { $count++; continue; } if ($i === 0) { continue; } break; } return $count; } protected static function treeMeta(int $recordDays): array { if ($recordDays >= 7) { return ['level' => 4, 'emoji' => '🌸', 'title' => '控糖树 · 开花啦', 'desc' => '本周每天都记录了']; } if ($recordDays >= 5) { return ['level' => 3, 'emoji' => '🌳', 'title' => '控糖树 · 枝繁叶茂', 'desc' => "本周 {$recordDays}/7 天有记录"]; } if ($recordDays >= 3) { return ['level' => 2, 'emoji' => '🌿', 'title' => '控糖树 · 茁壮成长', 'desc' => "本周 {$recordDays}/7 天有记录"]; } if ($recordDays >= 1) { return ['level' => 1, 'emoji' => '🌱', 'title' => '控糖树 · 破土发芽', 'desc' => '本周已开始记录']; } return ['level' => 0, 'emoji' => '🪴', 'title' => '控糖树 · 等待浇水', 'desc' => '本周暂无记录']; } protected static function buildQuote(int $recordDays, int $completeDays): string { if ($recordDays >= 7) { return '本周每天都留下了记录,这份自律值得骄傲!'; } if ($completeDays >= 5) { return "本周有 {$completeDays} 天完成了空腹+餐后记录,习惯越来越稳。"; } if ($recordDays >= 4) { return "本周已记录 {$recordDays} 天,坚持就是胜利。"; } if ($recordDays > 0) { return '好的开始!继续记录会更稳。'; } return '分享者本周尚未记录,鼓励 Ta 每天记一笔。'; } protected static function weekOfYear(\DateTime $date): int { return (int) $date->format('W'); } }