Files
invest-mind-store/packages/design-document/我编写的文档/财务报表模块-产品与数据库设计.md
R524809 1f7fded275 tq[MESSAGE]:
[TYPE]:
[JIRA]:
2026-02-11 16:05:45 +08:00

14 KiB
Raw Blame History

财务报表模块 - 产品与数据库设计

一、需求摘要

  • 用途:个人研究用,上传并落库公司历次财务报表(资产负债表、利润表、现金流量表),支持按年/按季;后续做绘制、分类、对比查看。
  • 数据来源:页面上传 Excel每次一家公司、一种表型、多期数据表中部分时间/列需要丢弃,部分需要落库。
  • 扩展需求:不同市场(如 A 股 / 港股)科目名不同;需支持自定义分类(如类现金资产、投资资产、应收类、经营类等);常用统计希望预存避免每次计算。

二、三个核心问题的建议

1. 数据库如何支持不同市场、不同公司的财务数据(科目不一致)?

思路:用「报表 + 行项目」的通用结构,行项目以「原始科目名 + 数值」存储,不强行统一科目编码。

  • 不采用:为每个市场建一套固定列(如 A 股一张表、港股一张表)。维护成本高,且新增市场或科目时要改表结构。
  • 推荐
    • 报表主表:一条记录 = 某公司、某报告期、某表型(资产负债表/利润表/现金流量表)、某市场。
    • 行项目表:每条记录 = 某报表下的一个科目一行,存「科目名称(原文)」「数值」「单位」「顺序」等。
    • A 股、港股、美股的科目名可以完全不同,都落在同一张行项目表里,用 report_id + item_name 区分;前端或分析时再通过「分类配置」把不同市场的不同科目名映射到你自己的分类(类现金、投资资产等)。

这样:

  • 同一套表支持所有市场。
  • 新市场只需新数据,不必改表结构。
  • 科目差异体现在「行项目」的 item_name 上,分类通过单独的分类配置表完成(见下)。

2. 常用统计数据如何存储(具体存哪些可后续再定)?

思路:单独建「报表统计值」表,按统计项键值存储;具体要存哪些可以逐步加。

  • 不推荐:每次从行项目实时聚合(如 SUM、比率在报表很多时会有性能和复杂度问题。
  • 推荐
    • 新增表 financial_report_stats
      • 关联到某一份报表report_id
      • 存「统计项 key」total_assetsnet_profitroeoperating_cash_flowcurrent_ratio 等)。
      • 存「统计值」(数值型);必要时可加「维度」字段(如按子类型、按口径)。
    • 何时写入:在上传/覆盖该报表时,由后端解析并计算这些统计项,写入或更新本表。
    • 具体要存哪些可以在代码里先实现一批常用项总资产、净资产、净利润、营收、经营现金流、ROE、资产负债率、流动比率等后续要加新指标时只需加新的 stat_key 和计算逻辑,无需改表结构。若希望更灵活,可以用一条 JSONB 列存「多个 stat_key → value」但查询和索引会稍复杂键值表更清晰、易查、易扩展。

这样:常用统计只算一次、落库,后续绘图、筛选、对比都直接读预计算结果。


3. Node + NestJS 能否准确处理 Excel 并落库?

可以。用成熟库读 Excel在服务层做校验与映射再落库。

  • 选型

    • xlsx (SheetJS):轻量、使用广,适合「读入整表 → 转 JSON/二维数组」再自己解析。
    • exceljsAPI 更细(按单元格、按行),适合格式复杂或需要严格按行列解析的场景。
      建议:若 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_infostock_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 + bodystock_code, market, statement_type, period_type, report_dates_to_import[] 等)。
    • 文件类型校验:.xlsx / .xls(若需要),大小限制(如 10MB
  2. 解析服务(新模块,如 FinancialReportParseService

    • 使用 xlsxexceljs 读 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_statsupsert 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。

按上述设计,你可以在不开放的前提下,先实现「上传 → 解析 → 落库 → 预计算」闭环,再在现有库上扩展列表/筛选/分类展示与绘图。