让 AI 登录 SAP 写代码:一次 把 MM031 接口函数的全程托管开发实录

本文记录我如何用 AI(TraeWork + SAP MCP 工具链 + 自定义技能)从一份 Excel 接口文档出发,端到端开发并实测了一个 SAP 库存地点调拨过账函数
ZFM_MM_WMS_TRANSFER。
一、需求从哪来:一份 Excel 接口文档
需求是接口清单里的 MM031:WMS 库存地点调拨过账后同步 SAP,移动类型 311,同步调用。WMS 侧完成调拨后,把单据信息推给 SAP,SAP 侧执行过账并回传物料凭证号。
我把这份 Excel 直接丢给了 AI。它调用文件解析技能读出字段定义,然后自动生成了函数接口设计:
输入参数(10 个)
| 参数 | 类型 | 说明 |
|---|---|---|
| IV_ZWMSNO | CHAR25 | WMS 单号 |
| IV_BUDAT | BUDAT | 过账日期 |
| IV_BWART | BWART | 移动类型 |
| IV_MATNR | MATNR | 物料号 |
| ... |
输出参数(3 个)
| 参数 | 类型 | 说明 |
|---|---|---|
| EV_TYPE | ZE_MSGTY | 消息类型(S 成功 / E 失败) |
| EV_MESSAGE | BAPI_MSG | 消息文本 |
| EV_MBLNR | MBLNR | 物料凭证号 |
人工要做的只有一件事:确认字段与文档一致。剩下的从建传输请求到激活测试,AI 一路走完。
二、我给 AI 配了哪些技能

这次开发不是"一句提示词出代码",而是靠一套分层的技能体系撑起来的。整体分四层:
| 层次 | 名称 | 作用 |
|---|---|---|
| 业务技能 | sap-abap-standard | 内置公司 ABAP 开发规范 V3.0,写出的代码强制过规范检查 |
| 文件技能 | xlsx | 解析 Excel 接口文档,提取字段清单 |
| 知识包 | sap-knowledge-base | 强制执行"加锁 → 写源码 → 解锁 → 激活"的标准对象修改流程 |
| 知识包 | sap-query-routing | 路由数据查询:什么问题查哪张表、走哪个工具 |
| 知识包 | sap-function-call-routing | 标准功能选型:调拨过账场景路由到 BAPI_GOODSMVT_CREATE |
| 底层通道 | SAP MCP 工具链 | 直接连通开发系统的 ADT 与 RFC 双协议 |
其中知识包这一层最有意思。开发过程中有一个 skill_gate 机制,AI 每执行几次数据操作就会被强制要求重读知识包规则,规则包括:修改对象前必须先加锁、写完必须验证激活状态(REPOSRC 中 R3STATE = 'A')、禁止 SELECT *、同一错误连续出现两次必须停下换思路。这套约束让 AI 的操作非常"守规矩",全程没有写出过一条危险语句。
底层 MCP 工具链按用途分三类:
| 类别 | 工具 | 用途 |
|---|---|---|
| 连接与体检 | list_sys* / doc* | 确认开发系统可达、5 项健康检查通过 |
| 对象开发 | createTrans* / adt_create_pack* / adt_create_function_gro* / adt_create_dom* / adt_create_dataelem* / lock / setObjectSour* / unl* / activateByNam* | 从传输请求到函数模块的完整对象链创建 |
| 数据与验证 | runQuer* / runProgra* / tableContent* / getObjectSourc* / transportInf* | 查表防撞名、跑测试程序、验证库存与激活状态 |
三、开发全流程还原
3.1 登录:一条命令打通双通道
AI 调用统一登录工具,用一套凭证同时建立 ADT(REST)与 RFC 两条通道,连接的目标是内网测试系统(地址脱敏为 47.xx.xxx.xxx,账号脱敏为 Z*****147,客户端 1**)。登录后自动做健康检查,5 项全部通过才继续。这一步人只需要在配置里填一次地址和凭据,之后 AI 的所有操作都走这条已认证通道。
3.2 防撞名:先查 TADIR 再动手
写代码之前 AI 先查询对象目录表 TADIR,确认函数组、函数、域、数据元素等所有计划命名在目标系统里都没有占用。这一步人工开发时经常被忽略,撞名了才回头改;AI 把它做成了前置动作,一次通过。
3.3 建传输请求:第一次踩坑
AI 调用创建传输请求工具,第一次提交的请求描述超长被系统拒绝(描述里带了完整中文需求说明),缩短描述后成功创建请求(脱敏为 S4FK9xxxxx),并自动把后续所有对象挂到这个请求下。这里 AI 的处理方式值得学习:报错后它没有重试原样参数,而是截断文本换了个更短的描述一次成功。
3.4 建包与函数组
依次创建开发包 ZMM_INT 和函数组 ZFG_MM_WMS,两个对象创建后自动挂到传输请求。这一步完全无人工干预。
3.5 DDIC 对象:结构命名的坑
输出消息类型需要一个自定义类型,AI 先建域 ZD_MSGTY(CHAR1),再建数据元素 ZE_MSGTY(标签"消息类型"),这一步顺利。
接着它尝试为返回结构建一个结构体,连续两次失败。失败原因很冷门:系统限制结构体名第 2、3 位不能是下划线 ,ZS_WMS_RETURN、ZWMS_RETURN 都撞在这条规则上。AI 的应对是换思路而不是死磕------它评估后发现根本不需要结构体,三个导出参数足够承载返回信息,直接放弃结构体方案继续往下走。这个"及时止损、绕道而行"的决策比多试十次改名字更有价值。
3.6 写函数主体:校验、BAPI、提交三段式
函数 ZFM_MM_WMS_TRANSFER 的主体分三段。第一段是基础校验:
abap
" WMS 单号必输
IF iv_zwmsno IS INITIAL.
ev_type = 'E'.
ev_message = 'WMS单号不能为空'.
RETURN.
ENDIF.
" 移动类型白名单:311/312/313/315
*****
" 工厂与库存地点必输
****
" 311 调拨:转入转出不能相同
***
" 数量必须大于 0
***
第二段组装过账数据。抬头用 BAPI2017_GM_HEAD_01(过账日期取入参、凭证日期取当天、抬头文本和参考单号都填 WMS 单号,方便对账),行项目用 BAPI2017_GM_ITEM_CREATE(物料、工厂、转出/转入地点、移动类型、数量、单位、项目文本)。关键是 GOODSMVT_CODE 传 '04',对应"调拨过账"这个业务场景------这个值不是背出来的,是 AI 通过功能选型知识包路由到的标准答案。
第三段调用过账并处理提交:
abap
CALL FUNCTION 'BAPI_GOODSMVT_CREATE'
EXPORTING
goodsmvt_header = ls_header
goodsmvt_code = '04'
IMPORTING
materialdocument = lv_mblnr
TABLES
return = lt_return.
READ TABLE lt_return INTO ls_return WITH KEY type = 'E'.
*****
BAPI 的经典套路都在:错误立即 ROLLBACK、成功 COMMIT 且带 WAIT = ABAP_TRUE 保证提交落地后再返回凭证号。
3.7 最深的坑:EXCEPTIONS 段导致写入失败
函数写完第一次激活就失败了。AI 做了一轮很漂亮的隔离实验:它构造了一个只有几行的最小函数用例,逐步加入代码片段定位问题,最后锁定------函数源码里只要带 EXCEPTIONS 段(不管是 SYSTEM_FAILURE 这类系统异常还是自定义异常),ADT 写入通道就会失败。
定位后它的解法也很干脆:删掉整个 EXCEPTIONS 段。因为 RFC 框架对 SYSTEM_FAILURE 和 COMMUNICATION_FAILURE 本来就是自动抛出的,自定义异常在这个接口场景里用不上。删掉后语法检查直接通过。
这个过程是全程最有含金量的部分:面对一个报错信息完全不指向根因的问题,AI 用"最小复现 + 变量隔离"的方法,只用了三四个用例就锁定了真凶。
3.8 激活验证:不只看返回值
激活后 AI 没有只看工具返回的"成功",而是查了 REPOSRC 表确认 R3STATE = 'A'(激活态),再用传输请求查询工具确认函数确实挂在请求 S4FK9xxxxx 下。双重验证后才宣布激活完成。

四、Code Review 要点
AI 提交前自检了五条,全部通过:

- 返回结构用消息类型 S/E + 文本,符合公司接口规范
- 失败一律 ROLLBACK,成功 COMMIT 且 WAIT = ABAP_TRUE
- 头表参考单号写入 WMS 单号,方便两边对账
- 无 SELECT *、无嵌套深查、校验前置不过账
- 所有对象挂在同一传输请求,可整体传输
五、复盘:AI 托管开发的体感
从登录到实测通过,全程约 15
分钟,人工只出现三次:给文档、确认接口、看测试结果。传统开发里建请求、建包、建函数组、写代码、调试激活、写测试、查库存这一串动作,AI 全部自主完成,而且每一步都留了验证痕迹。
最深的体感是:AI 的价值不在生成代码的速度,而在决策质量------撞名时先查表、失败时最小复现、期间关闭时换日期、激活后双重验证。这些动作配上知识包的强制规则(加锁、验证激活态、禁 SELECT *),产出的代码直接达到可交付标准。
完整函数代码
ABAP
function zfm_mm_wms_transfer.
*"----------------------------------------------------------------------
*"*"本地接口:
*" IMPORTING
*" VALUE(IV_ZWMSNO) TYPE CHAR25
*" VALUE(IV_BUDAT) TYPE BUDAT
*" VALUE(IV_BWART) TYPE BWART
*" VALUE(IV_MATNR) TYPE MATNR
*" VALUE(IV_ERFMG) TYPE ERFMG
*" VALUE(IV_ERFME) TYPE ERFME
*" VALUE(IV_WERKS) TYPE WERKS_D
*" VALUE(IV_LGORT) TYPE LGORT_D
*" VALUE(IV_UMLGO) TYPE LGORT_D
*" VALUE(IV_SGTXT) TYPE SGTXT OPTIONAL
*" EXPORTING
*" VALUE(EV_TYPE) TYPE ZE_MSGTY
*" VALUE(EV_MESSAGE) TYPE BAPI_MSG
*" VALUE(EV_MBLNR) TYPE MBLNR
*"----------------------------------------------------------------------
DATA: ls_goodsmvt_header TYPE bapi2017_gm_head_01,
ls_goodsmvt_code TYPE bapi2017_gm_code,
ls_goodsmvt_item TYPE bapi2017_gm_item_create,
lt_goodsmvt_item TYPE STANDARD TABLE OF bapi2017_gm_item_create,
lt_return TYPE STANDARD TABLE OF bapiret2,
ls_return TYPE bapiret2,
lv_matdoc TYPE bapi2017_gm_head_ret-mat_doc,
lv_matdoc_year TYPE bapi2017_gm_head_ret-doc_year.
CLEAR: ev_type, ev_message, ev_mblnr.
* 1.基础校验
IF iv_zwmsno IS INITIAL.
ev_type = 'E'.
ev_message = 'WMS单号为空,禁止过账'.
RETURN.
ENDIF.
IF iv_bwart <> '311' AND iv_bwart <> '312'
AND iv_bwart <> '313' AND iv_bwart <> '315'.
ev_type = 'E'.
ev_message = '移动类型不支持,仅支持311/312/313/315'.
RETURN.
ENDIF.
IF iv_werks IS INITIAL OR iv_lgort IS INITIAL OR iv_umlgo IS INITIAL.
ev_type = 'E'.
ev_message = '工厂或库位不能为空'.
RETURN.
ENDIF.
IF iv_bwart = '311' AND iv_lgort = iv_umlgo.
ev_type = 'E'.
ev_message = '转出库位与接收库位不能相同'.
RETURN.
ENDIF.
IF iv_matnr IS INITIAL OR iv_erfme IS INITIAL.
ev_type = 'E'.
ev_message = '物料号或基本计量单位不能为空'.
RETURN.
ENDIF.
IF iv_erfmg IS INITIAL OR iv_erfmg < 0.
ev_type = 'E'.
ev_message = '数量必须大于0'.
RETURN.
ENDIF.
* 2.组装BAPI参数(库存地点调拨:GM_CODE=04)
ls_goodsmvt_header-pstng_date = iv_budat. "过账日期
ls_goodsmvt_header-doc_date = iv_budat. "凭证日期
ls_goodsmvt_header-header_txt = iv_zwmsno. "WMS单号
ls_goodsmvt_header-ref_doc_no = iv_zwmsno. "参考单号
ls_goodsmvt_code-gm_code = '04'. "转移过账
ls_goodsmvt_item-material = iv_matnr. "物料号
ls_goodsmvt_item-plant = iv_werks. "工厂
ls_goodsmvt_item-stge_loc = iv_lgort. "转出库位
ls_goodsmvt_item-move_plant = iv_werks. "接收工厂(同工厂)
ls_goodsmvt_item-move_stloc = iv_umlgo. "接收库位
ls_goodsmvt_item-move_type = iv_bwart. "移动类型
ls_goodsmvt_item-entry_qnt = iv_erfmg. "数量
ls_goodsmvt_item-entry_uom = iv_erfme. "基本计量单位
IF iv_sgtxt IS NOT INITIAL.
ls_goodsmvt_item-item_text = iv_sgtxt. "备注信息
ELSE.
ls_goodsmvt_item-item_text = iv_zwmsno.
ENDIF.
APPEND ls_goodsmvt_item TO lt_goodsmvt_item.
* 3.调用BAPI过账
CALL FUNCTION 'BAPI_GOODSMVT_CREATE'
EXPORTING
goodsmvt_header = ls_goodsmvt_header
goodsmvt_code = ls_goodsmvt_code
IMPORTING
materialdocument = lv_matdoc
matdocumentyear = lv_matdoc_year
TABLES
goodsmvt_item = lt_goodsmvt_item
return = lt_return.
READ TABLE lt_return INTO ls_return WITH KEY type = 'E'.
IF sy-subrc <> 0.
READ TABLE lt_return INTO ls_return WITH KEY type = 'A'.
ENDIF.
IF sy-subrc = 0.
ev_type = 'E'.
ev_message = ls_return-message.
CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
RETURN.
ENDIF.
* 4.提交事务(等待V1更新完成,确保凭证生效)
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
EXPORTING
wait = abap_true.
IF lv_matdoc IS NOT INITIAL.
ev_type = 'S'.
ev_message = |WMS单号{ iv_zwmsno }库存地点调拨过账成功|.
ev_mblnr = lv_matdoc.
ELSE.
ev_type = 'E'.
ev_message = '调拨过账未生成物料凭证'.
ENDIF.
endfunction.
完成的测试Demo
ABAP
REPORT z_test_wms_transfer.
* 临时测试:验证 ZFM_MM_WMS_TRANSFER 函数接口功能(过账期间 2026/07)
DATA: lv_type TYPE ze_msgty,
lv_message TYPE bapi_msg,
lv_mblnr TYPE mblnr,
lv_matnr TYPE matnr,
lv_stock TYPE mard-labst,
lv_mjahr TYPE mkpf-mjahr.
lv_matnr = '000000000010000002'.
* 调拨前库存
SELECT SINGLE labst INTO lv_stock FROM mard
WHERE matnr = lv_matnr AND werks = '1010' AND lgort = '1010'.
WRITE: / '调拨前 1010库位库存:', lv_stock.
SELECT SINGLE labst INTO lv_stock FROM mard
WHERE matnr = lv_matnr AND werks = '1010' AND lgort = '1011'.
WRITE: / '调拨前 1011库位库存:', lv_stock.
* 正常调拨(311) 1010工厂 1010->1011 库位 过账日期20260731
CALL FUNCTION 'ZFM_MM_WMS_TRANSFER'
EXPORTING
iv_zwmsno = 'WMS20260731001'
iv_budat = '20260731'
iv_bwart = '311'
iv_matnr = lv_matnr
iv_erfmg = '2.000'
iv_erfme = 'ST'
iv_werks = '1010'
iv_lgort = '1010'
iv_umlgo = '1011'
iv_sgtxt = '接口测试'
IMPORTING
ev_type = lv_type
ev_message = lv_message
ev_mblnr = lv_mblnr.
WRITE: / '场景1(正常311调拨):', lv_type, lv_message, lv_mblnr.
IF lv_mblnr IS NOT INITIAL.
* 凭证核验:MKPF
SELECT SINGLE mjahr INTO lv_mjahr FROM mkpf
WHERE mblnr = lv_mblnr AND mjahr = '2026'.
WRITE: / ' 凭证年:', lv_mjahr.
SELECT SINGLE labst INTO lv_stock FROM mard
WHERE matnr = lv_matnr AND werks = '1010' AND lgort = '1010'.
WRITE: / '调拨后 1010库位库存:', lv_stock.
SELECT SINGLE labst INTO lv_stock FROM mard
WHERE matnr = lv_matnr AND werks = '1010' AND lgort = '1011'.
WRITE: / '调拨后 1011库位库存:', lv_stock.
ENDIF.