#!/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"