DocumentBlock.java 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. package com.lingyue.document.entity;
  2. import com.baomidou.mybatisplus.annotation.TableField;
  3. import com.baomidou.mybatisplus.annotation.TableName;
  4. import com.lingyue.common.domain.entity.SimpleModel;
  5. import com.lingyue.common.mybatis.PostgreSqlJsonbTypeHandler;
  6. import io.swagger.v3.oas.annotations.media.Schema;
  7. import lombok.Data;
  8. import lombok.EqualsAndHashCode;
  9. import java.util.List;
  10. /**
  11. * 文档块实体(参考飞书 Block 设计)
  12. * 表示文档中的一个结构化内容块(段落、标题、表格、列表等)
  13. *
  14. * 核心设计:
  15. * - 块内容由多个 TextElement 组成,实体作为元素嵌入,而非通过字符偏移定位
  16. * - 这样编辑时只需修改对应元素,不会导致其他元素位置失效
  17. *
  18. * @author lingyue
  19. * @since 2026-01-21
  20. */
  21. @EqualsAndHashCode(callSuper = true)
  22. @Data
  23. @TableName(value = "document_blocks", autoResultMap = true)
  24. @Schema(description = "文档块实体")
  25. public class DocumentBlock extends SimpleModel {
  26. @Schema(description = "文档ID")
  27. @TableField("document_id")
  28. private String documentId;
  29. @Schema(description = "父块ID(支持嵌套结构)")
  30. @TableField("parent_id")
  31. private String parentId;
  32. @Schema(description = "子块ID列表")
  33. @TableField(value = "children", typeHandler = PostgreSqlJsonbTypeHandler.class)
  34. private List<String> children;
  35. @Schema(description = "块序号(在同级中的顺序)")
  36. @TableField("block_index")
  37. private Integer blockIndex;
  38. @Schema(description = "块类型", example = "page/heading1/heading2/text/bullet/ordered/table/image/code/quote")
  39. @TableField("block_type")
  40. private String blockType;
  41. @Schema(description = "块内元素列表(TextElement数组)",
  42. example = "[{\"type\":\"text_run\",\"content\":\"Hello\"},{\"type\":\"entity\",\"entityId\":\"xxx\"}]")
  43. @TableField(value = "elements", typeHandler = PostgreSqlJsonbTypeHandler.class)
  44. private List<TextElement> elements;
  45. @Schema(description = "块样式")
  46. @TableField(value = "style", typeHandler = PostgreSqlJsonbTypeHandler.class)
  47. private Object style;
  48. @Schema(description = "块元数据")
  49. @TableField(value = "metadata", typeHandler = PostgreSqlJsonbTypeHandler.class)
  50. private Object metadata;
  51. /**
  52. * 文本元素(内嵌类)
  53. * 参考飞书的 TextElement 设计
  54. */
  55. @Data
  56. @Schema(description = "文本元素")
  57. public static class TextElement {
  58. @Schema(description = "元素类型", example = "text_run/entity/mention_doc/link/equation")
  59. private String type;
  60. // ===== text_run 类型 =====
  61. @Schema(description = "文本内容(type=text_run时)")
  62. private String content;
  63. @Schema(description = "文本样式")
  64. private TextStyle style;
  65. // ===== entity 类型(我们的特色:实体标注)=====
  66. @Schema(description = "实体ID(type=entity时)")
  67. private String entityId;
  68. @Schema(description = "实体显示文本")
  69. private String entityText;
  70. @Schema(description = "实体类型", example = "PERSON/ORG/LOC/DATE/MONEY")
  71. private String entityType;
  72. @Schema(description = "是否已确认")
  73. private Boolean confirmed;
  74. // ===== link 类型 =====
  75. @Schema(description = "链接URL")
  76. private String url;
  77. // ===== mention_doc 类型 =====
  78. @Schema(description = "引用文档ID")
  79. private String refDocId;
  80. @Schema(description = "引用文档标题")
  81. private String refDocTitle;
  82. }
  83. /**
  84. * 文本样式
  85. */
  86. @Data
  87. @Schema(description = "文本样式")
  88. public static class TextStyle {
  89. private Boolean bold;
  90. private Boolean italic;
  91. private Boolean underline;
  92. private Boolean strikethrough;
  93. private String textColor;
  94. private String backgroundColor;
  95. private Boolean inlineCode;
  96. }
  97. /**
  98. * 获取块的纯文本内容(用于搜索和NER)
  99. */
  100. public String getPlainText() {
  101. if (elements == null || elements.isEmpty()) {
  102. return "";
  103. }
  104. StringBuilder sb = new StringBuilder();
  105. for (TextElement el : elements) {
  106. if ("text_run".equals(el.getType()) && el.getContent() != null) {
  107. sb.append(el.getContent());
  108. } else if ("entity".equals(el.getType()) && el.getEntityText() != null) {
  109. sb.append(el.getEntityText());
  110. }
  111. }
  112. return sb.toString();
  113. }
  114. /**
  115. * 获取块的HTML渲染(原文视图)
  116. */
  117. public String toHtml() {
  118. if (elements == null || elements.isEmpty()) {
  119. return "";
  120. }
  121. StringBuilder sb = new StringBuilder();
  122. for (TextElement el : elements) {
  123. if ("text_run".equals(el.getType())) {
  124. sb.append(escapeHtml(el.getContent()));
  125. } else if ("entity".equals(el.getType())) {
  126. sb.append(escapeHtml(el.getEntityText()));
  127. } else if ("link".equals(el.getType())) {
  128. sb.append("<a href=\"").append(el.getUrl()).append("\">")
  129. .append(escapeHtml(el.getContent())).append("</a>");
  130. }
  131. }
  132. return wrapWithTag(sb.toString());
  133. }
  134. /**
  135. * 获取块的HTML渲染(标记视图,实体高亮)
  136. */
  137. public String toMarkedHtml() {
  138. if (elements == null || elements.isEmpty()) {
  139. return "";
  140. }
  141. StringBuilder sb = new StringBuilder();
  142. for (TextElement el : elements) {
  143. if ("text_run".equals(el.getType())) {
  144. sb.append(escapeHtml(el.getContent()));
  145. } else if ("entity".equals(el.getType())) {
  146. String cssClass = getEntityCssClass(el.getEntityType());
  147. String safeEntityId = escapeHtml(el.getEntityId());
  148. String safeEntityType = escapeHtml(el.getEntityType());
  149. sb.append("<span class=\"").append(cssClass).append("\" ")
  150. .append("data-entity-id=\"").append(safeEntityId).append("\" ")
  151. .append("data-type=\"").append(safeEntityType).append("\" ")
  152. .append("onclick=\"showEntityEditModal(event,'").append(safeEntityId).append("')\" ")
  153. .append("contenteditable=\"false\">")
  154. .append(escapeHtml(el.getEntityText()))
  155. .append("</span>");
  156. }
  157. }
  158. return wrapWithTag(sb.toString());
  159. }
  160. private String wrapWithTag(String content) {
  161. return switch (blockType) {
  162. case "heading1" -> "<h1>" + content + "</h1>";
  163. case "heading2" -> "<h2>" + content + "</h2>";
  164. case "heading3" -> "<h3>" + content + "</h3>";
  165. case "bullet" -> "<li>" + content + "</li>";
  166. case "ordered" -> "<li>" + content + "</li>";
  167. case "quote" -> "<blockquote>" + content + "</blockquote>";
  168. case "code" -> "<pre><code>" + content + "</code></pre>";
  169. default -> "<p>" + content + "</p>";
  170. };
  171. }
  172. private String getEntityCssClass(String entityType) {
  173. return switch (entityType) {
  174. case "PERSON" -> "entity-highlight person";
  175. case "ORG" -> "entity-highlight org";
  176. case "LOC" -> "entity-highlight location";
  177. case "DATE" -> "entity-highlight date";
  178. case "NUMBER", "MONEY", "DATA" -> "entity-highlight data";
  179. case "CONCEPT" -> "entity-highlight concept";
  180. default -> "entity-highlight entity";
  181. };
  182. }
  183. private String escapeHtml(String text) {
  184. if (text == null) return "";
  185. return text.replace("&", "&amp;")
  186. .replace("<", "&lt;")
  187. .replace(">", "&gt;")
  188. .replace("\"", "&quot;");
  189. }
  190. }