feat(api): 初始化笔记应用后端基础架构
- 添加环境配置文件 .env.example 包含数据库、JWT、CORS等配置 - 创建 .gitignore 文件忽略敏感文件和临时文件 - 配置 Apache 重写规则支持路由转发 - 实现 JWT 认证中间件提供用户身份验证功能 - 添加 MySQL 数据库初始化脚本包含分组、图片、笔记表结构
This commit is contained in:
18
biji-php/.env.example
Normal file
18
biji-php/.env.example
Normal file
@@ -0,0 +1,18 @@
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=biji_db
|
||||
DB_USER=root
|
||||
DB_PASS=
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET=your-secret-key-change-this
|
||||
JWT_EXPIRE=86400
|
||||
|
||||
# 应用配置
|
||||
APP_ENV=development
|
||||
APP_DEBUG=true
|
||||
UPLOAD_DIR=../uploads
|
||||
|
||||
# CORS 配置
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
8
biji-php/.gitignore
vendored
Normal file
8
biji-php/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/vendor/
|
||||
/.env
|
||||
/uploads/*
|
||||
!/uploads/.gitkeep
|
||||
.DS_Store
|
||||
.idea/
|
||||
*.log
|
||||
composer.lock
|
||||
130
biji-php/README.md
Normal file
130
biji-php/README.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# 笔记管理系统 - PHP 后端
|
||||
|
||||
基于 PHP 7.4 的笔记管理系统后端,使用 Slim Framework 4 构建 RESTful API。
|
||||
|
||||
## 环境要求
|
||||
|
||||
- PHP 7.4(线程安全版)
|
||||
- MySQL 5.7+
|
||||
- Apache/Nginx
|
||||
- Composer
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
cd biji-php
|
||||
composer install
|
||||
```
|
||||
|
||||
### 2. 配置环境变量
|
||||
|
||||
复制 `.env.example` 为 `.env` 并修改配置:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
编辑 `.env` 文件,配置数据库连接信息:
|
||||
|
||||
```
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=biji_db
|
||||
DB_USER=root
|
||||
DB_PASS=your_password
|
||||
|
||||
JWT_SECRET=your-secret-key-change-this
|
||||
JWT_EXPIRE=86400
|
||||
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
```
|
||||
|
||||
### 3. 导入数据库
|
||||
|
||||
使用 `sql/mysql/all.sql` 文件创建数据库表结构。
|
||||
|
||||
```bash
|
||||
mysql -u root -p biji_db < ../sql/mysql/all.sql
|
||||
```
|
||||
|
||||
### 4. 配置 Web 服务器
|
||||
|
||||
#### Apache 配置
|
||||
|
||||
确保启用了 `mod_rewrite` 模块,并将网站根目录指向 `public` 目录。
|
||||
|
||||
#### Nginx 配置示例
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
root /path/to/biji-php/public;
|
||||
index index.php;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API 接口文档
|
||||
|
||||
### 用户接口 (`/api/user`)
|
||||
|
||||
- `POST /register` - 用户注册
|
||||
- `POST /login` - 用户登录
|
||||
- `POST /validate-token` - 验证 Token(需认证)
|
||||
- `PUT /password` - 更新密码(需认证)
|
||||
- `DELETE /deleteUser` - 删除用户(需认证)
|
||||
|
||||
### Markdown 接口 (`/api/markdown`)
|
||||
|
||||
- `GET /{id}` - 获取笔记内容
|
||||
- `GET /` - 获取所有笔记
|
||||
- `POST /updateMarkdown` - 更新笔记(需认证)
|
||||
- `DELETE /{id}` - 删除笔记(需认证)
|
||||
|
||||
### 分组接口 (`/api/groupings`)
|
||||
|
||||
- `GET /` - 获取所有分组(需认证)
|
||||
- `POST /` - 创建分组(需认证)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
biji-php/
|
||||
├── public/ # Web 根目录
|
||||
│ ├── index.php # 入口文件
|
||||
│ └── .htaccess # Apache 重写规则
|
||||
├── src/ # 源代码
|
||||
│ ├── Controllers/ # 控制器
|
||||
│ ├── Models/ # 模型
|
||||
│ ├── Middleware/ # 中间件
|
||||
│ └── Utils/ # 工具类
|
||||
├── config/ # 配置文件
|
||||
├── uploads/ # 文件上传目录
|
||||
├── vendor/ # Composer 依赖
|
||||
└── composer.json # Composer 配置
|
||||
```
|
||||
|
||||
## 开发说明
|
||||
|
||||
本项目遵循以下原则:
|
||||
- PSR-4 自动加载
|
||||
- RESTful API 设计
|
||||
- JWT Token 认证
|
||||
- MVC 架构模式
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
23
biji-php/composer.json
Normal file
23
biji-php/composer.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "biji/php-backend",
|
||||
"description": "笔记管理系统 PHP 后端",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": "^7.4",
|
||||
"slim/slim": "^4.10",
|
||||
"slim/psr7": "^1.6",
|
||||
"php-di/php-di": "^6.4",
|
||||
"firebase/php-jwt": "^6.3",
|
||||
"vlucas/phpdotenv": "^5.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
biji-php/config/database.php
Normal file
15
biji-php/config/database.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'host' => $_ENV['DB_HOST'] ?? 'localhost',
|
||||
'port' => $_ENV['DB_PORT'] ?? '3306',
|
||||
'database' => $_ENV['DB_NAME'] ?? 'biji_db',
|
||||
'username' => $_ENV['DB_USER'] ?? 'root',
|
||||
'password' => $_ENV['DB_PASS'] ?? '',
|
||||
'charset' => 'utf8mb4',
|
||||
'options' => [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]
|
||||
];
|
||||
62
biji-php/config/routes.php
Normal file
62
biji-php/config/routes.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
use Slim\Routing\RouteCollectorProxy;
|
||||
use App\Controllers\UserController;
|
||||
use App\Controllers\MarkdownController;
|
||||
use App\Controllers\GroupingController;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
// 根路径测试路由
|
||||
$app->get('/', function ($request, $response) {
|
||||
$data = [
|
||||
'status' => 'success',
|
||||
'message' => 'Biji PHP API is running',
|
||||
'version' => '1.0.0',
|
||||
'endpoints' => [
|
||||
'POST /api/user/register' => '用户注册',
|
||||
'POST /api/user/login' => '用户登录',
|
||||
'GET /api/markdown' => '获取所有笔记',
|
||||
'GET /api/markdown/{id}' => '获取单个笔记',
|
||||
'GET /api/groupings' => '获取分组列表'
|
||||
]
|
||||
];
|
||||
$response->getBody()->write(json_encode($data, JSON_UNESCAPED_UNICODE));
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
});
|
||||
|
||||
// 用户相关路由(无需认证)
|
||||
$app->group('/api/user', function (RouteCollectorProxy $group) {
|
||||
$group->post('/register', [UserController::class, 'register']);
|
||||
$group->post('/login', [UserController::class, 'login']);
|
||||
});
|
||||
|
||||
// 用户相关路由(需要认证)
|
||||
$app->group('/api/user', function (RouteCollectorProxy $group) {
|
||||
$group->post('/validate-token', [UserController::class, 'validateToken']);
|
||||
$group->put('/password', [UserController::class, 'updatePassword']);
|
||||
$group->delete('/deleteUser', [UserController::class, 'deleteUser']);
|
||||
})->add(new AuthMiddleware());
|
||||
|
||||
// Markdown 相关路由
|
||||
$app->group('/api/markdown', function (RouteCollectorProxy $group) {
|
||||
$group->get('/{id}', [MarkdownController::class, 'getById']);
|
||||
$group->get('', [MarkdownController::class, 'getAll']);
|
||||
});
|
||||
|
||||
// Markdown 相关路由(需要认证)
|
||||
$app->group('/api/markdown', function (RouteCollectorProxy $group) {
|
||||
$group->post('/updateMarkdown', [MarkdownController::class, 'update']);
|
||||
$group->delete('/{id}', [MarkdownController::class, 'delete']);
|
||||
})->add(new AuthMiddleware());
|
||||
|
||||
// 分组相关路由(无需认证)
|
||||
$app->group('/api/groupings', function (RouteCollectorProxy $group) {
|
||||
$group->get('', [GroupingController::class, 'getAll']);
|
||||
});
|
||||
|
||||
// 分组相关路由(需要认证)
|
||||
$app->group('/api/groupings', function (RouteCollectorProxy $group) {
|
||||
$group->post('', [GroupingController::class, 'create']);
|
||||
$group->put('/{id}', [GroupingController::class, 'update']);
|
||||
$group->delete('/{id}', [GroupingController::class, 'delete']);
|
||||
})->add(new AuthMiddleware());
|
||||
4
biji-php/public/.htaccess
Normal file
4
biji-php/public/.htaccess
Normal file
@@ -0,0 +1,4 @@
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^ index.php [QSA,L]
|
||||
37
biji-php/public/index.php
Normal file
37
biji-php/public/index.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Slim\Factory\AppFactory;
|
||||
use DI\Container;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// 加载环境变量
|
||||
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||
$dotenv->load();
|
||||
|
||||
// 创建容器
|
||||
$container = new Container();
|
||||
AppFactory::setContainer($container);
|
||||
|
||||
// 创建应用
|
||||
$app = AppFactory::create();
|
||||
|
||||
// 添加路由中间件(必须在路由定义之前)
|
||||
$app->addRoutingMiddleware();
|
||||
|
||||
// 添加错误处理
|
||||
$app->addErrorMiddleware(true, true, true);
|
||||
|
||||
// 添加 CORS 中间件
|
||||
$app->add(new \App\Middleware\CorsMiddleware());
|
||||
|
||||
// 处理 OPTIONS 请求
|
||||
$app->options('/{routes:.+}', function ($request, $response) {
|
||||
return $response;
|
||||
});
|
||||
|
||||
// 加载路由
|
||||
require __DIR__ . '/../config/routes.php';
|
||||
|
||||
$app->run();
|
||||
42
biji-php/src/Controllers/GroupingController.php
Normal file
42
biji-php/src/Controllers/GroupingController.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use App\Models\Grouping;
|
||||
use App\Utils\Response as ApiResponse;
|
||||
|
||||
class GroupingController
|
||||
{
|
||||
public function getAll(Request $request, Response $response)
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
$parentId = $params['parentId'] ?? null;
|
||||
|
||||
$model = new Grouping();
|
||||
$groupings = $model->getAll($parentId);
|
||||
|
||||
$response->getBody()->write(json_encode(ApiResponse::success($groupings), JSON_UNESCAPED_UNICODE));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
public function create(Request $request, Response $response)
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$grouping = $data['grouping'] ?? '';
|
||||
$parentId = $data['parentId'] ?? 0;
|
||||
|
||||
if (empty($grouping)) {
|
||||
$response->getBody()->write(json_encode(ApiResponse::fail('分组名称不能为空'), JSON_UNESCAPED_UNICODE));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(400);
|
||||
}
|
||||
|
||||
$model = new Grouping();
|
||||
$id = $model->create($grouping, $parentId);
|
||||
$created = $model->findById($id);
|
||||
|
||||
$response->getBody()->write(json_encode(ApiResponse::success($created), JSON_UNESCAPED_UNICODE));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
}
|
||||
}
|
||||
76
biji-php/src/Controllers/MarkdownController.php
Normal file
76
biji-php/src/Controllers/MarkdownController.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use App\Models\MarkdownFile;
|
||||
use App\Utils\Response as ApiResponse;
|
||||
|
||||
class MarkdownController
|
||||
{
|
||||
public function getById(Request $request, Response $response, $args)
|
||||
{
|
||||
$id = $args['id'];
|
||||
$model = new MarkdownFile();
|
||||
$file = $model->findById($id);
|
||||
|
||||
if (!$file) {
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('文件不存在')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(404);
|
||||
}
|
||||
|
||||
// 检查是否为私密笔记
|
||||
if ($file['is_private'] == 1) {
|
||||
// 检查是否已认证
|
||||
$userId = $request->getAttribute('userId');
|
||||
if (!$userId) {
|
||||
// 未认证,返回空内容
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::success('')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::success($file['content'])));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
public function getAll(Request $request, Response $response)
|
||||
{
|
||||
$model = new MarkdownFile();
|
||||
$files = $model->getAll();
|
||||
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::success($files)));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
public function update(Request $request, Response $response)
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$id = $data['id'] ?? null;
|
||||
|
||||
if (!$id) {
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('缺少文件ID')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(400);
|
||||
}
|
||||
|
||||
$model = new MarkdownFile();
|
||||
$model->update($id, $data);
|
||||
|
||||
$file = $model->findById($id);
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::success($file)));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
public function delete(Request $request, Response $response, $args)
|
||||
{
|
||||
$id = $args['id'];
|
||||
$userId = $request->getAttribute('userId');
|
||||
|
||||
$model = new MarkdownFile();
|
||||
$model->softDelete($id, $userId);
|
||||
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::success(null, '删除成功')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
}
|
||||
}
|
||||
137
biji-php/src/Controllers/UserController.php
Normal file
137
biji-php/src/Controllers/UserController.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use App\Models\User;
|
||||
use App\Models\SystemSetting;
|
||||
use App\Models\RegistrationCode;
|
||||
use App\Utils\JWTUtil;
|
||||
use App\Utils\Response as ApiResponse;
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function register(Request $request, Response $response)
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$username = $data['username'] ?? '';
|
||||
$password = $data['password'] ?? '';
|
||||
$email = $data['email'] ?? '';
|
||||
$registrationCode = $data['registrationCode'] ?? '';
|
||||
|
||||
// 验证输入
|
||||
if (empty($username) || empty($password) || empty($email)) {
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('用户名、密码和邮箱不能为空')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(400);
|
||||
}
|
||||
|
||||
// 检查注册功能是否开启
|
||||
$systemSetting = new SystemSetting();
|
||||
if (!$systemSetting->isRegistrationEnabled()) {
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('注册功能已关闭')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(403);
|
||||
}
|
||||
|
||||
// 验证注册码
|
||||
$regCodeModel = new RegistrationCode();
|
||||
if (!$regCodeModel->validateCode($registrationCode)) {
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('无效或已过期的注册码')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(400);
|
||||
}
|
||||
|
||||
$userModel = new User();
|
||||
|
||||
// 检查用户名是否已存在
|
||||
if ($userModel->findByUsername($username)) {
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('用户名已存在')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(400);
|
||||
}
|
||||
|
||||
try {
|
||||
$userId = $userModel->create($username, $password, $email);
|
||||
$user = $userModel->findById($userId);
|
||||
unset($user['password']); // 不返回密码
|
||||
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::success($user, '注册成功')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
} catch (\Exception $e) {
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::error('注册失败: ' . $e->getMessage())));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(500);
|
||||
}
|
||||
}
|
||||
|
||||
public function login(Request $request, Response $response)
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$username = $data['username'] ?? '';
|
||||
$password = $data['password'] ?? '';
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('用户名和密码不能为空')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(400);
|
||||
}
|
||||
|
||||
$userModel = new User();
|
||||
$user = $userModel->findByUsername($username);
|
||||
|
||||
if (!$user || !$userModel->verifyPassword($password, $user['password'])) {
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('用户名或密码错误')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(401);
|
||||
}
|
||||
|
||||
// 生成 JWT token
|
||||
$token = JWTUtil::encode($user['id'], $user['username']);
|
||||
$expireTime = date('Y-m-d H:i:s', time() + ($_ENV['JWT_EXPIRE'] ?? 86400));
|
||||
|
||||
// 更新数据库中的 token
|
||||
$userModel->updateToken($user['id'], $token, $expireTime);
|
||||
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::success(['token' => $token], '登录成功')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
public function validateToken(Request $request, Response $response)
|
||||
{
|
||||
// 如果能到达这里,说明 token 已经通过中间件验证
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::success(null, 'Token is valid')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
public function updatePassword(Request $request, Response $response)
|
||||
{
|
||||
$userId = $request->getAttribute('userId');
|
||||
$data = $request->getParsedBody();
|
||||
$oldPassword = $data['oldPassword'] ?? '';
|
||||
$newPassword = $data['newPassword'] ?? '';
|
||||
|
||||
if (empty($oldPassword) || empty($newPassword)) {
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('旧密码和新密码不能为空')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(400);
|
||||
}
|
||||
|
||||
$userModel = new User();
|
||||
$user = $userModel->findById($userId);
|
||||
|
||||
if (!$userModel->verifyPassword($oldPassword, $user['password'])) {
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('旧密码错误')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(400);
|
||||
}
|
||||
|
||||
$userModel->updatePassword($userId, $newPassword);
|
||||
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::success(null, '密码更新成功')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
public function deleteUser(Request $request, Response $response)
|
||||
{
|
||||
$userId = $request->getAttribute('userId');
|
||||
|
||||
$userModel = new User();
|
||||
$userModel->delete($userId);
|
||||
|
||||
$response->getBody()->write(ApiResponse::json(ApiResponse::success(null, '用户删除成功')));
|
||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
}
|
||||
}
|
||||
47
biji-php/src/Middleware/AuthMiddleware.php
Normal file
47
biji-php/src/Middleware/AuthMiddleware.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||
use Slim\Psr7\Response;
|
||||
use App\Utils\JWTUtil;
|
||||
|
||||
class AuthMiddleware
|
||||
{
|
||||
public function __invoke(Request $request, RequestHandler $handler): Response
|
||||
{
|
||||
$authHeader = $request->getHeaderLine('Authorization');
|
||||
|
||||
if (empty($authHeader)) {
|
||||
$response = new Response();
|
||||
$response->getBody()->write(json_encode([
|
||||
'code' => 401,
|
||||
'message' => '未提供认证令牌',
|
||||
'data' => null
|
||||
]));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
|
||||
}
|
||||
|
||||
// 提取 token (Bearer token)
|
||||
$token = str_replace('Bearer ', '', $authHeader);
|
||||
|
||||
$decoded = JWTUtil::decode($token);
|
||||
|
||||
if ($decoded === null) {
|
||||
$response = new Response();
|
||||
$response->getBody()->write(json_encode([
|
||||
'code' => 401,
|
||||
'message' => '无效或过期的令牌',
|
||||
'data' => null
|
||||
]));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
|
||||
}
|
||||
|
||||
// 将用户信息添加到请求属性中
|
||||
$request = $request->withAttribute('userId', $decoded['userId']);
|
||||
$request = $request->withAttribute('username', $decoded['username']);
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
23
biji-php/src/Middleware/CorsMiddleware.php
Normal file
23
biji-php/src/Middleware/CorsMiddleware.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||
use Slim\Psr7\Response;
|
||||
|
||||
class CorsMiddleware
|
||||
{
|
||||
public function __invoke(Request $request, RequestHandler $handler): Response
|
||||
{
|
||||
$response = $handler->handle($request);
|
||||
|
||||
$origin = $_ENV['CORS_ORIGIN'] ?? '*';
|
||||
|
||||
return $response
|
||||
->withHeader('Access-Control-Allow-Origin', $origin)
|
||||
->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization')
|
||||
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
|
||||
->withHeader('Access-Control-Allow-Credentials', 'true');
|
||||
}
|
||||
}
|
||||
57
biji-php/src/Models/Grouping.php
Normal file
57
biji-php/src/Models/Grouping.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Utils\Database;
|
||||
use PDO;
|
||||
|
||||
class Grouping
|
||||
{
|
||||
private $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance()->getConnection();
|
||||
}
|
||||
|
||||
public function getAll($parentId = null)
|
||||
{
|
||||
if ($parentId === null) {
|
||||
$stmt = $this->db->prepare("SELECT * FROM `grouping` WHERE is_deleted = 0 ORDER BY id");
|
||||
} else {
|
||||
$stmt = $this->db->prepare("SELECT * FROM `grouping` WHERE parentId = ? AND is_deleted = 0 ORDER BY id");
|
||||
$stmt->execute([$parentId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function findById($id)
|
||||
{
|
||||
$stmt = $this->db->prepare("SELECT * FROM `grouping` WHERE id = ? AND is_deleted = 0");
|
||||
$stmt->execute([$id]);
|
||||
return $stmt->fetch();
|
||||
}
|
||||
|
||||
public function create($grouping, $parentId = 0)
|
||||
{
|
||||
$stmt = $this->db->prepare("INSERT INTO `grouping` (`grouping`, parentId) VALUES (?, ?)");
|
||||
$stmt->execute([$grouping, $parentId]);
|
||||
return $this->db->lastInsertId();
|
||||
}
|
||||
|
||||
public function update($id, $grouping)
|
||||
{
|
||||
$stmt = $this->db->prepare("UPDATE `grouping` SET `grouping` = ? WHERE id = ?");
|
||||
return $stmt->execute([$grouping, $id]);
|
||||
}
|
||||
|
||||
public function softDelete($id, $userId)
|
||||
{
|
||||
$stmt = $this->db->prepare(
|
||||
"UPDATE `grouping` SET is_deleted = 1, deleted_at = NOW(), deleted_by = ? WHERE id = ?"
|
||||
);
|
||||
return $stmt->execute([$userId, $id]);
|
||||
}
|
||||
}
|
||||
122
biji-php/src/Models/MarkdownFile.php
Normal file
122
biji-php/src/Models/MarkdownFile.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Utils\Database;
|
||||
use PDO;
|
||||
|
||||
class MarkdownFile
|
||||
{
|
||||
private $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance()->getConnection();
|
||||
}
|
||||
|
||||
public function findById($id, $includeDeleted = false)
|
||||
{
|
||||
$sql = "SELECT * FROM markdown_file WHERE id = ?";
|
||||
if (!$includeDeleted) {
|
||||
$sql .= " AND is_deleted = 0";
|
||||
}
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([$id]);
|
||||
return $stmt->fetch();
|
||||
}
|
||||
|
||||
public function getAll()
|
||||
{
|
||||
$stmt = $this->db->prepare("SELECT * FROM markdown_file WHERE is_deleted = 0 ORDER BY updated_at DESC");
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function getByGroupingId($groupingId)
|
||||
{
|
||||
$stmt = $this->db->prepare(
|
||||
"SELECT id, title, file_name, created_at, updated_at, is_private, grouping_id
|
||||
FROM markdown_file
|
||||
WHERE grouping_id = ? AND is_deleted = 0
|
||||
ORDER BY updated_at DESC"
|
||||
);
|
||||
$stmt->execute([$groupingId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function searchByTitle($keyword)
|
||||
{
|
||||
$stmt = $this->db->prepare(
|
||||
"SELECT * FROM markdown_file
|
||||
WHERE title LIKE ? AND is_deleted = 0
|
||||
ORDER BY updated_at DESC"
|
||||
);
|
||||
$stmt->execute(['%' . $keyword . '%']);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function getRecent($limit = 12)
|
||||
{
|
||||
$stmt = $this->db->prepare(
|
||||
"SELECT id, title, file_name, created_at, updated_at, is_private, grouping_id
|
||||
FROM markdown_file
|
||||
WHERE is_deleted = 0
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT ?"
|
||||
);
|
||||
$stmt->execute([$limit]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function update($id, $data)
|
||||
{
|
||||
$fields = [];
|
||||
$values = [];
|
||||
|
||||
if (isset($data['title'])) {
|
||||
$fields[] = "title = ?";
|
||||
$values[] = $data['title'];
|
||||
}
|
||||
if (isset($data['content'])) {
|
||||
$fields[] = "content = ?";
|
||||
$values[] = $data['content'];
|
||||
}
|
||||
if (isset($data['grouping_id'])) {
|
||||
$fields[] = "grouping_id = ?";
|
||||
$values[] = $data['grouping_id'];
|
||||
}
|
||||
if (isset($data['is_private'])) {
|
||||
$fields[] = "is_private = ?";
|
||||
$values[] = $data['is_private'];
|
||||
}
|
||||
|
||||
$fields[] = "updated_at = NOW()";
|
||||
$values[] = $id;
|
||||
|
||||
$sql = "UPDATE markdown_file SET " . implode(", ", $fields) . " WHERE id = ?";
|
||||
$stmt = $this->db->prepare($sql);
|
||||
return $stmt->execute($values);
|
||||
}
|
||||
|
||||
public function softDelete($id, $userId)
|
||||
{
|
||||
$stmt = $this->db->prepare(
|
||||
"UPDATE markdown_file SET is_deleted = 1, deleted_at = NOW(), deleted_by = ? WHERE id = ?"
|
||||
);
|
||||
return $stmt->execute([$userId, $id]);
|
||||
}
|
||||
|
||||
public function restore($id)
|
||||
{
|
||||
$stmt = $this->db->prepare(
|
||||
"UPDATE markdown_file SET is_deleted = 0, deleted_at = NULL, deleted_by = NULL WHERE id = ?"
|
||||
);
|
||||
return $stmt->execute([$id]);
|
||||
}
|
||||
|
||||
public function permanentDelete($id)
|
||||
{
|
||||
$stmt = $this->db->prepare("DELETE FROM markdown_file WHERE id = ?");
|
||||
return $stmt->execute([$id]);
|
||||
}
|
||||
}
|
||||
38
biji-php/src/Models/RegistrationCode.php
Normal file
38
biji-php/src/Models/RegistrationCode.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Utils\Database;
|
||||
use PDO;
|
||||
|
||||
class RegistrationCode
|
||||
{
|
||||
private $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance()->getConnection();
|
||||
}
|
||||
|
||||
public function generateCode($createdBy)
|
||||
{
|
||||
$code = bin2hex(random_bytes(16));
|
||||
$expiryTime = date('Y-m-d H:i:s', strtotime('+7 days'));
|
||||
|
||||
$stmt = $this->db->prepare(
|
||||
"INSERT INTO registration_codes (code, expiry_time, created_by, created_at) VALUES (?, ?, ?, NOW())"
|
||||
);
|
||||
$stmt->execute([$code, $expiryTime, $createdBy]);
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
public function validateCode($code)
|
||||
{
|
||||
$stmt = $this->db->prepare(
|
||||
"SELECT * FROM registration_codes WHERE code = ? AND expiry_time > NOW()"
|
||||
);
|
||||
$stmt->execute([$code]);
|
||||
return $stmt->fetch() !== false;
|
||||
}
|
||||
}
|
||||
44
biji-php/src/Models/SystemSetting.php
Normal file
44
biji-php/src/Models/SystemSetting.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Utils\Database;
|
||||
use PDO;
|
||||
|
||||
class SystemSetting
|
||||
{
|
||||
private $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance()->getConnection();
|
||||
}
|
||||
|
||||
public function getSetting($key)
|
||||
{
|
||||
$stmt = $this->db->prepare("SELECT setting_value FROM system_settings WHERE setting_key = ?");
|
||||
$stmt->execute([$key]);
|
||||
$result = $stmt->fetch();
|
||||
return $result ? $result['setting_value'] : null;
|
||||
}
|
||||
|
||||
public function setSetting($key, $value)
|
||||
{
|
||||
$stmt = $this->db->prepare(
|
||||
"INSERT INTO system_settings (setting_key, setting_value) VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE setting_value = ?"
|
||||
);
|
||||
return $stmt->execute([$key, $value, $value]);
|
||||
}
|
||||
|
||||
public function isRegistrationEnabled()
|
||||
{
|
||||
$value = $this->getSetting('registration_enabled');
|
||||
return $value === '1' || $value === 'true';
|
||||
}
|
||||
|
||||
public function setRegistrationEnabled($enabled)
|
||||
{
|
||||
return $this->setSetting('registration_enabled', $enabled ? '1' : '0');
|
||||
}
|
||||
}
|
||||
64
biji-php/src/Models/User.php
Normal file
64
biji-php/src/Models/User.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Utils\Database;
|
||||
use PDO;
|
||||
|
||||
class User
|
||||
{
|
||||
private $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance()->getConnection();
|
||||
}
|
||||
|
||||
public function findByUsername($username)
|
||||
{
|
||||
$stmt = $this->db->prepare("SELECT * FROM user WHERE username = ?");
|
||||
$stmt->execute([$username]);
|
||||
return $stmt->fetch();
|
||||
}
|
||||
|
||||
public function findById($id)
|
||||
{
|
||||
$stmt = $this->db->prepare("SELECT * FROM user WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
return $stmt->fetch();
|
||||
}
|
||||
|
||||
public function create($username, $password, $email)
|
||||
{
|
||||
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
|
||||
$stmt = $this->db->prepare(
|
||||
"INSERT INTO user (username, password, email, created_at, updated_at) VALUES (?, ?, ?, NOW(), NOW())"
|
||||
);
|
||||
$stmt->execute([$username, $hashedPassword, $email]);
|
||||
return $this->db->lastInsertId();
|
||||
}
|
||||
|
||||
public function updateToken($userId, $token, $expireTime)
|
||||
{
|
||||
$stmt = $this->db->prepare("UPDATE user SET token = ?, token_enddata = ? WHERE id = ?");
|
||||
return $stmt->execute([$token, $expireTime, $userId]);
|
||||
}
|
||||
|
||||
public function updatePassword($userId, $newPassword)
|
||||
{
|
||||
$hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
|
||||
$stmt = $this->db->prepare("UPDATE user SET password = ?, updated_at = NOW() WHERE id = ?");
|
||||
return $stmt->execute([$hashedPassword, $userId]);
|
||||
}
|
||||
|
||||
public function delete($userId)
|
||||
{
|
||||
$stmt = $this->db->prepare("DELETE FROM user WHERE id = ?");
|
||||
return $stmt->execute([$userId]);
|
||||
}
|
||||
|
||||
public function verifyPassword($password, $hashedPassword)
|
||||
{
|
||||
return password_verify($password, $hashedPassword);
|
||||
}
|
||||
}
|
||||
49
biji-php/src/Utils/Database.php
Normal file
49
biji-php/src/Utils/Database.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Utils;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
|
||||
class Database
|
||||
{
|
||||
private static $instance = null;
|
||||
private $connection;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
$config = require __DIR__ . '/../../config/database.php';
|
||||
|
||||
$dsn = sprintf(
|
||||
"mysql:host=%s;port=%s;dbname=%s;charset=%s",
|
||||
$config['host'],
|
||||
$config['port'],
|
||||
$config['database'],
|
||||
$config['charset']
|
||||
);
|
||||
|
||||
try {
|
||||
$this->connection = new PDO(
|
||||
$dsn,
|
||||
$config['username'],
|
||||
$config['password'],
|
||||
$config['options']
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
throw new \Exception("数据库连接失败: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function getConnection()
|
||||
{
|
||||
return $this->connection;
|
||||
}
|
||||
}
|
||||
45
biji-php/src/Utils/JWTUtil.php
Normal file
45
biji-php/src/Utils/JWTUtil.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Utils;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
|
||||
class JWTUtil
|
||||
{
|
||||
private static $secret;
|
||||
private static $expire;
|
||||
|
||||
public static function init()
|
||||
{
|
||||
self::$secret = $_ENV['JWT_SECRET'] ?? 'default-secret-key';
|
||||
self::$expire = (int)($_ENV['JWT_EXPIRE'] ?? 86400);
|
||||
}
|
||||
|
||||
public static function encode($userId, $username)
|
||||
{
|
||||
self::init();
|
||||
|
||||
$payload = [
|
||||
'iss' => 'biji-php',
|
||||
'iat' => time(),
|
||||
'exp' => time() + self::$expire,
|
||||
'userId' => $userId,
|
||||
'username' => $username
|
||||
];
|
||||
|
||||
return JWT::encode($payload, self::$secret, 'HS256');
|
||||
}
|
||||
|
||||
public static function decode($token)
|
||||
{
|
||||
self::init();
|
||||
|
||||
try {
|
||||
$decoded = JWT::decode($token, new Key(self::$secret, 'HS256'));
|
||||
return (array)$decoded;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
biji-php/src/Utils/Response.php
Normal file
41
biji-php/src/Utils/Response.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Utils;
|
||||
|
||||
class Response
|
||||
{
|
||||
public static function success($data = null, $message = '操作成功')
|
||||
{
|
||||
return [
|
||||
'code' => 200,
|
||||
'message' => $message,
|
||||
'data' => $data
|
||||
];
|
||||
}
|
||||
|
||||
public static function fail($message = '操作失败', $code = 400)
|
||||
{
|
||||
return [
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
|
||||
public static function error($message = '服务器错误', $code = 500)
|
||||
{
|
||||
return [
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数组转换为 JSON 字符串(中文不转义)
|
||||
*/
|
||||
public static function json($data)
|
||||
{
|
||||
return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
||||
0
biji-php/uploads/.gitkeep
Normal file
0
biji-php/uploads/.gitkeep
Normal file
Reference in New Issue
Block a user