Content & Document Domain - Deep Dive Analysis
1. Domain Overview
1.1 领域职责描述
Content & Document 域负责平台中所有文档和内容资产的管理,涵盖三个核心子领域:
- 文档模板管理(XDocReport):管理 Word 文档模板(.docx),支持 Velocity 模板引擎的合并字段(merge fields),用于生成会议相关文档(签到表、确认信、合同等)。支持 DOCX、PDF、XHTML 三种输出格式。
- 文件管理(File):通用文件上传/下载服务,支持多种文件类型(演示文稿、培训材料、图片、附件等),基于本地文件系统存储,使用 MD5 哈希命名去重。
- 内容库管理(Content Library):管理演讲者可用的内容资产,包括演示文稿(presentations)、模块(modules)、案例研究(case studies)和其他内容(other content)。支持幻灯片级别的管理和演讲者定制化演示。
1.2 涉及的后端模块和包
| 模块/包 | 路径 | 职责 |
|---|---|---|
| xdoc 模块 | modules/v1/xdoc/ | 文档模板 CRUD、XDocReport 文档生成、合并字段管理 |
| file 模块 | modules/v1/file/ | 通用文件上传/下载/列表 |
| speaker/ContentController | modules/v1/speaker/controller/ContentController.java | 内容库 API(演示文稿、模块、幻灯片管理) |
| site/AttendeeDownloadController | modules/v1/site/controller/AttendeeDownloadController.java | 与会者文档下载入口 |
| compliance/ComplianceController | modules/v1/compliance/controller/ComplianceController.java | 合规文档清单管理 |
| FileServer 枚举 | common/utils/FileServer.java | 文件存储路径映射(26种文件类型) |
2. Data Model Analysis
2.1 Entity Overview Table
核心文档/模板实体
| 实体 | 表名 | 主键 | 字段数 | 文件位置 | 说明 |
|---|---|---|---|---|---|
| Template | t_template | template_id (seq) | 10 | entity/Template.java:25 | 文档模板配置(关联模板类型、产品、项目类型) |
| TemplateType | t_template_type | template_type_id (seq) | 5 | entity/TemplateType.java:24 | 模板类型定义(如签到表、确认信等) |
| Document | t_document | document_id (seq) | 7 | entity/Document.java:8 | 演讲者文档(W-9、简历等),关联 speaker_id |
| File | t_file | file_id (seq) | 5 | entity/File.java:7 | 通用文件元数据(文件名、路径、大小、类型) |
| Content | t_content | id (seq) | 13 | entity/Content.java:22 | 内容资产(演示文稿、培训模块、其他内容、视频) |
关联/映射实体
| 实体 | 表名 | 主键 | 字段数 | 文件位置 | 说明 |
|---|---|---|---|---|---|
| ContentGroup | t_content_group | content_id + group_id | 2 | entity/ContentGroup.java:7 | 内容与演讲者组的多对多关联 |
| ContentTopic | t_content_topic | content_id + topic_id | 2 | entity/ContentTopic.java:7 | 内容与主题的多对多关联 |
| ComplianceDocument | t_compliance_document | id (seq) | 9 | entity/ComplianceDocument.java:22 | 合规文档清单定义 |
| MeetingComplianceDocument | t_meeting_compliance_document | meeting_request_id + compliance_document_id | 4 | entity/MeetingComplianceDocument.java:22 | 会议与合规文档的关联(含文件和审核状态) |
| MappingContractDocument | t_mapping_contract_document | contract_id + document_id | 2 | entity/MappingContractDocument.java:7 | 合同与文档的多对多关联 |
注册站点模板实体(Registration Site Builder)
| 实体 | 表名 | 主键 | 字段数 | 文件位置 | 说明 |
|---|---|---|---|---|---|
| SiteTemplate | t_site_template | attendee_type_id | 14 | entity/SiteTemplate.java:21 | 注册站点样式配置(导航、颜色、banner) |
| SiteTemplatePage | t_site_template_page | page_id (seq) | 6 | entity/SiteTemplatePage.java:7 | 注册站点页面定义 |
| SiteTemplateField | t_site_template_field | field_id (seq) | 23 | entity/SiteTemplateField.java:21 | 注册站点字段定义(含验证、格式、报告映射) |
其他模板实体(非文档模板,属于其他域但名称包含 Template)
| 实体 | 表名 | 主键 | 文件位置 | 说明 |
|---|---|---|---|---|
| BudgetItemTemplate | t_budget_item_template | budget_item_template_id | entity/BudgetItemTemplate.java:9 | 预算模板(属于 Budget 域) |
| ProjectTaskTemplate | t_project_task_template | id | entity/ProjectTaskTemplate.java:21 | 项目任务模板(属于 ProjectTask 域) |
2.2 Table Relationships (ER Diagram - ASCII)
┌──────────────────┐
│ t_template_type │
│──────────────────│
│ template_type_id │◄─────────────────────┐
│ name │ │
│ filename │ │
│ description │ │
│ status │ │
│ template_type_ │ │
│ context_id │ │
└──────────────────┘ │
│
┌─────────────┐ ┌──────────────────┐ │
│ t_product │◄──── │ t_template │──────────────────────┘
│ product_id │ │──────────────────│
└─────────────┘ │ template_id │
│ template_type_id │──► t_template_type
┌─────────────┐ │ product_id │──► t_product
│ t_meeting_ │◄──── │ program_type_id │──► t_meeting_program_type
│ program_type│ │ program_service_id│──► t_meeting_program_service_type
└─────────────┘ │ company_id │
│ status │
│ template_name │
│ template_description│
│ created_at/by │
│ updated_at/by │
└──────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ t_content │ │ t_file │
│──────────────────│ │────────────────── │
│ id │ │ file_id │
│ content_name │ │ file_name │
│ type (0-3) │ │ content_type │
│ sub_type (0-5) │ │ type │
│ product_id │──► │ path │
│ file_id │──────►│ size │
│ description │ └────────────────── │
│ module_maximum │ ▲
│ case_study_max │ │
│ company_id │ │
│ expiration_date │ ┌──────────────────┐
│ comment │ │ t_document │
│ url │ │──────────────────│
│ del_flag │ │ document_id │
└──────────────────┘ │ file_id │──────►t_file
│ │ │ document_name │
│ │ │ document_type_id │
▼ ▼ │ speaker_id │──────►t_speaker
┌──────────┐ ┌──────────┐ │ description │
│t_content │ │t_content │ │ create_date │
│ _group │ │ _topic │ │ deleted │
│──────────│ │──────────│ └────────────────── │
│content_id│ │content_id│
│group_id │ │topic_id │
└──────────┘ └──────────┘
┌───────────────────────────┐ ┌────────────────────────────────┐
│ t_compliance_document │ │ t_meeting_compliance_document │
│───────────────────────────│ │────────────────────────────────│
│ id │◄────│ compliance_document_id │
│ name │ │ meeting_request_id │──►t_meeting_request
│ description │ │ files (JSON) │
│ uploaded_by (0:planner, │ │ status (0:unready, 1:ready) │
│ 1:sales rep) │ │ ready_for_review_time │
│ sequence │ └────────────────────────────────┘
│ config (JSON) │
│ status │
│ created_at/by │
│ updated_at/by │
└───────────────────────────┘
┌─────────────────────────────┐
│ t_mapping_contract_document │
│─────────────────────────────│
│ contract_id │──► t_speaker_contract
│ document_id │──► t_document
└─────────────────────────────┘2.3 Data Model Issues
DM-1: Content.type 使用魔法数字,无枚举定义
entity/Content.java:34- type 字段注释0: presentation 1: online training modules 2: other content 3: videoentity/Content.java:40- sub_type 字段注释0: core 1: module 2: case study 3: iSpring 4: video 5: other- 这些类型码散布在前后端各处,没有统一的枚举类。前端 saga 中用
_params.type == 0/_params.type == 2硬编码判断(speakerview/ContentLibrary/sagas.js:111-114)
DM-2: File.type 与 FileServer 枚举存在隐式映射但未强制约束
FileServer.java定义了 26 种文件类型(0-25),但File.type是 Integer 字段无数据库约束FileQuery.java:11注释写了0:document 1:presentation 2:training 3:other 4:site builder 5:attachment 6:image但不完整
DM-3: Document 实体使用软删除但命名不一致
Document.java:31使用deleted(Integer)Content.java:56使用del_flag(Integer)- 其他实体用
status表示激活/删除
DM-4: SiteTemplate 主键设计异常
SiteTemplate.java:24- 使用attendee_type_id作为唯一 @Id,但实际逻辑上应该是attendee_type_id + meeting_id复合主键。当前定义可能导致同一 meeting 的不同 attendee type 无法各自有模板
DM-5: MeetingComplianceDocument.files 使用 JSON 字段存储文件列表
MeetingComplianceDocument.java:32-files字段类型为Object,使用JdbcType.OTHER- 缺乏类型安全,无法在数据库层面做约束和关联查询
DM-6: Content 和 Document 实体职责重叠
Content(t_content)用于管理内容库中的演示文稿等内容资产Document(t_document)用于管理演讲者的个人文档(W-9、简历等)- 两者都通过
file_id关联File实体,但没有统一的抽象层
DM-7: TemplateType.templateTypeContextId 缺乏外键约束
- 该字段对应
TemplateTypeContext枚举(1=Meeting, 2=Speaker),但数据库中无约束 TemplateTypeContext.java:10-11仅在 Java 枚举中定义
3. Business Flow Analysis
3.1 Core Business Flows
3.1.1 文档模板生成流程(XDocReport)
Planner/SalesRep 前端 后端 API 文件系统
│ │ │
│ 选择文档类型 + 格式(DOCX/PDF) │ │
├─────────────────────────────────────► │ │
│ GET /api/v1/attendees/download/{type}│ │
│ ?meetingRequestId=X&fileFormat=pdf │ │
│ │ │
│ AttendeeDownloadController │
│ │ │
│ ▼ │
│ DocumentTemplateService.download() │
│ │ │
│ ├── 1. 获取 ProgramResponse │
│ │ (programService.getProgramDetail) │
│ │ │
│ ├── 2. 查找 TemplateType │
│ │ (templateTypeMapper.selectByPrimaryKey) │
│ │ │
│ ├── 3. 查找最匹配的 Template │
│ │ (templateMapper.getAvailableTemplate) │
│ │ 优先级: serviceType > programType > │
│ │ productId > 全局 │
│ │ │
│ ├── 4. 读取模板文件 │
│ │ ┌────────────────────────────────────► │
│ │ │ doctemplates/{filename}_{productId} │
│ │ │ _{programTypeId}_{serviceTypeId}.docx │
│ │ └────────────────────────────────────── │
│ │ │
│ ├── 5. 加载 XDocReport (Velocity 引擎) │
│ │ │
│ ├── 6. 填充上下文 (populateContext) │
│ │ ├ meetingrequest: 会议信息 │
│ │ ├ registration/meeting: 注册站点信息 │
│ │ ├ speaker/speaker2: 演讲者信息 │
│ │ ├ planner: 计划者信息 │
│ │ ├ salesrep: 销售代表信息 │
│ │ ├ attendees: 与会者列表 │
│ │ ├ attendee: 单个与会者(可选) │
│ │ ├ speakerImage: 演讲者照片 │
│ │ ├ ppcSignatureImage: PPC 签名图片 │
│ │ ├ regQRCode: 注册二维码 │
│ │ └ utility: 工具方法(换行、制表等) │
│ │ │
│ ├── 7. 格式转换输出 │
│ │ ├ DOCX: 直接输出 │
│ │ ├ PDF: Docx4J 本地转换 │
│ │ │ 或 CloudConvert API 远程转换 │
│ │ └ XHTML: Docx4J HTML 导出 │
│ │ │
│ ◄────────────────────────┤ 8. 流式写入 HttpServletResponse │
│ (下载文件) │ │3.1.2 文件上传/下载流程
前端 FileController FileService 文件系统
│ │ │ │
│ POST /api/v1/files │ │ │
│ (uploadFile + type) │ │ │
├────────────────────────────────────►│ │ │
│ ├──────────────────────────────►│ │
│ │ createFile(uploadFile, type) │ │
│ │ │ │
│ │ ├── 1. 确定存储路径 │
│ │ │ FileServer.getPathByType │
│ │ │ (如 type=0 → "speakerdocument") │
│ │ │ │
│ │ ├── 2. 保存临时文件 │
│ │ │ uploadFile.transferTo() │
│ │ │ ──────────────────────► │
│ │ │ │
│ │ ├── 3. 计算 MD5 哈希 │
│ │ │ Files.hash(file, MD5) │
│ │ │ │
│ │ ├── 4. 以哈希值重命名文件 │
│ │ │ Files.move(file, md5.ext)│
│ │ │ ──────────────────────► │
│ │ │ │
│ │ ├── 5. 写入 t_file 记录 │
│ │ │ path = relativePath/md5.ext │
│ │ │ size = bytes / 1024 │
│ │ │ │
│ ◄──────────────────────────────────│◄──────────────────────────────│ │
│ {fileId, fileName, type, url, size}│ │ │
│ │ │ │
│ GET /api/v1/files/{id}/download │ │ │
├────────────────────────────────────►│ │ │
│ ├──────────────────────────────►│ │
│ │ downloadFile(id, response) │ │
│ │ ├── 查询 t_file 获取 path │
│ │ │ │
│ │ ├── 读取文件流 │
│ │ │ ◄────────────────────── │
│ │ │ │
│ ◄──────────────────────────────────│◄──────────────────────────────│ │
│ (文件流) │ application/octet-stream │ │3.1.3 内容库管理流程(演示文稿)
Speakerview Admin ContentController ContentService
│ │ │
│ POST /api/v1/contents │ │
│ {productId, type:0, │ │
│ subType:0, fileId, name} │ │
├──────────────────────────────►│ │
│ │ createContent(request) │
│ ├──────────────────────────────►│
│ │ │ 保存 Content + 关联 File
│ │ │
│ GET /api/v1/contents │ │
│ ?type=0&pageSize=10 │ │
├──────────────────────────────►│ │
│ │ listContents(query) │
│ ├──────────────────────────────►│
│ │ │ 根据 type 过滤
│ │ │ Admin: 全部可见
│ │ │ Speaker: 按 group 过滤
│ │ │
│ PUT /api/v1/contents/groups │ │
│ {contentIds, groupIds} │ │
├──────────────────────────────►│ │
│ │ setContentGroup(request) │
│ ├──────────────────────────────►│
│ │ │ 更新 t_content_group
│ │ │
│ PUT /api/v1/contents/{id}/ │ │
│ topics {topicIds} │ │
├──────────────────────────────►│ │
│ │ updateTopic(id, request) │
│ ├──────────────────────────────►│
│ │ │ 更新 t_content_topic
│ │ │
│ PUT /api/v1/contents/{id}/ │ │
│ conversion {subType} │ │
├──────────────────────────────►│ │
│ │ convertModule(id, request) │
│ ├──────────────────────────────►│
│ │ │ Core ←→ Module 转换3.1.4 演讲者合同文档生成流程
Speakerview 前端 SpeakerContract 相关 Controller DocumentTemplateService
│ │ │
│ 下载合同文档 │ │
├──────────────────────────────►│ │
│ │ downloadSpeakerTemplate │
│ │ (contractId, format) │
│ ├──────────────────────────────►│
│ │ │
│ │ ├── 1. 查询 SpeakerContract
│ │ ├── 2. 查询 SpeakerProfile
│ │ ├── 3. 查询 Template (contract.templateId)
│ │ ├── 4. 查询 TemplateType
│ │ ├── 5. 读取模板文件 (Speaker context)
│ │ │ 文件名: {filename}_{templateName}.docx
│ │ ├── 6. 填充上下文:
│ │ │ consultant = SpeakerProfile
│ │ │ contract = ContractResponse
│ │ │ currentDate = 当前日期
│ │ ├── 7. 格式转换输出
│ │ │
│ ◄────────────────────────────│◄──────────────────────────────│
│ (下载合同 DOCX/PDF) │ │3.2 Validation Rules
模板上传验证
TemplateService.java:74-79- 上传时用 XDocReport 引擎试加载文件,空上下文处理,验证是否为合法 Velocity 模板- 不验证文件大小限制
- 不验证文件扩展名(仅依赖处理失败来拒绝)
文件上传验证
FileService.java:56-82- 仅接受MultipartFile,无文件类型白名单检查- 无文件大小限制验证
- 无恶意内容扫描
内容管理验证
ContentController.java:78-@Validated @RequestBody CreateContentRequest使用 Bean Validation- 内容删除为硬删除 + 软删除混用模式
模板查询匹配规则
TemplateMapper.xml:36-41- 模板选择采用"最具体匹配"策略:- 优先匹配
serviceType + programType + productId完全匹配 - 然后逐级放宽到 NULL(使用
NULLS LAST排序 +LIMIT 1) - 这意味着通用模板(productId=NULL)是 fallback
- 优先匹配
3.3 Business Logic Issues
BL-1: PDF 转换存在两条路径,逻辑混乱
DocumentTemplateService.java:345-401- PDF 转换根据 templateType.filename 判断使用哪种方式:SOW、MSA、PostProgramCertificationTemplate→ 使用 Docx4J 本地转换- 其他 → 使用 CloudConvert API 远程转换
- 硬编码文件名判断哪些模板使用哪种 PDF 引擎,极不灵活
- CloudConvert API 调用无错误重试、无超时控制
BL-2: 多线程模板处理使用原始 Thread 而非线程池
DocumentTemplateService.java:315-325- XHTML 转换中用new Thread(new Runnable()...)启动异步处理DocumentTemplateService.java:351-361- PDF 转换同样使用裸线程- 无异常传播机制,线程失败时仅日志记录
BL-3: 临时文件清理不可靠
DocumentTemplateService.java:388-394- PDF 转换先写临时文件再转换,使用tempFile.delete()清理DocumentTemplateService.java:557-571- QR 码生成同样创建临时文件- 若中间发生异常,临时文件不会被清理(无 finally 块保护)
BL-4: replaceNull 方法隐藏了空值问题
DocumentTemplateService.java多处调用replaceNull()方法(继承自 AbstractService),将 null 字段替换为空字符串- 这掩盖了数据完整性问题,应在数据层解决
BL-5: FileService 中存在 SQL 注入风险
FileService.java:42-criteria.andCondition(" UPPER(file_name) LIKE '%" + fileName.toUpperCase() + "%'")直接拼接字符串- 用户输入的
fileName未做转义处理
BL-6: 文件存储使用 MD5 哈希命名,存在碰撞风险
FileService.java:65-Files.hash(file, Hashing.md5())使用 MD5 生成文件名- MD5 已被证明不安全(碰撞攻击),虽然在文件命名场景风险较低
- 更重要的问题:同一文件上传多次会覆盖前一个文件的数据库记录但物理文件相同
BL-7: 模板文件名生成规则不支持 Speaker 上下文的完整匹配
TemplateService.java:217-234- MEETING 上下文按productId_programTypeId_serviceTypeId后缀生成文件名- SPEAKER 上下文按
templateName后缀生成,但 switch 语句缺少break导致 SPEAKER case 会 fall through 到 default
BL-8: AttendeeSignatureUtils 中的签名渲染使用固定分辨率
AttendeeSignatureUtils.java:24-27- 硬编码TEMPLATE_WIDTH = 134,TEMPLATE_HEIGHT = 67- 不适应高分辨率显示或不同模板尺寸需求
4. API Inventory
4.1 REST Endpoints Table
文档模板管理 (xdoc 模块)
| 方法 | 路径 | 权限 | 控制器 | 说明 |
|---|---|---|---|---|
| GET | /v1/templates | - | TemplateController:50 | 获取模板列表(支持按类型、上下文过滤) |
| GET | /v1/templates/{templateId} | - | TemplateController:56 | 获取单个模板详情 |
| GET | /v1/templates/{templateId}/file | - | TemplateController:61 | 下载模板原始文件(.docx) |
| POST | /v1/templates | DOCUMENT_TEMPLATE_ADMIN (instance) | TemplateController:67 | 创建模板(上传 docx 文件) |
| POST | /v1/templates/{templateId} | DOCUMENT_TEMPLATE_ADMIN (instance) | TemplateController:88 | 更新模板文件 |
| PUT | /v1/templates/companies/{companyId}/{templateId} | DOCUMENT_TEMPLATE_ADMIN (company) | TemplateController:74 | 编辑公司模板字段 |
| POST | /v1/templates/companies/{companyId}/{templateId} | DOCUMENT_TEMPLATE_ADMIN (company) | TemplateController:81 | 更新公司模板文件 |
| POST | /v1/templates/companies/{companyId} | DOCUMENT_TEMPLATE_ADMIN (company) | TemplateController:109 | 创建公司级模板 |
| DELETE | /v1/templates/{templateId} | DOCUMENT_TEMPLATE_ADMIN (instance) | TemplateController:95 | 删除模板 |
| DELETE | /v1/templates/companies/{companyId}/{templateId} | DOCUMENT_TEMPLATE_ADMIN (company) | TemplateController:102 | 删除公司模板 |
模板类型管理
| 方法 | 路径 | 权限 | 控制器 | 说明 |
|---|---|---|---|---|
| GET | /v1/templateTypes | - | TemplateTypeController:41 | 获取模板类型列表 |
| GET | /v1/templateTypes/{templateTypeId} | - | TemplateTypeController:47 | 获取单个模板类型 |
| POST | /v1/templateTypes | DOCUMENT_TEMPLATE_ADMIN (instance) | TemplateTypeController:54 | 创建模板类型 |
| PUT | /v1/templateTypes/{templateTypeId} | DOCUMENT_TEMPLATE_ADMIN (instance) | TemplateTypeController:60 | 更新模板类型 |
XDocReport 管理
| 方法 | 路径 | 权限 | 控制器 | 说明 |
|---|---|---|---|---|
| GET | /v1/xdocreport/products/{productId} | - | DocXManagementController:39 | 列出文件系统中的模板文件 |
文档下载(生成)
| 方法 | 路径 | 权限 | 控制器 | 说明 |
|---|---|---|---|---|
| GET | /v1/attendees/download/{type} | - | AttendeeDownloadController:34 | 下载生成的会议文档(type=0:与会者列表, 其他:模板文档) |
| GET | /v1/public/document-url | public | PublicDocumentURLController:20 | 公开接口,通过 attendeeId 下载文档(PDF) |
文件管理
| 方法 | 路径 | 权限 | 控制器 | 说明 |
|---|---|---|---|---|
| GET | /v1/files | - | FileController:46 | 文件列表(分页,支持按 type/fileName 过滤) |
| POST | /v1/files | - | FileController:53 | 上传文件 |
| GET | /v1/files/{id} | - | FileController:60 | 获取文件信息 |
| GET | /v1/files/{id}/download | - | FileController:67 | 下载文件 |
内容库管理
| 方法 | 路径 | 权限 | 控制器 | 说明 |
|---|---|---|---|---|
| GET | /v1/contents | - | ContentController:62 | 内容列表(Admin/Speaker 不同结果) |
| GET | /v1/contents/modules | - | ContentController:71 | 模块列表 |
| POST | /v1/contents | - | ContentController:78 | 创建内容 |
| GET | /v1/contents/{id} | - | ContentController:90 | 获取内容详情 |
| GET | /v1/contents/presentations/{id} | - | ContentController:96 | 获取演示文稿详情(含幻灯片) |
| GET | /v1/contents/presentations/{id}/download | - | ContentController:102 | 下载演示文稿 |
| DELETE | /v1/contents/{id} | - | ContentController:108 | 删除内容 |
| PUT | /v1/contents/{id}/description | - | ContentController:114 | 更新描述 |
| PUT | /v1/contents/{id}/url | - | ContentController:119 | 更新 URL |
| PUT | /v1/contents/{id}/topics | - | ContentController:125 | 设置主题 |
| PUT | /v1/contents/{id}/expiration | - | ContentController:131 | 设置过期日期 |
| PUT | /v1/contents/{id}/maximums | - | ContentController:84 | 设置模块/案例研究数量上限 |
| PUT | /v1/contents/groups | - | ContentController:138 | 设置内容用户组关联 |
| DELETE | /v1/contents/groups | - | ContentController:143 | 移除内容用户组关联 |
| POST | /v1/contents/{id}/modules | - | ContentController:150 | 设置允许的模块/案例研究 |
| GET | /v1/contents/{id}/modules | - | ContentController:156 | 获取模块/案例研究 |
| PUT | /v1/contents/{id}/conversion | - | ContentController:165 | 内容类型转换(Core ↔ Module) |
| PUT | /v1/contents/{id}/insertions | - | ContentController:172 | 保存幻灯片插入点 |
| GET | /v1/contents/presentations/slide/{id} | - | ContentController:178 | 获取演示文稿幻灯片 |
合规文档
| 方法 | 路径 | 权限 | 控制器 | 说明 |
|---|---|---|---|---|
| GET | /v1/compliance/document-checklist | - | ComplianceController:74 | 获取文档清单列表 |
| POST | /v1/compliance/document-checklist | - | ComplianceController:79 | 创建文档清单项 |
| PUT | /v1/compliance/document-checklist/{id} | - | ComplianceController:84 | 更新文档清单项 |
| PUT | /v1/compliance/document-checklist/{id}/activate | - | ComplianceController:89 | 激活文档清单项 |
| PUT | /v1/compliance/document-checklist/{id}/deactivate | - | ComplianceController:93 | 停用文档清单项 |
| DELETE | /v1/compliance/document-checklist/{id} | - | ComplianceController:98 | 删除文档清单项 |
4.2 API Design Issues
API-1: POST 用于更新操作,违反 REST 语义
TemplateController:88-POST /v1/templates/{templateId}应该是PUTTemplateController:81-POST /v1/templates/companies/{companyId}/{templateId}同理- 原因可能是因为需要接收
MultipartFile,但现代框架 PUT 也可以处理
API-2: 文档下载端点混乱
AttendeeDownloadController:34-/v1/attendees/download/{type}中type既可以是 0(下载与会者列表,与文档无关)也可以是 templateTypeId- 与会者列表下载和文档模板生成混在同一个端点
API-3: DocXManagementController 直接暴露文件系统信息
DocXManagementController.java:39-43- 直接列出服务器文件系统目录内容,暴露文件名、大小、修改时间- 安全风险:泄露服务器路径和文件系统结构
API-4: PublicDocumentURLController 无认证保护
PublicDocumentURLController.java:15-/v1/public/document-url是公开端点- 仅通过 attendeeId 就能下载文档,attendeeId 若可猜测则存在信息泄露风险
API-5: 内容管理 API 大部分缺乏权限控制
ContentController.java- 除了内部isSpeaker()判断外,没有@PreAuthorize注解- 文件操作(上传/下载/删除)同样无权限控制(
FileController.java)
API-6: 合规文档清单 API 无权限注解
ComplianceController.java:74-101所有文档清单 CRUD 操作无@PreAuthorize
API-7: Swagger 注解不完整
- 部分 API 有
@ApiOperation,部分没有(如updateUrl、合规文档清单等) deleteCompanyTemplate和deleteTemplate的 Swagger 描述完全相同(TemplateController:100,95)
5. Frontend Analysis
5.1 Pages & Components
Plannerview (模板管理)
| 组件 | 路径 | 说明 |
|---|---|---|
TemplateManagement | plannerview/components/Template/index.js | 模板管理主页面,包含 Templates 和 Template Types 两个 Tab |
Template (templates.js) | plannerview/components/Template/templates.js | 模板列表,支持新建/上传/下载/测试/删除模板 |
TemplateType (templateTypes.js) | plannerview/components/Template/templateTypes.js | 模板类型列表,支持新建/编辑模板类型 |
NewTemplateForm | plannerview/components/Template/newTemplateForm.js | 新建模板表单 |
NewTemplateTypeForm | plannerview/components/Template/newTemplateTypeForm.js | 新建模板类型表单 |
MeetingDocuments | plannerview/components/MTDocuments/index.js | 会议文档页面,列出可用文档模板,支持选择格式下载 |
DocumentChecklist | plannerview/containers/Compliance/DocumentChecklist.js | 合规文档清单管理 |
MeetingComplianceDocumentChecklist | plannerview/containers/Compliance/MeetingComplianceDocumentChecklist.js | 会议级合规文档清单 |
Speakerview (内容库 + 文档)
| 组件 | 路径 | 说明 |
|---|---|---|
ContentLibrary | speakerview/containers/ContentLibrary/index.js | 内容库主页面,包含 Presentations 和 Other Content 两个 Tab |
Presentations | speakerview/components/Presentations/index.js | 演示文稿管理(Admin 视图) |
SpeakerPresentations | speakerview/components/SpeakerPresentations/index.js | 演示文稿浏览(Speaker 视图) |
OtherContents | speakerview/components/OtherContents/index.js | 其他内容管理 |
Slide | speakerview/components/Slide/index.js | 幻灯片编辑器(拖拽排序、插入点管理) |
Presentation | speakerview/containers/Presentation/index.js | 独立幻灯片查看窗口 |
SpeakerDocuments | speakerview/containers/SpeakerDocuments/index.js | 演讲者文档管理(W-9、简历等) |
UploadDocument | speakerview/containers/SpeakerDocuments/modal/UploadDocument.js | 文档上传弹窗 |
Templates | speakerview/components/Templates/index.js | 演讲者合同模板管理 |
PDFViewer | speakerview/components/Templates/PDFViewer.js | PDF 预览组件 |
Salesview (演示文稿 + 文档)
| 组件 | 路径 | 说明 |
|---|---|---|
Presentations | salesview/src/pages/Presentations/index.js | 资源页面,展示 Resources 和 Other Files 两个表格 |
ProgramPresentations | salesview/src/pages/Programs/Profile/ProgramPresentations.js | 项目关联的演示文稿 |
Documentation | salesview/src/pages/Reports/ComplianceAudit/Documentation.js | 合规审计文档视图 |
DocumentationFileList | salesview/src/pages/Reports/ComplianceAudit/DocumentationFileList.js | 文档文件列表 |
PDFViewer | salesview/src/components/PDFViewer/index.js | PDF 预览组件 |
5.2 Redux State Structure
Plannerview Template State
// 路径: plannerview/components/Template/reducer.js
state.templates = {
requestStatus: '', // 'request' | 'success' | 'error'
templates: {}, // 模板列表数据
testMeetingRequestId: null // 测试模板用的会议ID
}Speakerview Content State
// 路径: speakerview/containers/ContentLibrary/reducer.js
state.content = {
presentations: {}, // 演示文稿分页数据
presentationInfo: {}, // (未使用)
loading: false,
httpStatus: '', // 'request' | 'success' | 'error'
optionStatus: '', // 操作状态
presentationsData: [], // 允许的模块数据
allowedModules: [], // 已允许的模块
topics: [], // 主题列表
userGroups: [], // 用户组列表
speakerPresentations: [], // 演讲者的演示文稿
groupPresentationModules: [], // 组级演示文稿模块
products: [], // 产品列表
presentationDetail: {}, // 演示文稿详情
speakerPresentationDetail: null, // 演讲者演示文稿详情
otherContents: {}, // 其他内容分页数据
downloadStatus: null, // 下载状态
setLimitStatus: null, // 设置限制状态
message: null, // 错误消息
speakerMeetings: [] // 演讲者会议列表
}5.3 Frontend Issues
FE-1: 模板上传使用 Ant Design Upload 直接调用 API,绕过 Redux 流程
templates.js:85-96- 使用<Upload action={...} headers={...}>直接 POST 到 API- 手动拼接 Authorization header,不走 saga 中间件
- 与其他操作(如 createTemplate)使用 saga 模式不一致
FE-2: 内容库 saga 混用两种模式
speakerview/ContentLibrary/sagas.js同时使用:while(true) { yield take(ACTION); yield fork(handler) }模式(大部分)yield takeLatest(ACTION, handler)模式(getPresentations, getSpeakerMeetings 等)
- 前者无法取消旧请求,可能导致竞态条件
FE-3: componentWillReceiveProps 使用已废弃的生命周期方法
templates.js:56-componentWillReceiveProps在 React 16.3+ 已废弃Presentation/index.js:30同样使用了空的componentWillReceiveProps
FE-4: MeetingDocuments 硬编码模板文件名判断逻辑
MTDocuments/index.js:128,135-document.filename == 'SignatureReportTemplate'硬编码判断是否需要显示 "SignedIn Only" 选项- 应该由后端配置驱动
FE-5: Content 域状态管理过于复杂
speakerview/ContentLibrary/reducer.js包含 350+ 行,处理 50+ 个 action typespeakerview/ContentLibrary/constants.js定义了大量 REQUEST/SUCCESS/ERROR 常量- 应该按功能拆分为多个子 reducer
FE-6: 前端 URL 拼接缺乏统一管理
speakerview/ContentLibrary/sagas.js中 API URL 硬编码在各个 saga 函数中plannerview/Template/sagas.js同样硬编码
FE-7: 已移除功能残留 [REMOVED]
speakerview/ContentLibrary/index.js:46-53- ContentLibrary 仍引用 Presentations 组件(Core presentations),但 presentationEnabled 功能已标记为移除speakerview/ContentLibrary/sagas.js中仍有大量 presentation 相关的 slide/insertion 管理 saga- 注意:Content 域中"Presentations"一词指的是演讲者使用的内容资产(幻灯片集),与已移除的 "Presentation Manager"(
presentationEnabled,enableOnlinePresentation,ppt2Html5Enabled)是不同的概念。Content Presentations 仍然是活跃功能。
6. Problem Summary
6.1 Critical Issues (must fix in rewrite)
| # | 问题 | 位置 | 影响 |
|---|---|---|---|
| C-1 | SQL 注入漏洞 | FileService.java:42 | 文件名搜索直接拼接字符串到 SQL 条件,攻击者可通过 fileName 参数注入 SQL |
| C-2 | 公开端点无认证 | PublicDocumentURLController.java:15 | /v1/public/document-url 通过 attendeeId 即可下载 PDF,无任何鉴权 |
| C-3 | 文件系统路径泄露 | DocXManagementController.java:39-43 | 直接列出服务器文件系统目录结构 |
| C-4 | 本地文件系统存储不可扩展 | FileService.java, TemplateService.java | 所有文件存储在本地磁盘,无法水平扩展,无灾备 |
| C-5 | PDF 转换硬编码判断 | DocumentTemplateService.java:345-401 | 根据模板文件名硬编码选择 PDF 转换方式(Docx4J vs CloudConvert) |
| C-6 | 大部分内容/文件 API 无权限控制 | ContentController.java, FileController.java | 缺乏 @PreAuthorize 注解 |
| C-7 | 临时文件泄漏 | DocumentTemplateService.java:388,557 | 异常情况下临时文件(DOCX、PNG)不会被清理 |
6.2 Design Defects (should improve)
| # | 问题 | 位置 | 建议 |
|---|---|---|---|
| D-1 | 魔法数字泛滥 | Content.java:34,40, FileServer.java | 为 Content.type, Content.subType 创建枚举类 |
| D-2 | 软删除命名不一致 | Document.deleted, Content.del_flag | 统一命名规范 |
| D-3 | REST 动词误用 | TemplateController:81,88 | POST 用于更新应改为 PUT |
| D-4 | 多线程使用裸 Thread | DocumentTemplateService.java:315,351 | 改用线程池或异步框架 |
| D-5 | 合并字段(MergeField)模型继承混乱 | MeetingRequestMergeField extends ProgramResponse | MergeField DTO 不应继承业务响应对象 |
| D-6 | 文档生成服务过于庞大 | DocumentTemplateService.java (954 行) | 一个 Service 类负责所有上下文组装、格式转换、文件IO,应拆分 |
| D-7 | TemplateType.filename 自动生成规则过于简单 | TemplateTypeService.java:88-90 | 仅移除非字母数字字符,可能导致冲突 |
| D-8 | FileServer 枚举过于庞大 | FileServer.java (26 种类型) | 部分类型已废弃(如 SITE_BUILDER, APP_PRESENTATION, APP_VIDEO, APP_AUDIO)应清理 |
| D-9 | Content 和 Document 概念混淆 | 两个不同实体 | 应在架构层面明确区分"可分发内容资产"和"合规/个人文档" |
| D-10 | 前端 saga 模式不统一 | ContentLibrary/sagas.js | 混用 take/fork 和 takeLatest 两种模式 |
6.3 Technical Debt (nice to have)
| # | 问题 | 位置 | 说明 |
|---|---|---|---|
| T-1 | MD5 哈希算法已过时 | FileService.java:65 | 应使用 SHA-256 或 UUID |
| T-2 | 模板文件备份使用时间戳后缀 | TemplateService.java:253-256 | 备份文件名 name.timestamp 不易管理 |
| T-3 | SiteTemplate 主键设计有误 | SiteTemplate.java:24 | 仅 attendee_type_id 作为主键,缺少 meeting_id |
| T-4 | 前端 componentWillReceiveProps 废弃 | templates.js:56, Presentation/index.js:30 | 应迁移到 getDerivedStateFromProps 或 hooks |
| T-5 | Swagger 注解不完整 | 多个 Controller | 补充所有 API 的文档 |
| T-6 | 文件 content-type 检测不足 | FileService.java | 未根据实际文件内容验证 MIME type |
| T-7 | 前端 Redux 常量过多 | ContentLibrary/constants.js | 50+ 常量可通过 createRoutine 简化 |
| T-8 | CloudConvert 客户端每次新建 | DocumentTemplateService.java:891 | new CloudConvertClient() 在每次请求中创建,应使用单例或 Bean |
| T-9 | 图片处理硬编码尺寸 | AttendeeSignatureUtils.java:24-27 | 签名图片固定 134x67 像素 |
| T-10 | MeetingComplianceDocument.files 使用 JSON | MeetingComplianceDocument.java:32 | 应建立关联表替代 JSON 字段 |
7. Rewrite Recommendations
7.1 架构重构
引入对象存储服务(S3/MinIO)
- 替换当前的本地文件系统存储(
FileServer+fileServerPath) - 统一所有文件存储到对象存储
- 支持水平扩展和灾备
- 替换当前的本地文件系统存储(
拆分 DocumentTemplateService
TemplateContextBuilder- 负责组装合并字段上下文DocumentRenderer- 负责 XDocReport 处理和格式转换FileStorageService- 统一的文件存储抽象层PdfConversionService- 统一的 PDF 转换策略(配置驱动而非硬编码)
统一内容资产模型
- 合并
Content和Document的概念到统一的Asset模型 - 使用枚举类替代魔法数字(
AssetType,AssetSubType) - 引入版本管理
- 合并
7.2 安全修复
- 立即修复 SQL 注入 -
FileService.java:42使用参数化查询 - 为公开端点添加认证 - 使用签名 URL 或 JWT token 替代简单 attendeeId
- 移除文件系统目录列表接口 -
DocXManagementController - 为所有内容/文件 API 添加权限控制
- 文件上传添加白名单、大小限制、恶意内容扫描
7.3 前端现代化
- 迁移到 React Hooks + TypeScript
- 统一 saga 模式 - 全部使用
takeLatest+createRoutine - 引入 API 层抽象 - 统一管理所有 API URL 和请求逻辑
- 拆分 Content reducer - 按功能领域拆分为 presentations、modules、otherContent 等子模块
- 模板配置驱动 UI - 后端返回模板下载选项配置(如是否需要 signedInOnly 选项),而非前端硬编码
7.4 FileServer 枚举清理建议
当前 FileServer.java 中 26 种文件类型中,以下应标记为废弃或移除:
| 类型 | 代码 | 说明 |
|---|---|---|
| SITE_BUILDER (4) | "sitebuilder" | site-builder 模块已移除 |
| APP_PRESENTATION (15) | "app_presentation" | PharmaginConnect 移动应用已移除 |
| APP_VIDEO (16) | "app_video" | PharmaginConnect 移动应用已移除 |
| APP_AUDIO (17) | "app_audio" | PharmaginConnect 移动应用已移除 |
| VIDEO (11) | "video" | videoEnabled 已移除 |
| SPEAKER_VIDEO (25) | "speakervideo" | 可能与已移除的视频功能相关 |