{"repo": "breeze-mi/hula-server", "files": {"luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/service/model/AiApiKeyServiceImpl.java": {"611199efbbe715dbec71474e9fa2cec56d886b0c": {"t": 1785746117.2204688, "n": 14997, "h": "e75aaece887222d2", "c": "package com.luohuo.flex.ai.service.model;\n\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.HttpRequest;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport com.luohuo.flex.ai.common.pojo.PageResult;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeyBalanceRespVO;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeyPageReqVO;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeySaveReqVO;\nimport com.luohuo.flex.ai.dal.model.AiApiKeyDO;\nimport com.luohuo.flex.ai.enums.AiPlatformEnum;\nimport com.luohuo.flex.ai.enums.CommonStatusEnum;\nimport com.luohuo.flex.ai.mapper.LambdaQueryWrapperX;\nimport com.luohuo.flex.ai.mapper.model.AiApiKeyMapper;\nimport com.luohuo.flex.ai.utils.BeanUtils;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\nimport org.springframework.validation.annotation.Validated;\n\nimport java.math.BigDecimal;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_DISABLE;\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_NOT_EXISTS;\nimport static com.luohuo.flex.ai.utils.ServiceExceptionUtil.exception;\n\n/**\n * AI API 密钥 Service 实现类\n *\n * @author 乾乾\n */\n@Slf4j\n@Service\n@Validated\npublic class AiApiKeyServiceImpl implements AiApiKeyService {\n\n\t@Resource\n\tprivate AiApiKeyMapper apiKeyMapper;\n\n\t@Override\n\tpublic Long createApiKey(AiApiKeySaveReqVO createReqVO, Long userId) {\n\t\t// 插入\n\t\tAiApiKeyDO apiKey = BeanUtils.toBean(createReqVO, AiApiKeyDO.class);\n\n\t\t// 如果是私有密钥,设置 userId\n\t\tif (createReqVO.getPublicStatus() == null || Boolean.FALSE.equals(createReqVO.getPublicStatus())) {\n\t\t\tapiKey.setUserId(userId)\n\t\t\t\t\t.setPublicStatus(false);\n\t\t\t// 私有密钥默认启用\n\t\t\tif (createReqVO.getStatus() == null) {\n\t\t\t\tapiKey.setStatus(CommonStatusEnum.ENABLE.getStatus());\n\t\t\t}\n\t\t}\n\n\t\tapiKeyMapper.insert(apiKey);\n\t\treturn apiKey.getId();\n\t}\n\n\t@Override\n\tpublic void updateApiKey(AiApiKeySaveReqVO updateReqVO, Long userId) {\n\t\t// 校验存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(updateReqVO.getId());\n\n\t\t// 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以编辑\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t} else {\n\t\t\t// 公开密钥,需要管理员权限\n\t\t\tif (userId >= 10937855681025L) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 更新\n\t\tAiApiKeyDO updateObj = BeanUtils.toBean(updateReqVO, AiApiKeyDO.class);\n\t\tapiKeyMapper.updateById(updateObj);\n\t}\n\n\t@Override\n\tpublic void deleteApiKey(Long id, Long userId) {\n\t\t// 校验存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\n\t\t// 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以删除\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t} else {\n\t\t\t// 公开密钥,需要管理员权限\n\t\t\tif (userId >= 10937855681025L) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 删除\n\t\tapiKeyMapper.deleteById(id);\n\t}\n\n\t@Override\n\tpublic void updateApiKeyAdmin(AiApiKeySaveReqVO updateReqVO) {\n\t\tvalidateApiKeyExists(updateReqVO.getId());\n\n\t\t// 管理员更新,无权限校验\n\t\tAiApiKeyDO updateObj = BeanUtils.toBean(updateReqVO, AiApiKeyDO.class);\n\t\tapiKeyMapper.updateById(updateObj);\n\t}\n\n\t@Override\n\tpublic void deleteApiKeyAdmin(Long id) {\n\t\t// 校验存在\n\t\tvalidateApiKeyExists(id);\n\n\t\t// 管理员删除,无权限校验\n\t\tapiKeyMapper.deleteById(id);\n\t}\n\n\tprivate AiApiKeyDO validateApiKeyExists(Long id) {\n\t\tAiApiKeyDO apiKey = apiKeyMapper.selectById(id);\n\t\tif (apiKey == null) {\n\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO getApiKey(Long id) {\n\t\treturn apiKeyMapper.selectById(id);\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO validateApiKey(Long id) {\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\t\tif (CommonStatusEnum.isDisable(apiKey.getStatus())) {\n\t\t\tthrow exception(API_KEY_DISABLE);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic PageResult getApiKeyPage(AiApiKeyPageReqVO pageReqVO, Long userId) {\n\t\treturn apiKeyMapper.selectPageByMy(pageReqVO, userId);\n\t}\n\n\t@Override\n\tpublic List getApiKeyList(Long userId) {\n\t\t// 返回所有公开密钥 + 用户私有密钥\n\t\treturn apiKeyMapper.selectList(new LambdaQueryWrapperX()\n\t\t\t\t.and(w -> w\n\t\t\t\t\t\t.eq(AiApiKeyDO::getPublicStatus, true)\n\t\t\t\t\t\t.or()\n\t\t\t\t\t\t.eq(AiApiKeyDO::getUserId, userId)\n\t\t\t\t)\n\t\t\t\t.orderByDesc(AiApiKeyDO::getId));\n\t}\n\n\t@Override\n\tpublic List getAllApiKeyList() {\n\t\t// 返回所有 API 密钥(后台管理专用)\n\t\treturn apiKeyMapper.selectList(new LambdaQueryWrapperX()\n\t\t\t\t.orderByDesc(AiApiKeyDO::getId));\n\t}\n\n\t@Override\n\tpublic PageResult getAdminApiKeyPage(AiApiKeyPageReqVO pageReqVO) {\n\t\treturn apiKeyMapper.selectPage(pageReqVO);\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO getRequiredDefaultApiKey(String platform, Integer status) {\n\t\tAiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform, status);\n\t\tif (apiKey == null) {\n\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic AiApiKeyBalanceRespVO getApiKeyBalance(Long id, Long userId) {\n\t\t// 1. 校验 API Key 是否存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\n\t\t// 2. 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以查询\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 3. 根据平台查询余额\n\t\tAiPlatformEnum platformEnum;\n\t\ttry {\n\t\t\tplatformEnum = AiPlatformEnum.validatePlatform(apiKey.getPlatform());\n\t\t} catch (IllegalArgumentException e) {\n\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t.id(id)\n\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t.supported(false)\n\t\t\t\t\t.success(false)\n\t\t\t\t\t.errorMessage(\"不支持的平台: \" + apiKey.getPlatform())\n\t\t\t\t\t.build();\n\t\t}\n\n\t\treturn queryBalanceByPlatform(apiKey, platformEnum);\n\t}\n\n\t/**\n\t * 根据平台查询余额\n\t */\n\tprivate AiApiKeyBalanceRespVO queryBalanceByPlatform(AiApiKeyDO apiKey, AiPlatformEnum platform) {\n\t\ttry {\n\t\t\tswitch (platform) {\n\t\t\t\tcase MOONSHOT:\n\t\t\t\t\treturn queryMoonshotBalance(apiKey);\n\t\t\t\tcase DEEP_SEEK:\n\t\t\t\t\treturn queryDeepSeekBalance(apiKey);\n\t\t\t\tcase SILICON_FLOW:\n\t\t\t\t\treturn querySiliconFlowBalance(apiKey);\n\t\t\t\tcase OPENROUTER:\n\t\t\t\t\treturn queryOpenRouterBalance(apiKey);\n\t\t\t\tdefault:\n\t\t\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t\t\t.id(apiKey.getId())\n\t\t\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t\t\t.supported(false)\n\t\t\t\t\t\t\t.success(false)\n\t\t\t\t\t\t\t.errorMessage(\"该平台暂不支持余额查询\")\n\t\t\t\t\t\t\t.build();\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tlog.error(\"查询API Key余额失败, platform={}, error={}\", platform.getPlatform(), e.getMessage(), e);\n\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t.id(apiKey.getId())\n\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t.supported(true)\n\t\t\t\t\t.success(false)\n\t\t\t\t\t.errorMessage(\"查询失败: \" + e.getMessage())\n\t\t\t\t\t.build();\n\t\t}\n\t}\n\n\t/**\n\t * 查询月之暗面(Moonshot/Kimi)余额\n\t * API文档: https://platform.moonshot.cn/docs/api/balance\n\t */\n\tprivate AiApiKeyBalanceRespVO queryMoonshotBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.moonshot.cn\";\n\t\tString url = baseUrl + \"/v1/users/me/balance\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Content-Type\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.containsKey(\"data\")) {\n\t\t\tJSONObject data = jsonResponse.getJSONObject(\"data\");\n\t\t\tif (data.containsKey(\"available_balance\")) {\n\t\t\t\tBigDecimal balance = data.getBigDecimal(\"available_balance\");\n\t\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t\t.currency(\"CNY\")\n\t\t\t\t\t\t.totalBalance(balance)\n\t\t\t\t\t\t.available(true)\n\t\t\t\t\t\t.build());\n\t\t\t\ttotalBalance = balance;\n\t\t\t}\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n\t/**\n\t * 查询DeepSeek余额\n\t * API文档: https://api-docs.deepseek.com/api/get-user-balance\n\t */\n\tprivate AiApiKeyBalanceRespVO queryDeepSeekBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.deepseek.com\";\n\t\tString url = baseUrl + \"/user/balance\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Accept\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.getBool(\"is_available\", false) && jsonResponse.containsKey(\"balance_infos\")) {\n\t\t\tfor (Object item : jsonResponse.getJSONArray(\"balance_infos\")) {\n\t\t\t\tJSONObject balanceInfo = (JSONObject) item;\n\t\t\t\tString currency = balanceInfo.getStr(\"currency\");\n\t\t\t\tBigDecimal total = new BigDecimal(balanceInfo.getStr(\"total_balance\"));\n\t\t\t\tBigDecimal granted = balanceInfo.containsKey(\"granted_balance\")\n\t\t\t\t\t\t? new BigDecimal(balanceInfo.getStr(\"granted_balance\")) : BigDecimal.ZERO;\n\t\t\t\tBigDecimal toppedUp = balanceInfo.containsKey(\"topped_up_balance\")\n\t\t\t\t\t\t? new BigDecimal(balanceInfo.getStr(\"topped_up_balance\")) : BigDecimal.ZERO;\n\n\t\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t\t.currency(currency)\n\t\t\t\t\t\t.totalBalance(total)\n\t\t\t\t\t\t.grantedBalance(granted)\n\t\t\t\t\t\t.toppedUpBalance(toppedUp)\n\t\t\t\t\t\t.available(true)\n\t\t\t\t\t\t.build());\n\n\t\t\t\t// 简单累加(实际应该考虑汇率转换)\n\t\t\t\ttotalBalance = totalBalance.add(total);\n\t\t\t}\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n\t/**\n\t * 查询硅基流动余额\n\t * API文档: https://docs.siliconflow.cn/cn/api-reference/userinfo/get-user-info\n\t */\n\tprivate AiApiKeyBalanceRespVO querySiliconFlowBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.siliconflow.cn\";\n\t\tString url = baseUrl + \"/v1/user/info\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Content-Type\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.getInt(\"code\") == 20000 && jsonResponse.containsKey(\"data\")) {\n\t\t\tJSONObject data = jsonResponse.getJSONObject(\"data\");\n\n\t\t\t// 硅基流动返回的余额字段\n\t\t\tBigDecimal balance = data.getBigDecimal(\"balance\", BigDecimal.ZERO);\n\t\t\tBigDecimal chargeBalance = data.getBigDecimal(\"chargeBalance\", BigDecimal.ZERO);\n\t\t\tBigDecimal total = data.getBigDecimal(\"totalBalance\", BigDecimal.ZERO);\n\n\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t.currency(\"CNY\")\n\t\t\t\t\t.totalBalance(total)\n\t\t\t\t\t.grantedBalance(balance.subtract(chargeBalance).max(BigDecimal.ZERO))\n\t\t\t\t\t.toppedUpBalance(chargeBalance)\n\t\t\t\t\t.available(\"normal\".equals(data.getStr(\"status\")))\n\t\t\t\t\t.build());\n\n\t\t\ttotalBalance = total;\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n\tprivate AiApiKeyBalanceRespVO queryOpenRouterBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://openrouter.ai/api\";\n\t\tString url = baseUrl + \"/v1/key\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Accept\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\t\tJSONObject data = jsonResponse.getJSONObject(\"data\");\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\t\tBoolean freeTier = data == null ? null : data.getBool(\"is_free_tier\");\n\t\tBigDecimal limitRemaining = null;\n\t\tif (data != null && data.get(\"limit_remaining\") != null && !(data.get(\"limit_remaining\") instanceof cn.hutool.json.JSONNull)) {\n\t\t\tlimitRemaining = new BigDecimal(String.valueOf(data.get(\"limit_remaining\")));\n\t\t}\n\t\tif (limitRemaining != null) {\n\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t.currency(\"USD\")\n\t\t\t\t\t.totalBalance(limitRemaining)\n\t\t\t\t\t.available(true)\n\t\t\t\t\t.build());\n\t\t\ttotalBalance = limitRemaining;\n\t\t} else {\n\t\t\tBigDecimal limit = null;\n\t\t\tBigDecimal usage = null;\n\t\t\tif (data != null && data.get(\"limit\") != null && !(data.get(\"limit\") instanceof cn.hutool.json.JSONNull)) {\n\t\t\t\tlimit = new BigDecimal(String.valueOf(data.get(\"limit\")));\n\t\t\t}\n if (data != null && data.get(\"usage\") != null && !(data.get(\"usage\") instanceof cn.hutool.json.JSONNull)) {\n usage = new BigDecimal(String.valueOf(data.get(\"usage\")));\n }\n\t\t\tBigDecimal remaining = null;\n\t\t\tif (limit != null) {\n\t\t\t\tremaining = usage != null ? limit.subtract(usage) : limit;\n\t\t\t}\n if (remaining != null) {\n balanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n .currency(\"USD\")\n .totalBalance(remaining)\n .usageTotal(usage)\n .available(true)\n .build());\n totalBalance = remaining;\n } else {\n balanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n .currency(\"USD\")\n .totalBalance(BigDecimal.ZERO)\n .grantedBalance(BigDecimal.ZERO)\n .toppedUpBalance(BigDecimal.ZERO)\n .usageTotal(BigDecimal.ZERO)\n .available(Boolean.TRUE.equals(freeTier))\n .build());\n totalBalance = BigDecimal.ZERO;\n }\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n}"}, "d152fea9fcf79e2a0323363f8a21dd8b8780508f": {"t": 1785759987.4463444, "n": 14997, "h": "e75aaece887222d2", "c": "package com.luohuo.flex.ai.service.model;\n\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.HttpRequest;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport com.luohuo.flex.ai.common.pojo.PageResult;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeyBalanceRespVO;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeyPageReqVO;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeySaveReqVO;\nimport com.luohuo.flex.ai.dal.model.AiApiKeyDO;\nimport com.luohuo.flex.ai.enums.AiPlatformEnum;\nimport com.luohuo.flex.ai.enums.CommonStatusEnum;\nimport com.luohuo.flex.ai.mapper.LambdaQueryWrapperX;\nimport com.luohuo.flex.ai.mapper.model.AiApiKeyMapper;\nimport com.luohuo.flex.ai.utils.BeanUtils;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\nimport org.springframework.validation.annotation.Validated;\n\nimport java.math.BigDecimal;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_DISABLE;\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_NOT_EXISTS;\nimport static com.luohuo.flex.ai.utils.ServiceExceptionUtil.exception;\n\n/**\n * AI API 密钥 Service 实现类\n *\n * @author 乾乾\n */\n@Slf4j\n@Service\n@Validated\npublic class AiApiKeyServiceImpl implements AiApiKeyService {\n\n\t@Resource\n\tprivate AiApiKeyMapper apiKeyMapper;\n\n\t@Override\n\tpublic Long createApiKey(AiApiKeySaveReqVO createReqVO, Long userId) {\n\t\t// 插入\n\t\tAiApiKeyDO apiKey = BeanUtils.toBean(createReqVO, AiApiKeyDO.class);\n\n\t\t// 如果是私有密钥,设置 userId\n\t\tif (createReqVO.getPublicStatus() == null || Boolean.FALSE.equals(createReqVO.getPublicStatus())) {\n\t\t\tapiKey.setUserId(userId)\n\t\t\t\t\t.setPublicStatus(false);\n\t\t\t// 私有密钥默认启用\n\t\t\tif (createReqVO.getStatus() == null) {\n\t\t\t\tapiKey.setStatus(CommonStatusEnum.ENABLE.getStatus());\n\t\t\t}\n\t\t}\n\n\t\tapiKeyMapper.insert(apiKey);\n\t\treturn apiKey.getId();\n\t}\n\n\t@Override\n\tpublic void updateApiKey(AiApiKeySaveReqVO updateReqVO, Long userId) {\n\t\t// 校验存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(updateReqVO.getId());\n\n\t\t// 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以编辑\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t} else {\n\t\t\t// 公开密钥,需要管理员权限\n\t\t\tif (userId >= 10937855681025L) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 更新\n\t\tAiApiKeyDO updateObj = BeanUtils.toBean(updateReqVO, AiApiKeyDO.class);\n\t\tapiKeyMapper.updateById(updateObj);\n\t}\n\n\t@Override\n\tpublic void deleteApiKey(Long id, Long userId) {\n\t\t// 校验存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\n\t\t// 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以删除\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t} else {\n\t\t\t// 公开密钥,需要管理员权限\n\t\t\tif (userId >= 10937855681025L) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 删除\n\t\tapiKeyMapper.deleteById(id);\n\t}\n\n\t@Override\n\tpublic void updateApiKeyAdmin(AiApiKeySaveReqVO updateReqVO) {\n\t\tvalidateApiKeyExists(updateReqVO.getId());\n\n\t\t// 管理员更新,无权限校验\n\t\tAiApiKeyDO updateObj = BeanUtils.toBean(updateReqVO, AiApiKeyDO.class);\n\t\tapiKeyMapper.updateById(updateObj);\n\t}\n\n\t@Override\n\tpublic void deleteApiKeyAdmin(Long id) {\n\t\t// 校验存在\n\t\tvalidateApiKeyExists(id);\n\n\t\t// 管理员删除,无权限校验\n\t\tapiKeyMapper.deleteById(id);\n\t}\n\n\tprivate AiApiKeyDO validateApiKeyExists(Long id) {\n\t\tAiApiKeyDO apiKey = apiKeyMapper.selectById(id);\n\t\tif (apiKey == null) {\n\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO getApiKey(Long id) {\n\t\treturn apiKeyMapper.selectById(id);\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO validateApiKey(Long id) {\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\t\tif (CommonStatusEnum.isDisable(apiKey.getStatus())) {\n\t\t\tthrow exception(API_KEY_DISABLE);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic PageResult getApiKeyPage(AiApiKeyPageReqVO pageReqVO, Long userId) {\n\t\treturn apiKeyMapper.selectPageByMy(pageReqVO, userId);\n\t}\n\n\t@Override\n\tpublic List getApiKeyList(Long userId) {\n\t\t// 返回所有公开密钥 + 用户私有密钥\n\t\treturn apiKeyMapper.selectList(new LambdaQueryWrapperX()\n\t\t\t\t.and(w -> w\n\t\t\t\t\t\t.eq(AiApiKeyDO::getPublicStatus, true)\n\t\t\t\t\t\t.or()\n\t\t\t\t\t\t.eq(AiApiKeyDO::getUserId, userId)\n\t\t\t\t)\n\t\t\t\t.orderByDesc(AiApiKeyDO::getId));\n\t}\n\n\t@Override\n\tpublic List getAllApiKeyList() {\n\t\t// 返回所有 API 密钥(后台管理专用)\n\t\treturn apiKeyMapper.selectList(new LambdaQueryWrapperX()\n\t\t\t\t.orderByDesc(AiApiKeyDO::getId));\n\t}\n\n\t@Override\n\tpublic PageResult getAdminApiKeyPage(AiApiKeyPageReqVO pageReqVO) {\n\t\treturn apiKeyMapper.selectPage(pageReqVO);\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO getRequiredDefaultApiKey(String platform, Integer status) {\n\t\tAiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform, status);\n\t\tif (apiKey == null) {\n\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic AiApiKeyBalanceRespVO getApiKeyBalance(Long id, Long userId) {\n\t\t// 1. 校验 API Key 是否存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\n\t\t// 2. 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以查询\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 3. 根据平台查询余额\n\t\tAiPlatformEnum platformEnum;\n\t\ttry {\n\t\t\tplatformEnum = AiPlatformEnum.validatePlatform(apiKey.getPlatform());\n\t\t} catch (IllegalArgumentException e) {\n\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t.id(id)\n\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t.supported(false)\n\t\t\t\t\t.success(false)\n\t\t\t\t\t.errorMessage(\"不支持的平台: \" + apiKey.getPlatform())\n\t\t\t\t\t.build();\n\t\t}\n\n\t\treturn queryBalanceByPlatform(apiKey, platformEnum);\n\t}\n\n\t/**\n\t * 根据平台查询余额\n\t */\n\tprivate AiApiKeyBalanceRespVO queryBalanceByPlatform(AiApiKeyDO apiKey, AiPlatformEnum platform) {\n\t\ttry {\n\t\t\tswitch (platform) {\n\t\t\t\tcase MOONSHOT:\n\t\t\t\t\treturn queryMoonshotBalance(apiKey);\n\t\t\t\tcase DEEP_SEEK:\n\t\t\t\t\treturn queryDeepSeekBalance(apiKey);\n\t\t\t\tcase SILICON_FLOW:\n\t\t\t\t\treturn querySiliconFlowBalance(apiKey);\n\t\t\t\tcase OPENROUTER:\n\t\t\t\t\treturn queryOpenRouterBalance(apiKey);\n\t\t\t\tdefault:\n\t\t\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t\t\t.id(apiKey.getId())\n\t\t\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t\t\t.supported(false)\n\t\t\t\t\t\t\t.success(false)\n\t\t\t\t\t\t\t.errorMessage(\"该平台暂不支持余额查询\")\n\t\t\t\t\t\t\t.build();\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tlog.error(\"查询API Key余额失败, platform={}, error={}\", platform.getPlatform(), e.getMessage(), e);\n\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t.id(apiKey.getId())\n\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t.supported(true)\n\t\t\t\t\t.success(false)\n\t\t\t\t\t.errorMessage(\"查询失败: \" + e.getMessage())\n\t\t\t\t\t.build();\n\t\t}\n\t}\n\n\t/**\n\t * 查询月之暗面(Moonshot/Kimi)余额\n\t * API文档: https://platform.moonshot.cn/docs/api/balance\n\t */\n\tprivate AiApiKeyBalanceRespVO queryMoonshotBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.moonshot.cn\";\n\t\tString url = baseUrl + \"/v1/users/me/balance\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Content-Type\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.containsKey(\"data\")) {\n\t\t\tJSONObject data = jsonResponse.getJSONObject(\"data\");\n\t\t\tif (data.containsKey(\"available_balance\")) {\n\t\t\t\tBigDecimal balance = data.getBigDecimal(\"available_balance\");\n\t\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t\t.currency(\"CNY\")\n\t\t\t\t\t\t.totalBalance(balance)\n\t\t\t\t\t\t.available(true)\n\t\t\t\t\t\t.build());\n\t\t\t\ttotalBalance = balance;\n\t\t\t}\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n\t/**\n\t * 查询DeepSeek余额\n\t * API文档: https://api-docs.deepseek.com/api/get-user-balance\n\t */\n\tprivate AiApiKeyBalanceRespVO queryDeepSeekBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.deepseek.com\";\n\t\tString url = baseUrl + \"/user/balance\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Accept\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.getBool(\"is_available\", false) && jsonResponse.containsKey(\"balance_infos\")) {\n\t\t\tfor (Object item : jsonResponse.getJSONArray(\"balance_infos\")) {\n\t\t\t\tJSONObject balanceInfo = (JSONObject) item;\n\t\t\t\tString currency = balanceInfo.getStr(\"currency\");\n\t\t\t\tBigDecimal total = new BigDecimal(balanceInfo.getStr(\"total_balance\"));\n\t\t\t\tBigDecimal granted = balanceInfo.containsKey(\"granted_balance\")\n\t\t\t\t\t\t? new BigDecimal(balanceInfo.getStr(\"granted_balance\")) : BigDecimal.ZERO;\n\t\t\t\tBigDecimal toppedUp = balanceInfo.containsKey(\"topped_up_balance\")\n\t\t\t\t\t\t? new BigDecimal(balanceInfo.getStr(\"topped_up_balance\")) : BigDecimal.ZERO;\n\n\t\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t\t.currency(currency)\n\t\t\t\t\t\t.totalBalance(total)\n\t\t\t\t\t\t.grantedBalance(granted)\n\t\t\t\t\t\t.toppedUpBalance(toppedUp)\n\t\t\t\t\t\t.available(true)\n\t\t\t\t\t\t.build());\n\n\t\t\t\t// 简单累加(实际应该考虑汇率转换)\n\t\t\t\ttotalBalance = totalBalance.add(total);\n\t\t\t}\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n\t/**\n\t * 查询硅基流动余额\n\t * API文档: https://docs.siliconflow.cn/cn/api-reference/userinfo/get-user-info\n\t */\n\tprivate AiApiKeyBalanceRespVO querySiliconFlowBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.siliconflow.cn\";\n\t\tString url = baseUrl + \"/v1/user/info\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Content-Type\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.getInt(\"code\") == 20000 && jsonResponse.containsKey(\"data\")) {\n\t\t\tJSONObject data = jsonResponse.getJSONObject(\"data\");\n\n\t\t\t// 硅基流动返回的余额字段\n\t\t\tBigDecimal balance = data.getBigDecimal(\"balance\", BigDecimal.ZERO);\n\t\t\tBigDecimal chargeBalance = data.getBigDecimal(\"chargeBalance\", BigDecimal.ZERO);\n\t\t\tBigDecimal total = data.getBigDecimal(\"totalBalance\", BigDecimal.ZERO);\n\n\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t.currency(\"CNY\")\n\t\t\t\t\t.totalBalance(total)\n\t\t\t\t\t.grantedBalance(balance.subtract(chargeBalance).max(BigDecimal.ZERO))\n\t\t\t\t\t.toppedUpBalance(chargeBalance)\n\t\t\t\t\t.available(\"normal\".equals(data.getStr(\"status\")))\n\t\t\t\t\t.build());\n\n\t\t\ttotalBalance = total;\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n\tprivate AiApiKeyBalanceRespVO queryOpenRouterBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://openrouter.ai/api\";\n\t\tString url = baseUrl + \"/v1/key\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Accept\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\t\tJSONObject data = jsonResponse.getJSONObject(\"data\");\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\t\tBoolean freeTier = data == null ? null : data.getBool(\"is_free_tier\");\n\t\tBigDecimal limitRemaining = null;\n\t\tif (data != null && data.get(\"limit_remaining\") != null && !(data.get(\"limit_remaining\") instanceof cn.hutool.json.JSONNull)) {\n\t\t\tlimitRemaining = new BigDecimal(String.valueOf(data.get(\"limit_remaining\")));\n\t\t}\n\t\tif (limitRemaining != null) {\n\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t.currency(\"USD\")\n\t\t\t\t\t.totalBalance(limitRemaining)\n\t\t\t\t\t.available(true)\n\t\t\t\t\t.build());\n\t\t\ttotalBalance = limitRemaining;\n\t\t} else {\n\t\t\tBigDecimal limit = null;\n\t\t\tBigDecimal usage = null;\n\t\t\tif (data != null && data.get(\"limit\") != null && !(data.get(\"limit\") instanceof cn.hutool.json.JSONNull)) {\n\t\t\t\tlimit = new BigDecimal(String.valueOf(data.get(\"limit\")));\n\t\t\t}\n if (data != null && data.get(\"usage\") != null && !(data.get(\"usage\") instanceof cn.hutool.json.JSONNull)) {\n usage = new BigDecimal(String.valueOf(data.get(\"usage\")));\n }\n\t\t\tBigDecimal remaining = null;\n\t\t\tif (limit != null) {\n\t\t\t\tremaining = usage != null ? limit.subtract(usage) : limit;\n\t\t\t}\n if (remaining != null) {\n balanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n .currency(\"USD\")\n .totalBalance(remaining)\n .usageTotal(usage)\n .available(true)\n .build());\n totalBalance = remaining;\n } else {\n balanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n .currency(\"USD\")\n .totalBalance(BigDecimal.ZERO)\n .grantedBalance(BigDecimal.ZERO)\n .toppedUpBalance(BigDecimal.ZERO)\n .usageTotal(BigDecimal.ZERO)\n .available(Boolean.TRUE.equals(freeTier))\n .build());\n totalBalance = BigDecimal.ZERO;\n }\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n}"}, "7f3979e38f3e8165dbe51ea20f8d7118e1602e68": {"t": 1785759988.8829796, "n": 14616, "h": "adeda936fe5a1996", "c": "package com.luohuo.flex.ai.service.model;\n\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.HttpRequest;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport com.luohuo.flex.ai.common.pojo.PageResult;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeyBalanceRespVO;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeyPageReqVO;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeySaveReqVO;\nimport com.luohuo.flex.ai.dal.model.AiApiKeyDO;\nimport com.luohuo.flex.ai.enums.AiPlatformEnum;\nimport com.luohuo.flex.ai.enums.CommonStatusEnum;\nimport com.luohuo.flex.ai.mapper.LambdaQueryWrapperX;\nimport com.luohuo.flex.ai.mapper.model.AiApiKeyMapper;\nimport com.luohuo.flex.ai.utils.BeanUtils;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\nimport org.springframework.validation.annotation.Validated;\n\nimport java.math.BigDecimal;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_DISABLE;\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_NOT_EXISTS;\nimport static com.luohuo.flex.ai.utils.ServiceExceptionUtil.exception;\n\n/**\n * AI API 密钥 Service 实现类\n *\n * @author 乾乾\n */\n@Slf4j\n@Service\n@Validated\npublic class AiApiKeyServiceImpl implements AiApiKeyService {\n\n\t@Resource\n\tprivate AiApiKeyMapper apiKeyMapper;\n\n\t@Override\n\tpublic Long createApiKey(AiApiKeySaveReqVO createReqVO, Long userId) {\n\t\t// 插入\n\t\tAiApiKeyDO apiKey = BeanUtils.toBean(createReqVO, AiApiKeyDO.class);\n\n\t\t// 如果是私有密钥,设置 userId\n\t\tif (createReqVO.getPublicStatus() == null || Boolean.FALSE.equals(createReqVO.getPublicStatus())) {\n\t\t\tapiKey.setUserId(userId)\n\t\t\t\t\t.setPublicStatus(false);\n\t\t\t// 私有密钥默认启用\n\t\t\tif (createReqVO.getStatus() == null) {\n\t\t\t\tapiKey.setStatus(CommonStatusEnum.ENABLE.getStatus());\n\t\t\t}\n\t\t}\n\n\t\tapiKeyMapper.insert(apiKey);\n\t\treturn apiKey.getId();\n\t}\n\n\t@Override\n\tpublic void updateApiKey(AiApiKeySaveReqVO updateReqVO, Long userId) {\n\t\t// 校验存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(updateReqVO.getId());\n\n\t\t// 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以编辑\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t} else {\n\t\t\t// 公开密钥,需要管理员权限\n\t\t\tif (userId >= 10937855681025L) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 更新\n\t\tAiApiKeyDO updateObj = BeanUtils.toBean(updateReqVO, AiApiKeyDO.class);\n\t\tapiKeyMapper.updateById(updateObj);\n\t}\n\n\t@Override\n\tpublic void deleteApiKey(Long id, Long userId) {\n\t\t// 校验存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\n\t\t// 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以删除\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t} else {\n\t\t\t// 公开密钥,需要管理员权限\n\t\t\tif (userId >= 10937855681025L) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 删除\n\t\tapiKeyMapper.deleteById(id);\n\t}\n\n\t@Override\n\tpublic void updateApiKeyAdmin(AiApiKeySaveReqVO updateReqVO) {\n\t\tvalidateApiKeyExists(updateReqVO.getId());\n\n\t\t// 管理员更新,无权限校验\n\t\tAiApiKeyDO updateObj = BeanUtils.toBean(updateReqVO, AiApiKeyDO.class);\n\t\tapiKeyMapper.updateById(updateObj);\n\t}\n\n\t@Override\n\tpublic void deleteApiKeyAdmin(Long id) {\n\t\t// 校验存在\n\t\tvalidateApiKeyExists(id);\n\n\t\t// 管理员删除,无权限校验\n\t\tapiKeyMapper.deleteById(id);\n\t}\n\n\tprivate AiApiKeyDO validateApiKeyExists(Long id) {\n\t\tAiApiKeyDO apiKey = apiKeyMapper.selectById(id);\n\t\tif (apiKey == null) {\n\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO getApiKey(Long id) {\n\t\treturn apiKeyMapper.selectById(id);\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO validateApiKey(Long id) {\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\t\tif (CommonStatusEnum.isDisable(apiKey.getStatus())) {\n\t\t\tthrow exception(API_KEY_DISABLE);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic PageResult getApiKeyPage(AiApiKeyPageReqVO pageReqVO, Long userId) {\n\t\treturn apiKeyMapper.selectPageByMy(pageReqVO, userId);\n\t}\n\n\t@Override\n\tpublic List getApiKeyList(Long userId) {\n\t\t// 返回所有公开密钥 + 用户私有密钥\n\t\treturn apiKeyMapper.selectList(new LambdaQueryWrapperX()\n\t\t\t\t.and(w -> w\n\t\t\t\t\t\t.eq(AiApiKeyDO::getPublicStatus, true)\n\t\t\t\t\t\t.or()\n\t\t\t\t\t\t.eq(AiApiKeyDO::getUserId, userId)\n\t\t\t\t)\n\t\t\t\t.orderByDesc(AiApiKeyDO::getId));\n\t}\n\n\t@Override\n\tpublic List getAllApiKeyList() {\n\t\t// 返回所有 API 密钥(后台管理专用)\n\t\treturn apiKeyMapper.selectList(new LambdaQueryWrapperX()\n\t\t\t\t.orderByDesc(AiApiKeyDO::getId));\n\t}\n\n\t@Override\n\tpublic PageResult getAdminApiKeyPage(AiApiKeyPageReqVO pageReqVO) {\n\t\treturn apiKeyMapper.selectPage(pageReqVO);\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO getRequiredDefaultApiKey(String platform, Integer status) {\n\t\tAiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform, status);\n\t\tif (apiKey == null) {\n\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic AiApiKeyBalanceRespVO getApiKeyBalance(Long id, Long userId) {\n\t\t// 1. 校验 API Key 是否存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\n\t\t// 2. 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以查询\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 3. 根据平台查询余额\n\t\tAiPlatformEnum platformEnum;\n\t\ttry {\n\t\t\tplatformEnum = AiPlatformEnum.validatePlatform(apiKey.getPlatform());\n\t\t} catch (IllegalArgumentException e) {\n\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t.id(id)\n\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t.supported(false)\n\t\t\t\t\t.success(false)\n\t\t\t\t\t.errorMessage(\"不支持的平台: \" + apiKey.getPlatform())\n\t\t\t\t\t.build();\n\t\t}\n\n\t\treturn queryBalanceByPlatform(apiKey, platformEnum);\n\t}\n\n\t/**\n\t * 根据平台查询余额\n\t */\n\tprivate AiApiKeyBalanceRespVO queryBalanceByPlatform(AiApiKeyDO apiKey, AiPlatformEnum platform) {\n\t\ttry {\n\t\t\tswitch (platform) {\n\t\t\t\tcase MOONSHOT:\n\t\t\t\t\treturn queryMoonshotBalance(apiKey);\n\t\t\t\tcase DEEP_SEEK:\n\t\t\t\t\treturn queryDeepSeekBalance(apiKey);\n\t\t\t\tcase SILICON_FLOW:\n\t\t\t\t\treturn querySiliconFlowBalance(apiKey);\n\t\t\t\tcase OPENROUTER:\n\t\t\t\t\treturn queryOpenRouterBalance(apiKey);\n\t\t\t\tdefault:\n\t\t\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t\t\t.id(apiKey.getId())\n\t\t\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t\t\t.supported(false)\n\t\t\t\t\t\t\t.success(false)\n\t\t\t\t\t\t\t.errorMessage(\"该平台暂不支持余额查询\")\n\t\t\t\t\t\t\t.build();\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tlog.error(\"查询API Key余额失败, platform={}, error={}\", platform.getPlatform(), e.getMessage(), e);\n\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t.id(apiKey.getId())\n\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t.supported(true)\n\t\t\t\t\t.success(false)\n\t\t\t\t\t.errorMessage(\"查询失败: \" + e.getMessage())\n\t\t\t\t\t.build();\n\t\t}\n\t}\n\n\t/**\n\t * 查询月之暗面(Moonshot/Kimi)余额\n\t * API文档: https://platform.moonshot.cn/docs/api/balance\n\t */\n\tprivate AiApiKeyBalanceRespVO queryMoonshotBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.moonshot.cn\";\n\t\tString url = baseUrl + \"/v1/users/me/balance\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Content-Type\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.containsKey(\"data\")) {\n\t\t\tJSONObject data = jsonResponse.getJSONObject(\"data\");\n\t\t\tif (data.containsKey(\"available_balance\")) {\n\t\t\t\tBigDecimal balance = data.getBigDecimal(\"available_balance\");\n\t\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t\t.currency(\"CNY\")\n\t\t\t\t\t\t.totalBalance(balance)\n\t\t\t\t\t\t.available(true)\n\t\t\t\t\t\t.build());\n\t\t\t\ttotalBalance = balance;\n\t\t\t}\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n\t/**\n\t * 查询DeepSeek余额\n\t * API文档: https://api-docs.deepseek.com/api/get-user-balance\n\t */\n\tprivate AiApiKeyBalanceRespVO queryDeepSeekBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.deepseek.com\";\n\t\tString url = baseUrl + \"/user/balance\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Accept\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.getBool(\"is_available\", false) && jsonResponse.containsKey(\"balance_infos\")) {\n\t\t\tfor (Object item : jsonResponse.getJSONArray(\"balance_infos\")) {\n\t\t\t\tJSONObject balanceInfo = (JSONObject) item;\n\t\t\t\tString currency = balanceInfo.getStr(\"currency\");\n\t\t\t\tBigDecimal total = new BigDecimal(balanceInfo.getStr(\"total_balance\"));\n\t\t\t\tBigDecimal granted = balanceInfo.containsKey(\"granted_balance\")\n\t\t\t\t\t\t? new BigDecimal(balanceInfo.getStr(\"granted_balance\")) : BigDecimal.ZERO;\n\t\t\t\tBigDecimal toppedUp = balanceInfo.containsKey(\"topped_up_balance\")\n\t\t\t\t\t\t? new BigDecimal(balanceInfo.getStr(\"topped_up_balance\")) : BigDecimal.ZERO;\n\n\t\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t\t.currency(currency)\n\t\t\t\t\t\t.totalBalance(total)\n\t\t\t\t\t\t.grantedBalance(granted)\n\t\t\t\t\t\t.toppedUpBalance(toppedUp)\n\t\t\t\t\t\t.available(true)\n\t\t\t\t\t\t.build());\n\n\t\t\t\t// 简单累加(实际应该考虑汇率转换)\n\t\t\t\ttotalBalance = totalBalance.add(total);\n\t\t\t}\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n\t/**\n\t * 查询硅基流动余额\n\t * API文档: https://docs.siliconflow.cn/cn/api-reference/userinfo/get-user-info\n\t */\n\tprivate AiApiKeyBalanceRespVO querySiliconFlowBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.siliconflow.cn\";\n\t\tString url = baseUrl + \"/v1/user/info\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Content-Type\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.getInt(\"code\") == 20000 && jsonResponse.containsKey(\"data\")) {\n\t\t\tJSONObject data = jsonResponse.getJSONObject(\"data\");\n\n\t\t\t// 硅基流动返回的余额字段\n\t\t\tBigDecimal balance = data.getBigDecimal(\"balance\", BigDecimal.ZERO);\n\t\t\tBigDecimal chargeBalance = data.getBigDecimal(\"chargeBalance\", BigDecimal.ZERO);\n\t\t\tBigDecimal total = data.getBigDecimal(\"totalBalance\", BigDecimal.ZERO);\n\n\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t.currency(\"CNY\")\n\t\t\t\t\t.totalBalance(total)\n\t\t\t\t\t.grantedBalance(balance.subtract(chargeBalance).max(BigDecimal.ZERO))\n\t\t\t\t\t.toppedUpBalance(chargeBalance)\n\t\t\t\t\t.available(\"normal\".equals(data.getStr(\"status\")))\n\t\t\t\t\t.build());\n\n\t\t\ttotalBalance = total;\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n\tprivate AiApiKeyBalanceRespVO queryOpenRouterBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://openrouter.ai/api\";\n\t\tString url = baseUrl + \"/v1/key\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Accept\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\t\tJSONObject data = jsonResponse.getJSONObject(\"data\");\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\t\tBoolean freeTier = data == null ? null : data.getBool(\"is_free_tier\");\n\t\tBigDecimal limitRemaining = null;\n\t\tif (data != null && data.get(\"limit_remaining\") != null && !(data.get(\"limit_remaining\") instanceof cn.hutool.json.JSONNull)) {\n\t\t\tlimitRemaining = new BigDecimal(String.valueOf(data.get(\"limit_remaining\")));\n\t\t}\n\t\tif (limitRemaining != null) {\n\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t.currency(\"USD\")\n\t\t\t\t\t.totalBalance(limitRemaining)\n\t\t\t\t\t.available(true)\n\t\t\t\t\t.build());\n\t\t\ttotalBalance = limitRemaining;\n\t\t} else {\n\t\t\tBigDecimal limit = null;\n\t\t\tBigDecimal usage = null;\n\t\t\tif (data != null && data.get(\"limit\") != null && !(data.get(\"limit\") instanceof cn.hutool.json.JSONNull)) {\n\t\t\t\tlimit = new BigDecimal(String.valueOf(data.get(\"limit\")));\n\t\t\t}\n\t\t\tif (data != null && data.get(\"usage\") != null && !(data.get(\"usage\") instanceof cn.hutool.json.JSONNull)) {\n\t\t\t\tusage = new BigDecimal(String.valueOf(data.get(\"usage\")));\n\t\t\t}\n\t\t\tBigDecimal remaining = null;\n\t\t\tif (limit != null) {\n\t\t\t\tremaining = usage != null ? limit.subtract(usage) : limit;\n\t\t\t}\n\t\t\tif (remaining != null) {\n\t\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t\t.currency(\"USD\")\n\t\t\t\t\t\t.totalBalance(remaining)\n\t\t\t\t\t\t.available(true)\n\t\t\t\t\t\t.build());\n\t\t\t\ttotalBalance = remaining;\n\t\t\t} else {\n\t\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t\t.currency(\"USD\")\n\t\t\t\t\t\t.totalBalance(BigDecimal.ZERO)\n\t\t\t\t\t\t.grantedBalance(BigDecimal.ZERO)\n\t\t\t\t\t\t.toppedUpBalance(BigDecimal.ZERO)\n\t\t\t\t\t\t.available(Boolean.TRUE.equals(freeTier))\n\t\t\t\t\t\t.build());\n\t\t\t\ttotalBalance = BigDecimal.ZERO;\n\t\t\t}\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n}"}, "c9b4ece8c1d02735094faef179491271356e62db": {"t": 1785759990.326636, "n": 11207, "h": "7b168027ba87c8a2", "c": "package com.luohuo.flex.ai.service.model;\n\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.HttpRequest;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport com.luohuo.flex.ai.common.pojo.PageResult;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeyBalanceRespVO;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeyPageReqVO;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeySaveReqVO;\nimport com.luohuo.flex.ai.dal.model.AiApiKeyDO;\nimport com.luohuo.flex.ai.enums.AiPlatformEnum;\nimport com.luohuo.flex.ai.enums.CommonStatusEnum;\nimport com.luohuo.flex.ai.mapper.LambdaQueryWrapperX;\nimport com.luohuo.flex.ai.mapper.model.AiApiKeyMapper;\nimport com.luohuo.flex.ai.utils.BeanUtils;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\nimport org.springframework.validation.annotation.Validated;\n\nimport java.math.BigDecimal;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_DISABLE;\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_NOT_EXISTS;\nimport static com.luohuo.flex.ai.utils.ServiceExceptionUtil.exception;\n\n/**\n * AI API 密钥 Service 实现类\n *\n * @author 芋道源码\n */\n@Slf4j\n@Service\n@Validated\npublic class AiApiKeyServiceImpl implements AiApiKeyService {\n\n\t@Resource\n\tprivate AiApiKeyMapper apiKeyMapper;\n\n\t@Override\n\tpublic Long createApiKey(AiApiKeySaveReqVO createReqVO, Long userId) {\n\t\t// 插入\n\t\tAiApiKeyDO apiKey = BeanUtils.toBean(createReqVO, AiApiKeyDO.class);\n\n\t\t// 如果是私有密钥,设置 userId\n\t\tif (createReqVO.getPublicStatus() == null || Boolean.FALSE.equals(createReqVO.getPublicStatus())) {\n\t\t\tapiKey.setUserId(userId)\n\t\t\t\t\t.setPublicStatus(false);\n\t\t\t// 私有密钥默认启用\n\t\t\tif (createReqVO.getStatus() == null) {\n\t\t\t\tapiKey.setStatus(CommonStatusEnum.ENABLE.getStatus());\n\t\t\t}\n\t\t}\n\n\t\tapiKeyMapper.insert(apiKey);\n\t\treturn apiKey.getId();\n\t}\n\n\t@Override\n\tpublic void updateApiKey(AiApiKeySaveReqVO updateReqVO, Long userId) {\n\t\t// 校验存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(updateReqVO.getId());\n\n\t\t// 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以编辑\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t} else {\n\t\t\t// 公开密钥,需要管理员权限\n\t\t\tif (userId >= 10937855681025L) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 更新\n\t\tAiApiKeyDO updateObj = BeanUtils.toBean(updateReqVO, AiApiKeyDO.class);\n\t\tapiKeyMapper.updateById(updateObj);\n\t}\n\n\t@Override\n\tpublic void deleteApiKey(Long id, Long userId) {\n\t\t// 校验存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\n\t\t// 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以删除\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t} else {\n\t\t\t// 公开密钥,需要管理员权限\n\t\t\tif (userId >= 10937855681025L) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 删除\n\t\tapiKeyMapper.deleteById(id);\n\t}\n\n\tprivate AiApiKeyDO validateApiKeyExists(Long id) {\n\t\tAiApiKeyDO apiKey = apiKeyMapper.selectById(id);\n\t\tif (apiKey == null) {\n\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO getApiKey(Long id) {\n\t\treturn apiKeyMapper.selectById(id);\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO validateApiKey(Long id) {\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\t\tif (CommonStatusEnum.isDisable(apiKey.getStatus())) {\n\t\t\tthrow exception(API_KEY_DISABLE);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic PageResult getApiKeyPage(AiApiKeyPageReqVO pageReqVO, Long userId) {\n\t\treturn apiKeyMapper.selectPageByMy(pageReqVO, userId);\n\t}\n\n\t@Override\n\tpublic List getApiKeyList(Long userId) {\n\t\t// 返回所有公开密钥 + 用户私有密钥\n\t\treturn apiKeyMapper.selectList(new LambdaQueryWrapperX()\n\t\t\t\t.and(w -> w\n\t\t\t\t\t\t.eq(AiApiKeyDO::getPublicStatus, true)\n\t\t\t\t\t\t.or()\n\t\t\t\t\t\t.eq(AiApiKeyDO::getUserId, userId)\n\t\t\t\t)\n\t\t\t\t.orderByDesc(AiApiKeyDO::getId));\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO getRequiredDefaultApiKey(String platform, Integer status) {\n\t\tAiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform, status);\n\t\tif (apiKey == null) {\n\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic AiApiKeyBalanceRespVO getApiKeyBalance(Long id, Long userId) {\n\t\t// 1. 校验 API Key 是否存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\n\t\t// 2. 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以查询\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 3. 根据平台查询余额\n\t\tAiPlatformEnum platformEnum;\n\t\ttry {\n\t\t\tplatformEnum = AiPlatformEnum.validatePlatform(apiKey.getPlatform());\n\t\t} catch (IllegalArgumentException e) {\n\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t.id(id)\n\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t.supported(false)\n\t\t\t\t\t.success(false)\n\t\t\t\t\t.errorMessage(\"不支持的平台: \" + apiKey.getPlatform())\n\t\t\t\t\t.build();\n\t\t}\n\n\t\treturn queryBalanceByPlatform(apiKey, platformEnum);\n\t}\n\n\t/**\n\t * 根据平台查询余额\n\t */\n\tprivate AiApiKeyBalanceRespVO queryBalanceByPlatform(AiApiKeyDO apiKey, AiPlatformEnum platform) {\n\t\ttry {\n\t\t\tswitch (platform) {\n\t\t\t\tcase MOONSHOT:\n\t\t\t\t\treturn queryMoonshotBalance(apiKey);\n\t\t\t\tcase DEEP_SEEK:\n\t\t\t\t\treturn queryDeepSeekBalance(apiKey);\n\t\t\t\tcase SILICON_FLOW:\n\t\t\t\t\treturn querySiliconFlowBalance(apiKey);\n\t\t\t\tdefault:\n\t\t\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t\t\t.id(apiKey.getId())\n\t\t\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t\t\t.supported(false)\n\t\t\t\t\t\t\t.success(false)\n\t\t\t\t\t\t\t.errorMessage(\"该平台暂不支持余额查询\")\n\t\t\t\t\t\t\t.build();\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tlog.error(\"查询API Key余额失败, platform={}, error={}\", platform.getPlatform(), e.getMessage(), e);\n\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t.id(apiKey.getId())\n\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t.supported(true)\n\t\t\t\t\t.success(false)\n\t\t\t\t\t.errorMessage(\"查询失败: \" + e.getMessage())\n\t\t\t\t\t.build();\n\t\t}\n\t}\n\n\t/**\n\t * 查询月之暗面(Moonshot/Kimi)余额\n\t * API文档: https://platform.moonshot.cn/docs/api/balance\n\t */\n\tprivate AiApiKeyBalanceRespVO queryMoonshotBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.moonshot.cn\";\n\t\tString url = baseUrl + \"/v1/user/balance\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Content-Type\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.containsKey(\"data\")) {\n\t\t\tJSONObject data = jsonResponse.getJSONObject(\"data\");\n\t\t\tif (data.containsKey(\"available_balance\")) {\n\t\t\t\tBigDecimal balance = data.getBigDecimal(\"available_balance\");\n\t\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t\t.currency(\"CNY\")\n\t\t\t\t\t\t.totalBalance(balance)\n\t\t\t\t\t\t.available(true)\n\t\t\t\t\t\t.build());\n\t\t\t\ttotalBalance = balance;\n\t\t\t}\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n\t/**\n\t * 查询DeepSeek余额\n\t * API文档: https://api-docs.deepseek.com/api/get-user-balance\n\t */\n\tprivate AiApiKeyBalanceRespVO queryDeepSeekBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.deepseek.com\";\n\t\tString url = baseUrl + \"/user/balance\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Accept\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.getBool(\"is_available\", false) && jsonResponse.containsKey(\"balance_infos\")) {\n\t\t\tfor (Object item : jsonResponse.getJSONArray(\"balance_infos\")) {\n\t\t\t\tJSONObject balanceInfo = (JSONObject) item;\n\t\t\t\tString currency = balanceInfo.getStr(\"currency\");\n\t\t\t\tBigDecimal total = new BigDecimal(balanceInfo.getStr(\"total_balance\"));\n\t\t\t\tBigDecimal granted = balanceInfo.containsKey(\"granted_balance\")\n\t\t\t\t\t\t? new BigDecimal(balanceInfo.getStr(\"granted_balance\")) : BigDecimal.ZERO;\n\t\t\t\tBigDecimal toppedUp = balanceInfo.containsKey(\"topped_up_balance\")\n\t\t\t\t\t\t? new BigDecimal(balanceInfo.getStr(\"topped_up_balance\")) : BigDecimal.ZERO;\n\n\t\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t\t.currency(currency)\n\t\t\t\t\t\t.totalBalance(total)\n\t\t\t\t\t\t.grantedBalance(granted)\n\t\t\t\t\t\t.toppedUpBalance(toppedUp)\n\t\t\t\t\t\t.available(true)\n\t\t\t\t\t\t.build());\n\n\t\t\t\t// 简单累加(实际应该考虑汇率转换)\n\t\t\t\ttotalBalance = totalBalance.add(total);\n\t\t\t}\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n\t/**\n\t * 查询硅基流动余额\n\t * API文档: https://docs.siliconflow.cn/cn/api-reference/userinfo/get-user-info\n\t */\n\tprivate AiApiKeyBalanceRespVO querySiliconFlowBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.siliconflow.cn\";\n\t\tString url = baseUrl + \"/v1/user/info\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Content-Type\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.getInt(\"code\") == 20000 && jsonResponse.containsKey(\"data\")) {\n\t\t\tJSONObject data = jsonResponse.getJSONObject(\"data\");\n\n\t\t\t// 硅基流动返回的余额字段\n\t\t\tBigDecimal balance = data.getBigDecimal(\"balance\", BigDecimal.ZERO);\n\t\t\tBigDecimal chargeBalance = data.getBigDecimal(\"chargeBalance\", BigDecimal.ZERO);\n\t\t\tBigDecimal total = data.getBigDecimal(\"totalBalance\", BigDecimal.ZERO);\n\n\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t.currency(\"CNY\")\n\t\t\t\t\t.totalBalance(total)\n\t\t\t\t\t.grantedBalance(balance.subtract(chargeBalance).max(BigDecimal.ZERO))\n\t\t\t\t\t.toppedUpBalance(chargeBalance)\n\t\t\t\t\t.available(\"normal\".equals(data.getStr(\"status\")))\n\t\t\t\t\t.build());\n\n\t\t\ttotalBalance = total;\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n}"}, "716db7ae4b518c339086dc7660caaccf7d31c1ad": {"t": 1785759992.0186377, "n": 10926, "h": "28540f42bd9e8bd8", "c": "package com.luohuo.flex.ai.service.model;\n\nimport cn.hutool.core.util.ObjectUtil;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.http.HttpRequest;\nimport cn.hutool.http.HttpResponse;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.json.JSONUtil;\nimport com.luohuo.flex.ai.common.pojo.PageResult;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeyBalanceRespVO;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeyPageReqVO;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeySaveReqVO;\nimport com.luohuo.flex.ai.dal.model.AiApiKeyDO;\nimport com.luohuo.flex.ai.enums.AiPlatformEnum;\nimport com.luohuo.flex.ai.enums.CommonStatusEnum;\nimport com.luohuo.flex.ai.mapper.model.AiApiKeyMapper;\nimport com.luohuo.flex.ai.utils.BeanUtils;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\nimport org.springframework.validation.annotation.Validated;\n\nimport java.math.BigDecimal;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_DISABLE;\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_NOT_EXISTS;\nimport static com.luohuo.flex.ai.utils.ServiceExceptionUtil.exception;\n\n/**\n * AI API 密钥 Service 实现类\n *\n * @author 芋道源码\n */\n@Slf4j\n@Service\n@Validated\npublic class AiApiKeyServiceImpl implements AiApiKeyService {\n\n\t@Resource\n\tprivate AiApiKeyMapper apiKeyMapper;\n\n\t@Override\n\tpublic Long createApiKey(AiApiKeySaveReqVO createReqVO, Long userId) {\n\t\t// 插入\n\t\tAiApiKeyDO apiKey = BeanUtils.toBean(createReqVO, AiApiKeyDO.class);\n\n\t\t// 如果是私有密钥,设置 userId\n\t\tif (createReqVO.getPublicStatus() == null || Boolean.FALSE.equals(createReqVO.getPublicStatus())) {\n\t\t\tapiKey.setUserId(userId)\n\t\t\t\t\t.setPublicStatus(false);\n\t\t\t// 私有密钥默认启用\n\t\t\tif (createReqVO.getStatus() == null) {\n\t\t\t\tapiKey.setStatus(CommonStatusEnum.ENABLE.getStatus());\n\t\t\t}\n\t\t}\n\n\t\tapiKeyMapper.insert(apiKey);\n\t\treturn apiKey.getId();\n\t}\n\n\t@Override\n\tpublic void updateApiKey(AiApiKeySaveReqVO updateReqVO, Long userId) {\n\t\t// 校验存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(updateReqVO.getId());\n\n\t\t// 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以编辑\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t} else {\n\t\t\t// 公开密钥,需要管理员权限\n\t\t\tif (userId >= 10937855681025L) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 更新\n\t\tAiApiKeyDO updateObj = BeanUtils.toBean(updateReqVO, AiApiKeyDO.class);\n\t\tapiKeyMapper.updateById(updateObj);\n\t}\n\n\t@Override\n\tpublic void deleteApiKey(Long id, Long userId) {\n\t\t// 校验存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\n\t\t// 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以删除\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t} else {\n\t\t\t// 公开密钥,需要管理员权限\n\t\t\tif (userId >= 10937855681025L) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 删除\n\t\tapiKeyMapper.deleteById(id);\n\t}\n\n\tprivate AiApiKeyDO validateApiKeyExists(Long id) {\n\t\tAiApiKeyDO apiKey = apiKeyMapper.selectById(id);\n\t\tif (apiKey == null) {\n\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO getApiKey(Long id) {\n\t\treturn apiKeyMapper.selectById(id);\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO validateApiKey(Long id) {\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\t\tif (CommonStatusEnum.isDisable(apiKey.getStatus())) {\n\t\t\tthrow exception(API_KEY_DISABLE);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic PageResult getApiKeyPage(AiApiKeyPageReqVO pageReqVO, Long userId) {\n\t\treturn apiKeyMapper.selectPageByMy(pageReqVO, userId);\n\t}\n\n\t@Override\n\tpublic List getApiKeyList() {\n\t\treturn apiKeyMapper.selectList();\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO getRequiredDefaultApiKey(String platform, Integer status) {\n\t\tAiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform, status);\n\t\tif (apiKey == null) {\n\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic AiApiKeyBalanceRespVO getApiKeyBalance(Long id, Long userId) {\n\t\t// 1. 校验 API Key 是否存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\n\t\t// 2. 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以查询\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 3. 根据平台查询余额\n\t\tAiPlatformEnum platformEnum;\n\t\ttry {\n\t\t\tplatformEnum = AiPlatformEnum.validatePlatform(apiKey.getPlatform());\n\t\t} catch (IllegalArgumentException e) {\n\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t.id(id)\n\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t.supported(false)\n\t\t\t\t\t.success(false)\n\t\t\t\t\t.errorMessage(\"不支持的平台: \" + apiKey.getPlatform())\n\t\t\t\t\t.build();\n\t\t}\n\n\t\treturn queryBalanceByPlatform(apiKey, platformEnum);\n\t}\n\n\t/**\n\t * 根据平台查询余额\n\t */\n\tprivate AiApiKeyBalanceRespVO queryBalanceByPlatform(AiApiKeyDO apiKey, AiPlatformEnum platform) {\n\t\ttry {\n\t\t\tswitch (platform) {\n\t\t\t\tcase MOONSHOT:\n\t\t\t\t\treturn queryMoonshotBalance(apiKey);\n\t\t\t\tcase DEEP_SEEK:\n\t\t\t\t\treturn queryDeepSeekBalance(apiKey);\n\t\t\t\tcase SILICON_FLOW:\n\t\t\t\t\treturn querySiliconFlowBalance(apiKey);\n\t\t\t\tdefault:\n\t\t\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t\t\t.id(apiKey.getId())\n\t\t\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t\t\t.supported(false)\n\t\t\t\t\t\t\t.success(false)\n\t\t\t\t\t\t\t.errorMessage(\"该平台暂不支持余额查询\")\n\t\t\t\t\t\t\t.build();\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tlog.error(\"查询API Key余额失败, platform={}, error={}\", platform.getPlatform(), e.getMessage(), e);\n\t\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t\t.id(apiKey.getId())\n\t\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t\t.supported(true)\n\t\t\t\t\t.success(false)\n\t\t\t\t\t.errorMessage(\"查询失败: \" + e.getMessage())\n\t\t\t\t\t.build();\n\t\t}\n\t}\n\n\t/**\n\t * 查询月之暗面(Moonshot/Kimi)余额\n\t * API文档: https://platform.moonshot.cn/docs/api/balance\n\t */\n\tprivate AiApiKeyBalanceRespVO queryMoonshotBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.moonshot.cn\";\n\t\tString url = baseUrl + \"/v1/user/balance\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Content-Type\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.containsKey(\"data\")) {\n\t\t\tJSONObject data = jsonResponse.getJSONObject(\"data\");\n\t\t\tif (data.containsKey(\"available_balance\")) {\n\t\t\t\tBigDecimal balance = data.getBigDecimal(\"available_balance\");\n\t\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t\t.currency(\"CNY\")\n\t\t\t\t\t\t.totalBalance(balance)\n\t\t\t\t\t\t.available(true)\n\t\t\t\t\t\t.build());\n\t\t\t\ttotalBalance = balance;\n\t\t\t}\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n\t/**\n\t * 查询DeepSeek余额\n\t * API文档: https://api-docs.deepseek.com/api/get-user-balance\n\t */\n\tprivate AiApiKeyBalanceRespVO queryDeepSeekBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.deepseek.com\";\n\t\tString url = baseUrl + \"/user/balance\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Accept\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.getBool(\"is_available\", false) && jsonResponse.containsKey(\"balance_infos\")) {\n\t\t\tfor (Object item : jsonResponse.getJSONArray(\"balance_infos\")) {\n\t\t\t\tJSONObject balanceInfo = (JSONObject) item;\n\t\t\t\tString currency = balanceInfo.getStr(\"currency\");\n\t\t\t\tBigDecimal total = new BigDecimal(balanceInfo.getStr(\"total_balance\"));\n\t\t\t\tBigDecimal granted = balanceInfo.containsKey(\"granted_balance\")\n\t\t\t\t\t\t? new BigDecimal(balanceInfo.getStr(\"granted_balance\")) : BigDecimal.ZERO;\n\t\t\t\tBigDecimal toppedUp = balanceInfo.containsKey(\"topped_up_balance\")\n\t\t\t\t\t\t? new BigDecimal(balanceInfo.getStr(\"topped_up_balance\")) : BigDecimal.ZERO;\n\n\t\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t\t.currency(currency)\n\t\t\t\t\t\t.totalBalance(total)\n\t\t\t\t\t\t.grantedBalance(granted)\n\t\t\t\t\t\t.toppedUpBalance(toppedUp)\n\t\t\t\t\t\t.available(true)\n\t\t\t\t\t\t.build());\n\n\t\t\t\t// 简单累加(实际应该考虑汇率转换)\n\t\t\t\ttotalBalance = totalBalance.add(total);\n\t\t\t}\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n\t/**\n\t * 查询硅基流动余额\n\t * API文档: https://docs.siliconflow.cn/cn/api-reference/userinfo/get-user-info\n\t */\n\tprivate AiApiKeyBalanceRespVO querySiliconFlowBalance(AiApiKeyDO apiKey) {\n\t\tString baseUrl = StrUtil.isNotBlank(apiKey.getUrl()) ? apiKey.getUrl() : \"https://api.siliconflow.cn\";\n\t\tString url = baseUrl + \"/v1/user/info\";\n\n\t\tHttpResponse response = HttpRequest.get(url)\n\t\t\t\t.header(\"Authorization\", \"Bearer \" + apiKey.getApiKey())\n\t\t\t\t.header(\"Content-Type\", \"application/json\")\n\t\t\t\t.timeout(10000)\n\t\t\t\t.execute();\n\n\t\tif (!response.isOk()) {\n\t\t\tthrow new RuntimeException(\"HTTP请求失败: \" + response.getStatus());\n\t\t}\n\n\t\tJSONObject jsonResponse = JSONUtil.parseObj(response.body());\n\n\t\t// 解析响应\n\t\tList balanceInfos = new ArrayList<>();\n\t\tBigDecimal totalBalance = BigDecimal.ZERO;\n\n\t\tif (jsonResponse.getInt(\"code\") == 20000 && jsonResponse.containsKey(\"data\")) {\n\t\t\tJSONObject data = jsonResponse.getJSONObject(\"data\");\n\n\t\t\t// 硅基流动返回的余额字段\n\t\t\tBigDecimal balance = data.getBigDecimal(\"balance\", BigDecimal.ZERO);\n\t\t\tBigDecimal chargeBalance = data.getBigDecimal(\"chargeBalance\", BigDecimal.ZERO);\n\t\t\tBigDecimal total = data.getBigDecimal(\"totalBalance\", BigDecimal.ZERO);\n\n\t\t\tbalanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()\n\t\t\t\t\t.currency(\"CNY\")\n\t\t\t\t\t.totalBalance(total)\n\t\t\t\t\t.grantedBalance(balance.subtract(chargeBalance).max(BigDecimal.ZERO))\n\t\t\t\t\t.toppedUpBalance(chargeBalance)\n\t\t\t\t\t.available(\"normal\".equals(data.getStr(\"status\")))\n\t\t\t\t\t.build());\n\n\t\t\ttotalBalance = total;\n\t\t}\n\n\t\treturn AiApiKeyBalanceRespVO.builder()\n\t\t\t\t.id(apiKey.getId())\n\t\t\t\t.platform(apiKey.getPlatform())\n\t\t\t\t.supported(true)\n\t\t\t\t.success(true)\n\t\t\t\t.balanceInfos(balanceInfos)\n\t\t\t\t.totalBalance(totalBalance)\n\t\t\t\t.build();\n\t}\n\n}"}, "a6faaf100e88d6375d188ba172cfeb1f88d1c0fe": {"t": 1785759994.2418296, "n": 3671, "h": "f51afd2f3b426b03", "c": "package com.luohuo.flex.ai.service.model;\n\nimport cn.hutool.core.util.ObjectUtil;\nimport com.luohuo.flex.ai.common.pojo.PageResult;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeyPageReqVO;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeySaveReqVO;\nimport com.luohuo.flex.ai.dal.model.AiApiKeyDO;\nimport com.luohuo.flex.ai.enums.CommonStatusEnum;\nimport com.luohuo.flex.ai.mapper.model.AiApiKeyMapper;\nimport com.luohuo.flex.ai.utils.BeanUtils;\nimport jakarta.annotation.Resource;\nimport org.springframework.stereotype.Service;\nimport org.springframework.validation.annotation.Validated;\n\nimport java.util.List;\n\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_DISABLE;\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_NOT_EXISTS;\nimport static com.luohuo.flex.ai.utils.ServiceExceptionUtil.exception;\n\n/**\n * AI API 密钥 Service 实现类\n *\n * @author 芋道源码\n */\n@Service\n@Validated\npublic class AiApiKeyServiceImpl implements AiApiKeyService {\n\n\t@Resource\n\tprivate AiApiKeyMapper apiKeyMapper;\n\n\t@Override\n\tpublic Long createApiKey(AiApiKeySaveReqVO createReqVO, Long userId) {\n\t\t// 插入\n\t\tAiApiKeyDO apiKey = BeanUtils.toBean(createReqVO, AiApiKeyDO.class);\n\n\t\t// 如果是私有密钥,设置 userId\n\t\tif (createReqVO.getPublicStatus() == null || Boolean.FALSE.equals(createReqVO.getPublicStatus())) {\n\t\t\tapiKey.setUserId(userId)\n\t\t\t\t\t.setPublicStatus(false);\n\t\t\t// 私有密钥默认启用\n\t\t\tif (createReqVO.getStatus() == null) {\n\t\t\t\tapiKey.setStatus(CommonStatusEnum.ENABLE.getStatus());\n\t\t\t}\n\t\t}\n\n\t\tapiKeyMapper.insert(apiKey);\n\t\treturn apiKey.getId();\n\t}\n\n\t@Override\n\tpublic void updateApiKey(AiApiKeySaveReqVO updateReqVO, Long userId) {\n\t\t// 校验存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(updateReqVO.getId());\n\n\t\t// 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以编辑\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t} else {\n\t\t\t// 公开密钥,需要管理员权限\n\t\t\tif (userId >= 10937855681025L) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 更新\n\t\tAiApiKeyDO updateObj = BeanUtils.toBean(updateReqVO, AiApiKeyDO.class);\n\t\tapiKeyMapper.updateById(updateObj);\n\t}\n\n\t@Override\n\tpublic void deleteApiKey(Long id, Long userId) {\n\t\t// 校验存在\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\n\t\t// 权限校验\n\t\tif (Boolean.FALSE.equals(apiKey.getPublicStatus())) {\n\t\t\t// 私有密钥,只有创建者可以删除\n\t\t\tif (ObjectUtil.notEqual(apiKey.getUserId(), userId)) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t} else {\n\t\t\t// 公开密钥,需要管理员权限\n\t\t\tif (userId >= 10937855681025L) {\n\t\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t\t}\n\t\t}\n\n\t\t// 删除\n\t\tapiKeyMapper.deleteById(id);\n\t}\n\n\tprivate AiApiKeyDO validateApiKeyExists(Long id) {\n\t\tAiApiKeyDO apiKey = apiKeyMapper.selectById(id);\n\t\tif (apiKey == null) {\n\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO getApiKey(Long id) {\n\t\treturn apiKeyMapper.selectById(id);\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO validateApiKey(Long id) {\n\t\tAiApiKeyDO apiKey = validateApiKeyExists(id);\n\t\tif (CommonStatusEnum.isDisable(apiKey.getStatus())) {\n\t\t\tthrow exception(API_KEY_DISABLE);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n\t@Override\n\tpublic PageResult getApiKeyPage(AiApiKeyPageReqVO pageReqVO, Long userId) {\n\t\treturn apiKeyMapper.selectPageByMy(pageReqVO, userId);\n\t}\n\n\t@Override\n\tpublic List getApiKeyList() {\n\t\treturn apiKeyMapper.selectList();\n\t}\n\n\t@Override\n\tpublic AiApiKeyDO getRequiredDefaultApiKey(String platform, Integer status) {\n\t\tAiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform, status);\n\t\tif (apiKey == null) {\n\t\t\tthrow exception(API_KEY_NOT_EXISTS);\n\t\t}\n\t\treturn apiKey;\n\t}\n\n}"}, "f45fd7f8af98d26e063ca9c6b6c2544f70a29cc5": {"t": 1785759996.006087, "n": 2894, "h": "3274c21afd0d7f75", "c": "package com.luohuo.flex.ai.service.model;\n\nimport com.luohuo.flex.ai.common.pojo.PageResult;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeyPageReqVO;\nimport com.luohuo.flex.ai.controller.model.vo.apikey.AiApiKeySaveReqVO;\nimport com.luohuo.flex.ai.dal.model.AiApiKeyDO;\nimport com.luohuo.flex.ai.enums.CommonStatusEnum;\nimport com.luohuo.flex.ai.mapper.model.AiApiKeyMapper;\nimport com.luohuo.flex.ai.utils.BeanUtils;\nimport jakarta.annotation.Resource;\nimport org.springframework.stereotype.Service;\nimport org.springframework.validation.annotation.Validated;\n\nimport java.util.List;\n\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_DISABLE;\nimport static com.luohuo.flex.ai.enums.ErrorCodeConstants.API_KEY_NOT_EXISTS;\nimport static com.luohuo.flex.ai.utils.ServiceExceptionUtil.exception;\n\n\n/**\n * AI API 密钥 Service 实现类\n *\n * @author 芋道源码\n */\n@Service\n@Validated\npublic class AiApiKeyServiceImpl implements AiApiKeyService {\n\n @Resource\n private AiApiKeyMapper apiKeyMapper;\n\n @Override\n public Long createApiKey(AiApiKeySaveReqVO createReqVO) {\n // 插入\n AiApiKeyDO apiKey = BeanUtils.toBean(createReqVO, AiApiKeyDO.class);\n apiKeyMapper.insert(apiKey);\n // 返回\n return apiKey.getId();\n }\n\n @Override\n public void updateApiKey(AiApiKeySaveReqVO updateReqVO) {\n // 校验存在\n validateApiKeyExists(updateReqVO.getId());\n // 更新\n AiApiKeyDO updateObj = BeanUtils.toBean(updateReqVO, AiApiKeyDO.class);\n apiKeyMapper.updateById(updateObj);\n }\n\n @Override\n public void deleteApiKey(Long id) {\n // 校验存在\n validateApiKeyExists(id);\n // 删除\n apiKeyMapper.deleteById(id);\n }\n\n private AiApiKeyDO validateApiKeyExists(Long id) {\n AiApiKeyDO apiKey = apiKeyMapper.selectById(id);\n if (apiKey == null) {\n throw exception(API_KEY_NOT_EXISTS);\n }\n return apiKey;\n }\n\n @Override\n public AiApiKeyDO getApiKey(Long id) {\n return apiKeyMapper.selectById(id);\n }\n\n @Override\n public AiApiKeyDO validateApiKey(Long id) {\n AiApiKeyDO apiKey = validateApiKeyExists(id);\n if (CommonStatusEnum.isDisable(apiKey.getStatus())) {\n throw exception(API_KEY_DISABLE);\n }\n return apiKey;\n }\n\n @Override\n public PageResult getApiKeyPage(AiApiKeyPageReqVO pageReqVO) {\n return apiKeyMapper.selectPage(pageReqVO);\n }\n\n @Override\n public List getApiKeyList() {\n return apiKeyMapper.selectList();\n }\n\n @Override\n public AiApiKeyDO getRequiredDefaultApiKey(String platform, Integer status) {\n AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform, status);\n if (apiKey == null) {\n throw exception(API_KEY_NOT_EXISTS);\n }\n return apiKey;\n }\n\n}"}}, "luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/HunYuanChatModel.java": {"611199efbbe715dbec71474e9fa2cec56d886b0c": {"t": 1785809890.40397, "n": 1516, "h": "cd767e8257cecd03", "c": "package com.luohuo.flex.ai.core.model;\n\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.ai.chat.model.ChatModel;\nimport org.springframework.ai.chat.model.ChatResponse;\nimport org.springframework.ai.chat.prompt.ChatOptions;\nimport org.springframework.ai.chat.prompt.Prompt;\nimport org.springframework.ai.openai.OpenAiChatModel;\nimport reactor.core.publisher.Flux;\n\n/**\n * 腾云混元 {@link ChatModel} 实现类\n *\n * 1. 混元大模型:基于 知识引擎原子能力 实现\n * 2. 知识引擎原子能力:基于 知识引擎原子能力 实现\n *\n * @author 乾乾\n */\n@Slf4j\n@RequiredArgsConstructor\npublic class HunYuanChatModel implements ChatModel {\n\n public static final String BASE_URL = \"https://api.hunyuan.cloud.tencent.com\";\n\n public static final String MODEL_DEFAULT = \"hunyuan-turbo\";\n\n public static final String DEEP_SEEK_BASE_URL = \"https://api.lkeap.cloud.tencent.com\";\n\n public static final String DEEP_SEEK_MODEL_DEFAULT = \"deepseek-v3\";\n\n /**\n * 兼容 OpenAI 接口,进行复用\n */\n private final OpenAiChatModel openAiChatModel;\n\n @Override\n public ChatResponse call(Prompt prompt) {\n return openAiChatModel.call(prompt);\n }\n\n @Override\n public Flux stream(Prompt prompt) {\n return openAiChatModel.stream(prompt);\n }\n\n @Override\n public ChatOptions getDefaultOptions() {\n return openAiChatModel.getDefaultOptions();\n }\n\n}\n"}}}}