| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- package com.lingyue.document.entity;
- import com.baomidou.mybatisplus.annotation.TableField;
- import com.baomidou.mybatisplus.annotation.TableName;
- import com.lingyue.common.domain.entity.SimpleModel;
- import com.lingyue.common.mybatis.PostgreSqlJsonbTypeHandler;
- import io.swagger.v3.oas.annotations.media.Schema;
- import lombok.Data;
- import lombok.EqualsAndHashCode;
- import java.util.List;
- /**
- * 文档块实体(参考飞书 Block 设计)
- * 表示文档中的一个结构化内容块(段落、标题、表格、列表等)
- *
- * 核心设计:
- * - 块内容由多个 TextElement 组成,实体作为元素嵌入,而非通过字符偏移定位
- * - 这样编辑时只需修改对应元素,不会导致其他元素位置失效
- *
- * @author lingyue
- * @since 2026-01-21
- */
- @EqualsAndHashCode(callSuper = true)
- @Data
- @TableName(value = "document_blocks", autoResultMap = true)
- @Schema(description = "文档块实体")
- public class DocumentBlock extends SimpleModel {
-
- @Schema(description = "文档ID")
- @TableField("document_id")
- private String documentId;
-
- @Schema(description = "父块ID(支持嵌套结构)")
- @TableField("parent_id")
- private String parentId;
-
- @Schema(description = "子块ID列表")
- @TableField(value = "children", typeHandler = PostgreSqlJsonbTypeHandler.class)
- private List<String> children;
-
- @Schema(description = "块序号(在同级中的顺序)")
- @TableField("block_index")
- private Integer blockIndex;
-
- @Schema(description = "块类型", example = "page/heading1/heading2/text/bullet/ordered/table/image/code/quote")
- @TableField("block_type")
- private String blockType;
-
- @Schema(description = "块内元素列表(TextElement数组)",
- example = "[{\"type\":\"text_run\",\"content\":\"Hello\"},{\"type\":\"entity\",\"entityId\":\"xxx\"}]")
- @TableField(value = "elements", typeHandler = PostgreSqlJsonbTypeHandler.class)
- private List<TextElement> elements;
-
- @Schema(description = "块样式")
- @TableField(value = "style", typeHandler = PostgreSqlJsonbTypeHandler.class)
- private Object style;
-
- @Schema(description = "块元数据")
- @TableField(value = "metadata", typeHandler = PostgreSqlJsonbTypeHandler.class)
- private Object metadata;
-
- /**
- * 文本元素(内嵌类)
- * 参考飞书的 TextElement 设计
- */
- @Data
- @Schema(description = "文本元素")
- public static class TextElement {
-
- @Schema(description = "元素类型", example = "text_run/entity/mention_doc/link/equation")
- private String type;
-
- // ===== text_run 类型 =====
- @Schema(description = "文本内容(type=text_run时)")
- private String content;
-
- @Schema(description = "文本样式")
- private TextStyle style;
-
- // ===== entity 类型(我们的特色:实体标注)=====
- @Schema(description = "实体ID(type=entity时)")
- private String entityId;
-
- @Schema(description = "实体显示文本")
- private String entityText;
-
- @Schema(description = "实体类型", example = "PERSON/ORG/LOC/DATE/MONEY")
- private String entityType;
-
- @Schema(description = "是否已确认")
- private Boolean confirmed;
-
- // ===== link 类型 =====
- @Schema(description = "链接URL")
- private String url;
-
- // ===== mention_doc 类型 =====
- @Schema(description = "引用文档ID")
- private String refDocId;
-
- @Schema(description = "引用文档标题")
- private String refDocTitle;
- }
-
- /**
- * 文本样式
- */
- @Data
- @Schema(description = "文本样式")
- public static class TextStyle {
- private Boolean bold;
- private Boolean italic;
- private Boolean underline;
- private Boolean strikethrough;
- private String textColor;
- private String backgroundColor;
- private Boolean inlineCode;
- }
-
- /**
- * 获取块的纯文本内容(用于搜索和NER)
- */
- public String getPlainText() {
- if (elements == null || elements.isEmpty()) {
- return "";
- }
- StringBuilder sb = new StringBuilder();
- for (TextElement el : elements) {
- if ("text_run".equals(el.getType()) && el.getContent() != null) {
- sb.append(el.getContent());
- } else if ("entity".equals(el.getType()) && el.getEntityText() != null) {
- sb.append(el.getEntityText());
- }
- }
- return sb.toString();
- }
-
- /**
- * 获取块的HTML渲染(原文视图)
- */
- public String toHtml() {
- if (elements == null || elements.isEmpty()) {
- return "";
- }
- StringBuilder sb = new StringBuilder();
- for (TextElement el : elements) {
- if ("text_run".equals(el.getType())) {
- sb.append(escapeHtml(el.getContent()));
- } else if ("entity".equals(el.getType())) {
- sb.append(escapeHtml(el.getEntityText()));
- } else if ("link".equals(el.getType())) {
- sb.append("<a href=\"").append(el.getUrl()).append("\">")
- .append(escapeHtml(el.getContent())).append("</a>");
- }
- }
- return wrapWithTag(sb.toString());
- }
-
- /**
- * 获取块的HTML渲染(标记视图,实体高亮)
- */
- public String toMarkedHtml() {
- if (elements == null || elements.isEmpty()) {
- return "";
- }
- StringBuilder sb = new StringBuilder();
- for (TextElement el : elements) {
- if ("text_run".equals(el.getType())) {
- sb.append(escapeHtml(el.getContent()));
- } else if ("entity".equals(el.getType())) {
- String cssClass = getEntityCssClass(el.getEntityType());
- String safeEntityId = escapeHtml(el.getEntityId());
- String safeEntityType = escapeHtml(el.getEntityType());
- sb.append("<span class=\"").append(cssClass).append("\" ")
- .append("data-entity-id=\"").append(safeEntityId).append("\" ")
- .append("data-type=\"").append(safeEntityType).append("\" ")
- .append("onclick=\"showEntityEditModal(event,'").append(safeEntityId).append("')\" ")
- .append("contenteditable=\"false\">")
- .append(escapeHtml(el.getEntityText()))
- .append("</span>");
- }
- }
- return wrapWithTag(sb.toString());
- }
-
- private String wrapWithTag(String content) {
- return switch (blockType) {
- case "heading1" -> "<h1>" + content + "</h1>";
- case "heading2" -> "<h2>" + content + "</h2>";
- case "heading3" -> "<h3>" + content + "</h3>";
- case "bullet" -> "<li>" + content + "</li>";
- case "ordered" -> "<li>" + content + "</li>";
- case "quote" -> "<blockquote>" + content + "</blockquote>";
- case "code" -> "<pre><code>" + content + "</code></pre>";
- default -> "<p>" + content + "</p>";
- };
- }
-
- private String getEntityCssClass(String entityType) {
- return switch (entityType) {
- case "PERSON" -> "entity-highlight person";
- case "ORG" -> "entity-highlight org";
- case "LOC" -> "entity-highlight location";
- case "DATE" -> "entity-highlight date";
- case "NUMBER", "MONEY", "DATA" -> "entity-highlight data";
- case "CONCEPT" -> "entity-highlight concept";
- default -> "entity-highlight entity";
- };
- }
-
- private String escapeHtml(String text) {
- if (text == null) return "";
- return text.replace("&", "&")
- .replace("<", "<")
- .replace(">", ">")
- .replace("\"", """);
- }
- }
|