接口类对象解析
This commit is contained in:
@@ -34,6 +34,8 @@ class ParameterChange:
|
|||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
old_description: Optional[str] = None
|
old_description: Optional[str] = None
|
||||||
source: str = "query"
|
source: str = "query"
|
||||||
|
parent_dto: Optional[str] = None
|
||||||
|
body_param_name: Optional[str] = None
|
||||||
|
|
||||||
def _change_tag(self) -> str:
|
def _change_tag(self) -> str:
|
||||||
"""变更类型标签(企微颜色)。"""
|
"""变更类型标签(企微颜色)。"""
|
||||||
@@ -192,6 +194,8 @@ def compare_parameters(
|
|||||||
description=new_p.description,
|
description=new_p.description,
|
||||||
old_description=old_p.description,
|
old_description=old_p.description,
|
||||||
source=new_p.source,
|
source=new_p.source,
|
||||||
|
parent_dto=new_p.parent_dto,
|
||||||
|
body_param_name=new_p.body_param_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -223,6 +227,8 @@ def compare_parameters(
|
|||||||
description=a_param.description,
|
description=a_param.description,
|
||||||
old_description=r_param.description,
|
old_description=r_param.description,
|
||||||
source=a_param.source,
|
source=a_param.source,
|
||||||
|
parent_dto=a_param.parent_dto,
|
||||||
|
body_param_name=a_param.body_param_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
matched_removed.add(r_key)
|
matched_removed.add(r_key)
|
||||||
@@ -239,6 +245,8 @@ def compare_parameters(
|
|||||||
param_type=param.type,
|
param_type=param.type,
|
||||||
description=param.description,
|
description=param.description,
|
||||||
source=param.source,
|
source=param.source,
|
||||||
|
parent_dto=param.parent_dto,
|
||||||
|
body_param_name=param.body_param_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -253,6 +261,8 @@ def compare_parameters(
|
|||||||
required=param.required,
|
required=param.required,
|
||||||
description=param.description,
|
description=param.description,
|
||||||
source=param.source,
|
source=param.source,
|
||||||
|
parent_dto=param.parent_dto,
|
||||||
|
body_param_name=param.body_param_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -411,6 +421,8 @@ def compare_endpoints(
|
|||||||
required=p.required,
|
required=p.required,
|
||||||
description=p.description,
|
description=p.description,
|
||||||
source=p.source,
|
source=p.source,
|
||||||
|
parent_dto=p.parent_dto,
|
||||||
|
body_param_name=p.body_param_name,
|
||||||
)
|
)
|
||||||
for p in ep.parameters
|
for p in ep.parameters
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ from javalang.tree import (
|
|||||||
|
|
||||||
from models import ApiEndpoint, ApiParameter
|
from models import ApiEndpoint, ApiParameter
|
||||||
|
|
||||||
MAPPING_ANNS = {"GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "PatchMapping", "RequestMapping"}
|
# javax.validation 必填注解
|
||||||
|
REQUIRED_FIELD_ANNS = {"NotNull", "NotEmpty", "NotBlank"}
|
||||||
CONTROLLER_ANNS = {"RestController", "Controller"}
|
CONTROLLER_ANNS = {"RestController", "Controller"}
|
||||||
|
|
||||||
# Spring MVC 框架自动注入参数,不属于 API 调用方入参,解析时忽略
|
# Spring MVC 框架自动注入参数,不属于 API 调用方入参,解析时忽略
|
||||||
@@ -281,6 +282,16 @@ def _extract_javadoc_before_line(source: str, target_line: int) -> str:
|
|||||||
return "\n".join(lines[idx : end_idx + 1])
|
return "\n".join(lines[idx : end_idx + 1])
|
||||||
|
|
||||||
|
|
||||||
|
def _field_required(field: FieldDeclaration) -> bool:
|
||||||
|
"""DTO 字段是否必填(@NotNull / @NotEmpty / @NotBlank)。"""
|
||||||
|
if _has_ann(field, "Nullable"):
|
||||||
|
return False
|
||||||
|
for ann in field.annotations or []:
|
||||||
|
if _ann_simple_name(ann) in REQUIRED_FIELD_ANNS:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _lookup_param_description(
|
def _lookup_param_description(
|
||||||
javadoc_params: Dict[str, str], param: FormalParameter, resolved_name: str
|
javadoc_params: Dict[str, str], param: FormalParameter, resolved_name: str
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
@@ -297,13 +308,13 @@ class ControllerAstParser:
|
|||||||
只解析传入的文件,不扫描整个目录(CI 更快)。
|
只解析传入的文件,不扫描整个目录(CI 更快)。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, repo_root: Path, source_dir: Path):
|
def __init__(self, repo_root: Path, source_dirs: List[Path]):
|
||||||
"""
|
"""
|
||||||
:param repo_root: 仓库根目录
|
:param repo_root: 仓库根目录
|
||||||
:param source_dir: Java 源码根目录(repo_root 下的相对路径对应的绝对路径)
|
:param source_dirs: Java 源码根目录列表(用于查找 DTO 等)
|
||||||
"""
|
"""
|
||||||
self.repo_root = repo_root
|
self.repo_root = repo_root
|
||||||
self.source_dir = source_dir
|
self.source_dirs = source_dirs
|
||||||
self._dto_cache: Dict[str, List[ApiParameter]] = {}
|
self._dto_cache: Dict[str, List[ApiParameter]] = {}
|
||||||
self._current_source = ""
|
self._current_source = ""
|
||||||
|
|
||||||
@@ -393,7 +404,13 @@ class ControllerAstParser:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
if _has_ann(param, "RequestBody"):
|
if _has_ann(param, "RequestBody"):
|
||||||
return self._expand_dto(type_name, "body")
|
body_desc = _lookup_param_description(javadoc_params, param, name)
|
||||||
|
return self._expand_dto(
|
||||||
|
type_name,
|
||||||
|
"body",
|
||||||
|
body_param_name=param.name,
|
||||||
|
body_param_desc=body_desc,
|
||||||
|
)
|
||||||
|
|
||||||
description = _lookup_param_description(javadoc_params, param, name)
|
description = _lookup_param_description(javadoc_params, param, name)
|
||||||
return [
|
return [
|
||||||
@@ -406,24 +423,51 @@ class ControllerAstParser:
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
def _expand_dto(self, type_name: str, source: str) -> List[ApiParameter]:
|
def _expand_dto(
|
||||||
"""展开 @RequestBody DTO 字段。"""
|
self,
|
||||||
|
type_name: str,
|
||||||
|
source: str,
|
||||||
|
body_param_name: str = "",
|
||||||
|
body_param_desc: Optional[str] = None,
|
||||||
|
) -> List[ApiParameter]:
|
||||||
|
"""展开 @RequestBody DTO 一级字段。"""
|
||||||
simple = type_name.split(".")[-1].replace(">", "").replace("<", "").strip()
|
simple = type_name.split(".")[-1].replace(">", "").replace("<", "").strip()
|
||||||
if simple in self._dto_cache:
|
cache_key = f"{simple}:{body_param_name}"
|
||||||
return self._dto_cache[simple]
|
if cache_key in self._dto_cache:
|
||||||
|
return self._dto_cache[cache_key]
|
||||||
|
|
||||||
dto_file = self._find_dto_file(simple)
|
dto_file = self._find_dto_file(simple)
|
||||||
if not dto_file:
|
if not dto_file:
|
||||||
result = [ApiParameter(name=simple, type=type_name, required=True, source=source)]
|
result = [
|
||||||
self._dto_cache[simple] = result
|
ApiParameter(
|
||||||
|
name=simple,
|
||||||
|
type=type_name,
|
||||||
|
required=True,
|
||||||
|
source=source,
|
||||||
|
description=body_param_desc,
|
||||||
|
parent_dto=simple,
|
||||||
|
body_param_name=body_param_name or None,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
self._dto_cache[cache_key] = result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dto_source = dto_file.read_text(encoding="utf-8", errors="ignore")
|
dto_source = dto_file.read_text(encoding="utf-8", errors="ignore")
|
||||||
tree = javalang.parse.parse(dto_source)
|
tree = javalang.parse.parse(dto_source)
|
||||||
except (javalang.parser.JavaSyntaxError, OSError):
|
except (javalang.parser.JavaSyntaxError, OSError):
|
||||||
result = [ApiParameter(name=simple, type=type_name, required=True, source=source)]
|
result = [
|
||||||
self._dto_cache[simple] = result
|
ApiParameter(
|
||||||
|
name=simple,
|
||||||
|
type=type_name,
|
||||||
|
required=True,
|
||||||
|
source=source,
|
||||||
|
description=body_param_desc,
|
||||||
|
parent_dto=simple,
|
||||||
|
body_param_name=body_param_name or None,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
self._dto_cache[cache_key] = result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
fields: List[ApiParameter] = []
|
fields: List[ApiParameter] = []
|
||||||
@@ -446,31 +490,47 @@ class ControllerAstParser:
|
|||||||
ApiParameter(
|
ApiParameter(
|
||||||
name=decl.name,
|
name=decl.name,
|
||||||
type=_type_to_str(field.type),
|
type=_type_to_str(field.type),
|
||||||
required=not _has_ann(field, "Nullable"),
|
required=_field_required(field),
|
||||||
source=source,
|
source=source,
|
||||||
description=field_desc,
|
description=field_desc,
|
||||||
|
parent_dto=simple,
|
||||||
|
body_param_name=body_param_name or None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not fields:
|
if not fields:
|
||||||
fields = [ApiParameter(name=simple, type=type_name, required=True, source=source)]
|
fields = [
|
||||||
|
ApiParameter(
|
||||||
|
name=simple,
|
||||||
|
type=type_name,
|
||||||
|
required=True,
|
||||||
|
source=source,
|
||||||
|
description=body_param_desc,
|
||||||
|
parent_dto=simple,
|
||||||
|
body_param_name=body_param_name or None,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
self._dto_cache[simple] = fields
|
self._dto_cache[cache_key] = fields
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def _find_dto_file(self, simple_name: str) -> Optional[Path]:
|
def _find_dto_file(self, simple_name: str) -> Optional[Path]:
|
||||||
"""在源码目录中查找 DTO 文件。"""
|
"""在配置的源码目录及仓库内 src/main/java 中查找 DTO 文件。"""
|
||||||
if not self.source_dir.exists():
|
|
||||||
return None
|
|
||||||
target = f"{simple_name}.java"
|
target = f"{simple_name}.java"
|
||||||
for path in self.source_dir.rglob(target):
|
for source_dir in self.source_dirs:
|
||||||
|
if source_dir.exists():
|
||||||
|
for path in source_dir.rglob(target):
|
||||||
|
return path
|
||||||
|
if self.repo_root.exists():
|
||||||
|
for path in self.repo_root.rglob(target):
|
||||||
|
if "src/main/java" in path.as_posix():
|
||||||
return path
|
return path
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def parse_controller_files(
|
def parse_controller_files(
|
||||||
repo_root: Path,
|
repo_root: Path,
|
||||||
source_subdir: str,
|
source_subdirs: List[str],
|
||||||
file_paths: List[str],
|
file_paths: List[str],
|
||||||
file_contents: Dict[str, str],
|
file_contents: Dict[str, str],
|
||||||
) -> List[ApiEndpoint]:
|
) -> List[ApiEndpoint]:
|
||||||
@@ -478,13 +538,13 @@ def parse_controller_files(
|
|||||||
批量解析指定 Controller 文件(仅解析传入的文件,不全量扫描)。
|
批量解析指定 Controller 文件(仅解析传入的文件,不全量扫描)。
|
||||||
|
|
||||||
:param repo_root: 仓库根目录
|
:param repo_root: 仓库根目录
|
||||||
:param source_subdir: 源码子目录(相对仓库根)
|
:param source_subdirs: 源码子目录列表(相对仓库根)
|
||||||
:param file_paths: 要解析的文件路径列表(相对仓库根)
|
:param file_paths: 要解析的文件路径列表(相对仓库根)
|
||||||
:param file_contents: {文件路径: 源码内容}
|
:param file_contents: {文件路径: 源码内容}
|
||||||
:return: 所有端点
|
:return: 所有端点
|
||||||
"""
|
"""
|
||||||
source_dir = (repo_root / source_subdir).resolve()
|
source_dirs = [(repo_root / sub).resolve() for sub in source_subdirs]
|
||||||
parser = ControllerAstParser(repo_root, source_dir)
|
parser = ControllerAstParser(repo_root, source_dirs)
|
||||||
endpoints: List[ApiEndpoint] = []
|
endpoints: List[ApiEndpoint] = []
|
||||||
|
|
||||||
for path in file_paths:
|
for path in file_paths:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def filter_endpoints_by_files(
|
|||||||
|
|
||||||
def parse_endpoints_from_files(
|
def parse_endpoints_from_files(
|
||||||
repo_root: Path,
|
repo_root: Path,
|
||||||
source_subdir: str,
|
source_subdirs: List[str],
|
||||||
file_paths: List[str],
|
file_paths: List[str],
|
||||||
file_contents: Dict[str, str],
|
file_contents: Dict[str, str],
|
||||||
) -> List[ApiEndpoint]:
|
) -> List[ApiEndpoint]:
|
||||||
@@ -33,11 +33,11 @@ def parse_endpoints_from_files(
|
|||||||
解析指定 Controller 文件,提取接口参数(仅解析传入文件,不全量扫描)。
|
解析指定 Controller 文件,提取接口参数(仅解析传入文件,不全量扫描)。
|
||||||
|
|
||||||
:param repo_root: 仓库根
|
:param repo_root: 仓库根
|
||||||
:param source_subdir: 源码目录(相对仓库根)
|
:param source_subdirs: 源码目录列表(相对仓库根)
|
||||||
:param file_paths: 文件路径列表
|
:param file_paths: 文件路径列表
|
||||||
:param file_contents: 路径 -> 源码内容
|
:param file_contents: 路径 -> 源码内容
|
||||||
:return: ApiEndpoint 列表
|
:return: ApiEndpoint 列表
|
||||||
"""
|
"""
|
||||||
from controller_ast_parser import parse_controller_files
|
from controller_ast_parser import parse_controller_files
|
||||||
|
|
||||||
return parse_controller_files(repo_root, source_subdir, file_paths, file_contents)
|
return parse_controller_files(repo_root, source_subdirs, file_paths, file_contents)
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ def load_config(config_path: Path) -> dict:
|
|||||||
return yaml.safe_load(f) or {}
|
return yaml.safe_load(f) or {}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_source_subdirs(config: dict) -> list:
|
||||||
|
"""从配置解析 Java 源码目录列表(支持 source_dirs 多模块)。"""
|
||||||
|
dirs = config.get("source_dirs")
|
||||||
|
if dirs:
|
||||||
|
return [str(d) for d in dirs]
|
||||||
|
return [config.get("source_dir", "src/main/java")]
|
||||||
|
|
||||||
|
|
||||||
def _read_file_safe(path: Path) -> str:
|
def _read_file_safe(path: Path) -> str:
|
||||||
"""读取文件内容。"""
|
"""读取文件内容。"""
|
||||||
try:
|
try:
|
||||||
@@ -73,7 +81,7 @@ def _load_version_contents(
|
|||||||
|
|
||||||
def parse_changed_endpoints(
|
def parse_changed_endpoints(
|
||||||
repo_root: Path,
|
repo_root: Path,
|
||||||
source_subdir: str,
|
source_subdirs: list,
|
||||||
changed_files: list,
|
changed_files: list,
|
||||||
old_sha: str,
|
old_sha: str,
|
||||||
label: str,
|
label: str,
|
||||||
@@ -86,7 +94,7 @@ def parse_changed_endpoints(
|
|||||||
|
|
||||||
print(f"[AST] 解析 {label} 版本 {len(contents)} 个 Controller 文件")
|
print(f"[AST] 解析 {label} 版本 {len(contents)} 个 Controller 文件")
|
||||||
endpoints = parse_endpoints_from_files(
|
endpoints = parse_endpoints_from_files(
|
||||||
repo_root, source_subdir, changed_files, contents
|
repo_root, source_subdirs, changed_files, contents
|
||||||
)
|
)
|
||||||
print(f"[AST] {label} 版本共 {len(endpoints)} 个接口")
|
print(f"[AST] {label} 版本共 {len(endpoints)} 个接口")
|
||||||
return endpoints_to_map(endpoints)
|
return endpoints_to_map(endpoints)
|
||||||
@@ -111,7 +119,11 @@ def main() -> int:
|
|||||||
config_path = repo_root / config_path
|
config_path = repo_root / config_path
|
||||||
config = load_config(config_path)
|
config = load_config(config_path)
|
||||||
|
|
||||||
source_subdir = config.get("source_dir", "src/main/java")
|
if not config.get("check", {}).get("enabled", True):
|
||||||
|
print("[检查] API 变动检查已关闭(check.enabled=false),跳过。")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
source_subdirs = resolve_source_subdirs(config)
|
||||||
|
|
||||||
commit_info = get_current_commit()
|
commit_info = get_current_commit()
|
||||||
push_user = args.push_user or commit_info.author
|
push_user = args.push_user or commit_info.author
|
||||||
@@ -121,6 +133,8 @@ def main() -> int:
|
|||||||
print("=" * 40)
|
print("=" * 40)
|
||||||
print(f"推送人: {push_user}")
|
print(f"推送人: {push_user}")
|
||||||
print(f"推送时间: {push_time}")
|
print(f"推送时间: {push_time}")
|
||||||
|
print(f"API 变动检查: {config.get('check', {}).get('enabled', True)}")
|
||||||
|
print(f"源码目录: {', '.join(source_subdirs)}")
|
||||||
print(f"LLM 审核: {config.get('llm', {}).get('enabled', True)}")
|
print(f"LLM 审核: {config.get('llm', {}).get('enabled', True)}")
|
||||||
print(f"记录日志: {config.get('log', {}).get('enabled', False)}")
|
print(f"记录日志: {config.get('log', {}).get('enabled', False)}")
|
||||||
print("=" * 40)
|
print("=" * 40)
|
||||||
@@ -142,10 +156,10 @@ def main() -> int:
|
|||||||
git_diff = get_controller_files_diff(prev_sha, commit_info.sha, changed_files)
|
git_diff = get_controller_files_diff(prev_sha, commit_info.sha, changed_files)
|
||||||
|
|
||||||
new_map = parse_changed_endpoints(
|
new_map = parse_changed_endpoints(
|
||||||
repo_root, source_subdir, changed_files, prev_sha, "new"
|
repo_root, source_subdirs, changed_files, prev_sha, "new"
|
||||||
)
|
)
|
||||||
old_map = parse_changed_endpoints(
|
old_map = parse_changed_endpoints(
|
||||||
repo_root, source_subdir, changed_files, prev_sha, "old"
|
repo_root, source_subdirs, changed_files, prev_sha, "old"
|
||||||
)
|
)
|
||||||
|
|
||||||
new_filtered = endpoints_to_map(
|
new_filtered = endpoints_to_map(
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class ApiParameter:
|
|||||||
required: bool = True
|
required: bool = True
|
||||||
source: str = "query"
|
source: str = "query"
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
parent_dto: Optional[str] = None
|
||||||
|
body_param_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import List, Optional
|
from collections import OrderedDict
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from comparator import EndpointChangeReport
|
from comparator import EndpointChangeReport, ParameterChange
|
||||||
|
|
||||||
# 企微 Markdown 单条上限 4096 字符,留余量
|
# 企微 Markdown 单条上限 4096 字符,留余量
|
||||||
MAX_MD_LENGTH = 3800
|
MAX_MD_LENGTH = 3800
|
||||||
@@ -22,8 +23,8 @@ def truncate_text(text: str, max_length: int = MAX_MD_LENGTH) -> str:
|
|||||||
return text[:max_length] + "\n\n<font color=\"comment\">... 消息过长,已截断</font>"
|
return text[:max_length] + "\n\n<font color=\"comment\">... 消息过长,已截断</font>"
|
||||||
|
|
||||||
|
|
||||||
def _format_param_change_list(changes: List) -> List[str]:
|
def _format_param_change_list(changes: List[ParameterChange]) -> List[str]:
|
||||||
"""生成企微友好的参数变更列表(卡片式)。"""
|
"""生成企微友好的普通参数变更列表(卡片式)。"""
|
||||||
if not changes:
|
if not changes:
|
||||||
return ['<font color="comment">无</font>']
|
return ['<font color="comment">无</font>']
|
||||||
lines = ["", f"共 **{len(changes)}** 项变更", ""]
|
lines = ["", f"共 **{len(changes)}** 项变更", ""]
|
||||||
@@ -34,6 +35,52 @@ def _format_param_change_list(changes: List) -> List[str]:
|
|||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def _body_dto_group_key(change: ParameterChange) -> Tuple[str, str]:
|
||||||
|
"""类对象变更分组键:(body 参数名, DTO 类名)。"""
|
||||||
|
return (change.body_param_name or "body", change.parent_dto or "")
|
||||||
|
|
||||||
|
|
||||||
|
def _format_body_field_line(change: ParameterChange, *, is_last: bool) -> List[str]:
|
||||||
|
"""格式化 DTO 一级字段变更行。"""
|
||||||
|
branch = "└─" if is_last else "├─"
|
||||||
|
desc = change.description or change.old_description
|
||||||
|
type_part = f" · `{change.param_type}`" if change.param_type else ""
|
||||||
|
req_part = f" · {change._required_tag()}" if change._required_tag() else ""
|
||||||
|
lines = [f"{branch} `{change.param_name}`{type_part}{req_part} {change._change_tag()}"]
|
||||||
|
if desc:
|
||||||
|
lines.append(f"> 说明:{desc}")
|
||||||
|
if change.change_type.value == "modified" and change.detail:
|
||||||
|
lines.append(f"> 变更:{change.detail}")
|
||||||
|
if change.change_type.value == "renamed":
|
||||||
|
lines.append(f"> `{change.old_name}` → `{change.param_name}`")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def _format_body_dto_groups(changes: List[ParameterChange]) -> List[str]:
|
||||||
|
"""按 DTO 分组展示 @RequestBody 一级字段。"""
|
||||||
|
if not changes:
|
||||||
|
return ['<font color="comment">无</font>']
|
||||||
|
|
||||||
|
groups: OrderedDict[Tuple[str, str], List[ParameterChange]] = OrderedDict()
|
||||||
|
for change in changes:
|
||||||
|
key = _body_dto_group_key(change)
|
||||||
|
groups.setdefault(key, []).append(change)
|
||||||
|
|
||||||
|
lines: List[str] = ["", f"共 **{len(groups)}** 个类对象 · **{len(changes)}** 项字段变更", ""]
|
||||||
|
for (param_name, dto_name), group in groups.items():
|
||||||
|
label = param_name or "body"
|
||||||
|
dto_part = f" · `{dto_name}`" if dto_name else ""
|
||||||
|
lines.append(f"**{label}**{dto_part}")
|
||||||
|
lines.append("")
|
||||||
|
for i, change in enumerate(group):
|
||||||
|
lines.extend(_format_body_field_line(change, is_last=(i == len(group) - 1)))
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if lines and lines[-1] == "":
|
||||||
|
lines.pop()
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def _format_param_details_section(report: EndpointChangeReport) -> List[str]:
|
def _format_param_details_section(report: EndpointChangeReport) -> List[str]:
|
||||||
"""生成接口参数变动详情区块。"""
|
"""生成接口参数变动详情区块。"""
|
||||||
body_changes = [c for c in report.parameter_changes if c.source == "body"]
|
body_changes = [c for c in report.parameter_changes if c.source == "body"]
|
||||||
@@ -41,12 +88,12 @@ def _format_param_details_section(report: EndpointChangeReport) -> List[str]:
|
|||||||
lines = ["", "---------------------------------------", "", "#### 【接口参数变动详情】", ""]
|
lines = ["", "---------------------------------------", "", "#### 【接口参数变动详情】", ""]
|
||||||
|
|
||||||
if body_changes:
|
if body_changes:
|
||||||
lines.append("**类对象变更**")
|
lines.append("**类对象变更(一级字段)**")
|
||||||
lines.extend(_format_param_change_list(body_changes))
|
lines.extend(_format_body_dto_groups(body_changes))
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
if regular_changes:
|
if regular_changes:
|
||||||
# lines.append("**普通参数变更**")
|
lines.append("**普通参数变更**")
|
||||||
lines.extend(_format_param_change_list(regular_changes))
|
lines.extend(_format_param_change_list(regular_changes))
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,17 @@
|
|||||||
# AI-Check 配置文件(位于 .gitea/ 目录,与业务代码解耦)
|
# AI-Check 配置文件(位于 .gitea/ 目录,与业务代码解耦)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
# ---------- API 变动检查 ----------
|
||||||
|
# 总开关:false 时跳过 Controller 接口参数变更检测(不对比、不通知)
|
||||||
|
check:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
# 业务 Java 源码目录(相对仓库根目录)
|
# 业务 Java 源码目录(相对仓库根目录)
|
||||||
# 单模块: src/main/java
|
# 单模块: source_dir: "src/main/java"
|
||||||
# 多模块: ftb/src/main/java
|
# 多模块: 使用 source_dirs(优先于 source_dir)
|
||||||
|
source_dirs:
|
||||||
|
- "jnpf-ftb/jnpf-ftb-biz/src/main/java"
|
||||||
|
- "jnpf-ftb/jnpf-ftb-entity/src/main/java"
|
||||||
source_dir: "ftb/src/main/java"
|
source_dir: "ftb/src/main/java"
|
||||||
|
|
||||||
# ---------- 企业微信机器人 ----------
|
# ---------- 企业微信机器人 ----------
|
||||||
|
|||||||
Reference in New Issue
Block a user