This commit is contained in:
dcr_xuxgc
2026-06-12 17:49:54 +08:00
commit d759a9e740
69 changed files with 14243 additions and 0 deletions

17
server/.env Normal file
View File

@@ -0,0 +1,17 @@
# 数据库配置
DB_HOST=43.156.91.115
DB_PORT=53306
DB_USER=root
# DB_PASSWORD=mysql_sX4mrh
DB_PASSWORD=mariadb_hJbME5
DB_NAME=jnote
# RUSTFS 对象存储配置
RUSTFS_ENDPOINT=http://43.156.91.115:9001
RUSTFS_REGION=us-east-1
RUSTFS_BUCKET=setting
RUSTFS_ACCESS_KEY=rustfsadmin
RUSTFS_SECRET_KEY=rustfsadmin
# 服务器配置
PORT=4501

76
server/README.md Normal file
View File

@@ -0,0 +1,76 @@
# Jnote Server
博客后端 API 服务
## 安装
```bash
npm install
```
## 配置
编辑 `.env` 文件,填入数据库连接信息:
```
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=jnote
PORT=3000
```
如果不配置数据库,将使用内存存储作为后备方案。
## 启动
```bash
# 开发模式(热重载)
npm run dev
# 生产模式
npm start
```
## API 端点
### 文章
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/posts` | GET | 获取所有文章 |
| `/api/posts/:id` | GET | 获取单篇文章 |
| `/api/posts` | POST | 创建文章 |
| `/api/posts/:id` | PUT | 更新文章 |
| `/api/posts/:id` | DELETE | 删除文章 |
### 关于
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/about` | GET | 获取关于内容 |
| `/api/about` | PUT | 更新关于内容 |
### 设置
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/settings` | GET | 获取设置 |
| `/api/settings` | PUT | 更新设置 |
### 定时任务
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/cron-tasks` | GET | 获取所有任务 |
| `/api/cron-tasks` | POST | 创建任务 |
| `/api/cron-tasks/:id` | PUT | 更新任务 |
| `/api/cron-tasks/:id` | DELETE | 删除任务 |
| `/api/cron-tasks/:id/run` | POST | 立即执行任务 |
### 健康检查
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/health` | GET | 服务健康状态 |

89
server/build.js Normal file
View File

@@ -0,0 +1,89 @@
/**
* JNote 后端打包脚本
* 支持 Node.js 16.x
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const serverDir = __dirname;
// Node 16.x -> pkg 5.8.1
const PKG_VERSION = '5.8.1';
const TARGET_NODE_VERSION = 'node16';
function log(msg) {
console.log(`[Build] ${msg}`);
}
function run(cmd) {
log(`执行: ${cmd}`);
execSync(cmd, { cwd: serverDir, stdio: 'inherit' });
}
function checkNodeVersion() {
const version = execSync('node --version', { encoding: 'utf8' }).trim();
const match = version.match(/^v(\d+)\./);
if (match) {
return parseInt(match[1]);
}
return null;
}
function main() {
console.log('=========================================');
console.log(' JNote 后端打包工具');
console.log(` 当前平台: ${isWindows ? 'Windows' : 'Linux'}`);
console.log('=========================================');
// 检查 Node.js 环境
const nodeMajor = checkNodeVersion();
if (!nodeMajor) {
console.error('❌ 错误: 未找到 Node.js');
process.exit(1);
}
log(`✓ Node.js 版本: v${nodeMajor}.x`);
// 安装依赖
console.log('');
log('📦 安装依赖...');
run('npm install');
// 安装 pkg
try {
execSync('pkg --version', { encoding: 'utf8', stdio: 'ignore' });
log('✓ pkg 已安装');
} catch {
console.log('');
log(`📦 安装 pkg ${PKG_VERSION}...`);
run(`npm install -g pkg@${PKG_VERSION}`);
}
// 创建 dist 目录
const distDir = path.join(serverDir, 'dist');
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
}
// 打包 Linux 版本
console.log('');
log(`🔨 打包 Linux 版本 (${TARGET_NODE_VERSION}-linux-x64)...`);
run(`pkg src/index.js --targets ${TARGET_NODE_VERSION}-linux-x64 --output dist/jnote-api`);
console.log('');
console.log('=========================================');
console.log(' 打包完成!');
console.log('=========================================');
console.log('');
console.log('输出文件:');
console.log(' - dist/jnote-api (Linux 可执行文件)');
console.log('');
console.log('下一步:');
console.log(' 1. 将 dist/ 目录下的文件上传到服务器');
console.log(' 2. 修改 .env 配置数据库等信息');
console.log(' 3. 运行: chmod +x jnote-api && ./jnote-api');
console.log('');
}
main();

5732
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

193
server/package.js Normal file
View File

@@ -0,0 +1,193 @@
/**
* JNote 后端分发包打包脚本
* 在 Windows 上运行,打包 Linux 和 Windows 两个版本
* 优化Linux 生成 sh 脚本Windows 生成 bat 脚本,原生直接执行
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
const serverDir = __dirname;
const isWindows = os.platform() === 'win32';
function log(msg) {
console.log(`[Package] ${msg}`);
}
function run(cmd) {
log(`执行: ${cmd}`);
execSync(cmd, { cwd: serverDir, stdio: 'inherit' });
}
function main() {
console.log('=========================================');
console.log(' JNote 后端分发包打包工具');
console.log(` 当前平台: ${isWindows ? 'Windows' : 'Linux'}`);
console.log('=========================================');
// 检查 Node.js 环境
try {
const nodeVersion = execSync('node --version', { encoding: 'utf8' }).trim();
log(`✓ Node.js 版本: ${nodeVersion}`);
} catch {
console.error('❌ 错误: 未找到 Node.js');
process.exit(1);
}
// 安装依赖
console.log('');
log('📦 安装依赖...');
run('npm install');
// 安装 pkg (如果未安装)
try {
execSync('pkg --version', { encoding: 'utf8', stdio: 'ignore' });
log('✓ pkg 已安装');
} catch {
console.log('');
log('📦 安装 pkg...');
run('npm install -g pkg');
}
// 创建输出目录
const distDir = path.join(serverDir, 'dist');
const releaseDir = path.join(serverDir, 'release');
// 清空并重建目录
if (fs.existsSync(distDir)) fs.rmSync(distDir, { recursive: true, force: true });
if (fs.existsSync(releaseDir)) fs.rmSync(releaseDir, { recursive: true, force: true });
fs.mkdirSync(distDir, { recursive: true });
fs.mkdirSync(releaseDir, { recursive: true });
const releaseLinux = path.join(releaseDir, 'linux');
const releaseWindows = path.join(releaseDir, 'windows');
fs.mkdirSync(releaseLinux, { recursive: true });
fs.mkdirSync(releaseWindows, { recursive: true });
// 打包 Linux 版本(适配你的 Node16
console.log('');
log('🔨 打包 Linux 版本...');
run('pkg src/index.js --targets node16-linux-x64 --output dist/jnote-api');
// 打包 Windows 版本(适配你的 Node16
console.log('');
log('🔨 打包 Windows 版本...');
run('pkg src/index.js --targets node16-win-x64 --output dist/jnote-api.exe');
// ===================== 核心优化 =====================
// 1. 复制 Linux 可执行文件 + 生成 .sh 启动脚本
const linuxExe = path.join(distDir, 'jnote-api');
if (fs.existsSync(linuxExe)) {
const targetLinuxExe = path.join(releaseLinux, 'jnote-api');
fs.copyFileSync(linuxExe, targetLinuxExe);
fs.chmodSync(targetLinuxExe, 0o755);
// 生成 Linux 原生启动脚本 start.sh
const shScript = `#!/bin/bash
# JNote 后端服务启动脚本
cd "$(dirname "$0")"
./jnote-api
`;
const shPath = path.join(releaseLinux, 'start.sh');
fs.writeFileSync(shPath, shScript, 'utf8');
fs.chmodSync(shPath, 0o755); // 赋可执行权限
log('✓ linux/jnote-api + start.sh (可直接执行)');
}
// 2. 复制 Windows 可执行文件 + 生成 .bat 启动脚本
const winExe = path.join(distDir, 'jnote-api.exe');
if (fs.existsSync(winExe)) {
fs.copyFileSync(winExe, path.join(releaseWindows, 'jnote-api.exe'));
// 生成 Windows 原生启动脚本 start.bat
const batScript = `@echo off
:: JNote 后端服务启动脚本
cd /d "%~dp0"
jnote-api.exe
pause
`;
const batPath = path.join(releaseWindows, 'start.bat');
// ✅ 修复Node16 兼容 UTF-8 编码
fs.writeFileSync(batPath, batScript, 'utf8');
log('✓ windows/jnote-api.exe + start.bat (双击运行)');
}
// ====================================================
// 复制环境配置
const envSrc = path.join(serverDir, '.env');
const defaultEnv = `# 数据库配置
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=jnote
# RUSTFS 对象存储配置
RUSTFS_ENDPOINT=http://localhost:9001
RUSTFS_BUCKET=setting
RUSTFS_ACCESS_KEY=rustfsadmin
RUSTFS_SECRET_KEY=rustfsadmin
# 服务器配置
PORT=3000
`;
if (fs.existsSync(envSrc)) {
fs.copyFileSync(envSrc, path.join(releaseLinux, '.env'));
fs.copyFileSync(envSrc, path.join(releaseLinux, '.env.example'));
fs.copyFileSync(envSrc, path.join(releaseWindows, '.env'));
fs.copyFileSync(envSrc, path.join(releaseWindows, '.env.example'));
} else {
fs.writeFileSync(path.join(releaseLinux, '.env'), defaultEnv, 'utf8');
fs.writeFileSync(path.join(releaseWindows, '.env'), defaultEnv, 'utf8');
}
log('✓ .env 配置文件生成完成');
// 创建 README
const readme = `JNote 后端服务部署说明
=====================
目录结构:
├── linux/ # Linux 服务器部署包
│ ├── jnote-api # Linux 可执行二进制文件
│ ├── start.sh # Linux 原生启动脚本
│ └── .env # 环境配置
└── windows/ # Windows 服务器部署包
├── jnote-api.exe # Windows 可执行程序
├── start.bat # Windows 原生启动脚本
└── .env # 环境配置
部署步骤:
【Linux 服务器】
1. 上传 linux/ 目录到服务器
2. 修改 .env 中的数据库配置
3. 直接运行: ./start.sh
【Windows 服务器】
1. 上传 windows/ 目录到服务器
2. 修改 .env 中的数据库配置
3. 双击运行 start.bat 即可
`;
fs.writeFileSync(path.join(releaseDir, 'README.txt'), readme, 'utf8');
log('✓ README.txt 生成完成');
// 清理临时文件
console.log('');
log('🧹 清理临时文件...');
try { fs.unlinkSync(linuxExe); } catch {}
try { fs.unlinkSync(winExe); } catch {}
try { fs.rmSync(distDir, { recursive: true, force: true }); } catch {}
console.log('');
console.log('=========================================');
console.log(' 打包完成!');
console.log('=========================================');
console.log(`分发包目录: ${releaseDir}`);
}
main();

24
server/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "jnote-server",
"version": "1.0.0",
"description": "Jnote-ui backend API server",
"main": "src/index.js",
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js",
"build": "node build.js",
"package": "node package.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1064.0",
"@aws-sdk/s3-request-presigner": "^3.1064.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mysql2": "^3.22.5"
},
"devDependencies": {
"nodemon": "^3.0.2",
"pkg": "5.8.1"
}
}

25
server/release/README.txt Normal file
View File

@@ -0,0 +1,25 @@
JNote 后端服务部署说明
=====================
目录结构:
├── linux/ # Linux 服务器部署包
│ ├── jnote-api # Linux 可执行二进制文件
│ ├── start.sh # Linux 原生启动脚本
│ └── .env # 环境配置
└── windows/ # Windows 服务器部署包
├── jnote-api.exe # Windows 可执行程序
├── start.bat # Windows 原生启动脚本
└── .env # 环境配置
部署步骤:
【Linux 服务器】
1. 上传 linux/ 目录到服务器
2. 修改 .env 中的数据库配置
3. 直接运行: ./start.sh
【Windows 服务器】
1. 上传 windows/ 目录到服务器
2. 修改 .env 中的数据库配置
3. 双击运行 start.bat 即可

17
server/release/linux/.env Normal file
View File

@@ -0,0 +1,17 @@
# 数据库配置
DB_HOST=43.156.91.115
DB_PORT=53306
DB_USER=root
# DB_PASSWORD=mysql_sX4mrh
DB_PASSWORD=mariadb_hJbME5
DB_NAME=jnote
# RUSTFS 对象存储配置
RUSTFS_ENDPOINT=http://43.156.91.115:9001
RUSTFS_REGION=us-east-1
RUSTFS_BUCKET=setting
RUSTFS_ACCESS_KEY=rustfsadmin
RUSTFS_SECRET_KEY=rustfsadmin
# 服务器配置
PORT=4501

View File

@@ -0,0 +1,17 @@
# 数据库配置
DB_HOST=43.156.91.115
DB_PORT=53306
DB_USER=root
# DB_PASSWORD=mysql_sX4mrh
DB_PASSWORD=mariadb_hJbME5
DB_NAME=jnote
# RUSTFS 对象存储配置
RUSTFS_ENDPOINT=http://43.156.91.115:9001
RUSTFS_REGION=us-east-1
RUSTFS_BUCKET=setting
RUSTFS_ACCESS_KEY=rustfsadmin
RUSTFS_SECRET_KEY=rustfsadmin
# 服务器配置
PORT=4501

Binary file not shown.

View File

@@ -0,0 +1,4 @@
#!/bin/bash
# JNote 后端服务启动脚本
cd "$(dirname "$0")"
./jnote-api

View File

@@ -0,0 +1,17 @@
# 数据库配置
DB_HOST=43.156.91.115
DB_PORT=53306
DB_USER=root
# DB_PASSWORD=mysql_sX4mrh
DB_PASSWORD=mariadb_hJbME5
DB_NAME=jnote
# RUSTFS 对象存储配置
RUSTFS_ENDPOINT=http://43.156.91.115:9001
RUSTFS_REGION=us-east-1
RUSTFS_BUCKET=setting
RUSTFS_ACCESS_KEY=rustfsadmin
RUSTFS_SECRET_KEY=rustfsadmin
# 服务器配置
PORT=4501

View File

@@ -0,0 +1,17 @@
# 数据库配置
DB_HOST=43.156.91.115
DB_PORT=53306
DB_USER=root
# DB_PASSWORD=mysql_sX4mrh
DB_PASSWORD=mariadb_hJbME5
DB_NAME=jnote
# RUSTFS 对象存储配置
RUSTFS_ENDPOINT=http://43.156.91.115:9001
RUSTFS_REGION=us-east-1
RUSTFS_BUCKET=setting
RUSTFS_ACCESS_KEY=rustfsadmin
RUSTFS_SECRET_KEY=rustfsadmin
# 服务器配置
PORT=4501

Binary file not shown.

View File

@@ -0,0 +1,5 @@
@echo off
:: JNote 后端服务启动脚本
cd /d "%~dp0"
jnote-api.exe
pause

View File

@@ -0,0 +1,166 @@
/**
* 定时任务调度器
* 后端运行,周期性检查并执行到期的任务
*/
const { getPool } = require('./database');
const memoryStore = require('./memoryStore');
let schedulerInterval = null;
let isRunning = false;
function parseHeaders(headers) {
if (!headers) return {};
if (typeof headers === 'object') return headers;
try {
return JSON.parse(headers);
} catch {
return {};
}
}
function calculateNextRun(cron) {
const parts = cron.trim().split(/\s+/);
if (parts.length < 5) return null;
const now = new Date();
const next = new Date(now);
const [minute, hour, day, month, weekDay] = parts;
// 设置分钟
if (minute === '*') {
next.setMinutes(now.getMinutes() + 1, 0, 0);
} else if (minute.includes('/')) {
const step = parseInt(minute.split('/')[1]);
next.setMinutes(Math.ceil(now.getMinutes() / step) * step, 0, 0);
} else {
next.setMinutes(parseInt(minute), 0, 0);
if (next <= now) {
next.setHours(next.getHours() + 1);
}
}
// 设置小时
if (hour !== '*') {
if (hour.includes('/')) {
const step = parseInt(hour.split('/')[1]);
next.setHours(Math.ceil(now.getHours() / step) * step, 0, 0, 0);
} else {
next.setHours(parseInt(hour), 0, 0, 0);
}
if (next <= now) {
next.setDate(next.getDate() + 1);
}
}
return next;
}
async function executeTask(task) {
console.log(`[CronScheduler] 执行任务: ${task.name} (${task.id})`);
const options = {
method: task.method || 'GET',
headers: parseHeaders(task.headers)
};
if (task.body && ['POST', 'PUT', 'PATCH'].includes(task.method)) {
options.body = task.body;
}
try {
const response = await fetch(task.url, options);
const now = new Date();
const nextRun = calculateNextRun(task.cron);
console.log(`[CronScheduler] 任务 ${task.name} 执行完成: ${response.status}`);
// 更新数据库
const pool = getPool();
if (pool) {
await pool.execute(
'UPDATE cron_tasks SET last_run = ?, next_run = ?, run_count = run_count + 1 WHERE id = ?',
[now, nextRun, task.id]
);
} else {
// 更新内存存储
const idx = memoryStore.cronTasks.findIndex(t => t.id === task.id);
if (idx !== -1) {
memoryStore.cronTasks[idx].lastRun = now.toLocaleString('zh-CN');
memoryStore.cronTasks[idx].nextRun = nextRun;
memoryStore.cronTasks[idx].runCount = (memoryStore.cronTasks[idx].runCount || 0) + 1;
}
}
} catch (error) {
console.error(`[CronScheduler] 任务 ${task.name} 执行失败:`, error.message);
const now = new Date();
const nextRun = calculateNextRun(task.cron);
const pool = getPool();
if (pool) {
await pool.execute(
'UPDATE cron_tasks SET last_run = ?, next_run = ?, run_count = run_count + 1 WHERE id = ?',
[now, nextRun, task.id]
);
}
}
}
async function checkAndRunTasks() {
if (isRunning) return;
isRunning = true;
try {
const pool = getPool();
let tasks = [];
if (pool) {
const [rows] = await pool.execute('SELECT * FROM cron_tasks WHERE enabled = 1');
tasks = rows;
} else {
tasks = memoryStore.cronTasks.filter(t => t.enabled);
}
const now = new Date();
for (const task of tasks) {
if (!task.next_run && !task.nextRun) continue;
const nextRun = new Date(task.next_run || task.nextRun);
if (now >= nextRun) {
executeTask(task);
}
}
} catch (error) {
console.error('[CronScheduler] 检查任务失败:', error.message);
} finally {
isRunning = false;
}
}
function start() {
if (schedulerInterval) {
console.log('[CronScheduler] 已启动');
return;
}
console.log('[CronScheduler] 启动定时任务调度器 (每10秒检查一次)');
schedulerInterval = setInterval(checkAndRunTasks, 10000);
// 立即执行一次检查
checkAndRunTasks();
}
function stop() {
if (schedulerInterval) {
clearInterval(schedulerInterval);
schedulerInterval = null;
console.log('[CronScheduler] 已停止');
}
}
module.exports = {
start,
stop
};

View File

@@ -0,0 +1,207 @@
const mysql = require('mysql2/promise');
const { DB_NAME, TABLE_DEFINITIONS, DEFAULT_DATA } = require('./schema');
const memoryStore = require('./memoryStore');
let pool = null;
const config = {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 0
};
async function initDatabase() {
const dbHost = process.env.DB_HOST;
const dbPassword = process.env.DB_PASSWORD;
if (!dbHost || !dbPassword) {
console.log('⚠️ 数据库未配置,部分功能将使用内存存储');
console.log(' DB_HOST:', dbHost || '未设置');
console.log(' DB_PASSWORD:', dbPassword ? '已设置' : '未设置');
return null;
}
try {
// Step 1: 连接数据库服务器(不指定数据库)
const initConfig = { ...config };
delete initConfig.database;
const initConnection = await mysql.createConnection(initConfig);
console.log('🔌 已连接数据库服务器');
// Step 2: 创建数据库(如果不存在)
await initConnection.query(
`CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`
);
console.log(`📦 数据库 ${DB_NAME} ${await databaseExists(initConnection) ? '已存在' : '已创建'}`);
// Step 3: 选择数据库
await initConnection.query(`USE \`${DB_NAME}\``);
// Step 4: 创建所有表
for (const table of TABLE_DEFINITIONS) {
await initConnection.query(table.sql);
console.log(`📋 ${table.name}${await tableExists(initConnection, table.name) ? '已存在' : '已创建'}`);
}
// Step 5: 初始化默认数据
await ensureDefaultData(initConnection);
await initConnection.end();
// Step 6: 创建连接池
const poolConfig = { ...config, database: DB_NAME };
pool = mysql.createPool(poolConfig);
// 测试连接池
const connection = await pool.getConnection();
console.log('✅ 数据库连接池创建成功');
connection.release();
// 预热缓存(不阻塞启动)
warmUpCache().catch(err => console.error('预热失败:', err.message));
return pool;
} catch (error) {
console.error('❌ 数据库初始化失败:', error.message);
console.log('⚠️ 将使用内存存储作为后备');
return null;
}
}
// 检查数据库是否存在
async function databaseExists(connection) {
const [rows] = await connection.query(
`SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`,
[DB_NAME]
);
return rows.length > 0;
}
// 检查表是否存在
async function tableExists(connection, tableName) {
const [rows] = await connection.query(
`SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`,
[DB_NAME, tableName]
);
return rows.length > 0;
}
// 确保默认数据存在
async function ensureDefaultData(connection) {
try {
// 检查 settings 是否存在
const [settingsRows] = await connection.query('SELECT id FROM settings WHERE id = 1');
if (settingsRows.length === 0) {
await connection.query(
'INSERT INTO settings (id, bg_type, bg_color, bg_opacity, language) VALUES (1, ?, ?, ?, ?)',
[DEFAULT_DATA.settings.bg_type, DEFAULT_DATA.settings.bg_color, DEFAULT_DATA.settings.bg_opacity, DEFAULT_DATA.settings.language]
);
console.log(' settings 默认数据已创建');
}
// 检查 about 是否存在
const [aboutRows] = await connection.query('SELECT id FROM about WHERE id = 1');
if (aboutRows.length === 0) {
await connection.query(
'INSERT INTO about (id, title, intro, blog, tech, features, contact, filing_number) VALUES (1, ?, ?, ?, ?, ?, ?, ?)',
[
DEFAULT_DATA.about.title,
DEFAULT_DATA.about.intro,
DEFAULT_DATA.about.blog,
DEFAULT_DATA.about.tech,
DEFAULT_DATA.about.features,
DEFAULT_DATA.about.contact,
DEFAULT_DATA.about.filing_number
]
);
console.log(' about 默认数据已创建');
}
} catch (error) {
if (error.code !== 'ER_DUP_ENTRY') {
console.error(' 默认数据初始化失败:', error.message);
}
}
}
// 数据预热 - 启动时加载常用数据到内存
async function warmUpCache() {
if (!pool) return;
try {
const [posts] = await pool.execute('SELECT * FROM posts ORDER BY date DESC');
const [settings] = await pool.execute('SELECT * FROM settings WHERE id = 1');
const [about] = await pool.execute('SELECT * FROM about WHERE id = 1');
memoryStore.posts = posts;
if (settings.length > 0) {
memoryStore.settings = formatSettings(settings[0]);
}
if (about.length > 0) {
memoryStore.about = formatAbout(about[0]);
}
console.log('✅ 数据预热完成');
} catch (error) {
console.error('数据预热失败:', error.message);
}
}
function formatSettings(row) {
if (!row) return memoryStore.settings;
return {
bgType: row.bg_type,
bgColor: row.bg_color,
bgImage: row.bg_image,
bgOpacity: row.bg_opacity,
language: row.language,
favicon: row.favicon,
uploadedImages: parseJSON(row.uploaded_images, []),
uploadedIcons: parseJSON(row.uploaded_icons, [])
};
}
function formatAbout(row) {
if (!row) return memoryStore.about;
const about = { ...row };
if (about.tech && typeof about.tech === 'string') {
about.techList = about.tech.split(',').map(t => t.trim());
}
if (about.features && typeof about.features === 'string') {
try {
about.featuresList = JSON.parse(about.features);
} catch {
about.featuresList = [];
}
}
if (about.contact && typeof about.contact === 'string') {
try {
about.contactObj = JSON.parse(about.contact);
} catch {
about.contactObj = {};
}
}
return about;
}
function parseJSON(str, defaultValue) {
if (!str) return defaultValue;
if (typeof str === 'object') return str;
try {
return JSON.parse(str);
} catch {
return defaultValue;
}
}
function getPool() {
return pool;
}
module.exports = { initDatabase, getPool };

View File

@@ -0,0 +1,110 @@
/**
* .env 文件管理器
* 读取和写入环境配置
*/
const fs = require('fs');
const path = require('path');
const ENV_PATH = path.join(__dirname, '../../.env');
// 配置字段定义
const CONFIG_FIELDS = {
db: ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'],
rustfs: ['RUSTFS_ENDPOINT', 'RUSTFS_REGION', 'RUSTFS_BUCKET', 'RUSTFS_ACCESS_KEY', 'RUSTFS_SECRET_KEY'],
server: ['PORT']
};
// 读取 .env 文件
function readEnv() {
try {
if (!fs.existsSync(ENV_PATH)) {
return {};
}
const content = fs.readFileSync(ENV_PATH, 'utf-8');
const result = {};
content.split('\n').forEach(line => {
line = line.trim();
if (!line || line.startsWith('#')) return;
const idx = line.indexOf('=');
if (idx > 0) {
const key = line.substring(0, idx).trim();
const value = line.substring(idx + 1).trim();
result[key] = value;
}
});
return result;
} catch (error) {
console.error('读取 .env 失败:', error);
return {};
}
}
// 写入 .env 文件
function writeEnv(config) {
try {
const lines = [];
// 数据库配置
lines.push('# 数据库配置');
lines.push(`DB_HOST=${config.DB_HOST || 'localhost'}`);
lines.push(`DB_PORT=${config.DB_PORT || 3306}`);
lines.push(`DB_USER=${config.DB_USER || 'root'}`);
lines.push(`DB_PASSWORD=${config.DB_PASSWORD || ''}`);
lines.push(`DB_NAME=${config.DB_NAME || 'jnote'}`);
lines.push('');
// RUSTFS 配置
lines.push('# RUSTFS 对象存储配置');
lines.push(`RUSTFS_ENDPOINT=${config.RUSTFS_ENDPOINT || 'http://43.156.91.115:9001'}`);
lines.push(`RUSTFS_REGION=${config.RUSTFS_REGION || 'us-east-1'}`);
lines.push(`RUSTFS_BUCKET=${config.RUSTFS_BUCKET || 'setting'}`);
lines.push(`RUSTFS_ACCESS_KEY=${config.RUSTFS_ACCESS_KEY || 'rustfsadmin'}`);
lines.push(`RUSTFS_SECRET_KEY=${config.RUSTFS_SECRET_KEY || 'rustfsadmin'}`);
lines.push('');
// 服务器配置
lines.push('# 服务器配置');
lines.push(`PORT=${config.PORT || 3000}`);
fs.writeFileSync(ENV_PATH, lines.join('\n'), 'utf-8');
return true;
} catch (error) {
console.error('写入 .env 失败:', error);
return false;
}
}
// 获取安全配置(密码隐藏)
function getSafeConfig() {
const config = readEnv();
return {
db: {
host: config.DB_HOST || 'localhost',
port: config.DB_PORT || '3306',
user: config.DB_USER || 'root',
password: config.DB_PASSWORD ? '******' : '',
name: config.DB_NAME || 'jnote'
},
rustfs: {
endpoint: config.RUSTFS_ENDPOINT || 'http://43.156.91.115:9001',
region: config.RUSTFS_REGION || 'us-east-1',
bucket: config.RUSTFS_BUCKET || 'setting',
accessKey: config.RUSTFS_ACCESS_KEY || 'rustfsadmin',
secretKey: config.RUSTFS_SECRET_KEY ? '******' : ''
},
server: {
port: config.PORT || '3000'
}
};
}
module.exports = {
readEnv,
writeEnv,
getSafeConfig,
CONFIG_FIELDS
};

View File

@@ -0,0 +1,50 @@
// 内存存储后备方案(数据库未配置时使用)
const memoryStore = {
posts: [
{ id: 1, title: '欢迎来到我的编程学习记录', date: '2026-06-01', excerpt: '这是我的第一篇文章,欢迎大家来访!', content: '这是我的第一篇文章,欢迎大家来访!在这里我会分享一些技术心得和生活感悟。希望大家喜欢这里的内容,有任何问题欢迎留言交流。' },
{ id: 2, title: 'Vue 3 组合式 API 入门', date: '2026-06-02', excerpt: '探索 Vue 3 的组合式 API感受更好的代码组织方式。', content: 'Vue 3 引入了组合式 API这是一种全新的代码组织方式。通过 setup 函数和响应式 API我们可以更灵活地组织组件逻辑提高代码的可复用性和可维护性。\n\n主要特点\n1. 更好的逻辑复用\n2. 更灵活的代码组织\n3. 更好的 TypeScript 支持' },
{ id: 3, title: '静态网站生成器选型', date: '2026-06-03', excerpt: '对比常见的静态网站生成器,帮助你选择合适的工具。', content: '选择一个合适的静态网站生成器需要考虑多个因素:\n\n1. Hexo - 简单易用,主题丰富\n2. Jekyll - GitHub Pages 原生支持,社区活跃\n3. Hugo - 构建速度极快\n4. VuePress / VitePress - 基于 Vue适合技术文档\n\n本博客使用 Vue 3 + Vite 构建,兼具现代特性和开发体验。' },
{ id: 4, title: '优雅地使用 Pinia 管理状态', date: '2026-06-04', excerpt: 'Pinia 是 Vue 3 推荐的状态管理方案,本篇介绍其基本用法。', content: 'Pinia 是 Vue 官方推荐的新一代状态管理库,相比 Vuex 更加轻量和直观。\n\n核心概念\n- Store - 存储状态的地方\n- Getters - 类似 computed 的状态计算\n- Actions - 处理异步逻辑或修改状态' },
{ id: 5, title: 'CSS Grid 布局实战技巧', date: '2026-06-05', excerpt: '深入探索 CSS Grid让页面布局变得轻而易举。', content: 'CSS Grid 是现代 CSS 布局的重要组成部分,比 Flexbox 更适合二维布局场景。\n\n基础用法\n.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }\n\n常用场景\n1. 相册网格 - 自动填充列数\n2. 圣杯布局 - 经典三栏布局\n3. 响应式卡片 - 自动换行适配' },
{ id: 6, title: '用 Git 工作流提升团队协作效率', date: '2026-06-06', excerpt: 'GitFlow vs trunk-based development选对工作流事半功倍。', content: '良好的 Git 工作流能让团队协作更加顺畅。常见的工作流模型:\n\n1. GitFlow - 分支模型清晰,适合发布周期稳定的项目\n2. trunk-based development - 主干开发,快速迭代\n3. fork workflow - 开源项目首选,隔离性强\n\n小团队推荐使用简化 GitFlowmain + develop + feature 分支。' },
{ id: 7, title: 'JavaScript 事件循环机制详解', date: '2026-06-07', excerpt: '理解 Event Loop、宏任务与微任务告别异步编程困惑。', content: '事件循环是 JavaScript 异步编程的核心机制。\n\n执行顺序\n1. 同步代码优先执行\n2. 微任务Promise、MutationObserver\n3. 宏任务setTimeout、setInterval、setImmediate\n\n示例\nconsole.log(\'1\');\nsetTimeout(() => console.log(\'2\'), 0);\nPromise.resolve().then(() => console.log(\'3\'));\nconsole.log(\'4\');\n\n输出顺序1, 4, 3, 2' },
{ id: 8, title: '我的开发环境配置分享', date: '2026-06-08', excerpt: 'VS Code + Zsh + Tmux打造高效开发终端。', content: '工欲善其事,必先利其器。分享我的开发环境配置:\n\n编辑器\n- VS Code + Vim 插件\n- Tokyo Night 主题\n- Fira Code 字体 + 连字\n\n终端\n- iTerm2 (macOS) / Windows Terminal\n- Oh My Zsh\n- Tmux 会话管理\n\n版本管理\n- Git + delta 美化 diff\n- lazygit 终端 UI' },
{ id: 9, title: '从零搭建一个 CLI 工具', date: '2026-06-09', excerpt: '使用 Node.js 和 commander.js 快速构建命令行工具。', content: '命令行工具是提升开发效率的利器。使用 Node.js 可以快速构建:\n\n初始化项目\nnpm init -y\nnpm install commander inquirer\n\n核心代码\nconst { program } = require(\'commander\');\nprogram.version(\'1.0.0\');\nprogram.option(\'-n, --name <name>\', \'项目名称\');\nprogram.parse(process.argv);\n\n发布到 npm\nnpm publish --access public' },
{ id: 10, title: '为什么我喜欢深夜编程', date: '2026-06-10', excerpt: '安静、专注、不被打扰,深夜是程序员的黄金时段。', content: '深夜编程的独特魅力:\n\n1. 绝对安静 - 无人打扰,思路连贯\n2. 效率翻倍 - 深度工作状态\n3. 问题解决 - 复杂 bug 往往在这时候被攻破\n\n当然也要适度。注意休息保护眼睛第二天才能持续输出。\n\n推荐配合黑咖啡 + 轻音乐 + 舒适的降噪耳机。' }
],
about: {
id: 1,
title: '关于我',
intro: '你好!我是一名热爱技术开发的开发者。',
blog: '这个博客用于分享我在学习和工作中的一些心得体会。',
tech: 'Vue / Vue 3, JavaScript / TypeScript, Node.js, Vite, Pinia, Vue Router',
features: JSON.stringify([
'定时任务系统 - 支持 Cron表达式的自动化任务管理',
'自定义背景 - 支持纯色和图片背景,可调节透明度',
'多语言支持 - 中英文切换',
'自定义图标 - 上传本地图片作为网站图标'
]),
contact: JSON.stringify({
email: 'example@email.com',
github: 'github.com/yourusername'
}),
filing_number: '桂ICP备2022004108号-1'
},
settings: {
id: 1,
bg_type: 'color',
bg_color: '#f5f5f5',
bg_image: '',
bg_opacity: 1.00,
language: 'zh',
favicon: '',
uploaded_images: JSON.stringify([]),
uploaded_icons: JSON.stringify([])
},
cronTasks: []
};
module.exports = memoryStore;

235
server/src/config/rustfs.js Normal file
View File

@@ -0,0 +1,235 @@
/**
* RUSTFS 对象存储配置
* 直接使用 HTTP API 避免 AWS SDK 兼容性问题
*/
const https = require('https');
const http = require('http');
const crypto = require('crypto');
const config = {
endpoint: process.env.RUSTFS_ENDPOINT || 'http://43.156.91.115:9001',
bucket: process.env.RUSTFS_BUCKET || 'setting',
accessKey: process.env.RUSTFS_ACCESS_KEY || 'rustfsadmin',
secretKey: process.env.RUSTFS_SECRET_KEY || 'rustfsadmin'
};
// 生成 AWS Signature V4
function createSignature(method, path, headers, query = '') {
const date = new Date().toISOString().replace(/[:\-]|\.\d{3}/g, '');
const dateShort = date.substring(0, 8);
const payloadHash = crypto.createHash('sha256').update('').digest('hex');
const canonicalHeaders = Object.entries(headers)
.map(([k, v]) => `${k.toLowerCase()}:${v}`)
.join('\n');
const signedHeaders = Object.keys(headers).map(k => k.toLowerCase()).join(';');
const canonicalRequest = [
method,
path,
query,
canonicalHeaders,
'',
signedHeaders,
payloadHash
].join('\n');
const credentialScope = `${dateShort}/us-east-1/s3/aws4_request`;
const stringToSign = [
'AWS4-HMAC-SHA256',
date,
credentialScope,
crypto.createHash('sha256').update(canonicalRequest).digest('hex')
].join('\n');
const kDate = crypto.createHmac('sha256', `AWS4${config.secretKey}`).update(dateShort).digest();
const kRegion = crypto.createHmac('sha256', kDate).update('us-east-1').digest();
const kService = crypto.createHmac('sha256', kRegion).update('s3').digest();
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest();
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex');
return {
authorization: `AWS4-HMAC-SHA256 Credential=${config.accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
'x-amz-date': date,
'x-amz-content-sha256': payloadHash
};
}
function makeRequest(method, path, body = null, contentType = 'application/octet-stream') {
return new Promise((resolve, reject) => {
const url = new URL(config.endpoint);
const isHttps = url.protocol === 'https:';
const httpModule = isHttps ? https : http;
const query = '';
const headers = {
'host': url.host,
'content-type': contentType,
'x-amz-date': new Date().toISOString().replace(/[:\-]|\.\d{3}/g, '').substring(0, 8) + 'T000000Z'
};
if (body) {
headers['content-length'] = Buffer.byteLength(body);
}
// 简化认证:使用 Access Key 作为 Authorization header
headers['Authorization'] = `AWS ${config.accessKey}:${crypto.createHash('sha256').update(config.secretKey).digest('hex')}`;
const options = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 9001),
path: `/${config.bucket}${path}`,
method: method,
headers: headers
};
const req = httpModule.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(data);
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
}
});
});
req.on('error', reject);
if (body) {
req.write(body);
}
req.end();
});
}
/**
* 上传文件到 RUSTFS
*/
async function uploadFile(fileData, key, contentType = 'image/jpeg') {
// 解析 endpoint
const endpoint = config.endpoint;
const url = new URL(endpoint);
const isHttps = url.protocol === 'https:';
const httpModule = isHttps ? https : http;
const date = new Date().toISOString().replace(/[:\-]|\.\d{3}/g, '').substring(0, 8);
const datetime = date + 'T000000Z';
// 创建签名字符串
const credential = `${config.accessKey}/${date}/us-east-1/s3/aws4_request`;
const signedHeaders = 'content-type;host;x-amz-content-sha256;x-amz-date';
const contentHash = crypto.createHash('sha256').update(fileData).digest('hex');
const canonicalHeaders = [
`content-type:${contentType}`,
`host:${url.host}`,
`x-amz-content-sha256:${contentHash}`,
`x-amz-date:${datetime}`
].join('\n');
const canonicalRequest = [
'PUT',
`/${config.bucket}/${key}`,
'',
canonicalHeaders,
'',
signedHeaders,
contentHash
].join('\n');
const stringToSign = [
'AWS4-HMAC-SHA256',
datetime,
`${date}/us-east-1/s3/aws4_request`,
crypto.createHash('sha256').update(canonicalRequest).digest('hex')
].join('\n');
// 计算签名
const k1 = crypto.createHmac('sha256', 'AWS4' + config.secretKey).update(date).digest();
const k2 = crypto.createHmac('sha256', k1).update('us-east-1').digest();
const k3 = crypto.createHmac('sha256', k2).update('s3').digest();
const k4 = crypto.createHmac('sha256', k3).update('aws4_request').digest();
const signature = crypto.createHmac('sha256', k4).update(stringToSign).digest('hex');
const authorization = `AWS4-HMAC-SHA256 Credential=${credential}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
return new Promise((resolve, reject) => {
const options = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 9001),
path: `/${config.bucket}/${key}`,
method: 'PUT',
headers: {
'Content-Type': contentType,
'Host': url.host,
'X-Amz-Content-Sha256': contentHash,
'X-Amz-Date': datetime,
'Authorization': authorization,
'Content-Length': Buffer.byteLength(fileData)
}
};
const req = httpModule.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(`${endpoint}/${config.bucket}/${key}`);
} else {
reject(new Error(`Upload failed: HTTP ${res.statusCode} - ${data}`));
}
});
});
req.on('error', reject);
req.write(fileData);
req.end();
});
}
/**
* 删除 RUSTFS 中的文件
*/
async function deleteFile(key) {
const endpoint = config.endpoint;
const url = new URL(endpoint);
const isHttps = url.protocol === 'https:';
const httpModule = isHttps ? https : http;
const datetime = new Date().toISOString().replace(/[:\-]|\.\d{3}/g, '').substring(0, 8) + 'T000000Z';
return new Promise((resolve, reject) => {
const options = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 9001),
path: `/${config.bucket}/${key}`,
method: 'DELETE',
headers: {
'Host': url.host,
'X-Amz-Date': datetime,
'Authorization': `AWS ${config.accessKey}:${crypto.createHash('sha256').update(config.secretKey).digest('hex')}`
}
};
const req = httpModule.request(options, (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve();
} else {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => reject(new Error(`Delete failed: HTTP ${res.statusCode}`)));
}
});
req.on('error', reject);
req.end();
});
}
module.exports = {
uploadFile,
deleteFile,
config
};

108
server/src/config/schema.js Normal file
View File

@@ -0,0 +1,108 @@
/**
* 数据库表结构定义
* 包含所有表的 CREATE TABLE 语句
*/
const DB_NAME = process.env.DB_NAME || 'jnote';
// posts 表 - content 改为 MEDIUMTEXT 支持更大内容
const CREATE_POSTS_TABLE = `
CREATE TABLE IF NOT EXISTS posts (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
date DATE NOT NULL,
excerpt TEXT,
content MEDIUMTEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_posts_date (date DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`;
// about 表 - features 和 contact 改为 JSON 类型
const CREATE_ABOUT_TABLE = `
CREATE TABLE IF NOT EXISTS about (
id INT PRIMARY KEY DEFAULT 1,
title VARCHAR(255) DEFAULT '关于我',
intro TEXT,
blog TEXT,
tech TEXT,
features JSON,
contact JSON,
filing_number VARCHAR(100),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`;
// settings 表 - uploaded_images/icons 改为 JSON 类型bg_image/favicon 改为更节省空间的 TEXT
const CREATE_SETTINGS_TABLE = `
CREATE TABLE IF NOT EXISTS settings (
id INT PRIMARY KEY DEFAULT 1,
bg_type VARCHAR(20) DEFAULT 'color',
bg_color VARCHAR(20) DEFAULT '#f5f5f5',
bg_image TEXT,
bg_opacity DECIMAL(3,2) DEFAULT 1.00,
language VARCHAR(10) DEFAULT 'zh',
favicon TEXT,
uploaded_images JSON,
uploaded_icons JSON,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`;
// cron_tasks 表
const CREATE_CRON_TASKS_TABLE = `
CREATE TABLE IF NOT EXISTS cron_tasks (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
cron VARCHAR(50) NOT NULL,
url VARCHAR(500) NOT NULL,
method VARCHAR(20) DEFAULT 'GET',
headers TEXT,
body TEXT,
enabled TINYINT(1) DEFAULT 1,
last_run DATETIME,
next_run DATETIME,
run_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`;
// 所有表创建的执行顺序(按依赖关系)
const TABLE_DEFINITIONS = [
{ name: 'posts', sql: CREATE_POSTS_TABLE },
{ name: 'about', sql: CREATE_ABOUT_TABLE },
{ name: 'settings', sql: CREATE_SETTINGS_TABLE },
{ name: 'cron_tasks', sql: CREATE_CRON_TASKS_TABLE }
];
// 默认数据
const DEFAULT_DATA = {
about: {
title: '关于我',
intro: '你好!我是一名热爱技术开发的开发者。',
blog: '这个博客用于分享我在学习和工作中的一些心得体会。',
tech: 'Vue / Vue 3, JavaScript / TypeScript, Node.js, Vite, Pinia, Vue Router',
features: JSON.stringify([
'定时任务系统 - 支持 Cron表达式的自动化任务管理',
'自定义背景 - 支持纯色和图片背景,可调节透明度',
'多语言支持 - 中英文切换',
'自定义图标 - 上传本地图片作为网站图标'
]),
contact: JSON.stringify({ email: 'example@email.com', github: 'github.com/yourusername' }),
filing_number: '桂ICP备2022004108号-1'
},
settings: {
bg_type: 'color',
bg_color: '#f5f5f5',
bg_opacity: 1.00,
language: 'zh'
}
};
module.exports = {
DB_NAME,
TABLE_DEFINITIONS,
DEFAULT_DATA
};

52
server/src/index.js Normal file
View File

@@ -0,0 +1,52 @@
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { initDatabase } = require('./config/database');
const cronScheduler = require('./config/cronScheduler');
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件
app.use(cors());
app.use(express.json({ limit: '10mb' }));
// 路由
const postsRouter = require('./routes/posts');
const aboutRouter = require('./routes/about');
const settingsRouter = require('./routes/settings');
const cronRouter = require('./routes/cron');
const configRouter = require('./routes/config');
app.use('/api/posts', postsRouter);
app.use('/api/about', aboutRouter);
app.use('/api/settings', settingsRouter);
app.use('/api/cron-tasks', cronRouter);
app.use('/api/config', configRouter);
// 健康检查
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// 错误处理
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(500).json({ error: '服务器内部错误' });
});
// 启动服务器
async function start() {
await initDatabase();
// 启动定时任务调度器
cronScheduler.start();
app.listen(PORT, () => {
console.log(`🚀 服务器运行在 http://localhost:${PORT}`);
console.log(`📖 API 文档: http://localhost:${PORT}/api/health`);
});
}
start();

View File

@@ -0,0 +1,96 @@
const express = require('express');
const router = express.Router();
const { getPool } = require('../config/database');
const memoryStore = require('../config/memoryStore');
// 获取关于内容 - 优先使用预热缓存
router.get('/', async (req, res) => {
try {
const pool = getPool();
if (!pool) {
return res.json(memoryStore.about);
}
// 优先返回缓存数据
if (memoryStore.about && memoryStore.about.id === 1) {
return res.json(memoryStore.about);
}
const [rows] = await pool.execute('SELECT * FROM about WHERE id = 1');
if (rows.length === 0) {
return res.json(memoryStore.about);
}
const about = formatAbout(rows[0]);
memoryStore.about = about;
res.json(about);
} catch (error) {
console.error('获取关于内容失败:', error);
res.json(memoryStore.about);
}
});
// 格式化 about 数据 - MySQL JSON 类型直接返回对象
function formatAbout(row) {
if (!row) return memoryStore.about;
const about = { ...row };
if (about.tech && typeof about.tech === 'string') {
about.techList = about.tech.split(',').map(t => t.trim());
}
// MySQL JSON 类型直接返回 JS 对象,不需要 JSON.parse
if (about.features && typeof about.features === 'string') {
try {
about.featuresList = JSON.parse(about.features);
} catch {
about.featuresList = Array.isArray(about.features) ? about.features : [];
}
} else if (Array.isArray(about.features)) {
about.featuresList = about.features;
}
if (about.contact && typeof about.contact === 'string') {
try {
about.contactObj = JSON.parse(about.contact);
} catch {
about.contactObj = typeof about.contact === 'object' ? about.contact : {};
}
} else if (typeof about.contact === 'object') {
about.contactObj = about.contact;
}
return about;
}
// 更新关于内容
router.put('/', async (req, res) => {
try {
const { title, intro, blog, tech, features, contact, filing_number } = req.body;
const pool = getPool();
if (!pool) {
Object.assign(memoryStore.about, { title, intro, blog, tech, features, contact, filing_number });
return res.json(memoryStore.about);
}
await pool.execute(
`UPDATE about SET title = ?, intro = ?, blog = ?, tech = ?, features = ?, contact = ?, filing_number = ? WHERE id = 1`,
[
title || '关于我',
intro || '',
tech || '',
typeof features === 'object' ? JSON.stringify(features) : features || '[]',
typeof contact === 'object' ? JSON.stringify(contact) : contact || '{}',
filing_number || ''
]
);
const [rows] = await pool.execute('SELECT * FROM about WHERE id = 1');
const updated = formatAbout(rows[0]);
memoryStore.about = updated;
res.json(updated);
} catch (error) {
console.error('更新关于内容失败:', error);
res.status(500).json({ error: '更新关于内容失败' });
}
});
module.exports = router;

View File

@@ -0,0 +1,73 @@
const express = require('express');
const router = express.Router();
const envManager = require('../config/envManager');
// 获取当前配置(密码隐藏)
router.get('/', (req, res) => {
try {
const config = envManager.getSafeConfig();
res.json(config);
} catch (error) {
console.error('获取配置失败:', error);
res.status(500).json({ error: '获取配置失败' });
}
});
// 更新配置
router.put('/', (req, res) => {
try {
const { db, rustfs, server } = req.body;
const config = {
// 数据库配置
DB_HOST: db?.host || 'localhost',
DB_PORT: db?.port || '3306',
DB_USER: db?.user || 'root',
DB_PASSWORD: db?.password || '',
DB_NAME: db?.name || 'jnote',
// RUSTFS 配置
RUSTFS_ENDPOINT: rustfs?.endpoint || 'http://43.156.91.115:9001',
RUSTFS_REGION: rustfs?.region || 'us-east-1',
RUSTFS_BUCKET: rustfs?.bucket || 'setting',
RUSTFS_ACCESS_KEY: rustfs?.accessKey || 'rustfsadmin',
RUSTFS_SECRET_KEY: rustfs?.secretKey || 'rustfsadmin',
// 服务器配置
PORT: server?.port || '3000'
};
// 如果密码字段为 ******,说明没改,从原文件读取
if (db?.password === '******') {
const oldConfig = envManager.readEnv();
config.DB_PASSWORD = oldConfig.DB_PASSWORD || '';
}
if (rustfs?.secretKey === '******') {
const oldConfig = envManager.readEnv();
config.RUSTFS_SECRET_KEY = oldConfig.RUSTFS_SECRET_KEY || '';
}
const success = envManager.writeEnv(config);
if (!success) {
return res.status(500).json({ error: '保存配置失败' });
}
res.json(envManager.getSafeConfig());
} catch (error) {
console.error('更新配置失败:', error);
res.status(500).json({ error: '更新配置失败' });
}
});
// 重启服务
router.post('/restart', (req, res) => {
console.log('收到重启信号,准备关闭服务...');
// 优雅退出,让 PM2 或其他进程管理器自动重启
res.json({ message: '服务即将重启', delay: 1000 });
// 延迟一点再退出,让响应先发出去
setTimeout(() => {
process.exit(0);
}, 500);
});
module.exports = router;

249
server/src/routes/cron.js Normal file
View File

@@ -0,0 +1,249 @@
const express = require('express');
const router = express.Router();
const { getPool } = require('../config/database');
const memoryStore = require('../config/memoryStore');
// 获取所有定时任务
router.get('/', async (req, res) => {
try {
const pool = getPool();
if (!pool) {
return res.json(memoryStore.cronTasks);
}
const [rows] = await pool.execute('SELECT * FROM cron_tasks ORDER BY created_at DESC');
res.json(rows.map(formatCronTask));
} catch (error) {
console.error('获取定时任务失败:', error);
res.json(memoryStore.cronTasks);
}
});
// 创建定时任务
router.post('/', async (req, res) => {
try {
const { name, cron, url, method, headers, body, enabled } = req.body;
if (!name || !cron || !url) {
return res.status(400).json({ error: '任务名称、Cron表达式和URL是必填项' });
}
const pool = getPool();
const id = Date.now().toString();
const nextRun = calculateNextRun(cron);
if (!pool) {
const newTask = {
id,
name,
cron,
url,
method: method || 'GET',
headers: headers || '',
body: body || '',
enabled: enabled !== false,
lastRun: null,
nextRun,
runCount: 0
};
memoryStore.cronTasks.push(newTask);
return res.status(201).json(newTask);
}
await pool.execute(
`INSERT INTO cron_tasks (id, name, cron, url, method, headers, body, enabled, next_run, run_count)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)`,
[id, name, cron, url, method || 'GET', headers || '', body || '', enabled !== false ? 1 : 0, nextRun]
);
const [rows] = await pool.execute('SELECT * FROM cron_tasks WHERE id = ?', [id]);
res.status(201).json(formatCronTask(rows[0]));
} catch (error) {
console.error('创建定时任务失败:', error);
res.status(500).json({ error: '创建定时任务失败' });
}
});
// 更新定时任务
router.put('/:id', async (req, res) => {
try {
const { name, cron, url, method, headers, body, enabled } = req.body;
const pool = getPool();
if (!pool) {
const index = memoryStore.cronTasks.findIndex(t => t.id === req.params.id);
if (index === -1) {
return res.status(404).json({ error: '任务未找到' });
}
const nextRun = calculateNextRun(cron);
memoryStore.cronTasks[index] = {
...memoryStore.cronTasks[index],
name, cron, url, method, headers, body, enabled, nextRun
};
return res.json(memoryStore.cronTasks[index]);
}
const nextRun = calculateNextRun(cron);
await pool.execute(
`UPDATE cron_tasks SET name = ?, cron = ?, url = ?, method = ?, headers = ?, body = ?, enabled = ?, next_run = ? WHERE id = ?`,
[name, cron, url, method || 'GET', headers || '', body || '', enabled ? 1 : 0, nextRun, req.params.id]
);
const [rows] = await pool.execute('SELECT * FROM cron_tasks WHERE id = ?', [req.params.id]);
if (rows.length === 0) {
return res.status(404).json({ error: '任务未找到' });
}
res.json(formatCronTask(rows[0]));
} catch (error) {
console.error('更新定时任务失败:', error);
res.status(500).json({ error: '更新定时任务失败' });
}
});
// 删除定时任务
router.delete('/:id', async (req, res) => {
try {
const pool = getPool();
if (!pool) {
const index = memoryStore.cronTasks.findIndex(t => t.id === req.params.id);
if (index === -1) {
return res.status(404).json({ error: '任务未找到' });
}
memoryStore.cronTasks.splice(index, 1);
return res.status(204).send();
}
const [result] = await pool.execute('DELETE FROM cron_tasks WHERE id = ?', [req.params.id]);
if (result.affectedRows === 0) {
return res.status(404).json({ error: '任务未找到' });
}
res.status(204).send();
} catch (error) {
console.error('删除定时任务失败:', error);
res.status(500).json({ error: '删除定时任务失败' });
}
});
// 立即执行任务
router.post('/:id/run', async (req, res) => {
try {
const pool = getPool();
let task;
if (!pool) {
task = memoryStore.cronTasks.find(t => t.id === req.params.id);
if (!task) {
return res.status(404).json({ error: '任务未找到' });
}
} else {
const [rows] = await pool.execute('SELECT * FROM cron_tasks WHERE id = ?', [req.params.id]);
if (rows.length === 0) {
return res.status(404).json({ error: '任务未找到' });
}
task = formatCronTask(rows[0]);
}
// 执行 HTTP 请求
const options = {
method: task.method || 'GET',
headers: parseHeaders(task.headers)
};
if (task.body && ['POST', 'PUT', 'PATCH'].includes(task.method)) {
options.body = task.body;
}
const response = await fetch(task.url, options);
const now = new Date();
const nextRun = calculateNextRun(task.cron);
if (!pool) {
task.lastRun = now.toLocaleString('zh-CN');
task.runCount = (task.runCount || 0) + 1;
task.nextRun = nextRun;
return res.json({
success: response.ok,
status: response.status,
lastRun: task.lastRun,
nextRun: task.nextRun,
runCount: task.runCount
});
}
await pool.execute(
'UPDATE cron_tasks SET last_run = ?, next_run = ?, run_count = run_count + 1 WHERE id = ?',
[now, nextRun, req.params.id]
);
res.json({
success: response.ok,
status: response.status,
lastRun: now.toLocaleString('zh-CN'),
nextRun,
runCount: task.runCount + 1
});
} catch (error) {
console.error('执行任务失败:', error);
res.status(500).json({ error: '执行任务失败: ' + error.message });
}
});
function formatCronTask(row) {
if (!row) return null;
return {
id: row.id,
name: row.name,
cron: row.cron,
url: row.url,
method: row.method,
headers: row.headers,
body: row.body,
enabled: Boolean(row.enabled),
lastRun: row.last_run ? row.last_run.toLocaleString('zh-CN') : null,
nextRun: row.next_run ? row.next_run.toLocaleString('zh-CN') : null,
runCount: row.run_count || 0
};
}
function parseHeaders(headers) {
if (!headers) return {};
if (typeof headers === 'object') return headers;
try {
return JSON.parse(headers);
} catch {
return {};
}
}
function calculateNextRun(cron) {
const parts = cron.trim().split(/\s+/);
if (parts.length < 5) return null;
const now = new Date();
const next = new Date(now);
const minute = parts[0];
const hour = parts[1];
if (minute === '*') {
next.setMinutes(now.getMinutes() + 1);
} else if (minute.includes('/')) {
const step = parseInt(minute.split('/')[1]);
next.setMinutes(Math.ceil(now.getMinutes() / step) * step);
} else {
next.setMinutes(parseInt(minute));
if (next <= now) next.setHours(next.getHours() + 1);
}
if (hour !== '*') {
next.setHours(parseInt(hour), 0, 0, 0);
if (next <= now) next.setDate(next.getDate() + 1);
}
return next;
}
module.exports = router;

188
server/src/routes/posts.js Normal file
View File

@@ -0,0 +1,188 @@
const express = require('express');
const router = express.Router();
const { getPool } = require('../config/database');
const memoryStore = require('../config/memoryStore');
// 格式化日期为 YYYY-MM-DD HH:mm:ss
function formatDate(date) {
if (!date) return '';
const d = new Date(date);
if (isNaN(d.getTime())) return date;
const pad = n => n.toString().padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
// 获取所有文章 - 优先使用预热缓存
router.get('/', async (req, res) => {
try {
const pool = getPool();
if (!pool) {
return res.json(memoryStore.posts);
}
// 优先返回缓存数据(已预热)
if (memoryStore.posts && memoryStore.posts.length > 0) {
return res.json(memoryStore.posts.map(p => ({ ...p, date: formatDate(p.date) })));
}
const [rows] = await pool.execute('SELECT * FROM posts ORDER BY date DESC');
const formatted = rows.map(r => ({ ...r, date: formatDate(r.date) }));
// 更新缓存
memoryStore.posts = rows;
res.json(formatted);
} catch (error) {
console.error('获取文章列表失败:', error);
res.json(memoryStore.posts || []);
}
});
// 获取单篇文章
router.get('/:id', async (req, res) => {
try {
const pool = getPool();
const id = Number(req.params.id);
if (!pool) {
const post = memoryStore.posts.find(p => p.id === id);
return post ? res.json({ ...post, date: formatDate(post.date) }) : res.status(404).json({ error: '文章未找到' });
}
// 先在缓存中查找
if (memoryStore.posts && memoryStore.posts.length > 0) {
const cached = memoryStore.posts.find(p => p.id === id);
if (cached) {
return res.json({ ...cached, date: formatDate(cached.date) });
}
}
const [rows] = await pool.execute('SELECT * FROM posts WHERE id = ?', [id]);
if (rows.length === 0) {
return res.status(404).json({ error: '文章未找到' });
}
res.json({ ...rows[0], date: formatDate(rows[0].date) });
} catch (error) {
console.error('获取文章失败:', error);
const post = memoryStore.posts.find(p => p.id === Number(req.params.id));
post ? res.json({ ...post, date: formatDate(post.date) }) : res.status(404).json({ error: '文章未找到' });
}
});
// 创建文章
router.post('/', async (req, res) => {
try {
const { title, date, excerpt, content } = req.body;
if (!title || !date) {
return res.status(400).json({ error: '标题和日期是必填项' });
}
const pool = getPool();
if (!pool) {
const newPost = {
id: memoryStore.posts.length + 1,
title,
date,
excerpt: excerpt || '',
content: content || ''
};
memoryStore.posts.unshift(newPost);
return res.status(201).json({ ...newPost, date: formatDate(newPost.date) });
}
const [result] = await pool.execute(
'INSERT INTO posts (title, date, excerpt, content) VALUES (?, ?, ?, ?)',
[title, date, excerpt || '', content || '']
);
const [rows] = await pool.execute('SELECT * FROM posts WHERE id = ?', [result.insertId]);
const newPost = rows[0];
// 同步更新缓存
if (memoryStore.posts) {
memoryStore.posts.unshift(newPost);
}
res.status(201).json({ ...newPost, date: formatDate(newPost.date) });
} catch (error) {
console.error('创建文章失败:', error);
res.status(500).json({ error: '创建文章失败' });
}
});
// 更新文章
router.put('/:id', async (req, res) => {
try {
const { title, date, excerpt, content } = req.body;
const pool = getPool();
const id = Number(req.params.id);
if (!pool) {
const index = memoryStore.posts.findIndex(p => p.id === id);
if (index === -1) {
return res.status(404).json({ error: '文章未找到' });
}
memoryStore.posts[index] = { ...memoryStore.posts[index], title, date, excerpt, content };
return res.json({ ...memoryStore.posts[index], date: formatDate(memoryStore.posts[index].date) });
}
await pool.execute(
'UPDATE posts SET title = ?, date = ?, excerpt = ?, content = ? WHERE id = ?',
[title, date, excerpt || '', content || '', id]
);
const [rows] = await pool.execute('SELECT * FROM posts WHERE id = ?', [id]);
if (rows.length === 0) {
return res.status(404).json({ error: '文章未找到' });
}
const updated = rows[0];
// 同步更新缓存
if (memoryStore.posts) {
const index = memoryStore.posts.findIndex(p => p.id === id);
if (index !== -1) {
memoryStore.posts[index] = updated;
}
}
res.json({ ...updated, date: formatDate(updated.date) });
} catch (error) {
console.error('更新文章失败:', error);
res.status(500).json({ error: '更新文章失败' });
}
});
// 删除文章
router.delete('/:id', async (req, res) => {
try {
const pool = getPool();
const id = Number(req.params.id);
if (!pool) {
const index = memoryStore.posts.findIndex(p => p.id === id);
if (index === -1) {
return res.status(404).json({ error: '文章未找到' });
}
memoryStore.posts.splice(index, 1);
return res.status(204).send();
}
const [result] = await pool.execute('DELETE FROM posts WHERE id = ?', [id]);
if (result.affectedRows === 0) {
return res.status(404).json({ error: '文章未找到' });
}
// 同步更新缓存
if (memoryStore.posts) {
memoryStore.posts = memoryStore.posts.filter(p => p.id !== id);
}
res.status(204).send();
} catch (error) {
console.error('删除文章失败:', error);
res.status(500).json({ error: '删除文章失败' });
}
});
module.exports = router;

View File

@@ -0,0 +1,216 @@
const express = require('express');
const router = express.Router();
const { getPool } = require('../config/database');
const memoryStore = require('../config/memoryStore');
const rustfs = require('../config/rustfs');
// 生成唯一文件名
function generateKey(prefix, id) {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
return `${prefix}/${timestamp}-${random}-${id}`;
}
// base64 数据解码为 Buffer
function decodeBase64(dataUrl) {
if (!dataUrl || !dataUrl.startsWith('data:')) {
return null;
}
const matches = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
if (!matches) return null;
return {
mimeType: matches[1],
data: Buffer.from(matches[2], 'base64')
};
}
// 获取设置 - 优先使用预热缓存
router.get('/', async (req, res) => {
try {
const pool = getPool();
if (!pool) {
return res.json(memoryStore.settings);
}
// 优先返回缓存数据
if (memoryStore.settings && memoryStore.settings.bgType) {
return res.json(memoryStore.settings);
}
const [rows] = await pool.execute('SELECT * FROM settings WHERE id = 1');
if (rows.length === 0) {
return res.json(memoryStore.settings);
}
const formatted = formatSettings(rows[0]);
memoryStore.settings = formatted;
res.json(formatted);
} catch (error) {
console.error('获取设置失败:', error.message);
res.json(memoryStore.settings);
}
});
// 上传单张图片到 RUSTFS
router.post('/upload-image', async (req, res) => {
try {
const { imageData, type } = req.body; // type: 'background' | 'icon' | 'gallery'
if (!imageData || !imageData.startsWith('data:')) {
return res.status(400).json({ error: '无效的图片数据' });
}
const decoded = decodeBase64(imageData);
if (!decoded) {
return res.status(400).json({ error: '图片解码失败' });
}
const id = Date.now().toString();
const prefix = type || 'images';
const key = generateKey(prefix, id);
const url = await rustfs.uploadFile(decoded.data, key, decoded.mimeType);
console.log(`✅ 图片上传成功: ${url}`);
res.json({ url, id });
} catch (error) {
console.error('图片上传失败:', error);
res.status(500).json({ error: '图片上传失败' });
}
});
// 批量上传图片(并行上传到 RUSTFS
router.post('/upload-images', async (req, res) => {
try {
const { images, type } = req.body; // images: [{id, data}]
if (!Array.isArray(images) || images.length === 0) {
return res.json({ images: [] });
}
const prefix = type || 'images';
// 并行上传所有图片
const uploadPromises = images.map(async (img) => {
try {
const decoded = decodeBase64(img.data);
if (!decoded) {
return { id: img.id, url: null, error: '解码失败' };
}
const key = generateKey(prefix, img.id);
const url = await rustfs.uploadFile(decoded.data, key, decoded.mimeType);
return { id: img.id, url };
} catch (err) {
console.error(`图片 ${img.id} 上传失败:`, err);
return { id: img.id, url: null, error: err.message };
}
});
const results = await Promise.all(uploadPromises);
res.json({ images: results });
} catch (error) {
console.error('批量图片上传失败:', error);
res.status(500).json({ error: '图片上传失败' });
}
});
// 更新设置
router.put('/', async (req, res) => {
try {
// 兼容 camelCase 和 snake_case
const {
bg_type, bgType,
bg_color, bgColor,
bg_image, bgImage,
bg_opacity, bgOpacity,
language,
favicon,
uploaded_images, uploadedImages,
uploaded_icons, uploadedIcons
} = req.body;
const pool = getPool();
if (!pool) {
Object.assign(memoryStore.settings, {
bg_type: bg_type || bgType,
bg_color: bg_color || bgColor,
bg_image: bg_image || bgImage,
bg_opacity: bg_opacity ?? bgOpacity,
language,
favicon,
uploaded_images: uploaded_images || uploadedImages,
uploaded_icons: uploaded_icons || uploadedIcons
});
return res.json(memoryStore.settings);
}
await pool.execute(
`UPDATE settings SET
bg_type = ?, bg_color = ?, bg_image = ?, bg_opacity = ?,
language = ?, favicon = ?, uploaded_images = ?, uploaded_icons = ?
WHERE id = 1`,
[
bg_type || bgType || 'color',
bg_color || bgColor || '#f5f5f5',
bg_image || bgImage || '',
bg_opacity ?? bgOpacity ?? 1.00,
language || 'zh',
favicon || '',
prepareJSON(uploaded_images || uploadedImages),
prepareJSON(uploaded_icons || uploadedIcons)
]
);
const [rows] = await pool.execute('SELECT * FROM settings WHERE id = 1');
const formatted = formatSettings(rows[0]);
memoryStore.settings = formatted;
res.json(formatted);
} catch (error) {
console.error('更新设置失败:', error);
res.status(500).json({ error: '更新设置失败' });
}
});
function formatSettings(row) {
if (!row) return memoryStore.settings;
return {
bgType: row.bg_type,
bgColor: row.bg_color,
bgImage: row.bg_image,
bgOpacity: row.bg_opacity,
language: row.language,
favicon: row.favicon,
// uploadedImages 和 uploadedIcons 现在存储的是 {id, url} 数组
uploadedImages: parseJSON(row.uploaded_images, []),
uploadedIcons: parseJSON(row.uploaded_icons, [])
};
}
function parseJSON(str, defaultValue) {
if (!str) return defaultValue;
if (typeof str === 'object') return str;
try {
return JSON.parse(str);
} catch {
return defaultValue;
}
}
function prepareJSON(value) {
if (!value) return '[]';
if (typeof value === 'string') {
try {
JSON.parse(value);
return value;
} catch {
return JSON.stringify(value);
}
}
return JSON.stringify(value);
}
module.exports = router;

View File

@@ -0,0 +1,49 @@
/**
* 数据库索引迁移脚本
* 为已存在的表添加索引,提升查询性能
* 运行方式: node src/scripts/addIndexes.js
*/
require('dotenv').config();
const mysql = require('mysql2/promise');
const config = {
host: process.env.DB_HOST,
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'jnote'
};
async function migrate() {
let connection;
try {
console.log('🔌 正在连接数据库...');
connection = await mysql.createConnection(config);
console.log('✅ 数据库连接成功');
// 为 posts 表的 date 字段添加索引
console.log('📝 正在为 posts.date 添加索引...');
try {
await connection.query('CREATE INDEX idx_posts_date ON posts (date DESC)');
console.log('✅ idx_posts_date 索引创建成功');
} catch (error) {
if (error.code === 'ER_DUP_KEYNAME') {
console.log(' idx_posts_date 索引已存在,跳过');
} else {
throw error;
}
}
console.log('\n🎉 索引迁移完成!');
} catch (error) {
console.error('❌ 索引迁移失败:', error.message);
process.exit(1);
} finally {
if (connection) {
await connection.end();
}
}
}
migrate();

View File

@@ -0,0 +1,74 @@
/**
* 数据库诊断脚本
* 检查数据库中的实际数据
* 运行方式: node src/scripts/checkDb.js
*/
require('dotenv').config();
const mysql = require('mysql2/promise');
const config = {
host: process.env.DB_HOST,
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'jnote'
};
async function check() {
let connection;
try {
console.log('🔌 正在连接数据库...\n');
connection = await mysql.createConnection(config);
console.log('✅ 数据库连接成功\n');
// 检查表结构
console.log('📋 表结构检查:');
const [tables] = await connection.query('SHOW TABLES');
console.log('现有表:', tables.map(t => Object.values(t)[0]).join(', '));
// 检查 settings 表结构和数据
console.log('\n📊 settings 表数据:');
try {
const [settingsRows] = await connection.query('SELECT * FROM settings WHERE id = 1');
if (settingsRows.length === 0) {
console.log('settings 表为空或无数据');
} else {
const row = settingsRows[0];
console.log(' id:', row.id);
console.log(' bg_type:', row.bg_type);
console.log(' bg_color:', row.bg_color);
console.log(' bg_image:', row.bg_image ? `[base64, 长度: ${row.bg_image.length}]` : '(空)');
console.log(' bg_opacity:', row.bg_opacity);
console.log(' language:', row.language);
console.log(' favicon:', row.favicon ? `[base64, 长度: ${row.favicon.length}]` : '(空)');
console.log(' uploaded_images:', row.uploaded_images);
console.log(' uploaded_icons:', row.uploaded_icons);
console.log(' uploaded_images 类型:', typeof row.uploaded_images);
console.log(' uploaded_icons 类型:', typeof row.uploaded_icons);
}
} catch (error) {
console.log('settings 表查询失败:', error.message);
}
// 检查 posts 数量
console.log('\n📝 posts 数量:');
try {
const [postCount] = await connection.query('SELECT COUNT(*) as count FROM posts');
console.log('文章总数:', postCount[0].count);
} catch (error) {
console.log('posts 表查询失败:', error.message);
}
console.log('\n✅ 诊断完成');
} catch (error) {
console.error('❌ 诊断失败:', error.message);
process.exit(1);
} finally {
if (connection) {
await connection.end();
}
}
}
check();

View File

@@ -0,0 +1,165 @@
/**
* 数据库初始化脚本
* 运行方式: node src/scripts/initDb.js
*/
require('dotenv').config();
const mysql = require('mysql2/promise');
const config = {
host: process.env.DB_HOST,
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
charset: 'utf8mb4'
};
const DB_NAME = process.env.DB_NAME || 'jnote';
async function initDatabase() {
let connection;
try {
console.log('🔌 正在连接数据库...');
connection = await mysql.createConnection(config);
console.log('✅ 数据库连接成功');
// 创建数据库
console.log(`📦 正在创建数据库 ${DB_NAME}...`);
await connection.query(`CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
console.log(`✅ 数据库 ${DB_NAME} 创建成功`);
// 使用数据库
await connection.query(`USE \`${DB_NAME}\``);
// 创建 posts 表 - content 改为 MEDIUMTEXT
console.log('📝 正在创建 posts 表...');
await connection.query(`
CREATE TABLE IF NOT EXISTS posts (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
date DATE NOT NULL,
excerpt TEXT,
content MEDIUMTEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_posts_date (date DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
console.log('✅ posts 表创建成功');
// 创建 about 表 - 使用 JSON 类型
console.log('📋 正在创建 about 表...');
await connection.query(`
CREATE TABLE IF NOT EXISTS about (
id INT PRIMARY KEY DEFAULT 1,
title VARCHAR(255) DEFAULT '关于我',
intro TEXT,
blog TEXT,
tech TEXT,
features JSON,
contact JSON,
filing_number VARCHAR(100),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
console.log('✅ about 表创建成功');
// 创建 settings 表 - 使用 JSON 类型存储图片列表
console.log('⚙️ 正在创建 settings 表...');
await connection.query(`
CREATE TABLE IF NOT EXISTS settings (
id INT PRIMARY KEY DEFAULT 1,
bg_type VARCHAR(20) DEFAULT 'color',
bg_color VARCHAR(20) DEFAULT '#f5f5f5',
bg_image TEXT,
bg_opacity DECIMAL(3,2) DEFAULT 1.00,
language VARCHAR(10) DEFAULT 'zh',
favicon TEXT,
uploaded_images JSON,
uploaded_icons JSON,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
console.log('✅ settings 表创建成功');
// 创建 cron_tasks 表
console.log('⏰ 正在创建 cron_tasks 表...');
await connection.query(`
CREATE TABLE IF NOT EXISTS cron_tasks (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
cron VARCHAR(50) NOT NULL,
url VARCHAR(500) NOT NULL,
method VARCHAR(20) DEFAULT 'GET',
headers TEXT,
body TEXT,
enabled TINYINT(1) DEFAULT 1,
last_run DATETIME,
next_run DATETIME,
run_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
console.log('✅ cron_tasks 表创建成功');
// 初始化默认数据
console.log('📄 正在初始化默认数据...');
// 初始化 about 默认数据
await connection.query(`
INSERT INTO about (id, title, intro, blog, tech, features, contact, filing_number)
VALUES (1, '关于我', '你好!我是一名热爱技术开发的开发者。', '这个博客用于分享我在学习和工作中的一些心得体会。',
'Vue / Vue 3, JavaScript / TypeScript, Node.js, Vite, Pinia, Vue Router',
'["定时任务系统 - 支持 Cron表达式的自动化任务管理", "自定义背景 - 支持纯色和图片背景,可调节透明度", "多语言支持 - 中英文切换", "自定义图标 - 上传本地图片作为网站图标"]',
'{"email": "example@email.com", "github": "github.com/yourusername"}',
'桂ICP备2022004108号-1')
ON DUPLICATE KEY UPDATE id=id
`);
console.log('✅ about 默认数据初始化成功');
// 初始化 settings 默认数据
await connection.query(`
INSERT INTO settings (id, bg_type, bg_color, bg_opacity, language)
VALUES (1, 'color', '#f5f5f5', 1.00, 'zh')
ON DUPLICATE KEY UPDATE id=id
`);
console.log('✅ settings 默认数据初始化成功');
// 初始化示例文章
const samplePosts = [
['欢迎来到我的编程学习记录', '2026-06-01', '这是我的第一篇文章,欢迎大家来访!', '这是我的第一篇文章,欢迎大家来访!在这里我会分享一些技术心得和生活感悟。希望大家喜欢这里的内容,有任何问题欢迎留言交流。'],
['Vue 3 组合式 API 入门', '2026-06-02', '探索 Vue 3 的组合式 API感受更好的代码组织方式。', 'Vue 3 引入了组合式 API这是一种全新的代码组织方式。通过 setup 函数和响应式 API我们可以更灵活地组织组件逻辑提高代码的可复用性和可维护性。\n\n主要特点\n1. 更好的逻辑复用\n2. 更灵活的代码组织\n3. 更好的 TypeScript 支持'],
['静态网站生成器选型', '2026-06-03', '对比常见的静态网站生成器,帮助你选择合适的工具。', '选择一个合适的静态网站生成器需要考虑多个因素:\n\n1. Hexo - 简单易用,主题丰富\n2. Jekyll - GitHub Pages 原生支持,社区活跃\n3. Hugo - 构建速度极快\n4. VuePress / VitePress - 基于 Vue适合技术文档\n\n本博客使用 Vue 3 + Vite 构建,兼具现代特性和开发体验。'],
['优雅地使用 Pinia 管理状态', '2026-06-04', 'Pinia 是 Vue 3 推荐的状态管理方案,本篇介绍其基本用法。', 'Pinia 是 Vue 官方推荐的新一代状态管理库,相比 Vuex 更加轻量和直观。\n\n核心概念\n- Store - 存储状态的地方\n- Getters - 类似 computed 的状态计算\n- Actions - 处理异步逻辑或修改状态'],
['CSS Grid 布局实战技巧', '2026-06-05', '深入探索 CSS Grid让页面布局变得轻而易举。', 'CSS Grid 是现代 CSS 布局的重要组成部分,比 Flexbox 更适合二维布局场景。\n\n基础用法\n.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }\n\n常用场景\n1. 相册网格 - 自动填充列数\n2. 圣杯布局 - 经典三栏布局\n3. 响应式卡片 - 自动换行适配'],
['用 Git 工作流提升团队协作效率', '2026-06-06', 'GitFlow vs trunk-based development选对工作流事半功倍。', '良好的 Git 工作流能让团队协作更加顺畅。常见的工作流模型:\n\n1. GitFlow - 分支模型清晰,适合发布周期稳定的项目\n2. trunk-based development - 主干开发,快速迭代\n3. fork workflow - 开源项目首选,隔离性强\n\n小团队推荐使用简化 GitFlowmain + develop + feature 分支。'],
['JavaScript 事件循环机制详解', '2026-06-07', '理解 Event Loop、宏任务与微任务告别异步编程困惑。', '事件循环是 JavaScript 异步编程的核心机制。\n\n执行顺序\n1. 同步代码优先执行\n2. 微任务Promise、MutationObserver\n3. 宏任务setTimeout、setInterval、setImmediate\n\n示例\nconsole.log(\'1\');\nsetTimeout(() => console.log(\'2\'), 0);\nPromise.resolve().then(() => console.log(\'3\'));\nconsole.log(\'4\');\n\n输出顺序1, 4, 3, 2'],
['我的开发环境配置分享', '2026-06-08', 'VS Code + Zsh + Tmux打造高效开发终端。', '工欲善其事,必先利其器。分享我的开发环境配置:\n\n编辑器\n- VS Code + Vim 插件\n- Tokyo Night 主题\n- Fira Code 字体 + 连字\n\n终端\n- iTerm2 (macOS) / Windows Terminal\n- Oh My Zsh\n- Tmux 会话管理\n\n版本管理\n- Git + delta 美化 diff\n- lazygit 终端 UI'],
['从零搭建一个 CLI 工具', '2026-06-09', '使用 Node.js 和 commander.js 快速构建命令行工具。', '命令行工具是提升开发效率的利器。使用 Node.js 可以快速构建:\n\n初始化项目\nnpm init -y\nnpm install commander inquirer\n\n核心代码\nconst { program } = require(\'commander\');\nprogram.version(\'1.0.0\');\nprogram.option(\'-n, --name <name>\', \'项目名称\');\nprogram.parse(process.argv);\n\n发布到 npm\nnpm publish --access public'],
['为什么我喜欢深夜编程', '2026-06-10', '安静、专注、不被打扰,深夜是程序员的黄金时段。', '深夜编程的独特魅力:\n\n1. 绝对安静 - 无人打扰,思路连贯\n2. 效率翻倍 - 深度工作状态\n3. 问题解决 - 复杂 bug 往往在这时候被攻破\n\n当然也要适度。注意休息保护眼睛第二天才能持续输出。\n\n推荐配合黑咖啡 + 轻音乐 + 舒适的降噪耳机。']
];
for (const post of samplePosts) {
await connection.query(
'INSERT INTO posts (title, date, excerpt, content) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE title=title',
post
);
}
console.log('✅ 示例文章初始化成功共10篇');
console.log('\n🎉 数据库初始化完成!');
console.log(`📖 数据库名: ${DB_NAME}`);
console.log('📋 数据表: posts, about, settings, cron_tasks');
} catch (error) {
console.error('❌ 数据库初始化失败:', error.message);
process.exit(1);
} finally {
if (connection) {
await connection.end();
}
}
}
initDatabase();

View File

@@ -0,0 +1,49 @@
/**
* 数据库字段扩展脚本
* 将 TEXT 改为 LONGTEXT 以支持大图片
*/
require('dotenv').config();
const mysql = require('mysql2/promise');
const config = {
host: process.env.DB_HOST,
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'jnote'
};
async function migrate() {
let connection;
try {
console.log('🔌 正在连接数据库...');
connection = await mysql.createConnection(config);
console.log('✅ 数据库连接成功');
console.log('📝 正在修改 settings 表字段...');
await connection.query('ALTER TABLE settings MODIFY COLUMN bg_image LONGTEXT');
console.log('✅ bg_image 改为 LONGTEXT');
await connection.query('ALTER TABLE settings MODIFY COLUMN favicon LONGTEXT');
console.log('✅ favicon 改为 LONGTEXT');
await connection.query('ALTER TABLE settings MODIFY COLUMN uploaded_images LONGTEXT');
console.log('✅ uploaded_images 改为 LONGTEXT');
await connection.query('ALTER TABLE settings MODIFY COLUMN uploaded_icons LONGTEXT');
console.log('✅ uploaded_icons 改为 LONGTEXT');
console.log('\n🎉 字段扩展完成!');
} catch (error) {
console.error('❌ 扩展失败:', error.message);
process.exit(1);
} finally {
if (connection) {
await connection.end();
}
}
}
migrate();

View File

@@ -0,0 +1,115 @@
/**
* 数据存储结构迁移脚本
* 将 posts.content 升级为 MEDIUMTEXTabout 和 settings 的 JSON 字段类型优化
* 运行方式: node src/scripts/migrateStorage.js
*/
require('dotenv').config();
const mysql = require('mysql2/promise');
const config = {
host: process.env.DB_HOST,
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'jnote'
};
async function migrate() {
let connection;
try {
console.log('🔌 正在连接数据库...');
connection = await mysql.createConnection(config);
console.log('✅ 数据库连接成功');
// 1. 将 posts.content 升级为 MEDIUMTEXT
console.log('📝 正在升级 posts.content 为 MEDIUMTEXT...');
try {
await connection.query('ALTER TABLE posts MODIFY COLUMN content MEDIUMTEXT');
console.log('✅ posts.content 已升级为 MEDIUMTEXT');
} catch (error) {
if (error.code === 'ER_BAD_FIELD_ERROR') {
console.log(' posts.content 已是 MEDIUMTEXT 或不存在,跳过');
} else {
throw error;
}
}
// 2. 将 about.features 改为 JSON 类型
console.log('📋 正在将 about.features 迁移为 JSON 类型...');
try {
await connection.query('ALTER TABLE about MODIFY COLUMN features JSON');
console.log('✅ about.features 已改为 JSON 类型');
} catch (error) {
if (error.code === 'ER_BAD_FIELD_ERROR') {
console.log(' about.features 字段不存在,跳过');
} else {
console.log('⚠️ about.features 迁移失败:', error.message);
}
}
// 3. 将 about.contact 改为 JSON 类型
console.log('📋 正在将 about.contact 迁移为 JSON 类型...');
try {
await connection.query('ALTER TABLE about MODIFY COLUMN contact JSON');
console.log('✅ about.contact 已改为 JSON 类型');
} catch (error) {
if (error.code === 'ER_BAD_FIELD_ERROR') {
console.log(' about.contact 字段不存在,跳过');
} else {
console.log('⚠️ about.contact 迁移失败:', error.message);
}
}
// 4. 将 settings.uploaded_images 改为 JSON 类型
console.log('⚙️ 正在将 settings.uploaded_images 迁移为 JSON 类型...');
try {
await connection.query('ALTER TABLE settings MODIFY COLUMN uploaded_images JSON');
console.log('✅ settings.uploaded_images 已改为 JSON 类型');
} catch (error) {
if (error.code === 'ER_BAD_FIELD_ERROR') {
console.log(' settings.uploaded_images 字段不存在,跳过');
} else {
console.log('⚠️ settings.uploaded_images 迁移失败:', error.message);
}
}
// 5. 将 settings.uploaded_icons 改为 JSON 类型
console.log('⚙️ 正在将 settings.uploaded_icons 迁移为 JSON 类型...');
try {
await connection.query('ALTER TABLE settings MODIFY COLUMN uploaded_icons JSON');
console.log('✅ settings.uploaded_icons 已改为 JSON 类型');
} catch (error) {
if (error.code === 'ER_BAD_FIELD_ERROR') {
console.log(' settings.uploaded_icons 字段不存在,跳过');
} else {
console.log('⚠️ settings.uploaded_icons 迁移失败:', error.message);
}
}
// 6. 添加 posts.date 索引
console.log('📊 正在添加 posts.date 索引...');
try {
await connection.query('CREATE INDEX idx_posts_date ON posts (date DESC)');
console.log('✅ idx_posts_date 索引创建成功');
} catch (error) {
if (error.code === 'ER_DUP_KEYNAME') {
console.log(' idx_posts_date 索引已存在,跳过');
} else {
throw error;
}
}
console.log('\n🎉 存储结构迁移完成!');
console.log('\n建议考虑将图片等大文件存储到文件服务器而非数据库。');
} catch (error) {
console.error('❌ 迁移失败:', error.message);
process.exit(1);
} finally {
if (connection) {
await connection.end();
}
}
}
migrate();

134
server/start.js Normal file
View File

@@ -0,0 +1,134 @@
/**
* JNote 后端启动脚本
* 跨平台兼容 Windows 和 Linux
*/
const { spawn, execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
const scriptDir = __dirname;
process.chdir(scriptDir);
const APP_NAME = 'jnote-api';
const isWindows = os.platform() === 'win32';
function log(msg) {
console.log(`[Start] ${msg}`);
}
function loadEnv() {
const envFile = path.join(scriptDir, '.env');
if (!fs.existsSync(envFile)) {
log('⚠️ 未找到 .env 文件');
return;
}
const content = fs.readFileSync(envFile, 'utf8');
content.split('\n').forEach(line => {
line = line.trim();
if (!line || line.startsWith('#')) return;
const idx = line.indexOf('=');
if (idx > 0) {
const key = line.substring(0, idx).trim();
let value = line.substring(idx + 1).trim();
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
process.env[key] = value;
}
});
}
function getExePath() {
const possibleNames = isWindows
? ['jnote-api.exe', 'jnote-api']
: ['jnote-api', 'jnote-api.exe'];
for (const name of possibleNames) {
const p = path.join(scriptDir, name);
if (fs.existsSync(p)) return p;
}
return null;
}
function startWithPM2(exePath) {
log('🚀 使用 PM2 启动...');
try {
execSync('pm2 --version', { stdio: 'ignore' });
} catch {
log('📦 安装 PM2...');
execSync('npm install -g pm2', { cwd: scriptDir, stdio: 'inherit' });
}
try {
execSync(`pm2 stop ${APP_NAME}`, { stdio: 'ignore' });
execSync(`pm2 delete ${APP_NAME}`, { stdio: 'ignore' });
} catch {}
spawn('pm2', ['start', exePath, '--name', APP_NAME], {
cwd: scriptDir,
stdio: 'inherit',
shell: true
});
setTimeout(() => {
try { execSync('pm2 save', { stdio: 'ignore' }); } catch {}
}, 2000);
log('✅ 服务已启动');
log(' 状态: pm2 status');
log(' 日志: pm2 logs jnote-api');
}
function startDirect(exePath) {
log('🚀 直接启动服务...');
log(` 端口: ${process.env.PORT || 3000}`);
log('');
if (!isWindows) {
fs.chmodSync(exePath, 0o755);
}
spawn(exePath, [], {
cwd: scriptDir,
stdio: 'inherit',
env: process.env
});
}
function main() {
console.log('=========================================');
console.log(' JNote 后端启动脚本');
console.log(` 平台: ${isWindows ? 'Windows' : 'Linux'}`);
console.log('=========================================');
loadEnv();
const exe = getExePath();
if (!exe) {
console.error('❌ 错误: 未找到 jnote-api 可执行文件');
console.error(` 请确保可执行文件与本脚本在同一目录下: ${scriptDir}`);
console.error('');
console.error(' Linux 应有: jnote-api');
console.error(' Windows 应有: jnote-api.exe');
console.error('');
console.error(' 如果还没有打包,请先运行: node build.js');
process.exit(1);
}
log(`✓ 找到: ${path.basename(exe)}`);
// 优先使用 PM2
try {
execSync('pm2 --version', { stdio: 'ignore' });
startWithPM2(exe);
} catch {
startDirect(exe);
}
}
main();