Skip to content

Attendee & Registration Domain - Deep Dive Analysis

1. Domain Overview

1.1 领域职责描述

Attendee & Registration 领域是制药行业 Speaker Program 平台的核心业务域,负责管理参会人员(Attendee)从添加、邀请、注册、签到到合规审查的完整生命周期。该领域涵盖以下核心职责:

  1. 参会人员管理(Attendee Management):创建、编辑、删除参会人员记录,支持 Excel/CSV 批量导入、跨 Meeting 复制参会人员
  2. 注册工作流(Registration Workflow):管理从 Pending -> Invited -> Accepted -> Registered -> Declined/Canceled 的状态流转
  3. HCP 合规调和(Reconciliation):将参会人员与 NPI 数据库、Target 列表或 Salesforce CRM 进行身份匹配,确保 HCP(Healthcare Professional)的合规追踪
  4. 出席频率检查(Frequency Check):控制 HCP 在特定时间范围内参加 Speaker Program 的次数,防止合规风险
  5. 签到与签名(Check-In & Signature):现场签到、电子签名采集、地理位置记录
  6. 住宿管理(Sleeping Room Management):酒店房间预订、房型库存管理、入住/退房日期管理
  7. 注册站点(Registration Site / Site Builder):通过可视化模板配置面向公众的注册页面,包含自定义字段、页面布局和品牌样式
  8. Transfer of Value(TOV):追踪和分配对参会人员的价值转移金额
  9. 邀请通知(Invitation & Notification):邮件邀请发送、邀请状态跟踪(已读、已访问)
  10. 问卷调查(Attendee Survey):参会人员满意度调查的创建和响应收集

1.2 涉及的后端模块和包

模块路径说明
modules/v1/site/核心模块:Attendee CRUD、AttendeeType、SiteBuilder、SleepingRoom、Contact、TOV、RegistrationWorkflow、ChangeLog
modules/v1/registration/注册流程模块:注册表单获取与提交、批量注册、邮件邀请、频率检查、接受/拒绝注册
modules/v1/npi/NPI 数据查询模块:从 npi schema 查询 NPPES 国家医疗人员标识数据
modules/v1/target/Target 列表模块:客户上传的 HCP 目标列表查询
common/persistence/entity/15 个核心 Entity
plus/resolver/FrequencyCheckResolver - Plus 包中的频率检查解析器
modules/v1/survey/问卷调查模块(AttendeeSurvey 相关)

2. Data Model Analysis

2.1 Entity Overview Table

2.1.1 Attendee (t_attendee)

主表,每条记录代表一个 Meeting 中的一个参会人员。

字段名Java 类型数据库列名注解说明
attendeeIdStringattendee_id@Id, @GeneratedValue(generator="JDBC")UUID 主键
meetingIdIntegermeeting_id@Column关联 Meeting(注册站点)
attendeeTypeIdIntegerattendee_type_id@Column关联 AttendeeType
firstNameStringfirst_name@Column
lastNameStringlast_name@Column
middleNameStringmiddle_name@Column中间名
emailStringemail@Column邮箱
hcpStatusIntegerhcp_status@ColumnHCP 状态:1=Not Reconciled, 2=Reconciled, 3=Not HCP
phoneStringphone@Column电话
officeAddress1Stringoffice_address1@Column办公地址1
officeAddress2Stringoffice_address2@Column办公地址2
cityStringcity@Column城市
stateStringstate@Column
zipCodeStringzip_code@Column邮编
specialtyStringspecialty@Column专科
affiliationStringaffiliation@Column从属机构
licenseNumberStringlicense_number@Column执照号
licenseStateStringlicense_state@Column执照所在州
npiStringnpi@ColumnNPI 号码
reconciliationTypeIntegerreconciliation_type@Column调和类型:1=NPI, 2=Target, 3=Salesforce
meNumberStringme_number@ColumnME 号码
occupationStringoccupation@Column职业 / attendee type
credentialsStringcredentials@Column资质证书
governmentEmployeeStringgovernment_employee@Column是否政府雇员
vermontLicenseStringvermont_license@Column佛蒙特州执照
vermontLicenseNumberStringvermont_license_number@Column佛蒙特州执照号
mealRequirementsStringmeal_requirements@Column餐饮要求
allergiesStringallergies@Column过敏信息
noteStringnote@Column备注
attendeeCategoryStringattendee_category@Column参会人员分类(如 HCP, Pharma, Speaker)
salesforceContactWhoIdStringsalesforce_contact_who_id@ColumnSalesforce 联系人 ID
salesforceEventIdStringsalesforce_event_id@ColumnSalesforce 事件 ID
registrationStatusIntegerregistration_status@Column注册状态:0=Pending, 10=Invited, 20=Accepted, 40=Registered, 50=Canceled, 60=Declined, 70=Rep Invited
signInStatusIntegersign_in_status@Column签到状态:0=Default, 1=Signed In, 2=No Show, 3=Checked In
createdAtDatecreated_at@Column创建时间(注册日期)
updatedAtDateupdated_at@Column最后更新时间
reconciliationIdStringreconciliation_id@Column调和 ID(NPI 号 / Target externalId / SF contactId)
declineReasonStringdecline_reason@Column拒绝原因
transferOfValueBigDecimaltransfer_of_value@Column价值转移金额
tovChangeReasonStringtov_change_reason@ColumnTOV 变更原因
draftAttendeeUuidStringdraft_attendee_uuid@Column草稿 UUID
externalIdLongexternal_id@Column外部 ID
attendeeType2Stringattendee_type2@Column第二参会人类型
reconciliationSubTypeIntegerreconciliation_sub_type@Column调和子类型:0=t_target, 1=t_secondary_target
parentIdStringparent_id@Column父级 attendee_id(Guest 关联)
ccEmailStringcc_email@Column抄送邮箱
signInTimeDatesign_in_time@Column签到时间
pvExtUserIdStringpv_ext_user_id@ColumnPV 外部用户 ID(遗留字段)
primaryHcpIdStringprimary_hcp_id@Column主要 HCP ID
primaryHcpNameStringprimary_hcp_name@Column主要 HCP 姓名
virtualProgramAttendeeInfoObject (JSON)virtual_program_attendee_info@ColumnType(JdbcType.OTHER)虚拟会议参会信息(JSON)
registrationSourceObject (JSON)registration_source@ColumnType(JdbcType.OTHER)注册来源(JSON:{source, createdBy})
createdByStringcreated_by@Column创建者
attendanceFormatStringattendance_format@Column参会格式

2.1.2 AttendeeType (t_attendee_type)

定义 Meeting 下的参会人员类别(如 HCP、Pharma、Speaker)。

字段名Java 类型数据库列名说明
attendeeTypeIdIntegerattendee_type_id自增主键 (s_attendee_type)
meetingIdIntegermeeting_id关联 Meeting
attendeeTypeNameStringattendee_type_name类型名称
statusIntegerstatus状态
isDefaultIntegeris_default是否默认类型
siteTemplateStringsite_template站点模板
pageNumberIntegerpage_number页面数量
completeContactInfoStatusIntegercomplete_contact_info_status联系信息完成状态
completeActivityStatusIntegercomplete_activity_status活动完成状态
completeQaStatusIntegercomplete_qa_statusQA 完成状态
completeSleepingRoomStatusIntegercomplete_sleeping_room_status住宿完成状态
attendeeManagementStatusIntegerattendee_management_status管理状态
siteTitleStringsite_title站点标题
createdByWizardIntegercreated_by_wizard是否由向导创建
isModifiedIntegeris_modified是否已修改
attendeeInfoTypeIntegerattendee_info_type参会信息类型
attendeeTypeIntegerattendee_type参会类型(数值)
sequenceIntegersequence排序序号
attendeeCategoryStringattendee_category参会分类(HCP/Pharma/Speaker 等)
lookupEnabledIntegerlookup_enabled是否启用查找
addInRegistrationStatusIntegeradd_in_registration_status添加时的注册状态
typeShorttype0=Primary, 1=Guest
externalLookupRuleIntegerexternal_lookup_rule外部查找规则:0=No lookup, 1=Primary registrant, 2=Secondary registrant

2.1.3 AttendeeResponse (t_attendee_response)

存储参会人员对注册表单中每个字段的响应值。

字段名Java 类型数据库列名说明
attendeeResponseIdIntegerattendee_response_id自增主键 (s_attendee_response)
attendeeIdStringattendee_id关联 Attendee
siteTemplateFieldIdIntegersite_template_field_id关联 SiteTemplateField
responseStringresponse响应值
meetingIdIntegermeeting_id关联 Meeting
attendeeTypeIdIntegerattendee_type_id关联 AttendeeType
reportFieldIdIntegerreport_field_id报表字段 ID
reportFieldLabelStringreport_field_label报表字段标签
subResponseStringsub_response子响应值(用于 Other 选项等)

2.1.4 AttendeeSignature (t_attendee_signature)

参会人员签名记录。

字段名Java 类型数据库列名说明
idIntegerid自增主键 (s_attendee_signature)
meetingIdIntegermeeting_id关联 Meeting
signTimeDatesign_time签名时间
signatureStringsignature签名字符串
willPayMealBooleanwill_pay_meal是否自付餐费
attendeeIdStringattendee_id关联 Attendee
longitudeDoublelongitude经度
latitudeDoublelatitude纬度
signatureDataObject (JSON)signature_data签名原始数据(JSON)
base64StringStringbase64_string签名 Base64 图片

2.1.5 AttendeeChangeLog (t_attendee_change_log)

参会人员信息变更审计日志。

字段名Java 类型数据库列名说明
idIntegerid自增主键 (s_attendee_change_log)
attendeeIdStringattendee_id关联 Attendee
fieldIdIntegerfield_id变更字段 ID(关联 SiteTemplateField)
oldValueStringold_value旧值
newValueStringnew_value新值
createdAtDatecreated_at变更时间

2.1.6 AttendeeComplianceQA (无 @Table 注解)

参会人员合规问答。

字段名Java 类型数据库列名说明
complianceIdIntegercompliance_id主键
attendeeIdStringattendee_id关联 Attendee
questionStringquestion合规问题
answerStringanswer回答
createdAtDatecreated_at创建时间

2.1.7 AttendeeInfoField (t_attendee_info_field)

参会信息字段定义(元数据)。

字段名Java 类型数据库列名说明
fieldNameStringfield_name字段名(主键)
fieldLabelStringfield_label字段标签
sequenceIntegersequence排序序号

2.1.8 AttendeeSurvey (t_attendee_survey)

参会人员问卷模板。

字段名Java 类型数据库列名说明
idIntegerid自增主键
nameStringname问卷名称
contentObject (JSON)content问卷内容(JSON)
createdAtDatecreated_at创建时间
createdByStringcreated_by创建者
updatedAtDateupdated_at更新时间
updatedByStringupdated_by更新者
statusIntegerstatus状态:0=inactive, 1=active
delFlagIntegerdel_flag删除标记:0=未删除, 1=已删除
activatedAtDateactivated_at激活时间
typeIntegertype类型:0=Attendee Survey, 1=Speaker Survey, 2=Sales Rep Survey
productsObject (JSON)products适用产品/项目类型

2.1.9 AttendeeSurveyResponse (t_attendee_survey_response)

问卷填写响应。

字段名Java 类型数据库列名说明
surveyIdIntegersurvey_id关联 AttendeeSurvey
attendeeIdStringattendee_id关联 Attendee
meetingRequestIdIntegermeeting_request_id关联 MeetingRequest
responseObject (JSON)response响应数据(JSON)
createdAtDatecreated_at创建时间
userIdIntegeruser_id填写用户 ID

2.1.10 RegistrationStatus (t_registration_status)

注册状态字典表。

字段名Java 类型数据库列名说明
idIntegerid自增主键
codeIntegercode状态码
nameStringname状态名称

2.1.11 RegistrationWorkflow (t_registration_workflow)

注册状态转换规则矩阵。

字段名Java 类型数据库列名说明
idIntegerid自增主键
productIdIntegerproduct_id关联 Product
registrationStatusIntegerregistration_status当前注册状态
pendingIntegerpending是否可转为 Pending
invitedIntegerinvited是否可转为 Invited
acceptedIntegeraccepted是否可转为 Accepted
registeredIntegerregistered是否可转为 Registered
declinedIntegerdeclined是否可转为 Declined
cancelledIntegercancelled是否可转为 Cancelled

2.1.12 SleepingRoom (t_sleeping_room)

住宿房间信息。

字段名Java 类型数据库列名说明
sleepingRoomIdIntegersleeping_room_id自增主键
meetingIdIntegermeeting_id关联 Meeting
statusIntegerstatus状态
hotelNameStringhotel_name酒店名称
hotelDescriptionStringhotel_description酒店描述
hotelDescriptionHtmlStringhotel_description_html酒店描述(HTML)
checkInDateFromDatecheck_in_date_from入住日期起
checkInDateToDatecheck_in_date_to入住日期止
checkOutDateFromDatecheck_out_date_from退房日期起
checkOutDateToDatecheck_out_date_to退房日期止
hasSmokingPreferenceIntegerhas_smoking_preference是否有吸烟偏好
hasSpecialRequirementIntegerhas_special_requirement是否有特殊要求
isRunOfHouseIntegeris_run_of_house是否为 Run of House
createdAtDatecreated_at创建时间
updatedAtDateupdated_at更新时间
askForExplanationIntegerask_for_explanation是否要求解释:0=no, 1=yes

2.1.13 SleepingRoomInventory (t_sleeping_room_inventory)

按日期和房型的房间库存。

字段名Java 类型数据库列名说明
sleepingRoomInventoryIdIntegersleeping_room_inventory_id自增主键
sleepingRoomIdIntegersleeping_room_id关联 SleepingRoom
roomCountIntegerroom_count总房间数
registeredCountIntegerregistered_count已预订数
roomTypeIdIntegerroom_type_id关联 RoomType
inventoryDateDateinventory_date库存日期
meetingIdIntegermeeting_id关联 Meeting(冗余)

2.1.14 SiteTemplate (t_site_template)

注册站点模板样式配置。

字段名Java 类型数据库列名说明
attendeeTypeIdIntegerattendee_type_id主键(与 AttendeeType 1:1)
meetingIdIntegermeeting_id关联 Meeting
navLayoutStringnav_layout导航布局
navDirectionStringnav_direction导航方向
navBgColorStringnav_bg_color导航背景色
navTextColorStringnav_text_color导航文字色
bannerImgStringbanner_imgBanner 图片
filedPositionStringfiled_position字段位置
fieldLabelColorStringfield_label_color字段标签颜色
pageBgColorStringpage_bg_color页面背景色
containerBgColorStringcontainer_bg_color容器背景色
headerBgColorStringheader_bg_color头部背景色
headerTextColorStringheader_text_color头部文字色
tabStyleStringtab_styleTab 样式
statusIntegerstatus状态:0=Draft, 1=Active, 2=Suspend, 3=Completed
footerImgStringfooter_imgFooter 图片

2.1.15 SiteTemplateField (t_site_template_field)

注册表单字段定义。

字段名Java 类型数据库列名说明
fieldIdIntegerfield_id自增主键
clientIdStringclient_id客户端 ID
pageIdIntegerpage_id关联 SiteTemplatePage
containerIdStringcontainer_id容器 ID(attendeeInfo 等)
meetingIdIntegermeeting_id关联 Meeting
attendeeTypeIdIntegerattendee_type_id关联 AttendeeType
parentIdIntegerparent_id父字段 ID
fieldLabelStringfield_label字段标签
fieldNameStringfield_name字段名(映射到 Attendee 实体字段)
fieldTypeStringfield_type字段类型(inputBox/radioButton/checkBoxButton/datePicker/dropDownList/image/attendeeInfo/boxHeader/text/sleepingRoom/radioButtonCondition/conditionItem)
fieldValueStringfield_value字段默认值
fieldOrderIntegerfield_order排序序号
itemsStringitems选项列表(用 @-@ 分隔)
imgUrlStringimg_url图片 URL
imgWidthStringimg_width图片宽度
imgPositionStringimg_position图片位置
verticalityBooleanverticality是否纵向布局
requiredBooleanrequired是否必填
formatStringformat格式
rangeBooleanrange是否有范围
minStringmin最小值
maxStringmax最大值
fieldRestrictStringfield_restrict字段限制
regexpStringregexp正则验证
reportFieldIdIntegerreport_field_id报表字段 ID
reportFieldLabelStringreport_field_label报表字段标签
ipadRequiredBooleanipad_requirediPad 端是否必填
registrationSiteEnabledBooleanregistration_site_enabled是否在注册站点启用
salesRepRequiredBooleansales_rep_requiredSales Rep 端是否必填
fieldSubTypeStringfield_sub_type字段子类型

2.1.16 SiteTemplatePage (t_site_template_page)

注册表单页面定义(多步骤表单)。

字段名Java 类型数据库列名说明
pageIdIntegerpage_id自增主键
clientIdStringclient_id客户端 ID
meetingIdIntegermeeting_id关联 Meeting
attendeeTypeIdIntegerattendee_type_id关联 AttendeeType
titleStringtitle页面标题
layoutStringlayout布局
pageOrderIntegerpage_order页面排序

2.1.17 Target (t_target)

客户上传的 HCP 目标列表。数据存储在 target schema 中。

字段名Java 类型数据库列名说明
targetIdLongtarget_id自增主键 (s_target)
externalCodeStringexternal_code外部代码(硬编码 100)
externalIdStringexternal_id外部 ID(调和用的关键标识)
attendeeTypeCodeStringattendee_type_code参会类型代码
currencyCodeStringcurrency_code币种代码
lastNameStringlast_name
firstNameStringfirst_name
companyStringcompany公司
titleStringtitle头衔/职称
inactiveStringinactive是否无效
ytdTotalStringytd_total年初至今总额
middleNameStringmiddle_name中间名
suffixNameStringsuffix_name后缀名
stateLicenseNumberStringstate_license_number州执照号
deaNumberStringdea_numberDEA 号码
licenseStateStringlicense_state执照州
typeTitleStringtype_title专科描述
addressLine1Stringaddress_line_1地址1
addressLine2Stringaddress_line_2地址2
cityStringcity城市
stateStringstate
zipCodeStringzip_code邮编
npiNumberStringnpi_numberNPI 号码
meNumberStringme_numberME 号码
companyIdIntegercompany_id关联公司(制药公司)
custom1-custom20Stringcustom1-custom2020 个自定义字段
productIdIntegerproduct_id关联 Product
statusIntegerstatus状态:0=inactive, 1=active
updatedAtDateupdated_at更新时间
emailStringemail邮箱
affiliationStringaffiliation从属机构

2.2 Table Relationships (ER Diagram - ASCII)

                              +-------------------+
                              |   t_meeting       |
                              | (Meeting/Site)    |
                              +--------+----------+
                                       | 1
                                       |
              +----------+------------+---------+-----------+
              |          |            |         |           |
              | N        | N          | N       | N         | N
     +--------+---+ +----+------+ +--+-------+ +--+------+ +-----+------+
     |t_attendee  | |t_attendee | |t_sleeping| |t_site   | |t_site      |
     |_type       | |           | |_room     | |_template| |_template   |
     +-----+------+ +-----+----+ +----+-----+ +---------+ |_page       |
           | 1             | 1         | 1                  +-----+------+
           |               |           |                          |
           | N             | N         | N                        | N
     +-----+-------+ +----+------+ +--+---------+          +-----+------+
     |t_site       | |t_attendee | |t_sleeping  |          |t_site      |
     |_template    | |_response  | |_room       |          |_template   |
     |_field       | +-----------+ |_inventory  |          |_field      |
     +------+------+               +------------+          +------+-----+
            |                                                     |
            | 1                                                   |
            |                                                     |
     +------+------+                                       +------+-----+
     |t_attendee   |                                       |t_attendee  |
     |_change_log  |                                       |_response   |
     +-------------+                                       +------------+

  t_attendee 关系:
  +--------------+      +-------------------+
  | t_attendee   |----->| t_attendee        | (parentId: Guest->Primary)
  | (Self-Ref)   |      | _signature        |
  +--------------+      +-------------------+
        |
        |-----> t_attendee_response (1:N via attendeeId)
        |-----> t_attendee_change_log (1:N via attendeeId)
        |-----> t_attendee_survey_response (1:N via attendeeId)
        |-----> t_attendee_compliance_qa (1:N via attendeeId)

  外部关联:
  t_attendee.reconciliationId ----> NpiData.npi (when reconciliationType=1)
  t_attendee.reconciliationId ----> t_target.externalId (when reconciliationType=2)
  t_attendee.reconciliationId ----> Salesforce Contact (when reconciliationType=3)

  t_registration_workflow ----> t_product (via productId)
  t_registration_status: 独立字典表
  t_attendee_info_field: 独立元数据表
  t_attendee_survey: 独立配置表

2.3 Data Model Issues

DM-1: Attendee 主键使用 String UUID 而非自增整数

  • 文件:Attendee.java:27@GeneratedValue(generator = "JDBC") 实际由 Java 端 IdGenerator.getUUIDString() 生成
  • 影响:索引效率低、JOIN 性能差、外键约束复杂

DM-2: 状态码使用魔数整数而非枚举列

  • registrationStatus 使用 0/10/20/40/50/60/70 的非连续整数
  • hcpStatus 使用 1/2/3
  • signInStatus 使用 0/1/2/3
  • 这些值仅在 Constants.java 中定义,数据库层面无约束

DM-3: JSON 字段滥用

  • Attendee.virtualProgramAttendeeInfo — JSON 存储虚拟会议参会信息
  • Attendee.registrationSource — JSON 存储 {source, createdBy},应该是两个独立列
  • AttendeeSurvey.content — JSON 存储问卷内容
  • AttendeeSurveyResponse.response — JSON 存储响应数据

DM-4: AttendeeComplianceQA 缺少 @Table 注解

  • 文件:AttendeeComplianceQA.java:8 — 没有 @Table 注解,表名约定不明确

DM-5: SleepingRoomInventory 中 meetingId 冗余

  • 文件:SleepingRoomInventory.java:29-30 — meetingId 已在 SleepingRoom 中存在,此处冗余

DM-6: AttendeeResponse 数据模型设计问题

  • AttendeeResponse 将参会人员的表单填写值作为 EAV(Entity-Attribute-Value)模式存储
  • 导致查询需要多表关联和 pivot,性能差
  • Attendee 实体本身也存储了部分相同数据(firstName, lastName 等),存在数据重复

DM-7: Target 实体有 20 个 custom 字段(custom1-custom20)

  • 典型的 schema-less 反模式,无法进行类型检查和约束

DM-8: 编码风格不一致

  • Attendee.java 使用 Lombok (@Data, @Builder),AttendeeChangeLog.java 使用手写 getter/setter
  • 部分实体使用 GenerationType.IDENTITY,部分使用 generator = "JDBC"

3. Business Flow Analysis

3.1 Core Business Flows

3.1.1 NPI Lookup and Validation Flow

┌─────────────────┐
│ 前端发起 NPI 查询│
│ GET v1/npis     │
└────────┬────────┘
         │ NpiQuery(npi, firstName, lastName, city, state, zip)
         v
┌────────────────────────┐
│ NpiService.listNpis()  │
│ 查询 npi schema 的     │
│ NpiData 表(NPPES 数据)│
└────────┬───────────────┘
         │ 返回分页 Npi 列表
         v
┌────────────────────────────────┐
│ 前端选择 NPI 记录进行调和       │
│ PUT v1/attendees/{id}/reconcile│
└────────┬───────────────────────┘
         │ ReconcileNpiRequest {npi, mergeFields}
         v
┌─────────────────────────────┐
│ AttendeeService              │
│ .reconcileAttendee()         │
│ 1. 查找 attendee             │
│ 2. 设置 reconciliationId     │
│ 3. mergeNpiData():           │
│    - 合并 NPI/License/       │
│      Address/Specialty/      │
│      Credential 等字段       │
│ 4. hcpStatus = Reconciled(2) │
│ 5. reconciliationType = NPI  │
│ 6. 更新 Attendee             │
│ 7. 同步更新 AttendeeResponse │
└─────────────────────────────┘

3.1.2 Target Reconciliation Flow (3 remaining types)

调和类型由 Product.npiTarget 字段决定,当前有 3 种(QIMS type=5 和 MedPro type=6 已移除):

Product.npiTarget 决定调和类型

        ├── 1 (NPI): 查询 npi schema -> NpiData 表
        │           ContactService.getNpis()

        ├── 2 (TARGET): 查询 target schema -> t_target 表
        │              ContactService.getTargets()
        │              按 companyId + productId 过滤

        └── 3 (SALESFORCE): 调用 Salesforce Force API
                           ContactService.getSalesForceResult()
                           通过 ReconcileResolver -> SF ContactResponse

调和操作流程(SalesView 端):
┌──────────────────────────────┐
│ PUT v1/attendees/{id}/       │
│     reconciliation           │
│ ReconcileAttendeeRequest:    │
│   { reconciliationId }       │
└──────────┬───────────────────┘
           v
┌──────────────────────────────┐
│ AttendeeService.reconcile()  │
│ 1. 获取 productConfiguration │
│ 2. 根据 reconciliationType:  │
│    TARGET: 从 Target 表获取  │
│    Contact 信息并合并到      │
│    Attendee (firstName,      │
│    lastName, city, state,    │
│    zipCode, address, phone,  │
│    email, meNumber, npi,     │
│    specialty, credentials,   │
│    licenseNumber, etc.)      │
│    SALESFORCE: 设置          │
│    salesforceContactWhoId    │
│ 3. hcpStatus = Reconciled    │
│ 4. 更新 Attendee + Response  │
└──────────────────────────────┘

批量自动调和:
┌──────────────────────────────┐
│ PUT v1/attendees/reconcile   │
│ ReconcileAttendeesRequest:   │
│   { meetingId,               │
│     attendeeTypeIds,         │
│     mergeFields }            │
└──────────┬───────────────────┘
           v
┌──────────────────────────────┐
│ reconcileAttendees()         │
│ 遍历所有未调和 attendee      │
│ 通过 NPI+Name+City+State    │
│ 搜索 NPI 数据库              │
│ 仅当精确匹配到 1 条时        │
│ 自动合并                     │
└──────────────────────────────┘

3.1.3 Registration Workflow and Status Transitions

注册状态值定义 (Constants.AttendeeRegistrationStatus):
  0  = Pending (Not Yet Invited)
  10 = Invited (No Response)
  20 = Accepted
  40 = Registered
  50 = Canceled
  60 = Declined
  70 = Rep Invited

RegistrationWorkflow 矩阵(按 Product 配置):
  每行表示一个当前状态,列表示可转换到的目标状态(0/1 标志)

状态流转逻辑 (getNextRegistrationStatus):
┌─────────────┐                ┌────────────┐
│  Pending(0) │──邀请邮件──>   │ Invited(10)│
│             │  或 Rep 邀请-->│ RepInvited │
└─────────────┘                │    (70)    │
                               └─────┬──────┘
                      ┌──────────────┼──────────────┐
                      v              v              v
              ┌───────────┐  ┌────────────┐  ┌──────────┐
              │Accepted(20)│  │Declined(60)│  │Canceled  │
              │(按 Product │  │(含 decline │  │  (50)    │
              │ 配置决定)  │  │  reason)   │  └──────────┘
              └──────┬─────┘  └────────────┘
                     v
              ┌──────────────┐
              │Registered(40)│
              │(完成注册)    │
              └──────┬───────┘
                     v
              ┌──────────────┐
              │ Check-In     │
              │ signInStatus │
              │ 0->1 or 3   │
              └──────────────┘

注册来源 (RegistrationSource):
  1=Open Reg, 2=Email Invitation, 3=Sales Rep(Quick Reg),
  4=Planner(Contact Import), 5=Pharmagin Connect(已移除),
  6=Walk-In(Sign-In Attendees), 7=Copy Attendees,
  8=Auto Reg, 9=Sales Rep(Batch Reg), 10=Set Registration Status

3.1.4 Frequency Check Logic

频率检查入口:
  GET v1/registration/frequency-check
  FrequencyCheckRequest: {reconciliationId, meetingRequestId,
                          firstName, lastName, state, attendeeId, attendeeTypeId}

┌─────────────────────────────────┐
│ RegistrationManager             │
│ .frequencyCheck()               │
│                                 │
│ 1. 检查 bypassedFrequencyCheck  │
│    Categories(如 attendee      │
│    category 在白名单中则跳过)  │
│                                 │
│ 2. 有 reconciliationId:         │
│    FrequencyCheckResolver       │
│    .getFrequencyCheckResponse() │
│    (Plus 包提供,按 productId   │
│     和 meetingRequestId 检查    │
│     AttendanceConfiguration)    │
│                                 │
│ 3. 无 reconciliationId:         │
│    FrequencyCheckResolver       │
│    .getAttendanceFrequency      │
│     CheckResponse()             │
│    (按 firstName+lastName+      │
│     state 匹配历史记录)         │
│                                 │
│ 4. 返回 message + 历史记录      │
│    message 非空 = 触发频率限制  │
│                                 │
│ 5. 发送通知邮件给 Planner       │
│    (FrequencyCheckReminder 配置)│
└─────────────────────────────────┘

Noven 特殊频率检查 (NovenAttendanceFrequencyCheckService):
  - 独立于 Plus 包的实现
  - 按 AttendanceConfig 配置:
    - frequencyPeriodType=0: 按月数范围
    - frequencyPeriodType=1: 按 FiscalYear 范围
  - 支持按 Topic 过滤 (topicEnabled)
  - 查询 t_noven_attendee_history 表
  - 发送 noven-frequency-check-notification 邮件模板

3.1.5 Sleeping Room Management

┌─────────────────────┐    ┌──────────────────────┐
│ SleepingRoom CRUD   │    │ SleepingRoomInventory │
│ v1/sleepingrooms    │    │ (按日期+房型的库存)   │
│                     │    │                       │
│ - hotelName         │--->│ - roomCount           │
│ - checkIn/Out dates │    │ - registeredCount     │
│ - smokingPref       │    │ - roomTypeId          │
│ - specialReq        │    │ - inventoryDate       │
└────────┬────────────┘    └───────────────────────┘

         │ 注册时选择住宿
         v
┌────────────────────────────────────┐
│ saveAttendeeSleepingRoomResponse() │
│ 1. 遍历 SleepingRoom fields       │
│ 2. 保存 MappingSleepingRoomContact│
│    (attendeeId + sleepingRoomId +  │
│     checkIn/Out + roomType +       │
│     smoking + specialReq)          │
│ 3. 更新 SleepingRoomInventory     │
│    registeredCount + 1             │
│ 4. 取消时 registeredCount - 1     │
└────────────────────────────────────┘

下载报表:
  Sheet1: Rooming List (按入住信息列表)
  Sheet2: Pickup Report (按日期汇总)
  Sheet3: Attendees No Reservation (未预订的参会人员)

3.2 Validation Rules

验证规则位置说明
最大参会人数AttendeeService.java:585-611meeting.expectedAttendees 非空时,已注册(Accepted + Registered)人数 + guest 数不能超过上限
注册条件检查AttendeeService.java:567-583Meeting、AttendeeType、SiteTemplate、Fields 必须都存在
导入查重AttendeeService.java:1226-1229按 firstName.toUpperCase() + lastName.toUpperCase() + email 判断是否重复
频率检查RegistrationService.java:234-254新注册时调用 frequencyCheck(),超频则抛出 FORBIDDEN
频率检查可跳过RegistrationManager.java:75-88attendeeCategory 在 bypassedFrequencyCheckCategories 白名单中时跳过
FirstName/LastName 必填AttendeeService.java:1222-1224导入联系人时必须提供
Attendee Type 列必填AttendeeService.java:1004-1005Excel 上传时 "Attendee Type" 列必须存在
邮件取消订阅检查RegistrationService.java:390-394发送邀请前检查邮箱是否在 unsubscribed 列表中

3.3 Business Logic Issues

BL-1: AttendeeService 文件超过 2100 行(God Class)

  • 文件:AttendeeService.java — 集中了注册、调和、导入、签到、TOV 等所有逻辑
  • 违反单一职责原则

BL-2: 注册逻辑分散在两个模块中

  • modules/v1/site/controller/AttendeeController.java 中有 POST v1/attendees/registrationPUT v1/attendees/registration/{id}
  • modules/v1/registration/controller/RegistrationController.java 中有 POST v1/registration/{attendeeTypeId}
  • 两条路径最终都调用 AttendeeService.saveRegistration(),但入参封装和验证逻辑不同
  • SalesView 使用 registration 模块,PlannView 的 OpenReg 使用 site 模块

BL-3: reconcile 方法有 3 个不同入口,逻辑重复

  • reconcileAttendee() (line 1692) — PlannView NPI 调和
  • reconcile() (line 1703) — SalesView Target/SF 调和
  • reconcileAttendees() (line 1552) — 批量自动调和
  • 三者的数据合并逻辑各自独立实现

BL-4: NpiService.getReconciliationNpiMap 有 BUG

  • 文件:NpiService.java:69 — 过滤条件使用 attendee.getAttendeeTypeId() 而不是 attendee.getReconciliationType()
  • TargetService.getReconciliationTargetMap 也有同样的 bug(line 61)
  • 这导致只有 attendeeTypeId 恰好等于 ReconciliationType 值时才能匹配

BL-5: getNextRegistrationStatus 逻辑复杂且分散

  • 文件:AttendeeService.java:614-637
  • 依赖 Product 配置来决定 Invited 状态的下一状态
  • 缺少状态机的清晰定义

BL-6: saveAttendeeResponse 每次先删除再全量插入

  • 文件:AttendeeService.java:725-746
  • delete 所有 response 再循环 insertSelective,无事务保护
  • 性能差且有数据不一致风险

BL-7: RegistrationSource.PHARMAGIN_CONNECT (value=5) 仍在枚举中

  • 文件:RegistrationSource.java:14 — PharmaginConnect 已被移除但枚举值保留
  • 应标记为 @Deprecated 或删除

BL-8: 频率检查有两套独立实现

  • NovenAttendanceFrequencyCheckService — Noven 客户特定实现,查询 t_noven_attendee_history
  • FrequencyCheckResolver (Plus 包) — 通用实现
  • 没有统一的接口抽象

4. API Inventory

4.1 REST Endpoints Table

4.1.1 AttendeeController (v1/attendees)

MethodPathParametersRequest BodyResponseDescription
GET/v1/attendeesAttendeeQuery(meetingId, keyword, etc.)-PageResult<AttendeesResponse>参会人员分页列表
GET/v1/attendees/{attendeeId}attendeeId (path)-Attendee获取单个参会人员
GET/v1/attendees/registrationsAttendeeRegistrationQuery-PageResult<AttendeeRegistration>参会人员注册列表
PUT/v1/attendees/{id}id (path)AttendeeDTOAttendeeDTO更新参会人员
DELETE/v1/attendees/{id}id (path)-void (204)删除单个参会人员
DELETE/v1/attendees-DeleteAttendeesRequestvoid (204)批量删除参会人员
PUT/v1/attendees/signinstatus-UpdateSignInStatusRequestvoid批量签到/取消签到
PUT/v1/attendees/attendance-format-UpdateAttendanceFormatRequestvoid更新参会格式
PUT/v1/attendees/categories-UpdateCategoryRequestvoid批量标记分类
GET/v1/attendees/registrationmeetingId, attendeeTypeId, openRegistration?, attendeeId?-SiteTemplateDTO获取注册表单模板
GET/v1/attendees/confirmationattendeeId, openRegistration?-SiteTemplateDTO获取确认页信息
POST/v1/attendees/registration-Registration (201)Registration保存注册
PUT/v1/attendees/registration/{id}id (path)RegistrationObject更新注册
DELETE/v1/attendees/registration/{id}id (path), sendMail (query)-void (204)取消注册
GET/v1/attendees/registration/{id}/declineid (path)-DeclinedRegistration获取拒绝信息
PUT/v1/attendees/registration/{id}/declineid (path)DeclineRegistrationRequestvoid拒绝注册
POST/v1/attendees/uploadfile (multipart), meetingId-UploadAttendeeResponse上传 Excel/CSV
POST/v1/attendees/import-ImportAttendeeRequestImportAttendeeResponse导入联系人
PUT/v1/attendees/{attendeeId}/reconcileattendeeId (path)ReconcileNpiRequestvoidNPI 调和
PUT/v1/attendees/{attendeeId}/merge-npi-dataattendeeId (path)MergeNpiDataRequestvoid合并 NPI 数据
PUT/v1/attendees/{attendeeId}/reconciliationattendeeId (path)ReconcileAttendeeRequestvoidSalesView 调和
PUT/v1/attendees/{attendeeId}/unreconcileattendeeId (path)-void取消调和
PUT/v1/attendees/{attendeeId}/registration-statusattendeeId (path)SetRegistrationStatusRequestvoid设置注册状态
PUT/v1/attendees/reconcile-ReconcileAttendeesRequestInteger批量自动调和
PUT/v1/attendees/foodbeverage-FoodAndBeverageRequestvoid批量更新餐饮
PUT/v1/attendees/{id}/foodbeverageid (path)FoodAndBeverageRequestvoid更新单个餐饮
GET/v1/attendees/summarymeetingId (query)-AttendeeReport汇总报告
POST/v1/attendees/copy-CopyAttendeesRequestvoid跨 Meeting 复制参会人员
GET/v1/attendees/unsynchronized-attendeesmeetingId (query)-List<UnsynchronizedAttendeesResponse>未同步到 SF 的参会人员
PUT/v1/attendees/update-registration-in-salesforce-List<String> (attendeeIds)void同步注册到 SF
POST/v1/attendees/{attendeeId}/add-to-salesforceattendeeId (path)-void添加到 SF

4.1.2 AttendeeSignatureController (v1/attendees)

MethodPathParametersRequest BodyResponseDescription
GET/v1/attendees/signature/declaimer/{programTypeId}programTypeId (path)-Map<String, String>获取签名免责声明
GET/v1/attendees/{attendeeId}/signatureattendeeId (path)-AttendeeSignature获取签名
POST/v1/attendees/{attendeeId}/signatureattendeeId (path)SignatureDTOvoid保存签名
POST/v1/attendees/{attendeeId}/checkedInattendeeId (path)SignatureDTOvoidCheck-In
POST/v1/attendees/{attendeeId}/noShowattendeeId (path)-void标记 No Show

4.1.3 AttendeeTovController (v1/attendees)

MethodPathParametersRequest BodyResponseDescription
GET/v1/attendees/tovAttendeeTovQuery-AttendeeTovResponseTOV 列表
GET/v1/attendees/tov/downloadmeetingRequestId (query)-Excel 下载下载 TOV
PUT/v1/attendees/tov/allocation-AllocateTovRequestAttendeeTovResponseTOV 分配
PUT/v1/attendees/tov-UpdateAttendeeTovRequestAttendeeTovResponse批量更新 TOV
PUT/v1/attendees/{attendeeId}/tovattendeeId (path)UpdateAttendeeTovRequestvoid更新单个 TOV
POST/v1/attendees/{attendeeId}/transfer-of-valueattendeeId (path)List<AddTransferOfValueRequest>void添加 TOV

4.1.4 AttendeeTypeController (v1/attendeetypes)

MethodPathParametersRequest BodyResponseDescription
GET/v1/attendeetypesmeetingId (query)-List<AttendeeTypeDTO>AttendeeType 列表
POST/v1/attendeetypes-AttendeeTypeDTOMeetingDTO创建 AttendeeType
PUT/v1/attendeetypes/{id}id (path)AttendeeTypevoid更新 AttendeeType
PUT/v1/attendeetypes/{attendeeTypeId}/external-lookup-ruleattendeeTypeId (path)SetExternalLookupRuleRequestvoid设置外部查找规则
DELETE/v1/attendeetypes/{id}id (path)-void (204)删除 AttendeeType
PUT/v1/attendeetypes/sequences-List<AttendeeTypeSequence>void排序

4.1.5 RegistrationController (v1/registration)

MethodPathParametersRequest BodyResponseDescription
GET/v1/registration/{attendeeTypeId}attendeeTypeId (path), attendeeId? (query)-RegistrationResponse获取注册表单
POST/v1/registration/{attendeeTypeId}attendeeTypeId (path)SaveRegistrationRequestRegistrationSalesView 保存注册
POST/v1/registration/{attendeeTypeId}/batch-registrationattendeeTypeId (path)SaveBatchRegistrationRequestSaveBatchRegistrationResponse批量注册
GET/v1/registration/attendeesmeetingRequestId, lastName, state-List<Attendee>查询参会人员(频率检查用)
GET/v1/registration/attendees/{attendeeId}/program-invitationattendeeId (path)-ProgramInvitationResponse获取邀请内容
POST/v1/registration/attendees/{attendeeId}/program-invitationattendeeId (path)SendProgramInvitationRequestvoid发送邀请
GET/v1/registration/attendees/{attendeeId}/email-invitationattendeeId (path)-EmailInvitationResponse获取邮件邀请
POST/v1/registration/attendees/{attendeeId}/email-invitationattendeeId (path)SendEmailInvitationRequestvoid发送邮件邀请
POST/v1/registration/attendees/email-invitation-SendMultipleEmailInvitationRequestSendMultipleEmailInvitationResponse批量发送邮件邀请
GET/v1/registration/attendees/{attendeeId}/email-invitation/{id}/openattendeeId, id (path)-void邀请打开跟踪
GET/v1/registration/attendees/{attendeeId}/email-invitation/{id}/accessattendeeId, id (path)-void邀请访问跟踪
GET/v1/registration/attendees/{attendeeId}/acceptattendeeId (path)-String接受注册
GET/v1/registration/attendees/{attendeeId}/declineattendeeId (path)-String拒绝注册
GET/v1/registration/frequency-checkFrequencyCheckRequest-AttendanceFrequencyCheckResponse频率检查
GET/v1/registration/quick-email-invitationmeetingRequestId, attendeeId?-EmailInvitationResponse快速邮件邀请
POST/v1/registration/quick-email-invitation-SendQuickEmailInvitationRequestvoid发送快速邮件邀请

4.1.6 SiteController (v1/sites)

MethodPathParametersRequest BodyResponseDescription
GET/v1/sitesSiteQuery-PageResult<ListSitesResponse>注册站点列表
POST/v1/sites-MeetingDTO (201)MeetingDTO创建站点
GET/v1/sites/{id}id (path)-MeetingDTO获取站点
PUT/v1/sites/{id}id (path)MeetingDTOMeetingDTO更新站点
DELETE/v1/sites/{id}id (path)-void (204)删除站点
POST/v1/sites/{id}/copyid (path), meetingName (query)-MeetingDTO (201)复制站点
GET/v1/sites/{meetingId}/templatesmeetingId (path)-List<Integer>获取模板 ProgramType
PUT/v1/sites/{id}/templatesid (path), programTypeIds (query)-void设置模板
GET/v1/sites/{meetingId}/urlmeetingId (path)-SiteUrlDTO获取站点 URL
PUT/v1/sites/{meetingId}/urlmeetingId (path)SiteUrlDTOSiteUrlDTO更新站点 URL
PUT/v1/sites/{meetingId}/policymeetingId (path)PolicyPolicy更新策略
GET/v1/sites/{meetingId}/policymeetingId (path)-Policy获取策略
GET/v1/sites/reg/{url}url (path)-MeetingDTO通过自定义 URL 获取站点
GET/v1/sites/{meetingId}/shared-usersmeetingId (path)-List<Integer>获取共享用户
POST/v1/sites/{meetingId}/shared-usersmeetingId (path)List<Integer>void添加共享用户
PUT/v1/sites/{meetingId}/activatemeetingId (path)ActivateRequestMeetingDTO激活注册站点
PUT/v1/sites/{meetingId}/suspendmeetingId (path)-MeetingDTO暂停注册站点

4.1.7 SleepingRoomController (v1/sleepingrooms)

MethodPathParametersRequest BodyResponseDescription
GET/v1/sleepingroomsmeetingId (query)-List<ListSleepingRoomsResponse>住宿列表
GET/v1/sleepingrooms/{sleepingRoomId}sleepingRoomId (path)-GetSleepingRoomResponse获取住宿详情
POST/v1/sleepingrooms-CreateSleepingRoomRequest (201)GetSleepingRoomResponse创建住宿
PUT/v1/sleepingrooms/{sleepingRoomId}sleepingRoomId (path)CreateSleepingRoomRequestGetSleepingRoomResponse更新住宿
DELETE/v1/sleepingrooms/{sleepingRoomId}sleepingRoomId (path)-void (204)删除住宿
GET/v1/sleepingrooms/{sleepingRoomId}/downloadsleepingRoomId (path)-Excel下载住宿报表

4.1.8 其他相关 Controller

MethodPathControllerDescription
GET/v1/npisNpiControllerNPI 查询
GET/v1/targetsTargetControllerTarget 列表查询
GET/v1/contactsContactController统一联系人查询(根据 reconciliationType 路由到 NPI/Target/SF)
GET/v1/contacts/npisContactControllerNPI 联系人查询
GET/v1/public/contactsContactController公共联系人查询(无需认证)
GET/v1/registrationworkflowsRegistrationWorkflowController获取注册工作流配置
PUT/v1/registrationworkflowsRegistrationWorkflowController更新注册工作流配置
GET/v1/attendees/changelogAttendeeChangeLogController变更日志
GET/v1/attendees/downloadAttendeeDownloadController下载参会列表

4.2 API Design Issues

API-1: URL 路径命名不一致

  • /v1/attendees/registration (site 模块) vs /v1/registration/{attendeeTypeId} (registration 模块) — 两个功能重叠的注册端点
  • /v1/attendees/{id}/reconcile vs /v1/attendees/{id}/reconciliation — 相似操作不同命名

API-2: HTTP 方法使用不当

  • GET /v1/registration/attendees/{attendeeId}/accept — 接受注册应该使用 POST/PUT 而非 GET(有副作用)
  • GET /v1/registration/attendees/{attendeeId}/decline — 同上

API-3: AttendeeSignatureController 和 AttendeeTovController 共用 v1/attendees 路径

  • 三个 Controller 映射到同一前缀 v1/attendees,容易冲突
  • 应拆分为独立的 URL namespace

API-4: SiteController 中存在 SQL 注入风险

  • 文件:SiteController.java:168andCondition("lower(customized_sub_domain) = '" + url.toLowerCase() + "'")
  • 直接拼接用户输入到 SQL 条件中

API-5: DELETE /v1/attendees 使用 RequestBody

  • HTTP DELETE 请求带 body 在部分 HTTP 客户端中不被支持
  • 应改为 query parameter 或使用 POST

API-6: 缺少版本化和统一的错误响应格式

  • 虽然使用了 v1 前缀,但没有其他版本
  • 错误通过 ServiceException 抛出,但缺少统一的错误响应 DTO

5. Frontend Analysis

5.1 Pages & Components

Plannerview (pharmagin-plannerview/legacy/src/)

路径/组件说明
containers/Sitebuilder/index.jsSite Builder 主容器 — 注册站点可视化配置
containers/Sitebuilder/registration.js注册表单渲染
containers/Sitebuilder/confirmation.js注册确认页
containers/Sitebuilder/openRegistration.js公开注册入口
containers/Sitebuilder/invitation.js邀请注册入口
containers/Sitebuilder/declineForm.js拒绝注册表单
containers/Sitebuilder/guestRegistration.jsGuest 注册
containers/Sitebuilder/LookupRegistrant.js注册人查找
containers/Sitebuilder/unavailable.js注册站点不可用页
containers/Sitebuilder/reducer.jsSitebuilder Redux reducer
containers/Sitebuilder/sagas.jsSitebuilder Redux sagas
components/RegReport/index.js参会人员报告主组件
components/RegReport/LookUpNPI.jsNPI 查找对话框
components/RegReport/ReconcileNPI.jsNPI 调和对话框
components/RegReport/ReconcileAll.js批量自动调和
components/RegReport/MergeNPIDataForm.jsNPI 数据合并表单
components/RegReport/TargetList.jsTarget 列表查找
components/RegReport/TagSelect.js标签选择(分类)
components/RegReport/AddTransferOfValueModal.jsTOV 添加弹窗
components/RegReport/Download.js下载参会列表
components/RegReport/EditNote.js编辑备注
components/RegReport/InvitationTrackReport.js邀请跟踪报告
components/RegReport/SendSurvey.js发送调查
components/RegSiteBuilder/index.js注册站点配置组件
components/RegAttendeeType/index.jsAttendeeType 管理组件
components/RegAttendeeType/AttendeeCategorySelect.js分类选择器
components/RegistrationWorkflow/index.js注册工作流配置
components/RegMeetingForm/index.jsMeeting 表单
路由 /registrations注册列表页
路由 /registrations/:id(/:type)注册详情页
路由 /sitebuilderSite Builder 配置页
路由 /confirmation/:attendeeId(/:planner)确认页
路由 /invitations/:attendeeId(/:planner)邀请页
路由 /decline/:attendeeId拒绝页
路由 /unsubscribe/:attendeeId退订页
路由 /resubscribe/:attendeeId重新订阅页

Salesview (pharmagin-salesview/src/)

路径/组件说明
pages/Programs/Profile/Attendees/index.js项目详情中的参会人员 Tab
pages/Programs/Profile/Attendees/Filters.js参会人员筛选器
pages/Programs/Profile/Attendees/InvitationTrackReport.js邀请跟踪
pages/Programs/Profile/Attendees/surveyType.js调查类型选择
pages/Programs/Profile/actions.js包含参会人员相关 Redux actions
pages/Programs/Profile/saga.js包含参会人员相关 sagas
pages/Programs/Profile/reducer.js包含参会人员相关 state
pages/Reports/Survey/调查报告

Speakerview (pharmagin-speakerview/legacy/src/)

路径/组件说明
containers/SpeakerDetail/sagas.js包含少量参会人员相关的数据获取逻辑(Speaker 查看自己参会的 meeting 信息)

5.2 Redux State Structure

Plannerview:

state = {
  sitebuilder: {            // containers/Sitebuilder/reducer.js
    template: {},           // SiteTemplateDTO
    loading: false,
    registration: {},       // 注册表单数据
    confirmation: {},       // 确认页数据
    registrationStatus: null,
    signInStatus: null,
    ...
  },
  registrations: {},        // 注册列表
  registrationsDetail: {},  // 注册详情(Meeting + Attendees)
  siteBuilderConfig: {},    // components/RegSiteBuilder/reducer.js
  attendeeTypes: {},        // components/RegAttendeeType/reducer.js
  registrationWorkflow: {}, // components/RegistrationWorkflow/reducer.js
}

Salesview:

state = {
  programProfile: {          // pages/Programs/Profile/reducer.js
    attendees: [],           // 参会人员列表
    attendeeSummary: {},     // 参会人员汇总
    registrationConfig: {},  // 注册配置
    ...
  }
}

5.3 Frontend Issues

FE-1: Sitebuilder 组件使用 React Router v3 和 Redux 旧模式

  • containers/Sitebuilder/ 使用 injectReducer 动态注入,代码分割方式过时
  • React 15.x/16.x 混用

FE-2: 前端注册逻辑与后端强耦合

  • Sitebuilder 的 registration.js 直接映射后端 SiteTemplateDTO 结构
  • 字段验证逻辑在前端重新实现一遍
  • 表单渲染完全依赖后端返回的字段定义

FE-3: Plannerview 和 Salesview 的参会人员管理界面代码重复

  • 两个前端应用都实现了参会人员列表、NPI 查找、调和等功能
  • 没有共享组件库

FE-4: Speakerview 几乎没有参会人员相关界面

  • Speaker 端无法查看自己的注册状态详情
  • 仅在 sagas 中有少量数据获取逻辑

6. Problem Summary

6.1 Critical Issues (must fix in rewrite)

ID问题影响文件位置
C-1AttendeeService 超过 2100 行 God Class可维护性极差,难以测试和修改site/service/AttendeeService.java
C-2注册逻辑双入口重复site 模块和 registration 模块有重叠的注册功能,维护困难site/controller/AttendeeController.java:178 vs registration/controller/RegistrationController.java:51
C-3SQL 注入漏洞SiteController 直接拼接用户输入到 SQLsite/controller/SiteController.java:168
C-4NpiService/TargetService 过滤条件 BUG使用 attendeeTypeId 代替 reconciliationType 进行过滤NpiService.java:69, TargetService.java:61
C-5saveAttendeeResponse 先全删再插入无事务保护并发操作可能导致数据丢失AttendeeService.java:725-746
C-6Accept/Decline 使用 GET 方法违反 HTTP 语义,GET 不应有副作用,且可被爬虫/预加载触发RegistrationController.java:103-109

6.2 Design Defects (should improve)

ID问题影响文件位置
D-1EAV 模式存储表单响应AttendeeResponse 表成为数据瓶颈,查询需要 pivotentity/AttendeeResponse.java
D-2三套独立的 reconcile 实现代码重复、合并逻辑不一致AttendeeService.java:1552,1692,1703
D-3频率检查双实现Noven 特殊实现 + Plus 包通用实现,缺少统一接口NovenAttendanceFrequencyCheckService.java + Plus FrequencyCheckResolver
D-4状态码使用魔数registrationStatus(0/10/20/40/50/60/70) 无数据库约束Constants.java:69-148
D-5三个 Controller 共用 /v1/attendees 路径URL 冲突风险AttendeeController, AttendeeSignatureController, AttendeeTovController
D-6JSON 字段滥用registrationSource 应为结构化列,virtualProgramAttendeeInfo 应为独立表Attendee.java:207-215
D-7RegistrationWorkflow 矩阵设计状态转换规则存储为扁平列(pending/invited/accepted/...),不易扩展entity/RegistrationWorkflow.java
D-8Target 表 20 个 custom 字段schema-less 反模式,无类型安全entity/Target.java:169-227
D-9Attendee 与 AttendeeResponse 数据冗余firstName/lastName/email 等在 Attendee 表和 AttendeeResponse 中都存储AttendeeService.java:1628-1681

6.3 Technical Debt (nice to have)

ID问题影响文件位置
T-1编码风格不一致部分 Entity 用 Lombok,部分手写 getter/setterAttendeeChangeLog.java vs Attendee.java
T-2AttendeeComplianceQA 缺少 @Table 注解表名映射不明确AttendeeComplianceQA.java:8
T-3RegistrationSource.PHARMAGIN_CONNECT 遗留已移除功能的枚举值未清理RegistrationSource.java:14
T-4pvExtUserId 遗留字段Attendee 中的 PV 外部用户 ID,PharmaginConnect 已移除Attendee.java:195
T-5UUID 主键性能问题Attendee 使用 String UUID 作为主键Attendee.java:27
T-6Excel 解析逻辑嵌入 Service 层parseExcel 方法应提取为独立工具类AttendeeService.java:958-1063
T-7前端 React 版本过旧React 15.x-16.x,应升级到 18.xpharmagin-plannerview/legacy/
T-8前端无共享组件库PlannerView 和 SalesView 的参会人员组件独立开发两个前端项目
T-9硬编码邮件主题"Repeat Attendee Eligibility Verification Against 2024 Noven SP Attendees"NovenAttendanceFrequencyCheckService.java:98
T-10DELETE 带 RequestBodyHTTP DELETE 带 body 不符合部分 HTTP 规范AttendeeController.java:112-115

7. Rewrite Recommendations

7.1 Domain 拆分建议

将当前的 God Class AttendeeService 拆分为以下清晰的服务:

新服务职责当前代码来源
AttendeeServiceAttendee CRUD、查询、分页保留核心 CRUD
RegistrationService注册表单获取、提交、状态流转合并 site + registration 模块
ReconciliationServiceNPI/Target/SF 调和统一入口3 个 reconcile 方法合并
FrequencyCheckService频率检查统一接口Noven 实现 + Plus 实现统一
CheckInService签到、签名、No Showcheck-in 相关逻辑
AttendeeImportServiceExcel/CSV 解析和导入parseExcel + importAttendee
TransferOfValueServiceTOV 管理(已独立为 AttendeeTovService)保持
SleepingRoomService住宿管理(已独立)保持

7.2 数据模型改进

  1. Attendee 主键改为 Long 自增,保留 UUID 作为公开标识符(externalId)
  2. registrationStatus/hcpStatus/signInStatus 改为 PostgreSQL ENUM 类型,增加数据库层约束
  3. registrationSource 拆分为 registration_source_type (ENUM) + registered_by (VARCHAR) 两个结构化列
  4. 评估是否保留 EAV 模式:考虑将核心字段固化到 Attendee 表,仅自定义字段保留 EAV
  5. 引入状态机框架(如 Spring Statemachine)管理注册状态转换
  6. Target 的 custom 字段改为 JSONB,利用 PostgreSQL 的 JSONB 索引能力

7.3 API 改进

  1. 统一注册入口:合并 v1/attendees/registrationv1/registration/{attendeeTypeId} 为一个端点
  2. Accept/Decline 改为 POST/PUT 方法
  3. 拆分 Controller URL namespace:签名 -> v1/signatures, TOV -> v1/transfer-of-value
  4. 修复 SQL 注入:使用参数化查询
  5. 添加 API 版本策略:为 v2 重写准备
  6. 统一错误响应格式:定义标准的 ErrorResponse DTO

7.4 前端改进

  1. 建立共享组件库:NPI 查找、调和、参会人员列表等通用组件
  2. 升级 React 到 18.x,使用 hooks 替代 class 组件
  3. 用 React Query 或 SWR 替代 Redux-Saga 进行数据获取
  4. 统一前端表单验证:与后端共享验证规则 schema
  5. SpeakerView 增加参会信息查看功能:让 Speaker 可以查看注册确认详情