后端(新增 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 返回数组、 咨询时段跨年错误
222 行
7.6 KiB
PHP
222 行
7.6 KiB
PHP
<?php
|
||
|
||
namespace app\api\controller;
|
||
|
||
use app\common\controller\Api;
|
||
use think\Db;
|
||
use think\Validate;
|
||
|
||
/**
|
||
* 情绪疗愈接口(M2)
|
||
*/
|
||
class Mood extends Api
|
||
{
|
||
protected $noNeedLogin = [];
|
||
protected $noNeedRight = '*';
|
||
|
||
/** 情绪枚举 */
|
||
public static $moods = [
|
||
'happy' => ['text' => '开心', 'emoji' => '☀️', 'score' => 5, 'color' => '#F5A65B'],
|
||
'calm' => ['text' => '平静', 'emoji' => '🌤', 'score' => 4, 'color' => '#9BC1A6'],
|
||
'anxious' => ['text' => '焦虑', 'emoji' => '🌪', 'score' => 2, 'color' => '#E8A87C'],
|
||
'low' => ['text' => '低落', 'emoji' => '🌧', 'score' => 2, 'color' => '#8FA6C4'],
|
||
'angry' => ['text' => '烦躁', 'emoji' => '⚡', 'score' => 1, 'color' => '#E8604C'],
|
||
];
|
||
|
||
/**
|
||
* 记录情绪(一天可多条)
|
||
* POST log_date, mood, intensity, triggers, content
|
||
*/
|
||
public function save()
|
||
{
|
||
$userId = $this->auth->id;
|
||
$mood = (string)$this->request->post('mood', '');
|
||
if (!isset(self::$moods[$mood])) {
|
||
$this->error('请选择情绪');
|
||
}
|
||
$logDate = $this->request->post('log_date', date('Y-m-d'));
|
||
if (!strtotime($logDate)) {
|
||
$this->error('日期格式不正确');
|
||
}
|
||
if (strtotime($logDate) > strtotime('+1 day')) {
|
||
$this->error('不能记录未来日期');
|
||
}
|
||
|
||
$content = mb_substr((string)$this->request->post('content', ''), 0, 1000);
|
||
$id = Db::name('mood_log')->insertGetId([
|
||
'user_id' => $userId,
|
||
'log_date' => $logDate,
|
||
'mood' => $mood,
|
||
'intensity' => min(5, max(1, (int)$this->request->post('intensity', 3))),
|
||
'triggers' => mb_substr((string)$this->request->post('triggers', ''), 0, 255),
|
||
'content' => $content,
|
||
'createtime' => time(),
|
||
'updatetime' => time(),
|
||
]);
|
||
|
||
$this->success('已记录', [
|
||
'id' => (int)$id,
|
||
'mood' => self::$moods[$mood],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 情绪急救包(M2)
|
||
* 返回 4 个即时可用的安抚工具,按当前情绪排序
|
||
*/
|
||
public function firstaid()
|
||
{
|
||
$data = \app\common\service\ContentService::lists($this->auth->id, [
|
||
'type' => 'firstaid',
|
||
'limit' => 10,
|
||
]);
|
||
|
||
// 按当前情绪优先排序(若今日有记录)
|
||
$today = Db::name('mood_log')
|
||
->where('user_id', $this->auth->id)
|
||
->where('log_date', date('Y-m-d'))
|
||
->order('id', 'desc')
|
||
->find();
|
||
$mood = $today ? $today['mood'] : '';
|
||
if ($mood) {
|
||
usort($data['list'], function ($a, $b) use ($mood) {
|
||
$aHit = strpos((string)$a['summary'] . (string)$a['title'], $mood) !== false ? 1 : 0;
|
||
$bHit = strpos((string)$b['summary'] . (string)$b['title'], $mood) !== false ? 1 : 0;
|
||
return $bHit - $aHit;
|
||
});
|
||
}
|
||
|
||
$this->success('', [
|
||
'list' => $data['list'],
|
||
'today_mood' => $mood,
|
||
'tip' => '情绪上头时,先照顾身体,再处理问题',
|
||
'hotline' => ['name' => '心理援助热线', 'phone' => '12356'],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 情绪列表
|
||
* GET limit=20 month=2026-11
|
||
*/
|
||
public function index()
|
||
{
|
||
$userId = $this->auth->id;
|
||
$limit = min(100, max(1, (int)$this->request->param('limit', 20)));
|
||
$month = $this->request->param('month', '');
|
||
|
||
$query = Db::name('mood_log')
|
||
->where('user_id', $userId)
|
||
->whereNull('deletetime');
|
||
|
||
if ($month && Validate::regex($month, '/^\d{4}-\d{2}$/')) {
|
||
$query->whereBetween('log_date', [$month . '-01', date('Y-m-t', strtotime($month . '-01'))]);
|
||
}
|
||
|
||
$list = $query->order('log_date', 'desc')->order('id', 'desc')->limit($limit)->select();
|
||
$list = array_map([$this, 'format'], $list);
|
||
|
||
$this->success('', ['list' => $list, 'moods' => $this->moodOptions()]);
|
||
}
|
||
|
||
/**
|
||
* 情绪趋势
|
||
* GET days=7|30
|
||
*/
|
||
public function trend()
|
||
{
|
||
$userId = $this->auth->id;
|
||
$days = (int)$this->request->param('days', 7);
|
||
$days = in_array($days, [7, 14, 30]) ? $days : 7;
|
||
|
||
$start = date('Y-m-d', strtotime('-' . ($days - 1) . ' day'));
|
||
$logs = Db::name('mood_log')
|
||
->where('user_id', $userId)
|
||
->whereNull('deletetime')
|
||
->where('log_date', '>=', $start)
|
||
->order('log_date', 'asc')
|
||
->select();
|
||
|
||
$bucket = [];
|
||
for ($i = $days - 1; $i >= 0; $i--) {
|
||
$d = date('Y-m-d', strtotime('-' . $i . ' day'));
|
||
$bucket[$d] = ['date' => $d, 'label' => date('n/j', strtotime($d)), 'score' => null, 'count' => 0, 'mood' => ''];
|
||
}
|
||
|
||
foreach ($logs as $log) {
|
||
if (!isset($bucket[$log['log_date']])) {
|
||
continue;
|
||
}
|
||
$score = self::$moods[$log['mood']]['score'] ?? 3;
|
||
$item = &$bucket[$log['log_date']];
|
||
$item['count']++;
|
||
$item['score'] = $item['score'] === null ? $score : round(($item['score'] * ($item['count'] - 1) + $score) / $item['count'], 1);
|
||
$item['mood'] = $log['mood'];
|
||
unset($item);
|
||
}
|
||
|
||
$recorded = array_values(array_filter($bucket, function ($v) {
|
||
return $v['score'] !== null;
|
||
}));
|
||
$avg = $recorded ? round(array_sum(array_column($recorded, 'score')) / count($recorded), 1) : 0;
|
||
|
||
// 主情绪统计
|
||
$count = [];
|
||
foreach ($logs as $log) {
|
||
$count[$log['mood']] = isset($count[$log['mood']]) ? $count[$log['mood']] + 1 : 1;
|
||
}
|
||
arsort($count);
|
||
$topMood = key($count) ?: '';
|
||
|
||
$this->success('', [
|
||
'days' => $days,
|
||
'list' => array_values($bucket),
|
||
'recorded' => count($recorded),
|
||
'avg_score' => $avg,
|
||
'top_mood' => $topMood ? array_merge(['key' => $topMood], self::$moods[$topMood]) : null,
|
||
'moods' => $this->moodOptions(),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 删除一条情绪记录
|
||
*/
|
||
public function remove()
|
||
{
|
||
$id = (int)$this->request->post('id', 0);
|
||
$row = Db::name('mood_log')->where('id', $id)->where('user_id', $this->auth->id)->find();
|
||
if (!$row) {
|
||
$this->error('记录不存在');
|
||
}
|
||
Db::name('mood_log')->where('id', $id)->update(['deletetime' => time()]);
|
||
$this->success('已删除');
|
||
}
|
||
|
||
protected function moodOptions()
|
||
{
|
||
$list = [];
|
||
foreach (self::$moods as $key => $item) {
|
||
$list[] = array_merge(['key' => $key], $item);
|
||
}
|
||
return $list;
|
||
}
|
||
|
||
protected function format($log)
|
||
{
|
||
$mood = self::$moods[$log['mood']] ?? ['text' => '', 'emoji' => '', 'score' => 0, 'color' => ''];
|
||
return [
|
||
'id' => (int)$log['id'],
|
||
'log_date' => $log['log_date'],
|
||
'date_text' => date('n月j日', strtotime($log['log_date'])),
|
||
'week_text' => '周' . ['日', '一', '二', '三', '四', '五', '六'][(int)date('w', strtotime($log['log_date']))],
|
||
'mood' => $log['mood'],
|
||
'mood_text' => $mood['text'],
|
||
'emoji' => $mood['emoji'],
|
||
'color' => $mood['color'],
|
||
'intensity' => (int)$log['intensity'],
|
||
'triggers' => $log['triggers'],
|
||
'content' => $log['content'],
|
||
'time_text' => date('H:i', (int)$log['createtime']),
|
||
];
|
||
}
|
||
}
|