# 一、数据表结构（monitor_rule 复合主键，支持规则+实例精细化管控）
## 建表SQL（先执行备份再运行）
```sql
USE alert_mail_stat;
DROP TABLE IF EXISTS monitor_rule;
CREATE TABLE IF NOT EXISTS monitor_rule (
    rule_name VARCHAR(128) NOT NULL COMMENT '监控规则名称，对应alert_name',
    instance VARCHAR(128) NOT NULL COMMENT '主机实例地址',
    status TINYINT NOT NULL DEFAULT 1 COMMENT '1=启用展示 0=下线隐藏',
    create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_at DATETIME NULL COMMENT '更新时间',
    PRIMARY KEY (rule_name, instance)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规则+实例存活注册表';
```

# 二、完整最终版 api/index.php（包含全部新增4个接口）
```php
<?php
session_start();
// 未登录直接拒绝所有接口访问
if(empty($_SESSION['user_id'])){
    header("Content-Type:application/json;charset=utf-8");
    echo json_encode(["code"=>401,"msg"=>"未登录，请前往登录页面"]);
    exit;
}
// 统一数据库配置
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";

// 统一数据库连接函数
function getDbConn() {
    global $dbHost, $dbUser, $dbPass, $dbName;
    $conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
    mysqli_set_charset($conn, "utf8mb4");
    return $conn;
}

$act = $_GET['act'] ?? '';

// CSV导出接口（独立流式下载，不输出JSON头）
if ($act === "export_csv") {
    $conn = getDbConn();
    if (!$conn) {
        header("Content-Type:text/html;charset=utf-8");
        echo "数据库连接失败：" . mysqli_connect_error();
        exit;
    }
    $today = date("Y-m-d");
    $sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
            FROM alert_log WHERE DATE(receive_time) = '$today' ORDER BY receive_time DESC";
    $res = mysqli_query($conn, $sql);

    ob_clean();
    header("Content-Type: text/csv; charset=utf-8");
    header("Content-Disposition: attachment; filename=告警报表_" . $today . ".csv");
    echo "\xEF\xBB\xBF";
    $header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"];
    echo implode(",", $header) . "\r\n";

    while ($row = mysqli_fetch_assoc($res)) {
        $status = $row['alert_type'] == 1 ? "触发" : "恢复";
        $line = [
            $status,
            $row['alert_name'],
            $row['instance'],
            $row['severity'],
            $row['starts_at'],
            $row['receive_time']
        ];
        foreach ($line as &$v) {
            $v = '"' . str_replace('"', '""', $v) . '"';
        }
        echo implode(",", $line) . "\r\n";
    }
    mysqli_close($conn);
    exit;
}

// 所有JSON接口统一返回头
header("Content-Type:application/json;charset=utf-8");
$conn = getDbConn();
if (!$conn) {
    echo json_encode(["code" => 500, "msg" => "数据库连接失败：" . mysqli_connect_error()], JSON_UNESCAPED_UNICODE);
    exit;
}
$today = date("Y-m-d");

// 接口1：下线整条监控规则（该规则下所有实例全部清理）
if($act === "clear_offline_rule"){
    $rule = $_GET['rule_name'] ?? '';
    if(empty($rule)){
        echo json_encode(["code"=>400,"msg"=>"缺少rule_name参数"],JSON_UNESCAPED_UNICODE);
        exit;
    }
    $ruleEsc = mysqli_real_escape_string($conn, $rule);
    // 标记该规则下全部实例为下线
    $updateSql = "UPDATE monitor_rule SET status=0, update_at=NOW() WHERE rule_name='$ruleEsc'";
    mysqli_query($conn, $updateSql);
    // 批量插入恢复记录，消除页面残留告警
    $insertSql = "
        INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
        SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
        FROM alert_log
        WHERE alert_name='$ruleEsc'
        AND (alert_name,instance) NOT IN (
            SELECT alert_name,instance FROM alert_log WHERE alert_type=2
        )
    ";
    mysqli_query($conn, $insertSql);
    echo json_encode(["code"=>0,"msg"=>"整条规则下线完成，所有实例告警已清除"],JSON_UNESCAPED_UNICODE);
    mysqli_close($conn);
    exit;
}

// 接口2：单独下线单台实例（仅隐藏指定机器，同规则其他主机不受影响）
if($act === "clear_single_instance"){
    $rule = $_GET['rule_name'] ?? '';
    $inst = $_GET['instance'] ?? '';
    if(empty($rule) || empty($inst)){
        echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
        exit;
    }
    $ruleEsc = mysqli_real_escape_string($conn, $rule);
    $instEsc = mysqli_real_escape_string($conn, $inst);
    // 仅给当前机器插入恢复日志
    $insertSql = "
        INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
        SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
        FROM alert_log
        WHERE alert_name='$ruleEsc' AND instance='$instEsc'
        AND (alert_name,instance) NOT IN (
            SELECT alert_name,instance FROM alert_log WHERE alert_type=2
        )
    ";
    mysqli_query($conn, $insertSql);
    echo json_encode(["code"=>0,"msg"=>"单实例告警清理完成"],JSON_UNESCAPED_UNICODE);
    mysqli_close($conn);
    exit;
}

// 接口3：恢复整条已下线规则（status改为1，重新展示所有实例告警）
if($act === "restore_rule"){
    $rule = $_GET['rule_name'] ?? '';
    if(empty($rule)){
        echo json_encode(["code"=>400,"msg"=>"缺少rule_name参数"],JSON_UNESCAPED_UNICODE);
        exit;
    }
    $ruleEsc = mysqli_real_escape_string($conn, $rule);
    $updateSql = "UPDATE monitor_rule SET status=1, update_at=NOW() WHERE rule_name='$ruleEsc'";
    mysqli_query($conn, $updateSql);
    echo json_encode(["code"=>0,"msg"=>"监控规则已恢复，新故障会正常展示"],JSON_UNESCAPED_UNICODE);
    mysqli_close($conn);
    exit;
}

// 1. 今日统计卡片
if ($act === "day_total") {
    $sql = "SELECT
        SUM(IF(alert_type=1,1,0)) as fire_count,
        SUM(IF(alert_type=2,1,0)) as resolve_count,
        COUNT(DISTINCT instance) as instance_num
        FROM alert_log WHERE DATE(receive_time) = '$today'";
    $data = mysqli_fetch_assoc(mysqli_query($conn, $sql));
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    mysqli_close($conn);
    exit;
}

// 2. 今日告警TOP10图表
if ($act === "top_alert") {
    $sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
    $list = [];
    $q = mysqli_query($conn, $sql);
    while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
    echo json_encode($list, JSON_UNESCAPED_UNICODE);
    mysqli_close($conn);
    exit;
}

// 3. 今日全量告警明细表格
if ($act === "log_list") {
    $sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
    FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
    $list = [];
    $q = mysqli_query($conn, $sql);
    while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
    echo json_encode($list, JSON_UNESCAPED_UNICODE);
    mysqli_close($conn);
    exit;
}

// 4. 全局跨天未恢复告警（过滤整条下线规则，单实例仅靠恢复日志隐藏）
if ($act === "active_firing") {
    $maxSql = "SELECT alert_name,instance,MAX(receive_time) max_rt FROM alert_log GROUP BY alert_name,instance";
    $maxRes = mysqli_query($conn, $maxSql);
    $activeList = [];
    while ($maxRow = mysqli_fetch_assoc($maxRes)) {
        $an = $conn->real_escape_string($maxRow['alert_name']);
        $ins = $conn->real_escape_string($maxRow['instance']);
        $mrt = $conn->real_escape_string($maxRow['max_rt']);

        // 判断该规则是否整条下线，下线直接跳过
        $ruleCheck = mysqli_query($conn, "SELECT status FROM monitor_rule WHERE rule_name='$an' LIMIT 1");
        $ruleStatus = 1;
        if($ruleCheck && $ruleRow = mysqli_fetch_assoc($ruleCheck)){
            $ruleStatus = intval($ruleRow['status']);
        }
        if($ruleStatus === 0){
            continue;
        }

        $rowSql = "SELECT alert_name,instance,severity,starts_at,receive_time,alert_type
                   FROM alert_log
                   WHERE alert_name='$an' AND instance='$ins' AND receive_time='$mrt'";
        $rowRes = mysqli_query($conn, $rowSql);
        $data = mysqli_fetch_assoc($rowRes);
        if ($data && (int)$data['alert_type'] === 1) {
            $activeList[] = $data;
        }
    }
    echo json_encode($activeList, JSON_UNESCAPED_UNICODE);
    mysqli_close($conn);
    exit;
}

// 非法请求兜底
echo json_encode(["code" => 400, "msg" => "无效请求参数act"], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
```

# 三、4个管控接口完整调用地址（示例）
## 1、下线整条规则（全部机器隐藏）
```
http://10.150.117.190:8080/api/index.php?act=clear_offline_rule&rule_name=Host_Ping_Offline
```

## 2、下线单台实例（仅隐藏 10.150.10.118，同规则其他机器正常）
```
http://10.150.117.190:8080/api/index.php?act=clear_single_instance&rule_name=Host_Ping_Offline&instance=10.150.10.118
```

## 3、恢复整条下线规则（重新展示该规则所有机器）
```
http://10.150.117.190:8080/api/index.php?act=restore_rule&rule_name=Host_Ping_Offline
```

## 4、兜底自动恢复机制（无需手动调用）
只要该规则下任意机器再次上报告警，webhook会自动将对应 rule_name+instance 状态置为1，自动恢复展示。

# 四、配套修改后的 webhook.py（同步规则+实例到monitor_rule）
```python
from flask import Flask, request, jsonify
import pymysql

app = Flask(__name__)

def get_db():
    return pymysql.connect(
        host="127.0.0.1",
        user="root",
        password="hp93000",
        database="alert_mail_stat",
        charset="utf8mb4"
    )

@app.route("/alert", methods=["POST"])
def alert_receive():
    try:
        data = request.get_json()
        alerts = data.get("alerts", [])
        insert_num = 0
        update_num = 0
        db = get_db()
        cur = db.cursor()

        for alert in alerts:
            fp = alert["fingerprint"]
            alert_name = alert["labels"].get("alertname", "")
            instance = alert["labels"].get("instance", "")
            severity = alert["labels"].get("severity", "warning")
            status = alert["status"]
            start_at = alert.get("startsAt", "")
            end_at = alert.get("endsAt")
            alert_type = 2 if status == "resolved" else 1
            content_str = str(alert)

            # 告警入库逻辑不变
            sql = """
            INSERT INTO alert_log(mail_uid,alert_type,alert_name,instance,severity,starts_at,ends_at,content,receive_time)
            VALUES(%s,%s,%s,%s,%s,%s,%s,%s,NOW())
            """
            cur.execute(sql, (fp, alert_type, alert_name, instance, severity, start_at, end_at, content_str))
            insert_num += 1

            # 同步 规则+实例 联合记录，存在自动置为启用1
            if alert_name and instance:
                sync_sql = """
                INSERT INTO monitor_rule (rule_name, instance, status)
                VALUES (%s, %s, 1)
                ON DUPLICATE KEY UPDATE status = 1
                """
                cur.execute(sync_sql, [alert_name, instance])

        db.commit()
        cur.close()
        db.close()
        return jsonify({"code": 200, "insert": insert_num, "update": update_num})

    except Exception as e:
        if "db" in locals():
            db.rollback()
            db.close()
        return jsonify({"code": 500, "msg": str(e)}), 200

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=909, debug=False)
```

# 五、定时兜底清理脚本 clear_offline_rule_cron.py
```python
import pymysql
DB_CFG = {
    "host": "127.0.0.1",
    "user": "root",
    "password": "hp93000",
    "database": "alert_mail_stat",
    "charset": "utf8mb4"
}

def run_clear():
    conn = pymysql.connect(**DB_CFG)
    cur = conn.cursor()
    cur.execute("SELECT DISTINCT rule_name FROM monitor_rule WHERE status=0")
    offline_rules = [row[0] for row in cur.fetchall()]
    for rule in offline_rules:
        insert_sql = """
        INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
        SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
        FROM alert_log
        WHERE alert_name=%s
        AND (alert_name,instance) NOT IN (
            SELECT alert_name,instance FROM alert_log WHERE alert_type=2
        )
        """
        cur.execute(insert_sql, [rule])
    conn.commit()
    cur.close()
    conn.close()

if __name__ == "__main__":
    run_clear()
```
定时任务配置：
```cron
0 * * * * /usr/bin/python3 /opt/alert_dashboard/clear_offline_rule_cron.py >> /opt/alert_dashboard/clear_rule.log 2>&1
```

# 六、整体逻辑说明
1. 单台机器下线：仅插入恢复日志，不修改monitor_rule；机器重新告警自动恢复
2. 整条规则下线：monitor_rule全实例status=0，批量补恢复日志；需调用restore_rule恢复
3. 兜底保障：每小时定时脚本自动清理下线规则残留告警
4. 自动激活：任意机器上报告警，webhook强制将该rule+instance置为启用