diff --git a/muse-cloud/muse-framework/muse-spring-boot-starter-mybatis/src/main/java/cn/iocoder/muse/framework/mybatis/config/MuseMybatisAutoConfiguration.java b/muse-cloud/muse-framework/muse-spring-boot-starter-mybatis/src/main/java/cn/iocoder/muse/framework/mybatis/config/MuseMybatisAutoConfiguration.java
index 0e1c3601..c5f3cd85 100644
--- a/muse-cloud/muse-framework/muse-spring-boot-starter-mybatis/src/main/java/cn/iocoder/muse/framework/mybatis/config/MuseMybatisAutoConfiguration.java
+++ b/muse-cloud/muse-framework/muse-spring-boot-starter-mybatis/src/main/java/cn/iocoder/muse/framework/mybatis/config/MuseMybatisAutoConfiguration.java
@@ -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 {
diff --git a/muse-cloud/muse-framework/muse-spring-boot-starter-mybatis/src/main/java/cn/iocoder/muse/framework/mybatis/core/muse/MuseContractPersistenceService.java b/muse-cloud/muse-framework/muse-spring-boot-starter-mybatis/src/main/java/cn/iocoder/muse/framework/mybatis/core/muse/MuseContractPersistenceService.java
new file mode 100644
index 00000000..a3ef3ae0
--- /dev/null
+++ b/muse-cloud/muse-framework/muse-spring-boot-starter-mybatis/src/main/java/cn/iocoder/muse/framework/mybatis/core/muse/MuseContractPersistenceService.java
@@ -0,0 +1,2107 @@
+package cn.iocoder.muse.framework.mybatis.core.muse;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.muse.framework.common.exception.ServiceException;
+import cn.iocoder.muse.framework.common.muse.MuseApiContractSupport;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.Resource;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.LocalDateTime;
+import java.util.*;
+
+/**
+ * Muse 合同入口持久化应用服务。
+ *
+ *
P1 阶段仍保留 catch-all Controller,但请求不再停留在占位响应:本服务统一完成合同校验、命令幂等、
+ * 操作审计,并把关键业务事实同步写入各领域表。具体领域事实仍由调用方传入的 domain 决定,admin/app 只是不
+ * 同入口。
+ */
+@Service
+public class MuseContractPersistenceService {
+
+ /** Muse 合同写命令冲突错误码。 */
+ private static final int CONTRACT_CONFLICT = 1000000007;
+ /** Muse 合同资源不存在错误码。 */
+ private static final int CONTRACT_NOT_FOUND = 1000000008;
+ /** Muse 合同暂不支持的资源错误码。 */
+ private static final int CONTRACT_UNSUPPORTED = 1000000009;
+ /** Muse 合同写命令尚未实现错误码。 */
+ private static final int CONTRACT_NOT_IMPLEMENTED = 1000000010;
+ /** P1 默认租户 ID,后续接入 Yudao 租户上下文后替换。 */
+ private static final long DEFAULT_TENANT_ID = 0L;
+ /** 默认分页页码。 */
+ private static final int DEFAULT_PAGE_NO = 1;
+ /** 默认分页大小。 */
+ private static final int DEFAULT_PAGE_SIZE = 20;
+
+ @Resource
+ private JdbcTemplate jdbcTemplate;
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * 处理 Muse 合同请求,并将读写行为落到领域持久化层。
+ *
+ * @param domains 当前入口拥有的领域
+ * @param side app 或 admin
+ * @param httpMethod HTTP 方法
+ * @param requestUri 请求路径
+ * @param headerCommandId X-Command-Id 请求头
+ * @param queryParams 查询参数
+ * @param body JSON 请求体
+ * @param actorUserId 当前操作者
+ * @return 可序列化业务响应
+ */
+ @Transactional(rollbackFor = Exception.class)
+ public Map handle(Set domains, String side, String httpMethod, String requestUri,
+ String headerCommandId, Map queryParams,
+ Map body, Long actorUserId) {
+ Map contract = MuseApiContractSupport.handle(domains, side, httpMethod, requestUri,
+ headerCommandId, queryParams, body, actorUserId);
+ OperationContext context = OperationContext.from(contract, httpMethod, requestUri, headerCommandId,
+ queryParams, body, actorUserId);
+ if ("GET".equals(httpMethod) || isReadOnlyCommand(context.operationId())) {
+ return handleRead(context);
+ }
+ return handleWrite(context);
+ }
+
+ private Map handleRead(OperationContext context) {
+ if (isCurrentUserOperation(context.operationId())) {
+ return currentUserResponse(context);
+ }
+ ResourceSpec spec = resolveSpec(context);
+ if (spec == null || spec.tableName() == null) {
+ return readOperationRecords(context, spec == null ? "operation" : spec.resourceType());
+ }
+ if (isListOperation(context.operationId())) {
+ return listRows(context, spec);
+ }
+ return getRow(context, spec);
+ }
+
+ private Map handleWrite(OperationContext context) {
+ Map replay = replayCommand(context);
+ if (replay != null) {
+ replay.put("idempotentReplay", true);
+ return replay;
+ }
+ if (!isMaterializedWrite(context)) {
+ throw new ServiceException(CONTRACT_NOT_IMPLEMENTED,
+ "P1 暂未实现该写命令的业务落库: " + context.operationId());
+ }
+
+ ResourceSpec spec = resolveSpec(context);
+ Map response = acceptedResponse(context, spec);
+ Long operationRecordId;
+ try {
+ // 先占用 commandId 对应的统一操作记录,再执行业务写入,避免并发请求同时撞业务唯一键。
+ operationRecordId = insertOperationRecord(context, spec, null, response);
+ } catch (DuplicateKeyException duplicate) {
+ replay = replayCommand(context);
+ if (replay != null) {
+ replay.put("idempotentReplay", true);
+ return replay;
+ }
+ throw duplicate;
+ }
+ response.put("operationRecordId", operationRecordId);
+ Long resourceId = materializeDomainFact(context, spec, response);
+ if (resourceId != null) {
+ response.put("resourceId", resourceId);
+ }
+ updateOperationResponse(operationRecordId, resourceId, response);
+ return response;
+ }
+
+ private Map acceptedResponse(OperationContext context, ResourceSpec spec) {
+ Map response = new LinkedHashMap<>();
+ response.put("operationId", context.operationId());
+ response.put("module", context.domain());
+ response.put("entry", context.side());
+ response.put("status", "accepted");
+ response.put("pathVariables", context.pathVariables());
+ response.put("query", context.queryParams());
+ response.put("commandId", context.commandId());
+ response.put("resourceType", spec == null ? "operation" : spec.resourceType());
+ response.put("persisted", true);
+ response.put("audit", audit(context));
+ return response;
+ }
+
+ private Map audit(OperationContext context) {
+ Map audit = new LinkedHashMap<>();
+ audit.put("actorUserId", context.actorUserId());
+ audit.put("acceptedAt", LocalDateTime.now().toString());
+ audit.put("contractOperation", context.operationId());
+ return audit;
+ }
+
+ private Map currentUserResponse(OperationContext context) {
+ Map data = baseReadResponse(context, "profile");
+ data.put("id", context.actorUserId());
+ data.put("userId", context.actorUserId());
+ data.put("revision", 1);
+ data.put("profile", latestOperationPayload(context, "profile"));
+ data.put("entitlements", selectAccountEntitlement(context.actorUserId()));
+ return data;
+ }
+
+ private Map listRows(OperationContext context, ResourceSpec spec) {
+ WhereClause where = buildWhere(context, spec, true);
+ int pageNo = intValue(context.queryParams().get("pageNo"), DEFAULT_PAGE_NO);
+ int pageSize = intValue(context.queryParams().get("pageSize"), DEFAULT_PAGE_SIZE);
+ int offset = Math.max(pageNo - 1, 0) * pageSize;
+ List