CaiWuXiTong/deploy/local-init.sh
chenxi 26556a9169 Add warehouse short names to inventory report
Restore warehouse name mapping ledger and inventory short-name column.
2026-09-28 16:33:38 +08:00

451 lines
15 KiB
Bash
Raw 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.

#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
compose_file="docker-compose.local.yml"
environment_file="runtime/.env"
credential_file="runtime/bootstrap-credentials.env"
app_image="jiangling_akaunting:local"
build_mode="auto"
for argument in "$@"; do
case "$argument" in
--build)
build_mode="build"
;;
--skip-build)
build_mode="skip"
;;
--help|-h)
echo "用法:bash deploy/local-init.sh [--build|--skip-build]"
echo " --build 强制重新构建本地开发镜像"
echo " --skip-build 只启动已有镜像;镜像不存在时停止"
exit 0
;;
*)
echo "错误:未知参数 $argument" >&2
exit 1
;;
esac
done
read_env_value() {
local key="$1"
sed -n "s/^${key}=//p" "$environment_file" \
| tail -n 1 \
| tr -d '\r' \
| sed 's/^"//; s/"$//'
}
set_env_value() {
local key="$1"
local value="$2"
local temporary
temporary="${environment_file}.tmp.$$"
awk -v key="$key" -v value="$value" '
BEGIN { found = 0 }
index($0, key "=") == 1 {
print key "=" value
found = 1
next
}
{ print }
END {
if (!found) {
print key "=" value
}
}
' "$environment_file" > "$temporary"
cp "$temporary" "$environment_file"
rm -f "$temporary"
chmod 600 "$environment_file" 2>/dev/null || true
}
calculate_local_image_fingerprint() {
{
printf '%s\0' \
.dockerignore \
composer.json \
composer.lock \
package.json \
package-lock.json \
webpack.mix.js \
tailwind.config.js \
presets.js \
safelist.txt \
manifest.json \
serviceworker.js \
deploy/Dockerfile.local \
deploy/apache-vhost.conf \
deploy/php.ini \
deploy/php-local.ini \
deploy/entrypoint.sh \
deploy/local-healthcheck.sh
find resources/assets resources/views resources/lang -type f -print0
find public -type f \
! -path 'public/js/*' \
! -path 'public/vendor/*' \
! -path 'public/css/app.css' \
! -name 'mix-manifest.json' \
-print0
} \
| sort -z \
| xargs -0 sha256sum \
| sha256sum \
| awk '{ print $1 }'
}
image_has_required_assets() {
docker run --rm --entrypoint sh "$app_image" -lc '
for required_asset in \
/var/www/html/public/js/manifest.js \
/var/www/html/public/js/vendor.js \
/var/www/html/public/css/app.css \
/var/www/html/public/mix-manifest.json \
/var/www/html/public/vendor/apexcharts/apexcharts.min.js \
/var/www/html/public/vendor/js-cookie/js.cookie.js
do
test -s "$required_asset" || exit 1
done
' >/dev/null 2>&1
}
echo "==> 工作目录: $(pwd)"
if ! command -v docker >/dev/null 2>&1; then
echo "错误:未找到 Docker,请先启动 Docker Desktop。" >&2
exit 1
fi
if ! docker version >/dev/null 2>&1; then
echo "错误:Docker 尚未就绪,请先启动 Docker Desktop。" >&2
exit 1
fi
if [ ! -f "$environment_file" ]; then
mkdir -p runtime
app_key="base64:$(head -c 32 /dev/urandom | base64 | tr -d '\r\n')"
cat > "$environment_file" <<EOF
APP_NAME=CaiWuXiTong
APP_ENV=local
APP_DEBUG=true
APP_URL=http://127.0.0.1:18088
APP_KEY=$app_key
APP_INSTALLED=false
APP_LOCALE=zh-CN
APP_FALLBACK_LOCALE=en-GB
APP_TIMEZONE=Asia/Shanghai
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=akaunting
DB_USERNAME=akaunting
DB_PASSWORD=akaunting123
DB_PREFIX=ak_
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync
SYSTEM_DATABASE_MODE=bootstrap
UPSTREAM_STORED_CONFIG_ENABLED=false
UPSTREAM_LOCAL_CONFIG_ENABLED=false
EOF
chmod 600 "$environment_file" 2>/dev/null || true
echo "==> 已生成本机私有 runtime/.env(不会提交到 Git)。"
fi
image_exists=0
if docker image inspect "$app_image" >/dev/null 2>&1; then
image_exists=1
fi
local_image_fingerprint="$(calculate_local_image_fingerprint)"
stored_image_fingerprint=""
image_contract_ok=0
if [ "$image_exists" -eq 1 ]; then
stored_image_fingerprint="$(
docker image inspect "$app_image" \
--format '{{ index .Config.Labels "com.caiwu.local-build-fingerprint" }}' \
2>/dev/null \
|| true
)"
if image_has_required_assets; then
image_contract_ok=1
fi
fi
build_reason=""
if [ "$build_mode" = "build" ]; then
build_reason="已要求强制构建"
elif [ "$image_exists" -eq 0 ]; then
build_reason="本地镜像不存在"
elif [ "$image_contract_ok" -ne 1 ]; then
build_reason="现有镜像缺少必要静态资源"
elif [ "$stored_image_fingerprint" != "$local_image_fingerprint" ]; then
build_reason="前端、依赖或运行镜像配置已经变化"
fi
if [ "$build_mode" = "skip" ] && [ -n "$build_reason" ]; then
echo "错误:$build_reason,不能安全使用 --skip-build。" >&2
echo "请不带参数重新运行本脚本,由系统自动更新镜像。" >&2
exit 1
fi
if [ -n "$build_reason" ]; then
build_attempt=1
while [ "$build_attempt" -le 2 ]; do
local_image_fingerprint="$(calculate_local_image_fingerprint)"
echo "==> [1/5] 构建本地开发镜像:${build_reason}。"
docker compose -f "$compose_file" build \
--build-arg "LOCAL_BUILD_FINGERPRINT=$local_image_fingerprint" \
app
built_image_fingerprint="$(
docker image inspect "$app_image" \
--format '{{ index .Config.Labels "com.caiwu.local-build-fingerprint" }}'
)"
current_image_fingerprint="$(calculate_local_image_fingerprint)"
if [ "$current_image_fingerprint" != "$local_image_fingerprint" ]; then
if [ "$build_attempt" -lt 2 ]; then
echo " 构建期间检测到源码变化,自动按最新源码再构建一次。"
build_reason="源码在构建期间发生变化"
build_attempt=$((build_attempt + 1))
continue
fi
echo "错误:构建期间源码持续变化,请等待当前文件修改完成后重试。" >&2
exit 1
fi
if [ "$built_image_fingerprint" != "$local_image_fingerprint" ]; then
echo "错误:本地镜像版本标识与当前源码不一致。" >&2
exit 1
fi
if ! image_has_required_assets; then
echo "错误:本地镜像缺少必要静态资源,请查看上方构建日志。" >&2
exit 1
fi
break
done
else
echo "==> [1/5] 本地开发镜像与当前源码一致,直接复用。"
fi
# 共享快照只由生产中央节点刷新。先清理旧版 worker,避免 Compose 报孤儿
# 容器,也避免不同分支继续重复访问上游。
docker rm -f jiangling_snapshot_worker >/dev/null 2>&1 || true
echo "==> [2/5] 启动本地容器..."
# 先只启动数据库并确认 bootstrap 状态,再启动网页容器,避免旧的
# APP_INSTALLED=true 让 entrypoint 与首次安装命令并发迁移空数据库。
docker compose -f "$compose_file" up -d --no-build mysql
echo "==> [3/5] 等待临时 MySQL 就绪..."
mysql_ready=0
for i in $(seq 1 60); do
if docker compose -f "$compose_file" exec -T mysql \
mysqladmin ping -h 127.0.0.1 -u akaunting -pakaunting123 >/dev/null 2>&1; then
mysql_ready=1
echo " 临时 MySQL 已就绪。"
break
fi
echo " 等待中... ($i/60)"
sleep 2
done
if [ "$mysql_ready" -ne 1 ]; then
echo "错误:临时 MySQL 未能在规定时间内启动。" >&2
exit 1
fi
db_host="$(read_env_value DB_HOST)"
app_installed="$(read_env_value APP_INSTALLED | tr '[:upper:]' '[:lower:]')"
if [ "$db_host" = "mysql" ]; then
set_env_value SYSTEM_DATABASE_MODE bootstrap
set_env_value UPSTREAM_STORED_CONFIG_ENABLED false
set_env_value UPSTREAM_LOCAL_CONFIG_ENABLED false
db_prefix="$(read_env_value DB_PREFIX)"
if ! [[ "$db_prefix" =~ ^[A-Za-z0-9_]{0,50}$ ]]; then
echo "错误:本机引导库表前缀无效,已停止初始化。" >&2
exit 1
fi
bootstrap_table_count="$(
docker compose -f "$compose_file" exec -T mysql \
mysql -N -h 127.0.0.1 -u akaunting -pakaunting123 akaunting \
-e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'akaunting' AND table_name IN ('${db_prefix}users', '${db_prefix}settings', '${db_prefix}companies')" \
2>/dev/null \
| tr -d '\r' \
|| true
)"
bootstrap_any_table_count="$(
docker compose -f "$compose_file" exec -T mysql \
mysql -N -h 127.0.0.1 -u akaunting -pakaunting123 akaunting \
-e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'akaunting'" \
2>/dev/null \
| tr -d '\r' \
|| true
)"
if [ "$bootstrap_table_count" != "3" ]; then
app_installed="false"
set_env_value APP_INSTALLED false
echo "==> 检测到本机引导库尚未安装或数据卷已重建,将自动执行 bootstrap 初始化。"
fi
if [ "$app_installed" = "false" ] && [ "${bootstrap_any_table_count:-0}" -gt 0 ]; then
echo "==> 清理上次中断后留下的本机 bootstrap 半安装表..."
docker compose -f "$compose_file" exec -T mysql \
mysql -h 127.0.0.1 -u root -prootpass \
-e "DROP DATABASE IF EXISTS akaunting; CREATE DATABASE akaunting CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; GRANT ALL PRIVILEGES ON akaunting.* TO 'akaunting'@'%'; FLUSH PRIVILEGES;"
fi
fi
# 已保存的团队密码可解锁最新端点;服务器迁移不应要求每台开发机手改 .env。
# 无有效团队密码的首次引导保持原状,仍通过网页完成一次接入。
echo "==> 自动对齐本机团队共享连接..."
if docker compose -f "$compose_file" run --rm --no-deps -T --entrypoint sh app -c \
'jiangling-entrypoint true && exec runuser -u www-data -- php artisan system-database:connect-team --no-interaction --no-ansi'; then
team_connect_status=0
else
team_connect_status=$?
fi
if [ "$team_connect_status" -ne 0 ] && [ "$team_connect_status" -ne 13 ]; then
echo " 最新端点自动对齐未完成,验证当前已保存的共享连接..."
if docker compose -f "$compose_file" run --rm --no-deps -T --entrypoint sh app -c \
'jiangling-entrypoint true && exec runuser -u www-data -- php artisan system-database:verify --no-interaction --no-ansi'; then
echo " 当前共享连接仍可用,保留原配置并继续初始化。"
else
echo "错误:最新端点和当前共享连接均不可用;原配置已保留,请核对网络或首次输入团队数据库密码。" >&2
exit "$team_connect_status"
fi
fi
db_host="$(read_env_value DB_HOST)"
app_installed="$(read_env_value APP_INSTALLED | tr '[:upper:]' '[:lower:]')"
docker compose -f "$compose_file" up -d --no-build app frontend
# 容器可能已经存在而没有重新执行 entrypoint。显式复用同一权限初始化逻辑,
# 修复历史 root CLI 遗留目录;该命令本身不运行 Artisan。
docker compose -f "$compose_file" exec -T -u root app jiangling-entrypoint true
if [ "$db_host" = "mysql" ] && [ "$app_installed" != "true" ] && [ "$app_installed" != "1" ]; then
echo "==> [4/5] 初始化仅供首次登录使用的本机引导库..."
if [ ! -f "$credential_file" ]; then
umask 077
bootstrap_password="Cw!$(od -An -N16 -tx1 /dev/urandom | tr -d ' \r\n')"
cat > "$credential_file" <<EOF
BOOTSTRAP_EMAIL=local-bootstrap@caiwu.test
BOOTSTRAP_PASSWORD=$bootstrap_password
EOF
chmod 600 "$credential_file" 2>/dev/null || true
fi
# 文件由脚本生成,值只包含固定邮箱和受限字符集的随机密码。
# shellcheck disable=SC1090
source "$credential_file"
docker compose -f "$compose_file" exec -T -u www-data app \
php artisan install --no-interaction \
--db-host=mysql \
--db-port=3306 \
--db-name=akaunting \
--db-username=akaunting \
--db-password=akaunting123 \
--db-prefix=ak_ \
--company-name="本地引导环境" \
--company-email="$BOOTSTRAP_EMAIL" \
--admin-email="$BOOTSTRAP_EMAIL" \
--admin-password="$BOOTSTRAP_PASSWORD" \
--locale=zh-CN
else
echo "==> [4/5] 当前环境已经初始化,跳过本机引导库安装。"
fi
echo "==> 自动检查本机临时引导库结构..."
docker compose -f "$compose_file" exec -T -u www-data app php artisan optimize:clear >/dev/null
echo "==> 自动检查供应商应付本地维护库结构..."
docker compose -f "$compose_file" exec -T -u www-data app \
php artisan migrate --database=supplier_payable_maintenance \
--path=database/migrations/2026_09_17_000000_create_supplier_payable_override_tables.php \
--force --no-ansi
docker compose -f "$compose_file" exec -T -u www-data app \
php artisan local-bootstrap:update --no-ansi
echo "==> [5/5] 验证安装状态、数据库和管理员权限..."
ready=0
for i in $(seq 1 30); do
if docker compose -f "$compose_file" exec -T -u www-data app php artisan app:ready --quiet; then
ready=1
break
fi
sleep 2
done
if [ "$ready" -ne 1 ]; then
echo "错误:应用未通过就绪检查,请运行以下命令查看日志:" >&2
echo "docker compose -f $compose_file logs --tail=100 app" >&2
exit 1
fi
http_code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 15 \
http://127.0.0.1:18088/auth/login || true)"
if [ "$http_code" != "200" ]; then
echo "错误:登录页返回 HTTP ${http_code:-000}。" >&2
exit 1
fi
echo ""
echo "====================================================="
echo "本地网页可访问:http://127.0.0.1:18088"
# Docker healthy 只表示当前连接可用;共享接入必须由新 Laravel 进程实测,
# 不能根据 DB_HOST 不是 mysql 就宣称团队数据已连接。
if docker compose -f "$compose_file" exec -T -u www-data app \
php artisan system-database:verify --no-ansi; then
shared_status=0
else
shared_status=$?
fi
case "$shared_status" in
0)
echo "团队共享系统数据库验证通过,请使用其中属于自己的正式账号登录。"
;;
13)
echo "团队数据接入尚未完成;后台 worker 启动不代表数据已经同步。"
echo "临时登录凭据保存在本机私有文件:$credential_file"
echo "请登录后输入一次系统数据库密码,连接成功后改用团队正式账号登录。"
;;
*)
echo "团队共享系统数据库未通过验证;不要将 Docker healthy 当作接入成功。" >&2
echo "请让 Codex 根据上方失败阶段检查本机配置,保留现有配置和未提交代码。" >&2
exit "$shared_status"
;;
esac
echo "====================================================="