Files
mini_tp/app/smt/controller/v1/Smoke.php
T
2026-04-26 09:24:08 +08:00

485 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\smt\controller\v1;
use app\smt\common\Response;
use app\smt\controller\BaseController;
use app\smt\service\AchievementService;
use app\smt\service\QuitPlanService;
use app\smt\service\SmokeAiService;
use app\smt\service\SmokeService;
use app\smt\service\Support;
use think\App;
class Smoke extends BaseController
{
protected $smokeService;
protected $smokeAiService;
protected $quitPlanService;
protected $achievementService;
public function __construct(App $app)
{
parent::__construct($app);
$this->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,
];
}
}