#!/bin/bash # 用户购课完课统计定时任务脚本 # 执行时间:每天9:00 # 输出:Excel报表,发送到指定群 set -euo pipefail # 配置参数 DB_HOST="bj-postgres-16pob4sg.sql.tencentcdb.com" DB_PORT="28591" DB_NAME="vala_bi" DB_USER="ai_member" SQL_PATH="/root/.openclaw/workspace/scripts/用户购课完课统计.sql" OUTPUT_CSV="/tmp/user_course_stat_$(date +%Y%m%d).csv" OUTPUT_EXCEL="/root/.openclaw/workspace/output/用户购课完课统计_$(date +%Y%m%d).xlsx" TARGET_CHAT_ID="oc_af81515caefe26918736ad1941286224" LOG_FILE="/var/log/user_course_stat.log" # 确保 PATH 包含必要工具 export PATH="/usr/bin:/bin:/usr/local/bin:/root/.nvm/versions/node/v24.14.0/bin:$PATH" # 加载数据库密码 source /root/.openclaw/workspace/secrets.env log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" >&2; } log "========== 开始执行用户购课完课统计任务 ==========" # 1. 运行SQL导出CSV log "执行SQL查询..." export PGPASSWORD="${PG_ONLINE_PASSWORD}" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" --csv -f "$SQL_PATH" > "$OUTPUT_CSV" 2>> "$LOG_FILE" if [ ! -s "$OUTPUT_CSV" ]; then log "ERROR: SQL查询结果为空" exit 1 fi log "SQL查询完成,CSV大小: $(wc -c < "$OUTPUT_CSV") bytes" # 2. CSV转Excel log "生成Excel..." python3 -c " import pandas as pd import numpy as np try: np._get_promotion_state = lambda *args, **kwargs: 0 except: pass df = pd.read_csv('${OUTPUT_CSV}', low_memory=False) with pd.ExcelWriter('${OUTPUT_EXCEL}') as writer: df.to_excel(writer, sheet_name='用户统计数据', index=False) " 2>> "$LOG_FILE" if [ ! -f "$OUTPUT_EXCEL" ]; then log "ERROR: Excel生成失败" exit 1 fi log "Excel生成完成,大小: $(wc -c < "$OUTPUT_EXCEL") bytes" # 3. 获取飞书token log "获取飞书访问令牌..." APP_ID=$(jq -r '.apps[0].appId' /root/.openclaw/credentials/xiaoxi/config.json) APP_SECRET=$(jq -r '.apps[0].appSecret' /root/.openclaw/credentials/xiaoxi/config.json) TOKEN=$(curl -s -X POST "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" \ -H "Content-Type: application/json" \ -d "{\"app_id\":\"$APP_ID\",\"app_secret\":\"$APP_SECRET\"}" \ | jq -r '.tenant_access_token') if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then log "ERROR: 获取飞书token失败" exit 1 fi # 4. 先发文本消息 log "发送文本通知..." TEXT_PAYLOAD=$(jq -n --arg rid "$TARGET_CHAT_ID" --arg text "【每日定时统计】$(date +%Y年%m月%d日)用户购课与完课情况统计,请查收附件。" \ '{receive_id: $rid, msg_type: "text", content: ({text: $text} | tostring)}') TEXT_RESP=$(curl -s -X POST "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "$TEXT_PAYLOAD") TEXT_CODE=$(echo "$TEXT_RESP" | jq -r '.code') if [ "$TEXT_CODE" != "0" ]; then log "WARN: 文本消息发送失败: $TEXT_RESP" fi # 5. 上传Excel文件(带重试) upload_file() { local attempt=1 local max_attempts=3 while [ $attempt -le $max_attempts ]; do log "上传文件 (第${attempt}次尝试)..." local fk fk=$(curl -s --max-time 30 -X POST "https://open.feishu.cn/open-apis/im/v1/files" \ -H "Authorization: Bearer $TOKEN" \ -F "file_type=xls" \ -F "file_name=用户购课完课统计_$(date +%Y%m%d).xlsx" \ -F "file=@${OUTPUT_EXCEL}" | jq -r '.data.file_key') if [ -n "$fk" ] && [ "$fk" != "null" ]; then echo "$fk" return 0 fi log "WARN: 第${attempt}次文件上传失败,等待3秒后重试..." sleep 3 attempt=$((attempt + 1)) done return 1 } FILE_KEY=$(upload_file) if [ -z "$FILE_KEY" ] || [ "$FILE_KEY" = "null" ]; then log "ERROR: 文件上传失败(已重试3次)" exit 1 fi log "文件上传成功,file_key: ${FILE_KEY}" # 6. 发送文件消息到群(带重试) send_file_msg() { local attempt=1 local max_attempts=3 local fk="$1" while [ $attempt -le $max_attempts ]; do log "发送文件消息 (第${attempt}次尝试)..." local payload payload=$(jq -n --arg rid "$TARGET_CHAT_ID" --arg fk "$fk" \ '{receive_id: $rid, msg_type: "file", content: ({file_key: $fk} | tostring)}') local resp resp=$(curl -s --max-time 15 -X POST "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "$payload") local code code=$(echo "$resp" | jq -r '.code') if [ "$code" = "0" ]; then log "文件消息发送成功" return 0 fi log "WARN: 第${attempt}次文件消息发送失败 (code=${code}): $resp" sleep 3 attempt=$((attempt + 1)) done return 1 } if ! send_file_msg "$FILE_KEY"; then log "ERROR: 文件消息发送失败(已重试3次)" exit 1 fi # 7. 清理临时CSV文件 rm -f "$OUTPUT_CSV" log "========== 任务执行完成 =========="