Skip to content

Reporting & Analytics Domain - Deep Dive Analysis

1. Domain Overview

1.1 领域职责描述

报告与分析领域是 Speaker Platform 中面向数据消费的核心领域,负责为三类用户(Planner、Sales Rep、Speaker)提供多维度的数据可视化、统计分析和数据导出能力。该领域覆盖以下核心职责:

  1. Program Activity 报告体系 - 项目活动统计、预算花费分析、地理区域分析、销售代表活动分析
  2. Attendee Analytics 报告体系 - 参会者追踪、注册摘要、目标/非目标分析、按专业/地理/项目统计
  3. At a Glance 仪表板 - 整体概览(项目数量、出席者总数、Top Speaker、整体花费)
  4. Ad-hoc Report Builder - 用户自定义报告字段选择、拖拽排序、运行与导出
  5. Custom Reports - 基于 R/Python 脚本的自定义报告(服务端执行脚本生成 Excel)
  6. Compliance Audit - 合规审计报告
  7. Data Extract - 原始数据提取服务(15 种数据表直接导出)
  8. Aggregate Spend Report - 聚合花费报告及 Porzio/Verona 合规报告
  9. Survey Reports - 调查问卷统计分析
  10. Excel/CSV 导出引擎 - 基于注解驱动的通用导出框架

1.2 涉及的后端模块和包

模块/包路径职责
reportmodules/v1/report/主报告模块(3 个 Service, 2 个 Controller, 20+ Request/Response)
adhocmodules/v1/adhoc/Ad-hoc 报告构建器(1 个 Service, 1 个 Controller)
compliancemodules/v1/compliance/合规报告及聚合花费(1 个 Service, 1 个 Controller)
ExcelServicecommon/service/ExcelService.java通用 Excel/CSV 导出引擎
ExcelColumncommon/annotation/ExcelColumn.java字段导出元数据注解

2. Data Model Analysis

2.1 Entity Overview Table

EntityTable文件路径字段说明
AdhocReportt_adhoc_reportcommon/persistence/entity/AdhocReport.javaid, meetingId, reportName, createdAt, updatedAtAd-hoc 报告定义
AdhocReportDetailt_adhoc_report_detailcommon/persistence/entity/AdhocReportDetail.javaid, adhocReportId, fieldLabel, sequenceAd-hoc 报告字段选择(哪些列、什么顺序)
ReportFieldt_report_fieldcommon/persistence/entity/ReportField.javareportFieldId, reportFieldLabel报告字段字典(自定义注册字段)
AggregateSpendReportt_aggregate_spend_reportcommon/persistence/entity/AggregateSpendReport.javaid, productId, fieldId, createdAt, createdBy聚合花费报告配置
AggregateSpendReportFieldt_aggregate_spend_report_fieldcommon/persistence/entity/AggregateSpendReportField.javaid, fieldName, fieldLabel聚合花费报告字段定义

2.2 Table Relationships (ER Diagram - ASCII)

+-------------------+       1:N       +------------------------+
|  t_adhoc_report   |<--------------->| t_adhoc_report_detail  |
|-------------------|                 |------------------------|
| id (PK)           |                 | id (PK)                |
| meeting_id (FK)   |                 | adhoc_report_id (FK)   |
| report_name       |                 | field_label            |
| created_at        |                 | sequence               |
| updated_at        |                 +------------------------+
+-------------------+                         |
       |                                      | field_label maps to
       | meeting_id                            v
       v                              +-------------------+
+-------------------+                 |  t_report_field   |
|    t_meeting      |                 |-------------------|
| (program context) |                 | report_field_id   |
+-------------------+                 | report_field_label|
                                      +-------------------+
                                              ^
                                              | also maps to
                                      +---------------------------+
                                      | AdhocReportFieldStorage   |
                                      | (hardcoded field mapping) |
                                      | 34 system fields          |
                                      +---------------------------+

+----------------------------+       1:N       +----------------------------------+
| t_aggregate_spend_report   |<--------------->| (implicit: field mapping via     |
|----------------------------|                 |  t_aggregate_spend_report_field) |
| id (PK)                   |                 |----------------------------------|
| product_id (FK)           |                 | id (PK)                          |
| field_id (FK)             |                 | field_name                       |
| created_at                |                 | field_label                      |
| created_by                |                 +----------------------------------+
+----------------------------+

2.3 Data Model Issues

  1. AdhocReportDetail 双 @Id 注解错误 (AdhocReportDetail.java:10-14)

    • idadhocReportId 都标记了 @Id,这在 JPA 中意味着复合主键,但实际上 adhocReportId 是外键,不应该是 @Id。这可能导致 MyBatis tk.mapper 行为异常。
  2. 报告领域无专属数据模型,严重依赖其他领域表

    • 报告数据全部通过 ProductReportMapper 的复杂 SQL JOIN 查询从 meeting、attendee、budget、speaker、user 等核心表中汇总。报告领域本身只有 5 张表,但 ProductReportMapper.xml 中的 SQL 可能包含数十张表的 JOIN。
  3. AdhocReportFieldStorage 硬编码字段映射 (adhoc/model/AdhocReportFieldStorage.java:11-50)

    • 34 个系统字段的 label-to-fieldName 映射硬编码在 Java 静态 Map 中,无法通过配置或数据库动态扩展。新增参会者字段必须改代码。
  4. ReportField 表与 AdhocReportFieldStorage 职责重叠

    • t_report_field 存储动态的自定义注册字段,而 AdhocReportFieldStorage 存储系统预定义字段。两者合并后才是 Ad-hoc 报告可用的完整字段列表。这种拆分增加了理解和维护成本。

3. Business Flow Analysis

3.1 Core Business Flows (ASCII Flow Diagrams)

3.1.1 标准报告生成流程(以 Program Summary Download 为例)

+------------------+     GET /v1/products/{id}/reports/          +--------------------+
| Salesview        |     program-activity/program-summary         | ProductReport      |
| (Frontend)       |-------------------------------------------->| Controller         |
+------------------+     query params: year, periodFrom,          +--------------------+
                         periodTo, productId                              |
                                                                          v
                                                               +--------------------+
                                                               | ProductReport      |
                                                               | Service            |
                                                               +--------------------+
                                                                          |
                         +------------------------------------------------+
                         |
                         v
              +-------------------------+
              | 1. Check user           |
              |    permissions          |
              |    (Product/Region/     |
              |     District scope)     |
              +-------------------------+
                         |
                         v
              +-------------------------+
              | 2. ProductReportMapper  |
              |    .getProgramSummary   |
              |    Responses(query)     |
              |    [Complex SQL JOIN]   |
              +-------------------------+
                         |
                         v
              +-------------------------+
              | 3. Optional: Calculate  |
              |    AttendeeMetrics      |
              |    (prescribers,        |
              |     targets) from       |
              |    targets.xlsx file    |
              +-------------------------+
                         |
                         v
              +-------------------------+
              | 4. Optional: Add       |
              |    BudgetCategory      |
              |    dynamic columns     |
              +-------------------------+
                         |
                         v
              +-------------------------+
              | 5. ExcelService.export |
              |    (annotation-driven  |
              |     XLSX generation)   |
              +-------------------------+
                         |
                         v
              +-------------------------+
              | 6. HTTP Response       |
              |    Content-Disposition |
              |    attachment          |
              +-------------------------+

3.1.2 Ad-hoc Report Builder 流程

+------------------+                      +--------------------+
| Plannerview      |                      | AdhocReport        |
| (Frontend)       |                      | Controller         |
+------------------+                      +--------------------+
       |                                          |
       | 1. GET /v1/adhoc/fields?meetingId=X      |
       |----------------------------------------->|  systemFields()
       |<-----------------------------------------|  => AdhocReportFieldStorage
       |   [34 system fields + custom fields]     |     + ReportFieldMapper
       |                                          |
       | 2. User drag-drop fields to build report |
       |                                          |
       | 3. POST /v1/adhoc                        |
       |   {meetingId, reportName,                |
       |    reportField: [{label, sequence}]}     |
       |----------------------------------------->|  saveAdhoc()
       |                                          |  => INSERT t_adhoc_report
       |                                          |  => INSERT t_adhoc_report_detail (per field)
       |                                          |
       | 4. GET /v1/adhoc/run?id=X&meetingId=Y    |
       |----------------------------------------->|  listAdhocReportDetails()
       |                                          |  => AttendeeMapper.listAttendeeRegistrations()
       |                                          |  => Map fields dynamically
       |<-----------------------------------------|
       |   [Paginated attendee data with          |
       |    selected fields only]                 |
       |                                          |
       | 5. GET /v1/adhoc/run/download            |
       |----------------------------------------->|  adhocReportDownload()
       |                                          |  => DynamicField-based Excel export
       |<-----------------------------------------|
       |   [Excel file download]                  |

3.1.3 Custom Reports 流程(R/Python 脚本执行)

+------------------+    GET /v1/products/{id}/    +------------------------+
| Salesview        |    custom-reports             | ProductCustomReport    |
| (Frontend)       |----------------------------->| Controller             |
+------------------+                              +------------------------+
       |                                                   |
       |  listReports()                                    v
       |<------------------------------+  Scan filesystem for .R/.py files
       |  [report name, comment,       |  Parse first 2 lines:
       |   permitted roles]            |    Line 1: # comment/description
       |                               |    Line 2: # ROLE1,ROLE2 (permissions)
       |                               +  Filter by user roles
       |
       | GET /v1/products/{id}/custom-reports/{name}
       |----------------------------->|
       |                              |  download()
       |                              v
       |                   +-------------------------+
       |                   | 1. Run SQL data extract |
       |                   |    (COPY TO CSV via     |
       |                   |     PGConnection)       |
       |                   +-------------------------+
       |                              |
       |                   +-------------------------+
       |                   | 2. Some reports:        |
       |                   |    generate input data  |
       |                   |    via Java (e.g.       |
       |                   |    SpeakerProgramCost   |
       |                   |    Summary, Verona)     |
       |                   +-------------------------+
       |                              |
       |                   +-------------------------+
       |                   | 3. Execute script:      |
       |                   |    Rscript <file>.R     |
       |                   |    or python3 <file>.py |
       |                   |    (ProcessBuilder)     |
       |                   +-------------------------+
       |                              |
       |                   +-------------------------+
       |                   | 4. Find OUTPUT files:   |
       |                   |    "OUTPUT - {name}.xlsx"|
       |                   |    Single: download     |
       |                   |    Multiple: ZIP        |
       |                   +-------------------------+
       |                              |
       |<-----------------------------|
       |  [File download]             |

3.1.4 导出引擎流程

+------------------+
| ExcelService     |
| .export(list,    |
|  fileName)       |
+------------------+
         |
         v
+---------------------------+
| 1. Reflection: scan       |
|    @ExcelColumn fields    |
|    on entity class        |
+---------------------------+
         |
         v
+---------------------------+
| 2. isOwnColumn() check:  |
|    - optional: check      |
|      agencyConfig         |
|    - excludeByProperty:   |
|      check ProductConfig  |
|    - includeByProperty:   |
|      check ProductConfig  |
+---------------------------+
         |
         v
+---------------------------+
| 3. Build header map       |
|    (colIndex -> title)    |
|    Build data rows        |
|    (colIndex -> value)    |
+---------------------------+
         |
         v
+---------------------------+
| 4. Handle DynamicField    |
|    (ExcelEntity subclass) |
|    for runtime columns    |
+---------------------------+
         |
         v
+---------------------------+
| 5. Generate Workbook      |
|    (SXSSFWorkbook/HSSF)   |
|    or CSV/TXT             |
+---------------------------+
         |
         v
+---------------------------+
| 6. Write to HTTP response |
|    or server filesystem   |
+---------------------------+

3.2 Validation Rules

规则位置说明
报告名唯一性AdhocReportService.java:128-141同一 meeting 下 Ad-hoc 报告名称不能重复,保存前调用 checkAdhocName
报告字段非空AdhocReportRequestDTO.java:19@NotEmpty 注解,报告必须至少选择一个字段
meetingId 非空AdhocReportRequestDTO.java:14@NotNull 注解
reportName 非空AdhocReportRequestDTO.java:17@NotBlank 注解
权限范围过滤ProductReportService.java:266-278根据用户角色(Product/Region/District)自动过滤数据范围
空数据校验ProductReportService.java:281-283导出时若查询结果为空,抛出 NOT_FOUND 异常
Custom Report 角色权限ProductCustomReportService.java:86-98R/Python 脚本第二行定义允许角色,运行时校验用户角色

3.3 Business Logic Issues

  1. 权限过滤逻辑大量重复 (ProductReportService.java:266-278, 538-551, 569-583, 634-651, 703-724)

    • 至少 8 个方法中重复了相同的 Product/Region/District 权限判断逻辑。应抽取为通用方法。
  2. 目标匹配依赖文件系统 (ProductReportService.java:1246-1273)

    • loadTargetLookupMaps() 方法从文件系统读取 targets.xlsx 文件来判断 attendee 是否为 target,而不是查询数据库。每次报告生成都要重新读取解析 Excel 文件。
  3. Noven 客户特定逻辑硬编码 (ProductReportService.java:429-436)

    • 多个 Noven 客户特定的字段映射硬编码在代码中(如 "Will you be joining live or virtually?""URO/ONC"),违反了多租户配置化原则。
  4. Custom Report 通过 ProcessBuilder 执行外部脚本 (ProductCustomReportService.java:314-324)

    • 安全风险:执行服务器上的 R/Python 脚本,脚本内容完全由文件系统控制。
    • 可靠性:依赖服务器安装 R 和 Python 环境。
    • 报告名硬编码分支处理:"Speaker Program Cost Summary""Verona Aggregate Spend Report""Tracking Campaign Stats" 三个报告名在代码中用 if-else 特殊处理 (ProductCustomReportService.java:159-256)。
  5. Data Extract 通过 PostgreSQL COPY 命令直接导出 (ProductCustomReportService.java:348-394)

    • 使用 PGConnection.getCopyAPI() 执行原始 SQL 文件导出 CSV,SQL 文件存储在文件系统的 dataextract 目录。这绕过了所有权限控制和数据过滤。

4. API Inventory

4.1 REST Endpoints Table

ProductReportController (v1/products/{productId}/reports)

HTTP MethodPath方法说明返回类型
GET/at-a-glancegetAtAGlance概览仪表板AtAGlanceResponse
GET/program-activity/program-summaryexportPrograms项目摘要下载 (Excel)void (file)
GET/program-activity/dashboardprogramActivityDashboard项目活动仪表板ProgramActivityDashboardResponse
GET/program-activity/dashboard-program-activity-summarygetProgramActivitySummary按品牌的活动摘要ProgramActivitySummaryResponse
GET/program-activity/dashboard-program-activity-by-branddashboardProgramActivityByBrand按品牌跨年度的活动分析ProgramActivitySummaryResponse
GET/program-activity/fiscal-ytd-spend-by-brandgetFiscalYearToDateSpendByBrand年度品牌花费List<FiscalYearToDateSpendResponse>
GET/program-activity/fiscal-ytd-operated-future-programsgetFiscalYtdOperatedFuturePrograms年度已执行/未来项目List<FiscalYearToDateProgramsResponse>
GET/program-activity/programs-by-budget-statusgetProgramsByBudgetStatus按预算状态的项目分布List<ProgramsByStatusResponse>
GET/program-activity/geography-and-spendgetProgramActivitiesByGeographySpend地理区域花费List<GeographySpendResponse>
GET/program-activity/geography-and-spend/downloaddownloadProgramActivities地理区域花费下载void (file)
GET/program-activity/geography-and-spend/program-type-detailsgetGeographySpendProgramTypeDetails地理区域项目类型详情List<GeographySpendProgramTypeDetailsResponse>
GET/program-activity/geography-and-spend/program-type-details/downloaddownloadGeographySpendDistrictDetails地理区域项目类型详情下载void (file)
GET/program-activity/geography-and-spend/program-detailsgetGeographySpendProgramDetails地理区域项目详情List<GeographySpendProgramDetailsResponse>
GET/program-activity/geography-and-spend/program-details/downloaddownloadGeographySpendProgramDetails地理区域项目详情下载void (file)
GET/program-activity/sales-representativesgetProgramActivitiesBySalesRep按销售代表的活动List<ActivityBySalesRepResponse>
GET/program-activity/sales-representatives/downloaddownloadProgramActivitiesBySalesRep按销售代表的活动下载void (file)
GET/program-activity/sales-representatives/{userId}/programsgetProgramsBySalesRep特定销售代表的项目列表List<ProgramsBySalesRepResponse>
GET/program-activity/sales-representatives/{userId}/programs/downloaddownloadProgramsBySalesRep特定销售代表项目下载void (file)
GET/program-activity/speaker-utilizationlistSpeakerUtilizationSpeaker 利用率List<SpeakerUtilizationResponse>
GET/program-activity/speaker-utilization/downloaddownloadSpeakerUtilizationSpeaker 利用率下载void (file)
GET/program-activity/speaker-utilization-detaillistSpeakerUtilizationDetailSpeaker 利用率详情List<SpeakerUtilizationDetailResponse>
GET/program-activity/speaker-utilization-detail/downloaddownloadSpeakerUtilizationDetailSpeaker 利用率详情下载void (file)
GET/attendee-analytics/dashboardattendeeDashboard参会者分析仪表板AttendeeAnalyticsDashboardResponse
GET/attendee-analytics/attendeetrackersdownloadAttendeeTrackers参会者追踪下载 (Excel)void (file)
GET/attendee-analytics/registration-summarygetRegistrationSummary注册摘要List<RegistrationSummaryResponse>
GET/attendee-analytics/target-reach-by-specialtygetTargetReachBySpecialty按专业的目标覆盖List<TargetReachBySpecialtyResponse>
GET/attendee-analytics/registration-by-geographygetRegistrationByGeography按地理的注册分布List<RegistrationByGeographyResponse>
GET/attendee-analytics/registration-by-programgetRegistrationByProgram按项目的注册分布List<RegistrationByProgramResponse>
GET/compliance-audit-listgetComplianceAuditList合规审计列表List<ComplianceAuditResponse>

总计: 29 个端点

ProductCustomReportController (v1/products/{productId}/custom-reports)

HTTP MethodPath方法说明返回类型
GET/listReports列出可用的自定义报告List<CustomReportResponse>
GET/{reportName}download执行并下载自定义报告void (file)

总计: 2 个端点

AdhocReportController (v1/adhoc)

HTTP MethodPath方法说明返回类型
GET/listAdhocReports分页列出 Ad-hoc 报告PageResult<AdhocReport>
GET/{id}adhocDetail获取 Ad-hoc 报告详情AdhocResponseDTO
GET/fieldsfields获取可用报告字段List<ReportFieldDTO>
POST/saveAdhoc保存/更新 Ad-hoc 报告void
GET/checkcheckAdhocName检查报告名唯一性List<AdhocReport>
DELETE/{id}deleteAdhoc删除 Ad-hoc 报告void
GET/runadhocRunList运行 Ad-hoc 报告(分页)PageResult<ListAdhocReportDetailsResponse>
GET/run/downloadadhocReportDownload下载 Ad-hoc 报告 Excelvoid (file)

总计: 8 个端点

ComplianceController (v1/compliance) - 报告相关部分

HTTP MethodPath方法说明返回类型
GET/listCompliance合规列表PageResult
GET/exportexport合规数据导出void (file)
GET/{attendeeId}/profileprofile参会者合规详情ComplianceProfileResponse
GET/configurationgetAggregateSpendConfiguration聚合花费报告配置List<AggregateSpendConfiguration>
POST/configurationsaveAggregateSpendConfiguration保存配置void
GET/field-mappingsgetAllFieldMappings字段映射列表List<AggregateSpendFieldMapping>
POST/field-mappingsaddFieldMapping添加字段映射void
DELETE/field-mappingsdeleteFieldMapping删除字段映射void

总计: 8 个端点

所有报告相关端点总计: 47 个

4.2 API Design Issues

  1. 下载端点使用 GET 方法但返回 void - 所有 download 端点(约 15 个)使用 GET 请求方法但直接操作 HttpServletResponse 写文件流。虽然功能正确,但违反 RESTful 设计:GET 应该是幂等的数据查询,文件下载应使用 POST 或至少明确标注 produces 类型。

  2. 报告查询参数通过 query string 传递复杂过滤条件 - 如 ProgramSummaryQuery 包含多个 List 类型参数(regionIds, districtIds),通过 Spring MVC 的自动绑定虽然能工作,但不如 POST + RequestBody 清晰。

  3. 缺乏统一的报告 API 命名规范 - 混用 downloadexportrun/download、直接 GET(返回 void)等多种下载模式。

  4. Ad-hoc 报告的 check 端点设计 (AdhocReportController.java:63-67) - 使用 GET 返回同名报告列表,前端需要额外判断。应设计为返回布尔值的校验端点。

  5. Controller 层缺乏 @ApiOperation 注解一致性 - ProductReportControllergetComplianceAuditList 方法(第 279 行)缺少 @ApiOperation 注解。


5. Frontend Analysis

5.1 Pages & Components

Salesview 报告页面结构(核心报告 UI 所在)

pharmagin-salesview/src/pages/Reports/
├── index.js                    # 主路由入口 + Redux/Saga 注入
├── Navigation.js               # 左侧导航菜单
├── menus.js                    # 菜单配置(权限过滤)
├── saga.js                     # 顶层 saga(At-a-Glance, Attendee Dashboard, Download 等)
├── reducer.js                  # 顶层 reducer
├── actions.js                  # 顶层 actions
├── selectors.js                # 顶层 selectors

├── Glance/                     # At a Glance 仪表板
│   ├── index.js                # 4 个指标卡片 + 图表
│   ├── Filters.js              # 年份筛选器
│   └── Loadable.js             # React.lazy 包装

├── ProgramActivity/            # 项目活动报告(最复杂的子模块)
│   ├── index.js                # 子路由分发(11 个子路由)
│   ├── actions.js              # 18 个 Redux actions (createRoutine)
│   ├── reducer.js              # 13 个状态字段
│   ├── saga.js                 # 18 个 saga workers
│   ├── selectors.js            # Redux selectors
│   ├── constants.js            # 查询参数常量
│   └── components/
│       ├── Dashboard/              # 仪表板(Program Spend, Booked Programs, Spend vs Budget)
│       ├── DashboardProgramActivitySummary.js
│       ├── DashboardProgramActivityByBrand.js
│       ├── ProgramSummaryReport.js # 项目摘要报告下载
│       ├── ReportDownload.js       # 通用下载组件
│       ├── FiscalYtdSpendByBrand.js
│       ├── FiscalYtdOperatedFuturePrograms.js
│       ├── ProgramsByBudgetStatus.js
│       ├── ProgramActivityByGeographyAndSpend.js
│       ├── ProgramActivityBySalesRepresentatives.js
│       ├── SpeakerUtilization.js
│       ├── GeographySpendReportList/   # 多层级钻取列表
│       │   ├── SpendReportDetailList/
│       │   └── ProgramDetailList/
│       ├── SalesRepReportList/         # 销售代表报告钻取
│       │   └── SalesReport/
│       ├── SpeakerUtilizationList/     # Speaker 利用率钻取
│       │   └── SpeakerUtilizationDetail/
│       ├── SpendByBrandList/
│       ├── ProgramsByBudgetStatusList/
│       └── FiscalPeriodToDateOperatedFutureProgramsList/

├── AttendeeAnalytics/          # 参会者分析
│   ├── AttendeeTracker.js      # 参会者追踪下载
│   ├── Dashboard/              # 参会者仪表板(4 个图表)
│   ├── RegistrationSummary/    # 注册摘要表格
│   ├── RegistrationBySpecialty/# 按专业的目标覆盖
│   ├── RegistrationByGeography/# 按地理的注册分布
│   └── RegistrationByProgram/  # 按项目的注册分布

├── Survey/                     # 调查问卷报告
│   ├── index.js                # 调查列表
│   └── SurveyProfile.js       # 调查详情和统计

├── OtherSurvey/                # 其他调查

├── CustomReports/              # 自定义报告
│   ├── index.js                # 子路由(列表 + 运行)
│   ├── ReportsList.js          # 报告列表(从硬编码 data.js 读取 demo 数据)
│   ├── RunReport.js            # 运行报告分发
│   ├── ReportForm.js           # 报告字段选择表单(拖拽)
│   ├── ProgramActivityReport.js # 项目活动类报告
│   ├── AttendeeReport.js       # 参会者类报告
│   ├── data.js                 # 硬编码 demo 数据和字段定义
│   └── Filters.js              # 筛选器

├── ComplianceAudit/            # 合规审计
│   ├── index.js                # 审计列表
│   ├── List.js                 # 列表组件
│   ├── Detail.js               # 详情页(嵌套 Tab)
│   ├── ProgramInfo.js          # 项目信息
│   ├── Attendees.js            # 参会者
│   ├── Budget.js               # 预算
│   ├── TransferOfValue.js      # 价值转移
│   ├── Documentation.js        # 文档清单
│   ├── DocumentationFileList.js
│   └── Filters.js              # 筛选器

└── Form/
    └── DownloadForm.js         # 通用下载表单组件

Salesview 报告组件总计: ~80+ 个文件

Plannerview Ad-hoc Report 组件

pharmagin-plannerview/legacy/src/components/AdhocReport/
├── index.js              # 主组件(Ad-hoc 报告管理 + 运行)
├── AdhocReportForm.js    # 报告创建/编辑表单(拖拽字段选择)
├── actions.js            # 10 个 Redux actions
├── reducer.js            # Redux reducer
├── sagas.js              # 8 个 saga workers
├── selectors.js          # Redux selectors
└── Drag/
    ├── Card.js           # 拖拽卡片组件
    ├── Container.js      # 拖拽容器
    └── SystemFieldBox.js # 系统字段面板

Plannerview Ad-hoc 报告组件总计: 9 个文件

Speakerview 报告功能

Speakerview 中没有独立的报告模块。唯一与报告相关的是 Speaker Detail 页面中的部分统计展示(如 speaker 的项目历史),不属于报告领域。

5.2 Redux State Structure

Salesview Reports 顶层 State (key: 'reports')

javascript
{
  reports: {
    glanceDashboard: {},           // At a Glance 数据
    attendeeDashboard: {},         // Attendee Analytics Dashboard
    registrationSummary: [],       // 注册摘要
    registrationBySpecialty: [],   // 按专业统计
    registrationByGeography: [],   // 按地理统计
    registrationByProgram: [],     // 按项目统计
    surveys: {},                   // 调查列表
    surveyResult: {},              // 调查结果
  }
}

Salesview ProgramActivity State (key: 'reports_program_activity')

javascript
{
  reports_program_activity: {
    ytdSpendByBrandData: [],           // 年度品牌花费
    programsByBudgetStatus: [],        // 预算状态项目分布
    geographySpendReport: [],          // 地理花费
    geographySpendReportDetail: [],    // 地理花费-项目类型详情
    geographySpendProgramDetail: [],   // 地理花费-项目详情
    allSalesReportList: [],            // 全部销售代表报告
    salesReportList: [],               // 特定销售代表报告
    speakerUtilizationReport: [],      // Speaker 利用率
    speakerUtilizationDetail: [],      // Speaker 利用率详情
    fiscalPeriodToDateOperatedReport: [], // 年度项目
    activitySummaryData: {},           // 活动摘要
    activitySummaryDataByBrand: {},    // 按品牌活动摘要
    programsDashboard: {},             // 仪表板
  }
}

Plannerview AdhocReport State (key: 'adhocReport')

javascript
{
  adhocReport: {
    adhocLoading: false,
    adhocData: {},              // 报告列表 (paginated)
    adhocDetail: {},            // 当前报告详情
    adhocDetailUUID: '',        // 表单重新渲染 key
    adhocModalVisible: false,   // 编辑弹窗可见性
    systemFields: [],           // 可选字段列表
    attendeeTypes: [],          // 参会者类型(过滤)
    runData: {},                // 运行结果数据 (paginated)
  }
}

5.3 Frontend Issues

  1. CustomReports 的 data.js 包含硬编码 demo 数据 (salesview/src/pages/Reports/CustomReports/data.js:1-254)

    • getReportsData() 返回 2 条硬编码 demo 记录("Program Activity Demo"、"Attendee Demo")。
    • getProgramActivityFields() 返回 42 个硬编码字段定义。
    • getAttendeeFields() 返回 46 个硬编码字段定义。
    • getProgramActivityReportData()getAttendeeReportData() 返回各 5 条硬编码样本数据。
    • 这个"Custom Reports"页面实际上是一个未完成的原型/demo 功能,并没有连接到后端 API。ReportsList.js 直接从 data.js 读取数据,没有任何 API 调用。
  2. Salesview 的 CustomReports 与后端 ProductCustomReportController 不是同一个功能

    • 前端 CustomReports/ReportsList.js 使用 getReportsData() 硬编码数据,是一个拖拽式报告构建器 demo。
    • 后端 ProductCustomReportController 是 R/Python 脚本执行器。
    • 真正调用后端 custom-reports API 的前端代码不在 CustomReports 目录中,而是通过 Navigation 菜单中 enableCustomReport 配置开关控制的独立功能。
  3. 双重 Redux Store - 报告模块使用了 2 个独立的 Redux store key(reportsreports_program_activity),增加了状态管理复杂度。两个 saga 文件独立运行,共计 28 个 saga worker。

  4. Plannerview Ad-hoc Report 使用过时的 React 模式 (plannerview/legacy/src/components/AdhocReport/index.js)

    • 使用 this.refs.adhocReportForm(string refs,React 16.3+ 已废弃)。
    • 使用 href="javascript:void(0);" 模式。
    • 使用 class component 而非 hooks。
    • 直接调用 this.props.dispatch() 而非 mapDispatchToProps 中的 action creators。
  5. 前端大量重复的 download saga 逻辑 (salesview/src/pages/Reports/ProgramActivity/saga.js)

    • 18 个 saga worker 中,download 类的 saga 与 data-fetch 类的 saga 代码结构完全相同,仅 URL 和 action 不同。应抽取为通用 saga 工厂函数。
  6. Ad-hoc 报告的 saveName 校验重复执行 (plannerview/legacy/src/components/AdhocReport/sagas.js:105-121)

    • 前端 saga 中在 POST 保存之前先调用 GET /v1/adhoc/check 校验名称唯一性,但后端 saveAdhoc 方法内部也做了相同的校验。双重校验浪费了一次网络请求。

6. Problem Summary

6.1 Critical Issues (must fix in rewrite)

#问题严重程度影响范围相关文件
C1报告框架碎片化严重 - 存在 4 种完全不同的报告机制:(1) 标准报告 (ProductReportService)、(2) Ad-hoc 报告 (AdhocReportService)、(3) Custom R/Python 脚本报告 (ProductCustomReportService)、(4) Data Extract 直接 SQL 导出 (DataExtractService)。没有统一的报告框架或抽象层。严重全局所有 report 模块
C2Custom Reports 执行任意脚本 - ProcessBuilder 执行文件系统上的 R/Python 脚本,存在安全风险(命令注入、权限提升)。脚本内容不受版本控制,难以审计。严重ProductCustomReportService.java:314-324安全
C3Data Extract 绕过权限控制 - runDataExtractReport 使用 PGConnection 直接执行 SQL 文件,不经过任何用户权限过滤。SQL 文件来自文件系统,可能导出敏感数据。严重ProductCustomReportService.java:348-394安全
C4CustomReports 前端是未完成的 demo - salesview 的 Custom Reports 页面使用硬编码数据,不连接后端 API。这意味着用户看到的是无用的原型界面。严重salesview/src/pages/Reports/CustomReports/data.js前端
C5目标匹配依赖文件系统 Excel - targets.xlsx 文件用于判断 attendee 是否为 target,每次报告生成都重新读取解析,且文件内容无版本控制。严重ProductReportService.java:1246-1273数据

6.2 Design Defects (should improve)

#问题严重程度影响范围相关文件
D1权限过滤逻辑重复 8+ 次 - Product/Region/District 三级权限判断代码在 ProductReportService 中至少重复了 8 次。中等ProductReportService.java代码质量
D2Ad-hoc 字段系统硬编码 34 个字段 - AdhocReportFieldStorage 使用静态 Map 存储字段映射,无法通过配置扩展。中等AdhocReportFieldStorage.java:11-50可维护性
D3客户特定逻辑硬编码 - Noven 客户特定的参会者字段映射(如 "Will you be joining live or virtually?"、"URO/ONC")硬编码在 downloadAttendeeTrackers 方法中。中等ProductReportService.java:429-436多租户
D4报告名称硬编码的 if-else 分支 - Custom Report 下载中 "Speaker Program Cost Summary"、"Verona Aggregate Spend Report"、"Tracking Campaign Stats" 三个报告名使用 if-else 特殊处理。中等ProductCustomReportService.java:159-256可维护性
D5Map<String, Map<String, Object>> 类型嵌套过深 - 多个 Response 使用 3 层嵌套 Map 作为数据结构(如 AtAGlanceResponseProgramActivityDashboardResponse),类型不安全,前端解析困难。中等ProductReportService.java:164-230API 设计
D6双重 Redux Store - 报告模块使用 2 个独立的 store key + 2 个 saga 文件,共 28 个 saga worker,大量重复模板代码。中等salesview Reports/前端架构
D7AdhocReportDetail 双 @Id 注解 - 可能导致 MyBatis 操作异常。中等AdhocReportDetail.java:10-14数据模型
D8ExcelService 同步导出阻塞请求线程 - 大报告导出时整个过程(数据库查询 + Excel 生成 + HTTP 写入)全部在请求线程中同步完成,可能导致线程超时。中等ExcelService.java性能

6.3 Technical Debt (nice to have)

#问题影响相关文件
T1Plannerview Ad-hoc Report 使用废弃的 React API - string refs、javascript:void(0)、class components可维护性AdhocReport/index.js
T2Download saga 模板代码重复 - 18 个 saga worker 可抽取为通用工厂可维护性ProgramActivity/saga.js
T3前后端校验重复 - Ad-hoc 报告名称唯一性在前端 saga 和后端 service 中都做了校验性能sagas.js:108-121, AdhocReportService.java:128-141
T4ExcelColumn 注解的 optional/include/exclude 机制复杂 - 字段可见性由 3 种不同机制控制(optional 配置、includeByProperty、excludeByProperty),增加理解难度可维护性ExcelService.java:205-255
T5Response 类型爆炸 - report 模块有 30+ 个独立的 Response/Download Response 类,很多只是字段子集差异代码量report/response/
T6GlobalSessionStorage 线程安全隐患 - ExcelService 使用 GlobalSessionStorage.setIds() 来传递 productId/companyId 给注解处理逻辑,这是 ThreadLocal 模式,但在异步场景下可能出错稳定性ProductReportService.java:261,459
T7DataExtractService 15 个无过滤器的全量查询 - 每个方法直接查全表,适合小数据量但扩展性差性能DataExtractService.java
T8survey 报告功能混在 Reports 模块中 - Survey 统计逻辑属于 Survey 领域,不应耦合在 Reports 路由和 Redux store 中架构salesview Reports/Survey/

7. Rewrite Recommendations

7.1 统一报告框架设计

目标: 将 4 种碎片化的报告机制统一为一个可扩展的报告框架。

ReportEngine (新框架)
├── ReportDefinition         # 报告定义(模板元数据)
│   ├── StandardReport       # 预定义报告(替代 ProductReportService 的各方法)
│   ├── AdhocReport          # 用户自定义报告(替代 AdhocReportService)
│   └── ScriptReport         # 脚本报告(替代 ProductCustomReportService,但沙箱化)
├── ReportDataSource         # 数据源抽象
│   ├── SqlDataSource        # MyBatis/JPA 查询
│   └── FileDataSource       # 文件系统数据源
├── ReportExporter           # 导出器
│   ├── ExcelExporter        # XLSX 导出
│   ├── CsvExporter          # CSV 导出
│   └── PdfExporter          # PDF 导出(未来扩展)
├── ReportPermission         # 权限过滤(统一的 Product/Region/District 过滤)
└── ReportScheduler          # 异步报告生成 + 通知

7.2 关键改进建议

  1. 废弃 R/Python 脚本执行机制 - 将所有脚本报告的逻辑迁移到 Java 服务端或使用沙箱化的报告模板引擎(如 JasperReports)。

  2. 异步报告生成 - 大报告应采用异步模式:提交报告任务 -> 后台生成 -> 完成后通知用户下载。避免同步阻塞请求线程。

  3. 统一权限过滤 - 将 Product/Region/District 三级权限过滤抽取为 ReportPermissionFilter,在报告框架层统一应用。

  4. 目标匹配数据库化 - 将 targets.xlsx 文件导入数据库表,通过 SQL JOIN 实现目标匹配,而不是每次读取文件。

  5. 前端报告框架统一 - 将 2 个 Redux store 合并,使用通用的报告 hooks/HOC 减少重复代码。移除 CustomReports demo 代码或实现真正的功能。

  6. Ad-hoc 报告字段配置化 - 将 AdhocReportFieldStorage 的硬编码字段迁移到数据库配置,支持动态扩展。

  7. 移除客户硬编码 - 将 Noven 特定字段映射迁移到 pharmagin-config-repo 配置文件或数据库。

  8. Response 类型简化 - 使用泛型 Response 包装器 + 字段投影机制替代 30+ 个独立的 Response 类。考虑使用 GraphQL 或类似机制让前端指定需要的字段。