- 新增 deploy/post-deploy.sh:回填 → 业务配置 → 同步 → 验收四阶段编排,支持 --only/--dry-run/窗口参数 - 新增 tools 镜像与 scripts/verify-finance-launch.sql(15 项上线验收) - 验收口径:成本闸门二选一、要求已过账凭证、备用金支出关联、成本覆盖 WARN - 加固:参数校验、工具镜像每次刷新、docker run 兜底超时、迁移前缀探测、验收失败退出码 2 - 工具镜像预建 /app/data,消除日志 EACCES - 回填后与 restore-db 恢复后自动 VACUUM (ANALYZE),修复订单列表分页 COUNT 全表扫描(41 万单实测 27s → 0.17s) - 文档:41 万单同步用 7 天窗 + 180s 超时;回填/恢复后刷新统计
485 lines
18 KiB
Bash
Executable File
485 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
|
||
# 财务上线「数据阶段」编排脚本
|
||
#
|
||
# 位置:在 deploy/ubuntu-deploy.sh 成功之后执行(代码与迁移已生效、backend 已 ready)。
|
||
#
|
||
# 它把以前散落在文档里的脚本步骤串成一条可重复执行的流水线:
|
||
# ① 存量订单项收入回填 revenue:backfill
|
||
# ② 履约方式回填 fulfillment:backfill
|
||
# ③ 财务历史同步(分窗口) finance:sync --mode=all
|
||
# ④ 上线验收(只读 SQL) scripts/verify-finance-launch.sql
|
||
#
|
||
# 设计原则:
|
||
# * 幂等:每一步都能重跑,中断后直接再跑一次即可;
|
||
# * 串行:回填与同步都会争 order_items 行锁,**不要并发**;
|
||
# * 只读验收:验收脚本不写业务表,随时可跑;
|
||
# * 不碰人工步骤:备用金/积分/商品目录/人工 SKU/总账同步仍需在后台完成,脚本末尾会打印清单。
|
||
#
|
||
# 常用:
|
||
# bash deploy/post-deploy.sh --dry-run # 只打印计划与待回填量
|
||
# bash deploy/post-deploy.sh --no-verify # 回填 + 同步,跳过验收(日常重跑)
|
||
# bash deploy/post-deploy.sh # 全流程(默认最近 90 天)
|
||
# bash deploy/post-deploy.sh --from=2026-07-01 --to=2026-10-27
|
||
# bash deploy/post-deploy.sh --only=backfill # 只跑 ①②
|
||
# bash deploy/post-deploy.sh --only=sync # 只跑 ③
|
||
# bash deploy/post-deploy.sh --only=verify # 只跑 ④
|
||
# bash deploy/post-deploy.sh --no-build --no-verify
|
||
#
|
||
# 依赖:docker compose(或 docker-compose)已在项目根目录可用,且 .env 存在。
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
cd "$ROOT_DIR"
|
||
|
||
TOOLS_IMAGE="${TOOLS_IMAGE:-order-site/tools:latest}"
|
||
TOOLS_DOCKERFILE="deploy/docker/tools.Dockerfile"
|
||
VERIFY_SQL="scripts/verify-finance-launch.sql"
|
||
|
||
WINDOW_DAYS="${WINDOW_DAYS:-30}"
|
||
BATCH_SIZE="${BATCH_SIZE:-5000}"
|
||
MAX_BATCHES="${MAX_BATCHES:-1000}"
|
||
MAX_BACKFILL_ROUNDS="${MAX_BACKFILL_ROUNDS:-5}"
|
||
|
||
DRY_RUN=false
|
||
SKIP_BACKFILL=false
|
||
SKIP_SYNC=false
|
||
NO_VERIFY=false
|
||
NO_BUILD=false
|
||
ONLY=""
|
||
FROM_DATE=""
|
||
TO_DATE=""
|
||
LAUNCH_AT=""
|
||
|
||
log() { printf '\033[1;34m[post-deploy]\033[0m %s\n' "$*"; }
|
||
warn() { printf '\033[1;33m[post-deploy]\033[0m %s\n' "$*"; }
|
||
fail() {
|
||
printf '\033[1;31m[post-deploy]\033[0m %s\n' "$*" >&2
|
||
exit 1
|
||
}
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
用法:bash deploy/post-deploy.sh [选项]
|
||
|
||
选项:
|
||
--dry-run 只打印计划 + 只读统计,不写库、不构建镜像
|
||
--from=YYYY-MM-DD 财务同步起始日(默认:今天往前 90 天)
|
||
--to=YYYY-MM-DD 财务同步结束日(默认:今天)
|
||
--window-days=N 同步窗口天数(默认 30;41 万单规模建议 7)
|
||
--batch-size=N 回填批大小(默认 5000)
|
||
--max-batches=N 单次回填最大批数(默认 1000)
|
||
--launch-at=TIMESTAMP 验收基准时间(默认:本次执行时间),用于「上线后」类检查
|
||
--only=backfill|sync|verify 只跑指定阶段
|
||
--skip-backfill 跳过 ①②(回填)
|
||
--skip-sync 跳过 ③(财务历史同步)
|
||
--no-verify 跳过 ④(验收)
|
||
--no-build 不构建/刷新工具镜像(镜像不存在时直接失败)
|
||
-h, --help 显示本帮助
|
||
|
||
环境变量:
|
||
POST_DEPLOY_COMPOSE_FILE 指定 compose 文件(默认 docker-compose.yml)
|
||
TOOLS_IMAGE 工具镜像名(默认 order-site/tools:latest)
|
||
WINDOW_DAYS / BATCH_SIZE / MAX_BATCHES 同上对应选项的默认值
|
||
|
||
首次上线顺序(避免商品未归一先同步产生大量异常):
|
||
① bash deploy/post-deploy.sh --only=backfill
|
||
(结束后自动 VACUUM ANALYZE order_items/orders,约 30–60 秒,不阻塞读写)
|
||
② 后台完成业务配置 1)–4)(商品目录必须先于财务同步)
|
||
③ bash deploy/post-deploy.sh --only=sync
|
||
④ 后台完成业务配置 5)(总账同步)
|
||
⑤ bash deploy/post-deploy.sh --only=verify
|
||
|
||
业务配置(后台 UI):
|
||
1) 钱包管理 → 补建客服账户 → 拨付备用金
|
||
2) 商品与成本 → 积分钱包 → 发放三平台积分
|
||
3) 商品与成本 → 标准商品 → 模板初始化 / 按工单回填 / 归一人工商品
|
||
4) 商品与成本 → 人工 SKU 配置 → 扫描并逐项配置
|
||
5) 财务 → 总账与对账 → 同步销售事实到总账 → 试算平衡 → 按需关账
|
||
详见 docs/财务上线初始化与验收-2026-10-26.md
|
||
|
||
退出码:0 = 完成且验收无 FAIL;1 = 执行失败;2 = 验收未通过。
|
||
存量初始化完成后,日常重跑可直接 `bash deploy/post-deploy.sh`(回填 + 同步 + 验收一条龙)。
|
||
EOF
|
||
}
|
||
|
||
# ---------- 参数解析 ----------
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--dry-run) DRY_RUN=true ;;
|
||
--skip-backfill) SKIP_BACKFILL=true ;;
|
||
--skip-sync) SKIP_SYNC=true ;;
|
||
--no-verify) NO_VERIFY=true ;;
|
||
--no-build) NO_BUILD=true ;;
|
||
--from=*) FROM_DATE="${1#*=}" ;;
|
||
--to=*) TO_DATE="${1#*=}" ;;
|
||
--launch-at=*) LAUNCH_AT="${1#*=}" ;;
|
||
--window-days=*) WINDOW_DAYS="${1#*=}" ;;
|
||
--batch-size=*) BATCH_SIZE="${1#*=}" ;;
|
||
--max-batches=*) MAX_BATCHES="${1#*=}" ;;
|
||
--only=*) ONLY="${1#*=}" ;;
|
||
-h | --help)
|
||
usage
|
||
exit 0
|
||
;;
|
||
*) fail "未知参数:$1(--help 查看用法)" ;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
case "$ONLY" in
|
||
'' | backfill | sync | verify) ;;
|
||
*) fail "--only 只能是 backfill / sync / verify(收到:${ONLY})" ;;
|
||
esac
|
||
|
||
# 数值参数必须在进入循环前校验:WINDOW_DAYS<=0 会让分窗循环原地打转。
|
||
validate_integer() {
|
||
local name="$1" value="$2" min="$3" max="$4"
|
||
[[ "$value" =~ ^[0-9]+$ ]] || fail "${name} 需要整数(收到:${value})"
|
||
local number=$((10#$value))
|
||
if ((number < min || number > max)); then
|
||
fail "${name} 需要在 ${min}–${max} 之间(收到:${value})"
|
||
fi
|
||
}
|
||
|
||
validate_integer "--window-days" "$WINDOW_DAYS" 1 400
|
||
validate_integer "--batch-size" "$BATCH_SIZE" 1 50000
|
||
validate_integer "--max-batches" "$MAX_BATCHES" 1 10000
|
||
validate_integer "MAX_BACKFILL_ROUNDS" "$MAX_BACKFILL_ROUNDS" 1 100
|
||
|
||
should_run() {
|
||
[[ -z "$ONLY" || "$ONLY" == "$1" ]]
|
||
}
|
||
|
||
# ---------- 日期工具(兼容 GNU date 与 macOS BSD date) ----------
|
||
|
||
if date -u -d '2026-01-01 + 1 day' +%F >/dev/null 2>&1; then
|
||
DATE_STYLE=gnu
|
||
else
|
||
DATE_STYLE=bsd
|
||
fi
|
||
|
||
date_today() { date +%F; }
|
||
|
||
date_add_days() {
|
||
local base="$1" days="$2" signed="$2"
|
||
if [[ "$days" == "0" ]]; then
|
||
printf '%s\n' "$base"
|
||
return 0
|
||
fi
|
||
# BSD date 必须显式带符号(-v+30d / -v-90d),且 -v 必须写在 -f 之前。
|
||
[[ "$days" != -* ]] && signed="+$days"
|
||
if [[ "$DATE_STYLE" == gnu ]]; then
|
||
date -d "$base $days days" +%F
|
||
else
|
||
date -j -v"${signed}d" -f '%F' "$base" +%F
|
||
fi
|
||
}
|
||
|
||
date_days_ago() { date_add_days "$(date_today)" "-$1"; }
|
||
|
||
is_iso_date() { [[ "$1" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; }
|
||
|
||
# ---------- compose / docker 封装 ----------
|
||
|
||
COMPOSE_FILE_OVERRIDE="${POST_DEPLOY_COMPOSE_FILE:-}"
|
||
|
||
if [[ -n "$COMPOSE_FILE_OVERRIDE" ]]; then
|
||
COMPOSE_CMD=(docker compose -f "$COMPOSE_FILE_OVERRIDE")
|
||
elif docker compose version >/dev/null 2>&1; then
|
||
COMPOSE_CMD=(docker compose)
|
||
elif command -v docker-compose >/dev/null 2>&1; then
|
||
COMPOSE_CMD=(docker-compose)
|
||
else
|
||
fail "找不到 docker compose(或 docker-compose)"
|
||
fi
|
||
|
||
compose() { "${COMPOSE_CMD[@]}" "$@"; }
|
||
|
||
HAS_TOOLS_SERVICE=false
|
||
PROFILE_SUPPORTED=false
|
||
TOOLS_IMAGE_READY=false
|
||
NETWORK=""
|
||
|
||
detect_capabilities() {
|
||
if compose --profile tools config --services 2>/dev/null | grep -qx 'tools'; then
|
||
HAS_TOOLS_SERVICE=true
|
||
fi
|
||
if compose --profile tools config >/dev/null 2>&1; then
|
||
PROFILE_SUPPORTED=true
|
||
fi
|
||
}
|
||
|
||
detect_network() {
|
||
local cid
|
||
cid="$(compose ps -q postgres 2>/dev/null | head -1 || true)"
|
||
if [[ -n "$cid" ]]; then
|
||
NETWORK="$(docker inspect "$cid" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' 2>/dev/null | head -1 || true)"
|
||
fi
|
||
if [[ -z "$NETWORK" ]]; then
|
||
NETWORK="$(basename "$ROOT_DIR" | tr '[:upper:]' '[:lower:]')_default"
|
||
fi
|
||
}
|
||
|
||
env_database_url() {
|
||
local value=""
|
||
if [[ -f .env ]]; then
|
||
value="$(grep -E '^DATABASE_URL=' .env | head -1 | cut -d= -f2- || true)"
|
||
fi
|
||
if [[ -z "$value" ]]; then
|
||
value="postgres://postgres:postgres@postgres:5432/order_site"
|
||
fi
|
||
printf '%s' "$value"
|
||
}
|
||
|
||
# ---------- psql 封装(全部在 postgres 容器内执行) ----------
|
||
|
||
psql_scalar() {
|
||
local sql="$1"
|
||
compose exec -T -e SQL_QUERY="$sql" postgres sh -lc \
|
||
'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAX -v ON_ERROR_STOP=1 -c "$SQL_QUERY"'
|
||
}
|
||
|
||
# ---------- 工具镜像 ----------
|
||
|
||
ensure_tools_image() {
|
||
# 全流程里 backfill 和 sync 各调用一次,同一次运行只构建一遍。
|
||
[[ "$TOOLS_IMAGE_READY" == true ]] && return 0
|
||
|
||
local image_exists=false
|
||
if docker image inspect "$TOOLS_IMAGE" >/dev/null 2>&1; then
|
||
image_exists=true
|
||
fi
|
||
|
||
if [[ "$DRY_RUN" == true ]]; then
|
||
[[ "$image_exists" == true ]] || warn "工具镜像 $TOOLS_IMAGE 不存在(dry-run 不构建);正式执行时会自动构建"
|
||
TOOLS_IMAGE_READY=true
|
||
return 0
|
||
fi
|
||
|
||
if [[ "$NO_BUILD" == true ]]; then
|
||
[[ "$image_exists" == true ]] || fail "工具镜像 $TOOLS_IMAGE 不存在,且指定了 --no-build"
|
||
log "沿用现有工具镜像 ${TOOLS_IMAGE}(--no-build)"
|
||
TOOLS_IMAGE_READY=true
|
||
return 0
|
||
fi
|
||
|
||
# 每次都重建:有层缓存时通常只要几秒,但能保证 git pull 后跑的是当前脚本,
|
||
# 而不是上次部署留在服务器上的旧 tools:latest。
|
||
log "刷新工具镜像 ${TOOLS_IMAGE}(首次约 2–4 分钟,之后走层缓存)"
|
||
if [[ "$HAS_TOOLS_SERVICE" == true && "$PROFILE_SUPPORTED" == true ]]; then
|
||
compose --profile tools build tools
|
||
else
|
||
docker build -f "$TOOLS_DOCKERFILE" -t "$TOOLS_IMAGE" .
|
||
fi
|
||
TOOLS_IMAGE_READY=true
|
||
}
|
||
|
||
run_tool() {
|
||
local script="$1"
|
||
shift
|
||
if [[ "$HAS_TOOLS_SERVICE" == true && "$PROFILE_SUPPORTED" == true ]]; then
|
||
compose --profile tools run --rm --no-deps -T tools npm run "$script" -- "$@"
|
||
else
|
||
docker run --rm \
|
||
--network "$NETWORK" \
|
||
--env-file .env \
|
||
-e DATABASE_URL="$(env_database_url)" \
|
||
-e DATABASE_STATEMENT_TIMEOUT_MS="${DATABASE_STATEMENT_TIMEOUT_MS:-60000}" \
|
||
-e TZ="${TZ:-Asia/Shanghai}" \
|
||
"$TOOLS_IMAGE" \
|
||
npm run "$script" -- "$@"
|
||
fi
|
||
}
|
||
|
||
# 回填脚本在「还有剩余」时返回 2,语义是“继续跑下一轮”,不是失败。
|
||
run_backfill() {
|
||
local script="$1"
|
||
shift
|
||
local round=1 code=0
|
||
while :; do
|
||
log " 第 ${round} 轮:npm run ${script} -- $*"
|
||
set +e
|
||
run_tool "$script" "$@"
|
||
code=$?
|
||
set -e
|
||
if [[ $code -eq 0 ]]; then
|
||
return 0
|
||
fi
|
||
if [[ $code -eq 2 && $round -lt $MAX_BACKFILL_ROUNDS ]]; then
|
||
warn " 仍有剩余(退出码 2),继续下一轮"
|
||
round=$((round + 1))
|
||
continue
|
||
fi
|
||
if [[ $code -eq 2 ]]; then
|
||
fail "${script} 跑满 ${MAX_BACKFILL_ROUNDS} 轮仍有剩余;调大 --max-batches 后重跑"
|
||
fi
|
||
fail "${script} 失败(退出码 ${code})"
|
||
done
|
||
}
|
||
|
||
# ---------- 各阶段 ----------
|
||
|
||
PENDING_REVENUE="?"
|
||
PENDING_FULFILLMENT="?"
|
||
|
||
preflight() {
|
||
log "预检"
|
||
[[ -f .env ]] || fail "项目根目录缺少 .env(生产由 ubuntu-deploy.sh 检查)"
|
||
[[ -f "$VERIFY_SQL" ]] || fail "缺少验收脚本 $VERIFY_SQL"
|
||
|
||
local running
|
||
running="$(compose ps -q postgres 2>/dev/null | head -1 || true)"
|
||
[[ -n "$running" ]] || fail "postgres 容器未运行:先执行 bash deploy/ubuntu-deploy.sh"
|
||
|
||
detect_capabilities
|
||
detect_network
|
||
|
||
local migrations
|
||
migrations="$(psql_scalar "SELECT (EXISTS (SELECT 1 FROM schema_migrations WHERE COALESCE(filename, name) ~ '^103_'))::int + (EXISTS (SELECT 1 FROM schema_migrations WHERE COALESCE(filename, name) ~ '^104_'))::int + (EXISTS (SELECT 1 FROM schema_migrations WHERE COALESCE(filename, name) ~ '^105_'))::int" | tr -d '[:space:]')"
|
||
if [[ "$migrations" != "3" ]]; then
|
||
fail "迁移 103–105 未全部应用(当前 ${migrations}/3):先确认 deploy/ubuntu-deploy.sh 成功、backend 日志无迁移错误"
|
||
fi
|
||
|
||
PENDING_REVENUE="$(psql_scalar "SELECT count(*) FROM order_items WHERE revenue_basis = 'unknown'" | tr -d '[:space:]')"
|
||
PENDING_FULFILLMENT="$(psql_scalar "SELECT count(*) FROM order_items WHERE fulfillment_mode IS NULL" | tr -d '[:space:]')"
|
||
|
||
log " postgres 运行中 / 迁移 103–105 已应用"
|
||
log " 待回填收入项:${PENDING_REVENUE} 行;待回填履约方式:${PENDING_FULFILLMENT} 行"
|
||
log " 工具镜像:${TOOLS_IMAGE}(services=${HAS_TOOLS_SERVICE}, network=${NETWORK})"
|
||
}
|
||
|
||
step_backfill() {
|
||
log "① 回填存量订单项收入(revenue:backfill)"
|
||
if [[ "$DRY_RUN" == true ]]; then
|
||
if docker image inspect "$TOOLS_IMAGE" >/dev/null 2>&1; then
|
||
run_tool revenue:backfill --dryRun || warn "dry-run 预览失败(不影响后续)"
|
||
else
|
||
log " [dry-run] 跳过执行(工具镜像未构建)"
|
||
fi
|
||
else
|
||
run_backfill revenue:backfill --batchSize="$BATCH_SIZE" --maxBatches="$MAX_BATCHES"
|
||
fi
|
||
|
||
log "② 回填履约方式(fulfillment:backfill,只填 NULL)"
|
||
if [[ "$DRY_RUN" == true ]]; then
|
||
log " [dry-run] 跳过执行"
|
||
else
|
||
run_backfill fulfillment:backfill --batchSize="$BATCH_SIZE" --maxBatches="$MAX_BATCHES"
|
||
fi
|
||
|
||
if [[ "$DRY_RUN" != true ]]; then
|
||
refresh_stats
|
||
fi
|
||
}
|
||
|
||
# 回填会 UPDATE 40 万行 order_items:既产生大量死元组,又给新列 fulfillment_mode
|
||
# 留下空统计。autovacuum 追不上时,订单列表的 COUNT 会被估算成 orders 全表扫描,
|
||
# 冷缓存实测 20 秒级(测试库曾因此 500)。这里在回填后立刻补一次,VACUUM 只拿
|
||
# SHARE UPDATE EXCLUSIVE,不阻塞读写。
|
||
refresh_stats() {
|
||
log "②' 刷新统计信息与可见性(VACUUM ANALYZE order_items/orders)"
|
||
if ! compose exec -T postgres sh -lc \
|
||
'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c "VACUUM (ANALYZE) order_items" -c "VACUUM (ANALYZE) orders"'; then
|
||
warn "VACUUM ANALYZE 失败:统计信息未刷新,订单列表 COUNT 可能变慢;请手动补跑后再放开后台"
|
||
fi
|
||
}
|
||
|
||
step_sync() {
|
||
local from="$FROM_DATE" to="$TO_DATE" win_to
|
||
local products
|
||
products="$(psql_scalar 'SELECT count(*) FROM product_catalog' | tr -d '[:space:]')"
|
||
if [[ "$products" == "0" ]]; then
|
||
warn "商品目录为空:finance:sync 会为未归一订单项生成大量 product_mapping_missing 异常;建议先完成商品初始化再跑 --only=sync"
|
||
fi
|
||
log "③ 财务历史同步(finance:sync --mode=all),区间 ${from} → ${to},每窗 ${WINDOW_DAYS} 天"
|
||
while :; do
|
||
win_to="$(date_add_days "$from" $((WINDOW_DAYS - 1)))"
|
||
if [[ "$win_to" > "$to" ]]; then
|
||
win_to="$to"
|
||
fi
|
||
if [[ "$DRY_RUN" == true ]]; then
|
||
log " [dry-run] npm run finance:sync -- --mode=all --from=${from} --to=${win_to}"
|
||
else
|
||
log " 窗口 ${from} → ${win_to}"
|
||
run_tool finance:sync --mode=all --from="$from" --to="$win_to"
|
||
fi
|
||
[[ "$win_to" == "$to" ]] && break
|
||
from="$(date_add_days "$win_to" 1)"
|
||
done
|
||
}
|
||
|
||
step_verify() {
|
||
log "④ 上线验收(只读)"
|
||
local code=0
|
||
set +e
|
||
compose exec -T -e LAUNCH_AT="$LAUNCH_AT" postgres sh -lc \
|
||
'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v launch_at="$LAUNCH_AT" -f -' \
|
||
<"$VERIFY_SQL"
|
||
code=$?
|
||
set -e
|
||
if [[ $code -ne 0 ]]; then
|
||
warn "验收未通过(psql 退出码 ${code}):按上面 FAIL 项的「处理建议」逐条处理后,重跑 --only=verify"
|
||
return 1
|
||
fi
|
||
log "验收全部通过"
|
||
}
|
||
|
||
manual_checklist() {
|
||
cat <<'EOF'
|
||
|
||
────────────────────────────────────────────────────────────
|
||
剩余人工步骤(后台 UI,脚本无法代替):
|
||
1) 钱包管理 → 补建客服账户 → 拨付备用金
|
||
2) 商品与成本 → 积分钱包 → 发放三平台积分
|
||
3) 商品与成本 → 标准商品 →「从接单模板初始化」→「按工单回填商品」→「归一人工商品」
|
||
4) 商品与成本 → 人工 SKU 配置 →「扫描」并逐项配置
|
||
5) 财务 → 总账与对账 →「同步销售事实到总账」→ 试算平衡 → 按需关账
|
||
首次上线顺序:--only=backfill → 完成 1)–4) → --only=sync → 完成 5) → --only=verify
|
||
手册:docs/财务上线初始化与验收-2026-10-26.md
|
||
────────────────────────────────────────────────────────────
|
||
EOF
|
||
}
|
||
|
||
# ---------- 主流程 ----------
|
||
|
||
main() {
|
||
if [[ -z "$FROM_DATE" ]]; then FROM_DATE="$(date_days_ago 90)"; fi
|
||
if [[ -z "$TO_DATE" ]]; then TO_DATE="$(date_today)"; fi
|
||
if [[ -z "$LAUNCH_AT" ]]; then LAUNCH_AT="$(date '+%Y-%m-%d %H:%M:%S%z')"; fi
|
||
|
||
is_iso_date "$FROM_DATE" || fail "--from 需要 YYYY-MM-DD(收到:${FROM_DATE})"
|
||
is_iso_date "$TO_DATE" || fail "--to 需要 YYYY-MM-DD(收到:${TO_DATE})"
|
||
[[ "$FROM_DATE" < "$TO_DATE" || "$FROM_DATE" == "$TO_DATE" ]] || fail "--from 不能晚于 --to"
|
||
|
||
log "计划:dry-run=${DRY_RUN} only=${ONLY:-all} from=${FROM_DATE} to=${TO_DATE} window=${WINDOW_DAYS}d launch-at=${LAUNCH_AT}"
|
||
|
||
preflight
|
||
|
||
if should_run backfill && [[ "$SKIP_BACKFILL" != true ]]; then
|
||
ensure_tools_image
|
||
step_backfill
|
||
fi
|
||
|
||
if should_run sync && [[ "$SKIP_SYNC" != true ]]; then
|
||
ensure_tools_image
|
||
step_sync
|
||
fi
|
||
|
||
local verify_code=0
|
||
if should_run verify && [[ "$NO_VERIFY" != true ]]; then
|
||
step_verify || verify_code=1
|
||
fi
|
||
|
||
manual_checklist
|
||
|
||
if [[ $verify_code -ne 0 && "$DRY_RUN" != true ]]; then
|
||
warn "数据阶段已完成,但验收未通过:按上面的 FAIL 项处理后重跑 bash deploy/post-deploy.sh --only=verify"
|
||
exit 2
|
||
fi
|
||
log "完成"
|
||
}
|
||
|
||
main "$@"
|