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 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 listKeys(int page, int size, String sortField, String sortOrder) { QueryWrapper countWrapper = new QueryWrapper<>(); Long total = apiKeyMapper.selectCount(countWrapper); QueryWrapper 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 records = apiKeyMapper.selectList(listWrapper); // 补充读取 JSONB role_ids 字段(MyBatis Plus 无法正确读取 JSONB) if (!records.isEmpty()) { Map 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 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 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 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 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 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>() {}); } catch (Exception e) { log.warn("解析 role_ids 失败: {}", apiKey.getRoleIds(), e); return List.of(); } } }