前端(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
188 行
6.4 KiB
PHP
188 行
6.4 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],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 情绪列表
|
||
* 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']),
|
||
];
|
||
}
|
||
}
|