更新
This commit is contained in:
@@ -16,9 +16,14 @@ namespace app\api\controller;
|
||||
|
||||
use app\adminapi\logic\tcm\DiagnosisLogic;
|
||||
use app\adminapi\logic\tcm\TrackingNoteLogic;
|
||||
use app\api\logic\tcm\DailyGamifyLogic;
|
||||
use app\api\logic\tcm\DailyFamilyLikeLogic;
|
||||
use app\api\logic\tcm\DailyShareLogic;
|
||||
use app\adminapi\logic\ConfigLogic;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\BloodRecord;
|
||||
use app\common\model\tcm\DietRecord;
|
||||
use app\common\model\tcm\ExerciseRecord;
|
||||
|
||||
/**
|
||||
* 中医诊断控制器(供小程序调用)
|
||||
@@ -31,7 +36,7 @@ class TcmController extends BaseApiController
|
||||
* @notes 不需要登录的方法
|
||||
* @var array
|
||||
*/
|
||||
public array $notNeedLogin = ['getPatientSignature', 'diagnosisDetail', 'getDict', 'confirmDiagnosis', 'getCardList', 'getOrderByNo', 'patientHangupVideo', 'dailyTrackingWindow', 'dailyTrackingNotes'];
|
||||
public array $notNeedLogin = ['getPatientSignature', 'diagnosisDetail', 'getDict', 'confirmDiagnosis', 'getCardList', 'getOrderByNo', 'patientHangupVideo', 'dailySharePreview', 'dailyFamilyLike'];
|
||||
|
||||
/**
|
||||
* @notes 获取患者签名(供小程序调用)
|
||||
@@ -350,16 +355,13 @@ class TcmController extends BaseApiController
|
||||
$startDate = (string) $this->request->get('start_date', '');
|
||||
$endDate = (string) $this->request->get('end_date', '');
|
||||
|
||||
if ($diagnosisId <= 0) {
|
||||
return $this->fail('诊单ID不能为空');
|
||||
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
|
||||
if (!$check['ok']) {
|
||||
return $this->fail($check['error']);
|
||||
}
|
||||
$diagnosis = $check['diagnosis'];
|
||||
|
||||
try {
|
||||
// 患者端越权校验:诊单必须存在;如传入 patient_id 则须与诊单匹配
|
||||
$diagnosis = Diagnosis::where('id', $diagnosisId)->find();
|
||||
if (!$diagnosis) {
|
||||
return $this->fail('诊单不存在');
|
||||
}
|
||||
if ($patientId > 0 && (int) $diagnosis['patient_id'] !== $patientId) {
|
||||
return $this->fail('无权查看该诊单的日常记录');
|
||||
}
|
||||
@@ -393,15 +395,13 @@ class TcmController extends BaseApiController
|
||||
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
|
||||
$patientId = (int) $this->request->get('patient_id', 0);
|
||||
|
||||
if ($diagnosisId <= 0) {
|
||||
return $this->fail('诊单ID不能为空');
|
||||
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
|
||||
if (!$check['ok']) {
|
||||
return $this->fail($check['error']);
|
||||
}
|
||||
$diagnosis = $check['diagnosis'];
|
||||
|
||||
try {
|
||||
$diagnosis = Diagnosis::where('id', $diagnosisId)->find();
|
||||
if (!$diagnosis) {
|
||||
return $this->fail('诊单不存在');
|
||||
}
|
||||
if ($patientId > 0 && (int) $diagnosis['patient_id'] !== $patientId) {
|
||||
return $this->fail('无权查看该诊单的跟踪备注');
|
||||
}
|
||||
@@ -413,6 +413,28 @@ class TcmController extends BaseApiController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析小程序 form-urlencoded 提交的 JSON 字段
|
||||
*
|
||||
* @param mixed $raw
|
||||
* @param string $arrayKey ThinkPHP 数组字段名,如 badges/a
|
||||
* @return array
|
||||
*/
|
||||
private function decodeJsonPostField($raw, string $arrayKey): array
|
||||
{
|
||||
if (is_string($raw) && $raw !== '') {
|
||||
$decoded = json_decode($raw, true);
|
||||
if (is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
if (is_array($raw)) {
|
||||
return $raw;
|
||||
}
|
||||
$arr = $this->request->post($arrayKey, []);
|
||||
return is_array($arr) ? $arr : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 校验当前登录患者是否拥有指定诊单(通过 diagnosis_view_records 关联)
|
||||
* @param int $diagnosisId
|
||||
@@ -630,6 +652,328 @@ class TcmController extends BaseApiController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取稳糖乐园状态(积分、今日任务、可领取积分)
|
||||
*
|
||||
* 路由:GET /api/tcm/dailyGetGamify?diagnosis_id=:id
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function dailyGetGamify()
|
||||
{
|
||||
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
|
||||
$result = DailyGamifyLogic::getState((int) $this->userId, $diagnosisId);
|
||||
if ($result === false) {
|
||||
return $this->fail(DailyGamifyLogic::getError());
|
||||
}
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 给小树浇水(领取今日已完成任务的积分,服务端校验)
|
||||
*
|
||||
* 路由:POST /api/tcm/dailyWaterTree
|
||||
* 必填:diagnosis_id
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function dailyWaterTree()
|
||||
{
|
||||
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
|
||||
$result = DailyGamifyLogic::waterTree((int) $this->userId, $diagnosisId);
|
||||
if ($result === false) {
|
||||
return $this->fail(DailyGamifyLogic::getError());
|
||||
}
|
||||
// 统一走 success,保证 msg 与 data 同时返回,前端可提示且刷新状态
|
||||
return $this->success($result['message'] ?? '操作成功', $result, 1, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 日常记录-获取今日饮食记录
|
||||
*
|
||||
* 路由:GET /api/tcm/dailyTodayDietRecord?diagnosis_id=:id
|
||||
*/
|
||||
public function dailyTodayDietRecord()
|
||||
{
|
||||
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
|
||||
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
|
||||
if (!$check['ok']) {
|
||||
return $this->fail($check['error']);
|
||||
}
|
||||
|
||||
try {
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
|
||||
$record = DietRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('record_date', '>=', $todayStart)
|
||||
->where('record_date', '<=', $todayEnd)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
if (!$record) {
|
||||
return $this->data(['record' => null, 'today' => date('Y-m-d')]);
|
||||
}
|
||||
|
||||
$data = $record->toArray();
|
||||
$data['record_date'] = date('Y-m-d', (int) $data['record_date']);
|
||||
return $this->data(['record' => $data, 'today' => date('Y-m-d')]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 日常记录-保存今日饮食
|
||||
*
|
||||
* 路由:POST /api/tcm/dailySaveDietRecord
|
||||
*/
|
||||
public function dailySaveDietRecord()
|
||||
{
|
||||
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
|
||||
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
|
||||
if (!$check['ok']) {
|
||||
return $this->fail($check['error']);
|
||||
}
|
||||
$diagnosis = $check['diagnosis'];
|
||||
|
||||
$breakfast = trim((string) $this->request->post('breakfast_foods', ''));
|
||||
$lunch = trim((string) $this->request->post('lunch_foods', ''));
|
||||
$dinner = trim((string) $this->request->post('dinner_foods', ''));
|
||||
$note = trim((string) $this->request->post('note', ''));
|
||||
|
||||
if ($breakfast === '' && $lunch === '' && $dinner === '') {
|
||||
return $this->fail('请至少填写一餐饮食');
|
||||
}
|
||||
|
||||
try {
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
|
||||
$existing = DietRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('record_date', '>=', $todayStart)
|
||||
->where('record_date', '<=', $todayEnd)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
$payload = [
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'patient_id' => (int) $diagnosis['patient_id'],
|
||||
'record_date' => $todayStart,
|
||||
'breakfast_foods' => $breakfast,
|
||||
'lunch_foods' => $lunch,
|
||||
'dinner_foods' => $dinner,
|
||||
'note' => $note,
|
||||
];
|
||||
|
||||
if ($existing) {
|
||||
$payload['id'] = $existing['id'];
|
||||
DietRecord::update($payload);
|
||||
return $this->success('已更新今日饮食', ['id' => $existing['id']]);
|
||||
}
|
||||
|
||||
$created = DietRecord::create($payload);
|
||||
return $this->success('饮食记录已保存', ['id' => $created->id]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 日常记录-获取今日运动记录
|
||||
*
|
||||
* 路由:GET /api/tcm/dailyTodayExerciseRecord?diagnosis_id=:id
|
||||
*/
|
||||
public function dailyTodayExerciseRecord()
|
||||
{
|
||||
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
|
||||
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
|
||||
if (!$check['ok']) {
|
||||
return $this->fail($check['error']);
|
||||
}
|
||||
|
||||
try {
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
|
||||
$record = ExerciseRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('record_date', '>=', $todayStart)
|
||||
->where('record_date', '<=', $todayEnd)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
if (!$record) {
|
||||
return $this->data(['record' => null, 'today' => date('Y-m-d')]);
|
||||
}
|
||||
|
||||
$data = $record->toArray();
|
||||
$data['record_date'] = date('Y-m-d', (int) $data['record_date']);
|
||||
return $this->data(['record' => $data, 'today' => date('Y-m-d')]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 日常记录-保存今日运动
|
||||
*
|
||||
* 路由:POST /api/tcm/dailySaveExerciseRecord
|
||||
*/
|
||||
public function dailySaveExerciseRecord()
|
||||
{
|
||||
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
|
||||
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
|
||||
if (!$check['ok']) {
|
||||
return $this->fail($check['error']);
|
||||
}
|
||||
$diagnosis = $check['diagnosis'];
|
||||
|
||||
$exerciseType = trim((string) $this->request->post('exercise_type', ''));
|
||||
$durationRaw = $this->request->post('duration', '');
|
||||
$intensity = (int) $this->request->post('intensity', 2);
|
||||
$note = trim((string) $this->request->post('note', ''));
|
||||
|
||||
$duration = null;
|
||||
if ($durationRaw !== '' && $durationRaw !== null && is_numeric($durationRaw)) {
|
||||
$d = (int) $durationRaw;
|
||||
if ($d > 0) {
|
||||
$duration = $d;
|
||||
}
|
||||
}
|
||||
|
||||
if ($exerciseType === '' && $duration === null) {
|
||||
return $this->fail('请填写运动类型或时长');
|
||||
}
|
||||
|
||||
if ($intensity < 1 || $intensity > 3) {
|
||||
$intensity = 2;
|
||||
}
|
||||
|
||||
try {
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
|
||||
$existing = ExerciseRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('record_date', '>=', $todayStart)
|
||||
->where('record_date', '<=', $todayEnd)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
$payload = [
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'patient_id' => (int) $diagnosis['patient_id'],
|
||||
'record_date' => $todayStart,
|
||||
'exercise_type' => $exerciseType,
|
||||
'duration' => $duration,
|
||||
'intensity' => $intensity,
|
||||
'note' => $note,
|
||||
];
|
||||
|
||||
if ($existing) {
|
||||
$payload['id'] = $existing['id'];
|
||||
ExerciseRecord::update($payload);
|
||||
return $this->success('已更新今日运动', ['id' => $existing['id']]);
|
||||
}
|
||||
|
||||
$created = ExerciseRecord::create($payload);
|
||||
return $this->success('运动记录已保存', ['id' => $created->id]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 保存稳糖分 / 勋章 / 任务领奖状态
|
||||
*
|
||||
* 路由:POST /api/tcm/dailySaveGamify
|
||||
* 参数:diagnosis_id, points, badges(数组), task_awards(对象)
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function dailySaveGamify()
|
||||
{
|
||||
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
|
||||
$points = (int) $this->request->post('points', 0);
|
||||
$badges = $this->decodeJsonPostField($this->request->post('badges', ''), 'badges/a');
|
||||
$taskAwards = $this->decodeJsonPostField($this->request->post('task_awards', ''), 'task_awards/a');
|
||||
|
||||
$result = DailyGamifyLogic::saveState((int) $this->userId, $diagnosisId, $points, $badges, $taskAwards);
|
||||
if ($result === false) {
|
||||
return $this->fail(DailyGamifyLogic::getError());
|
||||
}
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 生成日常记录分享邀请码(仅当天有效)
|
||||
*
|
||||
* 路由:POST /api/tcm/dailyCreateShareInvite
|
||||
* 必填:diagnosis_id
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function dailyCreateShareInvite()
|
||||
{
|
||||
$diagnosisId = (int) $this->request->post('diagnosis_id', 0);
|
||||
$result = DailyShareLogic::createInvite((int) $this->userId, $diagnosisId);
|
||||
if ($result === false) {
|
||||
return $this->fail(DailyShareLogic::getError());
|
||||
}
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 凭邀请码查看分享战报(无需登录,含近7日血糖/血压记录)
|
||||
*
|
||||
* 路由:GET /api/tcm/dailySharePreview?invite_code=XXXXXXXX
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function dailySharePreview()
|
||||
{
|
||||
$inviteCode = (string) $this->request->get('invite_code', '');
|
||||
$viewerKey = (string) $this->request->get('viewer_key', '');
|
||||
$result = DailyShareLogic::previewByInviteCode($inviteCode, $viewerKey);
|
||||
if ($result === false) {
|
||||
return $this->fail(DailyShareLogic::getError());
|
||||
}
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 家人点赞(邀请观看页,无需登录)
|
||||
*
|
||||
* 路由:POST /api/tcm/dailyFamilyLike
|
||||
* 必填:invite_code, viewer_key
|
||||
* 可选:nickname(家人/亲友/老伴 等)
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function dailyFamilyLike()
|
||||
{
|
||||
$inviteCode = (string) $this->request->post('invite_code', '');
|
||||
$viewerKey = (string) $this->request->post('viewer_key', '');
|
||||
$nickname = (string) $this->request->post('nickname', '');
|
||||
$result = DailyFamilyLikeLogic::addLike($inviteCode, $viewerKey, $nickname);
|
||||
if ($result === false) {
|
||||
return $this->fail(DailyFamilyLikeLogic::getError());
|
||||
}
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 患者查看今日家人点赞汇总
|
||||
*
|
||||
* 路由:GET /api/tcm/dailyFamilyLikeSummary?diagnosis_id=
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function dailyFamilyLikeSummary()
|
||||
{
|
||||
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
|
||||
$result = DailyFamilyLikeLogic::summaryForPatient((int) $this->userId, $diagnosisId);
|
||||
if ($result === false) {
|
||||
return $this->fail(DailyFamilyLikeLogic::getError());
|
||||
}
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 根据订单号获取订单详情(供小程序支付页调用)
|
||||
* @return \think\response\Json
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\logic\tcm;
|
||||
|
||||
use app\common\model\tcm\DailyFamilyLike;
|
||||
use app\common\model\tcm\DailyShareInvite;
|
||||
|
||||
/**
|
||||
* 家人点赞(邀请观看页 → 患者日常页展示)
|
||||
*/
|
||||
class DailyFamilyLikeLogic
|
||||
{
|
||||
protected static string $error = '';
|
||||
|
||||
/** @var string[] */
|
||||
protected static array $praisePool = [
|
||||
'坚持得很好,为您点赞!',
|
||||
'每天记录真棒,继续保持!',
|
||||
'您的自律让人佩服!',
|
||||
'加油,家人一直支持您!',
|
||||
'稳糖路上,您并不孤单!',
|
||||
'好习惯正在养成,真为您高兴!',
|
||||
];
|
||||
|
||||
public static function getError(): string
|
||||
{
|
||||
return self::$error;
|
||||
}
|
||||
|
||||
protected static function setError(string $msg): bool
|
||||
{
|
||||
self::$error = $msg;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验当日邀请码
|
||||
*
|
||||
* @return array{diagnosis_id:int,invite_code:string}|false
|
||||
*/
|
||||
protected static function resolveInvite(string $inviteCode): array|false
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
return [
|
||||
'diagnosis_id' => (int) $row['diagnosis_id'],
|
||||
'invite_code' => $inviteCode,
|
||||
];
|
||||
}
|
||||
|
||||
protected static function normalizeViewerKey(string $viewerKey): string
|
||||
{
|
||||
$viewerKey = preg_replace('/[^\w\-]/', '', trim($viewerKey)) ?? '';
|
||||
if (strlen($viewerKey) < 8) {
|
||||
return '';
|
||||
}
|
||||
return substr($viewerKey, 0, 64);
|
||||
}
|
||||
|
||||
protected static function normalizeNickname(string $nickname): string
|
||||
{
|
||||
$nickname = trim($nickname);
|
||||
if ($nickname === '') {
|
||||
return '家人';
|
||||
}
|
||||
$nickname = mb_substr($nickname, 0, 8, 'UTF-8');
|
||||
return $nickname !== '' ? $nickname : '家人';
|
||||
}
|
||||
|
||||
public static function countToday(int $diagnosisId, ?string $likeDate = null): int
|
||||
{
|
||||
$likeDate = $likeDate ?: date('Y-m-d');
|
||||
return (int) DailyFamilyLike::where('diagnosis_id', $diagnosisId)
|
||||
->where('like_date', $likeDate)
|
||||
->count();
|
||||
}
|
||||
|
||||
public static function likedByViewer(int $diagnosisId, string $viewerKey, ?string $likeDate = null): bool
|
||||
{
|
||||
$viewerKey = self::normalizeViewerKey($viewerKey);
|
||||
if ($viewerKey === '') {
|
||||
return false;
|
||||
}
|
||||
$likeDate = $likeDate ?: date('Y-m-d');
|
||||
return DailyFamilyLike::where('diagnosis_id', $diagnosisId)
|
||||
->where('like_date', $likeDate)
|
||||
->where('viewer_key', $viewerKey)
|
||||
->find() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请预览附加点赞信息
|
||||
*/
|
||||
public static function metaForInvite(string $inviteCode, string $viewerKey = ''): array
|
||||
{
|
||||
$resolved = self::resolveInvite($inviteCode);
|
||||
if ($resolved === false) {
|
||||
return [
|
||||
'like_count' => 0,
|
||||
'liked_by_me' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$diagnosisId = $resolved['diagnosis_id'];
|
||||
$viewerKey = self::normalizeViewerKey($viewerKey);
|
||||
|
||||
return [
|
||||
'like_count' => self::countToday($diagnosisId),
|
||||
'liked_by_me' => $viewerKey !== '' && self::likedByViewer($diagnosisId, $viewerKey),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 家人点赞(无需登录)
|
||||
*/
|
||||
public static function addLike(string $inviteCode, string $viewerKey, string $nickname = ''): array|false
|
||||
{
|
||||
self::$error = '';
|
||||
$resolved = self::resolveInvite($inviteCode);
|
||||
if ($resolved === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$viewerKey = self::normalizeViewerKey($viewerKey);
|
||||
if ($viewerKey === '') {
|
||||
return self::setError('设备标识无效,请重试') ? false : false;
|
||||
}
|
||||
|
||||
$diagnosisId = $resolved['diagnosis_id'];
|
||||
$likeDate = date('Y-m-d');
|
||||
$nickname = self::normalizeNickname($nickname);
|
||||
|
||||
$exists = DailyFamilyLike::where('diagnosis_id', $diagnosisId)
|
||||
->where('like_date', $likeDate)
|
||||
->where('viewer_key', $viewerKey)
|
||||
->find();
|
||||
|
||||
if ($exists) {
|
||||
return [
|
||||
'already_liked' => true,
|
||||
'like_count' => self::countToday($diagnosisId, $likeDate),
|
||||
'praise_message' => '您今天已经点过赞啦,谢谢您的鼓励',
|
||||
'nickname' => (string) ($exists['nickname'] ?? '家人'),
|
||||
];
|
||||
}
|
||||
|
||||
$todayCount = self::countToday($diagnosisId, $likeDate);
|
||||
if ($todayCount >= 50) {
|
||||
return self::setError('今日点赞已满,明天再来吧') ? false : false;
|
||||
}
|
||||
|
||||
DailyFamilyLike::create([
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'like_date' => $likeDate,
|
||||
'invite_code' => $resolved['invite_code'],
|
||||
'viewer_key' => $viewerKey,
|
||||
'nickname' => $nickname,
|
||||
'create_time' => time(),
|
||||
]);
|
||||
|
||||
$likeCount = self::countToday($diagnosisId, $likeDate);
|
||||
$idx = $likeCount > 0 ? ($likeCount - 1) % count(self::$praisePool) : 0;
|
||||
|
||||
return [
|
||||
'already_liked' => false,
|
||||
'like_count' => $likeCount,
|
||||
'praise_message' => self::$praisePool[$idx],
|
||||
'nickname' => $nickname,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 患者查看今日家人点赞(需登录且拥有诊单)
|
||||
*/
|
||||
public static function summaryForPatient(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;
|
||||
}
|
||||
|
||||
$likeDate = date('Y-m-d');
|
||||
$count = self::countToday($diagnosisId, $likeDate);
|
||||
|
||||
$rows = DailyFamilyLike::where('diagnosis_id', $diagnosisId)
|
||||
->where('like_date', $likeDate)
|
||||
->order('id', 'desc')
|
||||
->limit(8)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$recent = [];
|
||||
foreach ($rows as $r) {
|
||||
$recent[] = [
|
||||
'nickname' => (string) ($r['nickname'] ?? '家人'),
|
||||
'time_label' => self::timeLabel((int) ($r['create_time'] ?? 0)),
|
||||
];
|
||||
}
|
||||
|
||||
$summaryLine = $count > 0
|
||||
? "今日有 {$count} 位家人为您点赞,继续加油!"
|
||||
: '分享战报给家人,邀请他们为您点赞鼓劲';
|
||||
|
||||
return [
|
||||
'like_count' => $count,
|
||||
'summary_line' => $summaryLine,
|
||||
'recent' => $recent,
|
||||
];
|
||||
}
|
||||
|
||||
protected static function timeLabel(int $ts): string
|
||||
{
|
||||
if ($ts <= 0) {
|
||||
return '刚刚';
|
||||
}
|
||||
$diff = time() - $ts;
|
||||
if ($diff < 60) {
|
||||
return '刚刚';
|
||||
}
|
||||
if ($diff < 3600) {
|
||||
return (int) floor($diff / 60) . '分钟前';
|
||||
}
|
||||
return date('H:i', $ts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\logic\tcm;
|
||||
|
||||
use app\common\model\tcm\BloodRecord;
|
||||
use app\common\model\tcm\DailyGamify;
|
||||
use app\common\model\tcm\DietRecord;
|
||||
use app\common\model\tcm\ExerciseRecord;
|
||||
|
||||
/**
|
||||
* 稳糖分 / 勋章 / 浇水领奖
|
||||
*/
|
||||
class DailyGamifyLogic
|
||||
{
|
||||
protected static string $error = '';
|
||||
|
||||
/** @var array<string,array{name:string,points:int}> */
|
||||
protected static array $taskDefs = [
|
||||
'blood' => ['name' => '测血糖血压', 'points' => 10],
|
||||
'diet' => ['name' => '记今日饮食', 'points' => 10],
|
||||
'exercise' => ['name' => '记今日运动', 'points' => 10],
|
||||
];
|
||||
|
||||
public static function getError(): string
|
||||
{
|
||||
return self::$error;
|
||||
}
|
||||
|
||||
protected static function setError(string $msg): bool
|
||||
{
|
||||
self::$error = $msg;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static function assertOwned(int $userId, int $diagnosisId): bool
|
||||
{
|
||||
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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function todayRange(): array
|
||||
{
|
||||
return [
|
||||
strtotime(date('Y-m-d 00:00:00')),
|
||||
strtotime(date('Y-m-d 23:59:59')),
|
||||
];
|
||||
}
|
||||
|
||||
protected static function hasValue($v): bool
|
||||
{
|
||||
if ($v === null || $v === '' || $v === '0' || $v === 0) {
|
||||
return false;
|
||||
}
|
||||
if (is_string($v)) {
|
||||
return trim($v) !== '';
|
||||
}
|
||||
return is_numeric($v) && (float) $v > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 今日任务是否已完成(依据真实业务记录)
|
||||
*/
|
||||
public static function evaluateTaskCompletion(int $diagnosisId): array
|
||||
{
|
||||
[$start, $end] = self::todayRange();
|
||||
|
||||
$blood = BloodRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('source', 1)
|
||||
->where('record_date', '>=', $start)
|
||||
->where('record_date', '<=', $end)
|
||||
->find();
|
||||
|
||||
$bloodDone = false;
|
||||
if ($blood) {
|
||||
$bloodDone = self::hasValue($blood['fasting_blood_sugar'] ?? null)
|
||||
|| self::hasValue($blood['postprandial_blood_sugar'] ?? null)
|
||||
|| self::hasValue($blood['other_blood_sugar'] ?? null)
|
||||
|| self::hasValue($blood['systolic_pressure'] ?? null)
|
||||
|| self::hasValue($blood['diastolic_pressure'] ?? null);
|
||||
}
|
||||
|
||||
$diet = DietRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('record_date', '>=', $start)
|
||||
->where('record_date', '<=', $end)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
$dietDone = false;
|
||||
if ($diet) {
|
||||
$dietDone = self::hasValue($diet['breakfast_foods'] ?? null)
|
||||
|| self::hasValue($diet['lunch_foods'] ?? null)
|
||||
|| self::hasValue($diet['dinner_foods'] ?? null);
|
||||
}
|
||||
|
||||
$exercise = ExerciseRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('record_date', '>=', $start)
|
||||
->where('record_date', '<=', $end)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
$exerciseDone = false;
|
||||
if ($exercise) {
|
||||
$exerciseDone = self::hasValue($exercise['exercise_type'] ?? null)
|
||||
|| self::hasValue($exercise['duration'] ?? null);
|
||||
}
|
||||
|
||||
return [
|
||||
'blood' => $bloodDone,
|
||||
'diet' => $dietDone,
|
||||
'exercise' => $exerciseDone,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,array<string,bool>> $taskAwards
|
||||
* @return array<int,array{id:string,name:string,points:int,completed:bool,claimed:bool}>
|
||||
*/
|
||||
public static function buildTodayTasks(int $diagnosisId, array $taskAwards): array
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
$awards = isset($taskAwards[$today]) && is_array($taskAwards[$today]) ? $taskAwards[$today] : [];
|
||||
$completion = self::evaluateTaskCompletion($diagnosisId);
|
||||
$list = [];
|
||||
|
||||
foreach (self::$taskDefs as $id => $def) {
|
||||
$list[] = [
|
||||
'id' => $id,
|
||||
'name' => $def['name'],
|
||||
'points' => $def['points'],
|
||||
'completed' => !empty($completion[$id]),
|
||||
'claimed' => !empty($awards[$id]),
|
||||
];
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
protected static function treeMeta(int $points): array
|
||||
{
|
||||
$level = min(4, (int) floor($points / 100));
|
||||
$names = ['嫩芽期', '成长雏形', '枝繁叶茂', '含苞待放', '花开稳糖'];
|
||||
return [
|
||||
'tree_level' => $level,
|
||||
'tree_progress' => $level >= 4 ? 100 : ($points % 100),
|
||||
'tree_level_name' => $names[$level] ?? '嫩芽期',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取稳糖乐园状态(含今日任务)
|
||||
*/
|
||||
public static function getState(int $userId, int $diagnosisId): array|false
|
||||
{
|
||||
self::$error = '';
|
||||
if (!self::assertOwned($userId, $diagnosisId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$row = DailyGamify::where('diagnosis_id', $diagnosisId)
|
||||
->where('user_id', $userId)
|
||||
->find();
|
||||
|
||||
$points = $row ? (int) $row['points'] : 0;
|
||||
$badges = $row ? self::decodeJsonArray((string) ($row['badges'] ?? '')) : [];
|
||||
$taskAwards = $row ? self::decodeJsonObject((string) ($row['task_awards'] ?? '')) : [];
|
||||
|
||||
$todayTasks = self::buildTodayTasks($diagnosisId, $taskAwards);
|
||||
$claimable = 0;
|
||||
foreach ($todayTasks as $t) {
|
||||
if ($t['completed'] && !$t['claimed']) {
|
||||
$claimable += (int) $t['points'];
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge([
|
||||
'points' => $points,
|
||||
'badges' => $badges,
|
||||
'task_awards' => $taskAwards,
|
||||
'today_tasks' => $todayTasks,
|
||||
'claimable_points' => $claimable,
|
||||
], self::treeMeta($points));
|
||||
}
|
||||
|
||||
/**
|
||||
* 浇水:领取今日已完成且未领取的任务积分
|
||||
*/
|
||||
public static function waterTree(int $userId, int $diagnosisId): array|false
|
||||
{
|
||||
self::$error = '';
|
||||
if (!self::assertOwned($userId, $diagnosisId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$row = DailyGamify::where('diagnosis_id', $diagnosisId)
|
||||
->where('user_id', $userId)
|
||||
->find();
|
||||
|
||||
$points = $row ? (int) $row['points'] : 0;
|
||||
$badges = $row ? self::decodeJsonArray((string) ($row['badges'] ?? '')) : [];
|
||||
$taskAwards = $row ? self::decodeJsonObject((string) ($row['task_awards'] ?? '')) : [];
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$todayTasks = self::buildTodayTasks($diagnosisId, $taskAwards);
|
||||
$addedPoints = 0;
|
||||
$claimedIds = [];
|
||||
$pending = [];
|
||||
|
||||
if (!isset($taskAwards[$today]) || !is_array($taskAwards[$today])) {
|
||||
$taskAwards[$today] = [];
|
||||
}
|
||||
|
||||
foreach ($todayTasks as $task) {
|
||||
if ($task['completed'] && !$task['claimed']) {
|
||||
$id = (string) $task['id'];
|
||||
$taskAwards[$today][$id] = true;
|
||||
$addedPoints += (int) $task['points'];
|
||||
$claimedIds[] = $id;
|
||||
} elseif (!$task['completed']) {
|
||||
$pending[] = [
|
||||
'id' => $task['id'],
|
||||
'name' => $task['name'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($addedPoints <= 0) {
|
||||
$claimable = 0;
|
||||
foreach ($todayTasks as $t) {
|
||||
if ($t['completed'] && !$t['claimed']) {
|
||||
$claimable += (int) $t['points'];
|
||||
}
|
||||
}
|
||||
$refreshedTasks = self::buildTodayTasks($diagnosisId, $taskAwards);
|
||||
return [
|
||||
'added_points' => 0,
|
||||
'claimed_tasks' => [],
|
||||
'claimable_points' => $claimable,
|
||||
'pending_tasks' => $pending,
|
||||
'points' => $points,
|
||||
'badges' => $badges,
|
||||
'task_awards' => $taskAwards,
|
||||
'today_tasks' => $refreshedTasks,
|
||||
'message' => $claimable > 0 ? '请先点击浇水领取积分' : (count($pending) ? '请先完成今日任务再浇水' : '今日奖励已全部领取'),
|
||||
] + self::treeMeta($points);
|
||||
}
|
||||
|
||||
$newPoints = $points + $addedPoints;
|
||||
$saved = self::saveState($userId, $diagnosisId, $newPoints, $badges, $taskAwards);
|
||||
if ($saved === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$refreshed = self::buildTodayTasks($diagnosisId, $taskAwards);
|
||||
|
||||
return [
|
||||
'added_points' => $addedPoints,
|
||||
'claimed_tasks' => $claimedIds,
|
||||
'claimable_points' => 0,
|
||||
'pending_tasks' => $pending,
|
||||
'points' => $newPoints,
|
||||
'badges' => $badges,
|
||||
'task_awards' => $taskAwards,
|
||||
'today_tasks' => $refreshed,
|
||||
'message' => "浇水成功,获得 {$addedPoints} 稳糖积分",
|
||||
] + self::treeMeta($newPoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $badges
|
||||
* @param array<string,array<string,bool>> $taskAwards
|
||||
*/
|
||||
public static function saveState(int $userId, int $diagnosisId, int $points, array $badges, array $taskAwards): array|false
|
||||
{
|
||||
self::$error = '';
|
||||
if (!self::assertOwned($userId, $diagnosisId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$points = max(0, (int) $points);
|
||||
$badges = array_values(array_unique(array_filter(array_map('strval', $badges))));
|
||||
if (!is_array($taskAwards)) {
|
||||
$taskAwards = [];
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$row = DailyGamify::where('diagnosis_id', $diagnosisId)
|
||||
->where('user_id', $userId)
|
||||
->find();
|
||||
|
||||
$data = [
|
||||
'points' => $points,
|
||||
'badges' => json_encode($badges, JSON_UNESCAPED_UNICODE),
|
||||
'task_awards' => json_encode($taskAwards, JSON_UNESCAPED_UNICODE),
|
||||
'update_time' => $now,
|
||||
];
|
||||
|
||||
if ($row) {
|
||||
DailyGamify::where('id', (int) $row['id'])->update($data);
|
||||
} else {
|
||||
$data['diagnosis_id'] = $diagnosisId;
|
||||
$data['user_id'] = $userId;
|
||||
$data['create_time'] = $now;
|
||||
DailyGamify::create($data);
|
||||
}
|
||||
|
||||
return [
|
||||
'points' => $points,
|
||||
'badges' => $badges,
|
||||
'task_awards' => $taskAwards,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地缓存迁到服务端:取 points/badges/task_awards 的较大合并
|
||||
*/
|
||||
public static function mergeFromClient(int $userId, int $diagnosisId, int $points, array $badges, array $taskAwards): array|false
|
||||
{
|
||||
$server = self::getState($userId, $diagnosisId);
|
||||
if ($server === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mergedPoints = max((int) $server['points'], max(0, $points));
|
||||
$mergedBadges = array_values(array_unique(array_merge($server['badges'], $badges)));
|
||||
$mergedAwards = $server['task_awards'];
|
||||
foreach ($taskAwards as $date => $tasks) {
|
||||
if (!is_array($tasks)) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($mergedAwards[$date]) || !is_array($mergedAwards[$date])) {
|
||||
$mergedAwards[$date] = [];
|
||||
}
|
||||
foreach ($tasks as $taskId => $flag) {
|
||||
if ($flag) {
|
||||
$mergedAwards[$date][(string) $taskId] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::saveState($userId, $diagnosisId, $mergedPoints, $mergedBadges, $mergedAwards);
|
||||
}
|
||||
|
||||
protected static function decodeJsonArray(string $json): array
|
||||
{
|
||||
if ($json === '') {
|
||||
return [];
|
||||
}
|
||||
$data = json_decode($json, true);
|
||||
return is_array($data) ? array_values(array_map('strval', $data)) : [];
|
||||
}
|
||||
|
||||
protected static function decodeJsonObject(string $json): array
|
||||
{
|
||||
if ($json === '') {
|
||||
return [];
|
||||
}
|
||||
$data = json_decode($json, true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model\tcm;
|
||||
|
||||
use app\common\model\BaseModel;
|
||||
|
||||
/**
|
||||
* 日常记录家人点赞
|
||||
*/
|
||||
class DailyFamilyLike extends BaseModel
|
||||
{
|
||||
protected $name = 'tcm_daily_family_like';
|
||||
|
||||
protected $autoWriteTimestamp = false;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model\tcm;
|
||||
|
||||
use app\common\model\BaseModel;
|
||||
|
||||
/**
|
||||
* 日常记录游戏化(稳糖分 / 勋章 / 任务领奖)
|
||||
*/
|
||||
class DailyGamify extends BaseModel
|
||||
{
|
||||
protected $name = 'tcm_daily_gamify';
|
||||
|
||||
protected $autoWriteTimestamp = false;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model\tcm;
|
||||
|
||||
use app\common\model\BaseModel;
|
||||
|
||||
/**
|
||||
* 日常记录分享邀请码
|
||||
*/
|
||||
class DailyShareInvite extends BaseModel
|
||||
{
|
||||
protected $name = 'tcm_daily_share_invite';
|
||||
|
||||
protected $autoWriteTimestamp = false;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 家人点赞(邀请码观看页点赞,按诊单+日期汇总,患者端可见)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `zyt_tcm_daily_family_like` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`diagnosis_id` int unsigned NOT NULL COMMENT '诊单ID',
|
||||
`like_date` date NOT NULL COMMENT '点赞日期(仅当日有效)',
|
||||
`invite_code` varchar(16) NOT NULL DEFAULT '' COMMENT '来源邀请码',
|
||||
`viewer_key` varchar(64) NOT NULL COMMENT '访客设备标识,防重复点赞',
|
||||
`nickname` varchar(16) NOT NULL DEFAULT '家人' COMMENT '显示称呼',
|
||||
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_day_viewer` (`diagnosis_id`, `like_date`, `viewer_key`),
|
||||
KEY `idx_diagnosis_date` (`diagnosis_id`, `like_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='日常记录家人点赞';
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 日常记录游戏化:稳糖分、勋章、每日任务领奖记录(按诊单+用户)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `zyt_tcm_daily_gamify` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`diagnosis_id` int unsigned NOT NULL COMMENT '诊单ID',
|
||||
`user_id` int unsigned NOT NULL COMMENT '小程序用户ID',
|
||||
`points` int unsigned NOT NULL DEFAULT 0 COMMENT '稳糖分累计',
|
||||
`badges` text COMMENT '已解锁勋章ID列表 JSON',
|
||||
`task_awards` mediumtext COMMENT '每日任务领奖记录 JSON,键为日期',
|
||||
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||
`update_time` int unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_diagnosis_user` (`diagnosis_id`, `user_id`),
|
||||
KEY `idx_user_id` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='日常记录稳糖分与勋章';
|
||||
@@ -0,0 +1,13 @@
|
||||
-- 日常记录分享邀请码(仅当天有效,用于家属查看脱敏战报)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `zyt_tcm_daily_share_invite` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`invite_code` varchar(16) NOT NULL COMMENT '邀请码',
|
||||
`diagnosis_id` int unsigned NOT NULL COMMENT '诊单ID',
|
||||
`user_id` int unsigned NOT NULL DEFAULT 0 COMMENT '分享人小程序用户ID',
|
||||
`invite_date` date NOT NULL COMMENT '有效日期,仅当天可查看',
|
||||
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_invite_code` (`invite_code`),
|
||||
KEY `idx_diagnosis_date` (`diagnosis_id`, `invite_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='日常记录分享邀请码';
|
||||
Reference in New Issue
Block a user