Initial commit: 智能项目定价模型

This commit is contained in:
kuzma
2026-01-31 21:33:06 +08:00
commit ef0824303f
174 changed files with 31705 additions and 0 deletions

178
scripts/backup.sh Executable file
View File

@@ -0,0 +1,178 @@
#!/bin/bash
# 智能项目定价模型 - 数据库备份脚本
# 遵循瑞小美部署规范
set -e
# 配置
BACKUP_DIR="/data/backups/pricing_model"
RETENTION_DAYS=7
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="pricing_model_${DATE}.sql.gz"
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() {
echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"
}
# 加载环境变量
load_env() {
cd "$(dirname "$0")/.."
if [ -f ".env" ]; then
source .env
else
log_error ".env 文件不存在"
exit 1
fi
}
# 创建备份目录
create_backup_dir() {
if [ ! -d "$BACKUP_DIR" ]; then
mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"
fi
}
# 执行备份
do_backup() {
log_info "开始备份数据库..."
# 使用 docker exec 执行 mysqldump
docker exec pricing-mysql mysqldump \
-u root \
-p"${MYSQL_ROOT_PASSWORD}" \
--single-transaction \
--routines \
--triggers \
--databases pricing_model \
2>/dev/null | gzip > "${BACKUP_DIR}/${BACKUP_FILE}"
if [ $? -eq 0 ]; then
local size=$(du -h "${BACKUP_DIR}/${BACKUP_FILE}" | cut -f1)
log_info "备份完成: ${BACKUP_FILE} (${size})"
else
log_error "备份失败"
rm -f "${BACKUP_DIR}/${BACKUP_FILE}"
exit 1
fi
}
# 清理旧备份
cleanup_old_backups() {
log_info "清理 ${RETENTION_DAYS} 天前的备份..."
local deleted=0
while IFS= read -r file; do
rm -f "$file"
deleted=$((deleted + 1))
done < <(find "$BACKUP_DIR" -name "pricing_model_*.sql.gz" -mtime +${RETENTION_DAYS} -type f)
if [ $deleted -gt 0 ]; then
log_info "已删除 ${deleted} 个旧备份"
fi
}
# 列出备份
list_backups() {
log_info "备份列表:"
echo ""
ls -lh "${BACKUP_DIR}"/pricing_model_*.sql.gz 2>/dev/null || echo "暂无备份"
echo ""
}
# 恢复备份
restore_backup() {
local backup_file="$1"
if [ -z "$backup_file" ]; then
log_error "请指定备份文件"
list_backups
exit 1
fi
if [ ! -f "$backup_file" ]; then
# 尝试在备份目录中查找
backup_file="${BACKUP_DIR}/${backup_file}"
if [ ! -f "$backup_file" ]; then
log_error "备份文件不存在: $backup_file"
exit 1
fi
fi
log_warn "即将恢复数据库,当前数据将被覆盖!"
read -p "确认恢复? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then
log_info "取消恢复"
exit 0
fi
log_info "开始恢复数据库..."
gunzip -c "$backup_file" | docker exec -i pricing-mysql mysql \
-u root \
-p"${MYSQL_ROOT_PASSWORD}" \
2>/dev/null
if [ $? -eq 0 ]; then
log_info "数据库恢复完成"
else
log_error "恢复失败"
exit 1
fi
}
# 主函数
main() {
local action="${1:-backup}"
load_env
create_backup_dir
case $action in
backup)
do_backup
cleanup_old_backups
;;
restore)
restore_backup "$2"
;;
list)
list_backups
;;
cleanup)
cleanup_old_backups
;;
*)
echo "用法: $0 {backup|restore <file>|list|cleanup}"
echo ""
echo "命令:"
echo " backup 执行备份"
echo " restore <file> 恢复指定备份"
echo " list 列出所有备份"
echo " cleanup 清理旧备份"
exit 1
;;
esac
}
main "$@"

246
scripts/deploy.sh Executable file
View File

@@ -0,0 +1,246 @@
#!/bin/bash
# 智能项目定价模型 - 生产环境部署脚本
# 遵循瑞小美部署规范
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 检查必要命令
check_requirements() {
log_info "检查环境依赖..."
local requirements=("docker" "docker-compose")
for cmd in "${requirements[@]}"; do
if ! command -v $cmd &> /dev/null; then
log_error "$cmd 未安装"
exit 1
fi
done
log_info "环境检查通过"
}
# 检查 .env 文件
check_env_file() {
log_info "检查环境配置..."
if [ ! -f ".env" ]; then
log_error ".env 文件不存在,请从 env.example 复制并配置"
log_info "执行: cp env.example .env && chmod 600 .env"
exit 1
fi
# 检查 .env 文件权限
local perms=$(stat -c %a .env 2>/dev/null || stat -f %OLp .env 2>/dev/null)
if [ "$perms" != "600" ]; then
log_warn ".env 文件权限不是 600正在修复..."
chmod 600 .env
fi
# 检查必要配置项
local required_vars=("DATABASE_URL" "MYSQL_ROOT_PASSWORD" "MYSQL_PASSWORD" "SECRET_KEY")
source .env
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
log_error "缺少必要配置: $var"
exit 1
fi
done
# 检查是否修改了默认密码
if [[ "$SECRET_KEY" == *"change-in-production"* ]]; then
log_error "请修改 SECRET_KEY 为随机字符串"
exit 1
fi
log_info "配置检查通过"
}
# 检查网络
check_network() {
log_info "检查 Docker 网络..."
# 检查 scrm_network 是否存在
if ! docker network ls | grep -q "scrm_network"; then
log_info "创建 scrm_network 网络..."
docker network create scrm_network
fi
log_info "网络检查通过"
}
# 拉取/构建镜像
build_images() {
log_info "构建 Docker 镜像..."
docker-compose build --no-cache
log_info "镜像构建完成"
}
# 停止旧服务
stop_services() {
log_info "停止旧服务..."
docker-compose down --remove-orphans 2>/dev/null || true
log_info "旧服务已停止"
}
# 启动服务
start_services() {
log_info "启动服务..."
docker-compose up -d
log_info "服务启动中..."
}
# 等待服务就绪
wait_for_services() {
log_info "等待服务就绪..."
local max_attempts=30
local attempt=0
# 等待后端健康检查
while [ $attempt -lt $max_attempts ]; do
if docker-compose exec -T pricing-backend curl -sf http://localhost:8000/health > /dev/null 2>&1; then
log_info "后端服务就绪"
break
fi
attempt=$((attempt + 1))
echo -n "."
sleep 2
done
if [ $attempt -eq $max_attempts ]; then
log_error "服务启动超时"
docker-compose logs --tail=50
exit 1
fi
echo ""
}
# 刷新 Nginx DNS 缓存
refresh_nginx() {
log_info "刷新 Nginx DNS 缓存..."
# 检查 nginx_proxy 容器是否存在
if docker ps | grep -q "nginx_proxy"; then
docker exec nginx_proxy nginx -s reload 2>/dev/null || log_warn "Nginx reload 失败,请手动执行"
else
log_warn "nginx_proxy 容器未运行,请确保 Nginx 配置正确"
fi
}
# 显示服务状态
show_status() {
log_info "服务状态:"
echo ""
docker-compose ps
echo ""
log_info "健康检查:"
curl -sf http://localhost:8000/health 2>/dev/null && echo "" || log_warn "后端服务不可访问"
echo ""
log_info "部署完成!"
echo ""
echo "访问地址:"
echo " - 前端: https://pricing.example.com (需配置 Nginx)"
echo " - 后端 API: http://localhost:8000"
echo " - API 文档: http://localhost:8000/docs (仅开发环境)"
}
# 回滚
rollback() {
log_warn "执行回滚..."
docker-compose down
# 如果有备份,恢复
if [ -f ".env.backup" ]; then
mv .env.backup .env
fi
log_info "回滚完成,请检查日志排查问题"
}
# 主函数
main() {
local action="${1:-deploy}"
cd "$(dirname "$0")/.."
case $action in
deploy)
log_info "开始部署智能项目定价模型..."
echo ""
check_requirements
check_env_file
check_network
build_images
stop_services
start_services
wait_for_services
refresh_nginx
show_status
;;
restart)
log_info "重启服务..."
docker-compose restart
wait_for_services
refresh_nginx
show_status
;;
stop)
log_info "停止服务..."
docker-compose down
log_info "服务已停止"
;;
status)
show_status
;;
logs)
docker-compose logs -f --tail=100
;;
rollback)
rollback
;;
*)
echo "用法: $0 {deploy|restart|stop|status|logs|rollback}"
exit 1
;;
esac
}
# 捕获错误
trap 'log_error "部署失败"; exit 1' ERR
main "$@"

318
scripts/monitor.sh Executable file
View File

@@ -0,0 +1,318 @@
#!/bin/bash
# 智能项目定价模型 - 监控检查脚本
# 遵循瑞小美部署规范
set -e
# 配置
ALERT_EMAIL="${ALERT_EMAIL:-admin@example.com}"
WEBHOOK_URL="${WEBHOOK_URL:-}" # 企业微信/钉钉 webhook
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() {
echo -e "${GREEN}[OK]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 发送告警
send_alert() {
local title="$1"
local message="$2"
local level="${3:-warning}" # warning, error
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local full_message="[$timestamp] [智能项目定价模型] $title: $message"
# 控制台输出
if [ "$level" = "error" ]; then
log_error "$full_message"
else
log_warn "$full_message"
fi
# 发送企业微信/钉钉通知
if [ -n "$WEBHOOK_URL" ]; then
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"$full_message\"}}" \
> /dev/null 2>&1 || true
fi
# 发送邮件(如果配置了 sendmail
if [ -n "$ALERT_EMAIL" ] && command -v sendmail &> /dev/null; then
echo -e "Subject: [告警] 智能项目定价模型 - $title\n\n$full_message" | \
sendmail "$ALERT_EMAIL" 2>/dev/null || true
fi
}
# 检查 Docker 容器状态
check_containers() {
echo "检查容器状态..."
echo ""
local containers=("pricing-frontend" "pricing-backend" "pricing-mysql")
local all_healthy=true
for container in "${containers[@]}"; do
local status=$(docker inspect --format='{{.State.Status}}' "$container" 2>/dev/null || echo "not_found")
local health=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "unknown")
if [ "$status" = "running" ]; then
if [ "$health" = "healthy" ] || [ "$health" = "unknown" ]; then
log_info "$container: $status (health: $health)"
else
log_warn "$container: $status (health: $health)"
all_healthy=false
fi
else
log_error "$container: $status"
send_alert "容器异常" "$container 状态异常: $status" "error"
all_healthy=false
fi
done
echo ""
return $([ "$all_healthy" = true ] && echo 0 || echo 1)
}
# 检查服务健康
check_health() {
echo "检查服务健康..."
echo ""
# 后端健康检查
local backend_health=$(curl -sf http://localhost:8000/health 2>/dev/null)
if [ $? -eq 0 ]; then
log_info "后端服务: 健康"
echo " 响应: $backend_health"
else
log_error "后端服务: 不可访问"
send_alert "服务异常" "后端服务健康检查失败" "error"
fi
# 前端健康检查(通过 Nginx
local frontend_health=$(curl -sf http://localhost/health 2>/dev/null)
if [ $? -eq 0 ]; then
log_info "前端服务: 健康"
else
log_warn "前端服务: 不可访问(可能需要通过 Nginx 代理)"
fi
echo ""
}
# 检查数据库连接
check_database() {
echo "检查数据库..."
echo ""
local db_status=$(docker exec pricing-mysql mysqladmin ping -h localhost 2>&1 || echo "failed")
if [[ "$db_status" == *"alive"* ]]; then
log_info "MySQL: 连接正常"
# 检查表数量
local table_count=$(docker exec pricing-mysql mysql -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='pricing_model'" 2>/dev/null || echo "0")
echo " 数据库表数量: $table_count"
else
log_error "MySQL: 连接失败"
send_alert "数据库异常" "MySQL 连接失败" "error"
fi
echo ""
}
# 检查磁盘空间
check_disk() {
echo "检查磁盘空间..."
echo ""
# 检查根目录
local disk_usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$disk_usage" -lt 80 ]; then
log_info "磁盘使用率: ${disk_usage}%"
elif [ "$disk_usage" -lt 90 ]; then
log_warn "磁盘使用率: ${disk_usage}% (警告)"
send_alert "磁盘空间不足" "磁盘使用率达到 ${disk_usage}%" "warning"
else
log_error "磁盘使用率: ${disk_usage}% (危险)"
send_alert "磁盘空间严重不足" "磁盘使用率达到 ${disk_usage}%" "error"
fi
# 检查 Docker 卷
local docker_disk=$(docker system df --format '{{.Size}}' 2>/dev/null | head -1)
echo " Docker 磁盘占用: $docker_disk"
echo ""
}
# 检查内存
check_memory() {
echo "检查内存使用..."
echo ""
local mem_usage=$(free | awk 'NR==2 {printf "%.0f", $3*100/$2}')
if [ "$mem_usage" -lt 80 ]; then
log_info "内存使用率: ${mem_usage}%"
elif [ "$mem_usage" -lt 90 ]; then
log_warn "内存使用率: ${mem_usage}% (警告)"
send_alert "内存不足" "内存使用率达到 ${mem_usage}%" "warning"
else
log_error "内存使用率: ${mem_usage}% (危险)"
send_alert "内存严重不足" "内存使用率达到 ${mem_usage}%" "error"
fi
# 容器内存使用
echo " 容器内存使用:"
docker stats --no-stream --format " {{.Name}}: {{.MemUsage}}" 2>/dev/null | grep pricing || true
echo ""
}
# 检查日志错误
check_logs() {
echo "检查最近错误日志..."
echo ""
# 检查后端错误日志(最近 100 行)
local error_count=$(docker logs pricing-backend --tail 100 2>&1 | grep -c -i "error" || echo "0")
if [ "$error_count" -eq 0 ]; then
log_info "后端日志: 无错误"
elif [ "$error_count" -lt 10 ]; then
log_warn "后端日志: 发现 $error_count 个错误"
else
log_error "后端日志: 发现 $error_count 个错误"
send_alert "日志错误过多" "后端日志发现 $error_count 个错误" "warning"
fi
echo ""
}
# 检查 API 响应时间
check_api_performance() {
echo "检查 API 性能..."
echo ""
local start_time=$(date +%s%N)
local response=$(curl -sf -o /dev/null -w '%{http_code}' http://localhost:8000/health 2>/dev/null || echo "000")
local end_time=$(date +%s%N)
local latency=$(( (end_time - start_time) / 1000000 ))
if [ "$response" = "200" ]; then
if [ "$latency" -lt 500 ]; then
log_info "健康检查 API: ${latency}ms"
elif [ "$latency" -lt 2000 ]; then
log_warn "健康检查 API: ${latency}ms (较慢)"
else
log_error "健康检查 API: ${latency}ms (过慢)"
send_alert "API 响应过慢" "健康检查 API 响应时间 ${latency}ms" "warning"
fi
else
log_error "健康检查 API: 请求失败 (HTTP $response)"
fi
echo ""
}
# 生成报告
generate_report() {
echo "=========================================="
echo " 智能项目定价模型 - 监控报告"
echo " $(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="
echo ""
check_containers
check_health
check_database
check_disk
check_memory
check_logs
check_api_performance
echo "=========================================="
echo " 检查完成"
echo "=========================================="
}
# 主函数
main() {
local action="${1:-report}"
cd "$(dirname "$0")/.."
case $action in
report)
generate_report
;;
containers)
check_containers
;;
health)
check_health
;;
database)
check_database
;;
disk)
check_disk
;;
memory)
check_memory
;;
logs)
check_logs
;;
quick)
# 快速检查(适合 cron
check_containers || exit 1
check_database || exit 1
check_disk || exit 1
;;
*)
echo "智能项目定价模型 - 监控检查脚本"
echo ""
echo "用法: $0 {report|containers|health|database|disk|memory|logs|quick}"
echo ""
echo "命令:"
echo " report 完整监控报告"
echo " containers 检查容器状态"
echo " health 检查服务健康"
echo " database 检查数据库"
echo " disk 检查磁盘空间"
echo " memory 检查内存使用"
echo " logs 检查错误日志"
echo " quick 快速检查(适合 cron"
echo ""
echo "环境变量:"
echo " ALERT_EMAIL 告警邮箱"
echo " WEBHOOK_URL 企业微信/钉钉 webhook"
;;
esac
}
main "$@"

235
scripts/setup-ssl.sh Executable file
View File

@@ -0,0 +1,235 @@
#!/bin/bash
# 智能项目定价模型 - SSL 证书配置脚本
# 使用 Let's Encrypt 申请免费 SSL 证书
# 遵循瑞小美部署规范
set -e
# 配置
DOMAIN="${DOMAIN:-pricing.example.com}"
EMAIL="${EMAIL:-admin@example.com}"
WEBROOT="/var/www/certbot"
CERT_DIR="/etc/letsencrypt/live/${DOMAIN}"
NGINX_SSL_DIR="/etc/nginx/ssl"
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 检查 certbot
check_certbot() {
if ! command -v certbot &> /dev/null; then
log_info "安装 certbot..."
# Debian/Ubuntu
if command -v apt-get &> /dev/null; then
apt-get update
apt-get install -y certbot
# CentOS/RHEL
elif command -v yum &> /dev/null; then
yum install -y epel-release
yum install -y certbot
else
log_error "不支持的系统,请手动安装 certbot"
exit 1
fi
fi
log_info "certbot 版本: $(certbot --version)"
}
# 创建 webroot 目录
create_webroot() {
if [ ! -d "$WEBROOT" ]; then
mkdir -p "$WEBROOT"
chmod 755 "$WEBROOT"
fi
}
# 申请证书
request_certificate() {
log_info "申请 SSL 证书: $DOMAIN"
# 确保 webroot 可访问
create_webroot
# 申请证书
certbot certonly \
--webroot \
--webroot-path="$WEBROOT" \
--email "$EMAIL" \
--agree-tos \
--no-eff-email \
-d "$DOMAIN"
if [ $? -eq 0 ]; then
log_info "证书申请成功"
else
log_error "证书申请失败"
exit 1
fi
}
# 复制证书到 Nginx 目录
copy_certificates() {
log_info "复制证书到 Nginx 配置目录..."
if [ ! -d "$NGINX_SSL_DIR" ]; then
mkdir -p "$NGINX_SSL_DIR"
fi
# Let's Encrypt 证书路径
if [ -d "$CERT_DIR" ]; then
cp "${CERT_DIR}/fullchain.pem" "${NGINX_SSL_DIR}/${DOMAIN}.pem"
cp "${CERT_DIR}/privkey.pem" "${NGINX_SSL_DIR}/${DOMAIN}.key"
chmod 600 "${NGINX_SSL_DIR}/${DOMAIN}.key"
log_info "证书已复制到 ${NGINX_SSL_DIR}/"
else
log_error "证书目录不存在: $CERT_DIR"
exit 1
fi
}
# 配置自动续期
setup_auto_renewal() {
log_info "配置证书自动续期..."
# 创建续期后的钩子脚本
local hook_script="/etc/letsencrypt/renewal-hooks/post/pricing-ssl-renewal.sh"
cat > "$hook_script" << 'EOF'
#!/bin/bash
# SSL 证书续期后自动更新
DOMAIN="pricing.example.com"
NGINX_SSL_DIR="/etc/nginx/ssl"
CERT_DIR="/etc/letsencrypt/live/${DOMAIN}"
cp "${CERT_DIR}/fullchain.pem" "${NGINX_SSL_DIR}/${DOMAIN}.pem"
cp "${CERT_DIR}/privkey.pem" "${NGINX_SSL_DIR}/${DOMAIN}.key"
chmod 600 "${NGINX_SSL_DIR}/${DOMAIN}.key"
# 重载 Nginx
docker exec nginx_proxy nginx -s reload 2>/dev/null || nginx -s reload 2>/dev/null || true
EOF
chmod +x "$hook_script"
# 测试续期
certbot renew --dry-run
log_info "自动续期已配置(每天自动检查)"
}
# 显示证书信息
show_certificate_info() {
log_info "证书信息:"
echo ""
if [ -f "${NGINX_SSL_DIR}/${DOMAIN}.pem" ]; then
openssl x509 -in "${NGINX_SSL_DIR}/${DOMAIN}.pem" -noout -subject -dates
else
log_warn "证书文件不存在"
fi
echo ""
}
# 手动证书配置说明
show_manual_instructions() {
cat << EOF
===============================================
手动配置 SSL 证书说明
===============================================
如果您已有 SSL 证书(购买的或其他方式获取),请按以下步骤配置:
1. 将证书文件复制到 Nginx SSL 目录:
mkdir -p /etc/nginx/ssl
cp your_certificate.pem /etc/nginx/ssl/${DOMAIN}.pem
cp your_private_key.key /etc/nginx/ssl/${DOMAIN}.key
chmod 600 /etc/nginx/ssl/${DOMAIN}.key
2. 确保 nginx.conf 中的证书路径正确:
ssl_certificate /etc/nginx/ssl/${DOMAIN}.pem;
ssl_certificate_key /etc/nginx/ssl/${DOMAIN}.key;
3. 重载 Nginx:
docker exec nginx_proxy nginx -s reload
===============================================
EOF
}
# 主函数
main() {
local action="${1:-help}"
case $action in
request)
check_certbot
request_certificate
copy_certificates
setup_auto_renewal
show_certificate_info
;;
renew)
certbot renew
copy_certificates
docker exec nginx_proxy nginx -s reload 2>/dev/null || log_warn "请手动重载 Nginx"
;;
copy)
copy_certificates
;;
info)
show_certificate_info
;;
manual)
show_manual_instructions
;;
*)
echo "智能项目定价模型 - SSL 证书配置"
echo ""
echo "用法: DOMAIN=your.domain.com EMAIL=your@email.com $0 {request|renew|copy|info|manual}"
echo ""
echo "命令:"
echo " request 申请 Let's Encrypt 证书"
echo " renew 续期证书"
echo " copy 复制证书到 Nginx 目录"
echo " info 显示证书信息"
echo " manual 显示手动配置说明"
echo ""
echo "环境变量:"
echo " DOMAIN 域名 (默认: pricing.example.com)"
echo " EMAIL 管理员邮箱 (默认: admin@example.com)"
echo ""
echo "示例:"
echo " DOMAIN=pricing.mycompany.com EMAIL=admin@mycompany.com $0 request"
;;
esac
}
main "$@"