Files
tyapi-server/internal/application/api/api_application_service.go
2025-09-01 18:29:59 +08:00

683 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/application/api/commands"
"tyapi-server/internal/application/api/dto"
"tyapi-server/internal/application/api/utils"
"tyapi-server/internal/config"
entities "tyapi-server/internal/domains/api/entities"
"tyapi-server/internal/domains/api/repositories"
"tyapi-server/internal/domains/api/services"
"tyapi-server/internal/domains/api/services/processors"
finance_services "tyapi-server/internal/domains/finance/services"
product_services "tyapi-server/internal/domains/product/services"
user_repositories "tyapi-server/internal/domains/user/repositories"
"tyapi-server/internal/shared/crypto"
"tyapi-server/internal/shared/database"
"tyapi-server/internal/shared/interfaces"
"go.uber.org/zap"
)
type ApiApplicationService interface {
CallApi(ctx context.Context, cmd *commands.ApiCallCommand) (string, string, error)
// 获取用户API密钥
GetUserApiKeys(ctx context.Context, userID string) (*dto.ApiKeysResponse, error)
// 用户白名单管理
GetUserWhiteList(ctx context.Context, userID string) (*dto.WhiteListListResponse, error)
AddWhiteListIP(ctx context.Context, userID string, ipAddress string) error
DeleteWhiteListIP(ctx context.Context, userID string, ipAddress string) error
// 获取用户API调用记录
GetUserApiCalls(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*dto.ApiCallListResponse, error)
// 管理端API调用记录
GetAdminApiCalls(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*dto.ApiCallListResponse, error)
// 加密参数接口
EncryptParams(ctx context.Context, userID string, cmd *commands.EncryptCommand) (string, error)
// 解密参数接口
DecryptParams(ctx context.Context, userID string, cmd *commands.DecryptCommand) (map[string]interface{}, error)
// 获取表单配置
GetFormConfig(ctx context.Context, apiCode string) (*dto.FormConfigResponse, error)
}
type ApiApplicationServiceImpl struct {
apiCallService services.ApiCallAggregateService
apiUserService services.ApiUserAggregateService
apiRequestService *services.ApiRequestService
formConfigService services.FormConfigService
apiCallRepository repositories.ApiCallRepository
walletService finance_services.WalletAggregateService
contractInfoService user_repositories.ContractInfoRepository
productManagementService *product_services.ProductManagementService
productSubscriptionService *product_services.ProductSubscriptionService
userRepo user_repositories.UserRepository
txManager *database.TransactionManager
config *config.Config
logger *zap.Logger
}
func NewApiApplicationService(apiCallService services.ApiCallAggregateService, apiUserService services.ApiUserAggregateService, apiRequestService *services.ApiRequestService, formConfigService services.FormConfigService, apiCallRepository repositories.ApiCallRepository, walletService finance_services.WalletAggregateService, productManagementService *product_services.ProductManagementService, productSubscriptionService *product_services.ProductSubscriptionService, userRepo user_repositories.UserRepository, txManager *database.TransactionManager, config *config.Config, logger *zap.Logger, contractInfoService user_repositories.ContractInfoRepository) ApiApplicationService {
return &ApiApplicationServiceImpl{apiCallService: apiCallService, apiUserService: apiUserService, apiRequestService: apiRequestService, formConfigService: formConfigService, apiCallRepository: apiCallRepository, walletService: walletService, productManagementService: productManagementService, productSubscriptionService: productSubscriptionService, userRepo: userRepo, txManager: txManager, config: config, logger: logger, contractInfoService: contractInfoService}
}
// CallApi 应用服务层统一入口
func (s *ApiApplicationServiceImpl) CallApi(ctx context.Context, cmd *commands.ApiCallCommand) (string, string, error) {
// 在事务外创建ApiCall
apiCall, err := s.apiCallService.CreateApiCall(cmd.AccessId, cmd.Data, cmd.ClientIP)
if err != nil {
s.logger.Error("创建ApiCall失败", zap.Error(err))
return "", "", ErrSystem
}
transactionId := apiCall.TransactionId
// 先保存初始状态
err = s.apiCallService.SaveApiCall(ctx, apiCall)
if err != nil {
s.logger.Error("保存ApiCall初始状态失败", zap.Error(err))
return "", "", ErrSystem
}
var encryptedResponse string
var businessError error
// 在事务中执行业务逻辑
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
// 1. 查ApiUser
apiUser, err := s.apiUserService.LoadApiUserByAccessId(txCtx, cmd.AccessId)
if err != nil {
s.logger.Error("查ApiUser失败", zap.Error(err))
businessError = ErrInvalidAccessId
return ErrInvalidAccessId
}
// 3. 查产品
product, err := s.productManagementService.GetProductByCode(txCtx, cmd.ApiName)
if err != nil {
s.logger.Error("查产品失败", zap.Error(err))
businessError = ErrProductNotFound
return ErrProductNotFound
}
apiCall.ProductId = &product.ID
// 加入UserId
apiCall.UserId = &apiUser.UserId
if apiUser.IsFrozen() {
s.logger.Error("账户已冻结", zap.String("userId", apiUser.UserId))
businessError = ErrFrozenAccount
return ErrFrozenAccount
}
// 在开发环境或调试模式下跳过IP白名单校验
if s.config.App.IsDevelopment() || cmd.Options.IsDebug {
s.logger.Info("跳过IP白名单校验",
zap.String("userId", apiUser.UserId),
zap.String("ip", cmd.ClientIP),
zap.Bool("isDevelopment", s.config.App.IsDevelopment()),
zap.Bool("isDebug", cmd.Options.IsDebug))
} else {
if !apiUser.IsWhiteListed(cmd.ClientIP) {
s.logger.Error("IP不在白名单内", zap.String("userId", apiUser.UserId), zap.String("ip", cmd.ClientIP))
businessError = ErrInvalidIP
return ErrInvalidIP
}
}
// 2. 查钱包
wallet, err := s.walletService.LoadWalletByUserId(txCtx, apiUser.UserId)
if err != nil {
s.logger.Error("查钱包失败", zap.Error(err))
businessError = ErrSystem
return ErrSystem
}
if wallet.IsArrears() {
s.logger.Error("账户已欠费", zap.String("userId", apiUser.UserId))
businessError = ErrArrears
return ErrArrears
}
// 4. 查订阅
subscription, err := s.productSubscriptionService.GetUserSubscribedProduct(txCtx, apiUser.UserId, product.ID)
if err != nil {
s.logger.Error("查订阅失败", zap.Error(err))
businessError = ErrSystem
return ErrSystem
}
if subscription == nil {
s.logger.Error("用户未订阅该产品", zap.String("userId", apiUser.UserId), zap.String("productId", product.ID))
businessError = ErrNotSubscribed
return ErrNotSubscribed
}
if !product.IsValid() {
s.logger.Error("产品已停用", zap.String("productId", product.ID))
businessError = ErrProductDisabled
return ErrProductDisabled
}
// 5. 解密参数
requestParams, err := crypto.AesDecrypt(cmd.Data, apiUser.SecretKey)
if err != nil {
s.logger.Error("解密参数失败", zap.Error(err))
businessError = ErrDecryptFail
return ErrDecryptFail
}
// 6. 调用API
// 查询用户的合同信息获取合同编号
contractCode := "" // 默认空字符串
contractInfo, err := s.contractInfoService.FindByUserID(txCtx, apiUser.UserId)
if err == nil && len(contractInfo) > 0 {
contractCode = contractInfo[0].ContractCode
} else {
s.logger.Error("查合同信息失败", zap.Error(err))
businessError = ErrSystem
return ErrSystem
}
// 创建CallContext传递合同编号
callContext := &processors.CallContext{
ContractCode: contractCode,
}
// 将transactionId放入ctx中供外部服务使用
ctxWithTransactionId := context.WithValue(txCtx, "transaction_id", transactionId)
response, err := s.apiRequestService.PreprocessRequestApi(ctxWithTransactionId, cmd.ApiName, requestParams, &cmd.Options, callContext)
if err != nil {
if errors.Is(err, processors.ErrDatasource) {
s.logger.Error("调用API失败", zap.Error(err))
businessError = ErrSystem
return ErrSystem
} else if errors.Is(err, processors.ErrInvalidParam) {
s.logger.Error("调用API失败", zap.Error(err))
businessError = ErrInvalidParam
return ErrInvalidParam
} else if errors.Is(err, processors.ErrNotFound) {
s.logger.Error("调用API失败", zap.Error(err))
businessError = ErrQueryEmpty
return ErrQueryEmpty
} else {
s.logger.Error("调用API失败", zap.Error(err))
businessError = ErrSystem
return ErrSystem
}
}
// 7. 加密响应
encryptedResponse, err = crypto.AesEncrypt(response, apiUser.SecretKey)
if err != nil {
s.logger.Error("加密响应失败", zap.Error(err))
businessError = ErrSystem
return ErrSystem
}
// apiCall.ResponseData = &encryptedResponse
// 8. 更新订阅使用次数(使用乐观锁)
// err = s.productSubscriptionService.IncrementSubscriptionAPIUsage(txCtx, subscription.ID, 1)
// if err != nil {
// s.logger.Error("更新订阅使用次数失败", zap.Error(err))
// businessError = ErrSystem
// return ErrSystem
// }
// 9. 扣钱
err = s.walletService.Deduct(txCtx, apiUser.UserId, subscription.Price, apiCall.ID, transactionId, product.ID)
if err != nil {
s.logger.Error("扣钱失败", zap.Error(err))
businessError = ErrSystem
return ErrSystem
}
apiCall.Cost = &subscription.Price
// 10. 标记成功
apiCall.MarkSuccess(subscription.Price)
return nil
})
// 根据事务结果更新ApiCall状态
if err != nil {
// 事务失败根据错误类型标记ApiCall
if businessError != nil {
// 使用业务错误类型
switch businessError {
case ErrInvalidAccessId:
apiCall.MarkFailed(entities.ApiCallErrorInvalidAccess, err.Error())
case ErrFrozenAccount:
apiCall.MarkFailed(entities.ApiCallErrorFrozenAccount, "")
case ErrInvalidIP:
apiCall.MarkFailed(entities.ApiCallErrorInvalidIP, "")
case ErrArrears:
apiCall.MarkFailed(entities.ApiCallErrorArrears, "")
case ErrProductNotFound:
apiCall.MarkFailed(entities.ApiCallErrorProductNotFound, err.Error())
case ErrProductDisabled:
apiCall.MarkFailed(entities.ApiCallErrorProductDisabled, "")
case ErrNotSubscribed:
apiCall.MarkFailed(entities.ApiCallErrorNotSubscribed, "")
case ErrDecryptFail:
apiCall.MarkFailed(entities.ApiCallErrorDecryptFail, err.Error())
case ErrInvalidParam:
apiCall.MarkFailed(entities.ApiCallErrorInvalidParam, err.Error())
case ErrQueryEmpty:
apiCall.MarkFailed(entities.ApiCallErrorQueryEmpty, "")
default:
apiCall.MarkFailed(entities.ApiCallErrorSystem, err.Error())
}
} else {
// 系统错误
apiCall.MarkFailed(entities.ApiCallErrorSystem, err.Error())
}
}
// 保存最终状态
err = s.apiCallService.SaveApiCall(ctx, apiCall)
if err != nil {
s.logger.Error("保存ApiCall最终状态失败", zap.Error(err))
// 即使保存失败,也返回业务结果
}
if businessError != nil {
return transactionId, "", businessError
}
return transactionId, encryptedResponse, nil
}
// GetUserApiKeys 获取用户API密钥
func (s *ApiApplicationServiceImpl) GetUserApiKeys(ctx context.Context, userID string) (*dto.ApiKeysResponse, error) {
apiUser, err := s.apiUserService.LoadApiUserByUserId(ctx, userID)
if err != nil {
return nil, err
}
return &dto.ApiKeysResponse{
ID: apiUser.ID,
UserID: apiUser.UserId,
AccessID: apiUser.AccessId,
SecretKey: apiUser.SecretKey,
Status: apiUser.Status,
CreatedAt: apiUser.CreatedAt,
UpdatedAt: apiUser.UpdatedAt,
}, nil
}
// GetUserWhiteList 获取用户白名单列表
func (s *ApiApplicationServiceImpl) GetUserWhiteList(ctx context.Context, userID string) (*dto.WhiteListListResponse, error) {
apiUser, err := s.apiUserService.LoadApiUserByUserId(ctx, userID)
if err != nil {
return nil, err
}
// 确保WhiteList不为nil
if apiUser.WhiteList == nil {
apiUser.WhiteList = []string{}
}
// 将白名单字符串数组转换为响应格式
var items []dto.WhiteListResponse
for _, ip := range apiUser.WhiteList {
items = append(items, dto.WhiteListResponse{
ID: apiUser.ID, // 使用API用户ID作为标识
UserID: apiUser.UserId,
IPAddress: ip,
CreatedAt: apiUser.CreatedAt, // 使用API用户创建时间
})
}
return &dto.WhiteListListResponse{
Items: items,
Total: len(items),
}, nil
}
// AddWhiteListIP 添加白名单IP
func (s *ApiApplicationServiceImpl) AddWhiteListIP(ctx context.Context, userID string, ipAddress string) error {
apiUser, err := s.apiUserService.LoadApiUserByUserId(ctx, userID)
if err != nil {
return err
}
// 确保WhiteList不为nil
if apiUser.WhiteList == nil {
apiUser.WhiteList = []string{}
}
// 使用实体的领域方法添加IP到白名单
err = apiUser.AddToWhiteList(ipAddress)
if err != nil {
return err
}
// 保存更新
err = s.apiUserService.SaveApiUser(ctx, apiUser)
if err != nil {
return err
}
return nil
}
// DeleteWhiteListIP 删除白名单IP
func (s *ApiApplicationServiceImpl) DeleteWhiteListIP(ctx context.Context, userID string, ipAddress string) error {
apiUser, err := s.apiUserService.LoadApiUserByUserId(ctx, userID)
if err != nil {
return err
}
// 确保WhiteList不为nil
if apiUser.WhiteList == nil {
apiUser.WhiteList = []string{}
}
// 使用实体的领域方法删除IP
err = apiUser.RemoveFromWhiteList(ipAddress)
if err != nil {
return err
}
// 保存更新
err = s.apiUserService.SaveApiUser(ctx, apiUser)
if err != nil {
return err
}
return nil
}
// GetUserApiCalls 获取用户API调用记录
func (s *ApiApplicationServiceImpl) GetUserApiCalls(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*dto.ApiCallListResponse, error) {
// 查询API调用记录包含产品名称
productNameMap, calls, total, err := s.apiCallRepository.ListByUserIdWithFiltersAndProductName(ctx, userID, filters, options)
if err != nil {
s.logger.Error("查询API调用记录失败", zap.Error(err), zap.String("userID", userID))
return nil, err
}
// 转换为响应DTO
var items []dto.ApiCallRecordResponse
for _, call := range calls {
item := dto.ApiCallRecordResponse{
ID: call.ID,
AccessId: call.AccessId,
UserId: *call.UserId,
TransactionId: call.TransactionId,
ClientIp: call.ClientIp,
Status: call.Status,
StartAt: call.StartAt.Format("2006-01-02 15:04:05"),
CreatedAt: call.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: call.UpdatedAt.Format("2006-01-02 15:04:05"),
}
// 处理可选字段
if call.ProductId != nil {
item.ProductId = call.ProductId
}
// 从映射中获取产品名称
if productName, exists := productNameMap[call.ID]; exists {
item.ProductName = &productName
}
if call.EndAt != nil {
endAt := call.EndAt.Format("2006-01-02 15:04:05")
item.EndAt = &endAt
}
if call.Cost != nil {
cost := call.Cost.String()
item.Cost = &cost
}
if call.ErrorType != nil {
item.ErrorType = call.ErrorType
}
if call.ErrorMsg != nil {
item.ErrorMsg = call.ErrorMsg
// 添加翻译后的错误信息
item.TranslatedErrorMsg = utils.TranslateErrorMsg(call.ErrorType, call.ErrorMsg)
}
items = append(items, item)
}
return &dto.ApiCallListResponse{
Items: items,
Total: total,
Page: options.Page,
Size: options.PageSize,
}, nil
}
// GetAdminApiCalls 获取管理端API调用记录
func (s *ApiApplicationServiceImpl) GetAdminApiCalls(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*dto.ApiCallListResponse, error) {
// 查询API调用记录包含产品名称
productNameMap, calls, total, err := s.apiCallRepository.ListWithFiltersAndProductName(ctx, filters, options)
if err != nil {
s.logger.Error("查询API调用记录失败", zap.Error(err))
return nil, err
}
// 转换为响应DTO
var items []dto.ApiCallRecordResponse
for _, call := range calls {
// 基础字段安全检查
if call.ID == "" {
s.logger.Warn("跳过无效的API调用记录ID为空")
continue
}
item := dto.ApiCallRecordResponse{
ID: call.ID,
AccessId: call.AccessId,
TransactionId: call.TransactionId,
ClientIp: call.ClientIp,
Status: call.Status,
}
// 安全设置用户ID
if call.UserId != nil && *call.UserId != "" {
item.UserId = *call.UserId
} else {
item.UserId = "未知用户"
}
// 安全设置时间字段
if !call.StartAt.IsZero() {
item.StartAt = call.StartAt.Format("2006-01-02 15:04:05")
} else {
item.StartAt = "未知时间"
}
if !call.CreatedAt.IsZero() {
item.CreatedAt = call.CreatedAt.Format("2006-01-02 15:04:05")
} else {
item.CreatedAt = "未知时间"
}
if !call.UpdatedAt.IsZero() {
item.UpdatedAt = call.UpdatedAt.Format("2006-01-02 15:04:05")
} else {
item.UpdatedAt = "未知时间"
}
// 处理可选字段
if call.ProductId != nil && *call.ProductId != "" {
item.ProductId = call.ProductId
}
// 从映射中获取产品名称
if productName, exists := productNameMap[call.ID]; exists && productName != "" {
item.ProductName = &productName
}
// 安全设置结束时间
if call.EndAt != nil && !call.EndAt.IsZero() {
endAt := call.EndAt.Format("2006-01-02 15:04:05")
item.EndAt = &endAt
}
// 安全设置费用
if call.Cost != nil {
cost := call.Cost.String()
if cost != "" {
item.Cost = &cost
}
}
// 安全设置错误类型
if call.ErrorType != nil && *call.ErrorType != "" {
item.ErrorType = call.ErrorType
}
// 安全设置错误信息
if call.ErrorMsg != nil && *call.ErrorMsg != "" {
item.ErrorMsg = call.ErrorMsg
// 添加翻译后的错误信息
if call.ErrorType != nil && *call.ErrorType != "" {
item.TranslatedErrorMsg = utils.TranslateErrorMsg(call.ErrorType, call.ErrorMsg)
}
}
// 获取用户信息和企业名称(增强空指针防护)
if call.UserId != nil && *call.UserId != "" {
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, *call.UserId)
if err == nil && user.ID != "" {
companyName := "未知企业"
// 安全获取企业名称
if user.EnterpriseInfo != nil && user.EnterpriseInfo.CompanyName != "" {
companyName = user.EnterpriseInfo.CompanyName
}
item.CompanyName = &companyName
// 安全构建用户响应
item.User = &dto.UserSimpleResponse{
ID: user.ID,
CompanyName: companyName,
Phone: user.Phone,
}
// 验证用户数据的完整性
if user.Phone == "" {
s.logger.Warn("用户手机号为空",
zap.String("user_id", user.ID),
zap.String("call_id", call.ID))
item.User.Phone = "未知手机号"
}
} else {
// 用户查询失败或用户数据不完整时的处理
if err != nil {
s.logger.Warn("获取用户信息失败",
zap.String("user_id", *call.UserId),
zap.String("call_id", call.ID),
zap.Error(err))
} else if user.ID == "" {
s.logger.Warn("用户ID为空",
zap.String("call_user_id", *call.UserId),
zap.String("call_id", call.ID))
}
// 设置默认值
defaultCompanyName := "未知企业"
item.CompanyName = &defaultCompanyName
item.User = &dto.UserSimpleResponse{
ID: "未知用户",
CompanyName: defaultCompanyName,
Phone: "未知手机号",
}
}
} else {
// 用户ID为空时的处理
defaultCompanyName := "未知企业"
item.CompanyName = &defaultCompanyName
item.User = &dto.UserSimpleResponse{
ID: "未知用户",
CompanyName: defaultCompanyName,
Phone: "未知手机号",
}
}
items = append(items, item)
}
return &dto.ApiCallListResponse{
Items: items,
Total: total,
Page: options.Page,
Size: options.PageSize,
}, nil
}
// EncryptParams 加密参数
func (s *ApiApplicationServiceImpl) EncryptParams(ctx context.Context, userID string, cmd *commands.EncryptCommand) (string, error) {
// 1. 将数据转换为JSON字节数组
jsonData, err := json.Marshal(cmd.Data)
if err != nil {
s.logger.Error("序列化参数失败", zap.Error(err))
return "", err
}
// 2. 使用前端传来的SecretKey进行加密
encryptedData, err := crypto.AesEncrypt(jsonData, cmd.SecretKey)
if err != nil {
s.logger.Error("加密参数失败", zap.Error(err))
return "", err
}
return encryptedData, nil
}
// DecryptParams 解密参数
func (s *ApiApplicationServiceImpl) DecryptParams(ctx context.Context, userID string, cmd *commands.DecryptCommand) (map[string]interface{}, error) {
// 1. 使用前端传来的SecretKey进行解密
decryptedData, err := crypto.AesDecrypt(cmd.EncryptedData, cmd.SecretKey)
if err != nil {
s.logger.Error("解密参数失败", zap.Error(err))
return nil, err
}
// 2. 将解密后的JSON字节数组转换为map
var result map[string]interface{}
err = json.Unmarshal(decryptedData, &result)
if err != nil {
s.logger.Error("反序列化解密数据失败", zap.Error(err))
return nil, err
}
return result, nil
}
// GetFormConfig 获取指定API的表单配置
func (s *ApiApplicationServiceImpl) GetFormConfig(ctx context.Context, apiCode string) (*dto.FormConfigResponse, error) {
// 调用领域服务获取表单配置
config, err := s.formConfigService.GetFormConfig(apiCode)
if err != nil {
s.logger.Error("获取表单配置失败", zap.String("api_code", apiCode), zap.Error(err))
return nil, err
}
if config == nil {
return nil, nil
}
// 转换为应用层DTO
response := &dto.FormConfigResponse{
ApiCode: config.ApiCode,
Fields: make([]dto.FormField, len(config.Fields)),
}
for i, field := range config.Fields {
response.Fields[i] = dto.FormField{
Name: field.Name,
Label: field.Label,
Type: field.Type,
Required: field.Required,
Validation: field.Validation,
Description: field.Description,
Example: field.Example,
Placeholder: field.Placeholder,
}
}
return response, nil
}