docs: 完善项目文档并清理过时文件

新增文档:
- API_INTEGRATION_GUIDE.md: API集成指南(快速开始、SDK示例、常见场景)
- DEPLOYMENT_GUIDE.md: 部署指南(环境要求、生产部署、Docker部署)
- CONFIGURATION_GUIDE.md: 配置指南(环境配置、数据库、Redis、安全)
- DEVELOPMENT_GUIDE.md: 开发指南(环境搭建、项目结构、开发规范)

文档更新:
- api.md: 补充8个缺失的API端点(分享跟踪、回调、用户奖励)

文档清理:
- 归档18个过时文档到 docs/archive/2026-03-04-cleanup/
- 删除3个调试文档(ralph-loop-*)

代码清理:
- 删除4个.bak备份文件
- 删除1个.disabled测试文件

文档结构优化:
- 从~40个文档精简到12个核心文档
- 建立清晰的文档导航体系
- 完善文档间的交叉引用
This commit is contained in:
Your Name
2026-03-04 10:41:38 +08:00
parent e79d69f0af
commit 0eed01e9eb
31 changed files with 3229 additions and 1476 deletions

View File

@@ -1,90 +0,0 @@
package com.mosquito.project.controller;
import com.mosquito.project.dto.ApiKeyCreateRequest;
import com.mosquito.project.dto.ApiKeyResponse;
import com.mosquito.project.service.ApiKeySecurityService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Optional;
/**
* API密钥安全控制器
* 提供密钥的恢复、轮换等安全功能
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/api-keys")
@Tag(name = "API Key Security", description = "API密钥安全管理")
@RequiredArgsConstructor
public class ApiKeySecurityController {
private final ApiKeySecurityService apiKeySecurityService;
/**
* 重新显示API密钥
*/
@PostMapping("/{id}/reveal")
@Operation(summary = "重新显示API密钥", description = "在验证权限后重新显示API密钥")
public ResponseEntity<ApiKeyResponse> revealApiKey(
@PathVariable Long id,
@RequestBody Map<String, String> request) {
String verificationCode = request.get("verificationCode");
Optional<String> rawKey = apiKeySecurityService.revealApiKey(id, verificationCode);
if (rawKey.isPresent()) {
log.info("API key revealed successfully for id: {}", id);
return ResponseEntity.ok(
new ApiKeyResponse("API密钥重新显示成功", rawKey.get())
);
} else {
return ResponseEntity.notFound().build();
}
}
/**
* 轮换API密钥
*/
@PostMapping("/{id}/rotate")
@Operation(summary = "轮换API密钥", description = "撤销旧密钥并生成新密钥")
public ResponseEntity<ApiKeyResponse> rotateApiKey(
@PathVariable Long id) {
try {
var newApiKey = apiKeySecurityService.rotateApiKey(id);
log.info("API key rotated successfully for id: {}", id);
return ResponseEntity.ok(
new ApiKeyResponse("API密钥轮换成功",
"新密钥已生成,请妥善保存。旧密钥已撤销。")
);
} catch (Exception e) {
log.error("Failed to rotate API key: {}", id, e);
return ResponseEntity.badRequest()
.body(new ApiKeyResponse("轮换失败", e.getMessage()));
}
}
/**
* 获取API密钥使用信息
*/
@GetMapping("/{id}/info")
@Operation(summary = "获取API密钥信息", description = "获取API密钥的使用统计和安全状态")
public ResponseEntity<Map<String, Object>> getApiKeyInfo(@PathVariable Long id) {
// 这里可以添加密钥使用统计、最后访问时间等信息
Map<String, Object> info = Map.of(
"apiKeyId", id,
"status", "active",
"lastAccess", System.currentTimeMillis(),
"rotationAvailable", true
);
return ResponseEntity.ok(info);
}
}

View File

@@ -1,115 +0,0 @@
package com.mosquito.project.interceptor;
import com.mosquito.project.exception.RateLimitExceededException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;
/**
* 分布式速率限制拦截器
* 生产环境强制使用Redis进行限流
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RateLimitInterceptor implements HandlerInterceptor {
private final RedisTemplate<String, Object> redisTemplate;
@Value("${app.rate-limit.per-minute:100}")
private int perMinuteLimit;
@Value("${app.rate-limit.window-size:1}")
private int windowSizeMinutes;
@Value("${spring.profiles.active:dev}")
private String activeProfile;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 生产环境强制使用Redis
if ("prod".equals(activeProfile)) {
if (redisTemplate == null) {
log.error("Production mode requires Redis for rate limiting, but Redis is not configured");
throw new IllegalStateException("Production环境必须配置Redis进行速率限制");
}
return checkRateLimitWithRedis(request);
} else {
log.debug("Development mode: rate limiting using Redis (if available)");
return checkRateLimitWithRedis(request);
}
}
/**
* 使用Redis进行分布式速率限制
*/
private boolean checkRateLimitWithRedis(HttpServletRequest request) {
String clientIp = getClientIp(request);
String endpoint = request.getRequestURI();
String key = String.format("rate_limit:%s:%s", clientIp, endpoint);
try {
// Redis原子操作检查并设置
Long currentCount = (Long) redisTemplate.opsForValue().increment(key);
if (currentCount == 1) {
// 第一次访问,设置过期时间
redisTemplate.expire(key, windowSizeMinutes, TimeUnit.MINUTES);
log.debug("Rate limit counter initialized for key: {}", key);
}
if (currentCount > perMinuteLimit) {
log.warn("Rate limit exceeded for client: {}, endpoint: {}, count: {}",
clientIp, endpoint, currentCount);
throw new RateLimitExceededException(
String.format("请求过于频繁,请%d分钟后再试", windowSizeMinutes));
}
log.debug("Rate limit check passed for client: {}, count: {}", clientIp, currentCount);
return true;
} catch (Exception e) {
log.error("Redis rate limiting failed, falling back to allow: {}", e.getMessage());
// Redis故障时允许请求通过但记录警告
return true;
}
}
/**
* 获取客户端真实IP地址
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 处理多个IP的情况X-Forwarded-For可能包含多个IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip != null ? ip : "unknown";
}
}

View File

@@ -1,181 +0,0 @@
package com.mosquito.project.service;
import com.mosquito.project.persistence.entity.ApiKeyEntity;
import com.mosquito.project.persistence.repository.ApiKeyRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Optional;
/**
* API密钥安全管理服务
* 提供密钥的加密存储、恢复和轮换功能
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ApiKeySecurityService {
private final ApiKeyRepository apiKeyRepository;
@Value("${app.security.encryption-key:}")
private String encryptionKey;
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int TAG_LENGTH_BIT = 128;
private static final int IV_LENGTH_BYTE = 12;
/**
* 生成新的API密钥并加密存储
*/
@Transactional
public ApiKeyEntity generateAndStoreApiKey(Long activityId, String description) {
String rawApiKey = generateRawApiKey();
String encryptedKey = encrypt(rawApiKey);
ApiKeyEntity apiKey = new ApiKeyEntity();
apiKey.setActivityId(activityId);
apiKey.setDescription(description);
apiKey.setEncryptedKey(encryptedKey);
apiKey.setIsActive(true);
apiKey.setCreatedAt(java.time.LocalDateTime.now());
return apiKeyRepository.save(apiKey);
}
/**
* 解密API密钥仅用于重新显示
*/
public String decryptApiKey(ApiKeyEntity apiKey) {
try {
return decrypt(apiKey.getEncryptedKey());
} catch (Exception e) {
log.error("Failed to decrypt API key for id: {}", apiKey.getId(), e);
throw new RuntimeException("API密钥解密失败");
}
}
/**
* 重新显示API密钥需要额外验证
*/
@Transactional(readOnly = true)
public Optional<String> revealApiKey(Long apiKeyId, String verificationCode) {
ApiKeyEntity apiKey = apiKeyRepository.findById(apiKeyId)
.orElseThrow(() -> new RuntimeException("API密钥不存在"));
// 验证密钥状态
if (!apiKey.getIsActive()) {
throw new RuntimeException("API密钥已被撤销");
}
// 验证访问权限(这里可以添加邮箱/手机验证逻辑)
if (!verifyAccessPermission(apiKey, verificationCode)) {
log.warn("Unauthorized attempt to reveal API key: {}", apiKeyId);
throw new RuntimeException("访问权限验证失败");
}
return Optional.of(decryptApiKey(apiKey));
}
/**
* 轮换API密钥
*/
@Transactional
public ApiKeyEntity rotateApiKey(Long apiKeyId) {
ApiKeyEntity oldKey = apiKeyRepository.findById(apiKeyId)
.orElseThrow(() -> new RuntimeException("API密钥不存在"));
// 撤销旧密钥
oldKey.setIsActive(false);
oldKey.setRevokedAt(java.time.LocalDateTime.now());
apiKeyRepository.save(oldKey);
// 生成新密钥
return generateAndStoreApiKey(oldKey.getActivityId(),
oldKey.getDescription() + " (轮换)");
}
/**
* 生成原始API密钥
*/
private String generateRawApiKey() {
return java.util.UUID.randomUUID().toString() + "-" +
java.util.UUID.randomUUID().toString();
}
/**
* 加密密钥
*/
private String encrypt(String data) {
try {
byte[] keyBytes = encryptionKey.getBytes(StandardCharsets.UTF_8);
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
byte[] iv = new byte[IV_LENGTH_BYTE];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
// IV + encrypted data
byte[] combined = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);
return Base64.getEncoder().encodeToString(combined);
} catch (Exception e) {
log.error("Encryption failed", e);
throw new RuntimeException("加密失败");
}
}
/**
* 解密密钥
*/
private String decrypt(String encryptedData) {
try {
byte[] combined = Base64.getDecoder().decode(encryptedData);
// 提取IV和加密数据
byte[] iv = new byte[IV_LENGTH_BYTE];
byte[] encrypted = new byte[combined.length - IV_LENGTH_BYTE];
System.arraycopy(combined, 0, iv, 0, IV_LENGTH_BYTE);
System.arraycopy(combined, IV_LENGTH_BYTE, encrypted, 0, encrypted.length);
byte[] keyBytes = encryptionKey.getBytes(StandardCharsets.UTF_8);
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
byte[] decrypted = cipher.doFinal(encrypted);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("Decryption failed", e);
throw new RuntimeException("解密失败");
}
}
/**
* 验证访问权限(可扩展为邮箱/手机验证)
*/
private boolean verifyAccessPermission(ApiKeyEntity apiKey, String verificationCode) {
// 这里可以实现复杂的验证逻辑
// 例如:验证邮箱验证码、手机验证码、安全问题等
return true; // 简化实现
}
}