Initial commit: 心理咨询预约系统

PHP 前后端一体:首页咨询师列表、预约表单、管理后台、二维码准入与邮件通知;敏感配置与提交数据通过 .gitignore 排除。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
litianming
2026-06-04 10:17:09 +08:00
commit 08ce60344d
49 changed files with 3490 additions and 0 deletions

178
api/bootstrap.php Normal file
View 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');
}