419 lines
10 KiB
JavaScript
419 lines
10 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* API 文档自动生成脚本
|
|||
|
|
* 使用 OpenAPI 从后端 Swagger 自动生成前端文档
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
const axios = require('axios');
|
|||
|
|
|
|||
|
|
// 配置
|
|||
|
|
const config = {
|
|||
|
|
// 后端服务地址
|
|||
|
|
backendUrl: process.env.BACKEND_URL || 'http://localhost:3000',
|
|||
|
|
|
|||
|
|
// 文档输出目录
|
|||
|
|
docsDir: path.join(__dirname, '../src/wwjcloud/openapi/api'),
|
|||
|
|
|
|||
|
|
// API 分组配置
|
|||
|
|
apiGroups: {
|
|||
|
|
api: {
|
|||
|
|
name: '前台 API',
|
|||
|
|
swaggerPath: '/api/api-json',
|
|||
|
|
outputDir: 'frontend',
|
|||
|
|
description: '前台用户访问的 API 接口',
|
|||
|
|
prefix: '/api'
|
|||
|
|
},
|
|||
|
|
adminapi: {
|
|||
|
|
name: '后台 API',
|
|||
|
|
swaggerPath: '/api/adminapi-json',
|
|||
|
|
outputDir: 'adminapi',
|
|||
|
|
description: '后台管理员访问的 API 接口',
|
|||
|
|
prefix: '/adminapi'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取 Swagger JSON
|
|||
|
|
*/
|
|||
|
|
async function getSwaggerJson(group) {
|
|||
|
|
try {
|
|||
|
|
const url = `${config.backendUrl}${group.swaggerPath}`;
|
|||
|
|
console.log(`正在获取 ${group.name} API 文档: ${url}`);
|
|||
|
|
|
|||
|
|
const response = await axios.get(url);
|
|||
|
|
return response.data;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`获取 ${group.name} API 文档失败:`, error.message);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生成 API 概览文档
|
|||
|
|
*/
|
|||
|
|
function generateOverview(group) {
|
|||
|
|
return `---
|
|||
|
|
title: ${group.name}
|
|||
|
|
description: ${group.description}
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# ${group.name}
|
|||
|
|
|
|||
|
|
::: info ${group.name} 说明
|
|||
|
|
|
|||
|
|
${group.description}
|
|||
|
|
|
|||
|
|
:::
|
|||
|
|
|
|||
|
|
## API 基础信息
|
|||
|
|
|
|||
|
|
- **基础路径**: \`${group.prefix}/\`
|
|||
|
|
- **认证方式**: JWT Bearer Token
|
|||
|
|
- **内容类型**: \`application/json\`
|
|||
|
|
- **字符编码**: \`UTF-8\`
|
|||
|
|
|
|||
|
|
## 模块列表
|
|||
|
|
|
|||
|
|
::: tip 自动生成
|
|||
|
|
|
|||
|
|
此文档由后端 Swagger 自动生成,包含最新的 API 接口信息。
|
|||
|
|
|
|||
|
|
:::
|
|||
|
|
|
|||
|
|
## 认证流程
|
|||
|
|
|
|||
|
|
### 获取 Token
|
|||
|
|
|
|||
|
|
\`\`\`http
|
|||
|
|
POST ${group.prefix}/auth/login
|
|||
|
|
Content-Type: application/json
|
|||
|
|
|
|||
|
|
{
|
|||
|
|
"username": "user@example.com",
|
|||
|
|
"password": "password123"
|
|||
|
|
}
|
|||
|
|
\`\`\`
|
|||
|
|
|
|||
|
|
## 响应格式
|
|||
|
|
|
|||
|
|
所有 API 都遵循统一的响应格式:
|
|||
|
|
|
|||
|
|
\`\`\`json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"message": "success",
|
|||
|
|
"data": {
|
|||
|
|
// 具体数据
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
\`\`\`
|
|||
|
|
|
|||
|
|
## 错误处理
|
|||
|
|
|
|||
|
|
API 错误响应格式:
|
|||
|
|
|
|||
|
|
\`\`\`json
|
|||
|
|
{
|
|||
|
|
"code": 400,
|
|||
|
|
"message": "参数错误",
|
|||
|
|
"data": null
|
|||
|
|
}
|
|||
|
|
\`\`\`
|
|||
|
|
|
|||
|
|
## 状态码说明
|
|||
|
|
|
|||
|
|
| 状态码 | 说明 |
|
|||
|
|
|--------|------|
|
|||
|
|
| 200 | 请求成功 |
|
|||
|
|
| 201 | 创建成功 |
|
|||
|
|
| 400 | 请求参数错误 |
|
|||
|
|
| 401 | 未授权 |
|
|||
|
|
| 403 | 禁止访问 |
|
|||
|
|
| 404 | 资源不存在 |
|
|||
|
|
| 500 | 服务器内部错误 |
|
|||
|
|
|
|||
|
|
## 开发工具
|
|||
|
|
|
|||
|
|
### Swagger UI
|
|||
|
|
|
|||
|
|
访问 \`${config.backendUrl}${group.prefix}\` 查看完整的 API 文档和在线测试功能。
|
|||
|
|
|
|||
|
|
### 在线测试
|
|||
|
|
|
|||
|
|
每个 API 分组都提供 Swagger UI 界面,支持:
|
|||
|
|
|
|||
|
|
- API 文档查看
|
|||
|
|
- 在线测试
|
|||
|
|
- 参数验证
|
|||
|
|
- 响应示例
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生成模块 API 文档
|
|||
|
|
*/
|
|||
|
|
function generateModuleDoc(group, tag, paths) {
|
|||
|
|
const moduleName = tag.name.toLowerCase().replace(/\s+/g, '-');
|
|||
|
|
const moduleDesc = tag.description || `${moduleName} 相关接口`;
|
|||
|
|
|
|||
|
|
let markdown = `---
|
|||
|
|
title: ${moduleName} API
|
|||
|
|
description: ${moduleDesc}
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# ${moduleName} API
|
|||
|
|
|
|||
|
|
::: info ${moduleName} API
|
|||
|
|
|
|||
|
|
${moduleDesc}
|
|||
|
|
|
|||
|
|
:::
|
|||
|
|
|
|||
|
|
## 基础信息
|
|||
|
|
|
|||
|
|
- **模块路径**: \`${group.prefix}/${moduleName.toLowerCase()}/\`
|
|||
|
|
- **认证方式**: JWT Bearer Token(部分接口需要)
|
|||
|
|
- **内容类型**: \`application/json\`
|
|||
|
|
|
|||
|
|
## API 列表
|
|||
|
|
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// 查找该标签下的所有接口
|
|||
|
|
Object.keys(paths).forEach(path => {
|
|||
|
|
Object.keys(paths[path]).forEach(method => {
|
|||
|
|
const operation = paths[path][method];
|
|||
|
|
|
|||
|
|
if (operation.tags && operation.tags.includes(tag.name)) {
|
|||
|
|
markdown += generateApiDoc(path, method, operation, group.prefix);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return markdown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生成单个 API 文档
|
|||
|
|
*/
|
|||
|
|
function generateApiDoc(path, method, operation, prefix) {
|
|||
|
|
const summary = operation.summary || operation.operationId || '未命名接口';
|
|||
|
|
const description = operation.description || '';
|
|||
|
|
|
|||
|
|
let doc = `### ${summary}\n\n`;
|
|||
|
|
|
|||
|
|
// 接口地址
|
|||
|
|
doc += `**接口地址**: \`${method.toUpperCase()} ${path}\`\n\n`;
|
|||
|
|
|
|||
|
|
// 接口描述
|
|||
|
|
if (description) {
|
|||
|
|
doc += `**接口描述**: ${description}\n\n`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 请求参数
|
|||
|
|
if (operation.parameters && operation.parameters.length > 0) {
|
|||
|
|
doc += `**请求参数**:\n\n`;
|
|||
|
|
|
|||
|
|
if (method === 'get') {
|
|||
|
|
doc += `| 参数 | 类型 | 必填 | 说明 |\n`;
|
|||
|
|
doc += `|------|------|------|------|\n`;
|
|||
|
|
|
|||
|
|
operation.parameters.forEach(param => {
|
|||
|
|
const required = param.required ? '是' : '否';
|
|||
|
|
const type = param.type || param.schema?.type || 'string';
|
|||
|
|
doc += `| ${param.name} | ${type} | ${required} | ${param.description || ''} |\n`;
|
|||
|
|
});
|
|||
|
|
doc += `\n`;
|
|||
|
|
} else {
|
|||
|
|
doc += `\`\`\`json\n`;
|
|||
|
|
doc += `{\n`;
|
|||
|
|
operation.parameters.forEach((param, index) => {
|
|||
|
|
const comma = index < operation.parameters.length - 1 ? ',' : '';
|
|||
|
|
const type = param.type || param.schema?.type || 'string';
|
|||
|
|
doc += ` "${param.name}": "${type}"${comma}\n`;
|
|||
|
|
});
|
|||
|
|
doc += `}\n`;
|
|||
|
|
doc += `\`\`\`\n\n`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 请求体
|
|||
|
|
if (operation.requestBody) {
|
|||
|
|
doc += `**请求体**:\n\n`;
|
|||
|
|
doc += `\`\`\`json\n`;
|
|||
|
|
if (operation.requestBody.content && operation.requestBody.content['application/json']) {
|
|||
|
|
const schema = operation.requestBody.content['application/json'].schema;
|
|||
|
|
if (schema && schema.properties) {
|
|||
|
|
doc += `{\n`;
|
|||
|
|
Object.keys(schema.properties).forEach((prop, index) => {
|
|||
|
|
const comma = index < Object.keys(schema.properties).length - 1 ? ',' : '';
|
|||
|
|
const type = schema.properties[prop].type || 'string';
|
|||
|
|
doc += ` "${prop}": "${type}"${comma}\n`;
|
|||
|
|
});
|
|||
|
|
doc += `}\n`;
|
|||
|
|
} else {
|
|||
|
|
doc += `{\n`;
|
|||
|
|
doc += ` // 请求体内容\n`;
|
|||
|
|
doc += `}\n`;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
doc += `{\n`;
|
|||
|
|
doc += ` // 请求体内容\n`;
|
|||
|
|
doc += `}\n`;
|
|||
|
|
}
|
|||
|
|
doc += `\`\`\`\n\n`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 响应示例
|
|||
|
|
if (operation.responses) {
|
|||
|
|
doc += `**响应示例**:\n\n`;
|
|||
|
|
const successResponse = operation.responses['200'] || operation.responses['201'];
|
|||
|
|
if (successResponse) {
|
|||
|
|
doc += `\`\`\`json\n`;
|
|||
|
|
doc += `{\n`;
|
|||
|
|
doc += ` "code": 200,\n`;
|
|||
|
|
doc += ` "message": "success",\n`;
|
|||
|
|
doc += ` "data": {\n`;
|
|||
|
|
if (successResponse.content && successResponse.content['application/json']) {
|
|||
|
|
const schema = successResponse.content['application/json'].schema;
|
|||
|
|
if (schema && schema.properties) {
|
|||
|
|
Object.keys(schema.properties).forEach((prop, index) => {
|
|||
|
|
const comma = index < Object.keys(schema.properties).length - 1 ? ',' : '';
|
|||
|
|
const type = schema.properties[prop].type || 'string';
|
|||
|
|
doc += ` "${prop}": "${type}"${comma}\n`;
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
doc += ` // 响应数据\n`;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
doc += ` // 响应数据\n`;
|
|||
|
|
}
|
|||
|
|
doc += ` }\n`;
|
|||
|
|
doc += `}\n`;
|
|||
|
|
doc += `\`\`\`\n\n`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
doc += `---\n\n`;
|
|||
|
|
return doc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 保存文档到文件
|
|||
|
|
*/
|
|||
|
|
function saveDocument(outputPath, content) {
|
|||
|
|
try {
|
|||
|
|
// 确保目录存在
|
|||
|
|
const dir = path.dirname(outputPath);
|
|||
|
|
if (!fs.existsSync(dir)) {
|
|||
|
|
fs.mkdirSync(dir, { recursive: true });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fs.writeFileSync(outputPath, content, 'utf8');
|
|||
|
|
console.log(`文档已保存: ${outputPath}`);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`保存文档失败: ${outputPath}`, error.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新侧边栏配置
|
|||
|
|
*/
|
|||
|
|
function updateSidebar(group, tags) {
|
|||
|
|
const sidebarPath = path.join(__dirname, '../.vitepress/config/zh.mts');
|
|||
|
|
let sidebarContent = fs.readFileSync(sidebarPath, 'utf8');
|
|||
|
|
|
|||
|
|
// 生成模块链接
|
|||
|
|
const moduleLinks = tags.map(tag => {
|
|||
|
|
const moduleName = tag.name.toLowerCase().replace(/\s+/g, '-');
|
|||
|
|
return ` { link: 'openapi/api/${group.outputDir}/${moduleName}', text: '${tag.name} API' }`;
|
|||
|
|
}).join(',\n');
|
|||
|
|
|
|||
|
|
// 更新侧边栏配置
|
|||
|
|
const sidebarPattern = new RegExp(`(text: '${group.name}',\\s*items: \\[\\s*\\{[^}]+\\},?\\s*)(\\])`, 's');
|
|||
|
|
const replacement = `$1,\n${moduleLinks}\n $2`;
|
|||
|
|
|
|||
|
|
if (sidebarContent.match(sidebarPattern)) {
|
|||
|
|
sidebarContent = sidebarContent.replace(sidebarPattern, replacement);
|
|||
|
|
fs.writeFileSync(sidebarPath, sidebarContent, 'utf8');
|
|||
|
|
console.log(`侧边栏配置已更新: ${group.name}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 主函数
|
|||
|
|
*/
|
|||
|
|
async function main() {
|
|||
|
|
console.log('开始自动生成 API 文档...\n');
|
|||
|
|
|
|||
|
|
for (const [key, group] of Object.entries(config.apiGroups)) {
|
|||
|
|
console.log(`处理 ${group.name}...`);
|
|||
|
|
|
|||
|
|
// 获取 Swagger JSON(统一)
|
|||
|
|
const swaggerData = await getSwaggerJson(group);
|
|||
|
|
if (!swaggerData) {
|
|||
|
|
console.log(`跳过 ${group.name},无法获取数据\n`);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 基于分组前缀过滤 paths
|
|||
|
|
const filterByPrefix = (doc, prefix) => {
|
|||
|
|
const cloned = JSON.parse(JSON.stringify(doc));
|
|||
|
|
const filteredPaths = {};
|
|||
|
|
Object.keys(cloned.paths || {}).forEach((p) => {
|
|||
|
|
if (p.startsWith(prefix)) filteredPaths[p] = cloned.paths[p];
|
|||
|
|
});
|
|||
|
|
cloned.paths = filteredPaths;
|
|||
|
|
return cloned;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const filtered = filterByPrefix(swaggerData, group.prefix + '/');
|
|||
|
|
|
|||
|
|
// 生成概览文档
|
|||
|
|
const overviewContent = generateOverview(group);
|
|||
|
|
const overviewPath = path.join(config.docsDir, group.outputDir, 'index.md');
|
|||
|
|
saveDocument(overviewPath, overviewContent);
|
|||
|
|
|
|||
|
|
// 按标签生成模块文档
|
|||
|
|
const tags = filtered.tags || [];
|
|||
|
|
const paths = filtered.paths || {};
|
|||
|
|
|
|||
|
|
for (const tag of tags) {
|
|||
|
|
const moduleName = tag.name.toLowerCase().replace(/\s+/g, '-');
|
|||
|
|
const moduleContent = generateModuleDoc(group, tag, paths);
|
|||
|
|
const modulePath = path.join(config.docsDir, group.outputDir, `${moduleName}.md`);
|
|||
|
|
saveDocument(modulePath, moduleContent);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新侧边栏配置
|
|||
|
|
updateSidebar(group, tags);
|
|||
|
|
|
|||
|
|
console.log(`${group.name} 处理完成\n`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('API 文档自动生成完成!');
|
|||
|
|
console.log('\n下一步:');
|
|||
|
|
console.log('1. 启动文档服务: npm run dev');
|
|||
|
|
console.log('2. 访问: http://localhost:6173/wwjcloud/openapi/api/');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 运行脚本
|
|||
|
|
if (require.main === module) {
|
|||
|
|
main().catch(error => {
|
|||
|
|
console.error('脚本执行失败:', error);
|
|||
|
|
process.exit(1);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = {
|
|||
|
|
config,
|
|||
|
|
getSwaggerJson,
|
|||
|
|
generateOverview,
|
|||
|
|
generateModuleDoc,
|
|||
|
|
saveDocument,
|
|||
|
|
updateSidebar
|
|||
|
|
};
|