refactor: 本仓升为唯一源(原 sap-cli 源码仓归档)
方向反转:此前 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 重写为单源开发流程。
This commit is contained in:
@@ -0,0 +1,684 @@
|
||||
---
|
||||
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.修改代码-原理|修改代码原理]] — 锁定/编辑/激活工作流
|
||||
Reference in New Issue
Block a user