Skip to content

Program & Meeting Domain - Deep Dive Analysis

1. Domain Overview

1.1 领域职责描述

Program & Meeting 领域是 Pharmagin Speaker Platform 的核心业务领域,负责管理制药公司演讲者项目(Speaker Programs)的完整生命周期。该领域覆盖以下核心职责:

  1. Program 生命周期管理 - 从创建、审批、执行到关闭的完整流程
  2. Meeting/Registration Site 管理 - 为每个 Program 生成注册站点(Registration Site),管理参会者注册
  3. 审批工作流 - 多级审批(DM/RM/UM 或 RBD/Marketing/Legal),支持部分审批和预算验证
  4. 日历视图 - 以日历形式展示项目时间安排
  5. Project Task 管理 - 为每个 Program 生成和追踪任务清单
  6. 附件和变更日志 - Program 附件管理和字段变更历史追踪
  7. Speaker Randomization - 根据条件随机匹配和分配演讲者
  8. 合规文档管理 - 合规文档清单和审计状态管理

1.2 涉及的后端模块和包

模块路径职责
modules/v1/meeting/原始 Meeting 模块(部分已 Deprecated),负责列表查询、删除、复制、分配、附件、工作流等
modules/v1/program/新的 Program 模块,负责创建、更新、审批、关闭/重开、日历、数据异常等
modules/v1/calendar/日历视图,查询指定时间范围内的 Program
modules/v1/project/项目任务管理,任务模板、Program 任务生成和状态追踪
common/persistence/entity/Meeting*.java13 个 Meeting 相关实体类
common/persistence/entity/Program*.javaProgramServiceType 实体
common/persistence/entity/Project*.javaProjectTask, ProjectTaskTemplate 实体

关键设计特征: 系统中存在 MeetingMeetingRequest 两个核心概念的混淆:

  • 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

字段名类型注解说明
meetingRequestIdInteger@Id, @GeneratedValue(seq: s_meeting_request)主键
requestDateDate请求日期
venueString场地名称
cityString城市
stateString
portalUserIdInteger门户用户ID(Sales Rep)
speakerHonorariaBigDecimal演讲者酬金
meetingCostBigDecimal会议成本
totalCostBigDecimal总成本
programTypeInteger项目类型ID(FK->t_meeting_program_type)
serviceTypeInteger服务类型ID(FK->t_meeting_program_service_type)
meetingStartTimeDate会议开始时间
meetingEndTimeDate会议结束时间
productIdInteger产品ID
meetingRequestInfoString自定义表单信息(使用@-@@=@分隔的KV串)
firstSpeakerInteger第一演讲者ID(FK->speaker)
secondSpeakerInteger第二演讲者ID
thirdSpeakerInteger第三演讲者ID
topicIdInteger主题ID
pprStringPPR 信息
meetingIdInteger关联的注册站点 Meeting ID(FK->t_meeting)
meetingIdPrefixString会议ID前缀
attendeeInfoString参会者信息
speakersString演讲者信息(字符串)
patientAdvocateString患者倡导者
paHonorariaBigDecimal患者倡导者酬金
topicNameString主题名称(冗余)
budgetStatusString预算状态(SOW/EST/ACT)
meetingStatusInteger会议状态(0-14枚举)
plannerIdInteger策划人ID
pprDateDatePPR日期
attendeeNumberInteger参会者人数
budgetVersionIdInteger预算版本ID
meetingRequestNameString会议请求名称(自动生成)
statusInteger删除标志(0=正常,1=删除)
invitationsNumberInteger邀请人数
actualAttendeeNumberInteger实际参会人数
requestorFirstNameString请求者名
requestorLastNameString请求者姓
requestorJobTitleString请求者职位
requestorPhoneNumberString请求者电话
requestorEmailString请求者邮箱
chargeNumberString费用编号
estimatedBudgetBigDecimal预算估算
firstNameOfBudgetApproverString预算审批人名
lastNameOfBudgetApproverString预算审批人姓
emailOfBudgetApproverString预算审批人邮箱
meetingNameString会议名称
meetingPurposeInteger会议目的
purposeDescriptionString目的描述
divisionInteger部门
businessGroupInteger业务组
attendeeTypeInteger参会者类型
goalsAndObjectivesString目标与目的
dateOnCalendarBoolean日历上的日期
postalCodeString邮政编码
countryIdInteger国家ID
lowestPriceVenueChosenBoolean是否选择最低价场地
reasonForVenueChosenString选择场地理由
venueNotesString场地备注
savingReasonString节省理由
otherSavingReasonString其他节省理由
savingNotesString节省备注
otherCanceledReasonString其他取消理由
totalLaborHoursBigDecimal总工时
sourcedByInteger来源
contractDueDateDate合同截止日期
depositDueDateDate押金截止日期
roomingListDueDateDate住宿清单截止日期
canceledReasonString取消原因
cancellationDeadline1-3Date3个取消截止日期
cancellationAmount1-3BigDecimal3个取消金额
singleDayEventBoolean是否单日活动
durationInteger持续时间
commissionableRateInteger佣金率
commissionRateBigDecimal佣金比率
estimatedCommissionAmountBigDecimal预计佣金金额
commissionCollectedDateDate佣金收取日期
commissionCollectedAmountBigDecimal佣金收取金额
meetingIdentifierInteger会议标识
alignmentInteger对齐方式
hotelNameString酒店名称
address1String地址1
address2String地址2
approvalStatusInteger审批状态(1:待审批,2:已审批,3:已拒绝)
oldMeetingStatusInteger旧会议状态(审批前保存的原始状态)
regionIdInteger区域ID
territoryIdInteger领地ID
districtIdInteger地区ID
surveyString调查问卷
latDouble纬度
lngDouble经度
radiusInteger半径
gpsStatusIntegerGPS状态
updateTimeDate更新时间
integrationMeetingIdString集成会议ID(Salesforce/Veeva)
externalIdLong外部ID
timezoneIdInteger时区ID
attachmentStatusInteger附件状态(0:正常,1:关闭)
sentReminderInteger是否已发送提醒(0/1)
teamIdInteger团队ID
salesForceIdInteger销售区域ID
showTrainedSpeakersBoolean是否显示已培训演讲者
userIdInteger创建用户ID(Sales Rep)
virtualProgramInfoObject(JSON)@ColumnType(JdbcType.OTHER)虚拟项目信息
approverCommentString审批人评论
dynamicProgramFieldsObject(JSON)@ColumnType(JdbcType.OTHER)动态项目字段
approvalsObject(JSON)@ColumnType(JdbcType.OTHER)多级审批配置和状态
disableChannelBoolean是否禁用渠道
zoomProgramInfoObject(JSON)@ColumnType(JdbcType.OTHER)Zoom会议信息
aggregateSpendReportStatusInteger聚合支出报告状态
signedTimeDate签名时间
enableAuditBoolean是否启用审计
speakerRandomizationObject(JSON)@ColumnType(JdbcType.OTHER)演讲者随机化配置
geogString@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

字段名类型说明
meetingIdInteger主键(seq: s_meeting)
plannerIdInteger策划人ID
campaignIdInteger营销活动ID
meetingResumeIdInteger会议简历ID
categoryIdInteger类别ID
meetingNameString会议名称
endDateDate结束日期
descriptionString描述
createdDateDate创建日期
locationString位置
expectedAttendeesInteger预期参会者
budgetInteger预算
expectedCostPerInteger预期人均成本
othersString其他
lastUpdatetimeDate最后更新时间
durationInteger持续时间
actualAttendeesInteger实际参会者
customizedSubDomainString自定义子域名(注册URL)
completeMeetingInfoStatusInteger完成会议信息状态
statusInteger状态
policyRegistrationDeadlineDate策略:注册截止日期
policyChangeDeadlineDate策略:变更截止日期
policySleepingRoomChangeDeadlineDate策略:住宿变更截止日期
policyNeedSleepingRoomInteger策略:需要住宿
policyNeedTravelInteger策略:需要交通
policyNeedLoginInteger策略:需要登录
policyNeedSelectAttendeeTypeInteger策略:需要选择参会者类型
policyIsOpenInteger策略:是否开放
policyTicketBasedMeetingInteger策略:票务式会议
policyAttendeeTypePricingInteger策略:参会者类型定价
policyTimeConflictInteger策略:时间冲突
policyConcurrentConflictInteger策略:并发冲突
policyOverlapConflictInteger策略:重叠冲突
locationIdInteger位置ID
policyTicketStyleInteger策略:票务样式
chooseAttendeeStatusInteger选择参会者状态
chooseAttendeeTemplateString选择参会者模板
canWaitInteger是否可等待
waitingCountInteger等待人数
waitingHoursBigDecimal等待小时
waitingStatusInteger等待状态
timezoneIdInteger时区ID
allDayEventInteger是否全天活动
endTimeDate结束时间
startDateDate开始日期
policyCurrencyIdInteger策略:货币ID
policyEnableAtUrlInteger策略:启用URL
policyIsMailToPlannerInteger策略:邮件通知策划人
policyMailAddressToPlannerString策略:策划人邮箱地址
registrationPreviewInfoString注册预览信息
registrationConfirmInfoString注册确认信息
registrationDeadLineInfoString注册截止信息
registrationCancelInfoString注册取消信息
confirmationTextString确认文本
policyBarCodeInteger策略:条形码
policyEnableChangeAliasInteger策略:启用别名变更
confirmationEmailAddressString确认邮箱地址
confirmationAliasString确认别名
policyIsHiddenMeetingDateInteger策略:隐藏会议日期
policyAttachIcsInteger策略:附加ICS文件
policyIsMailCopyToPlannerInteger策略:抄送邮件给策划人
notesString备注
regSiteStatusInteger注册站点状态
siteTitleString站点标题
attendeeListStatusInteger参会者列表状态
externalIdLong外部ID
attendeeListClosedTimeDate参会者列表关闭时间
policyOpenRegistrationEnabledShort策略:启用开放注册
typeInteger类型(0:HCP Programs, 1:Other Programs)
policyIsMailDeclineToPlannerShort策略:拒绝邮件通知策划人
policyMailDeclineToPlannerString策略:策划人拒绝邮箱
confirmationTextByAttendeeTypeObject(JSON)按参会者类型确认文本
confirmationEmailTextByAttendeeTypeObject(JSON)按参会者类型确认邮件文本
cancelRegistrationEnabledShort启用取消注册
changeRegistrationEnabledShort启用变更注册
confirmationTextEnabledShort启用确认文本
confirmationEmailTextEnabledShort启用确认邮件文本
enableRegistrationFullTextInteger启用注册满额文本
registrationFullTextByAttendeeTypeObject(JSON)按参会者类型满额文本
enableOtherEmailAlertInteger启用其他邮件提醒
otherEmailAlertObject(JSON)其他邮件提醒
enableRegistrationGatesInteger启用注册门控
registrationGatesObject(JSON)注册门控
enableAutomaticRegistrantReminderInteger启用自动提醒
daysBeforeProgramInteger项目前天数

总计: 77 个字段

2.1.3 MeetingProgramType (t_meeting_program_type)

文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingProgramType.java

字段名类型说明
programTypeIdInteger主键(seq: s_program)
programTypeNameString项目类型名称
descriptionString描述
productIdInteger产品ID
meetingIdPrefixString会议ID前缀
projectCodeString项目代码
yearString年份
needApprovalInteger是否需要审批
meetingIdInteger模板 Meeting ID(用于注册站点复制)
signatureTextString签名文本
statusInteger状态
sequenceInteger排序
brandIdInteger品牌ID
userIdInteger用户ID
approvalsObject(JSON)审批配置(多级审批JSON)
colorString颜色
fieldExhibitBoolean是否字段展览(影响审批流向)

2.1.4 ProgramServiceType (t_meeting_program_service_type)

文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/ProgramServiceType.java

字段名类型说明
programServiceIdInteger主键(seq: s_service_type)
programServiceTypeNameString服务类型名称
productIdInteger产品ID
programTypeIdInteger项目类型ID
descriptionString描述
managementFeeBigDecimal管理费
creditInteger信用额度
programCategoryString项目类别
statusInteger状态
sequenceInteger排序
accessLevelInteger访问级别(0:所有用户,1:仅UM)
meetingIdPrefixString会议ID前缀(覆盖ProgramType的前缀)
projectCodeString项目代码
updatedAtDate更新时间
virtualServiceTypeIdInteger虚拟服务类型ID
spaceConfigObject(JSON)空间配置
secondSpeakerRequiredBoolean是否需要第二演讲者
virtualProgramInfoObject(JSON)虚拟项目信息
hybridBoolean是否混合模式

2.1.5 MeetingAlert (t_meeting_alert)

文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingAlert.java

字段名类型说明
idInteger主键(seq: s_meeting_alert)
meetingRequestIdInteger会议请求ID
nameString提醒名称
createdAtDate创建时间

2.1.6 MeetingAttachment (t_meeting_attachment)

文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingAttachment.java

字段名类型说明
idInteger主键(seq: s_meeting_attachment)
meetingRequestIdInteger会议请求ID
fileNameString文件名
descriptionString描述
typeInteger类型(0:sales上传,1:planner上传)
fileIdInteger文件ID
createdByString创建人
createdAtDate创建时间
updatedByString更新人
updatedAtDate更新时间

2.1.7 MeetingAttachmentComment (t_meeting_attachment_comment)

文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingAttachmentComment.java

字段名类型说明
idInteger主键(seq: t_meeting_attachment_comment_id_seq)
meetingRequestIdInteger会议请求ID
commentString评论内容
createdAtDate创建时间
createdByString创建人

2.1.8 MeetingChangeLog (t_meeting_change_log)

文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingChangeLog.java

字段名类型说明
meetingRequestIdInteger会议请求ID(无@Id注解!)
fieldNameString字段名称
oldValueString旧值
newValueString新值
createdAtDate创建时间
createdByString创建人

注意: 此实体缺少 @Id 注解,没有主键定义。

2.1.9 MeetingComplianceDocument (t_meeting_compliance_document)

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

字段名类型说明
meetingRequestIdInteger@Id 会议请求ID(复合主键)
complianceDocumentIdInteger@Id 合规文档ID(复合主键)
filesObject(JSON)文件列表
statusInteger状态(0:未就绪,1:就绪)
readyForReviewTimeDate就绪审查时间

2.1.10 MeetingProjectTask (t_meeting_project_task)

文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingProjectTask.java

字段名类型说明
idInteger主键(seq: t_meeting_project_task_id_seq)
meetingRequestIdInteger会议请求ID
projectTaskIdInteger项目任务ID
userIdInteger分配用户ID
statusInteger状态(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

字段名类型说明
idInteger主键(seq: s_meeting_request_ppc)
meetingRequestIdInteger会议请求ID
salesRepIdInteger销售代表ID
signatureString签名
createdAtDate创建时间
createdByString创建人

2.1.12 MeetingRequestStatus (t_meeting_request_status)

文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingRequestStatus.java

字段名类型说明
statusIdInteger主键(seq: s_meeting_request_status)
statusNameString状态名称
budgetVersionIdInteger预算版本ID
budgetStatusString预算状态

2.1.13 MeetingRequestWorkflow (t_meeting_request_workflow)

文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingRequestWorkflow.java

字段名类型说明
meetingRequestIdInteger@Id 会议请求ID(复合主键)
workflowIdInteger@Id 工作流ID(复合主键)

2.1.14 MeetingSharedUser (t_meeting_shared_user)

文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/MeetingSharedUser.java

字段名类型说明
meetingRequestIdInteger@Id 会议请求ID(复合主键)
userIdInteger@Id 用户ID(复合主键)

2.1.15 ProjectTask (t_project_task)

文件: /pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/ProjectTask.java

字段名类型说明
idInteger主键(seq: t_project_task_id_seq)
serviceTypeIdInteger服务类型ID
projectTaskTemplateIdInteger任务模板ID
dueDaysInteger到期天数
createdAtDate创建时间
createdByString创建人
updatedAtDate更新时间
updatedByString更新人
statusInteger状态(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

字段名类型说明
idInteger主键(seq: t_project_task_template_id_seq)
nameString模板名称
dueDaysInteger默认到期天数
typeInteger类型(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-441MeetingRequestInfo.javaMeetingRequest.java:69
DM-4缺失主键MeetingChangeLog 实体缺少 @Id 注解,没有主键定义MeetingChangeLog.java
DM-5冗余字段MeetingRequest.topicName 冗余(可从 topic_id join 获取);MeetingRequest.meetingNamemeetingRequestName 语义重复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非结构化 JSONapprovals, dynamicProgramFields, virtualProgramInfo, zoomProgramInfo, speakerRandomization 都存为 Object (JdbcType.OTHER → jsonb),但无类型安全性MeetingRequest.java:367-393
DM-9Lombok 混用部分实体用 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
预算不足拒绝(按配置)disallowProgramIfBudgetReachedProgramService.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硬编码产品IDMeetingRequestDTO.buildMeetingRequestInfo()if (productId == 24) 硬编码 Tesaro 产品IDMeetingRequestDTO.java:298
BL-2硬编码国家IDCreateProgramRequest.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-4Deprecated 方法仍在使用MeetingService 中多个方法标注 @Deprecated //use ProgramService methods 但仍被 MeetingController 调用MeetingService.java:221,235,255,531
BL-5不安全的类型转换审批逻辑中大量 (Map) meetingRequest.getApprovals() 强转,无类型安全保障ProgramService.java:496-800
BL-6GPS 数据迁移死循环doGPSDataMigration() 先用 for 循环遍历并更新,末尾又用 forEach 再次遍历更新,实际执行了两次ProgramService.java:1553-1564
BL-7删除全部警报deletesMeetingAlerts() 使用空 Example 删除所有记录,影响所有用户MeetingController.java:153-156
BL-8空字符串权限检查authorities.contains("") 检查空字符串,可能是 bugProgramService.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)

MethodPathParametersRequest BodyResponseDescription
GET/v1/meetingsMeetingQuery(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}/copyid:Integer, meetingRequestName:String-ProgramResponse (201)复制会议
PUT/v1/meetings/{id}/assignid:Integer, plannerId:Integer-void分配策划人
GET/v1/meetings/{id}/changelogsid:Integer-List<MeetingChangeLog>变更日志
GET/v1/meetings/changelogs/downloadDownloadMeetingChangeLogsQuery-void (Excel)下载变更日志
GET/v1/meetings/alerts--List<MeetingAlertsResponse>会议提醒列表
DELETE/v1/meetings/alerts--void (204)删除所有提醒
GET/v1/meetings/downloadMeetingQuery-void (Excel)下载会议列表
GET/v1/meetings/testMeetingTestMeetingQuery-Integer获取测试用会议
PUT/v1/meetings/{id}/channel/enablemeetingRequestId:Integer-void启用渠道
PUT/v1/meetings/{id}/channel/disablemeetingRequestId:Integer-void禁用渠道

4.1.2 Meeting Attachment Module (v1/meetings/{meetingRequestId}/attachments)

MethodPathParametersRequest BodyResponseDescription
GET/v1/meetings/{mrId}/attachmentsmeetingRequestId, type(0/1)-List<MeetingAttachmentDTO>附件列表
POST/v1/meetings/{mrId}/attachmentsmeetingRequestIdMeetingAttachmentDTOvoid (201)创建附件
PUT/v1/meetings/{mrId}/attachments/{aid}attachmentIdMeetingAttachmentDTOvoid更新附件
DELETE/v1/meetings/{mrId}/attachments/{aid}attachmentId-void (204)删除附件
PUT/v1/meetings/{mrId}/attachments/closeoutmeetingRequestId-void关闭附件
GET/v1/meetings/{mrId}/attachments/commentsmeetingRequestId-List<AttachmentCommentResponse>附件评论列表
POST/v1/meetings/{mrId}/attachments/commentsmeetingRequestIdAttachmentCommentRequestvoid (201)创建评论

4.1.3 Meeting Status Module (v1/meetings/status)

MethodPathParametersRequest BodyResponseDescription
GET/v1/meetings/status--Object状态列表

4.1.4 Workflow Module (v1/workflow)

MethodPathParametersRequest BodyResponseDescription
GET/v1/workflowmeetingRequestId?(optional)-List<WorkflowDTO>工作流列表
POST/v1/workflow-WorkflowRequestvoid更新会议工作流

4.1.5 Program Module (v1/programs)

MethodPathParametersRequest BodyResponseDescription
GET/v1/programsProgramQuery(productId,meetingRequestName,meetingStatus,...queryType)-PageResult<ProgramsResponse>Program列表(SalesView)
POST/v1/programs-CreateProgramRequestCreateProgramResponse创建Program
GET/v1/programs/{mrId}meetingRequestId-ProgramResponseProgram详情
PUT/v1/programs/{mrId}meetingRequestIdUpdateProgramRequestProgramResponse更新Program
PUT/v1/programs/{mrId}/shared-usersmeetingRequestIdSharedUsersRequestvoid保存共享用户
GET/v1/programs/{mrId}/shared-usersmeetingRequestId-List<SharedUsersResponse>获取共享用户
PUT/v1/programs/{mrId}/close-attendee-listmeetingRequestId-void关闭参会者列表
PUT/v1/programs/{mrId}/open-attendee-listmeetingRequestId-void打开参会者列表
PUT/v1/programs/{mrId}/approval-statusmeetingRequestIdApprovalStatusRequestvoid更新审批状态
GET/v1/programs/precondition-checkPreconditionCheckRequest-void前置条件检查
PUT/v1/programs/{mrId}/cancelmeetingRequestId-void取消Program
GET/v1/programs/plannerPlannerQuery-PlannerResponse获取策划人信息
GET/v1/programs/{productId}/alertsproductId-List<AlertsResponse>提醒列表
POST/v1/programs/{productId}/alerts/historyproductIdList<SaveAlertHistoryRequest>void保存提醒查看历史
GET/v1/programs/{mrId}/pprsmeetingRequestId-PprsResponsePPR信息
GET/v1/programs/frequency-checkTargetFrequencyCheckRequest-AttendanceFrequencyCheckResponse频率检查
GET/v1/programs/invitation-track-reportmeetingId-InvitationTrackReportResponse邀请追踪报告
GET/v1/programs/{mrId}/email-invitation-templatemeetingRequestId-GetEmailInvitationTemplateResponse邮件邀请模板
PUT/v1/programs/aggregate-spend-report-status-AggregateSpendReportStatusRequestvoid批量更新聚合支出状态
PUT/v1/programs/{mrId}/aggregate-spend-report-statusmeetingRequestIdAggregateSpendReportStatusRequestvoid单个更新聚合支出状态
PUT/v1/programs/{mrId}/signmeetingRequestId-void签署Program认证
GET/v1/programs/{mrId}/automatic-registration-statemeetingRequestId-AutomaticRegistrationStateResponse自动注册状态
POST/v1/programs/{mrId}/register-speakersmeetingRequestId-void注册演讲者
GET/v1/programs/{mrId}/virtual-program-urlsmeetingRequestId, ListVirtualProgramURLsQuery-List<ListVirtualProgramURLsResponse>虚拟Program URL列表
GET/v1/programs/{mrId}/virtual-program-urls/downloadmeetingRequestId, query-void (Excel)下载虚拟Program URL
PUT/v1/programs/{mrId}/closemeetingRequestIdCloseProgramRequestvoid关闭Program
PUT/v1/programs/{mrId}/openmeetingRequestIdOpenProgramRequestvoid重开Program
GET/v1/programs/programinfomeetingRequestId?, meetingId?-ProgramResponse获取Program信息(双入口)
GET/v1/programs/data-exceptionsDataExceptionQuery-List<DataExceptionResponse>数据异常列表
GET/v1/programs/data-exceptions/downloadDataExceptionQuery-void (Excel)下载数据异常
GET/v1/programs/data-exceptions/codes--List<DataExceptionCodeDictionary>异常代码字典
GET/v1/programs/gps-migration--voidGPS数据迁移(应为POST)
GET/v1/programs/{mrId}/compliance-document-checklistmeetingRequestId-List<MeetingDocumentChecklistResponse>合规文档清单
GET/v1/programs/{mrId}/compliance-document-checklist/{docId}meetingRequestId, documentId-MeetingDocumentChecklistResponse单个合规文档
PUT/v1/programs/{mrId}/compliance-document-checklist/{docId}/ready-for-auditmeetingRequestId, documentId-void标记就绪审计
PUT/v1/programs/{mrId}/compliance-document-checklist/{docId}/unready-for-auditmeetingRequestId, documentId-void标记未就绪审计
POST/v1/programs/{mrId}/compliance-document-checklist/{docId}/uploadmeetingRequestId, documentId, fileMultipartFilevoid上传合规文档
DELETE/v1/programs/{mrId}/compliance-document-checklist/{docId}/files/{fileId}meetingRequestId, documentId, fileId-void删除合规文档文件
GET/v1/programs/{mrId}/auditmeetingRequestId-Boolean获取审计状态
PUT/v1/programs/{mrId}/auditmeetingRequestIdUpdateAuditStatusRequestvoid更新审计状态
GET/v1/programs/{mrId}/compliance-tov-listmeetingRequestId-List<MeetingToVData>ToV数据列表
POST/v1/programs/admin/backfill-geog--Map<String, Integer>回填geog字段

4.1.6 Speaker Randomization Module (v1/programs)

MethodPathParametersRequest BodyResponseDescription
GET/v1/programs/{id}/matching-speakersmeetingRequestId-List<MatchingSpeakerResponse>匹配演讲者
GET/v1/programs/{id}/speaker-randomizationmeetingRequestId-SpeakerRandomizationDTO获取随机化数据
POST/v1/programs/{id}/speaker-candidatesmeetingRequestIdSubmitSpeakerCandidatesRequestvoid提交候选演讲者
POST/v1/programs/{id}/assign-speakermeetingRequestIdAssignSpeakerRequestvoid分配演讲者
PUT/v1/programs/{id}/speaker-criteriameetingRequestIdSpeakerCriteriaRequestvoid更新演讲者条件

4.1.7 Calendar Module (v1/calendar)

MethodPathParametersRequest BodyResponseDescription
GET/v1/calendarCalendarRequest(productId,year,month,...)-List<CalendarResponse>日历视图

4.1.8 Project Task Module (v1/tasks)

MethodPathParametersRequest BodyResponseDescription
GET/v1/tasksproductId-List<ProjectTasksResponse>任务配置列表
POST/v1/tasks-ProjectTaskRequestvoid保存任务配置
GET/v1/tasks/templates--List<ProjectTaskTemplate>任务模板列表
POST/v1/tasks/templates-TaskTemplateRequestvoid创建任务模板
PUT/v1/tasks/templates/{id}idTaskTemplateRequestvoid更新任务模板
DELETE/v1/tasks/templates/{id}id-void (204)删除任务模板
GET/v1/tasks/meeting-tasksmeetingRequestId-List<MeetingProjectTasksResponse>Program任务列表
PUT/v1/tasks/meeting-tasks/{taskId}/assigneetaskIdUpdateAssigneeRequestvoid更新任务分配
PUT/v1/tasks/meeting-tasks/{taskId}/statustaskIdUpdateStatusRequestvoid更新任务状态
GET/v1/tasks/calendarTaskCalendarRequest-List<TaskCalendarResponse>任务日历视图

总计: 63 个 API 端点

4.2 API Design Issues

编号类型问题描述位置
API-1双重入口Program 管理分散在 /v1/meetings/v1/programs 两个路径下,meetings 仍提供部分 CRUD 操作MeetingController.java, ProgramController.java
API-2Deprecated 端点未移除MeetingControllersave(), get(), update() 方法被标记 @Deprecated 但未加 @RequestMapping,变成了死代码MeetingController.java:73-89
API-3路径参数命名不一致有时用 {id},有时用 {meetingRequestId},甚至混用(如 SpeakerRandomization 中用 {id} 但语义是 meetingRequestId)多处
API-4HTTP 方法不当GPS 迁移用 GET (/gps-migration),应为 POST 或 PUTProgramController.java:251
API-5路径变量语义混淆/{productId}/alertsproductId 作为路径变量但没有花括号说明,与 /{meetingRequestId} 路径冲突潜在风险ProgramController.java:151
API-6返回类型不一致MeetingStatusController.list() 返回 Object 而非具体类型MeetingStatusController.java:22
API-7聚合支出报告双端点批量更新和单个更新使用两个端点,逻辑重复ProgramController.java:182-191
API-8Compliance 端点耦合合规文档管理的 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功能描述
/meetingscontainers/MeetingTablemeetingTableProgram 列表视图(Planner视图),包含筛选、分页、签名、注册等
/meetings/:id(/:productId)containers/MeetingDetailmeetingDetailProgram 详情页,加载 10+ 子模块(RegMeeting, SiteBuilder, AttendeeType, Contacts, Accommodation, Communication, Report, Budget, ChangeLog, Workflow, Attachments, Transfer, Task, VirtualProgramSetup)
/registrationscontainers/RegHomePageregistrations注册管理首页
/registrations/:id(/:type)containers/RegMeetingregistrationsDetail注册详情页(同样加载 10+ 子模块)
/calendarcontainers/Calendarcalendar日历视图
/data-exceptionscontainers/DataExceptions-数据异常报告
/admin (含 ProgramType)components/ProgramTypeprogramType项目类型管理(Admin)
/admin (含 ProjectTask)components/ProjectTaskprojectTask项目任务管理(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, UploadAttachmentsProgram 创建表单(分步)
pages/Programs/Profile/Information, Attendees, ProgramEvaluation, ProgramPresentations, SignInAttendees, Surveys, EditProgramProgram 详情 Profile
pages/Programs/Modal/Registration, ShareProgram, Attachments, MeetingBudget, EmailInvitationForm, etc.各种模态框
pages/Programs/SpeakerCandidates/SpeakerCandidates演讲者候选人

5.1.3 Speakerview (pharmagin-speakerview)

路由组件功能描述
/speakers/:speakerId/scheduled-programscontainers/SpeakerScheduledMeetings演讲者的已安排项目列表
/speakers/:speakerId/completed-programscontainers/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-5System.importSpeakerview 仍使用 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-1Meeting vs MeetingRequest 命名混淆新开发人员无法理解系统核心概念;Meeting 实际是 Registration Site,MeetingRequest 实际是 Program全局
C-2MeetingRequest 实体过度膨胀(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 castMeetingRequest.java:367-393

6.2 Design Defects (should improve)

ID问题影响位置
D-1Speaker 槽位硬编码无法灵活支持 0-N 个演讲者;firstSpeaker/secondSpeaker/thirdSpeaker 作为固定列MeetingRequest.java:73-79
D-2事件驱动迁移不完整MeetingService 中大量注释掉的代码表明从直接调用迁移到事件驱动,但旧代码未清理MeetingService.java:270-330
D-3Service 层循环依赖MeetingServiceProgramService 互相依赖(ProgramService 注入 MeetingService,且两者都操作 MeetingRequest)两个 Service
D-4MeetingChangeLog 无主键无法精确删除/更新单条变更日志MeetingChangeLog.java
D-5Compliance 端点耦合ProgramController 中直接调用 ComplianceService,合规功能应有独立模块入口ProgramController.java:255-302
D-6MeetingTable/MeetingDetail 前端巨页面单个路由加载 15+ 个 reducer 和组件,初始加载慢,代码耦合routes.js:181-354
D-7布尔类型表示不一致Integer(0/1)、Short 和 Boolean 混用表示布尔值多处
D-8Program 列表查询过于复杂listPrograms() 方法包含 280+ 行代码,处理 6 种 queryType、权限过滤、审批状态名称计算ProgramService.java:221-282

6.3 Technical Debt (nice to have)

ID问题影响位置
T-1Lombok 使用不统一部分实体用 Lombok,部分手写 getter/setter多个实体
T-2硬编码值产品ID(24/Tesaro)、国家ID(2/US)、持续时间(2小时)多处
T-3deletesMeetingAlerts 删除全部空 Example 删除所有警报记录,影响全局MeetingController.java:153-156
T-4GPS 迁移双重执行doGPSDataMigration() 中存在 for 循环和 forEach 双重遍历ProgramService.java:1553-1564
T-5拼写错误"parmaeters" 在异常消息中MeetingService.java:444
T-6空字符串权限检查authorities.contains("") 可能是 bugProgramService.java:1466
T-7错误处理不一致部分方法返回 null,部分抛异常ProgramController.java:142-148
T-8Speakerview 使用 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, fieldLabel

7.1.2 统一命名

  • MeetingRequestProgram
  • MeetingRegistrationSite
  • MeetingProgramTypeProgramType
  • ProgramServiceTypeServiceType
  • MeetingProjectTaskProgramTask

7.1.3 JSON 字段类型化

为所有 JSON 字段创建强类型 POJO:

java
// 替代 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 设计

使用命令模式替代直接字段更新:

json
PUT /api/v2/programs/{id}/status
{
  "action": "CANCEL",        // CANCEL, CLOSE, REOPEN, POSTPONE
  "reason": "Budget issue",
  "reopenAttendeeList": true // 仅 REOPEN 时有效
}

7.3 前端重构建议

  1. 统一术语: 全部使用 "Program" 替代 "Meeting",消除术语混淆
  2. 拆分巨页面: 将 MeetingDetail 15+ 子模块拆分为独立的路由页面或懒加载标签页
  3. 消除路由重复: /meetings/:id/registrations/:id 共享 200 行重复配置,应提取为共享模块
  4. 升级技术栈: React 18+, React Router 6+, Redux Toolkit 或 Zustand
  5. 统一状态管理: 减少 15+ Redux store 分片,使用 RTK Query 或 SWR 管理 API 状态

7.4 新技术/架构建议

  1. 审批引擎: 使用状态机(如 Spring Statemachine)替代 200+ 行 if/else 审批逻辑
  2. 事件溯源: 基于 Event Sourcing 管理 Program 状态变更,天然支持变更日志
  3. CQRS: 读写分离,Program 列表查询(6种 queryType)用独立的 Read Model
  4. 自定义字段系统: 使用 JSON Schema 替代 meetingRequestInfo 的 KV 格式和 dynamicProgramFields
  5. PostGIS 空间查询: 已有 geog 字段基础,可构建基于地理位置的 Program 搜索
  6. OpenAPI/Swagger 3.0: 升级 API 文档,自动生成客户端代码