377 lines
14 KiB
PHP
377 lines
14 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;
|
|
}
|
|
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');
|
|
}
|
|
}
|