老赵写字的地方

Nop 实战:新建项目与已有模块扩展

程序员在开发中有两个常见的情景。一个是从零开始构建一个项目,这个时候我们先根据项目需求,创建 model,产出 Excel 文件,再用 nop-cli 生成代码;另一种情形是使用已有的代码,就像前面我们做的一样——基于 nop-auth 扩展出我们自己的认证微服务。

本文分别介绍这两种情形。

情景一:从零开始构建一个新项目

配置好环境后,如何快速写一个 API?Nop 平台的流程是模型驱动的:设计 Excel 数据模型 → 代码生成 → 增量定制。

第一步:设计 Excel 数据模型

在项目根目录下创建 model 文件夹。Nop 使用特定格式的 Excel 文件(.orm.xlsx)作为模型定义源,一个文件包含多个 Sheet。

以 my-app.orm.xlsx 为例,说明各 Sheet 的格式。

1. 目录 Sheet——列出所有实体和对应的表名:

对象名 表名 中文名 说明
Article my_article 文章 文章管理

2. 配置 Sheet——应用名、包名、数据库方言等:

键 值 说明
appName my-app
maven.groupId com.example
maven.artifactId my-app
entityPackageName com.example.myapp.entity
dialect mysql

3. 域定义 Sheet——自定义数据类型:

名称 类型 长度 小数位数 标准域
userName VARCHAR 50
email VARCHAR 100
phone VARCHAR 50
image VARCHAR 100 file

4. 实体 Sheet——每个实体一张,以 Article 为例,列头为 Nop ORM 标准 14 列:

编号 主键 标签 字段名 显示 英文名 中文名 数据域 非空 类型 长度 小数位数 缺省值 字典
1 PK seq articleId articleId 文章 ID TRUE VARCHAR 50
2 title title 文章标题 TRUE VARCHAR 200
3 content content 文章内容 TRUE VARCHAR 4000
4 author author 作者 VARCHAR 100
5 clock createTime createTime 创建时间 DATE

各列含义:

  • 编号 — 字段序号,从 1 开始
  • 主键 — 标记 PK 的字段为主键
  • 标签 — 特殊标记,如 seq(自动编号)、clock(时间戳)、disp(显示用)
  • 字段名 — Java 属性名,自动转为下划线数据库列名
  • 显示 — 表单显示控制(X=隐藏、C=创建、R=只读等)
  • 英文名 / 中文名 — 字段标签
  • 数据域 — 引用域定义中的域名
  • 非空 — TRUE 表示 NOT NULL
  • 类型 — VARCHAR、INTEGER、DATE、TIMESTAMP、TEXT 等
  • 长度 — 字符串长度
  • 小数位数 — 数字精度
  • 缺省值 — 数据库默认值
  • 字典 — 关联的字典编码

第二步:运行代码生成器

Nop 平台提供 nop-cli 命令行工具,可以直接通过 CLI 调用代码生成。nop-cli 是一个可独立运行的 JAR,不需要 Maven 插件配置。

# 使用 nop-cli 从 Excel 模型生成代码
java -jar nop-cli.jar gen \
  -o=./my-app \
  -t=/nop/templates/orm \
  ./model/my-app.orm.xlsx

生成的目录结构:

my-app/
  _gen/                    ← 自动生成的代码
    model/
      _App.orm.xml         ← ORM 模型定义
      _Article.java        ← 实体类
    service/
      _ArticleBizModel.java ← CRUD 服务
    api/
      _ArticleApi.java     ← API 接口
  src/
    main/
      resources/
        my-app.orm.xml     ← 可定制的 ORM 模型(继承 _gen 版本)

_gen 目录下的文件都以 _ 开头,标记为“只读生成”——不要手动修改。定制化在 src/ 中完成。

第三步:编写 Delta 定制层

自动生成的服务已经可以运行,提供了完整的 CRUD 操作。如果需要定制,不要修改 _gen 下的代码,而是添加 Delta 层。

例如添加一个“获取文章统计”的接口:

在 src/main/resources/_my-app/service/ 下创建文件,覆盖自动生成的服务定义:

<beans>
  <bean id="ArticleBizModel" class="my.app.service.ArticleBizModelEx" />
</beans>

在 Java 中编写扩展类:

public class ArticleBizModelEx extends _ArticleBizModel {
 
    @BizQuery
    public List<Article> getRecentPosts(@Name("days") int days) {
        return dao().findRecent(days);
    }
}

Delta 层的 Java 文件放在 src/main/java/ 下,与自动生成的代码隔离。

第四步:启动服务

在 my-app 模块中配置好 quarkus-maven-plugin 后,运行:

cd my-app
./mvnw compile quarkus:dev

启动后,自动注册的 API 端点:

第五步:测试 API

保存一篇文章:

curl -X POST http://localhost:8080/r/ArticleBizModel__save \
  -H "Content-Type: application/json" \
  -H "nop-tenant: 0" \
  -d '{"data":{"title":"Hello Nop","content":"我的第一篇文章","author":"test"}}'

查询文章列表:

curl -X POST http://localhost:8080/r/ArticleBizModel__findPage \
  -H "Content-Type: application/json" \
  -H "nop-tenant: 0" \
  -d '{"query":{"title__contains":"Nop"}}'

情景二:基于已有项目进行扩展(Delta 定制)

在实际项目中,更多时候不是从零开始,而是对已有的模块进行扩展。Nop 平台的可逆计算(Delta)机制使得我们可以在不修改上游源码的前提下,为已有的模块增加新功能。

下面以我们实际的工程为例——基于 nop-auth 扩展出物业系统的认证微服务(tom-auth-app)。整个模块的目录结构如下(标注了新增和生成的文件):

tom-auth-app/
├── pom.xml                          ← 新建:Maven 配置,依赖 nop-auth-*
├── model/
│   └── tom-auth.orm.xml             ← 新建:源模型,定义新增实体
├── postcompile/
│   └── gen-orm.xgen                 ← 新建:代码生成脚本
└── src/main/
    ├── java/com/estate/auth/
    │   ├── TomAuthApplication.java  ← 新建:Quarkus 启动入口
    │   ├── TomAuthCodeGen.java      ← 新建:代码生成启动器
    │   ├── entity/
    │   │   ├── OrgUnit.java         ← 生成:实体包装类
    │   │   ├── UserOrgRel.java      ← 生成:实体包装类
    │   │   └── _gen/
    │   │       ├── _OrgUnit.java    ← 生成:实体基类
    │   │       └── _UserOrgRel.java ← 生成:实体基类
    │   ├── biz/
    │   │   ├── IOrgUnitBiz.java     ← 生成:业务接口
    │   │   └── IUserOrgRelBiz.java  ← 生成:业务接口
    │   └── dao/
    │       ├── NopAuthDaoConstants.java     ← 生成:DAO 常量
    │       └── _NopAuthDaoConstants.java    ← 生成:DAO 常量基类
    └── resources/
        ├── application.yaml                ← 新建:应用配置
        └── _vfs/_delta/default/nop/auth/
            ├── _module                     ← 生成:模块声明
            ├── orm/
            │   ├── app.orm.xml             ← 生成:Delta ORM 入口
            │   └── default/_app.orm.xml    ← 生成:Delta ORM 定义
            └── beans/
                └── _dao.beans.xml          ← 生成:DAO Bean 注册

说明:

  • 新建(手工编写):pom.xml、model/ 源模型、postcompile/ 脚本、TomAuthApplication.java、TomAuthCodeGen.java、application.yaml
  • 生成(从模型自动产出):实体类、业务接口、DAO 常量、Delta VFS 资源

第一步:引入上游模块为依赖

在 pom.xml 中引入上游 nop-auth 模块为依赖,不把源码复制到项目里:

<dependencies>
    <dependency>
        <groupId>io.github.entropy-cloud</groupId>
        <artifactId>nop-auth-dao</artifactId>
    </dependency>
    <dependency>
        <groupId>io.github.entropy-cloud</groupId>
        <artifactId>nop-auth-service</artifactId>
    </dependency>
    <dependency>
        <groupId>io.github.entropy-cloud</groupId>
        <artifactId>nop-auth-web</artifactId>
    </dependency>
</dependencies>

上游模块的 VFS(虚拟文件系统)资源会随 JAR 一起加载,包含 ORM 模型、xbiz 业务逻辑、AMIS 页面等。

第二步:创建 ORM 源模型

在 model/ 目录下编写 tom-auth.orm.xml,定义新增的实体和字段。这些实体是对上游的补充,而非替代。

<orm ext:entityPackageName="com.estate.auth.entity"
     ext:basePackageName="com.estate.auth"
     ext:deltaDir="default">
    <entities>
        <entity className="com.estate.auth.entity.OrgUnit"
                name="com.estate.auth.entity.OrgUnit"
                tableName="estate_org_unit">
            <columns>
                <column code="ORG_ID" name="orgId" primary="true"/>
                <column code="TENANT_ID" name="tenantId"/>
                <column code="PARENT_ID" name="parentId"/>
                <column code="PATH" name="path"/>
            </columns>
        </entity>
        <entity className="com.estate.auth.entity.UserOrgRel"
                name="com.estate.auth.entity.UserOrgRel"
                tableName="estate_user_org_rel"/>
    </entities>
</orm>

第三步:配置代码生成器

在 postcompile/gen-orm.xgen 中编写代码生成脚本,指定模型路径和模板:

<c:script>
codeGenerator.withTargetDir("../")
    .renderModel('../model/tom-auth.orm.xml',
        '/nop/templates/orm-delta', '/', $scope);
</c:script>

在 pom.xml 中配置 exec-maven-plugin,绑定到 process-classes 阶段,指向代码生成启动器 TomAuthCodeGen:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>3.1.0</version>
    <executions>
        <execution>
            <id>codegen</id>
            <phase>process-classes</phase>
            <goals><goal>java</goal></goals>
        </execution>
    </executions>
    <configuration>
        <mainClass>com.estate.auth.codegen.TomAuthCodeGen</mainClass>
    </configuration>
</plugin>

运行 mvn package 时,编译完成后自动触发代码生成:TomAuthCodeGen 启动 → 加载 ORM 模型 → 根据 orm-delta 模板生成实体类和 Delta VFS 资源。

代码生成器产出的内容(对应目录结构中标为“生成”的文件):

Java 实体类:

  • entity/OrgUnit.java / entity/UserOrgRel.java — 可定制的实体包装类
  • entity/_gen/_OrgUnit.java / entity/_gen/_UserOrgRel.java — 生成的基类(含 getter/setter/ORM 方法)
  • biz/IOrgUnitBiz.java / biz/IUserOrgRelBiz.java — 业务接口
  • dao/NopAuthDaoConstants.java / dao/_NopAuthDaoConstants.java — DAO 常量

Delta VFS 资源(放在 _vfs/_delta/default/nop/auth/ 下):

  • orm/app.orm.xml — Delta ORM 入口,标记为 x:extends=“super,default/_app.orm.xml”
  • orm/default/_app.orm.xml — 实际的 Delta ORM 定义
  • beans/_dao.beans.xml — DAO Bean 注册
  • _module — 模块声明

启动时,上游 nop-auth 的 ORM 和我们的 Delta ORM 自动合并,新增的 OrgUnit 和 UserOrgRel 实体就生效了。

第四步:编写应用启动入口

在 src/main/java/com/estate/auth/ 下创建两个 Java 文件。

TomAuthApplication.java——Quarkus 应用入口,负责启动 HTTP 服务和 Nop 平台:

@QuarkusMain
public class TomAuthApplication {
    static String[] globalArgs;
 
    public void start(@Observes StartupEvent event) {
        QuarkusIntegration.start();
        new NopApplication().run(globalArgs);
    }
 
    public void stop(@Observes ShutdownEvent event) {
        CoreInitialization.destroy();
    }
 
    public static void main(String... args) {
        globalArgs = args;
        Quarkus.run(args);
    }
}

TomAuthCodeGen.java——代码生成启动器,放在同一个包下,负责初始化 Nop、执行 postcompile/ 下的 .xgen 脚本:

public class TomAuthCodeGen {
    public static void main(String[] args) {
        AppConfig.getConfigProvider().updateConfigValue(
            CoreConfigs.CFG_CORE_MAX_INITIALIZE_LEVEL,
            CoreConstants.INITIALIZER_PRIORITY_ANALYZE);
        CoreInitialization.initialize();
        try {
            File projectDir = MavenDirHelper.projectDir(TomAuthCodeGen.class);
            XCodeGenerator.runPostcompile(projectDir, "/", false);
        } finally {
            CoreInitialization.destroy();
        }
    }
}

然后配置 application.yaml,放到 src/main/resources/ 下:

nop:
  auth:
    login:
      allow-create-default-user: true
    jwt:
      enc-key: your-jwt-secret-key
    site-map:
      static-config-path: /nop/auth/auth/app.action-auth.xml
  orm:
    init-database-schema: true
  datasource:
    driver-class-name: org.h2.Driver
    jdbc-url: jdbc:h2:mem:test

第五步:编译打包为独立微服务

mvn clean package -DskipTests -Dquarkus.package.type=uber-jar

产物是一个可独立运行的 uber-jar,包含了上游 nop-auth 的全部功能和我们的 Delta 定制:

java -jar tom-auth-app/target/tom-auth-app-1.0.0-SNAPSHOT-runner.jar

启动后:

  • 自动加载上游 nop-auth 所有功能(用户/角色/权限/部门管理)
  • 自动合并 Delta 定制(OrgUnit 组织架构、UserOrgRel 用户组织关系)
  • GraphQL API 和 AMIS 管理页面全部可用

REST API 路由说明

Nop 平台所有业务对象的 REST API 统一使用以下 URL 格式:

/r/{BizObjName}__{method}

例如:

  • /r/LoginApi__login → 调用 LoginApi 的 login 方法
  • /r/SiteMapApi__getSiteMap → 调用 SiteMapApi 的 getSiteMap 方法
  • /r/NopAuthUser__findPage → 调用 NopAuthUser 的 findPage 方法

其中 /r/ 是 Nop 框架的固定路由前缀,不是转义符。完整的路由体系:

前缀 用途 说明
/r/ REST API 调用任意业务对象的 CRUD 或自定义方法
/graphql GraphQL GraphQL 查询端点
/p/ 页面资源 AMIS 页面 JSON 配置
/f/ 文件资源 文件上传/下载
/q/ 查询 简化查询接口

网关和 Vite 代理需要正确转发这些路径,确保 /r/ 请求到达对应的微服务。

两种情景的对比

环节 从零开始 扩展已有项目
模型来源 新建 Excel 定义 编写 ORM XML,引用上游实体
代码生成 nop-cli gen exec-maven-plugin 绑定到生命周期
模板 /nop/templates/orm /nop/templates/orm-delta
定制方式 在 src/ 中覆盖 _gen/ 在 _vfs/_delta/ 中覆盖上游 VFS
升级影响 重新运行 codegen 只改上游版本号,Delta 层不受影响

总结

无论从零开始还是扩展已有项目,Nop 的核心模式不变:模型驱动 → 代码生成 → Delta 定制。区别在于:

  • 从零开始:模型是 Excel,模板是 orm,生成全套 CRUD
  • 扩展已有:模型是 ORM XML,模板是 orm-delta,生成 Delta 叠加层

Delta 机制的好处是升级友好——上游 nop-entropy 发新版本时,只需改版本号,定制代码完全不受影响。

进一步阅读

zh/nop/practical_guide/first_api_guide.txt · 最后更改: 由 127.0.0.1