Skip to content

Content & Document Domain - Deep Dive Analysis

1. Domain Overview

1.1 领域职责描述

Content & Document 域负责平台中所有文档和内容资产的管理,涵盖三个核心子领域:

  1. 文档模板管理(XDocReport):管理 Word 文档模板(.docx),支持 Velocity 模板引擎的合并字段(merge fields),用于生成会议相关文档(签到表、确认信、合同等)。支持 DOCX、PDF、XHTML 三种输出格式。
  2. 文件管理(File):通用文件上传/下载服务,支持多种文件类型(演示文稿、培训材料、图片、附件等),基于本地文件系统存储,使用 MD5 哈希命名去重。
  3. 内容库管理(Content Library):管理演讲者可用的内容资产,包括演示文稿(presentations)、模块(modules)、案例研究(case studies)和其他内容(other content)。支持幻灯片级别的管理和演讲者定制化演示。

1.2 涉及的后端模块和包

模块/包路径职责
xdoc 模块modules/v1/xdoc/文档模板 CRUD、XDocReport 文档生成、合并字段管理
file 模块modules/v1/file/通用文件上传/下载/列表
speaker/ContentControllermodules/v1/speaker/controller/ContentController.java内容库 API(演示文稿、模块、幻灯片管理)
site/AttendeeDownloadControllermodules/v1/site/controller/AttendeeDownloadController.java与会者文档下载入口
compliance/ComplianceControllermodules/v1/compliance/controller/ComplianceController.java合规文档清单管理
FileServer 枚举common/utils/FileServer.java文件存储路径映射(26种文件类型)

2. Data Model Analysis

2.1 Entity Overview Table

核心文档/模板实体

实体表名主键字段数文件位置说明
Templatet_templatetemplate_id (seq)10entity/Template.java:25文档模板配置(关联模板类型、产品、项目类型)
TemplateTypet_template_typetemplate_type_id (seq)5entity/TemplateType.java:24模板类型定义(如签到表、确认信等)
Documentt_documentdocument_id (seq)7entity/Document.java:8演讲者文档(W-9、简历等),关联 speaker_id
Filet_filefile_id (seq)5entity/File.java:7通用文件元数据(文件名、路径、大小、类型)
Contentt_contentid (seq)13entity/Content.java:22内容资产(演示文稿、培训模块、其他内容、视频)

关联/映射实体

实体表名主键字段数文件位置说明
ContentGroupt_content_groupcontent_id + group_id2entity/ContentGroup.java:7内容与演讲者组的多对多关联
ContentTopict_content_topiccontent_id + topic_id2entity/ContentTopic.java:7内容与主题的多对多关联
ComplianceDocumentt_compliance_documentid (seq)9entity/ComplianceDocument.java:22合规文档清单定义
MeetingComplianceDocumentt_meeting_compliance_documentmeeting_request_id + compliance_document_id4entity/MeetingComplianceDocument.java:22会议与合规文档的关联(含文件和审核状态)
MappingContractDocumentt_mapping_contract_documentcontract_id + document_id2entity/MappingContractDocument.java:7合同与文档的多对多关联

注册站点模板实体(Registration Site Builder)

实体表名主键字段数文件位置说明
SiteTemplatet_site_templateattendee_type_id14entity/SiteTemplate.java:21注册站点样式配置(导航、颜色、banner)
SiteTemplatePaget_site_template_pagepage_id (seq)6entity/SiteTemplatePage.java:7注册站点页面定义
SiteTemplateFieldt_site_template_fieldfield_id (seq)23entity/SiteTemplateField.java:21注册站点字段定义(含验证、格式、报告映射)

其他模板实体(非文档模板,属于其他域但名称包含 Template)

实体表名主键文件位置说明
BudgetItemTemplatet_budget_item_templatebudget_item_template_identity/BudgetItemTemplate.java:9预算模板(属于 Budget 域)
ProjectTaskTemplatet_project_task_templateidentity/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: video
  • entity/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 判断使用哪种方式:
    • SOWMSAPostProgramCertificationTemplate → 使用 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/templatesDOCUMENT_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/templateTypesDOCUMENT_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-urlpublicPublicDocumentURLController: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} 应该是 PUT
  • TemplateController: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、合规文档清单等)
  • deleteCompanyTemplatedeleteTemplate 的 Swagger 描述完全相同(TemplateController:100,95

5. Frontend Analysis

5.1 Pages & Components

Plannerview (模板管理)

组件路径说明
TemplateManagementplannerview/components/Template/index.js模板管理主页面,包含 Templates 和 Template Types 两个 Tab
Template (templates.js)plannerview/components/Template/templates.js模板列表,支持新建/上传/下载/测试/删除模板
TemplateType (templateTypes.js)plannerview/components/Template/templateTypes.js模板类型列表,支持新建/编辑模板类型
NewTemplateFormplannerview/components/Template/newTemplateForm.js新建模板表单
NewTemplateTypeFormplannerview/components/Template/newTemplateTypeForm.js新建模板类型表单
MeetingDocumentsplannerview/components/MTDocuments/index.js会议文档页面,列出可用文档模板,支持选择格式下载
DocumentChecklistplannerview/containers/Compliance/DocumentChecklist.js合规文档清单管理
MeetingComplianceDocumentChecklistplannerview/containers/Compliance/MeetingComplianceDocumentChecklist.js会议级合规文档清单

Speakerview (内容库 + 文档)

组件路径说明
ContentLibraryspeakerview/containers/ContentLibrary/index.js内容库主页面,包含 Presentations 和 Other Content 两个 Tab
Presentationsspeakerview/components/Presentations/index.js演示文稿管理(Admin 视图)
SpeakerPresentationsspeakerview/components/SpeakerPresentations/index.js演示文稿浏览(Speaker 视图)
OtherContentsspeakerview/components/OtherContents/index.js其他内容管理
Slidespeakerview/components/Slide/index.js幻灯片编辑器(拖拽排序、插入点管理)
Presentationspeakerview/containers/Presentation/index.js独立幻灯片查看窗口
SpeakerDocumentsspeakerview/containers/SpeakerDocuments/index.js演讲者文档管理(W-9、简历等)
UploadDocumentspeakerview/containers/SpeakerDocuments/modal/UploadDocument.js文档上传弹窗
Templatesspeakerview/components/Templates/index.js演讲者合同模板管理
PDFViewerspeakerview/components/Templates/PDFViewer.jsPDF 预览组件

Salesview (演示文稿 + 文档)

组件路径说明
Presentationssalesview/src/pages/Presentations/index.js资源页面,展示 Resources 和 Other Files 两个表格
ProgramPresentationssalesview/src/pages/Programs/Profile/ProgramPresentations.js项目关联的演示文稿
Documentationsalesview/src/pages/Reports/ComplianceAudit/Documentation.js合规审计文档视图
DocumentationFileListsalesview/src/pages/Reports/ComplianceAudit/DocumentationFileList.js文档文件列表
PDFViewersalesview/src/components/PDFViewer/index.jsPDF 预览组件

5.2 Redux State Structure

Plannerview Template State

javascript
// 路径: plannerview/components/Template/reducer.js
state.templates = {
  requestStatus: '',        // 'request' | 'success' | 'error'
  templates: {},             // 模板列表数据
  testMeetingRequestId: null // 测试模板用的会议ID
}

Speakerview Content State

javascript
// 路径: 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 type
  • speakerview/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-1SQL 注入漏洞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-5PDF 转换硬编码判断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-3REST 动词误用TemplateController:81,88POST 用于更新应改为 PUT
D-4多线程使用裸 ThreadDocumentTemplateService.java:315,351改用线程池或异步框架
D-5合并字段(MergeField)模型继承混乱MeetingRequestMergeField extends ProgramResponseMergeField DTO 不应继承业务响应对象
D-6文档生成服务过于庞大DocumentTemplateService.java (954 行)一个 Service 类负责所有上下文组装、格式转换、文件IO,应拆分
D-7TemplateType.filename 自动生成规则过于简单TemplateTypeService.java:88-90仅移除非字母数字字符,可能导致冲突
D-8FileServer 枚举过于庞大FileServer.java (26 种类型)部分类型已废弃(如 SITE_BUILDER, APP_PRESENTATION, APP_VIDEO, APP_AUDIO)应清理
D-9Content 和 Document 概念混淆两个不同实体应在架构层面明确区分"可分发内容资产"和"合规/个人文档"
D-10前端 saga 模式不统一ContentLibrary/sagas.js混用 take/forktakeLatest 两种模式

6.3 Technical Debt (nice to have)

#问题位置说明
T-1MD5 哈希算法已过时FileService.java:65应使用 SHA-256 或 UUID
T-2模板文件备份使用时间戳后缀TemplateService.java:253-256备份文件名 name.timestamp 不易管理
T-3SiteTemplate 主键设计有误SiteTemplate.java:24attendee_type_id 作为主键,缺少 meeting_id
T-4前端 componentWillReceiveProps 废弃templates.js:56, Presentation/index.js:30应迁移到 getDerivedStateFromProps 或 hooks
T-5Swagger 注解不完整多个 Controller补充所有 API 的文档
T-6文件 content-type 检测不足FileService.java未根据实际文件内容验证 MIME type
T-7前端 Redux 常量过多ContentLibrary/constants.js50+ 常量可通过 createRoutine 简化
T-8CloudConvert 客户端每次新建DocumentTemplateService.java:891new CloudConvertClient() 在每次请求中创建,应使用单例或 Bean
T-9图片处理硬编码尺寸AttendeeSignatureUtils.java:24-27签名图片固定 134x67 像素
T-10MeetingComplianceDocument.files 使用 JSONMeetingComplianceDocument.java:32应建立关联表替代 JSON 字段

7. Rewrite Recommendations

7.1 架构重构

  1. 引入对象存储服务(S3/MinIO)

    • 替换当前的本地文件系统存储(FileServer + fileServerPath
    • 统一所有文件存储到对象存储
    • 支持水平扩展和灾备
  2. 拆分 DocumentTemplateService

    • TemplateContextBuilder - 负责组装合并字段上下文
    • DocumentRenderer - 负责 XDocReport 处理和格式转换
    • FileStorageService - 统一的文件存储抽象层
    • PdfConversionService - 统一的 PDF 转换策略(配置驱动而非硬编码)
  3. 统一内容资产模型

    • 合并 ContentDocument 的概念到统一的 Asset 模型
    • 使用枚举类替代魔法数字(AssetType, AssetSubType
    • 引入版本管理

7.2 安全修复

  1. 立即修复 SQL 注入 - FileService.java:42 使用参数化查询
  2. 为公开端点添加认证 - 使用签名 URL 或 JWT token 替代简单 attendeeId
  3. 移除文件系统目录列表接口 - DocXManagementController
  4. 为所有内容/文件 API 添加权限控制
  5. 文件上传添加白名单、大小限制、恶意内容扫描

7.3 前端现代化

  1. 迁移到 React Hooks + TypeScript
  2. 统一 saga 模式 - 全部使用 takeLatest + createRoutine
  3. 引入 API 层抽象 - 统一管理所有 API URL 和请求逻辑
  4. 拆分 Content reducer - 按功能领域拆分为 presentations、modules、otherContent 等子模块
  5. 模板配置驱动 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"可能与已移除的视频功能相关