Skip to content

Compliance & Regulatory Domain - Deep Dive Analysis

1. Domain Overview

1.1 领域职责描述

合规与监管领域(Compliance & Regulatory)是制药行业 Speaker Program 平台的核心业务模块之一,主要负责追踪和报告制药公司向医疗专业人员(HCPs)的价值转移(Transfer of Value, TOV),满足美国 Sunshine Act(Physician Payments Sunshine Act)等法规对透明度报告的合规要求。

该领域包含三个核心子领域:

  1. Aggregate Spend 报告(聚合支出报告):追踪每个 attendee/speaker 在会议中的支出,生成符合监管要求的汇总报告,支持多种报告格式(标准格式、Porzio 格式、Verona 自定义格式),可配置每个 product 导出哪些字段。
  2. Transfer of Value(TOV)追踪:计算每个参会者的价值转移金额,核心逻辑是将会议的 Food & Beverage 预算按照消费人头数平均分摊到每个参会者。同时追踪 speaker 的 honoraria、travel 等直接费用。
  3. Compliance Document Audit(合规文件审计):管理每种会议类型所需的合规文件清单,追踪文件的上传和审计就绪状态,供 Sales Rep 和 Planner 在会议后提交合规文档。

1.2 涉及的后端模块和包

模块/包位置职责
modules/v1/compliance/主合规模块Aggregate Spend 报告、合规文件管理、TOV 数据查询
modules/v1/site/ (AttendeeTov*)TOV 子模块TOV 计算、分配、批量更新
modules/v1/program/ (部分)程序模块Aggregate Spend 报告状态、审计状态、合规文件 checklist 端点
modules/v1/report/ (部分)报告模块Compliance Audit List 查询
modules/v1/bus/listener/事件监听TOV 计算通知触发
modules/v1/config/配置ComplianceConfiguration、enablePorzioAggSpendReport、enableTOVCalculationNotification

2. Data Model Analysis

2.1 Entity Overview Table

2.1.1 ComplianceDocument(合规文件模板)

表名: t_compliance_document文件: pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/ComplianceDocument.java

字段名Java 类型数据库列名注解说明
idIntegerid@Id, @GeneratedValue(IDENTITY)主键,自增
nameStringname文件名称
descriptionStringdescription文件描述
uploadedByIntegeruploaded_by0: planner, 1: sales rep
sequenceIntegersequence在清单中的排序序号
configObjectconfig@ColumnType(JdbcType.OTHER)JSON 配置(含 productIds, programTypeIds, serviceOptionIds, checkedKeys)
statusIntegerstatus状态(0: inactive, 1: active)
createdAtDatecreated_at创建时间
createdByStringcreated_by创建人
updatedAtDateupdated_at更新时间
updatedByStringupdated_by更新人

2.1.2 MeetingComplianceDocument(会议合规文件实例)

表名: t_meeting_compliance_document文件: pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingComplianceDocument.java

字段名Java 类型数据库列名注解说明
meetingRequestIdIntegermeeting_request_id@Id联合主键之一,会议请求 ID
complianceDocumentIdIntegercompliance_document_id@Id联合主键之二,合规文件模板 ID
filesObjectfiles@ColumnType(JdbcType.OTHER)JSON 数组,存储上传的文件列表(fileId, fileName, type, size, createdAt)
statusIntegerstatus0: unready for audit, 1: ready for audit
readyForReviewTimeDateready_for_review_time标记为"Ready for Audit"的时间

2.1.3 AttendeeComplianceQA(参会者合规问答)

表名: 未标注 @Table(可能是 t_attendee_compliance_qa文件: pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/AttendeeComplianceQA.java

字段名Java 类型数据库列名注解说明
complianceIdIntegercompliance_id@Id主键
attendeeIdStringattendee_id参会者 ID
questionStringquestion合规问题
answerStringanswer合规回答
createdAtDatecreated_at创建时间

注意: 该实体没有使用 Lombok,采用手写 getter/setter,代码风格与其他实体不一致。且在 ComplianceService 中未被引用,可能是遗留/未使用的实体。

2.1.4 AggregateSpendReport(聚合支出报告配置)

表名: t_aggregate_spend_report文件: pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/AggregateSpendReport.java

字段名Java 类型数据库列名注解说明
idIntegerid@Id, @GeneratedValue(IDENTITY)主键,自增
productIdIntegerproduct_id产品 ID
fieldIdIntegerfield_id关联 t_aggregate_spend_report_field 的字段 ID
createdAtDatecreated_at创建时间
createdByStringcreated_by创建人

说明: 这是一个关联表,记录每个 product 选择了哪些报告字段用于 Aggregate Spend 导出。

2.1.5 AggregateSpendReportField(聚合支出报告字段定义)

表名: t_aggregate_spend_report_field文件: pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/AggregateSpendReportField.java

字段名Java 类型数据库列名注解说明
idIntegerid@Id, @GeneratedValue(IDENTITY)主键,自增
fieldNameStringfield_name字段名(Java 属性名)
fieldLabelStringfield_label字段标签(Excel 列头)

2.1.6 AggregateSpendFieldMapping(聚合支出字段值映射)

表名: aggregate_spend_field_mapping文件: pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/AggregateSpendFieldMapping.java

字段名Java 类型数据库列名注解说明
idIntegerid@Id, @GeneratedValue(IDENTITY)主键,自增
fieldNameStringfield_name目标字段名称(Excel 列名)
sourceStringsource原始值
targetStringtarget映射后的目标值
createdAtDatecreated_at创建时间
createdByStringcreated_by创建人

说明: 用于在 Aggregate Spend 导出时替换字段值(如将内部名称映射为监管机构要求的标准名称)。

2.1.7 AllocationTov(TOV 分配配置)

表名: t_allocation_tov文件: pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/AllocationTov.java

字段名Java 类型数据库列名注解说明
idIntegerid@Id, @GeneratedValue(IDENTITY)主键,自增
meetingRequestIdIntegermeeting_request_id会议请求 ID
usedTovBigDecimalused_tov用于 TOV 计算的自定义金额(覆盖 F&B 预算)
allocationHeadcountIntegerallocation_headcount自定义分配人数(覆盖实际消费人数)
tovChangeReasonStringtov_change_reasonTOV 金额变更原因
headcountChangeReasonStringheadcount_change_reason人数变更原因

注意: 该实体没有使用 Lombok,采用手写 getter/setter,代码风格不一致。

2.1.8 MeetingRequest 中的合规相关字段

表名: t_meeting_request文件: pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingRequest.java

字段名Java 类型数据库列名说明
aggregateSpendReportStatusIntegeraggregate_spend_report_status报告状态:0=Unreported, 1=Reported (line 383)
enableAuditBooleanenable_audit是否启用合规审计 (line 389)

2.1.9 Attendee 中的 TOV 相关字段

表名: t_attendee文件: pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/Attendee.java

字段名Java 类型数据库列名说明
transferOfValueBigDecimaltransfer_of_value参会者的 TOV 金额 (line 161-162)
tovChangeReasonStringtov_change_reasonTOV 变更原因 (line 164-165)
attendeeCategoryStringattendee_category参会者分类(Pharma, Speaker 等) (line 122-123)
signInStatusIntegersign_in_status签到状态 (line 140-141)

2.2 Table Relationships (ER Diagram - ASCII)

t_product
    |
    |--- 1:N --- t_aggregate_spend_report --- N:1 --- t_aggregate_spend_report_field
    |                   (product_id, field_id)              (id, field_name, field_label)
    |
    |--- 1:N --- t_meeting_request
                     |
                     |--- aggregate_spend_report_status (0/1)
                     |--- enable_audit (boolean)
                     |
                     |--- 1:N --- t_meeting_compliance_document --- N:1 --- t_compliance_document
                     |              (meeting_request_id,                      (id, name, description,
                     |               compliance_document_id,                  config: {productIds,
                     |               files: JSON[], status)                   serviceOptionIds})
                     |
                     |--- 1:1 --- t_allocation_tov
                     |              (meeting_request_id, used_tov,
                     |               allocation_headcount)
                     |
                     |--- 1:N (via meeting_id) --- t_attendee
                                                     |--- transfer_of_value
                                                     |--- tov_change_reason
                                                     |--- attendee_category

aggregate_spend_field_mapping (全局配置,无外键)
    (id, field_name, source, target)

t_attendee_compliance_qa (可能未使用)
    (compliance_id, attendee_id, question, answer)

--- 视图 ---
v_attendee_consumed_food   (attendee_id -> consumed_food boolean)
v_meeting_detail           (meeting_request_id -> sales_rep, first_speaker_name)
v_meeting_attendee_count   (meeting_id -> count)

2.3 Data Model Issues

Issue 1: config 字段使用 JSON 存储复杂配置 (ComplianceDocument.java:45-46)

  • config 字段类型为 Object,存储 {productIds, programTypeIds, serviceOptionIds, checkedKeys} 的 JSON
  • 查询时使用 PostgreSQL JSON 运算符 @> 进行过滤(ComplianceMapper.xml:19-23)
  • 问题:无法建立外键约束,无法做 JOIN 查询优化,JSON 内容的数据完整性无法保证

Issue 2: files 字段使用 JSON 数组存储文件列表 (MeetingComplianceDocument.java:32-33)

  • files 字段为 Object 类型,存储文件信息的 JSON 数组
  • 代码中通过强制类型转换 (List<Map<String, Object>>) 使用(ComplianceService.java:405, 478)
  • 问题:类型不安全,运行时可能出现 ClassCastException

Issue 3: AttendeeComplianceQA 实体未被使用

  • 该实体定义了合规问答,但在 ComplianceService 和整个代码库中没有任何引用
  • 没有对应的 Mapper 接口被发现
  • 可能是遗留功能或计划中但未实现的功能

Issue 4: 表命名不一致

  • 多数表使用 t_ 前缀(如 t_compliance_document, t_aggregate_spend_report
  • aggregate_spend_field_mapping 表没有 t_ 前缀,不符合项目命名规范

Issue 5: 代码风格不一致

  • AttendeeComplianceQAAllocationTov 使用手写 getter/setter
  • 其他实体使用 Lombok @Data 注解
  • 这增加了代码维护的复杂性

3. Business Flow Analysis

3.1 Core Business Flows (ASCII Flow Diagrams)

3.1.1 Aggregate Spend Report 配置与生成

=== 配置流程(Admin) ===

Admin 登录 Plannerview
        |
        v
Admin Page -> "Configure Aggregate Spend Reports"
        |
        v
GET /api/v1/compliance/configuration
        |
        v
展示所有 Product 及其已选字段
        |
        v
Admin 选择字段 -> POST /api/v1/compliance/configuration
        |                 {productId, fieldIds}
        v
ComplianceService.saveAggregateSpendConfiguration()
        |
        v
DELETE 该 product 原有配置 -> INSERT 新的 field 关联
        (事务性操作 @Transactional)


=== 导出流程(Planner) ===

Planner 进入 Compliance -> Aggregate Spend By Attendees
        |
        v
点击 "Export All Attendees"
        |
        v
GET /api/v1/compliance/export?productId=X&startDate=Y&endDate=Z
        |
        v
ComplianceService.export()
        |
        +---> attendeeMapper.getAggSpendReport()
        |         |
        |         v
        |     复杂 SQL UNION ALL:
        |       Part 1: F&B TOV per attendee (Food and Beverage)
        |       Part 2: Speaker expenses from budget items (Honoraria, Travel, etc.)
        |
        +---> 批量加载 PLID 数据
        |         plidMapper.selectByExample(productId)
        |
        +---> 丰富报告数据:
        |         - 匹配 PLID (by id or name)
        |         - 填充 NPI, License, ReconciliationId
        |         - 处理无匹配("UNIQUE_ID_NO_MATCH")
        |         - 处理多匹配("MULTIPLE_UNIQUE_ID_MATCHED")
        |
        +---> 分支: enablePorzioAggSpendReport?
        |     |
        |     +-- YES --> 转为 PorzioAggSpendReport 格式
        |     |           过滤 "Noven Staff"
        |     |           应用 FieldMapping 值转换
        |     |           导出 Excel
        |     |
        |     +-- NO --> 检查 product 是否有自定义字段配置
        |                |
        |                +-- 有 --> 按配置的字段列表动态生成 Excel
        |                |
        |                +-- 无 --> 使用 AggSpendReport 默认列,应用 FieldMapping,导出
        v
返回 Excel 文件

3.1.2 Transfer of Value (TOV) 计算流程

=== TOV 计算触发条件 ===

ProgramEventListener 监听以下事件时触发 sendTOVCalculationNotification:
  1. Meeting 状态变为 CLOSED
  2. Meeting 状态变为 CANCELLED_CLOSED
  3. Attendee List 被关闭 (AttendeeListClosedEvent)
  4. Budget Version 变更 (BudgetVersionChangedEvent)

仅在满足以下条件时发送通知:
  - Budget Version = BILL (4)
  - Meeting Status = CLOSED 或 CANCELLED_CLOSED
  - Attendee List Status = CLOSE
  - ProductConfiguration.enableTOVCalculationNotification = true


=== TOV 计算/分配流程 ===

Planner 进入 Meeting Detail -> Transfer of Value Tab
        |
        v
GET /api/v1/attendees/tov?meetingRequestId=X
        |
        v
AttendeeTovService.listAttendeeTov()
        |
        +---> 验证: Budget Version = ACT, Attendee List = Closed
        |
        +---> 获取 Food & Beverage 预算 (budget_item WHERE category=3, sub_category=15923)
        |
        +---> 获取 AllocationTov(自定义覆盖值)
        |
        +---> 获取参会者列表(sign_in_status IN (1,3), budget_version=4)
        |
        +---> 计算 TOV:
        |       headcount = allocationHeadcount ?? actual consumed food headcount
        |       budget = usedTov ?? foodBeverageBudget
        |       attendeeTov = budget / headcount (HALF_UP, 4 位小数)
        v
返回 AttendeeTovResponse
  {foodBeverageBudget, usedCalculationBudget, headcount, allocationHeadcount, attendeeTov, list}


=== TOV 手动调整 ===

PUT /api/v1/attendees/tov  (批量)
  -> 更新所有 consumedFood=Yes 的 attendee 的 transfer_of_value

PUT /api/v1/attendees/{attendeeId}/tov  (单个)
  -> 更新单个 attendee 的 transfer_of_value

PUT /api/v1/attendees/tov/allocation  (调整计算参数)
  -> 保存/更新 AllocationTov (usedTov, allocationHeadcount)
  -> 支持记录 tovChangeReason, headcountChangeReason


=== Speaker 费用 TOV ===

POST /api/v1/attendees/{attendeeId}/transfer-of-value
  -> 为 attendee 创建/更新 BudgetItem
  -> 自动查找或创建 PLID
  -> 将费用记录到预算体系中

3.1.3 Compliance Document Audit 流程

=== 文件模板管理(Admin/Planner) ===

Admin Page -> "Compliance Document Checklist"
        |
        v
GET /api/v1/compliance/document-checklist?productId=X&serviceOptionId=Y
        |
        v
展示文件清单列表(支持按 Product/ServiceOption 过滤)
        |
        +---> "Add Document" -> POST /api/v1/compliance/document-checklist
        |       {name, description, uploadedBy, sequence, config}
        |
        +---> "Edit" -> PUT /api/v1/compliance/document-checklist/{id}
        |
        +---> "Activate" -> PUT /api/v1/compliance/document-checklist/{id}/activate
        |
        +---> "Deactivate" -> PUT /api/v1/compliance/document-checklist/{id}/deactivate
        |
        +---> "Delete" -> DELETE /api/v1/compliance/document-checklist/{id}


=== 会议文件审计(Planner/Sales Rep) ===

Meeting Detail -> "Compliance Document Checklist" Tab
        |
        v
GET /api/v1/programs/{meetingRequestId}/compliance-document-checklist
        |
        v
ComplianceService.listMeetingDocumentChecklist()
  -> 根据 meeting 的 serviceType 查找适用的文件模板
  -> 关联 t_meeting_compliance_document 获取已上传的文件和审计状态
        |
        v
展示文件清单,每个文件显示:
  - Document Name, Description, Updated On
  - Ready for Audit (Yes/No 图标)
        |
        +---> 点击文件名 -> 展开文件列表
        |       |
        |       +---> Upload -> POST .../upload (MultipartFile)
        |       +---> Download -> GET /api/v1/files/{fileId}/download
        |       +---> Delete -> DELETE .../files/{fileId}
        |
        +---> Mark as Ready for Audit
        |       PUT .../ready-for-audit
        |       -> status = 1, readyForReviewTime = now
        |
        +---> Mark as Unready for Audit
                PUT .../unready-for-audit
                -> status = 0, readyForReviewTime = null


=== Compliance Audit 查看(Sales Rep via Salesview) ===

Salesview -> Reports -> Compliance Audit
        |
        v
GET /api/v1/products/{productId}/reports/compliance-audit-list
  ?fiscalYear=X&reviewStatus=Y&documentationStatus=Z&programName=...
        |
        v
ProductReportMapper.getComplianceAuditList()
  -> 只显示 enable_audit = TRUE 的会议
  -> 统计: readyForAuditCount / documentCount
        |
        v
展示列表(含 Documents Ready for Review: "X of Y")
        |
        v
点击 Meeting -> 进入 Compliance Audit Detail
  Tabs: Program Info | Attendees | Budget | Transfer of Value | Documentation
        |
        +---> Transfer of Value: GET /api/v1/programs/{id}/compliance-tov-list
        |       显示每个参会者/speaker 的 spend type 和 amount
        |
        +---> Documentation: GET /api/v1/programs/{id}/compliance-document-checklist
                只读查看(Sales Rep 不能修改审计状态),可下载文件


=== 审计状态管理 ===

Planner 可以在 Meeting Detail 启用/关闭审计:
  GET  /api/v1/programs/{id}/audit   -> 获取当前 enableAudit 状态
  PUT  /api/v1/programs/{id}/audit   -> 更新 enableAudit 状态

3.2 Validation Rules

规则位置说明
TOV 计算前提条件AttendeeTovService.java:87-88Budget Version 必须为 ACT,Attendee List 必须关闭
F&B 预算必须存在AttendeeTovService.java:93-95如果会议没有 Food & Beverage 预算项,抛出异常禁止 TOV 计算
报告配置 productId 必填AggregateSpendConfigurationRequest.java:12@NotNull
报告配置 fieldIds 不能为空AggregateSpendConfigurationRequest.java:15@NotEmpty
TOV 更新金额必填UpdateAttendeeTovRequest.java:15@NotNull
TOV 分配会议 ID 必填AllocateTovRequest.java:11@NotNull
文件清单删除前检查存在性ComplianceService.java:381-386不存在则抛出 NOT_FOUND
字段映射唯一性AggregateSpendFieldMappingService.java:27-29数据库唯一键冲突时抛出友好错误信息
导出时过滤 "Noven Staff"ComplianceService.java:218-219Porzio 报告格式下排除内部员工
AggSpendReport 只含已关闭会议AttendeeMapper.xml:591meeting_status IN (3, 4) 即 CLOSED 和 CANCELLED_CLOSED
只计算已签到参会者AttendeeMapper.xml:588sign_in_status IN (1, 3)

3.3 Business Logic Issues

Issue 1: TOV 计算通知条件分散在多个事件处理器中 (ProgramEventListener.java:284, 288, 355, 479)

  • sendTOVCalculationNotification 在 4 个不同事件中被调用
  • 条件判断逻辑重复
  • 建议:使用统一的 domain event 聚合

Issue 2: Aggregate Spend 导出中的 PLID 匹配逻辑存在二义性 (ComplianceService.java:163-174)

  • 当无 PLID 时,通过姓名匹配 activePlidsByName
  • 匹配失败设置为字符串 "UNIQUE_ID_NO_MATCH""MULTIPLE_UNIQUE_ID_MATCHED"
  • 这些魔术字符串直接出现在导出的 Excel 中,缺乏枚举定义

Issue 3: 导出逻辑复杂且分支多 (ComplianceService.java:131-253)

  • export() 方法 120+ 行,包含:标准格式、Porzio 格式两大分支
  • Porzio 分支中硬编码了 "Noven Staff" 过滤(line 218)、"XELSTRYM" 产品名(PorzioAggSpendReport.java:74)、"Medical Leverage" 公司名(line 25)
  • 不同客户部署需要修改代码

Issue 4: AggSpendReport SQL 查询中的超长复杂表达式 (AttendeeMapper.xml:633-636)

  • 预算总额计算包含嵌套的 CASE WHEN 表达式(约 500 字符的单行 SQL)
  • 计算逻辑涉及 quantity * unit_cost + tax + gratuity 的多种组合
  • 与 BudgetHelper.calculateTotalCostByBudgetItem() 中的 Java 计算逻辑重复
  • 极难维护和调试

Issue 5: getMeetingToVData 和 export 方法中的 PLID enrichment 逻辑重复 (ComplianceService.java:524-566 vs 131-175)

  • 两个方法中有几乎完全相同的 PLID 加载和匹配代码块
  • 违反 DRY 原则

Issue 6: TOV 计算中的硬编码常量 (AttendeeTovService.java:75-78)

java
private static final Integer MEETING_EXPENSES_CATEGORY = 3;
private static final Integer FOOD_BEVERAGE_SUB_CATEGORY = 15923;
  • 这些 ID 来自数据库的 t_budget_category 表,硬编码导致不同客户部署可能出错

Issue 7: Aggregate Spend 配置保存使用 DELETE + INSERT 策略 (ComplianceService.java:322-337)

  • 每次保存配置时先删除该 product 的所有记录,再逐一插入
  • 这种方式虽然在 @Transactional 保护下不会丢数据,但在并发场景下可能有问题
  • 且每次插入使用循环而非批量插入

4. API Inventory

4.1 REST Endpoints Table

4.1.1 Compliance Module APIs (/api/v1/compliance)

MethodPathParametersRequest BodyResponseDescription
GET/v1/complianceComplianceQuery: productId, attendeeName, programName, state, npi, consumedFood, startDate, endDate, aggregateSpendReportStatus, type (0/1), current, pageSize, orderBy, direction-PageResult<ComplianceResponse>合规列表(type=0 按 attendees, type=1 按 programs)
GET/v1/compliance/exportComplianceExportQuery: programName, startDate, endDate, productId, meetingRequestId, aggregateSpendReportStatus-Excel 文件下载导出 Aggregate Spend 报告
GET/v1/compliance/{attendeeId}/profileattendeeId (PathVariable)-ComplianceProfileResponse参会者合规 profile(含历史 TOV 详情)
GET/v1/compliance/configuration--List<AggregateSpendConfiguration>获取所有 product 的 Agg Spend 报告字段配置
POST/v1/compliance/configuration-AggregateSpendConfigurationRequest: productId, fieldIdsvoid保存 Agg Spend 报告字段配置
GET/v1/compliance/document-checklistDocumentChecklistQuery: productId, serviceOptionId-List<DocumentChecklistResponse>获取合规文件清单模板
POST/v1/compliance/document-checklist-DocumentChecklistRequest: name, description, uploadedBy, sequence, configvoid创建合规文件模板
PUT/v1/compliance/document-checklist/{id}id (PathVariable)DocumentChecklistRequestvoid更新合规文件模板
PUT/v1/compliance/document-checklist/{id}/activateid (PathVariable)-void激活文件模板
PUT/v1/compliance/document-checklist/{id}/deactivateid (PathVariable)-void停用文件模板
DELETE/v1/compliance/document-checklist/{id}id (PathVariable)-void删除文件模板
GET/v1/compliance/field-mappings--List<AggregateSpendFieldMapping>获取所有字段值映射
POST/v1/compliance/field-mappings-AggregateSpendFieldMapping: fieldName, source, targetvoid添加字段值映射
DELETE/v1/compliance/field-mappingsfieldName, source (RequestParam)-void删除字段值映射

4.1.2 Program 模块中的合规 APIs (/api/v1/programs)

MethodPathParametersRequest BodyResponseDescription
GET/v1/programs/{meetingRequestId}/compliance-document-checklistmeetingRequestId-List<MeetingDocumentChecklistResponse>获取会议的合规文件清单及审计状态
GET/v1/programs/{meetingRequestId}/compliance-document-checklist/{documentId}meetingRequestId, documentId-MeetingDocumentChecklistResponse获取单个会议合规文件详情及文件列表
PUT/v1/programs/{meetingRequestId}/compliance-document-checklist/{documentId}/ready-for-auditmeetingRequestId, documentId-void标记文件为"Ready for Audit"
PUT/v1/programs/{meetingRequestId}/compliance-document-checklist/{documentId}/unready-for-auditmeetingRequestId, documentId-void标记文件为"Unready for Audit"
POST/v1/programs/{meetingRequestId}/compliance-document-checklist/{documentId}/uploadmeetingRequestId, documentId, file (MultipartFile)-void上传合规文件
DELETE/v1/programs/{meetingRequestId}/compliance-document-checklist/{documentId}/files/{fileId}meetingRequestId, documentId, fileId-void删除已上传的合规文件
GET/v1/programs/{meetingRequestId}/auditmeetingRequestId-Boolean获取审计启用状态
PUT/v1/programs/{meetingRequestId}/auditmeetingRequestIdUpdateAuditStatusRequest: enableAuditvoid更新审计启用状态
GET/v1/programs/{meetingRequestId}/compliance-tov-listmeetingRequestId-List<MeetingToVData>获取会议的 TOV 数据(用于 Sales Rep 审计)
PUT/v1/programs/aggregate-spend-report-status-AggregateSpendReportStatusRequest: meetingRequestIds, aggregateSpendReportStatusvoid批量更新 Agg Spend 报告状态
PUT/v1/programs/{meetingRequestId}/aggregate-spend-report-statusmeetingRequestIdAggregateSpendReportStatusRequest: aggregateSpendReportStatusvoid更新单个会议报告状态 + 记录 changelog

4.1.3 TOV APIs (/api/v1/attendees)

MethodPathParametersRequest BodyResponseDescription
GET/v1/attendees/tovAttendeeTovQuery: meetingRequestId, consumedFood, current, pageSize-AttendeeTovResponse获取 TOV 列表及计算结果
GET/v1/attendees/tov/downloadmeetingRequestId-Excel 文件下载下载 TOV 明细
PUT/v1/attendees/tov/allocation-AllocateTovRequest: meetingRequestId, allocationHeadcount, usedTov, tovChangeReason, headcountChangeReasonAttendeeTovResponse调整 TOV 计算参数
PUT/v1/attendees/tov-UpdateAttendeeTovRequest: meetingRequestId, transferOfValue, allocationHeadcount, usedTov, tovChangeReasonAttendeeTovResponse批量更新所有消费 F&B 参会者的 TOV
PUT/v1/attendees/{attendeeId}/tovattendeeIdUpdateAttendeeTovRequest: transferOfValue, tovChangeReasonvoid更新单个参会者 TOV
POST/v1/attendees/{attendeeId}/transfer-of-valueattendeeIdList<AddTransferOfValueRequest>: categoryId, amount, notevoid为参会者添加 speaker 费用类 TOV

4.1.4 Report 模块中的 Compliance API

MethodPathParametersRequest BodyResponseDescription
GET/v1/products/{productId}/reports/compliance-audit-listproductId, ComplianceAuditQuery: fiscalYear, reviewStatus, documentationStatus, programName-List<ComplianceAuditResponse>Compliance Audit 列表(Sales Rep 使用)

4.2 API Design Issues

Issue 1: 合规 API 分散在多个 Controller 中

  • ComplianceController - 主合规 API
  • ProgramController - 会议级合规文件和审计状态
  • AttendeeTovController - TOV 管理
  • ProductReportController - Compliance Audit List
  • 建议:在重写中整合为统一的 Compliance 领域 API

Issue 2: listCompliance 使用 type 参数切换完全不同的查询 (ComplianceController.java:44-47, ComplianceService.java:107-129)

  • type=0 查询按 attendees 的合规数据
  • type=1 查询按 programs(实际调用 MeetingService.listMeetings)
  • 两个完全不同的业务逻辑通过一个 type 参数路由,违反单一职责原则
  • 返回 null 当 type 既不是 0 也不是 1(line 128)

Issue 3: export 端点不返回值 (ComplianceController.java:50-53)

  • export 方法返回 void,通过 ExcelService 直接写入 HttpServletResponse
  • 缺少明确的 @Produces 注解
  • 异步导出可能导致超时

Issue 4: 批量和单个更新 aggregate-spend-report-status 的端点不一致 (ProgramController.java:182-190)

  • 批量端点: PUT /v1/programs/aggregate-spend-report-status
  • 单个端点: PUT /v1/programs/{meetingRequestId}/aggregate-spend-report-status
  • 单个端点额外记录 changelog,批量端点不记录
  • 功能不对等

Issue 5: DELETE field-mappings 使用 RequestParam 而非 PathVariable (ComplianceController.java:119-123)

  • DELETE /v1/compliance/field-mappings?fieldName=X&source=Y
  • 更 RESTful 的设计应使用 DELETE /v1/compliance/field-mappings/{id}DELETE /v1/compliance/field-mappings/{fieldName}/{source}

Issue 6: 缺少权限控制注解

  • ComplianceController 没有任何 @PreAuthorize 或角色检查
  • Sales Rep 不应该能修改 Document Checklist 模板或 Aggregate Spend 配置
  • 所有端点都对所有认证用户开放

5. Frontend Analysis

5.1 Pages & Components

5.1.1 Plannerview(Agency Planner)

组件文件功能
Compliance (index)pharmagin-plannerview/legacy/src/containers/Compliance/index.js路由组件,根据 type 参数分发到 ByAttendees 或 ByPrograms
AggregateSpendByAttendeescontainers/Compliance/AggregateSpendByAttendees.js按参会者查看 Agg Spend 列表,支持导出、profile 查看、字段映射管理
AggregateSpendByAttendeesFiltercontainers/Compliance/AggregateSpendByAttendeesFilter.js过滤器组件(Product, State, Name, NPI, Date Range)
AggregateSpendByProgramscontainers/Compliance/AggregateSpendByPrograms.js按 Program 查看 Agg Spend 列表,支持排序、批量标记报告状态
AggregateSpendByProgramsFiltercontainers/Compliance/AggregateSpendByProgramsFilter.js过滤器组件(Product, Date Range, Report Status)
AggregateSpendFieldMappingModalcontainers/Compliance/AggregateSpendFieldMappingModal.js字段值映射管理弹窗(CRUD)
DocumentChecklistcontainers/Compliance/DocumentChecklist.jsAdmin 页面的合规文件清单管理(位于 AdminPage)
DocumentChecklistFormcontainers/Compliance/DocumentChecklistForm.js文件模板编辑弹窗(含 Product/ProgramType/ServiceOption 树形选择)
MeetingComplianceDocumentChecklistcontainers/Compliance/MeetingComplianceDocumentChecklist.jsMeeting Detail 中的合规文件上传和审计状态管理
AggregateSpendReportConfiguration (sagas)components/AggregateSpendReportConfiguration/sagas.jsAdmin 页面的 Agg Spend 报告字段配置

路由配置 (routes.js:799):

  • /compliance/0 -> AggregateSpendByAttendees
  • /compliance/1 -> AggregateSpendByPrograms

Admin 页面入口 (AdminPage/constants.js:62-76):

  • complianceDocumentChecklist -> DocumentChecklist 组件
  • configureAggregateSpendReports -> AggregateSpendReportConfiguration 组件

Meeting Detail Tab (MeetingDetail/constants.js:128-131):

  • complianceDocumentChecklist tab -> MeetingComplianceDocumentChecklist 组件

5.1.2 Salesview(Sales Rep)

组件文件功能
ComplianceAudit (index)pharmagin-salesview/src/pages/Reports/ComplianceAudit/index.js路由容器
ComplianceAuditList (List)pages/Reports/ComplianceAudit/List.jsCompliance Audit 列表,按 Fiscal Year/Review Status/Documentation Status 过滤
ComplianceAuditDetail (Detail)pages/Reports/ComplianceAudit/Detail.jsAudit 详情页面,含 5 个 Tab
TransferOfValuepages/Reports/ComplianceAudit/TransferOfValue.jsTOV 数据只读展示表格,显示总计
Documentationpages/Reports/ComplianceAudit/Documentation.js文件清单只读展示(含 Ready for Audit 图标,但 Sales Rep 不能修改)
DocumentationFileListpages/Reports/ComplianceAudit/DocumentationFileList.js文件列表只读查看(只能下载,不能上传/删除)
constant.jspages/Reports/ComplianceAudit/constant.js枚举定义:ReviewStatus, DocumentStatus, ComplianceAuditTab

5.1.3 Speakerview

  • 无合规相关组件。Speaker 不参与合规审计流程。

5.2 Redux State Structure

Plannerview Compliance State

javascript
state.compliance = {
  // via redux-saga-routines
  listAggregateSpend: {
    loading: boolean,
    data: PageResult<ComplianceResponse>,
    error: string
  },
  exportAggregateSpend: {
    loading: boolean,
    error: string
  },
  getAggregateSpendProfile: {
    loading: boolean,
    data: ComplianceProfileResponse,
    error: string
  },
  changeAggregateSpendReportStatus: {
    loading: boolean,
    error: string
  }
}

Salesview

Salesview 的 ComplianceAudit 组件不使用 Redux,而是在组件内部通过 Network.get() 直接发起 API 请求,将数据存储在 this.state 中。

5.3 Frontend Issues

Issue 1: Plannerview 使用 Redux + Saga,Salesview 使用组件内部状态

  • 两个前端应用对同一领域采用完全不同的状态管理方式
  • Salesview 的 ComplianceAudit 页面在每个 Tab 切换时重新请求数据,没有缓存

Issue 2: AggregateSpendByAttendees 和 AggregateSpendByPrograms 组件重复度高

  • 两个组件有几乎相同的 lifecycle 方法(componentDidMount, requestAggregateSpend, handleTableChange, handleFilterChange)
  • 建议抽取基础类或高阶组件

Issue 3: MeetingComplianceDocumentChecklist 中的文件上传使用 sessionStorage 获取 token (line 271)

javascript
authorization: `Bearer ${sessionStorage.getItem('accessToken')}`
  • 直接操作 sessionStorage 而非通过统一的 auth 工具
  • 如果 token 过期,用户体验差

Issue 4: 类组件而非函数组件

  • 所有合规相关组件都使用 React Class Components
  • 没有使用 Hooks、函数组件等现代 React 模式

Issue 5: Salesview ComplianceAuditDetail 中 Tab 组件延迟加载但无 loading 指示器 (Detail.js:61-84)

  • 每个 Tab 仅在 activeTabKey 匹配时渲染
  • 切换 Tab 时数据请求可能有延迟但无 loading 反馈

Issue 6: TransferOfValue 组件使用 reduce 计算 total 时无 null 安全 (TransferOfValue.js:70-71)

javascript
return list.map(item => item.transferOfValue).reduce((a, b) => a + b);
  • 如果 transferOfValue 为 null,会导致 NaN

Issue 7: AggregateSpendFieldMappingModal 的分页逻辑有硬编码值 (AggregateSpendFieldMappingModal.js:175-181)

javascript
pagination={{
  pageSize: 20,
  current: 1,
  total: 0,  // total 始终为 0,分页不会正常工作
  ...
}}
  • total: 0 导致分页组件始终显示 0 条数据

6. Problem Summary

6.1 Critical Issues (must fix in rewrite)

#Issue位置影响
C1TOV 计算逻辑与预算计算逻辑不一致SQL (AttendeeMapper.xml:633) vs Java (BudgetHelper)预算总额计算在 SQL 和 Java 中有两套实现,可能产生不同结果
C2Porzio 报告格式硬编码客户特定值PorzioAggSpendReport.java:19,25,28,42,74"Noven Therapeutics, LLC", "Medical Leverage", "XELSTRYM" 等值硬编码在代码中,多客户部署时必须修改代码
C3合规 API 无权限控制ComplianceController 全部端点任何认证用户都能修改 Agg Spend 配置和 Document Checklist 模板
C4listCompliance type 参数设计不合理ComplianceService.java:107-129type=1 实际调用 MeetingService,与 type=0 的返回结构完全不同;type 无效时返回 null
C5TOV 计算中的硬编码 category IDAttendeeTovService.java:75-78FOOD_BEVERAGE_SUB_CATEGORY = 15923 在不同客户部署中可能不同

6.2 Design Defects (should improve)

#Issue位置影响
D1PLID enrichment 逻辑重复ComplianceService.java export() & getMeetingToVData()两处约 40 行相同代码
D2export() 方法过长过复杂ComplianceService.java:131-253120+ 行,3 个分支路径,难以测试
D3JSON 字段类型不安全MeetingComplianceDocument.files, ComplianceDocument.config使用 Object 类型 + 强制转换,无编译期类型检查
D4Aggregate Spend 配置保存策略ComplianceService.java:322-337DELETE ALL + INSERT ONE-BY-ONE,非原子且低效
D5AttendeeComplianceQA 实体可能废弃AttendeeComplianceQA.java无任何代码引用,无对应 Mapper
D6表命名不一致aggregate_spend_field_mapping vs t_* 命名规范不符合项目约定
D7合规功能跨 3 个 Controller 分散ComplianceController, ProgramController, AttendeeTovController, ProductReportController领域边界不清晰
D8批量/单个 report status 更新行为不一致ProgramController.java:182-190单个更新记录 changelog,批量不记录
D9AggSpendReport SQL 中 500+ 字符的嵌套 CASE 表达式AttendeeMapper.xml:633-636预算计算逻辑在 SQL 中无法维护
D10PLID 匹配结果使用魔术字符串ComplianceService.java:166, 170"UNIQUE_ID_NO_MATCH", "MULTIPLE_UNIQUE_ID_MATCHED"

6.3 Technical Debt (nice to have)

#Issue位置影响
T1AllocationTov 和 AttendeeComplianceQA 未使用 Lombokentity 文件代码冗余
T2前端组件全部使用 Class ComponentCompliance 目录下所有组件无法使用 React Hooks
T3Salesview 合规组件不使用 ReduxComplianceAudit/*.js与 Plannerview 模式不一致
T4TransferOfValue 组件的 total 计算无 null 安全TransferOfValue.js:70-71可能显示 NaN
T5AggregateSpendFieldMappingModal 分页不生效AggregateSpendFieldMappingModal.js:178total 固定为 0
T6前端文件上传直接从 sessionStorage 取 tokenMeetingComplianceDocumentChecklist.js:271不经过统一 auth 工具
T7VeronaCustomAggSpendReport 类存在但未被使用VeronaCustomAggSpendReport.java可能是废弃的自定义格式
T8ComplianceService 中使用 @Autowired 和构造器注入混合风格与 AttendeeTovService 的 @Autowired 不同不一致但功能正常

7. Rewrite Recommendations

7.1 领域模型重构

  1. 统一合规领域边界:将散布在 compliance, site, program, report 模块中的合规代码整合为独立的 compliance bounded context,包含:

    • AggregateSpendService - 聚合支出报告
    • TransferOfValueService - TOV 计算和管理
    • ComplianceDocumentService - 文件审计管理
    • ComplianceAuditService - 审计查询
  2. 消除 JSON 字段滥用

    • ComplianceDocument.config -> 新建 t_compliance_document_scope 关联表 (document_id, product_id, program_type_id, service_option_id)
    • MeetingComplianceDocument.files -> 新建 t_meeting_compliance_document_file 关联表 (meeting_request_id, compliance_document_id, file_id, ...)
  3. 类型安全的报告配置

    • 将 AggSpendReport 的 @ExcelColumn 注解改为运行时可配置的字段定义
    • 支持不同客户的报告格式配置化,消除 Porzio、Verona 等硬编码格式类

7.2 业务逻辑优化

  1. 统一预算计算

    • 提取 BudgetCalculator 工具类,在 Java 和 SQL 中统一使用
    • 或完全在应用层计算,避免 SQL 中嵌入业务逻辑
  2. 提取 PLID 匹配为独立服务

    • 创建 PLIDMatchingService,消除 export() 和 getMeetingToVData() 中的重复代码
    • 使用枚举而非魔术字符串表示匹配结果
  3. TOV 计算通知统一

    • 使用 domain event + 策略模式统一触发条件
    • 将分散在 ProgramEventListener 中的 4 处调用合并为统一的规则引擎

7.3 API 重设计

  1. 拆分 listCompliance endpoint

    • GET /api/v2/compliance/attendee-spend (原 type=0)
    • GET /api/v2/compliance/program-spend (原 type=1)
  2. 统一 aggregate-spend-report-status 端点行为

    • 无论批量还是单个更新都应记录 changelog
  3. 添加 RBAC 权限控制

    • Admin/Planner: 管理文件模板、配置报告、标记审计状态
    • Sales Rep: 只读查看审计数据、上传指定文件
    • 使用 Spring Security @PreAuthorize 注解

7.4 前端重构

  1. 使用 React 函数组件 + Hooks:重写所有合规组件

  2. 统一状态管理:Plannerview 和 Salesview 使用相同的数据获取模式(如 React Query/SWR)

  3. 抽取复用组件:AggregateSpendByAttendees 和 AggregateSpendByPrograms 合并为可配置的单一组件

7.5 数据模型建议

  1. 清理废弃实体

    • 确认 AttendeeComplianceQA 是否仍需保留,如果废弃则删除
    • 确认 VeronaCustomAggSpendReport 是否仍需保留
  2. 表命名规范化aggregate_spend_field_mapping -> t_aggregate_spend_field_mapping

  3. 添加审计字段MeetingComplianceDocument 缺少 created_by/updated_by 审计字段