- 成长记录: images 统一为 string[],修复 OpenAPI/Java DTO/前端类型 - 租户更新: TenantUpdateRequest 新增 forceRemove,ErrorCode 新增 REMOVE_PACKAGE_HAS_SCHEDULES - 异常处理: BusinessException 支持附加 data,GlobalExceptionHandler 返回 data 供前端确认弹窗 Made-with: Cursor
108 lines
3.4 KiB
JavaScript
108 lines
3.4 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:8480/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[]'];
|
||
}
|
||
// 修复成长记录 images 字段:统一为 array of string(避免 SpringDoc 误生成为 string)
|
||
fixGrowthRecordImagesSchema(spec.components?.schemas);
|
||
|
||
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);
|
||
}
|
||
|
||
/** 将 GrowthRecordCreateRequest/GrowthRecordUpdateRequest 的 images 统一为 array of string */
|
||
function fixGrowthRecordImagesSchema(schemas) {
|
||
if (!schemas) return;
|
||
const arrayOfString = {
|
||
type: 'array',
|
||
items: { type: 'string', description: '图片 URL' },
|
||
description: '图片 URL 列表',
|
||
};
|
||
for (const name of ['GrowthRecordCreateRequest', 'GrowthRecordUpdateRequest']) {
|
||
const s = schemas[name];
|
||
if (s?.properties?.images?.type === 'string') {
|
||
schemas[name].properties.images = arrayOfString;
|
||
}
|
||
}
|
||
}
|
||
|
||
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);
|
||
});
|