25 KiB
25 KiB
设计文档
元数据
- 需求编号:001
- 子需求编号:001-01
- 创建时间:2026-01-29
- 创建人:AI Assistant
- 状态:已完成
设计概述
本设计文档基于 Spring Boot 和若依框架,实现 Salesforce Partner API 的认证和会话管理功能。核心设计包括:
- 工厂模式实现:通过 PartnerConnectionFactory 工厂类创建和管理 PartnerConnection 连接,继承 AbstractConnectionFactory 抽象类
- 连接缓存机制:利用 AbstractConnectionFactory 提供的连接缓存和自动 Session 有效性检查功能
- 会话管理集成:集成 SessionManager 获取会话信息,支持 Session 过期时自动重新登录
- RESTful API 设计:提供标准的 REST API 接口,包括获取会话信息、获取用户信息、修改密码等功能
- 异常处理机制:使用 datai-salesforce-common 模块的异常体系,统一处理各种异常情况
架构设计
系统架构图
graph TB
subgraph "Controller 层"
A[PartnerConnectionController]
end
subgraph "Service 层"
B[PartnerConnectionService]
end
subgraph "Factory 层"
C[PartnerConnectionFactory]
end
subgraph "Auth 模块"
D[SessionManager]
E[AbstractConnectionFactory]
end
subgraph "Common 模块"
F[异常体系]
end
subgraph "Salesforce API"
G[PartnerConnection]
H[Connector]
end
A --> B
B --> C
C --> D
C --> E
B --> F
C --> H
H --> G
模块架构图
datai-salesforce-partner/
├── controller/
│ └── PartnerConnectionController.java
├── service/
│ ├── IPartnerConnectionService.java
│ └── impl/
│ └── PartnerConnectionServiceImpl.java
├── factory/
│ └── PartnerConnectionFactory.java
└── model/
├── dto/
│ ├── SessionInfoDto.java
│ ├── UserInfoDto.java
│ └── ChangePasswordDto.java
└── vo/
├── SessionInfoVo.java
├── UserInfoVo.java
└── ChangePasswordVo.java
数据流图
sequenceDiagram
participant Client as 客户端
participant Controller as Controller
participant Service as Service
participant Factory as Factory
participant SessionManager as SessionManager
participant Salesforce as Salesforce API
Client->>Controller: GET /api/partner/session
Controller->>Service: getSessionInfo()
Service->>SessionManager: getCurrentSession("source")
SessionManager-->>Service: sessionId
Service->>SessionManager: getInstanceUrl("source")
SessionManager-->>Service: instanceUrl
Service-->>Controller: SessionInfo
Controller-->>Client: JSON Response
Client->>Controller: GET /api/partner/user-info
Controller->>Service: getUserInfo()
Service->>Factory: getConnection("source")
Factory->>SessionManager: getCurrentSession("source")
Factory->>SessionManager: getInstanceUrl("source")
Factory->>Salesforce: createConnection()
Salesforce-->>Factory: PartnerConnection
Factory-->>Service: PartnerConnection
Service->>PartnerConnection: getUserInfo()
PartnerConnection-->>Service: GetUserInfoResult
Service-->>Controller: UserInfo
Controller-->>Client: JSON Response
Client->>Controller: POST /api/partner/change-password
Controller->>Service: changePassword(oldPassword, newPassword)
Service->>Factory: getConnection("source")
Factory-->>Service: PartnerConnection
Service->>PartnerConnection: changeOwnPassword()
PartnerConnection-->>Service: ChangeOwnPasswordResult
Service-->>Controller: Result
Controller-->>Client: JSON Response
技术方案
技术选型
| 技术组件 | 版本/类型 | 选择理由 |
|---|---|---|
| Spring Boot | 3.x | 成熟稳定,生态丰富,支持自动配置 |
| Spring Web | 3.x | 提供 RESTful API 支持 |
| Spring Security | 6.x | 提供权限控制和认证 |
| MyBatis Plus | 3.x | 简化数据库操作,提供 CRUD 功能 |
| Salesforce Partner API | WSC 58.x | 官方 Java 客户端,功能完整 |
| Lombok | 1.18.x | 简化 Java 代码,减少样板代码 |
| Swagger/OpenAPI | 3.x | 自动生成 API 文档 |
| 若依框架 | 最新版 | 提供系统基础功能,如用户管理、权限管理 |
核心算法设计
连接创建算法
protected PartnerConnection createConnection(String orgType) {
String sessionId = sessionManager.getCurrentSession(orgType);
String instanceUrl = sessionManager.getInstanceUrl(orgType);
ConnectorConfig config = new ConnectorConfig();
config.setSessionId(sessionId);
config.setRestEndpoint(instanceUrl + "/services/Soap/u/" + API_VERSION);
config.setCompression(true);
config.setTraceMessage(false);
return Connector.newConnection(config);
}
复杂度分析:
- 时间复杂度:O(1) - 直接创建连接对象
- 空间复杂度:O(1) - 只创建一个连接对象
优化点:
- 使用连接缓存避免重复创建
- 自动检查 Session 有效性
- 支持多线程安全访问
Session 有效性检查算法
public boolean isSessionValid(String orgType) {
SalesforceLoginResult loginResult = getCurrentLoginResult(orgType);
return loginResult != null
&& loginResult.isSuccess()
&& !loginResult.isSessionExpired();
}
复杂度分析:
- 时间复杂度:O(1) - 直接检查登录结果
- 空间复杂度:O(1) - 不需要额外空间
优化点:
- 使用缓存存储登录结果
- 支持自动重新登录
- 异常处理完善
集成方案设计
与 SessionManager 集成
集成方式:
- 通过依赖注入获取 SessionManager 实例
- 调用
getCurrentSession("source")获取 Session ID - 调用
getInstanceUrl("source")获取实例 URL - 自动处理 Session 过期情况
数据交换格式:
- Session ID:String 类型
- 实例 URL:String 类型
- 登录结果:SalesforceLoginResult 对象
错误处理机制:
- 捕获 SessionManager 抛出的异常
- 转换为 SalesforceAuthException
- 返回友好的错误消息
重试机制:
- SessionManager 内部已实现自动重新登录
- 不需要在 Service 层额外实现重试
- 最多重试 3 次(由 SessionManager 控制)
与 Salesforce Partner API 集成
集成方式:
- 使用 WSC (Web Services Connector) 库
- 通过 Connector.newConnection() 创建连接
- 调用 PartnerConnection 的方法执行操作
数据交换格式:
- 请求:SOAP/XML 格式
- 响应:SOAP/XML 格式
错误处理机制:
- 捕获 ConnectionException
- 捕获 ApiFault
- 转换为自定义异常
重试机制:
- 使用 SalesforceConstants.MAX_RETRY_COUNT 控制重试次数
- 使用 SalesforceConstants.RETRY_DELAY_MS 控制重试延迟
数据模型
数据库表设计
本需求不涉及数据库表的创建或修改,所有数据均来自 Salesforce API。
实体类设计
SessionInfoDto
package com.datai.partner.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "会话信息 DTO")
public class SessionInfoDto {
@Schema(description = "会话 ID")
private String sessionId;
@Schema(description = "实例 URL")
private String instanceUrl;
@Schema(description = "是否成功")
private Boolean success;
}
UserInfoDto
package com.datai.partner.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "用户信息 DTO")
public class UserInfoDto {
@Schema(description = "用户 ID")
private String userId;
@Schema(description = "用户名")
private String userName;
@Schema(description = "用户全名")
private String userFullName;
@Schema(description = "用户邮箱")
private String userEmail;
@Schema(description = "组织 ID")
private String organizationId;
@Schema(description = "组织名称")
private String organizationName;
@Schema(description = "用户语言")
private String userLanguage;
@Schema(description = "用户时区")
private String userTimeZone;
@Schema(description = "货币符号")
private String currencySymbol;
}
ChangePasswordDto
package com.datai.partner.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "修改密码 DTO")
public class ChangePasswordDto {
@NotBlank(message = "旧密码不能为空")
@Schema(description = "旧密码")
private String oldPassword;
@NotBlank(message = "新密码不能为空")
@Schema(description = "新密码")
private String newPassword;
}
SessionInfoVo
package com.datai.partner.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "会话信息 VO")
public class SessionInfoVo {
@Schema(description = "会话 ID")
private String sessionId;
@Schema(description = "实例 URL")
private String instanceUrl;
}
UserInfoVo
package com.datai.partner.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "用户信息 VO")
public class UserInfoVo {
@Schema(description = "用户 ID")
private String userId;
@Schema(description = "用户名")
private String userName;
@Schema(description = "用户全名")
private String userFullName;
@Schema(description = "用户邮箱")
private String userEmail;
@Schema(description = "组织 ID")
private String organizationId;
@Schema(description = "组织名称")
private String organizationName;
@Schema(description = "用户语言")
private String userLanguage;
@Schema(description = "用户时区")
private String userTimeZone;
@Schema(description = "货币符号")
private String currencySymbol;
}
数据字典设计
本需求不涉及数据字典的创建或修改。
接口设计
RESTful API 设计
1. 获取会话信息
接口路径:GET /api/partner/session
接口描述:获取当前 Salesforce 会话信息
请求参数:无
响应格式:
{
"code": 200,
"msg": "查询成功",
"data": {
"sessionId": "00D...!AQEAQ...",
"instanceUrl": "https://your-instance.salesforce.com"
}
}
错误响应:
{
"code": 500,
"msg": "获取会话信息失败: 没有有效的会话信息",
"data": null
}
2. 获取用户信息
接口路径:GET /api/partner/user-info
接口描述:获取当前 Salesforce 用户信息
请求参数:无
响应格式:
{
"code": 200,
"msg": "查询成功",
"data": {
"userId": "005...",
"userName": "user@example.com",
"userFullName": "John Doe",
"userEmail": "user@example.com",
"organizationId": "00D...",
"organizationName": "Acme Corp",
"userLanguage": "en_US",
"userTimeZone": "America/Los_Angeles",
"currencySymbol": "$"
}
}
错误响应:
{
"code": 500,
"msg": "获取用户信息失败: Session 无效",
"data": null
}
3. 修改密码
接口路径:POST /api/partner/change-password
接口描述:修改当前用户密码
请求参数:
{
"oldPassword": "OldPassword123",
"newPassword": "NewPassword456"
}
响应格式:
{
"code": 200,
"msg": "密码修改成功",
"data": null
}
错误响应:
{
"code": 500,
"msg": "密码修改失败: 旧密码不正确",
"data": null
}
接口权限设计
所有接口都需要登录认证,但不需要特定权限。
@PreAuthorize("@ss.hasLogin()")
接口文档设计
Swagger 注解示例
@Tag(name = "【Partner API】认证和会话管理")
@RestController
@RequestMapping("/api/partner")
public class PartnerConnectionController extends BaseController {
@Operation(summary = "获取会话信息")
@PreAuthorize("@ss.hasLogin()")
@GetMapping("/session")
public AjaxResult getSessionInfo() {
// 实现
}
@Operation(summary = "获取用户信息")
@PreAuthorize("@ss.hasLogin()")
@GetMapping("/user-info")
public AjaxResult getUserInfo() {
// 实现
}
@Operation(summary = "修改密码")
@PreAuthorize("@ss.hasLogin()")
@PostMapping("/change-password")
public AjaxResult changePassword(@Valid @RequestBody ChangePasswordDto dto) {
// 实现
}
}
实现要点
关键实现逻辑
1. 工厂层实现
PartnerConnectionFactory 类:
package com.datai.partner.factory;
import com.datai.auth.core.SessionManager;
import com.datai.auth.factory.AbstractConnectionFactory;
import com.datai.common.constant.SalesforceConstants;
import com.sforce.soap.partner.Connector;
import com.sforce.soap.partner.ConnectorConfig;
import com.sforce.soap.partner.PartnerConnection;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class PartnerConnectionFactory extends AbstractConnectionFactory<PartnerConnection> {
private static final String API_VERSION = "v58.0";
@Override
protected PartnerConnection createConnection(String orgType) {
try {
String sessionId = sessionManager.getCurrentSession(orgType);
String instanceUrl = sessionManager.getInstanceUrl(orgType);
ConnectorConfig config = new ConnectorConfig();
config.setSessionId(sessionId);
config.setRestEndpoint(instanceUrl + "/services/Soap/u/" + API_VERSION);
config.setCompression(true);
config.setTraceMessage(false);
PartnerConnection connection = Connector.newConnection(config);
log.info("PartnerConnection 创建成功,ORG类型: {}", orgType);
return connection;
} catch (Exception e) {
log.error("创建 PartnerConnection 失败,ORG类型: {}, 错误: {}", orgType, e.getMessage(), e);
throw new RuntimeException("创建 PartnerConnection 失败: " + e.getMessage(), e);
}
}
}
实现要点:
- 继承 AbstractConnectionFactory,利用其提供的连接缓存和 Session 有效性检查功能
- 通过 SessionManager 获取 Session ID 和实例 URL
- 创建 ConnectorConfig 并设置必要的配置
- 使用 Connector.newConnection() 创建 PartnerConnection 实例
- 异常处理完善,记录详细日志
2. Service 层实现
IPartnerConnectionService 接口:
package com.datai.partner.service;
import com.datai.partner.model.dto.ChangePasswordDto;
import com.datai.partner.model.dto.UserInfoDto;
import com.sforce.soap.partner.GetUserInfoResult;
public interface IPartnerConnectionService {
String getSessionId();
String getInstanceUrl();
GetUserInfoResult getUserInfo();
void changePassword(String oldPassword, String newPassword);
}
PartnerConnectionServiceImpl 实现类:
package com.datai.partner.service.impl;
import com.datai.auth.core.SessionManager;
import com.datai.common.exception.SalesforceAuthException;
import com.datai.common.exception.SalesforceLoginException;
import com.datai.partner.factory.PartnerConnectionFactory;
import com.datai.partner.service.IPartnerConnectionService;
import com.sforce.soap.partner.GetUserInfoResult;
import com.sforce.soap.partner.PartnerConnection;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class PartnerConnectionServiceImpl implements IPartnerConnectionService {
@Autowired
private PartnerConnectionFactory partnerConnectionFactory;
@Autowired
private SessionManager sessionManager;
@Override
public String getSessionId() {
try {
return sessionManager.getCurrentSession("source");
} catch (Exception e) {
log.error("获取 Session ID 失败: {}", e.getMessage(), e);
throw new SalesforceAuthException("获取 Session ID 失败: " + e.getMessage());
}
}
@Override
public String getInstanceUrl() {
try {
return sessionManager.getInstanceUrl("source");
} catch (Exception e) {
log.error("获取实例 URL 失败: {}", e.getMessage(), e);
throw new SalesforceAuthException("获取实例 URL 失败: " + e.getMessage());
}
}
@Override
public GetUserInfoResult getUserInfo() {
try {
PartnerConnection connection = partnerConnectionFactory.getConnection("source");
return connection.getUserInfo();
} catch (Exception e) {
log.error("获取用户信息失败: {}", e.getMessage(), e);
throw new SalesforceAuthException("获取用户信息失败: " + e.getMessage());
}
}
@Override
public void changePassword(String oldPassword, String newPassword) {
try {
PartnerConnection connection = partnerConnectionFactory.getConnection("source");
connection.changeOwnPassword(oldPassword, newPassword);
log.info("密码修改成功");
} catch (Exception e) {
log.error("修改密码失败: {}", e.getMessage(), e);
throw new SalesforceAuthException("修改密码失败: " + e.getMessage());
}
}
}
实现要点:
- 使用 @Autowired 注入 PartnerConnectionFactory 和 SessionManager
- 所有方法固定使用 "source" orgType
- 通过工厂获取 PartnerConnection 实例
- 调用 PartnerConnection 的方法执行操作
- 异常处理完善,使用 datai-salesforce-common 的异常类
3. Controller 层实现
PartnerConnectionController 类:
package com.datai.partner.controller;
import com.datai.common.core.controller.BaseController;
import com.datai.common.core.domain.AjaxResult;
import com.datai.partner.model.dto.ChangePasswordDto;
import com.datai.partner.model.dto.UserInfoDto;
import com.datai.partner.service.IPartnerConnectionService;
import com.sforce.soap.partner.GetUserInfoResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@Slf4j
@Tag(name = "【Partner API】认证和会话管理")
@RestController
@RequestMapping("/api/partner")
public class PartnerConnectionController extends BaseController {
@Autowired
private IPartnerConnectionService partnerConnectionService;
@Operation(summary = "获取会话信息")
@PreAuthorize("@ss.hasLogin()")
@GetMapping("/session")
public AjaxResult getSessionInfo() {
try {
String sessionId = partnerConnectionService.getSessionId();
String instanceUrl = partnerConnectionService.getInstanceUrl();
return success(sessionId, instanceUrl);
} catch (Exception e) {
log.error("获取会话信息失败: {}", e.getMessage(), e);
return error("获取会话信息失败: " + e.getMessage());
}
}
@Operation(summary = "获取用户信息")
@PreAuthorize("@ss.hasLogin()")
@GetMapping("/user-info")
public AjaxResult getUserInfo() {
try {
GetUserInfoResult result = partnerConnectionService.getUserInfo();
UserInfoDto dto = new UserInfoDto();
dto.setUserId(result.getUserId());
dto.setUserName(result.getUserName());
dto.setUserFullName(result.getUserFullName());
dto.setUserEmail(result.getUserEmail());
dto.setOrganizationId(result.getOrganizationId());
dto.setOrganizationName(result.getOrganizationName());
dto.setUserLanguage(result.getUserLanguage());
dto.setUserTimeZone(result.getUserTimeZone());
dto.setCurrencySymbol(result.getCurrencySymbol());
return success(dto);
} catch (Exception e) {
log.error("获取用户信息失败: {}", e.getMessage(), e);
return error("获取用户信息失败: " + e.getMessage());
}
}
@Operation(summary = "修改密码")
@PreAuthorize("@ss.hasLogin()")
@PostMapping("/change-password")
public AjaxResult changePassword(@Valid @RequestBody ChangePasswordDto dto) {
try {
partnerConnectionService.changePassword(dto.getOldPassword(), dto.getNewPassword());
return success("密码修改成功");
} catch (Exception e) {
log.error("修改密码失败: {}", e.getMessage(), e);
return error("修改密码失败: " + e.getMessage());
}
}
}
实现要点:
- 继承 BaseController,使用其提供的 success() 和 error() 方法
- 使用 @PreAuthorize 注解进行权限控制
- 使用 @Valid 注解进行参数验证
- 使用 @Tag 和 @Operation 注解生成 Swagger 文档
- 异常处理完善,返回友好的错误消息
异常处理设计
可能出现的异常
-
SalesforceAuthException:认证异常
- 原因:Session 无效、Session 过期、获取会话信息失败
- 处理:记录日志,返回友好的错误消息
-
SalesforceLoginException:登录异常
- 原因:登录失败、用户名或密码错误
- 处理:记录日志,返回友好的错误消息
-
ConnectionException:连接异常
- 原因:网络问题、Salesforce API 不可用
- 处理:记录日志,返回友好的错误消息
-
ApiFault:API 错误
- 原因:Salesforce API 返回错误
- 处理:记录日志,返回友好的错误消息
异常处理方式
try {
PartnerConnection connection = partnerConnectionFactory.getConnection("source");
GetUserInfoResult result = connection.getUserInfo();
return success(result);
} catch (ConnectionException e) {
log.error("获取用户信息失败: {}", e.getMessage(), e);
throw new SalesforceAuthException("获取用户信息失败: " + e.getMessage());
} catch (ApiFault e) {
log.error("获取用户信息失败: {}", e.getMessage(), e);
throw new SalesforceAuthException("获取用户信息失败: " + e.getMessage());
} catch (Exception e) {
log.error("获取用户信息失败: {}", e.getMessage(), e);
throw new SalesforceAuthException("获取用户信息失败: " + e.getMessage());
}
异常提示信息
- "获取会话信息失败: 没有有效的会话信息"
- "获取用户信息失败: Session 无效"
- "修改密码失败: 旧密码不正确"
- "创建 PartnerConnection 失败: 网络连接失败"
性能优化设计
1. 连接缓存
优化策略:
- 使用 AbstractConnectionFactory 提供的连接缓存
- 避免重复创建连接,提高性能
- 自动检查 Session 有效性,过期时自动清除缓存
性能目标:
- 连接创建时间:< 100ms
- 缓存命中率:> 90%
- 并发支持:> 100 TPS
2. 多线程安全
优化策略:
- 使用 ReentrantLock 保证多线程环境下的连接获取安全
- 使用 ConcurrentHashMap 存储连接缓存
- 避免竞态条件和死锁
性能目标:
- 并发访问:支持 > 100 个并发请求
- 响应时间:< 50ms(缓存命中)
3. 压缩传输
优化策略:
- 启用 ConnectorConfig.setCompression(true)
- 减少 SOAP 消息传输大小
- 提高网络传输效率
性能目标:
- 传输数据量减少:> 50%
- 网络传输时间减少:> 30%
安全设计
1. 数据加密
加密方式:
- 使用 HTTPS 协议传输数据
- Session ID 通过加密通道传输
- 密码修改时使用加密传输
2. 权限控制
控制方式:
- 使用 @PreAuthorize 注解进行权限控制
- 所有接口都需要登录认证
- 不需要特定权限,但需要登录
3. 防注入方式
防 SQL 注入:
- 不直接拼接 SQL 语句
- 使用参数化查询
防 XSS 攻击:
- 对用户输入进行验证和过滤
- 使用 @Valid 注解进行参数验证
- 不直接输出用户输入的内容