Kaidi 财务项目基础系统
本仓库当前处于 R1 开发执行阶段,需求基线见下方唯一总方案。
唯一需求、开发、测试和交付基线:
本地开发
本地开发数据库由环境变量显式指定,启动 Java 服务。仓库中的 deploy/compose.yaml 只是可选的本地开发夹具;
需要它时必须显式启用 local-db profile,生产安装器不会执行 Compose、创建 MySQL 或安装 MySQL 客户端:
docker compose -f deploy/compose.yaml --profile local-db up -d
随后启动 Java 服务:
cd backend
SPRING_PROFILES_ACTIVE=local \
DB_URL='jdbc:mysql://127.0.0.1:3307/kaidi_finance?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&serverTimezone=UTC' \
DB_USERNAME=kaidi DB_PASSWORD=kaidi_local_2026 \
./mvnw spring-boot:run
前端使用官方 TDesign Vue Next Starter,来源记录见 frontend/UPSTREAM.md。
cd frontend
HUSKY=0 npm ci
npm run dev:linux -- --host 0.0.0.0 --port 3002
本地 local profile 会创建 admin、project、finance、archive、demo 演示账号,初始密码均为
LocalOnly@123;该固定密码只用于本机开发,生产安装会生成随机管理员密码。
正式构建:
cd frontend
npm run build
后端运行后更新唯一 OpenAPI 机器制品:
./scripts/export-openapi.sh http://127.0.0.1:18080
R1 Preview 部署方式
代码仓库和 Release 已公开,提供两种互斥方式:
- systemd 一键安装(推荐):程序独占
/opt/kaidi和kaidi-finance.service,不创建宝塔 Java 项目。 - 宝塔手动部署:下载压缩包后解压并运行本地初始化脚本,再创建 Spring Boot 项目。
两种方式不能同时运行在同一个端口。程序不会安装 MySQL,数据库只支持运维人员预先准备的外部 MySQL 8.4.x, PostgreSQL 18 兼容工作继续冻结。
生产部署契约(先看这里)
- 生产机不执行
git clone、git pull或远程脚本拼接。 源码仓库是https://git.awaioi.com/ERP-Team/kaidi.git,生产安装和后台更新使用同一仓库生成的公开 Gitea Release。 - 安装器先读取 Gitea Release API,再下载
release-manifest.json、签名、公钥和压缩包,校验固定公钥指纹、RSA 签名、版本、文件名和 SHA-256;校验失败不会安装。 - 后台“获取版本”只查询 Release;“下载”才下载并校验;“立即更新并重启”才切换
current并重启服务。更新器默认不访问数据库;如明确设置KAIDI_DB_BACKUP_MODE=mysqldump,才会调用主机已有的备份工具。更新器下载的是 Release 制品,不是 Git 工作树。 - MySQL 是已有数据库,应用只通过向导写入的
DB_URL、DB_USERNAME、DB_PASSWORD连接它;安装器不安装 MySQL、不创建数据库、不修改数据库服务。填写127.0.0.1表示 MySQL 与 Java 应用在同一台服务器,端口以实际监听端口为准,不默认假设3307。 - 安装阶段只校验 JDBC 配置格式,不调用
mysql/mariadb客户端,也不执行CREATE、INSERT、UPDATE、DELETE或DROP。首次向导点击“完成安装”后,应用才会在专用空 schema中运行 Flyway 建表并初始化管理员;这是业务初始化,不是安装 MySQL 服务。在线更新默认跳过数据库备份;需要备份时由运维显式设置KAIDI_DB_BACKUP_MODE=mysqldump,更新器只调用已有工具,不会安装数据库。 KAIDI_APP_PORT只决定 Java 回环监听端口,默认18080;反向代理必须指向安装器输出的PROXY_TARGET。安装器不会替你修改 Nginx 或宝塔站点配置。- 发布归档使用无顶层目录的
tar.gz,只包含app.jar、public/、ops/、VERSION;打包阶段禁用 macOS 扩展属性并拒绝开发数据库回退值。
32 位 Linux 支持边界
当前支持的是 i386/i486/i586/i686 + glibc + systemd + 可运行的 32 位 Java 17。安装器会校验用户空间位数、Java 架构、glibc 和 /lib/ld-linux.so.2(兼容 /lib32、/lib/i386-linux-gnu 路径);musl 或缺少 32 位加载器的系统会在安装前明确失败,不会安装后才出现无法启动。
32 位服务器应使用首次访问 /setup 的向导输入外部 MySQL 8.4 连接信息,这条路径不需要在服务器安装 MySQL 客户端。在线更新默认不需要 mysqldump;如果要启用更新前备份,再确认 32 位系统能执行兼容的 mysqldump,否则保持默认 skip 或从独立备份机执行备份,不要临时安装数据库。
systemd 一键安装(推荐)
先在宝塔面板停止并删除当前错误的 Java 项目,再执行:
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.42/install.sh | sudo env KAIDI_APP_PORT=18080 bash
该命令只安装程序运行所需的 systemd 单元,自动创建 kaidi 用户并检测现有 Java 17(包括
/www/server/java/*/bin/java);没有可用 Java 时才下载匹配架构的运行时。它不会安装 MySQL,也不会要求宝塔面板提供
kaidi 用户。安装完成后查看 /root/kaidi-first-login.txt,通过反向代理域名进入 /setup。
彻底清理旧安装
重新安装前先在宝塔面板停止并删除名为 kaidi-finance 的 Java 项目,避免面板守护进程重新拉起旧 Java。
随后执行一键清理:
curl -fsSL 'https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.42/purge.sh' | sudo env KAIDI_PURGE_CONFIRM=DELETE_LOCAL_KAIDI_INSTALLATION bash
上面是一条完整命令:脚本通过管道直接交给 root 执行,不创建临时安装文件,避免终端自动换行导致 -o 参数丢失。
如果需要连同本地恢复备份一起删除(确认不再需要回滚后再执行),在同一条命令中增加
KAIDI_PURGE_DELETE_BACKUP=true。卸载前必须先停止并删除宝塔中的 kaidi-finance Java 项目;宝塔的项目元数据、
Nginx/反向代理和外部 MySQL 属于外部资源,卸载程序不会误删它们。
清理脚本会先停止 systemd 和遗留 Kaidi 进程,将旧配置、文件存储、安装码以及已有数据库备份归档到
/root/kaidi-reinstall-backups/,再删除 /opt/kaidi、/www/wwwroot/kaidi、/etc/kaidi、
/var/lib/kaidi、/var/lib/kaidi-update、/var/log/kaidi 和三个 systemd 单元。恢复包权限固定为
0600,确认新安装及数据无误后再由 root 删除。脚本不删除外部 MySQL、系统 Java、Nginx 或宝塔站点配置;
新安装必须连接一个新的空 MySQL 数据库,旧数据库保留用于回滚。
Preview.42 直链与宝塔手动部署
本版不使用一键安装脚本。发布物是一个不带顶层目录的压缩包,下载后直接解压到版本目录,再由宝塔面板创建 Spring Boot 项目。数据库、JDK、Nginx 和宝塔本身都由运维人员准备;程序不会自动安装 MySQL 或任何第三方服务。
下载地址:
https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.42/kaidi-finance-1.0.0-preview.42.tar.gz
校验文件:https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.42/SHA256SUMS
服务器要求:Linux、Java 17(宝塔项目选择 JDK 17)、可访问外部 MySQL 8.4.x;systemd 一键安装还需要
systemd/systemd-analyze,宝塔手动部署不要求 systemd。32 位 Linux 需要宿主机
已经提供可运行的 32 位 Java 17。程序只连接已有数据库,不安装数据库客户端或数据库服务。初始化只需要
bash、openssl、sha256sum 和基础 Linux 工具;systemd 在线更新默认不需要数据库客户端。只有将
KAIDI_DB_BACKUP_MODE 设为 mysqldump 时,才需要 curl、jq、flock、gzip、runuser 和兼容的
mysqldump;安装器和更新器会优先使用 PATH 中的工具,也会探测宝塔常见的 /www/server/mysql/bin/ 路径;如备份工具安装在其他位置,可设置
KAIDI_MYSQLDUMP_BIN,程序不会替你安装这些依赖。
更新器默认只下载、验签、切换和健康检查,不调用 Docker、不进入容器,也不创建或管理 MySQL 服务。
1. 下载、校验、解压
以下命令只做文件下载和解压,不执行远程脚本。VERSION 必须替换成发布版本;归档内的 app.jar、public/、ops/
必须直接位于 RELEASE_ROOT,不能再嵌套一层目录。
VERSION=1.0.0-preview.42
APP_ROOT=/www/wwwroot/kaidi
RELEASE_ROOT="$APP_ROOT/releases/$VERSION"
sudo install -d -m 0755 "$RELEASE_ROOT"
wget -O "/tmp/kaidi-finance-$VERSION.tar.gz" \
"https://git.awaioi.com/ERP-Team/kaidi/releases/download/v$VERSION/kaidi-finance-$VERSION.tar.gz"
wget -O /tmp/kaidi-SHA256SUMS \
"https://git.awaioi.com/ERP-Team/kaidi/releases/download/v$VERSION/SHA256SUMS"
(cd /tmp && grep " kaidi-finance-$VERSION.tar.gz$" kaidi-SHA256SUMS | sha256sum -c -)
sudo tar -xzf "/tmp/kaidi-finance-$VERSION.tar.gz" -C "$RELEASE_ROOT"
如果服务器没有 wget,可使用浏览器下载后上传同一文件;curl -fL URL -o FILE 也只用于下载压缩包,不能替代初始化脚本。
2. 执行本地初始化
初始化脚本来自已下载的归档,并在本机以 root 运行。它创建 kaidi 启动用户、受保护配置和一次性安装码;不会下载
Java、安装 MySQL 或修改反向代理。宝塔手动模式不会开放在线更新按钮。端口参数可改为 1024-65535 中的空闲端口,默认 18080。
sudo "$RELEASE_ROOT/ops/baota-init.sh" 18080
sudo cat /root/kaidi-first-login.txt
初始化不会安装或启用 kaidi-update.service/kaidi-update.path,避免宝塔守护进程与
systemd 更新器同时管理同一应用;宝塔模式只支持手动替换 Release,后台在线更新请使用 systemd 安装模式。
初始化失败会回滚本次写入的配置、unit 和 current 链接,可以直接修正原因后重试;已经存在正式安装时不要重复执行,先按“彻底清理旧安装”流程处理。
3. 宝塔 Spring Boot 项目字段
在宝塔面板创建项目,类型选择 Spring Boot,不要选择 Tomcat。填写如下:
| 字段 | 填写值 |
|---|---|
| 项目路径 | /www/wwwroot/kaidi/current |
| 项目名称 | kaidi-finance |
| 项目 JDK | JDK 17 |
| 项目启动命令 | /www/wwwroot/kaidi/current/ops/baota-start.sh |
| 启动用户 | kaidi |
| 项目端口 | 18080(与初始化参数一致) |
| 绑定域名 | 业务域名,例如 fi.awaioi.com |
| 后端路由 | / |
| 前端资源 | 留空,Spring Boot 直接提供 public/ |
| 进程守护 | 开启 |
| 项目意外重启 | 勾选 |
环境变量全部留空,不要创建 DB_URL、DB_USERNAME、DB_PASSWORD 或 FIELD_ENCRYPTION_KEY。
ops/baota.env.example 仅是说明文件,不能把其中的占位值粘贴到面板。启动器先读取 /etc/kaidi/kaidi.env,向导完成后
再读取 /var/lib/kaidi/setup/application.env;宝塔只有填写了非空覆盖值时才会覆盖受保护配置。数据库密码不会出现在
Java 启动命令或 ps 参数中。
4. 首次向导与反向代理
启动宝塔项目后,访问 https://业务域名/setup,输入 /root/kaidi-first-login.txt 中的一次性安装码,填写外部 MySQL
连接信息和系统管理员账号密码。迁移完成后应用会退出一次,宝塔守护进程会自动拉起正式模式;再次访问域名根路径即可登录。
首次配置数据库密码必须使用 HTTPS;没有域名时通过 SSH 隧道访问回环端口:
ssh -L 18080:127.0.0.1:18080 root@SERVER_IP
本机浏览器打开 http://127.0.0.1:18080/setup。反向代理只需把整个 / 转发到 http://127.0.0.1:18080,并保留以下请求头:
location / {
proxy_pass http://127.0.0.1:18080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
client_max_body_size 500m;
}
HTTPS 启用后,在 /etc/kaidi/kaidi.env 将 SESSION_COOKIE_SECURE 设为 true,再从宝塔面板重启项目。健康检查:
curl -fsS http://127.0.0.1:18080/actuator/health
curl -fsS http://127.0.0.1:18080/ | head
5. 手动部署模式的更新边界
宝塔手动部署不启用 Preview 的在线更新契约。需要升级时,先停止宝塔项目,下载并校验新的 Release 压缩包,
解压到新的 releases/<VERSION>,再由宝塔项目切换路径并启动;确认健康检查通过后再删除旧版本。
不要在宝塔项目仍运行时点击“系统治理 → 系统更新”,也不要让宝塔守护和 kaidi-finance.service 同时管理同一端口。
需要后台点击“获取版本 → 下载 → 立即更新并重启”的稳定流程时,使用上一节的 systemd 一键安装方式。
手动升级由运维人员决定是否先做外部 MySQL 备份;程序不会安装 MySQL 或客户端,也不会修改反向代理配置。
后续版本仍按同样方式直接下载并解压到新的 releases/<VERSION>,保留可回滚的旧目录。
Linux 32 位
压缩包本身不绑定 CPU 架构,关键是宿主机提供 glibc、systemd 和可运行的 32 位 Java 17。32 位主机不能在本机运行 64 位 JDK,也不应尝试在本机安装
MySQL 8.4;提前准备外部 MySQL,完成上述向导即可。systemd 在线更新默认不访问数据库;只有显式启用
KAIDI_DB_BACKUP_MODE=mysqldump 时才需要确认宿主机能执行与数据库版本兼容的工具。宝塔手动模式只做手动制品替换和人工数据库备份。
一键卸载
purge.sh 就是本项目的卸载程序:它会停止 Kaidi 进程、移除 systemd 单元、删除本地程序/配置/运行状态、
清理受管临时压缩包,并删除本次安装创建的 kaidi 服务账号;不会触碰外部 MySQL、Nginx、反向代理或系统 Java。
默认先把本地配置和运行数据保存到 root-only 恢复包,再删除受管路径。确认不需要回滚时,可在同一条命令中设置
KAIDI_PURGE_DELETE_BACKUP=true,实现本地受管文件和恢复包一并清除:
curl -fsSL 'https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.42/purge.sh' | sudo env KAIDI_PURGE_CONFIRM=DELETE_LOCAL_KAIDI_INSTALLATION KAIDI_PURGE_DELETE_BACKUP=true bash
执行前先在宝塔停止并删除 kaidi-finance Java 项目;宝塔面板元数据属于外部资源,必须由面板先停用,
否则守护进程会重新拉起 Java,卸载程序会明确报出原因而不误删其他服务。
Release 与在线更新
首次建立发布仓库时生成一次签名密钥:
./scripts/generate-release-key.sh release-signing-private.pem
base64 < release-signing-private.pem | tr -d '\n'
将私钥的 Base64 内容保存为 Gitea Actions Secret RELEASE_SIGNING_KEY_B64,并将命令输出的公钥 SHA-256
保存为 Gitea Actions Variable KAIDI_RELEASE_PUBLIC_KEY_SHA256。Gitea 发布 Token 只用于该工作流创建
Release;生产服务器从公共 Release 下载,不保存 Token。私钥不得提交到 Git。.gitea/workflows/release.yml 要求
act_runner 提供 ubuntu-latest 标签,并在 tag 发布时执行后端、前端、OpenAPI、Shell、安装/更新和浏览器门禁;如果
Actions 页面显示 “No matching online runner”,先启动并注册该标签的 act_runner。
工作流先建立不可见草稿,再显式上传并核对 10 个资产的名称和大小,最后才发布为 /releases/latest。Gitea 的
latest 会排除 prerelease=true,因此即使 tag 名含 preview,发布记录的 prerelease 也固定为 false;
Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布:
git tag v1.0.0-preview.42
git push origin v1.0.0-preview.42
在线更新仍使用独立的 TDesign 页面:系统管理员进入“系统治理 → 系统更新”。更新源由 root 在
/etc/kaidi/update.env 固定为公共 Gitea Latest Release API;页面和普通 API 不能提交 URL、Token、脚本或命令。
Preview 的稳定在线更新契约只支持 systemd 一键安装模式:更新器由
kaidi-update.service 执行,直接停止、切换并重启 kaidi-finance.service,不依赖宝塔面板的意外重启。
宝塔手动部署仅用于手动启动和反向代理;其面板进程管理与 systemd 更新器不是同一套生命周期,暂不作为在线更新的稳定路径。
更新流程固定为:
- 点击“获取版本”,只实时请求 Gitea Latest Release 版本、发布时间和发布说明,不排队下载。
- 发现新版本后显示“立即更新”;管理员点击后才排队
DOWNLOAD。更新器下载 manifest、签名和应用包,执行 RSA、SHA-256、版本、 文件名和压缩包路径校验,通过后缓存到/var/lib/kaidi-update/cache/<version>,业务服务继续运行。 - 页面实时轮询并依次显示下载已排队、下载中、校验中和
READY/下载完成;只有下载完成后才显示“立即更新并重启”。 - 管理员点击“立即更新并重启”,更新器默认不访问数据库;如运维已将
KAIDI_DB_BACKUP_MODE=mysqldump写入/etc/kaidi/update.env,才会调用宿主机已有的mysqldump,备份权限为0600,默认保留最近 5 份。程序不会安装 MySQL 或客户端。 - 更新器安装并保护新版本目录,原子切换
/opt/kaidi/current(宝塔手动部署才使用/www/wwwroot/kaidi/current),停止并重启 systemd 管理的应用服务。 - 健康端点、静态首页和运行版本全部通过后,页面等待 10 秒自动刷新;期间断线或命令响应丢失由前端状态重同步恢复。任一检查失败则切回上一应用版本,
如果启用了
mysqldump模式则保留数据库备份;否则保留事务证据供人工处理。
忙碌期间检查、下载和安装按钮保持禁用,防止重复请求;10 秒只用于成功后的页面刷新,不延迟服务端切换。数据库迁移必须至少保持一个版本向后兼容。 查看状态和日志:
sudo systemctl status kaidi-update.path kaidi-update.service
sudo journalctl -u kaidi-update.service -n 100 --no-pager
cat /var/lib/kaidi-update/status.json
进程被中断时,kaidi-update.path 会根据 processing/request.json 或 transactions/active 自动恢复一次。
如果 status.json 显示 RECOVERY_REQUIRED,失败请求会归档到 /var/lib/kaidi-update/failed,事务证据会移到
transactions/recovery-required* 并退出自动触发路径,新下载和安装请求也会被拒绝。修复日志所示的磁盘、权限或
旧版本健康问题后,将唯一一份待恢复事务移回 active,再明确执行一次:
sudo mv /var/lib/kaidi-update/transactions/recovery-required /var/lib/kaidi-update/transactions/active
sudo systemctl reset-failed kaidi-update.service kaidi-update.path
sudo systemctl start kaidi-update.service
sudo journalctl -u kaidi-update.service -n 100 --no-pager
cat /var/lib/kaidi-update/status.json
如果目录带时间后缀,先通过 ls -1d /var/lib/kaidi-update/transactions/recovery-required* 确认唯一目录,再替换上面
命令中的源路径。只有状态恢复为 SUCCEEDED、CURRENT 或确定性的终态 FAILED,且 transactions/active 已处理
完成后,才算本次恢复结束。后台会保留真实失败状态,不会把待恢复请求误显示成普通排队。
手工生成 Release
KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
./scripts/package-release.sh 1.0.0-preview.42
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
./scripts/verify-release.sh dist/release
输出位于 dist/release/,包含安装脚本、彻底清理脚本、签名清单、公钥、SHA-256 清单、前后端 CycloneDX
SBOM、首次安装指纹文件和完整应用压缩包。
打包脚本会把 Maven revision 与 npm 包版本临时绑定到 Release SemVer,构建结束后恢复工作区源文件;
JAR、两个 SBOM、签名清单或 tag 的版本只要有一项不一致,发布即失败。
签名清单同时绑定应用包、两份 SBOM、安装器、公钥、bootstrap 指纹和 Git 源码修订;本地脏工作区会明确记录
source.dirty=true,tag 工作流只接受干净 checkout 并记录 source.dirty=false。
发布命令同时输出 Release 公钥和归档校验值;将压缩包 SHA-256 写入直链部署说明后再发布。