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

156 行
5.2 KiB
PHP

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

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

<?php
namespace app\api\controller;
use app\common\controller\Api;
use app\common\service\CycleService;
use app\common\service\MemberService;
use think\Db;
/**
* 内容接口(M2 心语/科普 + M5 健康建议 + 冥想音频)
*/
class Content extends Api
{
protected $noNeedLogin = [];
protected $noNeedRight = '*';
/**
* 内容列表
* GET type=jijing|mood|knowledge|advice|meditation phase=menstrual access=free
*/
public function index()
{
$type = $this->request->param('type', '');
$phase = $this->request->param('phase', '');
$categoryId = (int)$this->request->param('category_id', 0);
$page = max(1, (int)$this->request->param('page', 1));
$limit = min(50, max(1, (int)$this->request->param('limit', 10)));
// 未显式传阶段时,按当前周期阶段智能匹配
if (!$phase) {
$current = CycleService::current($this->auth->id);
$phase = $current['phase'] ?: '';
}
$query = Db::name('content')
->where('status', 'normal')
->where('verify', 'passed');
if ($type) {
$query->where('type', $type);
}
if ($categoryId) {
$query->where('category_id', $categoryId);
}
$all = $query->order('weigh', 'desc')->select();
// 阶段匹配优先:先取命中当前阶段(含 all)的内容,过少时回退全量,
// 避免某些阶段(如排卵期)内容过少导致列表空置
if ($phase) {
$matched = array_values(array_filter($all, function ($item) use ($phase) {
$phases = array_filter(explode(',', $item['phase']));
return in_array('all', $phases) || in_array($phase, $phases);
}));
if (count($matched) >= 3 || count($matched) === count($all)) {
$all = $matched;
} else {
$rest = array_values(array_filter($all, function ($item) use ($matched) {
return !in_array($item['id'], array_column($matched, 'id'));
}));
$all = array_merge($matched, $rest);
}
}
$total = count($all);
$list = array_slice($all, ($page - 1) * $limit, $limit);
$member = MemberService::status($this->auth->id);
$list = array_map(function ($item) use ($member) {
return [
'id' => (int)$item['id'],
'type' => $item['type'],
'title' => $item['title'],
'summary' => $item['summary'],
'emoji' => $item['cover_emoji'],
'media_type' => $item['media_type'],
'duration' => (int)$item['duration'],
'duration_text' => $item['duration'] ? floor($item['duration'] / 60) . ' 分钟' : '',
'access' => $item['access'],
'locked' => $item['access'] === 'member' && !$member['is_member'],
];
}, $list);
$this->success('', [
'list' => $list,
'total' => $total,
'page' => $page,
'limit' => $limit,
'has_more' => $page * $limit < $total,
'phase' => $phase,
'category' => $this->categoryList($type),
]);
}
/**
* 内容详情
* GET id=1
*/
public function detail()
{
$id = (int)$this->request->param('id', 0);
$item = Db::name('content')->where('id', $id)->where('status', 'normal')->find();
if (!$item) {
$this->error('内容不存在');
}
$member = MemberService::status($this->auth->id);
$locked = $item['access'] === 'member' && !$member['is_member'];
$this->success('', [
'id' => (int)$item['id'],
'type' => $item['type'],
'title' => $item['title'],
'summary' => $item['summary'],
'body' => $locked ? mb_substr($item['body'], 0, 80) . '…' : $item['body'],
'emoji' => $item['cover_emoji'],
'media_type' => $item['media_type'],
'duration' => (int)$item['duration'],
'access' => $item['access'],
'locked' => $locked,
'lock_tip' => $locked ? '该内容为会员专享,开通会员即可查看' : '',
'disclaimer' => $item['type'] === 'advice' || $item['type'] === 'knowledge'
? '以上内容仅供参考,不能替代医疗诊断。' : '',
]);
}
/**
* 分类
*/
public function categories()
{
$type = $this->request->param('type', '');
$query = Db::name('content_category')->where('status', 'normal');
if ($type) {
$query->where('type', $type);
}
$list = $query->order('weigh', 'desc')->select();
$this->success('', ['list' => $list]);
}
protected function categoryList($type)
{
if (!$type) {
return [];
}
$list = Db::name('content_category')
->where('type', $type)
->where('status', 'normal')
->order('weigh', 'desc')
->field('id,name')
->select();
return $list;
}
}