diff --git a/.gitea/checker/comparator.py b/.gitea/checker/comparator.py
index b5faeaa..b52b59c 100644
--- a/.gitea/checker/comparator.py
+++ b/.gitea/checker/comparator.py
@@ -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 = (
- '必填'
- if req_required
- else '可选'
- )
- return f'> `{self.param_type}` **{self.param_name}** · {tag}'
-
- if self.change_type == ChangeType.REMOVED:
- return (
- f'【删除】 '
- f'`{self.param_type}` ~~{self.param_name}~~'
- )
- if self.change_type == ChangeType.ADDED:
- tag = (
- '必填'
- if req_required
- else '可选'
- )
- return (
- f'【新增】 '
- f'`{self.param_type}` **{self.param_name}** · {tag}'
- )
- if self.change_type == ChangeType.RENAMED:
- return (
- f'【重命名】 '
- f'`{self.old_type}` {self.old_name} → '
- f'`{self.param_type}` **{self.param_name}**'
- )
- if self.change_type == ChangeType.MODIFIED:
- detail = f' · {self.detail}' if self.detail else ""
- return f'【修改】 **{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
],
diff --git a/.gitea/checker/controller_ast_parser.py b/.gitea/checker/controller_ast_parser.py
index ba76cc1..043bf28 100644
--- a/.gitea/checker/controller_ast_parser.py
+++ b/.gitea/checker/controller_ast_parser.py
@@ -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,
)
)
diff --git a/.gitea/checker/notifier.py b/.gitea/checker/notifier.py
index 3f3ad33..3a37066 100644
--- a/.gitea/checker/notifier.py
+++ b/.gitea/checker/notifier.py
@@ -22,6 +22,46 @@ def truncate_text(text: str, max_length: int = MAX_MD_LENGTH) -> str:
return text[:max_length] + "\n\n... 消息过长,已截断"
+def _format_param_table(changes: List) -> List[str]:
+ """按 model.md 生成参数变更 Markdown 表格。"""
+ if not changes:
+ return ['无']
+ 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('无')
+
+ 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"**该接口已被移除**"])
- detail_lines = ["", "---------------------------------------", "", "## 【接口参数变动详情】", ""]
-
- if report.is_new_endpoint:
- detail_lines.append("### **新增接口参数**")
- else:
- detail_lines.append("### **参数变更明细**")
-
- 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('无')
-
- 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("")
diff --git a/ftb/src/main/java/ftb/test/controller/CultureClockInController.java b/ftb/src/main/java/ftb/test/controller/CultureClockInController.java
index 75a7395..32b3952 100644
--- a/ftb/src/main/java/ftb/test/controller/CultureClockInController.java
+++ b/ftb/src/main/java/ftb/test/controller/CultureClockInController.java
@@ -132,7 +132,7 @@ public class CultureClockInController {
* @return jnpf.base.ActionResult
*/
@GetMapping(value = "/dynamic1")
- public ActionResult getRecordList(@RequestParam(value = "cursorDate", required = false) Boolean cursorDate,
+ public ActionResult 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));