Skip to content

External Integrations Domain - Deep Dive Analysis

1. Domain Overview

1.1 领域职责描述

外部集成领域负责平台与所有第三方系统之间的数据交换和功能协作。该领域覆盖了CRM数据同步(Salesforce/Veeva)、虚拟会议管理(Zoom)、邮件投递与追踪(SendGrid + iCal4j)、文件传输(SFTP)、文档转换(CloudConvert)、地理编码(Google Maps API)、身份认证(SSO/SAML)、数据导出(Mosaic)、外部自动化(Podio webhook)以及外部程序创建API等功能。

1.2 集成清单总览

外部系统集成方向协议/方式核心用途
Salesforce (Force API)双向SOAP Partner APICRM联系人/活动同步
Veeva CRM双向SOAP Partner API (Salesforce)Veeva CRM多渠道活动同步
Zoom双向REST API + Webhook虚拟会议/网络研讨会管理
SendGrid入向REST API邮件活动追踪数据同步
iCal4j出向iCalendar标准日历邀请生成(随邮件发送)
Google Maps出向REST API地理编码(地址转GPS坐标)
SFTP出向SSH/SFTP文件上传/下载/删除
CloudConvert出向REST APIDOCX到PDF文档转换
SSO/SAML入向SAML 2.0Azure AD/Okta企业SSO
Mosaic出向数据库查询 + Excel导出合规数据导出
Podio出向REST Webhook程序创建通知
External API入向REST API外部系统创建程序

2. Integration Inventory

2.1 Salesforce (Force API)

集成模式: 通过Salesforce SOAP Partner API进行双向数据同步。使用策略模式支持标准Salesforce和Veeva两种CRM变体。

核心文件:

  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/plus/integration/sf/SfdcFacade.java - 门面类,统一入口
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/plus/integration/sf/service/ForceService.java - SOAP操作基类
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/plus/integration/sf/service/IntegrationServiceConfiguration.java - 策略工厂配置
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/plus/integration/sf/service/DefaultContactService.java - 标准SF联系人
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/plus/integration/sf/service/DefaultAccountService.java - 标准SF账户
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/plus/integration/sf/service/VeevaAccountService.java - Veeva账户
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/plus/integration/sf/service/VeevaContactService.java - Veeva联系人(空实现)
  • pharmagin-api/pharmagin-web/src/main/resources/yaml/field-mapping*.yaml - 10个字段映射配置文件

同步实体:

  • Contact / Person Account -> NpiRecord(查找Speaker候选人)
  • Contact / Account -> ContactResponse(获取联系人详情)
  • Attendee -> Contact / Person Account(反向创建)
  • EventRequest -> Event / Multichannel_Activity_vod__c(活动同步)

架构模式:

  1. 策略模式选择CRM类型: IntegrationServiceConfiguration (line 17-58) 根据 agency.sf.type 配置值决定使用 DefaultContactService/DefaultAccountService(标准Salesforce)还是 VeevaContactService/VeevaAccountService(Veeva CRM)。

  2. YAML驱动的字段映射: FieldMappingFactory 从 YAML 文件加载字段映射配置,支持按客户定制。当前有10个映射文件覆盖不同客户(dendreon, lantheus, pharmagin, cutanea, ipsen, kala, noven, stemline, tesaro + 默认)。

  3. 单例连接管理: SfdcFacade (line 36-37) 声明为 @Scope("singleton")PartnerConnection 在首次认证时创建并缓存,仅在用户名变化时重建 (line 82-89)。

关键问题:

  • SQL注入风险: SfdcFacade.getSFDCUserId() (line 203) 直接拼接用户输入到SOQL查询中:"SELECT Id FROM User where Username = '" + email + "'" — 虽然SOQL注入的攻击面比SQL注入小,但仍存在风险。DefaultAccountService.searchPersonAccounts() (line 169-173) 同样直接拼接查询。
  • 连接共享问题: 单例中的 PartnerConnection 被所有请求共享 (line 52),在多线程环境下可能存在线程安全问题,且一个产品的Salesforce凭据可能与另一个产品的混淆。
  • 错误处理不一致: DefaultAccountService 多处使用 ce.printStackTrace() (line 79, 83, 98, 152, 202, 259) 而非结构化日志。VeevaAccountService 也有相同问题 (line 79, 151, 258)。
  • FieldMappingFactory 单例问题: 静态 instance 变量 (line 40-41) 意味着 mappings 列表在整个JVM中共享,updateFieldMappingConfiguration() 调用时会 clear() 并重新加载 (line 67),可能导致并发问题。
  • 硬编码的Veeva对象名: VeevaAccountService 硬编码了 "Multichannel_Activity_vod__c""Pharmagin_vod""Professional_vod" 等Veeva自定义对象名 (line 40, 133-134, 270)。

2.2 Zoom API

集成模式: 通过Zoom REST API管理虚拟会议和网络研讨会,通过Webhook接收实时事件。使用Server-to-Server OAuth (S2S) 认证。

核心文件:

  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/virtualProgram/service/ZoomAbstractService.java - 抽象基类,含认证和REST调用逻辑
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/virtualProgram/service/ZoomVirtualMeetingService.java - 会议CRUD
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/virtualProgram/service/ZoomWebinarService.java - 网络研讨会CRUD
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/virtualProgram/service/StubVirtualMeetingService.java - 第三方虚拟会议的空实现
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/virtualProgram/service/VirtualProgramServiceFactory.java - 工厂类
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/virtualProgram/controller/ZoomMeetingWebhookController.java - Webhook接收
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/virtualProgram/model/ZoomAccessTokenManager.java - Token缓存
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/config/VirtualProgramConfig.java - 配置模型

虚拟服务类型(VirtualServiceType 枚举):

  • 1 - Zoom Virtual Meeting
  • 2 - Zoom Webinar
  • 3 - Third Party Virtual Meeting(空实现,用于外部平台如Teams)

API操作覆盖:

操作Meeting APIWebinar API
创建POST /users/{userId}/meetingsPOST /users/{userId}/webinars
查询GET /meetings/GET /webinars/
更新PATCH /meetings/PATCH /webinars/
删除DELETE /meetings/DELETE /webinars/
添加参会者POST /meetings/{id}/registrantsPOST /webinars/{id}/registrants 或 panelists
更新参会者PUT /meetings/{id}/registrants/statusPUT /webinars/{id}/registrants/status
投票POST /meetings/{id}/pollsPOST /webinars/{id}/polls
报告GET /report/meetings/{id}/participantsGET /report/webinars/{id}/participants

Webhook处理:

ZoomMeetingWebhookController (line 37-198) 处理6种Webhook事件:

  • meeting/participant/join - 参会者加入会议
  • meeting/participant/leave - 参会者离开会议
  • meeting/ended - 会议结束
  • webinar/participant/join - 参会者加入研讨会
  • webinar/participant/leave - 参会者离开研讨会
  • webinar/ended - 研讨会结束

Webhook安全验证实现了完整的HMAC-SHA256签名验证 (line 124-151),包括时间戳防重放攻击(5分钟窗口)和Zoom Endpoint Validation协议 (line 104-109, 153-162)。

认证机制:

使用Server-to-Server OAuth2:getAccessToken() (ZoomAbstractService line 267-298) 向 s2sOAuthTokenUrl 发送 account_credentials grant请求。Token通过 ZoomAccessTokenManager(Guava Cache,55分钟过期)缓存 (line 12-13)。

重试机制:

restCall() (line 150-188) 有一层简单重试:当收到错误码 124(Token过期)时,清除缓存Token并重试一次 (line 176-179)。

关键问题:

  • 单次重试过于简单: restCall() 只在Token过期时重试一次 (line 178-179),对于网络瞬断、速率限制(429)等场景没有处理。
  • Token缓存全局单例: ZoomAccessTokenManager 使用静态 Cache (line 12),在多产品部署场景下所有产品共享同一个Token,可能导致Token对应错误的Zoom账户。
  • 遗留代码: generateZoomSignature() (line 221-243) 使用旧的API Key签名方式,已被 generateSignature() (line 245-265) 取代,但未删除。
  • 大量代码重复: ZoomVirtualMeetingServiceZoomWebinarService 之间存在大量逻辑重复(createProgram、updateProgram、deleteProgram、handleParticipantJoin/Leave、handleMeetingEnd 等),仅URL路径不同(/meetings/ vs /webinars/)。
  • 异常处理不精确: restCall() 捕获 HttpClientErrorException 后对所有非124错误统一抛出 ServiceException (line 183),丢失了原始HTTP状态码信息。
  • Webhook Controller缺乏权限限制: Webhook端点 v1/webhooks/zoom/ 虽然有签名验证,但位于一般的Controller路径下,应通过安全配置确保公开访问。

2.3 SendGrid

集成模式: 通过SendGrid REST API(v3 Messages端点)拉取邮件活动追踪数据,使用增量同步模式持久化到本地数据库。注意:邮件发送不通过SendGrid API——平台使用JavaMail + SMTP直接发送(见mail模块),SendGrid集成仅用于活动追踪。

核心文件:

  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/sendgrid/service/SendGridService.java - 核心同步服务
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/sendgrid/controller/SendGridController.java - 空Controller
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/sendgrid/DownloadSendGridEmailActivity.java - Excel导出模型
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/entity/SendGridEmailActivity.java - 数据实体

同步机制:

performIncrementalSync() (line 59-77) 实现增量同步:

  1. 从数据库获取最近一次事件时间 getNextStartTime() (line 79-106)
  2. 若无历史数据则从30天前开始
  3. 调用SendGrid API获取 last_event_time > {timestamp} 的消息
  4. 若结果达到1000条(API限制),启用时间分片算法
  5. 时间分片递归二分(先按天,然后按小时,最小5分钟分片)
  6. 结果通过 batchUpsertEmailActivities() 批量写入数据库

时间分片策略(处理API限制):

SendGrid API单次最多返回1000条记录。当结果满时:

  • fetchWithTimeSlicingSplit() (line 195-217):先按日期二分
  • 若单日超限,切换到 fetchWithTimeSlicingDateTime() (line 228-255):按小时二分
  • fetchWithDateTimeSplit() (line 257-295):最终降到5分钟粒度
  • 5分钟仍超限则截断并记录警告 (line 262-264)

关键问题:

  • Controller完全为空: SendGridController (line 11-13) 没有任何端点实现,同步似乎仅通过cron job触发,缺少手动触发和状态查询API。
  • SimpleDateFormat线程安全问题: dateFormat (line 41) 作为实例变量被多线程共享,SimpleDateFormat 非线程安全。
  • 递归深度未限制: 时间分片的递归二分没有最大深度限制,极端情况下可能栈溢出。
  • 凭据直接在Header中: API Key通过 "Bearer " + agencyConfiguration.getSendGrid().getApiKey() (line 165) 直接硬编码到HTTP Header,无密钥轮换机制。
  • URL未编码: buildUrl() (line 155-161) 将SOQL查询字符串直接拼接到URL中,未进行URL编码,包含空格和引号的查询可能导致请求失败。
  • 无速率限制感知: 未处理SendGrid API的速率限制响应(429 Too Many Requests)。

2.4 iCalendar (日历同步)

集成模式: 使用 iCal4j 库生成符合 iCalendar 标准的 .ics 文件,作为邮件附件发送给参会者,实现日历事件同步。不涉及Google Calendar API调用。

核心文件:

  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/mail/service/MailService.java - 包含 createICalendar() 方法 (line 488)

实现细节:

  • 使用 net.fortuna.ical4j 库创建 VEvent 日历事件 (line 546)
  • 生成的 .ics 数据作为 text/calendar;METHOD=PUBLISH MIME附件 (line 279-280)
  • 文件名固定为 invite.ics (line 281)
  • 支持参会者信息(Cn参数)、组织者信息

关键说明: 这不是Google Calendar API集成。Google API仅用于地理编码(见2.6节)。日历功能完全通过iCal标准实现,不依赖任何外部日历服务API。

2.5 Google Maps Geocoding API

集成模式: 通过Google Maps Geocoding API将地址字符串转换为GPS坐标(经纬度),用于地图展示。

核心文件:

  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/service/GoogleAPIService.java - 地理编码服务

实现细节:

  • API URL: https://maps.googleapis.com/maps/api/geocode/json (line 18)
  • 参数:地址通过URLEncoder编码,固定 region=us (line 18)
  • 返回 geometry.location 中的 lat/lng 坐标 (line 54)
  • 通过 agency.google.api.key 配置API密钥 (line 27-28)
  • 通过 agency.enableGoogleGeocodeAPI 功能开关控制 (line 22)

关键问题:

  • 自建RestTemplate: GoogleAPIService 在类级别直接 new RestTemplate() (line 19),不使用Spring管理的Bean,无连接池和超时配置。
  • 返回类型不安全: getGPS() 返回 Map<String, Object> (line 31),调用方需要自行转型,且无Null安全保证。
  • 无缓存: 每次调用都向Google API发送请求,相同地址不缓存,可能导致不必要的API费用。
  • API密钥在URL中: 密钥直接放在URL参数中 (line 39),可能被日志记录或浏览器历史泄露。

2.6 SFTP

集成模式: 通过 SSHJ 库进行 SSH/SFTP 文件传输操作,支持列出远程文件、上传、下载和删除。

核心文件:

  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/sftp/service/SFTPService.java - 核心SFTP服务
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/sftp/controller/SFTPController.java - REST端点

操作:

方法功能端点
listFiles()列出本地和远程文件GET /v1/sftp/files
uploadRemoteFiles()批量上传到远程POST /v1/sftp/remote-files/upload
upload()单文件上传内部调用
download()从远程下载内部调用
remove()删除远程文件内部调用

凭据管理: SFTP连接信息通过 AgencyConfiguration.SFTP 配置:

agency.sftp.host
agency.sftp.username
agency.sftp.password
agency.sftp.remoteDirectory

关键问题:

  • 禁用主机密钥验证: setupSshj() (line 140) 使用 PromiscuousVerifier,完全跳过SSH主机密钥验证,容易受到中间人攻击。这是严重安全隐患
  • 每次操作新建连接: 每个SFTP操作(list/upload/download/remove)都创建新的SSH连接 (line 138-158),没有连接池,连接建立开销大。
  • 密码明文存储: SSH密码直接存储在Spring Cloud Config中 (AgencyConfiguration.SFTP line 77),不支持密钥认证。
  • 异常处理混乱: upload() (line 116-136) 和 uploadRemoteFiles() (line 86-114) 在 finally 中抛出 RuntimeException,可能掩盖原始异常。
  • listFiles() 中的NPE风险: (line 40) localDirectory.listFiles() 可能返回null(目录不可读时),导致 NullPointerException

2.7 CloudConvert

集成模式: 使用CloudConvert Java SDK将DOCX文档转换为PDF。仅在文档模板服务中使用。

核心文件:

  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/xdoc/service/DocumentTemplateService.java (line 889-930)

转换流程:

  1. Upload Import - 上传本地DOCX文件
  2. Convert Task - 请求DOCX到PDF转换
  3. Wait - 等待转换完成
  4. URL Export - 获取转换后文件的下载URL
  5. Wait - 等待导出完成
  6. Download - 下载PDF文件流

关键问题:

  • 每次转换创建新客户端: new CloudConvertClient() (line 891) 每次都创建新实例,无连接复用。
  • 凭据配置不明: CloudConvert客户端未显式传入API密钥,依赖SDK的默认配置机制(环境变量或属性文件),不可控。
  • 同步阻塞调用: cloudConvertClient.tasks().wait() (line 908, 917) 是阻塞调用,长文档转换可能阻塞线程较长时间。
  • 异常被吞没: catch (Exception e) (line 927) 仅记录日志,不抛出异常,调用方无法知道转换失败。
  • 无超时控制: 没有对转换等待设置超时,CloudConvert服务不可用时线程会无限期阻塞。

2.8 SSO/SAML (Azure AD, Okta)

集成模式: 独立部署的SAML 2.0 Service Provider,接收来自Azure AD和Okta的SAML断言,生成应用级认证Cookie。

核心文件:

  • pharmagin-api/pharmagin-sso/src/main/java/com/pharmagin/sso/config/SecurityConfig.java - Spring Security SAML配置
  • pharmagin-api/pharmagin-sso/src/main/java/com/pharmagin/sso/config/CustomAuthenticationSuccessHandler.java - 认证成功处理器
  • pharmagin-api/pharmagin-sso/src/main/java/com/pharmagin/sso/config/PharmaginSsoProperties.java - SSO配置属性
  • pharmagin-api/pharmagin-sso/src/main/java/com/pharmagin/sso/service/UserAuthService.java - Token生成存储
  • pharmagin-api/pharmagin-sso/src/main/java/com/pharmagin/sso/controller/IdpSelectionController.java - IDP选择页

认证流程:

  1. 用户访问 / 页面,展示IDP选择页面 (Thymeleaf模板 idp-selection.html)
  2. 用户选择IDP后发起SAML认证
  3. Spring Security SAML2 处理SAML Response
  4. CustomAuthenticationSuccessHandler (line 51-71): a. 从SAML断言中提取用户邮箱 (line 73-143) b. 通过 UserAuthService.generateAndStoreToken() 生成UUID Token (line 38) c. Token存储到 user_auth_token 数据库表 d. 设置 pharmaginAuth Cookie,值为 email|token 格式 (line 146-149) e. 重定向到 defaultRedirectUrl

Cookie配置:

  • Cookie名称:pharmaginAuth(默认)(PharmaginSsoProperties line 43)
  • 最大有效期:900秒 (15分钟) (line 44)
  • HttpOnly和Secure被注释掉 (CustomAuthenticationSuccessHandler line 161-162)

IDP邮箱属性配置: pharmagin.sso.idp-email-attributes Map支持为不同IDP配置不同的SAML属性名用于提取邮箱 (PharmaginSsoProperties line 15)。如果属性不存在则回退到SAML NameID。

关键问题:

  • Cookie安全标志被禁用: HttpOnlySecure 标志被注释掉 (line 161-162),Cookie可被JavaScript读取且可在HTTP上传输,存在XSS和中间人攻击风险。
  • Token无过期清理: user_auth_token 表中的Token只有生成逻辑,没有过期清理机制 (UserAuthService),随时间推移表会无限增长。
  • Cookie值未加密: Cookie中直接包含明文邮箱 (line 147),虽然URL编码但未加密。
  • 邮箱提取逻辑过于复杂: getEmailFromAuthentication() (line 73-143) 有多层回退逻辑(SAML属性 -> NameID -> authentication.getName()),难以调试和维护。
  • pharmagin-login未投产: 新版SSO服务 pharmagin-login(端口9113)已开发但因需要IDP配置更新未部署,两套SSO代码同时存在。

2.9 Mosaic

集成模式: 不是外部API调用,而是将内部数据库数据按Mosaic格式导出。通过自定义SQL查询从本地数据库读取数据,格式化后供Mosaic合规系统消费。

核心文件:

  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/mosaic/service/MosaicService.java - 数据导出服务
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/mosaic/response/MosaicEvent.java - 事件数据模型
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/mosaic/response/MosaicAttendee.java - 参会者数据模型
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/common/persistence/mapper/MosaicMapper.java - MyBatis Mapper
  • pharmagin-api/pharmagin-web/src/main/resources/mybatis/MosaicMapper.xml - SQL映射

数据导出:

  • getEvents() - 导出所有会议事件(含开始时间、主办方、讲者、场地等)
  • getAttendees() - 导出所有参会者(含注册状态、签到状态、食物消费、邮件退订状态等)

参会者状态映射: getAttendeeStatus() (line 58-89) 将内部多种状态映射为Mosaic标准状态:Attended, No show, Cancelled, Confirmed, Invited, Targeted, Waitlist。

配置: 通过 agency.enableMosaicIntegration 控制 (AgencyConfiguration line 61)。

2.10 Podio Webhook

集成模式: 当程序创建时,异步向Podio webhook URL发送POST请求,传递程序信息。包含Spring Retry重试机制。

核心文件:

  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/program/service/PodioService.java - 异步包装
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/program/service/PodioRetryService.java - 带重试的发送

重试策略:

  • 最大尝试次数:3次 (PodioRetryService line 23)
  • 退避策略:固定3秒延迟 (line 24)
  • 恢复方法:仅记录日志 (line 40-42)

关键问题:

  • URL为空时静默返回 (line 29-31),无法区分"未配置"和"配置为空"。
  • @Recover 方法仅处理 RestClientException,对其他异常类型不生效。

2.11 External API

集成模式: 提供给外部系统调用的REST API,支持认证和程序创建。这是平台向外暴露的API,而非调用外部系统。

核心文件:

  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/external/controller/ExternalAPIController.java
  • pharmagin-api/pharmagin-web/src/main/java/com/pharmagin/modules/v1/external/service/ExternalAPIService.java

端点:

方法端点功能
POST/v1/external/authentication获取访问令牌
POST/v1/external/programs创建程序

3. Data Model Analysis

3.1 Entity Overview Table

实体/表模块数据库表用途
SendGridEmailActivitysendgridsendgrid_email_activitySendGrid邮件活动追踪
UserAuthTokenssouser_auth_tokenSSO认证Token存储
MosaicEventmosaic(MyBatis查询)Mosaic事件导出视图
MosaicAttendeemosaic(MyBatis查询)Mosaic参会者导出视图
ContactResponsesf(内存对象)Salesforce联系人响应
EventRequestsf(内存对象)Salesforce活动请求
NpiRecordsf(内存对象)NPI/联系人搜索结果
VirtualProgramResponsevirtualProgram(JSON字段)Zoom会议信息(存于meeting_request.virtual_program_info)
VirtualProgramAttendeeResponsevirtualProgram(JSON字段)Zoom参会者信息(存于attendee.virtual_program_attendee_info)

3.2 Data Model Issues

  1. Zoom数据以JSON存储在关系字段中: VirtualProgramResponseVirtualProgramAttendeeResponse 序列化为JSON存储在 meeting_requestattendee 表的 JSON 字段中,通过 BeanUtil.convertMapToEntity() 反序列化,无法进行SQL查询。

  2. Salesforce映射使用YAML而非数据库: 字段映射硬编码在10个YAML文件中,添加新客户需要新增YAML文件并重新部署。

  3. SendGrid数据缺少关联: sendgrid_email_activity 表中的邮件记录与平台内部的邮件发送记录(communication模块)没有显式关联。

  4. UserAuthToken表缺少过期时间列: 仅有 emailtokencreated_at 三列,没有 expires_at 列,无法通过SQL清理过期Token。


4. API Inventory

4.1 REST Endpoints Table

模块方法端点描述
virtualProgramGET/v1/virtualPrograms/attendees/{id}/resend重发Zoom邀请
virtualProgramPUT/v1/virtualPrograms/attendees/添加Zoom参会者
virtualProgramPOST/v1/virtualPrograms/meetings/创建Zoom会议
virtualProgramGET/v1/virtualPrograms/meetings/{id}/url获取Zoom加入链接
virtualProgramGET/v1/virtualPrograms/meetings/{id}/zoom获取Zoom Web SDK信息
zoom webhookPOST/v1/webhooks/zoom/meeting/participant/joinZoom参会者加入
zoom webhookPOST/v1/webhooks/zoom/meeting/participant/leaveZoom参会者离开
zoom webhookPOST/v1/webhooks/zoom/meeting/endedZoom会议结束
zoom webhookPOST/v1/webhooks/zoom/webinar/participant/joinZoom研讨会参会者加入
zoom webhookPOST/v1/webhooks/zoom/webinar/participant/leaveZoom研讨会参会者离开
zoom webhookPOST/v1/webhooks/zoom/webinar/endedZoom研讨会结束
sftpGET/v1/sftp/files列出SFTP文件
sftpPOST/v1/sftp/local-files/upload上传到本地
sftpPOST/v1/sftp/remote-files/upload上传到远程
externalPOST/v1/external/authentication外部认证
externalPOST/v1/external/programs外部创建程序
sendgrid(空)/api/v1/sendgrid无端点

4.2 API Design Issues

  1. 路径前缀不一致: SendGrid Controller使用 /api/v1/sendgrid 前缀,其他模块使用 /v1/ 前缀。
  2. VirtualProgram Controller的Swagger标签错误: @Api(tags = "User Management") (VirtualProgramController line 41),应该是 "Virtual Program Management"。
  3. Webhook端点URL设计: Zoom webhook使用 /v1/webhooks/zoom/meeting/participant/join 这种深层路径结构,可能导致Zoom webhook配置复杂。通常一个端点处理所有事件类型更简洁。
  4. 缺少SFTP下载和删除端点: SFTPServicedownload()remove() 方法,但Controller未暴露对应端点。

5. Integration Architecture Issues

5.1 Error Handling

当前状况:混乱且不一致

各集成的错误处理策略差异巨大:

集成错误处理方式问题
SalesforceIntegrationException (RuntimeException) + ce.printStackTrace()大量 printStackTrace() 代替结构化日志,部分异常被吞没
ZoomServiceException 包装所有HTTP错误统一处理,丢失原始状态码
SendGridRuntimeException 包装嵌套异常链冗长
SFTPRuntimeException 直接抛出finally块中也抛RuntimeException
CloudConvert异常完全吞没catch(Exception e) 仅日志不抛出
Google API返回null调用方无法区分"未配置"和"调用失败"
PodioSpring Retry @Recover仅日志,静默失败

具体问题:

  • ForceService.createEvent() (line 64) 使用 log.error() 记录正常请求数据,滥用ERROR级别。
  • VeevaAccountService 多个方法中 catch (ConnectionException ce) { ce.printStackTrace(); } (line 79, 151, 199, 258) 使用 printStackTrace() 而非 SLF4J。
  • SFTPServiceuploadRemoteFiles() (line 107-109) 在 finally 块中抛出 RuntimeException,可能掩盖在 try 块中抛出的原始异常。

5.2 Retry/Resilience Patterns

当前状况:大部分集成没有重试

集成重试机制断路器超时控制
SalesforceSessionRenewer (自动续Session)无显式超时
Zoom单次Token过期重试无显式超时
SendGrid无显式超时
SFTP无连接超时
CloudConvert无转换超时
Google APIRestTemplate默认超时
PodioSpring Retry (3次, 3秒延迟)
SSO/SAMLN/A (被动接收)

关键缺失:

  • 没有任何集成使用断路器模式(Circuit Breaker),外部服务不可用时会持续尝试并阻塞线程。
  • 除Podio外,没有系统化的重试策略(指数退避、抖动等)。
  • Salesforce的 ProductionSessionRenewer (SfdcFacade line 301-312) 提供了Session自动续期,但不处理其他类型的连接错误。
  • 没有任何集成有健康检查或连通性测试能力。

5.3 Security (Credential Management)

当前状况:凭据以明文存储在配置文件中

所有集成的凭据都通过Spring Cloud Config(pharmagin-config-repo)以YAML明文存储:

集成凭据类型存储位置风险
Salesforceusername + securityTokenagency.sf.*明文在配置文件
Zoom S2Ss2sClientId + s2sClientSecret + s2sAccountIdproduct.virtualPrograms.zoomVirtualMeeting.*明文在配置文件
Zoom SDKmeetingSDKClientId + meetingSDKClientSecret同上明文在配置文件
Zoom WebhookeventSecretToken同上明文在配置文件
SendGridapiKeyagency.sendGrid.apiKey明文在配置文件
SFTPpasswordagency.sftp.password明文在配置文件
Googleapi.keyagency.google.api.key明文在配置文件
CloudConvert(未知)SDK默认不可控
SSO SAML签名证书src/main/resources/credentials/文件系统

关键问题:

  • 没有使用任何密钥管理服务(如AWS Secrets Manager、HashiCorp Vault)。
  • 配置文件中的密码没有加密(Spring Cloud Config支持 {cipher} 前缀加密但未使用)。
  • Salesforce的 securityToken 字段实际传递的是password+token拼接值,字段命名误导 (SfdcFacade line 49, 288)。
  • SFTP使用密码认证而非SSH密钥对 (SFTPService line 149)。
  • SSO Cookie没有启用HttpOnly和Secure标志 (CustomAuthenticationSuccessHandler line 161-162)。

5.4 Monitoring/Logging

当前状况:日志记录不规范,无监控

日志问题:

  • Salesforce模块使用 log.error() 记录正常操作(如 createEvent 的请求内容,ForceService line 64),ERROR级别滥用。
  • 多处使用 e.printStackTrace() 代替SLF4J (VeevaAccountService line 79, 151, 258; DefaultAccountService line 83, 98, 202; SfdcFacade line 248)。
  • 日志中可能泄露敏感信息:SfdcFacade.getSFDCUserId() (line 209-210) 记录SFDC User ID,Zoom请求body可能包含API凭据。
  • 缺少结构化日志(如MDC上下文、correlation ID)。

监控缺失:

  • 无API调用指标(调用次数、延迟、错误率)。
  • 无外部服务健康检查。
  • 无告警机制(如Salesforce连接失败、Zoom Token获取失败等)。
  • SendGrid增量同步无状态暴露(无法通过API查看最后同步时间、同步记录数等)。

6. Problem Summary

6.1 Critical Issues (重写必须修复)

#问题影响范围严重程度
C1SFTP禁用主机密钥验证 (PromiscuousVerifier)SFTPService line 140安全:中间人攻击风险
C2SSO Cookie未设置HttpOnly/Secure标志CustomAuthenticationSuccessHandler line 161-162安全:Cookie可被窃取
C3所有凭据明文存储在配置文件中全部集成安全:凭据泄露风险
C4Salesforce SOQL查询拼接无参数化SfdcFacade line 203; DefaultAccountService line 169-185安全:SOQL注入
C5FieldMappingFactory静态单例与共享状态FieldMappingFactory line 40-41, 56-63并发:多产品环境数据错乱
C6SfdcFacade单例中共享PartnerConnectionSfdcFacade line 36-37, 52并发:多线程连接冲突
C7ZoomAccessTokenManager全局静态缓存ZoomAccessTokenManager line 12并发:多产品Token混淆

6.2 Design Defects (应该改进)

#问题影响范围
D1无断路器/超时/限流机制全部外部调用
D2错误处理不一致:printStackTrace vs log vs 吞没全部集成
D3无统一的重试策略(仅Podio有)Salesforce, Zoom, SendGrid, SFTP
D4ZoomVirtualMeetingService与ZoomWebinarService大量代码重复virtualProgram模块
D5SimpleDateFormat非线程安全SendGridService line 41
D6UserAuthToken表无过期清理机制SSO模块
D7CloudConvert异常被完全吞没DocumentTemplateService line 927-929
D8SendGrid URL未编码SendGridService line 155-161
D9Google API使用自建RestTemplate无连接池GoogleAPIService line 19
D10SFTP finally块中抛RuntimeException可能掩盖原始异常SFTPService line 107-109
D11外部API /v1/external/authentication 返回Token但无速率限制ExternalAPIService

6.3 Technical Debt (锦上添花)

#问题影响范围
T1Salesforce使用SOAP Partner API,应迁移到REST APIsf模块全部
T2YAML字段映射应迁移到数据库可配置10个YAML文件
T3SendGridController完全为空SendGrid模块
T4VirtualProgramController Swagger标签错误VirtualProgramController line 41
T5遗留的generateZoomSignature()方法未删除ZoomAbstractService line 221-243
T6Veeva对象名硬编码(如Multichannel_Activity_vod__cVeevaAccountService line 40
T7ForceService中log.error()用于记录正常请求数据ForceService line 64, 87
T8双SSO服务共存(pharmagin-sso + pharmagin-login)SSO架构
T9Google API返回无类型MapGoogleAPIService line 31
T10Mosaic导出使用SimpleDateFormatMosaicService line 29

7. Rewrite Recommendations

7.1 Integration Architecture (统一集成架构)

建议引入统一的集成基础设施层:

IntegrationClient (统一抽象)
├── RetryPolicy (指数退避 + 抖动)
├── CircuitBreaker (断路器)
├── TimeoutPolicy (超时控制)
├── RateLimiter (限流)
├── Metrics (Micrometer指标)
└── CredentialProvider (密钥管理)

技术选型建议:

  • 使用 Resilience4j 替代手工重试,提供CircuitBreaker + Retry + TimeLimiter + RateLimiter
  • 使用 AWS Secrets ManagerHashiCorp Vault 管理凭据
  • 使用 Micrometer 采集集成指标并对接Prometheus/Grafana
  • 使用 Spring WebClient (响应式) 替代 RestTemplate,提供更好的超时和错误处理

7.2 Salesforce Integration

  1. 从SOAP迁移到REST API: Salesforce SOAP Partner API是遗留接口,REST API更现代、性能更好
  2. 消除SOQL拼接: 使用参数化查询或至少严格的输入验证
  3. 按产品隔离连接: 每个产品/客户拥有独立的Salesforce连接,不共享单例
  4. 字段映射数据库化: 从YAML迁移到数据库表,支持运行时配置变更
  5. 异步化大批量同步: 使用消息队列(如SQS/RabbitMQ)处理大批量的Activity同步

7.3 Zoom Integration

  1. 合并Meeting和Webinar服务: 提取公共逻辑到基类,通过策略模式差异化URL路径
  2. 按产品隔离Token缓存: 使用 Map<productId, CachedToken> 替代全局静态缓存
  3. 增强重试逻辑: 处理429(Rate Limit)响应,实现指数退避
  4. 合并Webhook端点: 使用单一端点按event_type路由,减少配置复杂性
  5. 删除遗留签名方法: 移除 generateZoomSignature() 旧实现

7.4 SFTP

  1. 强制启用主机密钥验证: 配置已知主机密钥或使用证书认证
  2. SSH密钥认证替代密码: 使用私钥文件而非明文密码
  3. 引入连接池: 复用SSH连接避免频繁建连
  4. 增加超时和重试: 配置连接超时、读写超时

7.5 SSO

  1. 启用Cookie安全标志: 必须设置 HttpOnly=trueSecure=true
  2. Token过期清理: 添加 expires_at 列和定期清理Job
  3. 统一到pharmagin-login: 完成IDP配置迁移,废弃pharmagin-sso
  4. 考虑使用标准Token: 用JWT替代自定义UUID Token,减少数据库依赖

7.6 SendGrid

  1. 修复URL编码: 使用 UriComponentsBuilder 正确编码查询参数
  2. 修复SimpleDateFormat线程安全: 使用 DateTimeFormatter(线程安全)
  3. 添加速率限制处理: 检测429响应并退避
  4. 限制递归深度: 添加最大递归深度参数防止栈溢出
  5. 实现Controller端点: 添加手动同步触发和状态查询API

7.7 凭据管理

所有集成的凭据应统一迁移到密钥管理服务:

  • 开发环境可使用Spring Cloud Config的 {cipher} 加密
  • 生产环境应使用AWS Secrets Manager或Vault
  • 支持密钥自动轮换
  • 审计密钥访问日志

7.8 统一可观测性

  1. 结构化日志: 所有集成操作使用MDC记录correlation ID、集成名称、操作类型
  2. 指标采集: 每次外部调用记录延迟、成功/失败计数
  3. 健康检查端点: 暴露 /actuator/health 中各集成的连通性检查
  4. 告警: 配置错误率/延迟阈值告警