Skip to content

Survey & Feedback Domain - Deep Dive Analysis

1. Domain Overview

1.1 领域职责描述

Survey & Feedback 领域负责管理药品演讲项目相关的问卷调查功能,包括:

  • 问卷模板管理:创建、编辑、复制、激活/停用、删除问卷模板(由 Planner 在 plannerview 中操作)
  • 问卷与产品/项目类型绑定:将问卷模板关联到特定的产品和项目类型
  • 问卷填写:参会者(Attendee)、演讲者(Speaker)、销售代表(Sales Rep) 分别填写各自类型的问卷
  • 问卷结果统计:按问卷进行统计分析,支持按时间范围和产品筛选
  • 问卷结果下载:导出问卷结果为 Excel 文件
  • 历史数据迁移:将旧格式问卷数据迁移到新格式

1.2 涉及的后端模块和包

模块/包路径描述
survey modulemodules/v1/survey/问卷核心业务模块
entitycommon/persistence/entity/AttendeeSurvey.java问卷模板实体
entitycommon/persistence/entity/AttendeeSurveyResponse.java问卷回答实体
entitycommon/persistence/entity/SurveyMigrationLog.java数据迁移日志实体
mappercommon/persistence/mapper/AttendeeSurveyMapper问卷模板 Mapper
mappercommon/persistence/mapper/AttendeeSurveyResponseMapper问卷回答 Mapper
mappercommon/persistence/mapper/SurveyMigrationLogMapper迁移日志 Mapper

2. Data Model Analysis

2.1 Entity Overview Table

AttendeeSurvey (t_attendee_survey) - 问卷模板

字段类型说明
idInteger (PK, auto)问卷ID
nameString问卷名称
contentObject (JSONB)问卷内容(页面和字段定义),存储为 JSON
created_atDate创建时间
created_byString创建人
updated_atDate更新时间
updated_byString更新人
statusInteger状态: 0=停用, 1=激活
del_flagInteger删除标记: 0=未删除, 1=已删除
activated_atDate激活时间
typeInteger类型: 0=Attendee Survey, 1=Speaker Survey, 2=Sales Rep Survey, 3=Attendee Program Survey
productsObject (JSONB)关联的产品/项目类型列表,JSON 格式

AttendeeSurveyResponse (t_attendee_survey_response) - 问卷回答

字段类型说明
survey_idInteger关联的问卷ID (t_attendee_survey.id)
attendee_idString参会者ID
meeting_request_idInteger会议请求ID
responseObject (JSONB)回答内容,JSON 格式
created_atDate创建时间
user_idInteger用户ID

SurveyMigrationLog (t_survey_migration_log) - 迁移日志

字段类型说明
idInteger (PK, auto)日志ID
survey_idInteger新问卷ID
meeting_request_idInteger会议请求ID
old_survey_idInteger旧问卷ID
created_atDate迁移时间

2.2 Table Relationships (ER Diagram - ASCII)

t_attendee_survey (问卷模板)
  |-- id (PK)
  |-- products (JSONB) --> references t_product.product_id
  |                         and t_meeting_program_type.program_type_id
  |
  |-- 1:N --> t_attendee_survey_response (问卷回答)
  |             |-- survey_id (FK -> t_attendee_survey.id, 无DB约束)
  |             |-- attendee_id (-> t_attendee)
  |             |-- meeting_request_id (-> t_meeting_request)
  |
  |-- 1:N --> t_survey_migration_log (迁移日志)
                |-- survey_id (FK -> t_attendee_survey.id)
                |-- old_survey_id (-> t_survey 旧表)
                |-- meeting_request_id (-> t_meeting_request)

t_survey (旧问卷表, 用于数据迁移)
  |-- survey_id
  |-- meeting_request_id
  |-- survey (旧格式文本, 用 @-@ 和 @=@ 分隔)

2.3 Data Model Issues

DM-1: AttendeeSurveyResponse 缺少主键

  • 文件: common/persistence/entity/AttendeeSurveyResponse.java
  • AttendeeSurveyResponse 实体没有定义 @Id 注解的主键字段,完全依赖 survey_id + attendee_id + meeting_request_id 的组合作为逻辑标识
  • 这使得更新和删除操作困难,且无法使用 selectByPrimaryKey

DM-2: content 和 response 字段使用 Object 类型

  • 文件: common/persistence/entity/AttendeeSurvey.java:36, AttendeeSurveyResponse.java:37
  • contentresponse 字段都声明为 Object 类型,完全无类型约束
  • 在 Service 层大量使用 LinkedHashMap 和原始类型转换(如 (List<LinkedHashMap>)),缺乏类型安全

DM-3: products 字段使用 JSONB 存储关系数据

  • 文件: common/persistence/entity/AttendeeSurvey.java:76
  • 产品和项目类型的关联关系存储在 JSONB 字段中,而非独立的关联表
  • 导致 SQL 查询需要使用 jsonb_array_elements 展开,查询性能差且复杂

DM-4: 软删除与状态混合管理

  • del_flagstatus 两个字段分别控制删除和激活,增加了查询条件的复杂性
  • 每次查询都需要同时检查 del_flag = 0status

3. Business Flow Analysis

3.1 Core Business Flows (ASCII Flow Diagrams)

问卷创建与配置流程

Planner (plannerview)
  |
  v
[创建问卷模板] --> SurveyBuilder 组件 --> POST /v1/surveys
  |                                         |
  |                                         v
  |                                    createSurvey()
  |                                    - 清洗脏数据(cleanDirtyData)
  |                                    - 设置审计字段
  |                                    - 插入 t_attendee_survey
  |
  v
[配置产品/项目类型] --> Surveys/ProgramTree --> POST /v1/surveys/{id}/products
  |                                              |
  |                                              v
  |                                         setProducts()
  |                                         - 移除其他问卷的冲突绑定
  |                                         - 更新当前问卷的 products JSONB
  |
  v
[激活问卷] --> PUT /v1/surveys/{id}/activate
                |
                v
           activateSurvey()
           - 设置 status=1
           - 记录 activated_at

问卷填写流程

Attendee/Speaker/SalesRep (各前端)
  |
  v
[获取问卷模板] --> GET /v1/surveys/meetings/{meetingRequestId}/attendee-survey
  |                |
  |                v
  |           getSurvey(ATTENDEE_SURVEY, meetingRequestId, attendeeId, surveyId)
  |           - 查找会议关联的产品/项目类型
  |           - 匹配对应的问卷模板
  |           - 查找已有回答
  |           - 返回问卷内容 + 已有回答
  |
  v
[提交回答] --> POST /v1/surveys/{surveyId}/responses
                |
                v
           saveAttendeeSurveyResponse()
           - 验证会议存在
           - 检查项目是否关闭(enableCloseOutProgram)
           - 检查重复提交(Attendee 按 attendeeId 去重,其他按 meetingRequestId 去重)
           - 插入 t_attendee_survey_response

问卷统计与下载流程

Planner/SalesRep
  |
  v
[查看统计] --> GET /v1/surveys/{surveyId}/statistics?productId=X&startDate=Y&endDate=Z
  |              |
  |              v
  |         getStatisticsResult()
  |         - 获取问卷模板
  |         - 获取所有回答
  |         - 按字段统计各选项计数
  |         - 排除 InputBox 类型字段
  |
  v
[下载结果] --> GET /v1/surveys/{surveyId}/responses/download?productId=X
                |
                v
           downloadSurveyResponses()
           - 获取问卷模板字段定义
           - 查询所有回答及会议详情
           - 转换为 Excel 格式导出

3.2 Validation Rules

规则位置描述
名称唯一SurveyService.java:163问卷名称不能重复,通过数据库唯一约束实现
已有回答时更新需确认SurveyService.java:170更新已有回答的问卷时需 forceUpdate=true
更新后自动停用SurveyService.java:180更新问卷内容后自动设为 DEACTIVE 状态
回答不能重复提交SurveyService.java:366-372Attendee Survey 按 attendeeId 去重;非 AttendeeInProgram Survey 按 meetingRequestId 去重
关闭的项目不能提交SurveyService.java:346-348如果 enableCloseOutProgram 且项目已关闭,禁止提交问卷

3.3 Business Logic Issues

BL-1: Speaker Survey 使用硬编码的客户特定字段定义

  • 文件: pharmagin-speakerview/legacy/src/components/SpeakerSurvey/fields.js
  • Speaker Survey 没有使用后端的动态问卷模板系统,而是在前端硬编码了按 companyId 区分的字段定义
  • 支持的客户:Dendreon, Mayne, Verona, Noven,以及 default(commonList)
  • 提交格式使用旧格式 question@=@answer@-@question@=@answer,与新的 JSON 格式不兼容

BL-2: 数据迁移逻辑不应在 Service 层暴露为 API

  • 文件: SurveyController.java:158-161, SurveyService.java:859-967
  • processDataMigration 方法暴露为 GET 端点 /v1/surveys/migration?surveyId=X
  • 这是一次性迁移逻辑,不应该作为常规 API 保留
  • 迁移涉及旧的 t_survey 表和 t_meeting_request.ppr 字段

BL-3: removeUsedProgramTypes 的副作用

  • 文件: SurveyService.java:270-303
  • 当设置问卷的产品/项目类型时,会自动从其他问卷中移除冲突的绑定
  • 这种全局副作用缺乏事务一致性保护,且用户不知道其他问卷被修改

BL-4: 统计逻辑过于复杂且与模板耦合

  • 文件: SurveyService.java:487-569
  • 统计逻辑依赖于解析问卷模板结构,递归提取字段
  • 对 CheckBox 单选项(itemsArray.length == 1)有特殊处理逻辑(Yes/No)
  • @-@ 分隔符在常量和业务逻辑中同时使用,容易混淆

4. API Inventory

4.1 REST Endpoints Table

MethodPath描述Controller 行号
GET/v1/surveys分页查询问卷列表51
POST/v1/surveys创建问卷56
GET/v1/surveys/{surveyId}获取问卷详情62
PUT/v1/surveys/{surveyId}更新问卷67
PUT/v1/surveys/{surveyId}/name重命名问卷73
PUT/v1/surveys/{surveyId}/activate激活问卷79
PUT/v1/surveys/{surveyId}/deactivate停用问卷84
DELETE/v1/surveys/{surveyId}删除问卷(软删除)89
PUT/v1/surveys/{surveyId}/copy复制问卷95
GET/v1/surveys/products获取问卷产品列表101
POST/v1/surveys/{surveyId}/products设置问卷产品/项目类型106
GET/v1/surveys/{surveyId}/responses/{attendeeId}获取参会者回答112
POST/v1/surveys/{surveyId}/responses提交问卷回答117
GET/v1/surveys/{surveyId}/responses/download下载问卷结果122
GET/v1/surveys/responses/download下载会议问卷结果129
GET/v1/surveys/{surveyId}/statistics获取统计结果134
GET/v1/surveys/meetings/{meetingRequestId}/attendee-survey获取会议参会者问卷139
GET/v1/surveys/meetings/{meetingRequestId}/attendee-surveys获取会议所有参会者问卷列表144
GET/v1/surveys/meetings/{meetingRequestId}/speaker-survey获取会议演讲者问卷149
GET/v1/surveys/meetings/{meetingRequestId}/sales-rep-survey获取会议销售代表问卷154
GET/v1/surveys/migration执行数据迁移159

4.2 API Design Issues

API-1: 数据迁移端点使用 GET 方法

  • GET /v1/surveys/migration 执行数据写入操作,违反 REST 语义
  • 应为 POST 或直接移除

API-2: 重复的 attendee-survey 端点

  • /meetings/{id}/attendee-survey(单数)和 /meetings/{id}/attendee-surveys(复数)功能重叠
  • 前者返回单个问卷(含回答),后者返回问卷列表(含 URL)

API-3: 下载端点设计不一致

  • GET /v1/surveys/{surveyId}/responses/downloadGET /v1/surveys/responses/download 路径结构不统一
  • 前者需要 productId 查询参数,后者需要 meetingRequestId

API-4: copy 操作使用 PUT 而非 POST

  • PUT /v1/surveys/{surveyId}/copy 创建新资源,应使用 POST 方法

5. Frontend Analysis

5.1 Pages & Components

Plannerview (问卷管理)

组件路径描述
SurveyBuildercontainers/SurveyBuilder/index.js问卷构建器入口
SurveyBuilder/survey.jscontainers/SurveyBuilder/survey.js问卷模板编辑
SurveyBuilder/surveyForm.jscontainers/SurveyBuilder/surveyForm.js问卷表单渲染
SurveyBuilder/surveyResult.jscontainers/SurveyBuilder/surveyResult.js问卷结果显示
SurveyBuilder/Headercontainers/SurveyBuilder/components/Header/index.js问卷编辑器头部
Surveyscomponents/Surveys/index.js问卷列表管理
Surveys/ProgramTreecomponents/Surveys/ProgramTree.js产品/项目类型树选择
Surveys/RenameSurveycomponents/Surveys/RenameSurvey.js重命名弹窗
Surveys/SurveyFormcomponents/Surveys/SurveyForm.js问卷表单
Surveys/SurveyTypecomponents/Surveys/SurveyType.js问卷类型选择
AttendeeSurveyscontainers/AttendeeSurveys/index.js参会者问卷列表
AttendeeSurveys/QRCodeModalcontainers/AttendeeSurveys/QRCodeModal.jsQR码弹窗
AttendeeSurveys/SurveyResultcontainers/AttendeeSurveys/SurveyResult.js问卷结果视图
AttendeeSurveys/SurveyResultViewcontainers/AttendeeSurveys/SurveyResultView.js问卷结果详情
RegReport/SendSurveycomponents/RegReport/SendSurvey.js发送问卷链接

Speakerview (演讲者问卷)

组件路径描述
SpeakerSurveycomponents/SpeakerSurvey/index.js演讲者问卷填写弹窗
SpeakerSurvey/fieldscomponents/SpeakerSurvey/fields.js硬编码的问卷字段定义(按客户区分)

Salesview (销售代表问卷)

组件路径描述
Surveyspages/Programs/Profile/Surveys/index.js项目问卷列表
Surveys/SurveyResultpages/Programs/Profile/Surveys/SurveyResult.js问卷结果
Surveys/SurveyResultViewpages/Programs/Profile/Surveys/SurveyResultView.js问卷结果详情
ProgramEvaluationpages/Programs/Profile/ProgramEvaluation/index.js项目评估(含问卷)
Reports/Surveypages/Reports/Survey/index.js问卷报表
Reports/Survey/SurveyProfilepages/Reports/Survey/SurveyProfile.js问卷统计详情
Reports/Survey/SurveyResultpages/Reports/Survey/SurveyResult.js问卷结果
Reports/OtherSurveypages/Reports/OtherSurvey/index.js其他类型问卷报表
ExternalContainer/SalesRepSurveypages/ExternalContainer/SalesRepSurvey.js外部销售代表问卷
QRCodeModalpages/Programs/Modal/QRCodeModal.jsQR码弹窗

5.2 Redux State Structure

Plannerview

state.surveyBuilder:
  - survey: Object           // 当前编辑的问卷
  - loading: Boolean         // 加载状态
  - error: String           // 错误信息

state.surveys:
  - list: Array             // 问卷列表
  - loading: Boolean
  - checkedProgramTypes: Object  // 已选中的项目类型

Speakerview

  • 问卷状态嵌入在 state.appstate.speakerDetail

Salesview

state.programsSurveys:
  - surveys: Array          // 问卷列表
  - loading: Boolean
  - surveyResult: Object    // 问卷结果

5.3 Frontend Issues

FE-1: 两套完全不同的问卷系统并存

  • 后端提供了动态问卷模板系统(SurveyBuilder),支持 Attendee Survey 和 Sales Rep Survey
  • 但 Speaker Survey 在 speakerview 中使用完全不同的硬编码字段系统(fields.js
  • Speaker Survey 数据格式为 @-@ 分隔的字符串,而其他问卷使用 JSON

FE-2: Speaker Survey 的客户特定逻辑

  • fields.js 中按 companyId 硬编码了 Dendreon、Mayne、Verona、Noven 的问卷
  • 新增客户需要修改前端代码并重新部署
  • 应统一使用后端动态问卷模板

FE-3: SurveyBuilder 使用 CRACO 旧配置

  • SurveyBuilder 组件使用 React 15.x-16.x 和 Ant Design 3.x
  • 问卷表单渲染逻辑复杂,可维护性差

6. Problem Summary

6.1 Critical Issues (must fix in rewrite)

  1. Speaker Survey 与 Attendee Survey 使用不同系统 - 两套问卷系统并存,数据格式不兼容,应统一为一套动态问卷模板系统
  2. AttendeeSurveyResponse 缺少主键 - 无法高效地查询、更新、删除单条回答
  3. 问卷内容使用无类型的 Object/JSONB - 缺乏类型安全,Service 层充斥原始类型转换

6.2 Design Defects (should improve)

  1. products 关系存储在 JSONB 中 - 应使用关联表,避免 jsonb_array_elements 的复杂查询
  2. 数据迁移 API 残留 - GET /v1/surveys/migration 应移除,迁移逻辑不应作为运行时 API
  3. API 方法语义不规范 - copy 用 PUT、migration 用 GET,违反 REST 约定
  4. removeUsedProgramTypes 隐式副作用 - 设置产品关联时自动修改其他问卷,缺乏透明性
  5. 统计逻辑与问卷解析耦合 - 统计计算依赖递归解析 JSONB 模板结构

6.3 Technical Debt (nice to have)

  1. ActivateRequest 和 TypeRequest 类未使用 - ActivateRequest.java 为空类,TypeRequest.java 未被任何 Controller 引用
  2. SurveyResponseRequest.createdAt 的 TODO - 第16行注释 "removing this field after completing survey data migration"
  3. 多个 email 模板按 companyId 硬编码 - templates/survey/ 下有15个不同的模板文件
  4. 问卷模板中的 cleanDirtyData 逻辑 - 每次保存都需要清洗数据,应在前端规范数据格式
  5. @-@@=@ 分隔符 - 旧格式数据使用非标准分隔符,与 JSON 格式混用

7. Rewrite Recommendations

  1. 统一问卷系统 - 废弃 speakerview 中的硬编码 Speaker Survey,将 Speaker Survey 也纳入动态问卷模板系统(type=1)。前端统一使用 SurveyBuilder 组件渲染问卷。

  2. 重新设计数据模型:

    • AttendeeSurveyResponse 添加自增主键
    • products JSONB 字段拆分为 t_survey_product(survey_id, product_id)和 t_survey_program_type(survey_id, product_id, program_type_id)关联表
    • contentresponse 定义强类型的 DTO,使用 Jackson 的类型映射
  3. 简化问卷模板结构 - 定义清晰的问卷模板 Schema(Page -> Field -> Option),替代当前的 Map<String, List<Map<String, Object>>> 嵌套结构

  4. 移除迁移代码 - 完成数据迁移后删除 processDataMigration 方法、SurveyMigrationLog 实体、SurveyMigration 响应类,以及 SurveyMigrationLogMapper

  5. API 重新设计:

    • 统一下载端点: GET /v1/surveys/{surveyId}/export
    • 删除重复的 attendee-survey/attendee-surveys 端点
    • 使用 POST 进行 copy 操作
    • 移除 migration 端点
  6. 前端组件整合 - 三个前端应用共用相同的问卷渲染组件(可提取为共享库),只在入口和交互方式上有差异