mirror of
https://gitee.com/dromara/RuoYi-Vue-Plus.git
synced 2026-09-16 08:48:17 +08:00
281 lines
15 KiB
Markdown
281 lines
15 KiB
Markdown
# 数据同步后端(V1)
|
||
|
||
本模块把原来的“钉钉盘备份到 OSS”收敛为可扩展的数据同步内核。V1 支持:
|
||
|
||
- 源端:钉钉企业网盘 / 我的文件(DingTalk Workspace CLI,简称 DWS)
|
||
- 目标端:标准 S3 兼容存储、阿里云 OSS(复用项目原生 S3 客户端)
|
||
- 执行:手动运行、内置 Cron 调度、可选 SnailJob 入口
|
||
- 能力:全量或清单差异同步、在线文档导出、冲突策略、源端删除策略、连接测试、任务明细、对象清单和检查点
|
||
|
||
## 1. 架构与扩展点
|
||
|
||
```text
|
||
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`](../../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`:
|
||
|
||
```bash
|
||
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 触发,应设置:
|
||
|
||
```bash
|
||
SYNC_SCHEDULER_ENABLED=false
|
||
```
|
||
|
||
SnailJob 执行器名为 `syncPlanJobExecutor`,任务参数是同步计划 ID。内置调度和 SnailJob 二选一。
|
||
|
||
## 4. 钉钉连接
|
||
|
||
### 4.1 DWS 运行契约
|
||
|
||
运行节点需要安装与验收 DWS,并至少确认以下命令存在:
|
||
|
||
```bash
|
||
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 配置目录:
|
||
|
||
```text
|
||
${DWS_PROFILE_ROOT}/${connectionId}
|
||
```
|
||
|
||
登录 token 的加密 keychain 也按连接隔离,目录为:
|
||
|
||
```text
|
||
${DWS_KEYCHAIN_ROOT}/${connectionId}/dws-cli
|
||
```
|
||
|
||
所有业务命令还会显式传入该连接配置中的 `profile=corpId:userId`,不会依赖服务进程当前选中的账号。
|
||
|
||
### 4.2 登录态初始化
|
||
|
||
应用密钥不能代替用户 OAuth 登录态。现在首次登录可以完全从管理后台完成,不需要 SSH 到服务器:
|
||
|
||
1. 创建一个 `DINGTALK/SOURCE` 连接。`secretJson` 填写 AppKey/AppSecret;`configJson` 可以暂时不填 `profile`。
|
||
2. 网页调用 `POST /sync/connection/{connectionId}/auth/login/start`。
|
||
3. 页面展示返回的 `verificationUriComplete`(可生成二维码),同时展示 `verificationUri` 和 `userCode` 作为备用方式。
|
||
4. 用户在自己的浏览器中打开链接并在钉钉完成确认;页面每 2 秒左右调用状态接口,直到 `SUCCESS`、`FAILED`、`EXPIRED` 或 `CANCELLED`。
|
||
5. 授权成功后,服务器自动把 DWS 返回的稳定 `corpId:userId` 写入该连接的 `configJson.profile`,之后即可执行连接测试和同步。
|
||
|
||
启动接口返回的对象只含一次性授权链接、验证码和脱敏身份信息,绝不返回 access token、refresh token、device code 或 AppSecret。DWS 进程和加密登录态始终留在后端节点;浏览器只负责完成钉钉授权。
|
||
|
||
后端只接受 DWS 最终 JSON 中明确返回的 `corp_id`/`corpId` 与 `user_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 数据访问,且授权用户必须拥有目标空间的访问权限;否则会话会返回失败提示。
|
||
|
||
会话状态接口返回 `STARTING`、`WAITING_USER`、`FINALIZING`、`SUCCESS`、`FAILED`、`EXPIRED` 或 `CANCELLED`;进入 `FINALIZING` 后正在写回连接身份,页面应继续轮询并禁止重复发起登录。
|
||
|
||
生产部署必须持久化下面两个目录,否则重启后会丢失 DWS 登录态:
|
||
|
||
```text
|
||
${DWS_PROFILE_ROOT}
|
||
${DWS_KEYCHAIN_ROOT}
|
||
```
|
||
|
||
当前会话管理器保存在单节点内存中,因此 V1 要求登录页面的三次请求落到同一个后端实例(单实例部署天然满足;多实例请配置会话亲和或单独的认证节点)。后续需要无亲和的集群部署时,再把会话状态和进程协调迁移到 Redis/专用认证服务。
|
||
|
||
同一节点内,Web 授权会独占该连接的 DWS 配置/keychain 锁;正在执行的扫描或下载会让登录请求稍后重试,登录期间新发起的 DWS 业务命令也会被拒绝。若同步 worker 和 Web/API 分布在多个后端节点,除会话亲和外还需要把这把连接锁迁移到 Redis 或数据库分布式锁。
|
||
|
||
如需运维兜底,仍可在服务器上执行等价的设备流命令;这不是正常使用路径:
|
||
|
||
```bash
|
||
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 字符串为展示方便做了格式化;实际字段仍是字符串。
|
||
|
||
```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` 发现空间。
|
||
- `spaceType`:`orgSpace` 或 `mySpace`,默认 `orgSpace`。
|
||
- `sourceRoot`:`/` 表示空间根;指定子目录时必须填写该目录的 `dentryUuid`,且连接必须指定 `spaceId`。
|
||
- `downloadParallel`:1 至 8,默认 4。
|
||
- `downloadPartSize`:DWS 支持的容量字符串,例如 `32MB`。
|
||
- `configDir` 不允许由连接指定,避免一个连接读取另一个连接的登录态。
|
||
|
||
V1 只导出 `adoc -> docx`。`axls`、`able`、`appt`、`amind`、`adraw` 会被识别为在线对象并明确失败,不会错误地按普通文件下载;相应导出适配器留到后续版本。
|
||
|
||
## 5. S3 / 阿里云 OSS 连接
|
||
|
||
标准 S3 示例:
|
||
|
||
```json
|
||
{
|
||
"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 示例:
|
||
|
||
```json
|
||
{
|
||
"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. 同步语义
|
||
|
||
计划请求示例:
|
||
|
||
```json
|
||
{
|
||
"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 凭证时只能完成编译和静态契约验证;上线前必须做真实空间枚举、空文件、大文件、在线文档、权限变化、分页和删除保护验收。
|