feat: add smt module

This commit is contained in:
nepiedg
2026-04-26 09:24:08 +08:00
parent 69eb3e5019
commit 613e4a58a9
78 changed files with 4629 additions and 5673 deletions
+248
View File
@@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace app\smt\service;
use app\smt\model\SmokeQuitPlan;
use app\smt\model\SmokeQuitPlanDay;
use app\smt\model\SmokeUserProfile;
use think\facade\Db;
class QuitPlanService
{
public const STATUS_ACTIVE = 'active';
public const STATUS_FAILED = 'failed';
public const STAGE_RECORDING = 'recording';
public const STAGE_REDUCING = 'reducing';
public const STAGE_CONSOLIDATING = 'consolidating';
public function generate(int $userId, array $data = []): array
{
if ($this->findActivePlan($userId)) {
throw new \RuntimeException('已有进行中的戒烟计划,请先重置', 409);
}
$profile = SmokeUserProfile::findByUid($userId);
if (!$profile || (int) $profile->baseline_cigs_per_day <= 0) {
throw new \RuntimeException('请先完成个人资料填写', 400);
}
$startDate = Support::parseDate((string) ($data['start_date'] ?? ''), 'start_date') ?? Support::dateOnly();
$endDate = $startDate->modify('+29 day');
$days = $this->buildPlanDays($profile->toArray(), $startDate);
$now = time();
$summary = $this->buildSummary($profile->toArray());
Db::connect('mysql')->transaction(function () use ($userId, $profile, $startDate, $endDate, $days, $summary, $now) {
$plan = new SmokeQuitPlan();
$plan->uid = $userId;
$plan->status = self::STATUS_ACTIVE;
$plan->start_date = $startDate->format(Support::DATE_LAYOUT);
$plan->end_date = $endDate->format(Support::DATE_LAYOUT);
$plan->baseline_cigs_per_day = (int) $profile->baseline_cigs_per_day;
$plan->smoking_years = (float) $profile->smoking_years;
$plan->pack_price_cent = (int) $profile->pack_price_cent;
$plan->current_stage = self::STAGE_RECORDING;
$plan->current_day = 1;
$plan->completed_days = 0;
$plan->prompt_version = 'rule_v1';
$plan->provider = 'built-in';
$plan->model = 'rule-based';
$plan->summary = $summary;
$plan->createtime = $now;
$plan->updatetime = $now;
$plan->save();
foreach ($days as $day) {
$row = new SmokeQuitPlanDay();
$row->plan_id = (int) $plan->id;
$row->uid = $userId;
$row->plan_date = $day['plan_date'];
$row->stage = $day['stage'];
$row->day = $day['day'];
$row->target_cigs = $day['target_cigs'];
$row->target_reduced = $day['target_reduced'] ? 1 : 0;
$row->advice = $day['advice'];
$row->createtime = $now;
$row->updatetime = $now;
$row->save();
}
});
return $this->getActivePlan($userId);
}
public function reset(int $userId, array $data = []): array
{
$existing = $this->findActivePlan($userId);
if ($existing) {
$existing->status = self::STATUS_FAILED;
$existing->updatetime = time();
$existing->save();
}
return $this->generate($userId, $data);
}
public function getActivePlan(int $userId): array
{
$plan = $this->findActivePlan($userId);
if (!$plan) {
throw new \RuntimeException('暂无进行中的戒烟计划', 404);
}
$today = Support::dateOnly();
$startDate = Support::dateOnly((string) $plan->start_date);
if ($today < $startDate) {
$currentDay = 1;
} else {
$currentDay = max(1, min(30, (int) $startDate->diff($today)->days + 1));
}
$currentStage = $this->resolveStageByDay($currentDay);
$dayPlan = SmokeQuitPlanDay::where('uid', $userId)
->where('plan_date', $today->format(Support::DATE_LAYOUT))
->whereNull('deleted_at')
->find();
return [
'id' => (int) $plan->id,
'status' => (string) $plan->status,
'start_date' => (string) $plan->start_date,
'end_date' => (string) $plan->end_date,
'current_stage' => $currentStage,
'current_day' => $currentDay,
'completed_days' => (int) $plan->completed_days,
'baseline_cigs' => (int) $plan->baseline_cigs_per_day,
'summary' => (string) $plan->summary,
'today_target' => $dayPlan ? (int) $dayPlan->target_cigs : null,
'today_advice' => $dayPlan ? (string) $dayPlan->advice : null,
];
}
public function getPlanDays(int $userId, ?int $planId = null): array
{
if ($planId === null || $planId <= 0) {
$active = $this->findActivePlan($userId);
if (!$active) {
throw new \RuntimeException('暂无进行中的戒烟计划', 404);
}
$planId = (int) $active->id;
}
$plan = SmokeQuitPlan::where('id', $planId)->where('uid', $userId)->whereNull('deleted_at')->find();
if (!$plan) {
throw new \RuntimeException('戒烟计划不存在', 404);
}
$rows = SmokeQuitPlanDay::where('plan_id', $planId)
->whereNull('deleted_at')
->order('day', 'asc')
->select()
->all();
$days = array_map(static function ($row) {
return [
'day' => (int) $row->day,
'plan_date' => (string) $row->plan_date,
'stage' => (string) $row->stage,
'target_cigs' => (int) $row->target_cigs,
'target_reduced' => (bool) $row->target_reduced,
'advice' => (string) $row->advice,
'actual_cigs' => $row->actual_cigs !== null ? (int) $row->actual_cigs : null,
'resisted_cnt' => $row->resisted_cnt !== null ? (int) $row->resisted_cnt : null,
'achieved' => $row->achieved !== null ? (bool) $row->achieved : null,
];
}, $rows);
return [
'plan_id' => $planId,
'days' => $days,
];
}
private function findActivePlan(int $userId): ?SmokeQuitPlan
{
return SmokeQuitPlan::where('uid', $userId)
->where('status', self::STATUS_ACTIVE)
->whereNull('deleted_at')
->find();
}
private function buildPlanDays(array $profile, \DateTimeImmutable $startDate): array
{
$baseline = max(1, (int) ($profile['baseline_cigs_per_day'] ?? 0));
$quitMotivation = Support::jsonArray($profile['quit_motivations'] ?? []);
$smokeMotivation = Support::jsonArray($profile['smoke_motivations'] ?? []);
$days = [];
$previousTarget = $baseline;
$reducingFloor = max(1, (int) round($baseline * 0.4));
for ($day = 1; $day <= 30; $day++) {
$stage = $this->resolveStageByDay($day);
if ($day <= 7) {
$target = $baseline;
} elseif ($day <= 21) {
$progress = ($day - 8) / 13;
$target = (int) round($baseline - (($baseline - $reducingFloor) * $progress));
} else {
$progress = ($day - 22) / 8;
$target = (int) round($reducingFloor * (1 - $progress));
}
$target = max(0, min($baseline, $target));
$days[] = [
'day' => $day,
'plan_date' => $startDate->modify('+' . ($day - 1) . ' day')->format(Support::DATE_LAYOUT),
'stage' => $stage,
'target_cigs' => $target,
'target_reduced' => $day > 1 && $target < $previousTarget,
'advice' => $this->buildDayAdvice($stage, $target, $quitMotivation, $smokeMotivation),
];
$previousTarget = $target;
}
return $days;
}
private function buildSummary(array $profile): string
{
$baseline = max(1, (int) ($profile['baseline_cigs_per_day'] ?? 0));
$motivation = Support::jsonArray($profile['quit_motivations'] ?? []);
$goal = $motivation[0] ?? '让自己回到更轻松的状态';
return sprintf(
'这是一个 30 天的渐进式戒烟计划:前 7 天建立稳定记录,第 8-21 天逐步减量,最后 9 天把目标压低到接近清零。请始终把“%s”作为你每天复盘时的提醒。当前基线按日均 %d 支计算。',
$goal,
$baseline
);
}
private function buildDayAdvice(string $stage, int $target, array $quitMotivation, array $smokeMotivation): string
{
$quitLine = $quitMotivation[0] ?? '提醒自己坚持的理由';
$smokeLine = $smokeMotivation[0] ?? '高压或社交场景';
if ($stage === self::STAGE_RECORDING) {
return sprintf('今天先把记录做完整,目标控制在 %d 支以内。特别留意 %s 这些触发场景,先不急着求完美,把每次想抽前的情绪和时间点记下来。', $target, $smokeLine);
}
if ($stage === self::STAGE_REDUCING) {
return sprintf('今天目标降到 %d 支。把最容易多抽的一个时段延后 10-15 分钟,用喝水、走动或深呼吸替代,复盘时提醒自己:%s。', $target, $quitLine);
}
return sprintf('今天进入巩固期,目标控制在 %d 支。优先守住“第一根烟”和“最晚一根烟”两个节点,把注意力放在连续完成目标上,继续记住:%s。', $target, $quitLine);
}
private function resolveStageByDay(int $day): string
{
if ($day <= 7) {
return self::STAGE_RECORDING;
}
if ($day <= 21) {
return self::STAGE_REDUCING;
}
return self::STAGE_CONSOLIDATING;
}
}