feat(database): 添加 SQLite 数据库支持

实现基于 better-sqlite3 的数据库层:
- 数据库连接管理(进程级单例模式)
- 数据库迁移系统
- 基础表结构初始化
- 添加 better-sqlite3 使用最佳实践文档

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
dzq 2026-01-10 17:48:46 +08:00
parent f37680c3e3
commit 8216a83ec1
11 changed files with 1103 additions and 0 deletions

1
.gitignore vendored
View File

@ -21,6 +21,7 @@ __snapshots__/
.claude/
*.swp
*.swo
data/
# OS generated files
.DS_Store

476
doc/better-sqlite3.md Normal file
View File

@ -0,0 +1,476 @@
下面是一份结合目前(约 2026 年)社区共识的 **better-sqlite3 使用指南 + 最佳实践实战版**,你可以直接拿来做项目的参考模板。
---
# better-sqlite3 使用指南(含最佳实践)
## 一、定位与总体建议
### 1.1 为什么选 better-sqlite3
在 Node.js 中操作 SQLite目前主流选型是
- 新项目首选 **better-sqlite3**
- Node.js 24+ 内置的 `node:sqlite` 适合:轻量脚本、小工具、对依赖敏感的场景
- 旧项目上的 `node-sqlite3` 已被弃用,不建议新项目继续使用
**better-sqlite3 的优点:**
- **同步 API**:代码简单可读,事务逻辑不被 Promise/回调搅乱
- **性能优秀**:普遍被认为是 Node 生态里最快的 SQLite 绑定之一
- **完整事务支持**:内置事务封装,写多步业务逻辑非常顺手
- **PRAGMA / 备份等高级能力完备**
安装:
```bash
npm install better-sqlite3
```
---
## 二、连接管理:一个进程一个连接
### 2.1 最佳实践:进程级单例
SQLite 本质是操作一个文件;和 MySQL/Postgres 不同,连接很轻量,不需要连接池。
**推荐模式:**
- 应用启动时创建 **一个全局 Database 实例**
- 全程复用,不要在每个请求里频繁 open/close
- 在进程退出前再统一 close很多服务甚至不显式 close
```js
// db/index.js
import Database from 'better-sqlite3';
let db;
export function getDB() {
if (!db) {
db = new Database('app.sqlite', {
readonly: false,
fileMustExist: false,
timeout: 5000, // 等待锁超时时间
// verbose: console.log // 调试时可打开
});
// 基础 PRAGMA见后文性能调优部分
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
db.pragma('cache_size = -64000'); // ~64MB
db.pragma('temp_store = MEMORY');
}
return db;
}
```
**反例(不要这样写):**
```js
// ❌ 每次请求新建 & 关闭连接
function handler(req, res) {
const db = new Database('app.sqlite');
const row = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
db.close();
res.json(row);
}
```
问题:
- 无谓的 open/close 开销
- 预编译语句无法复用
- 容易出现并发锁和资源浪费
### 2.2 多线程Worker Threads场景
当读操作很多、单线程成为瓶颈时:
- 使用 **Worker Threads**
- **每个 worker 线程自己 new 一个 Database 实例**
- 不在多个线程之间共享同一个 `Database` 对象
示意:
```js
// workers/readWorker.js
import { workerData, parentPort } from 'node:worker_threads';
import Database from 'better-sqlite3';
const db = new Database('app.sqlite', {
readonly: true,
fileMustExist: true,
timeout: 3000
});
db.pragma('cache_size = -32000');
db.pragma('temp_store = MEMORY');
db.pragma('query_only = ON'); // 防止误写
const stmt = db.prepare(workerData.sql);
const result = stmt.all(...(workerData.params || []));
parentPort.postMessage(result);
```
---
## 三、查询与预编译语句Prepared Statements
### 3.1 必须坚持使用预编译
**目标:同时保证性能和安全(防 SQL 注入)**。
```js
const db = getDB();
// 预编译语句在模块级缓存
const getUserById = db.prepare('SELECT * FROM users WHERE id = ?');
const listUsersByEmail = db.prepare('SELECT * FROM users WHERE email = ?');
export function findUserById(id) {
return getUserById.get(id); // 单行
}
export function findUsersByEmail(email) {
return listUsersByEmail.all(email); // 多行
}
```
**防注入反例:**
```js
// ❌ 直接拼字符串,存在注入风险
const sql = `SELECT * FROM users WHERE email = '${email}'`;
db.prepare(sql).all();
// ✅ 使用参数绑定
db.prepare('SELECT * FROM users WHERE email = ?').all(email);
```
### 3.2 get / all / run / exec 的使用场景
| 方法 | 用途 | 返回值典型用法 |
|--------|-----------------------|--------------------------------------|
| `get` | 查询**一条**记录 | `const row = stmt.get(id)` |
| `all` | 查询**多条**记录 | `const rows = stmt.all(status)` |
| `run` | INSERT/UPDATE/DELETE | `const info = stmt.run(...params)` |
| `exec` | 执行多条 SQL/DDL 语句 | `db.exec('CREATE TABLE ...; ...')` |
示例:
```js
const db = getDB();
const findActiveUsers = db.prepare('SELECT * FROM users WHERE active = 1');
const deactivateUser = db.prepare('UPDATE users SET active = 0 WHERE id = ?');
// 多行
const users = findActiveUsers.all();
// 更新
const info = deactivateUser.run(123);
console.log(info.changes); // 受影响行数
console.log(info.lastInsertRowid); // 如是INSERT语句则有意义
```
### 3.3 善用 RETURNING 减少一次查询
如果 SQLite 版本支持 `RETURNING`
```js
const createOrder = db.prepare(`
INSERT INTO orders (user_id, amount)
VALUES (?, ?)
RETURNING id, user_id, amount, created_at
`);
const order = createOrder.get(userId, amount);
// 这里已经拿到刚插入的订单信息,无需再额外 SELECT
```
---
## 四、事务管理:写操作的必备武器
任何「多步写入」逻辑(比如:下单、转账、批量导入)都应该放在事务里。
### 4.1 使用 better-sqlite3 的事务封装
```js
const db = getDB();
const placeOrder = db.transaction((userId, items) => {
const insertOrder = db.prepare(`
INSERT INTO orders (user_id, status)
VALUES (?, 'pending')
`);
const insertOrderItem = db.prepare(`
INSERT INTO order_items (order_id, product_id, qty, price)
VALUES (?, ?, ?, ?)
`);
const updateStock = db.prepare(`
UPDATE products
SET stock = stock - ?
WHERE id = ?
`);
const { lastInsertRowid: orderId } = insertOrder.run(userId);
for (const item of items) {
insertOrderItem.run(orderId, item.productId, item.qty, item.price);
updateStock.run(item.qty, item.productId);
}
return orderId;
});
// 使用
try {
const orderId = placeOrder(userId, items);
// 成功自动 COMMIT
} catch (err) {
// 抛出异常时自动 ROLLBACK
console.error('placeOrder 失败', err);
}
```
**注意事项:**
- 事务函数体 **必须是同步代码**,不要在里面 `await`/Promise/`setTimeout`
- 所有需要原子性的写操作都要包进同一次 `db.transaction()`
- 对于批量插入/更新,**一个大事务远快于 N 个小事务**
---
## 五、性能调优:常用 PRAGMA 配置
### 5.1 典型 Web 服务推荐配置
在应用启动时统一设置一次:
```js
const db = getDB();
// 提升并发 & 写性能:建议生产使用
db.pragma('journal_mode = WAL'); // 写前日志
// 写安全性 & 性能折中
db.pragma('synchronous = NORMAL'); // 不要贸然用 OFF
// 内存缓存(负数表示 KiB
db.pragma('cache_size = -64000'); // ≈64MB
// 临时对象走内存
db.pragma('temp_store = MEMORY');
// 利用 mmap 加速 IO根据机器内存调整
db.pragma('mmap_size = 268435456'); // 256MB
```
**不推荐在有写入的生产环境用 `synchronous = OFF`**,否则断电/崩溃时有丢数据风险。
### 5.2 读密集型 Worker 优化
针对只读 worker
```js
const db = new Database('app.sqlite', { readonly: true, fileMustExist: true });
db.pragma('cache_size = -32000'); // ~32MB
db.pragma('temp_store = MEMORY');
db.pragma('mmap_size = 1073741824'); // ~1GB
db.pragma('query_only = ON'); // 强制只读
```
### 5.3 查询计划与优化
数据库结构或索引大改之后,可以执行一次:
```js
db.exec('ANALYZE;');
db.pragma('optimize');
```
有需要时,用 `EXPLAIN QUERY PLAN` 分析慢查询(开发/排查时用)。
---
## 六、安全实践:防注入、防误删、防泄露
### 6.1 防 SQL 注入
核心原则:
- 所有用户输入**必须**通过「参数绑定」进入 SQL
- 不拼接字符串、不直接插入变量
```js
// ✅ 推荐使用形式
const stmt = db.prepare('SELECT * FROM users WHERE email = ?');
const user = stmt.get(email);
```
### 6.2 参数校验
在进入 SQL 前尽量校验类型 / 长度 / 格式:
```js
function assertId(id) {
if (!Number.isInteger(id) || id <= 0) {
throw new Error('非法ID');
}
}
```
### 6.3 SQLite 文件存放与权限
- **不要**把 `.sqlite` 文件放在可被 Web 服务器直接访问的静态目录下
- 赋予最小权限:如仅运行用户可读写
```bash
chmod 600 app.sqlite
chown node_user:node_group app.sqlite
```
---
## 七、备份与恢复
### 7.1 使用 better-sqlite3 的备份 API在线备份
```js
const db = getDB();
// 备份到另一个文件(热备,无需停服务)
await db.backup('backup.sqlite');
```
你可以把这段逻辑放进定时任务里,例如每天低峰执行一次。
### 7.2 冷备份(停机拷贝)
如果可以短暂停机:
1. 停止 Node 进程
2. 直接复制数据库文件
```bash
cp app.sqlite app.sqlite.bak
```
在 WAL 模式下,确保 `.sqlite` 和相关的 `-wal`/`-shm` 文件也一并妥善处理(备份 API 已经帮你做了这件事,一般更推荐)。
---
## 八、迁移与工程化
### 8.1 简单迁移系统示例
建议至少维护一个简易迁移机制,例如:
- 建一张 `migrations` 表,记录已执行的版本
- 在应用启动时按顺序执行 `migrations/*.sql`
伪代码:
```js
// db/migrate.js
import { getDB } from './index.js';
import fs from 'node:fs';
import path from 'node:path';
export function runMigrations() {
const db = getDB();
db.exec(`CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
run_at TEXT NOT NULL
)`);
const applied = new Set(
db.prepare('SELECT name FROM migrations').all().map(r => r.name)
);
const dir = path.join(process.cwd(), 'src/db/migrations');
const files = fs.readdirSync(dir).sort(); // 如001_init.sql, 002_add_user.sql
const runMigration = db.transaction((name, sql) => {
db.exec(sql);
db.prepare('INSERT INTO migrations (name, run_at) VALUES (?, datetime(\'now\'))')
.run(name);
});
for (const file of files) {
if (applied.has(file)) continue;
const sql = fs.readFileSync(path.join(dir, file), 'utf8');
runMigration(file, sql);
console.log('已执行迁移:', file);
}
}
```
### 8.2 典型项目结构建议
```txt
src/
db/
index.js # 初始化 & 导出 db 实例 + PRAGMA
migrate.js # 迁移逻辑
migrations/ # *.sql 文件
models/
user.js # 用户相关 SQL & 事务
order.js
workers/
readWorker.js # 高并发只读 worker
services/
userService.js # 业务逻辑。只调用 models不直接写 SQL
app.js # 应用入口启动HTTP服务 & 调用 runMigrations()
```
---
## 九、与 Node.js 内置 `node:sqlite` 的关系
- Node.js 24 LTS 起,内置了 `node:sqlite` 模块,方便做无依赖的小工具
- 但在实际生产服务里:
- **性能、特性、生态** 综合考虑better-sqlite3 依然是首选
- 两者 API 思路类似:同步操作、小而美,更方便从一种迁移到另一种
可以简记为:
> **重业务 / 生产服务better-sqlite3**
> **简单脚本 / 工具node:sqlite 也可以考虑**
---
## 十、一页小抄(速查版)
1. **连接管理**
- 进程内只创建一个 `Database` 实例,全局复用
- Worker Threads 中,一线程一实例,不共享连接
2. **查询写法**
- 所有 SQL 使用 **prepared statement + 参数绑定**
- 单行用 `get`,多行用 `all`,写操作用 `run`
3. **事务**
- 多步写操作必须放在 `db.transaction()`
- 事务函数里面不要写任何异步代码
4. **PRAGMA 调优**
- `journal_mode = WAL`
- `synchronous = NORMAL`
- 合理设置 `cache_size` / `temp_store = MEMORY` / `mmap_size`
5. **安全**
- 禁止拼接 SQL 字符串
- 校验输入
- 数据库文件不要暴露在静态目录,限制文件权限
6. **扩展读性能**
- 使用 Worker Threads
- 每个 worker 使用只读连接 + `query_only = ON`
如果你后续有更具体的场景(例如 Electron 桌面应用、嵌入式设备、日志收集服务等),可以再描述一下,我可以基于这些最佳实践帮你落到更贴近场景的代码模板与配置。

View File

@ -25,6 +25,7 @@
"@mastra/libsql": "^0.16.4",
"@mastra/memory": "^0.15.13",
"axios": "^1.13.2",
"better-sqlite3": "^12.6.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"helmet": "^8.1.0",
@ -34,6 +35,7 @@
"zod": "^4.1.13"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/helmet": "^4.0.0",

View File

@ -26,6 +26,9 @@ importers:
axios:
specifier: ^1.13.2
version: 1.13.2
better-sqlite3:
specifier: ^12.6.0
version: 12.6.0
cors:
specifier: ^2.8.5
version: 2.8.5
@ -48,6 +51,9 @@ importers:
specifier: ^4.1.13
version: 4.1.13
devDependencies:
'@types/better-sqlite3':
specifier: ^7.6.13
version: 7.6.13
'@types/cors':
specifier: ^2.8.17
version: 2.8.19
@ -1628,6 +1634,9 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/better-sqlite3@7.6.13':
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
'@types/body-parser@1.19.6':
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
@ -1859,9 +1868,19 @@ packages:
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
engines: {node: '>= 0.8'}
better-sqlite3@12.6.0:
resolution: {integrity: sha512-FXI191x+D6UPWSze5IzZjhz+i9MK9nsuHsmTX9bXVl52k06AfZ2xql0lrgIUuzsMsJ7Vgl5kIptvDgBLIV3ZSQ==}
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
bignumber.js@9.3.1:
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
body-parser@1.20.4:
resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@ -1879,6 +1898,9 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
builtins@5.1.0:
resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==}
@ -1909,6 +1931,9 @@ packages:
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
cjs-module-lexer@1.4.3:
resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==}
@ -2041,6 +2066,14 @@ packages:
supports-color:
optional: true
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
deep-extend@0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@ -2181,6 +2214,10 @@ packages:
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
engines: {node: ^18.19.0 || >=20.5.0}
expand-template@2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
express@4.22.1:
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
engines: {node: '>= 0.10.0'}
@ -2225,6 +2262,9 @@ packages:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'}
file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@ -2272,6 +2312,9 @@ packages:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
fs-extra@11.3.2:
resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
engines: {node: '>=14.14'}
@ -2319,6 +2362,9 @@ packages:
get-tsconfig@4.13.0:
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@ -2434,12 +2480,18 @@ packages:
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
import-in-the-middle@1.15.0:
resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
ip-regex@4.3.0:
resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==}
engines: {node: '>=8'}
@ -2657,9 +2709,16 @@ packages:
engines: {node: '>=4'}
hasBin: true
mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
@ -2684,6 +2743,9 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
@ -2692,6 +2754,10 @@ packages:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
node-abi@3.85.0:
resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==}
engines: {node: '>=10'}
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
@ -2883,6 +2949,11 @@ packages:
resolution: {integrity: sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==}
engines: {node: '>=15.0.0'}
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
hasBin: true
prettier@3.7.4:
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
engines: {node: '>=14'}
@ -2941,6 +3012,10 @@ packages:
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
engines: {node: '>= 0.10'}
rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
react-refresh@0.18.0:
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
engines: {node: '>=0.10.0'}
@ -2949,6 +3024,10 @@ packages:
resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==}
engines: {node: '>=0.10.0'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
@ -3112,6 +3191,12 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
@ -3142,6 +3227,9 @@ packages:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@ -3150,6 +3238,10 @@ packages:
resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
engines: {node: '>=18'}
strip-json-comments@2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
strip-json-comments@5.0.3:
resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
engines: {node: '>=14.16'}
@ -3176,6 +3268,13 @@ packages:
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
tar-fs@2.1.4:
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
tcp-port-used@1.0.2:
resolution: {integrity: sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==}
@ -3209,6 +3308,9 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@ -3263,6 +3365,9 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
@ -5155,6 +5260,10 @@ snapshots:
dependencies:
'@babel/types': 7.28.5
'@types/better-sqlite3@7.6.13':
dependencies:
'@types/node': 25.0.0
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
@ -5443,8 +5552,23 @@ snapshots:
dependencies:
safe-buffer: 5.1.2
better-sqlite3@12.6.0:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.3
bignumber.js@9.3.1: {}
bindings@1.5.0:
dependencies:
file-uri-to-path: 1.0.0
bl@4.1.0:
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.2
body-parser@1.20.4:
dependencies:
bytes: 3.1.2
@ -5488,6 +5612,11 @@ snapshots:
node-releases: 2.0.27
update-browserslist-db: 1.2.2(browserslist@4.28.1)
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
builtins@5.1.0:
dependencies:
semver: 7.7.3
@ -5517,6 +5646,8 @@ snapshots:
chalk@5.6.2: {}
chownr@1.1.4: {}
cjs-module-lexer@1.4.3: {}
cliui@8.0.1:
@ -5606,6 +5737,12 @@ snapshots:
dependencies:
ms: 2.1.3
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
deep-extend@0.6.0: {}
deep-is@0.1.4: {}
deepmerge@4.3.1: {}
@ -5763,6 +5900,8 @@ snapshots:
strip-final-newline: 4.0.0
yoctocolors: 2.1.2
expand-template@2.0.3: {}
express@4.22.1:
dependencies:
accepts: 1.3.8
@ -5865,6 +6004,8 @@ snapshots:
dependencies:
is-unicode-supported: 2.1.0
file-uri-to-path@1.0.0: {}
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@ -5920,6 +6061,8 @@ snapshots:
fresh@2.0.0: {}
fs-constants@1.0.0: {}
fs-extra@11.3.2:
dependencies:
graceful-fs: 4.2.11
@ -5984,6 +6127,8 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
github-from-package@0.0.0: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@ -6055,6 +6200,8 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
ieee754@1.2.1: {}
import-in-the-middle@1.15.0:
dependencies:
acorn: 8.15.0
@ -6064,6 +6211,8 @@ snapshots:
inherits@2.0.4: {}
ini@1.3.8: {}
ip-regex@4.3.0: {}
ipaddr.js@1.9.1: {}
@ -6271,8 +6420,12 @@ snapshots:
mime@1.6.0: {}
mimic-response@3.1.0: {}
minimist@1.2.8: {}
mkdirp-classic@0.5.3: {}
mlly@1.8.0:
dependencies:
acorn: 8.15.0
@ -6300,10 +6453,16 @@ snapshots:
nanoid@3.3.11: {}
napi-build-utils@2.0.0: {}
negotiator@0.6.3: {}
negotiator@1.0.0: {}
node-abi@3.85.0:
dependencies:
semver: 7.7.3
node-domexception@1.0.0: {}
node-fetch@2.7.0:
@ -6491,6 +6650,21 @@ snapshots:
transitivePeerDependencies:
- debug
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.1.2
expand-template: 2.0.3
github-from-package: 0.0.0
minimist: 1.2.8
mkdirp-classic: 0.5.3
napi-build-utils: 2.0.0
node-abi: 3.85.0
pump: 3.0.3
rc: 1.2.8
simple-get: 4.0.1
tar-fs: 2.1.4
tunnel-agent: 0.6.0
prettier@3.7.4: {}
pretty-ms@9.3.0:
@ -6556,10 +6730,23 @@ snapshots:
iconv-lite: 0.7.0
unpipe: 1.0.0
rc@1.2.8:
dependencies:
deep-extend: 0.6.0
ini: 1.3.8
minimist: 1.2.8
strip-json-comments: 2.0.1
react-refresh@0.18.0: {}
react@19.2.1: {}
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
real-require@0.2.0: {}
redis@5.10.0:
@ -6794,6 +6981,14 @@ snapshots:
signal-exit@4.1.0: {}
simple-concat@1.0.1: {}
simple-get@4.0.1:
dependencies:
decompress-response: 6.0.0
once: 1.4.0
simple-concat: 1.0.1
sisteransi@1.0.5: {}
sonic-boom@4.2.0:
@ -6816,12 +7011,18 @@ snapshots:
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-final-newline@4.0.0: {}
strip-json-comments@2.0.1: {}
strip-json-comments@5.0.3: {}
supports-color@7.2.0:
@ -6842,6 +7043,21 @@ snapshots:
react: 19.2.1
use-sync-external-store: 1.6.0(react@19.2.1)
tar-fs@2.1.4:
dependencies:
chownr: 1.1.4
mkdirp-classic: 0.5.3
pump: 3.0.3
tar-stream: 2.2.0
tar-stream@2.2.0:
dependencies:
bl: 4.1.0
end-of-stream: 1.4.5
fs-constants: 1.0.0
inherits: 2.0.4
readable-stream: 3.6.2
tcp-port-used@1.0.2:
dependencies:
debug: 4.3.1
@ -6877,6 +7093,10 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
tunnel-agent@0.6.0:
dependencies:
safe-buffer: 5.2.1
type-is@1.6.18:
dependencies:
media-typer: 0.3.0
@ -6921,6 +7141,8 @@ snapshots:
dependencies:
react: 19.2.1
util-deprecate@1.0.2: {}
utils-merge@1.0.1: {}
uuid@11.1.0: {}

2
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,2 @@
ignoredBuiltDependencies:
- better-sqlite3

123
src/database/index.ts Normal file
View File

@ -0,0 +1,123 @@
import Database from 'better-sqlite3';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import fs from 'node:fs';
import type { DatabaseConfig } from './types.js';
let db: Database.Database | null = null;
/**
*
* @param config
* @returns Database
*/
export function getDB(config?: DatabaseConfig): Database.Database {
if (db) {
return db;
}
// 确定数据库路径
const dbPath = resolveDatabasePath(config?.path);
// 确保目录存在
ensureDirectoryExists(dbPath);
// 创建数据库实例
db = new Database(dbPath, {
readonly: config?.readonly ?? false,
fileMustExist: config?.fileMustExist ?? false,
timeout: config?.timeout ?? 5000,
});
// 应用性能优化 PRAGMA
applyPragmas(db);
return db;
}
/**
*
* 使 data/app.db
*/
function resolveDatabasePath(customPath?: string): string {
if (customPath) {
return path.isAbsolute(customPath)
? customPath
: path.resolve(process.cwd(), customPath);
}
// 默认路径:项目根目录下的 data/app.db
const projectRoot = findProjectRoot();
return path.join(projectRoot, 'data', 'app.db');
}
/**
*
*/
function findProjectRoot(): string {
// 尝试从 import.meta.url 获取模块路径
const modulePath = typeof import.meta !== 'undefined'
? import.meta.url
: undefined;
if (modulePath && modulePath.startsWith('file://')) {
const filePath = fileURLToPath(modulePath);
const dir = path.dirname(filePath);
// src/database -> 项目根目录
return path.resolve(dir, '../..');
}
return process.cwd();
}
/**
*
*/
function ensureDirectoryExists(dbPath: string): void {
const dir = path.dirname(dbPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* PRAGMA
*/
function applyPragmas(database: Database.Database): void {
// WAL 模式:提升并发写入性能
database.pragma('journal_mode = WAL');
// 同步级别:平衡安全性与性能
database.pragma('synchronous = NORMAL');
// 缓存大小:约 64MB负数表示 KiB
database.pragma('cache_size = -64000');
// 临时表存储方式:内存
database.pragma('temp_store = MEMORY');
// 内存映射大小256MB根据机器内存调整
// 注意:这可能需要根据实际环境调整
// database.pragma('mmap_size = 268435456');
}
/**
*
* 退
*/
export function closeDB(): void {
if (db) {
db.close();
db = null;
}
}
/**
*
*
*/
export function getRawDB(): Database.Database | null {
return db;
}
export { Database };

135
src/database/migrate.ts Normal file
View File

@ -0,0 +1,135 @@
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { getDB } from './index.js';
import type { MigrationResult, MigrationFile } from './types.js';
const MIGRATIONS_DIR = 'migrations';
/**
*
* @param migrationsDir src/database
* @returns
*/
export function runMigrations(migrationsDir: string = MIGRATIONS_DIR): MigrationResult {
const db = getDB();
const result: MigrationResult = {
applied: [],
skipped: [],
};
// 1. 创建 migrations 表(如果不存在)
db.exec(`
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
run_at TEXT NOT NULL
)
`);
// 2. 获取已执行的迁移列表
const appliedMigrations = db.prepare<MigrationFile[], any>(
'SELECT name FROM migrations'
).all();
const appliedSet = new Set(appliedMigrations.map((m) => m.name));
// 3. 扫描迁移目录
const migrationFiles = getMigrationFiles(migrationsDir);
// 4. 按文件名排序后执行
for (const file of migrationFiles) {
if (appliedSet.has(file.name)) {
result.skipped.push(file.name);
continue;
}
try {
executeMigration(db, file);
result.applied.push(file.name);
} catch (error) {
throw new Error(`Migration "${file.name}" failed: ${(error as Error).message}`);
}
}
return result;
}
/**
*
*/
function executeMigration(db: any, file: MigrationFile): void {
const runMigration = db.transaction(() => {
// 执行迁移 SQL
db.exec(file.content);
// 记录迁移已执行
db.prepare(
'INSERT INTO migrations (name, run_at) VALUES (?, datetime("now"))'
).run(file.name);
});
runMigration();
}
/**
*
*/
function getMigrationFiles(migrationsDir: string): MigrationFile[] {
const dir = resolveMigrationsDir(migrationsDir);
if (!fs.existsSync(dir)) {
console.warn(`Migrations directory not found: ${dir}`);
return [];
}
const files = fs.readdirSync(dir).filter(
(file) => file.endsWith('.sql') && /^\d+_/.test(file)
);
// 按文件名排序,确保按序号执行
files.sort();
return files.map((name) => ({
name,
content: fs.readFileSync(path.join(dir, name), 'utf8'),
}));
}
/**
*
*/
function resolveMigrationsDir(migrationsDir: string): string {
// 如果是绝对路径,直接使用
if (path.isAbsolute(migrationsDir)) {
return migrationsDir;
}
// 从当前模块位置解析
if (typeof import.meta !== 'undefined' && import.meta.url) {
const modulePath = fileURLToPath(import.meta.url);
const moduleDir = path.dirname(modulePath);
return path.resolve(moduleDir, migrationsDir);
}
// 兜底:相对于工作目录
return path.resolve(process.cwd(), 'src/database', migrationsDir);
}
/**
*
*/
export function getMigrationStatus(): { applied: string[]; pending: string[] } {
const db = getDB();
const applied = db.prepare<MigrationFile[], any>(
'SELECT name FROM migrations ORDER BY name'
).all().map((m) => m.name);
const migrationFiles = getMigrationFiles(MIGRATIONS_DIR);
const pending = migrationFiles
.filter((f) => !applied.includes(f.name))
.map((f) => f.name);
return { applied, pending };
}

View File

@ -0,0 +1,33 @@
-- 初始化迁移
-- 创建基础表结构
-- 示例表:用户表
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
deleted_at TEXT DEFAULT NULL
);
-- 示例表:系统配置表
CREATE TABLE IF NOT EXISTS system_config (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL,
description TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
-- 插入初始配置
INSERT OR IGNORE INTO system_config (key, value, description) VALUES
('app_name', 'BattleMonAgent', '应用名称'),
('version', '1.0.0', '应用版本'),
('db_version', '001', '数据库版本');

View File

@ -0,0 +1,36 @@
export const initSql = `
--
--
--
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
deleted_at TEXT DEFAULT NULL
);
--
CREATE TABLE IF NOT EXISTS system_config (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL,
description TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
--
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
--
INSERT OR IGNORE INTO system_config (key, value, description) VALUES
('app_name', 'BattleMonAgent', '应用名称'),
('version', '1.0.0', '应用版本'),
('db_version', '001', '数据库版本');
`

44
src/database/types.ts Normal file
View File

@ -0,0 +1,44 @@
import type Database from 'better-sqlite3';
/**
*
*/
export interface DatabaseConfig {
/** 数据库文件路径 */
path?: string;
/** 是否只读模式 */
readonly?: boolean;
/** 文件不存在时是否报错 */
fileMustExist?: boolean;
/** 等待锁超时时间 (ms) */
timeout?: number;
/** 是否打印 SQL 日志 */
verbose?: boolean;
}
/**
*
*/
export interface MigrationRecord {
id: number;
name: string;
run_at: string;
}
/**
*
*/
export interface MigrationFile {
/** 迁移文件名,如 '001_init.sql' */
name: string;
/** 迁移 SQL 内容 */
content: string;
}
/**
*
*/
export interface MigrationResult {
applied: string[];
skipped: string[];
}

View File

@ -6,10 +6,36 @@ import { mastra } from '../mastra';
import { agentRouter } from './routes/agent-routes';
import { responseFormatter } from './middleware/response-formatter';
import { errorHandler, notFoundHandler } from './middleware/error-handler';
import { getDB, closeDB } from '../database';
import { initSql } from '../database/migrations/init';
const app = express();
const PORT = process.env.PORT || 7328;
// 测试数据库连接
function testDatabaseConnection(): boolean {
try {
const db = getDB({
path: 'data/app.db',
readonly: false,
fileMustExist: false,
timeout: 5000,
});
// 执行简单查询验证连接
const result = db.prepare('SELECT * FROM system_config').all();
console.log('Database result:', result);
console.log('✅ Database connection established successfully');
return true;
} catch (error) {
console.error('❌ Database connection failed:', error instanceof Error ? error.message : error);
return false;
}
}
// Initialize database connection
const dbConnected = testDatabaseConnection();
// Security middleware
app.use(helmet());
@ -69,12 +95,14 @@ const server = app.listen(PORT, () => {
console.log(`🚀 Express server listening on port ${PORT}`);
console.log(`📡 Health check: http://localhost:${PORT}/health`);
console.log(`🤖 Agent API: http://localhost:${PORT}/api/agent`);
console.log(`🗄️ Database: ${dbConnected ? 'Connected' : 'Failed'}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
server.close(() => {
closeDB();
console.log('Server closed');
process.exit(0);
});
@ -83,6 +111,7 @@ process.on('SIGTERM', () => {
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully');
server.close(() => {
closeDB();
console.log('Server closed');
process.exit(0);
});