Merge branch 'release/2.1.2'

This commit is contained in:
Max 2025-11-05 18:38:57 +08:00
commit c1a6c94357
10 changed files with 20759 additions and 147 deletions

View File

@ -11,6 +11,9 @@ generator:
# 输入配置
input:
# Swagger 文档源(支持多版本)
# 注意:多个 URL 会按顺序合并,后面的文档会覆盖前面的同名模型和路径
# 因此建议将高版本(如 V2配置在低版本如 V1之后以确保高版本的模型覆盖低版本
# 例如V1 在前V2 在后,那么 V2 的模型会覆盖 V1 的同名模型
swagger_urls: # 完整形式:可以控制每个版本的启用状态
- url: "https://quanxue-test-api.w.23544.com:8843/swagger/v1/swagger.json"
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();
// paths models
//
// V2 V1
SwaggerDocument? mergedDocument;
for (int i = 0; i < SwaggerConfig.swaggerJsonUrls.length; i++) {
final url = SwaggerConfig.swaggerJsonUrls[i];
progress(
' [${i + 1}/${SwaggerConfig.swaggerJsonUrls.length}] 正在解析: $url');
final urls = SwaggerConfig.swaggerJsonUrls;
progress('URL 处理顺序: ${urls.join(" -> ")}');
for (int i = 0; i < urls.length; i++) {
final url = urls[i];
progress(' [${i + 1}/${urls.length}] 正在解析: $url');
final doc = await parser.fetchAndParseSwaggerDocument(url);
progress(' 解析完成: ${doc.models.length} 个模型, ${doc.paths.length} 个路径');
if (mergedDocument == null) {
mergedDocument = doc;
progress(' 初始文档: ${doc.models.length} 个模型');
} else {
// 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(
title: mergedDocument.title,
description: mergedDocument.description,
version: '${mergedDocument.version} + ${doc.version}',
// doc mergedDocument key
// V2 V1
paths: {...mergedDocument.paths, ...doc.paths},
models: {...mergedDocument.models, ...doc.models},
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 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;
@ -135,7 +180,6 @@ class GenerateCommand extends BaseCommand {
useSimpleModels: options.useSimpleModels,
);
final modelsDir = '$fullOutputDir/api_models';
await FileUtils.ensureDirectoryExists(modelsDir);
final modelFiles = generator.generateSeparateModelFiles();
@ -159,7 +203,6 @@ class GenerateCommand extends BaseCommand {
if (options.generateApi) {
progress('正在按版本和tags分组生成Retrofit风格API接口...');
final apiDir = '$fullOutputDir/api';
await FileUtils.ensureDirectoryExists(apiDir);
// 🎯 paths
@ -280,7 +323,6 @@ class GenerateCommand extends BaseCommand {
final parameterEntityFiles =
lastGenerator.generateParameterEntityFiles();
if (parameterEntityFiles.isNotEmpty) {
final modelsDir = '$fullOutputDir/api_models';
// Parameters parameters
final parametersDir = '$modelsDir/parameters';
await FileUtils.ensureDirectoryExists(parametersDir);
@ -307,7 +349,6 @@ class GenerateCommand extends BaseCommand {
// index.dart
if (options.generateModels || options.generateApi) {
progress('正在重新生成 index.dart 文件...');
final modelsDir = '$fullOutputDir/api_models';
final allFiles = await _getAllModelFiles(modelsDir);
final indexContent = _generateUpdatedIndexFile(allFiles);
final indexPath = '$modelsDir/index.dart';
@ -321,7 +362,7 @@ class GenerateCommand extends BaseCommand {
final generator = DocumentationGenerator(document);
final code = generator.generate();
final filePath = '$fullOutputDir/api_documentation.md';
final filePath = '$baseDir/api_documentation.md';
//
if (!ConfigLoader.shouldSkipFile(filePath)) {
@ -334,7 +375,7 @@ class GenerateCommand extends BaseCommand {
}
//
_generateSummary(document, fullOutputDir);
_generateSummary(document, baseDir);
success('代码生成完成!共生成 $generatedFiles 个文件');
return 0;
@ -428,7 +469,13 @@ class GenerateCommand extends BaseCommand {
final fileName = path.basename(entity.path);
// index.dart .g.dart
if (fileName != 'index.dart' && !fileName.endsWith('.g.dart')) {
dartFiles.add(fileName);
// ignored_files
final filePath = path.join(subDir, fileName);
if (!ConfigLoader.shouldSkipFile(filePath)) {
dartFiles.add(fileName);
} else {
progress(' 跳过导出文件: $fileName (在 ignored_files 配置中)');
}
}
}
}
@ -473,6 +520,22 @@ class GenerateCommand extends BaseCommand {
buffer.writeln('library;');
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) {
buffer.writeln('export \'$fileName\';');

View File

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

View File

@ -106,6 +106,10 @@ class ConfigLoader {
/// swagger_urls ()
/// : ["url1", "url2"]
/// : [{url: "...", enabled: true}]
///
/// URL
/// Swagger
/// V2 V1
static List<String> getSwaggerUrls([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
@ -430,4 +434,46 @@ class ConfigLoader {
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/config.dart';
import '../utils/string_utils.dart';
import 'base_generator.dart';
@ -430,6 +431,22 @@ class ModelCodeGenerator extends ModelGenerator {
buffer.writeln('library;');
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
final sortedDirs = modelsByDirectory.keys.toList()..sort();
@ -473,6 +490,22 @@ class ModelCodeGenerator extends ModelGenerator {
buffer.writeln('library;');
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();

View File

@ -137,7 +137,8 @@ class RetrofitApiGenerator extends BaseGenerator {
final fileName = _generateTagFileName(tagName);
//
buffer.writeln(generateFileHeader('$tagName API 接口定义', fileName: fileName));
buffer
.writeln(generateFileHeader('$tagName API 接口定义', fileName: fileName));
buffer.writeln('');
//
@ -154,12 +155,20 @@ class RetrofitApiGenerator extends BaseGenerator {
///
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:io\';');
buffer.writeln('import \'dart:typed_data\';');
buffer.writeln('');
// Dio Retrofit
// package:
if (useDio) {
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:path/path.dart\' as path;');
buffer.writeln('import \'package:http_parser/http_parser.dart\';');
buffer.writeln('');
//
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 使
// api_models/index.dart base_result base_page_result
buffer.writeln('import \'../../api_models/index.dart\';');
buffer.writeln('');
@ -1238,16 +1237,8 @@ class RetrofitApiGenerator extends BaseGenerator {
buffer.writeln('');
//
buffer.writeln(
'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\';');
}
// api_models/index.dart base_result base_page_result
buffer.writeln('import \'../../api_models/index.dart\';');
buffer.writeln('');
//
@ -1369,31 +1360,24 @@ class RetrofitApiGenerator extends BaseGenerator {
/// tag
void _generateTagImports(StringBuffer buffer, List<ApiPath> paths) {
// Dart
// 1. dart:xxx
// 2. package:xxx
// 3. package:project_name
// 4.
//
// package:
if (useDio) {
buffer.writeln('import \'package:dio/dio.dart\';');
}
if (useRetrofit) {
buffer.writeln('import \'package:retrofit/retrofit.dart\';');
}
buffer.writeln('');
//
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 使
// api_models/index.dart base_result base_page_result
buffer.writeln('import \'../../api_models/index.dart\';');
buffer.writeln('');
@ -1663,84 +1647,85 @@ class RetrofitApiGenerator extends BaseGenerator {
///
void _generateParameterEntity(
ApiPath path, String className, List<ApiParameter> queryParams) {
if (!_generatedParameterEntities.containsKey(className)) {
final buffer = StringBuffer();
//
//
// V2 V1
final buffer = StringBuffer();
//
buffer.writeln(generateFileHeader(
'参数实体类 - $className',
fileName: '${StringUtils.toSnakeCase(className)}.dart',
));
buffer.writeln('// 用于 ${path.method.value.toUpperCase()} ${path.path} 的查询参数');
buffer.writeln('');
//
buffer.writeln(generateFileHeader(
'参数实体类 - $className',
fileName: '${StringUtils.toSnakeCase(className)}.dart',
));
buffer
.writeln('// 用于 ${path.method.value.toUpperCase()} ${path.path} 的查询参数');
buffer.writeln('');
//
buffer
.writeln('import \'package:json_annotation/json_annotation.dart\';');
buffer.writeln('');
buffer.writeln('part \'${StringUtils.toSnakeCase(className)}.g.dart\';');
buffer.writeln('');
//
buffer.writeln('import \'package:json_annotation/json_annotation.dart\';');
buffer.writeln('');
buffer.writeln('part \'${StringUtils.toSnakeCase(className)}.g.dart\';');
buffer.writeln('');
//
buffer.writeln('@JsonSerializable(checked: true, includeIfNull: false)');
buffer.writeln('class $className {');
//
buffer.writeln('@JsonSerializable(checked: true, includeIfNull: false)');
buffer.writeln('class $className {');
//
for (final param in queryParams) {
final dartName = StringUtils.toDartPropertyName(param.name);
final dartType = _getDartType(param.type);
final nullable = param.required ? '' : '?';
//
for (final param in queryParams) {
final dartName = StringUtils.toDartPropertyName(param.name);
final dartType = _getDartType(param.type);
final nullable = param.required ? '' : '?';
//
final cleanDescription = param.description
.replaceAll('\r\n', ' ')
.replaceAll('\n', ' ')
.replaceAll('\r', ' ')
.trim();
buffer.writeln(
' /// ${cleanDescription.isNotEmpty ? cleanDescription : param.name}');
buffer.writeln(' @JsonKey(name: \'${param.name}\')');
buffer.writeln(' final $dartType$nullable $dartName;');
buffer.writeln('');
}
//
buffer.writeln(' const $className({');
for (final param in queryParams) {
final dartName = StringUtils.toDartPropertyName(param.name);
final required = param.required ? 'required ' : '';
buffer.writeln(' ${required}this.$dartName,');
}
buffer.writeln(' });');
buffer.writeln('');
// fromJson
//
final cleanDescription = param.description
.replaceAll('\r\n', ' ')
.replaceAll('\n', ' ')
.replaceAll('\r', ' ')
.trim();
buffer.writeln(
' factory $className.fromJson(Map<String, dynamic> json) =>');
buffer.writeln(' _\$${className}FromJson(json);');
' /// ${cleanDescription.isNotEmpty ? cleanDescription : param.name}');
buffer.writeln(' @JsonKey(name: \'${param.name}\')');
buffer.writeln(' final $dartType$nullable $dartName;');
buffer.writeln('');
// toJson
buffer.writeln(
' Map<String, dynamic> toJson() => _\$${className}ToJson(this);');
buffer.writeln('');
// toQueryMap Dio
buffer.writeln(' /// 转换为查询参数 Map');
buffer.writeln(' Map<String, dynamic> toQueryMap() {');
buffer.writeln(' final map = <String, dynamic>{};');
for (final param in queryParams) {
final dartName = StringUtils.toDartPropertyName(param.name);
buffer.writeln(
' if ($dartName != null) map[\'${param.name}\'] = $dartName;');
}
buffer.writeln(' return map;');
buffer.writeln(' }');
buffer.writeln('}');
_generatedParameterEntities[className] = buffer.toString();
}
//
buffer.writeln(' const $className({');
for (final param in queryParams) {
final dartName = StringUtils.toDartPropertyName(param.name);
final required = param.required ? 'required ' : '';
buffer.writeln(' ${required}this.$dartName,');
}
buffer.writeln(' });');
buffer.writeln('');
// fromJson
buffer
.writeln(' factory $className.fromJson(Map<String, dynamic> json) =>');
buffer.writeln(' _\$${className}FromJson(json);');
buffer.writeln('');
// toJson
buffer.writeln(
' Map<String, dynamic> toJson() => _\$${className}ToJson(this);');
buffer.writeln('');
// toQueryMap Dio
buffer.writeln(' /// 转换为查询参数 Map');
buffer.writeln(' Map<String, dynamic> toQueryMap() {');
buffer.writeln(' final map = <String, dynamic>{};');
for (final param in queryParams) {
final dartName = StringUtils.toDartPropertyName(param.name);
buffer.writeln(
' if ($dartName != null) map[\'${param.name}\'] = $dartName;');
}
buffer.writeln(' return map;');
buffer.writeln(' }');
buffer.writeln('}');
_generatedParameterEntities[className] = buffer.toString();
}
///
@ -1767,7 +1752,12 @@ class RetrofitApiGenerator extends BaseGenerator {
/// generate
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
.where((p) => p.location == ParameterLocation.query)
.toList();

View File

@ -1,13 +1,55 @@
import 'dart:io';
import 'package:path/path.dart' as path;
///
///
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 {
final directory = Directory(dirPath);
final resolvedPath = resolvePath(dirPath);
final directory = Directory(resolvedPath);
if (!await directory.exists()) {
await directory.create(recursive: true);
}
@ -17,7 +59,8 @@ class FileUtils {
///
static Future<void> safeWriteFile(String filePath, String content) async {
try {
final file = File(filePath);
final resolvedPath = resolvePath(filePath);
final file = File(resolvedPath);
final directory = file.parent;
//

View File

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