ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SAP BSP 报表开发实战:从 SE80 到可交互 HTML 报表完整落地

SAP BSP 报表开发实战:从 SE80 到可交互 HTML 报表完整落地

简介:这份资源是面向SAP ABAP开发者的BSP报表开发实例,适合已掌握ABAP基础、希望入门SAP Web应用与动态报表的初中级开发者。内容围绕SE80中创建BSP应用与页面、引用CSS样式、在HTML中嵌入ABAP脚本、通过表单与事件处理实现用户交互,以及用loop循环将内部表数据绑定到HTML表格等核心环节展开,帮助读者理解BSP如何把数据库数据与前端页面结合,构建可交互的Web报表。资源包为1个doc文档,约433KB,以图文步骤与代码片段为主,便于对照练习。目前已有378人学习下载。通过该实例,读者可掌握BSP应用创建流程、页面布局与样式引用、ABAP表达式动态输出、表单提交与onInputProcessing事件处理等关键技能,并积累将ABAP逻辑与HTML界面整合的实战经验,为后续开发更复杂的SAP Web报表打下基础。

1. BSP 报表开发实例:从 SE80 到可交互 HTML 报表的完整落地

手里有一份 2007 年的 BSP 报表开发实例,作者把 SE80 建应用、页面创建、CSS 引用、Layout 与 Event Handler 代码、Page Attribute 变量定义、Type Definition 类型定义、测试效果和注意事项全写进去了。这份东西放到今天看,技术栈确实老,但它的价值不在“新”,而在“完整”——它是一条从零到可运行 BSP 报表的最短路径,没有省略任何一步。如果你正在维护一套跑在 SAP 里的 BSP 应用,或者需要快速给业务部门出一个能输入客户编号和订单日期、点提交就出表格的查询页面,这份实例能直接抄作业。它适合两类人:一是刚接触 ABAP Web 开发、需要理解 BSP 页面生命周期和事件处理机制的新手;二是手头有老 BSP 系统、需要快速改一个报表或排查问题的熟手。下面我按“资源是什么、怎么用、坑在哪”的顺序,把这份实例拆开讲透。

2. BSP 应用创建与页面结构:SE80 里的五个关键动作

2.1 为什么选 BSP 而不是 Web Dynpro

这份实例用的是 BSP,不是 Web Dynpro。BSP 的核心思路是“HTML 里嵌 ABAP”,页面结构接近传统 JSP,Layout 负责展示、Event Handler 负责逻辑、Page Attribute 负责变量、Type Definition 负责类型。对于报表类需求——输入条件、查数据库、循环输出表格——BSP 的代码量比 Web Dynpro 少得多,页面结构也更直观。Web Dynpro 适合复杂交互和组件复用,但如果你只是要一个“输入框加提交按钮加结果表格”的查询页,BSP 的 Layout 里直接写 HTML 表格、用<%= %>绑定变量,开发速度更快。这份实例选 BSP 是合理的,因为它的目标就是一个简单报表,不需要复杂的页面导航和组件模型。

2.2 SE80 创建 BSP 应用的完整步骤

打开事务代码 SE80,在左侧对象导航里选择 BSP Application,输入你规划好的 BSP 程序名,回车后系统会提示是否创建,确认即可。创建完成后,在 BSP 应用节点下右键新建 Page,输入网页名称和描述,页面类型选 HTML,确认保存。这一步的关键是页面名称要和后续 Event Handler 里goto_page或next_page引用的名称一致,否则运行时会报页面找不到。常见做法是页面名用xxx.htm格式,比如custord_list.htm,这样在代码里引用时不容易写错。

创建页面后,你会看到 Layout、Event Handler、Page Attribute、Type Definition 等页签。Layout 是 HTML 和 ABAP 脚本混合的地方,Event Handler 是纯 ABAP 逻辑,Page Attribute 定义页面级变量,Type Definition 定义类型。这四个页签的代码是相互配合的,缺一个页面就跑不起来。

2.3 Layout 页签的代码结构与参数说明

Layout 页签的代码是这份实例的核心。开头<%@page language="abap" %>声明页面使用 ABAP 脚本语言,这是 BSP 的固定写法。接着<h3> <%= errormsg %> </h3>把 Page Attribute 里定义的errormsg变量输出到页面,用于显示错误提示。<link rel=stylesheet href="./style.css" type="text/css">引用 CSS 文件,路径是相对路径,CSS 文件需要放在 BSP 应用的 MIME 资源里或者通过其他方式关联。

表单部分用<form name="PageForm" id="PageForm" method="post">定义,里面有两个输入框:st_custno绑定gt_custno,st_ord绑定gt_dat。注意value="<%=gt_custno%>"这种写法,它把 ABAP 变量的值输出到 HTML 的 value 属性里,页面刷新后输入框会保留上次输入的值。提交按钮的name属性是onInputProcessing(sub),这个写法是 BSP 的事件绑定语法,意思是点击按钮时触发 Event Handler 里case event_id. when 'sub'.分支。

表格部分用<%DATA: wa_YSHDDCX LIKE LINE OF ZSDFB_YSHDDCX.loop at ZSDFB_YSHDDCX into wa_YSHDDCX.%>开始循环,然后在<tr>里用<%=wa_YSHDDCX-AUDAT%>这样的表达式逐列输出数据。循环结束用<%endloop.%>。这里要注意,ZSDFB_YSHDDCX是一个内表,在 Page Attribute 里定义为TYPE TT_YSHDDCX,而TT_YSHDDCX在 Type Definition 里定义为STANDARD TABLE OF ZSDFB_YSHDDCX。这个链条不能断:Type Definition 定义类型,Page Attribute 定义变量,Layout 里循环输出。

2.4 Event Handler 与 Page Attribute 的配合逻辑

Event Handler 里的代码负责处理用户输入和查询数据。case event_id. when 'sub'.匹配提交按钮的事件。首先用navigation->set_parameter('st_custno').和navigation->get_parameter( name = 'st_custno' ).取得输入值,存到gt_custno。然后做输入校验:如果gt_custno为空,设置errormsg = '请输入客户编号'并exit,页面会重新显示并带上错误信息。

校验通过后,用select ... into corresponding fields of table ZSDFB_YSHDDCX from VBAK where KUNNR eq gt_custno and AUDAT eq gt_dat.查询销售订单数据。接着loop at ZSDFB_YSHDDCX into wa_YSHDDCX.循环内表,在循环里用select single逐条取客户名称、销售组描述、销售雇员名称、订单金额、产品组描述、分销渠道描述,然后modify ZSDFB_YSHDDCX from wa_YSHDDCX.更新内表。这个“循环内 select single 再 modify”的模式是 BSP 报表里常见的写法,但也是性能隐患,后面避坑章节会细说。

Page Attribute 里定义的变量:errormsg TYPE STRING、gt_custno TYPE VBAK-KUNNR、gt_dat TYPE DZSDFB_YSHDDCX、ZSDFB_YSHDDCX TYPE TT_YSHDDCX。注意gt_dat的类型写的是DZSDFB_YSHDDCX,这可能是笔误或者自定义类型,实际应该是日期类型。ZSDFB_YSHDDCX是内表变量,在 Layout 里直接循环输出。

Type Definition 里定义TYPES: tt_YSHDDCX TYPE STANDARD TABLE OF ZSDFB_YSHDDCX INITIAL SIZE 0.,ZSDFB_YSHDDCX是自定义结构,字段包括 KUNNR、NAME1、VBELN、BSTNK、AUDAT、KZWI2、VDATU、SPART、ZJSL、YCHJE、VTEXT、VKGRP、BEZEI20、BUPARTNR、SMNAM、VTWEG、VTXTK 等。这些字段覆盖了报表需要展示的所有列。

3. 从输入到输出:BSP 报表的完整数据流与代码实现

3.1 输入校验与参数传递的代码细节

Event Handler 里的输入校验逻辑是报表的第一道关卡。代码先clear errormsg.清空错误信息,然后判断if gt_custno is not initial.,如果不为空,执行unpack gt_custno to gt_custno.。unpack的作用是去掉输入值的前导空格,因为 HTML 输入框提交的值可能带空格。如果为空,设置errormsg = '请输入客户编号'并exit,页面重新渲染时<h3> <%= errormsg %> </h3>就会显示错误提示。

订单日期的校验在实例里被注释掉了,但逻辑是一样的:判断gt_dat是否为空,为空则设置错误信息并退出。实际开发中建议把日期校验打开,因为select语句里AUDAT eq gt_dat如果gt_dat为空,查询条件会变成AUDAT eq '00000000',查不到任何数据,用户会以为系统坏了。

参数传递用navigation->set_parameter和navigation->get_parameter。set_parameter把输入框的值绑定到 navigation 对象,get_parameter从 navigation 对象取值赋给 ABAP 变量。这个机制是 BSP 页面间传参的标准做法,next_page和goto_page也依赖 navigation 对象传递参数。

3.2 数据库查询与内表循环的写法

查询主数据用select KUNNR VBELN BSTNK AUDAT VDATU SPART VKGRP VTWEG into corresponding fields of table ZSDFB_YSHDDCX from VBAK where KUNNR eq gt_custno and AUDAT eq gt_dat.。into corresponding fields of table表示按字段名匹配,把 VBAK 的字段值填入内表ZSDFB_YSHDDCX的同名字段。这种写法要求内表结构里的字段名和数据库表字段名一致,否则对应不上。

循环内表时,每一条订单记录都要补充客户名称、销售组描述、销售雇员、订单金额、产品组描述、分销渠道描述。代码用select single NAME1 from KNA1 into wa_YSHDDCX-NAME1 where KUNNR = wa_YSHDDCX-KUNNR.取客户名称,用select single BEZEI from TVGRT into wa_YSHDDCX-BEZEI20 where VKGRP = wa_YSHDDCX-VKGRP.取销售组描述,用select single T~PERNR from VBPA as T into wa_YSHDDCX-BUPARTNR where T~PARVW = 'VE' and T~VBELN = wa_YSHDDCX-VBELN.取销售雇员编号,再用select single PA0001~SNAME from PA0001 into wa_YSHDDCX-SMNAM where PA0001~PERNR = wa_YSHDDCX-BUPARTNR.取雇员姓名。订单金额用select sum( KZWI2 ) from VBAP into wa_YSHDDCX-KZWI2 where VBELN = wa_YSHDDCX-VBELN.汇总。产品组描述和分销渠道描述分别从 TSPAT 和 TVTWT 取。

每取完一条记录的所有补充字段,执行modify ZSDFB_YSHDDCX from wa_YSHDDCX.把更新后的行写回内表。循环结束后,内表ZSDFB_YSHDDCX就包含了完整的报表数据,Layout 里的loop at ZSDFB_YSHDDCX into wa_YSHDDCX.会逐行输出。

3.3 CSS 引用与页面样式控制

CSS 引用在 Layout 的<link rel=stylesheet href="./style.css" type="text/css">这一行。./style.css表示当前目录下的 style.css 文件。在 BSP 应用里,CSS 文件通常放在 MIME 资源节点下,或者通过 SE80 的 BSP 应用资源管理上传。引用后,HTML 里的class="InputBox"、class="form1"、class="TableDiv"、class="table1"、class="th"等样式类就会生效。

常见做法是把表格样式、输入框样式、按钮样式都写在 CSS 里,Layout 里只写结构。这样改样式不用动 ABAP 代码,维护更方便。注意 CSS 文件路径要写对,如果 BSP 应用部署在子路径下,相对路径可能失效,可以用绝对路径或者 BSP 提供的资源 URL 生成方式。

3.4 测试与激活的检查清单

保存所有页签的代码后,点击激活按钮。激活时系统会检查语法错误,如果有未定义的变量或类型,会报错并阻止激活。激活成功后,右键 BSP 应用或页面,选择测试,系统会在浏览器里打开页面。测试时重点检查:输入框是否能输入、提交按钮是否触发 Event Handler、错误信息是否显示、表格是否有数据、CSS 样式是否生效。

如果页面打开后空白,先检查 Layout 里是否有语法错误导致页面渲染中断。如果提交后没有反应,检查 Event Handler 里case event_id. when 'sub'.的sub是否和提交按钮的name属性一致。如果表格没有数据,检查select语句的查询条件是否匹配到了数据,可以在 Event Handler 里加break-point调试。

4. 避坑与排查:BSP 报表开发中容易翻车的五个点

4.1 内表定义与循环处理的类型不匹配

现象:激活时报错“类型 TT_YSHDDCX 未定义”或“ZSDFB_YSHDDCX 不是内表”。原因:Type Definition 里定义的tt_YSHDDCX和 Page Attribute 里定义的ZSDFB_YSHDDCX类型不一致,或者 Type Definition 没有激活。解决:确认 Type Definition 里TYPES: tt_YSHDDCX TYPE STANDARD TABLE OF ZSDFB_YSHDDCX INITIAL SIZE 0.已激活,Page Attribute 里ZSDFB_YSHDDCX TYPE TT_YSHDDCX引用正确。循环时用DATA: wa_YSHDDCX LIKE LINE OF ZSDFB_YSHDDCX.定义工作区,不要直接用内表名当工作区。

4.2 循环内 SELECT SINGLE 导致的性能问题

现象:报表数据量稍大时页面加载极慢,甚至超时。原因:loop at ZSDFB_YSHDDCX into wa_YSHDDCX.里每行都执行多个select single,如果内表有几百行,数据库查询次数就是几百乘以字段数。解决:把能合并的查询合并,比如用select ... into table ... for all entries in ZSDFB_YSHDDCX一次性取客户名称、销售组描述等,然后在内存里用read table匹配。如果数据量确实大,考虑分页或者限制查询条件。

4.3 输入框值绑定与 UNPACK 的遗漏

现象:用户输入客户编号后提交,查询不到数据,但数据库里明明有。原因:HTML 输入框提交的值可能带前导空格,gt_custno直接用于select的where条件时匹配不上。解决:在 Event Handler 里对输入值执行unpack gt_custno to gt_custno.去掉前导空格。日期字段也要做类似处理,或者用condense去掉多余空格。

4.4 CSS 文件路径错误导致样式失效

现象:页面能打开,数据也能显示,但表格没有边框、输入框没有样式。原因:<link rel=stylesheet href="./style.css" type="text/css">的路径不对,或者 CSS 文件没有上传到 BSP 应用的 MIME 资源里。解决:在 SE80 里检查 BSP 应用的 MIME 资源节点,确认 style.css 存在。如果路径不对,改成正确的相对路径或绝对路径。测试时可以在浏览器里按 F12 看 Network 面板,确认 style.css 是否 404。

4.5 事件绑定名称不一致导致提交无反应

现象:点击提交按钮后页面刷新,但没有任何查询结果,错误信息也不显示。原因:提交按钮的name属性是onInputProcessing(sub),Event Handler 里case event_id. when 'sub'.的sub必须和括号里的参数一致。如果写成onInputProcessing(Submit)而 Event Handler 里是when 'sub'.,事件匹配不上。解决:统一命名,提交按钮用onInputProcessing(sub),Event Handler 里用when 'sub'.。改完后重新激活页面再测试。

5. 进阶技巧:用 FOR ALL ENTRIES 优化 BSP 报表查询

5.1 从逐行 SELECT 到批量查询的改造思路

原始实例的查询逻辑是“先查主表到内表,再循环内表逐行补充字段”。这种写法在数据量小的时候没问题,但订单行数一多,数据库交互次数线性增长。优化的核心思路是把“循环内 select single”改成“批量 select into table for all entries”。具体做法是:主查询不变,把内表ZSDFB_YSHDDCX作为驱动表,用for all entries in ZSDFB_YSHDDCX一次性取客户名称、销售组描述、雇员信息等,存到对应的内表里,然后在循环里用read table匹配。

5.2 FOR ALL ENTRIES 的代码实现与参数说明

" 批量取客户名称 DATA: lt_kna1 TYPE STANDARD TABLE OF kna1. SELECT kunnr name1 INTO TABLE lt_kna1 FROM kna1 FOR ALL ENTRIES IN ZSDFB_YSHDDCX WHERE kunnr = ZSDFB_YSHDDCX-kunnr. " 批量取销售组描述 DATA: lt_tvgrt TYPE STANDARD TABLE OF tvgrt. SELECT vkgrp bezei INTO TABLE lt_tvgrt FROM tvgrt FOR ALL ENTRIES IN ZSDFB_YSHDDCX WHERE vkgrp = ZSDFB_YSHDDCX-vkgrp. " 循环内用 READ TABLE 匹配 LOOP AT ZSDFB_YSHDDCX INTO wa_YSHDDCX. READ TABLE lt_kna1 INTO DATA(ls_kna1) WITH KEY kunnr = wa_YSHDDCX-kunnr. IF sy-subrc = 0. wa_YSHDDCX-name1 = ls_kna1-name1. ENDIF. READ TABLE lt_tvgrt INTO DATA(ls_tvgrt) WITH KEY vkgrp = wa_YSHDDCX-vkgrp. IF sy-subrc = 0. wa_YSHDDCX-bezei20 = ls_tvgrt-bezei. ENDIF. MODIFY ZSDFB_YSHDDCX FROM wa_YSHDDCX. ENDLOOP.

这段代码的关键是FOR ALL ENTRIES IN ZSDFB_YSHDDCX,它把内表里的 KUNNR 和 VKGRP 作为查询条件,一次性从 KNA1 和 TVGRT 取回所有匹配记录。注意FOR ALL ENTRIES要求驱动内表不能为空,否则会全表扫描。所以在执行前要检查IF ZSDFB_YSHDDCX IS NOT INITIAL.。另外,READ TABLE的WITH KEY要用内表的主键或排序字段,如果内表没排序,可以先SORT ZSDFB_YSHDDCX BY kunnr vkgrp.再循环。

5.3 优化前后的性能对比与验证方法

优化前,假设内表有 100 行,每行执行 6 个select single,数据库交互 600 次。优化后,6 个批量查询各执行一次,数据库交互 6 次,加上循环内的read table内存操作,整体响应时间从秒级降到毫秒级。验证方法:在 Event Handler 里加GET RUN TIME FIELD记录开始和结束时间,输出到errormsg或日志里,对比优化前后的耗时。也可以在 ST05 里开 SQL Trace,看数据库交互次数是否明显减少。

我一般会在开发阶段就用FOR ALL ENTRIES替代循环内select single,因为后期数据量增长是不可控的。从那以后我每次写 BSP 报表,只要涉及循环内取数,都强制走一遍“能不能批量查”的检查,避免上线后被业务追着优化。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进