后端(新增 7 控制器 + 2 服务 + 17 张表) - Prepare:排卵预测、BBT(双向体温识别)、试纸识别(规则引擎+额度)、备孕任务、同房指导 - Family:邀请码绑定(≤5)、话术库、爱心积分、私密留言、家庭健康视图 - Health:按周期阶段输出饮食/运动/睡眠/痛经四类建议 - Relation:关系测评、引导卡、冲突工具、课程下单、双人任务(联动记忆)、纪念日 - Parenting:宝宝档案、成长记录、月龄活动匹配、共育任务、育儿情绪、疫苗儿保排期 - Consult:咨询师(资质/定价/时段)、预约下单/取消、量表测评(4 套)、危机干预转介 - Setting:自定义提醒 CRUD、数据备份快照与恢复(保留 5 份) - Mood 增补急救包;Home 提醒合并自定义提醒 - 服务层:ContentService(统一内容/锁态/购买态)、AssessmentService(计分分级+危机识别) 前端(新增 8 页面) - 备孕、家人共护、健康建议、两性关系、亲子教育、心理咨询、测评执行页、情绪急救包 - 首页快捷入口扩至 8 个模块;我的页新增提醒中心与数据备份 - utils/api.js 扩至 84 个接口 数据(可重复执行脚本) - 03_modules.sql:fa_reminder/bbt/lh_test/task/family_bind/care_message/point_log/memory/ assessment(+record)/course_order/baby/growth_record/vaccine/counselor/consult_order/user_backup - 04_module_seed.sql:话术6/引导卡6/冲突3/急救4/同房3/课程6/活动6/育儿情绪4/知识7、 4 套量表、17 项疫苗儿保、3 位咨询师 验证(全部通过) - 接口全链路 82 项断言(M1–M9,含高风险测评危机干预、备份恢复) - 契约校验 84 接口 0 偏差;Vue SFC 19/19、SCSS 18/18 编译通过 - 修复 3 个真实缺陷:action 内调用 action 导致响应被劫持、coTasks 返回数组、 咨询时段跨年错误
238 行
8.0 KiB
PHP
238 行
8.0 KiB
PHP
<?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' => '敏感易累是生理性的,允许自己慢一点',
|
||
];
|
||
}
|
||
}
|
||
// 自定义提醒(吃药 / 记录 / 复诊等)
|
||
$custom = Db::name('reminder')
|
||
->where('user_id', $this->auth->id)
|
||
->where('enabled', 1)
|
||
->whereNull('deletetime')
|
||
->order('remind_time', 'asc')
|
||
->select();
|
||
$today = date('Y-m-d');
|
||
foreach ($custom as $item) {
|
||
if ($item['repeat_rule'] === 'once' && $item['remind_date'] !== $today) {
|
||
continue;
|
||
}
|
||
$list[] = [
|
||
'type' => $item['type'] ?: 'custom',
|
||
'icon' => '⏰',
|
||
'title' => $item['title'],
|
||
'desc' => substr($item['remind_time'], 0, 5) . ' 提醒',
|
||
];
|
||
}
|
||
|
||
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))];
|
||
}
|
||
}
|