From 1f7fded275a82677f256bf8591e5b15eaa6a09e3 Mon Sep 17 00:00:00 2001 From: R524809 Date: Wed, 11 Feb 2026 16:05:45 +0800 Subject: [PATCH] tq[MESSAGE]: [TYPE]: [JIRA]: --- .../我编写的文档/数据库设计.md | 159 ++++++++++++- .../财务报表模块-产品与数据库设计.md | 214 ++++++++++++++++++ 2 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 packages/design-document/我编写的文档/财务报表模块-产品与数据库设计.md diff --git a/packages/design-document/我编写的文档/数据库设计.md b/packages/design-document/我编写的文档/数据库设计.md index 0c045d5..b9fab26 100644 --- a/packages/design-document/我编写的文档/数据库设计.md +++ b/packages/design-document/我编写的文档/数据库设计.md @@ -8,10 +8,16 @@ ├── user (用户表) 基础数据相关 -├── brokers (券商表) +├── broker (券商表) ├── stock_info (股票基本信息表) └── stock_daily_price (股票每日收盘价表) +财务报表相关(见 财务报表模块-产品与数据库设计.md) +├── financial_report (报表主表) +├── financial_report_line (行项目表) +├── financial_report_stats (预计算统计表) +└── financial_item_category (自定义分类表,可选) + 账户相关 ├── positions (持仓表) ├── position_changes (持仓变更记录表) @@ -513,6 +519,157 @@ WHERE p.auto_price_update = true --- +## 财务报表相关表设计 + +> 详细产品与设计说明见:[财务报表模块-产品与数据库设计.md](./财务报表模块-产品与数据库设计.md) + +### financial_report 报表主表 + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|---------|------|------| +| id | BIGSERIAL | PRIMARY KEY | 主键 | +| stock_code | VARCHAR(20) | NOT NULL | 股票代码,关联 stock_info | +| market | VARCHAR(20) | NOT NULL | 市场,关联 stock_info | +| report_date | DATE | NOT NULL | 报告期截止日 | +| period_type | VARCHAR(20) | NOT NULL | annual / quarterly | +| statement_type | VARCHAR(30) | NOT NULL | balance_sheet / income_statement / cash_flow | +| source_file_name | VARCHAR(255) | | 来源文件名 | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | + +```sql +CREATE TABLE financial_report ( + id BIGSERIAL PRIMARY KEY, + stock_code VARCHAR(20) NOT NULL, + market VARCHAR(20) NOT NULL, + report_date DATE NOT NULL, + period_type VARCHAR(20) NOT NULL, + statement_type VARCHAR(30) NOT NULL, + source_file_name VARCHAR(255), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(stock_code, market, report_date, statement_type), + CONSTRAINT fk_financial_report_stock FOREIGN KEY (stock_code, market) + REFERENCES stock_info(stock_code, market) ON DELETE CASCADE, + CONSTRAINT check_period_type CHECK (period_type IN ('annual', 'quarterly')), + CONSTRAINT check_statement_type CHECK (statement_type IN ('balance_sheet', 'income_statement', 'cash_flow')) +); + +CREATE INDEX idx_financial_report_stock_market ON financial_report(stock_code, market); +CREATE INDEX idx_financial_report_report_date ON financial_report(report_date); +CREATE INDEX idx_financial_report_statement_type ON financial_report(statement_type); + +CREATE TRIGGER update_financial_report_updated_at + BEFORE UPDATE ON financial_report + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +COMMENT ON TABLE financial_report IS '财务报表主表:公司+报告期+表型'; +COMMENT ON COLUMN financial_report.statement_type IS 'balance_sheet(资产负债表)/income_statement(利润表)/cash_flow(现金流量表)'; +``` + +### financial_report_line 行项目表 + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|---------|------|------| +| id | BIGSERIAL | PRIMARY KEY | 主键 | +| report_id | BIGINT | NOT NULL, FK | 所属报表 | +| item_name | VARCHAR(200) | NOT NULL | 科目名称(原文) | +| item_code | VARCHAR(50) | | 科目编码(可选) | +| value | DECIMAL(24, 6) | | 数值 | +| unit | VARCHAR(20) | | 单位 | +| row_order | INT | DEFAULT 0 | 行顺序 | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | + +```sql +CREATE TABLE financial_report_line ( + id BIGSERIAL PRIMARY KEY, + report_id BIGINT NOT NULL REFERENCES financial_report(id) ON DELETE CASCADE, + item_name VARCHAR(200) NOT NULL, + item_code VARCHAR(50), + value DECIMAL(24, 6), + unit VARCHAR(20), + row_order INT DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_financial_report_line_report_id ON financial_report_line(report_id); +CREATE INDEX idx_financial_report_line_item_name ON financial_report_line(report_id, item_name); + +COMMENT ON TABLE financial_report_line IS '财务报表行项目,存原始科目名与数值,支持多市场科目差异'; +``` + +### financial_report_stats 预计算统计表 + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|---------|------|------| +| id | BIGSERIAL | PRIMARY KEY | 主键 | +| report_id | BIGINT | NOT NULL, FK | 所属报表 | +| stat_key | VARCHAR(80) | NOT NULL | 如 total_assets, net_profit, roe | +| stat_value | DECIMAL(24, 6) | | 统计值 | +| dimension | VARCHAR(50) | | 可选维度 | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | + +```sql +CREATE TABLE financial_report_stats ( + id BIGSERIAL PRIMARY KEY, + report_id BIGINT NOT NULL REFERENCES financial_report(id) ON DELETE CASCADE, + stat_key VARCHAR(80) NOT NULL, + stat_value DECIMAL(24, 6), + dimension VARCHAR(50), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(report_id, stat_key, dimension) +); + +CREATE INDEX idx_financial_report_stats_report_id ON financial_report_stats(report_id); +CREATE INDEX idx_financial_report_stats_stat_key ON financial_report_stats(stat_key); + +CREATE TRIGGER update_financial_report_stats_updated_at + BEFORE UPDATE ON financial_report_stats + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +COMMENT ON TABLE financial_report_stats IS '报表预计算统计项,上传时计算并写入'; +``` + +### financial_item_category 自定义分类表(可选) + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|---------|------|------| +| id | BIGSERIAL | PRIMARY KEY | 主键 | +| user_id | BIGINT | FK user, 可选 | 多用户时按用户隔离 | +| market | VARCHAR(20) | NOT NULL | 市场 | +| statement_type | VARCHAR(30) | NOT NULL | 表型 | +| item_name_pattern | VARCHAR(200) | NOT NULL | 科目名或匹配模式 | +| category_slug | VARCHAR(60) | NOT NULL | 分类标签,如 cash_like, operating | +| sort_order | INT | DEFAULT 0 | 展示顺序 | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | + +```sql +CREATE TABLE financial_item_category ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT REFERENCES user(user_id) ON DELETE CASCADE, + market VARCHAR(20) NOT NULL, + statement_type VARCHAR(30) NOT NULL, + item_name_pattern VARCHAR(200) NOT NULL, + category_slug VARCHAR(60) NOT NULL, + sort_order INT DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_financial_item_category_user_market ON financial_item_category(user_id, market, statement_type); + +CREATE TRIGGER update_financial_item_category_updated_at + BEFORE UPDATE ON financial_item_category + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +COMMENT ON TABLE financial_item_category IS '科目名到自定义分类的映射,用于按类现金/投资资产等聚合展示'; +``` + +--- + ## 账户相关表设计 ### positions 持仓表 diff --git a/packages/design-document/我编写的文档/财务报表模块-产品与数据库设计.md b/packages/design-document/我编写的文档/财务报表模块-产品与数据库设计.md new file mode 100644 index 0000000..6d85eb4 --- /dev/null +++ b/packages/design-document/我编写的文档/财务报表模块-产品与数据库设计.md @@ -0,0 +1,214 @@ +# 财务报表模块 - 产品与数据库设计 + +## 一、需求摘要 + +- **用途**:个人研究用,上传并落库公司历次财务报表(资产负债表、利润表、现金流量表),支持按年/按季;后续做绘制、分类、对比查看。 +- **数据来源**:页面上传 Excel,每次一家公司、一种表型、多期数据;表中部分时间/列需要丢弃,部分需要落库。 +- **扩展需求**:不同市场(如 A 股 / 港股)科目名不同;需支持自定义分类(如类现金资产、投资资产、应收类、经营类等);常用统计希望预存避免每次计算。 + +--- + +## 二、三个核心问题的建议 + +### 1. 数据库如何支持不同市场、不同公司的财务数据(科目不一致)? + +**思路:用「报表 + 行项目」的通用结构,行项目以「原始科目名 + 数值」存储,不强行统一科目编码。** + +- **不采用**:为每个市场建一套固定列(如 A 股一张表、港股一张表)。维护成本高,且新增市场或科目时要改表结构。 +- **推荐**: + - **报表主表**:一条记录 = 某公司、某报告期、某表型(资产负债表/利润表/现金流量表)、某市场。 + - **行项目表**:每条记录 = 某报表下的一个科目一行,存「科目名称(原文)」「数值」「单位」「顺序」等。 + - A 股、港股、美股的科目名可以完全不同,都落在同一张行项目表里,用 `report_id` + `item_name` 区分;前端或分析时再通过「分类配置」把不同市场的不同科目名映射到你自己的分类(类现金、投资资产等)。 + +这样: + +- 同一套表支持所有市场。 +- 新市场只需新数据,不必改表结构。 +- 科目差异体现在「行项目」的 `item_name` 上,分类通过单独的分类配置表完成(见下)。 + +--- + +### 2. 常用统计数据如何存储(具体存哪些可后续再定)? + +**思路:单独建「报表统计值」表,按统计项键值存储;具体要存哪些可以逐步加。** + +- **不推荐**:每次从行项目实时聚合(如 SUM、比率),在报表很多时会有性能和复杂度问题。 +- **推荐**: + - 新增表 **financial_report_stats**: + - 关联到某一份报表(report_id)。 + - 存「统计项 key」(如 `total_assets`、`net_profit`、`roe`、`operating_cash_flow`、`current_ratio` 等)。 + - 存「统计值」(数值型);必要时可加「维度」字段(如按子类型、按口径)。 + - **何时写入**:在上传/覆盖该报表时,由后端解析并计算这些统计项,写入或更新本表。 + - **具体要存哪些**:可以在代码里先实现一批常用项(总资产、净资产、净利润、营收、经营现金流、ROE、资产负债率、流动比率等),后续要加新指标时只需加新的 `stat_key` 和计算逻辑,无需改表结构。若希望更灵活,可以用一条 JSONB 列存「多个 stat_key → value」,但查询和索引会稍复杂;键值表更清晰、易查、易扩展。 + +这样:常用统计只算一次、落库,后续绘图、筛选、对比都直接读预计算结果。 + +--- + +### 3. Node + NestJS 能否准确处理 Excel 并落库? + +**可以。用成熟库读 Excel,在服务层做校验与映射,再落库。** + +- **选型**: + - **xlsx (SheetJS)**:轻量、使用广,适合「读入整表 → 转 JSON/二维数组」再自己解析。 + - **exceljs**:API 更细(按单元格、按行),适合格式复杂或需要严格按行列解析的场景。 + 建议:若 Excel 格式相对固定(例如首行/首列是时间或科目名,其余为数值),用 **xlsx** 即可;若有多表、多标题行、合并单元格,可用 **exceljs** 按位置解析。 + +- **流程建议**: + 1. 上传接口:接收 Excel 文件 + 元数据(公司、市场、表型、报告期类型 年/季)。 + 2. 解析服务: + - 用 xlsx/exceljs 读取 sheet,得到二维数组或按行列访问。 + - 根据「市场 + 表型」选择或配置解析规则(例如:第几行是期数/时间,第几列是科目名,哪些列对应要落库的期数)。 + 3. 过滤:只保留你需要的报告期列(丢弃不需要的时间段)。 + 4. 校验: + - 必填项:公司、市场、表型、至少一期数据。 + - 数值列尽量做「可解析为数字」的校验;异常行可记录日志或返回错误行号。 + 5. 落库: + - 事务内:插入/更新报表主表 → 插入/更新行项目表(按行写入 item_name + value)。 + - 若同一公司、同一报告期、同一表型已存在,则采用「覆盖」或「跳过」策略(建议明确一种,并在接口文档说明)。 + 6. 预计算:在报表落库后调用统计计算逻辑,写入 **financial_report_stats**。 + +- **准确性**: + - 依赖 Excel 格式相对稳定。建议:为每种数据源(如 Wind 导出的 A 股、港交所格式等)定一个「解析模板」或配置(行列位置、科目名到内部 item_name 的映射),在代码里分支或配置化;这样同一来源的 Excel 能稳定、准确解析。 + - 可在解析后做简单一致性检查(如资产负债表左右是否平衡、利润表/现金流量表关键合计是否与行项目一致),不一致时打日志或返回警告,避免静默错误。 + +--- + +## 三、数据库设计(与现有库衔接) + +### 3.1 与现有表的关系 + +- **公司维度**:复用现有 **stock_info**(stock_code + market 唯一),不再新建「公司表」。一份报表归属到某公司 = 关联到 (stock_code, market)。 +- **用户**:若后续要做「我的自定义分类」按用户隔离,可关联 **user**;若仅自用单用户,可先不关联,分类配置全局即可。 + +### 3.2 表结构概览 + +``` +财务报表相关 +├── financial_report (报表主表:公司+报告期+表型+市场) +├── financial_report_line (行项目:科目名+数值,支持多市场科目差异) +├── financial_report_stats (预计算统计项:report_id + stat_key + value) +└── financial_item_category (可选,自定义分类:科目名/模式 → 你的分类标签) +``` + +### 3.3 表定义 + +#### financial_report 报表主表 + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|----------|------|------| +| id | BIGSERIAL | PRIMARY KEY | 主键 | +| stock_code | VARCHAR(20) | NOT NULL | 股票代码,关联 stock_info | +| market | VARCHAR(20) | NOT NULL | 市场,关联 stock_info | +| report_date | DATE | NOT NULL | 报告期截止日(如年报 2023-12-31,一季报 2023-03-31) | +| period_type | VARCHAR(20) | NOT NULL | 报告期类型:annual / quarterly | +| statement_type | VARCHAR(30) | NOT NULL | 表型:balance_sheet / income_statement / cash_flow | +| source_file_name | VARCHAR(255) | | 来源文件名(便于追溯) | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | +| UNIQUE(stock_code, market, report_date, statement_type) | | | 同一公司同一报告期同一表型唯一 | + +- 同一公司、同一报告期、同一表型只保留一份;上传时若已存在可覆盖(更新 updated_at 并删旧行项目、再插新行项目 + 重算 stats)。 + +#### financial_report_line 行项目表 + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|----------|------|------| +| id | BIGSERIAL | PRIMARY KEY | 主键 | +| report_id | BIGINT | NOT NULL, FK → financial_report.id | 所属报表 | +| item_name | VARCHAR(200) | NOT NULL | 科目名称(原文,如「货币资金」「Cash and bank balances」) | +| item_code | VARCHAR(50) | | 可选,科目编码(若 Excel 有) | +| value | DECIMAL(24, 6) | | 数值 | +| unit | VARCHAR(20) | | 单位(元、千元、万元等,可选) | +| row_order | INT | DEFAULT 0 | 行顺序(便于还原表序) | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | + +- 索引:report_id;(report_id, item_name) 若需防重复可加 UNIQUE,取决于你是否允许同名多行(多数情况下一表内科目名不重复)。 + +#### financial_report_stats 预计算统计表 + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|----------|------|------| +| id | BIGSERIAL | PRIMARY KEY | 主键 | +| report_id | BIGINT | NOT NULL, FK → financial_report.id | 所属报表 | +| stat_key | VARCHAR(80) | NOT NULL | 统计项键,如 total_assets, net_profit, roe, current_ratio | +| stat_value | DECIMAL(24, 6) | | 统计值 | +| dimension | VARCHAR(50) | | 可选维度(如口径、子类型) | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | +| UNIQUE(report_id, stat_key, dimension) | | | 同一报表同一 key(及维度)唯一,便于 upsert | + +- 具体 stat_key 可在代码中枚举或配置,逐步增加(如 total_assets, total_liabilities, net_assets, revenue, net_profit, operating_cash_flow, roe, roa, current_ratio, quick_ratio, asset_liability_ratio 等)。 + +#### financial_item_category 自定义分类(可选) + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|----------|------|------| +| id | BIGSERIAL | PRIMARY KEY | 主键 | +| user_id | BIGINT | FK → user.user_id, 可选 | 若多用户则按用户隔离;单用户可 NULL 表示全局 | +| market | VARCHAR(20) | NOT NULL | 市场 | +| statement_type | VARCHAR(30) | NOT NULL | 表型 | +| item_name_pattern | VARCHAR(200) | NOT NULL | 科目名或匹配模式(如精确「货币资金」或简单通配) | +| category_slug | VARCHAR(60) | NOT NULL | 你的分类标签,如 cash_like, investment, receivable, operating | +| sort_order | INT | DEFAULT 0 | 展示顺序 | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | + +- 查询时:根据报表的 market + statement_type,用 line 的 item_name 去匹配 item_name_pattern,得到 category_slug,再在前端或 API 里按 category 聚合/展示。后续若要支持「多对一」(多个科目同一分类),可多条记录相同 category_slug、不同 item_name_pattern。 + +--- + +## 四、Excel 解析与落库流程(NestJS 实现要点) + +1. **上传接口** + - 使用现有或新建 `StorageController` 的 multipart 接口,接受 `file` + body(stock_code, market, statement_type, period_type, report_dates_to_import[] 等)。 + - 文件类型校验:`.xlsx` / `.xls`(若需要),大小限制(如 10MB)。 + +2. **解析服务(新模块,如 FinancialReportParseService)** + - 使用 **xlsx** 或 **exceljs** 读 buffer。 + - 按 market + statement_type 选择解析规则(可从配置或策略 map 读取): + - 表头行、科目列、数据列对应关系; + - 哪些列对应「要落库的报告期」(对应 report_date)。 + - 输出:`{ reportDate, lines: [ { itemName, value, rowOrder } ] }[]`,只包含需要落库的期数。 + +3. **报表服务(FinancialReportService)** + - 对每个 reportDate: + - 若存在则删除旧 line、旧 stats; + - 插入 financial_report(或更新 updated_at); + - 批量插入 financial_report_line; + - 调用 StatsService 计算并写入 financial_report_stats。 + - 事务包裹,保证要么全部成功要么全部回滚。 + +4. **统计计算服务(FinancialReportStatsService)** + - 输入:report_id,以及该 report 的 lines(或从 DB 再查一次)。 + - 根据 statement_type 计算: + - 资产负债表:总资产、总负债、净资产、流动比率、速动比率等; + - 利润表:营收、净利润、毛利率等; + - 现金流量表:经营/投资/筹资现金流等。 + - 写入 financial_report_stats(upsert by report_id + stat_key + dimension)。 + +5. **校验与错误处理** + - 解析阶段:无法识别表头、缺少必填列时返回 400 + 明确错误信息。 + - 数值解析失败:可记录行号并跳过该行或整表拒绝,建议至少日志 + 返回「第 N 行解析失败」。 + - 若启用资产负债表平衡校验:资产合计 ≈ 负债+权益合计,不一致时记录 warning 或返回提示,不阻塞落库(因有时四舍五入或口径差异)。 + +--- + +## 五、后续可扩展点 + +- **多 sheet**:一个 Excel 内多 sheet 对应多表型或多公司时,可在解析时循环 sheet,每个 sheet 对应一个 (company, statement_type) 的导入。 +- **科目标准化**:若希望跨市场对比同一「逻辑科目」,可在 line 表增加 optional 的 `standard_item_code`,由解析时或后台任务根据 item_name + market 映射到统一编码。 +- **版本与审计**:若需保留历史版本,可在 financial_report 加 version 或不改主表、仅保留「每次上传生成新 report_id 并软删旧版」的策略。 +- **前端**:上传页选择公司(从 stock_info 查)、市场、表型、文件;解析结果可先预览「将落库的期数 + 行数」,确认后再写入;列表页按公司/时间/表型筛选,详情页按自定义分类展示行项目与预计算统计、并做简单图表。 + +--- + +## 六、总结 + +| 问题 | 建议 | +|------|------| +| 不同市场、不同公司科目不一致 | 用「报表主表 + 行项目表」存原始科目名与数值;分类用单独配置表做「科目名 → 自定义分类」映射。 | +| 常用统计不想每次算 | 用 financial_report_stats 表存 (report_id, stat_key, stat_value),在上传/更新报表时计算并写入;具体指标可后续逐步加。 | +| Node + NestJS 能否准确读 Excel 并落库 | 可以;用 xlsx 或 exceljs 解析,固定或配置化每种数据源格式,在服务层做校验与过滤后事务落库,并在落库后写 stats。 | + +按上述设计,你可以在不开放的前提下,先实现「上传 → 解析 → 落库 → 预计算」闭环,再在现有库上扩展列表/筛选/分类展示与绘图。