CaiWuXiTong/docs/主体维度经营报表需求说明.md
WuYongJie d48cb7d639 feat: 店铺维度报表条件查询与主体归属字段接入
实际修改:
- 店铺维度报表新增真实数据库条件查询:店铺字段走共享镜像 SQL
  WHERE,主体/能否变更/生效/失效走生命周期行级过滤(左闭右开)
- 店铺负责人/部门负责人/利润中心改为店铺账号 PostgreSQL 镜像值,
  按店铺名称关联,未匹配时回退经营分析副本库原值
- 能否变更按主表 platform 判定(唯品会MP/淘工厂=否,其余=是),
  筛选改为 是/否 下拉;默认生效时间 2026-07-01,失效未记录显示存续中
- 主体维度经营报表接入生命周期收入窗口与注册地址解析市字段
- 同步更新视图测试并重写 StoreReportRepositoryTest 匹配 SQLite 实现

验证:容器内 tests/Feature/Business + tests/Unit/EcommerceFinance +
tests/Unit/Business 共 324 passed / 4 skipped;真实数据端到端验证
(无筛选 1935 行、负责人/利润中心/失效区间筛选抽样全对)。
无新增数据库迁移,不涉及生产数据写入。
2026-09-06 19:27:51 +08:00

13 KiB
Raw Blame History

主体维度经营报表需求说明

1. 文档状态

  • 状态:需求口径已确认,等待字段细节和实现排期。
  • 页面:公司管理 → 主体维度经营报表。
  • 路由标识:company-management/subject-operating-report。
  • 本文是开发和验收依据;在本口径之外不得自行扩展业务规则。
  • 实现时不要重新设计,沿用现有项目框架、只读上游访问、页面布局和表格组件。

2. 目标和展示粒度

报表以“主体名称”为唯一展示粒度:

一行一个主体。

主体下的店铺只参与底层收入归集,不在本页面展示。一个主体关联多个店铺、多个历史生命周期或多个平台时,最终都合并为同一主体行。

历史主体和当前主体都必须显示。历史主体即使在 rpt.fys_companyInfo 中已经查不到,也不能因为缺少主体基础资料而删除其收入行。

3. 最终字段和顺序

页面表格必须严格按以下顺序展示,字段名和顺序是验收契约:

顺序 字段 说明
1 市 由主体基础资料 rpt.fys_companyInfo.registeredAddress 解析地级市(App\Business\RegisteredAddressCity::parse):直辖市直接返回「XX市」;其余取省/自治区前缀后第一个地级行政区;本地地址省略省市前缀时按常州区划前缀兜底;识别不出按统一缺失值展示,不伪造数据
2 主体 主体名称;作为主体合并键
3 公司类型 主体公司类型,来源于 rpt
4 最近12个月累计收入 最近12个完整自然月收入合计,排除当前月
5 1月收入 当前年份 1 月收入,当前月或无账单时为空
6 2月收入 当前年份 2 月收入,当前月或无账单时为空
7 3月收入 当前年份 3 月收入,当前月或无账单时为空
8 4月收入 当前年份 4 月收入,当前月或无账单时为空
9 5月收入 当前年份 5 月收入,当前月或无账单时为空
10 6月收入 当前年份 6 月收入,当前月或无账单时为空
11 7月收入 当前年份 7 月收入,当前月或无账单时为空
12 8月收入 当前年份 8 月收入,当前月或无账单时为空
13 9月收入 当前年份 9 月收入,当前月或无账单时为空
14 10月收入 当前年份 10 月收入,当前月或无账单时为空
15 11月收入 当前年份 11 月收入,当前月或无账单时为空
16 12月收入 当前年份 12 月收入,当前月或无账单时为空
17 能否变更主体 逻辑暂未提供,先留空/按统一缺失值规则展示
18 规划 逻辑暂未提供,先留空/按统一缺失值规则展示
19 新主体(变更流程同步新主体) 逻辑暂未提供,先留空/按统一缺失值规则展示

以下字段明确删除,不得出现在主体维度经营报表中:

  • ERP 系统店铺名称
  • 店铺所属利润中心
  • 店铺负责人
  • 部门负责人/闭门负责人
  • 开店时间
  • 店铺状态
  • 连续12个月店铺累计收入(原“连续12个月店铺累计销量”)
  • 其他仅描述单个店铺的字段

“销量”在本报表中统一改为“收入”。所有收入数值均来自 SQLite bills.income,不以件数、利润或其他金额字段替代。

4. 数据来源和职责

4.1 rpt 主表

rpt 是主体报表的主表入口,先由它确定参与报表的店铺集合和主体基础维度。

主要对象:

  • rpt.fys_shopInfo:店铺主表,至少使用 erpShopName 作为名称关联入口,并读取当前店铺对应的主体关联信息。
  • rpt.fys_companyInfo:主体基础资料表,至少提供 businessLicenseName(主体名称)和 businessLicenseType(公司类型)。

逻辑原则:

  • 以 rpt.fys_shopInfo 中的店铺作为报表入口;
  • SQLite 有对应历史主体记录时,优先使用 SQLite 的主体及生命周期;
  • SQLite 没有对应历史主体记录时,使用 rpt 当前主体;
  • 不因为 SQLite 缺少生命周期记录而丢弃 rpt 中的店铺或主体。

4.2 SQLite 店铺报表数据

SQLite 文件逻辑上为项目已有店铺报表数据集(当前本地文件为 runtime/bills.sqlite),只读使用以下表:

bills:账单明细,收入字段为 income,账单时间字段为已标准化的 occurred_at。

store_lifecycles:店铺主体历史,字段包括:

  • erp_store_name
  • company_name
  • effective_at
  • expires_at

4.3 名称关联

主体报表使用名称关联,不以店铺 ID 作为主关联键:

rpt.fys_shopInfo.erpShopName
    ↔ store_lifecycles.erp_store_name
    ↔ bills.store_name

主体详细资料使用主体名称关联:

当前主体行.entity
    ↔ rpt.fys_companyInfo.businessLicenseName

名称匹配必须使用参数化查询和现有只读数据访问链路,禁止把用户输入直接拼接为 SQL。

5. 主体收入计算规则

5.1 生命周期区间

账单归属主体时使用左闭右开区间:

[effective_at, expires_at)

等价筛选条件:

bills.occurred_at >= effective_at
AND bills.occurred_at < expires_at

规则:

  • effective_at 当天计入;
  • expires_at 当天不计入;
  • expires_at 为空表示持续有效,右边界使用报表时间窗口的结束时间;
  • 主体切换日只能归属一个主体,不能重复计入;
  • 生效/失效时间只用于筛选账单,不在主体报表主表中展示。

5.2 收入口径

所有收入指标严格使用:

SUM(bills.income)

计算前必须同时满足:

  1. 账单店铺名称与 rpt 店铺入口或其生命周期名称匹配;
  2. 账单时间落在主体生命周期区间内;
  3. 账单时间落在当前指标的月份或滚动窗口内。

不使用 expense,不计算利润、净收入或件数,不从 rpt 读取收入金额。

5.3 主体合并

先按店铺名称和生命周期切分账单,再按主体名称合并:

rpt 店铺入口
    ↓ 名称匹配 SQLite 生命周期
主体生命周期内的 bills.income
    ↓ 按月 SUM
店铺/生命周期月度收入
    ↓ 按主体名称 SUM
主体月度收入和主体滚动12个月收入

同一个主体名称在多个店铺、多个主体生命周期或多个平台出现时,全部合并为一行。

6. 月度字段和滚动12个月

6.1 当前年份 1–12月

1–12月字段始终对应运行时当前年份,不固定写死某个年份:

当前年份 1月收入 ... 当前年份 12月收入

当前月排除:

  • 当前月份即使已有账单,也不计入月度字段;
  • 当前月字段显示为空;
  • 尚未到达的未来月份字段显示为空;
  • 过去月份没有账单时显示为空,不显示 0。

对已完成月份,主体某月收入为该月内、且属于主体有效生命周期的全部 bills.income 之和。

6.2 最近12个月累计收入

“最近12个月累计收入”放在 1月收入之前,取最近12个完整自然月,并排除当前月。

以当前月月初为右边界:

[当前月月初 - 12个月, 当前月月初)

例如当前时间在 2026 年 9 月,则窗口为:

[2025-09-01 00:00:00, 2026-09-01 00:00:00)

在该滚动窗口内,再叠加主体生命周期区间,最终执行 SUM(bills.income)。

最近12个月可能跨自然年,因此不等同于当前年份 1–12月字段之和。

如果主体在该滚动窗口内没有任何账单,最近12个月累计收入显示为空。

7. 缺失值规则

场景 展示规则
某个已完成月份没有账单 收入单元格为空
当前月或未来月份 收入单元格为空
最近12个月没有账单 累计收入单元格为空
暂无来源的主体属性字段(市、能否变更主体、规划、新主体) 按页面统一缺失值规则显示 -,不伪造数据
SQLite 没有生命周期,但 rpt 有店铺主体 使用 rpt 当前主体,收入按后续确认的兜底时间规则处理
历史主体在 rpt.fys_companyInfo 中查不到 保留主体收入行;底部基础资料 Tab 显示无匹配资料状态

SQLite 无生命周期时使用 rpt 当前主体已经确定;但该兜底记录的收入起始时间(例如是否使用 rpt.openTime)尚未确认,开发前不得自行选择。

8. 底部“主体详细基础资料”Tab

8.1 交互

在主体经营报表底部增加局部 Tab:

主体经营汇总表
    ↓ 选中当前主体行
底部“主体详细基础资料”Tab显示当前行主体资料

切换主体时,Tab 内容同步切换到当前主体;不重新设计为独立店铺页面,也不显示店铺资料。

首次进入页面的默认选中行、主体行点击方式和多选行为沿用现有页面交互规范;不得在没有确认的情况下增加复杂筛选或弹窗流程。

8.2 查询关系

当前主体名称取主体报表行的 entity 字段,使用以下查询关系:

SELECT *
FROM rpt.fys_companyInfo
WHERE businessLicenseName = :entity_name

其中 :entity_name 为当前选中行的主体名称。

SELECT * 是本需求确认的来源查询关系。页面展示必须继续遵守项目现有的敏感字段保护规则,不得将账号、密码、账户等敏感数据明文暴露。

8.3 资料缺失和多记录策略

  • 查不到基础资料时,不删除主体经营行;Tab 显示“暂无匹配资料”或等价的空状态。
  • rpt.fys_companyInfo 同一主体名称返回多条记录时,展示全部记录还是选择一条,待后续确认;实现前不得擅自按最新记录或任意排序覆盖其他记录。
  • 基础资料字段顺序优先沿用现有公司基础信息页面,若需不同顺序另行确认。

9. 实现边界

本次要实现

  • 将主体经营报表改为一主体一行;
  • 删除所有店铺级列及店铺级累计指标;
  • 增加并固定显示当前年份 1–12月收入列;
  • 将最近12个月累计收入放到 1月收入之前;
  • 按 SQLite bills.income 汇总收入;
  • 按主体生命周期左闭右开区间筛选账单;
  • 合并历史主体和当前主体的主体行;
  • 按主体名称合并多个店铺和多个生命周期结果;
  • 增加底部主体详细基础资料 Tab,并按主体名称查询 rpt.fys_companyInfo。

本次不要实现

  • 不新增店铺维度字段到主体页面;
  • 不把收入改成销量件数、利润或净收入;
  • 不猜测“市”“能否变更主体”“规划”“新主体”的业务来源和计算逻辑;
  • 不擅自决定 SQLite 无生命周期时的收入起始日期;
  • 不擅自决定主体基础资料多记录的取数策略;
  • 不修改上游数据库,不向 rpt 或 SQLite 写回业务数据;
  • 不新增另一套报表数据源或绕过现有只读/镜像访问边界。

10. 验收标准

  1. 页面每个主体只出现一行,同名主体不会重复出现。
  2. 表头严格按第 3 节的 19 列顺序显示,最近12个月累计收入紧邻公司类型之后、1月收入之前。
  3. 页面不出现 ERP 店铺名称、利润中心、店铺负责人、开店时间、店铺状态或店铺累计收入等店铺字段。
  4. 月度字段显示当前年份 1–12 月;当前月、未来月份和无账单月份均为空。
  5. 最近12个月累计收入使用最近12个完整自然月,排除当前月,并支持跨年滚动。
  6. 所有收入均可追溯到匹配的 bills.income 汇总;不得使用 expense 或其他金额字段。
  7. 主体切换边界遵循 [effective_at, expires_at),生效日计入、失效日不计入,不能重复统计。
  8. SQLite 有生命周期时使用历史/当前主体;无生命周期时保留 rpt 当前主体。
  9. 底部 Tab 能随当前主体行切换,并按 businessLicenseName = 当前主体名称 查询主体基础资料。
  10. 历史主体即使没有匹配的 rpt.fys_companyInfo 资料,也保留收入行并显示资料缺失状态。
  11. 暂无来源的属性字段显示统一缺失值,不填充猜测数据。
  12. 验收必须覆盖:同主体多店铺、主体切换日、无生命周期店铺、无账单月份、跨年最近12个月、无匹配主体基础资料等场景。

11. 待后续确认事项

  • SQLite 无生命周期时,收入起始时间是否使用 rpt.openTime;
  • “能否变更主体”“规划”“新主体”的数据来源及维护方式;
  • rpt.fys_companyInfo 同一主体名称多条记录时的展示策略;
  • 底部 Tab 首次进入页面的默认选中主体;
  • 历史主体在基础资料中查不到时的最终空状态文案;
  • 当前年份是否需要显式显示在表头,例如“2026年1月收入”;
  • bills.occurred_at 的时区和月份边界是否统一按系统时区解释。