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 返回数组、 咨询时段跨年错误
这个提交包含在:
@@ -3,12 +3,12 @@
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\common\controller\Api;
|
||||
use app\common\service\ContentService;
|
||||
use app\common\service\CycleService;
|
||||
use app\common\service\MemberService;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 内容接口(M2 心语/科普 + M5 健康建议 + 冥想音频)
|
||||
* 内容接口(心语 / 科普 / 知识 / 健康建议 / 冥想 / 课程图文)
|
||||
*/
|
||||
class Content extends Api
|
||||
{
|
||||
@@ -17,112 +17,44 @@ class Content extends Api
|
||||
|
||||
/**
|
||||
* 内容列表
|
||||
* GET type=jijing|mood|knowledge|advice|meditation phase=menstrual access=free
|
||||
* GET type=jijing|mood|knowledge|advice|meditation|care|card|conflict|firstaid|guide|course|activity|emotion
|
||||
* phase= category_id= part=relation|parenting page= limit=
|
||||
*/
|
||||
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) {
|
||||
if (!$phase && !$this->request->param('type')) {
|
||||
$current = CycleService::current($this->auth->id);
|
||||
$phase = $current['phase'] ?: '';
|
||||
} elseif (!$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),
|
||||
$data = ContentService::lists($this->auth->id, [
|
||||
'type' => $this->request->param('type', ''),
|
||||
'phase' => $phase,
|
||||
'category_id' => (int)$this->request->param('category_id', 0),
|
||||
'part' => $this->request->param('part', ''),
|
||||
'page' => (int)$this->request->param('page', 1),
|
||||
'limit' => (int)$this->request->param('limit', 10),
|
||||
]);
|
||||
|
||||
$data['category'] = $this->categoryList($this->request->param('type', ''));
|
||||
$this->success('', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容详情
|
||||
* 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) {
|
||||
$data = ContentService::detail($this->auth->id, $this->request->param('id', 0));
|
||||
if (!$data) {
|
||||
$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'
|
||||
? '以上内容仅供参考,不能替代医疗诊断。' : '',
|
||||
]);
|
||||
$this->success('', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,8 +67,7 @@ class Content extends Api
|
||||
if ($type) {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
$list = $query->order('weigh', 'desc')->select();
|
||||
$this->success('', ['list' => $list]);
|
||||
$this->success('', ['list' => $query->order('weigh', 'desc')->select()]);
|
||||
}
|
||||
|
||||
protected function categoryList($type)
|
||||
@@ -144,12 +75,11 @@ class Content extends Api
|
||||
if (!$type) {
|
||||
return [];
|
||||
}
|
||||
$list = Db::name('content_category')
|
||||
return Db::name('content_category')
|
||||
->where('type', $type)
|
||||
->where('status', 'normal')
|
||||
->order('weigh', 'desc')
|
||||
->field('id,name')
|
||||
->select();
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户