Skip to content

Speaker Management Domain - Deep Dive Analysis

1. Domain Overview

1.1 领域职责描述

Speaker Management 是整个平台最核心的业务领域,负责管理制药行业演讲者(Speaker)的完整生命周期。主要职责包括:

  1. Speaker Profile 管理:演讲者基本信息、联系方式、医疗执照、NPI 注册信息、W9 税务信息、助理信息等
  2. 合同管理 (Contract):演讲者服务合同的创建、签署、续签、报酬上限管理
  3. 培训管理 (Training):演讲者 Topic 培训状态跟踪与合规检查
  4. 内容/演示文稿管理 (Content/Presentation):Core 演示文稿、Module/Case Study 的管理,以及 Speaker 的个人演示文稿组合
  5. 费用报销 (Expense):演讲者参加会议的费用申报、审批流程
  6. Speaker Group 管理:按用户组对演讲者进行分组,用于内容可见性控制和筛选
  7. 提名管理 (Nomination):演讲者候选人的提名与审核流程
  8. Topic 管理:演讲主题的 CRUD 及与演讲者、内容的关联
  9. Document 管理:演讲者相关文档(W9 表格、合同附件等)的上传与管理
  10. Alert 通知:培训、演示文稿下载、费用、调查问卷等事件的通知
  11. Speaker 登录/账户管理:演讲者账户的创建、密码重置、自助注册

1.2 涉及的后端模块和包

核心模块路径

  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/speaker/ - Speaker 模块主目录
    • controller/ - 12 个 Controller(SpeakerController, SpeakerContractController, SecureSpeakerContractController, SpeakerPresentationController, SpeakerMeetingController, ContentController, TopicController, SpeakerGroupController, ExpenseController, ExpenseItemController, ExpenseCategoryController, AlertController, DocumentController, TrainingController)
    • service/ - 16 个 Service
    • model/ - 约 70 个 DTO/Model 类
    • query/ - 9 个 Query 类
    • request/ - 7 个 Request 类
    • response/ - 若干 Response 类
    • constant/ - 10 个枚举常量类
    • manager/ - SpeakerManager

Entity 包路径

  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/

2. Data Model Analysis

2.1 Entity Overview Table

2.1.1 Speaker (t_speaker) - 主表,88 个字段

字段名类型注解说明
speakerIdInteger@Id, @GeneratedValue主键,自增序列 s_speaker
firstNameString
lastNameString
speakerNameString全名(冗余)
middleNameString中间名
suffixString后缀(如 MD, PhD)
titleString职称
resumeString简历
imagePathString头像文件路径
videoPathString视频文件路径
emailAddressString电子邮箱
phoneString办公电话
mobilePhoneString手机
faxString传真
street1String办公地址1
street2String办公地址2
cityString城市
stateString
zipString邮编
homeAddress1String家庭地址1
homeAddress2String家庭地址2
homeCityString家庭城市
homeStateString家庭州
homeZipString家庭邮编
w9Address1StringW9 税务地址1
w9Address2StringW9 税务地址2
w9CityStringW9 城市
w9StateStringW9 州
w9ZipCodeStringW9 邮编
npiRegistryAddress1StringNPI 注册地址1
npiRegistryAddress2StringNPI 注册地址2
npiRegistryCityStringNPI 注册城市
npiRegistryStateStringNPI 注册州
npiRegistryZipStringNPI 注册邮编
honorariaCapBigDecimal报酬上限(已移至合同,冗余)
travelFeeBigDecimal差旅费
webcastHonorariaBigDecimal网络研讨会报酬
localHonorariaBigDecimal本地报酬
contractStartDateDate合同开始日期(冗余,已移至合同)
contractExpirationDateDate合同到期日期(冗余)
contractTermString合同期限
contractHistoryString合同历史
adminContractString管理员合同
statusInteger状态:0=Inactive, 1=Active, 2=W9 Pending, 3=Contract Creation Pending, 4=Contract Signature Pending, 5=Expired
approvedInteger审批状态
deletedInteger软删除标记
speakerTypeInteger演讲者类型
speakerLevelString演讲者级别
speakerOrderInteger排序号
paymentString支付方式
companyIdInteger所属公司 ID
companyString公司名(冗余)
regionIdInteger区域 ID
districtIdInteger地区 ID
npiStringNPI 号码
ssnTaxIdStringSSN/Tax ID
w9YearStringW9 年份
bieStringBIE 标识
usNumberStringUS 编号
stateMedicalLicenseString州医疗执照
additionalMedicalLicenseString附加医疗执照
stateOfMedicalLicenseString执照所在州(与 licenseState 冗余)
licenseStateString执照州
stateLicenseExpDateString执照到期日期(类型应为 Date)
boardCertificationString董事会认证
expertiseString专业领域
specialtyString专科
institutionAffiliationString机构附属关系
affiliationListingString附属列表
businessNameString企业名称
websiteString个人网站
publicationsString出版物
travelPreferenceString差旅偏好
globalEntryStringGlobal Entry 编号
classificationString分类
audienceTypeString受众类型
notesString备注
meString用途不明
wkString用途不明
wdeaString用途不明
otherString其他信息
nominationStatusInteger提名状态
nominationNoteString提名备注(枚举值存在 String 中)
createdByInteger创建者
passwordString密码(明文存储,已废弃)
passwordQuestionString密码提示问题(已废弃)
passwordAnswerString密码提示答案(已废弃)
resetPasswordString重置密码 token
resetTimeDate重置密码时间
speakernameString用户名(与 speakerName 冗余,小写命名)
hideFromSpeakerSearchBoolean是否在搜索中隐藏
ambassadorTypeString大使类型
birthdayDate生日
maritalStatusString婚姻状态
employmentStatusString就业状态
primaryLanguageString主要语言
otherLanguageString其他语言
categoryString分类(PATIENT/CAREGIVER)
caregiverString护理者信息
physicianCareString医生护理信息
programSourceString项目来源
travelCompanionRequiredInteger是否需要旅伴(1/0 应为 Boolean)
handicappedAccessibleInteger是否需要无障碍设施(1/0 应为 Boolean)
connectedToSourceInteger是否连接到来源(1/0 应为 Boolean)
nurseNameString护士姓名
homeMedicalString家庭医疗信息
diagnosisDateDate诊断日期
treatmentStartDateDate治疗开始日期
indicationTypeString适应症类型
internalAreaContactString内部区域联系人
requiredNoticeString所需通知
unavailableDaysString不可用天数
assistantNameString助理姓名
assistantEmailString助理邮箱
assistantPhoneString助理电话
geogString地理坐标(PostGIS geography 类型)

2.1.2 SpeakerContract (t_speaker_contract) - 合同表,26 个字段

字段名类型说明
contractIdInteger主键
contractNumberString合同编号
startDateDate开始日期
endDateDate结束日期
descriptionString描述
honorariaCapBigDecimal报酬上限
travelHonorariaBigDecimal差旅报酬
webcastHonorariaBigDecimal网络研讨会报酬
localHonorariaBigDecimal本地报酬
speakerIdInteger关联 Speaker
statusInteger状态:0=未签署, 1=已签署
deletedInteger软删除
extendedTravelHonorariaBigDecimal长途差旅报酬
localAdvisoryBoardHonorariaBigDecimal本地顾问委员会报酬
localTravelHonorariaBigDecimal本地差旅报酬
dinnerMeetingHonorariaBigDecimal晚宴会议报酬
localAdBoardHonorariaBigDecimal本地咨询委员会报酬(与上面冗余)
localAdBoardTravelHonorariaBigDecimal本地咨询委员会差旅报酬
localAdBoardExtTravelHonorariaBigDecimal本地咨询委员会长途差旅报酬
dinnerMtgHonorariaBigDecimal晚餐会议报酬(与 dinnerMeetingHonoraria 冗余)
dinnerMtgTravelHonorariaBigDecimal晚餐会议差旅报酬
dinnerMtgTravelExtHonorariaBigDecimal晚餐会议长途差旅报酬
localLunchHonorariaBigDecimal本地午餐报酬
travelLunchHonorariaBigDecimal差旅午餐报酬
localDinnerHonorariaBigDecimal本地晚餐报酬
travelDinnerHonorariaBigDecimal差旅晚餐报酬
peerToPeerProgramsStringPeer-to-Peer 项目
contractTypeInteger合同类型:0=Speaker Program, 1=Consulting
levelString级别
templateIdInteger合同模板 ID
signedDateDate签署日期
customFieldsObject自定义字段(JSONB)
createdAtDate创建时间
includeTeInHonorariaCalculationBooleanT&E 是否计入报酬计算
enableProgramQuantityLimitBoolean是否启用项目数量限制
programQuantityLimitInteger项目数量上限
includedProgramTypeIdsObject包含的项目类型 ID(JSONB)

2.1.3 SpeakerTraining (t_speaker_training) - 培训记录表,8 个字段

字段名类型说明
idInteger主键
speakerIdInteger关联 Speaker
contentIdInteger关联 Content (培训模块)
statusInteger0=未完成, 1=已完成, 2=豁免
trainedDateDate培训日期
expiredDateDate过期日期
trainingMethodInteger0=Live, 1=Online/On Demand
createdAtDate创建时间
updatedAtDate更新时间

2.1.4 Content (t_content) - 内容库表,14 个字段

字段名类型说明
idInteger主键
contentNameString内容名称
typeInteger0=Presentation, 1=Training, 2=Other, 3=Video
subTypeInteger0=Core, 1=Module, 2=Case Study, 3=iSpring, 4=Video, 5=Other
productIdInteger关联产品
descriptionString描述
fileIdInteger关联文件
delFlagInteger软删除
createdAtDate创建时间
updatedAtDate更新时间
moduleMaximumIntegerModule 最大数量
caseStudyMaximumIntegerCase Study 最大数量
companyIdInteger公司 ID
expirationDateDate过期日期
commentString备注
urlString外部 URL

2.1.5 SpeakerSlide (t_speaker_slide) - Speaker 个人演示文稿表,8 个字段

字段名类型说明
idInteger主键
nameString演示文稿名称
contentIdInteger关联 Core Presentation (t_content.id)
contentNameString来源名称(冗余)
speakerIdInteger关联 Speaker
fileIdInteger生成的文件 ID
delFlagInteger软删除
createdAtDate创建时间
updatedAtDate更新时间

2.1.6 SlideDetail (t_slide_detail) - 演示文稿页面详情表,8 个字段

字段名类型说明
idInteger主键
contentIdInteger关联 Content
titleString页面标题
typeInteger0=普通页面, 1=插入点
sequenceInteger顺序号
minInteger最小插入数
maxInteger最大插入数
imageUrlString预览图 URL
createdAtDate创建时间

2.1.7 SlideInsertion (t_slide_insertion) - 演示文稿插入点表,5 个字段

字段名类型说明
idInteger主键(同时作为排序序号)
speakerSlideIdInteger关联 SpeakerSlide
detailIdInteger关联 SlideDetail(插入点)
contentIdInteger被插入的 Content ID
delFlagInteger软删除

2.1.8 SpeakerGroup (t_speaker_group) - 演讲者组表,6 个字段

字段名类型说明
groupIdInteger主键
groupNameString组名
descriptionString描述
delFlagInteger软删除
filterEnabledInteger是否在搜索筛选中启用
kclEmailStringKCL 邮箱(逗号分隔多个)
companyIdInteger公司 ID

2.1.9 Topic (t_topic) - 主题表,9 个字段

字段名类型说明
topicIdInteger主键
topicNameString主题名称
topicRefString主题引用
topicDescriptionString描述
productIdInteger关联产品
meetingTypeInteger会议类型
serviceTypeInteger服务类型
statusInteger状态
topicCodeString主题代码
shortNameString简称

2.1.10 Expense (t_expense) - 费用报销表,12 个字段

字段名类型说明
expenseIdInteger主键
submitterInteger提交者 ID
submitDateDate提交日期
meetingRequestIdInteger关联会议
amountBigDecimal金额
expenseStatusInteger0=Draft, 1=Approved, 2=Rejected, 3=Pending
rejectReasonString拒绝原因
approveAmountBigDecimal批准金额
updateDateDate更新日期
pdfReceiptsStringPDF 收据路径
fieldIdInteger字段 ID
checkNumberString支票号码
issueDateDate签发日期
pvExtUserIdStringPV 外部用户 ID

2.1.11 Alert (t_alert) - 通知表,6 个字段

字段名类型说明
idInteger主键
nameString通知名称
typeInteger0=Training Module, 1=Presentation, 2=Expense, 3=Survey, 5=Speaker Survey, 6=Video
objectIdInteger关联对象 ID
speakerIdInteger关联 Speaker
createdAtDate创建时间
companyIdInteger公司 ID

2.1.12 Mapping Tables(关联表)

MappingTopicSpeaker (t_mapping_topic_speaker):Topic 与 Speaker 的多对多关系及培训状态

字段名类型说明
topicIdInteger@Id, 关联 Topic
speakerIdInteger@Id, 关联 Speaker
statusInteger培训状态
trainnedDateDate培训日期(注意拼写错误:trainned)
expiredDateDate过期日期

MappingSpeakerGroup (t_mapping_speaker_group):Speaker 与 Group 的多对多

字段名类型说明
speakerIdInteger@Id
groupIdInteger@Id

MappingSpeakerDistrict (t_mapping_speaker_district):Speaker 与 District 的多对多

字段名类型说明
speakerIdInteger@Id
districtIdInteger@Id

MappingSpeakerPlace (t_mapping_speaker_place):Speaker 与 Territory 的关联

字段名类型说明
speakerTerritoryIdInteger@Id, 主键
speakerIdIntegerSpeaker ID
refIdIntegerTerritory 引用 ID

MappingPortalUserSpeaker (t_mapping_portal_user_speaker):Speaker 与 UnifiedUser 的关联

字段名类型说明
portalUserIdInteger历史字段
speakerIdIntegerSpeaker ID
userIdIntegerUnifiedUser ID

MappingContractDocument (t_mapping_contract_document):Contract 与 Document 关联

字段名类型说明
contractIdInteger@Id
documentIdInteger@Id

MappingContractProductProgramType (t_mapping_contract_product_program_type):合同的产品+项目类型关联

字段名类型说明
contractIdInteger@Id
productIdInteger@Id
programTypeIdInteger@Id
serviceTypeIdInteger服务类型 ID

2.2 Table Relationships (ER Diagram - ASCII)

                                    +-------------------+
                                    |    t_product      |
                                    |   productId (PK)  |
                                    +-------------------+
                                           |
                    +----------------------+----------------------+
                    |                      |                      |
                    v                      v                      v
+-------------------+     +-------------------+     +-------------------+
|    t_speaker      |     |    t_topic         |     |    t_content      |
|  speakerId (PK)   |     |   topicId (PK)     |     |     id (PK)       |
|  companyId (FK)   |     |  productId (FK)    |     |  productId (FK)   |
|  regionId (FK)    |     +-------------------+     |  companyId (FK)   |
|  districtId (FK)  |            |     |             |  fileId (FK)      |
+-------------------+            |     |             +-------------------+
   |   |   |   |   |            |     |                |        |
   |   |   |   |   |            |     |                |        |
   |   |   |   |   |     +------+     +-------+       |        |
   |   |   |   |   |     |                    |       |        |
   |   |   |   |   v     v                    v       v        v
   |   |   |   | +------------------------+  +-----------+ +-----------+
   |   |   |   | |t_mapping_topic_speaker  |  |t_content  | |t_slide    |
   |   |   |   | | topicId + speakerId (PK)|  |  _topic   | |  _detail  |
   |   |   |   | | status, trainnedDate    |  +-----------+ | contentId |
   |   |   |   | +------------------------+                +-----------+
   |   |   |   |                                                  |
   |   |   |   v                                                  |
   |   |   | +-------------------+                                |
   |   |   | |t_speaker_contract |     +--------------------------+
   |   |   | | contractId (PK)   |     |
   |   |   | | speakerId (FK)    |     v
   |   |   | | templateId (FK)   |  +--------------------+
   |   |   | +-------------------+  |  t_speaker_slide   |
   |   |   |    |         |         |    id (PK)          |
   |   |   |    |         |         |  contentId (FK)     |
   |   |   |    v         v         |  speakerId (FK)     |
   |   |   | +--------+ +--------+ |  fileId (FK)        |
   |   |   | |mapping  | |mapping | +--------------------+
   |   |   | |contract | |contract|          |
   |   |   | |document | |product |          v
   |   |   | +--------+ |progtype| +-------------------+
   |   |   |             +--------+ | t_slide_insertion |
   |   |   |                        | speakerSlideId FK |
   |   |   v                        | detailId FK       |
   |   | +-------------------+      | contentId FK      |
   |   | |t_mapping_speaker  |      +-------------------+
   |   | |    _group          |
   |   | | speakerId+groupId |
   |   | +-------------------+
   |   |          |
   |   |          v
   |   | +-------------------+
   |   | | t_speaker_group   |
   |   | |  groupId (PK)     |
   |   | +-------------------+
   |   |
   |   v
   | +------------------------+
   | |t_mapping_speaker_place |
   | | speakerTerritoryId (PK)|
   | | speakerId (FK)         |
   | +------------------------+
   |
   v
+----------------------------+          +-------------------+
|t_mapping_portal_user_speaker|         |   t_expense       |
| speakerId, userId           |-------->| expenseId (PK)    |
+----------------------------+          | meetingRequestId   |
                                        +-------------------+
                                               |
                                        +------+------+
                                        |             |
                                        v             v
                                 +-----------+  +-----------+
                                 |t_expense  |  |t_expense  |
                                 |  _item    |  | _history  |
                                 +-----------+  +-----------+

+-------------------+
|    t_alert         |
|    id (PK)         |
|  speakerId (FK)    |
|  companyId (FK)    |
+-------------------+

+-------------------+
|t_speaker_training |
| id (PK)           |
| speakerId (FK)    |
| contentId (FK)    |
+-------------------+

2.3 Data Model Issues

DM-1: Speaker 表严重膨胀(88 个字段)

  • 文件:Speaker.java:24-397
  • 问题:Speaker 表包含了至少 5 组应独立的数据:
    • 基本信息(姓名、联系方式)
    • 办公地址、家庭地址、W9 地址、NPI 注册地址(4 组地址,20+ 个字段)
    • 合同相关字段(contractStartDate, honorariaCap 等 - 已由 SpeakerContract 管理但未从 Speaker 中移除)
    • 提名相关字段(nominationStatus, nominationNote, category, caregiver 等)
    • 密码/认证字段(password, passwordQuestion, passwordAnswer - 已废弃但未移除)
    • 助理信息(assistantName, assistantEmail, assistantPhone)

DM-2: 字段冗余严重

  • speakerNamespeakername 两个字段(Speaker.java:36,186),只有大小写命名差异
  • stateOfMedicalLicenselicenseState 语义重复(Speaker.java:159,255
  • honorariaCap, contractStartDate, contractExpirationDate 在 Speaker 和 SpeakerContract 中均存在
  • SpeakerContract 中 dinnerMeetingHonorariadinnerMtgHonoraria 是相同概念的两个字段(SpeakerContract.java:71-84
  • localAdvisoryBoardHonorarialocalAdBoardHonoraria 也是冗余(SpeakerContract.java:65-76

DM-3: 布尔值使用 Integer 而非 Boolean

  • travelCompanionRequired, handicappedAccessible, connectedToSource 等使用 Integer(0/1) 而非 Boolean 类型(Speaker.java:313,319,331
  • SpeakerSlide, SpeakerGroup 的 delFlag 也是 Integer 而非 Boolean

DM-4: MappingTopicSpeaker 拼写错误

  • trainnedDate 字段拼写错误,应为 trainedDateMappingTopicSpeaker.java:33

DM-5: 培训状态数据存储在两个地方

  • t_speaker_training 表存储 Speaker 对 Content(Module)的培训状态
  • t_mapping_topic_speaker 表存储 Speaker 对 Topic 的培训状态
  • 两者概念上有重叠但结构不一致

DM-6: 密码字段残留

  • Speaker 实体仍保留 password, passwordQuestion, passwordAnswer 字段(Speaker.java:177-184),但实际认证已迁移到 UnifiedUser 系统
  • 这些是敏感数据字段,应该被移除

DM-7: MappingPortalUserSpeaker 缺少主键定义

  • MappingPortalUserSpeaker.java 实体没有 @Id 注解(MappingPortalUserSpeaker.java:7-16),无明确主键

DM-8: 日期类型字段存为 String

  • stateLicenseExpDate 使用 String 类型而非 Date(Speaker.java:258

3. Business Flow Analysis

3.1 Core Business Flows

3.1.1 Speaker Onboarding/Certification Flow

+------------------+     +-----------------+     +------------------+
| Sales Rep        |     | Admin/Planner   |     | Speaker          |
| Nominates        |---->| Reviews         |---->| Account Created  |
| Speaker          |     | Nomination      |     | via Portal       |
+------------------+     +-----------------+     +------------------+
                               |                        |
                               v                        |
                    +-----------------------+           |
                    | Status: Nomination    |           |
                    | (nominationStatus)    |           |
                    +-----------------------+           |
                               |                        |
                               | Approve                |
                               v                        |
                    +------------------------+          |
                    | Status: Contract       |          |
                    | Creation Pending (3)   |          |
                    +------------------------+          |
                               |                        |
                    Contract Created                    |
                               |                        |
                               v                        |
                    +------------------------+          |
                    | Status: Contract       |          |
                    | Signature Pending (4)  |          |
                    +------------------------+          |
                               |                        |
                    Speaker Signs Contract   <----------+
                               |
                               v
                    +------------------------+
                    | Status: W9 Pending (2) |
                    +------------------------+
                               |
                    W9 Submitted
                               |
                               v
                    +------------------------+
                    | Status: Active (1)     |
                    +------------------------+
                               |
                    +----------+----------+
                    |                     |
            Topic Training         Content Training
                    |                     |
                    v                     v
            +----------------+   +--------------------+
            | MappingTopic   |   | SpeakerTraining    |
            | Speaker        |   | (Content modules)  |
            +----------------+   +--------------------+
                    |                     |
                    +----------+----------+
                               |
                    Training Complete
                               |
                               v
                    +------------------------+
                    | Speaker Certified      |
                    | (can be selected for   |
                    |  meetings)             |
                    +------------------------+

3.1.2 Contract Lifecycle

+-----------+     +------------------+     +-----------------+
| Create    |---->| Not Signed (0)   |---->| Speaker Signs   |
| Contract  |     | (via Planner)    |     | (signContract)  |
+-----------+     +------------------+     +-----------------+
                         |                        |
                  +------+------+                 |
                  |             |                 v
           Download        Update          +-----------+
           Contract        Contract        | Signed(1) |
           (docx/pdf)     (PUT)           +-----------+
                                                |
                                          Download Signed
                                          PDF (saved copy)
                                                |
                                          +-----v------+
                                          |  Resign    |
                                          | (re-save   |
                                          |  PDF only) |
                                          +------------+

Contract Structure:
+--------------------------------------------------+
| SpeakerContract                                   |
|  - Multiple Honoraria Fields (15+ BigDecimal)    |
|  - customFields (JSONB for dynamic honoraria)    |
|  - programQuantityLimit (optional cap)           |
|  - includeTeInHonorariaCalculation               |
+--------------------------------------------------+
          |                     |
          v                     v
+-------------------+  +---------------------------+
| MappingContract   |  | MappingContractProduct    |
| Document          |  | ProgramType               |
| (attached docs)   |  | (eligible program types)  |
+-------------------+  +---------------------------+

3.1.3 Presentation Approval & Building Flow

Admin/Planner Side:
+------------------+     +-------------------+     +------------------+
| Upload PPT File  |---->| Create Content    |---->| Set Slide Detail |
| (via FileService)|     | (type=0,sub=0)    |     | + Insertion Pts  |
+------------------+     | = Core Present.   |     +------------------+
                          +-------------------+            |
                                 |                         |
                          Upload Modules            Set Allowed
                          (type=0,sub=1/2)         Modules/Cases
                                 |                         |
                                 v                         v
                          +-------------------+    +----------------+
                          | Module/CaseStudy  |    | MappingCore    |
                          | Content records   |    | Module         |
                          +-------------------+    +----------------+
                                                          |
                          Assign to Speaker Groups         |
                          Assign Topics                    |
                          Set Expiration Date               |
                                                          |
Speaker Side:                                              |
+-------------------+     +-------------------+            |
| View Available    |<----|  Content filtered  |<-----------+
| Presentations     |     |  by SpeakerGroup   |
+-------------------+     +-------------------+
        |
        v
+-------------------+     +-------------------+
| Create Personal   |---->| Select Modules    |
| Presentation      |     | for Insertion Pts |
| (SpeakerSlide)    |     +-------------------+
+-------------------+            |
        |                        |
        v                        v
+-------------------+     +-------------------+
| Save SlideInser-  |     | Download PPTX     |
| tion records      |     | (Aspose Slides)   |
+-------------------+     | Merge field subst.|
                          | Write protection  |
                          +-------------------+

3.2 Validation Rules

Speaker 创建/更新验证

  • SpeakerService.java:414 - 邮箱唯一性检查(ILIKE 忽略大小写,scoped by companyId)
  • SpeakerService.java:460-463 - 更新时检查新邮箱是否已存在
  • SpeakerService.java:830-834 - Speaker 角色用户只能访问自己的数据(checkSpeakerPermission)

合同验证

  • SpeakerContractService.java:186 - 签署合同时检查 Speaker 身份所有权(checkOwnerPermission)
  • SpeakerContractService.java:165-169 - 如果未启用 programQuantityLimit,清空相关字段

费用验证

  • 费用状态流转:Draft(0) -> Pending(3) -> Approved(1) / Rejected(2)

演示文稿验证

  • ContentService.java:871-885 - 下载时检查被插入的 Module 是否仍可用(delFlag 检查)
  • ContentService.java:685-688 - 检查 Speaker 对演示文稿的权限

Speaker 搜索验证

  • SpeakerService.java:176-178 - 根据 Product 配置决定是否启用 Training 检查
  • SpeakerService.java:247-255 - programQuantityLimit 超限时将 balance 设为 -1 禁用 Speaker

3.3 Business Logic Issues

BL-1: 报酬计算逻辑分散且复杂

  • 文件:SpeakerService.java:227-258
  • 问题:在 findSpeakers 中直接计算每个 Speaker 的 balance,涉及 honorariaCap、meetingHonoraria、speakerExpense、includeTeInHonorariaCalculation 等多个维度,逻辑分散在 Controller 调用的 Service 中,无独立的报酬计算领域服务

BL-2: 密码重置使用 Guava Cache

  • 文件:SpeakerService.java:133-134,136-137
  • 问题:密码重置 token 和激活码存储在 JVM 内存 Cache 中,在多实例部署或服务重启时会丢失

BL-3: 签署合同时存在两套 API

  • SpeakerContractController.java:93-96 路径 v1/contracts/{id}/sign(管理员用)
  • SecureSpeakerContractController.java:40-43 路径 v1/speakers/{speakerId}/contracts/{id}/sign(Speaker 用,带 @PreAuthorize)
  • 两个 API 调用同一个 signContract() 方法,但权限检查逻辑不一致:SecureSpeakerContractController 有 @PreAuthorize 注解但 SpeakerContractController 没有

BL-4: 演示文稿生成直接操作文件系统

  • 文件:ContentService.java:896-957
  • 问题:使用 Aspose Slides 直接读写文件系统来合并 PPT,没有异步处理或缓存策略,可能在高并发下阻塞线程

BL-5: Speaker 删除是硬删除

  • 文件:SpeakerService.java:503
  • 问题:deleteSpeaker 调用 deleteByPrimaryKey 执行硬删除,而不是软删除(Speaker 表有 deleted 字段但未使用)。关联的 User 也会被删除角色和公司绑定

BL-6: 合同报酬字段硬编码

  • 文件:SpeakerContract.java:41-102
  • 问题:SpeakerContract 有 15+ 个固定的 honoraria 字段(localHonoraria, travelHonoraria, dinnerMeetingHonoraria 等),当客户需要不同的报酬结构时只能通过 customFields JSONB 扩展,导致部分数据在固定列、部分在 JSONB 中

4. API Inventory

4.1 REST Endpoints Table

4.1.1 SpeakerController (v1/speakers)

MethodPathParametersRequest BodyResponseDescription
GET/v1/speakersSpeakerQuery (speakerName, groupId, specialty, state, status, credential, speakerLevel)-PageResult<SpeakersResponse>获取 Speaker 列表
GET/v1/speakers/nominationsNominationQuery-PageResult<NominationSpeakersResponse>获取提名 Speaker 列表
GET/v1/speakers/searchFindSpeakerQuery (productId, topicId, meetingDate 等)-PageResult<FindSpeakersResponse>搜索 Speaker(含报酬余额计算)
GET/v1/speakers/downloadSpeakerQuery-Excel 下载下载 Speaker 列表 Excel
POST/v1/speakers-SpeakerProfilevoid (201)创建 Speaker
PUT/v1/speakers/idSpeakerProfileSpeakerProfile更新 Speaker
GET/v1/speakers/id-SpeakerProfile获取 Speaker 详情
DELETE/v1/speakers/id-void删除 Speaker(硬删除)
GET/v1/speakers/{id}/regiondistrictid-RegionDistrictsDetail获取 Speaker 区域地区信息
PUT/v1/speakers/nominations/{id}/statusid, status-void更新提名状态
PUT/v1/speakers/{id}/statusid, status-void更新 Speaker 状态
POST/v1/speakers/login-SpeakerLoginRequestAuthInfo<SpeakerInfo>Speaker 登录
PUT/v1/speakers/{speakerId}/accountspeakerIdSetSpeakerAccountRequestUser设置 Speaker 账户
POST/v1/speakers/{id}/uploadid, uploadFileMultipartFileGetFileResponse上传头像
POST/v1/speakers/{id}/videoid, uploadFileMultipartFileGetFileResponse上传视频
GET/v1/speakers/forget-passworduserId-void忘记密码(发送邮件)
PUT/v1/speakers/reset-password-ResetSpeakerPasswordRequestvoid重置密码
POST/v1/speakers/uploaduploadFileMultipartFilevoid批量上传 Speaker(Excel)
GET/v1/speakers/activation-codeemail-void获取激活码
POST/v1/speakers/activation-code/validation-ValidateActivationCodeRequestvoid验证激活码
POST/v1/speakers/signup-SignUpRequestvoidSpeaker 自助注册
GET/v1/speakers/{speakerId}/image/downloadspeakerId-ResponseEntity (file)下载 Speaker 头像

4.1.2 SpeakerContractController (v1/contracts) - 管理端

MethodPathParametersRequest BodyResponseDescription
GET/v1/contractsSpeakerContractQuery-PageResult<ListContractResponse>合同列表
POST/v1/contracts-SpeakerContractRequestSpeakerContractResponse创建合同
PUT/v1/contracts/idSpeakerContractRequestSpeakerContractResponse更新合同
GET/v1/contracts/id-SpeakerContractResponse获取合同详情
DELETE/v1/contracts/id-void删除合同
PUT/v1/contracts/regions-ContractRegionsDTOSpeaker更新合同区域和地区
GET/v1/contracts/downloadSpeakerContractQuery-Excel 下载下载合同列表
PUT/v1/contracts/{id}/signid-SpeakerContractResponse签署合同
GET/v1/contracts/{id}/fileid, fileFormat-docx/pdf 下载获取合同文件
GET/v1/contracts/{id}/signedFileid-ResponseEntity (PDF)获取已签署合同
GET/v1/contracts/resigncontractIds (逗号分隔)-void批量重新签署

4.1.3 SecureSpeakerContractController (v1/speakers/{speakerId}/contracts) - Speaker 端

MethodPathParametersRequest BodyResponseDescription
PUT/v1/speakers/{speakerId}/contracts/{id}/signspeakerId, id-SpeakerContractResponseSpeaker 签署合同
GET/v1/speakers/{speakerId}/contracts/{id}/fileid, fileFormat-文件下载获取合同文件
GET/v1/speakers/{speakerId}/contracts/{id}/signedFileid-ResponseEntity (PDF)获取已签署合同

4.1.4 SpeakerPresentationController (v1/speakers/{speakerId}/presentations)

MethodPathParametersRequest BodyResponseDescription
GET/v1/speakers/{speakerId}/presentationsspeakerId-List<PersonalPresentationList>个人演示文稿列表
GET/v1/speakers/{speakerId}/presentations/modulesspeakerId-List<ListContentsResponse>可用 Module 列表
GET/v1/speakers/{speakerId}/presentations/id-PresentationProfile个人演示文稿详情
DELETE/v1/speakers/{speakerId}/presentations/id-void (204)删除个人演示文稿
GET/v1/speakers/{speakerId}/presentations/{id}/downloadid, meetingRequestId-文件下载下载演示文稿
GET/v1/speakers/{speakerId}/presentations/downloadmeetingRequestId, fileId-文件下载下载会议演示文稿
POST/v1/speakers/{speakerId}/presentationsspeakerIdPresentationSlidePresentationProfile创建个人演示文稿

4.1.5 ContentController (v1/contents) - 内容库管理

MethodPathParametersRequest BodyResponseDescription
GET/v1/contentsContentQuery-PageResult<ListContentsResponse>内容列表
GET/v1/contents/modules--List<ListContentsResponse>Module 列表
POST/v1/contents-CreateContentRequestGetContentResponse (201)创建内容
PUT/v1/contents/{id}/maximumsidPresentMaximumRequestvoid设置演示文稿最大值
GET/v1/contents/id-GetContentResponse获取内容详情
GET/v1/contents/presentations/id-PresentationProfile获取演示文稿
GET/v1/contents/presentations/{id}/downloadid-文件下载下载演示文稿
DELETE/v1/contents/id-void (204)删除内容(软删除)
PUT/v1/contents/{id}/descriptionidUpdateContentRequestGetContentResponse更新描述
PUT/v1/contents/{id}/urlidUpdateURLRequestGetContentResponse更新 URL
PUT/v1/contents/{id}/topicsidUpdateContentRequestGetContentResponse设置 Topic
PUT/v1/contents/{id}/expirationidContentExpirationRequestvoid设置过期日期
PUT/v1/contents/groups-SetContentGroupRequestvoid设置内容用户组
DELETE/v1/contents/groups-SetContentGroupRequestvoid移除内容用户组
POST/v1/contents/{id}/modulesidPresentationDTOPresentationProfile设置允许的 Module
GET/v1/contents/{id}/modulesid-List<SlideDTO>获取 Module 列表
PUT/v1/contents/{id}/conversionidConvertModuleRequestPresentationProfile类型转换
PUT/v1/contents/{id}/insertionsidPresentationSlidePresentationProfile保存插入点
GET/v1/contents/presentations/slide/id-SpeakerSlide获取 Slide 信息

4.1.6 TopicController (v1/topicsv1/speaker/topics)

MethodPathParametersRequest BodyResponseDescription
GET/v1/topicsTopicQuery-PageResult<ListTopicsResponse>Topic 列表
GET/v1/speaker/topicsTopicQuery-PageResult<SpeakerTopicDetail>Speaker 的 Topic 列表
PUT/v1/speaker/{id}/topicsidSpeakerTopicDTOMappingTopicSpeaker保存/更新 Speaker-Topic 关系
POST/v1/topics-TopicProfile (201)void创建 Topic
PUT/v1/topics/idTopicProfileTopic更新 Topic
GET/v1/topics/id-TopicInfo获取 Topic 详情
PUT/v1/topics/{id}/activeid-void激活 Topic
PUT/v1/topics/{id}/deactiveid-void停用 Topic
GET/v1/topics/{id}/presentationsid-List<TopicPresentation>获取 Topic 下的演示文稿

4.1.7 SpeakerGroupController (v1/speakergroups)

MethodPathParametersRequest BodyResponseDescription
GET/v1/speakergroups--List<SpeakerGroupResponse>获取所有组
POST/v1/speakergroups-SpeakerGroupRequest (201)SpeakerGroupResponse创建组
PUT/v1/speakergroups/idSpeakerGroupRequestSpeakerGroupResponse更新组
DELETE/v1/speakergroups/id-void (204)删除组
POST/v1/speakergroups/users-SpeakerToGroupRequestvoid添加 Speaker 到组
DELETE/v1/speakergroups/users-SpeakerToGroupRequestvoid从组移除 Speaker
PUT/v1/speakergroups/filters-List<Integer> groupIdsvoid添加到搜索筛选
DELETE/v1/speakergroups/filters-List<Integer> groupIdsvoid (204)从搜索筛选移除

4.1.8 TrainingController (v1/training)

MethodPathParametersRequest BodyResponseDescription
GET/v1/trainingTrainingQuery-PageResult<ListTrainingResponse>培训列表
POST/v1/training/status-SetTrainingStatusRequestvoid设置培训状态
GET/v1/training/topics/downloadTrainingQuery-Excel 下载下载 Topic 培训报告

4.1.9 SpeakerMeetingController (v1/speakers/meetings)

MethodPathParametersRequest BodyResponseDescription
GET/v1/speakers/meetingsSpeakerMeetingQuery-PageResult<MeetingsResponse>Speaker 会议列表
GET/v1/speakers/meetings/meetingRequestId-SpeakerMeetingResponse获取会议详情
GET/v1/speakers/meetings/survey/download--Excel 下载下载调查问卷
GET/v1/speakers/meetings/{meetingRequestId}/surveymeetingRequestId-SpeakerSurvey获取调查问卷
PUT/v1/speakers/meetings/{meetingRequestId}/surveymeetingRequestIdSpeakerSurveyRequestMeetingRequest提交调查问卷

4.1.10 ExpenseController (v1/expenses)

MethodPathParametersRequest BodyResponseDescription
GET/v1/expensesExpenseQuery-PageResult<ListExpensesResponse>费用列表
GET/v1/expenses/expenseId-GetExpenseResponse费用详情
POST/v1/expensesmeetingRequestId- (201)Expense创建费用
PUT/v1/expenses/expenseIdUpdateExpenseRequestvoid更新费用
DELETE/v1/expenses/expenseId-void删除费用
PUT/v1/expenses/{expenseId}/approvalexpenseIdApproveExpenseRequestvoid批准费用
PUT/v1/expenses/{expenseId}/rejectionexpenseId, rejectReason-void拒绝费用
GET/v1/expenses/{expenseId}/historiesexpenseId, status-List<ExpenseHistory>费用历史
PUT/v1/expenses/{expenseId}/submitexpenseId, amount-void提交费用
POST/v1/expenses/{expenseId}/receiptsexpenseIdExpenseReceiptRequestvoid上传收据

4.1.11 ExpenseItemController (v1/expenses/items)

MethodPathParametersRequest BodyResponseDescription
GET/v1/expenses/itemsexpenseId-List<ListExpenseItemsResponse>费用明细列表
GET/v1/expenses/items/itemId-GetExpenseItemResponse费用明细详情
POST/v1/expenses/items-CreateExpenseItemRequest (201)void创建费用明细
PUT/v1/expenses/items/itemIdCreateExpenseItemRequestGetExpenseItemResponse更新费用明细
DELETE/v1/expenses/items/itemId-void (204)删除费用明细
POST/v1/expenses/items/{expenseItemId}/receiptsexpenseItemIdExpenseReceiptRequestvoid上传明细收据

4.1.12 ExpenseCategoryController (v1/expenses/categories)

MethodPathParametersRequest BodyResponseDescription
GET/v1/expenses/categories--List<ExpenseCategory>费用类别列表
GET/v1/expenses/categories/categoryId-List<ExpenseCategory>子类别列表

4.1.13 AlertController (v1/alerts)

MethodPathParametersRequest BodyResponseDescription
GET/v1/alerts--List<Alert>通知列表
DELETE/v1/alerts-AlertRemoveDTOvoid删除通知

4.1.14 DocumentController (v1/documents)

MethodPathParametersRequest BodyResponseDescription
GET/v1/documentsDocumentQuery-PageResult<DocumentsResponse>文档列表
DELETE/v1/documents/id, contractId-void删除文档
POST/v1/documents-DocumentDTO (201)void创建文档
PUT/v1/documents/idDocumentDTOvoid更新文档

4.2 API Design Issues

API-1: 同一资源两套 Controller

  • SpeakerContractController (v1/contracts) 和 SecureSpeakerContractController (v1/speakers/{speakerId}/contracts) 管理同一资源
  • 功能大量重复:签署合同、获取合同文件均有两份实现
  • 应统一为一个 Controller,通过角色权限控制访问

API-2: RESTful 设计不一致

  • PUT /v1/topics/{id}/activePUT /v1/topics/{id}/deactive 应为 PATCH /v1/topics/{id} + 状态字段
  • GET /v1/contracts/resign?contractIds=1,2,3 使用 GET 执行修改操作(重新签署),违反 REST 语义
  • PUT /v1/speakers/nominations/{id}/status?status=1 状态通过 Query Param 传递而非 Request Body
  • DELETE /v1/contents/groups 使用 DELETE + RequestBody,部分 HTTP 客户端不支持

API-3: 缺少版本化和统一错误处理

  • Topic 的路径混用 /v1/topics/v1/speaker/topics,没有统一的资源命名
  • Alert 的 DELETE 使用 RequestBody 传 ID 列表而非路径参数

API-4: 安全问题

  • SpeakerContractController.java:93-96signContract 接口没有 @PreAuthorize 注解,任何认证用户都可以签署任意合同
  • ContentController.java:178-180 直接暴露 Mapper 查询结果(speakerSlideMapper.selectByPrimaryKey),无权限检查
  • Speaker 登录接口(POST /v1/speakers/login)直接返回 token,而 companyId 来自 HTTP Header(Pharmagin-Company-ID),存在伪造风险

API-5: 返回值不统一

  • 部分创建接口返回 void(SpeakerController.save),部分返回实体(SpeakerContractController.createContract)
  • 部分列表接口返回 PageResult,部分返回 List

5. Frontend Analysis

5.1 Pages & Components

5.1.1 Speakerview (pharmagin-speakerview/legacy/src/)

这是面向 Speaker 和 Speaker Admin 的前端应用。路由文件:routes.js

核心页面路由

路由页面名称Container/Component说明
/speakersSpeaker 列表containers/SpeakersSpeaker 管理列表页
/speakers/:speakerId/profileSpeaker 详情containers/SpeakerProfileSpeaker 个人资料页
/speakers/:speakerId/documentsSpeaker 文档containers/SpeakerDocuments文档管理页
/speakers/:speakerId/contractsSpeaker 合同containers/SpeakerContracts合同列表和详情
/speakers/:speakerId/trainingSpeaker 培训containers/SpeakerModules培训模块状态
/speakers/:speakerId/scheduled-programs已安排的项目containers/SpeakerScheduledMeetings即将参加的会议
/speakers/:speakerId/completed-programs已完成的项目containers/SpeakerCompletedMeetings已完成的会议
/speakers/:speakerId/expenses费用报告containers/SpeakerExpenseReport费用管理
/contracts合同管理containers/Contracts合同管理页(Admin)
/content内容库containers/ContentLibrary内容/演示文稿管理
/presentation/:slideId/:accessToken演示文稿详情containers/Presentation演示文稿查看/编辑
/topicsTopic 管理containers/Topics主题管理(Admin)
/expenses(/:expenseId)费用报告containers/ExpenseReports费用管理(Admin)
/nominations提名管理containers/Nominations提名列表(Admin)
/nominationDetail/:id提名详情containers/NominationDetail提名查看
/training培训管理containers/Training培训状态管理(Admin)
/reports报告containers/Report报表
/admin管理页containers/Admin系统管理
/forgotPwd/:uuid忘记密码containers/ForgotPwd密码重置

核心 Components(约 40+ 个组件):

Component说明
Login登录页,含 ForgotPwd, SignUp 子组件
ContractForm合同表单
ContractProfile合同详情(含 actions, reducer, sagas)
ContractDistrict合同区域管理
SpeakerDocumentsSpeaker 文档上传/管理
SpeakerExpenseReportSpeaker 费用报告
SpeakerMeetingDetail会议详情
SpeakerPresentationsSpeaker 演示文稿选择
SpeakerSurvey调查问卷
Presentations演示文稿管理(Admin)含 Description, SetExpiration, SettingTopics, SettingUserGroup 子模态框
Slide幻灯片管理,含 DragBlock, DropBlock, SlideDetail 子组件
OtherContents其他内容管理
TopicFormTopic 表单
TopicPresentationTopic 演示文稿关联
TrainingTopics培训 Topic 管理
TrainingStatusForm培训状态表单
ExpenseAddForm费用添加
ExpenseApproveForm费用审批
ExpenseRejectForm费用拒绝
ExpenseItem费用明细
ExpenseMileageReimbursement里程报销
ExpenseReportItems费用报告明细(含打印功能)
NominationForm提名表单
NominationInfo提名信息
NominationProfile提名详情
UserGroupForm用户组表单
UserGroupList用户组列表
UserGroupSelect用户组选择
Templates合同模板管理
AdministerUserAccount用户账户管理
Alert通知列表
ReportTrainingStatus培训状态报表
ReportVideoViews视频观看报表

5.1.2 Plannerview (pharmagin-plannerview/legacy/src/)

Speaker 相关组件主要出现在会议创建和管理流程中:

Component/Container说明
MTProfile/meetingDetail.js会议详情中显示已选 Speaker 信息
MTProfile/meetingForm.js会议创建表单中的 Speaker 选择
MTProfile/SpeakerCriteriaEditForm.jsSpeaker 选择标准编辑
MTProfile/SpeakerRandomizationForm.jsSpeaker 随机化表单
Common/actions.js, sagas.js共享的 Speaker 数据获取 actions
Compliance/AggregateSpendByAttendeesProfile.js合规 - Speaker 支出汇总

5.1.3 Salesview (pharmagin-salesview/src/)

Speaker 相关功能主要用于 Sales Rep 查看和选择 Speaker:

Page/Component说明
pages/Speakers/index.jsSpeaker 列表页
pages/Speakers/filters.jsSpeaker 搜索筛选
pages/Speakers/constants.jsSpeaker 相关常量
pages/Speakers/reducer.jsSpeaker Redux 状态
pages/Speakers/saga.jsSpeaker 数据获取
pages/Speakers/selectors.jsSpeaker 状态选择器
pages/Programs/Form/SelectSpeakers/会议创建中的 Speaker 选择
pages/Programs/SpeakerCandidates/Speaker 候选人列表
pages/Programs/Modal/SpeakerCriteriaModal.jsSpeaker 选择标准模态框

5.2 Redux State Structure

Speakerview 的 Redux Store 结构

{
  app: {
    loginStatus: '',
    accessToken: '',
    user: {},                    // SpeakerInfo
    roleId: '',
    alertList: [],
    alertLoading: true,
    expenseInfo: {},
    surveyInfo: {},
    module: {},
    presentation: {},
    removeAlertsSuccess: false,
    meetingInfo: {},
    surveySuccess: false,
    config: {},                  // CompanyConfiguration
    authorities: [],
    products: [],
  },
  speakers: { ... },             // Speaker 列表页状态
  speakerDetail: { ... },        // Speaker 详情页状态(共享 reducer)
  contracts: { ... },            // 合同管理状态
  contractInfo: { ... },         // 合同详情状态
  content: { ... },              // 内容库状态
  topics: { ... },               // Topic 管理状态
  training: { ... },             // 培训管理状态
  expenseReports: { ... },       // 费用管理状态
  nominations: { ... },          // 提名管理状态
  nominationDetail: { ... },     // 提名详情状态
  forgotPwd: { ... },            // 密码重置状态
  reports: { ... },              // 报表状态
  admin: { ... },                // 管理页状态
}

5.3 Frontend Issues

FE-1: 过时的技术栈

  • 使用 React Router 3.x(System.import 动态加载,已过时的路由 API)
  • Ant Design 3.x(已停止维护)
  • Redux 使用 Object.assign 而非 immer 或 toolkit
  • 路由使用 getComponent(nextState, cb) 回调模式而非现代 hooks

FE-2: 代码分割和 Reducer 注入不一致

  • routes.js:110-139 中 SpeakerContracts 路由同时注入了 4 个 reducer(speakerDetail, contracts, admin, contractInfo),加载了不必要的模块
  • 部分 Container 使用 sagas 注入,部分不注入(如 SpeakerProfile 页面没有注入 sagas)

FE-3: Speaker 详情页面共享同一个 reducer

  • speakerDetail reducer 被 profile, documents, contracts, training, meetings, expenses 等 6 个页面共享
  • 导致页面间状态可能互相污染

FE-4: 认证流程安全性问题

  • 登录成功后将 accessToken 存储在 Redux state 中
  • CompanyId 通过 HTTP Header Pharmagin-Company-ID 传递,前端可被篡改
  • 密码重置使用 UUID 作为 token,通过 URL 明文传递

FE-5: XSS 风险

  • App/index.js:215 使用 dangerouslySetInnerHTML 渲染 config.headerText,如果配置数据被篡改可能导致 XSS

6. Problem Summary

6.1 Critical Issues (must fix in rewrite)

ID问题影响文件位置
C-1Speaker 表 88 个字段,严重违反单一职责维护困难,性能差,查询效率低Speaker.java
C-2Speaker 实体存储密码明文字段安全风险Speaker.java:177-184
C-3密码重置 token 存在 JVM 内存多实例不可用,重启丢失SpeakerService.java:133-137
C-4合同签署 API 缺少权限控制任何认证用户可签署任意合同SpeakerContractController.java:93-96
C-5Speaker 删除使用硬删除数据丢失不可恢复SpeakerService.java:503
C-6合同报酬字段硬编码 15+ 个扩展性差,大量冗余字段SpeakerContract.java:41-102
C-7过时的前端技术栈维护成本高,安全隐患speakerview 整体
C-8dangerouslySetInnerHTML XSS 风险安全漏洞App/index.js:215
C-9两套 Contract Controller 功能重叠代码重复,权限模型混乱SpeakerContractController + SecureSpeakerContractController

6.2 Design Defects (should improve)

ID问题影响文件位置
D-1培训状态存储在两个不同的表中数据不一致风险SpeakerTraining.java vs MappingTopicSpeaker.java
D-2报酬计算逻辑分散在 findSpeakers 方法中难以测试和维护SpeakerService.java:165-284
D-3演示文稿生成同步阻塞高并发下性能问题ContentService.java:896-957
D-4API 设计不符合 REST 规范客户端使用困难多处 Controller
D-5字段冗余(speakerName/speakername、多个重复报酬字段)数据一致性问题Speaker.java:36,186; SpeakerContract.java:71-84
D-6speakerDetail reducer 被 6 个页面共享状态污染speakerview routes.js
D-7Boolean 字段使用 Integer 表示代码可读性差Speaker.java:313,319,331
D-8缺少审计日志和变更追踪合规风险Speaker, Contract 相关 Service
D-9Content/Presentation 领域模型混淆Content 既是培训材料也是演示文稿,由 type 字段区分Content.java, ContentService.java
D-10邮箱唯一性仅限 company 范围内跨 company 数据不隔离SpeakerManager.java:112-119

6.3 Technical Debt (nice to have)

ID问题影响文件位置
T-1SpeakerSlide 未使用 Lombok代码风格不统一SpeakerSlide.java
T-2SpeakerGroup 未使用 Lombok代码风格不统一SpeakerGroup.java
T-3MappingTopicSpeaker 中 trainnedDate 拼写错误代码质量MappingTopicSpeaker.java:33
T-4stateLicenseExpDate 使用 String 而非 Date类型安全Speaker.java:258
T-5合同下载使用 GET 请求传逗号分隔 IDAPI 设计SpeakerContractController.java:117-123
T-6ContentService 中硬编码 Aspose 密码逻辑安全性ContentService.java:136,949
T-7AlertType 枚举中 type=4 缺失枚举值不连续AlertType.java
T-8SpeakerService 方法过多(约 30 个公开方法)类职责过重SpeakerService.java
T-9前端使用 System.import 而非标准 dynamic import过时语法routes.js
T-10Controller 层直接注入 Mapper(绕过 Service)违反分层架构ExpenseCategoryController.java:23, ContentController.java:58

7. Rewrite Recommendations

7.1 数据模型重构

  1. 拆分 Speaker 表

    • speakers - 核心身份信息(姓名、后缀、NPI、类型、级别、状态)
    • speaker_addresses - 地址信息(类型枚举:Office/Home/W9/NPI),一对多
    • speaker_contacts - 联系方式(电话、邮箱、传真)
    • speaker_credentials - 医疗执照和认证信息
    • speaker_preferences - 偏好设置(差旅、语言、可用性)
    • speaker_nominations - 提名信息独立表(包含 patient/caregiver 相关字段)
    • 移除密码相关字段,统一使用 UnifiedUser 认证
  2. 重构合同报酬模型

    • 使用 contract_honoraria_items 表替代 15+ 个硬编码字段
    • 结构:{contractId, honorariaType, amount}
    • honorariaType 使用枚举或字典表管理
    • 移除 customFields JSONB,统一走结构化存储
  3. 统一培训状态模型

    • 合并 t_speaker_trainingt_mapping_topic_speaker 为统一的培训记录表
    • 结构:{speakerId, trainingType(TOPIC/MODULE), referenceId, status, trainedDate, expiredDate}

7.2 API 重构

  1. 统一 Contract APIv1/speakers/{speakerId}/contracts,通过角色权限控制 Admin 和 Speaker 的访问级别
  2. 遵循 REST 规范:状态变更使用 PATCH,避免 GET 执行修改操作
  3. 引入 API 版本化策略(v2 命名空间)
  4. 统一返回值格式:所有创建操作返回创建的资源,所有列表使用统一的分页结构

7.3 业务逻辑重构

  1. 抽取报酬计算服务 HonorariaCalculationService,统一管理 balance 计算逻辑
  2. 演示文稿生成异步化:使用消息队列或异步任务处理 PPT 合并
  3. 密码重置 token 存储到数据库或 Redis,支持多实例部署
  4. Speaker 删除改为软删除,保留数据审计能力
  5. 引入事件驱动:Speaker 状态变更、合同签署等关键操作发布领域事件

7.4 前端重构

  1. 升级技术栈:React 18+、React Router 6+、Ant Design 5.x、Redux Toolkit / Zustand
  2. 拆分 speakerDetail reducer:每个页面使用独立的状态切片
  3. 移除 dangerouslySetInnerHTML:使用 sanitize-html 或 DOMPurify
  4. 现代化代码分割:使用 React.lazy + Suspense 替代 System.import
  5. 类型安全:引入 TypeScript

7.5 安全改进

  1. 为所有写操作添加基于角色的访问控制(RBAC)
  2. 移除 Speaker 实体中的密码相关字段
  3. CompanyId 从 JWT token 中提取而非 HTTP Header
  4. 合同签署流程增加电子签名验证
  5. 敏感字段(SSN/Tax ID)加密存储