File size: 10,962 Bytes
ae4ceef
 
 
 
 
 
 
 
 
 
716de21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae4ceef
 
716de21
 
 
ae4ceef
716de21
ae4ceef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
 
716de21
ae4ceef
 
 
716de21
ae4ceef
 
 
716de21
ae4ceef
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
 
 
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
 
 
 
 
 
 
 
 
 
716de21
ae4ceef
 
716de21
ae4ceef
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
 
 
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
 
 
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
 
 
 
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
 
 
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
 
 
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
 
 
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
716de21
ae4ceef
 
 
716de21
ae4ceef
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716de21
ae4ceef
 
 
 
 
 
 
 
 
 
716de21
ae4ceef
 
716de21
ae4ceef
 
 
 
 
716de21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
import path from 'path';
import { fileURLToPath } from 'url';
import bcrypt from 'bcryptjs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// 数据库文件存储在 /app/data 或项目根目录
const dataDir = process.env.DATA_DIR || path.resolve(process.cwd(), 'data');
const dbPath = path.resolve(dataDir, 'platform.db');

let db: any = null;

/**
 * 延迟加载数据库,防止 native 模块加载失败导致进程直接退出
 */
const getDB = async () => {
  if (db) return db;
  
  try {
    console.log(`[Database] 正在连接 SQLite: ${dbPath}`);
    // 使用动态导入,以便捕获加载原生模块时的错误
    const { default: Database } = await import('better-sqlite3');
    db = new Database(dbPath);
    return db;
  } catch (err: any) {
    console.error('[Database] SQLite 加载失败:', err.message);
    if (err.message.includes('bindings')) {
      console.error('[Database] 提示: 可能是 better-sqlite3 的原生模块未正确编译。');
    }
    throw err;
  }
};

// 初始化表结构
const initDB = async () => {
  const database = await getDB();
  
  // 1. 用户表 (增加配额与等级)
  database.exec(`
    CREATE TABLE IF NOT EXISTS users (
      id TEXT PRIMARY KEY,
      email TEXT UNIQUE NOT NULL,
      password TEXT NOT NULL,
      name TEXT,
      avatar TEXT, -- 用户头像 URL
      role TEXT DEFAULT 'user', -- 'user', 'admin'
      plan TEXT DEFAULT 'free', -- 'free', 'pro', 'enterprise'
      quota_remaining INTEGER DEFAULT 500, -- 每月剩余 AI 消息额度
      two_factor_enabled BOOLEAN DEFAULT 0, -- 是否开启双重认证
      login_alert_enabled BOOLEAN DEFAULT 1, -- 是否开启登录提醒
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);

  // 检查并增加新列 (针对已存在的数据库)
  try {
    const columns = database.prepare("PRAGMA table_info(users)").all();
    const hasAvatar = columns.some((c: any) => c.name === 'avatar');
    const has2FA = columns.some((c: any) => c.name === 'two_factor_enabled');
    const hasAlert = columns.some((c: any) => c.name === 'login_alert_enabled');
    
    if (!hasAvatar) {
      database.exec("ALTER TABLE users ADD COLUMN avatar TEXT");
      console.log('[Database] users 表已增加 avatar 列');
    }
    if (!has2FA) {
      database.exec("ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN DEFAULT 0");
      console.log('[Database] users 表已增加 two_factor_enabled 列');
    }
    if (!hasAlert) {
      database.exec("ALTER TABLE users ADD COLUMN login_alert_enabled BOOLEAN DEFAULT 1");
      console.log('[Database] users 表已增加 login_alert_enabled 列');
    }
  } catch (err) {
    console.error('[Database] 更新 users 表结构失败:', err);
  }

  // 2. 产品/计划表 (增加库存管理)
  database.exec(`
    CREATE TABLE IF NOT EXISTS products (
      id TEXT PRIMARY KEY,
      name TEXT NOT NULL,
      price INTEGER NOT NULL, -- 以分为单位
      type TEXT NOT NULL, -- 'subscription', 'one-time'
      stock INTEGER DEFAULT -1, -- -1 表示无限, 0+ 表示限购数量
      description TEXT,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);

  // 3. 订单表 (支持支付闭环)
  database.exec(`
    CREATE TABLE IF NOT EXISTS orders (
      id TEXT PRIMARY KEY,
      user_id TEXT NOT NULL,
      product_id TEXT NOT NULL,
      amount INTEGER NOT NULL,
      status TEXT DEFAULT 'pending', -- 'pending', 'paid', 'cancelled', 'refunded'
      payment_method TEXT, -- 'alipay', 'stripe', 'wechat'
      payment_id TEXT, -- 外部支付流水号
      idempotency_key TEXT UNIQUE, -- 幂等键
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users (id),
      FOREIGN KEY (product_id) REFERENCES products (id)
    )
  `);

  // 4. 聊天会话表 (增加知识库关联)
  database.exec(`
    CREATE TABLE IF NOT EXISTS chat_sessions (
      id TEXT PRIMARY KEY,
      user_id TEXT NOT NULL,
      title TEXT DEFAULT '新对话',
      knowledge_base_id TEXT, -- 关联的知识库 ID
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users (id),
      FOREIGN KEY (knowledge_base_id) REFERENCES knowledge_bases (id)
    )
  `);

  // 检查并增加 knowledge_base_id 列 (针对已存在的数据库)
  try {
    const columns = database.prepare("PRAGMA table_info(chat_sessions)").all();
    const hasKbId = columns.some((c: any) => c.name === 'knowledge_base_id');
    if (!hasKbId) {
      database.exec("ALTER TABLE chat_sessions ADD COLUMN knowledge_base_id TEXT");
      console.log('[Database] chat_sessions 表已增加 knowledge_base_id 列');
    }
  } catch (err) {
    console.error('[Database] 更新 chat_sessions 表结构失败:', err);
  }

  // 3. 聊天消息表 (增加向量存储)
  database.exec(`
    CREATE TABLE IF NOT EXISTS chat_messages (
      id TEXT PRIMARY KEY,
      session_id TEXT NOT NULL,
      role TEXT NOT NULL, -- 'user' or 'assistant'
      content TEXT NOT NULL,
      embedding TEXT, -- 存储向量 JSON 字符串
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (session_id) REFERENCES chat_sessions (id)
    )
  `);

  // 5. 审计日志表 (System Engineering: Observability)
  database.exec(`
    CREATE TABLE IF NOT EXISTS audit_logs (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id TEXT,
      action TEXT NOT NULL, -- 'LOGIN', 'CHAT', 'ORDER_CREATE', 'PAYMENT_COMPLETE'
      status TEXT, -- 'SUCCESS', 'FAILED', 'CIRCUIT_OPEN'
      payload TEXT, -- JSON data
      ip TEXT,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);

  // 6. 系统通知表 (Business: Message)
  database.exec(`
    CREATE TABLE IF NOT EXISTS notifications (
      id TEXT PRIMARY KEY,
      user_id TEXT NOT NULL,
      title TEXT NOT NULL,
      content TEXT NOT NULL,
      type TEXT DEFAULT 'info', -- 'info', 'success', 'warning', 'error'
      is_read BOOLEAN DEFAULT 0,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users (id)
    )
  `);

  // 7. 工作流表
  database.exec(`
    CREATE TABLE IF NOT EXISTS workflows (
      id TEXT PRIMARY KEY,
      user_id TEXT NOT NULL,
      name TEXT NOT NULL,
      data TEXT NOT NULL, -- JSON string of nodes and edges
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users (id)
    )
  `);

  // 8. 知识库表 (System: RAG)
  database.exec(`
    CREATE TABLE IF NOT EXISTS knowledge_bases (
      id TEXT PRIMARY KEY,
      user_id TEXT NOT NULL,
      name TEXT NOT NULL,
      description TEXT,
      status TEXT DEFAULT 'processing', -- 'processing', 'ready'
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users (id)
    )
  `);

  // 9. 知识切片表 (System: Vector Store)
  database.exec(`
    CREATE TABLE IF NOT EXISTS knowledge_chunks (
      id TEXT PRIMARY KEY,
      kb_id TEXT NOT NULL,
      content TEXT NOT NULL,
      embedding TEXT, -- JSON string of vector
      metadata TEXT, -- JSON string of extra info
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (kb_id) REFERENCES knowledge_bases (id)
    )
  `);

  // 10. API Keys 表 (System: OpenAPI)
  database.exec(`
    CREATE TABLE IF NOT EXISTS api_keys (
      id TEXT PRIMARY KEY,
      user_id TEXT NOT NULL,
      key_name TEXT NOT NULL,
      key_secret TEXT UNIQUE NOT NULL,
      status TEXT DEFAULT 'active', -- 'active', 'inactive'
      scopes TEXT DEFAULT '["all"]', -- JSON array of scopes
      last_used DATETIME,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users (id)
    )
  `);

  // 检查并增加 status 和 scopes 列 (针对已存在的数据库)
  try {
    const columns = database.prepare("PRAGMA table_info(api_keys)").all();
    const hasStatus = columns.some((c: any) => c.name === 'status');
    const hasScopes = columns.some((c: any) => c.name === 'scopes');
    
    if (!hasStatus) {
      database.exec("ALTER TABLE api_keys ADD COLUMN status TEXT DEFAULT 'active'");
      console.log('[Database] api_keys 表已增加 status 列');
    }
    if (!hasScopes) {
      database.exec("ALTER TABLE api_keys ADD COLUMN scopes TEXT DEFAULT '[\"all\"]'");
      console.log('[Database] api_keys 表已增加 scopes 列');
    }
  } catch (err) {
    console.error('[Database] 更新 api_keys 表结构失败:', err);
  }

  // 11. Hugging Face Projects Cache
  database.exec(`
    CREATE TABLE IF NOT EXISTS hf_projects (
      id TEXT PRIMARY KEY,
      full_name TEXT NOT NULL,
      name TEXT NOT NULL,
      title TEXT,
      description TEXT,
      url TEXT,
      iframe_url TEXT,
      type TEXT,
      created_at_hf DATETIME,
      likes INTEGER,
      sdk TEXT,
      synced_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);

  console.log('[Database] SQLite 已就绪:', dbPath);

  // 初始化基础产品数据
  const products = [
    { id: 'plan_pro', name: '专业版', price: 9900, type: 'subscription', stock: -1, description: '无限 AI 消息 + 高级工作流' },
    { id: 'plan_ent', name: '企业版', price: 29900, type: 'subscription', stock: -1, description: '全方位技术支持 + 专属域名' },
    { id: 'limited_offer', name: '限时秒杀 (Pro 体验卡)', price: 100, type: 'one-time', stock: 10, description: '限量 10 份,仅需 1 元' }
  ];

  const insertProduct = database.prepare(`
    INSERT OR IGNORE INTO products (id, name, price, type, stock, description)
    VALUES (?, ?, ?, ?, ?, ?)
  `);

  products.forEach(p => {
    insertProduct.run(p.id, p.name, p.price, p.type, p.stock, p.description);
  });

  // 初始化默认管理员 (如果不存在)
  const adminEmail = 'admin@codex.ai';
  const existingAdmin = database.prepare('SELECT id FROM users WHERE email = ?').get(adminEmail);
  if (!existingAdmin) {
    const hashedPassword = bcrypt.hashSync('admin123', 10);
    database.prepare('INSERT INTO users (id, email, password, name, role, plan) VALUES (?, ?, ?, ?, ?, ?)')
      .run('ADMIN_ROOT', adminEmail, hashedPassword, 'System Admin', 'admin', 'enterprise');
    console.log('[Database] 默认管理员已创建: admin@codex.ai / admin123');
  }
};

/**
 * 数据库代理对象,保持与现有代码的兼容性
 * 在调用 await initDB() 之后,可以直接使用 import db from './lib/db.js'
 */
const dbProxy = new Proxy({} as any, {
  get(target, prop) {
    if (!db) {
      throw new Error('[Database] 数据库未初始化。请确保在应用启动时先调用了 await initDB()');
    }
    const value = db[prop];
    if (typeof value === 'function') {
      return value.bind(db);
    }
    return value;
  }
});

export { getDB, initDB };
export default dbProxy;