1. 页面概览

1.1 是什么

数据同步页面(SyncPage.vue)是 LightFoundry「数据入平台」的关键一环,负责把外部数据源 / 本体对象按计划抽取为平台内的数据集。它补全了"数据入平台"的闭环:数据源接入(数据源页)→ 数据抽取入库(本页)→ 数据集管理(数据集页)→ 质量画像(数据质量页)→ 血缘追踪(数据血缘页)。

页面的核心对象是「同步目标」(Sync Target):一个同步目标把"某个来源"(平台数据源查询 platform_ds,或本体对象 object)以「全量重写(full)」或「增量水位追加(incremental)」两种模式写入「目标数据集」。用户可以给目标配置 cron 调度表达式实现定时抽取,也可以手动点击「立即运行」触发一次抽取。

同步引擎是 V5 Stage 1(B1-3)的产物,与平台统一调度器(scheduler)和异步任务系统(task manager)深度集成:手动运行走"任务化"模式——后端先落一条 running 运行记录,再提交一个 type=sync_run 的异步任务;前端拿到 task_id 后按 1.5 秒间隔轮询 GET /system/tasks/:id,实时展示进度条,直到任务进入 success / failed / cancelled 终态。这一"立即运行 → 任务化轮询 → 终态"语义是本页区别于普通表单页的核心机制。

1.2 核心价值

价值点说明
计划内抽取cron 调度表达式按分钟 / 小时级周期自动抽取,无需人工干预
两种同步模式full 全量重写保证目标行表与来源一致;incremental 增量水位追加高效且幂等
进度可视化任务化 + 1.5s 轮询进度条,运行过程全程可见,告别"卡死无响应"
运行历史可追溯每次运行落 fs_sync_runs,状态 / 行数 / 触发来源 / 起止时间全记录
血缘自动打点同步成功后自动写 source → dataset 血缘边,供数据血缘页回溯
质量画像联动同步成功链触发目标数据集已启用的质量画像,数据质量闭环
启停 / 删除安全停用即时反注册调度,删除保留运行历史,二次确认防误删

1.3 一句话总结

数据同步页面把"外部数据抽取为平台数据集"变成可计划、可观察、可追溯的一等公民操作。

2. 访问入口

2.1 路由与菜单

路由 path/foundry/sync
路由 nameFoundrySync
meta.titleFoundry 数据同步
侧边栏入口FoundryLayout 侧边栏「数据同步」(位于「数据集」之后、「SQL 工作台」之前)
前端源码action/web/src/views/SyncPage.vue(584 行)
API 客户端action/web/src/api/syncApi.js + action/web/src/api/datasetApi.js

路由注册见 action/web/src/router/index.js

{ path: 'sync', name: 'FoundrySync', component: SyncPage, meta: { title: 'Foundry 数据同步', requiresAuth: true } },

页面挂在 FoundryLayout 布局下(子路由 children),侧边栏入口见 action/web/src/views/FoundryLayout.vuemenuItems 数组 { path: '/foundry/sync', label: '数据同步' }。接线时路由注册与侧边栏入口由接线 agent 统一处理。

2.2 认证与权限

2.3 端口与 API 前缀

3. 界面布局

+--------------------------------------------------------------+
| 数据同步引擎                                                    |
| 外部数据源/对象按计划抽取为平台内数据集:全量重写(full)与增量水位追加(incremental)。 |
+--------------------------------------------------------------+
| [alert 操作结果提示条(可关闭)]                                    |
+--------------------------------------------------------------+
| [同步任务进度条卡片(job.progress !== null 时显示)]                 |
|  同步任务进度(task: xxx)  [████████████░░░░] 45% · 执行中...      |
+--------------------------------------------------------------+
| [工具栏]  [新建同步目标]        [数据集下拉过滤]  [刷新]              |
+--------------------------------------------------------------+
| [新建向导卡片(showCreateForm 时显示)]                            |
|  第一步:选择目标数据集与来源 / 第二步:写入与调度策略                  |
+--------------------------------------------------------------+
| [目标列表卡片] 同步目标(N)                                       |
|  名称 | 目标数据集 | 来源 | 模式 | 调度 | 水位 | 最近运行 | 状态 | 操作 |
+--------------------------------------------------------------+
| [运行历史抽屉(runsTarget 时右侧滑出)]                             |
|  运行历史 · {目标名}  状态/模式/同步行数/触发/开始/结束/任务            |
+--------------------------------------------------------------+

各板块职责:

4. 交互元素详解

4.1 工具栏与列表操作

元素位置含义必填/默认操作效果触发的后端调用
「新建同步目标」按钮工具栏左侧打开 / 收起新建向导(编辑态为「编辑同步目标」)切换 showCreateForm,展开表单卡片
「编辑」按钮列表操作列回填该目标到表单进入编辑态startEdit(t) 解析 source_ref / field_map 后展开表单无(提交时才发 PUT /api/v1/sync/targets/:id
「刷新」按钮工具栏右侧重新加载数据集与目标列表依次调 fetchDatasets + fetchTargetsGET /api/v1/datasetsGET /api/v1/sync/targets
数据集下拉工具栏右侧按目标数据集过滤列表默认「全部数据集」切换即触发 fetchTargetsGET /api/v1/sync/targets?dataset_id=xxx
「立即运行」按钮列表操作列手动触发一次同步(任务化)运行中禁用(busy)提交任务并启动 1.5s 进度轮询POST /api/v1/sync/targets/:id/run
「运行历史」按钮列表操作列打开该目标的运行历史抽屉打开抽屉并加载 50 条记录GET /api/v1/sync/targets/:id/runs?limit=50&offset=0
「停用/启用」按钮列表操作列切换目标启停状态切换后刷新列表PUT /api/v1/sync/targets/:id(body {enabled: !enabled}
「删除」按钮列表操作列删除目标(历史保留)window.confirm 二次确认后删除DELETE /api/v1/sync/targets/:id

4.2 新建向导表单

字段位置含义必填/默认操作效果触发的后端调用
目标名称第一步同步目标显示名必填,placeholder 如 orders_hourly_sync写入 createForm.namePOST /api/v1/sync/targets
目标数据集第一步同步落数的数据集必填,选项来自数据集列表写入 createForm.dataset_id同上
来源类型第一步platform_ds(平台数据源查询)/ object(本体对象,仅 full 模式)默认 platform_ds切换显示 ds_id/query/watermark_col 或 object_id 输入同上
数据源 ID(ds_id)第一步平台数据源数字 IDplatform_ds 必填,placeholder 如 1写入 createForm.ds_id同上
水位列 watermark_col第一步增量模式的推进列incremental 必填,placeholder 如 updated_at写入 createForm.watermark_col同上
来源查询 SQL第一步拉取数据的 SELECT 查询platform_ds 必填,textarea rows=3,placeholder 如 SELECT order_id, amount, updated_at FROM orders写入 createForm.query同上
对象 ID(object_id)第一步本体对象稳定标识object 必填写入 createForm.object_id同上
同步模式第二步full(清空目标行表后重写)/ incremental(水位追加,幂等键冲突跳过)默认 full写入 createForm.mode同上
调度(cron)第二步cron 表达式,空 = 仅手动可空,placeholder 如 0 */2 * * *(每 2 小时)写入 createForm.schedule同上
幂等键映射 field_map第二步JSON {"源列": "目标列"},增量模式下按该键冲突跳过可空,textarea rows=2,placeholder 如 {"order_id": "order_id"}解析为对象写入 payload.field_map同上
创建后立即启用第二步是否启用(含调度)默认勾选 enabled=true写入 createForm.enabled同上
「创建目标」按钮表单底部提交创建提交时禁用并显示「创建中...」成功后收起表单、清空名称/调度/field_map 并刷新列表POST /api/v1/sync/targets
「取消」按钮表单底部关闭新建向导隐藏表单,不提交

4.3 运行历史抽屉

元素含义
状态徽标success → 绿色 state-on;failed → 红色 state-failed;其余(running 等)→ 橙色 state-running
模式该次运行使用的同步模式(full / incremental)
同步行数本次实际写入目标行表的行数 rows_synced
触发triggered_by:manual(手动点击)/ cron(定时调度触发)
开始 / 结束时间started_at / finished_at,前端 fmtTime 格式化为 YYYY-MM-DD HH:MM:SS(空显示 -
任务关联的异步任务 ID task_id,用于交叉核对 GET /system/tasks/:id(空显示 -

5. 后端关联

5.1 API 客户端

syncApi.js 为独立 axios 实例:

导出函数:

函数请求
listSyncTargets(datasetId='', limit=0, offset=0)GET /sync/targets
createSyncTarget(payload)POST /sync/targets
getSyncTarget(id)GET /sync/targets/:id
updateSyncTarget(id, payload)PUT /sync/targets/:id
deleteSyncTarget(id)DELETE /sync/targets/:id
runSyncTarget(id)POST /sync/targets/:id/run
listSyncRuns(id, limit=20, offset=0)GET /sync/targets/:id/runs
getTask(taskId)GET /system/tasks/:id

datasetApi.js 提供 listDatasets(status='', limit=0, offset=0)GET /datasets,用于加载数据集下拉(本页调用 listDatasets('') 取全部活跃数据集,取 data.data.datasets)。

5.2 端点表

方法路径请求体超时
GET/api/v1/sync/targetsquery: dataset_id / limit / offset120s
POST/api/v1/sync/targetsCreateTargetRequest(name、dataset_id、source_type、source_ref、mode、schedule、field_map、enabled)120s
GET/api/v1/sync/targets/:id120s
PUT/api/v1/sync/targets/:idUpdateTargetRequest(指针语义:未提供字段保留原值;含 dataset_id 可变更目标数据集,变更即重置增量水位)120s
DELETE/api/v1/sync/targets/:id120s
POST/api/v1/sync/targets/:id/run—(triggered_by 取 currentUserID,缺省 manual)120s
GET/api/v1/sync/targets/:id/runsquery: limit / offset120s
GET/api/v1/system/tasks/:id120s

5.3 响应结构

列表响应(foundry 统一包装 {code:0, data:{...}}):

{
  "code": 0,
  "data": {
    "targets": [
      {
        "id": "0f8f3b12-...",
        "name": "orders_hourly_sync",
        "dataset_id": "ds-3f1a",
        "source_type": "platform_ds",
        "source_ref": "{\"ds_id\":\"1\",\"query\":\"SELECT order_id, amount, updated_at FROM orders\",\"watermark_col\":\"updated_at\"}",
        "mode": "incremental",
        "schedule": "0 */2 * * *",
        "field_map": "{\"order_id\":\"order_id\"}",
        "enabled": true,
        "last_run_at": "2026-08-30T10:00:00+08:00",
        "watermark_value": "2026-08-30 09:58:32",
        "created_at": "2026-08-30T09:00:00+08:00",
        "updated_at": "2026-08-30T10:00:00+08:00"
      }
    ],
    "total": 1
  }
}

运行接口响应(任务化,POST /sync/targets/:id/run):

{
  "code": 0,
  "data": {
    "run": { "id": "run-uuid", "target_id": "...", "status": "running", "mode": "incremental",
             "rows_synced": 0, "task_id": "task-uuid", "started_at": "...", "triggered_by": "manual" },
    "task_id": "task-uuid"
  }
}

降级同步执行时 run 已终态(status 为 success / failed),前端据此走"已完成"提示分支。

任务轮询响应(GET /system/tasks/:id):

{
  "code": 0,
  "data": {
    "id": "task-uuid", "type": "sync_run", "status": "running",
    "progress": 0.45, "message": "写入目标数据集",
    "result": null, "error": "",
    "created_by": "user-id", "created_at": "...", "started_at": "...", "finished_at": null
  }
}

5.4 关联模块表

后端包职责
products/foundry/sync同步引擎:目标 CRUD、调度注册(sync:{targetID})、RunSync 任务化(type=sync_run)、doSync 执行(full/incremental)
platform/task异步任务系统:tasks 表、worker 池、进度上报、终态判定(success/failed/cancelled)、重启恢复
platform/scheduler统一调度器:enabled+schedule 目标注册 cron 条目,定时触发(triggeredBy=cron)
products/foundry/dataset数据集服务:读取目标数据集元信息、行表命名约定 dataset.RowTableName
products/foundry/lineage血缘打点:RecordFieldMappingsource → dataset 边(失败仅记日志)
products/foundry/quality质量画像:同步成功链触发目标数据集启用的画像(无画像静默跳过)
products/foundry/server路由组装:sync.RegisterRoutes 挂 protected 组、task.RegisterRoutes/system 子组

5.5 关键机制

任务化与轮询(核心):手动运行 POST /sync/targets/:id/run 时,后端 RunSync 先落一条 fs_sync_runs(status=running),再向 task.Manager 提交 type=sync_run 的任务;15 秒等待窗口(taskWaitTimeout)内完成则直接返回结果 + task_id(快路径,前端看到的是"已完成"提示而非进度条),超时则前端经 GET /system/tasks/:id 轮询。前端 startPollingsetInterval(..., 1500) 每 1.5 秒调 getTask,读取 progress(0~1)与 message 更新进度条,直到 status ∈ {success, failed, cancelled} 判定终态,stopPolling 并刷新列表。单次轮询请求失败(网络抖动)静默忽略、下一轮继续,不打断任务。

full 模式:单事务内 DELETE 目标行表全部旧行后逐行参数化 INSERT 重写,保证目标与来源一致。

incremental 模式:源查询被包装为 SELECT * FROM ( <query> ) WHERE watermark_col > ?,水位值参数化绑定(可解析为数值时按数值绑定,规避 SQLite 数值/文本比较失真);首次运行无水位时全量拉取并建立水位;成功后推进 watermark_value。配置了 field_map 时按幂等键(目标列,业务主键语义)冲突跳过,未配置时以水位过滤本身作为防重机制。

调度注册:enabled+schedule 的目标注册 sync:{targetID} 到统一调度器,cron 触发走内部同步执行(不经任务系统,保持调度器防重入语义),运行记录 triggeredBy=cron。创建目标时注册失败(如非法 cron)会回滚删除目标行。

列名安全白名单:schema 列 / watermark 列 / field_map 列一律过 ^[a-z][a-z0-9_]*$ 白名单校验,非法即拒绝(validateColumnName),不做静默改写;全部行值参数化绑定,watermark 值不内联拼 SQL。

6. 核心流程详解

6.1 创建同步目标

  1. 点击「新建同步目标」展开向导(showCreateForm = true)。
  2. 填写目标名称、选择目标数据集(下拉来自 GET /datasets,展示 名称(来源类型,v当前版本))。
  3. 选择来源类型:
  1. 第二步选择同步模式 full / incremental、填写 cron 调度(可空)、可选 field_map JSON、勾选「创建后立即启用」。
  2. 点击「创建目标」:前端先本地 parseFieldMap 解析 field_map(非法 JSON 会提示「field_map 解析失败:...」且不提交),组装 payload 后 POST /sync/targets
  3. 成功后提示「同步目标已创建」,收起表单、清空名称/调度/field_map 字段,刷新目标列表。
  4. 后端 CreateTarget 校验:name 非空、dataset_id 非空且目标数据集存在未归档、source_type/mode/source_ref/field_map 白名单校验;enabled+schedule 时注册调度条目,注册失败回滚删除刚插入的目标行并返回错误。

6.2 编辑同步目标

  1. 点击目标行「编辑」→ startEdit(t):置 editingId,把 source_type/source_ref 拆解回表单(platform_ds 回显 ds_id/query/watermark_col,object 回显 object_id),field_map 由对象渲染为可编辑 JSON 文本,回填 name/dataset_id/mode/schedule/enabled,展开表单。
  2. 编辑态下第一步的目标数据集为可变更下拉(回填当前值),下方提示:变更后既有 field_map 的目标列须存在于新数据集 schema,且增量水位会被重置(新数据集需重新全量拉取后建立水位)。
  3. 点「保存修改」→ 本地解析 field_map(非法 JSON 提示「field_map 解析失败:...」且不提交)→ PUT /sync/targets/:id,body 含 {name, dataset_id, source_type, source_ref, mode, schedule, field_map, enabled}
  4. 成功后提示「同步目标已更新」,收起表单并刷新列表。
  5. 后端 UpdateTarget 为指针语义:请求体未含的字段保持原值;field_map 为空对象会被编码为空串(清空映射);enabled/schedule 变更会同步反注册/重注册调度条目。dataset_id 变更时:新数据集须存在且未归档(否则 404/400)、field_map 目标列须在新 schema(登记型空 schema 跳过)、若与当前不同则写库并重置 watermark_value

6.3 立即运行(任务化主流程)

  1. 点击行内「立即运行」(busy 期间按钮禁用)。
  2. 前端先 stopPolling() 清掉上一次轮询残留,再 POST /sync/targets/:id/run
  3. 响应解析(handleRun):
  1. 轮询循环(startPolling,每 1.5s):
  1. 页面卸载(onBeforeUnmount)调用 stopPolling,防止内存泄漏。

6.4 状态机与任务终态语义

任务状态机(platform/task):

queued ──→ running ──→ success
   │           │          │
   │           └──→ failed
   └──(cancel)─→ cancelled

前端把 ['success', 'failed', 'cancelled'] 视为终态集合,命中即 stopPolling。运行记录(fs_sync_runs)状态机与任务大体平行:running → success / failed

6.5 启停 / 删除 / 运行历史

7. 权限与安全

7.1 认证

7.2 数据级安全

本页面向"行表写入"场景,同步写路径的安全口径是列名白名单 + 参数化绑定

7.3 写操作防护

8. 常见问题与排错

8.1 任务卡在「任务排队中...」进度条不动

  1. 打开浏览器 DevTools Network,确认 GET /system/tasks/:id 返回 200 且 status 仍为 queued
  2. 若 queued 长期不变,说明 worker 未取走任务——检查 Foundry 进程是否卡顿、worker 并发是否被其他长任务(如管道 run)占满;
  3. 观察后端日志是否有「同步任务化失败,降级同步执行」告警(此告警说明走了降级路径);
  4. 若确认任务系统死锁,可重启 Foundry 进程(taskMgr.Restore 会把重启前僵尸 running 任务标记 failed、queued 任务按类型注册表重派)。

8.2 增量同步运行成功但一行都不写入

  1. 打开运行历史抽屉,查看该目标最近几次运行的「同步行数」与列表「水位」列;
  2. 确认源表新增行的 watermark 列值是否大于目标记录的 watermark_value
  3. 若配置了 field_map 且键值与已存在行重复,属预期冲突跳过——检查是否误把业务主键配置成幂等键;
  4. 若源表确实新增但未同步,检查来源查询 SQL 是否包含 watermark 列、列名与水位列一致(列名不一致会在运行失败分支报「源查询结果缺少 watermark 列」)。

8.3 运行失败,错误信息含 SQL 执行错误

  1. 查看错误详情(任务终态提示或运行历史「最近失败」)区分是连接问题还是 SQL 问题;
  2. 先在数据源页对该数据源点「测试连接」,确认连接可达(注意 connector 工厂默认仅放行 MYSQL/SQLSERVER/POSTGRESQL,其余类型需 AllowAdvancedTypes 显式授权);
  3. 将来源 SQL 拿到数据库客户端手工执行,确认可运行且结果含所需列;
  4. 确认目标数据集 schema 列名与源结果列名可映射(列名差异可用 field_map 显式指定 {"源列":"目标列"}),否则报「源结果列 ... 与目标 schema 列 ... 无可映射列」。

8.4 删除目标后旧任务仍在轮询

9. 已知缺陷与边界

缺陷/边界说明
对象来源仅支持 fullsource_type=object 配置 incremental 会返回「object 来源暂仅支持 full 模式」
对象读取器需接线注入ObjectRowReader 未注入时 object 源 RunSync 报「对象行读取器未注入(source_type=object 需接线注入 ObjectRowReader)」
水位文本存储watermark_value 存文本,绑定 SQL 时按内容转数值(可解析为数值则数值绑定,规避 SQLite 数值/文本比较失真)
运行历史默认 50 条前端 listSyncRuns(targetId, 50, 0) 硬编码;后端默认 50、上限 200(runsMaxLimit)
编辑可变更目标数据集(水位重置)行内「编辑」可回填并保存来源/模式/调度/field_map/enabled 与 dataset_idPUT /sync/targets/:id)。边界:dataset_id 变更时后端重置增量水位(watermark_value 置空,下次增量按首次运行全量拉取重建),且 field_map 目标列须在新数据集 schema 内(登记型空 schema 跳过)
field_map 为文本编辑编辑态以 JSON 文本回填 field_map,非法 JSON 前端拦截;不支持可视化列映射
指针语义未传字段保持原值编辑表单提交全部可编辑字段,故不会触发"未传保持不变"的差异;改单字段(如仅 enabled)仍可走 API PUT
任务化 15s 快路径15s 内完成的任务由后端直接返回结果,前端不展示进度条(行为一致,仅展示差异)
full 模式清空目标行表重跑 full 会 DELETE 旧行后重写,属预期语义,但会覆盖外部手动写入目标行表的行
增量防重依赖配置未配置 field_map 时增量以水位过滤本身作为防重机制,同水位不同业务键的合法新行可能被跳过(设计取舍)

10.2 后端文件

10.3 项目文档

10.4 相邻页面