news 2026/9/18 19:14:16

从BI需求报告解读零售数据仓库设计与ETL分层实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从BI需求报告解读零售数据仓库设计与ETL分层实践

简介:某零售集团商业智能系统需求分析报告以Word文档形式交付,面向零售行业信息化规划人员、商业智能产品经理、数据仓库工程师与实施顾问,用于在BI二期建设中理清需求边界、功能模块与数据流转。报告系统拆解三大功能:日常业务报表支持脱机/联机查询,可自定义内容、时间范围和展示形式;业务探索式分析基于多维数据立方体,支持区域、品类、时间等维度钻取、切片与切块;KPI指标分析围绕利润率、库存周转率等关键指标进行监测。流程部分描述了从ERP、CRM等系统抽取数据,经清洗整合进入数据仓库,再由BI平台处理并输出报表与分析结果的完整链路,并细分日常报表处理流程与OLAP交互流程。报告还包含数据说明、系统运行环境及界面基本形式,章节结构完整,可直接作为同类零售集团BI需求分析模板或评审依据。资源为1个Word文档,压缩包仅547KB,轻量易读,已有88人浏览/学习,适合需要快速搭建商业智能需求分析框架的读者。

1. 把BI需求分析报告读出数据仓库设计意图

拿到一份BI需求分析报告(软件工程里叫SRS),多数团队的读法是翻目标、看报表清单、然后交给开发估工时。但这类文档真正值钱的部分往往是那些容易被跳过的章节:数据来源怎么定义、报表和OLAP的边界划在哪、ETL做几层。某零售集团BI二期需求分析报告的价值恰恰在这里——它把一期跑通数据之后的扩展方向收敛成三层功能体系,并明确所有分析最终依赖“信息仓库(ODS + OLAP)”的两层存储结构。数据来源限定为总部主档库和各业务系统按日聚合的单品明细,日常报表拆成定制脱机报表与联机报表查询,KPI指标被刻意限制在5个以内。这些约束不是业务侧拍脑袋,而是直接决定数仓建模和ETL工作量的关键输入。这篇解读按功能体系、数据流、模型映射、评审验收的顺序展开,适合正在准备BI项目需求文档或接手类似零售数仓项目的同学参考。

2. BI功能三层体系拆解:固定报表、探索式OLAP与KPI指标的边界划分

2.1 为什么把“日常业务报表”拆成脱机与联机两条线

需求文档里把日常业务报表分成定制脱机报表和联机报表查询,这个拆分容易被误读成“一个导出Excel、一个在线看”,实际两者在数据时效和计算时点上完全不同。

定制脱机报表面向的是格式和周期都稳定的公共需求。典型的例子是门店中类销售报表,按日、周、月三种粒度汇总销售额、数量、毛利及同环比。这类报表的特点是查询条件相对固定,业务部门不希望每次打开都要等待实时计算,更倾向于系统在固定时间点批量生成结果,再以Excel或其他通用格式推送给指定用户或用户组。从实现角度看,这对应数仓里的定时批处理任务,比如每天凌晨计算前一日数据,输出到报表服务器目录。

联机报表查询则对应大量、经常性的历史数据查询需求,例如查询门店某大类商品在某个时间段的每日销售金额。核心差异是数据已经被预先处理并以数据表形式存放在信息仓库中,用户查询时只是读取汇总结果,而不是触发对明细数据的重算。文档里特别提到用户界面可以呈现柱状图或曲线图,说明这类查询还要预置好图表模型。

两类报表的另一个区别在于参数化程度。脱机报表的参数在生成时固化,比如指定门店、指定时段;联机查询则允许用户随时调整条件组合。这直接影响数据集市的设计——脱机报表对应的数据表可以按固定维度预聚合,联机查询则要求多维度的组合覆盖,通常要借助OLAP立方体或列式存储来保证响应速度。

2.2 业务探索式分析(OLAP):承接“报表装不下的随机问题”

固定报表能解决80%的日常查看需求,但业务分析人员经常会问出报表上没有的问题,比如“华东区便利店业态里,哪几个供应商的库存周转在最近两个月持续下降”。这类问题没有预设的报表模板,如果每一个都走开发流程,需求排期会拖垮项目。

文档把这块定义为业务探索式分析,即OLAP。核心价值在于给分析人员一个从不同角度和因素组合切入业务的平台,支持旋转、切片、钻取等操作。比如按区域看销售不行,就切到商品类别再看;时间从月度钻到周、到日;或者固定门店组合、自由切换度量指标。OLAP的多维立方体模型让这些操作都发生在预计算好的聚合数据上,避免了每次探索都扫描全量明细。

这个设计还隐含了一个重要判断:一期系统不能满足这类需求,最大的障碍不是没有前端工具,而是“决策数据的不一致性”。多个业务系统对同一信息的数据定义不一致,相同命名的数据可能指代不同的业务含义。不从数据源头把这些口径统一掉,OLAP查询出来的数字没人敢信。因此文档明确要求,做OLAP之前必须先做ODS信息仓库建设,把关键业务基础数据做抽取、清洗和整合,否则多维分析就是空中楼阁。

2.3 KPI指标分析报告:限制在5个以内反而更有价值

KPI部分在需求文档里很容易被写成“我们要一堆指标”,但这份需求里刻意做了减法——“KPI指标个数绝对不能超过5个”。理由写得很直接:信息过多不利于决策者的快速消化,KPI就失去意义了。

这不是保守,而是对KPI定位的准确理解。KPI是用来监测管理目标的指数化结果,比如库存周转率、毛利率、供应商贡献度,它解决的是“现状好不好”的问题,而不是“为什么好、为什么不好”。深挖原因交给OLAP和报表,KPI只负责把最关键的健康度指标呈现给管理层。如果一张KPI看板上堆了20个指标,决策者根本分不清优先级。

从实施角度看,KPI指标分析报告是报表和OLAP之上的一层应用。它基于信息仓库的数据,通过“分析框架”对报表数据进行计算处理和统计建模。文档里也明确提出这是示范性应用,目的是为三期建立更系统的KPI指标体系做铺垫。这里透露了一个务实的落地策略:KPI体系不要试图一步到位,先选不超过5个核心指标跑通全链路,验证数据质量和计算逻辑,再逐步扩展。

下面把三类功能模块的定位整理成表格,方便后续设计评审时对齐。

功能模块解决什么问题主要输出实现方式
日常业务报表(定制脱机)固定周期、固定格式的经营数据分发Excel或通用格式报表定时批处理,预计算结果后分发
日常业务报表(联机查询)经常性、条件组合相对固定的历史查询查询结果表、图表历史数据预聚合到信息仓库
业务探索式分析(OLAP)多角度、多因素组合的临时探索问题多维查询立方体及图表基于数据集市构建Cube,支持钻取、切片、旋转
KPI指标分析报告管理目标指数化、关键因素影响量化KPI分析报告基于分析框架计算指标,数量控制在5个以内

2.4 三层结构对前端选型和数据模型的影响

把这三层功能放在一起看,可以得出一个通用结论:不论前端最终选择Power BI、帆软FineBI还是自研报表平台,后端的数据准备方式都是由功能形态决定的。脱机报表需要的是“模块化、可定时执行的数据集”,联机查询需要的是“查询效率高的预聚合表”,OLAP需要的是“多维立方体或兼容MDX查询的模型”,KPI则依赖一个统一的口径计算层。

这意味着数仓建模不能只做一套明细层就交付,而是要把公共明细层(对应ODS)、主题汇总层(对应OLAP库)、应用层(报表数据集和KPI指标)从物理或逻辑上拆开。文档里“两次ETL”的提法已经把这个分层结构写清楚了:第一次从业务系统到ODS,第二次从ODS到OLAP库。

3. 双ETL数据流设计:从总部主档与业务库到ODS、再到OLAP的落地细节

3.1 先分清两类数据源:主档库与业务库

需求文档对数据来源的说明非常关键,原文提到“系统数据来源总体上可以分为某零售集团总部主档数据库和A业务系统等外部业务数据源”。这两类来源的性质完全不同,处理方式也必须分开。

总部主档库保存的是企业基础信息:供应商主档、商品主档、门店主档、公司组织机构、业务人员主档。主档数据的特点是变化频率低、业务含义稳定,通常作为维度表的来源。商品主档会包含商品编码、品类层级、品牌、规格等属性,门店主档则包含区域、业态、面积等维度属性。

业务数据源提供各业态、各销售单位前一天的按单品聚合的明细业务数据,包括销售、退货等。注意这里的关键词是“前一天的按单品聚合”,说明源系统已经做了最粗粒度的汇总,数仓拿到的是每日单品级事实数据,而不是交易流水。按单品聚合对零售场景是合理的——绝大多数分析需求到单品粒度就足够,不需要逐个交易明细。

3.2 第一次ETL:把杂乱的业务数据清洗进ODS

第一次ETL的目标是把总部主档库和业务系统的数据抽取、转换、加载到ODS库。ODS层做的事情可以概括为四件事:字段格式统一、编码规则转换、脏数据清洗、衍生变量生成。

格式统一最常见的场景是日期、金额、数量在不同系统中的存储格式不一致。A系统日期用yyyyMMdd,B系统用yyyy-MM-dd;金额有的含税、有的不含税;数量有的按销售单位、有的按库存单位。这些差异必须在ODS层彻底解决。

编码转换发生在业务系统的内部编码和统一编码不一致时。比如门店编码,总部主档里一套编码,业务系统里另一套,ETL时需要通过映射关系完成转换。

3.2.1 一个可运行的增量ETL脚本骨架
# daily_incremental_etl.py import logging from datetime import date, timedelta import pandas as pd def extract_business_data(table, biz_date): """ 从业务系统按业务日期抽取增量数据 零售场景下通常是前一天的单品聚合销售数据 """ sql = f""" SELECT store_code, sku_code, sale_qty, sale_amt, biz_date FROM {table} WHERE biz_date = '{biz_date}' """ # 实际项目中从业务库连接读取 df = pd.read_sql(sql, business_db_conn) logging.info("extracted %s rows from %s", len(df), table) return df def transform_ods(df, master_data): """ 清洗和转换: 1. 门店编码替换为统一编码 2. 金额统一转换为不含税金额 3. 过滤异常记录 """ df = df[df["sale_qty"] >= 0] df = df[df["sku_code"].isin(master_data["sku_list"])] df["store_id"] = df["store_code"].map(master_data["store_mapping"]) df["sale_amt_net"] = df["sale_amt"] / 1.13 # 默认13%增值税,具体税率以业务规则库为准 df["etl_date"] = date.today() return df def load_to_ods(df, ods_table): """追加写入ODS表,重复执行不产生脏数据""" append_sql = f"INSERT INTO {ods_table} VALUES (%s, %s, %s, ...)" # 实际使用批量写入,单条insert性能太差 logging.info("loaded %s rows to %s", len(df), ods_table) if __name__ == "__main__": biz_date = (date.today() - timedelta(days=1)).strftime("%Y-%m-%d") raw = extract_business_data("sale_daily_agg", biz_date) master = load_master_data_from_ods() cleaned = transform_ods(raw, master) load_to_ods(cleaned, "ods_sale_daily")

这个脚本演示的是最朴素的增量ETL写法,有几个细节值得注意:业务日期显式传入而不是依赖系统时间,是为了支持补数;过滤逻辑放在编码转换之前,减少无效维度查找开销;sale_amt_net的税率计算只是示意,真实项目中税率来源应该是业务规则库而不是硬编码,这一点在3.3节展开。

3.3 业务规则库与关联关系库:分析口径的固化

需求文档里提到系统会收集业务部门提供的业务规则,形成“业务规则库”,并对其进行人工关联关系分析后生成“关联关系库”。这两部分是零售BI里最容易忽略、但实际决定报表可信度的内容。

业务规则库存储的是计算公式和合并抵消原则。例如零售行业常见的返利计算规则——供应商按销售额的一定比例给门店返利,具体比例可能随品类和销售额档位变化。这类规则必须由业务部门确认后固化成可执行配置,而不是写在SQL注释里。一个规则库的典型结构包括:规则编码、规则名称、计算公式、生效时间、适用业态、状态。

关联关系库更偏分析模型层面,用于描述外部因素与业务指标的关联关系,比如天气与饮料品类销售的关联、节假日与客流量的关联。需求文档里提到“包括相关性的判定以及关联函数形式”,说明这部分已经超出了简单报表的范畴,是为后续的数据挖掘做铺垫。在实际项目中,我的建议是不在二期把这层做重,先只沉淀一份“影响因素清单”和对应的基础分析框架,真正建模放到三期。

3.4 第二次ETL:面向主题构建OLAP库

ODS层解决了数据一致性问题,但它仍然是关系型表结构,距离OLAP需要的多维模型还有距离。第二次ETL从ODS中提取数据,按照分析主题重新组织,生成数据集市和Cube。需求文档明确提到这个转换过程包括“生成新的衍生变量”,比如计算同比环比、累计值、排名、占比等。

分层存储内容典型表/模型更新频率
ODS按单品聚合的清洗后明细ods_sale_daily每日增量
数据集市面向主题的汇总事实表dm_store_cat_sale_daily每日增量
OLAP Cube多维预聚合按门店、品类、时间等维度构建每日刷新

数据集市层在零售数仓里通常按主题拆分:销售主题、库存主题、供应商主题、合同主题等。每个主题对应一组事实表和维度表,例如销售数据集市的事实表包含门店维度、商品维度、日期维度以及销售额、销售数量、毛利等多个度量。

第二次ETL的工作量往往被低估。需求文档特别强调“对整个BI系统而言共有两次ETL过程,在工作量上要予以充分评估”,这一点在实际项目中非常真实。第一次ETL解决的是单日增量数据的清洗入库,逻辑相对固定;第二次ETL因为要处理历史数据重建、维度缓慢变化、Cube预计算,调优空间和执行复杂度都要高出不少。

4. 从需求文到维度模型:事实表、维表与指标口径映射

4.1 用“中类销售报表”练习一次星型建模

需求文档里给了一个可以当练手素材的例子:“所选门店各中类的销售额、数量、毛利及同环比日报、周报、月报”。这句话翻译到数仓模型里,就是典型的星型模式:一张事实表挂多张维度表。

事实表应该记录的粒度量是“门店 × 中类 × 日期 = 一行”,度量字段包括销售额、销售数量、毛利额。为什么粒度选到中类而不是单品?因为报表本身只到中类,如果事实表粒度比需求更细(比如到单品),会浪费存储和计算资源;如果更粗(比如到大类),就没办法支持用户想看中类的需求。

门店维度表属性包括门店编码、门店名称、区域、业态、开业日期。日期维度表在零售BI里不能只存日期本身,还需要年、季、月、周、是否节假日、是否促销日等属性,否则同环比和节假日分析都要临时处理。类别维度表在这里是商品维度的简化版本,只需要中类编码、中类名称以及上一级大类的归属关系。

4.2 事实表与维表的SQL落地

4.2.1 销售事实表DDL
-- 销售事实表:粒度 = 门店 + 中类 + 日期 CREATE TABLE dm_sale_store_cat_daily ( store_id INT NOT NULL COMMENT '门店维度ID', cat_id INT NOT NULL COMMENT '中类维度ID', date_id INT NOT NULL COMMENT '日期维度ID,格式yyyyMMdd', sale_qty DECIMAL(18,2) NOT NULL COMMENT '销售数量,按中类汇总', sale_amt DECIMAL(18,2) NOT NULL COMMENT '销售额(不含税)', gross_profit DECIMAL(18,2) NOT NULL COMMENT '毛利额', etl_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (store_id, cat_id, date_id), KEY idx_date_cat (date_id, cat_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

事实表建模有几个关键决策点。主键用三张维表的ID组合,而不是直接用业务编码,因为维表ID更稳定,即使业务编码变化也不影响事实表。金额字段统一为DECIMAL而不是FLOAT,零售金额计算必须避免浮点误差。idx_date_cat这个联合索引服务于频繁的按日期和品类过滤查询,是实际业务中最高频的查询路径。

4.2.2 维度表公共设计
-- 门店维度表(SCD2方式处理区域调整) CREATE TABLE dim_store ( store_id INT PRIMARY KEY, store_code VARCHAR(32) NOT NULL COMMENT '门店业务编码', store_name VARCHAR(128) NOT NULL, region VARCHAR(64) COMMENT '所属区域', format_type VARCHAR(32) COMMENT '业态:超市/卖场/便利店', open_date DATE, eff_date DATE COMMENT '生效日期', exp_date DATE COMMENT '失效日期,默认9999-12-31', is_current TINYINT DEFAULT 1 );

零售集团的区域组织和业态归属可能会调整,门店从A区域划到B区域后,如果直接更新维度表记录,历史报表中的区域统计就会错乱。用SCD2策略保留历史版本,查询时通过eff_date <= 业务日期 AND exp_date > 业务日期关联,才能在区域变更后仍然回看正确的历史归属。

4.3 同环比指标在SQL中的实现

需求文档反复提到同环比,这是一个要看懂就实现的常见指标,但口径必须统一。同环比通常指“与上一统计周期比”和“与去年同期比”,日环比就是对比前一日,日同比就是对比去年同一天。麻烦的是节假日和促销日并不对齐,直接日期减1天或减1年会对出异常值。

-- 日报表同环比计算示例 SELECT s.date_id, d.year, d.month, d.day, s.sale_amt AS cur_sale_amt, LAG(s.sale_amt, 1) OVER (PARTITION BY s.store_id, s.cat_id ORDER BY s.date_id) AS prev_day_amt, SUM(s.sale_amt) OVER (PARTITION BY s.store_id, s.cat_id, d.year ORDER BY s.date_id) AS ytd_amt FROM dm_sale_store_cat_daily s JOIN dim_date d ON s.date_id = d.date_id WHERE s.store_id = 1001 AND s.cat_id = 2003 AND s.date_id = 20240601;

上例用的是窗口函数实现前一日金额,LAGPARTITION BY必须以门店和类别的组合为单位,否则跨门店错位。ytd_amt按年度累计,用于年报中的累计对比。如果需求是“七日平均”“本月至今”这类更灵活的口径,在Cube里预计算比在SQL里现算更高效,这也是OLAP库存在的原因之一。

4.4 指标口径字典:让每个数字说得清来源

零售数仓项目中,“同一个指标、不同人算出来数字不一样”是引发信任危机的最常见原因。比如销售额,按订单实付算、按商品原价算、含税不算税、退货冲减不冲减,结果差很多。需求文档里强调多个业务系统对同一信息的定义不一致,这套问题必须在指标字典层面解决。

指标名称口径定义计算时点来源表
销售额(不含税)销售订单金额减去退货金额,剔除增值税每日业务结束时点ods_sale_daily
毛利额销售额(不含税)减去商品进货成本每日业务结束时点ods_sale_daily join ods_goods_cost
库存周转天数期末库存金额 / 近30日平均销售成本每月末ods_stock_daily
供应商贡献度该供应商商品销售额 / 总销售额每周末dm_supplier_sale_weekly

指标字典是需求文档与数据模型之间的桥梁。在需求评审阶段,数据产品经理应该和业务部门逐项确认口径定义,确认后写入指标字典,开发人员严格按照字典建模型。这一份字典也可以直接作为测试阶段的验收依据——测试人员按字典口径手工核算样本数据,与系统输出对比。

5. 需求评审里能直接用上的验证与验收动作

5.1 用对账SQL做“数出一致”

需求评审到设计定稿,最怕的是数据流“看起来通、实际对不上”。评审时可以直接要求项目组拿出一套跨系统对账SQL,验证从业务源到ODS、再到数据集市的每个环节数据量守恒。

-- 对账验证:ODS日汇总 vs 源系统日汇总 SELECT COALESCE(ods.biz_date, src.biz_date) AS biz_date, COALESCE(ods.total_amt, 0) AS ods_amount, COALESCE(src.total_amt, 0) AS src_amount, COALESCE(ods.total_amt, 0) - COALESCE(src.total_amt, 0) AS diff_amt FROM ( SELECT biz_date, SUM(sale_amt_net) AS total_amt FROM ods_sale_daily WHERE biz_date = '2024-06-01' GROUP BY biz_date ) ods FULL JOIN ( SELECT biz_date, SUM(net_sale_amt) AS total_amt FROM business_sale_daily WHERE biz_date = '2024-06-01' GROUP BY biz_date ) src ON ods.biz_date = src.biz_date;

差值为0只能证明传输没丢,不能证明口径正确。评审时要进一步验证金额口径——如果源系统的net_sale_amt已经是不含税金额,而ODS层的转换逻辑里又一次做了税率换算,就会对不上。

5.2 KPI指标的复算验证

KPI限定的5个指标,每个都要能手工复算。找一个第三方工具(比如Excel)按指标字典的公式,取同一批源数据,手工算一遍,与系统KPI输出对比。误差出现在小数点后两位以上就算失败,需要回溯计算链路。特别要检查的是指标在跨年、跨月边界时的计算逻辑,很多数仓项目会在“去年累计”这类时间口径上栽跟头。

5.3 验收用例落到文档

需求文档的评审通过不代表开发可以放飞。建议把每类功能对应的验收标准直接写进需求文档附件的用例表,让测试阶段有据可依。

验收项前置条件操作步骤通过标准
中类销售日报已生成前一日ODS数据选择门店、日期,生成日报报表数据与源系统手工汇总一致
OLAP钻取Cube已刷新从大类钻取到中类各层级合计值逐级吻合
KPI库存周转率月末数据完整查看KPI看板手工复算误差小于0.01
验收项前置条件操作步骤通过标准
联机销售连续性查询2个月历史数据已加载查询某大类60日每日销售首尾日期完整,无断点无跳变
报表定时分发ETL任务调度已配置等待每日自动发送指定用户按时收到可打开的文件

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

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

从docx到可视化:用Python解析轻食消费者调查数据全流程

简介&#xff1a;中国轻食行业消费者行为调查数据以文档形式呈现&#xff0c;适合餐饮品牌市场人员、行业分析师以及健康食品方向的学生&#xff0c;用于快速了解轻食消费市场。内容基于2023年艾媒咨询调查&#xff0c;完整记录了消费者食用轻食频率、喜欢的轻食类型、运动习惯…

作者头像 李华
网站建设 2026/9/18 19:12:15

STM32CubeMX2生成代码在Keil µVision5中编译调试全流程

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

作者头像 李华
网站建设 2026/9/18 19:10:30

300节点大数据平台验收:TPC-DS、性能压测与量收迁移拆解

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

作者头像 李华
网站建设 2026/9/18 19:10:12

正交试验设计原理与工业实战:从数学构造到注塑工艺优化

简介&#xff1a;本资源是一份面向高校统计学、工业工程及实验设计相关专业师生的《正交试验设计原理及实例》教学课件&#xff0c;系统讲解多因素优化试验的核心方法论&#xff0c;解决传统全面试验成本高、周期长、实施难等实际问题。课件以PPT格式呈现&#xff0c;共1个文件…

作者头像 李华
网站建设 2026/9/18 19:09:31

PyCharm 安装与更新全流程:解释器、虚拟环境与依赖管理避坑指南

Python IDE 这个说法听起来挺正式&#xff0c;实际落到日常开发里&#xff0c;它就是我们每天开门第一件事要面对的东西。我装 PyCharm 的经历算不上顺利&#xff0c;最早那台 4G 内存的笔记本&#xff0c;装完打开一个稍大的项目&#xff0c;索引转了十几分钟&#xff0c;风扇…

作者头像 李华
网站建设 2026/9/18 19:07:15

C语言结构体尾部填充与__attribute__((packed))实战解析

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

作者头像 李华