mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +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 */
|
/** Page type: entity / concept / source / synthesis */
|
||||||
private String pageType;
|
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 */
|
/** Purpose hint for LLM ingest routing */
|
||||||
private String purposeHint;
|
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