从 CUBA 平台迁移
介绍
Jmix 框架直接继承了 CUBA 平台。提供了相同的快速应用程序开发方式,但采用了更现代化的基础架构。可以将 Jmix 视为 CUBA 的最新主版本,因此,如果你的 CUBA 项目正在开发中,可以考虑升级到 Jmix。为了使迁移过程更加方便,我们在 Jmix 中提供了一个 CUBA 兼容模块,其中包含一组 CUBA API 和在 Jmix 中已经更改或删除的功能。此外,Jmix Studio 提供从 CUBA 项目转换为 Jmix 项目的功能。
本章节中,我们介绍 Jmix 和 CUBA 的主要不同点,并详细说明如何从 CUBA 迁移至 Jmix。
与 CUBA 的不同点
项目结构
一个 CUBA 项目至少包含三个模块:global
、core
、web
。而在 Jmix 中,项目默认只有一个模块。
在 CUBA 项目中,所有类型的源文件(Java、Kotlin、XML、properties)都放置在每个模块的源码根目录下的同一目录下。而 Jmix 项目遵循标准的 Maven 目录结构,不同类型的文件有单独的根目录:
-
src/main
- 产品源码 -
src/test
- 测试源码 -
src/main/java
、src/test/java
- 放置 Java 类 -
src/main/kotlin
、src/test/kotlin
- 放置 Kotlin 文件 -
src/main/resources
、src/test/resources
- 放置 XML 和 properties 文件。
部署结构
CUBA 应用程序构建为两个独立的 Web 应用程序:core
和 web
。即便使用 “单一 WAR” 或 “uber JAR” 部署,这些制件实际上也包含 core
和 web
类,这些类分别使用不同的类加载器加载,并运行在各自独立的 Spring contexts 中。
Jmix 应用程序是单一 web 应用程序,使用标准的 Spring Boot Gradle 部署任务构建为可执行 JAR 或 WAR。
应用程序属性
由于 CUBA 的模块使用了分离部署,CUBA 项目的属性文件也分在了不同的文件中:core
模块的 app.properties
和 web
模块的 web-app.properties
文件。这些文件位于相应模块的根包目录。
在 Jmix 中,所有属性都在单一的 application.properties
文件定义,位于 src/main/resources
根目录中。
Spring 配置
在 CUBA 中,Spring 容器在 core
模块的 spring.xml
和 web
模块的 web-spring.xml
中定义。
Jmix 只使用基于 Java 的容器配置。Spring Boot 自动扫描应用程序包结构(从带 @SpringBootApplcation
注解的类所在包开始)中的所有带 Spring 注解的类。
数据模型
CUBA 数据模型的实体必须继承自某一个基类、BaseUuidEntity
、BaseGenericIdEntity
等)并实现某些可选的接口(Versioned
、Creatable
、Updatable
、SoftDelete
)。
Jmix 并不强制要求实体继承框架的某些基类。而只需将实体类标注 @JmixEntity
注解即可,框架会对类的字节码做增强,并实现之前 CUBA 中实体基类的功能。一些可选的实体行为(参阅 特性)也使用注解进行声明,例如,@CreatedBy
、@CreatedDate
等。当然,你也可以在项目级别根据需求创建自己的实体基类。jmix-cuba
兼容性模块提供了一组与 CUBA 一样的的基类,因此在迁移至 Jmix 时,无需编写自己的基类。
CUBA JPA 实体必须在 global
模块根包的 persistence.xml
文件中注册。而在 Jmix 中,persistence.xml
文件在构建时由 Jmix Gradle 插件自动创建。插件扫描 classpath,将所有带 @JmixEntity
注解的类写入应用程序 JAR/WAR 文件中的 <base-package>/persistence.xml
内。
CUBA 的非持久化实体和自定义数据类型在 metadata.xml
文件注册。Jmix 中没有该文件:
DataManager
Jmix 中的 io.jmix.core.DataManager
bean 与 CUBA 中的 TransactionalDataManager
类似,也是将带提交的数据合并至一个会话中,用 save()
方法替代了 commit()
。但是,在处理安全许可方面,这两者有关键的一点不同:Jmix 的 io.jmix.core.DataManager
默认会同时检查资源和行级策略,由单独的 io.jmix.core.UnconstrainedDataManager
提供无安全限制的访问。参阅 认证 章节。
jmix-cuba
兼容性模块提供 com.haulmont.cuba.core.global.DataManager
bean,提供与 CUBA 中同样的接口和行为,在安全性方面,只检查行级策略,并提供 secure()
方法返回一个 DataManager
的实现,该实现会检查资源策略。
数据库会话
与 CUBA 不同,Jmix 不提供任何管理数据库会话的特定接口。你需要在 bean 方法上使用 @Transactional
注解做区分,或者编程式通过 Spring 的 TransactionTemplate
类管理。
jmix-cuba
兼容性模块提供 com.haulmont.cuba.core.Persistence
和 com.haulmont.cuba.core.Transaction
接口,使用这两个接口无需重写 CUBA 业务逻辑。
EntityManager
Jmix 中,代码中处理 JPA 需要使用标准的 JPA EntityManager
、Query
和 TypedQuery
接口。EntityManager
通过 @PersistenceContext
注解注入获取。示例:
@PersistenceContext
private EntityManager entityManager;
jmix-cuba
兼容性模块中仍然提供 com.haulmont.cuba.core.EntityManager
,通过 com.haulmont.cuba.core.Persistence
获取。
EntityPersistingEvent
CUBA 中的 EntityPersistingEvent
由 EntitySavingEvent 替换,该事件会在对新实体持久化和更新已有实体时发送。jmix-cuba
兼容性模块中仍然提供 EntityPersistingEvent
,仅当保存新实体时发送。
EntitySavingEvent 和 EntityPersistingEvent 只有当使用 DataManager 保存实体时才会发送。如果用 EntityManager 、EntityPersistingEvent 持久化实体,则不论是否使用了 jmix-cuba 兼容性模块都不会发送。
|
Fetch Plans 和延迟加载
Jmix 通过使用 fetch plans 支持实体的部分加载,与 CUBA 中的 view 一样。此外,Jmix 还支持对使用 DataManager
加载的 JPA 实体的引用属性进行延迟加载,因此,可以按需使用 fetch plan,特别是有性能方面考虑时。参阅 加载数据。
jmix-cuba
兼容性模块提供向后兼容的 com.haulmont.cuba.core.global.View
、ViewBuilder
和 ViewRepository
类。
安全
Jmix 的 资源角色 和资源策略与 CUBA 中的角色和许可非常类似。主要区别是在设计时定义:CUBA 使用类,而 Jmix 使用接口。
Jmix 的 行级角色 与 CUBA 的访问组约束有相同的作用,但是有一些显著的不同:
-
Jmix 中,行级角色保存在扁平数组中,而非树形结构;
-
Jmix 中,用户可以有多个行级角色;
-
Jmix 中,没有访问组预定义会话属性的对等概念。
Studio 迁移程序会将 CUBA 设计时角色自动转换为 Jmix 资源角色。访问组和约束需要手动转换,参阅 API 变更。
迁移程序会保留数据库中的用户列表,但是所有运行时的安全配置(角色、策略、角色的分配)需要重新做。 |
会话属性
Jmix 提供 SessionData bean 用于在同一用户的不同请求中共享值。
jmix-cuba
兼容性模块提供向后兼容的 com.haulmont.cuba.security.global.UserSession
类,将 getAttribute()
/ setAttribute()
方法代理给 SessionData
。
Jmix 中删除的功能
下列是在 Jmix 中删除且不提供替代方案的 CUBA 功能:
-
DataManager
级别的属性访问控制。现在仅在 UI 组件中显示数据以及通过 REST API 返回实体时才考虑实体属性权限。参阅 数据访问检查。 -
使用
SetupAttributeAccessHandler
和SetupAttributeAccessHandler
的基于状态的实体属性访问控制。 -
界面组件权限。
-
访问组中定义的会话属性。
-
ClusterManagerAPI
接口及其实现。 -
编辑界面打开历史和
@TrackEditScreenHistory
注解。 -
使用
net.sourceforge.jtds.jdbc.Driver
支持 Microsoft SQL Server 2005。
API 更改
下面是 Studio 自动迁移程序不会转换且 jmix-cuba
模块中没有兼容性包装器的 API 列表。在遇到代码编译问题需要修复时,请参考。
访问组和约束
将注解类转换为接口。接口中的方法应该返回 void
,这些方法主要是用来对注解进行分组。参阅 行级角色。
-
com.haulmont.cuba.security.app.group.annotation.AccessGroup
→io.jmix.security.role.annotation.RowLevelRole
-
com.haulmont.cuba.security.app.group.annotation.JpqlConstraint
→io.jmix.security.role.annotation.JpqlRowLevelPolicy
-
com.haulmont.cuba.security.app.group.annotation.Constraint
→io.jmix.security.role.annotation.PredicateRowLevelPolicy
.
安全配置实体
以下是在运行时配置安全功能实体的大致对等实体:
-
com.haulmont.cuba.security.entity.Role
→io.jmix.securitydata.entity.ResourceRoleEntity
-
com.haulmont.cuba.security.entity.Group
→io.jmix.securitydata.entity.RowLevelRoleEntity
-
com.haulmont.cuba.security.entity.UserRole
→io.jmix.securitydata.entity.RoleAssignmentEntity
-
com.haulmont.cuba.security.entity.Permission
→io.jmix.securitydata.entity.ResourcePolicyEntity
-
com.haulmont.cuba.security.entity.Constraint
→io.jmix.securitydata.entity.RowLevelPolicyEntity
多租户
运行完自动迁移程序后,请按照下列步骤修复。
-
在项目中添加
StandardTenantEntity
:package com.company.app.entity; // replace with your base package import com.haulmont.cuba.core.entity.StandardEntity; import io.jmix.core.annotation.TenantId; import io.jmix.core.metamodel.annotation.JmixEntity; import javax.persistence.Column; import javax.persistence.MappedSuperclass; @MappedSuperclass @JmixEntity public abstract class StandardTenantEntity extends StandardEntity { private static final long serialVersionUID = -1215037188627831268L; @TenantId @Column(name = "TENANT_ID") protected String tenantId; public void setTenantId(String tenantId) { this.tenantId = tenantId; } public String getTenantId() { return tenantId; } }
将所有继承自 CUBA
com.haulmont.addon.sdbmt.entity.StandardTenantEntity
的实体都替换成继承上面的StandardTenantEntity
。 -
User
实体中,实现AcceptsTenant
接口,并添加带@TenantId
注解的tenant
属性,映射至数据库的SYS_TENANT_ID
列:public class User implements JmixUserDetails, HasTimeZone, AcceptsTenant { // ... @TenantId @Column(name = "SYS_TENANT_ID") private String tenant; public String getTenant() { return tenant; } public void setTenant(String tenant) { this.tenant = tenant; } @Override public String getTenantId() { return tenant; } }
-
按照 多租户/配置用户 部分的第 3、4、5 项中所述,将
tenant
属性添加到用户浏览和编辑界面。 -
使用以下 Liquibase 变更集将
CUBASDBMT_TENANT
表重命名为MTEN_TENANT
(仅在 Jmix 1.1.0 中需要,Jmix 1.1.1+ 中的jmix-cuba
模块已经包含此变更集):<changeSet id="10" author="me"> <preConditions onFail="MARK_RAN"> <tableExists tableName="CUBASDBMT_TENANT"/> </preConditions> <renameTable oldTableName="CUBASDBMT_TENANT" newTableName="MTEN_TENANT"/> </changeSet>
报表
-
com.haulmont.reports.app.service.ReportService
、com.haulmont.reports.gui.ReportGuiManager
→io.jmix.reports.runner.ReportRunner
实体快照
-
com.haulmont.cuba.core.app.EntitySnapshotService
→io.jmix.audit.snapshot.EntitySnapshotManager
-
com.haulmont.cuba.gui.app.core.entitydiff.EntityDiffViewer
→io.jmix.auditui.screen.snapshot.SnapshotDiffViewer
-
<frame id="diffFrame" src="/com/haulmont/cuba/gui/app/core/entitydiff/diff-view.xml"/>
→<fragment id="diffFrame" screen="snapshotDiff"/>
如何迁移
Jmix Studio 提供了一个将 CUBA 项目转换为 Jmix 项目的自动程序。该程序使用标准的 Jmix 模板创建一个新项目,然后将 CUBA 项目中的源代码复制到 Jmix 项目的新结构中。在复制时,Studio 对源文件进行了大量改动:替换包和已知框架类,将界面 XML 描述符转换为新架构,配置数据库连接,新扩展组件的依赖。迁移过程完成后,你需要手动修复剩余的问题。
迁移过程不会修改当前的 CUBA 项目,因此在项目的任何拷贝上运行该程序都是安全的。 |
自动迁移有如下局限性:
|
在 Jmix Studio v.1.1.4 及更低版本中,如果你的 IntelliJ IDEA 包含版本高于 1.5.10 的 Kotlin 插件,迁移过程可能会失败。此时,请将 Kotlin 插件降级到 1.5.10 或更低版本。 在 Jmix Studio v.1.1.5 及更高版本中,迁移不再依赖 Kotlin 插件。 |
主迁移过程
按照下列步骤运行自动迁移程序。
-
在 Jmix Studio 中打开 CUBA 项目。
-
等待项目导入且索引构建完成。观察 IDE 的进度条,直至无新消息展示为止。
-
然后,会在右下角弹出关于迁移至 Jmix 的通知。点击消息中的 Migrate 或在 IDE 主菜单选择 File → New → Jmix project from this CUBA project。
如果项目已经在 IDE 导入并打开过,则不会再次弹出提示。此时需要点击 Gradle 工具窗口的 Reload All Gradle Projects 按钮重新加载一次项目。
-
Studio 启动 New Jmix project 向导。
-
选择 Jmix 的最新版本(1.1.0 以上),JDK 用 CUBA 项目中使用的版本。点击 Next。
-
在向导的下一步,输入新 Jmix 项目的名称和存储位置。点击 Finish。
-
Studio 使用指定的 Jmix 模板创建新的项目,并启动迁移程序。IDE 的右下角会显示相关信息。
当迁移完成时,Studio 创建
MigrationResult.md
文件,并在编辑器窗口打开。该文件描述了自动迁移过程中完成的内容以及推荐需要手动修改的内容。 -
手动在
build.gradle
文件中添加项目需要的其他依赖。迁移程序只添加与 CUBA 扩展组件对等的 Jmix 扩展组件。 -
配置 User 的 特性。CUBA user 包含创建/更新审查特性和软删除特性。但是 Jmix 的 User 中,这些特性都是可选的,且默认不具备。
-
如果需要为 Jmix User 添加审查和软删除特性,按照下列步骤:
-
在 Studio 的 实体设计器 中启用实体特性。
-
修改对应属性的列名:
-
createdDate
:CREATED_DATE
→CREATE_TS
-
lastModifiedBy
:LAST_MODIFIED_BY
→UPDATED_BY
-
lastModifiedDate
:LAST_MODIFIED_DATE
→UPDATE_TS
-
deletedDate
:DELETED_DATE
→DELETE_TS
-
-
-
任何时候,如果不再需要审查特性,只需要在实体设计器中禁用特性,相关列会在自动生成的更改日志中删除。
-
如果不再需要软删除特性,则会将以前软删除的用户自动恢复。此时需要手动删除
SEC_USER
表中DELETE_TS
字段非空的所有记录。然后才能禁用软删除特性。相关列会在自动生成的更改日志中删除。
-
-
下一个目标是编译项目。点击 IDE 主菜单的 Build → Build Project。
分析构建输出中的编译错误并按照新的 API 修复代码。使用上面 API 更改 提供的信息。
-
成功编译后,在 Jmix 工具窗口的 Data Stores 检查主数据库连接。
Jmix Studio 会自动修改数据库结构并运行一些更新。因此,开发阶段千万不要使用生产环境的数据库! -
如需更新已有的 CUBA 数据库以兼容新的 Jmix 应用程序,按照下列步骤:
-
确保
application.properties
文件包含:jmix.liquibase.contexts = cuba
-
右键点击 Main Data Store,选择 Update。Studio 会运行
jmix-cuba
模块带的 Liquibase 更改集。如果成功运行,则数据库与项目中的 Jmix 模块兼容。
-
-
然后可以用 Jmix Application 运行/调试配置启动应用程序。
默认情况下,启动时首先检查数据库结构,如果数据库结构与应用程序数据模型不同时,生成 Liquibase 变更日志。需要仔细检查生成的变更日志,并从中删除有潜在危险的命令,例如,
drop
和alter
。-
针对
SEC_REMEMBER_ME
、SEC_SCREEN_HISTORY
和SEC_SEARCH_FOLDER
表的改动可以直接应用(也可以忽略)。 -
不到迁移过程的最后一刻,我们推荐不要 drop
SEC_USER
表的任何列。
可以在 Changelog Preview 窗口中使用 Remove and Ignore 命令来删除选定的命令。你的选择会记录在项目的
jmix-studio.xml
文件中,下次运行应用程序时,将不会再次生成忽略的命令。 -
-
如需为应用程序创建一个新的空数据库,按照下列步骤:
-
在
application.properties
中修改 Liquibase 上下文:jmix.liquibase.contexts = migrated
-
将
resources/<base-package>/liquibase/changelog/010-init-user.xml
文件内的所有 用户 表名改为SEC_USER
。例如,<createTable tableName="APP_USER">
→<createTable tableName="SEC_USER">
。 -
点击 Main Data Store 右键菜单中的 Recreate。Studio 会先 drop 然后再 create 数据库,并运行所有 Jmix 模块的 Liquibase 更改日志。
-
用 Jmix Application 运行/调试配置启动应用程序。Studio 会为项目中的数据模型生成 Liquibase 更改日志。或者,你可以手动创建一个更改日志文件,然后将 CUBA 项目中所有
create-db.sql
文件的 SQL 语句通过sql
指令添加进来。
-
文件存储
Jmix 中 本地文件存储 的结构与 CUBA 相同。你只需将所有文件从 CUBA 应用程序的 work/filestorage
文件夹移至 Jmix 文件存储的文件夹,默认为 {user.dir}/.jmix/work/filestorage
,可以用 jmix.localfs.storageDir
属性修改。
确保在界面描述中,与 FileDescriptor
属性关联的上传组件是使用 cuba:cubaUpload
定义。
WebDAV
本节介绍如何迁移与 WebDAV 扩展组件相关的代码和数据。
-
在
build.gradle
中添加 premium 仓库和扩展组件依赖:repositories { // ... maven { url = 'https://global.repo.jmix.io/repository/premium' credentials { username = rootProject['premiumRepoUser'] password = rootProject['premiumRepoPass'] } } } dependencies { implementation 'io.jmix.webdav:jmix-webdav-starter' implementation 'io.jmix.webdav:jmix-webdav-ui-starter' implementation 'io.jmix.webdav:jmix-webdav-rest-starter' // ... }
使用编辑窗口右上角的 Load Gradle Changes 按钮或 Gradle 工具窗口的 Reload All Gradle Projects 操作刷新项目。
-
代码中用 Jmix 相关的类替换 CUBA WebDAV 包中的类:
-
com.haulmont.webdav.entity.
→io.jmix.webdav.entity.
-
com.haulmont.webdav.annotation.
→io.jmix.webdav.annotation.
-
com.haulmont.webdav.components.
→io.jmix.webdavui.component.
-
-
在界面 XML 描述中修复 WebDAV UI 组件的声明:
-
替换
webdav
schema URI:xmlns:webdav="http://schemas.haulmont.com/webdav/ui-component.xsd
→xmlns:webdav="http://jmix.io/schema/webdav/ui
-
替换组件的 XML 元素:
-
document-link
→documentLink
-
document-version-link
→documentVersionLink
-
webdav-document-upload
→webdavDocumentUpload
-
-
-
Jmix WebDAV 扩展组件只能处理
WebdavDocument
类型的属性,因此,如果你有带@WebdavSupport
注解的FileDescriptor
属性,需要修改属性类型并迁移保存在对应列的数据。我们用一个示例来看看这个过程。假设你有下面的实体,带有
FileDescriptor
属性支持 WebDAV:@JmixEntity @Table(name = "DEMO_DOC") @Entity(name = "demo_Doc") public class Doc extends StandardEntity { @WebdavSupport @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "FILE_ID") private FileDescriptor file; ... }
首先,用
WebdavDocument
替换FileDescriptor
类型:@JmixEntity @Table(name = "DEMO_DOC") @Entity(name = "demo_Doc") public class Doc extends StandardEntity { @WebdavSupport @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "FILE_ID") private WebdavDocument file; ... }
@WebdavSupport
注解此时已经不需要了,但是可以用来避免版本。如果为该属性创建了
WebdavDocumentLink
组件,则需要用withWebdavDocument()
方法替换withFileDescriptor()
。下一步,需要创建 Liquibase 更改日志,用于更新
FILE_ID
列的数据。在src/main/resources/<base-package>/liquibase/changelog
目录创建一个 XML 文件(选一个合适的名称,例如,020-migrate-webdav.xml
),使用下列内容:<?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd" context="cuba"> <changeSet id="1" author="demo"> <dropForeignKeyConstraint baseTableName="DEMO_DOC" constraintName="FK_DEMO_DOC_ON_FILE"/> <update tableName="DEMO_DOC"> <column name="FILE_ID" valueComputed="(select wd.id from webdav_webdav_document_version wdv, webdav_webdav_document wd where wdv.file_descriptor_id = FILE_ID and wdv.webdav_document_id = wd.id)"/> </update> <addForeignKeyConstraint baseColumnNames="FILE_ID" baseTableName="DEMO_DOC" constraintName="FK_DEMO_DOC_ON_FILE" referencedColumnNames="ID" referencedTableName="WEBDAV_WEBDAV_DOCUMENT"/> </changeSet> </databaseChangeLog>
一般来讲,需要为每个转换为
WebdavDocument
的FileDescriptor
属性创建这样一个文件。更改日志要符合下面这种模式:<changeSet id="{NUM}" author="sample"> <dropForeignKeyConstraint baseTableName="{ENTITY_TABLE_NAME}" constraintName="{FK_FOR_DOCUMENT}"/> <update tableName="{ENTITY_TABLE_NAME}"> <column name="{DOCUMENT_COLUMN_NAME}" valueComputed="(select wd.id from webdav_webdav_document_version wdv, webdav_webdav_document wd where wdv.file_descriptor_id = {DOCUMENT_COLUMN_NAME} and wdv.webdav_document_id = wd.id)"/> </update> <addForeignKeyConstraint baseColumnNames="{DOCUMENT_COLUMN_NAME}" baseTableName="{ENTITY_TABLE_NAME}" constraintName="{FK_FOR_DOCUMENT}" referencedColumnNames="ID" referencedTableName="WEBDAV_WEBDAV_DOCUMENT"/> </changeSet>
这里:
-
{NUM}
- 文件中的更改日志编号。 -
{ENTITY_TABLE_NAME}
- 实体表名。 -
{FK_FOR_DOCUMENT}
-FileDescriptor
外键。 -
{DOCUMENT_COLUMN_NAME}
-FileDescriptor
列名称。
点击 Main Data Store 右键菜单的 Update,Studio 会执行 Liquibase 更改日志。
启动应用程序时,Studio 会根据数据库结构和数据模型的差异生成 Liquibase 更改日志。移除更改日志中 drop
FILE_DESCRIPTOR_ID.WEBDAV_WEBDAV_DOCUMENT_VERSION
列的命令(使用 Changelog Preview 窗口中的 Remove and Ignore 命令)。<dropColumn columnName="FILE_DESCRIPTOR_ID" tableName="WEBDAV_WEBDAV_DOCUMENT_VERSION"/>
在完成迁移之前都保留此列。
启动应用程序,切换至 Administration(管理) → JMX Console(JMX 控制台) 界面,打开
jmix.cuba:type=MigrationHelper
MBean。执行convertCubaFileDescriptorsForWebdav()
操作。 -
-
为应用程序配置 HTTPS 访问。参阅 配置 HTTPS 了解如何使用自签名的证书。
-
按照 上面 的介绍迁移本地文件存储的内容。
前端
如果你的项目使用了 CUBA React 客户端,可以用下面的步骤迁移至 Jmix:
-
复制 CUBA 项目中
modules/front
下面的public
、src
文件夹以及其他所有文件至 Jmix 项目的front
文件夹。 -
参阅 Jmix 前端 UI → 从 CUBA 迁移 的说明进行后续步骤。