request->param('month', date('Y-m')); if (!Validate::regex($month, '/^\d{4}-\d{2}$/')) { $month = date('Y-m'); } $data = CycleService::calendar($this->auth->id, $month); $this->success('', $data); } /** * 今日周期状态 */ public function current() { $this->success('', CycleService::current($this->auth->id)); } /** * 未来 3 个月预测 */ public function predict() { $this->success('', CycleService::predict($this->auth->id, 3)); } /** * 记录 / 修改一次经期 * POST start_date=2026-11-01 end_date=2026-11-05 */ public function save() { $userId = $this->auth->id; $startDate = $this->request->post('start_date', ''); $endDate = $this->request->post('end_date', ''); if (!$startDate || !strtotime($startDate)) { $this->error('请选择经期开始日期'); } if ($endDate && strtotime($endDate) < strtotime($startDate)) { $this->error('结束日期不能早于开始日期'); } if (strtotime($startDate) > time() + 86400) { $this->error('开始日期不能晚于今天'); } $periodLength = $endDate ? (int)((strtotime($endDate) - strtotime($startDate)) / 86400) + 1 : 5; if ($periodLength > 15) { $this->error('经期天数超出合理范围'); } $exist = Db::name('cycle') ->where('user_id', $userId) ->where('start_date', $startDate) ->whereNull('deletetime') ->find(); $params = CycleService::params($userId); $data = [ 'user_id' => $userId, 'start_date' => $startDate, 'end_date' => $endDate ?: null, 'period_length' => $periodLength, 'cycle_length' => $params['cycle_length'], 'ovulation_date' => date('Y-m-d', strtotime($startDate) + ($params['cycle_length'] - 14) * 86400), 'updatetime' => time(), ]; if ($exist) { Db::name('cycle')->where('id', $exist['id'])->update($data); $id = $exist['id']; } else { $data['createtime'] = time(); $id = Db::name('cycle')->insertGetId($data); } // 重新计算历史周期的平均长度(新周期写入后再回填) $cycles = CycleService::cycles($userId, 6); if (count($cycles) >= 2) { $days = (int)((strtotime($cycles[0]['start_date']) - strtotime($cycles[1]['start_date'])) / 86400); if ($days >= 18 && $days <= 60) { Db::name('cycle')->where('id', $cycles[0]['id'])->update(['cycle_length' => $days]); } } $this->success('记录成功', [ 'id' => (int)$id, 'cycle' => CycleService::current($userId), ]); } /** * 保存每日症状记录(同日覆盖) * POST log_date, flow, pain_level, symptom_ids, mood, sleep_hours, sex, note */ public function log() { $userId = $this->auth->id; $logDate = $this->request->post('log_date', date('Y-m-d')); if (!strtotime($logDate)) { $this->error('日期格式不正确'); } if (strtotime($logDate) > strtotime('+1 day')) { $this->error('不能记录未来日期'); } $data = [ 'user_id' => $userId, 'log_date' => $logDate, 'flow' => min(3, max(0, (int)$this->request->post('flow', 0))), 'pain_level' => min(5, max(0, (int)$this->request->post('pain_level', 0))), 'symptom_ids' => (string)$this->request->post('symptom_ids', ''), 'mood' => min(2, max(-2, (int)$this->request->post('mood', 0))), 'sex' => min(2, max(0, (int)$this->request->post('sex', 0))), 'note' => mb_substr((string)$this->request->post('note', ''), 0, 500), 'updatetime' => time(), ]; $sleep = $this->request->post('sleep_hours', ''); if ($sleep !== '' && is_numeric($sleep)) { $data['sleep_hours'] = min(24, max(0, round((float)$sleep, 1))); } $exist = Db::name('cycle_log') ->where('user_id', $userId) ->where('log_date', $logDate) ->whereNull('deletetime') ->find(); if ($exist) { Db::name('cycle_log')->where('id', $exist['id'])->update($data); $id = $exist['id']; } else { $data['createtime'] = time(); $id = Db::name('cycle_log')->insertGetId($data); } $this->success('已保存', ['id' => (int)$id]); } /** * 某天的记录详情 * GET date=2026-11-02 */ public function loginfo() { $date = $this->request->param('date', date('Y-m-d')); $log = Db::name('cycle_log') ->where('user_id', $this->auth->id) ->where('log_date', $date) ->whereNull('deletetime') ->find(); if ($log) { $log['symptom_ids'] = $log['symptom_ids'] === '' ? [] : array_map('intval', explode(',', $log['symptom_ids'])); } $this->success('', [ 'date' => $date, 'log' => $log ?: null, 'tags' => $this->tagsData(), ]); } /** * 症状标签(记录面板用) */ public function tags() { $this->success('', ['list' => $this->tagsData()]); } protected function tagsData() { $list = Db::name('symptom_tag') ->where('status', 'normal') ->where(function ($query) { $query->where('is_preset', 1)->whereOr('user_id', $this->auth->id); }) ->order('weigh', 'desc') ->select(); return array_map(function ($item) { return [ 'id' => (int)$item['id'], 'name' => $item['name'], 'icon' => $item['icon'], 'category' => $item['category'], ]; }, $list); } /** * 周期汇总 */ public function summary() { $this->success('', CycleService::summary($this->auth->id)); } /** * 健康分析报告(基础免费,深度为会员权益) * GET type=month|quarter|year month=2026-11 */ public function report() { $userId = $this->auth->id; $type = $this->request->param('type', 'month'); $month = $this->request->param('month', date('Y-m')); $start = date('Y-m-01', strtotime($month . '-01')); $end = date('Y-m-t', strtotime($month . '-01')); $logs = Db::name('cycle_log') ->where('user_id', $userId) ->whereNull('deletetime') ->whereBetween('log_date', [$start, $end]) ->select(); $painList = []; $flowList = []; $symptomCount = []; $sleepTotal = 0; $sleepCount = 0; foreach ($logs as $log) { if ($log['pain_level'] > 0) { $painList[] = (int)$log['pain_level']; } if ($log['flow'] > 0) { $flowList[] = (int)$log['flow']; } foreach (array_filter(explode(',', $log['symptom_ids'])) as $sid) { $symptomCount[$sid] = isset($symptomCount[$sid]) ? $symptomCount[$sid] + 1 : 1; } if ($log['sleep_hours'] !== null && $log['sleep_hours'] > 0) { $sleepTotal += (float)$log['sleep_hours']; $sleepCount++; } } arsort($symptomCount); $topSymptomIds = array_slice(array_keys($symptomCount), 0, 5); $tagNames = Db::name('symptom_tag')->whereIn('id', $topSymptomIds ?: [0])->column('name', 'id'); $topSymptoms = []; foreach ($topSymptomIds as $sid) { if (isset($tagNames[$sid])) { $topSymptoms[] = ['name' => $tagNames[$sid], 'count' => $symptomCount[$sid]]; } } $moodLogs = Db::name('mood_log') ->where('user_id', $userId) ->whereNull('deletetime') ->whereBetween('log_date', [$start, $end]) ->select(); $moodCount = []; foreach ($moodLogs as $m) { $moodCount[$m['mood']] = isset($moodCount[$m['mood']]) ? $moodCount[$m['mood']] + 1 : 1; } $summary = CycleService::summary($userId); $deep = MemberService::can($userId, 'report.deep'); $this->success('', [ 'type' => $type, 'month' => $month, 'range' => [$start, $end], 'summary' => $summary, 'stats' => [ 'log_days' => count($logs), 'avg_pain' => $painList ? round(array_sum($painList) / count($painList), 1) : 0, 'avg_flow' => $flowList ? round(array_sum($flowList) / count($flowList), 1) : 0, 'avg_sleep' => $sleepCount ? round($sleepTotal / $sleepCount, 1) : 0, 'top_symptoms' => $topSymptoms, 'mood_count' => $moodCount, 'mood_days' => count($moodLogs), ], 'access' => [ 'deep' => $deep, 'need_member' => !$deep, 'tip' => $deep ? '' : '开通会员可查看深度分析与导出报告', ], 'advice' => $this->advice($painList, $sleepCount ? $sleepTotal / $sleepCount : 0, $summary), ]); } protected function advice($painList, $avgSleep, $summary) { $list = []; $avgPain = $painList ? array_sum($painList) / count($painList) : 0; if ($avgPain >= 3) { $list[] = '本期痛经程度偏高,建议热敷并记录是否影响日常活动;若持续加重请及时就医。'; } if ($avgSleep > 0 && $avgSleep < 6.5) { $list[] = '本期平均睡眠 ' . round($avgSleep, 1) . ' 小时,偏少。优先固定起床时间并减少睡前屏幕。'; } if ($summary['regularity'] > 0 && $summary['regularity'] < 70) { $list[] = '周期波动略大,建议连续记录 3 个周期后再评估;压力与作息是常见影响因素。'; } if (!$list) { $list[] = '本期整体状态平稳,保持规律记录会让预测更准确。'; } $list[] = '以上内容仅供参考,不能替代医疗诊断。'; return $list; } /** * 删除某次经期记录(软删除) */ public function remove() { $id = (int)$this->request->post('id', 0); $row = Db::name('cycle')->where('id', $id)->where('user_id', $this->auth->id)->find(); if (!$row) { $this->error('记录不存在'); } Db::name('cycle')->where('id', $id)->update(['deletetime' => time()]); $this->success('已删除'); } }