方向反转:此前 SKILL.md 是「模板渲染产物」、sap-cli 是源;现 sap-cli 归档, sap-cli-skill 承接开发与分发,SKILL.md 回归手工维护的正本。 迁移(来自 sap-cli,共 104 文件): - tests/ 692 例测试(15 个文件的内联 sys.path 改指 assets/) - openspec/ SDD 规格与归档变更(42 文件) - docs/ 开发文档与 ADT 原理(含 dev/CLAUDE.md、AGENTS.md) - .claude/ rules 副本 + settings.json(供 Claude Code) - .github/ .hermes/ .pre-commit-config.yaml .editorconfig CLAUDE.md - scripts/ 保持仅 setup.py(pack_skill.py 已随旧仓归档,不迁) 修复(迁移暴露的真实缺陷): - assets/pyproject.toml 的 build-backend 写作 `setuptools.backends._legacy:_Backend`, 该模块在 setuptools 中不存在 → `pip install -e` 从来装不上。改为 build_meta。 实测:临时 venv 安装成功,sap-cli --help 正常列出 31 个命令 - pyproject readme 指向不存在的 assets/README.md(editable 安装会失败)→ 改内联文本 - pyproject urls 改指 sap-cli-skill 机制调整: - .github/workflows/ci.yml 适配 assets/ 布局;顶部注明该工作流仅 GitHub 执行, 本仓在 Gitee 不会自动跑 - pre-commit 增本地测试门禁(Gitee 上真正生效的那道) - .gitignore 合并旧仓完整规则(保留 log/ 下 md 知识库入库,只忽略运行日志) - 大文件上限 100KB→1MB(架构图 512KB) 守卫测试 tests/unit/test_repo_guards.py(10 → 18 例): - SKILL.md 须记录 parser 全部 CLI 命令 / 铁律 1-5 须为真实小节标题 / 示例不得违反铁律 5 - references/ 规则齐备;.claude/rules 与 references 必须一致(实测抓到一次真实漂移) - VERSION == sapcli.__version__ == README 版本 - 仓内不得再出现 pack_skill.py / skill-src(防废弃流程回潮) 698 tests OK;editable 安装与 CLI 入口经临时 venv 实测通过。 docs/RELEASING.md 重写为单源开发流程。
685 lines
23 KiB
Markdown
685 lines
23 KiB
Markdown
---
|
||
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": "<ISO时间戳或null>",
|
||
"last_sync_result": "<结果>"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.2 字段说明
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `version` | int | 清单格式版本号,当前为 `1` |
|
||
| `last_init` | string | 最近一次 `init` 的时间戳 |
|
||
| `last_refresh` | string | 最近一次 `refresh` 的时间戳 |
|
||
| `objects` | object | 以对象名为 key 的字典 |
|
||
| `objects.<name>.type` | string | 对象类型(report/class/interface/...) |
|
||
| `objects.<name>.file` | string | 相对于项目根目录的文件路径 |
|
||
| `objects.<name>.system_status` | string | `active` / `inactive` / `not_exists` |
|
||
| `objects.<name>.corr_nr` | string\|null | 传输请求号,`null` 表示本地对象 `$TMP` |
|
||
| `objects.<name>.depends_on` | string[] | 依赖的对象名列表 |
|
||
| `objects.<name>.last_sync` | string\|null | 最近一次成功同步的时间 |
|
||
| `objects.<name>.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.修改代码-原理|修改代码原理]] — 锁定/编辑/激活工作流
|