485 lines
19 KiB
PHP
485 lines
19 KiB
PHP
<?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,
|
||
];
|
||
}
|
||
}
|