Files
zyt/server/app/api/logic/tcm/DailyShareLogic.php
T
2026-05-27 15:50:54 +08:00

374 lines
13 KiB
PHP

<?php
namespace app\api\logic\tcm;
use app\common\model\tcm\BloodRecord;
use app\common\model\tcm\DailyShareInvite;
use app\common\model\tcm\Diagnosis;
/**
* 日常记录分享邀请码(当日有效、脱敏预览)
*/
class DailyShareLogic
{
protected static string $error = '';
public static function getError(): string
{
return self::$error;
}
protected static function setError(string $msg): bool
{
self::$error = $msg;
return false;
}
/**
* 生成当日邀请码(分享人须已拥有诊单)
*/
public static function createInvite(int $userId, int $diagnosisId): array|false
{
self::$error = '';
if ($userId <= 0) {
return self::setError('请先登录') ? false : false;
}
if ($diagnosisId <= 0) {
return self::setError('诊单ID不能为空') ? false : false;
}
$owned = \think\facade\Db::name('diagnosis_view_records')
->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;
}
$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');
}
}