Survey & Feedback Domain - Deep Dive Analysis
1. Domain Overview
1.1 领域职责描述
Survey & Feedback 领域负责管理药品演讲项目相关的问卷调查功能,包括:
- 问卷模板管理:创建、编辑、复制、激活/停用、删除问卷模板(由 Planner 在 plannerview 中操作)
- 问卷与产品/项目类型绑定:将问卷模板关联到特定的产品和项目类型
- 问卷填写:参会者(Attendee)、演讲者(Speaker)、销售代表(Sales Rep) 分别填写各自类型的问卷
- 问卷结果统计:按问卷进行统计分析,支持按时间范围和产品筛选
- 问卷结果下载:导出问卷结果为 Excel 文件
- 历史数据迁移:将旧格式问卷数据迁移到新格式
1.2 涉及的后端模块和包
| 模块/包 | 路径 | 描述 |
|---|---|---|
| survey module | modules/v1/survey/ | 问卷核心业务模块 |
| entity | common/persistence/entity/AttendeeSurvey.java | 问卷模板实体 |
| entity | common/persistence/entity/AttendeeSurveyResponse.java | 问卷回答实体 |
| entity | common/persistence/entity/SurveyMigrationLog.java | 数据迁移日志实体 |
| mapper | common/persistence/mapper/AttendeeSurveyMapper | 问卷模板 Mapper |
| mapper | common/persistence/mapper/AttendeeSurveyResponseMapper | 问卷回答 Mapper |
| mapper | common/persistence/mapper/SurveyMigrationLogMapper | 迁移日志 Mapper |
2. Data Model Analysis
2.1 Entity Overview Table
AttendeeSurvey (t_attendee_survey) - 问卷模板
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Integer (PK, auto) | 问卷ID |
| name | String | 问卷名称 |
| content | Object (JSONB) | 问卷内容(页面和字段定义),存储为 JSON |
| created_at | Date | 创建时间 |
| created_by | String | 创建人 |
| updated_at | Date | 更新时间 |
| updated_by | String | 更新人 |
| status | Integer | 状态: 0=停用, 1=激活 |
| del_flag | Integer | 删除标记: 0=未删除, 1=已删除 |
| activated_at | Date | 激活时间 |
| type | Integer | 类型: 0=Attendee Survey, 1=Speaker Survey, 2=Sales Rep Survey, 3=Attendee Program Survey |
| products | Object (JSONB) | 关联的产品/项目类型列表,JSON 格式 |
AttendeeSurveyResponse (t_attendee_survey_response) - 问卷回答
| 字段 | 类型 | 说明 |
|---|---|---|
| survey_id | Integer | 关联的问卷ID (t_attendee_survey.id) |
| attendee_id | String | 参会者ID |
| meeting_request_id | Integer | 会议请求ID |
| response | Object (JSONB) | 回答内容,JSON 格式 |
| created_at | Date | 创建时间 |
| user_id | Integer | 用户ID |
SurveyMigrationLog (t_survey_migration_log) - 迁移日志
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Integer (PK, auto) | 日志ID |
| survey_id | Integer | 新问卷ID |
| meeting_request_id | Integer | 会议请求ID |
| old_survey_id | Integer | 旧问卷ID |
| created_at | Date | 迁移时间 |
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 content和response字段都声明为Object类型,完全无类型约束- 在 Service 层大量使用
LinkedHashMap和原始类型转换(如(List<LinkedHashMap>)),缺乏类型安全
DM-3: products 字段使用 JSONB 存储关系数据
- 文件:
common/persistence/entity/AttendeeSurvey.java:76 - 产品和项目类型的关联关系存储在 JSONB 字段中,而非独立的关联表
- 导致 SQL 查询需要使用
jsonb_array_elements展开,查询性能差且复杂
DM-4: 软删除与状态混合管理
del_flag和status两个字段分别控制删除和激活,增加了查询条件的复杂性- 每次查询都需要同时检查
del_flag = 0和status
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-372 | Attendee 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
| Method | Path | 描述 | 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/download和GET /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 (问卷管理)
| 组件 | 路径 | 描述 |
|---|---|---|
| SurveyBuilder | containers/SurveyBuilder/index.js | 问卷构建器入口 |
| SurveyBuilder/survey.js | containers/SurveyBuilder/survey.js | 问卷模板编辑 |
| SurveyBuilder/surveyForm.js | containers/SurveyBuilder/surveyForm.js | 问卷表单渲染 |
| SurveyBuilder/surveyResult.js | containers/SurveyBuilder/surveyResult.js | 问卷结果显示 |
| SurveyBuilder/Header | containers/SurveyBuilder/components/Header/index.js | 问卷编辑器头部 |
| Surveys | components/Surveys/index.js | 问卷列表管理 |
| Surveys/ProgramTree | components/Surveys/ProgramTree.js | 产品/项目类型树选择 |
| Surveys/RenameSurvey | components/Surveys/RenameSurvey.js | 重命名弹窗 |
| Surveys/SurveyForm | components/Surveys/SurveyForm.js | 问卷表单 |
| Surveys/SurveyType | components/Surveys/SurveyType.js | 问卷类型选择 |
| AttendeeSurveys | containers/AttendeeSurveys/index.js | 参会者问卷列表 |
| AttendeeSurveys/QRCodeModal | containers/AttendeeSurveys/QRCodeModal.js | QR码弹窗 |
| AttendeeSurveys/SurveyResult | containers/AttendeeSurveys/SurveyResult.js | 问卷结果视图 |
| AttendeeSurveys/SurveyResultView | containers/AttendeeSurveys/SurveyResultView.js | 问卷结果详情 |
| RegReport/SendSurvey | components/RegReport/SendSurvey.js | 发送问卷链接 |
Speakerview (演讲者问卷)
| 组件 | 路径 | 描述 |
|---|---|---|
| SpeakerSurvey | components/SpeakerSurvey/index.js | 演讲者问卷填写弹窗 |
| SpeakerSurvey/fields | components/SpeakerSurvey/fields.js | 硬编码的问卷字段定义(按客户区分) |
Salesview (销售代表问卷)
| 组件 | 路径 | 描述 |
|---|---|---|
| Surveys | pages/Programs/Profile/Surveys/index.js | 项目问卷列表 |
| Surveys/SurveyResult | pages/Programs/Profile/Surveys/SurveyResult.js | 问卷结果 |
| Surveys/SurveyResultView | pages/Programs/Profile/Surveys/SurveyResultView.js | 问卷结果详情 |
| ProgramEvaluation | pages/Programs/Profile/ProgramEvaluation/index.js | 项目评估(含问卷) |
| Reports/Survey | pages/Reports/Survey/index.js | 问卷报表 |
| Reports/Survey/SurveyProfile | pages/Reports/Survey/SurveyProfile.js | 问卷统计详情 |
| Reports/Survey/SurveyResult | pages/Reports/Survey/SurveyResult.js | 问卷结果 |
| Reports/OtherSurvey | pages/Reports/OtherSurvey/index.js | 其他类型问卷报表 |
| ExternalContainer/SalesRepSurvey | pages/ExternalContainer/SalesRepSurvey.js | 外部销售代表问卷 |
| QRCodeModal | pages/Programs/Modal/QRCodeModal.js | QR码弹窗 |
5.2 Redux State Structure
Plannerview
state.surveyBuilder:
- survey: Object // 当前编辑的问卷
- loading: Boolean // 加载状态
- error: String // 错误信息
state.surveys:
- list: Array // 问卷列表
- loading: Boolean
- checkedProgramTypes: Object // 已选中的项目类型Speakerview
- 问卷状态嵌入在
state.app或state.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)
- Speaker Survey 与 Attendee Survey 使用不同系统 - 两套问卷系统并存,数据格式不兼容,应统一为一套动态问卷模板系统
- AttendeeSurveyResponse 缺少主键 - 无法高效地查询、更新、删除单条回答
- 问卷内容使用无类型的 Object/JSONB - 缺乏类型安全,Service 层充斥原始类型转换
6.2 Design Defects (should improve)
- products 关系存储在 JSONB 中 - 应使用关联表,避免
jsonb_array_elements的复杂查询 - 数据迁移 API 残留 -
GET /v1/surveys/migration应移除,迁移逻辑不应作为运行时 API - API 方法语义不规范 - copy 用 PUT、migration 用 GET,违反 REST 约定
- removeUsedProgramTypes 隐式副作用 - 设置产品关联时自动修改其他问卷,缺乏透明性
- 统计逻辑与问卷解析耦合 - 统计计算依赖递归解析 JSONB 模板结构
6.3 Technical Debt (nice to have)
- ActivateRequest 和 TypeRequest 类未使用 -
ActivateRequest.java为空类,TypeRequest.java未被任何 Controller 引用 - SurveyResponseRequest.createdAt 的 TODO - 第16行注释 "removing this field after completing survey data migration"
- 多个 email 模板按 companyId 硬编码 -
templates/survey/下有15个不同的模板文件 - 问卷模板中的 cleanDirtyData 逻辑 - 每次保存都需要清洗数据,应在前端规范数据格式
@-@和@=@分隔符 - 旧格式数据使用非标准分隔符,与 JSON 格式混用
7. Rewrite Recommendations
统一问卷系统 - 废弃 speakerview 中的硬编码 Speaker Survey,将 Speaker Survey 也纳入动态问卷模板系统(type=1)。前端统一使用 SurveyBuilder 组件渲染问卷。
重新设计数据模型:
- 为
AttendeeSurveyResponse添加自增主键 - 将
productsJSONB 字段拆分为t_survey_product(survey_id, product_id)和t_survey_program_type(survey_id, product_id, program_type_id)关联表 - 为
content和response定义强类型的 DTO,使用 Jackson 的类型映射
- 为
简化问卷模板结构 - 定义清晰的问卷模板 Schema(Page -> Field -> Option),替代当前的
Map<String, List<Map<String, Object>>>嵌套结构移除迁移代码 - 完成数据迁移后删除
processDataMigration方法、SurveyMigrationLog实体、SurveyMigration响应类,以及SurveyMigrationLogMapperAPI 重新设计:
- 统一下载端点:
GET /v1/surveys/{surveyId}/export - 删除重复的 attendee-survey/attendee-surveys 端点
- 使用 POST 进行 copy 操作
- 移除 migration 端点
- 统一下载端点:
前端组件整合 - 三个前端应用共用相同的问卷渲染组件(可提取为共享库),只在入口和交互方式上有差异