Compare commits

...

3 Commits

Author SHA1 Message Date
Max c1a6c94357 Merge branch 'release/2.1.2' 2025-11-05 18:38:57 +08:00
Max ca57ceb354 update 2025-11-05 18:38:05 +08:00
Max ff9bb34a31 Merge tag '2.1.1' into develop
2.1.1
2025-11-05 17:00:26 +08:00
10 changed files with 20759 additions and 147 deletions

View File

@ -11,6 +11,9 @@ generator:
# 输入配置 # 输入配置
input: input:
# Swagger 文档源(支持多版本) # Swagger 文档源(支持多版本)
# 注意:多个 URL 会按顺序合并,后面的文档会覆盖前面的同名模型和路径
# 因此建议将高版本(如 V2配置在低版本如 V1之后以确保高版本的模型覆盖低版本
# 例如V1 在前V2 在后,那么 V2 的模型会覆盖 V1 的同名模型
swagger_urls: # 完整形式:可以控制每个版本的启用状态 swagger_urls: # 完整形式:可以控制每个版本的启用状态
- url: "https://quanxue-test-api.w.23544.com:8843/swagger/v1/swagger.json" - url: "https://quanxue-test-api.w.23544.com:8843/swagger/v1/swagger.json"
enabled: true enabled: true

File diff suppressed because it is too large Load Diff

View File

@ -84,27 +84,64 @@ class GenerateCommand extends BaseCommand {
final parser = SwaggerDataParser(); final parser = SwaggerDataParser();
// paths models // paths models
//
// V2 V1
SwaggerDocument? mergedDocument; SwaggerDocument? mergedDocument;
for (int i = 0; i < SwaggerConfig.swaggerJsonUrls.length; i++) { final urls = SwaggerConfig.swaggerJsonUrls;
final url = SwaggerConfig.swaggerJsonUrls[i]; progress('URL 处理顺序: ${urls.join(" -> ")}');
progress(
' [${i + 1}/${SwaggerConfig.swaggerJsonUrls.length}] 正在解析: $url'); for (int i = 0; i < urls.length; i++) {
final url = urls[i];
progress(' [${i + 1}/${urls.length}] 正在解析: $url');
final doc = await parser.fetchAndParseSwaggerDocument(url); final doc = await parser.fetchAndParseSwaggerDocument(url);
progress(' 解析完成: ${doc.models.length} 个模型, ${doc.paths.length} 个路径');
if (mergedDocument == null) { if (mergedDocument == null) {
mergedDocument = doc; mergedDocument = doc;
progress(' 初始文档: ${doc.models.length} 个模型');
} else { } else {
// paths models // paths models
// 使 {...mergedDocument.models, ...doc.models}
// doc.models mergedDocument.models key
//
final beforeModelCount = mergedDocument.models.length;
final currentModelCount = doc.models.length;
//
final overlappingModels = <String>[];
for (final key in doc.models.keys) {
if (mergedDocument.models.containsKey(key)) {
overlappingModels.add(key);
}
}
if (overlappingModels.isNotEmpty) {
progress(
' 发现 ${overlappingModels.length} 个同名模型将被覆盖: ${overlappingModels.take(5).join(", ")}${overlappingModels.length > 5 ? "..." : ""}');
}
mergedDocument = SwaggerDocument( mergedDocument = SwaggerDocument(
title: mergedDocument.title, title: mergedDocument.title,
description: mergedDocument.description, description: mergedDocument.description,
version: '${mergedDocument.version} + ${doc.version}', version: '${mergedDocument.version} + ${doc.version}',
// doc mergedDocument key
// V2 V1
paths: {...mergedDocument.paths, ...doc.paths}, paths: {...mergedDocument.paths, ...doc.paths},
models: {...mergedDocument.models, ...doc.models}, models: {...mergedDocument.models, ...doc.models},
controllers: {...mergedDocument.controllers, ...doc.controllers}, controllers: {...mergedDocument.controllers, ...doc.controllers},
); );
final afterModelCount = mergedDocument.models.length;
progress(
' 合并后: $beforeModelCount + $currentModelCount -> $afterModelCount 个模型');
//
if (overlappingModels.isNotEmpty) {
progress(
' 同名模型列表: ${overlappingModels.take(10).join(", ")}${overlappingModels.length > 10 ? "..." : ""}');
}
} }
} }
@ -118,12 +155,20 @@ class GenerateCommand extends BaseCommand {
// //
final options = _parseGenerateOptions(parsedArgs); final options = _parseGenerateOptions(parsedArgs);
final fullOutputDir = FileUtils.getProjectRootGeneratorDir();
progress('输出目录: $fullOutputDir'); // 使
final baseDir = SwaggerConfig.generatorDir;
final apiDir = SwaggerConfig.apiDir;
final modelsDir = SwaggerConfig.modelsDir;
progress('输出目录: $baseDir');
progress('API 目录: $apiDir');
progress('模型目录: $modelsDir');
// //
await FileUtils.ensureDirectoryExists(fullOutputDir); await FileUtils.ensureDirectoryExists(baseDir);
await FileUtils.ensureDirectoryExists(apiDir);
await FileUtils.ensureDirectoryExists(modelsDir);
int generatedFiles = 0; int generatedFiles = 0;
@ -135,7 +180,6 @@ class GenerateCommand extends BaseCommand {
useSimpleModels: options.useSimpleModels, useSimpleModels: options.useSimpleModels,
); );
final modelsDir = '$fullOutputDir/api_models';
await FileUtils.ensureDirectoryExists(modelsDir); await FileUtils.ensureDirectoryExists(modelsDir);
final modelFiles = generator.generateSeparateModelFiles(); final modelFiles = generator.generateSeparateModelFiles();
@ -159,7 +203,6 @@ class GenerateCommand extends BaseCommand {
if (options.generateApi) { if (options.generateApi) {
progress('正在按版本和tags分组生成Retrofit风格API接口...'); progress('正在按版本和tags分组生成Retrofit风格API接口...');
final apiDir = '$fullOutputDir/api';
await FileUtils.ensureDirectoryExists(apiDir); await FileUtils.ensureDirectoryExists(apiDir);
// 🎯 paths // 🎯 paths
@ -280,7 +323,6 @@ class GenerateCommand extends BaseCommand {
final parameterEntityFiles = final parameterEntityFiles =
lastGenerator.generateParameterEntityFiles(); lastGenerator.generateParameterEntityFiles();
if (parameterEntityFiles.isNotEmpty) { if (parameterEntityFiles.isNotEmpty) {
final modelsDir = '$fullOutputDir/api_models';
// Parameters parameters // Parameters parameters
final parametersDir = '$modelsDir/parameters'; final parametersDir = '$modelsDir/parameters';
await FileUtils.ensureDirectoryExists(parametersDir); await FileUtils.ensureDirectoryExists(parametersDir);
@ -307,7 +349,6 @@ class GenerateCommand extends BaseCommand {
// index.dart // index.dart
if (options.generateModels || options.generateApi) { if (options.generateModels || options.generateApi) {
progress('正在重新生成 index.dart 文件...'); progress('正在重新生成 index.dart 文件...');
final modelsDir = '$fullOutputDir/api_models';
final allFiles = await _getAllModelFiles(modelsDir); final allFiles = await _getAllModelFiles(modelsDir);
final indexContent = _generateUpdatedIndexFile(allFiles); final indexContent = _generateUpdatedIndexFile(allFiles);
final indexPath = '$modelsDir/index.dart'; final indexPath = '$modelsDir/index.dart';
@ -321,7 +362,7 @@ class GenerateCommand extends BaseCommand {
final generator = DocumentationGenerator(document); final generator = DocumentationGenerator(document);
final code = generator.generate(); final code = generator.generate();
final filePath = '$fullOutputDir/api_documentation.md'; final filePath = '$baseDir/api_documentation.md';
// //
if (!ConfigLoader.shouldSkipFile(filePath)) { if (!ConfigLoader.shouldSkipFile(filePath)) {
@ -334,7 +375,7 @@ class GenerateCommand extends BaseCommand {
} }
// //
_generateSummary(document, fullOutputDir); _generateSummary(document, baseDir);
success('代码生成完成!共生成 $generatedFiles 个文件'); success('代码生成完成!共生成 $generatedFiles 个文件');
return 0; return 0;
@ -428,7 +469,13 @@ class GenerateCommand extends BaseCommand {
final fileName = path.basename(entity.path); final fileName = path.basename(entity.path);
// index.dart .g.dart // index.dart .g.dart
if (fileName != 'index.dart' && !fileName.endsWith('.g.dart')) { if (fileName != 'index.dart' && !fileName.endsWith('.g.dart')) {
// ignored_files
final filePath = path.join(subDir, fileName);
if (!ConfigLoader.shouldSkipFile(filePath)) {
dartFiles.add(fileName); dartFiles.add(fileName);
} else {
progress(' 跳过导出文件: $fileName (在 ignored_files 配置中)');
}
} }
} }
} }
@ -473,6 +520,22 @@ class GenerateCommand extends BaseCommand {
buffer.writeln('library;'); buffer.writeln('library;');
buffer.writeln(''); buffer.writeln('');
// base_result base_page_result
final baseResultImport = SwaggerConfig.baseResultImport;
final basePageResultImport = SwaggerConfig.basePageResultImport;
if (baseResultImport.isNotEmpty) {
buffer.writeln('export \'$baseResultImport\';');
}
if (basePageResultImport.isNotEmpty) {
buffer.writeln('export \'$basePageResultImport\';');
}
if ((baseResultImport.isNotEmpty || basePageResultImport.isNotEmpty) &&
fileNames.isNotEmpty) {
buffer.writeln('');
}
// //
for (final fileName in fileNames) { for (final fileName in fileNames) {
buffer.writeln('export \'$fileName\';'); buffer.writeln('export \'$fileName\';');

View File

@ -40,6 +40,12 @@ class SwaggerConfig {
/// ///
static String get modelsDir => ConfigLoader.getModelsDir(); static String get modelsDir => ConfigLoader.getModelsDir();
/// BaseResult
static String get baseResultImport => ConfigLoader.getBaseResultImport();
/// BasePageResult
static String get basePageResultImport => ConfigLoader.getBasePageResultImport();
/// ///
static const String defaultDocumentationFile = static const String defaultDocumentationFile =
'generated_api_documentation.md'; 'generated_api_documentation.md';

View File

@ -106,6 +106,10 @@ class ConfigLoader {
/// swagger_urls () /// swagger_urls ()
/// : ["url1", "url2"] /// : ["url1", "url2"]
/// : [{url: "...", enabled: true}] /// : [{url: "...", enabled: true}]
///
/// URL
/// Swagger
/// V2 V1
static List<String> getSwaggerUrls([Map<String, dynamic>? config]) { static List<String> getSwaggerUrls([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig(); final cfg = config ?? loadConfig();
if (cfg == null) { if (cfg == null) {
@ -430,4 +434,46 @@ class ConfigLoader {
return versionExtraction['default_version'] as String? ?? 'v1'; return versionExtraction['default_version'] as String? ?? 'v1';
} }
/// BaseResult
static String getBaseResultImport([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return 'package:learning_officer_oa/common/models/common/base_result.dart';
}
final generation = cfg['generation'] as Map<String, dynamic>?;
if (generation == null) {
return 'package:learning_officer_oa/common/models/common/base_result.dart';
}
final api = generation['api'] as Map<String, dynamic>?;
if (api == null) {
return 'package:learning_officer_oa/common/models/common/base_result.dart';
}
return api['base_result_import'] as String? ??
'package:learning_officer_oa/common/models/common/base_result.dart';
}
/// BasePageResult
static String getBasePageResultImport([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return 'package:learning_officer_oa/common/models/common/base_page_result.dart';
}
final generation = cfg['generation'] as Map<String, dynamic>?;
if (generation == null) {
return 'package:learning_officer_oa/common/models/common/base_page_result.dart';
}
final api = generation['api'] as Map<String, dynamic>?;
if (api == null) {
return 'package:learning_officer_oa/common/models/common/base_page_result.dart';
}
return api['base_page_result_import'] as String? ??
'package:learning_officer_oa/common/models/common/base_page_result.dart';
}
} }

View File

@ -1,4 +1,5 @@
import '../core/models.dart'; import '../core/models.dart';
import '../core/config.dart';
import '../utils/string_utils.dart'; import '../utils/string_utils.dart';
import 'base_generator.dart'; import 'base_generator.dart';
@ -430,6 +431,22 @@ class ModelCodeGenerator extends ModelGenerator {
buffer.writeln('library;'); buffer.writeln('library;');
buffer.writeln(''); buffer.writeln('');
// base_result base_page_result
final baseResultImport = SwaggerConfig.baseResultImport;
final basePageResultImport = SwaggerConfig.basePageResultImport;
if (baseResultImport.isNotEmpty) {
buffer.writeln('export \'$baseResultImport\';');
}
if (basePageResultImport.isNotEmpty) {
buffer.writeln('export \'$basePageResultImport\';');
}
if ((baseResultImport.isNotEmpty || basePageResultImport.isNotEmpty) &&
modelsByDirectory.isNotEmpty) {
buffer.writeln('');
}
// index.dart // index.dart
final sortedDirs = modelsByDirectory.keys.toList()..sort(); final sortedDirs = modelsByDirectory.keys.toList()..sort();
@ -473,6 +490,22 @@ class ModelCodeGenerator extends ModelGenerator {
buffer.writeln('library;'); buffer.writeln('library;');
buffer.writeln(''); buffer.writeln('');
// base_result base_page_result
final baseResultImport = SwaggerConfig.baseResultImport;
final basePageResultImport = SwaggerConfig.basePageResultImport;
if (baseResultImport.isNotEmpty) {
buffer.writeln('export \'$baseResultImport\';');
}
if (basePageResultImport.isNotEmpty) {
buffer.writeln('export \'$basePageResultImport\';');
}
if ((baseResultImport.isNotEmpty || basePageResultImport.isNotEmpty) &&
modelFileNames.isNotEmpty) {
buffer.writeln('');
}
// //
final sortedFiles = List<String>.from(modelFileNames)..sort(); final sortedFiles = List<String>.from(modelFileNames)..sort();

View File

@ -137,7 +137,8 @@ class RetrofitApiGenerator extends BaseGenerator {
final fileName = _generateTagFileName(tagName); final fileName = _generateTagFileName(tagName);
// //
buffer.writeln(generateFileHeader('$tagName API 接口定义', fileName: fileName)); buffer
.writeln(generateFileHeader('$tagName API 接口定义', fileName: fileName));
buffer.writeln(''); buffer.writeln('');
// //
@ -154,12 +155,20 @@ class RetrofitApiGenerator extends BaseGenerator {
/// ///
void _generateImports(StringBuffer buffer) { void _generateImports(StringBuffer buffer) {
// // Dart
// 1. dart:xxx
// 2. package:xxx
// 3. package:project_name
// 4.
//
// dart:
buffer.writeln('import \'dart:convert\';'); buffer.writeln('import \'dart:convert\';');
buffer.writeln('import \'dart:io\';'); buffer.writeln('import \'dart:io\';');
buffer.writeln('import \'dart:typed_data\';'); buffer.writeln('import \'dart:typed_data\';');
buffer.writeln('');
// Dio Retrofit // package:
if (useDio) { if (useDio) {
buffer.writeln('import \'package:dio/dio.dart\';'); buffer.writeln('import \'package:dio/dio.dart\';');
} }
@ -173,19 +182,9 @@ class RetrofitApiGenerator extends BaseGenerator {
buffer.writeln('import \'package:crypto/crypto.dart\';'); buffer.writeln('import \'package:crypto/crypto.dart\';');
buffer.writeln('import \'package:path/path.dart\' as path;'); buffer.writeln('import \'package:path/path.dart\' as path;');
buffer.writeln('import \'package:http_parser/http_parser.dart\';'); buffer.writeln('import \'package:http_parser/http_parser.dart\';');
buffer.writeln(''); buffer.writeln('');
// // api_models/index.dart base_result base_page_result
buffer.writeln(
'import \'package:learning_officer_oa/common/models/common/base_result.dart\';');
buffer.writeln(
'import \'package:learning_officer_oa/common/models/common/base_page_result.dart\';');
buffer.writeln('');
// index.dart
// 使 index.dart
// Dart tree-shaking 使
buffer.writeln('import \'../../api_models/index.dart\';'); buffer.writeln('import \'../../api_models/index.dart\';');
buffer.writeln(''); buffer.writeln('');
@ -1238,16 +1237,8 @@ class RetrofitApiGenerator extends BaseGenerator {
buffer.writeln(''); buffer.writeln('');
// // api_models/index.dart base_result base_page_result
buffer.writeln( buffer.writeln('import \'../../api_models/index.dart\';');
'import \'package:learning_officer_oa/common/models/common/base_result.dart\';');
// base_page_result.dart
if (_needsPaginationImportForDocument()) {
buffer.writeln(
'import \'package:learning_officer_oa/common/models/common/base_page_result.dart\';');
}
buffer.writeln(''); buffer.writeln('');
// //
@ -1369,31 +1360,24 @@ class RetrofitApiGenerator extends BaseGenerator {
/// tag /// tag
void _generateTagImports(StringBuffer buffer, List<ApiPath> paths) { void _generateTagImports(StringBuffer buffer, List<ApiPath> paths) {
// Dart
// 1. dart:xxx
// 2. package:xxx
// 3. package:project_name
// 4.
//
// package:
if (useDio) { if (useDio) {
buffer.writeln('import \'package:dio/dio.dart\';'); buffer.writeln('import \'package:dio/dio.dart\';');
} }
if (useRetrofit) { if (useRetrofit) {
buffer.writeln('import \'package:retrofit/retrofit.dart\';'); buffer.writeln('import \'package:retrofit/retrofit.dart\';');
} }
buffer.writeln(''); buffer.writeln('');
// // api_models/index.dart base_result base_page_result
buffer.writeln(
'import \'package:learning_officer_oa/common/models/common/base_result.dart\';');
// base_page_result.dart
if (_needsPaginationImport(paths)) {
buffer.writeln(
'import \'package:learning_officer_oa/common/models/common/base_page_result.dart\';');
}
buffer.writeln('');
// index.dart
// 使 index.dart
// Dart tree-shaking 使
buffer.writeln('import \'../../api_models/index.dart\';'); buffer.writeln('import \'../../api_models/index.dart\';');
buffer.writeln(''); buffer.writeln('');
@ -1663,7 +1647,9 @@ class RetrofitApiGenerator extends BaseGenerator {
/// ///
void _generateParameterEntity( void _generateParameterEntity(
ApiPath path, String className, List<ApiParameter> queryParams) { ApiPath path, String className, List<ApiParameter> queryParams) {
if (!_generatedParameterEntities.containsKey(className)) { //
//
// V2 V1
final buffer = StringBuffer(); final buffer = StringBuffer();
// //
@ -1671,12 +1657,12 @@ class RetrofitApiGenerator extends BaseGenerator {
'参数实体类 - $className', '参数实体类 - $className',
fileName: '${StringUtils.toSnakeCase(className)}.dart', fileName: '${StringUtils.toSnakeCase(className)}.dart',
)); ));
buffer.writeln('// 用于 ${path.method.value.toUpperCase()} ${path.path} 的查询参数'); buffer
.writeln('// 用于 ${path.method.value.toUpperCase()} ${path.path} 的查询参数');
buffer.writeln(''); buffer.writeln('');
// //
buffer buffer.writeln('import \'package:json_annotation/json_annotation.dart\';');
.writeln('import \'package:json_annotation/json_annotation.dart\';');
buffer.writeln(''); buffer.writeln('');
buffer.writeln('part \'${StringUtils.toSnakeCase(className)}.g.dart\';'); buffer.writeln('part \'${StringUtils.toSnakeCase(className)}.g.dart\';');
buffer.writeln(''); buffer.writeln('');
@ -1715,8 +1701,8 @@ class RetrofitApiGenerator extends BaseGenerator {
buffer.writeln(''); buffer.writeln('');
// fromJson // fromJson
buffer.writeln( buffer
' factory $className.fromJson(Map<String, dynamic> json) =>'); .writeln(' factory $className.fromJson(Map<String, dynamic> json) =>');
buffer.writeln(' _\$${className}FromJson(json);'); buffer.writeln(' _\$${className}FromJson(json);');
buffer.writeln(''); buffer.writeln('');
@ -1741,7 +1727,6 @@ class RetrofitApiGenerator extends BaseGenerator {
_generatedParameterEntities[className] = buffer.toString(); _generatedParameterEntities[className] = buffer.toString();
} }
}
/// ///
final Map<String, String> _generatedParameterEntities = {}; final Map<String, String> _generatedParameterEntities = {};
@ -1767,7 +1752,12 @@ class RetrofitApiGenerator extends BaseGenerator {
/// generate /// generate
void ensureParameterEntitiesGenerated() { void ensureParameterEntitiesGenerated() {
// //
for (final path in document.paths.values) { // V2 V1
//
final sortedPaths = document.paths.values.toList()
..sort((a, b) => a.path.compareTo(b.path));
for (final path in sortedPaths) {
final queryParams = path.parameters final queryParams = path.parameters
.where((p) => p.location == ParameterLocation.query) .where((p) => p.location == ParameterLocation.query)
.toList(); .toList();

View File

@ -1,13 +1,55 @@
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
/// ///
/// ///
class FileUtils { class FileUtils {
///
///
static String resolvePath(String filePath) {
//
if (path.isAbsolute(filePath)) {
return filePath;
}
//
//
final configFile = _findConfigFile();
if (configFile != null) {
final configDir = path.dirname(configFile);
return path.join(configDir, filePath);
}
// 使
return path.join(Directory.current.path, filePath);
}
///
static String? _findConfigFile() {
var currentDir = Directory.current;
final maxDepth = 10;
var depth = 0;
while (depth < maxDepth) {
final configFile = File(path.join(currentDir.path, 'generator_config.yaml'));
if (configFile.existsSync()) {
return configFile.path;
}
final parent = currentDir.parent;
if (parent.path == currentDir.path) {
break;
}
currentDir = parent;
depth++;
}
return null;
}
/// ///
static Future<Directory> ensureDirectoryExists(String dirPath) async { static Future<Directory> ensureDirectoryExists(String dirPath) async {
final directory = Directory(dirPath); final resolvedPath = resolvePath(dirPath);
final directory = Directory(resolvedPath);
if (!await directory.exists()) { if (!await directory.exists()) {
await directory.create(recursive: true); await directory.create(recursive: true);
} }
@ -17,7 +59,8 @@ class FileUtils {
/// ///
static Future<void> safeWriteFile(String filePath, String content) async { static Future<void> safeWriteFile(String filePath, String content) async {
try { try {
final file = File(filePath); final resolvedPath = resolvePath(filePath);
final file = File(resolvedPath);
final directory = file.parent; final directory = file.parent;
// //

View File

@ -386,7 +386,6 @@ class StringUtils {
final copyright = ConfigLoader.getCopyright(); final copyright = ConfigLoader.getCopyright();
return '''// $description return '''// $description
// Swagger API : $source
// $generatorName by $author // $generatorName by $author
// $copyright // $copyright
'''; ''';