RuoYi-Vue-Plus/ruoyi-modules/ruoyi-sync
2026-09-03 14:14:40 +08:00
..
src/main/java/org/dromara/sync init 2026-09-03 14:14:40 +08:00
pom.xml init 2026-09-03 14:14:40 +08:00
README.md init 2026-09-03 14:14:40 +08:00

数据同步后端V1

本模块把原来的“钉钉盘备份到 OSS”收敛为可扩展的数据同步内核。V1 支持:

  • 源端:钉钉企业网盘 / 我的文件DingTalk Workspace CLI简称 DWS
  • 目标端:标准 S3 兼容存储、阿里云 OSS复用项目原生 S3 客户端)
  • 执行:手动运行、内置 Cron 调度、可选 SnailJob 入口
  • 能力:全量或清单差异同步、在线文档导出、冲突策略、源端删除策略、连接测试、任务明细、对象清单和检查点

1. 架构与扩展点

DingTalkSourceConnector
        │  ScanResult / SourceContent
        ▼
SyncExecutionWorker ── sync_job / sync_job_item / sync_object / sync_checkpoint
        │  TargetWriteRequest
        ├──────────────► S3TargetConnector
        └──────────────► AliyunOssTargetConnector

源、目标均通过接口隔离:

  • SourceConnector:分页扫描、下载或导出源对象
  • TargetConnector:检查对象、上传、删除目标对象
  • ConnectorRegistry:按连接角色和类型选择实现

后续增加其他源或目标时,不需要修改计划、任务和对象清单的数据模型。

2. 数据库

先导入项目基础 SQL再导入 script/sql/ry_sync.sql。该脚本面向 MySQL包含菜单、权限和字典数据。

用途
sync_connection 源/目标连接及加密凭证
sync_plan 路径、策略、并发、Cron 和删除保护配置
sync_job 一次运行及汇总;数据库唯一键保证同一计划只有一个活动任务
sync_job_item 每个文件的上传、跳过或删除结果
sync_object 源对象与目标对象的持久清单,用于差异判断和删除发现
sync_checkpoint 已完成任务水位V1 不把未完成分页游标当作可提交检查点
sync_transfer_part 应用级分片续传的预留表V1 尚未写入

delete_guard_percent 默认是 50。若一次运行拟删除的目标文件比例超过阈值任务会失败且不会执行任何删除只有显式设为 100 才允许一次删除全部目标文件。

3. 必需的服务配置

生产环境必须启用字段加密,否则服务会拒绝保存 secretJson

MYBATIS_ENCRYPTOR_ENABLE=true
MYBATIS_ENCRYPTOR_PASSWORD=<由密钥管理系统注入的 AES 密钥>
DWS_EXECUTABLE=/opt/dws/bin/dws
DWS_PROFILE_ROOT=/var/lib/ruoyi-sync/dws
DWS_KEYCHAIN_ROOT=/var/lib/ruoyi-sync/dws-keychain

请不要把真实密钥写进 YAML、启动脚本或 Git。SyncConnectionVo 不含 secretJson,新增/修改接口的操作日志也排除了该字段。

后端运行时需要 JDK 21项目使用虚拟线程。DWS 建议固定到已验收版本;仓库提供的生产镜像默认使用 v1.0.60,升级 DWS 后应重新做一次设备登录和真实空间访问验收。

内置调度器默认开启。如改由 SnailJob 触发,应设置:

SYNC_SCHEDULER_ENABLED=false

SnailJob 执行器名为 syncPlanJobExecutor,任务参数是同步计划 ID。内置调度和 SnailJob 二选一。

4. 钉钉连接

4.1 DWS 运行契约

运行节点需要安装与验收 DWS并至少确认以下命令存在

dws drive list --help
dws drive download --help
dws doc export --help
dws wiki space list --help

本模块使用原子 drive list 保留版本、修改时间、扩展名等增量证据;普通文件使用原子 drive download 的分片、并发和断点能力;adoc 在线文档使用 doc export(自动提交、轮询并下载)导出为 docx

每个同步连接都有独立的 DWS 配置目录:

${DWS_PROFILE_ROOT}/${connectionId}

登录 token 的加密 keychain 也按连接隔离,目录为:

${DWS_KEYCHAIN_ROOT}/${connectionId}/dws-cli

所有业务命令还会显式传入该连接配置中的 profile=corpId:userId,不会依赖服务进程当前选中的账号。

4.2 登录态初始化

应用密钥不能代替用户 OAuth 登录态。现在首次登录可以完全从管理后台完成,不需要 SSH 到服务器:

  1. 创建一个 DINGTALK/SOURCE 连接。secretJson 填写 AppKey/AppSecretconfigJson 可以暂时不填 profile
  2. 网页调用 POST /sync/connection/{connectionId}/auth/login/start
  3. 页面展示返回的 verificationUriComplete(可生成二维码),同时展示 verificationUriuserCode 作为备用方式。
  4. 用户在自己的浏览器中打开链接并在钉钉完成确认;页面每 2 秒左右调用状态接口,直到 SUCCESSFAILEDEXPIREDCANCELLED
  5. 授权成功后,服务器自动把 DWS 返回的稳定 corpId:userId 写入该连接的 configJson.profile,之后即可执行连接测试和同步。

启动接口返回的对象只含一次性授权链接、验证码和脱敏身份信息,绝不返回 access token、refresh token、device code 或 AppSecret。DWS 进程和加密登录态始终留在后端节点;浏览器只负责完成钉钉授权。

后端只接受 DWS 最终 JSON 中明确返回的 corp_id/corpIduser_id/userId(或明确的 profile 字段),缺少任一 ID 会安全失败不会根据普通日志文本猜测登录人。连接凭证属于创建者普通用户只能查看和操作自己创建的连接超级管理员可跨创建者管理登录操作也仅允许连接创建人或超级管理员发起、查询和取消。登录进行中禁止修改或删除该连接Profile 只能由 Web 授权回调变更,编辑旧表单不会清掉已完成的登录态。

接口契约:

方法 地址 说明
POST /sync/connection/{id}/auth/login/start 启动设备流;可选查询参数 expectedCorpId 用于组织校验
GET /sync/connection/{id}/auth/login/{sessionId} 查询会话状态和展示信息
POST /sync/connection/{id}/auth/login/{sessionId}/cancel 取消会话并终止服务器上的 DWS 进程

DWS 设备流有服务端有效期(通常约 10 分钟过期后重新点击“登录”即可。DWS 会自动刷新已保存的登录态,日常同步不需要重复登录。组织必须允许 DWS/CLI 数据访问,且授权用户必须拥有目标空间的访问权限;否则会话会返回失败提示。

会话状态接口返回 STARTINGWAITING_USERFINALIZINGSUCCESSFAILEDEXPIREDCANCELLED;进入 FINALIZING 后正在写回连接身份,页面应继续轮询并禁止重复发起登录。

生产部署必须持久化下面两个目录,否则重启后会丢失 DWS 登录态:

${DWS_PROFILE_ROOT}
${DWS_KEYCHAIN_ROOT}

当前会话管理器保存在单节点内存中,因此 V1 要求登录页面的三次请求落到同一个后端实例(单实例部署天然满足;多实例请配置会话亲和或单独的认证节点)。后续需要无亲和的集群部署时,再把会话状态和进程协调迁移到 Redis/专用认证服务。

同一节点内Web 授权会独占该连接的 DWS 配置/keychain 锁;正在执行的扫描或下载会让登录请求稍后重试,登录期间新发起的 DWS 业务命令也会被拒绝。若同步 worker 和 Web/API 分布在多个后端节点,除会话亲和外还需要把这把连接锁迁移到 Redis 或数据库分布式锁。

如需运维兜底,仍可在服务器上执行等价的设备流命令;这不是正常使用路径:

DWS_CONFIG_DIR=/var/lib/ruoyi-sync/dws/<connectionId> \
DWS_KEYCHAIN_DIR=/var/lib/ruoyi-sync/dws-keychain/<connectionId> \
DWS_CLIENT_ID=<与该连接 secretJson 一致的 AppKey> \
DWS_CLIENT_SECRET=<与该连接 secretJson 一致的 AppSecret> \
dws auth login --device --no-browser

4.3 连接示例

请求中的 JSON 字符串为展示方便做了格式化;实际字段仍是字符串。

{
  "connectionName": "钉钉企业盘",
  "connectionRole": "SOURCE",
  "connectionType": "DINGTALK",
  "configJson": "{\"profile\":\"corpId:userId\",\"spaceType\":\"orgSpace\",\"spaceId\":\"可选\",\"downloadPartSize\":\"32MB\",\"downloadParallel\":\"4\"}",
  "secretJson": "{\"clientId\":\"AppKey\",\"clientSecret\":\"AppSecret\"}",
  "status": "0"
}
  • spaceId 可选:填写时只同步该空间;不填时按 spaceType 发现空间。
  • spaceTypeorgSpacemySpace,默认 orgSpace
  • sourceRoot/ 表示空间根;指定子目录时必须填写该目录的 dentryUuid,且连接必须指定 spaceId
  • downloadParallel1 至 8默认 4。
  • downloadPartSizeDWS 支持的容量字符串,例如 32MB
  • configDir 不允许由连接指定,避免一个连接读取另一个连接的登录态。

V1 只导出 adoc -> docxaxlsableapptamindadraw 会被识别为在线对象并明确失败,不会错误地按普通文件下载;相应导出适配器留到后续版本。

5. S3 / 阿里云 OSS 连接

标准 S3 示例:

{
  "connectionName": "备份 S3",
  "connectionRole": "TARGET",
  "connectionType": "S3",
  "endpoint": "s3.example.com",
  "region": "us-east-1",
  "bucketName": "company-backup",
  "basePath": "dingtalk",
  "configJson": "{\"useHttps\":true,\"pathStyleAccess\":true}",
  "secretJson": "{\"accessKey\":\"...\",\"secretKey\":\"...\"}",
  "status": "0"
}

阿里云 OSS 示例:

{
  "connectionName": "阿里云 OSS",
  "connectionRole": "TARGET",
  "connectionType": "ALIYUN_OSS",
  "endpoint": "oss-cn-hangzhou.aliyuncs.com",
  "region": "cn-hangzhou",
  "bucketName": "company-backup",
  "basePath": "dingtalk",
  "configJson": "{\"useHttps\":true,\"pathStyleAccess\":false}",
  "secretJson": "{\"accessKey\":\"...\",\"secretKey\":\"...\"}",
  "status": "0"
}

region 为可选配置。留空时与 ruoyi-vue-plus 原生 OSS 配置保持一致,客户端使用 us-east-1 作为默认 Region如已知存储桶所在地域仍建议填写实际值例如 cn-hangzhou)。

连接测试会真实执行 HeadBucket。上传完成后再执行 HeadObject以远端实际大小、ETag、版本 ID 和元数据作为任务结果。

6. 同步语义

计划请求示例:

{
  "planName": "钉钉企业盘每日备份",
  "sourceConnectionId": 1,
  "targetConnectionId": 2,
  "sourceRoot": "/",
  "targetPrefix": "daily",
  "syncMode": "INCREMENTAL",
  "scheduleType": "CRON",
  "cronExpression": "0 0 2 * * *",
  "conflictStrategy": "OVERWRITE",
  "deleteStrategy": "KEEP",
  "deleteGuardPercent": 50,
  "verifyMode": "SHA256",
  "maxConcurrency": 4,
  "bandwidthLimitKbps": 0,
  "status": "0"
}
  • FULL:扫描并重新处理全部文件。
  • INCREMENTAL:仍完整枚举权威目录树,然后用 versionToken -> hash -> modifiedTime + size 依次判断变化;证据不足时保守地重新传输,不会只凭相同大小跳过。
  • OVERWRITE:写入目标键。
  • SKIP:目标键已存在时跳过。
  • KEEP_BOTH:目标键已存在时附加稳定版本后缀。
  • KEEP / MARK:源端消失时保留目标,仅更新清单和任务记录。
  • DELETE:通过删除比例保护后删除目标;删除失败会保留待重试状态,不会误记为已完成。
  • SIZE:以 HeadObject.contentLength 校验。
  • ETAG:要求目标端回读到 ETag不能把不同厂商/不同分片策略的 ETag 当作源端内容哈希。
  • SHA256:本地计算摘要、写入对象元数据并通过 HeadObject 回读校验。

目录页缺少集合、条目缺少关键字段、部分错误、重复游标、重复对象 ID 或目录环路都会使任务失败。失败任务不会进入源删除发现阶段。

7. 后端接口

方法 地址 权限 用途
GET /sync/connection/list sync:connection:list 连接分页
GET /sync/connection/{id} sync:connection:query 连接详情(不返回凭证)
POST /sync/connection sync:connection:add 新增连接
PUT /sync/connection sync:connection:edit 修改连接;空 secretJson 保留原凭证
DELETE /sync/connection/{ids} sync:connection:remove 删除未被计划引用的连接
POST /sync/connection/test/{id} sync:connection:test 真实连通性测试
POST /sync/connection/{id}/auth/login/start sync:connection:auth Web 端启动钉钉设备授权
GET /sync/connection/{id}/auth/login/{sessionId} sync:connection:auth 查询 Web 授权状态
POST /sync/connection/{id}/auth/login/{sessionId}/cancel sync:connection:auth 取消 Web 授权
GET /sync/plan/list sync:plan:list 计划分页
GET /sync/plan/{id} sync:plan:query 计划详情
POST / PUT / DELETE /sync/plan 对应 add/edit/remove 计划维护
POST /sync/job/run/{planId} sync:job:run 立即运行
POST /sync/job/retry/{jobId} sync:job:retry 重新枚举并重试失败对象
POST /sync/job/cancel/{jobId} sync:job:cancel 取消并中断本节点任务
GET /sync/job/list sync:job:list 任务分页
GET /sync/job/{id} sync:job:query 任务汇总
GET /sync/job/{id}/items sync:job:query 文件明细
DELETE /sync/job/{ids} sync:job:remove 删除非活动任务及明细
GET /sync/object/list sync:object:list 对象清单分页
GET /sync/object/{id} sync:object:query 对象详情

8. 当前版本边界

  • Web 登录会话是单节点内存态;多实例部署需要网关会话亲和或专用认证节点。不会把 token 或 device code 写入数据库或返回给浏览器。
  • sync_transfer_part 为应用级、跨任务分片续传预留。当前 DWS 下载和项目原生 S3 客户端各自使用其内部大文件机制,但应用数据库尚不接管 uploadId/part 状态。
  • 带宽字段已预留,但 V1 会拒绝非 0 值,避免界面显示“已限速”而运行时未生效。
  • 应用进程异常退出可能遗留 PENDING/RUNNING/CANCELING 任务。为避免多实例误接管仍在其他节点运行的任务V1 不做激进的启动自动恢复;运维确认原节点已停止后,需要由 DBA 将遗留任务核销为 CANCELED,再点重试。
  • 取消会中断本节点 DWS 命令并在上传、校验、删除前复查数据库状态;对象存储 SDK 已经提交到远端的请求只能尽力取消,取消后重试仍按幂等对象键收敛。
  • 未接真实钉钉账号、S3 或 OSS 凭证时只能完成编译和静态契约验证;上线前必须做真实空间枚举、空文件、大文件、在线文档、权限变化、分页和删除保护验收。