# 多公司、多类型数据存储与展示 — 数据库与程序设计 ## 一、需求归纳 | 数据类型 | 示例 | 结构特点 | 公司覆盖 | | -------- | -------------------------------- | ---------------------------------------- | ---------- | | 三大报表 | 资产负债表、利润表、现金流量表 | 报告期 × 项目 → 数值,项目由 config 决定 | 所有公司 | | 业务构成 | 主营业务构成(产品/地区/业务线) | 报告期 × 维度(产品、地区等) → 数值/占比 | 部分公司 | | 业务指标 | 分众梯媒数量、门店数、产能等 | 报告期 × 指标名(或细分维度) → 数值 | 某公司特有 | 共性:**不同公司、不同类型的数据,维度与指标各异,且类型会持续增加**。 目标:**一套程序 + 一套库表** 支持所有类型,便于落库与图表展示。 --- ## 二、设计原则 1. **类型可扩展**:新增一种数据(如「门店数量」)不新增表、尽量不迁库。 2. **公司可扩展**:新公司只需配置「用哪些数据类型 + 解析/展示配置」。 3. **存储统一**:所有「公司 + 数据类型 + 时间 + 维度 → 数值」走同一套存储模型。 4. **查询与图表友好**:能按公司、类型、时间范围取出结构化数据,方便 ECharts 等前端作图。 --- ## 三、数据库选型结论 - **仍推荐 PostgreSQL**。 - 原因:需要按公司/类型/时间筛选、聚合、多公司对比;维度用 **JSONB** 表达,兼顾灵活性与查询(GIN 索引、jsonb 运算符);同一套表即可覆盖三大表 + 主营业务构成 + 梯媒数量等所有当前与未来类型。 --- ## 四、核心表结构设计 ### 4.1 概念模型(统一抽象) 所有「公司 × 数据类型 × 时间」下的数据,统一视为: - **数据集类型**(dataset type):如 balance_sheet、income_statement、main_business_composition、elevator_media_count … - **维度**(dimensions):因类型而异,用键值对表示,例如 - 三大表:`{ "category": "类现金", "item": "货币资金" }` 或仅 `{ "item": "资产总计" }` - 主营业务构成:`{ "segment": "产品", "name": "空调", "region": "国内" }` - 梯媒数量:`{ "metric": "梯媒数量", "sub_type": "电梯电视" }` 或仅 `{ "metric": "梯媒数量" }` - **数值**(value):统一为数值型(金额亿、数量、占比等)。 即:**一条记录 = 某公司、某数据集类型、某报告期、若干维度、一个数值**。 ### 4.2 表结构(PostgreSQL) ```sql -- 公司 CREATE TABLE companies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(200) NOT NULL, code VARCHAR(50) UNIQUE, -- 可选:股票/内部编码 created_at TIMESTAMPTZ DEFAULT now() ); -- 数据集类型(注册所有「可用的数据类型」) CREATE TABLE dataset_types ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(80) UNIQUE NOT NULL, -- balance_sheet, main_business_composition, elevator_media_count name VARCHAR(200) NOT NULL, -- 资产负债表、主营业务构成、梯媒数量 description TEXT, -- 用于解析与展示的默认结构(可选) default_dimension_keys JSONB, -- ["category","item"] 或 ["segment","region","metric"] created_at TIMESTAMPTZ DEFAULT now() ); -- 某公司对某数据集类型的「解析/展示」配置(对应现在的 config/公司名/*.json) CREATE TABLE company_dataset_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, dataset_type_id UUID NOT NULL REFERENCES dataset_types(id), config JSONB NOT NULL, -- 与现有 JSON 一致:categories / rows_to_delete / columns_to_keep / 自定义映射 UNIQUE(company_id, dataset_type_id), created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); -- 统一数据表:所有类型的数据都落在这里 CREATE TABLE company_data ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, dataset_type_id UUID NOT NULL REFERENCES dataset_types(id), report_period DATE NOT NULL, -- 报告期(季末/年末等) dimensions JSONB NOT NULL, -- 维度键值对,如 {"category":"类现金","item":"货币资金"} 或 {"metric":"梯媒数量"} value NUMERIC(20,4), -- 数值(亿、数量、比率等) created_at TIMESTAMPTZ DEFAULT now(), UNIQUE(company_id, dataset_type_id, report_period, dimensions) ); -- 便于按公司+类型+时间查询 CREATE INDEX idx_company_data_lookup ON company_data(company_id, dataset_type_id, report_period); -- 便于按 dimensions 内键查询(如按 item / metric 筛选) CREATE INDEX idx_company_data_dimensions ON company_data USING GIN (dimensions); ``` - **不同公司、不同数据类型的「字段差异」**:全部体现在 `dimensions` 的键值里,无需为每种类型或每个公司加列或建新表。 - 三大表:`dimensions` 用 `item`(及可选的 `category`)。 - 主营业务构成:用 `segment`、`name`、`region` 等。 - 梯媒数量:用 `metric`、`sub_type` 等。 - 以后新增类型:只需在 `dataset_types` 增加一行,并在解析逻辑里往 `dimensions` 填对应键即可。 ### 4.3 示例数据 **资产负债表(美的集团,2024-12-31)** | company_id | dataset_type_id | report_period | dimensions | value | | ---------- | --------------- | ------------- | --------------------------------------- | ------- | | 美的-uuid | balance_sheet | 2024-12-31 | {"category":"类现金","item":"货币资金"} | 140.41 | | 美的-uuid | balance_sheet | 2024-12-31 | {"item":"资产总计"} | 6043.52 | **主营业务构成(某公司,2024-12-31)** | company_id | dataset_type_id | report_period | dimensions | value | | ----------- | ------------------------- | ------------- | ------------------------------------------------ | ------ | | 某公司-uuid | main_business_composition | 2024-12-31 | {"segment":"产品","name":"空调","region":"国内"} | 1200.5 | **梯媒数量(分众传媒)** | company_id | dataset_type_id | report_period | dimensions | value | | ---------- | -------------------- | ------------- | ------------------------------------------- | ----- | | 分众-uuid | elevator_media_count | 2024-12-31 | {"metric":"梯媒数量","sub_type":"电梯电视"} | 80.2 | --- ## 五、程序架构设计(NestJS) ### 5.1 分层与职责 - **数据集类型**:只做「类型注册」和「默认维度/展示提示」,不承载具体解析逻辑。 - **公司 + 配置**:每个公司对每种类型有一份 config(对应现有 JSON),决定如何从 Excel 解析出 (report_period, dimensions, value)。 - **解析与写入**:按「类型 + 公司 config」驱动,输出统一格式 (company_id, dataset_type_id, report_period, dimensions, value),写入 `company_data`。 - **查询与展示**:按 company / dataset_type / 时间范围查 `company_data`,转成前端/ECharts 需要的结构(如按 item 的时序、按 segment 的饼图等)。 ### 5.2 模块划分建议 ``` src/ ├── companies/ # 公司 CRUD ├── dataset-types/ # 数据集类型注册、列表 ├── company-configs/ # 公司 × 类型的 config CRUD(对应 config/公司名/*.json) ├── import/ # 上传 Excel + 解析 + 落库 │ ├── parsers/ # 按类型分:balance-sheet.parser, income-statement.parser, cash-flow.parser, │ │ # main-business-composition.parser, elevator-media.parser, ... │ ├── import.service # 选公司、类型、文件 → 调对应 parser → 写 company_data │ └── import.controller ├── data/ # 查询接口:按公司/类型/时间范围返回数据,供图表用 └── chart-presets/ # 可选:按 dataset_type 的图表预设(折线、饼图、柱状等) ``` ### 5.3 解析层设计(应对「类型持续增加」) - **策略模式**:每种 `dataset_types.code` 对应一个 Parser(如 `BalanceSheetParser`, `MainBusinessParser`, `ElevatorMediaParser`)。 - Parser 输入:**文件流 + 该公司该类型的 config(JSONB)**。 - Parser 输出:**数组 of { report_period, dimensions, value }**,由 `ImportService` 统一写入 `company_data`。 - 新增类型时:在 `dataset_types` 插入一条;新增一个 Parser 并注册到工厂/映射表即可,无需改表结构。 ### 5.4 配置与维度的约定(程序侧) - 三大表:沿用现有 config 结构(categories / rows_to_delete / columns_to_keep),解析后 - `dimensions = { "item": "货币资金" }` 或 `{ "category": "类现金", "item": "货币资金" }`。 - 主营业务构成、梯媒数量等:在 `company_dataset_configs.config` 里约定字段,例如 - 主营业务:`{ "sheet_name": "主营业务构成", "row_header": "产品", "col_period": true, "value_key": "收入" }` - 梯媒:`{ "sheet_name": "业务数据", "metrics": ["梯媒数量","电梯电视","电梯海报"], "period_col": "报告期" }` 解析器按 config 读 Excel,组装出 `dimensions` 和 `value`。 这样,**程序与数据库都只依赖「公司 + 数据集类型 + config」**,不依赖具体有哪些列名,能适应不同公司、不同指标。 --- ## 六、查询与图表展示 - **按公司 + 类型 + 时间范围**: `SELECT report_period, dimensions, value FROM company_data WHERE company_id = ? AND dataset_type_id = ? AND report_period BETWEEN ? AND ? ORDER BY report_period, dimensions;` - 前端或后端将结果按 `dimensions` 转成 ECharts 所需格式,例如: - 折线图:dimensions.item 或 dimensions.metric 为系列,report_period 为 X 轴; - 饼图:某报告期的 dimensions.segment / dimensions.name 为扇区,value 为数值; - 可为每种 `dataset_type` 配置「图表预设」(折线/饼/柱状、默认维度),减少前端重复逻辑。 --- ## 七、小结 | 维度 | 选择与设计 | | -------- | -------------------------------------------------------------------------------------------------------- | | 数据库 | PostgreSQL;用「公司 + 数据集类型 + 报告期 + dimensions(JSONB) + value」统一存所有类型。 | | 表设计 | companies / dataset_types / company_dataset_configs / company_data;不按公司或按数据类型拆表。 | | 程序 | NestJS;按 dataset_type 的 Parser 策略 + 统一 Import 写 company_data;config 按公司×类型存 JSONB。 | | 扩展方式 | 新数据类型:新增 dataset_types 一行 + 新 Parser;新公司:新增 companies + 若干 company_dataset_configs。 | 这样即可在**不增加新表、不迁库**的前提下,支持三大表、主营业务构成、分众梯媒数量以及未来更多公司、更多类型的数据存储与图表展示。