Files
ERP/README.md
T
Qiufeng 670dfb0c7a
Signed Release / release (push) Successful in 10m18s
fix: preserve updater across systemd restart
2026-08-04 22:31:20 +08:00

483 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 凯迪 ERP + OA 一体化平台
凯迪 ERP + OA 是面向企业内部运营的综合业务平台,覆盖组织协同、审批、合同、采购、库存、财务、人力、项目、制造、运营、档案、审计和系统管理等工作域。前端采用 Vue 3 + TypeScript + Element Plus,后端采用 Spring Boot 3.2 + Spring Data JPA。
> 生产部署固定使用 PostgreSQL 15 或更高版本,不使用 Docker。源码目录保留的 SQLite 配置只用于本地开发和测试;正式 Release JAR 会排除 SQLite 驱动和社区方言。
## 项目现状
- 业务范围覆盖 29 个机构/部门,包含约 700 个 Vue 业务页面和大规模 REST/JPA 模型。
- 当前有效后端位于 `oa-backend/`
- 当前有效前端位于 `ofbiz-framework/plugins/modern-ui/app/`;其上层旧 OFBiz 运行框架已经弃用。
- 前端构建产物直接写入后端 `src/main/resources/static/`,由同一个 Spring Boot JAR 提供页面与 `/api/oa/*` API。
- 已实现非 Docker 一键安装、PostgreSQL Flyway 迁移、独立网页安装向导、Gitea Release 更新、Ed25519/SHA-256 校验、服务重启、健康检查和自动回滚。
- 业务功能和外部系统接入仍需按项目需求验收;技术部署链路通过不等于可以跳过生产安全、备份和数据迁移评审。
## 两种运行模式
| 模式 | 数据库 | 用途 | 关键约束 |
|---|---|---|---|
| 源码开发 | SQLite 或 PostgreSQL | 本机开发、界面调试、自动化测试 | SQLite 仅为零配置开发选项,不代表在线环境 |
| 正式安装 | PostgreSQL 15+ | 服务器部署、在线更新 | 使用签名 Release、systemd/launchd,生产 JAR 不含 SQLite |
## 架构
```text
Browser / Mobile Web
|
v
Spring Boot 3.2
|- Vue 3 static application
|- /api/oa/* REST API
|- authentication and permission gates
|- system update administration API
|
v
PostgreSQL 15+ <--- Flyway migrations
Administrator -> System Update UI -> erp-update helper
|- Gitea Release API
|- Ed25519 + SHA-256 verification
|- atomic current symlink switch
`- health check and rollback
```
## 仓库结构
```text
.
|- README.md
|- run.command # macOS 本地预览与 ngrok 启动器
|- install.sh # 非 Docker 一键安装器
|- uninstall.sh # Linux 完整卸载与数据库 schema 重置
|- distribution/
| |- bin/erp-run # 正式环境应用启动器
| `- bin/erp-update # 下载、校验、切换和回滚助手
|- scripts/package-release.sh # PostgreSQL-only Release 打包与签名
|- .gitea/workflows/release.yml # v* tag 发布流水线
|- oa-backend/ # Spring Boot 后端
| `- src/main/resources/
| |- application.yml # 源码开发默认配置
| |- application-postgres.yml # 正式 PostgreSQL profile
| `- db/migration/postgresql/ # Flyway 迁移
|- ofbiz-framework/plugins/modern-ui/app/
| |- src/data/oaModules.ts # 导航与路由数据源
| `- src/oa/pages/ # Vue 业务页面
|- tests/ # 启动器和 Release 脚本测试
|- docs/ # 部署、设计和实现文档
`- requirements/ # 原始需求与合规审计材料
```
## 本地构建
克隆仓库并进入开发分支:
```bash
git clone git@38.76.196.225:awaioi/ERP.git
cd ERP
git switch dev
git config core.hooksPath .githooks
```
### 环境要求
- Java 17 或更高版本
- Node.js 和 npm
- Python 3、curl、lsof
- 使用公网预览时需要已登录并配置好的 ngrok 3
- PostgreSQL 模式需要 PostgreSQL 15+ 以及可连接的数据库账号
### 1. 构建前端
```bash
cd ofbiz-framework/plugins/modern-ui/app
npm ci
NODE_OPTIONS=--max-old-space-size=8192 npm run build
```
Vite 会清空并重新生成 `oa-backend/src/main/resources/static/`。修改前端后必须先执行前端构建,再执行 `bootJar`,否则 JAR 中仍是旧页面。
### 2. 构建后端
开发构建保留 SQLite 运行库:
```bash
cd oa-backend
./gradlew test
./gradlew bootJar
```
PostgreSQL-only 正式构建:
```bash
cd oa-backend
./gradlew clean bootJar -PreleaseVersion=0.3.7 -PproductionBuild=true
```
正式 JAR 必须包含 PostgreSQL 驱动,并且不得包含 `sqlite-jdbc``hibernate-community-dialects`
## 一键启动本地预览
根目录的 `run.command` 会检查环境、启动或复用 ERP 后端、启动或复用固定 ngrok 隧道、验证本地登录和公网页面,然后打开浏览器。它不会安装 PostgreSQL、构建前端或生成 JAR。
```bash
chmod +x run.command
./run.command
```
macOS 也可以在 Finder 中双击 `run.command`。终端保持运行用于监控服务,按 `Ctrl+C` 只会停止本次启动器自己创建的进程,不会批量终止其他 Java 进程。
常用覆盖项:
| 环境变量 | 默认值 | 说明 |
|---|---|---|
| `ERP_RUN_BACKEND_PORT` | `8091` | 本地 Spring Boot 端口 |
| `ERP_RUN_JAR_PATH` | 最新的 `oa-backend-*.jar` | 指定要运行的 JAR |
| `ERP_RUN_PROFILE` | 当前环境的 Spring profile | 正式环境设为 `postgres` |
| `ERP_RUN_CONFIG_FILE` | 空 | 读取安装器生成的 `erp.env` |
| `ERP_RUN_PUBLIC_URL` | 固定 ngrok 域名 | 公网预览地址 |
| `ERP_RUN_NGROK_API_PORT` | `4040` | ngrok 本地管理端口 |
| `ERP_RUN_NO_OPEN` | `0` | 设为 `1` 时不自动打开浏览器 |
直接以 PostgreSQL profile 运行源码构建时:
```bash
export SPRING_PROFILES_ACTIVE=postgres
export OA_DB_URL='jdbc:postgresql://127.0.0.1:5432/kaidi_erp?sslmode=disable'
export OA_DB_USERNAME='kaidi_erp'
export OA_DB_PASSWORD='replace-with-a-strong-password'
./run.command
```
默认本地地址为 `http://127.0.0.1:8091`。开发预览固定域名为:
```text
https://resonant-elated-launder.ngrok-free.dev/
```
演示环境初始账号为 `admin / 123456`。任何共享或生产环境都必须立即更换默认密码,并限制公网访问。
## 非 Docker 正式安装
安装器支持 64 位 Linux `amd64/arm64` 和 macOS `arm64/amd64`。Linux 安装需要 rootmacOS 不应使用 sudo 启动 LaunchAgent。正式安装前必须已有包含下列四个资产的 Gitea Release
- `kaidi-erp-<version>.tar.gz`
- `kaidi-erp-installer-<version>.jar`
- `SHA256SUMS`
- `SHA256SUMS.sig`
### Linux 一键安装
生产环境应先为 Gitea 配置 HTTPS
```bash
curl -fsSL https://git.example.com/awaioi/ERP/raw/branch/main/install.sh \
| sudo -E bash -s -- \
--gitea-url https://git.example.com \
--repository awaioi/ERP
```
命令行只做环境准备:优先使用已有的 Java 17+,缺少时通过当前系统的 `apt-get``dnf``yum``zypper` 或 Homebrew 安装 Java、curl、tar、Python 3 和 OpenSSL 3,然后下载并启动独立安装器。数据库信息不在命令行输入。
Linux 生产服务要求主机使用 systemd;没有 systemd 的容器、WSL 或精简系统只能显式使用 `--no-service` 做开发验收,在线更新也会保持关闭。
安装器启动后会输出带一次性令牌的访问地址。优先级依次为:命令行 `--public-url`(或 `ERP_PUBLIC_URL`)、HTTPS 服务探测到的公网 IP、局域网 IP。无论使用哪一种方式,都会同时输出仅服务器本机可用的 `Local URL`;公网探测失败时还会明确提示正在回退局域网地址。公网服务器建议显式传入地址,避免 NAT、多网卡或代理环境识别错误:
```text
Setup URL: http://38.76.196.225:8091/?token=<one-time-token>
Local URL: http://127.0.0.1:8091/?token=<one-time-token>
```
`--public-url` 支持域名、端口、路径和已有查询参数,安装器会安全追加一次性 `token`。使用公网 IP 直连时需要在防火墙或安全组放行 ERP 端口;通过 HTTPS 反向代理安装时,应将公开域名作为 `--public-url`
首次打开该地址进入网页向导,依次完成环境检查、PostgreSQL 地址/端口/库名/账号/密码/SSL 测试、管理员账号/姓名/密码设置、数据库迁移和初始化。项目当前没有 Redis 依赖,因此向导不会显示 Redis 配置项。正式服务真实健康检查通过后,启动器才会原子写入安装锁并物理删除 `installer/``install.pending`
PostgreSQL 必须使用专用空数据库,网页中填写的账号必须是该数据库的所有者。该约束保证账号拥有 `public` schema 建表权限,并能持有安装器创建的 `pg_trgm` 扩展;只授予 `CONNECT` 权限不足以完成迁移。
### macOS
macOS 需要 Homebrew,并使用已有 PostgreSQL
```bash
curl -fsSL https://git.example.com/awaioi/ERP/raw/branch/main/install.sh \
| bash -s -- \
--gitea-url https://git.example.com \
--repository awaioi/ERP
```
### 当前 HTTP 测试服务器
当前 Gitea 地址 `http://38.76.196.225:10099` 只允许用于开发验收:
```bash
(
set -e
tmp="$(mktemp)"
trap 'rm -f -- "$tmp"' EXIT
curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.7/install.sh -o "$tmp"
printf '%s %s\n' '88328b3086ece360d3c05d4a22bee06b03ed3639a454c1fe7b9c04b1da80a980' "$tmp" | sha256sum -c -
sudo -E bash "$tmp" \
--gitea-url http://38.76.196.225:10099 \
--repository awaioi/ERP \
--version 0.3.7 \
--public-url http://38.76.196.225:8091 \
--allow-insecure
)
```
只有 `v0.3.7` Release 发布后这条命令才可下载安装包。固定 tag 和 SHA-256 只用于保护当前 HTTP 引导脚本不被传输途中篡改;Release 资产仍会继续执行 Ed25519 和 SHA-256 双重校验。HTTP 会暴露请求、Release 元数据和可能使用的访问令牌,不得作为长期生产方案。
### 完整卸载后重装
以下命令具有破坏性:它会先停止服务,使用现有配置中的 ERP 数据库账号删除并重建目标数据库的 `public` schema,然后删除 systemd unit、程序、配置、状态和日志。脚本只允许数据库所有者执行 schema 清理,并拒绝 `postgres``template0``template1` 和危险文件路径。
```bash
(
set -e
tmp="$(mktemp)"
trap 'rm -f -- "$tmp"' EXIT
curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.7/uninstall.sh -o "$tmp"
printf '%s %s\n' '98c56fed2fd4d01874e4ab5a1a4f3ec42ec3e29b315ffd87385a95488d587546' "$tmp" | sha256sum -c -
sudo -E bash "$tmp" --purge-database --yes
)
```
卸载器会先核对安装配置、路径和 systemd 停止状态,再清理数据库和文件。卸载成功后,再执行上面的 Linux 一键安装命令。不要对包含其他系统数据的共享数据库运行此命令。
### 正式安装目录
Linux 默认路径:
| 内容 | 路径 |
|---|---|
| 程序和版本目录 | `/opt/kaidi-erp` |
| 环境配置 | `/etc/kaidi-erp/erp.env` |
| 更新状态 | `/var/lib/kaidi-erp/update-state.json` |
| 安装状态 | `/var/lib/kaidi-erp/install.pending``/var/lib/kaidi-erp/install.lock` |
| 首次安装器 | `/opt/kaidi-erp/installer/`(健康后自动删除) |
| systemd 服务 | `kaidi-erp.service` |
使用 `--no-service` 只用于开发验收:启动器会在当前用户下运行,但默认设置 `OA_UPDATE_ENABLED=false`。生产环境应使用 systemd/launchd,让在线更新可以在 Java 进程退出后自动拉起新版本。
### HTTPS 反向代理
正式安装默认监听 `8091`(可通过 `ERP_SERVER_PORT` 覆盖)。Nginx、宝塔、Caddy 或 CDN 终止 HTTPS 后,必须把公网协议和主机转发给 Spring Boot;否则浏览器的同源 API 请求会被误判为跨域,并收到纯文本 `403 Invalid CORS request`,前端表现为“响应非 JSON (HTTP 403)”。Nginx 的代理位置至少包含:
```nginx
location / {
proxy_pass http://127.0.0.1:8091;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
如果 Nginx 前面还有 Cloudflare 等上游代理,`X-Forwarded-Proto` 必须保留浏览器实际使用的 `https`,不能被内层 HTTP 链路覆盖。前端与 `/api/oa/*` 推荐使用同一个公网域名;不要在 Nginx 中附加 `Access-Control-Allow-Origin *`
## 在线更新与回滚
管理员登录后可从以下任一入口进入:
```text
顶部工具栏 -> 系统更新
用户菜单 -> 系统更新
手机导航抽屉 -> 系统更新
应用定制平台 -> 系统更新
```
入口只对 `ADMIN` 角色显示,对应前端路由为 `/appdev/update`,后端 API 为 `/api/oa/system-update/*`。页面只显示当前版本、在线最新版本、检查时间、最新版本更新日志、历史正式版本,以及下载、验签、安装、重启和自动回滚进度。顶部和手机入口发现新版本时会显示版本提示。
更新源由安装器写入服务器的 `/etc/kaidi-erp/erp.env`,后台页面不会要求管理员重复填写 Gitea 地址、仓库、Token、通道或 HTTP 开关。需要变更基础设施配置时由服务器运维人员修改 `OA_UPDATE_*` 环境变量并重启服务;公开仓库无需 Token,正式环境应使用 HTTPS。
更新过程如下:
1. 从 Gitea 读取 stable channel 的最新 Release。
2. 下载归档、`SHA256SUMS` 和 Ed25519 签名(Release 里的独立安装器资产只用于首次安装)。
3. 先验证签名和 SHA-256,再拒绝路径穿越、绝对路径、符号链接、硬链接和结构不完整的归档。
4. 校验 `manifest.json` 中的版本、`database=postgresql``rollbackCompatible=true`
5. 可选执行 `pg_dump`,将新版本写入独立目录。
6. 原子切换 `current` 符号链接,并停止旧 Java 进程。
7. systemd/launchd 拉起新版本,更新助手等待新的 PID 和 `/api/oa/health`Linux unit 使用 `KillMode=process`,确保更新助手不会随旧 Java 进程一起被 systemd 清理。
8. 新版本不健康时切回上一链接,终止故障进程并再次验证旧版本健康状态。
同一安装目录使用操作系统文件锁,不能并发执行两个更新任务。也可以手工触发:
```bash
/opt/kaidi-erp/current/bin/erp-update install 0.3.7
```
应用回滚不等于数据库回滚。包含不可逆 Flyway 迁移的版本必须先保证旧应用仍兼容新结构,并建议在安装配置中启用:
```bash
ERP_UPDATE_BACKUP_MODE=pg_dump
```
更新助手不会自动覆盖生产数据库。需要恢复数据库时,应由管理员确认后使用 `pg_restore`
## Gitea Release 发布
推送 `v*` tag 会触发 `.gitea/workflows/release.yml`。流水线会先执行 shell、后端和独立安装器测试,再构建前端、生成 PostgreSQL-only JAR、打包、签名,并创建或更新对应 Gitea Release。
### Actions 前置配置
Gitea 1.27 仓库需要启用 Actions,并配置带 `ubuntu-latest` 标签的在线 Runner。Runner 必须提供:
- Java 17+
- Node.js 和 npm
- Python 3
- curl、tar
- OpenSSL 3,且支持 Ed25519
流水线使用 Gitea 内置短期 `GITEA_TOKEN`,权限限定为代码只读、当前仓库 Release 可写。仓库设置只需添加:
- Secret `RELEASE_PRIVATE_KEY_B64`
- 当前纯 HTTP 测试服务器额外添加 Variable `ERP_RELEASE_ALLOW_INSECURE_HTTP=1`Gitea 禁止变量名以保留前缀 `GITEA_``GITHUB_` 开头)
签名私钥不得提交到 Git。生成 Secret 值:
```bash
base64 < ~/.config/kaidi-erp/release-signing-key.pem | tr -d '\n'
```
发布稳定版本:
```bash
git switch main
git pull --ff-only origin main
git tag -a v0.3.7 -m 'Kaidi ERP v0.3.7'
git push origin v0.3.7
```
发布完成后必须确认 Release 页面存在四个资产,并使用仓库中的 `distribution/release-public-key.pem` 验证签名。私钥与该公钥不匹配时打包脚本会直接失败。
本地手工生成签名资产:
```bash
ERP_RELEASE_PRIVATE_KEY_FILE="$HOME/.config/kaidi-erp/release-signing-key.pem" \
bash scripts/package-release.sh 0.3.7
```
## 配置参考
| 环境变量 | 说明 | 默认值 |
|---|---|---|
| `SPRING_PROFILES_ACTIVE` | 正式环境必须为 `postgres` | 源码默认 profile |
| `OA_DB_URL` | JDBC PostgreSQL URL | 正式环境必填 |
| `OA_DB_USERNAME` | PostgreSQL 用户 | 正式环境必填 |
| `OA_DB_PASSWORD` | PostgreSQL 密码 | 正式环境必填 |
| `OA_DB_POOL_MAX` | 最大连接池 | `20` |
| `OA_DB_POOL_MIN` | 最小空闲连接 | `2` |
| `OA_UPDATE_ENABLED` | 启用管理后台在线更新 | 安装服务时为 `true` |
| `OA_UPDATE_GITEA_BASE_URL` | Gitea 外部地址 | 正式安装时写入 |
| `OA_UPDATE_REPOSITORY` | Release 仓库 | `awaioi/ERP` |
| `OA_UPDATE_CHANNEL` | 更新通道 | `stable` |
| `OA_UPDATE_TOKEN` | 私有仓库下载令牌 | 空;公开仓库不需要 |
| `OA_UPDATE_ALLOW_INSECURE_HTTP` | 允许 HTTP 更新地址 | `false` |
| `ERP_PUBLIC_URL` | 首次安装向导的公网 URL,等价于 `--public-url` | 自动探测公网 IP |
| `OA_SEED_DEMO` | 是否生成演示数据 | 正式安装为 `false` |
| `ERP_UPDATE_BACKUP_MODE` | 更新前数据库备份 | `none`,可设 `pg_dump` |
| `ERP_UPDATE_HEALTH_TIMEOUT_SECONDS` | 新旧版本健康检查超时 | `120` |
| `ERP_UPDATE_HEALTH_POLL_SECONDS` | 健康轮询间隔 | `2` |
## 测试与验收
### 后端
```bash
cd oa-backend
./gradlew test
```
### 前端类型检查与构建
```bash
cd ofbiz-framework/plugins/modern-ui/app
npm ci
NODE_OPTIONS=--max-old-space-size=8192 npm run build
```
### 启动器与发布脚本
```bash
bash tests/run-command.test.sh
bash tests/release-scripts.test.sh
```
### 已运行服务的 OA 冒烟和集成测试
```bash
OA=http://127.0.0.1:8091 bash oa-smoke.sh
OA=http://127.0.0.1:8091 bash oa-itest.sh
```
测试脚本会创建业务测试数据,应使用测试数据库,不要直接对生产数据库运行写入型集成测试。
## 安全与运维注意事项
- 正式 Gitea、安装和更新流量必须使用 HTTPS;`--allow-insecure` 仅限隔离测试环境。
- `RELEASE_PRIVATE_KEY_B64` 和 PostgreSQL 密码不得写入 Git、日志或聊天记录。
- 新增返回金额、个人信息或机密汇总的 API 时,必须复核 `AuthInterceptor` 的敏感读取权限前缀。
- 金额字段和计算统一使用 `BigDecimal`,禁止使用 `double` 处理货币。
- 不要使用 `pkill java``killall java`;只停止明确属于本项目的 PID。
- 修改前端后始终先执行前端构建,再打包后端。
- 每次包含数据库结构变更的 Release 都必须新增 Flyway 迁移并做新库首次迁移、旧库升级和回滚兼容性验证。
- 默认演示账号只能用于开发环境,生产环境需要更换密码并实施最小权限、备份、监控和审计策略。
## 常见问题
### 安装器提示没有 Release
Gitea 仓库尚未发布首个可安装版本,或 Release 缺少四个必需资产。先检查 `/api/v1/repos/awaioi/ERP/releases/latest` 和 Release 页面。
### 如何填写数据库
数据库连接信息只在首次网页向导填写。安装器会执行 `SELECT 1`、检查 PostgreSQL 15+ 并创建/验证 `pg_trgm`,错误凭据不会启动正式应用,也不会把密码写入响应或日志。数据库中已经有业务用户时,安装器会拒绝接管。
### PostgreSQL profile 启动失败
确认 `SPRING_PROFILES_ACTIVE=postgres`,并检查 `OA_DB_URL``OA_DB_USERNAME``OA_DB_PASSWORD`。正式 profile 会执行 Flyway,再使用 Hibernate `ddl-auto=validate` 校验实体与数据库结构。
### `run.command` 提示端口被占用
启动器只会复用能够通过项目健康检查的监听进程。其他程序占用端口时不会被自动终止;请停止对应程序或设置新的 `ERP_RUN_BACKEND_PORT`
### ngrok 无法启动
运行 `ngrok config check` 确认配置有效,并检查固定域名是否属于当前 ngrok 账号。可以通过 `ERP_RUN_NGROK_BIN``ERP_RUN_NGROK_CONFIG``ERP_RUN_NGROK_API_PORT` 覆盖路径与端口。
### 反向代理后提示“响应非 JSON (HTTP 403)”
应用自身的 401/403 权限错误始终是 JSON。该提示表示 Nginx、WAF 或 Spring CORS 层提前返回了纯文本/HTML。先确认代理目标为正式安装端口(默认 `http://127.0.0.1:8091`),再按“HTTPS 反向代理”一节补齐 `Host``X-Forwarded-*` 请求头;响应正文为 `Invalid CORS request` 时即可确认是协议/主机转发不完整。
### 签名验证失败
不要跳过验证。确认 Release 的四个资产来自同一次构建、`SHA256SUMS` 未被改写、签名私钥与 `distribution/release-public-key.pem` 匹配。
## Git 协作
- `dev` 是日常开发分支。
- `main` 只接收已经通过构建、测试、启动和冒烟验证的稳定提交。
- 新环境执行 `git config core.hooksPath .githooks` 启用仓库 hooks。
- 不要在脏工作区执行破坏性 reset;先确认哪些修改属于正在进行的开发。
远程仓库:
```text
git@38.76.196.225:awaioi/ERP.git
```
## 延伸文档
- [在线安装与更新](docs/online-install-and-update.md)
- [项目交接文档](go.md)
- [设计原则](DESIGN.md)
- [后端 API](oa-backend/API.md)
- [后端安全说明](oa-backend/SECURITY.md)
- [端点目录](go-endpoints.md)
- [实体目录](go-entities.md)
- [数据库目录](go-database.md)