kindergarten_java/reading-platform-frontend/scripts/fetch-openapi.js
zhonghua 029881f09f fix: 成长记录 images 序列化统一 + 租户套餐移除确认流程
- 成长记录: images 统一为 string[],修复 OpenAPI/Java DTO/前端类型
- 租户更新: TenantUpdateRequest 新增 forceRemove,ErrorCode 新增 REMOVE_PACKAGE_HAS_SCHEDULES
- 异常处理: BusinessException 支持附加 data,GlobalExceptionHandler 返回 data 供前端确认弹窗

Made-with: Cursor
2026-03-23 14:47:01 +08:00

108 lines
3.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 拉取 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);
});