在 SAP BTP 平台上搭建基于前后端分离的 CAP 应用(第二部分)

目标

在 SAP BTP 平台上搭建基于前后端分离的 CAP 应用

本文旨在指导开发者在 SAP Business Technology Platform (BTP) 上构建一个现代化的企业级应用,采用 前后端分离架构,以前端展示与后端服务解耦的方式提升系统可维护性与扩展性。整体技术方案基于 CAP(Cloud Application Programming)模型理念,结合成熟的前端框架与后端微服务技术栈,实现高效、灵活且易于集成的应用系统。

具体而言:

通过整合这两部分内容,本文将为你呈现一个完整的、可落地的 SAP BTP 应用开发实践路径。

架构概览

前端 Vue 3 + Vite + SAP UI5 Web Components 轻量级、响应式前端界面,支持 Fiori 风格组件,适用于企业管理系统
后端 Spring Boot 3 + MyBatis-Plus + SAP HANA Cloud JDBC 高性能微服务后端,对接 HANA Cloud 实现实时数据处理
数据库 SAP HANA Cloud 基于内存计算的云原生数据库,支持 OLTP 与 OLAP 混合负载
部署平台 SAP BTP(Cloud Foundry环境) 提供统一的身份认证、安全管控与服务集成能力

接上一部分:在 SAP BTP 平台上搭建基于前后端分离的 CAP 应用(第一部分)

调整前端项目

调整前端项目es-ui5-app,将数据读取方式替换为接口调用。

安装axios

npm i axios

创建axios.js
在这里插入图片描述

将 EmployeeSearch.vue中的模拟员工数据替换使用/api/employee/list来读取
在这里插入图片描述

<template>
  <div class="page-container">
    <!-- 顶部导航 -->
    <ui5-shellbar>
      <ui5-shellbar-branding slot="branding">
        Human Resource Information System
        <img
          slot="logo"
          src="https://sap.github.io/ui5-webcomponents/images/sap-logo-svg.svg"
        />
      </ui5-shellbar-branding>

      <ui5-button slot="startButton" icon="menu"></ui5-button>
    </ui5-shellbar>

    <!-- 查询表单 -->
    <div class="search-form">
      <ui5-label for="nameInput">First Name</ui5-label>
      <ui5-input
        id="nameInput"
        placeholder="enter "
        v-model="filters.name"
      ></ui5-input>

      <ui5-label for="deptSelect">Department</ui5-label>
      <ui5-select id="deptSelect" v-model="filters.department">
        <ui5-option value="">All</ui5-option>
        <ui5-option value="HR">HR</ui5-option>
        <ui5-option value="IT">IT</ui5-option>
        <ui5-option value="Finance">Finance</ui5-option>
        <ui5-option value="Marketing">Marketing</ui5-option>
      </ui5-select>

      <ui5-label for="datePicker">Hire Date</ui5-label>
      <ui5-date-picker
        id="datePicker"
        v-model="filters.joinDate"
        formatPattern="YYYY-MM-dd"
        @change="onChangeDate"
      ></ui5-date-picker>

      <ui5-button icon="search" @click="search" design="Emphasized"
        >Search</ui5-button
      >
      <ui5-button icon="reset" @click="reset">Reset</ui5-button>
    </div>

    <!-- 数据表格 -->
    <div
      class="table-container"
      style="height: calc(100vh - 200px); overflow: auto"
    >
      <ui5-table aria-label="员工列表" no-data-text="暂无数据" mode="None">
        <ui5-table-growing
          mode="Button"
          slot="features"
          @load-more="onLoadMore"
          v-if="hasMore"
        ></ui5-table-growing>
        <!-- 表头 -->
        <ui5-table-header-row slot="headerRow">
          <ui5-table-header-cell
            ><span>Employee ID</span></ui5-table-header-cell
          >
          <ui5-table-header-cell><span>First Name</span></ui5-table-header-cell>
          <ui5-table-header-cell><span>Last Name</span></ui5-table-header-cell>
          <ui5-table-header-cell><span>Email</span></ui5-table-header-cell>
          <ui5-table-header-cell><span>Department</span></ui5-table-header-cell>
          <ui5-table-header-cell><span>Salary</span></ui5-table-header-cell>
          <ui5-table-header-cell><span>Hire Date</span></ui5-table-header-cell>
          <ui5-table-header-cell><span>City</span></ui5-table-header-cell>
          <ui5-table-header-cell><span>Action</span></ui5-table-header-cell>
        </ui5-table-header-row>

        <!-- 表体 -->
        <ui5-table-row v-for="emp in employees" :key="emp.empId">
          <ui5-table-cell
            ><span>{{ emp.empId }}</span></ui5-table-cell
          >
          <ui5-table-cell
            ><span>{{ emp.firstName }}</span></ui5-table-cell
          >
          <ui5-table-cell
            ><span>{{ emp.lastName }}</span></ui5-table-cell
          >
          <ui5-table-cell
            ><span>{{ emp.email }}</span></ui5-table-cell
          >
          <ui5-table-cell
            ><span>{{ emp.department }}</span></ui5-table-cell
          >
          <ui5-table-cell
            ><span>{{ emp.salary }}</span></ui5-table-cell
          >
          <ui5-table-cell
            ><span>{{ emp.hireDate }}</span></ui5-table-cell
          >
          <ui5-table-cell
            ><span>{{ emp.city }}</span></ui5-table-cell
          >
          <ui5-table-cell>
            <ui5-button
              icon="action"
              size="small"
              title="更多操作"
            ></ui5-button>
          </ui5-table-cell>
        </ui5-table-row>
      </ui5-table>
    </div>
  </div>
</template>

<script setup>
import "@ui5/webcomponents-icons/dist/AllIcons.js";

import "@ui5/webcomponents/dist/Button.js";
import "@ui5/webcomponents/dist/Input.js";
import "@ui5/webcomponents/dist/Label.js";
import "@ui5/webcomponents/dist/Select.js";
import "@ui5/webcomponents/dist/Option.js";

import "@ui5/webcomponents/dist/DatePicker.js";

import "@ui5/webcomponents/dist/Table.js";
import "@ui5/webcomponents/dist/TableRow.js";
import "@ui5/webcomponents/dist/TableCell.js";
import "@ui5/webcomponents/dist/TableHeaderRow.js";
import "@ui5/webcomponents/dist/TableHeaderCell.js";
import "@ui5/webcomponents/dist/TableGrowing.js";

import "@ui5/webcomponents-fiori/dist/ShellBar.js";
import "@ui5/webcomponents-fiori/dist/ShellBarBranding.js";

import request from "@/util/axios.js";
import { ref, computed, onMounted } from "vue";

const employees = ref([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(5);

const logo =
  "https://images.sj33.cn/uploads/allimg/201401/7-140131225442O6.png";

onMounted(() => {
  loadEmployees(true);
});

// 加载员工数据(服务端查询 + 分页增长)
const loadEmployees = (reset = false) => {
  if (reset) {
    page.value = 1;
    employees.value = [];
  }
  request({
    method: "get",
    url: "/s4hana/api/employee/list",
    params: {
      name: filters.value.name || undefined,
      department: filters.value.department || undefined,
      joinDate: filters.value.joinDate || undefined,
      page: page.value,
      size: pageSize.value,
    },
  }).then((res) => {
    const data = res.data || {};
    const records = data.records || [];
    total.value = data.total || 0;
    if (page.value === 1) {
      employees.value = records;
    } else {
      employees.value = employees.value.concat(records);
    }
  });
};

// 筛选条件
const filters = ref({
  name: "",
  department: "",
  joinDate: "",
});

// 表格增长(服务端分页)
const growingStep = pageSize.value;

const onChangeDate = (e) => {
  filters.value.joinDate = e.target.value;
};

// 是否还有更多数据
const hasMore = computed(() => employees.value.length < total.value);

// 处理加载更多(下一页)
const onLoadMore = () => {
  if (!hasMore.value) return;
  page.value = page.value + 1;
  loadEmployees(false);
};

// 查询(重置并拉取第一页)
const search = () => {
  loadEmployees(true);
};

// 重置
const reset = () => {
  filters.value = {
    name: "",
    department: "",
    joinDate: "",
  };
  loadEmployees(true);
};
</script>

<style scoped>
.page-container {
  font-family: "Segoe UI", system-ui, sans-serif;
  max-width: 1200px;
  margin: 0 auto;
  background: #f9f9f9;
}

/* 表单样式 */
.search-form {
  display: grid;
  grid-template-columns: auto 1fr auto 1fr auto 1fr auto auto;
  gap: 12px;
  padding: 16px;
  background: white;
  border-bottom: 1px solid #e5e5e5;
  align-items: center;
}

.search-form ui5-label {
  text-align: right;
  font-weight: 500;
}

/* 表格容器 */
.table-container {
  padding: 16px;
  background: white;
  margin: 0 16px;
  border-radius: 8px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}

/* 分页 */
.pagination {
  display: flex;
  justify-content: flex-end;
  padding: 16px;
}
</style>

2. 本地测试前后端联通

在这里插入图片描述

3. 新建一个nodejs server项目

4. 创建 destination-lite 服务实例

在 SAP Business Technology Platform (BTP) 上创建 destination-lite 服务实例,需要以下步骤。这个服务实例是基于 Destination 服务(Destination Service)lite 计划创建的,用于让你的应用(如 Node.js、UI5、CAP 应用)安全连接到外部系统(如 S/4HANA、SuccessFactors 等)。

cf create-service destination lite destination-lite

在这里插入图片描述

5. 在BTP新建一个Destination

在这里插入图片描述

5. 提交到Cloud Foundry

cf push

在这里插入图片描述

6. 获取前端访问地址

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
页面正常访问,并能获取到数据
在这里插入图片描述