一、心理咨询(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
176 行
7.9 KiB
PHP
176 行
7.9 KiB
PHP
<?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";
|