11 KiB
11 KiB
多公司、多类型数据存储与展示 — 数据库与程序设计
一、需求归纳
| 数据类型 | 示例 | 结构特点 | 公司覆盖 |
|---|---|---|---|
| 三大报表 | 资产负债表、利润表、现金流量表 | 报告期 × 项目 → 数值,项目由 config 决定 | 所有公司 |
| 业务构成 | 主营业务构成(产品/地区/业务线) | 报告期 × 维度(产品、地区等) → 数值/占比 | 部分公司 |
| 业务指标 | 分众梯媒数量、门店数、产能等 | 报告期 × 指标名(或细分维度) → 数值 | 某公司特有 |
共性:不同公司、不同类型的数据,维度与指标各异,且类型会持续增加。
目标:一套程序 + 一套库表 支持所有类型,便于落库与图表展示。
二、设计原则
- 类型可扩展:新增一种数据(如「门店数量」)不新增表、尽量不迁库。
- 公司可扩展:新公司只需配置「用哪些数据类型 + 解析/展示配置」。
- 存储统一:所有「公司 + 数据类型 + 时间 + 维度 → 数值」走同一套存储模型。
- 查询与图表友好:能按公司、类型、时间范围取出结构化数据,方便 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)
-- 公司
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。 |
这样即可在不增加新表、不迁库的前提下,支持三大表、主营业务构成、分众梯媒数量以及未来更多公司、更多类型的数据存储与图表展示。