diff --git a/README.md b/README.md index be4ac22..1fb21f1 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ void main() async { #### 高级选项 - `--included-tags` / `-i`: 只生成指定 tags 的 API 和模型 +- `--excluded-tags` / `-e`: 从生成中排除指定的 tags **示例:** ```bash diff --git a/generator_config.template.yaml b/generator_config.template.yaml index ece96f9..dc24c64 100644 --- a/generator_config.template.yaml +++ b/generator_config.template.yaml @@ -46,6 +46,14 @@ output: # - "Pet" # - "Store" + # 从代码生成中排除指定的 tags + # 适用于内部、废弃或不需要的 API + # 如果一个 endpoint 的所有 tags 都被排除,则该 endpoint 不会生成 + excluded_tags: + # - "Internal" + # - "Deprecated" + # - "Legacy" + # 跳过的目录列表(这些目录下的文件将不会被生成) # 支持相对路径和绝对路径,支持目录名或完整路径 ignored_directories: @@ -94,10 +102,15 @@ generation: default_version: "v1" # 基础类型配置(根据您的项目调整) - base_result_type: "BaseResult" - base_page_result_type: "BasePageResult" - base_result_import: "package:your_project/common/models/base_result.dart" - base_page_result_import: "package:your_project/common/models/base_page_result.dart" + # 如果您的项目有统一的响应模型,请在此处配置 + base_result_type: "BaseResult" # 基础响应模型名称 + base_page_result_type: "BasePageResult" # 分页响应模型名称 + + # 基础模型的导入路径(可选) + # 如果提供了路径,将在 api_models/index.dart 中自动导出 + # 如果留空,则不会生成导出语句 + base_result_import: "" # 例如: "package:your_project/common/models/base_result.dart" + base_page_result_import: "" # 例如: "package:your_project/common/models/base_page_result.dart" # 方法命名 method_naming: "camelCase" # camelCase, snake_case diff --git a/lib/commands/generate_command.dart b/lib/commands/generate_command.dart index 2d1d925..85db41f 100644 --- a/lib/commands/generate_command.dart +++ b/lib/commands/generate_command.dart @@ -75,6 +75,12 @@ class GenerateCommand extends BaseCommand { description: '只生成指定tags的API和模型(逗号分隔,如:User,Pet,Store)', type: OptionType.string, ), + const CommandOption( + name: 'excluded-tags', + shortName: 'e', + description: '从生成中排除指定的tags(逗号分隔)', + type: OptionType.string, + ), ]; @override @@ -161,9 +167,12 @@ class GenerateCommand extends BaseCommand { // 解析生成选项 final options = _parseGenerateOptions(parsedArgs); - // 根据 includedTags 过滤文档 - final document = - _filterDocumentByTags(mergedDocument, options.includedTags); + // 根据 includedTags 和 excludedTags 过滤文档 + final document = _filterDocumentByTags( + mergedDocument, + options.includedTags, + options.excludedTags, + ); // 使用配置的输出目录 final baseDir = SwaggerConfig.generatorDir; @@ -233,6 +242,14 @@ class GenerateCommand extends BaseCommand { progress(' 正在生成 $version 版本 API(${versionPaths.length} 个接口)...'); + // 筛选出当前版本实际使用的 controllers + final versionTags = versionPaths.expand((p) => p.tags).toSet(); + final versionControllers = { + for (var tag in versionTags) + if (document.controllers.containsKey(tag)) + tag: document.controllers[tag]! + }; + // 创建该版本的临时文档 final versionDocument = SwaggerDocument( title: document.title, @@ -240,7 +257,7 @@ class GenerateCommand extends BaseCommand { version: document.version, paths: {for (var p in versionPaths) p.path: p}, models: document.models, - controllers: document.controllers, + controllers: versionControllers, // 使用过滤后的 controllers ); // 创建生成器(使用配置的类名) @@ -439,6 +456,29 @@ class GenerateCommand extends BaseCommand { progress('📂 [配置文件] 按 tags 分组: ${splitByTags ? "是" : "否"}'); } + // 解析 excluded-tags 参数 + // 优先级:命令行参数 > 配置文件 + List? excludedTags; + final excludedTagsStr = args.getOption('excluded-tags'); + if (excludedTagsStr != null && excludedTagsStr.isNotEmpty) { + // 从命令行参数读取 + excludedTags = excludedTagsStr + .split(',') + .map((tag) => tag.trim()) + .where((tag) => tag.isNotEmpty) + .toList(); + + if (excludedTags.isNotEmpty) { + progress('🚫 [命令行] 排除以下 tags: ${excludedTags.join(", ")}'); + } + } else { + // 从配置文件读取 + excludedTags = ConfigLoader.getExcludedTags(); + if (excludedTags != null && excludedTags.isNotEmpty) { + progress('🚫 [配置文件] 排除以下 tags: ${excludedTags.join(", ")}'); + } + } + return GenerateOptions( generateModels: hasAnyFlag ? (args.getOption('models') ?? false) @@ -452,6 +492,7 @@ class GenerateCommand extends BaseCommand { useSimpleModels: args.getOption('simple') ?? false, splitByTags: splitByTags, includedTags: includedTags, + excludedTags: excludedTags, ); } @@ -832,46 +873,60 @@ class GenerateCommand extends BaseCommand { await FileUtils.writeFile(indexPath, buffer.toString()); } - /// 根据 includedTags 过滤文档 - /// 如果 includedTags 为 null 或空,返回原文档 - /// 否则只保留包含指定 tags 的 paths 和相关的 models + /// 根据 includedTags 和 excludedTags 过滤文档 SwaggerDocument _filterDocumentByTags( SwaggerDocument document, List? includedTags, + List? excludedTags, ) { - // 如果没有指定 tags,返回原文档 - if (includedTags == null || includedTags.isEmpty) { + final hasIncludes = includedTags != null && includedTags.isNotEmpty; + final hasExcludes = excludedTags != null && excludedTags.isNotEmpty; + + // 如果没有指定任何过滤条件,返回原文档 + if (!hasIncludes && !hasExcludes) { return document; } - progress('🔍 过滤文档,只保留 tags: ${includedTags.join(", ")}'); + progress('🔍 正在根据 tags 过滤文档...'); + if (hasIncludes) progress(' 只保留 tags: ${includedTags.join(", ")}'); + if (hasExcludes) progress(' 排除 tags: ${excludedTags.join(", ")}'); - // 过滤 paths:只保留包含指定 tags 的 paths + // 过滤 paths final filteredPaths = {}; final usedModelNames = {}; for (final entry in document.paths.entries) { final path = entry.value; + final pathTags = path.tags; - // 检查该 path 是否包含任何指定的 tag - final hasIncludedTag = path.tags.any((tag) => includedTags.contains(tag)); - - if (hasIncludedTag) { - filteredPaths[entry.key] = path; - - // 收集该 path 使用的所有 model 名称 - _collectUsedModels(path, usedModelNames); + // 1. Inclusion check: 如果提供了 included_tags,则路径必须至少有一个 tag 在列表中 + final included = + !hasIncludes || pathTags.any((tag) => includedTags.contains(tag)); + if (!included) { + continue; // 不满足包含条件,跳过 } + + // 2. Exclusion check: 如果提供了 excluded_tags,则路径的所有 tags 不能都在排除列表中 + // 换句话说,如果路径的所有 tags 都在排除列表中,则排除该路径。 + final excluded = hasExcludes && + pathTags.isNotEmpty && + pathTags.every((tag) => excludedTags.contains(tag)); + if (excluded) { + continue; // 满足排除条件,跳过 + } + + // 如果路径通过了所有检查,则保留它 + filteredPaths[entry.key] = path; + _collectUsedModels(path, usedModelNames); } progress(' 保留了 ${filteredPaths.length}/${document.paths.length} 个接口'); - // 过滤 models:只保留被使用的 models + // 过滤 models:只保留被使用的 models (此逻辑与之前相同) final filteredModels = {}; final modelsToCheck = Set.from(usedModelNames); final checkedModels = {}; - // 递归收集所有依赖的 models while (modelsToCheck.isNotEmpty) { final modelName = modelsToCheck.first; modelsToCheck.remove(modelName); @@ -885,19 +940,25 @@ class GenerateCommand extends BaseCommand { final model = document.models[modelName]; if (model != null) { filteredModels[modelName] = model; - - // 收集该 model 依赖的其他 models _collectModelDependencies(model, modelsToCheck, checkedModels); } } progress(' 保留了 ${filteredModels.length}/${document.models.length} 个模型'); - // 过滤 controllers:只保留包含指定 tags 的 controllers + // 过滤 controllers final filteredControllers = {}; for (final entry in document.controllers.entries) { - if (includedTags.contains(entry.key)) { - filteredControllers[entry.key] = entry.value; + final tagName = entry.key; + bool shouldKeep = true; + if (hasIncludes && !includedTags.contains(tagName)) { + shouldKeep = false; + } + if (hasExcludes && excludedTags.contains(tagName)) { + shouldKeep = false; + } + if (shouldKeep) { + filteredControllers[tagName] = entry.value; } } @@ -920,40 +981,55 @@ class GenerateCommand extends BaseCommand { /// 收集 ApiPath 使用的所有 model 名称 void _collectUsedModels(ApiPath path, Set usedModelNames) { + // 递归地从 schema 中提取模型名称 + void extractModelsFromSchema(Map schema) { + if (schema.containsKey('\$ref')) { + final modelName = _extractModelNameFromRef(schema['\$ref']); + if (modelName != null) { + usedModelNames.add(modelName); + } + return; + } + + if (schema.containsKey('type')) { + final type = schema['type']; + if (type == 'array' && schema.containsKey('items')) { + extractModelsFromSchema(schema['items']); + } else if (type == 'object' && schema.containsKey('properties')) { + final properties = schema['properties'] as Map; + for (final propSchema in properties.values) { + extractModelsFromSchema(propSchema); + } + } + } + + for (final key in ['allOf', 'anyOf', 'oneOf']) { + if (schema.containsKey(key)) { + final subSchemas = schema[key] as List; + for (final subSchema in subSchemas) { + extractModelsFromSchema(subSchema); + } + } + } + } + // 从 requestBody 收集 if (path.requestBody != null) { - final content = path.requestBody!.content; - for (final mediaType in content.values) { + for (final mediaType in path.requestBody!.content.values) { if (mediaType.schema != null) { - final ref = mediaType.schema!['\$ref'] as String?; - if (ref != null) { - final modelName = _extractModelNameFromRef(ref); - if (modelName != null) { - usedModelNames.add(modelName); - } - } + extractModelsFromSchema(mediaType.schema!); } } } // 从 responses 收集 for (final response in path.responses.values) { - final content = response.content; - for (final mediaType in content.values) { + for (final mediaType in response.content.values) { if (mediaType.schema != null) { - final ref = mediaType.schema!['\$ref'] as String?; - if (ref != null) { - final modelName = _extractModelNameFromRef(ref); - if (modelName != null) { - usedModelNames.add(modelName); - } - } + extractModelsFromSchema(mediaType.schema!); } } } - - // 从 parameters 收集 - ApiParameter 没有 schema 字段,跳过 - // parameters 通常是基本类型,不需要收集 } /// 收集 ApiModel 依赖的其他 models @@ -1019,6 +1095,7 @@ class GenerateOptions { final bool useSimpleModels; final bool splitByTags; final List? includedTags; + final List? excludedTags; const GenerateOptions({ required this.generateModels, @@ -1027,5 +1104,6 @@ class GenerateOptions { required this.useSimpleModels, required this.splitByTags, this.includedTags, + this.excludedTags, }); } diff --git a/lib/core/config_loader.dart b/lib/core/config_loader.dart index 7b48632..72859ef 100644 --- a/lib/core/config_loader.dart +++ b/lib/core/config_loader.dart @@ -438,43 +438,17 @@ class ConfigLoader { /// 获取 BaseResult 导入路径 static String getBaseResultImport([Map? 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?; - if (generation == null) { - return 'package:learning_officer_oa/common/models/common/base_result.dart'; - } - - final api = generation['api'] as Map?; - 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'; + final generation = cfg?['generation'] as Map?; + final api = generation?['api'] as Map?; + return api?['base_result_import'] as String? ?? ''; } /// 获取 BasePageResult 导入路径 static String getBasePageResultImport([Map? 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?; - if (generation == null) { - return 'package:learning_officer_oa/common/models/common/base_page_result.dart'; - } - - final api = generation['api'] as Map?; - 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'; + final generation = cfg?['generation'] as Map?; + final api = generation?['api'] as Map?; + return api?['base_page_result_import'] as String? ?? ''; } /// 获取 API Client 类名 @@ -556,6 +530,33 @@ class ConfigLoader { return result.isEmpty ? null : result; } + /// 获取排除的 tags 列表 + /// 从配置文件的 output.excluded_tags 读取 + /// 如果未配置,返回 null + static List? getExcludedTags([Map? config]) { + final cfg = config ?? loadConfig(); + if (cfg == null) { + return null; + } + + final output = cfg['output'] as Map?; + if (output == null) { + return null; + } + + final excludedTags = output['excluded_tags']; + if (excludedTags is! List) { + return null; + } + + final result = excludedTags + .map((tag) => tag.toString().trim()) + .where((tag) => tag.isNotEmpty) + .toList(); + + return result.isEmpty ? null : result; + } + /// 获取是否按 tags 分组生成 API 文件 /// 从配置文件的 output.split_by_tags 读取 /// 默认: true diff --git a/lib/generators/retrofit_api_generator.dart b/lib/generators/retrofit_api_generator.dart index d3d2f03..37c1ecb 100644 --- a/lib/generators/retrofit_api_generator.dart +++ b/lib/generators/retrofit_api_generator.dart @@ -169,13 +169,12 @@ class RetrofitApiGenerator extends BaseGenerator { buffer.writeln(''); // package: 第三方库导入(按字母顺序) - if (useDio) { - buffer.writeln('import \'package:dio/dio.dart\';'); - } if (useRetrofit) { buffer.writeln('import \'package:retrofit/retrofit.dart\';'); buffer .writeln('import \'package:json_annotation/json_annotation.dart\';'); + } else if (useDio) { + buffer.writeln('import \'package:dio/dio.dart\';'); } // 其他工具包导入 @@ -1368,11 +1367,10 @@ class RetrofitApiGenerator extends BaseGenerator { // 每组之间用空行分隔 // package: 第三方库导入(按字母顺序) - if (useDio) { - buffer.writeln('import \'package:dio/dio.dart\';'); - } if (useRetrofit) { buffer.writeln('import \'package:retrofit/retrofit.dart\';'); + } else if (useDio) { + buffer.writeln('import \'package:dio/dio.dart\';'); } buffer.writeln(''); @@ -1430,18 +1428,6 @@ class RetrofitApiGenerator extends BaseGenerator { return '${StringUtils.toPascalCase(tagName)}Api'; } - /// 检查整个文档是否需要导入分页相关类型 - bool _needsPaginationImportForDocument() { - for (final path in document.paths.values) { - final returnType = _generateReturnType(path); - // 检查返回类型是否包含 BasePageResult - if (returnType.contains('BasePageResult')) { - return true; - } - } - return false; - } - /// 检查是否需要请求体 bool _needsRequestBody(ApiPath path) { // 如果有明确定义的 requestBody,则需要 @@ -1497,18 +1483,6 @@ class RetrofitApiGenerator extends BaseGenerator { return false; } - /// 检查指定路径列表是否需要导入分页相关类型 - bool _needsPaginationImport(List paths) { - for (final path in paths) { - final returnType = _generateReturnType(path); - // 检查返回类型是否包含 BasePageResult - if (returnType.contains('BasePageResult')) { - return true; - } - } - return false; - } - /// 获取指定路径列表所需的模型导入 Set _getRequiredModelImportsForPaths(List paths) { final imports = {};