mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(wiki): add KB-scoped pageType profile table and page metadata columns
This commit is contained in:
parent
ec47d19bf8
commit
368797e619
@ -49,6 +49,28 @@ public class WikiPageEntity {
|
||||
/** Page type: entity / concept / source / synthesis */
|
||||
private String pageType;
|
||||
|
||||
/**
|
||||
* Structured pageType metadata (schema-validated fields) as a JSON object.
|
||||
* Stored as a blob rather than exploded into per-field columns so each KB
|
||||
* can define its own schema without altering the table. Written with the
|
||||
* full page save path; partial column updates must avoid touching it.
|
||||
*/
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String metadataJson;
|
||||
|
||||
/** Last metadata validation outcome: {@code ok} / {@code warning} / {@code invalid}. */
|
||||
private String metadataValidationStatus;
|
||||
|
||||
/** Metadata validation warnings/errors as a JSON array (field, reason, source, rawValuePreview). */
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String metadataValidationJson;
|
||||
|
||||
/** Template key used when generating this page, when applicable. */
|
||||
private String templateKey;
|
||||
|
||||
/** Profile version in effect when the page was generated or last validated. */
|
||||
private Integer profileVersion;
|
||||
|
||||
/** Purpose hint for LLM ingest routing */
|
||||
private String purposeHint;
|
||||
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
package vip.mate.wiki.model;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* A KB-scoped pageType profile: the set of page types a knowledge base
|
||||
* recognises, plus each type's field schema, per-stage LLM instructions and
|
||||
* Markdown template, serialized into {@link #configJson}.
|
||||
*
|
||||
* <p>The built-in default profile is provided as a code constant and is NOT
|
||||
* stored in this table, so {@link #kbId} is always non-null. A virtual
|
||||
* generated column on the table enforces at most one enabled profile per KB.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
@Data
|
||||
@TableName("mate_wiki_page_type_profile")
|
||||
public class WikiPageTypeProfileEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/** Owning knowledge base (never null — built-in default is not stored). */
|
||||
private Long kbId;
|
||||
|
||||
/** Profile name, e.g. {@code default}, {@code regulation}. */
|
||||
private String name;
|
||||
|
||||
/** Profile version, bumped on each saved edit. */
|
||||
private Integer version;
|
||||
|
||||
/** Full pageType configuration as JSON. */
|
||||
private String configJson;
|
||||
|
||||
/** {@code 1} = the active profile for the KB. */
|
||||
private Integer enabled;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package vip.mate.wiki.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import vip.mate.wiki.model.WikiPageTypeProfileEntity;
|
||||
|
||||
/**
|
||||
* Mapper for {@link WikiPageTypeProfileEntity}.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
@Mapper
|
||||
public interface WikiPageTypeProfileMapper extends BaseMapper<WikiPageTypeProfileEntity> {
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
-- V134: KB-scoped pageType profile + structured page metadata columns.
|
||||
--
|
||||
-- A profile holds a KB's pageType definitions (field schema, per-stage LLM
|
||||
-- instructions, Markdown template) as config_json. The built-in default
|
||||
-- profile is NOT stored here — it lives as a code constant — so kb_id is
|
||||
-- NOT NULL and every stored row belongs to a concrete KB.
|
||||
--
|
||||
-- "At most one enabled profile per KB" is enforced at the DB level via a
|
||||
-- virtual generated column that yields kb_id only for the live-enabled
|
||||
-- subset (NULL otherwise) plus a plain UNIQUE constraint; NULLs are
|
||||
-- non-comparable under UNIQUE, so disabled/deleted rows coexist. This avoids
|
||||
-- a service-layer check-then-insert race across horizontally-scaled nodes.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mate_wiki_page_type_profile (
|
||||
id BIGINT NOT NULL,
|
||||
kb_id BIGINT NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
config_json CLOB NOT NULL,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted INT NOT NULL DEFAULT 0,
|
||||
-- Yields kb_id only for the live-enabled row; NULL otherwise. The UNIQUE
|
||||
-- constraint below then permits at most one enabled profile per KB.
|
||||
enabled_kb BIGINT GENERATED ALWAYS AS (
|
||||
CASE WHEN enabled = 1 AND deleted = 0 THEN kb_id ELSE NULL END
|
||||
),
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT uk_wiki_ptprofile_name UNIQUE (kb_id, name, deleted),
|
||||
CONSTRAINT uk_wiki_ptprofile_enabled UNIQUE (enabled_kb)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wiki_ptprofile_kb
|
||||
ON mate_wiki_page_type_profile (kb_id, enabled, deleted);
|
||||
|
||||
-- Structured page metadata: schema-validated pageType fields live in
|
||||
-- metadata_json (not exploded into columns); validation status/details and
|
||||
-- the generating profile/template are recorded alongside.
|
||||
ALTER TABLE mate_wiki_page ADD COLUMN IF NOT EXISTS metadata_json CLOB;
|
||||
ALTER TABLE mate_wiki_page ADD COLUMN IF NOT EXISTS metadata_validation_status VARCHAR(32);
|
||||
ALTER TABLE mate_wiki_page ADD COLUMN IF NOT EXISTS metadata_validation_json CLOB;
|
||||
ALTER TABLE mate_wiki_page ADD COLUMN IF NOT EXISTS template_key VARCHAR(128);
|
||||
ALTER TABLE mate_wiki_page ADD COLUMN IF NOT EXISTS profile_version INT;
|
||||
@ -0,0 +1,63 @@
|
||||
-- V134: KB-scoped pageType profile + structured page metadata columns.
|
||||
-- See the H2 file for the design rationale. MySQL 8 uses a VIRTUAL generated
|
||||
-- column for the "one enabled profile per KB" constraint, and an
|
||||
-- INFORMATION_SCHEMA guard for each idempotent ADD COLUMN (MySQL has no
|
||||
-- ADD COLUMN IF NOT EXISTS).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mate_wiki_page_type_profile (
|
||||
id BIGINT NOT NULL,
|
||||
kb_id BIGINT NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
config_json LONGTEXT NOT NULL,
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
deleted INT NOT NULL DEFAULT 0,
|
||||
-- Yields kb_id only for the live-enabled row; NULL otherwise. InnoDB
|
||||
-- ignores NULL keys for uniqueness, giving "at most one enabled per KB".
|
||||
enabled_kb BIGINT
|
||||
GENERATED ALWAYS AS (
|
||||
CASE WHEN enabled = 1 AND deleted = 0 THEN kb_id ELSE NULL END
|
||||
) VIRTUAL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_wiki_ptprofile_name (kb_id, name, deleted),
|
||||
UNIQUE KEY uk_wiki_ptprofile_enabled (enabled_kb),
|
||||
KEY idx_wiki_ptprofile_kb (kb_id, enabled, deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Structured page metadata columns (idempotent adds).
|
||||
SET @col_exists := (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_page'
|
||||
AND COLUMN_NAME = 'metadata_json');
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE mate_wiki_page ADD COLUMN metadata_json LONGTEXT', 'SELECT 1');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_page'
|
||||
AND COLUMN_NAME = 'metadata_validation_status');
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE mate_wiki_page ADD COLUMN metadata_validation_status VARCHAR(32)', 'SELECT 1');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_page'
|
||||
AND COLUMN_NAME = 'metadata_validation_json');
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE mate_wiki_page ADD COLUMN metadata_validation_json LONGTEXT', 'SELECT 1');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_page'
|
||||
AND COLUMN_NAME = 'template_key');
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE mate_wiki_page ADD COLUMN template_key VARCHAR(128)', 'SELECT 1');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_page'
|
||||
AND COLUMN_NAME = 'profile_version');
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE mate_wiki_page ADD COLUMN profile_version INT', 'SELECT 1');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@ -0,0 +1,85 @@
|
||||
package vip.mate.wiki.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import vip.mate.wiki.model.WikiPageTypeProfileEntity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Validates the DB-level invariants of the pageType profile table against H2:
|
||||
* the V134 generated-column UNIQUE permits at most one enabled profile per KB,
|
||||
* while disabled rows coexist freely.
|
||||
*/
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
properties = {
|
||||
"spring.flyway.enabled=true",
|
||||
"spring.flyway.locations=classpath:db/migration/h2",
|
||||
"mateclaw.feature-flag.refresh-ms=999999"
|
||||
}
|
||||
)
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
class WikiPageTypeProfileMapperE2ETest {
|
||||
|
||||
@Autowired
|
||||
private WikiPageTypeProfileMapper mapper;
|
||||
|
||||
private WikiPageTypeProfileEntity profile(long kbId, String name, int enabled) {
|
||||
WikiPageTypeProfileEntity p = new WikiPageTypeProfileEntity();
|
||||
p.setKbId(kbId);
|
||||
p.setName(name);
|
||||
p.setVersion(1);
|
||||
p.setConfigJson("{\"version\":1,\"pageTypes\":{}}");
|
||||
p.setEnabled(enabled);
|
||||
p.setCreateTime(LocalDateTime.now());
|
||||
p.setUpdateTime(LocalDateTime.now());
|
||||
return p;
|
||||
}
|
||||
|
||||
@Test
|
||||
void insertsAndReadsBack() {
|
||||
WikiPageTypeProfileEntity p = profile(9001L, "default", 1);
|
||||
mapper.insert(p);
|
||||
assertNotNull(p.getId());
|
||||
WikiPageTypeProfileEntity loaded = mapper.selectById(p.getId());
|
||||
assertEquals("default", loaded.getName());
|
||||
assertEquals(9001L, loaded.getKbId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void secondEnabledProfileForSameKb_isRejected() {
|
||||
mapper.insert(profile(9002L, "default", 1));
|
||||
// A different name but also enabled for the same KB must violate the
|
||||
// generated-column UNIQUE (one enabled profile per KB).
|
||||
assertThrows(Exception.class, () -> mapper.insert(profile(9002L, "regulation", 1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleDisabledProfilesForSameKb_coexist() {
|
||||
mapper.insert(profile(9003L, "default", 1));
|
||||
// enabled=0 rows yield NULL in the generated column and are exempt from
|
||||
// the unique check, so several may coexist.
|
||||
mapper.insert(profile(9003L, "draft-a", 0));
|
||||
mapper.insert(profile(9003L, "draft-b", 0));
|
||||
long count = mapper.selectCount(
|
||||
com.baomidou.mybatisplus.core.toolkit.Wrappers
|
||||
.<WikiPageTypeProfileEntity>lambdaQuery()
|
||||
.eq(WikiPageTypeProfileEntity::getKbId, 9003L));
|
||||
assertEquals(3, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
void enabledProfilesInDifferentKbs_coexist() {
|
||||
mapper.insert(profile(9101L, "default", 1));
|
||||
mapper.insert(profile(9102L, "default", 1));
|
||||
assertTrue(true); // no exception thrown — different KBs are independent
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user