文件
linda d243ece39c feat: 补齐 M1–M9 全部 47 个功能点并打通前后端
后端(新增 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 返回数组、
  咨询时段跨年错误
2026-11-05 23:27:21 +08:00

325 行
11 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\api\controller;
use app\common\controller\Api;
use app\common\service\AssessmentService;
use app\common\service\ContentService;
use think\Db;
/**
* 亲子教育接口(M8)
*
* 育儿知识库 / 成长记录 / 亲子活动 / 课程 / 共育任务 / 育儿情绪 / 疫苗与儿保
*/
class Parenting extends Api
{
protected $noNeedLogin = [];
protected $noNeedRight = '*';
/**
* 亲子模块总览
*/
public function index()
{
$userId = $this->auth->id;
$baby = Db::name('baby')->where('user_id', $userId)->where('status', 'normal')->order('id', 'desc')->find();
$monthAge = $baby ? $this->monthAge($baby) : null;
$this->success('', [
'baby' => $this->formatBaby($baby),
'month_age' => $monthAge,
'activities' => ContentService::lists($userId, ['type' => 'activity', 'limit' => 6])['list'],
'courses' => ['list' => $this->coursesData()],
'co_tasks' => $this->coTasksData(),
'emotions' => ContentService::lists($userId, ['type' => 'emotion', 'limit' => 4])['list'],
'vaccines' => $this->vaccinePlan($baby),
'growth' => $this->growthListData($baby),
'assessments' => AssessmentService::lists('parenting', $userId),
]);
}
/**
* 宝宝档案
*/
public function babyInfo()
{
$baby = Db::name('baby')->where('user_id', $this->auth->id)->where('status', 'normal')->order('id', 'desc')->find();
$this->success('', ['baby' => $this->formatBaby($baby), 'month_age' => $baby ? $this->monthAge($baby) : null]);
}
public function babySave()
{
$userId = $this->auth->id;
$data = [
'user_id' => $userId,
'nickname' => mb_substr((string)$this->request->post('nickname', '宝宝'), 0, 30),
'gender' => min(2, max(0, (int)$this->request->post('gender', 0))),
'status' => 'normal',
];
$birthday = (string)$this->request->post('birthday', '');
$dueDate = (string)$this->request->post('due_date', '');
if ($birthday && strtotime($birthday)) {
$data['birthday'] = date('Y-m-d', strtotime($birthday));
}
if ($dueDate && strtotime($dueDate)) {
$data['due_date'] = date('Y-m-d', strtotime($dueDate));
}
$exist = Db::name('baby')->where('user_id', $userId)->where('status', 'normal')->find();
if ($exist) {
Db::name('baby')->where('id', $exist['id'])->update($data);
$id = $exist['id'];
} else {
$data['createtime'] = time();
$id = Db::name('baby')->insertGetId($data);
}
$baby = Db::name('baby')->where('id', $id)->find();
$this->success('已保存', ['baby' => $this->formatBaby($baby), 'month_age' => $this->monthAge($baby)]);
}
/**
* 成长记录
*/
protected function growthListData($baby = null)
{
if (!$baby) {
$baby = Db::name('baby')->where('user_id', $this->auth->id)->where('status', 'normal')->order('id', 'desc')->find();
}
if (!$baby) {
return [];
}
$rows = Db::name('growth_record')
->where('baby_id', $baby['id'])
->order('record_date', 'desc')
->limit(30)
->select();
return array_map(function ($row) {
return [
'id' => (int)$row['id'],
'record_date' => $row['record_date'],
'height' => $row['height'] !== null ? (float)$row['height'] : null,
'weight' => $row['weight'] !== null ? (float)$row['weight'] : null,
'milestone' => $row['milestone'],
'note' => $row['note'],
];
}, $rows);
}
public function growthSave()
{
$userId = $this->auth->id;
$baby = Db::name('baby')->where('user_id', $userId)->where('status', 'normal')->order('id', 'desc')->find();
if (!$baby) {
$this->error('请先创建宝宝档案');
}
$date = (string)$this->request->post('record_date', date('Y-m-d'));
if (!strtotime($date)) {
$this->error('日期格式不正确');
}
$height = $this->request->post('height', '');
$weight = $this->request->post('weight', '');
Db::name('growth_record')->insert([
'user_id' => $userId,
'baby_id' => $baby['id'],
'record_date' => $date,
'height' => is_numeric($height) ? round((float)$height, 1) : null,
'weight' => is_numeric($weight) ? round((float)$weight, 2) : null,
'milestone' => mb_substr((string)$this->request->post('milestone', ''), 0, 100),
'note' => mb_substr((string)$this->request->post('note', ''), 0, 300),
'createtime' => time(),
]);
$this->success('已记录', ['list' => $this->growthListData($baby)]);
}
/**
* 亲子活动(按月龄匹配)
*/
public function activities()
{
$userId = $this->auth->id;
$baby = Db::name('baby')->where('user_id', $userId)->where('status', 'normal')->order('id', 'desc')->find();
$monthAge = $baby ? $this->monthAge($baby) : null;
$data = ContentService::lists($userId, ['type' => 'activity', 'limit' => 30]);
if ($monthAge !== null) {
$matched = array_values(array_filter($data['list'], function ($item) use ($monthAge) {
$extra = $item['extra'] ?: [];
$from = isset($extra['month_from']) ? (int)$extra['month_from'] : 0;
$to = isset($extra['month_to']) ? (int)$extra['month_to'] : 999;
return $monthAge >= $from && $monthAge <= $to;
}));
if ($matched) {
$data['list'] = $matched;
}
$data['matched_by_age'] = count($matched) > 0;
}
$data['month_age'] = $monthAge;
$this->success('', $data);
}
/**
* 课程
*/
public function courses()
{
$list = $this->coursesData();
$this->success('', ['list' => $list, 'total' => count($list)]);
}
protected function coursesData()
{
$data = ContentService::lists($this->auth->id, ['type' => 'course', 'part' => 'parenting', 'limit' => 30]);
return $data['list'];
}
/**
* 共育任务
*/
public function coTasks()
{
$this->success('', ['list' => $this->coTasksData()]);
}
protected function coTasksData()
{
$rows = Db::name('task')
->where('user_id', $this->auth->id)
->where('scope', 'parenting')
->whereNull('deletetime')
->order('done asc, id desc')
->select();
return array_map(function ($row) {
return [
'id' => (int)$row['id'],
'title' => $row['title'],
'category' => $row['category'],
'done' => (int)$row['done'],
'share_partner' => (int)$row['share_partner'],
];
}, $rows);
}
public function coTaskSave()
{
$userId = $this->auth->id;
$title = mb_substr(trim((string)$this->request->post('title', '')), 0, 80);
if (!$title) {
$this->error('请填写任务内容');
}
$id = Db::name('task')->insertGetId([
'user_id' => $userId,
'scope' => 'parenting',
'title' => $title,
'category' => $this->request->post('category', 'care'),
'share_partner' => 1,
'createtime' => time(),
'updatetime' => time(),
]);
$this->success('已添加', ['id' => (int)$id, 'list' => $this->coTasksData()]);
}
public function coTaskDone()
{
$id = (int)$this->request->post('id', 0);
$row = Db::name('task')
->where('id', $id)
->where('user_id', $this->auth->id)
->where('scope', 'parenting')
->find();
if (!$row) {
$this->error('任务不存在');
}
$done = $row['done'] ? 0 : 1;
Db::name('task')->where('id', $id)->update([
'done' => $done, 'done_time' => $done ? time() : null, 'updatetime' => time(),
]);
$this->success($done ? '已完成' : '已取消', ['done' => $done, 'list' => $this->coTasksData()]);
}
/**
* 育儿情绪
*/
public function emotions()
{
$this->success('', ContentService::lists($this->auth->id, ['type' => 'emotion', 'limit' => 30]));
}
/**
* 疫苗与儿保计划(按宝宝生日推算)
*/
public function vaccines()
{
$baby = Db::name('baby')->where('user_id', $this->auth->id)->where('status', 'normal')->order('id', 'desc')->find();
$this->success('', ['list' => $this->vaccinePlan($baby), 'baby' => $this->formatBaby($baby)]);
}
// ---------------- 内部方法 ----------------
protected function formatBaby($baby)
{
if (!$baby) {
return null;
}
return [
'id' => (int)$baby['id'],
'nickname' => $baby['nickname'],
'gender' => (int)$baby['gender'],
'birthday' => $baby['birthday'],
'due_date' => $baby['due_date'],
'is_pregnant' => empty($baby['birthday']) && !empty($baby['due_date']),
];
}
/**
* 月龄(孕期返回负数:距离预产期天数用于展示)
*/
protected function monthAge($baby)
{
if (!$baby) {
return null;
}
if (!empty($baby['birthday'])) {
$birth = strtotime($baby['birthday']);
$now = strtotime(date('Y-m-d'));
if ($now < $birth) {
return 0;
}
return (int)floor(($now - $birth) / (30.44 * 86400));
}
if (!empty($baby['due_date'])) {
$days = (int)ceil((strtotime($baby['due_date']) - strtotime(date('Y-m-d'))) / 86400);
return -max(1, $days);
}
return null;
}
protected function vaccinePlan($baby)
{
$list = Db::name('vaccine')->order('weigh', 'desc')->select();
$birthday = $baby && !empty($baby['birthday']) ? strtotime($baby['birthday']) : null;
$today = strtotime(date('Y-m-d'));
$result = [];
foreach ($list as $item) {
$dueDate = $birthday ? date('Y-m-d', strtotime('+' . (int)$item['month_from'] . ' month', $birthday)) : null;
$days = $dueDate ? (int)ceil((strtotime($dueDate) - $today) / 86400) : null;
$result[] = [
'id' => (int)$item['id'],
'name' => $item['name'],
'type' => $item['type'],
'month_from' => (int)$item['month_from'],
'note' => $item['note'],
'due_date' => $dueDate,
'days_left' => $days,
'status' => $days === null ? 'unknown' : ($days < 0 ? 'overdue' : ($days <= 30 ? 'upcoming' : 'later')),
];
}
return $result;
}
}