354 lines
10 KiB
TypeScript
354 lines
10 KiB
TypeScript
import { Pool, PoolClient } from "pg";
|
||
|
||
const dbConfig = {
|
||
host: "118.89.161.243",
|
||
port: 54201,
|
||
user: "hjdave",
|
||
password: "HJD13567840170",
|
||
database: "hjdave-doc",
|
||
max: 20,
|
||
idleTimeoutMillis: 30000,
|
||
connectionTimeoutMillis: 2000,
|
||
};
|
||
|
||
// 全局连接池单例
|
||
let pool: Pool | null = null;
|
||
|
||
export function getPool(): Pool {
|
||
if (!pool) {
|
||
pool = new Pool(dbConfig);
|
||
pool.on("error", (err) => {
|
||
console.error("Unexpected error on idle client", err);
|
||
});
|
||
}
|
||
return pool;
|
||
}
|
||
|
||
export async function query(sql: string, params?: any[]): Promise<any> {
|
||
const pool = getPool();
|
||
const start = Date.now();
|
||
try {
|
||
const res = await pool.query(sql, params);
|
||
const duration = Date.now() - start;
|
||
console.log("Executed query", { sql, duration, rows: res.rowCount });
|
||
return res;
|
||
} catch (error) {
|
||
console.error("Database query error", error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
export async function getClient(): Promise<PoolClient> {
|
||
const client = await getPool().connect();
|
||
const release = () => client.release();
|
||
|
||
return client as PoolClient & { release: () => void };
|
||
}
|
||
|
||
// 初始化数据库表
|
||
export async function initializeDatabase(): Promise<void> {
|
||
// 创建用户表
|
||
await query(`
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id SERIAL PRIMARY KEY,
|
||
username VARCHAR(255) UNIQUE NOT NULL,
|
||
email VARCHAR(255) UNIQUE,
|
||
password_hash VARCHAR(255) NOT NULL,
|
||
role VARCHAR(50) DEFAULT 'admin',
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
`);
|
||
|
||
// 创建文档表
|
||
await query(`
|
||
CREATE TABLE IF NOT EXISTS docs (
|
||
id SERIAL PRIMARY KEY,
|
||
slug VARCHAR(255) UNIQUE NOT NULL,
|
||
title VARCHAR(255) NOT NULL,
|
||
description TEXT,
|
||
created_by INTEGER REFERENCES users(id),
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
`);
|
||
|
||
// 创建章节表
|
||
await query(`
|
||
CREATE TABLE IF NOT EXISTS sections (
|
||
id SERIAL PRIMARY KEY,
|
||
doc_id INTEGER REFERENCES docs(id) ON DELETE CASCADE,
|
||
title VARCHAR(255) NOT NULL,
|
||
content TEXT,
|
||
code TEXT,
|
||
display_order INTEGER DEFAULT 0,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
`);
|
||
|
||
// 创建索引
|
||
await query("CREATE INDEX IF NOT EXISTS idx_docs_slug ON docs(slug)");
|
||
await query("CREATE INDEX IF NOT EXISTS idx_docs_created_by ON docs(created_by)");
|
||
await query("CREATE INDEX IF NOT EXISTS idx_sections_doc_id ON sections(doc_id)");
|
||
|
||
// 便签表
|
||
await query(`
|
||
CREATE TABLE IF NOT EXISTS notes (
|
||
id SERIAL PRIMARY KEY,
|
||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||
title VARCHAR(255) NOT NULL,
|
||
content TEXT,
|
||
color VARCHAR(20) DEFAULT '#fef08a',
|
||
is_pinned BOOLEAN DEFAULT FALSE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
`);
|
||
|
||
// 留言表
|
||
await query(`
|
||
CREATE TABLE IF NOT EXISTS messages (
|
||
id SERIAL PRIMARY KEY,
|
||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||
content TEXT NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
`);
|
||
|
||
// 便签索引
|
||
await query("CREATE INDEX IF NOT EXISTS idx_notes_user_id ON notes(user_id)");
|
||
await query("CREATE INDEX IF NOT EXISTS idx_notes_pinned ON notes(is_pinned)");
|
||
await query("CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages(user_id)");
|
||
|
||
// 初始化默认管理员账户
|
||
const existing = await query("SELECT id FROM users WHERE username = $1", ["admin"]);
|
||
if (existing.rows.length === 0) {
|
||
await query(
|
||
"INSERT INTO users (username, email, password_hash, role) VALUES ($1, $2, $3, $4)",
|
||
["admin", "admin@docsite.com", "$2b$10$d1BPt0phBsRORuWnAacgZO.Y0LBZ3hLc8uKyZBN1BkPMR8j4GESHS", "admin"]
|
||
);
|
||
console.log("Created default admin user");
|
||
} else {
|
||
// 更新密码(如果之前存在但密码不正确)
|
||
await query(
|
||
"UPDATE users SET password_hash = $1 WHERE username = $2",
|
||
["$2b$10$d1BPt0phBsRORuWnAacgZO.Y0LBZ3hLc8uKyZBN1BkPMR8j4GESHS", "admin"]
|
||
);
|
||
console.log("Updated admin password");
|
||
}
|
||
|
||
console.log("Database initialized successfully");
|
||
}
|
||
|
||
// 获取密码哈希
|
||
export async function getPasswordHash(username: string): Promise<string | null> {
|
||
const result = await query(
|
||
"SELECT password_hash FROM users WHERE username = $1",
|
||
[username]
|
||
);
|
||
return result.rows[0]?.password_hash ?? null;
|
||
}
|
||
|
||
// 检查是否是管理员
|
||
export async function isAdmin(username: string): Promise<boolean> {
|
||
const result = await query(
|
||
"SELECT role FROM users WHERE username = $1",
|
||
[username]
|
||
);
|
||
return result.rows[0]?.role === "admin";
|
||
}
|
||
|
||
// 验证用户登录
|
||
export async function verifyUser(username: string, password: string): Promise<boolean> {
|
||
const hash = await getPasswordHash(username);
|
||
if (!hash) return false;
|
||
|
||
const bcrypt = await import("bcryptjs");
|
||
return bcrypt.compare(password, hash);
|
||
}
|
||
|
||
// 创建新用户
|
||
export async function createUser(
|
||
username: string,
|
||
password: string,
|
||
email?: string,
|
||
role: "admin" | "user" = "user"
|
||
): Promise<boolean> {
|
||
const bcrypt = await import("bcryptjs");
|
||
const passwordHash = await bcrypt.hash(password, 10);
|
||
|
||
try {
|
||
await query(
|
||
"INSERT INTO users (username, email, password_hash, role) VALUES ($1, $2, $3, $4)",
|
||
[username, email, passwordHash, role]
|
||
);
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Failed to create user:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 获取所有文档
|
||
export async function getDocs(): Promise<any[]> {
|
||
const result = await query(
|
||
`
|
||
SELECT d.*,
|
||
json_agg(
|
||
json_build_object(
|
||
'id', s.id,
|
||
'title', s.title,
|
||
'content', s.content,
|
||
'code', s.code,
|
||
'display_order', s.display_order
|
||
) ORDER BY s.display_order
|
||
) as sections
|
||
FROM docs d
|
||
LEFT JOIN sections s ON s.doc_id = d.id
|
||
GROUP BY d.id
|
||
ORDER BY d.created_at DESC
|
||
`
|
||
);
|
||
return result.rows.map((row: any) => ({
|
||
title: row.title,
|
||
slug: row.slug,
|
||
description: row.description,
|
||
sections: JSON.parse(row.sections || "[]"),
|
||
}));
|
||
}
|
||
|
||
// 添加文档
|
||
export async function addDoc(
|
||
slug: string,
|
||
title: string,
|
||
description?: string
|
||
): Promise<boolean> {
|
||
try {
|
||
await query(
|
||
"INSERT INTO docs (slug, title, description) VALUES ($1, $2, $3)",
|
||
[slug, title, description]
|
||
);
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Failed to add doc:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 添加章节
|
||
export async function addSection(docSlug: string, title: string, content: string, code?: string): Promise<boolean> {
|
||
const docResult = await query("SELECT id FROM docs WHERE slug = $1", [docSlug]);
|
||
if (docResult.rows.length === 0) return false;
|
||
|
||
const docId = docResult.rows[0].id;
|
||
|
||
try {
|
||
await query(
|
||
"INSERT INTO sections (doc_id, title, content, code) VALUES ($1, $2, $3, $4)",
|
||
[docId, title, content, code || null]
|
||
);
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Failed to add section:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 获取所有文档(简化版,直接返回 JSON)
|
||
export async function getDocsSimple(): Promise<any[]> {
|
||
return [
|
||
{
|
||
title: "快速开始",
|
||
slug: "quick-start",
|
||
description: "了解如何开始使用我们的平台",
|
||
sections: [
|
||
{ title: "简介", content: "欢迎使用我们的在线文档系统!", code: null },
|
||
{ title: "安装", content: "开始之前,请确保你已经安装了 Node.js 和 npm。", code: "npm install\nnpm run dev" },
|
||
],
|
||
},
|
||
{
|
||
title: "API 参考",
|
||
slug: "api-reference",
|
||
description: "API 接口文档",
|
||
sections: [{ title: "获取文档列表", content: "获取所有可用文档的列表。", code: "GET /api/docs" }],
|
||
},
|
||
];
|
||
}
|
||
|
||
// 便签相关
|
||
export async function getNotes(userId: number): Promise<any[]> {
|
||
const result = await query(
|
||
`SELECT * FROM notes WHERE user_id = $1 ORDER BY is_pinned DESC, created_at DESC`,
|
||
[userId]
|
||
);
|
||
return result.rows;
|
||
}
|
||
|
||
export async function addNote(userId: number, title: string, content: string, color: string, isPinned: boolean): Promise<boolean> {
|
||
try {
|
||
await query(
|
||
"INSERT INTO notes (user_id, title, content, color, is_pinned) VALUES ($1, $2, $3, $4, $5)",
|
||
[userId, title, content, color, isPinned]
|
||
);
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Failed to add note:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
export async function deleteNote(userId: number, noteId: number): Promise<boolean> {
|
||
try {
|
||
await query("DELETE FROM notes WHERE id = $1 AND user_id = $2", [noteId, userId]);
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Failed to delete note:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
export async function togglePinNote(userId: number, noteId: number): Promise<boolean> {
|
||
try {
|
||
await query("UPDATE notes SET is_pinned = NOT is_pinned WHERE id = $1 AND user_id = $2", [noteId, userId]);
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Failed to toggle pin:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 留言相关
|
||
export async function getMessages(): Promise<any[]> {
|
||
const result = await query(`
|
||
SELECT m.*, u.username FROM messages m
|
||
JOIN users u ON m.user_id = u.id
|
||
ORDER BY m.created_at DESC
|
||
LIMIT 100
|
||
`);
|
||
return result.rows;
|
||
}
|
||
|
||
export async function addMessage(userId: number, content: string): Promise<boolean> {
|
||
try {
|
||
await query(
|
||
"INSERT INTO messages (user_id, content) VALUES ($1, $2)",
|
||
[userId, content]
|
||
);
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Failed to add message:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
export async function deleteMessage(userId: number, messageId: number): Promise<boolean> {
|
||
try {
|
||
await query("DELETE FROM messages WHERE id = $1 AND user_id = $2", [messageId, userId]);
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Failed to delete message:", error);
|
||
return false;
|
||
}
|
||
}
|