编辑 | blame | 历史 | 原始文档

JNPF 后端服务

基于 Spring Cloud Alibaba 的低代码微服务平台(v6.1.0-RELEASE),业务侧包含 LIMS(实验室信息管理)、DMS(文档管理)等模块。

配套仓库:

bostal/
├── jnpf-java-cloud-v6x                    # 后端(本仓库)
└── jnpf-web-monorepo-framework-v6.1       # 前端(pnpm monorepo)

一、macOS 本机启动(start-all.sh)

start-all.sh 是宿主机 java -jar 形态的批量启动脚本:Java 服务直接跑在 macOS 上,基础设施(PostgreSQL / Redis / 流程引擎 / OnlyOffice)跑在 Docker 里。相比全栈容器,改一行代码重启单个服务只要几十秒。

0. 前置条件

依赖 要求 说明
JDK 21(推荐)或 17 镜像基底是 bellsoft/liberica-openjre-rocky:21;流程引擎工程要求 21
Maven 3.6.3+ 无 mvnw wrapper,需系统安装
Docker Desktop 任意近期版本 起基础设施容器
PostgreSQL 已建好的实例 库结构与初始数据不在本仓库内,由甲方提供。需要两个库:业务库 jnpf_init(角色 jnpf_app)、流程引擎库 jnpf_flow(角色 jnpf_flow_app

macOS 自带 bash 是 3.2,脚本已按 3.2 语法编写(无关联数组、空数组用 ${arr[@]+...} 展开),直接 ./start-all.sh 即可,不需要装 bash 5。

1. 配置 hosts

宿主进程要用容器名解析到本机,一次性写入 /etc/hosts

127.0.0.1 cx-infra cx-redis cx-flow-engine

2. 准备 .env

cp .env.example .env

至少确认这几项(.env.example 内每一节都有详细注释):

说明
CUSTOMER_DB_HOST / CUSTOMER_DB_PORT 数据库坐标。业务库和流程引擎库都由这一组推导,**必然同源**
JNPF_APP_DB_USER / JNPF_APP_DB_PASSWORD 业务库凭据
JNPF_FLOW_DB_USER / JNPF_FLOW_DB_PASSWORD 流程引擎库凭据
JNPF_REDIS_PASSWORD Redis requirepass,业务侧同值透传
ONLYOFFICE_JWT_SECRET 必填,缺了 compose 起不来;只在需要文档在线编辑时才关心
COMPOSE_PROFILES 用外部数据库时**注释掉这一行**(它控制是否启动栈内 PG 容器)

两个坑:

  • 不要设 JNPF_FLOW_DB_URL。它是整串覆盖的逃生口,会把流程引擎库和业务库拆成两个来源,症状是流程模板大面积「找不到流程模板」。
  • 数据库坐标写 cx-postgres 时,start-all.sh 会**自动翻译成 127.0.0.1:5433**(compose 只把栈内 PG 发布到 5433,避免和系统 PG 撞车)。指向外部实例(IP 坐标)时这个翻译不生效。启动时脚本会打印实际使用的坐标,可以据此确认连的是哪个库。

3. 启动服务

./start-all.sh

前台运行,实时打印每个服务的就绪状态,Ctrl+C 统一停止。首次运行若缺 JAR 会自动编译对应模块。

命令速查

./start-all.sh                              # 前台启动全部(缺 JAR 自动补编)
./start-all.sh --build                      # 先全量编译再启动
./start-all.sh restart                      # 停止 → 编译 → 启动
./start-all.sh stop                         # 停止全部(可在另一个终端执行)
./start-all.sh status                       # 查看运行状态
./start-all.sh help                         # 完整帮助

./start-all.sh jnpf-lims                    # 只启动 lims
./start-all.sh restart jnpf-lims            # 只重启 lims(仅编译该模块,快很多)
./start-all.sh stop jnpf-lims               # 只停 lims,不影响其他服务
./start-all.sh status jnpf-lims             # 只看 lims
./start-all.sh restart jnpf-lims jnpf-platform   # 多服务一起

日常开发最常用的是 ./start-all.sh restart jnpf-lims —— 它用 mvn -pl <模块> -am 只编译目标模块及其依赖,比全量 mvn package 快一个数量级。

启动编排

脚本不是一股脑全拉起来的:

  1. 阶段 1:先起网关(30000),等端口就绪
  2. 阶段 2:业务服务分四波,波与波之间过负载闸门

| 波次 | 服务 |
|---|---|
| 1 | jnpf-platform |
| 2 | jnpf-biz-common |
| 3 | jnpf-limsjnpf-dms |
| 4 | 其余(当前是 jnpf-eln) |

闸门逻辑:读 vm.loadavg,负载 ≥ LOAD_GATE(默认 9)就等 20s 再放下一波。4 个业务 JVM 同时启动会瞬间打满 CPU,导致 Druid 建连超时。

  1. 指定服务子集且不超过 4 个时**直启不分波**,重启单个服务不会被波次拖慢。

可调环境变量:

变量 默认 说明
SERVICE_READY_TIMEOUT 360 等待就绪的最长秒数
JNPF_SERVICE_MAX_HEAP 384m 单服务堆上限
JNPF_PLATFORM_MAX_HEAP 1536m jnpf-platform 例外档(九个模块同一个 JVM)
LOAD_GATE 9 分波负载闸门

用法:JNPF_SERVICE_MAX_HEAP=512m ./start-all.sh jnpf-lims

日志与排障

位置 内容
log/<服务名>/startup.log 宿主栈的启动与运行日志(脚本重定向的 stdout/stderr)
.pids/<服务名>.pid 进程 PID,stop / status 靠它跨终端管理

常见状态含义:

  • [跳过] ... 已在运行 — PID 文件里的进程还活着
  • [失败] ... 端口 X 已被未登记进程占用 — 端口上有进程但不是本脚本启动的,脚本不会去动它,需要自己 lsof -i :X 处理
  • [未登记](status 输出)— 同上,端口在监听但没有对应 PID 文件
  • [超时] — 进程活着但超时未监听端口,去 log/<服务名>/startup.log 看栈

关于 OnlyOffice

宿主栈下,Document Server 在容器里而后端在宿主上,容器内的 127.0.0.1 指向容器自己。脚本已自动导出三个地址变量走 host.docker.internal,正常情况下不用在 .env 里配;显式配了则以 .env 为准。


二、模块划分

服务拓扑

服务 端口 职责
jnpf-gateway 30000 API 网关,路由转发。基于 WebFlux,与 servlet 栈不能同进程,必须独立部署
jnpf-platform 30002 平台聚合服务:下列九个平台模块合并在一个 JVM 内
jnpf-scheduletask 30009 定时任务(xxl-job admin),未并入 platform
jnpf-eln 30013 ELN 电子实验记录(当前为骨架)
jnpf-biz-common 30015 公共业务能力聚合:OnlyOffice、审计等
jnpf-dms 30016 文档管理
jnpf-lims 30019 LIMS 实验室信息管理,主要业务模块
jnpf-flow-engine 31000 Flowable 7.0.1 流程引擎。**独立工程,不在根 pom**,用独立数据库 jnpf_flow

jnpf-platform 内含的九个模块:

模块 原端口 职责
jnpf-oauth 30001 认证授权(Sa-Token + JWT)
jnpf-system 30002 平台核心:字典、权限、数据接口;同时是 Dubbo LogProvider 的唯一 provider(20880)
jnpf-visualdev 30003 在线开发引擎(表单/列表设计器)
jnpf-flowable 30004 工作流**业务层**,注册名是 jnpf-workflow(真引擎是上面的 flow-engine)
jnpf-file 30005 文件管理
jnpf-message 30008 消息中心
jnpf-permission 30010 权限管理
jnpf-visualdata 30011 数据可视化
jnpf-app 30012 移动端服务

九个模块合并进一个 JVM 后内存占用从约 6.7GB 降到 1.07GB。**九个服务名全部保留为服务发现表里的别名**,指向 30002 —— 所以 60 多个 @FeignClient、网关的 lb:// 路由、各类脚本都不需要改动,写代码时照旧按原服务名调用即可。九个 *-server 模块的源码也保留着,由各自父 pom 的 microservice profile 门控,回滚不需要改代码。

同理,审计能力已并入 jnpf-biz-common,但服务名 jnpf-audit 和路由前缀 /api/audit/ 都刻意保留为别名,调用方零改动。

API 路由前缀

前端请求 → 网关(30000)→ 按前缀转发:

前缀 目标
/api/lims//api/extend/(旧前缀,同指) jnpf-lims
/api/dms/ jnpf-dms
/api/eln/ jnpf-eln
/api/biz/ jnpf-biz-common(如 /api/biz/onlyoffice/xxx
/api/audit/ jnpf-biz-common(别名保留)
/api/oauth/ /api/system/ /api/visualdev/ /api/workflow/ /api/file/ /api/message/ jnpf-platform(经服务名别名解析)

完整路由表在 config/shared/router.yaml

业务模块分层

jnpf-lims 为例,其他业务模块(dms/eln)结构相同:

jnpf-lims/
├── jnpf-lims-entity        # 实体类、DTO、枚举
├── jnpf-lims-biz           # Service + Mapper
├── jnpf-lims-controller    # REST 控制器
├── jnpf-lims-api           # Feign 远程调用接口
└── jnpf-lims-server        # Spring Boot 启动入口 + 配置

包名统一用 lims 前缀:

路径
Entity jnpf-lims-entity/src/main/java/jnpf/limsEntity/
Mapper jnpf-lims-biz/src/main/java/jnpf/limsMapper/
Service jnpf-lims-biz/src/main/java/jnpf/limsService/
Controller jnpf-lims-controller/src/main/java/jnpf/limsController/

公共依赖链

jnpf-common(parent POM)
  └── jnpf-public/jnpf-cloud-base          # 静态服务发现 + 核心工具
       └── jnpf-public/jnpf-common-springaop  # AOP + Sa-Token + Sentinel + Swagger
            └── 各业务服务

开发约定

  • Controller 只做请求/响应转换,业务逻辑在 Service 层
  • 新建 Mapper 后确认启动类的 @MapperScan 能扫描到
  • 实体字段命名跟随数据库列名(拼音风格,如 jianyan_liushuihaochanpin_mingcheng
  • 🔴 业务字段禁止以 f_ 开头:这是平台技术列的保留前缀(f_id / f_tenant_id / f_version / f_delete_mark / f_creator_time / f_flow_state 等),审计层正是靠这个前缀剔除技术列
  • ORM 用 MyBatis-Plus(@TableName + BaseMapper<T>
  • 标准响应格式:{code: 200, msg: "...", data: ...}

构建

mvn clean package -DskipTests                                    # 全量(约 10 分钟)
mvn clean package -DskipTests -pl jnpf-lims/jnpf-lims-server -am  # 只构建 lims
make jars                                                        # 打包全部业务 jar

构建 profile:default-package(fat JAR,默认)、external-package(依赖外置)。


三、配置机制

没有配置中心。配置是仓库内的 YAML 文件 + 环境变量占位符,三层组合。

三层结构

位置 作用 是否进镜像
共享配置 config/shared/*.yaml 单一事实源。所有环境共用同一份结构 /config/shared
宿主覆盖 config/host-override/*.yaml 宿主 java -jar 栈的差异覆盖 ❌ 只存在于工作区
环境值 仓库根 .env 真实的密码、坐标、域名 ❌ 不入库

规则很简单:**config/shared/ 里只写结构和占位符,真实值一律走 .env**。

# config/shared/datasource.yaml 里长这样
  datasource:
    db-type: PostgreSQL
    db-name: jnpf_init
    host: ${CUSTOMER_DB_HOST:cx-postgres}
    port: ${CUSTOMER_DB_PORT:5432}
    username: ${JNPF_APP_DB_USER:jnpf_app}
    password: ${JNPF_APP_DB_PASSWORD:jnpf_app_2026}

占位符的默认值 = 本机 compose 开发形态,所以 cp .env.example .env 之后基本可以直接跑。

config/shared/ 下按关注点分文件:datasource.yaml(数据源/Redis)、discovery.yaml(服务发现)、router.yaml(网关路由)、logger.yaml(日志)、resources.yaml(文件存储)、system-config.yaml(域名等)、jnpf-biz-common.yamljnpf-dms.yaml(模块专属)等。

两种运行形态共用一份配置

.env 同时被两边读取:

  • 容器栈docker compose 自动读仓库根 .env,注入容器环境
  • 宿主栈start-all.sh 启动时逐行解析 .envexport

差异只在 config/host-override/。目前只有一份 discovery.yaml —— 服务发现表里,容器栈解析到容器名(cx-platform:30002),宿主栈整表覆盖为 127.0.0.1:30002。这个文件在容器内不存在,Spring 的 optional: 导入落空即跳过,所以同一份 application.yml 能双栈通用。

.env 里同名键**后出现的覆盖先出现的**;启动前已在环境里的变量不会被 .env 覆盖,所以可以 FOO=bar ./start-all.sh 临时改值。

静态服务发现

没有 Nacos。config/shared/discovery.yaml 是一张 SimpleDiscoveryClient 实例表,直接把服务名映射到 URI:

spring.cloud.discovery.client.simple.instances:
  jnpf-lims:
    - uri: http://cx-lims:30019

消费方是所有 @FeignClientlb 解析、网关的 lb:// 路由、knife4j 的文档聚合。

⚠️ 新增服务要同步三处config/shared/discovery.yamlconfig/host-override/discovery.yamldocker-compose.yml。漏掉任何一处,对应形态下的调用会 502。

改配置后怎么生效

场景 操作
config/**.env,宿主栈 ./start-all.sh stop && ./start-all.sh(Druid 连接池在启动时定死,不热更新)
config/**,容器栈 rebuild 镜像,或用 volume 挂载覆盖 /config/shared(开发用的 override 已经挂好)
交付现场改结构级配置 挂载覆盖 /config/shared 即可,不必重建镜像

数据库

  • 主库 PostgreSQL;代码层同时兼容 MySQL、Oracle、SQLServer、DM8、KingbaseES
  • 业务库 jnpf_init 和流程引擎库 jnpf_flow 由同一组坐标推导,**必须同源**
  • 连接池配额通过 JNPF_APP_CONN_LIMIT / JNPF_FLOW_CONN_LIMIT 分配,两者之和要小于实例 max_connections − 3,避免单个角色占满连接槽饿死另一个