Compare commits
43 Commits
8d1807d2b1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 524980c2f0 | |||
| 99ebd16de1 | |||
| d69aa6f872 | |||
| ea585442d3 | |||
| 1beb9d3476 | |||
| 24b8d25771 | |||
| 58ec002802 | |||
| b37a0f9794 | |||
| 07c2d15f02 | |||
| 4acd0eebad | |||
| 377061a780 | |||
| 5a8cf2cbec | |||
| 7d43eaabdc | |||
| b6fffab885 | |||
| bbcd2eb80f | |||
| 4e964c8a18 | |||
| 9f05541fa8 | |||
| 156291ac8c | |||
| 1fdd0e62f1 | |||
| 2300f9b7a0 | |||
| a487a13b89 | |||
| b0312ef19e | |||
| 9d32b4ac97 | |||
| 9427294682 | |||
| 21ddf36151 | |||
| 4532b29910 | |||
| 781e95c5c1 | |||
| 4e3ed1b849 | |||
| 23fbafedab | |||
| af4edfc4ae | |||
| b5d3a6aaf3 | |||
| cd38932eb0 | |||
| a1598ce243 | |||
| df5373adbc | |||
| cb59dba8e6 | |||
| b4081f82cc | |||
| a0cca46f87 | |||
| 058edd62e0 | |||
| 25c9002cbe | |||
| 7eba30ee46 | |||
| 9c46609f33 | |||
| 437976fdf2 | |||
| 608d05cf5e |
@@ -0,0 +1,58 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, develop]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# 代码检查
|
||||||
|
lint:
|
||||||
|
name: 代码检查
|
||||||
|
runs-on: linux
|
||||||
|
container:
|
||||||
|
image: gitea-runner-php:latest
|
||||||
|
steps:
|
||||||
|
- name: 检出代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 安装依赖
|
||||||
|
run: composer install --prefer-dist --no-progress --no-interaction
|
||||||
|
|
||||||
|
- name: PHP 语法检查2
|
||||||
|
run: |
|
||||||
|
echo "=== PHP 语法检查 ==="
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# 检查每个文件,把错误的文件名记录下来
|
||||||
|
for file in $(find . -name "*.php" -not -path "./vendor/*"); do
|
||||||
|
if ! php -l "$file" > /dev/null 2>&1; then
|
||||||
|
echo "❌ 错误文件: $file"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $ERRORS -gt 0 ]; then
|
||||||
|
echo "=== 发现 $ERRORS 个语法错误 ==="
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "=== 所有 PHP 文件语法正确 ==="
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 单元测试
|
||||||
|
test:
|
||||||
|
name: 单元测试
|
||||||
|
runs-on: linux
|
||||||
|
container:
|
||||||
|
image: gitea-runner-php:latest
|
||||||
|
needs: lint
|
||||||
|
steps:
|
||||||
|
- name: 检出代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 安装依赖
|
||||||
|
run: composer install --prefer-dist --no-progress --no-interaction
|
||||||
|
|
||||||
|
- name: 运行测试
|
||||||
|
run: composer test
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: '部署环境'
|
||||||
|
required: true
|
||||||
|
default: 'staging'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- staging
|
||||||
|
- production
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# 部署到测试环境
|
||||||
|
deploy-staging:
|
||||||
|
name: 部署测试环境
|
||||||
|
runs-on: linux
|
||||||
|
container:
|
||||||
|
image: gitea-runner-php:latest
|
||||||
|
if: github.ref == 'refs/heads/develop'
|
||||||
|
steps:
|
||||||
|
- name: 设置 SSH 密钥
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed255
|
||||||
|
echo "Host micecola.top" >> ~/.ssh/config
|
||||||
|
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
||||||
|
|
||||||
|
- name: 部署到测试服务器
|
||||||
|
run: |
|
||||||
|
echo "开始部署到测试环境..."
|
||||||
|
ssh root@micecola.top << 'EOF'
|
||||||
|
cd /var/www/kb-web-test
|
||||||
|
|
||||||
|
# 备份
|
||||||
|
BACKUP_DIR="/var/www/kb-web-test-backups"
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
CURRENT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
tar -czf "$BACKUP_DIR/backup_${CURRENT_COMMIT}_$(date +%Y%m%d_%H%M%S).tar.gz" . 2>/dev/null || true
|
||||||
|
|
||||||
|
# 更新代码
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard origin/develop
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
composer install --no-dev --prefer-dist --no-interaction
|
||||||
|
|
||||||
|
# 设置权限
|
||||||
|
chown -R www-data:www-data . 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "✅ 测试环境部署完成"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: 完成
|
||||||
|
run: echo "🚀 测试环境部署完成: http://micecola.top:8080"
|
||||||
|
|
||||||
|
# 部署到正式环境
|
||||||
|
deploy-production:
|
||||||
|
name: 部署正式环境
|
||||||
|
runs-on: linux
|
||||||
|
container:
|
||||||
|
image: gitea-runner-php:latest
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
steps:
|
||||||
|
- name: 设置 SSH 密钥
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed255
|
||||||
|
echo "Host micecola.top" >> ~/.ssh/config
|
||||||
|
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
||||||
|
|
||||||
|
- name: 备份并部署
|
||||||
|
run: |
|
||||||
|
echo "开始部署到正式环境..."
|
||||||
|
ssh root@micecola.top << 'EOF'
|
||||||
|
# 备份
|
||||||
|
BACKUP_DIR="/var/www/kb-web-prod-backups"
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
cd /var/www/kb-web-prod
|
||||||
|
CURRENT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
tar -czf "$BACKUP_DIR/backup_${CURRENT_COMMIT}_$(date +%Y%m%d_%H%M%S).tar.gz" . 2>/dev/null || true
|
||||||
|
|
||||||
|
# 保留最近 10 个备份
|
||||||
|
cd $BACKUP_DIR && ls -t | tail -n +11 | xargs -r rm -f
|
||||||
|
|
||||||
|
# 部署
|
||||||
|
cd /var/www/kb-web-prod
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard origin/main
|
||||||
|
composer install --no-dev --prefer-dist --no-interaction
|
||||||
|
chown -R www-data:www-data . 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "✅ 正式环境部署完成"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: 完成
|
||||||
|
run: echo "🚀 正式环境部署完成: https://kb.micecola.top"
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
name: Test CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['feature/**', 'bugfix/**', 'refactor/**']
|
||||||
|
pull_request:
|
||||||
|
branches: [main, develop]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# 快速测试
|
||||||
|
quick-test:
|
||||||
|
name: 快速测试
|
||||||
|
runs-on: linux
|
||||||
|
container:
|
||||||
|
image: gitea-runner-php:latest
|
||||||
|
steps:
|
||||||
|
- name: 检出代码
|
||||||
|
run: |
|
||||||
|
git clone -b ${{ github.ref_name }} ${{ github.server_url }}/${{ github.repository }}.git .
|
||||||
|
git checkout ${{ github.sha }}
|
||||||
|
|
||||||
|
- name: 检查环境
|
||||||
|
run: |
|
||||||
|
echo "PHP: $(php -v | head -1)"
|
||||||
|
echo "Composer: $(composer --version)"
|
||||||
|
echo "Node: $(node -v)"
|
||||||
|
|
||||||
|
- name: PHP 语法检查
|
||||||
|
run: |
|
||||||
|
echo "📝 PHP 语法检查..."
|
||||||
|
errors=0
|
||||||
|
for file in $(find . -name "*.php" -not -path "./vendor/*"); do
|
||||||
|
output=$(php -l "$file" 2>&1)
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "错误: $file"
|
||||||
|
echo "$output"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ $errors -eq 0 ]; then
|
||||||
|
echo "✅ 所有 PHP 文件语法正确"
|
||||||
|
else
|
||||||
|
echo "❌ 发现 $errors 个语法错误"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 安装依赖
|
||||||
|
run: composer install --prefer-dist --no-progress --no-interaction
|
||||||
|
|
||||||
|
- name: 运行测试
|
||||||
|
run: |
|
||||||
|
if composer test 2>/dev/null; then
|
||||||
|
echo "✅ 测试通过"
|
||||||
|
else
|
||||||
|
echo "⚠️ 测试未配置或跳过"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 完成
|
||||||
|
run: |
|
||||||
|
echo "=================================="
|
||||||
|
echo "✅ CI 测试通过!"
|
||||||
|
echo "分支: ${{ github.ref_name }}"
|
||||||
|
echo "提交: ${{ github.sha }}"
|
||||||
|
echo "=================================="
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
# 依赖
|
||||||
|
/vendor/
|
||||||
|
|
||||||
|
# 测试缓存
|
||||||
|
/.phpunit.cache/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# 系统文件
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# 环境配置
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
config.local.php
|
||||||
|
config.php
|
||||||
|
|
||||||
|
# 用户数据目录 (每个用户独立)
|
||||||
|
/*/
|
||||||
|
|
||||||
|
# 但保留这些目录
|
||||||
|
!/deploy/
|
||||||
|
!/tests/
|
||||||
|
!/src/
|
||||||
|
!/.gitea/
|
||||||
|
!/docs/
|
||||||
@@ -1 +1,138 @@
|
|||||||
11
|
# 知识库网页系统
|
||||||
|
|
||||||
|
一个简单的知识库文档管理系统,支持多用户登录、文档列表和搜索。1
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- ✅ 账号密码登录
|
||||||
|
- ✅ 每个用户有独立的知识库目录
|
||||||
|
- ✅ 文档列表展示
|
||||||
|
- ✅ 关键词搜索(标题+内容)
|
||||||
|
- ✅ 在线查看文档(支持 Markdown)
|
||||||
|
- ✅ 管理员添加用户
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
kb-web/
|
||||||
|
├── index.php # 登录页
|
||||||
|
├── dashboard.php # 文档列表 + 搜索
|
||||||
|
├── view.php # 查看文档内容
|
||||||
|
├── logout.php # 退出登录
|
||||||
|
├── config.php # 数据库配置
|
||||||
|
├── add_user.php # 添加用户脚本
|
||||||
|
├── style.css # 样式
|
||||||
|
├── init.sql # 数据库初始化
|
||||||
|
├── README.md # 说明文档
|
||||||
|
└── {username}/ # 用户文档目录
|
||||||
|
├── 文档1.md
|
||||||
|
└── 文档2.md
|
||||||
|
```
|
||||||
|
1
|
||||||
|
## 部署步骤
|
||||||
|
|
||||||
|
### 1. 上传文件
|
||||||
|
|
||||||
|
将整个 `kb-web` 目录上传到服务器的网站目录中。
|
||||||
|
|
||||||
|
### 2. 创建数据库
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 登录 MySQL
|
||||||
|
mysql -u root -p
|
||||||
|
|
||||||
|
# 执行初始化脚本
|
||||||
|
source init.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
或者直接在 MySQL 中执行:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE IF NOT EXISTS kb_web DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 修改配置
|
||||||
|
|
||||||
|
编辑 `config.php`,修改数据库连接信息:
|
||||||
|
|
||||||
|
```php
|
||||||
|
define('DB_HOST', 'localhost');
|
||||||
|
define('DB_NAME', 'kb_web');
|
||||||
|
define('DB_USER', 'your_db_user');
|
||||||
|
define('DB_PASS', 'your_db_password');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 添加用户
|
||||||
|
|
||||||
|
在服务器上执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/kb-web
|
||||||
|
php add_user.php 用户名 密码
|
||||||
|
```
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```bash
|
||||||
|
php add_user.php zhangqing mypassword123
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 添加文档
|
||||||
|
|
||||||
|
将用户的文档(.md 或 .txt 格式)放入对应用户目录:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp ~/knowledge-base/analyzed/*.md /path/to/kb-web/zhangqing/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 访问
|
||||||
|
|
||||||
|
打开浏览器访问:`http://your-domain.com/kb-web/`
|
||||||
|
|
||||||
|
## 管理员操作
|
||||||
|
|
||||||
|
### 添加新用户
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php add_user.php newuser newpassword
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重置密码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 删除旧用户
|
||||||
|
mysql -u root -p -e "DELETE FROM kb_web.users WHERE username='olduser';"
|
||||||
|
|
||||||
|
# 重新添加
|
||||||
|
php add_user.php olduser newpassword
|
||||||
|
```
|
||||||
|
|
||||||
|
### 批量导入文档
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 将分析好的文档复制到用户目录
|
||||||
|
cp ~/work/knowledge-base/analyzed/*.md /path/to/kb-web/zhangqing/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **安全性**
|
||||||
|
- 修改 `config.php` 中的数据库密码
|
||||||
|
- 确保 `config.php` 不可被外部访问(LNMP 默认已处理)
|
||||||
|
- 定期备份数据库
|
||||||
|
|
||||||
|
2. **文件权限**
|
||||||
|
```bash
|
||||||
|
chown -R www-data:www-data /path/to/kb-web
|
||||||
|
chmod -R 755 /path/to/kb-web
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **支持的文档格式**
|
||||||
|
- .md (Markdown)
|
||||||
|
- .txt (纯文本)
|
||||||
|
- .markdown
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- PHP 7.4+
|
||||||
|
- MySQL 5.7+
|
||||||
|
- 纯前端(HTML + CSS + JS)
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 添加用户脚本
|
||||||
|
* 使用方法: php add_user.php <username> <password>
|
||||||
|
* 示例: php add_user.php zhangqing mypassword123
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ($argc < 3) {
|
||||||
|
echo "使用方法: php add_user.php <username> <password>\n";
|
||||||
|
echo "示例: php add_user.php zhangqing mypassword123\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = $argv[1];
|
||||||
|
$password = $argv[2];
|
||||||
|
|
||||||
|
// 验证用户名格式
|
||||||
|
if (!preg_match('/^[a-zA-Z0-9_]{3,50}$/', $username)) {
|
||||||
|
echo "错误: 用户名只能是字母、数字、下划线,长度3-50\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码长度
|
||||||
|
if (strlen($password) < 6) {
|
||||||
|
echo "错误: 密码长度至少6位\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// 检查用户是否已存在
|
||||||
|
$stmt = $db->prepare("SELECT id FROM users WHERE username = ?");
|
||||||
|
$stmt->execute([$username]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
echo "错误: 用户 '$username' 已存在\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建密码哈希
|
||||||
|
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
// 插入用户
|
||||||
|
$stmt = $db->prepare("INSERT INTO users (username, password_hash) VALUES (?, ?)");
|
||||||
|
$stmt->execute([$username, $passwordHash]);
|
||||||
|
|
||||||
|
// 创建用户文档目录
|
||||||
|
$docDir = __DIR__ . '/' . $username;
|
||||||
|
if (!is_dir($docDir)) {
|
||||||
|
mkdir($docDir, 0755, true);
|
||||||
|
echo "✅ 用户目录已创建: $docDir\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✅ 用户 '$username' 添加成功!\n";
|
||||||
|
echo " 用户名: $username\n";
|
||||||
|
echo " 密码: $password\n";
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "数据库错误: " . $e->getMessage() . "\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "starmen1265/kb-web",
|
||||||
|
"description": "知识库网页系统",
|
||||||
|
"type": "project",
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^10.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "phpunit --testdox",
|
||||||
|
"test:coverage": "phpunit --coverage-html coverage"
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
// 知识库网页系统 - 配置文件
|
||||||
|
// 部署时请修改以下配置
|
||||||
|
|
||||||
|
// 数据库配置
|
||||||
|
define('DB_HOST', 'localhost');
|
||||||
|
define('DB_NAME', 'kb_web');
|
||||||
|
define('DB_USER', 'root');
|
||||||
|
define('DB_PASS', 'your_secret');
|
||||||
|
|
||||||
|
// Session 配置
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// 数据库连接
|
||||||
|
function getDB() {
|
||||||
|
static $pdo = null;
|
||||||
|
if ($pdo === null) {
|
||||||
|
try {
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
|
||||||
|
DB_USER,
|
||||||
|
DB_PASS,
|
||||||
|
[
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("数据库连接失败: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
function checkLogin() {
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
function getCurrentUser() {
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
|
return $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户文档目录
|
||||||
|
function getUserDocDir($username) {
|
||||||
|
return __DIR__ . '/' . $username;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
// 测试环境配置文件
|
||||||
|
// 复制此文件为 config.php 并修改数据库配置
|
||||||
|
|
||||||
|
// 数据库配置
|
||||||
|
define('DB_HOST', 'localhost');
|
||||||
|
define('DB_NAME', 'kb-web-test'); // 测试环境数据库
|
||||||
|
define('DB_USER', 'kb-admin');
|
||||||
|
define('DB_PASS', 'your_password'); // 修改为实际密码
|
||||||
|
|
||||||
|
// Session 配置
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// 数据库连接
|
||||||
|
function getDB() {
|
||||||
|
static $pdo = null;
|
||||||
|
if ($pdo === null) {
|
||||||
|
try {
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
|
||||||
|
DB_USER,
|
||||||
|
DB_PASS,
|
||||||
|
[
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("数据库连接失败: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
function checkLogin() {
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
function getCurrentUser() {
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
|
return $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户文档目录
|
||||||
|
function getUserDocDir($username) {
|
||||||
|
return __DIR__ . '/' . $username;
|
||||||
|
}
|
||||||
+268
@@ -0,0 +1,268 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'config.php';
|
||||||
|
checkLogin();
|
||||||
|
|
||||||
|
$user = getCurrentUser();
|
||||||
|
$username = $user['username'];
|
||||||
|
$docDir = getUserDocDir($username);
|
||||||
|
|
||||||
|
// 搜索关键词
|
||||||
|
$search = trim($_GET['q'] ?? '');
|
||||||
|
|
||||||
|
// 解析 Markdown 文件的 YAML front matter
|
||||||
|
function parseMarkdownWithMeta($filePath) {
|
||||||
|
$content = file_get_contents($filePath);
|
||||||
|
$meta = [
|
||||||
|
'title' => basename($filePath),
|
||||||
|
'author' => '',
|
||||||
|
'date' => '',
|
||||||
|
'platform' => '',
|
||||||
|
'url' => '',
|
||||||
|
'tags' => [],
|
||||||
|
'duration' => '',
|
||||||
|
'type' => 'article',
|
||||||
|
'word_count' => 0,
|
||||||
|
'summary' => ''
|
||||||
|
];
|
||||||
|
|
||||||
|
// 解析 YAML front matter
|
||||||
|
if (preg_match('/^---\s*\n(.*?)\n---\s*\n(.*)$/s', $content, $matches)) {
|
||||||
|
$yaml = $matches[1];
|
||||||
|
$body = $matches[2];
|
||||||
|
|
||||||
|
// 解析 YAML 字段
|
||||||
|
if (preg_match('/^title:\s*["\']?(.+?)["\']?\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['title'] = trim($m[1], '"\'');
|
||||||
|
}
|
||||||
|
if (preg_match('/^author:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['author'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^date:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['date'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^platform:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['platform'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^url:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['url'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^duration:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['duration'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
// 解析 type 字段
|
||||||
|
if (preg_match('/^type:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['type'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
// 如果有 duration 但没有明确 type,则设为 video
|
||||||
|
if ($meta['duration'] && $meta['type'] === 'article') {
|
||||||
|
$meta['type'] = 'video';
|
||||||
|
}
|
||||||
|
if (preg_match('/^tags:\s*\[(.+)\]\s*$/m', $yaml, $m)) {
|
||||||
|
$tagsStr = $m[1];
|
||||||
|
preg_match_all('/"([^"]+)"/', $tagsStr, $tagMatches);
|
||||||
|
$meta['tags'] = $tagMatches[1] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算正文字数(文章类型)
|
||||||
|
if ($meta['type'] === 'article') {
|
||||||
|
// 移除 Markdown 标记,计算纯文字字数
|
||||||
|
$plainText = strip_tags($body);
|
||||||
|
$plainText = preg_replace('/[#*_`\[\]()]/', '', $plainText);
|
||||||
|
$meta['word_count'] = mb_strlen(preg_replace('/\s+/', '', $plainText));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取正文概述
|
||||||
|
$lines = preg_split('/\r?\n/', trim($body));
|
||||||
|
$summaryLines = [];
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if (empty($line) || strpos($line, '##') === 0) continue;
|
||||||
|
$summaryLines[] = $line;
|
||||||
|
if (count($summaryLines) >= 2) break;
|
||||||
|
}
|
||||||
|
$meta['summary'] = implode(' ', $summaryLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归获取文档列表
|
||||||
|
$documents = [];
|
||||||
|
|
||||||
|
function scanDirectory($dir, $baseDir, $search) {
|
||||||
|
$docs = [];
|
||||||
|
if (!is_dir($dir)) return $docs;
|
||||||
|
|
||||||
|
$items = scandir($dir);
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if ($item === '.' || $item === '..') continue;
|
||||||
|
|
||||||
|
$fullPath = $dir . '/' . $item;
|
||||||
|
$relativePath = str_replace($baseDir . '/', '', $fullPath);
|
||||||
|
|
||||||
|
if (is_dir($fullPath)) {
|
||||||
|
$docs = array_merge($docs, scanDirectory($fullPath, $baseDir, $search));
|
||||||
|
} elseif (is_file($fullPath)) {
|
||||||
|
$ext = strtolower(pathinfo($item, PATHINFO_EXTENSION));
|
||||||
|
if (!in_array($ext, ['md', 'txt', 'markdown'])) continue;
|
||||||
|
|
||||||
|
$content = file_get_contents($fullPath);
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$searchLower = mb_strtolower($search);
|
||||||
|
$contentLower = mb_strtolower($content);
|
||||||
|
if (strpos($contentLower, $searchLower) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = parseMarkdownWithMeta($fullPath);
|
||||||
|
|
||||||
|
$docs[] = [
|
||||||
|
'name' => $item,
|
||||||
|
'path' => $relativePath,
|
||||||
|
'folder' => dirname($relativePath) === '.' ? '' : dirname($relativePath),
|
||||||
|
'mtime' => filemtime($fullPath),
|
||||||
|
'title' => $meta['title'],
|
||||||
|
'author' => $meta['author'],
|
||||||
|
'date' => $meta['date'],
|
||||||
|
'platform' => $meta['platform'],
|
||||||
|
'url' => $meta['url'],
|
||||||
|
'tags' => $meta['tags'],
|
||||||
|
'duration' => $meta['duration'],
|
||||||
|
'type' => $meta['type'],
|
||||||
|
'word_count' => $meta['word_count'],
|
||||||
|
'summary' => $meta['summary']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
$documents = scanDirectory($docDir, $docDir, $search);
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
usort($documents, function($a, $b) {
|
||||||
|
if ($a['date'] && $b['date']) {
|
||||||
|
return strcmp($b['date'], $a['date']);
|
||||||
|
}
|
||||||
|
return $b['mtime'] - $a['mtime'];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 平台配置
|
||||||
|
function getPlatformInfo($platform) {
|
||||||
|
$platforms = [
|
||||||
|
'bilibili' => ['name' => 'Bilibili', 'icon' => '📺', 'color' => '#fb7299'],
|
||||||
|
'youtube' => ['name' => 'YouTube', 'icon' => '▶️', 'color' => '#ff0000'],
|
||||||
|
'zhihu' => ['name' => '知乎', 'icon' => '💡', 'color' => '#0084ff'],
|
||||||
|
'wechat' => ['name' => '微信', 'icon' => '💬', 'color' => '#07c160'],
|
||||||
|
];
|
||||||
|
return $platforms[$platform] ?? ['name' => $platform ?: '未知', 'icon' => '📄', 'color' => '#666'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型图标
|
||||||
|
function getTypeIcon($type) {
|
||||||
|
return $type === 'video' ? '🎬' : '📝';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>知识库 - <?php echo htmlspecialchars($username); ?></title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="dashboard">
|
||||||
|
<header class="header">
|
||||||
|
<h1>📚 我的知识库</h1>
|
||||||
|
<div class="user-info">
|
||||||
|
<span><?php echo htmlspecialchars($username); ?></span>
|
||||||
|
<a href="logout.php" class="btn-logout">退出</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="search-bar">
|
||||||
|
<form method="GET" action="">
|
||||||
|
<input type="text" name="q" placeholder="搜索文档..."
|
||||||
|
value="<?php echo htmlspecialchars($search); ?>">
|
||||||
|
<button type="submit">搜索</button>
|
||||||
|
<?php if ($search): ?>
|
||||||
|
<a href="dashboard.php" class="btn-clear">清除</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
共 <?php echo count($documents); ?> 个文档
|
||||||
|
<?php if ($search): ?>
|
||||||
|
<span class="search-hint">(搜索: "<?php echo htmlspecialchars($search); ?>")</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="document-list">
|
||||||
|
<?php if (empty($documents)): ?>
|
||||||
|
<div class="empty">
|
||||||
|
<?php if ($search): ?>
|
||||||
|
没有找到匹配的文档
|
||||||
|
<?php else: ?>
|
||||||
|
暂无文档
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($documents as $doc):
|
||||||
|
$platformInfo = getPlatformInfo($doc['platform']);
|
||||||
|
?>
|
||||||
|
<a href="view.php?file=<?php echo urlencode($doc['path']); ?><?php echo $search ? '&q=' . urlencode($search) : ''; ?>"
|
||||||
|
class="document-card">
|
||||||
|
<div class="doc-icon"><?php echo $platformInfo['icon']; ?></div>
|
||||||
|
<div class="doc-info">
|
||||||
|
<div class="doc-header">
|
||||||
|
<span class="doc-type"><?php echo getTypeIcon($doc['type']); ?></span>
|
||||||
|
<span class="doc-title"><?php echo htmlspecialchars($doc['title']); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="doc-meta">
|
||||||
|
<?php if ($doc['author']): ?>
|
||||||
|
<span class="meta-item">👤 <?php echo htmlspecialchars($doc['author']); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($doc['type'] === 'article' && $doc['word_count'] > 0): ?>
|
||||||
|
<span class="meta-item">📝 <?php echo number_format($doc['word_count']); ?> 字</span>
|
||||||
|
<?php elseif ($doc['date']): ?>
|
||||||
|
<span class="meta-item">📅 <?php echo htmlspecialchars($doc['date']); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($doc['platform']): ?>
|
||||||
|
<span class="meta-item platform-badge" style="--platform-color: <?php echo $platformInfo['color']; ?>">
|
||||||
|
<?php echo $platformInfo['name']; ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($doc['duration']): ?>
|
||||||
|
<span class="meta-item">⏱️ <?php echo htmlspecialchars($doc['duration']); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($doc['url']): ?>
|
||||||
|
<span class="meta-item source-link" onclick="event.stopPropagation(); event.preventDefault(); window.open('<?php echo htmlspecialchars($doc['url']); ?>', '_blank');">
|
||||||
|
🔗 源链接
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php if (!empty($doc['tags'])): ?>
|
||||||
|
<div class="doc-tags">
|
||||||
|
<?php foreach (array_slice($doc['tags'], 0, 5) as $tag): ?>
|
||||||
|
<span class="tag"><?php echo htmlspecialchars($tag); ?></span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (count($doc['tags']) > 5): ?>
|
||||||
|
<span class="tag-more">+<?php echo count($doc['tags']) - 5; ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($doc['summary']): ?>
|
||||||
|
<div class="doc-summary"><?php echo htmlspecialchars(mb_substr($doc['summary'], 0, 150)); ?><?php echo mb_strlen($doc['summary']) > 150 ? '...' : ''; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 这是一个示例文档
|
||||||
|
|
||||||
|
这是您的知识库示例文档。
|
||||||
|
|
||||||
|
## 主要内容
|
||||||
|
|
||||||
|
- 支持搜索功能
|
||||||
|
- 支持 Markdown 格式
|
||||||
|
- 可以查看文档预览
|
||||||
|
|
||||||
|
## 代码示例
|
||||||
|
|
||||||
|
```python
|
||||||
|
def hello():
|
||||||
|
print("Hello, Knowledge Base!")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 链接
|
||||||
|
|
||||||
|
这是一个 [示例链接](https://example.com)。
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# kb-web 自动部署配置
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
|
||||||
|
```
|
||||||
|
本地 push → Gitea (192.168.1.22) → Webhook → micecola.top
|
||||||
|
├── /var/www/kb-web-test (测试 :8080)
|
||||||
|
└── /var/www/kb-web-prod (正式域名)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速配置
|
||||||
|
|
||||||
|
### 第一步:在服务器执行配置脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH 登录服务器
|
||||||
|
ssh root@micecola.top
|
||||||
|
|
||||||
|
# 下载并执行配置脚本
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/你的仓库/deploy/setup-server.sh | bash
|
||||||
|
|
||||||
|
# 或者直接复制脚本内容执行
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第二步:添加 Gitea 部署密钥
|
||||||
|
|
||||||
|
服务器执行后会显示一个公钥,将它添加到 Gitea:
|
||||||
|
|
||||||
|
1. 打开 https://192.168.1.22 用户设置 → SSH Keys
|
||||||
|
2. 或在仓库设置 → Deploy Keys
|
||||||
|
3. 粘贴公钥内容
|
||||||
|
|
||||||
|
### 第三步:配置 Gitea Webhook
|
||||||
|
|
||||||
|
在 Gitea 仓库 `kb-web` 的设置中:
|
||||||
|
|
||||||
|
| 配置项 | 值 |
|
||||||
|
|--------|-----|
|
||||||
|
| URL | `http://micecola.top:8080/webhook/receiver.php` |
|
||||||
|
| 密钥 | `kb-web-webhook-secret-2024` |
|
||||||
|
| 触发事件 | Push |
|
||||||
|
| 分支过滤 | main |
|
||||||
|
|
||||||
|
### 第四步:测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 本地修改后推送
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
# 查看服务器日志
|
||||||
|
ssh root@micecola.top "tail -f /var/log/webhook.log"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 访问地址
|
||||||
|
|
||||||
|
| 环境 | 地址 |
|
||||||
|
|------|------|
|
||||||
|
| 测试 | http://micecola.top:8080 |
|
||||||
|
| 正式 | https://kb.micecola.top (需配置域名) |
|
||||||
|
|
||||||
|
## 手动部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试环境
|
||||||
|
/var/www/webhook/deploy.sh test main
|
||||||
|
|
||||||
|
# 正式环境
|
||||||
|
/var/www/webhook/deploy.sh prod main
|
||||||
|
```
|
||||||
|
|
||||||
|
## 正式环境部署流程
|
||||||
|
|
||||||
|
推荐流程:先测试环境验证 → 再手动部署正式环境
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. push 自动部署到测试环境
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
# 2. 验证测试环境 OK
|
||||||
|
# 访问 http://micecola.top:8080
|
||||||
|
|
||||||
|
# 3. 手动部署到正式环境
|
||||||
|
ssh root@micecola.top "/var/www/webhook/deploy.sh prod main"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
```
|
||||||
|
deploy/
|
||||||
|
├── setup-server.sh # 服务器一键配置脚本
|
||||||
|
├── deploy.sh # 部署脚本 (部署到服务器 /var/www/webhook/)
|
||||||
|
├── webhook-receiver.php # Webhook 接收器 (部署到服务器)
|
||||||
|
└── README.md # 本文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 回滚
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看备份
|
||||||
|
ls /var/www/kb-web-test-backups/
|
||||||
|
|
||||||
|
# 恢复备份
|
||||||
|
tar -xzf /var/www/kb-web-test-backups/backup_xxx.tar.gz -C /var/www/kb-web-test
|
||||||
|
```
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# kb-web 自动部署脚本
|
||||||
|
# 用法: ./deploy.sh [test|prod] [branch]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DEPLOY_ENV=${1:-test}
|
||||||
|
BRANCH=${2:-main}
|
||||||
|
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
case $DEPLOY_ENV in
|
||||||
|
test)
|
||||||
|
DEPLOY_DIR="/var/www/kb-web-test"
|
||||||
|
BACKUP_DIR="/var/www/kb-web-test-backups"
|
||||||
|
;;
|
||||||
|
prod)
|
||||||
|
DEPLOY_DIR="/var/www/kb-web-prod"
|
||||||
|
BACKUP_DIR="/var/www/kb-web-prod-backups"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "用法: $0 [test|prod] [branch]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
REPO_URL="ssh://sc-gitea@192.168.1.22:2223/starmen1265/kb-web.git"
|
||||||
|
LOG_FILE="/var/log/kb-web-deploy.log"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
log "========== 开始部署 $DEPLOY_ENV 环境 =========="
|
||||||
|
log "分支: $BRANCH"
|
||||||
|
log "目标目录: $DEPLOY_DIR"
|
||||||
|
|
||||||
|
# 创建目录
|
||||||
|
mkdir -p "$DEPLOY_DIR" "$BACKUP_DIR" "$(dirname "$LOG_FILE")"
|
||||||
|
|
||||||
|
# 备份当前版本
|
||||||
|
if [ -d "$DEPLOY_DIR/.git" ]; then
|
||||||
|
log "备份当前版本..."
|
||||||
|
CURRENT_COMMIT=$(cd "$DEPLOY_DIR" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
tar -czf "$BACKUP_DIR/backup_${CURRENT_COMMIT}_${TIMESTAMP}.tar.gz" -C "$DEPLOY_DIR" . 2>/dev/null || true
|
||||||
|
# 只保留最近5个备份
|
||||||
|
cd "$BACKUP_DIR" && ls -t | tail -n +6 | xargs -r rm -f
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 拉取代码
|
||||||
|
if [ -d "$DEPLOY_DIR/.git" ]; then
|
||||||
|
log "更新代码..."
|
||||||
|
cd "$DEPLOY_DIR"
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard "origin/$BRANCH"
|
||||||
|
else
|
||||||
|
log "克隆代码..."
|
||||||
|
git clone -b "$BRANCH" "$REPO_URL" "$DEPLOY_DIR"
|
||||||
|
cd "$DEPLOY_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 设置权限
|
||||||
|
log "设置权限..."
|
||||||
|
chown -R www-data:www-data "$DEPLOY_DIR" 2>/dev/null || chown -R nginx:nginx "$DEPLOY_DIR" 2>/dev/null || true
|
||||||
|
find "$DEPLOY_DIR" -type d -exec chmod 755 {} \;
|
||||||
|
find "$DEPLOY_DIR" -type f -exec chmod 644 {} \;
|
||||||
|
|
||||||
|
# 记录部署信息
|
||||||
|
DEPLOY_INFO="$DEPLOY_DIR/.deploy_info"
|
||||||
|
cat > "$DEPLOY_INFO" << EOF
|
||||||
|
环境: $DEPLOY_ENV
|
||||||
|
分支: $BRANCH
|
||||||
|
提交: $(git rev-parse HEAD)
|
||||||
|
时间: $(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
部署者: webhook
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "部署完成!"
|
||||||
|
log "提交: $(git log -1 --oneline)"
|
||||||
|
log "=========================================="
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 服务器一键配置脚本
|
||||||
|
# 在 micecola.top 上执行此脚本
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo " kb-web 自动部署环境配置"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# 创建目录结构
|
||||||
|
echo "[1/5] 创建目录结构..."
|
||||||
|
mkdir -p /var/www/kb-web-test
|
||||||
|
mkdir -p /var/www/kb-web-prod
|
||||||
|
mkdir -p /var/www/kb-web-test-backups
|
||||||
|
mkdir -p /var/www/kb-web-prod-backups
|
||||||
|
mkdir -p /var/www/webhook
|
||||||
|
mkdir -p /var/log
|
||||||
|
|
||||||
|
# 创建部署脚本
|
||||||
|
echo "[2/5] 创建部署脚本..."
|
||||||
|
cat > /var/www/webhook/deploy.sh << 'DEPLOY_SCRIPT'
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DEPLOY_ENV=${1:-test}
|
||||||
|
BRANCH=${2:-main}
|
||||||
|
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||||
|
|
||||||
|
case $DEPLOY_ENV in
|
||||||
|
test) DEPLOY_DIR="/var/www/kb-web-test"; BACKUP_DIR="/var/www/kb-web-test-backups" ;;
|
||||||
|
prod) DEPLOY_DIR="/var/www/kb-web-prod"; BACKUP_DIR="/var/www/kb-web-prod-backups" ;;
|
||||||
|
*) echo "用法: $0 [test|prod] [branch]"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
REPO_URL="ssh://sc-gitea@192.168.1.22:2223/starmen1265/kb-web.git"
|
||||||
|
LOG_FILE="/var/log/kb-web-deploy.log"
|
||||||
|
|
||||||
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"; }
|
||||||
|
|
||||||
|
log "开始部署 $DEPLOY_ENV 环境, 分支: $BRANCH"
|
||||||
|
mkdir -p "$DEPLOY_DIR" "$BACKUP_DIR"
|
||||||
|
|
||||||
|
if [ -d "$DEPLOY_DIR/.git" ]; then
|
||||||
|
log "备份当前版本..."
|
||||||
|
CURRENT_COMMIT=$(cd "$DEPLOY_DIR" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
tar -czf "$BACKUP_DIR/backup_${CURRENT_COMMIT}_${TIMESTAMP}.tar.gz" -C "$DEPLOY_DIR" . 2>/dev/null || true
|
||||||
|
cd "$BACKUP_DIR" && ls -t | tail -n +6 | xargs -r rm -f
|
||||||
|
|
||||||
|
log "更新代码..."
|
||||||
|
cd "$DEPLOY_DIR"
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard "origin/$BRANCH"
|
||||||
|
else
|
||||||
|
log "克隆代码..."
|
||||||
|
git clone -b "$BRANCH" "$REPO_URL" "$DEPLOY_DIR"
|
||||||
|
cd "$DEPLOY_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "设置权限..."
|
||||||
|
chown -R www-data:www-data "$DEPLOY_DIR" 2>/dev/null || chown -R nginx:nginx "$DEPLOY_DIR" 2>/dev/null || true
|
||||||
|
|
||||||
|
DEPLOY_INFO="$DEPLOY_DIR/.deploy_info"
|
||||||
|
cat > "$DEPLOY_INFO" << EOF
|
||||||
|
环境: $DEPLOY_ENV
|
||||||
|
分支: $BRANCH
|
||||||
|
提交: $(git rev-parse HEAD)
|
||||||
|
时间: $(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "部署完成: $(git log -1 --oneline)"
|
||||||
|
DEPLOY_SCRIPT
|
||||||
|
|
||||||
|
chmod +x /var/www/webhook/deploy.sh
|
||||||
|
|
||||||
|
# 创建 webhook 接收器
|
||||||
|
echo "[3/5] 创建 webhook 接收器..."
|
||||||
|
cat > /var/www/webhook/receiver.php << 'RECEIVER_PHP'
|
||||||
|
<?php
|
||||||
|
const SECRET = 'kb-web-webhook-secret-2024';
|
||||||
|
const DEPLOY_SCRIPT = '/var/www/webhook/deploy.sh';
|
||||||
|
const LOG_FILE = '/var/log/webhook.log';
|
||||||
|
|
||||||
|
function log_msg($msg) {
|
||||||
|
file_put_contents(LOG_FILE, "[".date('Y-m-d H:i:s')."] $msg\n", FILE_APPEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verify_signature($payload, $signature) {
|
||||||
|
$expected = 'sha256=' . hash_hmac('sha256', $payload, SECRET);
|
||||||
|
return hash_equals($expected, $signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_msg("收到 webhook 请求");
|
||||||
|
$payload = file_get_contents('php://input');
|
||||||
|
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
||||||
|
|
||||||
|
$signature = '';
|
||||||
|
foreach ($headers as $k => $v) {
|
||||||
|
if (strtolower($k) === 'x-gitea-signature' || strtolower($k) === 'x-gitea-signature-sha256') {
|
||||||
|
$signature = $v;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verify_signature($payload, $signature)) {
|
||||||
|
log_msg("签名验证失败");
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Invalid signature']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($payload, true);
|
||||||
|
$repo = $data['repository']['name'] ?? '';
|
||||||
|
$branch = basename($data['ref'] ?? '');
|
||||||
|
|
||||||
|
log_msg("仓库: $repo, 分支: $branch");
|
||||||
|
|
||||||
|
if ($repo !== 'kb-web') {
|
||||||
|
echo json_encode(['status' => 'ignored']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$env = 'test';
|
||||||
|
log_msg("开始部署到 $env");
|
||||||
|
exec(sprintf('%s %s %s 2>&1', DEPLOY_SCRIPT, $env, $branch), $output, $ret);
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['status' => $ret === 0 ? 'success' : 'failed', 'output' => $output]);
|
||||||
|
RECEIVER_PHP
|
||||||
|
|
||||||
|
# 创建 SSH 密钥用于拉取代码
|
||||||
|
echo "[4/5] 配置 SSH 密钥..."
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
chmod 700 ~/.ssh
|
||||||
|
|
||||||
|
# 如果没有部署密钥,创建一个
|
||||||
|
if [ ! -f ~/.ssh/id_rsa_kb_deploy ]; then
|
||||||
|
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_kb_deploy -N "" -C "kb-deploy@micecola"
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo " 请将以下公钥添加到 Gitea 部署密钥:"
|
||||||
|
echo "=========================================="
|
||||||
|
cat ~/.ssh/id_rsa_kb_deploy.pub
|
||||||
|
echo "=========================================="
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 配置 SSH config
|
||||||
|
cat >> ~/.ssh/config << 'SSH_CONFIG'
|
||||||
|
|
||||||
|
# kb-web 部署
|
||||||
|
Host sc-gitea
|
||||||
|
HostName 192.168.1.22
|
||||||
|
Port 2223
|
||||||
|
User sc-gitea
|
||||||
|
IdentityFile ~/.ssh/id_rsa_kb_deploy
|
||||||
|
StrictHostKeyChecking no
|
||||||
|
SSH_CONFIG
|
||||||
|
|
||||||
|
chmod 600 ~/.ssh/config
|
||||||
|
|
||||||
|
# 配置 Nginx
|
||||||
|
echo "[5/5] 配置 Nginx..."
|
||||||
|
|
||||||
|
# 测试环境
|
||||||
|
cat > /etc/nginx/sites-available/kb-web-test.conf << 'NGINX_CONF'
|
||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name _;
|
||||||
|
root /var/www/kb-web-test;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/kb-web-test.access.log;
|
||||||
|
error_log /var/log/nginx/kb-web-test.error.log;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass unix:/run/php/php-fpm.sock;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NGINX_CONF
|
||||||
|
|
||||||
|
# 正式环境 (需要你配置 SSL)
|
||||||
|
cat > /etc/nginx/sites-available/kb-web-prod.conf << 'NGINX_CONF'
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name kb.micecola.top; # 修改为你的域名
|
||||||
|
|
||||||
|
root /var/www/kb-web-prod;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/kb-web-prod.access.log;
|
||||||
|
error_log /var/log/nginx/kb-web-prod.error.log;
|
||||||
|
|
||||||
|
# 强制 HTTPS (配置 SSL 后启用)
|
||||||
|
# return 301 https://$server_name$request_uri;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass unix:/run/php/php-fpm.sock;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS 配置 (需要 SSL 证书后启用)
|
||||||
|
# server {
|
||||||
|
# listen 443 ssl http2;
|
||||||
|
# server_name kb.micecola.top;
|
||||||
|
#
|
||||||
|
# ssl_certificate /etc/nginx/ssl/kb.micecola.top.crt;
|
||||||
|
# ssl_certificate_key /etc/nginx/ssl/kb.micecola.top.key;
|
||||||
|
#
|
||||||
|
# root /var/www/kb-web-prod;
|
||||||
|
# index index.php index.html;
|
||||||
|
#
|
||||||
|
# location / {
|
||||||
|
# try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# location ~ \.php$ {
|
||||||
|
# fastcgi_pass unix:/run/php/php-fpm.sock;
|
||||||
|
# fastcgi_index index.php;
|
||||||
|
# fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
# include fastcgi_params;
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
NGINX_CONF
|
||||||
|
|
||||||
|
# 启用站点
|
||||||
|
ln -sf /etc/nginx/sites-available/kb-web-test.conf /etc/nginx/sites-enabled/
|
||||||
|
ln -sf /etc/nginx/sites-available/kb-web-prod.conf /etc/nginx/sites-enabled/
|
||||||
|
|
||||||
|
# 测试并重载 Nginx
|
||||||
|
nginx -t && systemctl reload nginx
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo " 配置完成!"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "下一步操作:"
|
||||||
|
echo "1. 将上面的部署公钥添加到 Gitea 仓库的 Deploy Keys"
|
||||||
|
echo "2. 在 Gitea 仓库设置中添加 Webhook:"
|
||||||
|
echo " - URL: http://micecola.top:8080/webhook/receiver.php"
|
||||||
|
echo " - 密钥: kb-web-webhook-secret-2024"
|
||||||
|
echo " - 触发: Push 事件"
|
||||||
|
echo "3. 测试环境访问: http://micecola.top:8080"
|
||||||
|
echo "4. 正式环境域名: kb.micecola.top (需配置 DNS 和 SSL)"
|
||||||
|
echo ""
|
||||||
|
echo "手动部署命令:"
|
||||||
|
echo " 测试环境: /var/www/webhook/deploy.sh test main"
|
||||||
|
echo " 正式环境: /var/www/webhook/deploy.sh prod main"
|
||||||
|
echo "=========================================="
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 测试环境配置脚本
|
||||||
|
# 在服务器上执行: bash setup-test-env.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo " kb-web 测试环境配置"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
TEST_DIR="/www/wwwroot/kb-web-test"
|
||||||
|
TEST_DB="kb_web_test"
|
||||||
|
DB_USER="kb-admin"
|
||||||
|
DB_PASS="" # 在这里填入数据库密码
|
||||||
|
|
||||||
|
# 1. 创建测试目录
|
||||||
|
echo "[1/5] 创建测试目录..."
|
||||||
|
mkdir -p $TEST_DIR
|
||||||
|
mkdir -p /www/backup/kb-web
|
||||||
|
|
||||||
|
# 2. 创建测试数据库
|
||||||
|
echo "[2/5] 创建测试数据库..."
|
||||||
|
if [ -n "$DB_PASS" ]; then
|
||||||
|
mysql -u$DB_USER -p$DB_PASS -e "CREATE DATABASE IF NOT EXISTS $TEST_DB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null || echo "数据库可能已存在或密码未设置"
|
||||||
|
|
||||||
|
# 复制正式数据库表结构
|
||||||
|
mysql -u$DB_USER -p$DB_PASS -e "USE $TEST_DB; SOURCE /www/wwwroot/kb-web/kb-admin.sql;" 2>/dev/null || echo "跳过数据导入"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. 克隆代码
|
||||||
|
echo "[3/5] 克隆代码..."
|
||||||
|
if [ -d "$TEST_DIR/.git" ]; then
|
||||||
|
cd $TEST_DIR
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard origin/main
|
||||||
|
else
|
||||||
|
git clone ssh://sc-gitea@192.168.1.22:2223/starmen1265/kb-web.git $TEST_DIR
|
||||||
|
cd $TEST_DIR
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. 配置测试环境
|
||||||
|
echo "[4/5] 配置测试环境..."
|
||||||
|
# 复制测试配置
|
||||||
|
if [ -f "$TEST_DIR/config.test.php" ]; then
|
||||||
|
cp $TEST_DIR/config.test.php $TEST_DIR/config.php
|
||||||
|
echo "已复制测试配置文件"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建测试用户目录
|
||||||
|
mkdir -p $TEST_DIR/test_user
|
||||||
|
|
||||||
|
# 设置权限
|
||||||
|
chown -R www:www $TEST_DIR 2>/dev/null || true
|
||||||
|
|
||||||
|
# 5. 配置 Nginx
|
||||||
|
echo "[5/5] 配置 Nginx..."
|
||||||
|
cat > /www/server/panel/vhost/nginx/kb-web-test.conf << 'EOF'
|
||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name _;
|
||||||
|
root /www/wwwroot/kb-web-test;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
access_log /www/wwwlogs/kb-web-test.log;
|
||||||
|
error_log /www/wwwlogs/kb-web-test.error.log;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass unix:/tmp/php-cgi-81.sock;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 重载 Nginx
|
||||||
|
nginx -t && nginx -s reload
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo " ✅ 测试环境配置完成!"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "测试环境: http://micecola.top:8080"
|
||||||
|
echo "正式环境: http://micecola.top (端口 80)"
|
||||||
|
echo ""
|
||||||
|
echo "下一步:"
|
||||||
|
echo "1. 修改 $TEST_DIR/config.php 中的数据库密码"
|
||||||
|
echo "2. 访问 http://micecola.top:8080 测试"
|
||||||
|
echo "=========================================="
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Gitea Webhook 接收器
|
||||||
|
* 路径: /var/www/webhook/receiver.php
|
||||||
|
* 访问: https://webhook.micecola.top/kb-web.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 配置
|
||||||
|
const SECRET = 'kb-web-webhook-secret-2024'; // 与 Gitea webhook 配置的密钥一致
|
||||||
|
const DEPLOY_SCRIPT = '/var/www/webhook/deploy.sh';
|
||||||
|
const LOG_FILE = '/var/log/webhook.log';
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
function log_msg($msg) {
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
file_put_contents(LOG_FILE, "[$timestamp] $msg\n", FILE_APPEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证签名
|
||||||
|
function verify_signature($payload, $signature) {
|
||||||
|
$expected = 'sha256=' . hash_hmac('sha256', $payload, SECRET);
|
||||||
|
return hash_equals($expected, $signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主逻辑
|
||||||
|
log_msg("收到 webhook 请求");
|
||||||
|
|
||||||
|
$payload = file_get_contents('php://input');
|
||||||
|
$headers = getallheaders();
|
||||||
|
|
||||||
|
// 验证 Gitea 签名
|
||||||
|
$signature = $headers['X-Gitea-Signature'] ?? $headers['X-Gitea-Signature-Sha256'] ?? '';
|
||||||
|
if (!verify_signature($payload, $signature)) {
|
||||||
|
log_msg("签名验证失败");
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Invalid signature']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($payload, true);
|
||||||
|
$repo = $data['repository']['name'] ?? '';
|
||||||
|
$branch = basename($data['ref'] ?? '');
|
||||||
|
$commit = $data['after'] ?? '';
|
||||||
|
|
||||||
|
log_msg("仓库: $repo, 分支: $branch, 提交: $commit");
|
||||||
|
|
||||||
|
// 只处理 kb-web 仓库的 main 分支
|
||||||
|
if ($repo !== 'kb-web') {
|
||||||
|
log_msg("忽略非目标仓库: $repo");
|
||||||
|
echo json_encode(['status' => 'ignored', 'reason' => 'not target repo']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据分支决定部署环境
|
||||||
|
$env = 'test'; // 默认测试环境
|
||||||
|
if ($branch === 'main' || $branch === 'master') {
|
||||||
|
// main 分支也先部署到测试环境
|
||||||
|
// 正式环境需要手动触发
|
||||||
|
$env = 'test';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行部署脚本
|
||||||
|
log_msg("开始部署到 $env 环境");
|
||||||
|
$cmd = sprintf('%s %s %s 2>&1', DEPLOY_SCRIPT, $env, $branch);
|
||||||
|
exec($cmd, $output, $return_var);
|
||||||
|
|
||||||
|
log_msg("部署输出: " . implode("\n", $output));
|
||||||
|
log_msg("部署结果: " . ($return_var === 0 ? '成功' : '失败'));
|
||||||
|
|
||||||
|
// 返回结果
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode([
|
||||||
|
'status' => $return_var === 0 ? 'success' : 'failed',
|
||||||
|
'env' => $env,
|
||||||
|
'branch' => $branch,
|
||||||
|
'commit' => $commit,
|
||||||
|
'output' => $output
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Gitea Actions Runner 专用镜像
|
||||||
|
# 包含完整的开发环境:PHP + Node.js + Git + SSH
|
||||||
|
|
||||||
|
FROM php:8.2-cli-bookworm
|
||||||
|
|
||||||
|
# 安装系统依赖
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
unzip \
|
||||||
|
openssh-client \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
libzip-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
|
libonig-dev \
|
||||||
|
libmbstring-dev \
|
||||||
|
sqlite3 \
|
||||||
|
libsqlite3-dev \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 安装 PHP 扩展
|
||||||
|
RUN docker-php-ext-install \
|
||||||
|
zip \
|
||||||
|
xml \
|
||||||
|
curl \
|
||||||
|
mbstring \
|
||||||
|
sqlite3 \
|
||||||
|
pdo \
|
||||||
|
pdo_sqlite \
|
||||||
|
pdo_mysql \
|
||||||
|
mysqli
|
||||||
|
|
||||||
|
# 安装 Composer
|
||||||
|
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||||
|
|
||||||
|
# 验证安装
|
||||||
|
RUN php -v && composer --version && node -v && npm -v
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
# 默认命令
|
||||||
|
CMD ["bash"]
|
||||||
@@ -0,0 +1,469 @@
|
|||||||
|
# kb-web CI/CD 方案设计
|
||||||
|
|
||||||
|
## 架构概览
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ CI/CD Pipeline │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 本地开发 Gitea仓库 CI测试 CD部署 │
|
||||||
|
│ ┌────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ 代码 │─────▶│ feature │────────▶│ 语法检查 │───┐ │ │ │
|
||||||
|
│ │ 编写 │ │ 分支 │ │ 单元测试 │ │ │ Staging │ │
|
||||||
|
│ └────────┘ └──────────┘ └──────────┘ │ │ 测试环境 │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ PR + Review │ └────┬─────┘ │
|
||||||
|
│ ▼ │ │ │
|
||||||
|
│ ┌──────────┐ │ ┌────▼─────┐ │
|
||||||
|
│ │ develop │────────────────────────┼───▶│ 手动 │ │
|
||||||
|
│ │ 分支 │ │ │ 确认 │ │
|
||||||
|
│ └──────────┘ │ └────┬─────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ PR + Review + 测试通过 │ ┌────▼─────┐ │
|
||||||
|
│ ▼ │ │Production│ │
|
||||||
|
│ ┌──────────┐ │ │ 正式环境 │ │
|
||||||
|
│ │ main │────────────────────────┴───▶│ │ │
|
||||||
|
│ │ 分支 │ └──────────┘ │
|
||||||
|
│ └──────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 一、分支策略
|
||||||
|
|
||||||
|
```
|
||||||
|
main (生产环境)
|
||||||
|
│
|
||||||
|
│ ← PR (需要 Review + CI 通过)
|
||||||
|
│
|
||||||
|
├── develop (测试环境主分支)
|
||||||
|
│ │
|
||||||
|
│ │ ← PR (需要 CI 通过)
|
||||||
|
│ │
|
||||||
|
│ ├── feature/xxx (功能开发)
|
||||||
|
│ ├── feature/yyy
|
||||||
|
│ ├── bugfix/zzz (Bug 修复)
|
||||||
|
│ └── refactor/aaa (重构)
|
||||||
|
│
|
||||||
|
└── hotfix/xxx (紧急修复,直接合并到 main 和 develop)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分支命名规范
|
||||||
|
|
||||||
|
| 类型 | 命名 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| 功能 | `feature/描述` | `feature/user-login` |
|
||||||
|
| 修复 | `bugfix/描述` | `bugfix/fix-redirect` |
|
||||||
|
| 重构 | `refactor/描述` | `refactor/database-layer` |
|
||||||
|
| 紧急 | `hotfix/描述` | `hotfix/security-patch` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、CI/CD 流水线
|
||||||
|
|
||||||
|
### 阶段 1: 代码检查 (Lint)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
- PHP 语法检查
|
||||||
|
- 代码风格检查 (PHP-CS-Fixer)
|
||||||
|
- 安全漏洞扫描
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段 2: 自动化测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
- 单元测试 (PHPUnit)
|
||||||
|
- 功能测试
|
||||||
|
- 测试覆盖率报告
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段 3: 构建与打包
|
||||||
|
|
||||||
|
```bash
|
||||||
|
- 安装依赖 (composer install --no-dev)
|
||||||
|
- 生成版本信息文件
|
||||||
|
- 创建部署包
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段 4: 部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
- Staging: 自动部署 (develop 分支)
|
||||||
|
- Production: 手动确认后部署 (main 分支)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Gitea Actions 配置
|
||||||
|
|
||||||
|
### 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
kb-web/
|
||||||
|
├── .gitea/
|
||||||
|
│ └── workflows/
|
||||||
|
│ ├── ci.yml # 主 CI 流程
|
||||||
|
│ └── deploy.yml # 部署流程
|
||||||
|
├── tests/
|
||||||
|
├── deploy/
|
||||||
|
│ ├── deploy.sh
|
||||||
|
│ └── rollback.sh
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. CI 配置 (.gitea/workflows/ci.yml)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop, 'feature/**', 'bugfix/**']
|
||||||
|
pull_request:
|
||||||
|
branches: [main, develop]
|
||||||
|
|
||||||
|
env:
|
||||||
|
PHP_VERSION: '8.2'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# 代码检查
|
||||||
|
lint:
|
||||||
|
name: 代码检查
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 检出代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 设置 PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ env.PHP_VERSION }}
|
||||||
|
|
||||||
|
- name: PHP 语法检查
|
||||||
|
run: find . -name "*.php" -not -path "./vendor/*" -exec php -l {} \; | grep -v "No syntax errors"
|
||||||
|
|
||||||
|
- name: 安装依赖
|
||||||
|
run: composer install --prefer-dist --no-progress
|
||||||
|
|
||||||
|
# 单元测试
|
||||||
|
test:
|
||||||
|
name: 单元测试
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint
|
||||||
|
steps:
|
||||||
|
- name: 检出代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 设置 PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ env.PHP_VERSION }}
|
||||||
|
coverage: xdebug
|
||||||
|
|
||||||
|
- name: 安装依赖
|
||||||
|
run: composer install --prefer-dist --no-progress
|
||||||
|
|
||||||
|
- name: 运行测试
|
||||||
|
run: composer test
|
||||||
|
|
||||||
|
- name: 上传覆盖率报告
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: coverage-report
|
||||||
|
path: coverage/
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
# 构建确认
|
||||||
|
build-success:
|
||||||
|
name: 构建成功
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [lint, test]
|
||||||
|
if: success()
|
||||||
|
steps:
|
||||||
|
- name: 构建成功
|
||||||
|
run: echo "✅ CI 通过,可以合并"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 部署配置 (.gitea/workflows/deploy.yml)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: '部署环境'
|
||||||
|
required: true
|
||||||
|
default: 'staging'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- staging
|
||||||
|
- production
|
||||||
|
branch:
|
||||||
|
description: '部署分支'
|
||||||
|
required: true
|
||||||
|
default: 'develop'
|
||||||
|
type: string
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEPLOY_SERVER: root@micecola.top
|
||||||
|
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# 部署到测试环境
|
||||||
|
deploy-staging:
|
||||||
|
name: 部署测试环境
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/develop' || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'staging')
|
||||||
|
environment:
|
||||||
|
name: staging
|
||||||
|
url: http://micecola.top:8080
|
||||||
|
steps:
|
||||||
|
- name: 检出代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 设置 SSH
|
||||||
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
with:
|
||||||
|
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
|
||||||
|
|
||||||
|
- name: 部署到测试环境
|
||||||
|
run: |
|
||||||
|
ssh -o StrictHostKeyChecking=no $DEPLOY_SERVER << 'EOF'
|
||||||
|
cd /var/www/kb-web-test
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard origin/develop
|
||||||
|
composer install --no-dev --prefer-dist
|
||||||
|
echo "Deployed at $(date)" > .deploy_time
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: 部署完成通知
|
||||||
|
run: echo "✅ 测试环境部署完成: http://micecola.top:8080"
|
||||||
|
|
||||||
|
# 部署到正式环境
|
||||||
|
deploy-production:
|
||||||
|
name: 部署正式环境
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production')
|
||||||
|
environment:
|
||||||
|
name: production
|
||||||
|
url: https://kb.micecola.top
|
||||||
|
steps:
|
||||||
|
- name: 检出代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 设置 SSH
|
||||||
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
with:
|
||||||
|
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
|
||||||
|
|
||||||
|
- name: 备份当前版本
|
||||||
|
run: |
|
||||||
|
ssh -o StrictHostKeyChecking=no $DEPLOY_SERVER << 'EOF'
|
||||||
|
BACKUP_DIR="/var/www/kb-web-prod-backups"
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
cd /var/www/kb-web-prod
|
||||||
|
CURRENT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
tar -czf "$BACKUP_DIR/backup_${CURRENT_COMMIT}_$(date +%Y%m%d_%H%M%S).tar.gz" .
|
||||||
|
# 保留最近 10 个备份
|
||||||
|
cd $BACKUP_DIR && ls -t | tail -n +11 | xargs -r rm -f
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: 部署到正式环境
|
||||||
|
run: |
|
||||||
|
ssh -o StrictHostKeyChecking=no $DEPLOY_SERVER << 'EOF'
|
||||||
|
cd /var/www/kb-web-prod
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard origin/main
|
||||||
|
composer install --no-dev --prefer-dist
|
||||||
|
|
||||||
|
# 记录部署信息
|
||||||
|
cat > .deploy_info << DEPLOY_EOF
|
||||||
|
环境: production
|
||||||
|
分支: main
|
||||||
|
提交: $(git rev-parse HEAD)
|
||||||
|
时间: $(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
部署者: Gitea Actions
|
||||||
|
DEPLOY_EOF
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: 部署完成通知
|
||||||
|
run: echo "✅ 正式环境部署完成: https://kb.micecola.top"
|
||||||
|
|
||||||
|
# 回滚
|
||||||
|
rollback:
|
||||||
|
name: 回滚
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment != ''
|
||||||
|
environment:
|
||||||
|
name: ${{ github.event.inputs.environment }}
|
||||||
|
steps:
|
||||||
|
- name: 设置 SSH
|
||||||
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
with:
|
||||||
|
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
|
||||||
|
|
||||||
|
- name: 列出可用备份
|
||||||
|
run: |
|
||||||
|
ssh -o StrictHostKeyChecking=no $DEPLOY_SERVER << 'EOF'
|
||||||
|
echo "可用的备份:"
|
||||||
|
ls -la /var/www/kb-web-${{ github.event.inputs.environment }}-backups/
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、Runner 配置
|
||||||
|
|
||||||
|
### 方案 A: 使用 Gitea 官方 Runner(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在服务器上安装 Runner
|
||||||
|
curl -L -o gitea-runner https://gitea.com/gitea/actions/runner/releases/download/v3.4.1/act_runner-3.4.1-linux-amd64
|
||||||
|
chmod +x gitea-runner
|
||||||
|
sudo mv gitea-runner /usr/local/bin/
|
||||||
|
|
||||||
|
# 注册 Runner
|
||||||
|
gitea-runner register --instance https://your-gitea-server --token <your-token>
|
||||||
|
|
||||||
|
# 启动 Runner
|
||||||
|
gitea-runner daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案 B: 使用 GitHub Actions 兼容 Runner
|
||||||
|
|
||||||
|
如果你有 GitHub 账号,可以使用 GitHub 托管的 Runner,只需在 Gitea 中配置镜像。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、Gitea 仓库配置
|
||||||
|
|
||||||
|
### 1. 启用 Actions
|
||||||
|
|
||||||
|
仓库设置 → Actions → 启用
|
||||||
|
|
||||||
|
### 2. 配置 Secrets
|
||||||
|
|
||||||
|
| Secret 名称 | 用途 |
|
||||||
|
|-------------|------|
|
||||||
|
| `DEPLOY_KEY` | SSH 私钥(用于部署到服务器) |
|
||||||
|
| `DEPLOY_SERVER` | 部署服务器地址 |
|
||||||
|
|
||||||
|
### 3. 配置分支保护
|
||||||
|
|
||||||
|
| 分支 | 规则 |
|
||||||
|
|------|------|
|
||||||
|
| `main` | 需要 PR + 1 个 Review + CI 通过 |
|
||||||
|
| `develop` | 需要 PR + CI 通过 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、开发工作流
|
||||||
|
|
||||||
|
### 日常开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 创建功能分支
|
||||||
|
git checkout develop
|
||||||
|
git pull
|
||||||
|
git checkout -b feature/new-feature
|
||||||
|
|
||||||
|
# 2. 开发 + 本地测试
|
||||||
|
composer test
|
||||||
|
|
||||||
|
# 3. 提交代码
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: 新功能描述"
|
||||||
|
git push -u origin feature/new-feature
|
||||||
|
|
||||||
|
# 4. 创建 PR 到 develop
|
||||||
|
# Gitea 上操作,CI 自动运行
|
||||||
|
|
||||||
|
# 5. Review 通过后合并
|
||||||
|
# 自动部署到 Staging
|
||||||
|
|
||||||
|
# 6. 测试通过后,PR 到 main
|
||||||
|
# 手动确认后部署到 Production
|
||||||
|
```
|
||||||
|
|
||||||
|
### 紧急修复
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 从 main 创建 hotfix 分支
|
||||||
|
git checkout main
|
||||||
|
git checkout -b hotfix/urgent-fix
|
||||||
|
|
||||||
|
# 2. 修复 + 测试
|
||||||
|
git commit -m "hotfix: 紧急修复描述"
|
||||||
|
git push
|
||||||
|
|
||||||
|
# 3. PR 到 main(跳过 develop)
|
||||||
|
# 快速 Review + 合并 + 部署
|
||||||
|
|
||||||
|
# 4. 同步到 develop
|
||||||
|
git checkout develop
|
||||||
|
git merge hotfix/urgent-fix
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、监控与告警
|
||||||
|
|
||||||
|
### 部署状态通知
|
||||||
|
|
||||||
|
可集成:
|
||||||
|
- 飞书机器人
|
||||||
|
- 企业微信
|
||||||
|
- 邮件
|
||||||
|
|
||||||
|
### 健康检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 部署后自动检查
|
||||||
|
curl -f https://kb.micecola.top/health || exit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、实施步骤
|
||||||
|
|
||||||
|
### 第一阶段:基础设施(1-2天)
|
||||||
|
|
||||||
|
1. [ ] 确认 Gitea 版本支持 Actions(需要 1.19+)
|
||||||
|
2. [ ] 在服务器上安装 Gitea Runner
|
||||||
|
3. [ ] 配置 SSH 密钥和 Secrets
|
||||||
|
|
||||||
|
### 第二阶段:CI 流程(1天)
|
||||||
|
|
||||||
|
1. [ ] 创建 `.gitea/workflows/ci.yml`
|
||||||
|
2. [ ] 配置代码检查和测试
|
||||||
|
3. [ ] 设置分支保护规则
|
||||||
|
|
||||||
|
### 第三阶段:CD 流程(1天)
|
||||||
|
|
||||||
|
1. [ ] 创建 `.gitea/workflows/deploy.yml`
|
||||||
|
2. [ ] 配置自动部署到 Staging
|
||||||
|
3. [ ] 配置手动部署到 Production
|
||||||
|
|
||||||
|
### 第四阶段:优化(持续)
|
||||||
|
|
||||||
|
1. [ ] 添加测试覆盖率报告
|
||||||
|
2. [ ] 添加部署通知
|
||||||
|
3. [ ] 添加回滚脚本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、当前项目待办
|
||||||
|
|
||||||
|
- [ ] 确认 Gitea 服务器版本
|
||||||
|
- [ ] 部署 feature/article-word-count 分支到测试环境验证
|
||||||
|
- [ ] 合并到 develop,测试 CI/CD 流程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档版本: 1.0*
|
||||||
|
*创建时间: 2026-03-01*
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
# kb-web 需求与设计文档
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
### 1.1 项目名称
|
||||||
|
kb-web(Knowledge Base Web)- 知识库网页系统
|
||||||
|
|
||||||
|
### 1.2 项目描述
|
||||||
|
kb-web 是一个基于 PHP 的知识库管理系统,用于展示和管理用户的知识库内容。系统支持从多种平台(Bilibili、YouTube、知乎等)采集的内容,并提供统一的浏览和搜索界面。
|
||||||
|
|
||||||
|
### 1.3 目标用户
|
||||||
|
- 个人用户:管理自己的知识库
|
||||||
|
- 团队用户:共享和管理团队知识资产
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 功能需求
|
||||||
|
|
||||||
|
### 2.1 用户认证
|
||||||
|
|
||||||
|
| 功能 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| 用户登录 | 用户名 + 密码登录 |
|
||||||
|
| 会话管理 | 基于 PHP Session 的会话管理 |
|
||||||
|
| 登出功能 | 安全退出系统 |
|
||||||
|
|
||||||
|
### 2.2 文档管理
|
||||||
|
|
||||||
|
| 功能 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| 文档列表 | 分页展示所有文档 |
|
||||||
|
| 文档搜索 | 全文搜索文档内容 |
|
||||||
|
| 文档查看 | 在线阅读文档内容 |
|
||||||
|
| 目录结构 | 支持子目录分类展示 |
|
||||||
|
|
||||||
|
### 2.3 元数据展示
|
||||||
|
|
||||||
|
| 元数据 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| 标题 | 文档标题(从 YAML front matter 提取) |
|
||||||
|
| 作者 | 内容作者 |
|
||||||
|
| 发布时间 | 原始内容发布时间 |
|
||||||
|
| 平台 | 来源平台(Bilibili、YouTube、知乎等) |
|
||||||
|
| 标签 | 内容标签 |
|
||||||
|
| 时长 | 视频时长(仅视频类型) |
|
||||||
|
| 类型 | 文章 / 视频 |
|
||||||
|
| 源链接 | 原始内容链接 |
|
||||||
|
|
||||||
|
### 2.4 界面功能
|
||||||
|
|
||||||
|
| 功能 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| 响应式设计 | 支持桌面和移动设备 |
|
||||||
|
| 暗色主题 | 科技感暗色界面 |
|
||||||
|
| 搜索高亮 | 搜索结果关键词高亮 |
|
||||||
|
| Markdown 渲染 | 支持基本 Markdown 语法 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 技术架构
|
||||||
|
|
||||||
|
### 3.1 技术栈
|
||||||
|
|
||||||
|
| 层级 | 技术 |
|
||||||
|
|------|------|
|
||||||
|
| 前端 | HTML5, CSS3, JavaScript |
|
||||||
|
| 后端 | PHP 8.0+ |
|
||||||
|
| 数据库 | MySQL 5.7+ |
|
||||||
|
| Web 服务器 | Nginx |
|
||||||
|
| 部署 | Git + CI/CD (Gitea Actions) |
|
||||||
|
|
||||||
|
### 3.2 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
kb-web/
|
||||||
|
├── index.php # 登录页
|
||||||
|
├── dashboard.php # 文档列表页
|
||||||
|
├── view.php # 文档查看页
|
||||||
|
├── config.php # 配置文件(不纳入版本控制)
|
||||||
|
├── config.test.php # 测试环境配置模板
|
||||||
|
├── style.css # 样式文件
|
||||||
|
├── logout.php # 登出处理
|
||||||
|
├── add_user.php # 用户添加
|
||||||
|
├── init.sql # 数据库初始化脚本
|
||||||
|
├── docs/ # 文档目录
|
||||||
|
│ └── requirements.md
|
||||||
|
├── deploy/ # 部署脚本
|
||||||
|
│ ├── deploy.sh
|
||||||
|
│ ├── setup-server.sh
|
||||||
|
│ └── webhook-receiver.php
|
||||||
|
├── tests/ # 测试文件
|
||||||
|
│ ├── bootstrap.php
|
||||||
|
│ ├── Unit/
|
||||||
|
│ └── Feature/
|
||||||
|
├── .gitea/ # CI/CD 配置
|
||||||
|
│ └── workflows/
|
||||||
|
│ └── ci.yml
|
||||||
|
├── .gitignore
|
||||||
|
├── composer.json
|
||||||
|
├── phpunit.xml
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 数据库设计
|
||||||
|
|
||||||
|
#### 用户表 (users)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | INT | 主键,自增 |
|
||||||
|
| username | VARCHAR(50) | 用户名,唯一 |
|
||||||
|
| password_hash | VARCHAR(255) | 密码哈希 |
|
||||||
|
| created_at | TIMESTAMP | 创建时间 |
|
||||||
|
|
||||||
|
### 3.4 文件存储结构
|
||||||
|
|
||||||
|
```
|
||||||
|
用户目录/
|
||||||
|
├── 用户名1/
|
||||||
|
│ ├── analyzed/ # 分析后的 Markdown 文件
|
||||||
|
│ ├── metadata/ # 元数据 JSON 文件
|
||||||
|
│ ├── content/ # 原始内容文件
|
||||||
|
│ └── pending/ # 待处理文件
|
||||||
|
└── 用户名2/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 页面设计
|
||||||
|
|
||||||
|
### 4.1 登录页 (index.php)
|
||||||
|
|
||||||
|
**功能:**
|
||||||
|
- 用户名密码输入
|
||||||
|
- 错误提示
|
||||||
|
- 登录验证
|
||||||
|
|
||||||
|
**设计特点:**
|
||||||
|
- 居中登录框
|
||||||
|
- 渐变背景 + 旋转光效
|
||||||
|
- 玻璃拟态卡片
|
||||||
|
- 品牌标题 "知识库"
|
||||||
|
|
||||||
|
### 4.2 文档列表页 (dashboard.php)
|
||||||
|
|
||||||
|
**功能:**
|
||||||
|
- 搜索栏
|
||||||
|
- 文档统计
|
||||||
|
- 文档卡片列表
|
||||||
|
- 用户信息显示
|
||||||
|
|
||||||
|
**卡片信息展示:**
|
||||||
|
- 平台图标
|
||||||
|
- 类型图标
|
||||||
|
- 标题
|
||||||
|
- 作者、发布时间、平台、时长
|
||||||
|
- 标签(最多5个)
|
||||||
|
- 源链接
|
||||||
|
- 概述(前两行)
|
||||||
|
|
||||||
|
### 4.3 文档查看页 (view.php)
|
||||||
|
|
||||||
|
**功能:**
|
||||||
|
- 返回列表
|
||||||
|
- 元数据卡片
|
||||||
|
- Markdown 内容渲染
|
||||||
|
- 搜索高亮
|
||||||
|
|
||||||
|
**元数据卡片:**
|
||||||
|
- 作者
|
||||||
|
- 发布时间
|
||||||
|
- 平台(彩色徽章)
|
||||||
|
- 时长
|
||||||
|
- 类型
|
||||||
|
- 源链接
|
||||||
|
- 标签列表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. CI/CD 流程
|
||||||
|
|
||||||
|
### 5.1 部署流程
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ feature/* 分支 push │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 自动部署到测试环境 (http://micecola.top:8081) │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 运行测试 │
|
||||||
|
│ │ 通过 │
|
||||||
|
│ ▼ │
|
||||||
|
│ 自动合并到 main │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ⏸️ 等待手动确认... │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ 手动触发 │
|
||||||
|
│ 部署到正式环境 (http://micecola.top) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 环境配置
|
||||||
|
|
||||||
|
| 环境 | 域名 | 目录 | 触发方式 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| 测试 | http://micecola.top:8081 | /www/wwwroot/kb-web-test | 自动 |
|
||||||
|
| 正式 | http://micecola.top | /www/wwwroot/kb-web | 手动 |
|
||||||
|
|
||||||
|
### 5.3 部署特性
|
||||||
|
|
||||||
|
- ✅ 配置文件保护(config.php 不被覆盖)
|
||||||
|
- ✅ 自动备份(保留最近5个版本)
|
||||||
|
- ✅ 版本回滚支持
|
||||||
|
- ✅ 环境隔离
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 样式规范
|
||||||
|
|
||||||
|
### 6.1 色彩系统
|
||||||
|
|
||||||
|
| 名称 | 色值 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| 背景深色 | #0a0e17 | 页面背景 |
|
||||||
|
| 背景表面 | #111827 | 卡片背景 |
|
||||||
|
| 强调色-青 | #38bdf8 | 主色调、链接 |
|
||||||
|
| 强调色-紫 | #a855f7 | 辅助强调 |
|
||||||
|
| 强调色-绿 | #22c55e | 成功状态 |
|
||||||
|
| 文字主色 | #f1f5f9 | 主要文字 |
|
||||||
|
| 文字次色 | #94a3b8 | 次要文字 |
|
||||||
|
|
||||||
|
### 6.2 平台颜色
|
||||||
|
|
||||||
|
| 平台 | 色值 |
|
||||||
|
|------|------|
|
||||||
|
| Bilibili | #fb7299 |
|
||||||
|
| YouTube | #ff0000 |
|
||||||
|
| 知乎 | #0084ff |
|
||||||
|
| 微信 | #07c160 |
|
||||||
|
|
||||||
|
### 6.3 字体
|
||||||
|
|
||||||
|
| 用途 | 字体 |
|
||||||
|
|------|------|
|
||||||
|
| 正文 | Space Grotesk |
|
||||||
|
| 代码 | JetBrains Mono |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 安全设计
|
||||||
|
|
||||||
|
### 7.1 已实现
|
||||||
|
|
||||||
|
| 安全措施 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| 密码哈希 | 使用 password_hash() |
|
||||||
|
| SQL 注入防护 | PDO 预处理语句 |
|
||||||
|
| XSS 防护 | htmlspecialchars() |
|
||||||
|
| 目录遍历防护 | 路径检查 |
|
||||||
|
| 会话管理 | PHP Session |
|
||||||
|
|
||||||
|
### 7.2 待加强
|
||||||
|
|
||||||
|
- [ ] CSRF 防护
|
||||||
|
- [ ] 登录失败次数限制
|
||||||
|
- [ ] HTTPS 强制
|
||||||
|
- [ ] 密码强度验证
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 扩展计划
|
||||||
|
|
||||||
|
### 8.1 短期计划
|
||||||
|
|
||||||
|
- [ ] 分页功能
|
||||||
|
- [ ] 标签筛选
|
||||||
|
- [ ] 收藏功能
|
||||||
|
- [ ] 阅读进度记录
|
||||||
|
|
||||||
|
### 8.2 长期计划
|
||||||
|
|
||||||
|
- [ ] 多用户协作
|
||||||
|
- [ ] 权限管理
|
||||||
|
- [ ] API 接口
|
||||||
|
- [ ] 移动端 App
|
||||||
|
- [ ] 全文搜索优化(Elasticsearch)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 依赖项目
|
||||||
|
|
||||||
|
### 9.1 关联仓库
|
||||||
|
|
||||||
|
| 仓库 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| kb-skills | 内容处理技能模块 |
|
||||||
|
| knowledge-base | 知识库内容存储 |
|
||||||
|
|
||||||
|
### 9.2 知识库同步
|
||||||
|
|
||||||
|
knowledge-base 仓库独立部署,自动同步到用户目录:
|
||||||
|
- 测试环境:/www/wwwroot/kb-web-test/zhangqing/
|
||||||
|
- 正式环境:/www/wwwroot/kb-web/zhangqing/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 维护说明
|
||||||
|
|
||||||
|
### 10.1 日志位置
|
||||||
|
|
||||||
|
| 日志 | 路径 |
|
||||||
|
|------|------|
|
||||||
|
| Nginx 访问日志 | /www/wwwlogs/micecola.top.log |
|
||||||
|
| Nginx 错误日志 | /www/wwwlogs/micecola.top.error.log |
|
||||||
|
| 部署日志 | /var/log/kb-web-deploy.log |
|
||||||
|
| 备份目录 | /www/backup/kb-web/ |
|
||||||
|
|
||||||
|
### 10.2 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 手动部署测试环境
|
||||||
|
cd /www/wwwroot/kb-web-test && git pull origin main
|
||||||
|
|
||||||
|
# 手动部署正式环境
|
||||||
|
cd /www/wwwroot/kb-web && git pull origin main
|
||||||
|
|
||||||
|
# 查看备份
|
||||||
|
ls /www/backup/kb-web/
|
||||||
|
|
||||||
|
# 回滚到备份版本
|
||||||
|
tar -xzf /www/backup/kb-web/backup_XXX.tar.gz -C /www/wwwroot/kb-web/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 版本历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 变更内容 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 1.0.0 | 2026-02-13 | 初始版本,基础功能 |
|
||||||
|
| 1.1.0 | 2026-02-15 | 暗色科技感风格改造 |
|
||||||
|
| 1.2.0 | 2026-02-15 | CI/CD 自动部署 |
|
||||||
|
| 1.3.0 | 2026-02-15 | 元数据展示优化,标签支持 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档最后更新:2026-02-15*
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
// 已登录则跳转到主页
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: dashboard.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
if (empty($username) || empty($password)) {
|
||||||
|
$error = '请输入用户名和密码';
|
||||||
|
} else {
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("SELECT * FROM users WHERE username = ?");
|
||||||
|
$stmt->execute([$username]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($user && password_verify($password, $user['password_hash'])) {
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
$_SESSION['username'] = $user['username'];
|
||||||
|
header('Location: dashboard.php');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$error = '用户名或密码错误'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>知识库 - 登录</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-box">
|
||||||
|
<h1>📚 知识库</h1>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="error"><?php echo htmlspecialchars($error); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="POST" action="">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">用户名</label>
|
||||||
|
<input type="text" id="username" name="username" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary">登录</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- 知识库网页系统 - 数据库初始化脚本
|
||||||
|
-- 运行方式: mysql -u root -p kb_web < init.sql
|
||||||
|
|
||||||
|
-- 创建数据库(如果不存在)
|
||||||
|
CREATE DATABASE IF NOT EXISTS kb_web DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE kb_web;
|
||||||
|
|
||||||
|
-- 用户表
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_username (username)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 添加用户示例
|
||||||
|
-- ============================================
|
||||||
|
-- 密码使用 PHP password_hash() 生成
|
||||||
|
-- PHP 生成密码: echo password_hash('your_password', PASSWORD_DEFAULT);
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 示例:添加用户 demo,密码为 demo123
|
||||||
|
-- 密码哈希值需要用 PHP 生成,以下是示例格式:
|
||||||
|
-- INSERT INTO users (username, password_hash) VALUES
|
||||||
|
-- ('demo', '$2y$10$...');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 管理员添加用户的 PHP 脚本 (add_user.php)
|
||||||
|
-- 可以单独部署在管理目录中
|
||||||
|
-- ============================================
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
// 清除 session
|
||||||
|
session_destroy();
|
||||||
|
|
||||||
|
// 跳转到登录页
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="tests/bootstrap.php"
|
||||||
|
colors="true"
|
||||||
|
cacheDirectory=".phpunit.cache">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory>tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
<testsuite name="Feature">
|
||||||
|
<directory>tests/Feature</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 功能测试 - 安全检查
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class SecurityTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 测试配置文件不暴露敏感信息
|
||||||
|
*/
|
||||||
|
public function testConfigDoesNotExposePassword(): void
|
||||||
|
{
|
||||||
|
$config = file_get_contents(__DIR__ . '/../../config.php');
|
||||||
|
|
||||||
|
// 确保没有硬编码的真实密码
|
||||||
|
$this->assertStringNotContainsString('123456', $config, '配置中不应包含简单密码');
|
||||||
|
$this->assertStringNotContainsString('password', strtolower($config), '配置中不应包含 password 字符串');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试 SQL 注入防护
|
||||||
|
*/
|
||||||
|
public function testConfigUsesPreparedStatements(): void
|
||||||
|
{
|
||||||
|
$config = file_get_contents(__DIR__ . '/../../config.php');
|
||||||
|
|
||||||
|
// 确保使用 PDO 预处理
|
||||||
|
$this->assertStringContainsString('prepare', $config, '应该使用 PDO 预处理语句');
|
||||||
|
$this->assertStringContainsString('execute', $config, '应该使用 execute 执行预处理');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试 XSS 防护
|
||||||
|
*/
|
||||||
|
public function testOutputUsesHtmlspecialchars(): void
|
||||||
|
{
|
||||||
|
$files = [
|
||||||
|
__DIR__ . '/../../dashboard.php',
|
||||||
|
__DIR__ . '/../../view.php',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (file_exists($file)) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
$this->assertStringContainsString('htmlspecialchars', $content,
|
||||||
|
basename($file) . ' 应该使用 htmlspecialchars 防止 XSS');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试 Session 安全
|
||||||
|
*/
|
||||||
|
public function testSessionSecurity(): void
|
||||||
|
{
|
||||||
|
$config = file_get_contents(__DIR__ . '/../../config.php');
|
||||||
|
|
||||||
|
// 确保有 session 检查
|
||||||
|
$this->assertStringContainsString('session', strtolower($config), '应该有 session 相关配置');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 示例单元测试
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ConfigTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 测试配置文件存在
|
||||||
|
*/
|
||||||
|
public function testConfigFileExists(): void
|
||||||
|
{
|
||||||
|
$this->assertFileExists(__DIR__ . '/../../config.php', '配置文件应该存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试样式文件存在
|
||||||
|
*/
|
||||||
|
public function testStyleFileExists(): void
|
||||||
|
{
|
||||||
|
$this->assertFileExists(__DIR__ . '/../../style.css', '样式文件应该存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试入口文件存在
|
||||||
|
*/
|
||||||
|
public function testIndexFileExists(): void
|
||||||
|
{
|
||||||
|
$this->assertFileExists(__DIR__ . '/../../index.php', '入口文件应该存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试仪表盘文件存在
|
||||||
|
*/
|
||||||
|
public function testDashboardFileExists(): void
|
||||||
|
{
|
||||||
|
$this->assertFileExists(__DIR__ . '/../../dashboard.php', '仪表盘文件应该存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试查看文件存在
|
||||||
|
*/
|
||||||
|
public function testViewFileExists(): void
|
||||||
|
{
|
||||||
|
$this->assertFileExists(__DIR__ . '/../../view.php', '查看文件应该存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* PHPUnit bootstrap file
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 定义测试环境常量
|
||||||
|
define('TESTING', true);
|
||||||
|
|
||||||
|
// 加载配置(测试环境)
|
||||||
|
if (file_exists(__DIR__ . '/../config.php')) {
|
||||||
|
// 可以创建一个测试用的 config.test.php
|
||||||
|
// require_once __DIR__ . '/../config.test.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动加载
|
||||||
|
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'config.php';
|
||||||
|
checkLogin();
|
||||||
|
|
||||||
|
$user = getCurrentUser();
|
||||||
|
$username = $user['username'];
|
||||||
|
$docDir = getUserDocDir($username);
|
||||||
|
|
||||||
|
$file = $_GET['file'] ?? '';
|
||||||
|
$search = $_GET['q'] ?? '';
|
||||||
|
|
||||||
|
if (empty($file) || strpos($file, '..') !== false) {
|
||||||
|
die('无效的文件名');
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = $docDir . '/' . $file;
|
||||||
|
|
||||||
|
$realPath = realpath($filePath);
|
||||||
|
$realDocDir = realpath($docDir);
|
||||||
|
if ($realPath === false || strpos($realPath, $realDocDir) !== 0) {
|
||||||
|
die('文件不存在或无权访问');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
die('文件不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($filePath);
|
||||||
|
|
||||||
|
// 解析元数据
|
||||||
|
$meta = [
|
||||||
|
'title' => basename($file),
|
||||||
|
'author' => '',
|
||||||
|
'date' => '',
|
||||||
|
'platform' => '',
|
||||||
|
'url' => '',
|
||||||
|
'tags' => [],
|
||||||
|
'duration' => '',
|
||||||
|
'type' => 'article'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 提取并移除 YAML front matter
|
||||||
|
if (preg_match('/^---\s*\n(.*?)\n---\s*\n(.*)$/s', $content, $matches)) {
|
||||||
|
$yaml = $matches[1];
|
||||||
|
$content = $matches[2];
|
||||||
|
|
||||||
|
if (preg_match('/^title:\s*["\']?(.+?)["\']?\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['title'] = trim($m[1], '"\'');
|
||||||
|
}
|
||||||
|
if (preg_match('/^author:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['author'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^date:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['date'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^platform:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['platform'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^url:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['url'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^duration:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['duration'] = trim($m[1]);
|
||||||
|
$meta['type'] = 'video';
|
||||||
|
}
|
||||||
|
if (preg_match('/^tags:\s*\[(.+)\]\s*$/m', $yaml, $m)) {
|
||||||
|
$tagsStr = $m[1];
|
||||||
|
preg_match_all('/"([^"]+)"/', $tagsStr, $tagMatches);
|
||||||
|
$meta['tags'] = $tagMatches[1] ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markdown 转换
|
||||||
|
function markdownToHtml($text) {
|
||||||
|
$text = preg_replace('/^#### (.*$)/m', '<h4>$1</h4>', $text);
|
||||||
|
$text = preg_replace('/^### (.*$)/m', '<h3>$1</h3>', $text);
|
||||||
|
$text = preg_replace('/^## (.*$)/m', '<h2>$1</h2>', $text);
|
||||||
|
$text = preg_replace('/^# (.*$)/m', '<h1>$1</h1>', $text);
|
||||||
|
|
||||||
|
$text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
|
||||||
|
$text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text);
|
||||||
|
|
||||||
|
$text = preg_replace('/```(\w*)\n(.*?)```/s', '<pre><code class="language-$1">$2</code></pre>', $text);
|
||||||
|
$text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text);
|
||||||
|
|
||||||
|
$text = preg_replace('/\[(.+?)\]\((.+?)\)/', '<a href="$2" target="_blank">$1</a>', $text);
|
||||||
|
|
||||||
|
$text = preg_replace('/^- (.*$)/m', '<li>$1</li>', $text);
|
||||||
|
$text = preg_replace('/(<li>.*<\/li>\n?)+/', '<ul>$0</ul>', $text);
|
||||||
|
|
||||||
|
$text = preg_replace('/\n\n/', '</p><p>', $text);
|
||||||
|
$text = '<p>' . $text . '</p>';
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightSearch($text, $search) {
|
||||||
|
if (empty($search)) return $text;
|
||||||
|
return preg_replace('/(' . preg_quote($search, '/') . ')/i', '<mark>$1</mark>', $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
$htmlContent = markdownToHtml(htmlspecialchars($content));
|
||||||
|
if ($search) {
|
||||||
|
$htmlContent = highlightSearch($htmlContent, $search);
|
||||||
|
}
|
||||||
|
|
||||||
|
$folder = dirname($file) !== '.' ? dirname($file) : '';
|
||||||
|
|
||||||
|
// 平台配置
|
||||||
|
function getPlatformInfo($platform) {
|
||||||
|
$platforms = [
|
||||||
|
'bilibili' => ['name' => 'Bilibili', 'icon' => '📺', 'color' => '#fb7299'],
|
||||||
|
'youtube' => ['name' => 'YouTube', 'icon' => '▶️', 'color' => '#ff0000'],
|
||||||
|
'zhihu' => ['name' => '知乎', 'icon' => '💡', 'color' => '#0084ff'],
|
||||||
|
'wechat' => ['name' => '微信', 'icon' => '💬', 'color' => '#07c160'],
|
||||||
|
];
|
||||||
|
return $platforms[$platform] ?? ['name' => $platform ?: '未知', 'icon' => '📄', 'color' => '#666'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$platformInfo = getPlatformInfo($meta['platform']);
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?php echo htmlspecialchars($meta['title']); ?> - 知识库</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="view-page">
|
||||||
|
<header class="header">
|
||||||
|
<a href="dashboard.php<?php echo $search ? '?q=' . urlencode($search) : ''; ?>" class="btn-back">
|
||||||
|
← 返回列表
|
||||||
|
</a>
|
||||||
|
<h1><?php echo htmlspecialchars($meta['title']); ?></h1>
|
||||||
|
<?php if ($folder): ?>
|
||||||
|
<div class="folder-info">📁 <?php echo htmlspecialchars($folder); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 元数据卡片 -->
|
||||||
|
<div class="meta-card">
|
||||||
|
<div class="meta-row">
|
||||||
|
<?php if ($meta['author']): ?>
|
||||||
|
<div class="meta-field">
|
||||||
|
<span class="meta-label">作者</span>
|
||||||
|
<span class="meta-value">👤 <?php echo htmlspecialchars($meta['author']); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($meta['date']): ?>
|
||||||
|
<div class="meta-field">
|
||||||
|
<span class="meta-label">发布时间</span>
|
||||||
|
<span class="meta-value">📅 <?php echo htmlspecialchars($meta['date']); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($meta['platform']): ?>
|
||||||
|
<div class="meta-field">
|
||||||
|
<span class="meta-label">平台</span>
|
||||||
|
<span class="meta-value platform-badge" style="--platform-color: <?php echo $platformInfo['color']; ?>">
|
||||||
|
<?php echo $platformInfo['icon']; ?> <?php echo $platformInfo['name']; ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($meta['duration']): ?>
|
||||||
|
<div class="meta-field">
|
||||||
|
<span class="meta-label">时长</span>
|
||||||
|
<span class="meta-value">⏱️ <?php echo htmlspecialchars($meta['duration']); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="meta-field">
|
||||||
|
<span class="meta-label">类型</span>
|
||||||
|
<span class="meta-value"><?php echo $meta['type'] === 'video' ? '🎬 视频' : '📝 文章'; ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($meta['url']): ?>
|
||||||
|
<div class="meta-source">
|
||||||
|
<span class="meta-label">源链接</span>
|
||||||
|
<a href="<?php echo htmlspecialchars($meta['url']); ?>" target="_blank" class="source-url">
|
||||||
|
🔗 <?php echo htmlspecialchars($meta['url']); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($meta['tags'])): ?>
|
||||||
|
<div class="meta-tags">
|
||||||
|
<span class="meta-label">标签</span>
|
||||||
|
<div class="tags-list">
|
||||||
|
<?php foreach ($meta['tags'] as $tag): ?>
|
||||||
|
<span class="tag"><?php echo htmlspecialchars($tag); ?></span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="document-content">
|
||||||
|
<?php echo $htmlContent; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user