<!--
#Create by:ember.zhang
#为什么不问问神奇的海螺呢丶
--><!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>{{ report_data.title }}</title><style>/* 基础样式(保留原有,增强响应式) */:root{--primary: #165DFF;--secondary: #36CFC9;--success: #00B42A;--warning: #FF7D00;--danger: #F53F3F;--dark: #1D2129;--dark-2: #4E5969;--dark-3: #86909C;--light-1: #F2F3F5;--light-2: #F7F8FA;--light-3: #FFFFFF;}*{margin: 0;padding: 0;box-sizing: border-box;font-family: -apple-system, BlinkMacSystemFont,"Segoe UI", Roboto,"Helvetica Neue", Arial, sans-serif;}body{background-color: #F5F7FA;color:var(--dark);line-height: 1.6;padding: 10px;}.container{max-width: 100%;margin: 0 auto;padding: 15px;}/* 标题样式 */.page-title{text-align: center;margin-bottom: 20px;padding-bottom: 15px;border-bottom: 1px solid #E5E6EB;}.page-title h1{font-size: 24px;font-weight: 700;color:var(--dark);margin-bottom: 10px;}.page-title p{color:var(--dark-3);font-size: 13px;line-height: 1.5;}/* 卡片样式 */.card{background-color:var(--light-3);border-radius: 8px;box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);margin-bottom: 20px;overflow: hidden;transition: transform 0.3s ease, box-shadow 0.3s ease;}.card-header{padding: 12px 15px;background-color:var(--light-2);border-bottom: 1px solid #E5E6EB;}.card-header h2{font-size: 16px;font-weight: 600;color:var(--dark);margin: 0;}.card-body{padding: 15px;}/* 表格样式(重点优化:表头冻结) */.table-wrapper{overflow-x: auto;overflow-y: auto;/* 纵向滚动 */-webkit-overflow-scrolling: touch;margin-bottom: 15px;border-radius: 6px;border: 1px solid #E5E6EB;max-height: 500px;/* 表格最大高度,超出则滚动 */}.data-table{width: 100%;border-collapse: collapse;min-width: 1200px;/* 保证表格最小宽度,避免挤压 */}.data-table th,
.data-table td{padding: 8px 10px;text-align: left;border-bottom: 1px solid #E5E6EB;font-size: 12px;/* 缩小字体,增加列数容纳度 */white-space: nowrap;/* 关键列不换行,非关键列单独设置换行 */vertical-align: top;}.data-table th{background-color:var(--light-2);font-weight: 600;color:var(--dark-2);position: sticky;top: 0;/* 表头固定在容器顶部 */z-index: 10;/* 避免被内容覆盖 */box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);/* 增加阴影区分表头和内容 */}.data-table tr:hover{background-color:var(--light-1);}.data-table tr.danger td{background-color:rgba(245, 63, 63, 0.05);color:var(--danger);}.data-table tr.warning td{background-color:rgba(255, 125, 0, 0.05);color:var(--warning);}/* 长文本列允许换行 */.data-table td.long-text{white-space: pre-wrap;word-break: break-all;max-width: 300px;}.text-danger{color:var(--danger)!important;font-weight: 600;}.text-warning{color:var(--warning)!important;font-weight: 600;}/* 指标卡片 */.metrics{display: flex;flex-wrap: wrap;gap: 10px;margin-bottom: 15px;}.metric-card{flex: 1 1 150px;background-color:var(--light-3);border-radius: 6px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);padding: 12px;min-width: 120px;}.metric-card h3{font-size: 12px;color:var(--dark-3);margin-bottom: 8px;}.metric-value{font-size: 20px;font-weight: bold;color:var(--dark);margin-bottom: 3px;}.metric-unit{font-size: 11px;color:var(--dark-3);}.metric-card.warning .metric-value{color:var(--warning);}.metric-card.danger .metric-value{color:var(--danger);}/* SQL样式(重点优化) */.sql-block{background-color: #F5F7FA;padding: 12px;margin-top: 8px;border-radius: 4px;font-family:"Consolas","Monaco", monospace;font-size: 11px;line-height: 1.5;overflow-x: auto;white-space: pre;border: 1px solid #E5E6EB;}.sql-collapsed{height: 60px;overflow: hidden;position: relative;}.sql-mask{position: absolute;bottom: 0;left: 0;right: 0;height: 30px;background:linear-gradient(to top,var(--light-3), transparent);display: block;}.sql-toggle-btn{border: none;background:var(--light-2);color:var(--primary);padding: 4px 8px;border-radius: 4px;font-size: 12px;cursor: pointer;transition: background 0.2s ease;}.sql-toggle-btn:hover{background:var(--light-1);}/* 慢SQL卡片列表 */.sql-card-list{display: flex;flex-direction: column;gap: 15px;}/* 选项卡样式 */.tabs{display: flex;flex-wrap: wrap;margin-bottom: 15px;border-bottom: 1px solid #E5E6EB;gap: 5px;overflow-x: auto;padding-bottom: 5px;}.tab{padding: 8px 12px;cursor: pointer;border-bottom: 2px solid transparent;transition: all 0.3s ease;font-weight: 500;white-space: nowrap;font-size: 13px;}.tab:hover{background-color:var(--light-1);}.tab.active{border-bottom-color:var(--primary);color:var(--primary);}.tab-content{display: none;}.tab-content.active{display: block;}/* 响应式补充 */@media(max-width: 768px){.metrics{flex-direction: column;}.metric-card{width: 100%;}.page-title h1{font-size: 20px;}.data-table th, .data-table td{padding: 6px 8px;font-size: 11px;}.sql-collapsed{height: 40px;}.sql-mask{height: 20px;}.table-wrapper{max-height: 300px;/* 移动端缩小表格最大高度 */}}</style></head><body><divclass="container"><!-- 报告标题 --><divclass="page-title"><h1>{{ report_data.title }}</h1><p>数据库IP: {{ report_data.tb_zdb.dbip }} | 数据库: {{ report_data.tb_zdb.dbname }} | 版本: {{ report_data.tb_zdb.dbversion }} | 生成时间: {{ generate_time }}</p></div><!-- 数据库基本信息 --><divclass="card"><divclass="card-header"><h2>数据库基本信息</h2></div><divclass="card-body"><divclass="metrics"><divclass="metric-card"><h3>数据库名称</h3><divclass="metric-value">{{ report_data.tb_zdb.dbname }}</div></div><divclass="metric-card"><h3>数据库类型</h3><divclass="metric-value">{{ report_data.tb_zdb.dbtype }}</div></div><divclass="metric-card"><h3>数据库版本</h3><divclass="metric-value">{{ report_data.tb_zdb.dbversion }}</div></div><divclass="metric-card"><h3>日志模式</h3><divclass="metric-value {% if report_data.tb_zdb.logmode != 'ARCHIVELOG' %}text-warning{% endif %}">{{ report_data.tb_zdb.logmode }}</div></div><divclass="metric-card"><h3>数据库角色</h3><divclass="metric-value">{{ report_data.tb_zdb.dbrole }}</div></div><divclass="metric-card"><h3>启动时间</h3><divclass="metric-value">{{ report_data.tb_zdb.uptime }}</div></div>
{% if report_data.tb_zdb.israc == 1 %}
<divclass="metric-card"><h3>RAC实例数</h3><divclass="metric-value">{{ report_data.tb_zdb.racnum }}</div></div>
{% endif %}
</div><!-- 数据库参数 --><h3style="font-size:14px;margin-bottom:10px;">关键数据库参数</h3><divclass="table-wrapper"><tableclass="data-table"><thead><tr><thstyle="min-width:120px;">参数名称</th><thstyle="min-width:100px;">参数值</th><th>描述</th></tr></thead><tbody>
{% for param in report_data.tb_zdb_parameter %}
<tr><td>{{ param.pname }}</td><td>{{ param.pvalue }}</td><tdclass="long-text">{{ param.description }}</td></tr>
{% endfor %}
</tbody></table></div></div></div><!-- 巡检结果 --><divclass="card"><divclass="card-header"><h2>巡检详细结果</h2></div><divclass="card-body"><!-- 定义标签页标题映射 -->
{% set tab_config = {
'space': '表空间信息',
'asm': 'ASM磁盘组信息',
'user': '用户信息',
'slowsql': '慢SQL信息',
'inst': '实例信息',
'dblink': 'DBLink信息',
'parttable': '分区表信息',
'partindex': '分区索引信息',
'redolog': '重做日志信息'
} %}
<!-- 指标选择标签页 --><divclass="tabs">
{% for key, title in tab_config.items() %}
<divclass="tab {% if loop.first %}active{% endif %}"data-tab="{{ key }}">{{ title }}</div>
{% endfor %}
</div><!-- 表空间信息 --><divclass="tab-content active"id="space"><h3style="font-size:14px;margin-bottom:10px;">表空间使用情况</h3><divclass="metrics"><divclass="metric-card"><h3>表空间总数</h3><divclass="metric-value">{{ report_data.tb_zdb_space|length }}</div></div><divclass="metric-card warning"><h3>高使用率表空间(>90%)</h3><divclass="metric-value">
{% set high_usage = [] %}
{% for ts in report_data.tb_zdb_space %}
{% if ts.percent and ts.percent|replace('%', '')|float > 90 %}
{% set _ = high_usage.append(1) %}
{% endif %}
{% endfor %}
{{ high_usage|length }}
</div></div><divclass="metric-card danger"><h3>使用率超95%表空间</h3><divclass="metric-value">
{% set critical_usage = [] %}
{% for ts in report_data.tb_zdb_space %}
{% if ts.percent and ts.percent|replace('%', '')|float > 95 %}
{% set _ = critical_usage.append(1) %}
{% endif %}
{% endfor %}
{{ critical_usage|length }}
</div></div></div><divclass="table-wrapper"><tableclass="data-table"><thead><tr><thstyle="min-width:120px;">表空间名称</th><thstyle="min-width:80px;">总大小</th><thstyle="min-width:80px;">已使用</th><thstyle="min-width:80px;">空闲</th><thstyle="min-width:80px;">使用率</th></tr></thead><tbody>
{% for ts in report_data.tb_zdb_space %}
<trclass="{% if ts.percent and ts.percent|replace('%', '')|float > 95 %}danger{% elif ts.percent and ts.percent|replace('%', '')|float > 90 %}warning{% endif %}"><td>{{ ts.tablespace }}</td><td>{{ ts.total }}</td><td>{{ ts.used }}</td><td>{{ ts.free }}</td><tdclass="{% if ts.percent and ts.percent|replace('%', '')|float > 95 %}text-danger{% elif ts.percent and ts.percent|replace('%', '')|float > 90 %}text-warning{% endif %}">{{ ts.percent }}</td></tr>
{% endfor %}
</tbody></table></div></div><!-- ASM信息 --><divclass="tab-content"id="asm"><h3style="font-size:14px;margin-bottom:10px;">ASM磁盘组信息</h3><divclass="metrics"><divclass="metric-card"><h3>ASM磁盘组总数</h3><divclass="metric-value">{{ report_data.tb_zdb_asm|length }}</div></div><divclass="metric-card warning"><h3>高使用率磁盘组(>80%)</h3><divclass="metric-value">
{% set high_asm = [] %}
{% for asm in report_data.tb_zdb_asm %}
{% if asm.percent and asm.percent|replace('%', '')|float > 80 %}
{% set _ = high_asm.append(1) %}
{% endif %}
{% endfor %}
{{ high_asm|length }}
</div></div></div><divclass="table-wrapper"><tableclass="data-table"><thead><tr><thstyle="min-width:120px;">磁盘组名称</th><thstyle="min-width:80px;">总大小</th><thstyle="min-width:80px;">已使用</th><thstyle="min-width:80px;">空闲</th><thstyle="min-width:80px;">使用率</th></tr></thead><tbody>
{% for asm in report_data.tb_zdb_asm %}
<trclass="{% if asm.percent and asm.percent|replace('%', '')|float > 90 %}danger{% elif asm.percent and asm.percent|replace('%', '')|float > 80 %}warning{% endif %}"><td>{{ asm.name }}</td><td>{{ asm.total }}</td><td>{{ asm.used }}</td><td>{{ asm.free }}</td><tdclass="{% if asm.percent and asm.percent|replace('%', '')|float > 90 %}text-danger{% elif asm.percent and asm.percent|replace('%', '')|float > 80 %}text-warning{% endif %}">{{ asm.percent }}</td></tr>
{% endfor %}
</tbody></table></div></div><!-- 用户信息 --><divclass="tab-content"id="user"><h3style="font-size:14px;margin-bottom:10px;">数据库用户信息</h3><divclass="metrics"><divclass="metric-card"><h3>用户总数</h3><divclass="metric-value">{{ report_data.tb_zdb_user|length }}</div></div><divclass="metric-card warning"><h3>锁定用户数</h3><divclass="metric-value">
{% set locked_users = [] %}
{% for user in report_data.tb_zdb_user %}
{% if user.userstatus and 'LOCKED' in user.userstatus %}
{% set _ = locked_users.append(1) %}
{% endif %}
{% endfor %}
{{ locked_users|length }}
</div></div></div><divclass="table-wrapper"><tableclass="data-table"><thead><tr><thstyle="min-width:100px;">用户名</th><thstyle="min-width:80px;">用户状态</th><thstyle="min-width:100px;">创建时间</th><thstyle="min-width:100px;">锁定时间</th><thstyle="min-width:100px;">过期时间</th><thstyle="min-width:100px;">密码修改时间</th><thstyle="min-width:100px;">最后登录时间</th><thstyle="min-width:120px;">默认表空间</th><thstyle="min-width:120px;">临时表空间</th><thstyle="min-width:80px;">表数量</th><thstyle="min-width:80px;">索引数量</th><thstyle="min-width:80px;">视图数量</th><thstyle="min-width:80px;">函数数量</th><thstyle="min-width:80px;">存储过程数</th><thstyle="min-width:80px;">序列数量</th></tr></thead><tbody>
{% for user in report_data.tb_zdb_user %}
<trclass="{% if user.userstatus and 'LOCKED' in user.userstatus %}warning{% endif %}"><td>{{ user.username }}</td><tdclass="{% if user.userstatus and 'LOCKED' in user.userstatus %}text-warning{% endif %}">{{ user.userstatus }}</td><td>{{ user.createtime }}</td><td>{{ user.locktime }}</td><td>{{ user.expirytime }}</td><td>{{ user.pwdmodifytime|default(user.createtime) }}</td><td>{{ user.lastlogintime }}</td><td>{{ user.dtablespace }}</td><td>{{ user.ttablespace }}</td><td>{{ user.tablenum }}</td><td>{{ user.indexnum }}</td><td>{{ user.viewnum }}</td><td>{{ user.functionnum }}</td><td>{{ user.procedurenum }}</td><td>{{ user.sequencenum }}</td></tr>
{% endfor %}
</tbody></table></div></div><!-- 慢SQL信息(优化后) --><divclass="tab-content"id="slowsql"><h3style="font-size:14px;margin-bottom:10px;">慢SQL信息</h3><divclass="metrics"><divclass="metric-card"><h3>慢SQL总数</h3><divclass="metric-value">{{ report_data.tb_zdb_slowsql|length }}</div></div><divclass="metric-card danger"><h3>高耗时SQL(>100秒)</h3><divclass="metric-value">
{% set slow_sqls = [] %}
{% for sql in report_data.tb_zdb_slowsql %}
{% if sql.totaltime and sql.totaltime|float > 100 %}
{% set _ = slow_sqls.append(1) %}
{% endif %}
{% endfor %}
{{ slow_sqls|length }}
</div></div></div><!-- 慢SQL卡片列表 --><divclass="sql-card-list">
{% for sql in report_data.tb_zdb_slowsql %}
<divclass="card"style="margin-bottom:0;"><divclass="card-body"style="padding:12px;"><ulstyle="list-style: none;display: flex;flex-wrap: wrap;gap: 15px;margin-bottom: 10px;"><listyle="min-width:120px;"><spanstyle="color:var(--dark-3);font-size:12px;">SQL ID:</span><spanstyle="font-weight:500;">{{ sql.sqlid }}</span></li><listyle="min-width:100px;"><spanstyle="color:var(--dark-3);font-size:12px;">用户名:</span><span>{{ sql.username }}</span></li><listyle="min-width:100px;"><spanstyle="color:var(--dark-3);font-size:12px;">执行次数:</span><span>{{ sql.execcount }}</span></li><listyle="min-width:120px;"><spanstyle="color:var(--dark-3);font-size:12px;">总执行时间:</span><spanclass="{% if sql.totaltime and sql.totaltime|float > 100 %}text-danger{% elif sql.totaltime and sql.totaltime|float > 50 %}text-warning{% endif %}">
{{ sql.totaltime }}秒
</span></li><listyle="min-width:120px;"><spanstyle="color:var(--dark-3);font-size:12px;">平均时间:</span><spanclass="{% if sql.avgtime and sql.avgtime|float > 50 %}text-danger{% elif sql.avgtime and sql.avgtime|float > 20 %}text-warning{% endif %}">
{{ sql.avgtime }}秒
</span></li><listyle="min-width:100px;"><spanstyle="color:var(--dark-3);font-size:12px;">物理读:</span><spanclass="{% if sql.reads and sql.reads|int > 100000 %}text-danger{% elif sql.reads and sql.reads|int > 50000 %}text-warning{% endif %}">
{{ sql.reads }}
</span></li></ul><!-- SQL文本(折叠/展开) --><divstyle="position: relative;"><buttonclass="sql-toggle-btn"onclick="toggleSql(this)">
展开SQL
</button><divclass="sql-block sql-collapsed">
{{ sql.sqltext }}
<!-- 折叠遮罩层 --><divclass="sql-mask"></div></div></div></div></div>
{% endfor %}
</div></div><!-- 实例信息 --><divclass="tab-content"id="inst"><h3style="font-size:14px;margin-bottom:10px;">数据库实例信息</h3><divclass="table-wrapper"><tableclass="data-table"><thead><tr><thstyle="min-width:80px;">实例ID</th><thstyle="min-width:100px;">实例名</th><thstyle="min-width:100px;">主机名</th><thstyle="min-width:100px;">运行时间</th><thstyle="min-width:80px;">实例状态</th><thstyle="min-width:80px;">数据库状态</th></tr></thead><tbody>
{% for inst in report_data.tb_zdb_inst %}
<trclass="{% if inst.inststatus and inst.inststatus != 'OPEN' %}danger{% endif %}"><td>{{ inst.instid }}</td><td>{{ inst.instname }}</td><td>{{ inst.hostname }}</td><td>{{ inst.uptime }}</td><tdclass="{% if inst.inststatus and inst.inststatus != 'OPEN' %}text-danger{% endif %}">{{ inst.inststatus }}</td><tdclass="{% if inst.dbstatus and inst.dbstatus != 'ACTIVE' %}text-warning{% endif %}">{{ inst.dbstatus }}</td></tr>
{% endfor %}
</tbody></table></div></div><!-- DBLink信息 --><divclass="tab-content"id="dblink"><h3style="font-size:14px;margin-bottom:10px;">数据库链接(DBLink)信息</h3><divclass="metrics"><divclass="metric-card"><h3>DBLink总数</h3><divclass="metric-value">{{ report_data.tb_zdb_dblink|length }}</div></div><divclass="metric-card danger"><h3>无效DBLink数</h3><divclass="metric-value">
{% set invalid_dblinks = [] %}
{% for dblink in report_data.tb_zdb_dblink %}
{% if dblink.valid and dblink.valid != 'YES' %}
{% set _ = invalid_dblinks.append(1) %}
{% endif %}
{% endfor %}
{{ invalid_dblinks|length }}
</div></div></div><divclass="table-wrapper"><tableclass="data-table"><thead><tr><thstyle="min-width:100px;">所有者</th><thstyle="min-width:120px;">DBLink名称</th><thstyle="min-width:100px;">用户名</th><thstyle="min-width:150px;">主机</th><thstyle="min-width:100px;">创建时间</th><thstyle="min-width:80px;">有效性</th></tr></thead><tbody>
{% for dblink in report_data.tb_zdb_dblink %}
<trclass="{% if dblink.valid and dblink.valid != 'YES' %}danger{% endif %}"><td>{{ dblink.owner }}</td><td>{{ dblink.dblink }}</td><td>{{ dblink.username }}</td><tdclass="long-text">{{ dblink.host }}</td><td>{{ dblink.created }}</td><tdclass="{% if dblink.valid and dblink.valid != 'YES' %}text-danger{% endif %}">{{ dblink.valid }}</td></tr>
{% endfor %}
</tbody></table></div></div><!-- 分区表信息 --><divclass="tab-content"id="parttable"><h3style="font-size:14px;margin-bottom:10px;">分区表信息</h3><divclass="metrics"><divclass="metric-card"><h3>分区表总数</h3><divclass="metric-value">
{% set unique_tables = {} %}
{% for item in report_data.tb_zdb_parttable %}
{% if item.tablename %}
{% set _ = unique_tables.__setitem__(item.tablename, true) %}
{% endif %}
{% endfor %}
{{ unique_tables|length }}
</div></div><divclass="metric-card"><h3>大型分区(>100万行)</h3><divclass="metric-value">
{% set large_tables = [] %}
{% for table in report_data.tb_zdb_parttable %}
{% if table.numrows and table.numrows|int > 1000000 %}
{% set _ = large_tables.append(1) %}
{% endif %}
{% endfor %}
{{ large_tables|length }}
</div></div></div><divclass="table-wrapper"><tableclass="data-table"><thead><tr><thstyle="min-width:100px;">所有者</th><thstyle="min-width:120px;">表名</th><thstyle="min-width:100px;">分区键</th><thstyle="min-width:100px;">分区类型</th><thstyle="min-width:100px;">间隔值</th><thstyle="min-width:120px;">分区名称</th><thstyle="min-width:100px;">上限值</th><thstyle="min-width:120px;">表空间</th><thstyle="min-width:80px;">分区大小</th><thstyle="min-width:80px;">分区行数</th><thstyle="min-width:100px;">统计时间</th></tr></thead><tbody>
{% for table in report_data.tb_zdb_parttable %}
<tr><td>{{ table.username }}</td><td>{{ table.tablename }}</td><td>{{ table.partkey }}</td><td>{{ table.parttype }}</td><td>{{ table.interval }}</td><td>{{ table.partname }}</td><tdclass="long-text">{{ table.highvalue }}</td><td>{{ table.tablespace }}</td><td>{{ table.size }}</td><tdclass="{% if table.numrows and table.numrows|int > 1000000 %}text-warning{% endif %}">{{ table.numrows }}</td><td>{{ table.analyzed }}</td></tr>
{% endfor %}
</tbody></table></div></div><!-- 分区索引信息 --><divclass="tab-content"id="partindex"><h3style="font-size:14px;margin-bottom:10px;">分区索引信息</h3><divclass="metrics"><divclass="metric-card"><h3>分区索引总数</h3><divclass="metric-value">
{% set unique_index = {} %}
{% for item in report_data.tb_zdb_partindex %}
{% if item.indexname %}
{% set _ = unique_index.__setitem__(item.indexname, true) %}
{% endif %}
{% endfor %}
{{ unique_index|length }}
</div></div><divclass="metric-card warning"><h3>失效索引数</h3><divclass="metric-value">
{% set invalid_indexes = [] %}
{% for idx in report_data.tb_zdb_partindex %}
{% if idx.status and idx.status != 'USABLE' %}
{% set _ = invalid_indexes.append(1) %}
{% endif %}
{% endfor %}
{{ invalid_indexes|length }}
</div></div></div><divclass="table-wrapper"><tableclass="data-table"><thead><tr><thstyle="min-width:100px;">所有者</th><thstyle="min-width:120px;">表名</th><thstyle="min-width:120px;">索引名</th><thstyle="min-width:100px;">索引类型</th><thstyle="min-width:100px;">分区类型</th><thstyle="min-width:120px;">分区名称</th><thstyle="min-width:80px;">状态</th><thstyle="min-width:120px;">表空间</th><thstyle="min-width:100px;">上限值</th><thstyle="min-width:100px;">统计时间</th></tr></thead><tbody>
{% for idx in report_data.tb_zdb_partindex %}
<trclass="{% if idx.status and idx.status != 'USABLE' %}warning{% endif %}"><td>{{ idx.username }}</td><td>{{ idx.tablename }}</td><td>{{ idx.indexname }}</td><td>{{ idx.locality }}</td><td>{{ idx.parttype }}</td><td>{{ idx.partname }}</td><tdclass="{% if idx.status and idx.status != 'USABLE' %}text-danger{% endif %}">{{ idx.status }}</td><td>{{ idx.tablespace }}</td><tdclass="long-text">{{ idx.highvalue }}</td><td>{{ idx.analyzed }}</td></tr>
{% endfor %}
</tbody></table></div></div><!-- 重做日志信息 --><divclass="tab-content"id="redolog"><h3style="font-size:14px;margin-bottom:10px;">重做日志信息</h3><divclass="metrics"><divclass="metric-card"><h3>重做日志组总数</h3><divclass="metric-value">{{ report_data.tb_zdb_redolog|length }}</div></div><divclass="metric-card warning"><h3>未归档日志数</h3><divclass="metric-value">
{% set unarchived = [] %}
{% for log in report_data.tb_zdb_redolog %}
{% if log.archived and log.archived != 'YES' %}
{% set _ = unarchived.append(1) %}
{% endif %}
{% endfor %}
{{ unarchived|length }}
</div></div></div><divclass="table-wrapper"><tableclass="data-table"><thead><tr><thstyle="min-width:80px;">日志组</th><thstyle="min-width:200px;">文件路径</th><thstyle="min-width:80px;">线程</th><thstyle="min-width:80px;">序列</th><thstyle="min-width:80px;">大小</th><thstyle="min-width:80px;">状态</th><thstyle="min-width:80px;">是否归档</th><thstyle="min-width:100px;">首次时间</th></tr></thead><tbody>
{% for log in report_data.tb_zdb_redolog %}
<trclass="{% if log.status and log.status == 'CURRENT' %}warning{% elif log.archived and log.archived != 'YES' %}danger{% endif %}"><td>{{ log.group }}</td><tdclass="long-text">{{ log.filepath }}</td><td>{{ log.thread }}</td><td>{{ log.sequence }}</td><td>{{ log.size }}</td><tdclass="{% if log.status and log.status == 'CURRENT' %}text-warning{% endif %}">{{ log.status }}</td><tdclass="{% if log.archived and log.archived != 'YES' %}text-danger{% endif %}">{{ log.archived }}</td><td>{{ log.firsttime }}</td></tr>
{% endfor %}
</tbody></table></div></div></div></div></div><script>// 标签页切换功能
document.addEventListener('DOMContentLoaded',function(){const tabs = document.querySelectorAll('.tab');const tabContents = document.querySelectorAll('.tab-content');
tabs.forEach(tab=>{
tab.addEventListener('click',()=>{// 移除所有标签的活动状态
tabs.forEach(t=> t.classList.remove('active'));// 添加当前标签的活动状态
tab.classList.add('active');// 隐藏所有内容
tabContents.forEach(content=> content.classList.remove('active'));// 显示对应内容const tabId = tab.getAttribute('data-tab');
document.getElementById(tabId).classList.add('active');});});});// 折叠/展开SQL文本functiontoggleSql(btn){const sqlBlock = btn.nextElementSibling;const mask = sqlBlock.querySelector('.sql-mask');if(sqlBlock.classList.contains('sql-collapsed')){
sqlBlock.classList.remove('sql-collapsed');
sqlBlock.style.height ='auto';
mask.style.display ='none';
btn.textContent ='收起SQL';}else{
sqlBlock.classList.add('sql-collapsed');
sqlBlock.style.height ='60px';
mask.style.display ='block';
btn.textContent ='展开SQL';}}</script></body></html>
dbinspection.py
# Create by:ember.zhang# 为什么不问问神奇的海螺呢丶import cx_Oracle
import pandas as pd
from sqlalchemy import create_engine,text
from datetime import datetime
from jinja2 import Environment, FileSystemLoader
cx_Oracle.init_oracle_client(lib_dir=r'D:\oracleclient\instantclient_23_8')deforacle_inspection_to_mysql(oracle_config, oracle_sql, mysql_config, mysql_table):# 构建Oracle连接
oracle_engine = create_engine(f"oracle+cx_oracle://{oracle_config['user']}:{oracle_config['password']}@{oracle_config['host']}:{oracle_config['port']}/?service_name={oracle_config['service_name']}")
df = pd.read_sql(oracle_sql, oracle_engine)# oracle_conn.close()if df.empty:print("无数据可处理!")return# 数据内容调整 # 添加固定列
df["dbip"]= oracle_config["host"]
df["checktime"]= datetime.now().strftime("%Y-%m-%d")#写入MySQL
mysql_engine = create_engine(f"mysql+pymysql://{mysql_config['user']}:{mysql_config['password']}@{mysql_config['host']}/{mysql_config['db']}")
df.to_sql(
name=mysql_table,
con=mysql_engine,
if_exists="append",
index=False,
chunksize=1000)#写入数据至mysqldefw_inspection_data(mysql_config,oracle_config):
mysql_wz = create_engine(f"mysql+pymysql://{mysql_config['user']}:{mysql_config['password']}@{mysql_config['host']}/{mysql_config['db']}")
dbip = oracle_config["host"]
rsql=text("SELECT tablename,sourcesql FROM tb_dbinspectionconfig where dbtype='oracle' ")with mysql_wz.connect()as wz_conn:
result = wz_conn.execute(rsql)for row in result:
tablename = row.tablename
sourcesql = row.sourcesql
#检查目标表是否存在当天同一DBIP的数据
check_sql = text(f"SELECT 1 FROM {tablename} WHERE checktime = current_date AND dbip = :dbip")
check_result = wz_conn.execute(check_sql,{"dbip": dbip}).fetchone()#存在则删除对应数据if check_result:
delete_sql = text(f"DELETE FROM {tablename} WHERE checktime = current_date AND dbip = :dbip")
wz_conn.execute(delete_sql,{"dbip": dbip})
wz_conn.commit()# 提交删除事务print(f"已删除{tablename}表中 {dbip}的数据")#写入数据
oracle_inspection_to_mysql(oracle_config, sourcesql, mysql_config, tablename)#生成报告defload_inspection(mysql_config, dbip):# 创建数据库引擎
engine = create_engine(f"mysql+pymysql://{mysql_config['user']}:{mysql_config['password']}@{mysql_config['host']}/{mysql_config['db']}",
pool_pre_ping=True,
connect_args={"charset":"utf8mb4"})# 定义查询SQL字典(带参数绑定)
sql_queries ={"tb_zdb":"SELECT dbip, dbname, dbtype, dbversion, logmode, dbrole, dbuname, israc, racnum, uptime, checktime, inserttime FROM zapp.tb_zdb where checktime=current_date and dbip=:dbip;","tb_zdb_asm":"SELECT name, used, total, `free`, percent FROM zapp.tb_zdb_asm where checktime=current_date and dbip=:dbip;","tb_zdb_dblink":"SELECT owner, dblink, username, host, created, valid FROM zapp.tb_zdb_dblink where checktime=current_date and dbip=:dbip;","tb_zdb_inst":"SELECT instid, instname, hostname, uptime, inststatus, dbstatus FROM zapp.tb_zdb_inst where checktime=current_date and dbip=:dbip;","tb_zdb_parameter":"SELECT pname, pvalue, description FROM zapp.tb_zdb_parameter where checktime=current_date and dbip=:dbip;","tb_zdb_partindex":"SELECT username, tablename, indexname, locality, parttype, partname, status, tablespace, highvalue, analyzed FROM zapp.tb_zdb_partindex where checktime=current_date and dbip=:dbip;","tb_zdb_parttable":"SELECT username, tablename, partkey, parttype, `interval`, subparttype, partname, highvalue, tablespace, `size`, numrows, analyzed FROM zapp.tb_zdb_parttable where checktime=current_date and dbip=:dbip;","tb_zdb_redolog":"SELECT `group`, filepath, thread, `sequence`, `size`, status, archived, firsttime FROM zapp.tb_zdb_redolog where checktime=current_date and dbip=:dbip;","tb_zdb_slowsql":"SELECT sqlid, username, instnum, execcount, totaltime, avgtime, cputime, iowait, `reads`, sqltext FROM zapp.tb_zdb_slowsql where checktime=current_date and dbip=:dbip;","tb_zdb_space":"SELECT tablespace, used, total, `free`, percent FROM zapp.tb_zdb_space where checktime=current_date and dbip=:dbip;","tb_zdb_user":"SELECT username, userstatus, createtime, locktime, expirytime, changepstime, lastlogintime, dtablespace, ttablespace, tablenum, viewnum, functionnum, indexnum, procedurenum, triggernum, synonymnum, sequencenum, packagenum FROM zapp.tb_zdb_user where checktime=current_date and dbip=:dbip;"}# 存储所有表的数据
inspection_data ={}try:with engine.connect()as conn:# 读取每个表的数据(绑定dbip参数)for table_name, sql in sql_queries.items():# 执行SQL并绑定参数
result = conn.execute(text(sql),{"dbip": dbip})# 获取列名
columns = result.keys()# 转换为字典列表
rows =[dict(zip(columns, row))for row in result.fetchall()]if table_name =="tb_zdb"and rows:# tb_zdb通常只有一条记录,取第一条
inspection_data[table_name]= rows[0]else:# 其他表转为字典列表
inspection_data[table_name]= rows
# 构造最终返回数据结构
report_data ={"title":f"数据库巡检报告 - {inspection_data.get('tb_zdb', {}).get('dbname')}({dbip})",**inspection_data
}
generate_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S")# env = Environment(loader=FileSystemLoader('E:/MagicConch/work/python/dbinspection/'))# template = env.get_template("dbinspection.html")
env = Environment(loader=FileSystemLoader('/data/zz/zscript/dbinspection/'))
template = env.get_template("dbinspection.html")
html_content = template.render(
report_data=report_data,
generate_time=generate_time
)#with open(f"E:/MagicConch/work/python/dbinspection/zdbinspectionreport/inspection_report_{dbip}_{generate_time.replace(' ', '').replace(':', '') }.html", "w", encoding="utf-8") as f:withopen(f"/data/zz/zscript/dbinspection/zdbinspectionreport/inspection_report_{dbip}_{generate_time.replace(' ','').replace(':','')}.html","w", encoding="utf-8")as f:
f.write(html_content)print('数据库巡检报告已生成 '+generate_time)# return report_dataexcept Exception as e:print(f"生成巡检报告失败(DBIP: {dbip}): {str(e)}")raisefinally:# 关闭引擎
engine.dispose()#30 8 * * * /usr/local/bin/python3 /data/zz/zscript/dbinspection/dbinspection.py >> /data/zz/zscript/dbinspection/run.log 2>&1if __name__ =="__main__":
oracle_55 ={"user":"system","password":"z9m_xxxxx","host":"10.12.8.55","port":1521,"service_name":"gemesdb"}
oracle_61 ={"user":"system","password":"Zeta%xxxxx#$","host":"10.12.8.61","port":1521,"service_name":"edadb"}
mysql_config ={"user":"root","password":"root%xxxxx","host":"10.12.8.120","db":"zapp"}
w_inspection_data(mysql_config,oracle_55)
load_inspection(mysql_config, oracle_55["host"])
w_inspection_data(mysql_config,oracle_61)
load_inspection(mysql_config, oracle_61["host"])
zapp db相关表及数据
zapp.tb_dbinspectionconfig 基础配置表
-- zapp.tb_dbinspectionconfig definition 基础配置表CREATETABLE`tb_dbinspectionconfig`(`id`intNOTNULLAUTO_INCREMENTCOMMENT'主键Id',`tablename`varchar(255)CHARACTERSET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULTNULLCOMMENT'目标表名',`sourcesql`textCHARACTERSET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT'源sql',`dbtype`varchar(255)CHARACTERSET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULTNULLCOMMENT'数据库类型',`updatetime`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'检查时间',`inserttime`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'数据写入时间',PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=12DEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='数据库巡检配置sql';INSERTINTO zapp.tb_dbinspectionconfig (tablename,sourcesql,dbtype,updatetime,inserttime)VALUES('tb_zdb_asm','SELECT
name ,
round(total_mb/1024,2)||''GB'' AS total,
round(FREE_MB/1024,2)||''GB'' AS free,
to_char(round((total_mb -FREE_MB) /1024,2), ''990.99'') ||''GB'' AS used,
to_char(round((total_mb -FREE_MB)/total_mb *100 ,2), ''990.99'') ||''%'' AS PERCENT
FROM v$asm_diskgroup_stat','oracle','2025-12-03 17:26:18','2025-12-02 10:54:48'),('tb_zdb','SELECT
name AS dbname,
''oracle'' AS dbtype,
version AS dbversion,
log_mode AS logmode,
database_role AS dbrole,
(SELECT LISTAGG(DB_UNIQUE_NAME, '', '') WITHIN GROUP (ORDER BY dest_id) AS dbuname FROM v$archive_dest WHERE UPPER(db_unique_name) NOT IN ( SELECT UPPER(db_unique_name) FROM v$database) AND db_unique_name !=''NONE'') AS dbuname ,
CASE WHEN (SELECT count(*) FROM gv$instance) >1 THEN 1 ELSE 0 END AS israc ,
(SELECT count(*) FROM gv$instance) AS racnum,
TO_CHAR(startup_time, ''YYYY-MM-DD HH24:MI:SS'') AS uptime
FROM v$database, v$instance','oracle','2025-12-02 13:14:16','2025-12-02 13:14:16'),('tb_zdb_inst','SELECT inst_id AS instid ,
instance_name AS instname,
host_name AS hostname,
TO_CHAR(startup_time, ''YYYY-MM-DD HH24:MI:SS'') AS uptime,
status AS inststatus,
database_status AS dbstatus
FROM gv$instance
order by inst_id','oracle','2025-12-02 17:39:19','2025-12-02 13:16:31'),('tb_zdb_parameter','SELECT name AS pname, display_value AS pvalue, description AS description
FROM v$parameter
WHERE name IN (''memory_target'',
''sga_target'', ''pga_aggregate_target'',''processes'', ''sessions'', ''db_block_size'',
''log_buffer'', ''shared_pool_size'', ''buffer_cache_size'',''java_pool_size'',
''large_pool_size'',''db_recovery_file_dest'',''db_recovery_file_dest_size'')
','oracle','2025-12-02 14:13:46','2025-12-02 13:41:38'),('tb_zdb_user','WITH z AS (
SELECT
owner ,
NVL("TABLE", 0) AS tablenum,
NVL("VIEW", 0) AS viewnum,
NVL("FUNCTION", 0) AS functionnum,
NVL("INDEX", 0) AS indexnum,
NVL("PROCEDURE", 0) AS procedurenum,
NVL("TRIGGER", 0) AS triggernum,
NVL("SYNONYM", 0) AS synonymnum,
NVL("SEQUENCE", 0) AS sequencenum,
NVL("PACKAGE", 0) AS packagenum
FROM ( SELECT owner, object_type, COUNT(*) AS cnt
FROM all_objects
WHERE owner IN (SELECT username FROM dba_users WHERE account_status = ''OPEN'') AND owner NOT IN (''SYSTEM'', ''SYS'')
GROUP BY owner, object_type ) t
PIVOT (SUM(cnt)
FOR object_type IN (
''TABLE'' AS "TABLE",
''VIEW'' AS "VIEW",
''FUNCTION'' AS "FUNCTION",
''INDEX'' AS "INDEX",
''PROCEDURE'' AS "PROCEDURE",
''TRIGGER'' AS "TRIGGER",
''SYNONYM'' AS "SYNONYM",
''SEQUENCE'' AS "SEQUENCE",
''PACKAGE'' AS "PACKAGE" )) p)
SELECT
a.username,
a.account_status AS userstatus,
TO_CHAR(a.created, ''YYYY-MM-DD HH24:MI:SS'') AS createtime,
TO_CHAR(a.lock_date, ''YYYY-MM-DD HH24:MI:SS'') AS locktime,
TO_CHAR(a.expiry_date, ''YYYY-MM-DD HH24:MI:SS'') AS expirytime,
TO_CHAR(a.PASSWORD_CHANGE_DATE, ''YYYY-MM-DD HH24:MI:SS'') AS changepstime,
TO_CHAR(a.LAST_LOGIN, ''YYYY-MM-DD HH24:MI:SS'') AS lastlogintime,
a.default_tablespace AS dtablespace,a.temporary_tablespace AS ttablespace,
z.tablenum,z.viewnum,z.functionnum,z.indexnum,z.procedurenum,z.triggernum,z.synonymnum,z.sequencenum,z.packagenum
FROM dba_users a LEFT JOIN z ON a.username=z.owner
ORDER BY a.account_status DESC , a.username','oracle','2025-12-02 17:37:55','2025-12-02 14:05:47'),('tb_zdb_space','SELECT
dt.tablespace_name as tablespace,
to_char( round(dt.block_size * dtum.used_space /1024/1024/1024,2), ''990.99'')||''GB'' as used,
round(dt.block_size * dtum.tablespace_size /1024/1024/1024,2)||''GB'' as total,
round(dt.block_size * (dtum.tablespace_size - dtum.used_space) /1024/1024/1024,2)||''GB'' as free,
to_char(round(dtum.used_space/dtum.tablespace_size *100 ,2), ''990.99'') ||''%'' percent
FROM dba_tablespace_usage_metrics dtum, dba_tablespaces dt
WHERE dtum.tablespace_name = dt.tablespace_name
and dt.contents != ''TEMPORARY''
union
SELECT
dt.tablespace_name as tablespace,
to_char(round((dt.tablespace_size - dt.free_space) /1024/1024/1024,2), ''990.99'')||''GB'' as used,
round(dt.tablespace_size /1024/1024/1024,2)||''GB'' as total,
round(dt.free_space /1024/1024/1024,2)||''GB'' as free,
to_char(round((dt.tablespace_size - dt.free_space) / dt.tablespace_size *100 ,2), ''990.99'') ||''%'' AS percent
FROM dba_temp_free_space dt
order by percent DESC ','oracle','2025-12-02 14:13:03','2025-12-02 14:13:03'),('tb_zdb_parttable','WITH z AS (
SELECT k.owner,k.name, LISTAGG(k.column_name, '', '') WITHIN GROUP (ORDER BY k.column_position) AS partition_key
FROM dba_part_key_columns k WHERE k.owner NOT IN (''AUDSYS'', ''MDSYS'', ''SYS'', ''SYSTEM'')
AND k.object_type = ''TABLE'' GROUP BY k.owner,k.name )
SELECT t.owner AS username,
t.table_name AS tablename,
z.partition_key AS partkey,
t.partitioning_type AS parttype,
t.INTERVAL AS interval,
t.SUBPARTITIONING_TYPE AS subparttype,
p.partition_name AS partname,
p.high_value AS highvalue,
p.tablespace_name AS tablespace,
ROUND(s.bytes / 1024 / 1024, 2)||''MB'' AS "size",
p.num_rows AS numrows,
TO_CHAR( p.last_analyzed , ''YYYY-MM-DD HH24:MI:SS'') AS analyzed
FROM dba_part_tables t LEFT JOIN dba_tab_partitions p ON t.owner=p.table_owner AND t.table_name =p.table_name
LEFT JOIN z ON t.owner=z.owner AND t.table_name =z.name
LEFT JOIN dba_segments s ON t.owner = s.owner AND t.table_name = s.segment_name AND p.partition_name = s.partition_name
WHERE t.owner NOT IN (''AUDSYS'', ''MDSYS'', ''SYS'', ''SYSTEM'')
order by t.owner,t.table_name,p.partition_name','oracle','2025-12-03 16:24:20','2025-12-02 14:18:44'),('tb_zdb_partindex','SELECT
pi.owner AS username,
pi.table_name AS tablename,
pi.index_name AS indexname,
pi.locality AS locality,
pi.partitioning_type AS parttype,
ip.partition_name AS partname,
ip.status AS status,
ip.tablespace_name AS tablespace,
ip.high_value AS highvalue,
TO_CHAR(ip.last_analyzed, ''YYYY-MM-DD HH24:MI:SS'') AS analyzed
FROM
dba_part_indexes pi
JOIN
dba_ind_partitions ip
ON pi.owner = ip.index_owner
AND pi.index_name = ip.index_name
WHERE
pi.owner NOT IN (''AUDSYS'', ''MDSYS'', ''SYS'', ''SYSTEM'')
ORDER BY
pi.owner, pi.table_name, pi.index_name, ip.partition_name','oracle','2025-12-02 14:20:56','2025-12-02 14:20:56'),('tb_zdb_redolog','SELECT
l.group# AS "group",
l.member AS filepath,
lg.thread# AS thread,
lg.sequence# AS "sequence",
lg.bytes/1024/1024/1024||''GB'' AS "size",
lg.status AS status,
lg.archived AS archived,
TO_CHAR(lg.first_time, ''YYYY-MM-DD HH24:MI:SS'') AS firsttime
FROM
v$logfile l
JOIN
v$log lg ON l.group# = lg.group#
ORDER BY
lg.group#, l.member','oracle','2025-12-02 14:31:27','2025-12-02 14:31:27'),('tb_zdb_slowsql','WITH slow_sql AS (
SELECT
dhs.dbid,
dhs.sql_id AS sqlid,
SUM(nvl(dhs.executions_delta, 0)) AS execcount,
ROUND(SUM(nvl(dhs.elapsed_time_delta, 0))/1000000, 3) AS totaltime,
ROUND(CASE WHEN SUM(nvl(dhs.executions_delta, 0)) = 0 THEN 0 ELSE SUM(nvl(dhs.elapsed_time_delta, 0))/1000000/SUM(nvl(dhs.executions_delta, 0)) END, 3) AS avgtime,
ROUND(SUM(nvl(dhs.cpu_time_delta, 0))/1000000, 3) AS cputime,
ROUND(SUM(nvl(dhs.iowait_delta, 0))/1000000, 3) AS iowait,
SUM(nvl(dhs.disk_reads_delta, 0)) AS reads,
dhs.parsing_schema_name AS username,
COUNT(DISTINCT dhs.instance_number) AS instnum
FROM dba_hist_sqlstat dhs
JOIN dba_hist_snapshot dhsnap
ON dhs.snap_id = dhsnap.snap_id
AND dhs.dbid = dhsnap.dbid
AND dhs.instance_number = dhsnap.instance_number
WHERE
dhsnap.end_interval_time >= SYSDATE - 1
AND dhs.parsing_schema_name NOT IN (''SYS'', ''SYSTEM'', ''AUDSYS'', ''MDSYS'')
AND nvl(dhs.elapsed_time_delta, 0) > 0
GROUP BY dhs.dbid, dhs.sql_id, dhs.parsing_schema_name
)
SELECT
a.sqlid,
a.username,
a.instnum,
a.execcount,
a.totaltime,
a.avgtime,
a.cputime,
a.iowait,
a.reads,
t.sql_text AS sqltext
FROM slow_sql a
JOIN dba_hist_sqltext t
ON a.dbid = t.dbid
AND a.sqlid = t.sql_id
WHERE a.execcount>1 and (a.totaltime > 60 OR a.avgtime > 5 )
ORDER BY a.totaltime DESC, a.avgtime DESC','oracle','2025-12-03 16:06:23','2025-12-02 14:37:51');INSERTINTO zapp.tb_dbinspectionconfig (tablename,sourcesql,dbtype,updatetime,inserttime)VALUES('tb_zdb_dblink','SELECT
owner ,
db_link AS dblink ,
username ,
host,
TO_CHAR(created,''YYYY-MM-DD HH24:MI:SS'') AS created ,
valid
FROM dba_db_links','oracle','2025-12-02 14:39:49','2025-12-02 14:39:49');