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