字段说明测试
This commit is contained in:
@@ -31,48 +31,37 @@ class ParameterChange:
|
||||
required: Optional[bool] = None
|
||||
old_required: Optional[bool] = None
|
||||
detail: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
old_description: Optional[str] = None
|
||||
source: str = "query"
|
||||
|
||||
def _escape_cell(self, text: str) -> str:
|
||||
"""表格单元格转义。"""
|
||||
return text.replace("|", "\\|").replace("\n", " ")
|
||||
|
||||
def _change_label(self) -> str:
|
||||
"""变更列文案,对齐 model.md。"""
|
||||
if self.change_type == ChangeType.ADDED:
|
||||
if self.required is False:
|
||||
return "新增可选"
|
||||
return "新增必填"
|
||||
if self.change_type == ChangeType.REMOVED:
|
||||
return "删除"
|
||||
if self.change_type == ChangeType.RENAMED:
|
||||
return f"重命名 {self.old_name} → {self.param_name}"
|
||||
if self.change_type == ChangeType.MODIFIED:
|
||||
return self.detail or "修改"
|
||||
return "-"
|
||||
|
||||
def to_table_row(self) -> str:
|
||||
"""格式化为 model.md 参数变更表格行。"""
|
||||
desc = self._escape_cell(self.description or "-")
|
||||
change = self._escape_cell(self._change_label())
|
||||
return f"| `{self.param_name}` | {desc} | {change} |"
|
||||
|
||||
def to_markdown_line(self, *, plain: bool = False) -> str:
|
||||
"""
|
||||
格式化为企微 Markdown 行。
|
||||
plain=True 时用于新增接口,直接列出参数,不加「新增」前缀。
|
||||
"""
|
||||
req_optional = self.required is False
|
||||
req_required = self.required is True
|
||||
|
||||
if plain and self.change_type == ChangeType.ADDED:
|
||||
tag = (
|
||||
'<font color="warning">必填</font>'
|
||||
if req_required
|
||||
else '<font color="comment">可选</font>'
|
||||
)
|
||||
return f'> `{self.param_type}` **{self.param_name}** · {tag}'
|
||||
|
||||
if self.change_type == ChangeType.REMOVED:
|
||||
return (
|
||||
f'<font color="warning">【删除】</font> '
|
||||
f'`{self.param_type}` ~~{self.param_name}~~'
|
||||
)
|
||||
if self.change_type == ChangeType.ADDED:
|
||||
tag = (
|
||||
'<font color="warning">必填</font>'
|
||||
if req_required
|
||||
else '<font color="comment">可选</font>'
|
||||
)
|
||||
return (
|
||||
f'<font color="info">【新增】</font> '
|
||||
f'`{self.param_type}` **{self.param_name}** · {tag}'
|
||||
)
|
||||
if self.change_type == ChangeType.RENAMED:
|
||||
return (
|
||||
f'<font color="comment">【重命名】</font> '
|
||||
f'`{self.old_type}` {self.old_name} → '
|
||||
f'`{self.param_type}` **{self.param_name}**'
|
||||
)
|
||||
if self.change_type == ChangeType.MODIFIED:
|
||||
detail = f' · <font color="comment">{self.detail}</font>' if self.detail else ""
|
||||
return f'<font color="warning">【修改】</font> **{self.param_name}**{detail}'
|
||||
return f'- {self.param_name}'
|
||||
"""兼容旧调用,委托至表格行。"""
|
||||
return self.to_table_row()
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -113,6 +102,11 @@ def _param_key(p: ApiParameter) -> Tuple[str, str]:
|
||||
return (p.source, p.name)
|
||||
|
||||
|
||||
def _format_type_change(old_type: str, new_type: str) -> str:
|
||||
"""类型变更文案。"""
|
||||
return f"类型由{old_type}改为{new_type}"
|
||||
|
||||
|
||||
def compare_parameters(
|
||||
old_params: List[ApiParameter], new_params: List[ApiParameter]
|
||||
) -> List[ParameterChange]:
|
||||
@@ -143,9 +137,16 @@ def compare_parameters(
|
||||
new_p = new_map[key]
|
||||
detail_parts = []
|
||||
if old_p.type != new_p.type:
|
||||
detail_parts.append(f"类型 {old_p.type} -> {new_p.type}")
|
||||
detail_parts.append(_format_type_change(old_p.type, new_p.type))
|
||||
if old_p.required != new_p.required:
|
||||
detail_parts.append(f"必填 {old_p.required} -> {new_p.required}")
|
||||
req_label = lambda r: "必填" if r else "可选"
|
||||
detail_parts.append(
|
||||
f"必填性由{req_label(old_p.required)}改为{req_label(new_p.required)}"
|
||||
)
|
||||
if old_p.description != new_p.description:
|
||||
detail_parts.append(
|
||||
f"说明由{old_p.description or '-'}改为{new_p.description or '-'}"
|
||||
)
|
||||
if detail_parts:
|
||||
changes.append(
|
||||
ParameterChange(
|
||||
@@ -155,6 +156,9 @@ def compare_parameters(
|
||||
required=new_p.required,
|
||||
old_required=old_p.required,
|
||||
detail=", ".join(detail_parts),
|
||||
description=new_p.description,
|
||||
old_description=old_p.description,
|
||||
source=new_p.source,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -183,6 +187,9 @@ def compare_parameters(
|
||||
old_name=r_param.name,
|
||||
old_type=r_param.type,
|
||||
required=a_param.required,
|
||||
description=a_param.description,
|
||||
old_description=r_param.description,
|
||||
source=a_param.source,
|
||||
)
|
||||
)
|
||||
matched_removed.add(r_key)
|
||||
@@ -197,6 +204,8 @@ def compare_parameters(
|
||||
change_type=ChangeType.REMOVED,
|
||||
param_name=param.name,
|
||||
param_type=param.type,
|
||||
description=param.description,
|
||||
source=param.source,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -209,6 +218,8 @@ def compare_parameters(
|
||||
param_name=param.name,
|
||||
param_type=param.type,
|
||||
required=param.required,
|
||||
description=param.description,
|
||||
source=param.source,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -365,6 +376,8 @@ def compare_endpoints(
|
||||
param_name=p.name,
|
||||
param_type=p.type,
|
||||
required=p.required,
|
||||
description=p.description,
|
||||
source=p.source,
|
||||
)
|
||||
for p in ep.parameters
|
||||
],
|
||||
|
||||
@@ -233,6 +233,64 @@ def _param_required(param: FormalParameter) -> bool:
|
||||
return not _has_ann(param, "Nullable")
|
||||
|
||||
|
||||
JAVADOC_PARAM_RE = re.compile(
|
||||
r"@param\s+(\w+)\s+(.*?)(?=\n\s*\*\s*@|\n\s*\*/|\Z)",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def _clean_javadoc_text(text: str) -> str:
|
||||
"""清理 JavaDoc 行前缀与多余空白。"""
|
||||
cleaned = re.sub(r"\s*\*\s?", " ", text)
|
||||
return re.sub(r"\s+", " ", cleaned).strip()
|
||||
|
||||
|
||||
def _parse_javadoc_params(javadoc: str) -> Dict[str, str]:
|
||||
"""从 JavaDoc 块解析 @param 名称 -> 说明。"""
|
||||
if not javadoc:
|
||||
return {}
|
||||
result: Dict[str, str] = {}
|
||||
for match in JAVADOC_PARAM_RE.finditer(javadoc):
|
||||
name = match.group(1)
|
||||
desc = _clean_javadoc_text(match.group(2))
|
||||
if desc:
|
||||
result[name] = desc
|
||||
return result
|
||||
|
||||
|
||||
def _extract_javadoc_before_line(source: str, target_line: int) -> str:
|
||||
"""
|
||||
提取目标行之前紧邻的 JavaDoc 块。
|
||||
target_line 为 1-indexed(与方法声明行号一致)。
|
||||
"""
|
||||
if not source or target_line <= 1:
|
||||
return ""
|
||||
lines = source.splitlines()
|
||||
idx = target_line - 2
|
||||
while idx >= 0 and not lines[idx].strip():
|
||||
idx -= 1
|
||||
while idx >= 0 and lines[idx].strip().startswith("@"):
|
||||
idx -= 1
|
||||
if idx < 0 or not lines[idx].strip().endswith("*/"):
|
||||
return ""
|
||||
end_idx = idx
|
||||
while idx >= 0 and not lines[idx].strip().startswith("/**"):
|
||||
idx -= 1
|
||||
if idx < 0:
|
||||
return ""
|
||||
return "\n".join(lines[idx : end_idx + 1])
|
||||
|
||||
|
||||
def _lookup_param_description(
|
||||
javadoc_params: Dict[str, str], param: FormalParameter, resolved_name: str
|
||||
) -> Optional[str]:
|
||||
"""按注解名或形参名匹配 JavaDoc @param 说明。"""
|
||||
for key in (resolved_name, param.name):
|
||||
if key and key in javadoc_params:
|
||||
return javadoc_params[key]
|
||||
return None
|
||||
|
||||
|
||||
class ControllerAstParser:
|
||||
"""
|
||||
基于 javalang 的 Controller 解析器。
|
||||
@@ -247,6 +305,7 @@ class ControllerAstParser:
|
||||
self.repo_root = repo_root
|
||||
self.source_dir = source_dir
|
||||
self._dto_cache: Dict[str, List[ApiParameter]] = {}
|
||||
self._current_source = ""
|
||||
|
||||
def parse_file_content(self, source: str, repo_relative_path: str) -> List[ApiEndpoint]:
|
||||
"""
|
||||
@@ -257,6 +316,7 @@ class ControllerAstParser:
|
||||
:return: 端点列表
|
||||
"""
|
||||
endpoints: List[ApiEndpoint] = []
|
||||
self._current_source = source
|
||||
try:
|
||||
tree = javalang.parse.parse(source)
|
||||
except (javalang.parser.JavaSyntaxError, RecursionError) as exc:
|
||||
@@ -300,9 +360,16 @@ class ControllerAstParser:
|
||||
continue
|
||||
|
||||
method_path = _ann_string(ann, "value", "path")
|
||||
javadoc_params: Dict[str, str] = {}
|
||||
if getattr(method, "position", None) and method.position:
|
||||
javadoc = _extract_javadoc_before_line(
|
||||
self._current_source, method.position.line
|
||||
)
|
||||
javadoc_params = _parse_javadoc_params(javadoc)
|
||||
|
||||
params = []
|
||||
for p in method.parameters or []:
|
||||
params.extend(self._extract_param(p))
|
||||
params.extend(self._extract_param(p, javadoc_params))
|
||||
|
||||
return ApiEndpoint(
|
||||
http_method=_http_method(ann_name, ann),
|
||||
@@ -314,10 +381,13 @@ class ControllerAstParser:
|
||||
)
|
||||
return None
|
||||
|
||||
def _extract_param(self, param: FormalParameter) -> List[ApiParameter]:
|
||||
def _extract_param(
|
||||
self, param: FormalParameter, javadoc_params: Optional[Dict[str, str]] = None
|
||||
) -> List[ApiParameter]:
|
||||
"""提取方法参数,@RequestBody 展开 DTO 字段;忽略框架注入参数。"""
|
||||
type_name = _type_to_str(param.type)
|
||||
name = _param_name(param)
|
||||
javadoc_params = javadoc_params or {}
|
||||
|
||||
if _is_framework_param(type_name, name):
|
||||
return []
|
||||
@@ -325,12 +395,14 @@ class ControllerAstParser:
|
||||
if _has_ann(param, "RequestBody"):
|
||||
return self._expand_dto(type_name, "body")
|
||||
|
||||
description = _lookup_param_description(javadoc_params, param, name)
|
||||
return [
|
||||
ApiParameter(
|
||||
name=name,
|
||||
type=type_name,
|
||||
required=_param_required(param),
|
||||
source=_param_source(param),
|
||||
description=description,
|
||||
)
|
||||
]
|
||||
|
||||
@@ -347,7 +419,8 @@ class ControllerAstParser:
|
||||
return result
|
||||
|
||||
try:
|
||||
tree = javalang.parse.parse(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)
|
||||
except (javalang.parser.JavaSyntaxError, OSError):
|
||||
result = [ApiParameter(name=simple, type=type_name, required=True, source=source)]
|
||||
self._dto_cache[simple] = result
|
||||
@@ -360,6 +433,14 @@ class ControllerAstParser:
|
||||
for field in type_decl.fields or []:
|
||||
if "static" in (field.modifiers or []):
|
||||
continue
|
||||
field_javadoc = ""
|
||||
if getattr(field, "position", None) and field.position:
|
||||
field_javadoc = _extract_javadoc_before_line(
|
||||
dto_source, field.position.line
|
||||
)
|
||||
field_desc = _clean_javadoc_text(
|
||||
field_javadoc.replace("/**", "").replace("*/", "").strip()
|
||||
) or None
|
||||
for decl in field.declarators:
|
||||
fields.append(
|
||||
ApiParameter(
|
||||
@@ -367,6 +448,7 @@ class ControllerAstParser:
|
||||
type=_type_to_str(field.type),
|
||||
required=not _has_ann(field, "Nullable"),
|
||||
source=source,
|
||||
description=field_desc,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -22,6 +22,46 @@ def truncate_text(text: str, max_length: int = MAX_MD_LENGTH) -> str:
|
||||
return text[:max_length] + "\n\n<font color=\"comment\">... 消息过长,已截断</font>"
|
||||
|
||||
|
||||
def _format_param_table(changes: List) -> List[str]:
|
||||
"""按 model.md 生成参数变更 Markdown 表格。"""
|
||||
if not changes:
|
||||
return ['<font color="comment">无</font>']
|
||||
lines = [
|
||||
"",
|
||||
"| 字段 | 说明 | 变更 |",
|
||||
"|------|------|------|",
|
||||
]
|
||||
for change in changes:
|
||||
lines.append(change.to_table_row())
|
||||
return lines
|
||||
|
||||
|
||||
def _format_param_details_section(report: EndpointChangeReport) -> List[str]:
|
||||
"""生成接口参数变动详情区块(对齐 model.md)。"""
|
||||
body_changes = [c for c in report.parameter_changes if c.source == "body"]
|
||||
regular_changes = [c for c in report.parameter_changes if c.source != "body"]
|
||||
lines = ["", "---", "", "## 接口参数变动详情", "", "---", ""]
|
||||
|
||||
if body_changes:
|
||||
lines.append("### 类对象变更")
|
||||
lines.append("")
|
||||
lines.append("- **参数变更列表:**")
|
||||
lines.extend(_format_param_table(body_changes))
|
||||
lines.append("")
|
||||
|
||||
if regular_changes:
|
||||
lines.append("### 普通参数变更(非对象字段)")
|
||||
lines.append("")
|
||||
lines.append("- **参数变更列表:**")
|
||||
lines.extend(_format_param_table(regular_changes))
|
||||
lines.append("")
|
||||
|
||||
if not body_changes and not regular_changes:
|
||||
lines.append('<font color="comment">无</font>')
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _format_endpoint_block(report: EndpointChangeReport) -> str:
|
||||
"""
|
||||
格式化单个接口块,按模板匹配格式输出。
|
||||
@@ -41,21 +81,7 @@ def _format_endpoint_block(report: EndpointChangeReport) -> str:
|
||||
if report.is_removed_endpoint:
|
||||
return "\n".join(header + ["", f"<font color=\"warning\">**该接口已被移除**</font>"])
|
||||
|
||||
detail_lines = ["", "---------------------------------------", "", "## 【接口参数变动详情】", ""]
|
||||
|
||||
if report.is_new_endpoint:
|
||||
detail_lines.append("### <font color=\"info\">**新增接口参数**</font>")
|
||||
else:
|
||||
detail_lines.append("### <font color=\"warning\">**参数变更明细**</font>")
|
||||
|
||||
if report.parameter_changes:
|
||||
for change in report.parameter_changes:
|
||||
md = change.to_markdown_line(plain=report.is_new_endpoint)
|
||||
detail_lines.append(md)
|
||||
else:
|
||||
detail_lines.append('<font color="comment">无</font>')
|
||||
|
||||
return "\n".join(header + detail_lines)
|
||||
return "\n".join(header + _format_param_details_section(report))
|
||||
|
||||
|
||||
def build_markdown_notification(
|
||||
@@ -142,7 +168,7 @@ def build_markdown_notification(
|
||||
parts.append(path_md)
|
||||
parts.append("")
|
||||
|
||||
# 4. 普通参数变更(非路径变更)仍使用原有格式
|
||||
# 4. 普通参数变更(非路径变更)仍使用 model.md 格式
|
||||
if changed_reports:
|
||||
parts.append("# 【API参数变更通知】")
|
||||
parts.append(f"- **修改人:** {push_user}")
|
||||
@@ -291,6 +317,9 @@ def send_parameter_change_notification(
|
||||
push_time=push_time,
|
||||
file_name=report.source_file or report.controller_class,
|
||||
)
|
||||
if report.parameter_changes:
|
||||
param_section = "\n".join(_format_param_details_section(report)).strip()
|
||||
md = f"{md}\n\n{param_section}"
|
||||
if _post_wecom_markdown(webhook_url, md):
|
||||
sent += 1
|
||||
print(f"第 {sent} 条通知已发送到企业微信(新增接口)")
|
||||
@@ -325,9 +354,9 @@ def send_parameter_change_notification(
|
||||
|
||||
# ========== 3. 参数变更通知(独立分支) ==========
|
||||
if changed_reports:
|
||||
# 构建参数变更通知(只包含参数变更报告)
|
||||
# 构建参数变更通知(只包含参数变更报告,对齐 model.md)
|
||||
parts: List[str] = []
|
||||
parts.append("# API参数变更通知")
|
||||
parts.append("# 【API参数变更通知】")
|
||||
parts.append(f"- **修改人:** {push_user if push_user.startswith('@') else '@' + push_user}")
|
||||
parts.append(f"- **修改时间:** {push_time}")
|
||||
parts.append("")
|
||||
|
||||
@@ -132,7 +132,7 @@ public class CultureClockInController {
|
||||
* @return jnpf.base.ActionResult<jnpf.model.culture.vo.RecordListVo>
|
||||
*/
|
||||
@GetMapping(value = "/dynamic1")
|
||||
public ActionResult<RecordListVo> getRecordList(@RequestParam(value = "cursorDate", required = false) Boolean cursorDate,
|
||||
public ActionResult<RecordListVo> getRecordList(@RequestParam(value = "cursorDate1", required = false) Boolean cursorDate,
|
||||
@RequestParam(value = "limitNum", required = false, defaultValue = "10") Integer limitNum) {
|
||||
|
||||
limitNum = Math.max(10, Math.min(limitNum, 30));
|
||||
|
||||
Reference in New Issue
Block a user