文件
huaqiapp/server/application/api/controller/Home.php
linda 5ea1b461f3 feat: 花期 V1.0 纵向切片(暖色调设计系统 + 周期/情绪/会员全链路)
前端(uni-app · Vue3)
- 设计系统:uni.scss 品牌令牌(花红 #E8604C / 蜜橙 #F5A65B / 暖米白 #FFF9F5),
  App.vue 全局原子类(卡片/按钮/胶囊/单元格/空态/进度)
- 页面:登录、首页(聚合)、周期(花瓣日历+当日记录+预测+概览)、情绪(记录+趋势+日记+疗愈)、
  我的(资料/会员/隐私/提醒)、健康报告、会员套餐、内容详情
- 组件:hq-tabbar(自定义底栏)、bloom-calendar(阶段日历)
- 基础层:utils/{config,request,store,api,date}.js(统一 token、401 处理、接口集中定义)

后端(FastAdmin / ThinkPHP 5.0)
- 新增 api 控制器:Home / Cycle / Mood / Content / Member / Setting,User 增补 detail、updateprofile
- 新增服务层:CycleService(四阶段计算、预测、月历、汇总)、MemberService(权益判定)
- 新增表:fa_cycle / fa_cycle_log / fa_symptom_tag / fa_mood_log / fa_content /
  fa_content_category / fa_member_plan / fa_member_order / fa_user_setting
- 初始化数据:20 个症状标签、28 条内容(心语/科普/知识/食谱/运动/睡眠/痛经/冥想)、3 档会员

验证(全部通过)
- 接口全链路 48 项断言(登录→周期→首页→情绪→内容→会员→设置→资料)
- 前后端契约静态校验 24 个接口 0 偏差
- Vue SFC 编译 11/11、SCSS 编译 10/10
- 验证脚本见 scripts/verify/,报告见 docs/04-前端设计系统与联调验证.md
2026-11-05 22:54:02 +08:00

218 行
7.3 KiB
PHP

此文件含有不可见的 Unicode 字符

此文件含有人类无法区分的不可见的 Unicode 字符,但可以由计算机进行不同的处理。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\api\controller;
use app\common\controller\Api;
use app\common\service\CycleService;
use app\common\service\MemberService;
use think\Db;
/**
* 首页聚合接口
*
* 一次请求返回首页全部数据,减少端上串行请求,保证首屏流畅。
*/
class Home extends Api
{
protected $noNeedLogin = [];
protected $noNeedRight = '*';
public function index()
{
$userId = $this->auth->id;
$today = date('Y-m-d');
$hour = (int)date('H');
if ($hour < 6) {
$greeting = '夜深了';
} elseif ($hour < 11) {
$greeting = '早上好';
} elseif ($hour < 14) {
$greeting = '中午好';
} elseif ($hour < 18) {
$greeting = '下午好';
} else {
$greeting = '晚上好';
}
$cycle = CycleService::current($userId);
$loggedToday = Db::name('cycle_log')
->where('user_id', $userId)
->where('log_date', $today)
->whereNull('deletetime')
->count() > 0;
$this->success('', [
'greeting' => $greeting,
'nickname' => $this->auth->nickname ?: '亲爱的',
'today' => $today,
'date_text' => date('n月j日') . ' ' . $this->weekText($today),
'cycle' => $cycle,
'logged_today' => $loggedToday,
'phase_tip' => $this->phaseTip($cycle),
'jijing' => $this->pickJijing($cycle['phase'], $today),
'reminders' => $this->reminders($cycle, $loggedToday),
'mood' => $this->moodSummary($userId),
'member' => MemberService::status($userId),
]);
}
/**
* 按阶段/日期稳定挑选一条心语(同一天结果不变)
*/
protected function pickJijing($phase, $today)
{
$pool = Db::name('content')
->where('type', 'jijing')
->where('status', 'normal')
->where('verify', 'passed')
->order('weigh', 'desc')
->select();
if (!$pool) {
return null;
}
$matched = [];
foreach ($pool as $item) {
$phases = array_filter(explode(',', $item['phase']));
if (!$phase || in_array('all', $phases) || in_array($phase, $phases)) {
$matched[] = $item;
}
}
if (!$matched) {
$matched = $pool;
}
$index = crc32($today . ($phase ?: 'all')) % count($matched);
$item = $matched[$index];
return [
'id' => (int)$item['id'],
'title' => $item['title'],
'summary' => $item['summary'],
'body' => $item['body'],
'emoji' => $item['cover_emoji'],
'access' => $item['access'],
];
}
/**
* 阶段建议文案
*/
protected function phaseTip($cycle)
{
if (!$cycle['has_data']) {
return [
'title' => '先记录一次经期',
'desc' => '记录后即可获得阶段预测与个性化建议',
'action' => '去记录',
];
}
$map = [
'menstrual' => ['keyword' => '这几天适合放慢节奏', 'desc' => '注意保暖、补铁,减少高强度运动'],
'follicular' => ['keyword' => '能量回升期', 'desc' => '精力与情绪较好,适合安排计划与运动'],
'ovulation' => ['keyword' => '易孕期', 'desc' => '宫颈黏液变清亮,注意私处清洁与补水'],
'luteal' => ['keyword' => '黄体期', 'desc' => '情绪易波动,优先睡眠与温热饮食'],
];
$tip = $map[$cycle['phase']] ?: $map['follicular'];
return [
'title' => $tip['keyword'],
'desc' => $tip['desc'],
'action' => '',
];
}
/**
* 今日提醒(由周期与记录状态推导,非冗余推送)
*/
protected function reminders($cycle, $loggedToday)
{
$list = [];
if (!$loggedToday) {
$list[] = [
'type' => 'record',
'icon' => '✍️',
'title' => '今天还没有记录',
'desc' => '花 10 秒记录身体感受,预测会更准',
];
}
if ($cycle['has_data']) {
if ($cycle['is_period']) {
$list[] = [
'type' => 'care',
'icon' => '🌡',
'title' => '经期第 ' . $cycle['day'] . ' 天',
'desc' => '小腹保暖 + 温热饮食,避免冰饮',
];
} elseif ($cycle['is_fertile']) {
$list[] = [
'type' => 'fertile',
'icon' => '🌱',
'title' => '易孕期',
'desc' => '排卵期前后,注意休息与营养',
];
} elseif ($cycle['days_to_next'] !== null && $cycle['days_to_next'] <= 3 && $cycle['days_to_next'] >= 1) {
$list[] = [
'type' => 'period',
'icon' => '📅',
'title' => '经期还有 ' . $cycle['days_to_next'] . ' 天',
'desc' => '可以提前准备卫生用品与暖贴',
];
} elseif ($cycle['phase'] === 'luteal') {
$list[] = [
'type' => 'mood',
'icon' => '🍂',
'title' => '黄体期留意情绪',
'desc' => '敏感易累是生理性的,允许自己慢一点',
];
}
}
return $list;
}
/**
* 近 7 天情绪概览
*/
protected function moodSummary($userId)
{
$start = date('Y-m-d', strtotime('-6 day'));
$logs = Db::name('mood_log')
->where('user_id', $userId)
->whereNull('deletetime')
->where('log_date', '>=', $start)
->order('log_date', 'asc')
->select();
$scoreMap = ['happy' => 5, 'calm' => 4, 'low' => 2, 'anxious' => 2, 'angry' => 1];
$days = [];
for ($i = 6; $i >= 0; $i--) {
$d = date('Y-m-d', strtotime('-' . $i . ' day'));
$days[$d] = ['date' => $d, 'score' => null, 'mood' => ''];
}
foreach ($logs as $log) {
if (isset($days[$log['log_date']])) {
$days[$log['log_date']]['score'] = isset($scoreMap[$log['mood']]) ? $scoreMap[$log['mood']] : 3;
$days[$log['log_date']]['mood'] = $log['mood'];
}
}
$recorded = array_values(array_filter($days, function ($d) {
return $d['score'] !== null;
}));
$avg = $recorded ? round(array_sum(array_column($recorded, 'score')) / count($recorded), 1) : 0;
return [
'days' => array_values($days),
'recorded' => count($recorded),
'avg_score' => $avg,
'today_mood' => isset($days[date('Y-m-d')]) ? $days[date('Y-m-d')]['mood'] : '',
];
}
protected function weekText($date)
{
$weeks = ['日', '一', '二', '三', '四', '五', '六'];
return '星期' . $weeks[(int)date('w', strtotime($date))];
}
}