字段说明测试

This commit is contained in:
2026-06-05 15:42:29 +08:00
parent 77479a40a1
commit 021fc8d5e3
4 changed files with 188 additions and 64 deletions

View File

@@ -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
],

View File

@@ -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,
)
)

View File

@@ -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("")

View File

@@ -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));