--- title: 批量同步设计 — 项目清单驱动 created: 2026-05-25 updated: 2026-05-25 tags: - SAP - ADT - design parent: "[[sap-cli/doc/sap-cli使用指南|sap-cli使用指南]]" related: - "[[sap-cli/doc/技术说明|技术说明]]" - "[[sap-cli/doc/sap-cli使用指南#4.3 源代码同步激活 (`sync`)|sync 命令]]" --- # 批量同步设计 — 项目清单驱动 > [!abstract] 概述 > 为 sap-cli 工具增加**项目清单**机制,支持批量源代码同步。核心思路:在源代码目录下维护 `manifest.json` 清单文件,记录每个开发对象在 SAP 系统中的状态,后续操作基于清单驱动,避免重复查询。 > [!info] 设计动机 > 现有 `sync` 命令只支持单对象操作,缺少以下能力: > - 批量扫描本地文件并识别对象 > - 预查询 SAP 系统状态,制定执行计划 > - 按依赖关系排序,批量执行同步 > - 缓存对象状态,避免每次重复查询 ## 1. 目录约定 ### 1.1 项目目录结构 项目根目录下按对象类型组织子目录,`manifest.json` 位于根目录: ``` project/ ← --path 指向这里(项目根目录) ├── manifest.json ← 状态清单(init 自动生成) ├── reports/ │ └── {对象名小写}.abap ├── classes/ │ └── {对象名小写}.abap ├── interfaces/ │ └── {对象名小写}.abap ├── functions/ │ └── {函数组名小写}/ │ └── {函数模块名小写}.abap ├── domains/ │ └── {对象名小写}.abap ├── dataelements/ │ └── {对象名小写}.abap ├── tables/ │ └── {对象名小写}.abap └── tabletypes/ └── {对象名小写}.abap ``` ### 1.2 目录名 → 类型映射 `init` 扫描时按以下映射表从目录名推断对象类型: | 目录名 | 对象类型 | 文件 → 对象名规则 | |--------|---------|------------------| | `reports/` | `report` | `zmy_report.abap` → `ZMY_REPORT` | | `classes/` | `class` | `zcl_my_class.abap` → `ZCL_MY_CLASS` | | `interfaces/` | `interface` | `zif_my_interface.abap` → `ZIF_MY_INTERFACE` | | `functions/` | `function` | `zgroup/z_my_func.abap` → `ZGROUP/Z_MY_FUNC` | | `domains/` | `domain` | `z_status.abap` → `Z_STATUS` | | `dataelements/` | `dataelement` | `z_status.abap` → `Z_STATUS` | | `tables/` | `table` | `zmy_table.abap` → `ZMY_TABLE` | | `tabletypes/` | `tabletype` | `zty_table.abap` → `ZTY_TABLE` | > [!note] 文件名 → 对象名规则 > - 文件名去掉 `.abap` 后缀后转为大写即为对象名 > - `functions/` 下的子目录名作为函数组名,子目录内的文件名作为函数模块名 > - 对象名为 `函数组名大写/函数模块名大写` 格式 ## 2. 清单文件 (`manifest.json`) ### 2.1 结构定义 ```json { "version": 1, "last_init": "2026-05-25T10:00:00", "last_refresh": "2026-05-25T14:00:00", "objects": { "<对象名>": { "type": "<对象类型>", "file": "<相对文件路径>", "system_status": "<状态>", "corr_nr": "<传输请求号或null>", "depends_on": ["<依赖对象名>"], "last_sync": "", "last_sync_result": "<结果>" } } } ``` ### 2.2 字段说明 | 字段 | 类型 | 说明 | |------|------|------| | `version` | int | 清单格式版本号,当前为 `1` | | `last_init` | string | 最近一次 `init` 的时间戳 | | `last_refresh` | string | 最近一次 `refresh` 的时间戳 | | `objects` | object | 以对象名为 key 的字典 | | `objects..type` | string | 对象类型(report/class/interface/...) | | `objects..file` | string | 相对于项目根目录的文件路径 | | `objects..system_status` | string | `active` / `inactive` / `not_exists` | | `objects..corr_nr` | string\|null | 传输请求号,`null` 表示本地对象 `$TMP` | | `objects..depends_on` | string[] | 依赖的对象名列表 | | `objects..last_sync` | string\|null | 最近一次成功同步的时间 | | `objects..last_sync_result` | string | `success` / `failed` / `pending` | ### 2.3 示例 ```json { "version": 1, "last_init": "2026-05-25T10:00:00", "last_refresh": null, "objects": { "Z_STATUS": { "type": "domain", "file": "domains/z_status.abap", "system_status": "active", "corr_nr": "DEVK901362", "depends_on": [], "last_sync": "2026-05-25T10:30:00", "last_sync_result": "success" }, "ZIF_MY_INTERFACE": { "type": "interface", "file": "interfaces/zif_my_interface.abap", "system_status": "active", "corr_nr": "DEVK901362", "depends_on": [], "last_sync": "2026-05-25T10:30:00", "last_sync_result": "success" }, "ZCL_MY_CLASS": { "type": "class", "file": "classes/zcl_my_class.abap", "system_status": "active", "corr_nr": "DEVK901362", "depends_on": ["ZIF_MY_INTERFACE"], "last_sync": "2026-05-25T11:00:00", "last_sync_result": "success" }, "ZMY_REPORT": { "type": "report", "file": "reports/zmy_report.abap", "system_status": "inactive", "corr_nr": null, "depends_on": ["ZCL_MY_CLASS"], "last_sync": null, "last_sync_result": "pending" }, "ZGROUP/Z_MY_FUNC": { "type": "function", "file": "functions/zgroup/z_my_func.abap", "system_status": "active", "corr_nr": "DEVK901362", "depends_on": [], "last_sync": "2026-05-25T10:00:00", "last_sync_result": "success" } } } ``` ## 3. 命令变更一览 | 命令 | 变更类型 | 说明 | |------|---------|------| | `init` | **新增** | 扫描项目目录 → 查询 SAP → 生成 `manifest.json` | | `create` | **变更** | 新增 `--path` 参数;创建成功后自动写入清单 | | `sync` | **变更** | 新增 `--all` 批量模式;单对象模式操作后更新清单 | | `delete` | **变更** | 新增 `--path` 参数;删除成功后从清单移除 | | `refresh` | **新增** | 重新查询清单中所有对象的 SAP 状态 | ## 4. 命令详细设计 ### 4.1 `init` — 项目初始化(新增) #### 命令格式 ```bash python main.py init --path <项目根目录> ``` #### 执行流程 ``` 扫描项目目录 ├─ 按目录映射表识别文件 → 生成 {对象名, 类型, 文件路径} 列表 ├─ 输出: "扫描到 N 个本地文件" └─ 忽略非 .abap 文件和隐藏文件 逐个查询 SAP 系统 ├─ 对象存在 → 查询 info → 获取 system_status + corr_nr ├─ 对象不存在 → 标记 system_status: "not_exists" └─ 查询失败 → 标记 system_status: "unknown" 应用类型默认优先级生成初始 depends_on (init 时只设置类型层面的默认依赖,用户可后续手动编辑) domain/dataelement/table → 无依赖 tabletype → 依赖同项目中的 table interface → 无依赖 class → 依赖同项目中被引用的 interface function → 无显式依赖 report → 无显式依赖 写入 manifest.json 输出汇总报告 ``` #### 输出示例 ``` ============================================================ sap-cli 项目初始化 ============================================================ 项目路径: ./project 扫描到 5 个本地文件 → 正在查询 SAP 系统状态... ✓ Z_STATUS (domain) active corr: DEVK901362 ✓ ZIF_MY_INTERFACE (interface) active corr: DEVK901362 ✓ ZCL_MY_CLASS (class) active corr: DEVK901362 ⚠ ZMY_REPORT (report) inactive corr: 本地对象 ✗ ZCL_NEW_CLASS (class) 不存在 ✓ 清单已生成: ./project/manifest.json 5 个对象: 3 已激活, 1 未激活, 1 不存在 ``` > [!tip] 清单可手动编辑 > `manifest.json` 生成后,用户可以手动编辑 `depends_on` 字段来补充对象间的显式依赖关系。 > `init` 只设置类型层面的默认依赖,具体的引用关系(如某个 class 实现了哪个 interface)需要用户自行标注。 --- ### 4.2 `sync --all` — 批量同步(新增) #### 命令格式 ```bash python main.py sync --all --path <项目根目录> [--corr_nr <传输请求号>] [--fail-fast] [--dry-run] ``` | 参数 | 必需 | 说明 | |------|------|------| | `--all` | 是 | 启用批量模式 | | `--path` | 是 | 项目根目录(包含 `manifest.json`) | | `--corr_nr` | 否 | 统一传输请求号(跳过交互选择,所有对象共用) | | `--fail-fast` | 否 | 遇到失败立即停止(默认:跳过并继续) | | `--dry-run` | 否 | 仅输出执行计划,不实际操作 | #### 执行流程 ``` 读取 manifest.json ├─ 不存在 → 报错: "请先执行 init" └─ 存在 → 继续 扫描本地文件 vs 清单差异 ├─ 本地有但清单没有的文件 → 提示: "N 个未纳入清单的文件,建议先 create" └─ 清单有但本地被删的文件 → 警告,跳过 过滤待处理对象 ├─ system_status == "active" && last_sync_result == "success" → 跳过(已同步) ├─ system_status == "not_exists" → 标记为 [create + sync] └─ 其他 → 标记为 [sync] 拓扑排序 ├─ 按 depends_on 建有向图 ├─ 无显式依赖的按类型默认优先级排序 └─ 检测循环依赖 → 报错退出 --dry-run 模式: 输出执行计划,不实际操作 传输请求预选择(一次性交互): ├─ 扫描待处理对象,统计需要传输请求号的数量 ├─ 指定了 --corr_nr → 直接使用,无交互 ├─ 全部对象已绑定请求号或为本地对象 → 无交互 └─ 有对象需要请求号 → 弹出一次选择,选定后全局复用: ├─ 选择已有请求号 → 后续所有需要请求号的对象都使用此请求号 ├─ 新建请求号 → 同上 └─ 不使用请求号 → 所需对象都作为本地对象处理 执行阶段(按排序顺序逐个处理,使用预选的传输请求号): 对每个对象: ├─ 本地文件不存在 → 跳过,警告 ├─ system_status == "not_exists" → 先执行 create(使用预选请求号) ├─ 执行 sync 逻辑(使用预选请求号,跳过逐对象交互): │ ├─ 对象已绑定请求号 → 使用对象自身绑定的请求号 │ ├─ 本地对象($TMP)→ 无需请求号 │ └─ 未绑定 → 使用预选的统一请求号(不再弹出交互) ├─ 成功 → 更新 manifest: │ system_status: "active" │ corr_nr: 实际使用的请求号 │ last_sync: 当前时间 │ last_sync_result: "success" └─ 失败 → ├─ --fail-fast → 停止,输出已处理和未处理的对象 └─ 默认 → 标记 last_sync_result: "failed",跳过,继续下一个 后续依赖此对象的对象也自动跳过(标记 skipped) 输出汇总报告 ``` #### dry-run 输出示例 ``` ============================================================ sap-cli 批量同步 (dry-run) ============================================================ 项目路径: ./project 清单对象: 5 执行计划(按依赖排序): 1. Z_STATUS (domain) → sync [inactive → active] 2. ZIF_MY_INTERFACE (interface) → 跳过 [已是最新] 3. ZCL_MY_CLASS (class) → sync [依赖: ZIF_MY_INTERFACE ✓] 4. ZMY_REPORT (report) → sync [依赖: ZCL_MY_CLASS] 5. ZGROUP/Z_MY_FUNC (function) → 跳过 [已是最新] 待处理: 3 个 | 跳过: 2 个 ``` #### 执行输出示例 ``` ============================================================ sap-cli 批量同步 ============================================================ 项目路径: ./project 待处理: 3 个对象 → 预检传输请求需求... 2 个对象已绑定请求号: Z_STATUS(DEVK901362), ZCL_MY_CLASS(DEVK901362) 1 个对象需要传输请求: ZMY_REPORT → 查询可用的传输请求... 找到 2 个可修改的传输请求: 1. DEVK901362 Implement sales validation (所有者: ADMIN2) 2. DEVK901365 Bugfix for invoice printing (所有者: ADMIN2) 3. 新建传输请求 0. 不使用传输请求(本地对象) 请选择 [0-3]: 1 ✓ 统一传输请求: DEVK901362(后续所有对象共用此请求号) ── [1/3] Z_STATUS (domain) ── ✓ 锁定成功(对象已绑定传输请求: DEVK901362) ✓ 源代码写入成功 ✓ 解锁成功 ✓ 语法检查通过 ✓ 激活成功 ── [2/3] ZCL_MY_CLASS (class) ── ✓ 锁定成功(对象已绑定传输请求: DEVK901362) ✓ 源代码写入成功 ✓ 解锁成功 ✓ 语法检查通过 ✓ 激活成功 ── [3/3] ZMY_REPORT (report) ── ✓ 锁定成功(使用统一传输请求: DEVK901362) ✓ 源代码写入成功 ✓ 解锁成功 ✗ 语法检查未通过! 1 个错误 [错误] 行 10: Field "XXX" is unknown ⊘ 跳过(标记为 failed) ============================================================ 批量同步完成 ============================================================ ✓ 成功: Z_STATUS, ZCL_MY_CLASS ✗ 失败: ZMY_REPORT (语法错误,行 10) ⊘ 跳过: (无) 清单已更新: ./project/manifest.json ``` > [!tip] 指定 `--corr_nr` 跳过交互 > 如果已知传输请求号,使用 `--corr_nr` 参数可以直接跳过交互选择: > ```bash > python main.py sync --all --path ./project --corr_nr DEVK901362 > ``` > 此时所有未绑定请求号的对象都会使用指定的请求号,**全程零交互**。 --- ### 4.3 `refresh` — 刷新清单状态(新增) #### 命令格式 ```bash python main.py refresh --path <项目根目录> ``` #### 执行流程 ``` 读取 manifest.json 逐个重新查询 SAP info 更新每个对象的 system_status 和 corr_nr (不修改 last_sync 和 last_sync_result) 输出变更摘要 ``` #### 使用场景 - 其他人修改了 SAP 端的对象 - 传输请求状态发生变化(释放、导入等) - 需要确认当前系统状态但不想执行同步 #### 输出示例 ``` ============================================================ sap-cli 清单刷新 ============================================================ 项目路径: ./project 清单对象: 5 → 正在查询 SAP 系统状态... ✓ Z_STATUS (domain) active corr: DEVK901362 (未变化) ✓ ZIF_MY_INTERFACE (interface) active corr: DEVK901362 (未变化) ~ ZCL_MY_CLASS (class) inactive corr: DEVK901362 (状态变更: active → inactive) ✓ ZMY_REPORT (report) inactive corr: null (未变化) ✓ ZGROUP/Z_MY_FUNC (function) active corr: DEVK901362 (未变化) ✓ 刷新完成 变更: 1 个 | 未变化: 4 个 ``` --- ### 4.4 `create` — 变更:自动写入清单 #### 变更内容 - 新增 `--path` 参数:指向项目根目录 - 创建成功后:自动在 `manifest.json` 中新增一条记录 #### 新增的清单记录 ```json { "ZCL_NEW_CLASS": { "type": "class", "file": "classes/zcl_new_class.abap", "system_status": "active", "corr_nr": "DEVK901362", "depends_on": [], "last_sync": "2026-05-25T14:00:00", "last_sync_result": "success" } } ``` #### 向后兼容 - 如果未指定 `--path`,或指定目录下没有 `manifest.json`:仅创建对象,不写清单 - 现有不含 `--path` 的用法不受影响 --- ### 4.5 `delete` — 变更:从清单移除 #### 变更内容 - 新增 `--path` 参数:指向项目根目录 - 删除成功后:从 `manifest.json` 中移除该对象记录 #### 向后兼容 - 如果未指定 `--path`,或指定目录下没有 `manifest.json`:仅删除对象,不更新清单 --- ### 4.6 `sync`(单对象) — 变更:操作后更新清单 #### 变更内容 - 新增 `--path` 参数:指向项目根目录(可选) - sync 成功或失败后:更新 `manifest.json` 中对应对象的记录 #### 向后兼容 - 如果未指定 `--path`,或指定目录下没有 `manifest.json`:行为与现有完全一致 ## 5. 依赖排序策略 ### 5.1 类型默认优先级 无显式 `depends_on` 时,按类型优先级排序: | 优先级 | 数值 | 类型 | 说明 | |--------|------|------|------| | 1 | 10 | `domain` | 基础域定义 | | 2 | 20 | `dataelement` | 依赖 domain | | 3 | 30 | `table` | 依赖 dataelement | | 4 | 40 | `tabletype` | 依赖 table | | 5 | 50 | `interface` | 接口定义 | | 6 | 60 | `class` | 可能实现 interface | | 7 | 70 | `function` | 函数模块 | | 8 | 80 | `report` | 程序,可能引用以上所有 | ### 5.2 排序算法 采用拓扑排序(Kahn 算法): ``` 1. 以 manifest 中所有对象为节点 2. 建立有向边: - 显式依赖: depends_on 中列出的对象 → 当前对象 - 类型默认优先级: 仅当两个对象之间没有显式依赖时作为 tie-breaker 3. 执行 Kahn 拓扑排序 4. 检测循环依赖 → 若存在未入队的节点则报错,输出循环链 ``` ### 5.3 依赖失败传播 批量 `sync` 时,若对象 A 依赖对象 B,而 B 同步失败: ``` A.depends_on = ["B"] B.last_sync_result = "failed" → A 自动跳过,标记 last_sync_result: "skipped" → 依赖 A 的对象同样跳过(级联跳过) ``` ## 6. 边界情况处理 | 场景 | 处理方式 | |------|---------| | 本地新增了 .abap 文件但清单中没有 | `sync --all` 时提示: "N 个未纳入清单的文件",建议先执行 `create` | | 清单中有对象但本地文件被删了 | `sync --all` 时警告并跳过该对象 | | `manifest.json` 不存在 | `sync --all` 报错,提示先执行 `init` | | 依赖对象 sync 失败 | 跳过所有依赖它的对象(级联标记 skipped) | | 循环依赖 | 检测到后报错,输出循环链中的对象名 | | 非项目目录(无子目录结构) | `init` 时只扫描当前目录下的 `.abap` 文件 | | SAP 连接中断 | 标记已处理的对象,输出中断位置,下次可从断点继续 | | `manifest.json` 版本不匹配 | 提示版本不兼容,建议重新 `init` | | 部分对象已绑定请求号,部分未绑定 | 已绑定的使用自身请求号,未绑定的使用预选的统一请求号 | | 全部对象都是本地对象($TMP) | 无需传输请求,直接执行,零交互 | | 指定了 `--corr_nr` 但部分对象已绑定其他请求号 | 已绑定的使用自身请求号(不受 `--corr_nr` 覆盖) | ## 7. 项目生命周期 ``` ┌─────────────────────────────────────────────────────────┐ │ │ │ init ────────────────────────────────────────────── │ │ │ 扫描本地文件 → 查询 SAP → 生成 manifest.json │ │ │ │ │ ▼ │ │ create(新增对象时) ──────────────────────────────── │ │ │ 在 SAP 创建 → 自动写入 manifest │ │ │ │ │ ▼ │ │ sync --all(日常同步)───────────────────────────── │ │ │ 读 manifest → 拓扑排序 → 逐个 sync → 更新 manifest │ │ │ │ │ ▼ │ │ refresh(SAP 端有变化时)──────────────────────────── │ │ 重新查询 SAP → 更新 manifest 中的状态字段 │ │ │ └─────────────────────────────────────────────────────────┘ ``` > [!tip] 典型工作流 > ```bash > # 1. 首次使用:初始化项目清单 > python main.py init --path ./my_project > > # 2. 日常开发:新增对象 > python main.py create --name ZCL_NEW --type class --path ./my_project --description "新类" > > # 3. 日常开发:修改本地 .abap 文件后,批量同步 > python main.py sync --all --path ./my_project > > # 4. 如需查看执行计划(不实际操作) > python main.py sync --all --path ./my_project --dry-run > > # 5. 其他人修改了 SAP 端对象后,刷新清单 > python main.py refresh --path ./my_project > ``` ## 8. 模块设计(实现层面) ### 8.1 新增模块 | 模块 | 职责 | |------|------| | `sapcli/manifest.py` | 清单文件的读取、写入、查询、更新 | | `sapcli/scanner.py` | 扫描本地目录,识别对象名和类型 | | `sapcli/sorter.py` | 拓扑排序,依赖检测 | ### 8.2 清单模块 (`manifest.py`) 接口 ```python @dataclass class ManifestEntry: name: str type: str file: str system_status: str # "active" / "inactive" / "not_exists" corr_nr: str | None depends_on: list[str] last_sync: str | None # ISO timestamp last_sync_result: str # "success" / "failed" / "pending" class Manifest: version: int last_init: str | None last_refresh: str | None objects: dict[str, ManifestEntry] @classmethod def load(cls, project_path: str) -> "Manifest": ... def save(self) -> None: ... def upsert(self, entry: ManifestEntry) -> None: ... def remove(self, name: str) -> None: ... def get(self, name: str) -> ManifestEntry | None: ... def pending_objects(self) -> list[ManifestEntry]: ... ``` ### 8.3 扫描模块 (`scanner.py`) 接口 ```python DIRECTORY_TYPE_MAP = { "reports": "report", "classes": "class", "interfaces": "interface", "functions": "function", "domains": "domain", "dataelements": "dataelement", "tables": "table", "tabletypes": "tabletype", } @dataclass class ScannedObject: name: str # 大写对象名,function 类型含 / type: str # 对象类型 file: str # 相对于项目根目录的路径 def scan_project(project_path: str) -> list[ScannedObject]: ... ``` ### 8.4 排序模块 (`sorter.py`) 接口 ```python TYPE_PRIORITY = { "domain": 10, "dataelement": 20, "table": 30, "tabletype": 40, "interface": 50, "class": 60, "function": 70, "report": 80, } class CyclicDependencyError(SapCliError): ... def topological_sort( objects: list[ManifestEntry], ) -> list[ManifestEntry]: ... # → 按 depends_on + TYPE_PRIORITY 排序 # → 检测循环依赖,抛出 CyclicDependencyError ``` ## 9. 相关笔记 - [[sap-cli/doc/sap-cli使用指南|sap-cli 使用指南]] — 现有命令的使用说明 - [[sap-cli/doc/技术说明|技术说明]] — 技术架构与模块设计 - [[sap-cli/doc/ADT/03.修改代码-原理|修改代码原理]] — 锁定/编辑/激活工作流