# 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 ```bash 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. 启动服务 ```bash ./start-all.sh ``` 前台运行,实时打印每个服务的就绪状态,`Ctrl+C` 统一停止。首次运行若缺 JAR 会自动编译对应模块。 ### 命令速查 ```bash ./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-lims`、`jnpf-dms` | | 4 | 其余(当前是 `jnpf-eln`) | 闸门逻辑:读 `vm.loadavg`,负载 ≥ `LOAD_GATE`(默认 9)就等 20s 再放下一波。4 个业务 JVM 同时启动会瞬间打满 CPU,导致 Druid 建连超时。 3. 指定服务子集且不超过 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_liushuihao`、`chanpin_mingcheng`) - 🔴 **业务字段禁止以 `f_` 开头**:这是平台技术列的保留前缀(`f_id` / `f_tenant_id` / `f_version` / `f_delete_mark` / `f_creator_time` / `f_flow_state` 等),审计层正是靠这个前缀剔除技术列 - ORM 用 MyBatis-Plus(`@TableName` + `BaseMapper`) - 标准响应格式:`{code: 200, msg: "...", data: ...}` ### 构建 ```bash 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`**。 ```yaml # 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.yaml`、`jnpf-dms.yaml`(模块专属)等。 ### 两种运行形态共用一份配置 `.env` 同时被两边读取: - **容器栈**:`docker compose` 自动读仓库根 `.env`,注入容器环境 - **宿主栈**:`start-all.sh` 启动时逐行解析 `.env` 并 `export` 差异只在 `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: ```yaml spring.cloud.discovery.client.simple.instances: jnpf-lims: - uri: http://cx-lims:30019 ``` 消费方是所有 `@FeignClient` 的 `lb` 解析、网关的 `lb://` 路由、knife4j 的文档聚合。 > ⚠️ **新增服务要同步三处**:`config/shared/discovery.yaml`、`config/host-override/discovery.yaml`、`docker-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`,避免单个角色占满连接槽饿死另一个