实际修改: - 店铺维度报表新增真实数据库条件查询:店铺字段走共享镜像 SQL WHERE,主体/能否变更/生效/失效走生命周期行级过滤(左闭右开) - 店铺负责人/部门负责人/利润中心改为店铺账号 PostgreSQL 镜像值, 按店铺名称关联,未匹配时回退经营分析副本库原值 - 能否变更按主表 platform 判定(唯品会MP/淘工厂=否,其余=是), 筛选改为 是/否 下拉;默认生效时间 2026-07-01,失效未记录显示存续中 - 主体维度经营报表接入生命周期收入窗口与注册地址解析市字段 - 同步更新视图测试并重写 StoreReportRepositoryTest 匹配 SQLite 实现 验证:容器内 tests/Feature/Business + tests/Unit/EcommerceFinance + tests/Unit/Business 共 324 passed / 4 skipped;真实数据端到端验证 (无筛选 1935 行、负责人/利润中心/失效区间筛选抽样全对)。 无新增数据库迁移,不涉及生产数据写入。
13 KiB
主体维度经营报表需求说明
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_namecompany_nameeffective_atexpires_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)
计算前必须同时满足:
- 账单店铺名称与
rpt店铺入口或其生命周期名称匹配; - 账单时间落在主体生命周期区间内;
- 账单时间落在当前指标的月份或滚动窗口内。
不使用 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. 验收标准
- 页面每个主体只出现一行,同名主体不会重复出现。
- 表头严格按第 3 节的 19 列顺序显示,最近12个月累计收入紧邻公司类型之后、1月收入之前。
- 页面不出现 ERP 店铺名称、利润中心、店铺负责人、开店时间、店铺状态或店铺累计收入等店铺字段。
- 月度字段显示当前年份 1–12 月;当前月、未来月份和无账单月份均为空。
- 最近12个月累计收入使用最近12个完整自然月,排除当前月,并支持跨年滚动。
- 所有收入均可追溯到匹配的
bills.income汇总;不得使用expense或其他金额字段。 - 主体切换边界遵循
[effective_at, expires_at),生效日计入、失效日不计入,不能重复统计。 - SQLite 有生命周期时使用历史/当前主体;无生命周期时保留
rpt当前主体。 - 底部 Tab 能随当前主体行切换,并按
businessLicenseName = 当前主体名称查询主体基础资料。 - 历史主体即使没有匹配的
rpt.fys_companyInfo资料,也保留收入行并显示资料缺失状态。 - 暂无来源的属性字段显示统一缺失值,不填充猜测数据。
- 验收必须覆盖:同主体多店铺、主体切换日、无生命周期店铺、无账单月份、跨年最近12个月、无匹配主体基础资料等场景。
11. 待后续确认事项
- SQLite 无生命周期时,收入起始时间是否使用
rpt.openTime; - “能否变更主体”“规划”“新主体”的数据来源及维护方式;
rpt.fys_companyInfo同一主体名称多条记录时的展示策略;- 底部 Tab 首次进入页面的默认选中主体;
- 历史主体在基础资料中查不到时的最终空状态文案;
- 当前年份是否需要显式显示在表头,例如“2026年1月收入”;
bills.occurred_at的时区和月份边界是否统一按系统时区解释。