diff --git a/README.md b/README.md index 1ad73f3..3bfded5 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,10 @@ systemctl start oucshell.service systemctl enable oucshell.service ``` +# 效果演示 + +![img.png](show.png) + ## 🤝 贡献者 diff --git a/main.sh b/main.sh new file mode 100644 index 0000000..17c50cd --- /dev/null +++ b/main.sh @@ -0,0 +1,198 @@ +#!/bin/bash + +# ========================================== +# 主控脚本 +# ========================================== + +# --- 1. 路径定义 --- +BASE_DIR=$(cd $(dirname $0); pwd) +SRC_DIR="$BASE_DIR/src" +CONFIG_FILE="$BASE_DIR/config.toml" +LOG_FILE="$BASE_DIR/service.log" + +# 日志 +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" +} + +trap "log '服务停止'; exit" SIGTERM SIGINT + +# 依赖检查和安装 +check_and_install_dependencies() { + local dependencies=("curl" "jq" "bc") + local missing_packages=() + + for pkg in "${dependencies[@]}"; do + if ! command -v "$pkg" &> /dev/null; then + missing_packages+=("$pkg") + fi + done + + if [ ${#missing_packages[@]} -eq 0 ]; then + log "环境检查通过: 依赖已就绪。" + return 0 + fi + + log "检测到缺失依赖: ${missing_packages[*]},尝试自动安装..." + + local install_cmd="" + if command -v apt-get &> /dev/null; then + install_cmd="apt-get update && apt-get install -y" + elif command -v yum &> /dev/null; then + install_cmd="yum install -y" + elif command -v dnf &> /dev/null; then + install_cmd="dnf install -y" + elif command -v apk &> /dev/null; then + install_cmd="apk add --no-cache" + else + log "无法自动安装,请手动安装: ${missing_packages[*]}" + exit 1 + fi + + if [ "$EUID" -ne 0 ] && command -v sudo &> /dev/null; then + install_cmd="sudo $install_cmd" + fi + + # 这里也将安装日志通过管道传递给 log 函数,保持日志格式统一 + eval "$install_cmd ${missing_packages[*]}" 2>&1 | while IFS= read -r line; do + log "[依赖安装] $line" + done + + # 二次检查 + for pkg in "${missing_packages[@]}"; do + if ! command -v "$pkg" &> /dev/null; then + log "依赖 $pkg 安装失败,请检查网络或源。" + exit 1 + fi + done + log "依赖安装完成。" +} + +# 配置文件生成与检查 +check_and_create_config() { + if [ ! -f "$CONFIG_FILE" ]; then + log "配置文件不存在,正在生成默认配置..." + cat > "$CONFIG_FILE" << EOF +# ========================================== +# OUCShell配置文件 +# ========================================== + +[Global] +# 接收通知的邮箱 +TargetEmail = "your_email@example.com" + +[SMTP] +# 发件人邮箱服务器配置 +Host = "smtp.qq.com" +Port = "465" +User = "your_smtp_email@qq.com" +Password = "your_smtp_auth_code" + +# 电费提醒模块 +[Electricity] +Enabled = true # 是否启用本模块 +Campus = "xha" # 校区选择 +# xha = 西海岸 + +[Electricity.xha] +StudentID = "XXX" # 学号 +Token = "9f7c6e76979c4cb9dd3828f8cc44a5ef" # MD5(Sd1234) 居然加密这么简单吗? +# [照明警戒值, 空调警戒值] +RemindTime = [30.0, 30.0] + +# 网费提醒模块 +[Internet] +Enabled = true # 是否启用本模块 +Campus = "xha" # 校区选择 +# xha = 西海岸 + +[Internet.xha] +StudentID = "XXX" # 学号 +# [最低余额, 触发天数] +# 触发天数: 离下个月1号还有几天时开始检测。 +# e.g. +# 填 -1 表示忽略日期,只要余额低就提醒。 +# 填 5 表示只有余额低 且 离月底少于5天时才提醒。 +RemindTime = [10, -1] +EOF + log "配置文件已生成: $CONFIG_FILE" + log "检测到第一次运行,脚本将自动退出,请编辑配置文件后重新启动" + exit 0 + else + log "加载配置文件: $CONFIG_FILE" + fi +} + +# 主函数 + +log "服务启动..." + +check_and_install_dependencies +check_and_create_config + +# 电费提醒任务间隔 (秒) +INTERVAL_ELEC=7200 # 2小时 +LAST_RUN_ELEC=0 + +# 网费提醒任务间隔 (秒) +INTERVAL_NET=43200 # 半天 +LAST_RUN_NET=0 + +log "进入循环调度模式..." + +while true; do + CURRENT_TIME=$(date +%s) + + # 电费监控 + + # 计算时间差 + TIME_DIFF=$((CURRENT_TIME - LAST_RUN_ELEC)) + + if [ $TIME_DIFF -ge $INTERVAL_ELEC ]; then + SCRIPT_PATH="$SRC_DIR/elec_monitor.sh" + + if [ -f "$SCRIPT_PATH" ]; then + chmod +x "$SCRIPT_PATH" + log "调度任务: 电费监控..." + + /bin/bash "$SCRIPT_PATH" "$CONFIG_FILE" 2>&1 | while IFS= read -r line; do + log "[elec_monitor] $line" + done + + log "任务结束: 电费监控" + else + log "警告: 找不到脚本 $SCRIPT_PATH" + fi + + LAST_RUN_ELEC=$(date +%s) + fi + + # 网费监控 + + # 计算时间差 + TIME_DIFF=$((CURRENT_TIME - LAST_RUN_NET)) + + if [ $TIME_DIFF -ge $INTERVAL_NET ]; then + SCRIPT_PATH="$SRC_DIR/internet_monitor.sh" + + if [ -f "$SCRIPT_PATH" ]; then + chmod +x "$SCRIPT_PATH" + log "调度任务: 网费监控..." + + /bin/bash "$SCRIPT_PATH" "$CONFIG_FILE" 2>&1 | while IFS= read -r line; do + log "[net_monitor] $line" + done + + log "任务结束: 网费监控" + else + log "警告: 找不到脚本 $SCRIPT_PATH" + fi + + LAST_RUN_NET=$(date +%s) + fi + + # 其他任务(预留) + # TODO + + sleep 60 +done \ No newline at end of file diff --git a/show.png b/show.png new file mode 100644 index 0000000..bd3e9b6 Binary files /dev/null and b/show.png differ diff --git a/src/elec_monitor.sh b/src/elec_monitor.sh new file mode 100644 index 0000000..8e8171a --- /dev/null +++ b/src/elec_monitor.sh @@ -0,0 +1,157 @@ +#!/bin/bash + +# ========================================== +# 电费提醒功能脚本 +# ========================================== + +CONFIG_PATH="$1" + +# 检查参数 +if [ -z "$CONFIG_PATH" ]; then + echo "错误: 未传入配置文件路径。" + exit 1 +fi + +if [ ! -f "$CONFIG_PATH" ]; then + echo "错误: 配置文件不存在 ($CONFIG_PATH)" + exit 1 +fi + +# 配置读取 +get_val() { + local section=$1 + local key=$2 + local safe_section=$(echo "$section" | sed 's/\./\\./g') + + sed -n "/^\[$safe_section\]/,/^\[/p" "$CONFIG_PATH" \ + | grep "^$key" \ + | head -n 1 \ + | awk -F'=' '{print $2}' \ + | tr -d ' "' \ + | sed 's/#.*//' \ + | tr -d '\r' +} + +# 邮件发送函数 +send_email() { + local subject=$1 + local content=$2 + + local smtp_host=$(get_val "SMTP" "Host") + local smtp_port=$(get_val "SMTP" "Port") + local smtp_user=$(get_val "SMTP" "User") + local smtp_pass=$(get_val "SMTP" "Password") + local target_email=$(get_val "Global" "TargetEmail") + + if [ -z "$smtp_user" ] || [ -z "$target_email" ]; then + echo "错误: 邮箱配置不完整,跳过发送。" + return + fi + + echo "正在发送邮件到 $target_email ..." + + local curl_url="smtp://$smtp_host:$smtp_port" + if [ "$smtp_port" == "465" ]; then + curl_url="smtps://$smtp_host:$smtp_port" + fi + + local mail_data="From: \"电费助手\" <$smtp_user> +To: <$target_email> +Subject: $subject +MIME-Version: 1.0 +Content-Type: text/html; charset=utf-8 + +$content" + + echo "$mail_data" | curl --silent --ssl-reqd \ + --url "$curl_url" \ + --user "$smtp_user:$smtp_pass" \ + --mail-from "$smtp_user" \ + --mail-rcpt "$target_email" \ + --upload-file - +} + +# 西海岸电费获取 +check_xha() { + local sno=$(get_val "Electricity.xha" "StudentID") + local token=$(get_val "Electricity.xha" "Token") + + local thresholds_raw=$(get_val "Electricity.xha" "RemindTime" | tr -d '[]' | tr ',' ' ') + + local light_limit=$(echo $thresholds_raw | awk '{print $1}') + local ac_limit=$(echo $thresholds_raw | awk '{print $2}') + + # 如果解析失败默认设为0 + light_limit=${light_limit:-0} + ac_limit=${ac_limit:-0} + + if [ -z "$sno" ] || [ -z "$token" ]; then + echo "错误: 学号或Token未配置,请修改 config.toml" + return + fi + + echo "查询西海岸校区... (学号: $sno)" + + local response=$(curl -s -X POST "http://10.128.13.25/hydxcas/getCadByNo" \ + -H "Token: $token" \ + -H "Content-Type: application/json" \ + -d "{\"sno\": \"$sno\"}") + + local errcode=$(echo "$response" | jq -r '.errcode') + + if [ "$errcode" != "0" ]; then + echo "API 请求失败: $(echo "$response" | jq -r '.errmsg')" + return + fi + + # 解析数据 + local light_val=$(echo "$response" | jq -r '.value | fromjson | .eqptData[] | select(.categoryEnergyName == "照明与插座") | .buyElec') + local ac_val=$(echo "$response" | jq -r '.value | fromjson | .eqptData[] | select(.categoryEnergyName == "空调末端") | .buyElec') + + light_val=${light_val:-0} + ac_val=${ac_val:-0} + + echo "当前电量 -> 照明: $light_val, 空调: $ac_val" + echo "警戒阈值 -> 照明: $light_limit, 空调: $ac_limit" + + local need_alert=0 + local msg="

电费余额不足提醒

请及时充值!

" + + if [ $need_alert -eq 1 ]; then + send_email "【紧急】宿舍电费余额不足" "$msg" + else + echo "电量充足,无需提醒。" + fi +} + +# 主函数部分 + +ENABLED=$(get_val "Electricity" "Enabled") +if [ "$ENABLED" != "true" ]; then + echo "电费监控模块未启用。" + exit 0 +fi + +CAMPUS=$(get_val "Electricity" "Campus") + +case "$CAMPUS" in + "xha") + check_xha + ;; + *) + echo "未知的校区配置: $CAMPUS" + ;; +esac \ No newline at end of file diff --git a/src/internet_monitor.sh b/src/internet_monitor.sh new file mode 100644 index 0000000..6d390b0 --- /dev/null +++ b/src/internet_monitor.sh @@ -0,0 +1,171 @@ +#!/bin/bash + +# ========================================== +# 网费提醒功能脚本 +# ========================================== + +CONFIG_PATH="$1" + +# 基础检查 +if [ -z "$CONFIG_PATH" ] || [ ! -f "$CONFIG_PATH" ]; then + echo "错误: 配置文件未找到 ($CONFIG_PATH)" + exit 1 +fi + +# 读取配置 +get_val() { + local section=$1 + local key=$2 + local safe_section=$(echo "$section" | sed 's/\./\\./g') + + sed -n "/^\[$safe_section\]/,/^\[/p" "$CONFIG_PATH" \ + | grep "^$key" \ + | head -n 1 \ + | awk -F'=' '{print $2}' \ + | tr -d ' "' \ + | sed 's/#.*//' \ + | tr -d '\r' +} + +# 邮件发送函数 +send_email() { + local subject=$1 + local content=$2 + + local smtp_host=$(get_val "SMTP" "Host") + local smtp_port=$(get_val "SMTP" "Port") + local smtp_user=$(get_val "SMTP" "User") + local smtp_pass=$(get_val "SMTP" "Password") + local target_email=$(get_val "Global" "TargetEmail") + + if [ -z "$smtp_user" ] || [ -z "$target_email" ]; then + echo "错误: 邮箱配置不完整,跳过发送。" + return + fi + + echo "正在发送邮件到 $target_email ..." + + local curl_url="smtp://$smtp_host:$smtp_port" + if [ "$smtp_port" == "465" ]; then curl_url="smtps://$smtp_host:$smtp_port"; fi + + local mail_data="From: \"校园助手\" <$smtp_user> +To: <$target_email> +Subject: $subject +MIME-Version: 1.0 +Content-Type: text/html; charset=utf-8 + +$content" + + echo "$mail_data" | curl --silent --ssl-reqd --url "$curl_url" --user "$smtp_user:$smtp_pass" --mail-from "$smtp_user" --mail-rcpt "$target_email" --upload-file - +} + +# 西海岸网费检查 +check_xha_net() { + local sno=$(get_val "Internet.xha" "StudentID") + + # 解析 RemindTime = [10, 5] -> money_limit=10, day_limit=5 + local config_raw=$(get_val "Internet.xha" "RemindTime" | tr -d '[]' | tr ',' ' ') + local money_limit=$(echo $config_raw | awk '{print $1}') + local day_limit=$(echo $config_raw | awk '{print $2}') + + # 默认值保护 + money_limit=${money_limit:-10} + day_limit=${day_limit:--1} + + if [ -z "$sno" ]; then + echo "错误: 未配置学号 (StudentID)" + return + fi + + echo "查询西海岸校区网费... (学号: $sno)" + + # 1. 发送请求 + local url="https://xha.ouc.edu.cn:802/eportal/portal/page/loadUserInfo?callback=dr1002&lang=zh-CN&program_index=ctshNw1713845951&page_index=V5fmKw1713845966&user_account=${sno}&wlan_user_ip=0.0.0.0&wlan_user_mac=000000000000&jsVersion=4.1&v=3015&lang=zh" + + local response=$(curl -k -s --http1.1 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" "$url") + + # 2. 解析 JSONP + # 返回格式: dr1002({ ... }); 需要去掉 dr1002( 和 ); + local json_body=$(echo "$response" | sed -E 's/^[^(]*\(//;s/\)[^)]*$//') + + # 检查 parsed JSON 是否有效 + local code=$(echo "$json_body" | jq -r '.code') + if [ "$code" != "1" ]; then + echo "API 请求失败或未登录: $(echo "$json_body" | jq -r '.msg')" + return + fi + + # 3. 提取余额 + # 原始值可能是 "0 元" 或 "12.50 元" + local balance_str=$(echo "$json_body" | jq -r '.user_info.balance') + # 只保留数字和小数点 + local balance_num=$(echo "$balance_str" | sed 's/[^0-9.]//g') + + # 提取已用流量用于展示 + local flow_str=$(echo "$json_body" | jq -r '.user_info.use_flow') + + balance_num=${balance_num:-0} + echo "当前余额: ${balance_num} 元, 已用流量: ${flow_str}" + + # 4. 判断是否需要提醒 + local need_alert=0 + local reason="" + + # 只有当余额低于阈值才考虑发信 + if (( $(echo "$balance_num < $money_limit" | bc -l 2>/dev/null) )); then + + # 4.1 如果 day_limit 是 -1,不看日期,直接发 + if [ "$day_limit" == "-1" ]; then + need_alert=1 + reason="余额低于 ${money_limit} 元" + else + # 4.2 如果配置了日期限制,检查是否接近月底 + # 计算距离下个月1号还有几天 + local next_month_ts=$(date -d "$(date +%Y-%m-01) +1 month" +%s) + local now_ts=$(date +%s) + local diff_sec=$((next_month_ts - now_ts)) + local diff_days=$((diff_sec / 86400)) + + echo "距离月底结算还有: ${diff_days} 天 (配置阈值: ${day_limit} 天)" + + if [ "$diff_days" -le "$day_limit" ]; then + need_alert=1 + reason="临近月底且余额低于 ${money_limit} 元" + else + echo "余额虽然不足,但未到提醒日期 (当前 ${diff_days} > 阈值 ${day_limit}),跳过。" + fi + fi + fi + + # 5. 发送通知 + if [ $need_alert -eq 1 ]; then + local msg="

校园网费余额预警

+ +

请确认是否需要充值以避免下月断网。

" + + send_email "【提醒】校园网费余额不足 (${balance_num}元)" "$msg" + fi +} + +# 主函数 +ENABLED=$(get_val "Internet" "Enabled") +if [ "$ENABLED" != "true" ]; then + echo "网费监控模块未启用。" + exit 0 +fi + +CAMPUS=$(get_val "Internet" "Campus") + +case "$CAMPUS" in + "xha") + check_xha_net + ;; + *) + echo "未知的校区配置: $CAMPUS" + ;; +esac \ No newline at end of file