@mindbase/express-common 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mindbase.ts +52 -0
- package/commands/precache.ts +54 -0
- package/core/app.ts +200 -0
- package/core/module/CreateModule.ts +38 -0
- package/core/module/FindPackageRoot.ts +58 -0
- package/core/module/GetModulePath.ts +58 -0
- package/core/state.ts +72 -0
- package/feature/cron/CronManager.ts +63 -0
- package/feature/scanner/FileScanner.ts +288 -0
- package/index.ts +10 -0
- package/ipipfree.ipdb +0 -0
- package/middleware/Cors.ts +17 -0
- package/middleware/IpParser.ts +81 -0
- package/middleware/UaParser.ts +50 -0
- package/package.json +41 -0
- package/routes/Doc.route.ts +118 -0
- package/tests/Cors.test.ts +34 -0
- package/tests/Dayjs.test.ts +24 -0
- package/tests/FileScanner.test.ts +85 -0
- package/tests/GetModulePath.test.ts +32 -0
- package/tests/IpParser.test.ts +72 -0
- package/tests/Logger.test.ts +68 -0
- package/tests/UaParser.test.ts +41 -0
- package/tsconfig.json +9 -0
- package/types/DocTypes.ts +111 -0
- package/types/index.ts +19 -0
- package/utils/ComponentRegistry.ts +34 -0
- package/utils/DatabaseMigration.ts +121 -0
- package/utils/Dayjs.ts +16 -0
- package/utils/DocManager.ts +274 -0
- package/utils/HttpServer.ts +41 -0
- package/utils/InitDatabase.ts +149 -0
- package/utils/InitErrorHandler.ts +71 -0
- package/utils/InitExpress.ts +35 -0
- package/utils/Logger.ts +206 -0
- package/utils/MiddlewareRegistry.ts +14 -0
- package/utils/ProjectInitializer.ts +283 -0
- package/utils/RouteParser.ts +408 -0
- package/utils/RouteRegistry.ts +66 -0
- package/utils/SchemaMigrate.ts +73 -0
- package/utils/SchemaSync.ts +47 -0
- package/utils/TSTypeParser.ts +455 -0
- package/utils/Validate.ts +25 -0
- package/utils/ZodSchemaParser.ts +420 -0
- package/vitest.config.ts +18 -0
- package/zod/Doc.schema.ts +9 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { Database as DatabaseType } from "better-sqlite3";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import { RouteInfo, ModuleItem, DocDatabaseConfig } from "../types/DocTypes";
|
|
6
|
+
import logger from "./Logger";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 文档管理器
|
|
10
|
+
* 管理文档数据,与独立的 SQLite 数据库交互
|
|
11
|
+
*/
|
|
12
|
+
export class DocManager {
|
|
13
|
+
private db: DatabaseType | null = null;
|
|
14
|
+
private config: DocDatabaseConfig;
|
|
15
|
+
|
|
16
|
+
constructor(config: DocDatabaseConfig) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.init();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 初始化数据库
|
|
23
|
+
*/
|
|
24
|
+
public init(): void {
|
|
25
|
+
try {
|
|
26
|
+
// 确保数据库目录存在
|
|
27
|
+
const dbDir = path.dirname(this.config.path);
|
|
28
|
+
if (!fs.existsSync(dbDir)) {
|
|
29
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 连接数据库
|
|
33
|
+
this.db = new Database(this.config.path);
|
|
34
|
+
|
|
35
|
+
// 创建表结构
|
|
36
|
+
this.createTables();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
logger.error("文档数据库初始化失败:", error);
|
|
39
|
+
this.db = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 创建表结构
|
|
45
|
+
*/
|
|
46
|
+
private createTables(): void {
|
|
47
|
+
if (!this.db) return;
|
|
48
|
+
|
|
49
|
+
// 创建 doc_routes 表
|
|
50
|
+
this.db.exec(`
|
|
51
|
+
CREATE TABLE IF NOT EXISTS doc_routes (
|
|
52
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
53
|
+
module TEXT NOT NULL,
|
|
54
|
+
method TEXT NOT NULL,
|
|
55
|
+
path TEXT NOT NULL,
|
|
56
|
+
full_path TEXT NOT NULL,
|
|
57
|
+
summary TEXT,
|
|
58
|
+
description TEXT,
|
|
59
|
+
request_schema TEXT,
|
|
60
|
+
middlewares TEXT,
|
|
61
|
+
file_path TEXT NOT NULL,
|
|
62
|
+
created_at INTEGER NOT NULL,
|
|
63
|
+
updated_at INTEGER NOT NULL,
|
|
64
|
+
UNIQUE(module, method, path)
|
|
65
|
+
);
|
|
66
|
+
`);
|
|
67
|
+
|
|
68
|
+
// 创建索引
|
|
69
|
+
this.db.exec(`
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_doc_routes_module ON doc_routes(module);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_doc_routes_full_path ON doc_routes(full_path);
|
|
72
|
+
`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 保存路由信息
|
|
77
|
+
* @param routeInfo 路由信息
|
|
78
|
+
* @param apiPrefix API 前缀
|
|
79
|
+
* @param moduleName 模块名称
|
|
80
|
+
*/
|
|
81
|
+
public saveRoute(routeInfo: RouteInfo, apiPrefix: string = "/api", moduleName: string): void {
|
|
82
|
+
if (!this.db) return;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// 计算完整路径
|
|
86
|
+
const fullPath = `${apiPrefix}/${moduleName}${routeInfo.path}`.replace(/\/+/g, "/");
|
|
87
|
+
|
|
88
|
+
// 准备数据
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const data = {
|
|
91
|
+
module: moduleName,
|
|
92
|
+
method: routeInfo.method,
|
|
93
|
+
path: routeInfo.path,
|
|
94
|
+
full_path: fullPath,
|
|
95
|
+
summary: routeInfo.summary,
|
|
96
|
+
description: routeInfo.description,
|
|
97
|
+
request_schema: routeInfo.requestSchema ? JSON.stringify(routeInfo.requestSchema) : null,
|
|
98
|
+
middlewares: routeInfo.middlewares ? JSON.stringify(routeInfo.middlewares) : null,
|
|
99
|
+
file_path: routeInfo.filePath,
|
|
100
|
+
created_at: now,
|
|
101
|
+
updated_at: now,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// 插入或更新数据
|
|
105
|
+
const stmt = this.db.prepare(`
|
|
106
|
+
INSERT INTO doc_routes (
|
|
107
|
+
module, method, path, full_path, summary, description,
|
|
108
|
+
request_schema, middlewares, file_path, created_at, updated_at
|
|
109
|
+
) VALUES (
|
|
110
|
+
@module, @method, @path, @full_path, @summary, @description,
|
|
111
|
+
@request_schema, @middlewares, @file_path, @created_at, @updated_at
|
|
112
|
+
) ON CONFLICT(module, method, path) DO UPDATE SET
|
|
113
|
+
full_path = @full_path,
|
|
114
|
+
summary = @summary,
|
|
115
|
+
description = @description,
|
|
116
|
+
request_schema = @request_schema,
|
|
117
|
+
middlewares = @middlewares,
|
|
118
|
+
file_path = @file_path,
|
|
119
|
+
updated_at = @updated_at
|
|
120
|
+
`);
|
|
121
|
+
|
|
122
|
+
stmt.run(data);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
logger.error("保存路由信息失败:", error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 获取模块列表
|
|
130
|
+
* @returns 模块列表
|
|
131
|
+
*/
|
|
132
|
+
public getModules(): ModuleItem[] {
|
|
133
|
+
if (!this.db) return [];
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const stmt = this.db.prepare(`
|
|
137
|
+
SELECT module, COUNT(*) as count
|
|
138
|
+
FROM doc_routes
|
|
139
|
+
GROUP BY module
|
|
140
|
+
ORDER BY module
|
|
141
|
+
`);
|
|
142
|
+
|
|
143
|
+
return stmt.all() as ModuleItem[];
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logger.error("获取模块列表失败:", error);
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 获取路由列表
|
|
152
|
+
* @param module 模块名称(可选)
|
|
153
|
+
* @returns 路由列表
|
|
154
|
+
*/
|
|
155
|
+
public getRoutes(module?: string): RouteInfo[] {
|
|
156
|
+
if (!this.db) return [];
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
let query = `
|
|
160
|
+
SELECT id, module, method, path, full_path, summary, description,
|
|
161
|
+
request_schema, middlewares, file_path, created_at, updated_at
|
|
162
|
+
FROM doc_routes
|
|
163
|
+
`;
|
|
164
|
+
|
|
165
|
+
const params: any = {};
|
|
166
|
+
|
|
167
|
+
if (module) {
|
|
168
|
+
query += " WHERE module = @module";
|
|
169
|
+
params.module = module;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
query += " ORDER BY module, method, path";
|
|
173
|
+
|
|
174
|
+
const stmt = this.db.prepare(query);
|
|
175
|
+
const rows = stmt.all(params) as any[];
|
|
176
|
+
|
|
177
|
+
// 转换数据格式
|
|
178
|
+
return rows.map((row) => ({
|
|
179
|
+
id: row.id,
|
|
180
|
+
module: row.module,
|
|
181
|
+
method: row.method,
|
|
182
|
+
path: row.path,
|
|
183
|
+
fullPath: row.full_path,
|
|
184
|
+
summary: row.summary,
|
|
185
|
+
description: row.description,
|
|
186
|
+
requestSchema: row.request_schema ? JSON.parse(row.request_schema) : undefined,
|
|
187
|
+
middlewares: row.middlewares ? JSON.parse(row.middlewares) : undefined,
|
|
188
|
+
filePath: row.file_path,
|
|
189
|
+
createdAt: row.created_at,
|
|
190
|
+
updatedAt: row.updated_at,
|
|
191
|
+
}));
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger.error("获取路由列表失败:", error);
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 获取路由详情
|
|
200
|
+
* @param id 路由 ID
|
|
201
|
+
* @returns 路由详情
|
|
202
|
+
*/
|
|
203
|
+
public getRouteById(id: number): RouteInfo | null {
|
|
204
|
+
if (!this.db) return null;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const stmt = this.db.prepare(`
|
|
208
|
+
SELECT id, module, method, path, full_path, summary, description,
|
|
209
|
+
request_schema, middlewares, file_path, created_at, updated_at
|
|
210
|
+
FROM doc_routes
|
|
211
|
+
WHERE id = @id
|
|
212
|
+
`);
|
|
213
|
+
|
|
214
|
+
const row = stmt.get({ id }) as any;
|
|
215
|
+
|
|
216
|
+
if (!row) return null;
|
|
217
|
+
|
|
218
|
+
// 转换数据格式
|
|
219
|
+
return {
|
|
220
|
+
id: row.id,
|
|
221
|
+
module: row.module,
|
|
222
|
+
method: row.method,
|
|
223
|
+
path: row.path,
|
|
224
|
+
fullPath: row.full_path,
|
|
225
|
+
summary: row.summary,
|
|
226
|
+
description: row.description,
|
|
227
|
+
requestSchema: row.request_schema ? JSON.parse(row.request_schema) : undefined,
|
|
228
|
+
middlewares: row.middlewares ? JSON.parse(row.middlewares) : undefined,
|
|
229
|
+
filePath: row.file_path,
|
|
230
|
+
createdAt: row.created_at,
|
|
231
|
+
updatedAt: row.updated_at,
|
|
232
|
+
};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
logger.error("获取路由详情失败:", error);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 清空所有路由数据
|
|
241
|
+
*/
|
|
242
|
+
public clearAllRoutes(): void {
|
|
243
|
+
if (!this.db) return;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
this.db.exec("DELETE FROM doc_routes");
|
|
247
|
+
} catch (error) {
|
|
248
|
+
logger.error("清空路由数据失败:", error);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 关闭数据库连接
|
|
254
|
+
*/
|
|
255
|
+
public close(): void {
|
|
256
|
+
if (this.db) {
|
|
257
|
+
this.db.close();
|
|
258
|
+
this.db = null;
|
|
259
|
+
logger.info("文档数据库连接已关闭");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 导出单例实例
|
|
265
|
+
export let docManager: DocManager;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 初始化文档管理器
|
|
269
|
+
* @param config 配置
|
|
270
|
+
*/
|
|
271
|
+
export function initDocManager(config: DocDatabaseConfig): DocManager {
|
|
272
|
+
docManager = new DocManager(config);
|
|
273
|
+
return docManager;
|
|
274
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Express } from "express";
|
|
2
|
+
import logger from "./Logger";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 启动 HTTP 服务
|
|
6
|
+
* @param app Express 实例
|
|
7
|
+
* @param port 监听端口
|
|
8
|
+
* @returns Promise<void>
|
|
9
|
+
* @throws 如果服务器启动失败
|
|
10
|
+
*/
|
|
11
|
+
export async function startServer(app: Express, port: number): Promise<void> {
|
|
12
|
+
if (!app) {
|
|
13
|
+
throw new Error("Express 实例不能为空");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!port || typeof port !== "number" || port <= 0 || port > 65535) {
|
|
17
|
+
throw new Error(`无效的端口号: ${port}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return new Promise<void>((resolve, reject) => {
|
|
21
|
+
try {
|
|
22
|
+
const server = app.listen(port, () => {
|
|
23
|
+
resolve();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// 处理服务器错误
|
|
27
|
+
server.on("error", (error) => {
|
|
28
|
+
logger.error("服务器启动失败:", error);
|
|
29
|
+
reject(new Error(`服务器启动失败:${error instanceof Error ? error.message : String(error)}`));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 处理服务器关闭
|
|
33
|
+
server.on("close", () => {
|
|
34
|
+
logger.info("服务器已关闭");
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
logger.error("服务器启动异常:", error);
|
|
38
|
+
reject(new Error(`服务器启动异常:${error instanceof Error ? error.message : String(error)}`));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
4
|
+
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import { AppState } from "../core/state";
|
|
7
|
+
import { ScanResult } from "../types/Index";
|
|
8
|
+
import logger from "./Logger";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 封装细节:从扫描结果中提取 Schema 并初始化数据库
|
|
12
|
+
* @param state 应用状态
|
|
13
|
+
* @param scannedResults 扫描结果
|
|
14
|
+
* @returns 数据库实例
|
|
15
|
+
* @throws 如果数据库设置失败
|
|
16
|
+
*/
|
|
17
|
+
export function setupDatabase(state: AppState, scannedResults: ScanResult[]) {
|
|
18
|
+
try {
|
|
19
|
+
if (!state) {
|
|
20
|
+
throw new Error("应用状态对象不能为空");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!Array.isArray(scannedResults)) {
|
|
24
|
+
throw new Error("扫描结果必须是一个数组");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const schemas: Record<string, any> = {};
|
|
28
|
+
for (const item of scannedResults) {
|
|
29
|
+
if (item.type === "schema" && item.allExports) {
|
|
30
|
+
try {
|
|
31
|
+
Object.assign(schemas, item.allExports);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
logger.warn(`处理 Schema 文件失败: ${item.filePath}`, e);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
state.schemas = schemas;
|
|
38
|
+
|
|
39
|
+
logger.startup("扫描", `提取到 ${Object.keys(schemas).length} 个 Schema 定义`);
|
|
40
|
+
|
|
41
|
+
return initDatabase({
|
|
42
|
+
schemas,
|
|
43
|
+
state,
|
|
44
|
+
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.error("数据库设置失败:", error);
|
|
47
|
+
throw new Error(`数据库设置失败:${error instanceof Error ? error.message : String(error)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 初始化 SQLite 数据库
|
|
53
|
+
* @param options 配置选项
|
|
54
|
+
* @returns 数据库实例
|
|
55
|
+
* @throws 如果数据库初始化失败
|
|
56
|
+
*/
|
|
57
|
+
export function initDatabase(options: { schemas: Record<string, any>; dbPath?: string; state?: AppState }): any {
|
|
58
|
+
if (!options) {
|
|
59
|
+
throw new Error("初始化选项不能为空");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const currentDir = process.cwd();
|
|
64
|
+
const dbPath = options.dbPath || options.state?.options?.database?.path || "./data/app.db";
|
|
65
|
+
const absoluteDbPath = path.resolve(currentDir, dbPath);
|
|
66
|
+
const dbDir = path.dirname(absoluteDbPath);
|
|
67
|
+
|
|
68
|
+
logger.startup("数据库", `初始化:路径=${absoluteDbPath}`);
|
|
69
|
+
|
|
70
|
+
// 确保目录存在
|
|
71
|
+
try {
|
|
72
|
+
if (!fs.existsSync(dbDir)) {
|
|
73
|
+
logger.startup("数据库", `创建目录:${dbDir}`);
|
|
74
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
throw new Error(`创建数据库目录失败:${e instanceof Error ? e.message : String(e)}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 初始化 SQLite 数据库
|
|
81
|
+
let sqlite: Database.Database;
|
|
82
|
+
try {
|
|
83
|
+
sqlite = new Database(absoluteDbPath);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
throw new Error(`连接数据库失败:${e instanceof Error ? e.message : String(e)}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 推荐的 SQLite 性能设置
|
|
89
|
+
try {
|
|
90
|
+
sqlite.pragma("journal_mode = WAL");
|
|
91
|
+
sqlite.pragma("synchronous = NORMAL");
|
|
92
|
+
} catch (e) {
|
|
93
|
+
logger.warn("设置 SQLite 性能参数失败:", e);
|
|
94
|
+
// 继续执行,不中断初始化
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 初始化 Drizzle
|
|
98
|
+
let db: any;
|
|
99
|
+
try {
|
|
100
|
+
db = drizzle(sqlite, { schema: options.schemas || {} });
|
|
101
|
+
} catch (e) {
|
|
102
|
+
// 关闭 SQLite 连接
|
|
103
|
+
try {
|
|
104
|
+
sqlite.close();
|
|
105
|
+
} catch (closeError) {
|
|
106
|
+
logger.warn("关闭数据库连接失败:", closeError);
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`初始化 Drizzle ORM 失败:${e instanceof Error ? e.message : String(e)}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 如果提供了状态对象,更新它
|
|
112
|
+
if (options.state) {
|
|
113
|
+
options.state.db = db;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return db;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logger.error("数据库初始化失败:", error);
|
|
119
|
+
throw new Error(`数据库初始化失败:${error instanceof Error ? error.message : String(error)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 处理数据库表结构迁移
|
|
125
|
+
* @param db 数据库实例
|
|
126
|
+
* @param schemas 数据库 schema 定义
|
|
127
|
+
* @throws 如果迁移失败
|
|
128
|
+
*/
|
|
129
|
+
export async function handleDatabaseMigration(db: any, schemas: Record<string, any>) {
|
|
130
|
+
try {
|
|
131
|
+
if (!db) {
|
|
132
|
+
throw new Error("数据库实例不能为空");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!schemas) {
|
|
136
|
+
throw new Error("数据库 schema 定义不能为空");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 只提醒,不自动执行
|
|
140
|
+
logger.info("🔔 数据库表结构检查完成");
|
|
141
|
+
logger.info(" 如需同步数据库结构,请执行:");
|
|
142
|
+
logger.info(" npx drizzle-kit push");
|
|
143
|
+
|
|
144
|
+
logger.debug("数据库迁移提醒已显示");
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logger.error("数据库迁移提醒失败:", error);
|
|
147
|
+
throw new Error(`数据库迁移提醒失败:${error instanceof Error ? error.message : String(error)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Request, Response, NextFunction, Express } from "express";
|
|
2
|
+
import { ZodError } from "zod";
|
|
3
|
+
import logger from "./Logger";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 检测是否为 SQL 相关错误(表/字段不存在)
|
|
7
|
+
*/
|
|
8
|
+
function isSQLError(err: any): boolean {
|
|
9
|
+
if (!err) return false;
|
|
10
|
+
const msg = err.message || err.toString() || "";
|
|
11
|
+
return /no such table|table.*does not exist|column.*does not exist|database schema has changed/i.test(msg);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 封装细节:初始化错误处理和 404 中间件
|
|
16
|
+
*/
|
|
17
|
+
export function setupErrorHandlers(app: Express, logging?: boolean) {
|
|
18
|
+
// 404 处理
|
|
19
|
+
app.use((req: Request, res: Response) => {
|
|
20
|
+
res.status(404).json({
|
|
21
|
+
code: 404,
|
|
22
|
+
data: null,
|
|
23
|
+
msg: "接口不存在",
|
|
24
|
+
error: "Not Found",
|
|
25
|
+
fields: {},
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// 错误处理
|
|
30
|
+
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
|
31
|
+
// 处理 Zod 校验错误
|
|
32
|
+
if (err instanceof ZodError) {
|
|
33
|
+
const fields: Record<string, string> = {};
|
|
34
|
+
err.errors.forEach((e) => {
|
|
35
|
+
const path = e.path.join(".");
|
|
36
|
+
fields[path] = e.message;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return res.status(400).json({
|
|
40
|
+
code: 400,
|
|
41
|
+
data: null,
|
|
42
|
+
msg: "参数校验失败",
|
|
43
|
+
error: "ZodValidationError",
|
|
44
|
+
fields,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 处理 SQL 错误(给出友好提示)
|
|
49
|
+
if (isSQLError(err)) {
|
|
50
|
+
logger.error("");
|
|
51
|
+
logger.error("❌ 数据库结构可能已变化,请运行以下命令同步:");
|
|
52
|
+
logger.error("");
|
|
53
|
+
logger.error(" npm run db:migrate");
|
|
54
|
+
logger.error("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 处理普通错误
|
|
58
|
+
const statusCode = err.status || err.statusCode || 500;
|
|
59
|
+
if (!isSQLError(err)) {
|
|
60
|
+
logger.error("系统错误:", err);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
res.status(statusCode).json({
|
|
64
|
+
code: statusCode,
|
|
65
|
+
data: null,
|
|
66
|
+
msg: err.message || "服务器内部错误",
|
|
67
|
+
error: logging ? err.stack : "InternalServerError",
|
|
68
|
+
fields: {},
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import express, { Express } from "express";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import cookieParser from "cookie-parser";
|
|
4
|
+
import cors from "../middleware/Cors";
|
|
5
|
+
import ipParser from "../middleware/IpParser";
|
|
6
|
+
import uaParser from "../middleware/UaParser";
|
|
7
|
+
import { LightUpAppOptions } from "../types/Index";
|
|
8
|
+
import logger from "./Logger";
|
|
9
|
+
|
|
10
|
+
export default function initExpress(app: Express, options: LightUpAppOptions): void {
|
|
11
|
+
app.set("config", options);
|
|
12
|
+
app.disable("x-powered-by");
|
|
13
|
+
app.use(express.urlencoded({ extended: false, limit: "10mb" }));
|
|
14
|
+
app.use(express.json({ limit: "10mb" }));
|
|
15
|
+
app.use(cookieParser());
|
|
16
|
+
if (options.staticPath) {
|
|
17
|
+
if (!fs.existsSync(options.staticPath)) {
|
|
18
|
+
fs.mkdirSync(options.staticPath, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
app.use(express.static(options.staticPath));
|
|
21
|
+
logger.startup("设置", `静态目录: ${options.staticPath}`);
|
|
22
|
+
}
|
|
23
|
+
if (options.cors) {
|
|
24
|
+
app.use(cors);
|
|
25
|
+
logger.startup("设置", "跨域请求头");
|
|
26
|
+
}
|
|
27
|
+
if (options.ip) {
|
|
28
|
+
logger.startup("设置", "IP地址解析");
|
|
29
|
+
app.use(ipParser);
|
|
30
|
+
}
|
|
31
|
+
if (options.userAgent) {
|
|
32
|
+
logger.startup("设置", "用户代理解析");
|
|
33
|
+
app.use(uaParser);
|
|
34
|
+
}
|
|
35
|
+
}
|