Initial commit: 心理咨询预约系统
PHP 前后端一体:首页咨询师列表、预约表单、管理后台、二维码准入与邮件通知;敏感配置与提交数据通过 .gitignore 排除。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
178
api/bootstrap.php
Normal file
178
api/bootstrap.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
|
||||
define('ROOT_DIR', dirname(__DIR__));
|
||||
define('DATA_DIR', ROOT_DIR . '/data');
|
||||
define('CONFIG_FILE', ROOT_DIR . '/config/app.json');
|
||||
define('SUBMISSIONS_FILE', DATA_DIR . '/submissions.json');
|
||||
|
||||
function loadAppConfig(): array
|
||||
{
|
||||
static $config = null;
|
||||
if ($config !== null) {
|
||||
return $config;
|
||||
}
|
||||
if (!is_file(CONFIG_FILE)) {
|
||||
jsonError('配置文件不存在', 500);
|
||||
}
|
||||
$raw = file_get_contents(CONFIG_FILE);
|
||||
$config = json_decode($raw ?: '{}', true);
|
||||
if (!is_array($config)) {
|
||||
jsonError('配置文件格式错误', 500);
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
function jsonResponse($data, int $code = 200): void
|
||||
{
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
function jsonError(string $message, int $code = 400): void
|
||||
{
|
||||
jsonResponse(['ok' => false, 'message' => $message], $code);
|
||||
}
|
||||
|
||||
function readJsonFile(string $path): array
|
||||
{
|
||||
if (!is_file($path)) {
|
||||
jsonError('数据文件不存在: ' . basename($path), 500);
|
||||
}
|
||||
$data = json_decode(file_get_contents($path) ?: '[]', true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
function getRequestBody(): array
|
||||
{
|
||||
$raw = file_get_contents('php://input');
|
||||
$data = json_decode($raw ?: '{}', true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
function requireAdmin(): void
|
||||
{
|
||||
if (empty($_SESSION['admin'])) {
|
||||
jsonError('未登录或会话已过期', 401);
|
||||
}
|
||||
}
|
||||
|
||||
function readSubmissions(): array
|
||||
{
|
||||
if (!is_file(SUBMISSIONS_FILE)) {
|
||||
return [];
|
||||
}
|
||||
$data = json_decode(file_get_contents(SUBMISSIONS_FILE) ?: '[]', true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
function getDataWriteError(): ?string
|
||||
{
|
||||
if (!is_dir(DATA_DIR)) {
|
||||
if (!@mkdir(DATA_DIR, 0755, true) && !is_dir(DATA_DIR)) {
|
||||
return 'data 目录无法创建。请在宝塔:文件 → data → 权限设为 755,属主 www';
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_writable(DATA_DIR)) {
|
||||
return 'data 目录不可写。请在宝塔:文件 → data → 权限 755,属主 www,勾选「应用到子目录」';
|
||||
}
|
||||
|
||||
if (is_file(SUBMISSIONS_FILE) && !is_writable(SUBMISSIONS_FILE)) {
|
||||
return 'submissions.json 不可写。请在宝塔:data/submissions.json → 权限 664,属主 www';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveSubmissions(array $submissions): bool
|
||||
{
|
||||
$writeError = getDataWriteError();
|
||||
if ($writeError !== null) {
|
||||
error_log('[data] ' . $writeError);
|
||||
return false;
|
||||
}
|
||||
|
||||
$json = json_encode($submissions, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
$written = @file_put_contents(SUBMISSIONS_FILE, $json, LOCK_EX);
|
||||
|
||||
if ($written === false && !is_file(SUBMISSIONS_FILE)) {
|
||||
$written = @file_put_contents(SUBMISSIONS_FILE, $json, LOCK_EX);
|
||||
}
|
||||
|
||||
return $written !== false;
|
||||
}
|
||||
|
||||
function getSaveSubmissionsError(): string
|
||||
{
|
||||
$writeError = getDataWriteError();
|
||||
if ($writeError !== null) {
|
||||
return $writeError;
|
||||
}
|
||||
return '保存失败,请稍后重试';
|
||||
}
|
||||
|
||||
function validatePhone(string $phone): bool
|
||||
{
|
||||
return (bool) preg_match('/^1[3-9]\d{9}$/', $phone);
|
||||
}
|
||||
|
||||
function getAssetVersion(): string
|
||||
{
|
||||
static $version = null;
|
||||
if ($version !== null) {
|
||||
return $version;
|
||||
}
|
||||
|
||||
try {
|
||||
$config = loadAppConfig();
|
||||
if (!empty($config['assetVersion'])) {
|
||||
$version = (string) $config['assetVersion'];
|
||||
return $version;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// 页面未就绪时回退到文件时间戳
|
||||
}
|
||||
|
||||
$max = 0;
|
||||
foreach (['css', 'js', 'assets', 'assets/avatars'] as $dirName) {
|
||||
$dir = ROOT_DIR . '/' . $dirName;
|
||||
if (!is_dir($dir)) {
|
||||
continue;
|
||||
}
|
||||
$files = glob($dir . '/*');
|
||||
if ($files === false) {
|
||||
continue;
|
||||
}
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
$max = max($max, (int) filemtime($file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$version = $max > 0 ? (string) $max : date('YmdHis');
|
||||
return $version;
|
||||
}
|
||||
|
||||
function asset(string $path): string
|
||||
{
|
||||
$path = ltrim($path, '/');
|
||||
$sep = str_contains($path, '?') ? '&' : '?';
|
||||
return $path . $sep . 'v=' . rawurlencode(getAssetVersion());
|
||||
}
|
||||
|
||||
function sendHtmlNoCacheHeaders(): void
|
||||
{
|
||||
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
}
|
||||
Reference in New Issue
Block a user