This commit is contained in:
@@ -16,11 +16,11 @@
|
|||||||
每条变更占**一行**,标签、说明、类型横向排列,冒号后两空格:
|
每条变更占**一行**,标签、说明、类型横向排列,冒号后两空格:
|
||||||
|
|
||||||
```
|
```
|
||||||
> **共 <font color="warning">4</font> 项变更**
|
> **共 <font color="warning">2</font> 项变更**
|
||||||
|
|
||||||
> <font color="warning">[修改]</font> `taskId` 说明: <font color="comment">流程主键</font> 类型: <font color="warning">Integer</font> → <font color="info">String</font>
|
> <font color="warning">[重命名]</font> <font color="comment">taskId</font> → <font color="info">taskIds</font> 说明: <font color="comment">流程主键</font>
|
||||||
|
|
||||||
> <font color="info">[新增]</font> `storeId` 说明: <font color="comment">门店ID</font>
|
> <font color="warning">[删除]</font> `changeUserNickName` 说明: <font color="comment">变更人员别名</font>
|
||||||
```
|
```
|
||||||
|
|
||||||
| 操作 | 标签 | 类型段 |
|
| 操作 | 标签 | 类型段 |
|
||||||
@@ -28,6 +28,14 @@
|
|||||||
| 新增 | info `[新增]` | 无 |
|
| 新增 | info `[新增]` | 无 |
|
||||||
| 删除 | warning `[删除]` | 无 |
|
| 删除 | warning `[删除]` | 无 |
|
||||||
| 修改 | warning `[修改]` | 仅类型变化时出现 |
|
| 修改 | warning `[修改]` | 仅类型变化时出现 |
|
||||||
|
| 重命名 | warning `[重命名]` | 说明匹配时合并删除+新增;类型变化时附带类型行 |
|
||||||
|
|
||||||
|
### 重命名配对规则
|
||||||
|
|
||||||
|
- 删除+新增且**类型相同、说明相同**(非空)→ `[重命名]`
|
||||||
|
- 删除+新增且**说明相同但类型不同** → `[重命名]` + 类型行
|
||||||
|
- 说明均为空时也可配对
|
||||||
|
- 说明不同则不配对,保持删除+新增
|
||||||
|
|
||||||
- 统计行加粗,数字用 warning(橙色),文案为「共 N 项变更」(不含「字段」)
|
- 统计行加粗,数字用 warning(橙色),文案为「共 N 项变更」(不含「字段」)
|
||||||
- 多条变更之间用**空行**分隔
|
- 多条变更之间用**空行**分隔
|
||||||
@@ -43,5 +51,5 @@
|
|||||||
## 实现
|
## 实现
|
||||||
|
|
||||||
- `ClassFieldParser.extractFieldLabel()`
|
- `ClassFieldParser.extractFieldLabel()`
|
||||||
- `FieldDiffEngine` — 仅类型变化产生 `[修改]`
|
- `FieldDiffEngine` — 类型变化产生 `[修改]`,说明匹配的删除+新增合并为 `[重命名]`
|
||||||
- `WeComNotifier.formatFieldChange()`
|
- `WeComNotifier.formatFieldChange()`
|
||||||
|
|||||||
@@ -5,37 +5,146 @@ import com.aicheck.model.FieldInfo;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对比新旧字段列表,产出新增/删除/类型修改(纯注释变更忽略)。
|
* 对比新旧字段列表,产出新增/删除/类型修改/重命名(纯注释变更忽略)。
|
||||||
*/
|
*/
|
||||||
public class FieldDiffEngine {
|
public class FieldDiffEngine {
|
||||||
|
|
||||||
/** 按字段名对比,仅类型变化记为 MODIFIED */
|
/**
|
||||||
|
* 按字段名对比;删除+新增且说明匹配时合并为重命名。
|
||||||
|
* 输出顺序:按新字段声明顺序,未配对的删除字段置于末尾。
|
||||||
|
*/
|
||||||
public List<FieldChange> diff(List<FieldInfo> oldFields, List<FieldInfo> newFields) {
|
public List<FieldChange> diff(List<FieldInfo> oldFields, List<FieldInfo> newFields) {
|
||||||
Map<String, FieldInfo> oldMap = toMap(oldFields);
|
Map<String, FieldInfo> oldMap = toMap(oldFields);
|
||||||
Map<String, FieldInfo> newMap = toMap(newFields);
|
Map<String, FieldInfo> newMap = toMap(newFields);
|
||||||
List<FieldChange> changes = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Map.Entry<String, FieldInfo> entry : newMap.entrySet()) {
|
List<FieldChange> modified = new ArrayList<>();
|
||||||
FieldInfo oldField = oldMap.get(entry.getKey());
|
List<FieldInfo> added = new ArrayList<>();
|
||||||
FieldInfo newField = entry.getValue();
|
List<FieldInfo> removed = new ArrayList<>();
|
||||||
|
|
||||||
|
for (FieldInfo newField : newFields) {
|
||||||
|
FieldInfo oldField = oldMap.get(newField.getName());
|
||||||
if (oldField == null) {
|
if (oldField == null) {
|
||||||
changes.add(FieldChange.added(newField));
|
added.add(newField);
|
||||||
} else if (!oldField.getType().equals(newField.getType())) {
|
} else if (!oldField.getType().equals(newField.getType())) {
|
||||||
changes.add(FieldChange.modified(oldField, newField, buildTypeDetail(oldField, newField)));
|
modified.add(FieldChange.modified(oldField, newField, buildTypeDetail(oldField, newField)));
|
||||||
}
|
}
|
||||||
// 仅 @Schema / 注释文案变化:不纳入字段变更
|
// 仅 @Schema / 注释文案变化:不纳入字段变更
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Map.Entry<String, FieldInfo> entry : oldMap.entrySet()) {
|
for (FieldInfo oldField : oldFields) {
|
||||||
if (!newMap.containsKey(entry.getKey())) {
|
if (!newMap.containsKey(oldField.getName())) {
|
||||||
changes.add(FieldChange.removed(entry.getValue()));
|
removed.add(oldField);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return changes;
|
|
||||||
|
List<FieldChange> renamed = pairRenames(removed, added);
|
||||||
|
return mergeInOrder(newFields, renamed, modified, added, removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将删除+新增配对为字段重命名。
|
||||||
|
* 优先:说明相同且类型相同;其次:说明相同但类型不同(重命名+改类型)。
|
||||||
|
*/
|
||||||
|
private List<FieldChange> pairRenames(List<FieldInfo> removed, List<FieldInfo> added) {
|
||||||
|
List<FieldChange> renames = new ArrayList<>();
|
||||||
|
Set<FieldInfo> matchedRemoved = new LinkedHashSet<>();
|
||||||
|
Set<FieldInfo> matchedAdded = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
for (FieldInfo oldField : removed) {
|
||||||
|
FieldInfo pair = findRenamePair(oldField, added, matchedAdded, true);
|
||||||
|
if (pair == null) {
|
||||||
|
pair = findRenamePair(oldField, added, matchedAdded, false);
|
||||||
|
}
|
||||||
|
if (pair != null) {
|
||||||
|
renames.add(FieldChange.renamed(oldField, pair));
|
||||||
|
matchedRemoved.add(oldField);
|
||||||
|
matchedAdded.add(pair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removed.removeIf(matchedRemoved::contains);
|
||||||
|
added.removeIf(matchedAdded::contains);
|
||||||
|
return renames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FieldInfo findRenamePair(FieldInfo removed, List<FieldInfo> added,
|
||||||
|
Set<FieldInfo> excluded, boolean requireSameType) {
|
||||||
|
for (FieldInfo candidate : added) {
|
||||||
|
if (excluded.contains(candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!descriptionsMatch(removed, candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (requireSameType && !removed.getType().equals(candidate.getType())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!requireSameType && removed.getType().equals(candidate.getType())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 说明相同(非空)或双方均为空时视为匹配 */
|
||||||
|
private boolean descriptionsMatch(FieldInfo oldField, FieldInfo newField) {
|
||||||
|
String oldDesc = normalizeDescription(oldField.getDescription());
|
||||||
|
String newDesc = normalizeDescription(newField.getDescription());
|
||||||
|
if (oldDesc.isEmpty() && newDesc.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (oldDesc.isEmpty() || newDesc.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return oldDesc.equals(newDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeDescription(String description) {
|
||||||
|
return description == null ? "" : description.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按新字段声明顺序合并各变更类型 */
|
||||||
|
private List<FieldChange> mergeInOrder(List<FieldInfo> newFields, List<FieldChange> renamed,
|
||||||
|
List<FieldChange> modified, List<FieldInfo> added,
|
||||||
|
List<FieldInfo> removed) {
|
||||||
|
Map<String, FieldChange> renamedByNewName = new LinkedHashMap<>();
|
||||||
|
for (FieldChange change : renamed) {
|
||||||
|
renamedByNewName.put(change.getFieldName(), change);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, FieldChange> modifiedByName = new LinkedHashMap<>();
|
||||||
|
for (FieldChange change : modified) {
|
||||||
|
modifiedByName.put(change.getFieldName(), change);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> emitted = new LinkedHashSet<>();
|
||||||
|
List<FieldChange> result = new ArrayList<>();
|
||||||
|
|
||||||
|
for (FieldInfo newField : newFields) {
|
||||||
|
String name = newField.getName();
|
||||||
|
if (renamedByNewName.containsKey(name)) {
|
||||||
|
result.add(renamedByNewName.get(name));
|
||||||
|
emitted.add(name);
|
||||||
|
} else if (modifiedByName.containsKey(name)) {
|
||||||
|
result.add(modifiedByName.get(name));
|
||||||
|
emitted.add(name);
|
||||||
|
} else if (added.stream().anyMatch(f -> f.getName().equals(name))) {
|
||||||
|
result.add(FieldChange.added(newField));
|
||||||
|
emitted.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (FieldInfo oldField : removed) {
|
||||||
|
result.add(FieldChange.removed(oldField));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 字段列表转 LinkedHashMap,保持声明顺序 */
|
/** 字段列表转 LinkedHashMap,保持声明顺序 */
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
package com.aicheck.model;
|
package com.aicheck.model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字段级 diff 结果,用于通知中的 [新增]/[删除]/[修改] 行。
|
* 字段级 diff 结果,用于通知中的 [新增]/[删除]/[修改]/[重命名] 行。
|
||||||
*/
|
*/
|
||||||
public class FieldChange {
|
public class FieldChange {
|
||||||
/** 字段变更种类 */
|
/** 字段变更种类 */
|
||||||
public enum ChangeKind {
|
public enum ChangeKind {
|
||||||
ADDED, REMOVED, MODIFIED
|
ADDED, REMOVED, MODIFIED, RENAMED
|
||||||
}
|
}
|
||||||
|
|
||||||
private final ChangeKind kind;
|
private final ChangeKind kind;
|
||||||
private final String fieldName;
|
private final String fieldName;
|
||||||
|
private final String oldFieldName;
|
||||||
private final String description;
|
private final String description;
|
||||||
private final String oldType;
|
private final String oldType;
|
||||||
private final String newType;
|
private final String newType;
|
||||||
private final String oldDescription;
|
private final String oldDescription;
|
||||||
private final String detail;
|
private final String detail;
|
||||||
|
|
||||||
private FieldChange(ChangeKind kind, String fieldName, String description,
|
private FieldChange(ChangeKind kind, String fieldName, String oldFieldName, String description,
|
||||||
String oldType, String newType, String oldDescription, String detail) {
|
String oldType, String newType, String oldDescription, String detail) {
|
||||||
this.kind = kind;
|
this.kind = kind;
|
||||||
this.fieldName = fieldName;
|
this.fieldName = fieldName;
|
||||||
|
this.oldFieldName = oldFieldName;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.oldType = oldType;
|
this.oldType = oldType;
|
||||||
this.newType = newType;
|
this.newType = newType;
|
||||||
@@ -30,22 +32,32 @@ public class FieldChange {
|
|||||||
|
|
||||||
/** 构造新增字段变更 */
|
/** 构造新增字段变更 */
|
||||||
public static FieldChange added(FieldInfo field) {
|
public static FieldChange added(FieldInfo field) {
|
||||||
return new FieldChange(ChangeKind.ADDED, field.getName(), field.getDescription(),
|
return new FieldChange(ChangeKind.ADDED, field.getName(), null, field.getDescription(),
|
||||||
null, field.getType(), null, null);
|
null, field.getType(), null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 构造删除字段变更 */
|
/** 构造删除字段变更 */
|
||||||
public static FieldChange removed(FieldInfo field) {
|
public static FieldChange removed(FieldInfo field) {
|
||||||
return new FieldChange(ChangeKind.REMOVED, field.getName(), field.getDescription(),
|
return new FieldChange(ChangeKind.REMOVED, field.getName(), null, field.getDescription(),
|
||||||
field.getType(), null, field.getDescription(), null);
|
field.getType(), null, field.getDescription(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 构造修改字段变更,detail 通常为类型变化描述 */
|
/** 构造修改字段变更,detail 通常为类型变化描述 */
|
||||||
public static FieldChange modified(FieldInfo oldField, FieldInfo newField, String detail) {
|
public static FieldChange modified(FieldInfo oldField, FieldInfo newField, String detail) {
|
||||||
return new FieldChange(ChangeKind.MODIFIED, newField.getName(), newField.getDescription(),
|
return new FieldChange(ChangeKind.MODIFIED, newField.getName(), null, newField.getDescription(),
|
||||||
oldField.getType(), newField.getType(), oldField.getDescription(), detail);
|
oldField.getType(), newField.getType(), oldField.getDescription(), detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 构造字段重命名;类型变化时 detail 为 oldType → newType */
|
||||||
|
public static FieldChange renamed(FieldInfo oldField, FieldInfo newField) {
|
||||||
|
String typeDetail = oldField.getType().equals(newField.getType())
|
||||||
|
? null
|
||||||
|
: oldField.getType() + " → " + newField.getType();
|
||||||
|
return new FieldChange(ChangeKind.RENAMED, newField.getName(), oldField.getName(),
|
||||||
|
newField.getDescription(), oldField.getType(), newField.getType(),
|
||||||
|
oldField.getDescription(), typeDetail);
|
||||||
|
}
|
||||||
|
|
||||||
public ChangeKind getKind() {
|
public ChangeKind getKind() {
|
||||||
return kind;
|
return kind;
|
||||||
}
|
}
|
||||||
@@ -54,6 +66,11 @@ public class FieldChange {
|
|||||||
return fieldName;
|
return fieldName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 重命名前的字段名,仅 RENAMED 时有值 */
|
||||||
|
public String getOldFieldName() {
|
||||||
|
return oldFieldName;
|
||||||
|
}
|
||||||
|
|
||||||
/** 变更后的字段说明(通知「说明」段) */
|
/** 变更后的字段说明(通知「说明」段) */
|
||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
return description;
|
return description;
|
||||||
@@ -71,7 +88,7 @@ public class FieldChange {
|
|||||||
return oldDescription;
|
return oldDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 结构性变更详情,如 Integer → String */
|
/** 结构性变更详情,重命名时为类型变化描述 */
|
||||||
public String getDetail() {
|
public String getDetail() {
|
||||||
return detail;
|
return detail;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,6 +218,17 @@ public class WeComNotifier {
|
|||||||
return quoteLine(tagAdded() + " " + fieldName + " 说明: " + descPart);
|
return quoteLine(tagAdded() + " " + fieldName + " 说明: " + descPart);
|
||||||
case REMOVED:
|
case REMOVED:
|
||||||
return quoteLine(tagRemoved() + " " + fieldName + " 说明: " + descPart);
|
return quoteLine(tagRemoved() + " " + fieldName + " 说明: " + descPart);
|
||||||
|
case RENAMED:
|
||||||
|
StringBuilder renameLine = new StringBuilder();
|
||||||
|
renameLine.append(tagRenamed()).append(" ")
|
||||||
|
.append(colorComment(safe(change.getOldFieldName()))).append(" → ")
|
||||||
|
.append(colorInfo(safe(change.getFieldName())))
|
||||||
|
.append(" 说明: ").append(descPart);
|
||||||
|
String renameTypeDetail = change.getDetail();
|
||||||
|
if (renameTypeDetail != null && !renameTypeDetail.isBlank()) {
|
||||||
|
renameLine.append(" 类型: ").append(formatTypeChange(renameTypeDetail));
|
||||||
|
}
|
||||||
|
return quoteLine(renameLine.toString());
|
||||||
case MODIFIED:
|
case MODIFIED:
|
||||||
default:
|
default:
|
||||||
StringBuilder line = new StringBuilder();
|
StringBuilder line = new StringBuilder();
|
||||||
@@ -254,6 +265,10 @@ public class WeComNotifier {
|
|||||||
return colorWarning("[修改]");
|
return colorWarning("[修改]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String tagRenamed() {
|
||||||
|
return colorWarning("[重命名]");
|
||||||
|
}
|
||||||
|
|
||||||
/** 引用行:{@code >标签: 值}(冒号后两空格) */
|
/** 引用行:{@code >标签: 值}(冒号后两空格) */
|
||||||
private String quoteKv(String key, String value) {
|
private String quoteKv(String key, String value) {
|
||||||
return "> " + key + ": " + value;
|
return "> " + key + ": " + value;
|
||||||
|
|||||||
Reference in New Issue
Block a user