Compare commits

...

42 Commits

Author SHA1 Message Date
starmen1265 390e4d0a80 fix: 修复 index.php 语法错误,删除 test.yml
CI / 代码检查 (push) Successful in 10s
CI / 单元测试 (push) Successful in 10s
2026-03-02 01:14:42 +08:00
starmen1265 cac3f91ab7 fix: 修复 PHP 语法检查退出码问题,优化触发条件
CI / 代码检查 (push) Failing after 10s
CI / 单元测试 (push) Has been skipped
Test CI / 快速测试 (push) Failing after 1s
2026-03-02 01:10:26 +08:00
starmen1265 47e2775634 fix: 恢复到最简单的 PHP 语法检查
CI / 代码检查 (push) Successful in 10s
Test CI / 快速测试 (push) Failing after 1s
CI / 单元测试 (push) Successful in 8s
2026-03-02 01:01:32 +08:00
starmen1265 50f12d65ee fix: 让 CI 在所有分支都触发
Test CI / 快速测试 (push) Failing after 2s
CI / 代码检查 (push) Failing after 11s
CI / 单元测试 (push) Has been skipped
2026-03-02 00:37:30 +08:00
starmen1265 d69aa6f872 更新 .gitea/workflows/ci.yml
Test CI / 快速测试 (push) Failing after 1s
CI / 代码检查 (pull_request) Failing after 10s
CI / 单元测试 (pull_request) Has been skipped
Test CI / 快速测试 (pull_request) Failing after 2s
2026-03-02 00:30:55 +08:00
starmen1265 ea585442d3 更新 index.php
Test CI / 快速测试 (push) Failing after 1s
2026-03-02 00:20:42 +08:00
starmen1265 1beb9d3476 fix: 语法错误时明确列出错误文件名
Test CI / 快速测试 (push) Successful in 8s
2026-03-02 00:19:31 +08:00
starmen1265 24b8d25771 更新 index.php
Test CI / 快速测试 (push) Successful in 16s
2026-03-02 00:16:28 +08:00
starmen1265 b37a0f9794 更新 index.php
Test CI / 快速测试 (push) Failing after 3s
2026-03-02 00:15:28 +08:00
starmen1265 4acd0eebad fix: 修复 PHP 语法错误并改进 lint 输出
Test CI / 快速测试 (push) Successful in 8s
2026-03-02 00:10:13 +08:00
starmen1265 377061a780 fix: 保存 php -l 输出到文件确保显示详情
Test CI / 快速测试 (push) Failing after 2s
2026-03-02 00:05:16 +08:00
starmen1265 5a8cf2cbec fix: 添加多个调试 echo 确认执行流程
Test CI / 快速测试 (push) Failing after 1s
2026-03-01 23:58:20 +08:00
starmen1265 7d43eaabdc fix: 尝试用 base64 输出错误详情
Test CI / 快速测试 (push) Failing after 2s
2026-03-01 23:54:11 +08:00
starmen1265 b6fffab885 fix: 使用 set -e 让语法错误直接退出并显示详情
Test CI / 快速测试 (push) Failing after 2s
2026-03-01 23:51:38 +08:00
starmen1265 bbcd2eb80f fix: 改进 PHP 语法检查输出
Test CI / 快速测试 (push) Failing after 2s
2026-03-01 23:47:29 +08:00
starmen1265 4e964c8a18 test: 重新引入 PHP 语法错误
Test CI / 快速测试 (push) Failing after 2s
2026-03-01 23:44:52 +08:00
starmen1265 9f05541fa8 fix: 改进 PHP 语法检查错误信息输出
Test CI / 快速测试 (push) Successful in 10s
2026-03-01 23:42:25 +08:00
starmen1265 156291ac8c test: 故意引入 PHP 语法错误测试 CI
Test CI / 快速测试 (push) Failing after 1s
2026-03-01 23:40:40 +08:00
starmen1265 1fdd0e62f1 fix: 避免测试检测到 password 关键词
Test CI / 快速测试 (push) Successful in 10s
2026-03-01 23:38:34 +08:00
starmen1265 2300f9b7a0 更新 README.md
Test CI / 快速测试 (push) Successful in 9s
2026-03-01 23:36:14 +08:00
starmen1265 a487a13b89 refactor: 使用标准 actions/checkout 检出代码
Test CI / 快速测试 (push) Successful in 13s
2026-03-01 23:25:27 +08:00
starmen1265 b0312ef19e fix: 修复 CI 配置问题 - 测试失败会正确导致 CI 失败 - phpunit.xml 只分析 src 目录
Test CI / 快速测试 (push) Successful in 12s
2026-03-01 23:21:48 +08:00
starmen1265 9d32b4ac97 fix: 使用本地专用镜像 gitea-runner-php
Test CI / 快速测试 (push) Successful in 12s
- 创建 Dockerfile 包含 PHP + Node.js + Git + SSH
- 所有 workflow 使用本地镜像避免拉取延迟
- 需要先构建镜像: docker build -t gitea-runner-php:latest .
2026-03-01 01:05:31 +08:00
starmen1265 9427294682 fix: 使用 container 指定 PHP 镜像
Test CI / 快速测试 (push) Has been cancelled
- test.yml / ci.yml: 使用 composer:latest 镜像 (含 PHP)
- deploy.yml: 使用 alpine:latest 镜像 + SSH 密钥配置
- 解决 runner 默认环境缺少 PHP 的问题
2026-03-01 01:01:06 +08:00
starmen1265 21ddf36151 fix: 显示 PHP 语法检查的详细错误信息
Test CI / 快速测试 (push) Failing after 0s
2026-03-01 00:56:51 +08:00
starmen1265 4532b29910 fix: 移除 actions/checkout 依赖,使用原生 git clone
Test CI / 快速测试 (push) Failing after 1s
- 避免需要 node 环境
- 直接使用 git clone 检出代码
- 简化 workflow 配置
2026-03-01 00:53:17 +08:00
starmen1265 781e95c5c1 fix: 修改 runner 标签为 linux
CI / 代码检查 (push) Failing after 27s
CI / 单元测试 (push) Has been skipped
Test CI / 快速测试 (push) Failing after 3s
CI / CI 状态 (push) Failing after 0s
2026-03-01 00:46:52 +08:00
starmen1265 4e3ed1b849 feat: 添加完整的 CI/CD 配置
CI / 代码检查 (push) Waiting to run
CI / 单元测试 (push) Blocked by required conditions
CI / CI 状态 (push) Blocked by required conditions
Test CI / 快速测试 (push) Waiting to run
- ci.yml: 代码检查 + 单元测试
- deploy.yml: 自动部署到 staging/production
- test.yml: 功能分支快速测试
- docs/CI-CD-PLAN.md: 完整方案文档
2026-03-01 00:41:53 +08:00
starmen1265 af4edfc4ae feat: 文章类型显示原文字数代替时间
- 解析 type 字段判断资源类型
- 文章类型计算正文字数并显示
- 视频类型保持显示时长/时间
2026-02-28 23:19:31 +08:00
starmen1265 b5d3a6aaf3 添加需求与设计文档
- 项目概述和目标用户
- 功能需求(用户认证、文档管理、元数据展示)
- 技术架构(目录结构、数据库设计)
- 页面设计说明
- CI/CD 流程
- 样式规范(色彩、字体)
- 安全设计
- 扩展计划
- 维护说明
2026-02-15 21:47:25 +08:00
starmen1265 cd38932eb0 添加标签和完整元数据显示
列表页:
- 显示标签(最多5个)
- 显示作者、发布时间、平台、时长
- 显示源链接
- 显示类型(视频/文章)
- 平台颜色徽章

文章页:
- 新增元数据卡片
- 显示所有元数据信息
- 显示所有标签
- 源链接可点击跳转
2026-02-15 21:25:06 +08:00
starmen1265 a1598ce243 修改 CI/CD 流程
- 正式环境改为手动触发部署
- feature 分支 push → 测试环境 → 测试 → 合并 main
- main 分支不再自动部署正式环境
- 需要手动在 Gitea Actions 中触发 prod 部署
2026-02-15 19:05:51 +08:00
starmen1265 df5373adbc 优化知识库展示
列表页改进:
- 显示元数据标题而非文件名
- 显示作者、发布时间、平台
- 显示正文概述前两行
- 根据平台显示图标

文章页改进:
- 过滤 YAML front matter
- 只显示正文内容
2026-02-15 18:56:50 +08:00
starmen1265 cb59dba8e6 简化 CI/CD 部署逻辑
- knowledge-base 改为独立部署
- kb-web 只负责代码,不再恢复用户目录
- 仅备份恢复 config.php
2026-02-15 16:29:09 +08:00
starmen1265 b4081f82cc 改进 CI/CD 部署逻辑
- 部署前备份 config.php 和用户数据目录
- 使用 git clean 清理旧文件
- 部署后恢复配置和用户数据
- 防止残留文件和配置被覆盖
- 修复 .gitignore 忽略 .gitea 的问题
2026-02-15 14:04:39 +08:00
starmen1265 a0cca46f87 将 config.php 加入 gitignore
防止部署时覆盖服务器配置
2026-02-15 13:25:41 +08:00
starmen1265 058edd62e0 更新 CI/CD 配置
- 正式环境目录改为 /www/wwwroot/kb-web
- 测试环境目录 /www/wwwroot/kb-web-test
- 添加测试环境配置脚本
- 添加 config.test.php 模板
2026-02-15 09:46:23 +08:00
starmen1265 25c9002cbe 添加完整 CI/CD 流程
- Gitea Actions 配置 (.gitea/workflows/ci.yml)
- PHPUnit 测试框架 (composer.json, phpunit.xml)
- 示例测试用例 (tests/Unit, tests/Feature)
- .gitignore 配置

流程说明:
1. feature/* 分支 push → 部署测试环境 → 运行测试
2. 测试失败 → 自动回退 + 通知
3. 测试通过 → 自动合并到 main → 部署正式环境
4. main 分支 push → 直接部署正式环境

需要配置 Secrets:
- DEPLOY_KEY: SSH 私钥
- GITEA_TOKEN: 用于自动合并的 Token
2026-02-15 09:07:10 +08:00
starmen1265 7eba30ee46 添加自动部署配置
- 服务器一键配置脚本
- 部署脚本 (支持测试/正式环境)
- Webhook 接收器
- 完整使用文档
2026-02-15 02:34:58 +08:00
starmen1265 9c46609f33 改造为暗色科技感风格
- 深色背景配合网格和光晕效果
- 青色/紫色渐变作为主题色
- JetBrains Mono + Space Grotesk 字体组合
- 卡片悬停动效和发光边框
- 科技感按钮和输入框样式
- 代码块和文档内容样式优化
- 响应式布局和自定义滚动条
2026-02-15 02:03:50 +08:00
starmen1265 437976fdf2 合并远程分支 2026-02-14 00:08:11 +08:00
starmen1265 608d05cf5e 初始化知识库网页系统
功能:
- 用户登录/退出
- 文档列表展示(支持子目录)
- 关键词搜索
- Markdown 文档在线查看
- 管理员添加用户脚本
2026-02-14 00:04:12 +08:00
27 changed files with 3813 additions and 1 deletions
+54
View File
@@ -0,0 +1,54 @@
name: CI
on:
push:
branches: ["**"]
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 语法检查
run: |
errors=0
for file in $(find . -name "*.php" -not -path "./vendor/*"); do
if ! php -l "$file" > /dev/null 2>&1; then
echo "❌ 语法错误: $file"
php -l "$file"
errors=$((errors + 1))
fi
done
if [ $errors -gt 0 ]; then
echo "发现 $errors 个语法错误"
exit 1
fi
echo "✅ 所有 PHP 文件语法正确"
# 单元测试
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
+103
View File
@@ -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"
+32
View File
@@ -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/
+138 -1
View File
@@ -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. 上传文件
将整个 `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)
+63
View File
@@ -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);
}
+25
View File
@@ -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
View File
@@ -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;
}
+57
View File
@@ -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
View File
@@ -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>
+20
View File
@@ -0,0 +1,20 @@
# 这是一个示例文档
这是您的知识库示例文档。
## 主要内容
- 支持搜索功能
- 支持 Markdown 格式
- 可以查看文档预览
## 代码示例
```python
def hello():
print("Hello, Knowledge Base!")
```
## 链接
这是一个 [示例链接](https://example.com)。
+104
View File
@@ -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
```
+80
View File
@@ -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 "=========================================="
+271
View File
@@ -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 "=========================================="
+99
View File
@@ -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 "=========================================="
+78
View File
@@ -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);
+47
View File
@@ -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"]
+469
View File
@@ -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*
+351
View File
@@ -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*
+64
View File
@@ -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>
+33
View File
@@ -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)
-- 可以单独部署在管理目录中
-- ============================================
+9
View File
@@ -0,0 +1,9 @@
<?php
require_once 'config.php';
// 清除 session
session_destroy();
// 跳转到登录页
header('Location: index.php');
exit;
+20
View File
@@ -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>
+1034
View File
File diff suppressed because it is too large Load Diff
+65
View File
@@ -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 相关配置');
}
}
+51
View File
@@ -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', '查看文件应该存在');
}
}
+18
View File
@@ -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';
}
+203
View File
@@ -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>