文件
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

304 行
10 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 fast\Random;
use think\Db;
/**
* 两性关系接口(M7)
*
* 关系测评 / 沟通引导卡 / 冲突工具 / 关系课程 / 双人任务 / 纪念回顾
*/
class Relation extends Api
{
protected $noNeedLogin = [];
protected $noNeedRight = '*';
/**
* 关系模块总览
*/
public function index()
{
$userId = $this->auth->id;
$this->success('', [
'assessments' => AssessmentService::lists('relation', $userId),
'cards' => ContentService::lists($userId, ['type' => 'card', 'limit' => 3])['list'],
'conflicts' => ContentService::lists($userId, ['type' => 'conflict', 'limit' => 3])['list'],
'courses' => ['list' => $this->coursesData()],
'tasks' => $this->tasks(),
'memories' => $this->memoryList(5),
'stats' => $this->stats($userId),
]);
}
/**
* 关系测评列表 / 详情 / 提交
*/
public function assessments()
{
$this->success('', ['list' => AssessmentService::lists('relation', $this->auth->id)]);
}
public function assessmentDetail()
{
$data = AssessmentService::detail($this->request->param('id', 0));
if (!$data) {
$this->error('量表不存在');
}
$this->success('', $data);
}
public function assessmentSubmit()
{
$answers = $this->request->post('answers', '');
$answers = is_array($answers) ? $answers : json_decode(htmlspecialchars_decode((string)$answers, ENT_QUOTES), true);
if (!is_array($answers)) {
$this->error('请完成全部题目');
}
$result = AssessmentService::submit($this->auth->id, (int)$this->request->post('id', 0), $answers);
if (isset($result['error'])) {
$this->error($result['error']);
}
$this->success('测评完成', $result);
}
/**
* 沟通引导卡 / 冲突工具
*/
public function cards()
{
$this->success('', ContentService::lists($this->auth->id, ['type' => 'card', 'limit' => 30]));
}
public function conflicts()
{
$this->success('', ContentService::lists($this->auth->id, ['type' => 'conflict', 'limit' => 30]));
}
/**
* 关系课程列表 + 购买
*/
public function courses()
{
$this->success('', [
'list' => $this->coursesData(),
'total' => count($this->coursesData()),
]);
}
protected function coursesData()
{
$data = ContentService::lists($this->auth->id, ['type' => 'course', 'part' => 'relation', 'limit' => 30]);
return $data['list'];
}
public function courseBuy()
{
$userId = $this->auth->id;
$contentId = (int)$this->request->post('id', 0);
$course = Db::name('content')->where('id', $contentId)->where('type', 'course')->find();
if (!$course) {
$this->error('课程不存在');
}
if (ContentService::hasPurchased($userId, $contentId)) {
$this->success('已购买', ['purchased' => true]);
}
$orderNo = 'HC' . date('YmdHis') . Random::numeric(4);
$id = Db::name('course_order')->insertGetId([
'order_no' => $orderNo,
'user_id' => $userId,
'content_id' => $contentId,
'amount' => 0,
'pay_status' => 'unpaid',
'createtime' => time(),
]);
$this->success('订单已创建', [
'order_id' => (int)$id,
'order_no' => $orderNo,
'amount' => (float)0,
'pay_hint' => '课程支付通道待接入,可联系客服开通',
'purchased' => false,
]);
}
/**
* 双人任务
*/
public function taskList()
{
$this->success('', ['list' => $this->tasks()]);
}
public function taskSave()
{
$userId = $this->auth->id;
$id = (int)$this->request->post('id', 0);
$title = mb_substr(trim((string)$this->request->post('title', '')), 0, 80);
if (!$title) {
$this->error('请填写任务内容');
}
$data = [
'user_id' => $userId,
'scope' => 'relation',
'title' => $title,
'category' => $this->request->post('category', 'daily'),
'share_partner' => 1,
'updatetime' => time(),
];
if ($id) {
$row = Db::name('task')->where('id', $id)->where('user_id', $userId)->find();
if (!$row) {
$this->error('任务不存在');
}
Db::name('task')->where('id', $id)->update($data);
} else {
$data['createtime'] = time();
$id = Db::name('task')->insertGetId($data);
}
$this->success('已保存', ['id' => (int)$id, 'list' => $this->tasks()]);
}
public function taskDone()
{
$userId = $this->auth->id;
$id = (int)$this->request->post('id', 0);
$row = Db::name('task')->where('id', $id)->where('user_id', $userId)->where('scope', 'relation')->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(),
]);
// 完成双人任务,双人共同记录进「爱的记忆」
if ($done) {
Db::name('memory')->insert([
'user_id' => $userId,
'bind_id' => 0,
'title' => '一起完成:' . $row['title'],
'memory_date' => date('Y-m-d'),
'type' => 'daily',
'content' => '',
'remind' => 0,
'createtime' => time(),
]);
}
$this->success($done ? '已完成' : '已取消', ['done' => $done, 'list' => $this->tasks()]);
}
/**
* 纪念与回顾
*/
public function memories()
{
$this->success('', ['list' => $this->memoryList(50)]);
}
public function memorySave()
{
$userId = $this->auth->id;
$title = mb_substr(trim((string)$this->request->post('title', '')), 0, 60);
$date = (string)$this->request->post('memory_date', date('Y-m-d'));
if (!$title) {
$this->error('请填写纪念日名称');
}
if (!strtotime($date)) {
$this->error('日期格式不正确');
}
$id = Db::name('memory')->insertGetId([
'user_id' => $userId,
'title' => $title,
'memory_date' => $date,
'type' => $this->request->post('type', 'anniversary'),
'content' => mb_substr((string)$this->request->post('content', ''), 0, 500),
'remind' => (int)$this->request->post('remind', 1),
'createtime' => time(),
]);
$this->success('已记录', ['id' => (int)$id, 'list' => $this->memoryList(50)]);
}
public function memoryRemove()
{
$id = (int)$this->request->post('id', 0);
$row = Db::name('memory')->where('id', $id)->where('user_id', $this->auth->id)->find();
if (!$row) {
$this->error('记录不存在');
}
Db::name('memory')->where('id', $id)->update(['deletetime' => time()]);
$this->success('已删除', ['list' => $this->memoryList(50)]);
}
// ---------------- 内部方法 ----------------
protected function tasks()
{
$rows = Db::name('task')
->where('user_id', $this->auth->id)
->where('scope', 'relation')
->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'],
];
}, $rows);
}
protected function memoryList($limit)
{
$rows = Db::name('memory')
->where('user_id', $this->auth->id)
->whereNull('deletetime')
->order('memory_date', 'desc')
->limit($limit)
->select();
return array_map(function ($row) {
$days = (int)floor((strtotime(date('Y-m-d')) - strtotime($row['memory_date'])) / 86400);
$nextDays = null;
if ($row['type'] === 'anniversary') {
$nextTs = strtotime(date('Y') . '-' . date('m-d', strtotime($row['memory_date'])));
if ($nextTs < strtotime(date('Y-m-d'))) {
$nextTs = strtotime('+1 year', $nextTs);
}
$nextDays = (int)ceil(($nextTs - strtotime(date('Y-m-d'))) / 86400);
}
return [
'id' => (int)$row['id'],
'title' => $row['title'],
'memory_date' => $row['memory_date'],
'type' => $row['type'],
'content' => $row['content'],
'days_since' => $days,
'days_to_next' => $nextDays,
];
}, $rows);
}
protected function stats($userId)
{
return [
'task_total' => Db::name('task')->where('user_id', $userId)->where('scope', 'relation')->whereNull('deletetime')->count(),
'task_done' => Db::name('task')->where('user_id', $userId)->where('scope', 'relation')->where('done', 1)->whereNull('deletetime')->count(),
'memory_total' => Db::name('memory')->where('user_id', $userId)->whereNull('deletetime')->count(),
'test_count' => Db::name('assessment_record')
->where('user_id', $userId)
->whereIn('assessment_id', function ($query) {
$query->name('assessment')->where('scope', 'relation')->field('id');
})
->count(),
];
}
}