smokeService = new SmokeService(); $this->smokeAiService = new SmokeAiService(); $this->quitPlanService = new QuitPlanService(); $this->achievementService = new AchievementService(); } public function home() { try { $user = $this->getCurrentSmtUser(); $uid = (int) $user['id']; $now = Support::now(); $planDate = Support::dateOnly($now); $profileView = $this->smokeService->getProfileView($uid); $defaultSuggestion = $this->smokeService->getDefaultNextSuggestion($uid, $now, $planDate, $profileView); $homeSummary = $this->smokeService->getHomeSummary($uid, $now); $motivation = $this->smokeService->motivation($uid, $profileView['profile'], $now); $adviceDate = Support::dateOnly($now->modify('-1 day')); $adviceCard = [ 'title' => '智能控烟建议', 'date' => $adviceDate->format(Support::DATE_LAYOUT), 'message' => '', 'model' => '', 'status' => 'empty', ]; try { $advice = $this->smokeAiService->getOrGenerateAdvice($user, $adviceDate, 'v2'); $adviceCard['message'] = (string) ($advice['advice'] ?? ''); $adviceCard['model'] = (string) ($advice['model'] ?? ''); $adviceCard['status'] = 'available'; } catch (\RuntimeException $e) { if ($e->getCode() === 403) { $adviceCard['status'] = 'locked'; } elseif ($e->getCode() === 400) { $adviceCard['status'] = 'no_data'; } else { $adviceCard['status'] = 'unavailable'; } } $timer = [ 'label' => '距上次抽烟', 'last_smoke_at' => (string) ($homeSummary['last_smoke_at'] ?? ''), 'seconds_since_last' => (int) ($homeSummary['seconds_since_last'] ?? -1), 'next_suggested_at' => (string) ($defaultSuggestion['next_smoke_at'] ?? ''), 'next_suggested_clock' => Support::formatClock((string) ($defaultSuggestion['next_smoke_at'] ?? '')), 'not_before_at' => (string) ($defaultSuggestion['next_smoke_at'] ?? ''), 'suggestion_source' => 'default', 'suggestion_algorithm' => (string) ($defaultSuggestion['algorithm'] ?? ''), ]; $cachedAiNext = $this->smokeAiService->getCachedNextSmoke($user, $planDate, 'v1'); if ($cachedAiNext) { $timer['suggestion_source'] = 'ai'; $timer['suggestion_algorithm'] = 'ai_next_smoke_v1'; $timer['next_suggested_at'] = (string) ($cachedAiNext['suggested_at'] ?? ''); $timer['next_suggested_clock'] = Support::formatClock((string) ($cachedAiNext['suggested_at'] ?? '')); $timer['not_before_at'] = (string) ($cachedAiNext['not_before_at'] ?? ''); $timer['ai_time_nodes'] = $cachedAiNext['time_nodes'] ?? []; $timer['ai_advice'] = (string) ($cachedAiNext['advice'] ?? ''); $timer['ai_model'] = (string) ($cachedAiNext['model'] ?? ''); } $dailySummaryRecord = $this->smokeAiService->getCachedByType($uid, SmokeAiService::TYPE_DAILY_SUMMARY, $planDate, 'v1'); $dailySummary = null; if ($dailySummaryRecord) { $dailySummary = [ 'date' => (string) ($dailySummaryRecord['date'] ?? $planDate->format(Support::DATE_LAYOUT)), 'content' => (string) ($dailySummaryRecord['content'] ?? ''), 'model' => (string) ($dailySummaryRecord['model'] ?? ''), 'status' => 'available', ]; } return Response::success([ 'greeting' => $this->buildGreeting((string) ($user['nickname'] ?? ''), (string) ($user['avatar_url'] ?? ''), $now), 'profile' => $profileView, 'advice_card' => $adviceCard, 'campaign_card' => [ 'title' => '绿色生活,从戒烟开始', 'subtitle' => 'BRAND CAMPAIGN', 'badge' => '广告', ], 'timer' => $timer, 'summary' => [ 'today_count' => (int) ($homeSummary['today_count'] ?? 0), 'daily_target' => (int) (($profileView['profile']['baseline_cigs_per_day'] ?? 0)), 'resisted_count' => (int) ($homeSummary['resisted_count'] ?? 0), 'reduced_from_yesterday' => (int) ($homeSummary['reduced_from_yesterday'] ?? 0), 'exceeded_yesterday' => (bool) ($homeSummary['exceeded_yesterday'] ?? false), 'profile_completed' => (bool) ($profileView['is_completed'] ?? false), ], 'daily_summary' => $dailySummary, 'motivation' => $motivation, 'quick_actions' => [ ['type' => 'log_smoke', 'title' => '记录抽烟', 'primary' => false], ['type' => 'resist', 'title' => '想抽忍住了', 'primary' => true], ], 'data_sources' => [ 'ai_advice_date' => $adviceDate->format(Support::DATE_LAYOUT), 'plan_date' => $planDate->format(Support::DATE_LAYOUT), ], ]); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function profile() { try { return Response::success($this->smokeService->getProfileView($this->getCurrentSmtUserId())); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function saveProfile() { try { return Response::success($this->smokeService->upsertProfile($this->getCurrentSmtUserId(), $this->request->post())); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function createLog() { try { return Response::success($this->smokeService->createLog($this->getCurrentSmtUserId(), $this->request->post(), false)); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function createResistedLog() { try { return Response::success($this->smokeService->createLog($this->getCurrentSmtUserId(), $this->request->post(), true)); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function readLog(int $id) { try { return Response::success($this->smokeService->getLog($this->getCurrentSmtUserId(), $id)); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function logs() { try { return Response::success($this->smokeService->listLogs($this->getCurrentSmtUserId(), $this->request->get())); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function latestLogs() { try { return Response::success($this->smokeService->latestLogs($this->getCurrentSmtUserId(), (int) $this->request->get('limit', 20))); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function updateLog(int $id) { try { return Response::success($this->smokeService->updateLog($this->getCurrentSmtUserId(), $id, $this->request->post())); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function deleteLog(int $id) { try { return Response::success($this->smokeService->deleteLog($this->getCurrentSmtUserId(), $id)); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function dashboard() { try { return Response::success($this->smokeService->dashboard($this->getCurrentSmtUserId(), $this->request->get())); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function stats() { try { $uid = $this->getCurrentSmtUserId(); return Response::success($this->smokeService->stats($uid, $this->request->get(), $this->smokeService->getProfile($uid))); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function motivation() { try { $uid = $this->getCurrentSmtUserId(); return Response::success($this->smokeService->motivation($uid, $this->smokeService->getProfile($uid))); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function nextSmokeTime() { try { $user = $this->getCurrentSmtUser(); $uid = (int) $user['id']; $now = Support::now(); $planDate = $this->resolvePlanDate((string) $this->request->get('date', ''), $now); if ($planDate < Support::dateOnly($now)) { throw new \RuntimeException('date 不能早于今天', 400); } $mode = strtolower(trim((string) $this->request->get('mode', 'auto'))); if (!in_array($mode, ['auto', 'ai', 'default'], true)) { throw new \RuntimeException('mode 参数错误,应为 auto|ai|default', 400); } $profileView = $this->smokeService->getProfileView($uid); $defaultSuggestion = $this->smokeService->getDefaultNextSuggestion($uid, $now, $planDate, $profileView); $homeSummary = $this->smokeService->getHomeSummary($uid, $now); $response = [ 'source' => 'default', 'not_before_at' => (string) ($defaultSuggestion['next_smoke_at'] ?? ''), 'suggested_at' => (string) ($defaultSuggestion['next_smoke_at'] ?? ''), 'last_smoke_at' => (string) ($homeSummary['last_smoke_at'] ?? ''), 'today_count' => (int) ($homeSummary['today_count'] ?? 0), 'resisted_count' => (int) ($homeSummary['resisted_count'] ?? 0), 'reduced_from_yesterday' => (int) ($homeSummary['reduced_from_yesterday'] ?? 0), 'exceeded_yesterday' => (bool) ($homeSummary['exceeded_yesterday'] ?? false), 'default' => $defaultSuggestion, ]; if ($mode !== 'default') { $aiSuggestion = $mode === 'ai' ? $this->smokeAiService->getOrGenerateNextSmoke($user, $now, $planDate, 'v1', $defaultSuggestion) : $this->smokeAiService->getCachedNextSmoke($user, $planDate, 'v1'); if ($aiSuggestion) { $response['source'] = 'ai'; $response['not_before_at'] = (string) ($aiSuggestion['not_before_at'] ?? ''); $response['suggested_at'] = (string) ($aiSuggestion['suggested_at'] ?? ''); $response['time_nodes'] = $aiSuggestion['time_nodes'] ?? []; $response['advice'] = (string) ($aiSuggestion['advice'] ?? ''); $response['ai'] = $aiSuggestion; } } return Response::success($response); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function aiNextSmokeTime() { return $this->nextSmokeTime(); } public function aiAdvice() { try { $user = $this->getCurrentSmtUser(); $date = !empty($this->request->get('date')) ? Support::parseDate((string) $this->request->get('date'), 'date') : Support::dateOnly(Support::now()->modify('-1 day')); $record = $this->smokeAiService->getOrGenerateAdvice($user, $date, 'v2'); return Response::success([ 'date' => (string) ($record['date'] ?? $date->format(Support::DATE_LAYOUT)), 'advice' => (string) ($record['advice'] ?? ''), 'model' => (string) ($record['model'] ?? ''), ]); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function unlockAiAdvice() { try { $date = !empty($this->request->post('date')) ? Support::parseDate((string) $this->request->post('date'), 'date') : Support::dateOnly(Support::now()->modify('-1 day')); return Response::success($this->smokeAiService->unlock($this->getCurrentSmtUser(), $date)); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function aiDailySummary() { try { $user = $this->getCurrentSmtUser(); $date = !empty($this->request->get('date')) ? Support::parseDate((string) $this->request->get('date'), 'date') : Support::dateOnly(); $record = $this->smokeAiService->getOrGenerateDailySummary($user, $date, 'v1'); return Response::success([ 'date' => (string) ($record['date'] ?? $date->format(Support::DATE_LAYOUT)), 'content' => (string) ($record['content'] ?? ''), 'model' => (string) ($record['model'] ?? ''), ]); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function createShare() { try { return Response::success($this->smokeService->createShare($this->getCurrentSmtUserId(), $this->request->post())); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function shareRead(string $token) { try { return Response::success($this->smokeService->getShareView($token, $this->request->get())); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function revokeShare(string $token) { try { return Response::success($this->smokeService->revokeShare($this->getCurrentSmtUserId(), $token)); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function generateQuitPlan() { try { return Response::success($this->quitPlanService->generate($this->getCurrentSmtUserId(), $this->request->post())); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function quitPlan() { try { return Response::success($this->quitPlanService->getActivePlan($this->getCurrentSmtUserId())); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function quitPlanDays() { try { $planId = (int) $this->request->get('plan_id', 0); return Response::success($this->quitPlanService->getPlanDays($this->getCurrentSmtUserId(), $planId > 0 ? $planId : null)); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function resetQuitPlan() { try { return Response::success($this->quitPlanService->reset($this->getCurrentSmtUserId(), $this->request->post())); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function achievementThemes() { try { return Response::success(['themes' => $this->achievementService->listActiveThemes()]); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } public function achievement() { try { $uid = $this->getCurrentSmtUserId(); $profile = $this->smokeService->getProfile($uid); if (!$profile || empty($profile['achievement_theme_id'])) { return Response::success(['achievement' => null]); } $days = $this->smokeService->getStreakDays($uid); $achievement = $this->achievementService->getUserAchievement((int) $profile['achievement_theme_id'], $days); return Response::success(['achievement' => $achievement]); } catch (\Throwable $e) { return Response::error($e->getMessage(), $e->getCode() ?: 500); } } private function resolvePlanDate(string $raw, \DateTimeImmutable $now): \DateTimeImmutable { $value = strtolower(trim($raw)); if ($value === '' || $value === 'today') { return Support::dateOnly($now); } if ($value === 'tomorrow') { return Support::dateOnly($now->modify('+1 day')); } return Support::parseDate($value, 'date'); } private function buildGreeting(string $nickname, string $avatarUrl, \DateTimeImmutable $now): array { $nickname = trim($nickname) !== '' ? trim($nickname) : '朋友'; $hour = (int) $now->format('H'); if ($hour >= 5 && $hour < 11) { [$timeOfDay, $title, $subtitle] = ['morning', '早安', '今天也是清爽的一天']; } elseif ($hour >= 11 && $hour < 14) { [$timeOfDay, $title, $subtitle] = ['noon', '午安', '补充水分和能量']; } elseif ($hour >= 14 && $hour < 19) { [$timeOfDay, $title, $subtitle] = ['afternoon', '下午好', '把烟瘾留在昨天']; } else { [$timeOfDay, $title, $subtitle] = ['evening', '晚上好', '今晚早点休息']; } return [ 'title' => $title . ',' . $nickname, 'subtitle' => $subtitle, 'nickname' => $nickname, 'time_of_day' => $timeOfDay, 'avatar_url' => $avatarUrl, ]; } }