文件
huaqiapp/scripts/verify/admin_backend_check.php
linda 3baa52e95a feat(consult): 咨询改为在线预约表单 + 新增 SDS/SAS/16PF 付费心理测评
一、心理咨询(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
2026-11-06 16:00:52 +08:00

176 行
7.9 KiB
PHP

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

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

<?php
/**
* 花期 APP · 运营后台全链路自检(在服务器上执行)
*
* 用途:验证「运营后台 26 个业务模块 + 通用 CRUD + 前后端数据同步」
* 用法:
* php scripts/verify/admin_backend_check.php /www/wwwroot/huaqiapp.gzyakj.com.cn huaqiapp.gzyakj.com.cn
*
* 说明:
* - 通过读取 .env 获取数据库账号,构造一个管理员会话(避免依赖验证码)用于自检;
* - 仅做只读校验与一次「改标题→读前台接口→还原」的同步验证;
* - 不写入任何凭据到仓库。
*/
$site = $argv[1] ?? '';
$host = $argv[2] ?? '';
if (!$site || !$host) {
fwrite(STDERR, "用法: php admin_backend_check.php <站点目录> <站点域名>\n");
exit(2);
}
$env = parse_ini_file($site . '/.env', true);
$db = $env['database'] ?? [];
$pdo = new PDO(
'mysql:host=' . ($db['hostname'] ?? '127.0.0.1') . ';port=' . ($db['hostport'] ?? 3306) . ';dbname=' . $db['database'] . ';charset=utf8mb4',
$db['username'] ?? '',
$db['password'] ?? ''
);
$prefix = $db['prefix'] ?? 'fa_';
// 读取 token.key(用于构造管理员 safecode)
$config = file_get_contents($site . '/application/config.php');
preg_match("/'key'\s*=>\s*'([^']+)'/", $config, $m);
$tokenKey = $m[1] ?? '';
$admin = $pdo->query("SELECT * FROM {$prefix}admin WHERE id=1")->fetch(PDO::FETCH_ASSOC);
$sid = bin2hex(random_bytes(16));
$safecode = md5(md5($admin['username']) . md5(substr($admin['password'], 0, 6)) . $tokenKey);
$admin['safecode'] = $safecode;
$admin['loginip'] = '127.0.0.1';
$admin['logintime'] = time();
session_id($sid);
session_start();
$_SESSION['think'] = ['admin' => $admin];
session_write_close();
$sessPath = session_save_path() ?: sys_get_temp_dir();
$sessFile = rtrim($sessPath, '/') . '/sess_' . $sid;
// Web 进程(www)需要可读写该会话文件,否则 session_start 会因 O_RDWR 失败
@chown($sessFile, 'www');
@chgrp($sessFile, 'www');
@chmod($sessFile, 0660);
$base = 'http://127.0.0.1/wechLIGOut.php';
$cookie = 'PHPSESSID=' . $sid;
// 前台 API 基址(用于数据同步验证)
$apiBase = 'http://127.0.0.1';
function http_get($url, $cookie, $ajax = false)
{
$opts = ['http' => ['method' => 'GET', 'header' => "Host: {$GLOBALS['host']}\r\nCookie: {$cookie}\r\n" . ($ajax ? "X-Requested-With: XMLHttpRequest\r\n" : ''), 'timeout' => 20, 'ignore_errors' => true]];
$res = @file_get_contents($url, false, stream_context_create($opts));
$status = 0;
foreach ($http_response_header ?? [] as $line) {
if (preg_match('#HTTP/\S+\s+(\d{3})#', $line, $mm)) {
$status = (int)$mm[1];
}
}
return [$status, (string)$res];
}
function http_post($url, $cookie, $data)
{
$body = http_build_query($data);
$opts = ['http' => ['method' => 'POST', 'header' => "Host: {$GLOBALS['host']}\r\nCookie: {$cookie}\r\nX-Requested-With: XMLHttpRequest\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: " . strlen($body) . "\r\n", 'content' => $body, 'timeout' => 20, 'ignore_errors' => true]];
$res = @file_get_contents($url, false, stream_context_create($opts));
return json_decode((string)$res, true) ?: ['code' => 0, 'msg' => substr((string)$res, 0, 120)];
}
$modules = [
'cycle' => '周期档案', 'cycle_log' => '每日症状记录', 'symptom_tag' => '症状标签', 'reminder' => '智能提醒',
'mood_log' => '情绪日记', 'content' => '内容库', 'content_category' => '内容分类',
'bbt_record' => '基础体温', 'lh_test' => '试纸识别', 'task' => '任务管理',
'family_bind' => '家人绑定', 'care_message' => '沟通留言', 'point_log' => '爱心积分',
'memory' => '纪念日', 'course_order' => '课程订单',
'baby' => '宝宝档案', 'growth_record' => '成长记录', 'vaccine' => '疫苗与儿保',
'counselor' => '咨询师', 'consult_order' => '咨询订单', 'consult_booking' => '心理预约',
'assessment' => '量表管理', 'assessment_record' => '测评记录', 'assessment_order' => '测评订单',
'member_plan' => '会员套餐', 'member_order' => '会员订单', 'user_setting' => '隐私与提醒', 'user_backup' => '数据备份',
];
$pass = 0;
$fail = [];
// 1. 登录页品牌
list($code, $html) = http_get($base . '/index/login', '');
$ok = $code === 200 && strpos($html, '登录运营后台') !== false && strpos($html, '花期') !== false;
$ok ? $pass++ : $fail[] = '登录页品牌渲染';
echo ($ok ? ' ✅' : ' ❌') . " 登录页(暖色调品牌版)\n";
// 2. 品牌与菜单
list($code, $html) = http_get($base . '/index/index', $cookie);
$ok = $code === 200 && strpos($html, '花期运营') !== false && strpos($html, '周期档案') !== false;
$ok ? $pass++ : $fail[] = '后台框架与菜单';
echo ($ok ? ' ✅' : ' ❌') . " 后台框架 + 花期运营菜单\n";
// 3. 26 个模块页面与列表接口
foreach ($modules as $module => $title) {
list($code, $html) = http_get($base . '/' . $module . '/index', $cookie);
list($acode, $json) = http_get($base . '/' . $module . '/index?limit=2&offset=0', $cookie, true);
$data = json_decode($json, true);
$ok = $code === 200 && strpos($html, 'hq-admin') !== false && is_array($data) && isset($data['total']);
if ($ok) {
$pass++;
printf(" ✅ %-18s 页面200 · 数据 %s 条\n", $title, $data['total']);
} else {
$fail[] = $module;
printf(" ❌ %-18s http=%s ajax=%s\n", $title, $code, substr($json, 0, 80));
}
}
// 4. 数据同步:后台改内容标题 → 数据库 → 前台接口 → 还原
$row = $pdo->query("SELECT id,title FROM {$prefix}content WHERE type='advice' ORDER BY id LIMIT 1")->fetch(PDO::FETCH_ASSOC);
if ($row) {
$newTitle = '同步自检' . date('His');
// 4.1 后台编辑写库
$res = http_post($base . '/content/edit?ids=' . $row['id'], $cookie, ['title' => $newTitle]);
$dbTitle = $pdo->query("SELECT title FROM {$prefix}content WHERE id=" . (int)$row['id'])->fetchColumn();
$okWrite = (($res['code'] ?? 0) == 1) && $dbTitle === $newTitle;
$okWrite ? $pass++ : $fail[] = '后台编辑写库';
echo ($okWrite ? ' ✅' : ' ❌') . " 后台编辑写入数据库\n";
// 4.2 前台接口读取(会员 token)
$token = '';
$loginCtx = stream_context_create(['http' => [
'method' => 'POST',
'header' => "Host: {$host}\r\nContent-Type: application/x-www-form-urlencoded\r\n",
'content' => http_build_query(['account' => 'demo', 'password' => 'demo123456']),
'timeout' => 15, 'ignore_errors' => true,
]]);
$login = json_decode((string)@file_get_contents($apiBase . '/api/user/login', false, $loginCtx), true);
$token = $login['data']['userinfo']['token'] ?? '';
$apiTitle = '';
if ($token) {
$ctx = stream_context_create(['http' => [
'method' => 'GET',
'header' => "Host: {$host}\r\ntoken: {$token}\r\n",
'timeout' => 15, 'ignore_errors' => true,
]]);
$api = json_decode((string)@file_get_contents($apiBase . '/api/content/detail?id=' . $row['id'], false, $ctx), true);
$apiTitle = $api['data']['title'] ?? '';
}
$okSync = $apiTitle === $newTitle;
$okSync ? $pass++ : $fail[] = '前台接口同步';
echo ($okSync ? ' ✅' : ' ❌') . " 后台改动 → 前台 API 同步(读取到:{$apiTitle})\n";
// 4.3 还原
http_post($base . '/content/edit?ids=' . $row['id'], $cookie, ['title' => $row['title']]);
$restored = $pdo->query("SELECT title FROM {$prefix}content WHERE id=" . (int)$row['id'])->fetchColumn();
$okRestore = $restored === $row['title'];
$okRestore ? $pass++ : $fail[] = '内容还原';
echo ($okRestore ? ' ✅' : ' ❌') . " 数据还原({$restored})\n";
}
echo "=" . str_repeat('=', 60) . "\n";
echo "通过 {$pass} 项,失败 " . count($fail) . " 项\n";
@unlink($sessFile);
if ($fail) {
echo "失败项:" . implode('、', $fail) . "\n";
exit(1);
}
echo "🎉 运营后台全链路自检通过\n";