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)等法规对透明度报告的合规要求。
该领域包含三个核心子领域:
- Aggregate Spend 报告(聚合支出报告):追踪每个 attendee/speaker 在会议中的支出,生成符合监管要求的汇总报告,支持多种报告格式(标准格式、Porzio 格式、Verona 自定义格式),可配置每个 product 导出哪些字段。
- Transfer of Value(TOV)追踪:计算每个参会者的价值转移金额,核心逻辑是将会议的 Food & Beverage 预算按照消费人头数平均分摊到每个参会者。同时追踪 speaker 的 honoraria、travel 等直接费用。
- 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 类型 | 数据库列名 | 注解 | 说明 |
|---|---|---|---|---|
| id | Integer | id | @Id, @GeneratedValue(IDENTITY) | 主键,自增 |
| name | String | name | 文件名称 | |
| description | String | description | 文件描述 | |
| uploadedBy | Integer | uploaded_by | 0: planner, 1: sales rep | |
| sequence | Integer | sequence | 在清单中的排序序号 | |
| config | Object | config | @ColumnType(JdbcType.OTHER) | JSON 配置(含 productIds, programTypeIds, serviceOptionIds, checkedKeys) |
| status | Integer | status | 状态(0: inactive, 1: active) | |
| createdAt | Date | created_at | 创建时间 | |
| createdBy | String | created_by | 创建人 | |
| updatedAt | Date | updated_at | 更新时间 | |
| updatedBy | String | updated_by | 更新人 |
2.1.2 MeetingComplianceDocument(会议合规文件实例)
表名: t_meeting_compliance_document文件: pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingComplianceDocument.java
| 字段名 | Java 类型 | 数据库列名 | 注解 | 说明 |
|---|---|---|---|---|
| meetingRequestId | Integer | meeting_request_id | @Id | 联合主键之一,会议请求 ID |
| complianceDocumentId | Integer | compliance_document_id | @Id | 联合主键之二,合规文件模板 ID |
| files | Object | files | @ColumnType(JdbcType.OTHER) | JSON 数组,存储上传的文件列表(fileId, fileName, type, size, createdAt) |
| status | Integer | status | 0: unready for audit, 1: ready for audit | |
| readyForReviewTime | Date | ready_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 类型 | 数据库列名 | 注解 | 说明 |
|---|---|---|---|---|
| complianceId | Integer | compliance_id | @Id | 主键 |
| attendeeId | String | attendee_id | 参会者 ID | |
| question | String | question | 合规问题 | |
| answer | String | answer | 合规回答 | |
| createdAt | Date | created_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 类型 | 数据库列名 | 注解 | 说明 |
|---|---|---|---|---|
| id | Integer | id | @Id, @GeneratedValue(IDENTITY) | 主键,自增 |
| productId | Integer | product_id | 产品 ID | |
| fieldId | Integer | field_id | 关联 t_aggregate_spend_report_field 的字段 ID | |
| createdAt | Date | created_at | 创建时间 | |
| createdBy | String | created_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 类型 | 数据库列名 | 注解 | 说明 |
|---|---|---|---|---|
| id | Integer | id | @Id, @GeneratedValue(IDENTITY) | 主键,自增 |
| fieldName | String | field_name | 字段名(Java 属性名) | |
| fieldLabel | String | field_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 类型 | 数据库列名 | 注解 | 说明 |
|---|---|---|---|---|
| id | Integer | id | @Id, @GeneratedValue(IDENTITY) | 主键,自增 |
| fieldName | String | field_name | 目标字段名称(Excel 列名) | |
| source | String | source | 原始值 | |
| target | String | target | 映射后的目标值 | |
| createdAt | Date | created_at | 创建时间 | |
| createdBy | String | created_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 类型 | 数据库列名 | 注解 | 说明 |
|---|---|---|---|---|
| id | Integer | id | @Id, @GeneratedValue(IDENTITY) | 主键,自增 |
| meetingRequestId | Integer | meeting_request_id | 会议请求 ID | |
| usedTov | BigDecimal | used_tov | 用于 TOV 计算的自定义金额(覆盖 F&B 预算) | |
| allocationHeadcount | Integer | allocation_headcount | 自定义分配人数(覆盖实际消费人数) | |
| tovChangeReason | String | tov_change_reason | TOV 金额变更原因 | |
| headcountChangeReason | String | headcount_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 类型 | 数据库列名 | 说明 |
|---|---|---|---|
| aggregateSpendReportStatus | Integer | aggregate_spend_report_status | 报告状态:0=Unreported, 1=Reported (line 383) |
| enableAudit | Boolean | enable_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 类型 | 数据库列名 | 说明 |
|---|---|---|---|
| transferOfValue | BigDecimal | transfer_of_value | 参会者的 TOV 金额 (line 161-162) |
| tovChangeReason | String | tov_change_reason | TOV 变更原因 (line 164-165) |
| attendeeCategory | String | attendee_category | 参会者分类(Pharma, Speaker 等) (line 122-123) |
| signInStatus | Integer | sign_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: 代码风格不一致
AttendeeComplianceQA和AllocationTov使用手写 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-88 | Budget 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-219 | Porzio 报告格式下排除内部员工 |
| AggSpendReport 只含已关闭会议 | AttendeeMapper.xml:591 | meeting_status IN (3, 4) 即 CLOSED 和 CANCELLED_CLOSED |
| 只计算已签到参会者 | AttendeeMapper.xml:588 | sign_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)
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)
| Method | Path | Parameters | Request Body | Response | Description |
|---|---|---|---|---|---|
| GET | /v1/compliance | ComplianceQuery: 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/export | ComplianceExportQuery: programName, startDate, endDate, productId, meetingRequestId, aggregateSpendReportStatus | - | Excel 文件下载 | 导出 Aggregate Spend 报告 |
| GET | /v1/compliance/{attendeeId}/profile | attendeeId (PathVariable) | - | ComplianceProfileResponse | 参会者合规 profile(含历史 TOV 详情) |
| GET | /v1/compliance/configuration | - | - | List<AggregateSpendConfiguration> | 获取所有 product 的 Agg Spend 报告字段配置 |
| POST | /v1/compliance/configuration | - | AggregateSpendConfigurationRequest: productId, fieldIds | void | 保存 Agg Spend 报告字段配置 |
| GET | /v1/compliance/document-checklist | DocumentChecklistQuery: productId, serviceOptionId | - | List<DocumentChecklistResponse> | 获取合规文件清单模板 |
| POST | /v1/compliance/document-checklist | - | DocumentChecklistRequest: name, description, uploadedBy, sequence, config | void | 创建合规文件模板 |
| PUT | /v1/compliance/document-checklist/{id} | id (PathVariable) | DocumentChecklistRequest | void | 更新合规文件模板 |
| PUT | /v1/compliance/document-checklist/{id}/activate | id (PathVariable) | - | void | 激活文件模板 |
| PUT | /v1/compliance/document-checklist/{id}/deactivate | id (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, target | void | 添加字段值映射 |
| DELETE | /v1/compliance/field-mappings | fieldName, source (RequestParam) | - | void | 删除字段值映射 |
4.1.2 Program 模块中的合规 APIs (/api/v1/programs)
| Method | Path | Parameters | Request Body | Response | Description |
|---|---|---|---|---|---|
| GET | /v1/programs/{meetingRequestId}/compliance-document-checklist | meetingRequestId | - | List<MeetingDocumentChecklistResponse> | 获取会议的合规文件清单及审计状态 |
| GET | /v1/programs/{meetingRequestId}/compliance-document-checklist/{documentId} | meetingRequestId, documentId | - | MeetingDocumentChecklistResponse | 获取单个会议合规文件详情及文件列表 |
| PUT | /v1/programs/{meetingRequestId}/compliance-document-checklist/{documentId}/ready-for-audit | meetingRequestId, documentId | - | void | 标记文件为"Ready for Audit" |
| PUT | /v1/programs/{meetingRequestId}/compliance-document-checklist/{documentId}/unready-for-audit | meetingRequestId, documentId | - | void | 标记文件为"Unready for Audit" |
| POST | /v1/programs/{meetingRequestId}/compliance-document-checklist/{documentId}/upload | meetingRequestId, documentId, file (MultipartFile) | - | void | 上传合规文件 |
| DELETE | /v1/programs/{meetingRequestId}/compliance-document-checklist/{documentId}/files/{fileId} | meetingRequestId, documentId, fileId | - | void | 删除已上传的合规文件 |
| GET | /v1/programs/{meetingRequestId}/audit | meetingRequestId | - | Boolean | 获取审计启用状态 |
| PUT | /v1/programs/{meetingRequestId}/audit | meetingRequestId | UpdateAuditStatusRequest: enableAudit | void | 更新审计启用状态 |
| GET | /v1/programs/{meetingRequestId}/compliance-tov-list | meetingRequestId | - | List<MeetingToVData> | 获取会议的 TOV 数据(用于 Sales Rep 审计) |
| PUT | /v1/programs/aggregate-spend-report-status | - | AggregateSpendReportStatusRequest: meetingRequestIds, aggregateSpendReportStatus | void | 批量更新 Agg Spend 报告状态 |
| PUT | /v1/programs/{meetingRequestId}/aggregate-spend-report-status | meetingRequestId | AggregateSpendReportStatusRequest: aggregateSpendReportStatus | void | 更新单个会议报告状态 + 记录 changelog |
4.1.3 TOV APIs (/api/v1/attendees)
| Method | Path | Parameters | Request Body | Response | Description |
|---|---|---|---|---|---|
| GET | /v1/attendees/tov | AttendeeTovQuery: meetingRequestId, consumedFood, current, pageSize | - | AttendeeTovResponse | 获取 TOV 列表及计算结果 |
| GET | /v1/attendees/tov/download | meetingRequestId | - | Excel 文件下载 | 下载 TOV 明细 |
| PUT | /v1/attendees/tov/allocation | - | AllocateTovRequest: meetingRequestId, allocationHeadcount, usedTov, tovChangeReason, headcountChangeReason | AttendeeTovResponse | 调整 TOV 计算参数 |
| PUT | /v1/attendees/tov | - | UpdateAttendeeTovRequest: meetingRequestId, transferOfValue, allocationHeadcount, usedTov, tovChangeReason | AttendeeTovResponse | 批量更新所有消费 F&B 参会者的 TOV |
| PUT | /v1/attendees/{attendeeId}/tov | attendeeId | UpdateAttendeeTovRequest: transferOfValue, tovChangeReason | void | 更新单个参会者 TOV |
| POST | /v1/attendees/{attendeeId}/transfer-of-value | attendeeId | List<AddTransferOfValueRequest>: categoryId, amount, note | void | 为参会者添加 speaker 费用类 TOV |
4.1.4 Report 模块中的 Compliance API
| Method | Path | Parameters | Request Body | Response | Description |
|---|---|---|---|---|---|
| GET | /v1/products/{productId}/reports/compliance-audit-list | productId, ComplianceAuditQuery: fiscalYear, reviewStatus, documentationStatus, programName | - | List<ComplianceAuditResponse> | Compliance Audit 列表(Sales Rep 使用) |
4.2 API Design Issues
Issue 1: 合规 API 分散在多个 Controller 中
ComplianceController- 主合规 APIProgramController- 会议级合规文件和审计状态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 |
| AggregateSpendByAttendees | containers/Compliance/AggregateSpendByAttendees.js | 按参会者查看 Agg Spend 列表,支持导出、profile 查看、字段映射管理 |
| AggregateSpendByAttendeesFilter | containers/Compliance/AggregateSpendByAttendeesFilter.js | 过滤器组件(Product, State, Name, NPI, Date Range) |
| AggregateSpendByPrograms | containers/Compliance/AggregateSpendByPrograms.js | 按 Program 查看 Agg Spend 列表,支持排序、批量标记报告状态 |
| AggregateSpendByProgramsFilter | containers/Compliance/AggregateSpendByProgramsFilter.js | 过滤器组件(Product, Date Range, Report Status) |
| AggregateSpendFieldMappingModal | containers/Compliance/AggregateSpendFieldMappingModal.js | 字段值映射管理弹窗(CRUD) |
| DocumentChecklist | containers/Compliance/DocumentChecklist.js | Admin 页面的合规文件清单管理(位于 AdminPage) |
| DocumentChecklistForm | containers/Compliance/DocumentChecklistForm.js | 文件模板编辑弹窗(含 Product/ProgramType/ServiceOption 树形选择) |
| MeetingComplianceDocumentChecklist | containers/Compliance/MeetingComplianceDocumentChecklist.js | Meeting Detail 中的合规文件上传和审计状态管理 |
| AggregateSpendReportConfiguration (sagas) | components/AggregateSpendReportConfiguration/sagas.js | Admin 页面的 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):
complianceDocumentChecklisttab -> MeetingComplianceDocumentChecklist 组件
5.1.2 Salesview(Sales Rep)
| 组件 | 文件 | 功能 |
|---|---|---|
| ComplianceAudit (index) | pharmagin-salesview/src/pages/Reports/ComplianceAudit/index.js | 路由容器 |
| ComplianceAuditList (List) | pages/Reports/ComplianceAudit/List.js | Compliance Audit 列表,按 Fiscal Year/Review Status/Documentation Status 过滤 |
| ComplianceAuditDetail (Detail) | pages/Reports/ComplianceAudit/Detail.js | Audit 详情页面,含 5 个 Tab |
| TransferOfValue | pages/Reports/ComplianceAudit/TransferOfValue.js | TOV 数据只读展示表格,显示总计 |
| Documentation | pages/Reports/ComplianceAudit/Documentation.js | 文件清单只读展示(含 Ready for Audit 图标,但 Sales Rep 不能修改) |
| DocumentationFileList | pages/Reports/ComplianceAudit/DocumentationFileList.js | 文件列表只读查看(只能下载,不能上传/删除) |
| constant.js | pages/Reports/ComplianceAudit/constant.js | 枚举定义:ReviewStatus, DocumentStatus, ComplianceAuditTab |
5.1.3 Speakerview
- 无合规相关组件。Speaker 不参与合规审计流程。
5.2 Redux State Structure
Plannerview Compliance State
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)
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)
return list.map(item => item.transferOfValue).reduce((a, b) => a + b);- 如果
transferOfValue为 null,会导致 NaN
Issue 7: AggregateSpendFieldMappingModal 的分页逻辑有硬编码值 (AggregateSpendFieldMappingModal.js:175-181)
pagination={{
pageSize: 20,
current: 1,
total: 0, // total 始终为 0,分页不会正常工作
...
}}total: 0导致分页组件始终显示 0 条数据
6. Problem Summary
6.1 Critical Issues (must fix in rewrite)
| # | Issue | 位置 | 影响 |
|---|---|---|---|
| C1 | TOV 计算逻辑与预算计算逻辑不一致 | SQL (AttendeeMapper.xml:633) vs Java (BudgetHelper) | 预算总额计算在 SQL 和 Java 中有两套实现,可能产生不同结果 |
| C2 | Porzio 报告格式硬编码客户特定值 | PorzioAggSpendReport.java:19,25,28,42,74 | "Noven Therapeutics, LLC", "Medical Leverage", "XELSTRYM" 等值硬编码在代码中,多客户部署时必须修改代码 |
| C3 | 合规 API 无权限控制 | ComplianceController 全部端点 | 任何认证用户都能修改 Agg Spend 配置和 Document Checklist 模板 |
| C4 | listCompliance type 参数设计不合理 | ComplianceService.java:107-129 | type=1 实际调用 MeetingService,与 type=0 的返回结构完全不同;type 无效时返回 null |
| C5 | TOV 计算中的硬编码 category ID | AttendeeTovService.java:75-78 | FOOD_BEVERAGE_SUB_CATEGORY = 15923 在不同客户部署中可能不同 |
6.2 Design Defects (should improve)
| # | Issue | 位置 | 影响 |
|---|---|---|---|
| D1 | PLID enrichment 逻辑重复 | ComplianceService.java export() & getMeetingToVData() | 两处约 40 行相同代码 |
| D2 | export() 方法过长过复杂 | ComplianceService.java:131-253 | 120+ 行,3 个分支路径,难以测试 |
| D3 | JSON 字段类型不安全 | MeetingComplianceDocument.files, ComplianceDocument.config | 使用 Object 类型 + 强制转换,无编译期类型检查 |
| D4 | Aggregate Spend 配置保存策略 | ComplianceService.java:322-337 | DELETE ALL + INSERT ONE-BY-ONE,非原子且低效 |
| D5 | AttendeeComplianceQA 实体可能废弃 | 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,批量不记录 |
| D9 | AggSpendReport SQL 中 500+ 字符的嵌套 CASE 表达式 | AttendeeMapper.xml:633-636 | 预算计算逻辑在 SQL 中无法维护 |
| D10 | PLID 匹配结果使用魔术字符串 | ComplianceService.java:166, 170 | "UNIQUE_ID_NO_MATCH", "MULTIPLE_UNIQUE_ID_MATCHED" |
6.3 Technical Debt (nice to have)
| # | Issue | 位置 | 影响 |
|---|---|---|---|
| T1 | AllocationTov 和 AttendeeComplianceQA 未使用 Lombok | entity 文件 | 代码冗余 |
| T2 | 前端组件全部使用 Class Component | Compliance 目录下所有组件 | 无法使用 React Hooks |
| T3 | Salesview 合规组件不使用 Redux | ComplianceAudit/*.js | 与 Plannerview 模式不一致 |
| T4 | TransferOfValue 组件的 total 计算无 null 安全 | TransferOfValue.js:70-71 | 可能显示 NaN |
| T5 | AggregateSpendFieldMappingModal 分页不生效 | AggregateSpendFieldMappingModal.js:178 | total 固定为 0 |
| T6 | 前端文件上传直接从 sessionStorage 取 token | MeetingComplianceDocumentChecklist.js:271 | 不经过统一 auth 工具 |
| T7 | VeronaCustomAggSpendReport 类存在但未被使用 | VeronaCustomAggSpendReport.java | 可能是废弃的自定义格式 |
| T8 | ComplianceService 中使用 @Autowired 和构造器注入混合风格 | 与 AttendeeTovService 的 @Autowired 不同 | 不一致但功能正常 |
7. Rewrite Recommendations
7.1 领域模型重构
统一合规领域边界:将散布在
compliance,site,program,report模块中的合规代码整合为独立的compliancebounded context,包含:AggregateSpendService- 聚合支出报告TransferOfValueService- TOV 计算和管理ComplianceDocumentService- 文件审计管理ComplianceAuditService- 审计查询
消除 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, ...)
类型安全的报告配置:
- 将 AggSpendReport 的 @ExcelColumn 注解改为运行时可配置的字段定义
- 支持不同客户的报告格式配置化,消除 Porzio、Verona 等硬编码格式类
7.2 业务逻辑优化
统一预算计算:
- 提取
BudgetCalculator工具类,在 Java 和 SQL 中统一使用 - 或完全在应用层计算,避免 SQL 中嵌入业务逻辑
- 提取
提取 PLID 匹配为独立服务:
- 创建
PLIDMatchingService,消除 export() 和 getMeetingToVData() 中的重复代码 - 使用枚举而非魔术字符串表示匹配结果
- 创建
TOV 计算通知统一:
- 使用 domain event + 策略模式统一触发条件
- 将分散在 ProgramEventListener 中的 4 处调用合并为统一的规则引擎
7.3 API 重设计
拆分 listCompliance endpoint:
GET /api/v2/compliance/attendee-spend(原 type=0)GET /api/v2/compliance/program-spend(原 type=1)
统一 aggregate-spend-report-status 端点行为:
- 无论批量还是单个更新都应记录 changelog
添加 RBAC 权限控制:
- Admin/Planner: 管理文件模板、配置报告、标记审计状态
- Sales Rep: 只读查看审计数据、上传指定文件
- 使用 Spring Security @PreAuthorize 注解
7.4 前端重构
使用 React 函数组件 + Hooks:重写所有合规组件
统一状态管理:Plannerview 和 Salesview 使用相同的数据获取模式(如 React Query/SWR)
抽取复用组件:AggregateSpendByAttendees 和 AggregateSpendByPrograms 合并为可配置的单一组件
7.5 数据模型建议
清理废弃实体:
- 确认
AttendeeComplianceQA是否仍需保留,如果废弃则删除 - 确认
VeronaCustomAggSpendReport是否仍需保留
- 确认
表命名规范化:
aggregate_spend_field_mapping->t_aggregate_spend_field_mapping添加审计字段:
MeetingComplianceDocument缺少 created_by/updated_by 审计字段