feat(muse-cloud): 持久化 Muse 合同入口

This commit is contained in:
zizi 2026-05-25 12:11:27 +08:00
parent ec2775b9d0
commit 6410604e88
18 changed files with 2492 additions and 18 deletions

View File

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.muse.framework.common.util.json.JsonUtils;
import cn.iocoder.muse.framework.mybatis.core.handler.DefaultDBFieldHandler;
import cn.iocoder.muse.framework.mybatis.core.muse.MuseContractPersistenceService;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.baomidou.mybatisplus.core.handlers.IJsonTypeHandler;
@ -21,6 +22,7 @@ import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.List;
@ -34,6 +36,7 @@ import java.util.concurrent.TimeUnit;
@AutoConfiguration(before = MybatisPlusAutoConfiguration.class) // 目的:先于 MyBatis Plus 自动配置,避免 @MapperScan 可能扫描不到 Mapper 打印 warn 日志
@MapperScan(value = "${muse.info.base-package}", annotationClass = Mapper.class,
lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
@Import(MuseContractPersistenceService.class)
public class MuseMybatisAutoConfiguration {
static {

View File

@ -23,5 +23,9 @@
<groupId>cn.iocoder.cloud</groupId>
<artifactId>muse-spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>muse-spring-boot-starter-mybatis</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,9 +1,10 @@
package cn.iocoder.muse.module.ai.controller.admin;
import cn.iocoder.muse.framework.common.muse.MuseApiContractSupport;
import cn.iocoder.muse.framework.common.pojo.CommonResult;
import cn.iocoder.muse.framework.mybatis.core.muse.MuseContractPersistenceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -28,6 +29,9 @@ public class AdminMuseAiContractController {
/** 当前 Controller 拥有的 OpenAPI 合同域。 */
private static final Set<String> DOMAINS = Set.of("ai");
@Resource
private MuseContractPersistenceService contractPersistenceService;
/**
* 处理当前领域下尚未落成专用 Controller 的 OpenAPI 合同请求。
*
@ -41,7 +45,7 @@ public class AdminMuseAiContractController {
public CommonResult<Map<String, Object>> handle(HttpServletRequest request,
@RequestParam Map<String, String> queryParams,
@RequestBody(required = false) Map<String, Object> body) {
return success(MuseApiContractSupport.handle(DOMAINS, "admin", request.getMethod(), request.getRequestURI(),
return success(contractPersistenceService.handle(DOMAINS, "admin", request.getMethod(), request.getRequestURI(),
request.getHeader("X-Command-Id"), queryParams, body, getLoginUserId()));
}

View File

@ -1,9 +1,10 @@
package cn.iocoder.muse.module.ai.controller.app;
import cn.iocoder.muse.framework.common.muse.MuseApiContractSupport;
import cn.iocoder.muse.framework.common.pojo.CommonResult;
import cn.iocoder.muse.framework.mybatis.core.muse.MuseContractPersistenceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
@ -34,6 +35,9 @@ public class AppMuseAiContractController {
/** AI 任务 SSE 连接默认超时时间。 */
private static final long DEFAULT_TIMEOUT_MILLIS = 30_000L;
@Resource
private MuseContractPersistenceService contractPersistenceService;
/**
* 建立 AI 任务级 SSE 流。
*
@ -72,7 +76,7 @@ public class AppMuseAiContractController {
public CommonResult<Map<String, Object>> handle(HttpServletRequest request,
@RequestParam Map<String, String> queryParams,
@RequestBody(required = false) Map<String, Object> body) {
return success(MuseApiContractSupport.handle(DOMAINS, "app", request.getMethod(), request.getRequestURI(),
return success(contractPersistenceService.handle(DOMAINS, "app", request.getMethod(), request.getRequestURI(),
request.getHeader("X-Command-Id"), queryParams, body, getLoginUserId()));
}

View File

@ -22,5 +22,9 @@
<groupId>cn.iocoder.cloud</groupId>
<artifactId>muse-spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>muse-spring-boot-starter-mybatis</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,9 +1,10 @@
package cn.iocoder.muse.module.knowledge.controller.admin;
import cn.iocoder.muse.framework.common.muse.MuseApiContractSupport;
import cn.iocoder.muse.framework.common.pojo.CommonResult;
import cn.iocoder.muse.framework.mybatis.core.muse.MuseContractPersistenceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -28,6 +29,9 @@ public class AdminMuseKnowledgeContractController {
/** 当前 Controller 拥有的 OpenAPI 合同域。 */
private static final Set<String> DOMAINS = Set.of("knowledge");
@Resource
private MuseContractPersistenceService contractPersistenceService;
/**
* 处理当前领域下尚未落成专用 Controller 的 OpenAPI 合同请求。
*
@ -41,7 +45,7 @@ public class AdminMuseKnowledgeContractController {
public CommonResult<Map<String, Object>> handle(HttpServletRequest request,
@RequestParam Map<String, String> queryParams,
@RequestBody(required = false) Map<String, Object> body) {
return success(MuseApiContractSupport.handle(DOMAINS, "admin", request.getMethod(), request.getRequestURI(),
return success(contractPersistenceService.handle(DOMAINS, "admin", request.getMethod(), request.getRequestURI(),
request.getHeader("X-Command-Id"), queryParams, body, getLoginUserId()));
}

View File

@ -1,9 +1,10 @@
package cn.iocoder.muse.module.knowledge.controller.app;
import cn.iocoder.muse.framework.common.muse.MuseApiContractSupport;
import cn.iocoder.muse.framework.common.pojo.CommonResult;
import cn.iocoder.muse.framework.mybatis.core.muse.MuseContractPersistenceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -28,6 +29,9 @@ public class AppMuseKnowledgeContractController {
/** 当前 Controller 拥有的 OpenAPI 合同域。 */
private static final Set<String> DOMAINS = Set.of("knowledge");
@Resource
private MuseContractPersistenceService contractPersistenceService;
/**
* 处理当前领域下尚未落成专用 Controller 的 OpenAPI 合同请求。
*
@ -41,7 +45,7 @@ public class AppMuseKnowledgeContractController {
public CommonResult<Map<String, Object>> handle(HttpServletRequest request,
@RequestParam Map<String, String> queryParams,
@RequestBody(required = false) Map<String, Object> body) {
return success(MuseApiContractSupport.handle(DOMAINS, "app", request.getMethod(), request.getRequestURI(),
return success(contractPersistenceService.handle(DOMAINS, "app", request.getMethod(), request.getRequestURI(),
request.getHeader("X-Command-Id"), queryParams, body, getLoginUserId()));
}

View File

@ -22,5 +22,9 @@
<groupId>cn.iocoder.cloud</groupId>
<artifactId>muse-spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>muse-spring-boot-starter-mybatis</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,9 +1,10 @@
package cn.iocoder.muse.module.market.controller.admin;
import cn.iocoder.muse.framework.common.muse.MuseApiContractSupport;
import cn.iocoder.muse.framework.common.pojo.CommonResult;
import cn.iocoder.muse.framework.mybatis.core.muse.MuseContractPersistenceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -28,6 +29,9 @@ public class AdminMuseMarketContractController {
/** 当前 Controller 拥有的 OpenAPI 合同域。 */
private static final Set<String> DOMAINS = Set.of("market");
@Resource
private MuseContractPersistenceService contractPersistenceService;
/**
* 处理当前领域下尚未落成专用 Controller 的 OpenAPI 合同请求。
*
@ -41,7 +45,7 @@ public class AdminMuseMarketContractController {
public CommonResult<Map<String, Object>> handle(HttpServletRequest request,
@RequestParam Map<String, String> queryParams,
@RequestBody(required = false) Map<String, Object> body) {
return success(MuseApiContractSupport.handle(DOMAINS, "admin", request.getMethod(), request.getRequestURI(),
return success(contractPersistenceService.handle(DOMAINS, "admin", request.getMethod(), request.getRequestURI(),
request.getHeader("X-Command-Id"), queryParams, body, getLoginUserId()));
}

View File

@ -1,9 +1,10 @@
package cn.iocoder.muse.module.market.controller.app;
import cn.iocoder.muse.framework.common.muse.MuseApiContractSupport;
import cn.iocoder.muse.framework.common.pojo.CommonResult;
import cn.iocoder.muse.framework.mybatis.core.muse.MuseContractPersistenceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -28,6 +29,9 @@ public class AppMuseMarketContractController {
/** 当前 Controller 拥有的 OpenAPI 合同域。 */
private static final Set<String> DOMAINS = Set.of("market");
@Resource
private MuseContractPersistenceService contractPersistenceService;
/**
* 处理当前领域下尚未落成专用 Controller 的 OpenAPI 合同请求。
*
@ -41,7 +45,7 @@ public class AppMuseMarketContractController {
public CommonResult<Map<String, Object>> handle(HttpServletRequest request,
@RequestParam Map<String, String> queryParams,
@RequestBody(required = false) Map<String, Object> body) {
return success(MuseApiContractSupport.handle(DOMAINS, "app", request.getMethod(), request.getRequestURI(),
return success(contractPersistenceService.handle(DOMAINS, "app", request.getMethod(), request.getRequestURI(),
request.getHeader("X-Command-Id"), queryParams, body, getLoginUserId()));
}

View File

@ -1,9 +1,10 @@
package cn.iocoder.muse.module.member.controller.admin;
import cn.iocoder.muse.framework.common.muse.MuseApiContractSupport;
import cn.iocoder.muse.framework.common.pojo.CommonResult;
import cn.iocoder.muse.framework.mybatis.core.muse.MuseContractPersistenceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -28,6 +29,9 @@ public class AdminMuseAccountContractController {
/** 当前 Controller 拥有的 OpenAPI 合同域。 */
private static final Set<String> DOMAINS = Set.of("account");
@Resource
private MuseContractPersistenceService contractPersistenceService;
/**
* 处理当前领域下尚未落成专用 Controller 的 OpenAPI 合同请求。
*
@ -41,7 +45,7 @@ public class AdminMuseAccountContractController {
public CommonResult<Map<String, Object>> handle(HttpServletRequest request,
@RequestParam Map<String, String> queryParams,
@RequestBody(required = false) Map<String, Object> body) {
return success(MuseApiContractSupport.handle(DOMAINS, "admin", request.getMethod(), request.getRequestURI(),
return success(contractPersistenceService.handle(DOMAINS, "admin", request.getMethod(), request.getRequestURI(),
request.getHeader("X-Command-Id"), queryParams, body, getLoginUserId()));
}

View File

@ -1,9 +1,10 @@
package cn.iocoder.muse.module.member.controller.app;
import cn.iocoder.muse.framework.common.muse.MuseApiContractSupport;
import cn.iocoder.muse.framework.common.pojo.CommonResult;
import cn.iocoder.muse.framework.mybatis.core.muse.MuseContractPersistenceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -28,6 +29,9 @@ public class AppMuseAccountContractController {
/** 当前 Controller 拥有的 OpenAPI 合同域。 */
private static final Set<String> DOMAINS = Set.of("account");
@Resource
private MuseContractPersistenceService contractPersistenceService;
/**
* 处理当前领域下尚未落成专用 Controller 的 OpenAPI 合同请求。
*
@ -41,7 +45,7 @@ public class AppMuseAccountContractController {
public CommonResult<Map<String, Object>> handle(HttpServletRequest request,
@RequestParam Map<String, String> queryParams,
@RequestBody(required = false) Map<String, Object> body) {
return success(MuseApiContractSupport.handle(DOMAINS, "app", request.getMethod(), request.getRequestURI(),
return success(contractPersistenceService.handle(DOMAINS, "app", request.getMethod(), request.getRequestURI(),
request.getHeader("X-Command-Id"), queryParams, body, getLoginUserId()));
}

View File

@ -22,5 +22,9 @@
<groupId>cn.iocoder.cloud</groupId>
<artifactId>muse-spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>muse-spring-boot-starter-mybatis</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,9 +1,10 @@
package cn.iocoder.muse.module.meta.controller.admin;
import cn.iocoder.muse.framework.common.muse.MuseApiContractSupport;
import cn.iocoder.muse.framework.common.pojo.CommonResult;
import cn.iocoder.muse.framework.mybatis.core.muse.MuseContractPersistenceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -28,6 +29,9 @@ public class AdminMuseMetaContractController {
/** 当前 Controller 拥有的 OpenAPI 合同域。 */
private static final Set<String> DOMAINS = Set.of("meta");
@Resource
private MuseContractPersistenceService contractPersistenceService;
/**
* 处理当前领域下尚未落成专用 Controller 的 OpenAPI 合同请求。
*
@ -41,7 +45,7 @@ public class AdminMuseMetaContractController {
public CommonResult<Map<String, Object>> handle(HttpServletRequest request,
@RequestParam Map<String, String> queryParams,
@RequestBody(required = false) Map<String, Object> body) {
return success(MuseApiContractSupport.handle(DOMAINS, "admin", request.getMethod(), request.getRequestURI(),
return success(contractPersistenceService.handle(DOMAINS, "admin", request.getMethod(), request.getRequestURI(),
request.getHeader("X-Command-Id"), queryParams, body, getLoginUserId()));
}

View File

@ -0,0 +1,172 @@
package cn.iocoder.muse.server.framework.api;
import cn.iocoder.muse.framework.common.exception.ServiceException;
import cn.iocoder.muse.framework.mybatis.config.MuseMybatisAutoConfiguration;
import cn.iocoder.muse.framework.mybatis.core.muse.MuseContractPersistenceService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.context.annotation.Import;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Muse 合同持久化应用服务测试。
*/
class MuseContractPersistenceServiceTest {
private MuseContractPersistenceService service;
private JdbcTemplate jdbcTemplate;
@BeforeEach
void setUp() {
service = new MuseContractPersistenceService();
jdbcTemplate = mock(JdbcTemplate.class);
ReflectionTestUtils.setField(service, "jdbcTemplate", jdbcTemplate);
}
@Test
void should_replay_existing_command_when_commandIdAlreadyPersisted() {
// 已持久化的 commandId 再次提交时,后端必须返回历史响应,避免重复扣费、重复安装或重复发布。
when(jdbcTemplate.queryForObject(anyString(), eq(String.class), any(Object[].class)))
.thenReturn("{\"operationId\":\"purchaseAsset\",\"status\":\"accepted\",\"operationRecordId\":9}");
Map<String, Object> result = service.handle(Set.of("market"), "app", "POST",
"/app-api/muse/marketplace/assets/1/purchase", null, Map.of(),
Map.of("commandId", "cmd-market-1"), 1001L);
assertEquals("purchaseAsset", result.get("operationId"));
assertEquals("accepted", result.get("status"));
assertTrue((Boolean) result.get("idempotentReplay"));
}
@Test
void should_persist_ai_task_and_operation_audit_when_createAiTask() {
// 创建 AI 任务时需要同时落业务任务表和统一操作审计表,形成可追踪的 P1 后端事实。
when(jdbcTemplate.queryForObject(anyString(), eq(String.class), any(Object[].class)))
.thenReturn(null);
when(jdbcTemplate.queryForObject(argThat(sql -> sql != null && sql.contains("muse_ai_generation")), eq(Long.class), any(Object[].class)))
.thenReturn(101L);
when(jdbcTemplate.queryForObject(argThat(sql -> sql != null && sql.contains("muse_domain_operation_record")), eq(Long.class), any(Object[].class)))
.thenReturn(201L);
when(jdbcTemplate.update(anyString(), any(Object[].class))).thenReturn(1);
Map<String, Object> result = service.handle(Set.of("ai"), "app", "POST",
"/app-api/muse/ai/tasks", null, Map.of(),
Map.of("commandId", "cmd-ai-1", "intent", "outline"), 1001L);
assertEquals("createAiTask", result.get("operationId"));
assertEquals(101L, result.get("taskId"));
assertEquals(201L, result.get("operationRecordId"));
assertEquals("aiTask", result.get("resourceType"));
}
@Test
void should_persist_knowledge_base_when_createKnowledgeBase() {
// 用户侧创建知识库时不能只写统一操作审计,必须同时落知识库领域表。
when(jdbcTemplate.queryForObject(anyString(), eq(String.class), any(Object[].class)))
.thenReturn(null);
when(jdbcTemplate.queryForObject(argThat(sql -> sql != null && sql.contains("muse_knowledge_base")), eq(Long.class), any(Object[].class)))
.thenReturn(301L);
when(jdbcTemplate.queryForObject(argThat(sql -> sql != null && sql.contains("muse_domain_operation_record")), eq(Long.class), any(Object[].class)))
.thenReturn(401L);
when(jdbcTemplate.update(anyString(), any(Object[].class))).thenReturn(1);
Map<String, Object> result = service.handle(Set.of("knowledge"), "app", "POST",
"/app-api/muse/knowledge-bases", null, Map.of(),
Map.of("commandId", "cmd-kb-1", "name", "Story Bible"), 1001L);
assertEquals("createKnowledgeBase", result.get("operationId"));
assertEquals(301L, result.get("kbId"));
assertEquals(401L, result.get("operationRecordId"));
assertEquals("knowledgeBase", result.get("resourceType"));
}
@Test
void should_reject_meta_activation_when_expectedActiveVersionIsStale() {
// MetaSchema 激活/回滚必须校验调用方看到的当前活跃版本,避免过期治理命令覆盖新版本。
when(jdbcTemplate.queryForObject(anyString(), eq(String.class), any(Object[].class)))
.thenReturn(null);
when(jdbcTemplate.queryForObject(argThat(sql -> sql != null && sql.contains("SELECT v.version_no")), eq(Integer.class), any(Object[].class)))
.thenReturn(2);
assertThrows(ServiceException.class, () -> service.handle(Set.of("meta"), "admin", "POST",
"/admin-api/muse/governance/meta-schemas/work/versions/3/activate", null, Map.of(),
Map.of("commandId", "cmd-meta-1", "reason", "publish", "expectedActiveVersion", 1), 1001L));
}
@Test
void should_reject_unimplemented_write_command_insteadOfFakeAccepted() {
// 当前持久化服务未拥有的领域写命令不能返回 accepted,避免前端误以为动作已经生效。
when(jdbcTemplate.queryForObject(anyString(), eq(String.class), any(Object[].class)))
.thenReturn(null);
assertThrows(ServiceException.class, () -> service.handle(Set.of("content"), "app", "POST",
"/app-api/muse/works", null, Map.of(),
Map.of("commandId", "cmd-content-work-1", "title", "Draft"), 1001L));
}
@Test
void should_persist_market_purchase_when_purchaseAsset() {
// 购买是 Market 的明确业务事实,不能只写操作日志,需要落购买表并保留幂等 commandId。
when(jdbcTemplate.queryForObject(anyString(), eq(String.class), any(Object[].class)))
.thenReturn(null);
when(jdbcTemplate.queryForObject(argThat(sql -> sql != null && sql.contains("muse_domain_operation_record")), eq(Long.class), any(Object[].class)))
.thenReturn(501L);
when(jdbcTemplate.queryForObject(argThat(sql -> sql != null && sql.contains("muse_market_asset_version")), eq(Long.class), any(Object[].class)))
.thenReturn(601L);
when(jdbcTemplate.queryForObject(argThat(sql -> sql != null && sql.contains("muse_market_purchase")), eq(Long.class), any(Object[].class)))
.thenReturn(701L);
when(jdbcTemplate.update(anyString(), any(Object[].class))).thenReturn(1);
Map<String, Object> result = service.handle(Set.of("market"), "app", "POST",
"/app-api/muse/marketplace/assets/9/purchase", null, Map.of(),
Map.of("commandId", "cmd-market-purchase-1"), 1001L);
assertEquals("purchaseAsset", result.get("operationId"));
assertEquals(701L, result.get("purchaseId"));
assertEquals(501L, result.get("operationRecordId"));
assertEquals("purchase", result.get("resourceType"));
}
@Test
void should_persist_workflow_task_when_precheckAgentSlot() {
// 预检类命令本身就是可追踪工作流事实,需要落统一 workflow task,供后续 bind 使用。
when(jdbcTemplate.queryForObject(anyString(), eq(String.class), any(Object[].class)))
.thenReturn(null);
when(jdbcTemplate.queryForObject(argThat(sql -> sql != null && sql.contains("COUNT(1) FROM muse_content_work")), eq(Integer.class), any(Object[].class)))
.thenReturn(1);
when(jdbcTemplate.queryForObject(argThat(sql -> sql != null && sql.contains("muse_domain_operation_record")), eq(Long.class), any(Object[].class)))
.thenReturn(801L);
when(jdbcTemplate.queryForObject(argThat(sql -> sql != null && sql.contains("muse_domain_workflow_task")), eq(Long.class), any(Object[].class)))
.thenReturn(901L);
when(jdbcTemplate.update(anyString(), any(Object[].class))).thenReturn(1);
Map<String, Object> result = service.handle(Set.of("ai"), "app", "POST",
"/app-api/muse/works/88/agent-slots/draft/prechecks", null, Map.of(),
Map.of("commandId", "cmd-slot-precheck-1", "sourceAgentId", 12, "sourceAgentVersion", "1"), 1001L);
assertEquals("precheckAgentSlot", result.get("operationId"));
assertEquals(901L, result.get("agentSlotPrecheckId"));
assertEquals("aiWorkflowTask", result.get("resourceType"));
}
@Test
void should_import_contractPersistenceService_from_mybatisAutoConfiguration() {
// muse-server 只扫描 server/module 包,framework 下的合同持久化服务必须由 starter 自动导入。
Import importAnnotation = MuseMybatisAutoConfiguration.class.getAnnotation(Import.class);
assertTrue(Arrays.asList(importAnnotation.value()).contains(MuseContractPersistenceService.class));
}
}

View File

@ -0,0 +1,42 @@
-- Muse P1 合同操作审计与幂等 Schema(PostgreSQL)
-- 目的:为 Meta/Knowledge/Market/AI/Account 的全量合同入口提供统一幂等、审计和兜底查询状态。
CREATE TABLE muse_domain_operation_record (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
domain VARCHAR(50) NOT NULL,
side VARCHAR(20) NOT NULL,
operation_id VARCHAR(120) NOT NULL,
command_id VARCHAR(120),
actor_user_id BIGINT,
resource_type VARCHAR(80) NOT NULL,
resource_id BIGINT,
resource_key VARCHAR(200),
parent_type VARCHAR(80),
parent_id BIGINT,
status VARCHAR(30) NOT NULL DEFAULT 'accepted',
revision INT NOT NULL DEFAULT 1,
path_variables JSONB,
query_params JSONB,
request_payload JSONB,
response_payload JSONB,
creator VARCHAR(64) NOT NULL DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
tenant_id BIGINT NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX uk_muse_domain_op_command
ON muse_domain_operation_record(tenant_id, domain, command_id)
WHERE command_id IS NOT NULL;
CREATE INDEX idx_muse_domain_op_resource
ON muse_domain_operation_record(tenant_id, domain, resource_type, resource_id, create_time);
CREATE INDEX idx_muse_domain_op_actor
ON muse_domain_operation_record(tenant_id, actor_user_id, create_time);
CREATE TRIGGER trg_muse_domain_operation_record_updated_at
BEFORE UPDATE ON muse_domain_operation_record
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

View File

@ -0,0 +1,98 @@
-- Muse P1 合同工作流与市场交互 Schema(PostgreSQL)
-- 目的:承载预检、导出、重试、发布准备等异步工作流事实,以及市场收藏/购买状态。
CREATE TABLE muse_domain_workflow_task (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
domain VARCHAR(50) NOT NULL,
side VARCHAR(20) NOT NULL,
operation_id VARCHAR(120) NOT NULL,
task_type VARCHAR(80) NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'queued',
actor_user_id BIGINT,
owner_user_id BIGINT,
target_type VARCHAR(80),
target_id BIGINT,
parent_type VARCHAR(80),
parent_id BIGINT,
correlation_id VARCHAR(200),
source_type VARCHAR(80),
source_id BIGINT,
expected_status VARCHAR(80),
request_payload JSONB,
result_payload JSONB,
command_id VARCHAR(120),
revision INT NOT NULL DEFAULT 1,
creator VARCHAR(64) NOT NULL DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
tenant_id BIGINT NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX uk_muse_domain_workflow_command
ON muse_domain_workflow_task(tenant_id, domain, command_id)
WHERE command_id IS NOT NULL;
CREATE INDEX idx_muse_domain_workflow_target
ON muse_domain_workflow_task(tenant_id, domain, target_type, target_id, create_time);
CREATE INDEX idx_muse_domain_workflow_actor
ON muse_domain_workflow_task(tenant_id, domain, actor_user_id, create_time);
CREATE TRIGGER trg_muse_domain_workflow_task_updated_at
BEFORE UPDATE ON muse_domain_workflow_task
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TABLE muse_market_favorite (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
asset_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
command_id VARCHAR(120),
creator VARCHAR(64) NOT NULL DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
tenant_id BIGINT NOT NULL DEFAULT 0,
CONSTRAINT uk_muse_market_favorite_user_asset UNIQUE (tenant_id, user_id, asset_id)
);
CREATE UNIQUE INDEX uk_muse_market_favorite_command
ON muse_market_favorite(tenant_id, command_id)
WHERE command_id IS NOT NULL;
CREATE INDEX idx_muse_market_favorite_user
ON muse_market_favorite(tenant_id, user_id, status);
CREATE TRIGGER trg_muse_market_favorite_updated_at
BEFORE UPDATE ON muse_market_favorite
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TABLE muse_market_purchase (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
asset_id BIGINT NOT NULL,
asset_version_id BIGINT,
user_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'completed',
purchase_payload JSONB,
command_id VARCHAR(120) NOT NULL,
creator VARCHAR(64) NOT NULL DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
tenant_id BIGINT NOT NULL DEFAULT 0,
CONSTRAINT uk_muse_market_purchase_command UNIQUE (tenant_id, command_id)
);
CREATE INDEX idx_muse_market_purchase_user
ON muse_market_purchase(tenant_id, user_id, create_time);
CREATE INDEX idx_muse_market_purchase_asset
ON muse_market_purchase(tenant_id, asset_id, status);
CREATE TRIGGER trg_muse_market_purchase_updated_at
BEFORE UPDATE ON muse_market_purchase
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();