文件
huaqiapp/scripts/verify/admin_backend_check.php
linda 56f5f60aa5 feat(admin): 后台登录页品牌化 + 26 个业务模块与前端对齐 + 数据同步打通
一、登录页重构(暖色调品牌版)
- 左右分栏卡片:44% 品牌面板(花红渐变 + 光斑 + 内联 SVG 花瓣 Logo + 花期字标/口号)
  + 56% 表单面板(52px 输入框、内嵌矢量图标、聚焦暖色光晕、渐变主按钮)
- 与前台同一套设计令牌(#E8604C / #F58C7C / #FFF9F5 / #3E2C27),响应式适配
- 完整保留 #login-form/#pd-form-username/#profile-img/#errtips/token/验证码/保持登录等结构,后台登录 JS 零改动

二、后台业务模块(26 个,与前端 M1–M9 一一对应)
- 新增通用 Manage 控制器(列表/搜索/排序/新增/编辑/软删除/批量/列元数据)
- 新增 ManageConfig(26 张表的字段标签/类型/枚举/搜索配置)与 FormBuilder(表单控件渲染)
- 新增通用视图 manage/{index,add,edit,style}.html + public/assets/js/backend/manage.js
- 新增 26 个薄控制器(各 8 行,仅声明 )
- db/05_admin_menu.sql:花期运营菜单树 + 6 个通用操作权限,含站点名品牌化

三、品牌一致性
- site.php 站点名改为「花期运营中心」,顶栏 Logo 增加矢量花瓣
- 业务页暖色表头、渐变按钮、圆角输入、品牌色开关

四、验证(全绿)
- 后台自检 31 项:登录页品牌、框架菜单、26 模块页面 200 + 列表数据、后台编辑写库、
  后台改动 → 前台 API 同步读取、数据还原
- 前台接口 82 项断言、契约 84 接口、Vue 20/20、SCSS 19/19、无 emoji 均通过
- 修复 5 个真实问题:方法签名不兼容致 500、菜单缓存、SQL 字符集乱码、
  snake_case URL 与权限规则命名、会话文件属主导致的自检 500
2026-11-06 11:10:19 +08:00

175 行
7.8 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' => '咨询订单', 'assessment' => '量表管理', 'assessment_record' => '测评记录',
'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";