dy-dashboard/pack_deploy_full.sh
2026-10-28 11:14:17 +08:00

286 lines
13 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.

#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════
# 蝶雅智能体平台 — 打包部署脚本
#
# 默认:部署 代码+知识库+数据库结构(不覆盖数据)
# 加 --db 参数:同时全量覆盖数据库(含数据)
# ═══════════════════════════════════════════════════════════════
set -euo pipefail
# ═══ 配置区 ═══
REMOTE_IP="192.168.11.79"
REMOTE_USER="dieya"
REMOTE_DIR="/home/dieya/.openclaw/workspace/dy-dashboard"
LOCAL_DIR="/home/gxd/.openclaw/workspace/dy-dashboard"
VAULT_DIR="/home/gxd/蝶雅供应链/知识库"
PROD_VAULT_DIR="${REMOTE_DIR}/vault"
DB_USER="dy_app"
DB_PASS="dy_data_2026"
DB_NAME="dy_data"
# ═══ 解析参数 ═══
DEPLOY_DB=false
for arg in "$@"; do
[ "$arg" = "--db" ] && DEPLOY_DB=true
done
# 时间戳
TS=$(date +%Y%m%d_%H%M%S)
STAGE_DIR="/tmp/dy-pack-${TS}"
PACKAGE="/tmp/dy-dashboard-full-${TS}.tar.gz"
DUMP_FILE="${STAGE_DIR}/dy_data_full.sql"
SCHEMA_FILE="${STAGE_DIR}/dy_data_schema.sql"
# ═══ 颜色 ═══
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
# ═══════════════════════════════════════════════════════════════
# 本地打包
# ═══════════════════════════════════════════════════════════════
echo ""
echo -e "${GREEN}═══════════════════════════════════════════════${NC}"
echo -e "${GREEN} 蝶雅智能体平台 — 打包部署${NC}"
echo -e "${GREEN}═══════════════════════════════════════════════${NC}"
echo ""
info "时间:${TS}"
$DEPLOY_DB && info "模式:全量(代码+知识库+数据库含数据)⚠️" || info "模式:增量(代码+知识库+数据库结构,保留数据)"
# 1.1 导出数据库
mkdir -p "${STAGE_DIR}/vault/知识库"
# 导出数据库结构(始终导出,用于更新结构不覆盖数据)
info "导出数据库结构..."
mysqldump -u"${DB_USER}" -p"${DB_PASS}" --no-data --skip-add-drop-table \
--routines --triggers --events \
--no-tablespaces \
--ignore-table="${DB_NAME}.feedback" \
"${DB_NAME}" > "${SCHEMA_FILE}"
SCHEMA_SIZE=$(du -h "${SCHEMA_FILE}" | cut -f1)
ok "结构已导出(${SCHEMA_SIZE})"
# 导出完整数据库(仅 --db 时使用)
if $DEPLOY_DB; then
info "导出完整数据库..."
mysqldump -u"${DB_USER}" -p"${DB_PASS}" "${DB_NAME}" \
--routines --triggers --events --single-transaction \
--complete-insert --hex-blob --no-tablespaces \
--ignore-table="${DB_NAME}.feedback" \
> "${DUMP_FILE}"
SQL_SIZE=$(du -h "${DUMP_FILE}" | cut -f1)
ok "完整数据库已导出(${SQL_SIZE})"
fi
# 1.2 复制项目源码
info "复制项目文件..."
rsync -a --delete \
--exclude='venv' \
--exclude='__pycache__' \
--exclude='*.pyc' \
--exclude='backups' \
--exclude='logs' \
--exclude='.git' \
--exclude='*.tar.gz' \
--exclude='*.sql' \
--exclude='生产环境部署指南.md' \
--exclude='static/uploads/feedback' \
--exclude='static/uploads/new_products' \
--exclude='node_modules' \
--exclude='pack_deploy_full.sh' \
"${LOCAL_DIR}/" "${STAGE_DIR}/"
# 1.3 复制知识库
info "复制知识库文件..."
mkdir -p "${STAGE_DIR}/vault/知识库"
cp -r "${VAULT_DIR}/." "${STAGE_DIR}/vault/知识库/"
ok "项目与知识库已准备"
# 1.4 打包
info "打包..."
cd "${STAGE_DIR}"
tar -czf "${PACKAGE}" .
PACKAGE_SIZE=$(du -h "${PACKAGE}" | cut -f1)
ok "打包完成(${PACKAGE_SIZE}):${PACKAGE}"
# ═══════════════════════════════════════════════════════════════
# 上传到生产
# ═══════════════════════════════════════════════════════════════
echo ""
info "上传..."
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
"${REMOTE_USER}@${REMOTE_IP}" "echo SSH_OK" 2>/dev/null | grep -q "SSH_OK" || \
err "SSH 连接失败"
scp "${PACKAGE}" "${REMOTE_USER}@${REMOTE_IP}:/tmp/"
ok "上传完成"
# ═══════════════════════════════════════════════════════════════
# 生产环境部署
# ═══════════════════════════════════════════════════════════════
info "生产环境部署..."
ssh "${REMOTE_USER}@${REMOTE_IP}" "
set -e
echo ' → 停服务...'
PID=\$(lsof -t -i:18888 2>/dev/null || true)
[ -n \"\$PID\" ] && kill -9 \$PID 2>/dev/null || true
sleep 1
TIMESTAMP=\$(date +%Y%m%d_%H%M%S)
echo ' → 独立备份新品图片目录...'
mkdir -p ${REMOTE_DIR}_backups
if [ -d \"${REMOTE_DIR}/static/uploads/new_products\" ]; then
tar -czf \"${REMOTE_DIR}_backups/new_products_pre-${TS}-\${TIMESTAMP}.tar.gz\" \\
-C \"${REMOTE_DIR}/static/uploads\" new_products
echo \" 新品图片独立备份: new_products_pre-${TS}-\${TIMESTAMP}.tar.gz\"
fi
echo ' → 备份当前项目(包含 feedback 图片)...'
mkdir -p ${REMOTE_DIR}_backups
if [ -d \"${REMOTE_DIR}\" ] && [ -n \"\$(ls -A ${REMOTE_DIR} 2>/dev/null)\" ]; then
tar -czf \"${REMOTE_DIR}_backups/pre-${TS}-\${TIMESTAMP}.tar.gz\" -C \"${REMOTE_DIR}\" .
echo \" 备份: pre-${TS}-\${TIMESTAMP}.tar.gz\"
fi
echo ' → 保留上传图片...'
[ -d \"${REMOTE_DIR}/static/uploads/feedback\" ] && mv \"${REMOTE_DIR}/static/uploads/feedback\" /tmp/feedback-backup-${TS}
[ -d \"${REMOTE_DIR}/static/uploads/new_products\" ] && mv \"${REMOTE_DIR}/static/uploads/new_products\" /tmp/new-products-backup-${TS}
rm -rf \"${REMOTE_DIR}\"
mkdir -p \"${REMOTE_DIR}\"
echo ' → 解压...'
tar -xzf /tmp/dy-dashboard-full-${TS}.tar.gz -C \"${REMOTE_DIR}\"
echo ' → 恢复上传图片...'
mkdir -p \"${REMOTE_DIR}/static/uploads\"
if [ -d /tmp/feedback-backup-${TS} ]; then
mv /tmp/feedback-backup-${TS} \"${REMOTE_DIR}/static/uploads/feedback\"
echo \" ✅ 已恢复 feedback 图片\"
fi
mkdir -p \"${REMOTE_DIR}/static/uploads/new_products\"
if [ -d /tmp/new-products-backup-${TS} ]; then
cp -a /tmp/new-products-backup-${TS}/. \"${REMOTE_DIR}/static/uploads/new_products/\"
rm -rf /tmp/new-products-backup-${TS}
echo \" ✅ 已合并恢复生产新品图片\"
fi
mkdir -p \"${REMOTE_DIR}/static/uploads/feedback\" \"${REMOTE_DIR}/static/uploads/new_products\"
if [ -f \"${REMOTE_DIR}_backups/new_products_pre-${TS}-\${TIMESTAMP}.tar.gz\" ]; then
BEFORE_IMAGES=\$(tar -tzf \"${REMOTE_DIR}_backups/new_products_pre-${TS}-\${TIMESTAMP}.tar.gz\" 2>/dev/null | grep -Ev '/$' | wc -l)
AFTER_IMAGES=\$(find \"${REMOTE_DIR}/static/uploads/new_products\" -maxdepth 1 -type f | wc -l)
[ \"\${AFTER_IMAGES}\" -ge \"\${BEFORE_IMAGES}\" ] || {
echo \" ❌ 新品图片恢复不完整:部署前 \${BEFORE_IMAGES} 个,部署后 \${AFTER_IMAGES} 个\"
exit 1
}
echo \" ✅ 新品图片保护校验通过:部署前 \${BEFORE_IMAGES} 个,部署后 \${AFTER_IMAGES} 个\"
fi
echo ' → 更新 VAULT_PATH...'
sed -i 's|^VAULT_PATH =.*|VAULT_PATH = \"${PROD_VAULT_DIR}\"|' \"${REMOTE_DIR}/config.py\"
# ── 数据库结构更新 ──
if [ -f \"${REMOTE_DIR}/dy_data_schema.sql\" ]; then
echo ' → 更新数据库结构(保留数据)...'
sed 's/CREATE TABLE IF NOT EXISTS/CREATE TABLE/g; s/CREATE TABLE/CREATE TABLE IF NOT EXISTS/g' \
\"${REMOTE_DIR}/dy_data_schema.sql\" | \
sed 's/CREATE VIEW/CREATE OR REPLACE VIEW/g' | \
mysql -u\"${DB_USER}\" -p\"${DB_PASS}\" \"${DB_NAME}\" 2>&1 | grep -v 'already exists' || true
rm -f \"${REMOTE_DIR}/dy_data_schema.sql\"
echo ' ✅ 数据库结构已更新'
fi
echo ' → 重建 venv...'
cd \"${REMOTE_DIR}\"
if [ -f requirements.txt ]; then
python3 -m venv venv
source venv/bin/activate
pip install -q -r requirements.txt
deactivate
fi
echo ' → 清理缓存...'
find \"${REMOTE_DIR}\" -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null
find \"${REMOTE_DIR}\" -name '*.pyc' -delete 2>/dev/null
echo ' → 迁移新增列...'
cat > /tmp/migrate_cols.sql <<'SQLEOF'
ALTER TABLE \`domestic_orders\` ADD COLUMN \`待提数量\` INT DEFAULT 0;
ALTER TABLE \`overseas_orders\` ADD COLUMN \`待提数量\` INT DEFAULT 0;
ALTER TABLE \`overseas_orders\` ADD COLUMN \`订单下达供应商时间\` DATETIME DEFAULT NULL;
ALTER TABLE \`packaging_orders\` ADD COLUMN \`待提数量\` INT DEFAULT 0;
ALTER TABLE \`packaging_orders\` ADD COLUMN \`实际到货数量\` INT DEFAULT 0;
ALTER TABLE \`packaging_orders\` ADD COLUMN \`未到货数\` INT DEFAULT 0;
ALTER TABLE \`ai_analysis_history\` ADD COLUMN \`content\` MEDIUMTEXT DEFAULT NULL;
SQLEOF
mysql --force -u"${DB_USER}" -p"${DB_PASS}" "${DB_NAME}" < /tmp/migrate_cols.sql 2>&1 | grep -viE 'duplicate column|already exists' || true
rm -f /tmp/migrate_cols.sql
echo ' ✅ 数据库列迁移完成'
echo ' → 启动服务...'
mkdir -p logs
nohup venv/bin/gunicorn -w 4 -b 0.0.0.0:18888 --timeout 120 \
--access-logfile logs/access.log \
--error-logfile logs/error.log \
app:app > /dev/null 2>&1 &
echo ' → 验证...'
sleep 5
curl -s http://127.0.0.1:18888/ > /dev/null 2>&1 && echo '__DEPLOY_OK__' || echo '__DEPLOY_FAIL__'
" | grep -q "__DEPLOY_OK__" && ok "生产环境部署成功 ✅" || err "生产环境部署失败 ❌"
# ═══════════════════════════════════════════════════════════════
# 全量数据库覆盖(仅 --db 模式)
# ═══════════════════════════════════════════════════════════════
if $DEPLOY_DB; then
warn "准备全量恢复数据库(将覆盖生产数据)..."
scp "${DUMP_FILE}" "${REMOTE_USER}@${REMOTE_IP}:/tmp/"
ssh "${REMOTE_USER}@${REMOTE_IP}" "
set -e
kill -9 \$(lsof -t -i:18888 2>/dev/null) 2>/dev/null || true
sleep 1
echo ' → 恢复数据库...'
mysql -u\"${DB_USER}\" -p\"${DB_PASS}\" \"${DB_NAME}\" < /tmp/dy_data_full.sql
echo ' → 启动服务...'
cd ${REMOTE_DIR}
nohup venv/bin/gunicorn -w 4 -b 0.0.0.0:18888 --timeout 120 \
--access-logfile logs/access.log \
--error-logfile logs/error.log \
app:app > /dev/null 2>&1 &
sleep 5
curl -s http://127.0.0.1:18888/ > /dev/null 2>&1 && echo '__DB_OK__' || echo '__DB_FAIL__'
" | grep -q "__DB_OK__" && ok "数据库全量恢复完成 ✅" || err "数据库全量恢复失败 ❌"
fi
# ═══════════════════════════════════════════════════════════════
# 收尾
# ═══════════════════════════════════════════════════════════════
info "清理本地临时文件..."
rm -rf "${STAGE_DIR}" "${PACKAGE}"
ok "清理完成"
echo ""
echo -e "${GREEN}═══════════════════════════════════════════════${NC}"
echo -e "${GREEN} ✅ 部署完成!${NC}"
echo -e "${GREEN}═══════════════════════════════════════════════${NC}"
echo ""
echo -e " 生产地址:${CYAN}http://${REMOTE_IP}:18888${NC}"
echo -e " 包大小:${PACKAGE_SIZE}"
echo ""
if $DEPLOY_DB; then
echo -e " ${RED}⚠️ 全量模式:数据库结构和数据均已覆盖${NC}"
else
echo -e " ${GREEN}✅ 增量模式:代码 + 知识库 + 结构已更新,数据未覆盖${NC}"
echo -e " ${YELLOW}💡 如需全量覆盖数据库,运行: bash pack_deploy_full.sh --db${NC}"
fi
echo ""