- 新增 scripts/fetch-openapi.js:拉取并修复 OpenAPI 文档 - 内联 ResultObject[] 的 \,移除非法 schema 名 - orval 使用本地 openapi.json,api:update 自动执行 api:fetch - AdminStatsController: Result<Object[]> 改为 Result<List<Map<String, Object>>> - .gitignore: 忽略 openapi.json Made-with: Cursor
90 lines
2.7 KiB
JavaScript
90 lines
2.7 KiB
JavaScript
/**
|
|
* 拉取 OpenAPI 文档并修复 SpringDoc 生成的 oneOf schema 问题
|
|
* 解决 orval 报错: "oneOf must match exactly one schema in oneOf"
|
|
*/
|
|
import { writeFileSync } from 'fs';
|
|
import { join, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const TARGET = 'http://localhost:8080/v3/api-docs';
|
|
const OUTPUT = join(__dirname, '../openapi.json');
|
|
|
|
async function fetchAndFix() {
|
|
const res = await fetch(TARGET);
|
|
if (!res.ok) throw new Error(`Failed to fetch: ${res.status} ${res.statusText}. 请确保后端已启动 (mvn spring-boot:run)`);
|
|
let spec = await res.json();
|
|
|
|
const paths = spec.paths || {};
|
|
for (const path of Object.keys(paths)) {
|
|
let newKey = path.replace(/\/v1\/v1\//g, '/v1/');
|
|
if (newKey === path) newKey = path.replace(/^\/api\/(?!v1\/)/, '/api/v1/');
|
|
if (newKey !== path) {
|
|
paths[newKey] = paths[path];
|
|
delete paths[path];
|
|
}
|
|
}
|
|
|
|
fixOneOfInPaths(paths);
|
|
inlineResultObjectArrayRef(paths);
|
|
// 移除非法 schema 名 ResultObject[](含 [] 不符合 OpenAPI 规范)
|
|
if (spec.components?.schemas) {
|
|
delete spec.components.schemas['ResultObject[]'];
|
|
}
|
|
|
|
writeFileSync(OUTPUT, JSON.stringify(spec, null, 2));
|
|
console.log('OpenAPI spec written to:', OUTPUT);
|
|
}
|
|
|
|
function fixOneOfInPaths(paths) {
|
|
for (const pathObj of Object.values(paths)) {
|
|
for (const op of Object.values(pathObj)) {
|
|
const res200 = op?.responses?.['200'];
|
|
if (!res200?.content) continue;
|
|
for (const media of Object.values(res200.content)) {
|
|
if (media?.schema) fixSchema(media.schema);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function fixSchema(schema) {
|
|
if (!schema || typeof schema !== 'object') return;
|
|
if (schema.oneOf && Array.isArray(schema.oneOf)) {
|
|
schema.type = 'array';
|
|
schema.items = { type: 'object', additionalProperties: true };
|
|
delete schema.oneOf;
|
|
}
|
|
if (schema.properties) {
|
|
for (const p of Object.values(schema.properties)) fixSchema(p);
|
|
}
|
|
if (schema.items) fixSchema(schema.items);
|
|
}
|
|
|
|
function inlineResultObjectArrayRef(paths) {
|
|
const inlineSchema = {
|
|
type: 'object',
|
|
properties: {
|
|
code: { type: 'integer', format: 'int32' },
|
|
message: { type: 'string' },
|
|
data: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
},
|
|
};
|
|
for (const pathObj of Object.values(paths)) {
|
|
for (const op of Object.values(pathObj)) {
|
|
const res200 = op?.responses?.['200'];
|
|
if (!res200?.content) continue;
|
|
for (const media of Object.values(res200.content)) {
|
|
if (media?.schema?.['$ref']?.endsWith('ResultObject[]')) {
|
|
media.schema = { ...inlineSchema };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fetchAndFix().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|