文件
huaqiapp/server/application/api/controller/Mood.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

188 行
6.4 KiB
PHP

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

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

<?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],
]);
}
/**
* 情绪列表
* 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']),
];
}
}