gorig-cli 1.0.7 → 1.0.9

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.
@@ -0,0 +1,682 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import ejs from 'ejs';
5
+ import { spawn } from 'child_process'; // 从 exec 改为 spawn
6
+ import { fileURLToPath } from 'url';
7
+ import readline from 'readline';
8
+ import semver from 'semver'; // 用于处理版本号
9
+ import { Command } from 'commander'; // 正确导入 Command
10
+ import yaml from 'js-yaml'; // 导入 js-yaml 用于解析 YAML
11
+
12
+ // 获取当前文件的目录
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ /**
17
+ * 格式化日期为 "YYYY-MM-DD HH:MM:SS"
18
+ * @param {Date} date - 要格式化的日期对象
19
+ * @returns {string} - 格式化后的日期字符串
20
+ */
21
+ const formatDate = (date) => {
22
+ const pad = (n) => n < 10 ? '0' + n : n;
23
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
24
+ `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
25
+ };
26
+
27
+ /**
28
+ * 提取路径参数,例如 "/user/get/:id" -> ["id"]
29
+ */
30
+ const extractPathParams = (pathStr) => {
31
+ const regex = /:([\w]+)/g;
32
+ const params = [];
33
+ let match;
34
+ while ((match = regex.exec(pathStr)) !== null) {
35
+ params.push(match[1]);
36
+ }
37
+ return params;
38
+ };
39
+
40
+ /**
41
+ * 解析 router.go 获取路由信息
42
+ */
43
+ const scanRouterFile = async (routerFilePath) => {
44
+ try {
45
+ const routerContent = await fs.readFile(routerFilePath, 'utf-8');
46
+ const routeRegex = /(\w+)\.(GET|POST|PUT|DELETE)\("([^"]+)",\s*(\w+)\)/g;
47
+ const routes = [];
48
+ let match;
49
+ while ((match = routeRegex.exec(routerContent)) !== null) {
50
+ const method = match[2];
51
+ const path = match[3];
52
+ const handler = match[4];
53
+ const pathParams = extractPathParams(path);
54
+ routes.push({
55
+ method,
56
+ path,
57
+ handler,
58
+ pathParams
59
+ });
60
+ }
61
+ return routes;
62
+ } catch (error) {
63
+ console.error(chalk.red(`Failed to read router file: ${routerFilePath}`), error);
64
+ return [];
65
+ }
66
+ };
67
+
68
+ /**
69
+ * 解析结构体文件,提取带有 `form` 或 `json` 标签的字段
70
+ * 支持递归解析嵌套的结构体
71
+ * @param {string} structFilePath - 结构体文件路径
72
+ * @param {string} structName - 结构体名称
73
+ * @param {string} modelDir - 模型文件目录
74
+ * @returns {Array} - 字段数组,包含 name, type
75
+ */
76
+ const parseStructFields = async (structFilePath, structName, modelDir) => {
77
+ try {
78
+ const structContent = await fs.readFile(structFilePath, 'utf-8');
79
+ // 修正正则表达式,确保能够匹配结构体内容
80
+ const structRegex = new RegExp(`type\\s+${structName}\\s+struct\\s*{([\\s\\S]*?)}\\s*`, 'm');
81
+ const match = structRegex.exec(structContent);
82
+ if (!match) {
83
+ console.warn(chalk.yellow(`Struct ${structName} definition not found`));
84
+ return [];
85
+ }
86
+ const fieldsContent = match[1];
87
+ const fields = [];
88
+ const fieldRegex = /\s*(\w+)\s+([\w\.\*\[\]]+)\s+`[^`]*?(?:json|form):"([^"]+)"[^`]*`/g;
89
+ let fieldMatch;
90
+ while ((fieldMatch = fieldRegex.exec(fieldsContent)) !== null) {
91
+ const fieldName = fieldMatch[1];
92
+ const fieldType = fieldMatch[2];
93
+ const tagName = fieldMatch[3];
94
+ fields.push({
95
+ name: tagName,
96
+ type: fieldType
97
+ });
98
+
99
+ // 检查是否为嵌入的结构体,例如 model.UserD 或 req.UserReq
100
+ if (fieldType.startsWith('model.') || fieldType.startsWith('req.')) {
101
+ const embeddedStructName = fieldType.split('.')[1].replace('*', '');
102
+ let embeddedStructFile = '';
103
+
104
+ if (fieldType.startsWith('model.')) {
105
+ embeddedStructFile = path.join(modelDir, `${embeddedStructName.toLowerCase()}.go`);
106
+ } else if (fieldType.startsWith('req.')) {
107
+ embeddedStructFile = structFilePath; // 如果是 req 包,使用同一个文件
108
+ }
109
+
110
+ // 解析嵌入的结构体
111
+ if (await fs.pathExists(embeddedStructFile)) {
112
+ const embeddedFields = await parseStructFields(embeddedStructFile, embeddedStructName, modelDir);
113
+ fields.push(...embeddedFields);
114
+ } else {
115
+ console.warn(chalk.yellow(`Embedded struct file not found: ${embeddedStructFile}`));
116
+ }
117
+ }
118
+ }
119
+ return fields;
120
+ } catch (error) {
121
+ console.error(chalk.red(`Failed to parse struct file: ${structFilePath}`), error);
122
+ return [];
123
+ }
124
+ };
125
+
126
+ /**
127
+ * 解析 controller.go 获取方法中的参数
128
+ */
129
+ const extractParamsFromController = async (controllerContent, reqDir, modelDir) => {
130
+ const functionParams = {};
131
+ // 正则表达式匹配处理函数
132
+ const functionRegex = /func\s+(\w+)\s*\(ctx\s+\*gin\.Context\)\s*{([\s\S]*?)^}/gm;
133
+ let funcMatch;
134
+ while ((funcMatch = functionRegex.exec(controllerContent)) !== null) {
135
+ const functionName = funcMatch[1];
136
+ const functionBody = funcMatch[2];
137
+
138
+ const params = [];
139
+ let usesGetPageReq = false;
140
+
141
+ // 建立变量名到类型的映射
142
+ const varTypeMap = {};
143
+ const varRegex = /(?:var\s+)?(\w+)\s*(?:[:=])=\s*(?:&)?([\w\.]+)\s*(?:\{\})?/g;
144
+ let varMatch;
145
+ while ((varMatch = varRegex.exec(functionBody)) !== null) {
146
+ const varName = varMatch[1];
147
+ const varType = varMatch[2];
148
+ varTypeMap[varName] = varType;
149
+ }
150
+
151
+ // 检查是否有 apix.GetPageReq 调用
152
+ if (/apix\.GetPageReq\(/.test(functionBody)) {
153
+ usesGetPageReq = true;
154
+ }
155
+
156
+ // 正则表达式匹配 apix.GetParamX 调用
157
+ const paramRegex = /(\w+),\s*e\s*:=\s*apix\.GetParam(\w+)\s*\(\s*ctx\s*,\s*"([^"]+)"(?:\s*,\s*(apix\.\w+|true|false))?\s*\)/g;
158
+ let paramMatch;
159
+ while ((paramMatch = paramRegex.exec(functionBody)) !== null) {
160
+ const paramName = paramMatch[1]; // 变量名,例如: id
161
+ const paramTypeFunc = paramMatch[2]; // 类型函数,例如: Int64, Str
162
+ const paramKey = paramMatch[3]; // 参数键,例如: "id"
163
+ const paramForce = paramMatch[4]; // 可选的强制参数,例如: apix.Force, true
164
+
165
+ // 确定是否必填
166
+ let required = false;
167
+ if (paramForce) {
168
+ if (paramForce === 'apix.Force' || paramForce === 'true') {
169
+ required = true;
170
+ }
171
+ }
172
+
173
+ // 映射 Go 类型到 OpenAPI 类型
174
+ let schemaType = 'string';
175
+ switch (paramTypeFunc.toLowerCase()) {
176
+ case 'int':
177
+ case 'int32':
178
+ case 'int64':
179
+ schemaType = 'integer';
180
+ break;
181
+ case 'bool':
182
+ schemaType = 'boolean';
183
+ break;
184
+ case 'float32':
185
+ case 'float64':
186
+ schemaType = 'number';
187
+ break;
188
+ default:
189
+ schemaType = 'string';
190
+ }
191
+
192
+ const paramObject = {
193
+ name: paramKey,
194
+ in: 'query', // 默认设置为 query
195
+ required: required,
196
+ schema: { type: schemaType }
197
+ };
198
+
199
+ params.push(paramObject);
200
+ }
201
+
202
+ // 处理通过 apix.BindParams 绑定的参数
203
+ const bindParamsRegex = /e\s*:=\s*apix\.BindParams\s*\(\s*ctx\s*,\s*&?(\w+)\s*\)/g;
204
+ let bindMatch;
205
+ while ((bindMatch = bindParamsRegex.exec(functionBody)) !== null) {
206
+ const varName = bindMatch[1];
207
+ const fullType = varTypeMap[varName] || varName; // 获取变量的类型,若未声明则使用变量名
208
+
209
+ let bindStructName = fullType;
210
+
211
+ // 如果类型包含包名,例如 req.UserReq
212
+ if (fullType.includes('.')) {
213
+ bindStructName = fullType.split('.')[1];
214
+ }
215
+
216
+ params.push({
217
+ bindStructName: bindStructName
218
+ });
219
+ }
220
+
221
+ // 将是否使用分页的标记添加到参数中
222
+ if (usesGetPageReq) {
223
+ params.push({ usesGetPageReq: true });
224
+ }
225
+
226
+ functionParams[functionName] = params;
227
+ }
228
+ return functionParams;
229
+ };
230
+
231
+ /**
232
+ * 解析模型定义
233
+ */
234
+ const extractModelFields = async (modelFilePath) => {
235
+ try {
236
+ const modelContent = await fs.readFile(modelFilePath, 'utf-8');
237
+ const fields = [];
238
+ const modelRegex = /type\s+(\w+)\s+struct\s*{([\s\S]*?)^}/gm;
239
+ let match;
240
+ while ((match = modelRegex.exec(modelContent)) !== null) {
241
+ const modelName = match[1];
242
+ const modelFields = [];
243
+ const fieldsContent = match[2];
244
+ const fieldRegex = /\s*(\w+)\s+([\w\.\*\[\]]+)\s+`[^`]*?(?:json|form):"([^"]+)"[^`]*`/g;
245
+ let fieldMatch;
246
+ while ((fieldMatch = fieldRegex.exec(fieldsContent)) !== null) {
247
+ const fieldName = fieldMatch[1];
248
+ const fieldType = fieldMatch[2];
249
+ const tagName = fieldMatch[3];
250
+ modelFields.push({
251
+ field: fieldName,
252
+ type: fieldType,
253
+ json: tagName
254
+ });
255
+ }
256
+ fields.push({ modelName, modelFields });
257
+ }
258
+ return fields;
259
+ } catch (error) {
260
+ console.error(chalk.red(`Failed to parse model file: ${modelFilePath}`), error);
261
+ return [];
262
+ }
263
+ };
264
+
265
+ /**
266
+ * 获取版本号并递增
267
+ */
268
+ const getNextVersion = (existingVersion) => {
269
+ if (!existingVersion) return "1.0.0"; // 默认版本号
270
+
271
+ const nextVersion = semver.inc(existingVersion, 'patch'); // 按照补丁版本递增
272
+ return nextVersion || "1.0.0";
273
+ };
274
+
275
+ /**
276
+ * 生成或更新 redoc.html
277
+ * @param {string} docDir - 目标项目的 doc 目录路径
278
+ * @param {string} templatePath - redoc_template.html 的路径
279
+ */
280
+ const generateRedocHtml = async (docDir, templatePath) => {
281
+ try {
282
+ // 读取模板文件
283
+ const redocTemplate = await fs.readFile(templatePath, 'utf-8');
284
+
285
+ // 获取 doc 文件夹下所有 JSON 文件(不包�� redoc.html)
286
+ const jsonFiles = await fs.readdir(docDir);
287
+ const openApiJsonFiles = jsonFiles.filter(file => file.endsWith('.json'));
288
+
289
+ // 生成 API_OPTIONS
290
+ const apiOptions = openApiJsonFiles.map(file => {
291
+ const apiName = path.basename(file, '.json');
292
+ return `<option value="./${file}">${apiName}</option>`;
293
+ }).join('\n');
294
+
295
+ // 替换模板中的 {{API_OPTIONS}} 占位符
296
+ const redocHtmlContent = redocTemplate.replace('{{API_OPTIONS}}', apiOptions);
297
+
298
+ // 写入 redoc.html 到 doc 文件夹
299
+ const redocHtmlPath = path.join(docDir, 'redoc.html');
300
+ await fs.writeFile(redocHtmlPath, redocHtmlContent, 'utf-8');
301
+ console.log(chalk.green(`ReDoc page generated or updated: doc/redoc.html`));
302
+ } catch (error) {
303
+ console.error(chalk.red('Error generating ReDoc page:'), chalk.redBright(error.message));
304
+ }
305
+ };
306
+
307
+ /**
308
+ * 启动 http-server 以预览文档
309
+ * 仅显示 'Available on:' 和 'Hit CTRL-C to stop the server' 部分
310
+ * 将控制台提示改为英文
311
+ * @param {string} docDir - 目标项目的 doc 目录路径
312
+ */
313
+ const startHttpServer = (docDir) => { // 添加 docDir 参数
314
+ const server = spawn('http-server', ['-p', '8080', docDir], { shell: true });
315
+
316
+ // 监听 stdout 数据
317
+ server.stdout.on('data', (data) => {
318
+ const output = data.toString();
319
+ const lines = output.split('\n');
320
+ lines.forEach(line => {
321
+ const trimmedLine = line.trim();
322
+ if (trimmedLine === 'Available on:' || trimmedLine === 'Hit CTRL-C to stop the server') {
323
+ console.log(line);
324
+ return;
325
+ }
326
+ if (trimmedLine.startsWith('http://') || trimmedLine.startsWith('https://')) {
327
+ console.log(` ${trimmedLine}/redoc.html`); // 指向 redoc.html
328
+ }
329
+ });
330
+ });
331
+
332
+ // 监听 stderr 数据并过滤掉特定的 Deprecation Warning
333
+ server.stderr.on('data', (data) => {
334
+ const errorOutput = data.toString();
335
+ if (!errorOutput.includes('DEP0066')) {
336
+ console.error(chalk.red('http-server error:'), chalk.redBright(errorOutput));
337
+ }
338
+ });
339
+
340
+ // 监听错误事件
341
+ server.on('error', (err) => {
342
+ if (err.code === 'EADDRINUSE') {
343
+ console.error(chalk.red(`Port 8080 is already in use. Please use another port.`));
344
+ } else {
345
+ console.error(chalk.red('Error starting http-server:'), chalk.redBright(err.message));
346
+ }
347
+ });
348
+
349
+ // 监听关闭事件
350
+ server.on('close', (code) => {
351
+ if (code !== 0) {
352
+ console.error(chalk.red(`http-server process exited with code ${code}`));
353
+ }
354
+ });
355
+ };
356
+
357
+ /**
358
+ * 生成 OpenAPI 文档
359
+ * @param {string|null} moduleName - 指定的模块名
360
+ */
361
+ const generateOpenAPIDocs = async (moduleName = null) => {
362
+ try {
363
+ const cliDir = path.resolve(__dirname, '..'); // 指向 CLI 项目的根目录
364
+ const templatesDir = path.join(cliDir, 'templates');
365
+ const redocTemplatePath = path.join(templatesDir, 'redoc_template.html');
366
+
367
+ // 确保模板文件存在
368
+ if (!(await fs.pathExists(redocTemplatePath))) {
369
+ console.error(chalk.red(`Template file missing: ${redocTemplatePath}`));
370
+ return;
371
+ }
372
+
373
+ const domainDir = path.join(process.cwd(), 'domain');
374
+ const docDir = path.join(process.cwd(), 'doc');
375
+
376
+ // 确保 doc 目录存在
377
+ await fs.ensureDir(docDir);
378
+
379
+ // 读取并解析 _bin/local.yaml 获取 API 服务器地址
380
+ let serverUrl = 'http://localhost:8080'; // 默认值
381
+ const binDir = path.join(process.cwd(), '_bin');
382
+ const localYamlPath = path.join(binDir, 'local.yaml');
383
+
384
+ if (await fs.pathExists(localYamlPath)) {
385
+ try {
386
+ const yamlContent = await fs.readFile(localYamlPath, 'utf-8');
387
+ const config = yaml.load(yamlContent);
388
+ const addr = config?.api?.rest?.addr;
389
+ if (addr) {
390
+ // addr 例如:9527
391
+ let host = 'localhost';
392
+ let port = '8080';
393
+ const addrMatch = addr.match(/(.*):(\d+)/);
394
+ if (addrMatch) {
395
+ if (addrMatch[1]) {
396
+ host = addrMatch[1];
397
+ }
398
+ port = addrMatch[2];
399
+ }
400
+ serverUrl = `http://${host}:${port}`;
401
+ }
402
+ } catch (error) {
403
+ console.error(chalk.red(`Failed to parse YAML file: ${localYamlPath}`), error);
404
+ console.warn(chalk.yellow('Using default server URL: http://localhost:8080'));
405
+ }
406
+ } else {
407
+ console.warn(chalk.yellow(`YAML configuration file not found: ${localYamlPath}`));
408
+ console.warn(chalk.yellow('Using default server URL: http://localhost:8080'));
409
+ }
410
+
411
+ // 扫描所有模块
412
+ const moduleDirs = moduleName ? [moduleName] : await fs.readdir(domainDir);
413
+
414
+ // 过滤出目录
415
+ const modulesToProcess = [];
416
+ for (const module of moduleDirs) {
417
+ const modulePath = path.join(domainDir, module);
418
+ if (await fs.pathExists(modulePath) && (await fs.lstat(modulePath)).isDirectory()) {
419
+ modulesToProcess.push(module);
420
+ }
421
+ }
422
+
423
+ for (const module of modulesToProcess) {
424
+ const modulePath = path.join(domainDir, module);
425
+ const routerFilePath = path.join(modulePath, 'api/router.go');
426
+ const controllerFilePath = path.join(modulePath, 'api/controller.go');
427
+ const modelFilePath = path.join(modulePath, 'model', `${module}.go`);
428
+ const reqFilePath = path.join(modulePath, 'api', 'req', 'req.go');
429
+
430
+ // 检查文件是否存在
431
+ if (!(await fs.pathExists(routerFilePath)) || !(await fs.pathExists(controllerFilePath)) || !(await fs.pathExists(modelFilePath)) || !(await fs.pathExists(reqFilePath))) {
432
+ console.warn(chalk.yellow(`Module "${module}" is missing required files, skipping...`));
433
+ continue;
434
+ }
435
+
436
+ // 扫描 router.go 获取路由信息
437
+ const routes = await scanRouterFile(routerFilePath);
438
+ // console.log(`Scanned routes for module "${module}":`, routes);
439
+
440
+ // 扫描 controller.go 获取方法参数
441
+ const controllerContent = await fs.readFile(controllerFilePath, 'utf-8');
442
+ const controllerParams = await extractParamsFromController(controllerContent, path.dirname(reqFilePath), path.dirname(modelFilePath));
443
+ // console.log(`Extracted controller parameters for module "${module}":`, controllerParams);
444
+
445
+ // 扫描 model.go 和 req.go 获取字段
446
+ const modelFields = await extractModelFields(modelFilePath);
447
+ const reqFields = await extractModelFields(reqFilePath);
448
+ const allFields = [...modelFields, ...reqFields];
449
+
450
+ // 生成 OpenAPI 格式文档
451
+ const openAPIDoc = {
452
+ openapi: "3.0.0",
453
+ info: {
454
+ title: `${module} API`,
455
+ version: "1.0.0", // 默认版本号,稍后会递增
456
+ description: `${module} api doc, generated by gorig-cli
457
+ \n last updated: ${formatDate(new Date())}`, // 添加描述信息
458
+ },
459
+ servers: [
460
+ {
461
+ url: serverUrl, // 使用从 YAML 获取的 serverUrl
462
+ description: "API Server"
463
+ }
464
+ ],
465
+ paths: {},
466
+ components: {
467
+ schemas: {},
468
+ },
469
+ };
470
+
471
+ // 检查是否存在现有文档文件,读取并递增版本号
472
+ const existingDocPath = path.join(docDir, `${module}.json`);
473
+ if (await fs.pathExists(existingDocPath)) {
474
+ const existingDoc = await fs.readJson(existingDocPath);
475
+ openAPIDoc.info.version = getNextVersion(existingDoc.info.version);
476
+ // 复用 schemas
477
+ if (existingDoc.components && existingDoc.components.schemas) {
478
+ openAPIDoc.components.schemas = existingDoc.components.schemas;
479
+ }
480
+ }
481
+
482
+ // 添加所有结构体定义到 components.schemas
483
+ allFields.forEach(model => {
484
+ const properties = {};
485
+ model.modelFields.forEach(field => {
486
+ let fieldType = 'string'; // 默认类型
487
+ const lowerType = field.type.toLowerCase();
488
+ if (lowerType.includes('int')) {
489
+ fieldType = 'integer';
490
+ } else if (lowerType.includes('bool')) {
491
+ fieldType = 'boolean';
492
+ } else if (lowerType.includes('float')) {
493
+ fieldType = 'number';
494
+ } else if (lowerType.startsWith('[]')) {
495
+ fieldType = 'array';
496
+ }
497
+
498
+ if (fieldType === 'array') {
499
+ // 假设数组元素类型为 string,实际应根据具体类型调整
500
+ properties[field.json] = {
501
+ type: 'array',
502
+ items: { type: 'string' }
503
+ };
504
+ } else {
505
+ properties[field.json] = { type: fieldType };
506
+ }
507
+ });
508
+ openAPIDoc.components.schemas[model.modelName] = {
509
+ type: 'object',
510
+ properties,
511
+ };
512
+ });
513
+
514
+ // 手动合并嵌套结构体字段到 UserReq
515
+ if (openAPIDoc.components.schemas['UserReq'] && openAPIDoc.components.schemas['UserD']) {
516
+ const userReqProperties = openAPIDoc.components.schemas['UserReq'].properties;
517
+ const userDProperties = openAPIDoc.components.schemas['UserD'].properties;
518
+
519
+ // 合并 UserD 的字段到 UserReq
520
+ openAPIDoc.components.schemas['UserReq'].properties = {
521
+ ...userReqProperties,
522
+ ...userDProperties
523
+ };
524
+
525
+ // 如果不需要独立定义 UserD,可以删除它
526
+ // delete openAPIDoc.components.schemas['UserD'];
527
+ }
528
+
529
+ // 替换 'filter' schema reference with 'UserReq'
530
+ for (const [handler, params] of Object.entries(controllerParams)) {
531
+ for (const param of params) {
532
+ if (param.bindStructName === 'filter') {
533
+ param.bindStructName = 'UserReq';
534
+ }
535
+ }
536
+ }
537
+
538
+ // 生成完整路径并修复嵌套结构问题
539
+ for (const route of routes) {
540
+ const basePath = `/${module}`; // 从 groupRouter.Group("user") 获取
541
+ const fullPath = `${basePath}/${route.path}`.replace(/\/+/g, '/'); // 确保路径格式正确
542
+ const pathParamsList = route.pathParams; // 从路由中获取 path 参数列表
543
+
544
+ // 获取对应处理函数的参数
545
+ const funcParams = controllerParams[route.handler] || [];
546
+
547
+ let parameters = [];
548
+ let requestBody = undefined;
549
+ let usesGetPageReq = false;
550
+
551
+ // 分离参数和 requestBody
552
+ for (const param of funcParams) {
553
+ if (param.usesGetPageReq) {
554
+ usesGetPageReq = true;
555
+ } else if (param.bindStructName) {
556
+ // 处理绑定的结构体,作为 requestBody 或 query 参数
557
+ if (['POST', 'PUT'].includes(route.method.toUpperCase())) {
558
+ // POST 和 PUT 请求使用 requestBody
559
+ requestBody = {
560
+ required: true,
561
+ content: {
562
+ 'application/json': {
563
+ schema: {
564
+ $ref: `#/components/schemas/${param.bindStructName}`,
565
+ },
566
+ },
567
+ },
568
+ };
569
+ } else if (['GET', 'DELETE'].includes(route.method.toUpperCase())) {
570
+ // GET 和 DELETE 请求使用 query 参数
571
+ const schema = openAPIDoc.components.schemas[param.bindStructName];
572
+ if (schema && schema.properties) {
573
+ for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) {
574
+ parameters.push({
575
+ name: fieldName,
576
+ in: 'query',
577
+ required: false, // 根据实际情况调整
578
+ schema: fieldSchema
579
+ });
580
+ }
581
+ } else {
582
+ console.warn(chalk.yellow(`Function "${route.handler}" has undefined bindStructName "${param.bindStructName}"`));
583
+ }
584
+ }
585
+ } else {
586
+ // 确定参数位置
587
+ if (pathParamsList.includes(param.name)) {
588
+ param.in = 'path';
589
+ } else {
590
+ param.in = 'query';
591
+ }
592
+ parameters.push(param);
593
+ }
594
+ }
595
+
596
+ // 如果函数中使用了 apix.GetPageReq,则添加分页参数
597
+ if (usesGetPageReq) {
598
+ parameters = parameters.concat([
599
+ { name: "page", in: "query", required: false, schema: { type: "integer" } },
600
+ { name: "size", in: "query", required: false, schema: { type: "integer" } },
601
+ { name: "lastID", in: "query", required: false, schema: { type: "integer" } },
602
+ ]);
603
+ }
604
+
605
+ // 去重参数
606
+ const uniqueParameters = [];
607
+ const paramNames = new Set();
608
+ parameters.forEach(param => {
609
+ if (!paramNames.has(param.name)) {
610
+ paramNames.add(param.name);
611
+ uniqueParameters.push(param);
612
+ }
613
+ });
614
+
615
+ // 定义响应
616
+ const responses = {
617
+ '200': {
618
+ description: 'Success',
619
+ content: {
620
+ 'application/json': {
621
+ schema: {
622
+ type: 'object',
623
+ properties: {} // 可以根据需要进一步完善响应体
624
+ },
625
+ },
626
+ },
627
+ },
628
+ };
629
+
630
+ // 如果有 requestBody,添加到 operation
631
+ const operation = {
632
+ summary: route.handler,
633
+ parameters: uniqueParameters.length > 0 ? uniqueParameters : undefined,
634
+ responses,
635
+ };
636
+
637
+ if (requestBody) {
638
+ operation.requestBody = requestBody;
639
+ }
640
+
641
+ // 初始化路径对象
642
+ if (!openAPIDoc.paths[fullPath]) {
643
+ openAPIDoc.paths[fullPath] = {};
644
+ }
645
+
646
+ openAPIDoc.paths[fullPath][route.method.toLowerCase()] = operation;
647
+ }
648
+
649
+ // 写入 OpenAPI JSON 文件
650
+ await fs.writeJson(existingDocPath, openAPIDoc, { spaces: 2 });
651
+ console.log(chalk.green(`OpenAPI documentation generated: doc/${path.basename(existingDocPath)}`));
652
+ }
653
+
654
+ // 生成或更新 redoc.html 一次
655
+ await generateRedocHtml(docDir, redocTemplatePath);
656
+
657
+ // 启动 http-server 以预览文档
658
+ startHttpServer(docDir);
659
+ } catch (error) {
660
+ console.error(chalk.red('Error generating API documentation:'), chalk.redBright(error.message));
661
+ }
662
+ };
663
+
664
+ /**
665
+ * 处理命令行输入
666
+ */
667
+ const docCommand = async () => {
668
+ const program = new Command();
669
+
670
+ program
671
+ .command('doc')
672
+ .description('Generate API documentation')
673
+ .argument('[moduleName]', 'Module name. If specified, only generate documentation for that module.')
674
+ .action(async (moduleName) => {
675
+ await generateOpenAPIDocs(moduleName);
676
+ });
677
+
678
+ program.parse(process.argv);
679
+ };
680
+
681
+ // 默认导出一个函数
682
+ export default docCommand;