From aed561c70235f04cd0e48e7a99e9847a3a4b9dde Mon Sep 17 00:00:00 2001 From: Qiufeng Date: Mon, 17 Aug 2026 15:27:32 +0800 Subject: [PATCH] feat: simplify public preview installation --- README.md | 113 ++++++++++------------------ deploy/env.production.example | 4 +- deploy/install-from-git.sh | 36 +++------ deploy/install.sh | 71 +---------------- docs/财务系统开发交付总方案.md | 12 +-- scripts/test-git-install-fixture.sh | 16 +++- scripts/test-install-fixture.sh | 20 ++++- 7 files changed, 90 insertions(+), 182 deletions(-) diff --git a/README.md b/README.md index bd9c549..ad267e3 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,11 @@ npm run build ## R1 Preview 一键安装 -Release 发布后,在 Linux 服务器执行下面一组命令即可安装。代码仓库和更新源固定为私有 Gitea -`https://git.awaioi.com/ERP-Team/kaidi`;服务器读取 -`https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest`,不在生产机执行 `git pull` 或现场编译。 -先从受信任的 CI 输出或内部发布记录取得 `RELEASE_TAG`、`INSTALLER_SHA256` 和 `PUBLIC_KEY_SHA256`。 -后两项也会写入 Release 的 `bootstrap-checksums.txt`,但首次安装必须通过独立渠道核对,不能把同源下载值 -直接当作信任根。私有仓库 Token 只授予仓库/Release 读取权限,不得复用 CI 的发布写 Token。 +代码仓库和 Release 已公开,Linux 服务器不需要 Gitea Token。安装器读取 +`https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest`,生产机不执行 `git pull` 或现场编译。 +安装脚本内置固定的发布公钥指纹,下载的应用包、清单、SBOM 和更新脚本仍须通过 RSA 签名与 SHA-256 校验。 +本版只支持由运维人员预先准备的外部 MySQL 8.4.x;安装器不会安装 MySQL、创建数据库容器或修改现有 +PostgreSQL 18。PostgreSQL 兼容开发已冻结,不属于本次 Preview.9 发布范围。 执行命令的机器需预装 `bash`、`sudo`、`curl`、`mktemp` 和 `sha256sum`,并能访问目标 Gitea;`jq`、Java、 Nginx 和数据库客户端由安装器补齐。应用固定安装到 `/opt/kaidi`、`/var/lib/kaidi`、 @@ -58,60 +57,38 @@ Nginx 和数据库客户端由安装器补齐。应用固定安装到 `/opt/kaid ### 直接 curl 安装 ```bash -( - set -Eeuo pipefail - RELEASE_TAG=v1.0.0-preview.8 - INSTALLER_SHA256=33d4921bcad7ef2be12f20bc0512a5f6df4227ea6dd3b7e6225164077d9cd9e7 - PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 - REINSTALL=false - SETUP_WIZARD=true - read -rsp 'Gitea 只读 Token: ' GITEA_READ_TOKEN; printf '\n' - token_file=$(mktemp); header_file=$(mktemp); installer=$(mktemp) - trap 'rm -f "$token_file" "$header_file" "$installer"' EXIT - printf '%s' "$GITEA_READ_TOKEN" > "$token_file" - printf 'Authorization: token %s\n' "$GITEA_READ_TOKEN" > "$header_file" - unset GITEA_READ_TOKEN - chmod 0600 "$token_file" "$header_file" "$installer" - curl --fail --silent --show-error --proto '=https' --tlsv1.2 \ - --header "@$header_file" \ - "https://git.awaioi.com/ERP-Team/kaidi/releases/download/$RELEASE_TAG/install.sh" \ - -o "$installer" - printf '%s %s\n' "$INSTALLER_SHA256" "$installer" | sha256sum -c - - sudo env \ - KAIDI_RELEASE_API_URL=https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest \ - KAIDI_RELEASE_TOKEN_FILE="$token_file" \ - KAIDI_RELEASE_PUBLIC_KEY_SHA256="$PUBLIC_KEY_SHA256" \ - KAIDI_REINSTALL="$REINSTALL" \ - KAIDI_SETUP_WIZARD="$SETUP_WIZARD" \ - bash "$installer" -) +curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.9/install.sh | sudo bash +``` + +这条命令会安装最新签名 Release,并默认进入 `/setup` 安装向导。需要在执行前独立校验安装脚本时使用: + +```bash +curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.9/install.sh -o /tmp/kaidi-install.sh +printf '%s %s\n' faf52cc902abbc2bd48571caee3f4207de50ce339b82ce88fe23f9c4ce8f89ef /tmp/kaidi-install.sh | sha256sum -c - +sudo bash /tmp/kaidi-install.sh +rm -f /tmp/kaidi-install.sh ``` ### 从固定 Git tag 拉取后安装 -需要保留源码快照时,可以拉取与 Release 对应的固定 tag,再运行仓库内包装器。私有仓库会在 `git clone` -时要求 Gitea 凭据,包装器随后单独要求只读 Release Token;它会在 `sudo` 前校验 `deploy/install.sh` -的固定 SHA-256,再按同一公钥信任链安装最新签名 Release。 +需要保留源码快照时,可以拉取与 Release 对应的固定 tag,再运行仓库内包装器。公共仓库不要求凭据, +包装器会在 `sudo` 前校验 `deploy/install.sh` 的固定 SHA-256,再按同一公钥信任链安装最新签名 Release。 ```bash -git clone --branch v1.0.0-preview.8 --depth 1 https://git.awaioi.com/ERP-Team/kaidi.git kaidi-preview +git clone --branch v1.0.0-preview.9 --depth 1 https://git.awaioi.com/ERP-Team/kaidi.git kaidi-preview cd kaidi-preview -KAIDI_SETUP_WIZARD=true ./deploy/install-from-git.sh +./deploy/install-from-git.sh ``` 首次安装使用向导时不要传 `KAIDI_DB_URL`、`KAIDI_DB_USERNAME` 或 `KAIDI_DB_PASSWORD`;这些值在浏览器中填写。 只有已完成安装的修复性重装才从运行时配置读取数据库值,详见下文。 -```bash -KAIDI_DB_URL='jdbc:mysql://MYSQL_HOST:3306/kaidi_finance?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&serverTimezone=UTC' \ -KAIDI_DB_USERNAME=kaidi KAIDI_DB_PASSWORD='DB_PASSWORD' ./deploy/install-from-git.sh -``` - 安装器会完成以下动作: - 仅在 Linux + systemd 环境执行;首版 32 位支持基线为带 systemd 的 Debian/Ubuntu x86 32 位 Linux。 +- 不安装或创建数据库;在 `/setup` 中连接运维人员预先准备的外部 MySQL 8.4.x。 - 识别 `x86_64`、`aarch64`、`armv7` 或 32 位 `i386/i486/i586/i686`,校验 SHA-256 后安装对应的 Azul Java 17 JRE。 -- 使用安装命令固定的 SHA-256 指纹校验 Release 公钥,再用该公钥验证发布清单 RSA 签名。 +- 使用安装器内置的 SHA-256 指纹校验 Release 公钥,再用该公钥验证发布清单 RSA 签名。 - 首次安装默认启用 `/setup` 向导,不在命令行保存数据库密码;向导只接受 MySQL 8.4.x,并在提交前验证 DDL/DML 权限。 - 安装签名 Release 到 `/opt/kaidi/releases/`,以 `/opt/kaidi/current` 原子切换当前版本。 - 安装 Nginx、`kaidi-finance.service`、更新监听服务和健康检查。 @@ -137,21 +114,7 @@ sudo cat /root/kaidi-first-login.txt 32 位服务端镜像,因此 32 位主机需要预先连接一台 MySQL 8.4 数据库,之后仍然只执行一个安装命令: ```bash -( - set -Eeuo pipefail - RELEASE_TAG=v1.0.0-preview.8; INSTALLER_SHA256=33d4921bcad7ef2be12f20bc0512a5f6df4227ea6dd3b7e6225164077d9cd9e7; PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 - read -rsp 'Gitea 只读 Token: ' GITEA_READ_TOKEN; printf '\n' - token_file=$(mktemp); header_file=$(mktemp); installer=$(mktemp) - trap 'rm -f "$token_file" "$header_file" "$installer"' EXIT - printf '%s' "$GITEA_READ_TOKEN" > "$token_file" - printf 'Authorization: token %s\n' "$GITEA_READ_TOKEN" > "$header_file" - unset GITEA_READ_TOKEN; chmod 0600 "$token_file" "$header_file" "$installer" - curl --fail --silent --show-error --proto '=https' --tlsv1.2 --header "@$header_file" \ - "https://git.awaioi.com/ERP-Team/kaidi/releases/download/$RELEASE_TAG/install.sh" -o "$installer" - printf '%s %s\n' "$INSTALLER_SHA256" "$installer" | sha256sum -c - - sudo env KAIDI_RELEASE_TOKEN_FILE="$token_file" KAIDI_RELEASE_PUBLIC_KEY_SHA256="$PUBLIC_KEY_SHA256" \ - KAIDI_SETUP_WIZARD=true bash "$installer" -) +curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.9/install.sh | sudo bash ``` 32 位主机不能运行安装器自动创建的 MySQL 容器,因此在打开向导前,需要预先创建 `kaidi_finance`,并授予安装账号该库的 @@ -171,14 +134,19 @@ GRANT ALL PRIVILEGES ON kaidi_finance.* TO 'kaidi'@'KAIDI_SERVER_IP'; 管理员数据和运维人员新增的环境变量: ```bash -# 使用上方同一安装命令,把 REINSTALL=false 改成 REINSTALL=true。 +curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.9/install.sh \ + | sudo env KAIDI_REINSTALL=true KAIDI_SETUP_WIZARD=false bash ``` 重装失败会恢复原应用链接、Java 运行时、环境文件、systemd 单元和 Nginx 配置;脚本会明确报告回滚不完整, 不会把恢复失败吞掉。 -如果安装器已完成但向导尚未提交,可使用同一命令同时设置 `REINSTALL=true` 和 `KAIDI_SETUP_WIZARD=true` 重新生成一次性安装码; -该恢复路径只接受仍处于向导模式且未锁定的安装,正式模式不会被覆盖。 +如果安装器已完成但向导尚未提交,可执行下面的命令重新生成一次性安装码;该恢复路径只接受仍处于向导模式且未锁定的安装,正式模式不会被覆盖。 + +```bash +curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.9/install.sh \ + | sudo env KAIDI_REINSTALL=true bash +``` ### 停用并移除程序 @@ -205,8 +173,8 @@ 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 内建 `GITEA_TOKEN` 只用于该工作流创建 -Release;生产服务器使用另一个只读 Token。私钥不得提交到 Git。`.gitea/workflows/release.yml` 要求 +保存为 Gitea Actions Variable `KAIDI_RELEASE_PUBLIC_KEY_SHA256`。Gitea 发布 Token 只用于该工作流创建 +Release;生产服务器从公共 Release 下载,不保存 Token。私钥不得提交到 Git。`.gitea/workflows/release.yml` 要求 act_runner 提供 `ubuntu-24.04` 标签,并在 tag 发布时执行后端、前端、OpenAPI、Shell、安装/更新和浏览器门禁。 工作流先建立不可见草稿,再显式上传并核对 9 个资产的名称和大小,最后才发布为 `/releases/latest`。Gitea 的 @@ -214,13 +182,13 @@ act_runner 提供 `ubuntu-24.04` 标签,并在 tag 发布时执行后端、前 Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布: ```bash -git tag v1.0.0-preview.8 -git push origin v1.0.0-preview.8 +git tag v1.0.0-preview.9 +git push origin v1.0.0-preview.9 ``` 在线更新使用独立的 TDesign 页面:隔离的系统管理员进入“系统治理 → 系统更新”。权限与配置页只管理用户、角色、数据范围、表单模板和参数版本,不配置系统名称或域名。 -更新源由 root 在 `/etc/kaidi/update.env` 固定为私有 Gitea Latest Release API;页面和普通 API 都看不到 Token, -也不能提交 URL、脚本或命令。更新流程固定为: +更新源由 root 在 `/etc/kaidi/update.env` 固定为公共 Gitea Latest Release API;页面和普通 API 不能提交 +URL、Token、脚本或命令。更新流程固定为: 1. 点击“获取更新”,后端先读取 Release 元数据,再自动排队 `DOWNLOAD`;root 更新器从 Gitea 下载 manifest、签名和应用包,执行 RSA、 SHA-256、版本、文件名和压缩包路径校验后缓存到 `/var/lib/kaidi-update/cache/`。业务服务不停机。 @@ -235,10 +203,9 @@ git push origin v1.0.0-preview.8 忙碌期间检查、下载和安装按钮保持禁用,防止重复请求;这就是更新执行冷却。10 秒只用于成功后的页面刷新, 不会延迟服务端切换。systemd 在 300 秒内连续失败 3 次后停止自动重试,避免失败任务空跑。 -安装器会把 API 地址和只读 Token 同步写入 root-only 的 `/etc/kaidi/kaidi.env` 与 -`/etc/kaidi/update.env`,两者权限均为 `0600`:前者供 Java 后端“检查更新”读取,后者供 root 更新器下载资产。 -轮换 Token 时必须同时更新两个文件,再执行 `sudo systemctl restart kaidi-finance`;Token 不写入页面、状态 JSON、 -审计参数或更新日志。 +安装器会把公共 API 地址写入 root-only 的 `/etc/kaidi/kaidi.env` 与 `/etc/kaidi/update.env`,两者权限均为 +`0600`:前者供 Java 后端“检查更新”读取,后者供 root 更新器下载资产。默认 Token 为空;如改接私有镜像, +可由运维人员在这两个文件中配置独立只读 Token,Token 不写入页面、状态 JSON、审计参数或更新日志。 数据库迁移必须保持至少一个版本的向后兼容。查看状态和日志: @@ -267,7 +234,7 @@ cat /var/lib/kaidi-update/status.json ```bash KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \ KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \ - ./scripts/package-release.sh 1.0.0-preview.8 + ./scripts/package-release.sh 1.0.0-preview.9 KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \ ./scripts/verify-release.sh dist/release ``` diff --git a/deploy/env.production.example b/deploy/env.production.example index f11da98..8adcc94 100644 --- a/deploy/env.production.example +++ b/deploy/env.production.example @@ -14,10 +14,10 @@ FILE_SCANNER_ENABLED=false FINANCE_BOOTSTRAP_ENABLED=false FINANCE_BOOTSTRAP_PASSWORD= -APP_VERSION=1.0.0-preview.8 +APP_VERSION=1.0.0-preview.9 UPDATE_CURRENT_VERSION_FILE=/opt/kaidi/current/VERSION FINANCE_UPDATE_ENABLED=true -# Use either a stable direct asset base URL or the private Gitea latest-release API. +# Use either a stable direct asset base URL or the public Gitea latest-release API. UPDATE_RELEASE_BASE_URL= UPDATE_RELEASE_API_URL=https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest UPDATE_RELEASE_TOKEN=READ_ONLY_GITEA_TOKEN diff --git a/deploy/install-from-git.sh b/deploy/install-from-git.sh index 6784be8..e4c9c9f 100755 --- a/deploy/install-from-git.sh +++ b/deploy/install-from-git.sh @@ -5,20 +5,14 @@ umask 077 ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) INSTALLER="$ROOT/deploy/install.sh" -INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-33d4921bcad7ef2be12f20bc0512a5f6df4227ea6dd3b7e6225164077d9cd9e7} +INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-faf52cc902abbc2bd48571caee3f4207de50ce339b82ce88fe23f9c4ce8f89ef} RELEASE_API_URL=${KAIDI_RELEASE_API_URL:-https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest} PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9} TOKEN_FILE=${KAIDI_RELEASE_TOKEN_FILE:-} -TEMP_TOKEN_FILE= log() { printf '[kaidi-git-install] %s\n' "$*"; } die() { printf '[kaidi-git-install] ERROR: %s\n' "$*" >&2; exit 1; } -cleanup() { - [ -z "$TEMP_TOKEN_FILE" ] || rm -f "$TEMP_TOKEN_FILE" -} -trap cleanup EXIT - [ -f "$INSTALLER" ] || die "deploy/install.sh is missing; run this command from the Kaidi Git checkout" command -v sha256sum >/dev/null 2>&1 || die "sha256sum is required" [[ "$INSTALLER_SHA256" =~ ^[0-9A-Fa-f]{64}$ ]] || die "The installer SHA-256 is invalid" @@ -29,25 +23,6 @@ normalized_installer_sha256=$(printf '%s' "$INSTALLER_SHA256" | tr '[:upper:]' ' [ "$normalized_actual_installer_sha256" = "$normalized_installer_sha256" ] \ || die "deploy/install.sh does not match the trusted SHA-256 for this Git tag" -if [ -z "$TOKEN_FILE" ]; then - TOKEN_FILE=$(mktemp) - TEMP_TOKEN_FILE=$TOKEN_FILE - chmod 0600 "$TOKEN_FILE" - if [ -t 0 ]; then - read -rsp 'Gitea read-only token: ' token - printf '\n' - printf '%s' "$token" > "$TOKEN_FILE" - unset token - else - die "Set KAIDI_RELEASE_TOKEN_FILE or run interactively to enter the Gitea read-only token" - fi -fi - -[ -f "$TOKEN_FILE" ] && [ ! -L "$TOKEN_FILE" ] || die "The Gitea token file must be a regular file" -token_size=$(wc -c < "$TOKEN_FILE" | tr -d '[:space:]') -[ "$token_size" -gt 0 ] && [ "$token_size" -le 512 ] || die "The Gitea token file must contain 1 to 512 bytes" -LC_ALL=C grep -q '[[:cntrl:]]' "$TOKEN_FILE" && die "The Gitea token file contains invalid control characters" - root_command=(env) if [ "$(id -u)" -ne 0 ]; then command -v sudo >/dev/null 2>&1 || die "sudo is required when not running as root" @@ -56,10 +31,17 @@ fi install_env=( "KAIDI_RELEASE_API_URL=$RELEASE_API_URL" - "KAIDI_RELEASE_TOKEN_FILE=$TOKEN_FILE" "KAIDI_RELEASE_PUBLIC_KEY_SHA256=$PUBLIC_KEY_SHA256" ) +if [ -n "$TOKEN_FILE" ]; then + [ -f "$TOKEN_FILE" ] && [ ! -L "$TOKEN_FILE" ] || die "The Gitea token file must be a regular file" + token_size=$(wc -c < "$TOKEN_FILE" | tr -d '[:space:]') + [ "$token_size" -gt 0 ] && [ "$token_size" -le 512 ] || die "The Gitea token file must contain 1 to 512 bytes" + LC_ALL=C grep -q '[[:cntrl:]]' "$TOKEN_FILE" && die "The Gitea token file contains invalid control characters" + install_env+=("KAIDI_RELEASE_TOKEN_FILE=$TOKEN_FILE") +fi + KAIDI_REINSTALL=${KAIDI_REINSTALL:-} KAIDI_SETUP_WIZARD=${KAIDI_SETUP_WIZARD:-} KAIDI_DB_URL=${KAIDI_DB_URL:-} diff --git a/deploy/install.sh b/deploy/install.sh index 844253c..1345dd4 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -12,9 +12,9 @@ RELEASE_API_URL=${KAIDI_RELEASE_API_URL:-https://git.awaioi.com/api/v1/repos/ERP RELEASE_TOKEN=${KAIDI_RELEASE_TOKEN:-} RELEASE_TOKEN_FILE=${KAIDI_RELEASE_TOKEN_FILE:-} HEALTH_URL=${KAIDI_HEALTH_URL:-http://127.0.0.1:18080/actuator/health} -TRUSTED_PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-} +TRUSTED_PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9} REINSTALL=${KAIDI_REINSTALL:-false} -SETUP_WIZARD=${KAIDI_SETUP_WIZARD:-false} +SETUP_WIZARD=${KAIDI_SETUP_WIZARD:-true} WORK_DIR= RELEASE_AUTH_HEADER_FILE= INSTALL_TRANSACTION_ARMED=false @@ -24,8 +24,6 @@ BACKUP_DIR= JAVA_STAGED_DIR= JAVA_PREVIOUS_DIR= JAVA_ACTIVATED=false -CREATED_DB_CONTAINER=false -CREATED_DB_VOLUME=false log() { printf '[kaidi-install] %s\n' "$*"; } die() { printf '[kaidi-install] ERROR: %s\n' "$*" >&2; exit 1; } @@ -160,18 +158,6 @@ install_packages() { fi } -install_docker() { - command -v docker >/dev/null 2>&1 && return - if command -v apt-get >/dev/null 2>&1; then - apt-get install -y -qq docker.io - elif command -v dnf >/dev/null 2>&1; then - dnf install -y docker - else - yum install -y docker - fi - systemctl enable --now docker -} - azul_arch() { case "$(uname -m)" in x86_64|amd64) printf x86 ;; @@ -312,48 +298,7 @@ configure_database() { fi [ "$REINSTALL" != true ] \ || die "KAIDI_REINSTALL needs database credentials in the existing installation or command environment" - case "$(uname -m)" in - i386|i486|i586|i686) - die "32-bit Linux needs an external MySQL 8.4 database; set KAIDI_DB_URL, KAIDI_DB_USERNAME, and KAIDI_DB_PASSWORD" - ;; - esac - install_docker - DB_USERNAME=kaidi - DB_PASSWORD=$(random_secret) - DB_ROOT_PASSWORD=$(random_secret) - ! docker inspect kaidi-finance-mysql >/dev/null 2>&1 \ - || die "Docker container kaidi-finance-mysql already exists; provide explicit database parameters" - ! docker volume inspect kaidi_mysql_data >/dev/null 2>&1 \ - || die "Docker volume kaidi_mysql_data already exists; provide explicit database parameters" - docker volume create kaidi_mysql_data >/dev/null - CREATED_DB_VOLUME=true - docker run -d \ - --name kaidi-finance-mysql \ - --restart unless-stopped \ - -e MYSQL_DATABASE=kaidi_finance \ - -e MYSQL_USER="$DB_USERNAME" \ - -e MYSQL_PASSWORD="$DB_PASSWORD" \ - -e MYSQL_ROOT_PASSWORD="$DB_ROOT_PASSWORD" \ - -e TZ=Asia/Shanghai \ - -p 127.0.0.1:3307:3306 \ - -v kaidi_mysql_data:/var/lib/mysql \ - --health-cmd='mysqladmin ping -h localhost -uroot -p$MYSQL_ROOT_PASSWORD --silent' \ - --health-interval=5s --health-timeout=5s --health-retries=30 \ - mysql:8.4 \ - --character-set-server=utf8mb4 \ - --collation-server=utf8mb4_0900_ai_ci \ - --default-time-zone=+00:00 >/dev/null - CREATED_DB_CONTAINER=true - local attempts=0 health - while [ "$attempts" -lt 60 ]; do - health=$(docker inspect --format '{{.State.Health.Status}}' kaidi-finance-mysql 2>/dev/null || true) - [ "$health" = healthy ] && break - [ "$health" = unhealthy ] && die "MySQL container failed its health check" - attempts=$((attempts + 1)) - sleep 2 - done - [ "$health" = healthy ] || die "MySQL did not become ready" - DB_URL='jdbc:mysql://127.0.0.1:3307/kaidi_finance?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&serverTimezone=UTC' + die "An external MySQL 8.4 database is required; set KAIDI_DB_URL, KAIDI_DB_USERNAME, and KAIDI_DB_PASSWORD" } database_host() { @@ -599,14 +544,6 @@ cleanup() { && [ "$INSTALL_COMMITTED" != true ]; then rollback_install || log "ERROR: rollback was incomplete; inspect systemd and Nginx state" fi - if [ "$result" -ne 0 ] && [ "$INSTALL_COMMITTED" != true ] \ - && [ "$CREATED_DB_CONTAINER" = true ]; then - docker rm -f kaidi-finance-mysql >/dev/null 2>&1 || true - fi - if [ "$result" -ne 0 ] && [ "$INSTALL_COMMITTED" != true ] \ - && [ "$CREATED_DB_VOLUME" = true ]; then - docker volume rm kaidi_mysql_data >/dev/null 2>&1 || true - fi [ -z "$WORK_DIR" ] || rm -rf "$WORK_DIR" exit "$result" } @@ -755,7 +692,7 @@ write_env_file "$CONFIG_ROOT/update.env" \ KAIDI_UPDATE_STATE_ROOT "$UPDATE_STATE_ROOT" \ KAIDI_SERVICE_NAME kaidi-finance.service \ KAIDI_HEALTH_URL "$HEALTH_URL" \ - KAIDI_DB_CONTAINER "${KAIDI_DB_CONTAINER:-kaidi-finance-mysql}" \ + KAIDI_DB_CONTAINER "${KAIDI_DB_CONTAINER:-}" \ KAIDI_DB_HOST "$(database_host)" \ KAIDI_DB_PORT "$(database_port)" \ KAIDI_DB_NAME "$(database_name)" \ diff --git a/docs/财务系统开发交付总方案.md b/docs/财务系统开发交付总方案.md index 0522300..03cfe0e 100644 --- a/docs/财务系统开发交付总方案.md +++ b/docs/财务系统开发交付总方案.md @@ -1,10 +1,10 @@ # 财务项目基础系统开发交付总方案 > 文档编号:KD-FIN-DELIVERY-001
-> 版本:V3.25(Preview.8 首次安装向导与部署收口)
+> 版本:V3.26(Preview.9 公共 Release 一键安装)
> 状态:第一版 Preview 候选包已具备 22 页功能框架、核心业务链和可部署制品;PAGE-13/14 已通过模块验收,PAGE-12/16/17/18/19/20/21/22 为 `IMPLEMENTED_PENDING_ACCEPTANCE`,PAGE-15 正式业务矩阵仍为 `IN_PROGRESS`;整套 R1 尚未达到第 12.9 节 Definition of Done,不得把 Preview 上线等同于甲方最终验收
> 编制依据:2026-08-05 腾讯会议转写、14 张 OA 表单、财务岗位职责资料、项目管理流程图
-> 最新项目决策:按既定 Java/TDesign 技术栈交付第一版 Preview;2026-08-17 已完成 PAGE-08 来源、记账、档案双向穿透,PAGE-20 审计稳定排序/筛选/脱敏导出、PAGE-21 权限与配置、PAGE-22 私有 Gitea 签名在线更新、OA-04/OA-06 动态附件矩阵、OA-06 财务终审付款投影、Release/SBOM/依赖门禁、Linux i386/i486/i586/i686 外部 MySQL 8.4.x 单命令安装、更新请求持久领取/中断恢复/数据库备份校验/回滚健康复核,以及隔离超级管理员全部功能权限和全局数据范围;本轮新增 PAGE-23 首次安装向导、无业务数据库启动上下文、MySQL 8.4 DDL/DML 预检、一次性安装码和安装状态锁定,数据库基线新增 `V072`。远端仓库已固定为 `https://git.awaioi.com/ERP-Team/kaidi.git`;配置 Gitea Actions 签名 Secret、公钥指纹 Variable、可用 runner 和生产只读 Token 后,推送符合 SemVer 的 `v*` tag 即生成 Release;PAGE-15 完整业务矩阵继续保持 `DRAFT`,须经 D-02/D-09 确认后才允许作为正式规则发布
+> 最新项目决策:按既定 Java/TDesign 技术栈交付第一版 Preview;2026-08-17 已完成 PAGE-08 来源、记账、档案双向穿透,PAGE-20 审计稳定排序/筛选/脱敏导出、PAGE-21 权限与配置、PAGE-22 公共 Gitea 签名在线更新、OA-04/OA-06 动态附件矩阵、OA-06 财务终审付款投影、Release/SBOM/依赖门禁、Linux i386/i486/i586/i686 外部 MySQL 8.4.x 单命令安装、更新请求持久领取/中断恢复/数据库备份校验/回滚健康复核,以及隔离超级管理员全部功能权限和全局数据范围;本轮新增 PAGE-23 首次安装向导、无业务数据库启动上下文、MySQL 8.4 DDL/DML 预检、一次性安装码和安装状态锁定,数据库基线新增 `V072`。远端仓库 `https://git.awaioi.com/ERP-Team/kaidi.git` 与 Release 均已公开;安装和在线更新默认不使用 Token,安装器内置发布公钥指纹,推送符合 SemVer 的 `v*` tag 后由带签名密钥的发布流程生成 Release;PAGE-15 完整业务矩阵继续保持 `DRAFT`,须经 D-02/D-09 确认后才允许作为正式规则发布
> 实际开发技术:Java 17 + Spring Boot 3.5.16 + MyBatis 3.0.5 + MySQL 8.4 + TDesign Vue Next Starter;前端仍只允许在现有 `frontend/` Starter 中原位增加菜单、页面、组件和真实功能 > 文档性质:本项目唯一 PRD、SPEC、开发任务书、测试验收与交付基线
> 文档用途:甲方需求确认、产品设计、开发拆分、测试验收和交付培训
@@ -72,14 +72,14 @@ | 超级管理员权限 | V071 将 `SYSTEM_ADMIN` 固定为隔离超级管理员,逐条授予全部 `iam_permission` 和无额度 `GLOBAL` scope,并由启动同步补齐后续权限;前端菜单、直链和按钮统一放行全部 PAGE-04~21。管理员可管理跨人员表单/导入/付款草稿,PAGE-15 增加仅管理员可见的“全部付款”,三类业务工作台显示跨人员项目、资金、档案和全部待办;审批实际处理人、SOD、状态机、资金、附件安全和审计继续强制执行 | | OA-06 付款投影 | V070 发布 OA-06 v2,新增生效合同、供应商和已确认应付稳定引用;财务终审在来源审批事务内生成 `OFFLINE_PENDING` 付款,执行付款检查、冻结项目资金并追加资金流水。余额不足等投影失败时来源状态、审批任务、付款、资金控制和流水整体回滚;不产生银行付款指令,不接网银、U 盾或第三方平台 | | OA 动态附件矩阵 | OA-04/OA-06 的条件附件行支持 TDesign 布尔开关、长文本、不涉及原因和嵌套文件上传;仅 `required=true` 的行要求文件或原因,可选空行不阻断。上传状态按字段和表格单元隔离,上传期间锁定刷新、保存、校验和提交;更新请求不再携带创建专用字段,校验前自动保存当前草稿 | -| 本轮运行验收 | 最近完整后端 MySQL 8.4/Testcontainers 基线 `213/213`,Failures/Errors/Skipped 均为 0,迁移基线扩展至 `V072`;本轮新增 setup context smoke、安装器/包装器夹具和 PAGE-23 Playwright 回归;最新签名候选为 `1.0.0-preview.8`,正式 Release 仍须在干净 Git tag 上由 Gitea Actions 重新生成安装器摘要 | +| 本轮运行验收 | 最近完整后端 MySQL 8.4/Testcontainers 基线 `213/213`,Failures/Errors/Skipped 均为 0,迁移基线扩展至 `V072`;本轮新增 setup context smoke、安装器/包装器夹具和 PAGE-23 Playwright 回归;`1.0.0-preview.8` 已发布 9 个验签资产,Preview.9 进一步将公共仓库安装入口精简为单行 `curl | sudo bash` | | 第一版 Preview | PAGE-01~22 页面和菜单框架齐备,真实前后端核心链保持可用;Preview 上线与整套 R1 最终验收分开计量 | | 来源表格校验 | V068 为 14 类 OA 的 33 个 TABLE 字段补齐嵌套类型、必填和文件引用规则;V069 保留 OA-02 v1 历史账户表格契约,避免 v2 字段追溯污染 | | PAGE-20 审计 | 增加不可变事件序号、四种稳定排序、URL 查询状态、同排序 CSV 导出、脱敏详情及 OpenAPI `422` 参数门禁 | -| PAGE-22 在线更新 | 更新源固定为私有 Gitea Latest Release API;系统管理员点击“获取更新”后自动检查并下载,验签缓存进入 `READY` 后显示“立即重启”。安装健康检查成功后页面从 10 秒倒计时自动刷新;刷新或短暂断线发生在安装中时恢复轮询。后台只写固定结构请求,由 root oneshot 服务验签、验哈希、备份、切换和回滚,不接受页面传入地址、Token 或命令 | +| PAGE-22 在线更新 | 更新源固定为公共 Gitea Latest Release API,默认不使用 Token;系统管理员点击“获取更新”后自动检查并下载,验签缓存进入 `READY` 后显示“立即重启”。安装健康检查成功后页面从 10 秒倒计时自动刷新;刷新或短暂断线发生在安装中时恢复轮询。后台只写固定结构请求,由 root oneshot 服务验签、验哈希、备份、切换和回滚,不接受页面传入地址、Token 或命令 | | Release 工程 | `.gitea/workflows/release.yml` 监听严格 SemVer `v*` tag;Maven `revision`、npm、JAR、前后端 SBOM、签名 manifest 与 tag 必须同版本。工作流执行 Java/前端/OpenAPI/Playwright/依赖审计,使用至少 3072 位 RSA 密钥生成并独立验收恰好 9 个资产;先创建不可见草稿、逐项上传并核对名称/大小,最后发布为可被 `/releases/latest` 读取的正式 Release | | Linux 32 位 | i386/i486/i586/i686 下载 Java 17 i686 JRE;JRE 元数据和归档均使用仅允许 HTTPS/TLS 1.2 及以上的受限下载器;由于 MySQL 8.4 无对应服务端镜像,要求预置外部 MySQL 8.4.x,curl 安装只开启 `/setup` 向导,数据库密码在浏览器中提交并完成连接、版本及 DDL/DML 预检 | -| 更新可靠性 | 下载和安装拆为两个持久动作;下载阶段不停止业务服务,安装阶段只接受同版本 `READY` 缓存并重新验签。私有 Gitea Token 仅从权限为 `0600` 的 root 配置/临时文件读取,不进入 `curl` 参数、页面、状态或日志,且只允许同源 API/资产使用;请求原子领取到持久 `processing`,systemd 限制失败重试;`mysqldump`、新旧版本健康和回滚均有独立门禁,私有 Gitea、跨来源拒绝、成功、健康回滚、下载失败、备份失败和不安全请求夹具纳入 CI | +| 更新可靠性 | 下载和安装拆为两个持久动作;下载阶段不停止业务服务,安装阶段只接受同版本 `READY` 缓存并重新验签。公共 Gitea 默认不使用 Token;如改接私有镜像,Token 仅从权限为 `0600` 的 root 配置/临时文件读取,不进入 `curl` 参数、页面、状态或日志,且只允许同源 API/资产使用;请求原子领取到持久 `processing`,systemd 限制失败重试;`mysqldump`、新旧版本健康和回滚均有独立门禁,跨来源拒绝、成功、健康回滚、下载失败、备份失败和不安全请求夹具纳入 CI | | 阶段口径 | 把“开发前冻结”更新为“R1 开发中”;明确模块级验收与整套 R1 交付是两个层级,避免一页完成后虚报整套系统完成 | | PAGE-13 页面 | 完成合同执行汇总、筛选、服务端分页、来源下钻、CSV 导出、合同详情及变更/结算/应付操作;页面仅使用现有 TDesign Starter 和 TDesign 组件 | | PAGE-13 后端 | 新增 `contractcost:payable:create`,项目经理可登记但不可确认;同人登记/确认返回 `422 SOD_VIOLATION`;应付必须关联已批准 OA-04,结算必须关联已批准 OA-11,同一来源不得重复消费 | @@ -1006,7 +1006,7 @@ PAGE-21 只向超级管理员开放。超级管理员账号不得混授普通业 更新固定分两阶段。管理员点击“获取更新”后,前端先检查最新版本,再自动提交 `DOWNLOAD` 请求;root oneshot 从固定 Gitea Latest Release API 下载 manifest、签名和应用包,验签/验哈希后写入 root-only 版本缓存并进入 `READY`,期间当前业务版本继续运行。进入 `READY` 后页面显示“立即重启”,点击后只允许安装同版本缓存,并在切换前重新验证。健康检查成功后页面显示 10 秒倒计时并自动刷新;若页面在 `INSTALL_QUEUED/BACKING_UP/INSTALLING/RUNNING` 时刷新或短暂断线,重新进入后自动恢复 3 秒轮询。失败时保留明确状态并恢复已验证的上一版本。 -页面只提交目标版本和原因,不接收下载地址、Token、脚本或任意命令。Gitea 只读 Token 仅存在于权限为 `0600` 的 root 环境文件和临时 Header 文件,不进入进程参数、DTO、状态 JSON、审计参数或页面响应;API 和资产必须同 scheme、host、port,认证请求不跟随跨来源重定向。文件锁和 inbox/processing 双路径防止并发覆盖,独立 root oneshot 执行 RSA/SHA-256 校验、可验证数据库备份、持久事务、应用与运维文件原子切换、新旧版本健康检查和失败回滚。 +页面只提交目标版本和原因,不接收下载地址、Token、脚本或任意命令。公共 Gitea 默认不配置 Token;如改接私有镜像,只读 Token 仅存在于权限为 `0600` 的 root 环境文件和临时 Header 文件,不进入进程参数、DTO、状态 JSON、审计参数或页面响应;API 和资产必须同 scheme、host、port,认证请求不跟随跨来源重定向。文件锁和 inbox/processing 双路径防止并发覆盖,独立 root oneshot 执行 RSA/SHA-256 校验、可验证数据库备份、持久事务、应用与运维文件原子切换、新旧版本健康检查和失败回滚。 ### 8.23 页面通用要求 diff --git a/scripts/test-git-install-fixture.sh b/scripts/test-git-install-fixture.sh index 72f4ab5..f2fb48f 100755 --- a/scripts/test-git-install-fixture.sh +++ b/scripts/test-git-install-fixture.sh @@ -30,7 +30,11 @@ else [ "$KAIDI_DB_USERNAME" = 'kaidi' ] [ "$KAIDI_DB_PASSWORD" = 'fixture-password' ] fi -[ "$(cat "$KAIDI_RELEASE_TOKEN_FILE")" = 'fixture-read-token' ] +if [ "${EXPECT_TOKEN:-false}" = true ]; then + [ "$(cat "$KAIDI_RELEASE_TOKEN_FILE")" = 'fixture-read-token' ] +else + [ -z "${KAIDI_RELEASE_TOKEN_FILE:-}" ] +fi printf 'fixture-installer: PASS\n' SH @@ -40,7 +44,6 @@ chmod 0600 "$FIXTURE/token" installer_sha256=$(sha256sum "$FIXTURE/repo/deploy/install.sh" | awk '{print $1}') PATH="$FIXTURE/bin:$PATH" \ -KAIDI_RELEASE_TOKEN_FILE="$FIXTURE/token" \ KAIDI_INSTALLER_SHA256="$installer_sha256" \ KAIDI_RELEASE_API_URL=https://git.example.test/api/v1/repos/TEAM/REPO/releases/latest \ KAIDI_RELEASE_PUBLIC_KEY_SHA256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ @@ -50,6 +53,14 @@ KAIDI_DB_PASSWORD=fixture-password \ "$FIXTURE/repo/deploy/install-from-git.sh" PATH="$FIXTURE/bin:$PATH" \ +KAIDI_INSTALLER_SHA256="$installer_sha256" \ +KAIDI_RELEASE_API_URL=https://git.example.test/api/v1/repos/TEAM/REPO/releases/latest \ +KAIDI_RELEASE_PUBLIC_KEY_SHA256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ +KAIDI_SETUP_WIZARD=true \ + "$FIXTURE/repo/deploy/install-from-git.sh" + +PATH="$FIXTURE/bin:$PATH" \ +EXPECT_TOKEN=true \ KAIDI_RELEASE_TOKEN_FILE="$FIXTURE/token" \ KAIDI_INSTALLER_SHA256="$installer_sha256" \ KAIDI_RELEASE_API_URL=https://git.example.test/api/v1/repos/TEAM/REPO/releases/latest \ @@ -58,7 +69,6 @@ KAIDI_SETUP_WIZARD=true \ "$FIXTURE/repo/deploy/install-from-git.sh" if PATH="$FIXTURE/bin:$PATH" \ - KAIDI_RELEASE_TOKEN_FILE="$FIXTURE/token" \ KAIDI_INSTALLER_SHA256=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \ "$FIXTURE/repo/deploy/install-from-git.sh" > "$FIXTURE/hash-mismatch.log" 2>&1; then printf 'Mismatched installer SHA-256 was accepted\n' >&2 diff --git a/scripts/test-install-fixture.sh b/scripts/test-install-fixture.sh index cdbe344..b14c90e 100755 --- a/scripts/test-install-fixture.sh +++ b/scripts/test-install-fixture.sh @@ -199,7 +199,19 @@ grep -Fq 'java_sha256=$(jq -er' "$ROOT/deploy/install.sh" \ grep -Fq '[ "$actual_sha256" = "$java_sha256" ]' "$ROOT/deploy/install.sh" \ || fail 'installer no longer verifies the Java runtime SHA-256' grep -Fq 'https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest' "$ROOT/deploy/install.sh" \ - || fail 'installer default release source is not the private Gitea latest API' + || fail 'installer default release source is not the public Gitea latest API' +# shellcheck disable=SC2016 # Match literal installer source. +grep -Fq 'TRUSTED_PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9}' "$ROOT/deploy/install.sh" \ + || fail 'installer does not pin the default release public-key fingerprint' +# shellcheck disable=SC2016 # Match literal installer source. +grep -Fq 'SETUP_WIZARD=${KAIDI_SETUP_WIZARD:-true}' "$ROOT/deploy/install.sh" \ + || fail 'installer does not enable the first-run setup wizard by default' +! grep -Fq 'docker run' "$ROOT/deploy/install.sh" \ + || fail 'installer must not create a MySQL container' +! grep -Fq 'install_docker' "$ROOT/deploy/install.sh" \ + || fail 'installer must not install Docker or MySQL automatically' +grep -Fq 'An external MySQL 8.4 database is required' "$ROOT/deploy/install.sh" \ + || fail 'installer does not require an operator-managed external MySQL database' grep -Fq 'KAIDI_RELEASE_TOKEN_FILE' "$ROOT/deploy/install.sh" \ || fail 'installer no longer supports a protected initial Gitea token file' grep -Fq 'KAIDI_SETUP_WIZARD' "$ROOT/deploy/install.sh" \ @@ -215,7 +227,7 @@ grep -Fq 'EnvironmentFile=-/var/lib/kaidi/setup/application.env' \ # shellcheck disable=SC2016 # Match the literal installer source. grep -Fq 'chown root:kaidi "$UPDATE_STATE_ROOT"' "$ROOT/deploy/install.sh" \ || fail 'update state parent is not group-accessible to the application user' -grep -Fq 'KAIDI_SETUP_WIZARD=true' "$ROOT/README.md" \ - || fail 'README does not document the setup-wizard install path' +grep -Fq 'install.sh | sudo bash' "$ROOT/README.md" \ + || fail 'README does not document the public one-line setup-wizard install path' -printf 'Install configuration, private Gitea, i686, setup wizard, and MySQL 8.4 fixtures passed\n' +printf 'Install configuration, public Gitea, optional private token, i686, setup wizard, and MySQL 8.4 fixtures passed\n'