*/ protected static array $taskDefs = [ 'glucose' => ['name' => '测血糖', 'points' => 10], 'bp' => ['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; } $diagnosis = \app\common\model\tcm\Diagnosis::where('id', $diagnosisId) ->where('delete_time', null) ->field('show_card') ->find(); if (!$diagnosis || (int) ($diagnosis['show_card'] ?? 1) !== 1) { 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(); $glucoseDone = false; $bpDone = false; if ($blood) { $glucoseDone = self::hasValue($blood['fasting_blood_sugar'] ?? null) || self::hasValue($blood['postprandial_blood_sugar'] ?? null) || self::hasValue($blood['other_blood_sugar'] ?? null); $bpDone = 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 [ 'glucose' => $glucoseDone, 'bp' => $bpDone, 'diet' => $dietDone, 'exercise' => $exerciseDone, ]; } /** * @param array> $taskAwards * @return array */ 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' => self::isTaskClaimed($id, $awards, $completion), ]; } return $list; } /** * 任务是否已领取(兼容旧版 blood 合并任务) * * @param array $awards * @param array $completion */ protected static function isTaskClaimed(string $id, array $awards, array $completion = []): bool { if (!empty($awards[$id])) { return true; } // 旧版 blood 一次性领取:对应分项当日已有记录则视为已领,避免拆分后重复领奖/轮换引导 if (!empty($awards['blood'])) { if ($id === 'glucose' && !empty($completion['glucose'])) { return true; } if ($id === 'bp' && !empty($completion['bp'])) { return true; } } return false; } /** 与前端 tongji/utils/treeLevels.js 保持一致 */ protected const TREE_MAX_LEVEL = 9; protected const TREE_XP_PER_LEVEL = 50; protected static function treeMeta(int $points): array { $points = max(0, (int) $points); $level = min(self::TREE_MAX_LEVEL, (int) floor($points / self::TREE_XP_PER_LEVEL)); $names = ['种子眠', '破土芽', '展两叶', '小树苗', '青枝繁', '拔节高', '稳糖冠', '初绽香', '漫开花', '圆满树']; $xpIn = $level >= self::TREE_MAX_LEVEL ? self::TREE_XP_PER_LEVEL : ($points % self::TREE_XP_PER_LEVEL); $progress = $level >= self::TREE_MAX_LEVEL ? 100 : (int) round(($xpIn / self::TREE_XP_PER_LEVEL) * 100); $nextName = $level < self::TREE_MAX_LEVEL ? ($names[$level + 1] ?? '') : ''; $pointsToNext = $level >= self::TREE_MAX_LEVEL ? 0 : (self::TREE_XP_PER_LEVEL - $xpIn); return [ 'tree_level' => $level, 'tree_progress' => $progress, 'tree_level_name' => $names[$level] ?? '种子眠', 'tree_xp_in_level' => $xpIn, 'tree_xp_need' => self::TREE_XP_PER_LEVEL, 'tree_points_next' => $pointsToNext, 'tree_next_name' => $nextName, 'tree_is_max' => $level >= self::TREE_MAX_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 $badges * @param array> $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 : []; } }