## 任务占用与边界 - 任务名称:全项目上游业务镜像目录与通用同步框架 - 实际开发者:WuYongJie - 目标结果:登记当前代码实际使用的上游原表,提供低压、可断点的首次全量、增量重叠和全量校准框架。 - 实际修改:13 个文件;新增通用数据集目录、动态镜像建表、同步状态、同步命令、生产低压轮询、测试和团队清单。 - 将写入的公共契约:`business_mirror_table_states`、`business-mirror:sync-tables`、`BusinessMirrorDatasetRegistry`、`BusinessMirrorSchemaManager`。 - 仅复用、明确不修改:`UpstreamReadOnlyQueryFactory`、`upstream_schema_columns`、既有 `product_price_mirror_rows`。 - 明确不包含:本 PR 不切换其余业务页面 Repository,不执行生产迁移或首次全量,不提交真实业务数据或凭据。 ## 合并前自审 - [x] 已重新拉取最新 `origin/main`,分支仅领先 1 个提交 - [x] `git merge-tree --write-tree origin/main HEAD` 无冲突 - [x] 完整 diff 共 13 个文件,未发现越界文件或敏感内容 - [x] 生产共享数据字典核对 38 个数据集,缺失对象/字段为 0 - [x] 临时 MariaDB 空库迁移实测成功,验证库已删除 - [x] 上游异常空结果不会清空已有镜像 - [x] 定时任务只轮转已完成首次全量的数据集,不会自动扫描未初始化大表 ## 测试 - `BusinessMirrorSynchronizerTest` 等相关测试:14 tests / 65 assertions 全通过 - 新同步器专项:5 tests / 25 assertions 全通过 - 新增 PHP 文件语法检查全部通过 - 两份生产激活脚本 `bash -n` 通过 - `git diff --check` 通过 ## 风险与回滚 - 包含新增迁移,但发布脚本遇到迁移仍会要求独立高风险授权;合并本身不会执行生产迁移。 - 首次全量必须逐数据集显式执行并做数量/翻页/筛选验收,特别是约 280 万行的大表不得随发布自动全扫。 - 回滚代码可回退本合并提交;生产迁移执行前必须备份 `caiwu_system_config` 与 `caiwu_business_mirror`。 Reviewed-on: http://git.1080p.hk/WuYongJie/CaiWuXiTong/pulls/53
152 lines
5.3 KiB
Bash
152 lines
5.3 KiB
Bash
#!/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}"
|
|
IMAGE_NAME="caiwu_akaunting"
|
|
LOCK_FILE="$REMOTE_DIR/.deploy.lock"
|
|
|
|
cleanup() {
|
|
rm -f "$IMAGE_ARCHIVE"
|
|
}
|
|
|
|
trap cleanup EXIT INT 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
|
|
|
|
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
|
|
|
|
PREVIOUS_IMAGE="$(docker inspect caiwu_app --format '{{.Config.Image}}' 2>/dev/null || true)"
|
|
PREVIOUS_SHA="$(cat "$REMOTE_DIR/.deployed-sha" 2>/dev/null || true)"
|
|
|
|
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
|
|
|
|
cd "$REMOTE_DIR"
|
|
export IMAGE_TAG RUN_MIGRATIONS
|
|
|
|
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
|
|
|
|
healthy=0
|
|
attempt=1
|
|
while [ "$attempt" -le 30 ]; do
|
|
if curl -fsS -o /dev/null http://127.0.0.1:18088/; 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/; 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"
|
|
|
|
# 只运行已登记的上游后台读取,不启用整个 Laravel 调度器,避免意外启动
|
|
# 账单提醒等既有任务。同一把锁保证快照与业务镜像不会并发施压上游。
|
|
cat > /etc/cron.d/caiwu-jiansun-snapshots <<'EOF'
|
|
* * * * * root /usr/bin/flock -n /run/caiwu-upstream-sync.lock /bin/sh -c '/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
|
|
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"
|