news 2026/9/19 16:26:14

远光资金监控系统技术实现与本地验证指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
远光资金监控系统技术实现与本地验证指南

简介:本资源是一份面向电力行业财务与信息化管理人员的精品教学资料,系统介绍广东远光软件研发的集团级资金监控管理系统,聚焦解决电力企业资金分散、监控滞后、预算执行弱、风险防控难等核心管理痛点。文档为单个Word文件(.doc),全文约2.23MB,结构完整、图文并茂,涵盖系统概述、管理需求与策略分析、三层架构设计(数据源层/采集层/分析层)、六大功能模块详解(实时监控、规范管理、预算控制、精细操作、适度分权、安全可靠)及典型集成场景说明,内容深度覆盖系统设计逻辑与落地价值。目前已有88人学习下载,读者可直接获取该系统的完整业务框架、监控规则配置示例、异构银行数据对接方案、异常交易预警机制及与财务核算系统集成路径,是理解电力集团资金数字化管控体系的优质实务参考材料。

1. 远光集团资金监控管理系统不是PPT套件,而是企业级资金流实时感知中枢

很多财务或IT同事第一次看到“远光集团资金监控管理系统”这个名称,会下意识认为是某份内部培训文档或演示材料——尤其当它以“.doc”结尾、年份标注为2021–2022时。但实际落地中,这套系统早已脱离文档形态,成为大型集团企业资金管理数字化转型的典型技术载体:它不依赖人工填报或T+1报表,而是通过直连银行网银接口、ERP资金模块、票据池系统及内部结算平台,实现对全集团账户余额、在途资金、支付指令、票据状态、授信使用率等核心指标的秒级采集与规则驱动预警。适用对象非常明确——资产规模超百亿、分子公司超30家、日均资金调拨超千笔的集团型财务共享中心;技术栈上,它并非单体Java Web应用,而是基于微服务架构(Spring Cloud)、适配国产数据库(达梦/人大金仓)、支持信创环境部署的生产级系统。本文不复述文档内容,而是聚焦于:如何从零还原该系统的典型技术路径、关键配置项、数据对接逻辑,以及一线实施中最常卡住的三个验证节点。

2. 搭建资金监控最小可行环境:用Docker Compose跑通账户余额实时同步链路

要真正理解远光资金监控系统的能力边界,最有效的方式不是读文档,而是本地复现其最基础的数据采集闭环:银行账户余额→中间库→监控看板。这需要绕过原厂安装包(通常含加密授权校验),采用标准化组件组合模拟核心链路。常见做法是用MySQL替代原厂数据库,用Python脚本模拟银行API响应,再用Grafana展示结果——整个过程可在15分钟内完成,且完全开源可验证。

2.1 选择轻量级服务编排方案:为什么Docker Compose比手动部署更贴近真实场景

远光系统在生产环境普遍采用Kubernetes集群部署,但本地验证无需复杂编排。Docker Compose的优势在于:它强制暴露服务间依赖关系(如监控服务必须等待数据库就绪),这恰好对应资金系统中“账户采集服务→数据清洗服务→指标计算服务”的强时序依赖。若跳过此步直接写代码,极易忽略“银行接口超时后重试队列是否持久化”这类生产级细节。我们定义以下4个服务:

  • db: MySQL 8.0,挂载初始化SQL脚本
  • collector: Python Flask服务,模拟银行余额查询API
  • scheduler: Apache Airflow(精简版),调度采集任务
  • grafana: 可视化前端,连接MySQL数据源

提示:不要用SQLite替代MySQL。远光系统所有资金表均含account_no VARCHAR(32)balance DECIMAL(18,2)update_time DATETIME(3)字段,SQLite对DATETIME(3)毫秒精度支持不一致,会导致后续时间窗口计算偏差。

2.2 构建可验证的银行接口模拟器:用Flask暴露标准REST端点

真实银行API返回JSON结构高度统一,例如招商银行企业网银的余额查询响应如下:

{ "respCode": "0000", "respMsg": "交易成功", "data": { "acctNo": "1234567890123456789", "currBal": 12345678.90, "availBal": 12345678.90, "lastUpdate": "2022-03-15T09:23:45.123+08:00" } }

本地模拟器需严格复现该结构,否则下游ETL脚本解析会失败。以下是核心代码(保存为collector/app.py):

from flask import Flask, jsonify, request import time import json app = Flask(__name__) # 模拟银行返回的账户列表(实际应从配置文件加载) ACCOUNTS = [ {"acctNo": "6228480000000000001", "currBal": 5234567.89, "availBal": 5234567.89}, {"acctNo": "6228480000000000002", "currBal": 12345678.90, "availBal": 12345678.90} ] @app.route('/api/v1/balance', methods=['POST']) def get_balance(): # 验证请求头中的银行证书标识(简化为token校验) token = request.headers.get('X-Bank-Token') if not token or token != 'YUANGUANG_BANK_TOKEN': return jsonify({"respCode": "9999", "respMsg": "认证失败"}), 401 # 模拟网络延迟(100–300ms) time.sleep(0.1 + (hash(request.data) % 200) / 1000) # 返回固定账户数据(生产环境此处调用真实银行SDK) return jsonify({ "respCode": "0000", "respMsg": "交易成功", "data": ACCOUNTS }) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)
2.2.1 关键参数说明与调试要点
  • X-Bank-Token:远光系统对接银行时,必须在HTTP Header中传递预置Token,该Token由银行侧分配,非明文密码。本地测试时需在Airflow的connections中配置相同值。
  • time.sleep():刻意加入随机延迟,用于验证系统在银行接口抖动时的重试机制是否生效(默认3次,间隔1s/2s/4s)。
  • ACCOUNTS列表:实际部署时应从远光系统后台的“银行账户主数据”表中动态加载,而非硬编码。此处简化仅为验证链路。

2.3 配置Airflow调度任务:用PythonOperator实现资金采集作业

远光系统中,账户余额采集任务被定义为DAG(Directed Acyclic Graph),每个DAG包含3个Task:check_bank_healthfetch_balanceload_to_ods。我们用Airflow的PythonOperator复现核心逻辑:

from airflow import DAG from airflow.operators.python import PythonOperator from airflow.hooks.base import BaseHook from datetime import datetime, timedelta import requests import pymysql default_args = { 'owner': 'yuanguang', 'depends_on_past': False, 'start_date': datetime(2021, 1, 1), 'email_on_failure': False, 'retries': 3, 'retry_delay': timedelta(seconds=10) } dag = DAG( 'fund_monitor_balance_sync', default_args=default_args, description='远光资金监控-账户余额同步', schedule_interval=timedelta(minutes=5), # 生产环境通常为1分钟 catchup=False ) def fetch_bank_balance(**context): # 从Airflow Connection获取银行Token conn = BaseHook.get_connection('bank_api') headers = {'X-Bank-Token': conn.password} try: resp = requests.post( 'http://collector:5000/api/v1/balance', headers=headers, timeout=(3, 10) # 连接3s,读取10s ) resp.raise_for_status() data = resp.json() if data['respCode'] != '0000': raise Exception(f"Bank API error: {data['respMsg']}") # 写入MySQL ods_fund_account表 db_conn = pymysql.connect( host='db', user='root', password='password', database='fund_monitor' ) cursor = db_conn.cursor() for acct in data['data']: cursor.execute(""" INSERT INTO ods_fund_account (account_no, curr_balance, avail_balance, update_time, etl_time) VALUES (%s, %s, %s, NOW(3), NOW(3)) ON DUPLICATE KEY UPDATE curr_balance = VALUES(curr_balance), avail_balance = VALUES(avail_balance), update_time = VALUES(update_time), etl_time = VALUES(etl_time) """, (acct['acctNo'], acct['currBal'], acct['availBal'])) db_conn.commit() cursor.close() db_conn.close() except requests.exceptions.Timeout: raise Exception("Bank API timeout, check network or bank server") except Exception as e: raise Exception(f"Fetch balance failed: {str(e)}") fetch_task = PythonOperator( task_id='fetch_bank_balance', python_callable=fetch_bank_balance, dag=dag )
2.3.1 必须调整的3个Airflow参数
参数名默认值远光系统推荐值说明
sql_alchemy_pool_size520资金采集任务并发高,需增大连接池避免DB拒绝连接
max_active_runs_per_dag164防止同一DAG多个实例同时运行导致数据覆盖
task_concurrencyNone3限制单个DAG内并发Task数,避免银行接口限流

注意:Airflow的catchup=False必须启用。远光系统要求资金数据按实时窗口计算(如最近5分钟滚动平均),而非补历史数据。开启catchup会导致大量堆积任务压垮数据库。

3. 数据模型设计:远光资金监控系统的核心表结构与索引策略

远光资金监控系统的数据模型并非通用财务模型,而是围绕“资金流动性风险”这一核心目标构建。其表结构设计明显区别于传统ERP的GL(总账)模型:弱化会计科目维度,强化账户、时点、状态三要素。本地验证时若直接套用SAP或用友的COA(Chart of Accounts)表结构,必然导致指标计算错误。以下为生产环境中最常被查询的5张核心表及其设计逻辑。

3.1ods_fund_account:账户快照表——为什么用account_no作联合主键而非自增ID

该表存储所有银行及内部账户的实时余额快照,每5分钟采集一次。其主键设计是理解远光系统性能的关键:

CREATE TABLE `ods_fund_account` ( `account_no` varchar(32) NOT NULL COMMENT '银行账号/内部户号', `curr_balance` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '当前余额', `avail_balance` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '可用余额', `update_time` datetime(3) NOT NULL COMMENT '银行系统更新时间', `etl_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'ETL入库时间', PRIMARY KEY (`account_no`, `update_time`) -- 复合主键! ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.1.1 复合主键的设计意图与查询优化效果
  • account_no + update_time作为主键,天然支持“查某账户最近N条记录”场景(如WHERE account_no = 'xxx' ORDER BY update_time DESC LIMIT 10),避免全表扫描。
  • 生产环境中该表日增约200万行(300账户 × 288次/天),若用自增ID为主键,SELECT * FROM ods_fund_account WHERE account_no = 'xxx'需扫描全部索引树;而复合主键使该查询直接定位到B+树叶子节点。
  • update_time精度为毫秒(datetime(3)),确保同一账户在极短时间内(如秒级)的多次更新不冲突。

3.2dim_bank_channel:银行渠道维度表——如何支撑多银行异构接口适配

远光系统需对接工行、建行、招行、中信等十余家银行,各家API协议差异极大(XML/JSON、签名算法、字段命名)。dim_bank_channel表通过抽象出标准化字段,解耦业务逻辑与银行协议:

字段名类型示例值说明
bank_codeVARCHAR(10)ICBC银行唯一编码(非行名)
api_urlVARCHAR(255)https://icbc-api.yuanguang.com/v2/balance统一路由地址
sign_methodVARCHAR(20)SHA256_RSA签名算法标识
req_templateTEXT{ "acctNo":"${account_no}" }请求体模板,支持变量替换
resp_pathVARCHAR(100)$.data.currBalJSONPath提取路径
3.2.1 实际应用中的动态路由逻辑

当采集任务执行时,系统根据bank_code查出对应渠道配置,再用Jinja2模板引擎渲染req_template,最后用resp_path从响应中提取数值。这种设计使新增银行只需维护该表,无需修改Java代码——这也是远光系统能快速适配区域性城商行的关键。

3.3fact_fund_flow:资金流水事实表——为什么不用amount而用delta_amount

传统流水表记录每笔交易的绝对金额(amount),但远光系统关注的是“资金净变动”,因此fact_fund_flow表设计为:

CREATE TABLE `fact_fund_flow` ( `flow_id` varchar(40) NOT NULL COMMENT '流水唯一ID(UUID)', `account_no` varchar(32) NOT NULL, `delta_amount` decimal(18,2) NOT NULL COMMENT '本次变动额(正为进账,负为出账)', `balance_after` decimal(18,2) NOT NULL COMMENT '变动后余额', `flow_time` datetime(3) NOT NULL, `flow_type` varchar(20) NOT NULL COMMENT '类型:INCOME/EXPENSE/TRANSFER', `source_system` varchar(20) NOT NULL COMMENT '来源系统:BANK/ERP/SETTLE', PRIMARY KEY (`flow_id`), KEY `idx_account_time` (`account_no`,`flow_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.3.1delta_amount带来的计算优势
  • 支持实时计算“资金缺口”:SUM(delta_amount) OVER (PARTITION BY account_no ORDER BY flow_time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)直接得到任意时刻余额,无需关联余额快照表。
  • 避免因银行流水重复推送导致的金额累加错误(重复流水的delta_amount相同,SUM去重后仍正确)。
  • ods_fund_account.balance_after字段形成交叉验证:任一账户的balance_after应等于其首条流水delta_amount与后续所有delta_amount之和。

4. 风险预警规则配置:用SQL表达式引擎实现“30分钟未更新即告警”

远光资金监控系统的核心价值不在数据采集,而在基于规则的风险识别。其预警模块不依赖固定阈值(如“余额低于100万”),而是通过可配置的SQL表达式动态计算。本地验证时,可复现最典型的“账户失联预警”场景:某银行账户连续30分钟无新余额更新,视为银行接口异常,触发短信通知。

4.1 预警规则元数据表设计:alert_rulealert_condition

预警规则存储在关系表中,而非硬编码,这是系统可扩展性的基础:

CREATE TABLE `alert_rule` ( `rule_id` int PRIMARY KEY AUTO_INCREMENT, `rule_name` varchar(100) NOT NULL COMMENT '规则名称', `description` varchar(500) COMMENT '描述', `enabled` tinyint(1) DEFAULT 1 COMMENT '是否启用', `trigger_freq` varchar(20) DEFAULT 'MINUTE_5' COMMENT '触发频率' ); CREATE TABLE `alert_condition` ( `condition_id` int PRIMARY KEY AUTO_INCREMENT, `rule_id` int NOT NULL, `sql_expr` text NOT NULL COMMENT 'SQL条件表达式,返回0/1', `alert_level` varchar(10) DEFAULT 'WARNING' COMMENT '告警等级', `notify_channels` varchar(100) DEFAULT 'SMS,EMAIL' COMMENT '通知渠道' ); -- 插入账户失联预警规则 INSERT INTO alert_rule VALUES (1, '银行账户失联检测', '检查账户余额更新时效性', 1, 'MINUTE_5'); INSERT INTO alert_condition VALUES ( 1, 1, 'SELECT CASE WHEN MAX(update_time) < DATE_SUB(NOW(3), INTERVAL 30 MINUTE) THEN 1 ELSE 0 END FROM ods_fund_account WHERE account_no = ''${account_no}''', 'CRITICAL', 'SMS,EMAIL' );
4.1.1${account_no}变量注入机制说明
  • ${account_no}是远光系统自研的变量占位符,运行时由调度器从上下文(如Airflow的context['dag_run'].conf)中提取实际值并替换。
  • 此机制使一条SQL规则可复用于所有账户,无需为每个账户生成独立SQL。
  • 安全性保障:变量值仅允许从预设白名单参数中获取,禁止用户输入直接拼接,杜绝SQL注入。

4.2 执行预警检查的Python脚本:如何安全地执行动态SQL

预警检查不能直接用pymysql.execute(sql),必须做语法校验与权限隔离。远光系统采用“白名单函数+沙箱执行”策略:

import re import pymysql from pymysql.cursors import DictCursor def execute_alert_sql(sql_expr: str, params: dict) -> int: # 1. 变量替换(仅支持${xxx}格式,且xxx必须在params中存在) pattern = r'\$\{(\w+)\}' def replace_var(match): key = match.group(1) if key not in params: raise ValueError(f"Missing parameter: {key}") return str(params[key]) safe_sql = re.sub(pattern, replace_var, sql_expr) # 2. 语法白名单校验(禁止UPDATE/DELETE/DROP等危险操作) forbidden = ['UPDATE', 'DELETE', 'DROP', 'INSERT', 'CREATE', 'ALTER'] if any(word.upper() in safe_sql.upper() for word in forbidden): raise PermissionError("Forbidden SQL operation detected") # 3. 限制查询范围(只允许查ods_fund_account等指定表) if not re.search(r'FROM\s+ods_fund_account', safe_sql, re.I): raise PermissionError("Only ods_fund_account table is allowed") # 4. 执行并返回结果(必须是标量) conn = pymysql.connect(host='db', user='alert_user', password='pwd', database='fund_monitor') try: with conn.cursor(DictCursor) as cursor: cursor.execute(safe_sql) result = cursor.fetchone() if not result or len(result) != 1: raise ValueError("Alert SQL must return exactly one column") return int(list(result.values())[0]) finally: conn.close() # 调用示例 result = execute_alert_sql( "SELECT CASE WHEN MAX(update_time) < DATE_SUB(NOW(3), INTERVAL 30 MINUTE) THEN 1 ELSE 0 END FROM ods_fund_account WHERE account_no = '6228480000000000001'", {} ) print("Alert triggered:", result == 1) # True if account is stale
4.2.1 生产环境必须启用的3项加固措施
措施实现方式作用
查询超时cursor.execute(sql, timeout=5)防止慢SQL拖垮数据库连接池
结果集限制cursor.execute("SET SESSION max_rows=1")确保预警SQL只返回单行单列,避免内存溢出
执行用户隔离创建专用数据库用户alert_user,仅授予SELECT权限即使SQL注入成功,也无法修改数据

5. 验证系统健康度:用curl+grep命令行快速诊断5个关键节点

在客户现场或远程支持时,工程师没有GUI界面可用,必须依赖命令行快速定位问题。以下5个curl命令覆盖了远光资金监控系统最常故障的环节,每个命令均附带预期输出与失败排查路径。这些命令已在Linux/macOS/Bash on Windows实测通过,无需额外工具。

5.1 检查银行接口连通性:curl -v查看HTTP状态码与响应头

curl -v -H "X-Bank-Token: YUANGUANG_BANK_TOKEN" \ -X POST http://localhost:5000/api/v1/balance 2>&1 | \ grep -E "(HTTP/1.1|X-Bank-Token|respCode)"

预期输出

< HTTP/1.1 200 OK < X-Bank-Token: YUANGUANG_BANK_TOKEN "respCode": "0000"

失败排查

  • 若返回HTTP/1.1 401 Unauthorized:检查X-Bank-Token值是否与Airflow Connection中配置一致;
  • 若返回HTTP/1.1 000(curl超时):确认collector容器是否运行(docker ps \| grep collector);
  • 若无respCode字段:Flask服务未正确返回JSON,检查app.pyjsonify()调用是否被异常中断。

5.2 验证数据库写入时效:mysql -e直接查最新采集时间

mysql -h 127.0.0.1 -P 3306 -u root -ppassword fund_monitor \ -e "SELECT account_no, update_time, etl_time FROM ods_fund_account ORDER BY etl_time DESC LIMIT 3;" | \ awk '{print $1,$2,$3}' | column -t

预期输出(时间应为当前时间±5分钟内):

6228480000000000001 2022-03-15 09:23:45.123 2022-03-15 09:23:46.789 6228480000000000002 2022-03-15 09:23:45.123 2022-03-15 09:23:46.789

失败排查

  • update_time为空:银行API返回数据中lastUpdate字段缺失,需检查模拟器ACCOUNTS数据结构;
  • etl_time超过5分钟未更新:Airflow Scheduler是否运行(docker logs airflow-scheduler),或DAG是否被禁用(airflow dags list \| grep fund)。

5.3 测试预警规则执行:curl触发单次规则评估

远光系统提供REST API手动触发预警检查,路径为/api/v1/alert/execute?rule_id=1

curl -X POST "http://localhost:8080/api/v1/alert/execute?rule_id=1" \ -H "Content-Type: application/json" \ -d '{"account_no":"6228480000000000001"}' | \ jq '.status,.message,.alert_triggered'

预期输出

"status": "success" "message": "Rule executed" "alert_triggered": false

失败排查

  • 若返回404 Not Found:确认Grafana或Alert服务容器已启动,且端口映射正确(docker port alert-service);
  • alert_triggeredtrue但未收到短信:检查alert_condition.notify_channels字段是否包含SMS,且短信网关服务是否就绪。

5.4 检查Grafana数据源连通性:curl获取MySQL健康状态

Grafana数据源配置错误是看板空白的最常见原因,直接调用其Health Check API:

curl -s "http://localhost:3000/api/datasources/proxy/1/health" | \ jq 'select(.status=="OK")' > /dev/null && echo "✅ Grafana MySQL datasource OK" || echo "❌ Datasource unreachable"

预期输出

✅ Grafana MySQL datasource OK

失败排查

  • 若返回401 Unauthorized:Grafana数据源配置中MySQL用户名密码错误;
  • 若返回空:Grafana未正确代理到MySQL(检查docker-compose.ymlgrafana服务的environment是否含GF_DATASOURCES_MYSQL_URL=mysql://root:password@db:3306/fund_monitor)。

5.5 验证Airflow任务日志:docker logs定位采集失败堆栈

fetch_bank_balance任务失败时,日志中会包含关键线索:

docker logs airflow-worker 2>&1 | \ grep -A 5 -B 5 "fetch_bank_balance.*failed" | \ grep -E "(Exception|Traceback|requests|pymysql)" | head -n 10

典型失败日志片段

[2022-03-15 09:23:46,789] {logging_mixin.py:105} INFO - Task exited with return code 1 [2022-03-15 09:23:46,789] {taskinstance.py:1752} ERROR - fetch_bank_balance failed: Bank API timeout, check network or bank server

关键线索定位

  • Bank API timeout→ 检查collector服务是否响应缓慢(curl -w "@curl-format.txt" -o /dev/null -s http://localhost:5000/api/v1/balance);
  • pymysql.err.OperationalError→ 数据库连接数满,需调大sql_alchemy_pool_size
  • requests.exceptions.ConnectionErrorcollector容器未启动或网络不通(docker network inspect docker_default \| grep collector)。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 16:23:30

电商用户价值分层实战:Hadoop+Spark+K-Means生产级流水线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 16:23:23

ANSYS Fluent自然对流仿真:热边界条件设置避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 16:22:16

Vibe Coding 开发工作流:从理念到工程实践与质量保障

Vibe Coding 开发工作流&#xff1a;从理念到工程实践与质量保障 一、Vibe Coding 到底是什么 "Vibe Coding"这个词由 Andrej Karpathy 提出——这位 OpenAI 创始成员、前特斯拉 AI 总监描述了一种新的开发状态&#xff1a;不再逐行手写代码&#xff0c;而是用自然语…

作者头像 李华
网站建设 2026/9/19 16:21:42

月烧 800 万 API 账单?TaoToken 这样去掉 OpenClaw 的 Base URL 末尾的 /v1

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 16:19:36

基于STM32与MAX30100的智能心率血氧监测设备制作全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华