读取Excel 进行汇总

This commit is contained in:
2026-03-05 22:02:57 +08:00
commit 01b7759921
40 changed files with 3843 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
# 多公司、多类型数据存储与展示 — 数据库与程序设计
## 一、需求归纳
| 数据类型 | 示例 | 结构特点 | 公司覆盖 |
| -------- | -------------------------------- | ---------------------------------------- | ---------- |
| 三大报表 | 资产负债表、利润表、现金流量表 | 报告期 × 项目 → 数值,项目由 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 输入:**文件流 + 该公司该类型的 configJSONB**。
- 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_dataconfig 按公司×类型存 JSONB。 |
| 扩展方式 | 新数据类型:新增 dataset_types 一行 + 新 Parser新公司新增 companies + 若干 company_dataset_configs。 |
这样即可在**不增加新表、不迁库**的前提下,支持三大表、主营业务构成、分众梯媒数量以及未来更多公司、更多类型的数据存储与图表展示。