文件
huaqiapp/server/application/api/controller/Consult.php
linda d243ece39c 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 返回数组、
  咨询时段跨年错误
2026-11-05 23:27:21 +08:00

241 行
8.3 KiB
PHP

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

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

<?php
namespace app\api\controller;
use app\common\controller\Api;
use app\common\service\AssessmentService;
use app\common\service\MemberService;
use fast\Random;
use think\Db;
/**
* 心理咨询与测评接口(M9)
*
* 咨询师入驻与排班 / 一对一咨询预约 / 量表测评 / 危机干预转介
*/
class Consult extends Api
{
protected $noNeedLogin = [];
protected $noNeedRight = '*';
/**
* 总览
*/
public function index()
{
$userId = $this->auth->id;
$status = MemberService::status($userId);
$this->success('', [
'counselors' => $this->counselorList(5),
'assessments' => array_merge(
AssessmentService::lists('mood', $userId),
AssessmentService::lists('parenting', $userId)
),
'orders' => $this->orderList(),
'crisis' => AssessmentService::crisis(),
'member' => $status,
'discount' => MemberService::can($userId, 'consult.discount') ? 0.9 : 1,
]);
}
/**
* 咨询师列表(含资质、价格、可预约时段)
*/
public function counselors()
{
$this->success('', ['list' => $this->counselorList(20)]);
}
/**
* 量表列表 / 详情 / 提交
*/
public function assessments()
{
$scope = $this->request->param('scope', '');
$this->success('', ['list' => AssessmentService::lists($scope, $this->auth->id)]);
}
public function assessmentDetail()
{
$data = AssessmentService::detail($this->request->param('id', 0));
if (!$data) {
$this->error('量表不存在');
}
$this->success('', $data);
}
public function assessmentSubmit()
{
$answers = $this->request->post('answers', '');
$answers = is_array($answers) ? $answers : json_decode(htmlspecialchars_decode((string)$answers, ENT_QUOTES), true);
if (!is_array($answers)) {
$this->error('请完成全部题目');
}
$result = AssessmentService::submit($this->auth->id, (int)$this->request->post('id', 0), $answers);
if (isset($result['error'])) {
$this->error($result['error']);
}
$this->success('测评完成', $result);
}
/**
* 预约咨询
* POST counselor_id, mode=text|voice|video, start_time=Y-m-d H:i
*/
public function orderCreate()
{
$userId = $this->auth->id;
$counselorId = (int)$this->request->post('counselor_id', 0);
$mode = $this->request->post('mode', 'text');
$startTime = (string)$this->request->post('start_time', '');
$counselor = Db::name('counselor')->where('id', $counselorId)->where('status', 'on')->find();
if (!$counselor) {
$this->error('咨询师不可预约');
}
if (!in_array($mode, ['text', 'voice', 'video'], true)) {
$this->error('咨询方式不正确');
}
if (!$startTime || !strtotime($startTime)) {
$this->error('请选择预约时间');
}
if (strtotime($startTime) < time()) {
$this->error('预约时间需晚于当前时间');
}
$priceField = 'price_' . $mode;
$amount = (float)$counselor[$priceField];
$discount = MemberService::can($userId, 'consult.discount') ? 0.9 : 1;
$amount = round($amount * $discount, 2);
$rateAmount = round($amount * (float)$counselor['rate'], 2);
$orderNo = 'HQ' . date('YmdHis') . Random::numeric(4);
$id = Db::name('consult_order')->insertGetId([
'order_no' => $orderNo,
'user_id' => $userId,
'counselor_id' => $counselorId,
'mode' => $mode,
'start_time' => strtotime($startTime),
'duration' => 50,
'amount' => $amount,
'pay_status' => 'unpaid',
'consult_status' => 'pending',
'rate_amount' => $rateAmount,
'createtime' => time(),
]);
$this->success('预约已提交', [
'order_id' => (int)$id,
'order_no' => $orderNo,
'amount' => $amount,
'discount' => $discount,
'start_time' => date('Y-m-d H:i', strtotime($startTime)),
'pay_hint' => '支付通道待接入,可联系客服确认支付后开始咨询',
'counselor' => ['name' => $counselor['name'], 'title' => $counselor['title']],
'note' => '咨询内容将加密保存,仅你与咨询师可见',
]);
}
/**
* 我的咨询订单
*/
public function orders()
{
$this->success('', ['list' => $this->orderList()]);
}
/**
* 取消订单
*/
public function cancel()
{
$id = (int)$this->request->post('id', 0);
$row = Db::name('consult_order')->where('id', $id)->where('user_id', $this->auth->id)->find();
if (!$row) {
$this->error('订单不存在');
}
if ($row['consult_status'] === 'finished') {
$this->error('已完成的咨询无法取消');
}
Db::name('consult_order')->where('id', $id)->update(['consult_status' => 'canceled']);
$this->success('已取消', ['list' => $this->orderList()]);
}
/**
* 危机干预信息(前端在高风险结果后也可主动调用)
*/
public function crisis()
{
$this->success('', AssessmentService::crisis());
}
// ---------------- 内部方法 ----------------
protected function counselorList($limit)
{
$rows = Db::name('counselor')
->where('status', 'on')
->where('cert_verify', 'passed')
->order('weigh', 'desc')
->limit($limit)
->select();
$member = MemberService::can($this->auth->id, 'consult.discount');
return array_map(function ($row) use ($member) {
$tags = array_filter(explode(',', $row['tags']));
$slots = [];
for ($i = 1; $i <= 3; $i++) {
$day = date('Y-m-d', strtotime('+' . $i . ' day'));
$slots[] = $day . ' 10:00';
$slots[] = $day . ' 15:00';
}
return [
'id' => (int)$row['id'],
'name' => $row['name'],
'emoji' => $row['avatar_emoji'],
'title' => $row['title'],
'cert_verify' => $row['cert_verify'],
'intro' => $row['intro'],
'tags' => $tags,
'price' => [
'text' => (float)$row['price_text'],
'voice' => (float)$row['price_voice'],
'video' => (float)$row['price_video'],
],
'discount' => $member ? 0.9 : 1,
'slots' => $slots,
];
}, $rows);
}
protected function orderList()
{
$rows = Db::name('consult_order')
->where('user_id', $this->auth->id)
->order('id', 'desc')
->limit(20)
->select();
$names = Db::name('counselor')->column('name', 'id');
$modeText = ['text' => '文字', 'voice' => '语音', 'video' => '视频'];
$statusText = ['pending' => '待开始', 'ongoing' => '进行中', 'finished' => '已完成', 'canceled' => '已取消'];
return array_map(function ($row) use ($names, $modeText, $statusText) {
return [
'id' => (int)$row['id'],
'order_no' => $row['order_no'],
'counselor' => isset($names[$row['counselor_id']]) ? $names[$row['counselor_id']] : '',
'mode' => $row['mode'],
'mode_text' => isset($modeText[$row['mode']]) ? $modeText[$row['mode']] : $row['mode'],
'start_time' => $row['start_time'] ? date('Y-m-d H:i', (int)$row['start_time']) : '',
'amount' => (float)$row['amount'],
'pay_status' => $row['pay_status'],
'status' => $row['consult_status'],
'status_text' => isset($statusText[$row['consult_status']]) ? $statusText[$row['consult_status']] : $row['consult_status'],
];
}, $rows);
}
}