本地 RAG 知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

308 lines
11 KiB

package com.wok.supportbot.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wok.supportbot.common.SortUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wok.supportbot.dao.ApiKeyMapper;
import com.wok.supportbot.entity.ApiKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* API Key 管理服务
* 提供 API Key 的生成、查询、吊销、启用、删除、验证、角色绑定等功能。
*/
@Slf4j
@Service
public class ApiKeyService {
@Autowired
private ApiKeyMapper apiKeyMapper;
@Autowired
private org.springframework.jdbc.core.JdbcTemplate jdbcTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
/** API Key 列表排序字段白名单(前端 colKey -> 数据库列名),防 SQL 注入 */
private static final Map<String, String> API_KEY_SORT_COLUMNS = Map.of(
"name", "name",
"rateLimit", "rate_limit",
"enabled", "enabled",
"createTime", "create_time");
/**
* 生成新的 API Key
*
* @param userId 所属用户ID
* @param name Key 名称
* @param description 描述
* @param rateLimit 每分钟频率限制(null 则默认 60)
* @param maxCalls 最大调用次数(null 表示不限制)
* @param expireTime 过期时间(null 表示永不过期)
* @return 新创建的 ApiKey 实体
*/
public ApiKey generateKey(Long userId, String name, String description,
Integer rateLimit, Long maxCalls, Date expireTime) {
String keyValue = "sk_" + UUID.randomUUID().toString().replace("-", "");
ApiKey apiKey = ApiKey.builder()
.keyValue(keyValue)
.name(name)
.description(description)
.userId(userId)
.rateLimit(rateLimit != null ? rateLimit : 60)
.maxCalls(maxCalls)
.currentCalls(0L)
.expireTime(expireTime)
.enabled(true)
.build();
apiKeyMapper.insert(apiKey);
log.info("生成 API Key: name={}, id={}", name, apiKey.getId());
return apiKey;
}
/**
* 分页查询 API Key 列表
* 分两步查询:MyBatis Plus 读基础字段 + JdbcTemplate 读 JSONB role_ids
*
* @param page 页码(从1开始)
* @param size 每页条数
* @return 包含 records/total/page/size/pages 的结果
*/
public Map<String, Object> listKeys(int page, int size, String sortField, String sortOrder) {
QueryWrapper<ApiKey> countWrapper = new QueryWrapper<>();
Long total = apiKeyMapper.selectCount(countWrapper);
QueryWrapper<ApiKey> listWrapper = new QueryWrapper<>();
String sortColumn = SortUtils.resolveColumn(sortField, API_KEY_SORT_COLUMNS, "create_time");
if (SortUtils.isAscending(sortOrder)) {
listWrapper.orderByAsc(sortColumn);
} else {
listWrapper.orderByDesc(sortColumn);
}
listWrapper.last("LIMIT " + size + " OFFSET " + (long) (page - 1) * size);
List<ApiKey> records = apiKeyMapper.selectList(listWrapper);
// 补充读取 JSONB role_ids 字段(MyBatis Plus 无法正确读取 JSONB)
if (!records.isEmpty()) {
Map<Long, String> roleIdsMap = new HashMap<>();
String ids = records.stream().map(k -> k.getId().toString()).collect(Collectors.joining(","));
String sql = "SELECT id, COALESCE(role_ids::text, '[]') AS role_ids FROM api_key WHERE id IN (" + ids + ")";
jdbcTemplate.query(sql, (rs) -> {
roleIdsMap.put(rs.getLong("id"), rs.getString("role_ids"));
});
for (ApiKey key : records) {
key.setRoleIds(roleIdsMap.getOrDefault(key.getId(), "[]"));
}
}
Map<String, Object> result = new HashMap<>();
result.put("records", records);
result.put("total", total);
result.put("page", page);
result.put("size", size);
result.put("pages", (total + size - 1) / size);
return result;
}
/**
* 吊销 API Key(设置 enabled=false)
* 使用 JdbcTemplate 避免 MyBatis-Plus 的 role_ids JSONB 类型不匹配
*
* @param id Key ID
*/
public void revokeKey(Long id) {
ApiKey existing = apiKeyMapper.selectById(id);
if (existing == null) {
throw new IllegalArgumentException("API Key 不存在,ID:" + id);
}
jdbcTemplate.update(
"UPDATE api_key SET enabled = false, update_time = CURRENT_TIMESTAMP WHERE id = ? AND is_delete = false",
id);
log.info("吊销 API Key: id={}, name={}", id, existing.getName());
}
/**
* 启用 API Key
* 使用 JdbcTemplate 避免 MyBatis-Plus 的 role_ids JSONB 类型不匹配
*
* @param id Key ID
*/
public void enableKey(Long id) {
ApiKey existing = apiKeyMapper.selectById(id);
if (existing == null) {
throw new IllegalArgumentException("API Key 不存在,ID:" + id);
}
jdbcTemplate.update(
"UPDATE api_key SET enabled = true, update_time = CURRENT_TIMESTAMP WHERE id = ? AND is_delete = false",
id);
log.info("启用 API Key: id={}, name={}", id, existing.getName());
}
/**
* 逻辑删除 API Key
*
* @param id Key ID
*/
public void deleteKey(Long id) {
ApiKey existing = apiKeyMapper.selectById(id);
if (existing == null) {
throw new IllegalArgumentException("API Key 不存在,ID:" + id);
}
apiKeyMapper.deleteById(id);
log.info("删除 API Key: id={}, name={}", id, existing.getName());
}
/**
* 验证 API Key 有效性(不自增调用次数,自增由 Filter 处理)
*
* @param keyValue Key 值
* @return 有效的 ApiKey,无效时返回 null
*/
public ApiKey validateKey(String keyValue) {
if (keyValue == null || keyValue.isBlank()) {
return null;
}
return getKeyByKeyValue(keyValue);
}
/**
* 根据 keyValue 查询 API Key
*
* @param keyValue Key 值
* @return ApiKey 实体,不存在返回 null
*/
public ApiKey getKeyByKeyValue(String keyValue) {
QueryWrapper<ApiKey> wrapper = new QueryWrapper<>();
wrapper.eq("key_value", keyValue);
return apiKeyMapper.selectOne(wrapper);
}
/**
* 自增调用次数(SQL 原子操作,避免并发竞态)
*
* @param id Key ID
*/
public void incrementCallCount(Long id) {
try {
jdbcTemplate.update(
"UPDATE api_key SET current_calls = current_calls + 1 WHERE id = ? AND is_delete = false",
id);
} catch (Exception e) {
log.warn("自增 API Key 调用次数失败: id={}", id, e);
}
}
/**
* 更新 API Key 绑定的客服角色列表
* 使用 JdbcTemplate 显式 ::jsonb 转换,避免 MyBatis-Plus 类型不匹配
*
* @param id API Key ID
* @param roleIds 角色 ID 列表(空/null 表示不限制)
*/
public void updateKeyRoles(Long id, List<Long> roleIds) {
ApiKey existing = apiKeyMapper.selectById(id);
if (existing == null) {
throw new IllegalArgumentException("API Key 不存在,ID:" + id);
}
String json;
try {
json = objectMapper.writeValueAsString(roleIds != null ? roleIds : List.of());
} catch (Exception e) {
json = "[]";
}
int rows = jdbcTemplate.update(
"UPDATE api_key SET role_ids = ?::jsonb, update_time = CURRENT_TIMESTAMP WHERE id = ? AND is_delete = false",
json, id);
log.info("API Key 角色绑定已更新: id={}, roleIds={}", id, json);
}
/**
* 通用更新 API Key 的可编辑字段
* 仅更新传入的非 null 字段,expireTime 传 0 表示清除过期时间
* 使用 JdbcTemplate 动态 SQL,避免 MyBatis-Plus 的 role_ids JSONB 类型不匹配
*
* @param id Key ID
* @param name Key 名称(null 则不更新)
* @param description 描述(null 则不更新)
* @param rateLimit 每分钟频率限制(null 则不更新)
* @param maxCalls 最大调用次数(null 则不更新)
* @param expireTime 过期时间(null 则不更新,0 表示清除)
* @param enabled 是否启用(null 则不更新)
*/
public void updateKey(Long id, String name, String description,
Integer rateLimit, Long maxCalls, Date expireTime,
Boolean enabled) {
// 先验证记录存在
ApiKey existing = apiKeyMapper.selectById(id);
if (existing == null) {
throw new IllegalArgumentException("API Key 不存在,ID:" + id);
}
if (rateLimit != null && rateLimit < 1) {
throw new IllegalArgumentException("频率限制必须大于 0");
}
// 使用 JdbcTemplate 动态构建 SQL,避免 role_ids JSONB 类型不匹配
StringBuilder sql = new StringBuilder("UPDATE api_key SET update_time = CURRENT_TIMESTAMP");
List<Object> params = new ArrayList<>();
if (name != null) {
sql.append(", name = ?");
params.add(name);
}
if (description != null) {
sql.append(", description = ?");
params.add(description);
}
if (rateLimit != null) {
sql.append(", rate_limit = ?");
params.add(rateLimit);
}
if (maxCalls != null) {
sql.append(", max_calls = ?");
params.add(maxCalls);
}
if (expireTime != null && expireTime.getTime() == 0) {
sql.append(", expire_time = NULL");
} else if (expireTime != null) {
sql.append(", expire_time = ?");
params.add(expireTime);
}
if (enabled != null) {
sql.append(", enabled = ?");
params.add(enabled);
}
sql.append(" WHERE id = ? AND is_delete = false");
params.add(id);
jdbcTemplate.update(sql.toString(), params.toArray());
log.info("更新 API Key: id={}, name={}", id, name != null ? name : existing.getName());
}
/**
* 解析 API Key 绑定的角色 ID 列表
*
* @param apiKey API Key 实体
* @return 角色 ID 列表;空数组表示不限制
*/
public List<Long> parseRoleIds(ApiKey apiKey) {
if (apiKey == null || apiKey.getRoleIds() == null
|| apiKey.getRoleIds().isBlank() || "[]".equals(apiKey.getRoleIds().trim())) {
return List.of();
}
try {
return objectMapper.readValue(apiKey.getRoleIds(), new TypeReference<List<Long>>() {});
} catch (Exception e) {
log.warn("解析 role_ids 失败: {}", apiKey.getRoleIds(), e);
return List.of();
}
}
}