CaiWuXiTong/deploy/activate-release.sh

257 lines
14 KiB
Bash
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.

#!/bin/sh
set -eu
IMAGE_ARCHIVE="${1:?IMAGE_ARCHIVE is required}"
IMAGE_TAG="${2:?IMAGE_TAG is required}"
REMOTE_DIR="${3:-/opt/caiwuxitong}"
DEPLOY_SHA="${4:?DEPLOY_SHA is required}"
RUN_MIGRATIONS="${5:-0}"
KEEP_IMAGES="${6:-2}"
EXPECTED_PREVIOUS_SHA="${7:?Checked production revision is required; update the deployment scripts}"
DEPLOY_ACTOR="${8:-unknown}"
RELEASE_APP_URL="${9:-}"
RELEASE_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
. "$RELEASE_DIR/release-guard.sh"
IMAGE_NAME="caiwu_akaunting"
LOCK_FILE="$REMOTE_DIR/.deploy.lock"
cleanup() {
rm -f "$IMAGE_ARCHIVE"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if [ "$(id -u)" -ne 0 ]; then
echo "activate-release.sh must run as root" >&2
exit 1
fi
if [ ! -f "$REMOTE_DIR/runtime/.env" ]; then
echo "Missing $REMOTE_DIR/runtime/.env; run bootstrap-server.sh first" >&2
exit 1
fi
clickhouse_env="$REMOTE_DIR/runtime/clickhouse.env"
if [ ! -f "$clickhouse_env" ]; then
clickhouse_password="$(openssl rand -hex 24)"
umask 077
{
printf '%s\n' \
'CLICKHOUSE_DB=caiwu_business_analytics' \
'CLICKHOUSE_USER=caiwu_app' \
"CLICKHOUSE_PASSWORD=$clickhouse_password" \
'CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1' \
'BUSINESS_CLICKHOUSE_ENABLED=true' \
'BUSINESS_CLICKHOUSE_URL=http://clickhouse:8123' \
'BUSINESS_CLICKHOUSE_DATABASE=caiwu_business_analytics' \
'BUSINESS_CLICKHOUSE_USERNAME=caiwu_app' \
"BUSINESS_CLICKHOUSE_PASSWORD=$clickhouse_password"
} > "$clickhouse_env"
chmod 600 "$clickhouse_env"
fi
if ! grep -q '^CLICKHOUSE_PASSWORD=.' "$clickhouse_env" \
|| ! grep -q '^BUSINESS_CLICKHOUSE_PASSWORD=.' "$clickhouse_env"; then
echo "Incomplete $clickhouse_env; refusing to overwrite private ClickHouse credentials" >&2
exit 1
fi
if [ ! -f "$REMOTE_DIR/runtime/config-db.env" ]; then
echo "Missing $REMOTE_DIR/runtime/config-db.env; run bootstrap-server.sh first" >&2
exit 1
fi
if ! grep -q '^BUSINESS_STORAGE_DRIVER=qiniu$' "$REMOTE_DIR/runtime/qiniu-storage.env" 2>/dev/null \
|| ! grep -q '^QINIU_ACCESS_KEY=.' "$REMOTE_DIR/runtime/qiniu-storage.env" 2>/dev/null \
|| ! grep -q '^QINIU_SECRET_KEY=.' "$REMOTE_DIR/runtime/qiniu-storage.env" 2>/dev/null; then
echo "Private business storage runtime is incomplete" >&2
exit 1
fi
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
echo "Another CaiWuXiTong deployment is already running" >&2
exit 1
fi
RELEASE_STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
RELEASE_RESULT=blocked
trap 'release_status=$?; caiwu_record_release_result "$release_status"; cleanup' EXIT
# 容器替换会终止其中的同步进程;与生产刷新任务共享同一把锁。
exec 8>/run/caiwu-upstream-sync.lock
echo "Waiting for active upstream synchronization before replacing the application container"
if ! flock -w 1800 8; then
echo "Upstream synchronization is still running; application container was not replaced" >&2
exit 1
fi
PREVIOUS_IMAGE="$(docker inspect caiwu_app --format '{{.Config.Image}}' 2>/dev/null || true)"
caiwu_verify_release_base
RELEASE_RESULT=running
restore_previous_image() {
case "$PREVIOUS_IMAGE" in
"$IMAGE_NAME":*)
IMAGE_TAG="${PREVIOUS_IMAGE#*:}"
RUN_MIGRATIONS=0
export IMAGE_TAG RUN_MIGRATIONS
docker compose -f compose.jiangling.yml up -d app || true
;;
esac
}
docker load --input "$IMAGE_ARCHIVE"
rm -f "$IMAGE_ARCHIVE"
LOADED_SHA="$(docker image inspect "$IMAGE_NAME:$IMAGE_TAG" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')"
if [ "$LOADED_SHA" != "$DEPLOY_SHA" ]; then
echo "Image revision mismatch: expected $DEPLOY_SHA, got $LOADED_SHA" >&2
exit 1
fi
caiwu_install_release_files
cd "$REMOTE_DIR"
export IMAGE_TAG RUN_MIGRATIONS
# Pull the independent columnar runtime before migrations or app switching.
# A registry/network failure therefore leaves both the database and current app untouched.
docker compose -f compose.jiangling.yml pull clickhouse
if [ "$RUN_MIGRATIONS" = "1" ]; then
# 先创建独立业务镜像库并授权应用账号,再启动会执行迁移的新应用。
# 该脚本幂等;已有数据库只会补齐授权,不会覆盖业务数据。
docker compose -f compose.jiangling.yml up -d config_db
config_db_attempt=1
while [ "$config_db_attempt" -le 24 ]; do
if [ "$(docker inspect caiwu_config_db --format '{{.State.Health.Status}}' 2>/dev/null || true)" = "healthy" ]; then
break
fi
sleep 5
config_db_attempt=$((config_db_attempt + 1))
done
if [ "$(docker inspect caiwu_config_db --format '{{.State.Health.Status}}' 2>/dev/null || true)" != "healthy" ]; then
docker compose -f compose.jiangling.yml logs --tail 80 config_db >&2 || true
echo "Shared MariaDB did not become healthy before mirror database initialization" >&2
exit 1
fi
docker exec caiwu_config_db sh /docker-entrypoint-initdb.d/10-config-db-security.sh
fi
docker compose -f compose.jiangling.yml up -d --remove-orphans
clickhouse_healthy=0
clickhouse_attempt=1
while [ "$clickhouse_attempt" -le 24 ]; do
if [ "$(docker inspect caiwu_clickhouse --format '{{.State.Health.Status}}' 2>/dev/null || true)" = "healthy" ]; then
clickhouse_healthy=1
break
fi
sleep 5
clickhouse_attempt=$((clickhouse_attempt + 1))
done
if [ "$clickhouse_healthy" -ne 1 ]; then
docker compose -f compose.jiangling.yml logs --tail 120 clickhouse >&2 || true
restore_previous_image
echo "ClickHouse did not become healthy; previous application image was restored when available" >&2
exit 1
fi
healthy=0
attempt=1
while [ "$attempt" -le 30 ]; do
if curl -fsS -o /dev/null http://127.0.0.1:18088/serviceworker.js; then
healthy=1
break
fi
sleep 5
attempt=$((attempt + 1))
done
if [ "$healthy" -ne 1 ]; then
docker compose -f compose.jiangling.yml logs --tail 120 app >&2 || true
restore_previous_image
echo "Release failed health check; previous image was restored when available" >&2
exit 1
fi
if ! docker exec -u www-data caiwu_app php artisan storage:business-files:sync --require-qiniu; then
docker compose -f compose.jiangling.yml logs --tail 120 app >&2 || true
restore_previous_image
echo "Release failed private business storage verification; previous image was restored when available" >&2
exit 1
fi
if ! curl -fsS -o /dev/null http://127.0.0.1:18088/serviceworker.js; then
restore_previous_image
echo "Release failed after business storage activation; previous image was restored when available" >&2
exit 1
fi
printf '%s\n' "$DEPLOY_SHA" > "$REMOTE_DIR/.deployed-sha.tmp"
mv "$REMOTE_DIR/.deployed-sha.tmp" "$REMOTE_DIR/.deployed-sha"
# 只运行已登记的上游后台读取和 E 签宝状态轮询,不启用整个 Laravel
# 调度器,避免意外启动账单提醒等既有任务。上游读取共用一把锁,E 签宝单独加锁。
# 普通商品优先高频增量,并在配置的每周低峰窗口自动继续全量校准。
cat > /etc/cron.d/caiwu-jiansun-snapshots <<'EOF'
CRON_TZ=Asia/Shanghai
* * * * * root /usr/bin/flock -n /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-catalog --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-purchase-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan upstream:snapshots:refresh-all --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables --quiet' >/dev/null 2>&1
# 店铺经营报表按 15 分钟刷新;单轮覆盖 50000 行回看窗口,避免空闲时持续重读千万级账单。
7,22,37,52 * * * * root /usr/bin/flock -w 600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-store-report --max-batches=12 --quiet >/dev/null 2>&1
# 店铺候选平时仅读镜像变更;夜间全量核对历史名称。冷页面不扫描订单宽表。
45 * * * * root /usr/bin/flock -w 600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan order-data:warm-shop-candidates --quiet >/dev/null 2>&1
45 2 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh order-data:warm-shop-candidates --full --quiet >/dev/null 2>&1
# 页面字段自动跟随上游:先刷新数据字典,再把上游新增字段补进镜像并建列,页面自动多出这一列。
40 6 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:follow-upstream-columns --quiet >/dev/null 2>&1
0 0 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-third-party-warehouse-storage --max-batches=0 --quiet && /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:warm-report-caches --quiet' >/dev/null 2>&1
0 0 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-jushuitan-cost-price-history --quiet >/dev/null 2>&1
# 镜像价格观测独立保存,按索引分批续传,不替代正式操作历史。
3,13,23,33,43,53 * * * * root /usr/bin/flock -n /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-jushuitan-cost-price-history --source=snapshot --page-size=500 --max-pages=20 --quiet >/dev/null 2>&1
0 5 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-purchase-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables operations_replica.rpt.jst_Orders --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-ding-online-daifa-price --quiet' >/dev/null 2>&1
# 店铺违规信息预警:每天 05:30 把副本库抓取结果追平到统一表(本地补充字段不覆盖)。
30 5 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan store-violations:sync --quiet >/dev/null 2>&1
# 店铺违规预警推送:P0/P1 每天 09:00 起每 5 分钟检查;P2 每日 10:00 汇总(未配置 Webhook 时自动跳过)。
*/5 9-21 * * * root /usr/bin/flock -n /run/caiwu-violation-push.lock /usr/bin/docker exec -u www-data caiwu_app php artisan store-violations:push-instant --quiet >/dev/null 2>&1
0 10 * * * root /usr/bin/flock -n /run/caiwu-violation-push.lock /usr/bin/docker exec -u www-data caiwu_app php artisan store-violations:push-digest --quiet >/dev/null 2>&1
# 供应商管理每天北京时间 08:15 完整校准档案、合同、联系人与打款资料。
15 8 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-supplier-management --quiet >/dev/null 2>&1
*/2 * * * * root /usr/bin/flock -n /run/caiwu-esign-polling.lock /usr/bin/docker exec -u www-data caiwu_app php artisan esign:poll-signatures --quiet >/dev/null 2>&1
# 财务台账:每天 02:30 从 AI 库 rpt 同步动账明细并重算流水。
30 2 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan finance-ledger:sync-rpt --quiet >/dev/null 2>&1
# 科目映射保存后保留自动重跑队列,每分钟在受限容器内串行消费。
* * * * * root /usr/bin/flock -n /run/caiwu-account-mapping-resync.lock /bin/sh /opt/caiwuxitong/run-finance-batch.sh store-receipt:auto-resync --quiet >/dev/null 2>&1
* * * * * root /usr/bin/flock -n /run/caiwu-account-bills-export.lock /bin/sh /opt/caiwuxitong/run-finance-batch.sh account-bills:exports:run --quiet >/dev/null 2>&1
28 1 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh jushuitan:sync-shipping --days=3 >/dev/null 2>&1
# 头条放心购(店铺收款账户账单 › 抖音账户)资金流水:每天 09:30 定点把该渠道所用
# 原始账单镜像追平一次,保证上班前当日/前一日流水已进系统,而不是等每分钟轮转慢慢走。
30 9 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables operations_replica.rpt.jst_originalBillInfo --max-batches=20 --quiet >/dev/null 2>&1
# 附加费结算:每天滚动重算最近 7 天;每周一再把最近 90 天全量重算一次,
# 补齐晚到的退货进仓与审批变化(超长费按出库日期、退回费/拦截费按查件工单创建时间)。
30 6 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan courier-surcharge:pull-overlength --days=7 --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan courier-surcharge:pull-return --days=7 --quiet' >/dev/null 2>&1
40 6 * * 1 root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan courier-surcharge:pull-overlength --days=90 --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan courier-surcharge:pull-return --days=90 --quiet' >/dev/null 2>&1
EOF
chmod 644 /etc/cron.d/caiwu-jiansun-snapshots
docker image ls "$IMAGE_NAME" --format '{{.Repository}}:{{.Tag}}' \
| awk '!seen[$0]++' \
| tail -n "+$((KEEP_IMAGES + 1))" \
| while IFS= read -r old_image; do
[ -n "$old_image" ] && docker image rm "$old_image" >/dev/null 2>&1 || true
done
docker image prune -f --filter 'label=com.caiwu.project=CaiWuXiTong' >/dev/null || true
docker compose -f compose.jiangling.yml ps
echo "DEPLOYED_SHA=$DEPLOY_SHA"
# Keep the variable for rollback diagnostics without printing any secret.
: "$PREVIOUS_SHA"