重构:使用 MyBatis-Flex 迁移应用 ORM

将应用自管表的 JdbcClient 数据访问迁移为实体、Mapper、构造器查询和必要的显式 SQL。

保留 AgentScope 自管表及原有业务语义,并补充事务、查询与数据库集成测试。
This commit is contained in:
Zhu Junhao
2026-08-31 12:13:06 +08:00
parent c13302c0cb
commit e968a8ddc1
54 changed files with 3702 additions and 698 deletions

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tech.easyflow.manuagent.mapper.AgentEventMapper">
<!-- 显式结果映射避免 payload 列与实体 payloadJson 属性名称不同而丢失事件负载。 -->
<resultMap id="agentEventResultMap" type="tech.easyflow.manuagent.entity.AgentEventEntity">
<id property="id" column="id"/>
<result property="projectId" column="project_id"
typeHandler="tech.easyflow.manuagent.typehandler.UuidTypeHandler"/>
<result property="runId" column="run_id"
typeHandler="tech.easyflow.manuagent.typehandler.UuidTypeHandler"/>
<result property="eventType" column="event_type"/>
<result property="eventId" column="event_id"/>
<result property="payloadJson" column="payload"
typeHandler="tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<!--
PostgreSQL INSERT ... RETURNING 同时完成写入和序号读取,不使用“先插入、再查最大值”
这种在并发场景下会取错事件的实现。affectData 保留正确的事务与缓存语义。
-->
<select id="insertReturning" resultMap="agentEventResultMap" affectData="true" flushCache="true">
INSERT INTO app.agent_event(project_id, run_id, event_type, event_id, payload)
VALUES (
#{event.projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
#{event.runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
#{event.eventType},
#{event.eventId},
#{event.payloadJson, jdbcType=OTHER,
typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler})
<!-- 与迁移前 JDBC 返回字段一致event_id 已完成持久化,但无需再次回传给业务层。 -->
RETURNING id, project_id, run_id, event_type, payload, created_at
</select>
<!-- PostgreSQL JSONB 运算仅封装在数据库适配层,业务服务不感知方言细节。 -->
<select id="selectLatestStartedPhase" resultType="string">
SELECT payload -&gt;&gt; 'phase'
FROM app.agent_event
WHERE run_id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
AND event_type = 'RUN_STARTED'
ORDER BY id DESC
LIMIT 1
</select>
<select id="selectLatestMaterialResponseJson" resultType="string">
SELECT payload::text
FROM app.agent_event
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
AND event_type = 'ASK_RESPONDED'
AND jsonb_typeof(payload -&gt; 'decisions') = 'array'
ORDER BY id DESC
LIMIT 1
</select>
</mapper>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tech.easyflow.manuagent.mapper.AgentRunMapper">
<!-- 以下更新均将“当前状态”写进 WHERE更新行数就是状态机竞争结果。 -->
<update id="completeWaiting">
UPDATE app.agent_run
SET status = 'COMPLETED', pending_interrupt = NULL, updated_at = CURRENT_TIMESTAMP
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
AND status = 'WAITING_INPUT'
</update>
<update id="interruptRunning">
UPDATE app.agent_run
SET status = 'INTERRUPTED', pending_interrupt = NULL,
error_code = 'USER_STOPPED', error_message = '用户已停止运行',
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
AND status = 'RUNNING'
</update>
<update id="waitForInput">
UPDATE app.agent_run
SET status = 'WAITING_INPUT',
pending_interrupt = #{interruptJson, jdbcType=OTHER,
typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
AND status = 'RUNNING'
</update>
<update id="completeRunning">
UPDATE app.agent_run
SET status = 'COMPLETED', pending_interrupt = NULL,
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
AND status = 'RUNNING'
</update>
<update id="failRunning">
UPDATE app.agent_run
SET status = 'FAILED', pending_interrupt = NULL,
error_code = 'AGENT_RUN_FAILED', error_message = #{message},
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
AND status = 'RUNNING'
</update>
<update id="interruptRunningAfterRestart">
UPDATE app.agent_run
SET status = 'INTERRUPTED', pending_interrupt = NULL,
error_code = 'PROCESS_RESTARTED', error_message = '服务重启,运行已中断',
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE status = 'RUNNING'
</update>
</mapper>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tech.easyflow.manuagent.mapper.ArtifactMapper">
<!--
PostgreSQL 的 INSERT ... RETURNING 属于会修改数据的查询语句。
affectData 与 flushCache 确保 MyBatis 按 DML 事务语义处理并清理一级缓存。
-->
<select id="upsert"
resultType="tech.easyflow.manuagent.entity.ArtifactEntity"
affectData="true"
flushCache="true">
INSERT INTO app.artifact(
id, project_id, run_id, kind, name, relative_path, mime_type,
size_bytes, sha256, metadata_json)
VALUES (
#{artifact.id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
#{artifact.projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
#{artifact.runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
#{artifact.kind}, #{artifact.name}, #{artifact.relativePath}, #{artifact.mimeType},
#{artifact.sizeBytes}, #{artifact.sha256},
#{artifact.metadataJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler})
ON CONFLICT (project_id, relative_path) DO UPDATE SET
run_id = EXCLUDED.run_id,
kind = EXCLUDED.kind,
name = EXCLUDED.name,
mime_type = EXCLUDED.mime_type,
size_bytes = EXCLUDED.size_bytes,
sha256 = EXCLUDED.sha256,
metadata_json = EXCLUDED.metadata_json,
published_at = CURRENT_TIMESTAMP
<!-- 发布接口只需要产物视图字段,下载字段由独立下载查询按需读取。 -->
RETURNING id, project_id, run_id, kind, name, size_bytes, metadata_json, published_at
</select>
</mapper>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tech.easyflow.manuagent.mapper.ModelAssignmentMapper">
<!-- 角色为主键,单语句 upsert 避免并发设置默认模型时出现先查后写竞态。 -->
<insert id="upsert">
INSERT INTO app.model_assignment(role, model_config_id, assigned_by)
VALUES (
#{assignment.role},
#{assignment.modelConfigId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
#{assignment.assignedBy, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler})
ON CONFLICT (role) DO UPDATE SET
model_config_id = EXCLUDED.model_config_id,
assigned_by = EXCLUDED.assigned_by,
updated_at = CURRENT_TIMESTAMP
</insert>
</mapper>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tech.easyflow.manuagent.mapper.ModelConfigMapper">
<!--
JSONB 参数必须显式使用 JsonbStringTypeHandler。这样即使 Lambda Wrapper 在 Spring 初始化前
触发了 MyBatis-Flex 的全局 TableInfo 缓存,模型写入仍不会退化为 VARCHAR 参数绑定。
-->
<insert id="insertModel">
INSERT INTO app.model_config(
id, name, provider, base_url, model_id,
api_key_ciphertext, api_key_hint, key_version,
config_json, capabilities_json, enabled, is_default, created_by)
VALUES (
#{model.id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
#{model.name},
#{model.provider},
#{model.baseUrl},
#{model.modelId},
#{model.apiKeyCiphertext},
#{model.apiKeyHint},
#{model.keyVersion},
#{model.configJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
#{model.capabilitiesJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
COALESCE(#{model.enabled}, TRUE),
COALESCE(#{model.defaultModel}, FALSE),
#{model.createdBy, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler})
</insert>
<!--
API Key 为空表示保留已有密钥;更新时间统一由数据库生成,避免应用时钟和数据库时钟混用。
-->
<update id="updateModel">
UPDATE app.model_config
SET name = #{model.name},
base_url = #{model.baseUrl},
model_id = #{model.modelId},
config_json = #{model.configJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
capabilities_json = #{model.capabilitiesJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
<if test="model.apiKeyCiphertext != null">
api_key_ciphertext = #{model.apiKeyCiphertext},
api_key_hint = #{model.apiKeyHint},
key_version = #{model.keyVersion},
</if>
updated_at = CURRENT_TIMESTAMP
WHERE id = #{model.id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
</update>
<!-- 以下两条语句保持迁移前的执行顺序和条件,不额外引入模型启用状态判断。 -->
<update id="clearDefault">
UPDATE app.model_config
SET is_default = FALSE
WHERE is_default
</update>
<update id="setDefault">
UPDATE app.model_config
SET is_default = TRUE,
updated_at = CURRENT_TIMESTAMP
WHERE id = #{id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
</update>
</mapper>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tech.easyflow.manuagent.mapper.ProjectMapper">
<!-- 项目删除前必须先阻止仍在运行的任务。 -->
<select id="hasRunningRun" resultType="boolean">
SELECT EXISTS(
SELECT 1 FROM app.agent_run
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
AND status = 'RUNNING'
)
</select>
<!-- 以下删除顺序与外键依赖顺序一致,并由 ProjectService 的 Spring 事务统一提交或回滚。 -->
<delete id="deleteEvents">
DELETE FROM app.agent_event
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
</delete>
<delete id="deleteArtifacts">
DELETE FROM app.artifact
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
</delete>
<delete id="deletePlans">
DELETE FROM app.project_plan
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
</delete>
<delete id="deleteFiles">
DELETE FROM app.project_file
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
</delete>
<delete id="deleteRuns">
DELETE FROM app.agent_run
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
</delete>
<update id="updateStatus">
UPDATE app.project
SET status = #{status}, version = version + 1, updated_at = CURRENT_TIMESTAMP
WHERE id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
</update>
</mapper>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tech.easyflow.manuagent.mapper.ProjectPlanMapper">
<!--
版本号计算与写入保持在同一条 PostgreSQL 语句内;唯一约束继续作为并发冲突的最终保护。
-->
<select id="insertNextDraft"
resultType="tech.easyflow.manuagent.entity.ProjectPlanEntity"
affectData="true"
flushCache="true">
INSERT INTO app.project_plan(id, project_id, plan_version, status, plan_json, created_by)
SELECT
#{plan.id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
#{plan.projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
COALESCE(MAX(plan_version), 0) + 1,
'DRAFT',
#{plan.planJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
#{plan.createdBy, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
FROM app.project_plan
WHERE project_id = #{plan.projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
RETURNING id, project_id, plan_version, status, plan_json, confirmed_at, created_at
</select>
<select id="selectCurrent" resultType="tech.easyflow.manuagent.entity.ProjectPlanEntity">
SELECT id, project_id, plan_version, status, plan_json, confirmed_at, created_at
FROM app.project_plan
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
ORDER BY CASE status WHEN 'CONFIRMED' THEN 0 ELSE 1 END, plan_version DESC
LIMIT 1
</select>
<!-- 条件更新和 RETURNING 在同一语句中完成,避免确认状态检查与写入之间出现竞态。 -->
<select id="confirmDraft"
resultType="tech.easyflow.manuagent.entity.ProjectPlanEntity"
affectData="true"
flushCache="true">
UPDATE app.project_plan
SET status = 'CONFIRMED',
plan_json = #{planJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
confirmed_by = #{userId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
confirmed_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = #{planId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
AND project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
AND status = 'DRAFT'
RETURNING id, project_id, plan_version, status, plan_json, confirmed_at, created_at
</select>
</mapper>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tech.easyflow.manuagent.mapper.SkillConfigMapper">
<!-- AgentScope 表严格只读;应用只在 app.skill_config 保存启停、来源和校验状态。 -->
<sql id="skillViewColumns">
s.name, s.description, c.version, c.source_type, c.enabled, c.read_only,
c.validation_status, c.validation_message, c.updated_at
</sql>
<select id="selectViews" resultType="tech.easyflow.manuagent.mapper.SkillViewRow">
SELECT <include refid="skillViewColumns"/>
FROM agentscope.agentscope_skills s
JOIN app.skill_config c ON c.skill_name = s.name
ORDER BY c.source_type, s.name
</select>
<select id="selectView" resultType="tech.easyflow.manuagent.mapper.SkillViewRow">
SELECT <include refid="skillViewColumns"/>
FROM agentscope.agentscope_skills s
JOIN app.skill_config c ON c.skill_name = s.name
WHERE s.name = #{name}
</select>
<!-- 保持迁移前 JDBC SQL 的过滤条件和数据库时间戳语义。 -->
<update id="updateEnabled">
UPDATE app.skill_config
SET enabled = #{enabled},
updated_at = CURRENT_TIMESTAMP
WHERE skill_name = #{name}
AND validation_status = 'VALID'
</update>
</mapper>