一、心理咨询(M9) - 前端不再选咨询师时段,改为「分类 + 表单」:11 类(婚恋/青少年/职场人际/情绪疏导/ 人生卡点/成长迷茫/脱单指导/分手挽回/社恐咨询/抑郁焦虑/情绪压力) - 表单字段:称呼、手机号(11 位校验)、方便联系时间、困扰描述(≥5 字),提交后由咨询助理联系 - 支持查看/取消我的预约;同一用户 10 分钟内防重复提交 - 新增表 fa_consult_booking(含跟进状态与备注),后台可跟进 二、心理测评(付费) - 新增三个付费量表:抑郁自评 SDS(20 题,1-4 级 + 10 道反向题,标准分=粗分×1.25 取整)、 焦虑自评 SAS(20 题,5 道反向题)、卡特尔 16PF 简版(48 题 / 16 维度,三选项) - 华东:测评需付费,未付费拦截并返回 need_pay;新增 fa_assessment_order 订单表与下单接口 - 计分:标准分分级(正常/轻度/中度/重度)+ 16PF 维度剖面(1-10 分、高/中/低解读) - 风险识别:SDS 自伤条目高分或重度 → risk_flag + 危机热线 - 结果页新增 16PF 16 维条形剖面与「预约咨询师解读」入口 三、管理端 - 新增后台模块:心理预约(分类/状态/备注跟进)、测评订单(支付状态可改) - ManageConfig 增加对应字段配置;菜单脚本 db/06_consult_assessment.sql(可重复执行) - fa_assessment 增加 price 字段;测评列表/详情接口返回价格与购买状态 四、验证(全绿) - 咨询+测评专项:54 项断言(分类、表单校验、防重复、脱敏、取消、题库、付费拦截、 下单、付费后 SDS=62/轻度、SAS=56/轻度、16PF 16 维满分、危机干预) - 前台接口 78 项、后台自检 33 项(28 模块)、契约 0 偏差、Vue 20/20、SCSS 19/19、无 emoji
344 行
12 KiB
PHP
344 行
12 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)
|
||
*
|
||
* 咨询:线上预约表单(不选咨询师时段),按分类提交,后台跟进。
|
||
* 测评:抑郁自评 SDS / 焦虑自评 SAS / 卡特尔 16PF(简版)等,付费后作答。
|
||
*/
|
||
class Consult extends Api
|
||
{
|
||
protected $noNeedLogin = ['categories'];
|
||
protected $noNeedRight = '*';
|
||
|
||
/** 咨询分类 */
|
||
public static $categories = [
|
||
'love' => '婚恋咨询',
|
||
'teen' => '青少年心理',
|
||
'career' => '职场人际',
|
||
'emotion' => '情绪疏导',
|
||
'life' => '人生卡点',
|
||
'growth' => '成长迷茫',
|
||
'single' => '脱单指导',
|
||
'breakup' => '分手挽回',
|
||
'social' => '社恐咨询',
|
||
'depress' => '抑郁焦虑',
|
||
'pressure' => '情绪压力',
|
||
];
|
||
|
||
/**
|
||
* 咨询分类(无需登录,供表单下拉)
|
||
*/
|
||
public function categories()
|
||
{
|
||
$list = [];
|
||
foreach (self::$categories as $key => $text) {
|
||
$list[] = ['key' => $key, 'name' => $text];
|
||
}
|
||
$this->success('', ['list' => $list]);
|
||
}
|
||
|
||
/**
|
||
* 心理服务总览:分类 + 我的预约 + 可做测评 + 危机信息
|
||
*/
|
||
public function index()
|
||
{
|
||
$userId = $this->auth->id;
|
||
|
||
$this->success('', [
|
||
'categories' => $this->categoriesData(),
|
||
'bookings' => $this->bookingList($userId, 5),
|
||
'assessments' => AssessmentService::lists('mood', $userId),
|
||
'crisis' => AssessmentService::crisis(),
|
||
'member' => MemberService::status($userId),
|
||
'notice' => '提交后 1 个工作日内,咨询助理会通过电话或微信与你联系',
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 提交咨询预约表单
|
||
* POST name, mobile, category, prefer_time, description
|
||
*/
|
||
public function booking()
|
||
{
|
||
$userId = $this->auth->id;
|
||
$name = mb_substr(trim((string)$this->request->post('name', '')), 0, 30);
|
||
$mobile = trim((string)$this->request->post('mobile', ''));
|
||
$category = trim((string)$this->request->post('category', ''));
|
||
$preferTime = mb_substr(trim((string)$this->request->post('prefer_time', '')), 0, 40);
|
||
$description = mb_substr(trim((string)$this->request->post('description', '')), 0, 500);
|
||
|
||
if (!$name) {
|
||
$this->error('请填写你的称呼');
|
||
}
|
||
if (!preg_match('/^1\d{10}$/', $mobile)) {
|
||
$this->error('请填写正确的 11 位手机号');
|
||
}
|
||
if (!isset(self::$categories[$category])) {
|
||
$this->error('请选择咨询分类');
|
||
}
|
||
if (!$preferTime) {
|
||
$this->error('请填写方便联系的时间');
|
||
}
|
||
if (mb_strlen($description) < 5) {
|
||
$this->error('请简单描述一下你的困扰(至少 5 个字)');
|
||
}
|
||
|
||
// 同一用户 10 分钟内不允许重复提交
|
||
$recent = Db::name('consult_booking')
|
||
->where('user_id', $userId)
|
||
->where('createtime', '>', time() - 600)
|
||
->where('status', 'pending')
|
||
->count();
|
||
if ($recent > 0) {
|
||
$this->error('你已提交过预约,请等待咨询助理联系');
|
||
}
|
||
|
||
$id = Db::name('consult_booking')->insertGetId([
|
||
'user_id' => $userId,
|
||
'name' => $name,
|
||
'mobile' => $mobile,
|
||
'category' => $category,
|
||
'prefer_time' => $preferTime,
|
||
'description' => $description,
|
||
'status' => 'pending',
|
||
'createtime' => time(),
|
||
'updatetime' => time(),
|
||
]);
|
||
|
||
$this->success('预约已提交', [
|
||
'id' => (int)$id,
|
||
'category' => self::$categories[$category],
|
||
'status_text' => '待联系',
|
||
'notice' => '咨询助理会在 1 个工作日内联系你,请保持电话畅通',
|
||
'hotline' => '12356',
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 我的预约列表
|
||
*/
|
||
public function bookings()
|
||
{
|
||
$this->success('', ['list' => $this->bookingList($this->auth->id, 20)]);
|
||
}
|
||
|
||
/**
|
||
* 取消预约
|
||
*/
|
||
public function bookingCancel()
|
||
{
|
||
$id = (int)$this->request->post('id', 0);
|
||
$row = Db::name('consult_booking')->where('id', $id)->where('user_id', $this->auth->id)->find();
|
||
if (!$row) {
|
||
$this->error('预约不存在');
|
||
}
|
||
if ($row['status'] === 'finished') {
|
||
$this->error('已完成的预约无法取消');
|
||
}
|
||
Db::name('consult_booking')->where('id', $id)->update([
|
||
'status' => 'canceled', 'updatetime' => time(),
|
||
]);
|
||
$this->success('已取消', ['list' => $this->bookingList($this->auth->id, 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), $this->auth->id);
|
||
if (!$data) {
|
||
$this->error('量表不存在');
|
||
}
|
||
$this->success('', $data);
|
||
}
|
||
|
||
/**
|
||
* 创建测评订单(付费后解锁)
|
||
* POST id
|
||
*/
|
||
public function assessmentOrder()
|
||
{
|
||
$userId = $this->auth->id;
|
||
$id = (int)$this->request->post('id', 0);
|
||
$item = Db::name('assessment')->where('id', $id)->where('status', 'normal')->find();
|
||
if (!$item) {
|
||
$this->error('量表不存在');
|
||
}
|
||
|
||
$price = (float)($item['price'] ?? 0);
|
||
if ($item['access'] !== 'pay' || $price <= 0) {
|
||
$this->success('该测评无需付费', ['paid' => true, 'need_pay' => false]);
|
||
}
|
||
if (AssessmentService::isPaid($userId, $id)) {
|
||
$this->success('已购买,可直接开始测评', ['paid' => true, 'need_pay' => false]);
|
||
}
|
||
|
||
// 复用未支付订单,避免重复下单
|
||
$exist = Db::name('assessment_order')
|
||
->where('user_id', $userId)
|
||
->where('assessment_id', $id)
|
||
->where('pay_status', 'unpaid')
|
||
->order('id', 'desc')
|
||
->find();
|
||
|
||
if ($exist) {
|
||
$orderNo = $exist['order_no'];
|
||
$orderId = (int)$exist['id'];
|
||
} else {
|
||
$orderNo = 'HA' . date('YmdHis') . Random::numeric(4);
|
||
$orderId = Db::name('assessment_order')->insertGetId([
|
||
'order_no' => $orderNo,
|
||
'user_id' => $userId,
|
||
'assessment_id' => $id,
|
||
'amount' => $price,
|
||
'pay_status' => 'unpaid',
|
||
'createtime' => time(),
|
||
]);
|
||
}
|
||
|
||
$this->success('订单已创建', [
|
||
'order_id' => $orderId,
|
||
'order_no' => $orderNo,
|
||
'amount' => $price,
|
||
'title' => $item['title'],
|
||
'pay_hint' => '支付通道接入中:可联系客服完成支付,支付后即可开始测评',
|
||
'paid' => false,
|
||
'need_pay' => true,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 提交测评(需先付费)
|
||
* POST id, answers=[0,1,2...]
|
||
*/
|
||
public function assessmentSubmit()
|
||
{
|
||
$userId = $this->auth->id;
|
||
$id = (int)$this->request->post('id', 0);
|
||
$item = Db::name('assessment')->where('id', $id)->where('status', 'normal')->find();
|
||
if (!$item) {
|
||
$this->error('量表不存在');
|
||
}
|
||
|
||
$price = (float)($item['price'] ?? 0);
|
||
$needPay = $item['access'] === 'pay' && $price > 0;
|
||
if ($needPay && !AssessmentService::isPaid($userId, $id)) {
|
||
$this->error('该测评需付费后开始', [
|
||
'need_pay' => 1,
|
||
'price' => $price,
|
||
'title' => $item['title'],
|
||
'pay_hint' => '支付后即可开始测评',
|
||
]);
|
||
}
|
||
|
||
$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($userId, $id, $answers);
|
||
if (isset($result['error'])) {
|
||
$this->error($result['error']);
|
||
}
|
||
$this->success('测评完成', $result);
|
||
}
|
||
|
||
/**
|
||
* 危机干预信息
|
||
*/
|
||
public function crisis()
|
||
{
|
||
$this->success('', AssessmentService::crisis());
|
||
}
|
||
|
||
/* ============================================================
|
||
* 兼容旧接口:咨询师列表与咨询订单(前台已改为预约表单)
|
||
* ============================================================ */
|
||
public function counselors()
|
||
{
|
||
$rows = Db::name('counselor')
|
||
->where('status', 'on')
|
||
->where('cert_verify', 'passed')
|
||
->order('weigh', 'desc')
|
||
->limit(20)
|
||
->select();
|
||
$member = MemberService::can($this->auth->id, 'consult.discount');
|
||
$list = array_map(function ($row) use ($member) {
|
||
return [
|
||
'id' => (int)$row['id'],
|
||
'name' => $row['name'],
|
||
'emoji' => $row['avatar_emoji'],
|
||
'title' => $row['title'],
|
||
'intro' => $row['intro'],
|
||
'tags' => array_filter(explode(',', $row['tags'])),
|
||
'price' => [
|
||
'text' => (float)$row['price_text'],
|
||
'voice' => (float)$row['price_voice'],
|
||
'video' => (float)$row['price_video'],
|
||
],
|
||
'discount' => $member ? 0.9 : 1,
|
||
];
|
||
}, $rows);
|
||
$this->success('', ['list' => $list]);
|
||
}
|
||
|
||
/* ============================================================
|
||
* 内部方法
|
||
* ============================================================ */
|
||
protected function categoriesData()
|
||
{
|
||
$list = [];
|
||
foreach (self::$categories as $key => $text) {
|
||
$list[] = ['key' => $key, 'name' => $text];
|
||
}
|
||
return $list;
|
||
}
|
||
|
||
protected function bookingList($userId, $limit = 20)
|
||
{
|
||
$rows = Db::name('consult_booking')
|
||
->where('user_id', $userId)
|
||
->order('id', 'desc')
|
||
->limit($limit)
|
||
->select();
|
||
|
||
$statusText = ['pending' => '待联系', 'contacted' => '已联系', 'finished' => '已完成', 'canceled' => '已取消'];
|
||
$list = [];
|
||
foreach ($rows as $row) {
|
||
$list[] = [
|
||
'id' => (int)$row['id'],
|
||
'name' => $row['name'],
|
||
'mobile' => substr($row['mobile'], 0, 3) . '****' . substr($row['mobile'], -2),
|
||
'category' => $row['category'],
|
||
'category_text' => self::$categories[$row['category']] ?? $row['category'],
|
||
'prefer_time' => $row['prefer_time'],
|
||
'description' => $row['description'],
|
||
'status' => $row['status'],
|
||
'status_text' => $statusText[$row['status']] ?? $row['status'],
|
||
'create_time' => date('Y-m-d H:i', (int)$row['createtime']),
|
||
];
|
||
}
|
||
return $list;
|
||
}
|
||
}
|