Program & Meeting Domain - Deep Dive Analysis
1. Domain Overview
1.1 领域职责描述
Program & Meeting 领域是 Pharmagin Speaker Platform 的核心业务领域,负责管理制药公司演讲者项目(Speaker Programs)的完整生命周期。该领域覆盖以下核心职责:
- Program 生命周期管理 - 从创建、审批、执行到关闭的完整流程
- Meeting/Registration Site 管理 - 为每个 Program 生成注册站点(Registration Site),管理参会者注册
- 审批工作流 - 多级审批(DM/RM/UM 或 RBD/Marketing/Legal),支持部分审批和预算验证
- 日历视图 - 以日历形式展示项目时间安排
- Project Task 管理 - 为每个 Program 生成和追踪任务清单
- 附件和变更日志 - Program 附件管理和字段变更历史追踪
- Speaker Randomization - 根据条件随机匹配和分配演讲者
- 合规文档管理 - 合规文档清单和审计状态管理
1.2 涉及的后端模块和包
| 模块路径 | 职责 |
|---|---|
modules/v1/meeting/ | 原始 Meeting 模块(部分已 Deprecated),负责列表查询、删除、复制、分配、附件、工作流等 |
modules/v1/program/ | 新的 Program 模块,负责创建、更新、审批、关闭/重开、日历、数据异常等 |
modules/v1/calendar/ | 日历视图,查询指定时间范围内的 Program |
modules/v1/project/ | 项目任务管理,任务模板、Program 任务生成和状态追踪 |
common/persistence/entity/Meeting*.java | 13 个 Meeting 相关实体类 |
common/persistence/entity/Program*.java | ProgramServiceType 实体 |
common/persistence/entity/Project*.java | ProjectTask, ProjectTaskTemplate 实体 |
关键设计特征: 系统中存在 Meeting 和 MeetingRequest 两个核心概念的混淆:
MeetingRequest(t_meeting_request) = 业务层面的 "Program"(由 Sales Rep 提交的项目请求)Meeting(t_meeting) = 技术层面的 "Registration Site"(注册站点配置)- 一个 MeetingRequest 关联一个 Meeting,Meeting 用于生成注册页面
2. Data Model Analysis
2.1 Entity Overview Table
2.1.1 MeetingRequest (t_meeting_request) - 核心 Program 实体
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingRequest.java
| 字段名 | 类型 | 注解 | 说明 |
|---|---|---|---|
| meetingRequestId | Integer | @Id, @GeneratedValue(seq: s_meeting_request) | 主键 |
| requestDate | Date | 请求日期 | |
| venue | String | 场地名称 | |
| city | String | 城市 | |
| state | String | 州 | |
| portalUserId | Integer | 门户用户ID(Sales Rep) | |
| speakerHonoraria | BigDecimal | 演讲者酬金 | |
| meetingCost | BigDecimal | 会议成本 | |
| totalCost | BigDecimal | 总成本 | |
| programType | Integer | 项目类型ID(FK->t_meeting_program_type) | |
| serviceType | Integer | 服务类型ID(FK->t_meeting_program_service_type) | |
| meetingStartTime | Date | 会议开始时间 | |
| meetingEndTime | Date | 会议结束时间 | |
| productId | Integer | 产品ID | |
| meetingRequestInfo | String | 自定义表单信息(使用@-@和@=@分隔的KV串) | |
| firstSpeaker | Integer | 第一演讲者ID(FK->speaker) | |
| secondSpeaker | Integer | 第二演讲者ID | |
| thirdSpeaker | Integer | 第三演讲者ID | |
| topicId | Integer | 主题ID | |
| ppr | String | PPR 信息 | |
| meetingId | Integer | 关联的注册站点 Meeting ID(FK->t_meeting) | |
| meetingIdPrefix | String | 会议ID前缀 | |
| attendeeInfo | String | 参会者信息 | |
| speakers | String | 演讲者信息(字符串) | |
| patientAdvocate | String | 患者倡导者 | |
| paHonoraria | BigDecimal | 患者倡导者酬金 | |
| topicName | String | 主题名称(冗余) | |
| budgetStatus | String | 预算状态(SOW/EST/ACT) | |
| meetingStatus | Integer | 会议状态(0-14枚举) | |
| plannerId | Integer | 策划人ID | |
| pprDate | Date | PPR日期 | |
| attendeeNumber | Integer | 参会者人数 | |
| budgetVersionId | Integer | 预算版本ID | |
| meetingRequestName | String | 会议请求名称(自动生成) | |
| status | Integer | 删除标志(0=正常,1=删除) | |
| invitationsNumber | Integer | 邀请人数 | |
| actualAttendeeNumber | Integer | 实际参会人数 | |
| requestorFirstName | String | 请求者名 | |
| requestorLastName | String | 请求者姓 | |
| requestorJobTitle | String | 请求者职位 | |
| requestorPhoneNumber | String | 请求者电话 | |
| requestorEmail | String | 请求者邮箱 | |
| chargeNumber | String | 费用编号 | |
| estimatedBudget | BigDecimal | 预算估算 | |
| firstNameOfBudgetApprover | String | 预算审批人名 | |
| lastNameOfBudgetApprover | String | 预算审批人姓 | |
| emailOfBudgetApprover | String | 预算审批人邮箱 | |
| meetingName | String | 会议名称 | |
| meetingPurpose | Integer | 会议目的 | |
| purposeDescription | String | 目的描述 | |
| division | Integer | 部门 | |
| businessGroup | Integer | 业务组 | |
| attendeeType | Integer | 参会者类型 | |
| goalsAndObjectives | String | 目标与目的 | |
| dateOnCalendar | Boolean | 日历上的日期 | |
| postalCode | String | 邮政编码 | |
| countryId | Integer | 国家ID | |
| lowestPriceVenueChosen | Boolean | 是否选择最低价场地 | |
| reasonForVenueChosen | String | 选择场地理由 | |
| venueNotes | String | 场地备注 | |
| savingReason | String | 节省理由 | |
| otherSavingReason | String | 其他节省理由 | |
| savingNotes | String | 节省备注 | |
| otherCanceledReason | String | 其他取消理由 | |
| totalLaborHours | BigDecimal | 总工时 | |
| sourcedBy | Integer | 来源 | |
| contractDueDate | Date | 合同截止日期 | |
| depositDueDate | Date | 押金截止日期 | |
| roomingListDueDate | Date | 住宿清单截止日期 | |
| canceledReason | String | 取消原因 | |
| cancellationDeadline1-3 | Date | 3个取消截止日期 | |
| cancellationAmount1-3 | BigDecimal | 3个取消金额 | |
| singleDayEvent | Boolean | 是否单日活动 | |
| duration | Integer | 持续时间 | |
| commissionableRate | Integer | 佣金率 | |
| commissionRate | BigDecimal | 佣金比率 | |
| estimatedCommissionAmount | BigDecimal | 预计佣金金额 | |
| commissionCollectedDate | Date | 佣金收取日期 | |
| commissionCollectedAmount | BigDecimal | 佣金收取金额 | |
| meetingIdentifier | Integer | 会议标识 | |
| alignment | Integer | 对齐方式 | |
| hotelName | String | 酒店名称 | |
| address1 | String | 地址1 | |
| address2 | String | 地址2 | |
| approvalStatus | Integer | 审批状态(1:待审批,2:已审批,3:已拒绝) | |
| oldMeetingStatus | Integer | 旧会议状态(审批前保存的原始状态) | |
| regionId | Integer | 区域ID | |
| territoryId | Integer | 领地ID | |
| districtId | Integer | 地区ID | |
| survey | String | 调查问卷 | |
| lat | Double | 纬度 | |
| lng | Double | 经度 | |
| radius | Integer | 半径 | |
| gpsStatus | Integer | GPS状态 | |
| updateTime | Date | 更新时间 | |
| integrationMeetingId | String | 集成会议ID(Salesforce/Veeva) | |
| externalId | Long | 外部ID | |
| timezoneId | Integer | 时区ID | |
| attachmentStatus | Integer | 附件状态(0:正常,1:关闭) | |
| sentReminder | Integer | 是否已发送提醒(0/1) | |
| teamId | Integer | 团队ID | |
| salesForceId | Integer | 销售区域ID | |
| showTrainedSpeakers | Boolean | 是否显示已培训演讲者 | |
| userId | Integer | 创建用户ID(Sales Rep) | |
| virtualProgramInfo | Object(JSON) | @ColumnType(JdbcType.OTHER) | 虚拟项目信息 |
| approverComment | String | 审批人评论 | |
| dynamicProgramFields | Object(JSON) | @ColumnType(JdbcType.OTHER) | 动态项目字段 |
| approvals | Object(JSON) | @ColumnType(JdbcType.OTHER) | 多级审批配置和状态 |
| disableChannel | Boolean | 是否禁用渠道 | |
| zoomProgramInfo | Object(JSON) | @ColumnType(JdbcType.OTHER) | Zoom会议信息 |
| aggregateSpendReportStatus | Integer | 聚合支出报告状态 | |
| signedTime | Date | 签名时间 | |
| enableAudit | Boolean | 是否启用审计 | |
| speakerRandomization | Object(JSON) | @ColumnType(JdbcType.OTHER) | 演讲者随机化配置 |
| geog | String | @ColumnType(typeHandler=GeographyTypeHandler) | PostGIS 地理位置 |
总计: 98 个字段 -- 这是一个极度膨胀的实体。
2.1.2 Meeting (t_meeting) - Registration Site 实体
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/Meeting.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| meetingId | Integer | 主键(seq: s_meeting) |
| plannerId | Integer | 策划人ID |
| campaignId | Integer | 营销活动ID |
| meetingResumeId | Integer | 会议简历ID |
| categoryId | Integer | 类别ID |
| meetingName | String | 会议名称 |
| endDate | Date | 结束日期 |
| description | String | 描述 |
| createdDate | Date | 创建日期 |
| location | String | 位置 |
| expectedAttendees | Integer | 预期参会者 |
| budget | Integer | 预算 |
| expectedCostPer | Integer | 预期人均成本 |
| others | String | 其他 |
| lastUpdatetime | Date | 最后更新时间 |
| duration | Integer | 持续时间 |
| actualAttendees | Integer | 实际参会者 |
| customizedSubDomain | String | 自定义子域名(注册URL) |
| completeMeetingInfoStatus | Integer | 完成会议信息状态 |
| status | Integer | 状态 |
| policyRegistrationDeadline | Date | 策略:注册截止日期 |
| policyChangeDeadline | Date | 策略:变更截止日期 |
| policySleepingRoomChangeDeadline | Date | 策略:住宿变更截止日期 |
| policyNeedSleepingRoom | Integer | 策略:需要住宿 |
| policyNeedTravel | Integer | 策略:需要交通 |
| policyNeedLogin | Integer | 策略:需要登录 |
| policyNeedSelectAttendeeType | Integer | 策略:需要选择参会者类型 |
| policyIsOpen | Integer | 策略:是否开放 |
| policyTicketBasedMeeting | Integer | 策略:票务式会议 |
| policyAttendeeTypePricing | Integer | 策略:参会者类型定价 |
| policyTimeConflict | Integer | 策略:时间冲突 |
| policyConcurrentConflict | Integer | 策略:并发冲突 |
| policyOverlapConflict | Integer | 策略:重叠冲突 |
| locationId | Integer | 位置ID |
| policyTicketStyle | Integer | 策略:票务样式 |
| chooseAttendeeStatus | Integer | 选择参会者状态 |
| chooseAttendeeTemplate | String | 选择参会者模板 |
| canWait | Integer | 是否可等待 |
| waitingCount | Integer | 等待人数 |
| waitingHours | BigDecimal | 等待小时 |
| waitingStatus | Integer | 等待状态 |
| timezoneId | Integer | 时区ID |
| allDayEvent | Integer | 是否全天活动 |
| endTime | Date | 结束时间 |
| startDate | Date | 开始日期 |
| policyCurrencyId | Integer | 策略:货币ID |
| policyEnableAtUrl | Integer | 策略:启用URL |
| policyIsMailToPlanner | Integer | 策略:邮件通知策划人 |
| policyMailAddressToPlanner | String | 策略:策划人邮箱地址 |
| registrationPreviewInfo | String | 注册预览信息 |
| registrationConfirmInfo | String | 注册确认信息 |
| registrationDeadLineInfo | String | 注册截止信息 |
| registrationCancelInfo | String | 注册取消信息 |
| confirmationText | String | 确认文本 |
| policyBarCode | Integer | 策略:条形码 |
| policyEnableChangeAlias | Integer | 策略:启用别名变更 |
| confirmationEmailAddress | String | 确认邮箱地址 |
| confirmationAlias | String | 确认别名 |
| policyIsHiddenMeetingDate | Integer | 策略:隐藏会议日期 |
| policyAttachIcs | Integer | 策略:附加ICS文件 |
| policyIsMailCopyToPlanner | Integer | 策略:抄送邮件给策划人 |
| notes | String | 备注 |
| regSiteStatus | Integer | 注册站点状态 |
| siteTitle | String | 站点标题 |
| attendeeListStatus | Integer | 参会者列表状态 |
| externalId | Long | 外部ID |
| attendeeListClosedTime | Date | 参会者列表关闭时间 |
| policyOpenRegistrationEnabled | Short | 策略:启用开放注册 |
| type | Integer | 类型(0:HCP Programs, 1:Other Programs) |
| policyIsMailDeclineToPlanner | Short | 策略:拒绝邮件通知策划人 |
| policyMailDeclineToPlanner | String | 策略:策划人拒绝邮箱 |
| confirmationTextByAttendeeType | Object(JSON) | 按参会者类型确认文本 |
| confirmationEmailTextByAttendeeType | Object(JSON) | 按参会者类型确认邮件文本 |
| cancelRegistrationEnabled | Short | 启用取消注册 |
| changeRegistrationEnabled | Short | 启用变更注册 |
| confirmationTextEnabled | Short | 启用确认文本 |
| confirmationEmailTextEnabled | Short | 启用确认邮件文本 |
| enableRegistrationFullText | Integer | 启用注册满额文本 |
| registrationFullTextByAttendeeType | Object(JSON) | 按参会者类型满额文本 |
| enableOtherEmailAlert | Integer | 启用其他邮件提醒 |
| otherEmailAlert | Object(JSON) | 其他邮件提醒 |
| enableRegistrationGates | Integer | 启用注册门控 |
| registrationGates | Object(JSON) | 注册门控 |
| enableAutomaticRegistrantReminder | Integer | 启用自动提醒 |
| daysBeforeProgram | Integer | 项目前天数 |
总计: 77 个字段
2.1.3 MeetingProgramType (t_meeting_program_type)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingProgramType.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| programTypeId | Integer | 主键(seq: s_program) |
| programTypeName | String | 项目类型名称 |
| description | String | 描述 |
| productId | Integer | 产品ID |
| meetingIdPrefix | String | 会议ID前缀 |
| projectCode | String | 项目代码 |
| year | String | 年份 |
| needApproval | Integer | 是否需要审批 |
| meetingId | Integer | 模板 Meeting ID(用于注册站点复制) |
| signatureText | String | 签名文本 |
| status | Integer | 状态 |
| sequence | Integer | 排序 |
| brandId | Integer | 品牌ID |
| userId | Integer | 用户ID |
| approvals | Object(JSON) | 审批配置(多级审批JSON) |
| color | String | 颜色 |
| fieldExhibit | Boolean | 是否字段展览(影响审批流向) |
2.1.4 ProgramServiceType (t_meeting_program_service_type)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/ProgramServiceType.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| programServiceId | Integer | 主键(seq: s_service_type) |
| programServiceTypeName | String | 服务类型名称 |
| productId | Integer | 产品ID |
| programTypeId | Integer | 项目类型ID |
| description | String | 描述 |
| managementFee | BigDecimal | 管理费 |
| credit | Integer | 信用额度 |
| programCategory | String | 项目类别 |
| status | Integer | 状态 |
| sequence | Integer | 排序 |
| accessLevel | Integer | 访问级别(0:所有用户,1:仅UM) |
| meetingIdPrefix | String | 会议ID前缀(覆盖ProgramType的前缀) |
| projectCode | String | 项目代码 |
| updatedAt | Date | 更新时间 |
| virtualServiceTypeId | Integer | 虚拟服务类型ID |
| spaceConfig | Object(JSON) | 空间配置 |
| secondSpeakerRequired | Boolean | 是否需要第二演讲者 |
| virtualProgramInfo | Object(JSON) | 虚拟项目信息 |
| hybrid | Boolean | 是否混合模式 |
2.1.5 MeetingAlert (t_meeting_alert)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingAlert.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | Integer | 主键(seq: s_meeting_alert) |
| meetingRequestId | Integer | 会议请求ID |
| name | String | 提醒名称 |
| createdAt | Date | 创建时间 |
2.1.6 MeetingAttachment (t_meeting_attachment)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingAttachment.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | Integer | 主键(seq: s_meeting_attachment) |
| meetingRequestId | Integer | 会议请求ID |
| fileName | String | 文件名 |
| description | String | 描述 |
| type | Integer | 类型(0:sales上传,1:planner上传) |
| fileId | Integer | 文件ID |
| createdBy | String | 创建人 |
| createdAt | Date | 创建时间 |
| updatedBy | String | 更新人 |
| updatedAt | Date | 更新时间 |
2.1.7 MeetingAttachmentComment (t_meeting_attachment_comment)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingAttachmentComment.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | Integer | 主键(seq: t_meeting_attachment_comment_id_seq) |
| meetingRequestId | Integer | 会议请求ID |
| comment | String | 评论内容 |
| createdAt | Date | 创建时间 |
| createdBy | String | 创建人 |
2.1.8 MeetingChangeLog (t_meeting_change_log)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingChangeLog.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| meetingRequestId | Integer | 会议请求ID(无@Id注解!) |
| fieldName | String | 字段名称 |
| oldValue | String | 旧值 |
| newValue | String | 新值 |
| createdAt | Date | 创建时间 |
| createdBy | String | 创建人 |
注意: 此实体缺少 @Id 注解,没有主键定义。
2.1.9 MeetingComplianceDocument (t_meeting_compliance_document)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingComplianceDocument.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| meetingRequestId | Integer | @Id 会议请求ID(复合主键) |
| complianceDocumentId | Integer | @Id 合规文档ID(复合主键) |
| files | Object(JSON) | 文件列表 |
| status | Integer | 状态(0:未就绪,1:就绪) |
| readyForReviewTime | Date | 就绪审查时间 |
2.1.10 MeetingProjectTask (t_meeting_project_task)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingProjectTask.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | Integer | 主键(seq: t_meeting_project_task_id_seq) |
| meetingRequestId | Integer | 会议请求ID |
| projectTaskId | Integer | 项目任务ID |
| userId | Integer | 分配用户ID |
| status | Integer | 状态(0:Pending,1:Completed,2:Not Applicable) |
2.1.11 MeetingRequestPpc (t_meeting_request_ppc)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingRequestPpc.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | Integer | 主键(seq: s_meeting_request_ppc) |
| meetingRequestId | Integer | 会议请求ID |
| salesRepId | Integer | 销售代表ID |
| signature | String | 签名 |
| createdAt | Date | 创建时间 |
| createdBy | String | 创建人 |
2.1.12 MeetingRequestStatus (t_meeting_request_status)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingRequestStatus.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| statusId | Integer | 主键(seq: s_meeting_request_status) |
| statusName | String | 状态名称 |
| budgetVersionId | Integer | 预算版本ID |
| budgetStatus | String | 预算状态 |
2.1.13 MeetingRequestWorkflow (t_meeting_request_workflow)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingRequestWorkflow.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| meetingRequestId | Integer | @Id 会议请求ID(复合主键) |
| workflowId | Integer | @Id 工作流ID(复合主键) |
2.1.14 MeetingSharedUser (t_meeting_shared_user)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingSharedUser.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| meetingRequestId | Integer | @Id 会议请求ID(复合主键) |
| userId | Integer | @Id 用户ID(复合主键) |
2.1.15 ProjectTask (t_project_task)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/ProjectTask.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | Integer | 主键(seq: t_project_task_id_seq) |
| serviceTypeId | Integer | 服务类型ID |
| projectTaskTemplateId | Integer | 任务模板ID |
| dueDays | Integer | 到期天数 |
| createdAt | Date | 创建时间 |
| createdBy | String | 创建人 |
| updatedAt | Date | 更新时间 |
| updatedBy | String | 更新人 |
| status | Integer | 状态(0:inactive,1:active) |
2.1.16 ProjectTaskTemplate (t_project_task_template)
文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/ProjectTaskTemplate.java
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | Integer | 主键(seq: t_project_task_template_id_seq) |
| name | String | 模板名称 |
| dueDays | Integer | 默认到期天数 |
| type | Integer | 类型(0:before,1:after) |
2.2 Table Relationships (ER Diagram - ASCII)
+----------------------------+ +----------------------------+
| t_meeting_program_type | | t_meeting_program_ |
| (ProgramType) | | service_type |
+----------------------------+ | (ServiceType) |
| PK program_type_id |<---+ +----------------------------+
| product_id | | | PK program_service_id |
| meeting_id (template)---+-+ +--| FK program_type_id |
| brand_id | | | product_id |
| approvals (JSON) | | | virtual_service_type_id |
| field_exhibit | | | second_speaker_required |
+----------------------------+ | +----------------------------+
| |
+----------------------+ |
| |
v v
+----------------------------+ +---------------------------------+
| t_meeting | | t_meeting_request |
| (Registration Site) | | (Program / MeetingRequest) |
+----------------------------+ +---------------------------------+
| PK meeting_id |<---| FK meeting_id |
| planner_id | | PK meeting_request_id |
| customized_sub_domain | | FK program_type |
| start_date / end_time | | FK service_type |
| attendee_list_status | | FK product_id |
| policy_* (30+ fields) | | FK first/second/third_speaker|
| confirmation_* fields | | FK topic_id |
| registration_* fields | | FK planner_id |
+----------------------------+ | FK user_id |
| | FK region/district/territory |
| | meeting_status (0-14) |
v | approval_status (1-3) |
+----------------------------+ | approvals (JSON) |
| t_attendee | | meeting_request_info (KV str)|
| (via meetingId) | | dynamic_program_fields (JSON)|
+----------------------------+ | virtual/zoom_program_info |
| speaker_randomization (JSON) |
| geog (PostGIS) |
| 98 total fields |
+---------------------------------+
| | | | | |
+--------------------+ | | | | |
| | | | | |
v v | | | |
+------------------------+ +-------------+ | | | |
| t_meeting_shared_user | | t_meeting_ | | | | |
+------------------------+ | alert | | | | |
| PK meeting_request_id | +-------------+ | | | |
| PK user_id | v | | |
+------------------------+ +----------------+ | | |
| t_meeting_ | | | |
| attachment | | | |
+----------------+ | | |
| PK id | | | |
| FK meeting_ | | | |
| request_id | | | |
+----------------+ v | |
+------------------+ |
| t_meeting_change_| |
| log | |
+------------------+ |
| FK meeting_ | |
| request_id | |
| (NO PK!) | |
+------------------+ |
v
+------------------------+
| t_meeting_project_task |
+------------------------+
| PK id |
| FK meeting_request_id |
| FK project_task_id |
| FK user_id |
| status (0/1/2) |
+------------------------+
|
v
+------------------------+
| t_project_task |
+------------------------+
| PK id |
| FK service_type_id |
| FK project_task_ |
| template_id |
+------------------------+
|
v
+------------------------+
| t_project_task_ |
| template |
+------------------------+
| PK id |
| name, due_days, type |
+------------------------+2.3 Data Model Issues
| 编号 | 类型 | 问题描述 | 文件/位置 |
|---|---|---|---|
| DM-1 | 过大实体 | MeetingRequest 有 98 个字段,严重违反单一职责原则。包含: requestor 信息(6字段)、cancellation 信息(7字段)、commission 信息(5字段)、GPS 信息(4字段)、多个 JSON 字段等 | MeetingRequest.java |
| DM-2 | 命名混淆 | 核心概念命名极度混乱: Meeting 实际是 Registration Site,MeetingRequest 实际是 Program。表名和实体名都以 "Meeting" 开头,但业务语义完全不同 | 所有实体 |
| DM-3 | 自定义序列化 | meetingRequestInfo 字段使用自定义的 @-@/@=@ 分隔符存储 KV 对,不是标准 JSON。解析逻辑散布在 MeetingRequestDTO.java:405-441 和 MeetingRequestInfo.java | MeetingRequest.java:69 |
| DM-4 | 缺失主键 | MeetingChangeLog 实体缺少 @Id 注解,没有主键定义 | MeetingChangeLog.java |
| DM-5 | 冗余字段 | MeetingRequest.topicName 冗余(可从 topic_id join 获取);MeetingRequest.meetingName 与 meetingRequestName 语义重复 | MeetingRequest.java:106,168 |
| DM-6 | 类型不一致 | 布尔值使用 Integer(0/1)、Short 和 Boolean 三种方式表示,如 policyIsOpen: Integer, singleDayEvent: Boolean, policyOpenRegistrationEnabled: Short | 多处 |
| DM-7 | 硬编码 Speaker 槽位 | firstSpeaker, secondSpeaker, thirdSpeaker 作为固定列,不支持动态数量的演讲者 | MeetingRequest.java:73-79 |
| DM-8 | 非结构化 JSON | approvals, dynamicProgramFields, virtualProgramInfo, zoomProgramInfo, speakerRandomization 都存为 Object (JdbcType.OTHER → jsonb),但无类型安全性 | MeetingRequest.java:367-393 |
| DM-9 | Lombok 混用 | 部分实体用 Lombok(@Data),部分用手写 getter/setter(如 MeetingAlert, MeetingChangeLog),风格不统一 | 多处 |
3. Business Flow Analysis
3.1 Core Business Flows
3.1.1 Program 创建流程 (Sales Rep via SalesView)
Sales Rep 提交 Program 请求
|
v
+---------------------------+
| ProgramService. |
| createProgram() |
+---------------------------+
|
v
[检查 disallowProgramSubmission 配置]
|
v
[检查 Zoom 时间冲突] -- checkIfZoomMeetingTimeConflict()
|
v
[获取 ProductUser 权限信息]
|
v
[计算 meetingCost]
|
v
[检查预算是否充足] -- budgetAllocationService.hasEnoughBudget()
|
+----+----+
| |
有预算 无预算
| |
v v
ASSIGNED 是否disallowProgramIfBudgetReached?
(status=0) |
| +--+--+
| | |
| 是 否
| | |
| 抛异常 WAITLISTED(status=11)
| |
v v
[检查 needApproval?]
|
+--+--+
| |
需要 不需要
| |
v v
handleApprovalsFlow() → 直接 ASSIGNED + APPROVED
|
v
[多级审批逻辑 - 基于 approvals JSON]
|
v
PENDING_APPROVAL(status=12) + approvalStatus=1
oldMeetingStatus 保存原始状态
|
v
[分配 Planner] -- plannerService.getPlannerId()
|
v
[生成 meetingRequestName] -- meetingIdPrefix + sequence
|
v
[保存 MeetingRequest 到数据库]
|
v
[更新 GPS] -- Google API
|
v
[保存 SharedUsers, SpeakerCriteria, Attachments]
|
v
[发布 ProgramCreatedEvent]
→ 生成 Registration Site (Meeting)
→ 生成 Budget Items
→ 生成 Project Tasks
→ 发送通知3.1.2 Program 状态流转图
+-----------+
| 创建 |
+-----------+
|
+----------+----------+
| |
有足够预算 无足够预算
| |
v v
+----------+ +------------+
| ASSIGNED | | WAITLISTED |
| (0) | | (11) |
+----------+ +------------+
| |
需要审批? 需要审批?
+----+----+ +----+----+
| | | |
是 否 是 否
| | | |
v | v |
+-----------+ | +-----------+ |
| PENDING | | | PENDING | |
| APPROVAL | | | APPROVAL | |
| (12) | | | (12) | |
+-----------+ | | old=11 | |
| | +-----------+ |
+----+----+ | | |
| | | +----+----+ |
审批通过 审批拒绝 | | | |
| | | 通过 拒绝 |
v v | | | |
ASSIGNED DENIED | v v |
(0) (13) | WAITLISTED DENIED |
| | (11) (13) |
| | | |
+--------+-------+---+-----+--------+
| |
v v
+-----------+ +-----------+
| PLANNING | | CANCELLED |
| (6) | | (2) |
+-----------+ +-----------+
| |
v v
+-----------+ +---------------+
| REGISTERED| | CANCELLED |
| (9) | | CLOSED (3) |
+-----------+ +---------------+
|
v
+-----------+ +-----------+
| BILLING | | POSTPONED |
| (1) | | (7) |
+-----------+ +-----------+
| |
v v
+-----------+ +---------------+
| CLOSED | | POSTPONED |
| (4) | | CLOSED (8) |
+-----------+ +---------------+
|
v +-----------+
+-----------+ | REOPENED |
| (end) |<-----------| (14) |
+-----------+ +-----------+
其他状态:
ESTIMATE (5) - 估算
VOID (10) - 作废(删除时设置)3.1.3 审批流程 (Multi-level Approval)
审批配置来源: MeetingProgramType.approvals (JSON)
两种审批路径 (由 fieldExhibit 决定):
路径1 (fieldExhibit=false, 常规):
DM (District Manager) → RM (Region Manager) → UM (Upper Management)
路径2 (fieldExhibit=true, Field Exhibit):
RBD → Marketing → Legal
审批逻辑 (handleApprovalsFlow):
每级审批支持 rule=1: 成本阈值审批
- 如果 meetingCost < exceedsCost → 自动通过该级
- 如果 meetingCost >= exceedsCost → 需要该级审批
从低级到高级依次检查:
1. 如果当前级自动通过 → 检查下一级
2. 如果当前级需要手动审批 → 设置 PENDING_APPROVAL, 停止
部分审批 (Partial Approval):
- approvals JSON 中的 approvalStatus 字段记录当前待审批级别
- 如 "Partial Approval is for RM"
- 通过 PartialApprovalNotificationEvent 通知下一级审批人3.2 Validation Rules
| 规则 | Service/方法 | 文件:行号 |
|---|---|---|
| 禁止提交 Program (按配置) | ProgramService.createProgram() | ProgramService.java:409 |
| Zoom 时间冲突检查 | checkIfZoomMeetingTimeConflict() | ProgramService.java:1204-1220 |
| 预算不足拒绝(按配置) | disallowProgramIfBudgetReached | ProgramService.java:441-443 |
| 关闭 Program 需 ACT 预算版本 | closeProgram() | ProgramService.java:1409 |
| 关闭 Program 需关闭参会者列表 | closeProgram() | ProgramService.java:1414 |
| 关闭 Program 需完成 ToV 计算 | closeProgram() | ProgramService.java:1417-1422 |
| Topic 过期检查 | checkPrecondition() | ProgramService.java:868-880 |
| 关闭参会者列表时检查 Pending 状态 | closeAttendeeList() | ProgramService.java:844-846 |
| 已关闭的 Program 禁止更新 | updateProgram() | ProgramService.java:582-583 |
| 审批权限检查 | updateApprovalStatus() | ProgramService.java:636-637 |
| 品牌预算分配检查 | checkBudgetAllocation() | ProgramService.java:898-937 |
| 删除时检查 Meeting 存在 | deleteMeetingRequest() | MeetingService.java:908-910 |
3.3 Business Logic Issues
| 编号 | 类型 | 问题描述 | 文件:行号 |
|---|---|---|---|
| BL-1 | 硬编码产品ID | MeetingRequestDTO.buildMeetingRequestInfo() 中 if (productId == 24) 硬编码 Tesaro 产品ID | MeetingRequestDTO.java:298 |
| BL-2 | 硬编码国家ID | CreateProgramRequest.getCountryId() 默认返回 2 (United States) | CreateProgramRequest.java:134 |
| BL-3 | 硬编码持续时间 | Meeting 默认2小时: new Date(meetingStartTime.getTime() + 1000 * 60 * 60 * 2) | CreateProgramRequest.java:127, MeetingService.java:294 |
| BL-4 | Deprecated 方法仍在使用 | MeetingService 中多个方法标注 @Deprecated //use ProgramService methods 但仍被 MeetingController 调用 | MeetingService.java:221,235,255,531 |
| BL-5 | 不安全的类型转换 | 审批逻辑中大量 (Map) meetingRequest.getApprovals() 强转,无类型安全保障 | ProgramService.java:496-800 |
| BL-6 | GPS 数据迁移死循环 | doGPSDataMigration() 先用 for 循环遍历并更新,末尾又用 forEach 再次遍历更新,实际执行了两次 | ProgramService.java:1553-1564 |
| BL-7 | 删除全部警报 | deletesMeetingAlerts() 使用空 Example 删除所有记录,影响所有用户 | MeetingController.java:153-156 |
| BL-8 | 空字符串权限检查 | authorities.contains("") 检查空字符串,可能是 bug | ProgramService.java:1466 |
| BL-9 | 拼写错误 | parmaeters (应为 parameters) 出现在异常消息中 | MeetingService.java:444 |
| BL-10 | 事件重复处理 | saveMeetingRequest() 中大量注释掉的代码注明 "done by ProgramCreatedEvent now",说明从直接调用迁移到事件驱动但未清理旧代码 | MeetingService.java:276-330 |
| BL-11 | 不一致的错误处理 | getPlannerInfo() 捕获异常后返回 null,而非抛出明确的错误 | ProgramController.java:142-148 |
4. API Inventory
4.1 REST Endpoints Table
4.1.1 Meeting Module (v1/meetings)
| Method | Path | Parameters | Request Body | Response | Description |
|---|---|---|---|---|---|
| GET | /v1/meetings | MeetingQuery(type,meetingName,startDate,endDate,meetingStatus,budgetStatus,productId,programTypeId,salesId,speakerId,plannerId) | - | PageResult<MeetingsResponse> | 会议列表(Planner视图) |
| DELETE | /v1/meetings/{id} | id:Integer | - | void (204) | 删除会议(软删除+设VOID) |
| POST | /v1/meetings/{id}/copy | id:Integer, meetingRequestName:String | - | ProgramResponse (201) | 复制会议 |
| PUT | /v1/meetings/{id}/assign | id:Integer, plannerId:Integer | - | void | 分配策划人 |
| GET | /v1/meetings/{id}/changelogs | id:Integer | - | List<MeetingChangeLog> | 变更日志 |
| GET | /v1/meetings/changelogs/download | DownloadMeetingChangeLogsQuery | - | void (Excel) | 下载变更日志 |
| GET | /v1/meetings/alerts | - | - | List<MeetingAlertsResponse> | 会议提醒列表 |
| DELETE | /v1/meetings/alerts | - | - | void (204) | 删除所有提醒 |
| GET | /v1/meetings/download | MeetingQuery | - | void (Excel) | 下载会议列表 |
| GET | /v1/meetings/testMeeting | TestMeetingQuery | - | Integer | 获取测试用会议 |
| PUT | /v1/meetings/{id}/channel/enable | meetingRequestId:Integer | - | void | 启用渠道 |
| PUT | /v1/meetings/{id}/channel/disable | meetingRequestId:Integer | - | void | 禁用渠道 |
4.1.2 Meeting Attachment Module (v1/meetings/{meetingRequestId}/attachments)
| Method | Path | Parameters | Request Body | Response | Description |
|---|---|---|---|---|---|
| GET | /v1/meetings/{mrId}/attachments | meetingRequestId, type(0/1) | - | List<MeetingAttachmentDTO> | 附件列表 |
| POST | /v1/meetings/{mrId}/attachments | meetingRequestId | MeetingAttachmentDTO | void (201) | 创建附件 |
| PUT | /v1/meetings/{mrId}/attachments/{aid} | attachmentId | MeetingAttachmentDTO | void | 更新附件 |
| DELETE | /v1/meetings/{mrId}/attachments/{aid} | attachmentId | - | void (204) | 删除附件 |
| PUT | /v1/meetings/{mrId}/attachments/closeout | meetingRequestId | - | void | 关闭附件 |
| GET | /v1/meetings/{mrId}/attachments/comments | meetingRequestId | - | List<AttachmentCommentResponse> | 附件评论列表 |
| POST | /v1/meetings/{mrId}/attachments/comments | meetingRequestId | AttachmentCommentRequest | void (201) | 创建评论 |
4.1.3 Meeting Status Module (v1/meetings/status)
| Method | Path | Parameters | Request Body | Response | Description |
|---|---|---|---|---|---|
| GET | /v1/meetings/status | - | - | Object | 状态列表 |
4.1.4 Workflow Module (v1/workflow)
| Method | Path | Parameters | Request Body | Response | Description |
|---|---|---|---|---|---|
| GET | /v1/workflow | meetingRequestId?(optional) | - | List<WorkflowDTO> | 工作流列表 |
| POST | /v1/workflow | - | WorkflowRequest | void | 更新会议工作流 |
4.1.5 Program Module (v1/programs)
| Method | Path | Parameters | Request Body | Response | Description |
|---|---|---|---|---|---|
| GET | /v1/programs | ProgramQuery(productId,meetingRequestName,meetingStatus,...queryType) | - | PageResult<ProgramsResponse> | Program列表(SalesView) |
| POST | /v1/programs | - | CreateProgramRequest | CreateProgramResponse | 创建Program |
| GET | /v1/programs/{mrId} | meetingRequestId | - | ProgramResponse | Program详情 |
| PUT | /v1/programs/{mrId} | meetingRequestId | UpdateProgramRequest | ProgramResponse | 更新Program |
| PUT | /v1/programs/{mrId}/shared-users | meetingRequestId | SharedUsersRequest | void | 保存共享用户 |
| GET | /v1/programs/{mrId}/shared-users | meetingRequestId | - | List<SharedUsersResponse> | 获取共享用户 |
| PUT | /v1/programs/{mrId}/close-attendee-list | meetingRequestId | - | void | 关闭参会者列表 |
| PUT | /v1/programs/{mrId}/open-attendee-list | meetingRequestId | - | void | 打开参会者列表 |
| PUT | /v1/programs/{mrId}/approval-status | meetingRequestId | ApprovalStatusRequest | void | 更新审批状态 |
| GET | /v1/programs/precondition-check | PreconditionCheckRequest | - | void | 前置条件检查 |
| PUT | /v1/programs/{mrId}/cancel | meetingRequestId | - | void | 取消Program |
| GET | /v1/programs/planner | PlannerQuery | - | PlannerResponse | 获取策划人信息 |
| GET | /v1/programs/{productId}/alerts | productId | - | List<AlertsResponse> | 提醒列表 |
| POST | /v1/programs/{productId}/alerts/history | productId | List<SaveAlertHistoryRequest> | void | 保存提醒查看历史 |
| GET | /v1/programs/{mrId}/pprs | meetingRequestId | - | PprsResponse | PPR信息 |
| GET | /v1/programs/frequency-check | TargetFrequencyCheckRequest | - | AttendanceFrequencyCheckResponse | 频率检查 |
| GET | /v1/programs/invitation-track-report | meetingId | - | InvitationTrackReportResponse | 邀请追踪报告 |
| GET | /v1/programs/{mrId}/email-invitation-template | meetingRequestId | - | GetEmailInvitationTemplateResponse | 邮件邀请模板 |
| PUT | /v1/programs/aggregate-spend-report-status | - | AggregateSpendReportStatusRequest | void | 批量更新聚合支出状态 |
| PUT | /v1/programs/{mrId}/aggregate-spend-report-status | meetingRequestId | AggregateSpendReportStatusRequest | void | 单个更新聚合支出状态 |
| PUT | /v1/programs/{mrId}/sign | meetingRequestId | - | void | 签署Program认证 |
| GET | /v1/programs/{mrId}/automatic-registration-state | meetingRequestId | - | AutomaticRegistrationStateResponse | 自动注册状态 |
| POST | /v1/programs/{mrId}/register-speakers | meetingRequestId | - | void | 注册演讲者 |
| GET | /v1/programs/{mrId}/virtual-program-urls | meetingRequestId, ListVirtualProgramURLsQuery | - | List<ListVirtualProgramURLsResponse> | 虚拟Program URL列表 |
| GET | /v1/programs/{mrId}/virtual-program-urls/download | meetingRequestId, query | - | void (Excel) | 下载虚拟Program URL |
| PUT | /v1/programs/{mrId}/close | meetingRequestId | CloseProgramRequest | void | 关闭Program |
| PUT | /v1/programs/{mrId}/open | meetingRequestId | OpenProgramRequest | void | 重开Program |
| GET | /v1/programs/programinfo | meetingRequestId?, meetingId? | - | ProgramResponse | 获取Program信息(双入口) |
| GET | /v1/programs/data-exceptions | DataExceptionQuery | - | List<DataExceptionResponse> | 数据异常列表 |
| GET | /v1/programs/data-exceptions/download | DataExceptionQuery | - | void (Excel) | 下载数据异常 |
| GET | /v1/programs/data-exceptions/codes | - | - | List<DataExceptionCodeDictionary> | 异常代码字典 |
| GET | /v1/programs/gps-migration | - | - | void | GPS数据迁移(应为POST) |
| GET | /v1/programs/{mrId}/compliance-document-checklist | meetingRequestId | - | List<MeetingDocumentChecklistResponse> | 合规文档清单 |
| GET | /v1/programs/{mrId}/compliance-document-checklist/{docId} | meetingRequestId, documentId | - | MeetingDocumentChecklistResponse | 单个合规文档 |
| PUT | /v1/programs/{mrId}/compliance-document-checklist/{docId}/ready-for-audit | meetingRequestId, documentId | - | void | 标记就绪审计 |
| PUT | /v1/programs/{mrId}/compliance-document-checklist/{docId}/unready-for-audit | meetingRequestId, documentId | - | void | 标记未就绪审计 |
| POST | /v1/programs/{mrId}/compliance-document-checklist/{docId}/upload | meetingRequestId, documentId, file | MultipartFile | void | 上传合规文档 |
| DELETE | /v1/programs/{mrId}/compliance-document-checklist/{docId}/files/{fileId} | meetingRequestId, documentId, fileId | - | void | 删除合规文档文件 |
| GET | /v1/programs/{mrId}/audit | meetingRequestId | - | Boolean | 获取审计状态 |
| PUT | /v1/programs/{mrId}/audit | meetingRequestId | UpdateAuditStatusRequest | void | 更新审计状态 |
| GET | /v1/programs/{mrId}/compliance-tov-list | meetingRequestId | - | List<MeetingToVData> | ToV数据列表 |
| POST | /v1/programs/admin/backfill-geog | - | - | Map<String, Integer> | 回填geog字段 |
4.1.6 Speaker Randomization Module (v1/programs)
| Method | Path | Parameters | Request Body | Response | Description |
|---|---|---|---|---|---|
| GET | /v1/programs/{id}/matching-speakers | meetingRequestId | - | List<MatchingSpeakerResponse> | 匹配演讲者 |
| GET | /v1/programs/{id}/speaker-randomization | meetingRequestId | - | SpeakerRandomizationDTO | 获取随机化数据 |
| POST | /v1/programs/{id}/speaker-candidates | meetingRequestId | SubmitSpeakerCandidatesRequest | void | 提交候选演讲者 |
| POST | /v1/programs/{id}/assign-speaker | meetingRequestId | AssignSpeakerRequest | void | 分配演讲者 |
| PUT | /v1/programs/{id}/speaker-criteria | meetingRequestId | SpeakerCriteriaRequest | void | 更新演讲者条件 |
4.1.7 Calendar Module (v1/calendar)
| Method | Path | Parameters | Request Body | Response | Description |
|---|---|---|---|---|---|
| GET | /v1/calendar | CalendarRequest(productId,year,month,...) | - | List<CalendarResponse> | 日历视图 |
4.1.8 Project Task Module (v1/tasks)
| Method | Path | Parameters | Request Body | Response | Description |
|---|---|---|---|---|---|
| GET | /v1/tasks | productId | - | List<ProjectTasksResponse> | 任务配置列表 |
| POST | /v1/tasks | - | ProjectTaskRequest | void | 保存任务配置 |
| GET | /v1/tasks/templates | - | - | List<ProjectTaskTemplate> | 任务模板列表 |
| POST | /v1/tasks/templates | - | TaskTemplateRequest | void | 创建任务模板 |
| PUT | /v1/tasks/templates/{id} | id | TaskTemplateRequest | void | 更新任务模板 |
| DELETE | /v1/tasks/templates/{id} | id | - | void (204) | 删除任务模板 |
| GET | /v1/tasks/meeting-tasks | meetingRequestId | - | List<MeetingProjectTasksResponse> | Program任务列表 |
| PUT | /v1/tasks/meeting-tasks/{taskId}/assignee | taskId | UpdateAssigneeRequest | void | 更新任务分配 |
| PUT | /v1/tasks/meeting-tasks/{taskId}/status | taskId | UpdateStatusRequest | void | 更新任务状态 |
| GET | /v1/tasks/calendar | TaskCalendarRequest | - | List<TaskCalendarResponse> | 任务日历视图 |
总计: 63 个 API 端点
4.2 API Design Issues
| 编号 | 类型 | 问题描述 | 位置 |
|---|---|---|---|
| API-1 | 双重入口 | Program 管理分散在 /v1/meetings 和 /v1/programs 两个路径下,meetings 仍提供部分 CRUD 操作 | MeetingController.java, ProgramController.java |
| API-2 | Deprecated 端点未移除 | MeetingController 中 save(), get(), update() 方法被标记 @Deprecated 但未加 @RequestMapping,变成了死代码 | MeetingController.java:73-89 |
| API-3 | 路径参数命名不一致 | 有时用 {id},有时用 {meetingRequestId},甚至混用(如 SpeakerRandomization 中用 {id} 但语义是 meetingRequestId) | 多处 |
| API-4 | HTTP 方法不当 | GPS 迁移用 GET (/gps-migration),应为 POST 或 PUT | ProgramController.java:251 |
| API-5 | 路径变量语义混淆 | /{productId}/alerts 中 productId 作为路径变量但没有花括号说明,与 /{meetingRequestId} 路径冲突潜在风险 | ProgramController.java:151 |
| API-6 | 返回类型不一致 | MeetingStatusController.list() 返回 Object 而非具体类型 | MeetingStatusController.java:22 |
| API-7 | 聚合支出报告双端点 | 批量更新和单个更新使用两个端点,逻辑重复 | ProgramController.java:182-191 |
| API-8 | Compliance 端点耦合 | 合规文档管理的 API 放在 ProgramController 中,但实际由 ComplianceService 处理,违反模块边界 | ProgramController.java:255-302 |
| API-9 | 缺少版本控制 | 虽然路径有 v1/,但没有实际的版本控制机制 | 全局 |
5. Frontend Analysis
5.1 Pages & Components
5.1.1 Plannerview (pharmagin-plannerview)
| 路由 | 容器/组件 | Redux Store Key | 功能描述 |
|---|---|---|---|
/meetings | containers/MeetingTable | meetingTable | Program 列表视图(Planner视图),包含筛选、分页、签名、注册等 |
/meetings/:id(/:productId) | containers/MeetingDetail | meetingDetail | Program 详情页,加载 10+ 子模块(RegMeeting, SiteBuilder, AttendeeType, Contacts, Accommodation, Communication, Report, Budget, ChangeLog, Workflow, Attachments, Transfer, Task, VirtualProgramSetup) |
/registrations | containers/RegHomePage | registrations | 注册管理首页 |
/registrations/:id(/:type) | containers/RegMeeting | registrationsDetail | 注册详情页(同样加载 10+ 子模块) |
/calendar | containers/Calendar | calendar | 日历视图 |
/data-exceptions | containers/DataExceptions | - | 数据异常报告 |
/admin (含 ProgramType) | components/ProgramType | programType | 项目类型管理(Admin) |
/admin (含 ProjectTask) | components/ProjectTask | projectTask | 项目任务管理(Admin) |
MeetingDetail 子模块列表:
components/RegSiteBuilder(siteBuilderConfig) - 注册站点构建components/RegAttendeeType(attendeeTypes) - 参会者类型管理components/Contacts(contacts) - 联系人管理components/Accommodation(accommodation) - 住宿管理components/RegReport(report) - 注册报告components/AccommodationReport(accommodationReport) - 住宿报告components/ChangeReport(changeReport) - 变更报告components/AdhocReport(adhocReport) - 临时报告components/MTBudget(meetingBudget) - 会议预算components/MTChangeLog(changeLog) - 变更日志components/Workflow(workflow) - 工作流components/Attachments(attachments) - 附件components/Transfer(transfer) - 转移components/Task(meetingTask) - 项目任务components/VirtualProgramSetup(virtualProgramSetup) - 虚拟项目设置
5.1.2 Salesview (pharmagin-salesview)
| 路径 | 组件 | 功能描述 |
|---|---|---|
pages/Programs/List/ | AllPrograms, ScheduledPrograms, CompletedPrograms, MyPrograms, SharedPrograms, PendingApprovalPrograms, PendingApprovedPrograms, ApprovedPrograms | 多视图 Program 列表 |
pages/Programs/Form/ | SelectProgramType, SelectSpeakers, Speakers, TargetInvitees, UploadAttachments | Program 创建表单(分步) |
pages/Programs/Profile/ | Information, Attendees, ProgramEvaluation, ProgramPresentations, SignInAttendees, Surveys, EditProgram | Program 详情 Profile |
pages/Programs/Modal/ | Registration, ShareProgram, Attachments, MeetingBudget, EmailInvitationForm, etc. | 各种模态框 |
pages/Programs/SpeakerCandidates/ | SpeakerCandidates | 演讲者候选人 |
5.1.3 Speakerview (pharmagin-speakerview)
| 路由 | 组件 | 功能描述 |
|---|---|---|
/speakers/:speakerId/scheduled-programs | containers/SpeakerScheduledMeetings | 演讲者的已安排项目列表 |
/speakers/:speakerId/completed-programs | containers/SpeakerCompletedMeetings | 演讲者的已完成项目列表 |
5.2 Redux State Structure
Plannerview Redux Stores (Meeting/Program 相关):
store = {
meetingTable: {
// Program 列表状态 (containers/MeetingTable/reducer)
meetings: [],
loading: false,
filters: {},
pagination: {}
},
meetingDetail: {
// Program 详情状态 (containers/MeetingDetail/reducer)
meetingDetail: {},
loading: false
},
registrationsDetail: {
// 注册详情 (containers/RegMeeting/reducer)
},
siteBuilderConfig: {},
attendeeTypes: {},
contacts: {},
accommodation: {},
report: {},
accommodationReport: {},
changeReport: {},
adhocReport: {},
meetingBudget: {},
changeLog: {},
workflow: {},
attachments: {},
transfer: {},
meetingTask: {},
virtualProgramSetup: {},
calendar: {},
programType: {}, // Admin 项目类型
projectTask: {} // Admin 项目任务
}Salesview Redux Stores:
store = {
programs: {
// 主 Program 列表 (pages/Programs/reducer)
},
programForm: {
// Program 创建表单 (pages/Programs/Form/reducer)
},
programProfile: {
// Program 详情 (pages/Programs/Profile/reducer)
},
registration: {
// 注册模块 (pages/Programs/Modal/Registration/reducer)
},
surveys: {
// 调查问卷 (pages/Programs/Profile/Surveys/reducer)
}
}5.3 Frontend Issues
| 编号 | 类型 | 问题描述 | 文件/位置 |
|---|---|---|---|
| FE-1 | 巨大路由配置 | routes.js 中 /meetings/:id 路由加载了 15+ 个 reducer,形成了巨大的单页面 | plannerview/legacy/src/routes.js:181-354 |
| FE-2 | 重复的路由配置 | /meetings/:id 和 /registrations/:id 加载了几乎完全相同的模块集(约200行重复代码) | routes.js:181-354 vs routes.js:378-555 |
| FE-3 | 术语混用 | Plannerview 使用 "Meeting" 术语,Salesview 使用 "Program" 术语,增加了理解成本 | 全局 |
| FE-4 | 过时的 React 版本 | 使用 React 15.x-16.x 和 React Router 3.x-4.x,已远落后于当前版本 | 全局 |
| FE-5 | System.import | Speakerview 仍使用 System.import() 而非 import() 进行动态导入 | speakerview/legacy/src/routes.js:166-167 |
| FE-6 | 过多 Redux 分片 | MeetingDetail 页面涉及 15+ 个独立的 Redux store 分片,状态管理复杂度极高 | routes.js:181-354 |
6. Problem Summary
6.1 Critical Issues (must fix in rewrite)
| ID | 问题 | 影响 | 位置 |
|---|---|---|---|
| C-1 | Meeting vs MeetingRequest 命名混淆 | 新开发人员无法理解系统核心概念;Meeting 实际是 Registration Site,MeetingRequest 实际是 Program | 全局 |
| C-2 | MeetingRequest 实体过度膨胀(98字段) | 难以维护、性能问题(每次 update 都写 98 列)、职责不清 | MeetingRequest.java |
| C-3 | 自定义 KV 序列化格式(@-@/@=@) | 无法查询、无法索引、解析逻辑脆弱且散布在多处 | MeetingRequestDTO.java, MeetingRequestInfo.java |
| C-4 | 审批逻辑高度复杂且脆弱 | 200+ 行嵌套 if/else/switch 处理多级审批,使用无类型的 Map 操作 JSON,极易出错 | ProgramService.java:495-827 |
| C-5 | 双重控制器(meetings + programs) | API 入口分散,职责重叠,部分功能在两个模块中都有实现 | MeetingController.java, ProgramController.java |
| C-6 | 非结构化 JSON 字段无类型安全 | approvals, dynamicProgramFields, virtualProgramInfo 等 JSON 字段使用 Object 类型,所有操作都需要 unsafe cast | MeetingRequest.java:367-393 |
6.2 Design Defects (should improve)
| ID | 问题 | 影响 | 位置 |
|---|---|---|---|
| D-1 | Speaker 槽位硬编码 | 无法灵活支持 0-N 个演讲者;firstSpeaker/secondSpeaker/thirdSpeaker 作为固定列 | MeetingRequest.java:73-79 |
| D-2 | 事件驱动迁移不完整 | MeetingService 中大量注释掉的代码表明从直接调用迁移到事件驱动,但旧代码未清理 | MeetingService.java:270-330 |
| D-3 | Service 层循环依赖 | MeetingService 和 ProgramService 互相依赖(ProgramService 注入 MeetingService,且两者都操作 MeetingRequest) | 两个 Service |
| D-4 | MeetingChangeLog 无主键 | 无法精确删除/更新单条变更日志 | MeetingChangeLog.java |
| D-5 | Compliance 端点耦合 | ProgramController 中直接调用 ComplianceService,合规功能应有独立模块入口 | ProgramController.java:255-302 |
| D-6 | MeetingTable/MeetingDetail 前端巨页面 | 单个路由加载 15+ 个 reducer 和组件,初始加载慢,代码耦合 | routes.js:181-354 |
| D-7 | 布尔类型表示不一致 | Integer(0/1)、Short 和 Boolean 混用表示布尔值 | 多处 |
| D-8 | Program 列表查询过于复杂 | listPrograms() 方法包含 280+ 行代码,处理 6 种 queryType、权限过滤、审批状态名称计算 | ProgramService.java:221-282 |
6.3 Technical Debt (nice to have)
| ID | 问题 | 影响 | 位置 |
|---|---|---|---|
| T-1 | Lombok 使用不统一 | 部分实体用 Lombok,部分手写 getter/setter | 多个实体 |
| T-2 | 硬编码值 | 产品ID(24/Tesaro)、国家ID(2/US)、持续时间(2小时) | 多处 |
| T-3 | deletesMeetingAlerts 删除全部 | 空 Example 删除所有警报记录,影响全局 | MeetingController.java:153-156 |
| T-4 | GPS 迁移双重执行 | doGPSDataMigration() 中存在 for 循环和 forEach 双重遍历 | ProgramService.java:1553-1564 |
| T-5 | 拼写错误 | "parmaeters" 在异常消息中 | MeetingService.java:444 |
| T-6 | 空字符串权限检查 | authorities.contains("") 可能是 bug | ProgramService.java:1466 |
| T-7 | 错误处理不一致 | 部分方法返回 null,部分抛异常 | ProgramController.java:142-148 |
| T-8 | Speakerview 使用 System.import | 过时的动态导入语法 | speakerview/legacy/src/routes.js |
7. Rewrite Recommendations
7.1 数据模型重构建议
7.1.1 拆分 MeetingRequest 实体
将 98 字段的 MeetingRequest 拆分为多个聚焦的实体:
Program (核心)
├── id, name, productId, programTypeId, serviceTypeId
├── status, approvalStatus, budgetVersionId
├── meetingStartTime, meetingEndTime, timezoneId
├── plannerId, userId, teamId
└── createdAt, updatedAt
ProgramLocation (地理信息)
├── programId (FK)
├── venue, address1, address2, city, state, postalCode, countryId
├── lat, lng, radius, geog
└── gpsStatus
ProgramRequestor (请求者信息)
├── programId (FK)
├── firstName, lastName, jobTitle, phone, email
└── mobilePhone
ProgramSpeaker (演讲者关联 - 一对多)
├── programId (FK)
├── speakerId (FK)
├── role (PRIMARY/SECONDARY/TERTIARY)
├── confirmed (Boolean)
└── sequence
ProgramApproval (审批记录)
├── programId (FK)
├── approvalLevel (DM/RM/UM or RBD/Marketing/Legal)
├── status (PENDING/APPROVED/DENIED)
├── approverId, approverComment
└── createdAt
ProgramFinancial (财务信息)
├── programId (FK)
├── speakerHonoraria, meetingCost, totalCost
├── estimatedBudget, commissionRate, etc.
ProgramCustomField (动态字段 - 替代 meetingRequestInfo)
├── programId (FK)
├── fieldKey, fieldValue, fieldLabel7.1.2 统一命名
MeetingRequest→ProgramMeeting→RegistrationSiteMeetingProgramType→ProgramTypeProgramServiceType→ServiceTypeMeetingProjectTask→ProgramTask
7.1.3 JSON 字段类型化
为所有 JSON 字段创建强类型 POJO:
// 替代 Object approvals
public class ApprovalConfig {
private ApprovalLevel dmApproval;
private ApprovalLevel rmApproval;
private ApprovalLevel umApproval;
private String approvalStatus;
}
public class ApprovalLevel {
private Integer rule; // 0=always, 1=cost-based
private String exceedsCost;
private Boolean approved;
}7.2 API 重新设计建议
7.2.1 统一资源路径
# 核心 Program CRUD
GET /api/v2/programs # 列表(支持多种 queryType)
POST /api/v2/programs # 创建
GET /api/v2/programs/{id} # 详情
PUT /api/v2/programs/{id} # 更新
DELETE /api/v2/programs/{id} # 删除
# Program 子资源
PUT /api/v2/programs/{id}/status # 状态变更(取消/关闭/重开)
PUT /api/v2/programs/{id}/approval # 审批操作
GET /api/v2/programs/{id}/changelog # 变更日志
GET /api/v2/programs/{id}/tasks # 项目任务
PUT /api/v2/programs/{id}/tasks/{tid} # 更新任务
# 演讲者关联
GET /api/v2/programs/{id}/speakers # 获取演讲者
PUT /api/v2/programs/{id}/speakers # 更新演讲者
# 附件
GET /api/v2/programs/{id}/attachments
POST /api/v2/programs/{id}/attachments
DELETE /api/v2/programs/{id}/attachments/{aid}
# 注册站点
GET /api/v2/programs/{id}/registration-site
PUT /api/v2/programs/{id}/attendee-list/close
PUT /api/v2/programs/{id}/attendee-list/open
# 合规
GET /api/v2/programs/{id}/compliance
PUT /api/v2/programs/{id}/compliance/{docId}/audit-status
# 日历
GET /api/v2/calendar/programs # 日历视图
GET /api/v2/calendar/tasks # 任务日历
# 配置
GET /api/v2/program-types # 项目类型
GET /api/v2/service-types # 服务类型
GET /api/v2/task-templates # 任务模板7.2.2 状态变更 API 设计
使用命令模式替代直接字段更新:
PUT /api/v2/programs/{id}/status
{
"action": "CANCEL", // CANCEL, CLOSE, REOPEN, POSTPONE
"reason": "Budget issue",
"reopenAttendeeList": true // 仅 REOPEN 时有效
}7.3 前端重构建议
- 统一术语: 全部使用 "Program" 替代 "Meeting",消除术语混淆
- 拆分巨页面: 将 MeetingDetail 15+ 子模块拆分为独立的路由页面或懒加载标签页
- 消除路由重复:
/meetings/:id和/registrations/:id共享 200 行重复配置,应提取为共享模块 - 升级技术栈: React 18+, React Router 6+, Redux Toolkit 或 Zustand
- 统一状态管理: 减少 15+ Redux store 分片,使用 RTK Query 或 SWR 管理 API 状态
7.4 新技术/架构建议
- 审批引擎: 使用状态机(如 Spring Statemachine)替代 200+ 行 if/else 审批逻辑
- 事件溯源: 基于 Event Sourcing 管理 Program 状态变更,天然支持变更日志
- CQRS: 读写分离,Program 列表查询(6种 queryType)用独立的 Read Model
- 自定义字段系统: 使用 JSON Schema 替代
meetingRequestInfo的 KV 格式和dynamicProgramFields - PostGIS 空间查询: 已有
geog字段基础,可构建基于地理位置的 Program 搜索 - OpenAPI/Swagger 3.0: 升级 API 文档,自动生成客户端代码