后端(新增 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 返回数组、 咨询时段跨年错误
241 行
8.3 KiB
PHP
241 行
8.3 KiB
PHP
<?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);
|
||
}
|
||
}
|