16 KiB
16 KiB
提示词:日志记录功能
引用真源
需求描述
根据需求文档,实现 Salesforce Apex 日志记录功能,包括:
- 获取日志信息:通过日志 ID 获取日志详细信息
- 设置调试头部:设置调试头部信息,用于收集详细的调试日志
- 清除调试头部:清除调试头部信息
- 查询日志列表:分页查询日志列表,支持按时间、应用程序、状态等条件筛选
- 获取日志详情:获取日志详情,包括日志分类信息
- 执行匿名代码并获取日志:执行匿名 Apex 代码并获取执行日志
核心要求:
- 固定使用 source org 类型
- 使用 ApexConnectionFactory 获取 SoapConnection 实例
- 日志信息存储到数据库(datai_apex_log 和 datai_apex_log_category 表)
- 使用 ReentrantLock 保证多线程安全
- 提供 RESTful API 接口
设计方案
根据设计文档,采用以下技术方案:
- 模块:
datai-salesforce-apex - 架构:分层架构(Controller -> Service -> Mapper -> Entity)
- 服务层:
IApexLogService接口 +ApexLogServiceImpl实现 - 控制器:
ApexLogController提供 REST API - 数据模型:Entity、DTO 和 VO 类
- 异常处理:使用
SalesforceAuthException和SalesforceOperationException - 并发控制:使用
ReentrantLock保证多线程安全 - 数据库:MySQL 8.0+,使用 MyBatis Plus
包结构
com.datai.apex
├── controller
│ └── ApexLogController.java
├── service
│ ├── IApexLogService.java
│ └── impl
│ └── ApexLogServiceImpl.java
├── mapper
│ ├── DataiApexLogMapper.java
│ └── DataiApexLogCategoryMapper.java
├── domain
│ ├── DataiApexLog.java
│ └── DataiApexLogCategory.java
└── model
├── dto
│ ├── SetDebuggingHeaderDto.java
│ ├── QueryLogsDto.java
│ ├── ExecuteAndLogDto.java
│ └── LogCategoryDto.java
└── vo
├── LogInfoVo.java
├── LogDetailVo.java
├── LogCategoryVo.java
└── ExecuteAndLogResultVo.java
输出格式要求
必须包含的文件
-
服务接口:
IApexLogService.java- 路径:
datai-salesforce-apex/src/main/java/com/datai/apex/service/IApexLogService.java - 定义所有日志记录方法
- 路径:
-
服务实现:
ApexLogServiceImpl.java- 路径:
datai-salesforce-apex/src/main/java/com/datai/apex/service/impl/ApexLogServiceImpl.java - 实现所有服务方法
- 固定使用 source org 类型
- 使用 ReentrantLock 保证多线程安全
- 路径:
-
控制器:
ApexLogController.java- 路径:
datai-salesforce-apex/src/main/java/com/datai/apex/controller/ApexLogController.java - 提供 6 个 REST API 接口
- 使用
@Tag注解定义 Swagger 文档 - 使用
@PreAuthorize注解进行权限控制
- 路径:
-
Mapper 接口:
DataiApexLogMapper.java- 日志 MapperDataiApexLogCategoryMapper.java- 日志分类 Mapper- 路径:
datai-salesforce-apex/src/main/java/com/datai/apex/mapper/ - 继承
BaseMapper<T>
-
Entity 类:
DataiApexLog.java- 日志实体DataiApexLogCategory.java- 日志分类实体- 路径:
datai-salesforce-apex/src/main/java/com/datai/apex/domain/ - 使用
@TableName注解指定表名 - 使用
@TableId注解指定主键 - 使用
@TableField注解指定字段映射
-
DTO 类:
SetDebuggingHeaderDto.java- 设置调试头部请求参数QueryLogsDto.java- 查询日志列表请求参数ExecuteAndLogDto.java- 执行匿名代码并获取日志请求参数LogCategoryDto.java- 日志分类 DTO- 路径:
datai-salesforce-apex/src/main/java/com/datai/apex/model/dto/ - 使用
@Schema注解定义 Swagger 文档 - 使用
@NotBlank等注解进行参数校验
-
VO 类:
LogInfoVo.java- 日志信息 VOLogDetailVo.java- 日志详情 VOLogCategoryVo.java- 日志分类 VOExecuteAndLogResultVo.java- 执行并获取日志结果 VO- 路径:
datai-salesforce-apex/src/main/java/com/datai/apex/model/vo/ - 使用
@Schema注解定义 Swagger 文档
REST API 接口列表
-
获取日志信息
- 方法:GET
- 路径:
/api/apex/log - 权限:
apex:log:query - 响应:
LogInfoVo
-
设置调试头部
- 方法:POST
- 路径:
/api/apex/log/debug-header - 权限:
apex:log:manage - 请求:
SetDebuggingHeaderDto
-
清除调试头部
- 方法:DELETE
- 路径:
/api/apex/log/debug-header - 权限:
apex:log:manage
-
查询日志列表
- 方法:GET
- 路径:
/api/apex/log/list - 权限:
apex:log:query - 响应:
TableDataInfo
-
获取日志详情
- 方法:GET
- 路径:
/api/apex/log/detail - 权限:
apex:log:query - 响应:
LogDetailVo
-
执行匿名代码并获取日志
- 方法:POST
- 路径:
/api/apex/log/execute-and-log - 权限:
apex:log:execute - 请求:
ExecuteAndLogDto - 响应:
ExecuteAndLogResultVo
代码规范要求
- 类命名:首字母大写,驼峰命名,如
ApexLogController - 方法命名:首字母小写,驼峰命名,如
getLog - 变量命名:首字母小写,驼峰命名,如
apexLogService - 常量命名:全大写,下划线分隔,如
ORG_TYPE - 注释规范:
- 类注释:使用
/** */,包含类功能描述、作者、创建时间 - 方法注释:使用
/** */,包含方法功能、参数、返回值、异常描述 - 字段注释:使用
/** */,包含字段功能描述
- 类注释:使用
- 日志规范:使用 Lombok 的
@Slf4j注解,日志级别使用log.info()、log.error()、log.debug() - 异常处理:
- 捕获异常后记录日志
- 抛出
SalesforceAuthException或SalesforceOperationException - 在 Controller 层转换为
ServiceException或AjaxResult
- 代码格式:使用 4 个空格缩进,行宽不超过 120 字符
依赖要求
-
必须导入的类:
com.datai.common.exception.SalesforceAuthExceptioncom.datai.common.exception.SalesforceOperationExceptioncom.datai.common.exception.ServiceExceptioncom.datai.common.core.domain.AjaxResultcom.datai.common.core.page.TableDataInfocom.datai.auth.factory.ApexConnectionFactorycom.datai.auth.manager.SessionManagercom.sforce.soap.apex.SoapConnectioncom.sforce.soap.apex.LogInfocom.sforce.soap.apex.LogTypecom.sforce.soap.apex.LogCategorycom.sforce.soap.apex.LogCategoryLevelcom.sforce.soap.apex.ExecuteAnonymousResultcom.baomidou.mybatisplus.core.mapper.BaseMappercom.baomidou.mybatisplus.extension.service.impl.ServiceImpl
-
Spring 注解:
@RestController@RequestMapping@Autowired@Tag(Swagger)@Operation(Swagger)@PreAuthorize(Spring Security)@Valid@RequestBody@RequestParam@Schema(Swagger)@NotBlank(Validation)@NotNull(Validation)@DateTimeFormat
-
Lombok 注解:
@Slf4j@Data@Builder(可选)@NoArgsConstructor@AllArgsConstructor
-
MyBatis Plus 注解:
@TableName@TableId@TableField@TableLogic
测试要求
- 单元测试覆盖率:不低于 80%
- 测试用例场景:
- 正常获取日志信息成功
- 日志 ID 不存在
- 设置调试头部成功
- 清除调试头部成功
- 查询日志列表成功(分页)
- 查询日志列表成功(带条件筛选)
- 获取日志详情成功
- 执行匿名代码并获取日志成功
- 连接失败异常处理
- 日志保存失败异常处理
- 多线程并发设置调试头部
- 测试框架:使用 JUnit 5 和 Mockito
- 测试用例命名规范:
test+方法名+场景,如testGetLogSuccess - 测试数据:使用 Mockito 模拟 ApexConnectionFactory、Mapper 和 Salesforce API
实现要点
1. ApexLogServiceImpl 核心实现
@Service
@Slf4j
public class ApexLogServiceImpl extends ServiceImpl<DataiApexLogMapper, DataiApexLog> implements IApexLogService {
private static final String ORG_TYPE = "source";
@Autowired
private ApexConnectionFactory apexConnectionFactory;
@Autowired
private DataiApexLogMapper apexLogMapper;
@Autowired
private DataiApexLogCategoryMapper apexLogCategoryMapper;
private final ReentrantLock debugHeaderLock = new ReentrantLock();
@Override
public LogInfoVo getLog(String logId) {
try {
SoapConnection connection = apexConnectionFactory.getConnection(ORG_TYPE);
LogInfo logInfo = connection.getLog(logId);
return convertToLogInfoVo(logInfo);
} catch (ConnectionException e) {
throw new SalesforceAuthException("APEX_LOG_004", "连接失败", e);
} catch (Exception e) {
throw new SalesforceOperationException("APEX_LOG_005", "获取日志失败", e);
}
}
@Override
public void setDebuggingHeader(List<LogCategoryDto> logCategories, String logType) {
debugHeaderLock.lock();
try {
SoapConnection connection = apexConnectionFactory.getConnection(ORG_TYPE);
LogInfo[] categories = convertToLogInfoArray(logCategories);
LogType type = LogType.valueOf(logType);
connection.setDebuggingHeader(categories, type);
} finally {
debugHeaderLock.unlock();
}
}
@Override
public void clearDebuggingHeader() {
debugHeaderLock.lock();
try {
SoapConnection connection = apexConnectionFactory.getConnection(ORG_TYPE);
connection.clearDebuggingHeader();
} finally {
debugHeaderLock.unlock();
}
}
@Override
public TableDataInfo queryLogs(QueryLogsDto dto) {
Page<DataiApexLog> page = new Page<>(dto.getPage(), dto.getPageSize());
LambdaQueryWrapper<DataiApexLog> wrapper = new LambdaQueryWrapper<>();
wrapper.ge(dto.getStartTime() != null, DataiApexLog::getStartTime, dto.getStartTime())
.le(dto.getEndTime() != null, DataiApexLog::getStartTime, dto.getEndTime())
.eq(StringUtils.isNotBlank(dto.getApplication()), DataiApexLog::getApplication, dto.getApplication())
.eq(StringUtils.isNotBlank(dto.getStatus()), DataiApexLog::getStatus, dto.getStatus())
.orderByDesc(DataiApexLog::getCreateTime);
IPage<DataiApexLog> result = apexLogMapper.selectPage(page, wrapper);
return TableDataInfo.build(result);
}
@Override
public LogDetailVo getLogDetail(String logId) {
DataiApexLog log = apexLogMapper.selectOne(
new LambdaQueryWrapper<DataiApexLog>().eq(DataiApexLog::getLogId, logId)
);
if (log == null) {
throw new ServiceException("日志不存在");
}
List<DataiApexLogCategory> categories = apexLogCategoryMapper.selectList(
new LambdaQueryWrapper<DataiApexLogCategory>().eq(DataiApexLogCategory::getLogId, log.getId())
);
LogDetailVo vo = new LogDetailVo();
BeanUtils.copyProperties(log, vo);
vo.setCategories(convertToLogCategoryVoList(categories));
return vo;
}
@Override
public ExecuteAndLogResultVo executeAndLog(ExecuteAndLogDto dto) {
try {
setDebuggingHeader(dto.getLogCategories(), dto.getLogType());
SoapConnection connection = apexConnectionFactory.getConnection(ORG_TYPE);
ExecuteAnonymousResult result = connection.executeAnonymous(dto.getCode());
if (result.isCompiled() && StringUtils.isNotBlank(result.getApexLogId())) {
saveLogInfo(result.getApexLogId());
}
return convertToExecuteAndLogResultVo(result);
} catch (Exception e) {
throw new SalesforceOperationException("执行匿名代码失败", e);
}
}
private void saveLogInfo(String apexLogId) {
try {
SoapConnection connection = apexConnectionFactory.getConnection(ORG_TYPE);
LogInfo logInfo = connection.getLog(apexLogId);
DataiApexLog log = new DataiApexLog();
log.setLogId(logInfo.getId());
log.setApplication(logInfo.getApplication());
log.setDurationMilliseconds(logInfo.getDurationMilliseconds());
log.setLocation(logInfo.getLocation());
log.setLogLength(logInfo.getLogLength());
log.setOperation(logInfo.getOperation());
log.setStartTime(logInfo.getStartTime().getTime());
log.setStatus(logInfo.getStatus());
apexLogMapper.insert(log);
if (logInfo.getDebugLog() != null) {
saveLogCategories(log.getId(), logInfo.getDebugLog());
}
} catch (Exception e) {
log.error("保存日志信息失败", e);
}
}
private void saveLogCategories(Long logId, String debugLog) {
List<DataiApexLogCategory> categories = parseDebugLog(debugLog);
for (DataiApexLogCategory category : categories) {
category.setLogId(logId);
apexLogCategoryMapper.insert(category);
}
}
}
2. 固定使用 source org
所有方法中固定使用 "source" 作为 org 类型:
private static final String ORG_TYPE = "source";
SoapConnection connection = apexConnectionFactory.getConnection(ORG_TYPE);
3. 多线程安全设计
使用 ReentrantLock 保证调试头部设置和清除操作的原子性:
private final ReentrantLock debugHeaderLock = new ReentrantLock();
public void setDebuggingHeader(...) {
debugHeaderLock.lock();
try {
SoapConnection connection = apexConnectionFactory.getConnection(ORG_TYPE);
connection.setDebuggingHeader(logCategories, logType);
} finally {
debugHeaderLock.unlock();
}
}
4. 数据转换
将 Salesforce API 的 LogInfo 转换为 VO:
private LogInfoVo convertToLogInfoVo(LogInfo logInfo) {
LogInfoVo vo = new LogInfoVo();
vo.setId(logInfo.getId());
vo.setApplication(logInfo.getApplication());
vo.setDurationMilliseconds(logInfo.getDurationMilliseconds());
vo.setLocation(logInfo.getLocation());
vo.setLogLength(logInfo.getLogLength());
vo.setOperation(logInfo.getOperation());
vo.setStartTime(logInfo.getStartTime().getTime());
vo.setStatus(logInfo.getStatus());
vo.setSuccess(true);
return vo;
}
注意事项
- 固定使用 source org:所有方法必须固定使用 source org 类型,不要支持动态 org 类型
- 异常处理:必须正确处理 Salesforce API 的空值情况
- 日志记录:关键操作必须记录日志,包括成功和失败场景
- 权限控制:所有接口必须使用
@PreAuthorize注解进行权限控制 - 参数校验:使用 Bean Validation 进行请求参数校验
- 敏感信息:不在日志中记录完整的 Session ID
- 多线程安全:调试头部设置和清除操作必须使用 ReentrantLock 保证线程安全
- 数据库事务:保存日志信息时使用事务保证数据一致性
- 分页查询:使用 MyBatis Plus 的分页插件进行分页查询
- 级联删除:删除日志时,自动删除关联的日志分类(通过外键约束实现)
数据字典参考
日志分类(apex_log_category)
- DB:数据库操作
- APEX_CODE:Apex 代码
- VALIDATION:验证规则
- WORKFLOW:工作流
- CALLOUT:外部调用
- SYSTEM:系统操作
日志级别(apex_log_level)
- None:无
- Fine:细粒度
- Finer:更细粒度
- Finest:最细粒度
- Debug:调试
- Info:信息
- Warn:警告
- Error:错误
日志类型(apex_log_type)
- ApexCode:Apex 代码日志
- ApexProfiling:Apex 性能分析日志
- Callout:外部调用日志
- Database:数据库操作日志
- System:系统日志