From 133e8e7e5a024d50b77b6f62b599ffc61a2699e6 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Mon, 18 Aug 2025 14:13:16 +0800 Subject: [PATCH] fix --- .../api/api_application_service.go | 56 +++++- .../api/commands/api_call_commands.go | 12 +- .../subscription_application_service_impl.go | 18 +- .../processors/ivyz/ivyz9363_processor.go | 2 +- .../http/handlers/api_handler.go | 59 ++++--- .../infrastructure/http/routes/api_routes.go | 3 + .../shared/validator/custom_validators.go | 164 +++++++++++++++++- 7 files changed, 285 insertions(+), 29 deletions(-) diff --git a/internal/application/api/api_application_service.go b/internal/application/api/api_application_service.go index 7c3f301..51d5fd0 100644 --- a/internal/application/api/api_application_service.go +++ b/internal/application/api/api_application_service.go @@ -2,6 +2,7 @@ package api import ( "context" + "encoding/json" "errors" "tyapi-server/internal/application/api/commands" "tyapi-server/internal/application/api/dto" @@ -37,6 +38,12 @@ type ApiApplicationService interface { // 管理端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) } type ApiApplicationServiceImpl struct { @@ -102,9 +109,13 @@ func (s *ApiApplicationServiceImpl) CallApi(ctx context.Context, cmd *commands.A businessError = ErrFrozenAccount return ErrFrozenAccount } - // 在开发环境下跳过IP白名单校验 - if s.config.App.IsDevelopment() { - s.logger.Info("开发环境跳过IP白名单校验", zap.String("userId", apiUser.UserId), zap.String("ip", cmd.ClientIP)) + // 在开发环境或调试模式下跳过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)) @@ -503,3 +514,42 @@ func (s *ApiApplicationServiceImpl) GetAdminApiCalls(ctx context.Context, filter 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 +} diff --git a/internal/application/api/commands/api_call_commands.go b/internal/application/api/commands/api_call_commands.go index bf25353..152b24f 100644 --- a/internal/application/api/commands/api_call_commands.go +++ b/internal/application/api/commands/api_call_commands.go @@ -9,10 +9,18 @@ type ApiCallCommand struct { } type ApiCallOptions struct { - Json bool `json:"json,omitempty"` // 是否返回JSON格式 + Json bool `json:"json,omitempty"` // 是否返回JSON格式 + IsDebug bool `json:"is_debug,omitempty"` // 是否为调试调用 } // EncryptCommand 加密命令 type EncryptCommand struct { - Data map[string]interface{} `json:"data" binding:"required"` + Data map[string]interface{} `json:"data" binding:"required"` + SecretKey string `json:"secret_key" binding:"required"` +} + +// DecryptCommand 解密命令 +type DecryptCommand struct { + EncryptedData string `json:"encrypted_data" binding:"required"` + SecretKey string `json:"secret_key" binding:"required"` } diff --git a/internal/application/product/subscription_application_service_impl.go b/internal/application/product/subscription_application_service_impl.go index a54b9d8..caa108a 100644 --- a/internal/application/product/subscription_application_service_impl.go +++ b/internal/application/product/subscription_application_service_impl.go @@ -214,13 +214,18 @@ func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(s } } + var productResponse *responses.ProductSimpleResponse + if subscription.Product != nil { + productResponse = s.convertToProductSimpleResponse(subscription.Product) + } + return &responses.SubscriptionInfoResponse{ ID: subscription.ID, UserID: subscription.UserID, ProductID: subscription.ProductID, Price: subscription.Price.InexactFloat64(), User: userInfo, - Product: s.convertToProductSimpleResponse(subscription.Product), + Product: productResponse, APIUsed: subscription.APIUsed, CreatedAt: subscription.CreatedAt, UpdatedAt: subscription.UpdatedAt, @@ -229,6 +234,11 @@ func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(s // convertToProductSimpleResponse 转换为产品简单信息响应 func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleResponse(product *entities.Product) *responses.ProductSimpleResponse { + var categoryResponse *responses.CategorySimpleResponse + if product.Category != nil { + categoryResponse = s.convertToCategorySimpleResponse(product.Category) + } + return &responses.ProductSimpleResponse{ ID: product.ID, OldID: product.OldID, @@ -236,13 +246,17 @@ func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleResponse(prod Code: product.Code, Description: product.Description, Price: product.Price.InexactFloat64(), - Category: s.convertToCategorySimpleResponse(product.Category), + Category: categoryResponse, IsPackage: product.IsPackage, } } // convertToCategorySimpleResponse 转换为分类简单信息响应 func (s *SubscriptionApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse { + if category == nil { + return nil + } + return &responses.CategorySimpleResponse{ ID: category.ID, Name: category.Name, diff --git a/internal/domains/api/services/processors/ivyz/ivyz9363_processor.go b/internal/domains/api/services/processors/ivyz/ivyz9363_processor.go index fffd877..77c6413 100644 --- a/internal/domains/api/services/processors/ivyz/ivyz9363_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyz9363_processor.go @@ -51,7 +51,7 @@ func ProcessIVYZ9363Request(ctx context.Context, params []byte, deps *processors }, } - respBytes, err := deps.WestDexService.CallAPI("G10SC02", reqData) + respBytes, err := deps.WestDexService.CallAPI("G10XM02", reqData) if err != nil { if errors.Is(err, westdex.ErrDatasource) { return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err) diff --git a/internal/infrastructure/http/handlers/api_handler.go b/internal/infrastructure/http/handlers/api_handler.go index bc93fd1..c92c980 100644 --- a/internal/infrastructure/http/handlers/api_handler.go +++ b/internal/infrastructure/http/handlers/api_handler.go @@ -1,13 +1,11 @@ package handlers import ( - "encoding/json" "strconv" "time" "tyapi-server/internal/application/api" "tyapi-server/internal/application/api/commands" "tyapi-server/internal/application/api/dto" - "tyapi-server/internal/shared/crypto" "tyapi-server/internal/shared/interfaces" "github.com/gin-gonic/gin" @@ -194,24 +192,8 @@ func (h *ApiHandler) EncryptParams(c *gin.Context) { return } - // 获取用户的SecretKey - apiKeys, err := h.appService.GetUserApiKeys(c.Request.Context(), userID) - if err != nil { - h.logger.Error("获取用户API密钥失败", zap.Error(err)) - h.responseBuilder.BadRequest(c, "获取API密钥失败") - return - } - - // 将JSON对象转换为字节数组 - jsonData, err := json.Marshal(cmd.Data) - if err != nil { - h.logger.Error("序列化参数失败", zap.Error(err)) - h.responseBuilder.BadRequest(c, "参数序列化失败") - return - } - - // 加密参数 - encryptedData, err := crypto.AesEncrypt(jsonData, apiKeys.SecretKey) + // 调用应用服务层进行加密 + encryptedData, err := h.appService.EncryptParams(c.Request.Context(), userID, &cmd) if err != nil { h.logger.Error("加密参数失败", zap.Error(err)) h.responseBuilder.BadRequest(c, "加密参数失败") @@ -224,6 +206,43 @@ func (h *ApiHandler) EncryptParams(c *gin.Context) { h.responseBuilder.Success(c, response, "加密成功") } +// DecryptParams 解密参数 +// @Summary 解密参数 +// @Description 使用密钥解密加密的数据 +// @Tags API调试 +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body commands.DecryptCommand true "解密请求" +// @Success 200 {object} map[string]interface{} "解密成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未授权" +// @Failure 500 {object} map[string]interface{} "解密失败" +// @Router /api/v1/decrypt [post] +func (h *ApiHandler) DecryptParams(c *gin.Context) { + userID := h.getCurrentUserID(c) + if userID == "" { + h.responseBuilder.Unauthorized(c, "用户未登录") + return + } + + var cmd commands.DecryptCommand + if err := h.validator.BindAndValidate(c, &cmd); err != nil { + h.responseBuilder.BadRequest(c, "请求参数错误") + return + } + + // 调用应用服务层进行解密 + decryptedData, err := h.appService.DecryptParams(c.Request.Context(), userID, &cmd) + if err != nil { + h.logger.Error("解密参数失败", zap.Error(err)) + h.responseBuilder.BadRequest(c, "解密参数失败") + return + } + + h.responseBuilder.Success(c, decryptedData, "解密成功") +} + // getCurrentUserID 获取当前用户ID func (h *ApiHandler) getCurrentUserID(c *gin.Context) string { if userID, exists := c.Get("user_id"); exists { diff --git a/internal/infrastructure/http/routes/api_routes.go b/internal/infrastructure/http/routes/api_routes.go index 79c0089..06b3227 100644 --- a/internal/infrastructure/http/routes/api_routes.go +++ b/internal/infrastructure/http/routes/api_routes.go @@ -42,6 +42,9 @@ func (r *ApiRoutes) Register(router *sharedhttp.GinRouter) { // 加密接口(用于前端调试) apiGroup.POST("/encrypt", r.authMiddleware.Handle(), r.apiHandler.EncryptParams) + + // 解密接口(用于前端调试) + apiGroup.POST("/decrypt", r.authMiddleware.Handle(), r.apiHandler.DecryptParams) // API密钥管理接口 apiGroup.GET("/api-keys", r.authMiddleware.Handle(), r.apiHandler.GetUserApiKeys) diff --git a/internal/shared/validator/custom_validators.go b/internal/shared/validator/custom_validators.go index 740d2c1..45dc35d 100644 --- a/internal/shared/validator/custom_validators.go +++ b/internal/shared/validator/custom_validators.go @@ -25,9 +25,31 @@ func RegisterCustomValidators(validate *validator.Validate) { // 统一社会信用代码验证器 validate.RegisterValidation("social_credit_code", validateSocialCreditCode) - // 身份证号验证器 + // 姓名验证器(不能为空字符串,长度1-50字符) + validate.RegisterValidation("validName", validateName) + + // 身份证号验证器(兼容两种标签) + validate.RegisterValidation("validIDCard", validateIDCard) validate.RegisterValidation("id_card", validateIDCard) + // 统一社会信用代码验证器 + validate.RegisterValidation("validUSCI", validateUSCI) + + // 手机号验证器 + validate.RegisterValidation("validMobileNo", validateMobileNo) + + // 手机类型验证器 + validate.RegisterValidation("validMobileType", validateMobileType) + + // 日期验证器 + validate.RegisterValidation("validDate", validateDate) + + // 时间范围验证器 + validate.RegisterValidation("validTimeRange", validateTimeRange) + + // 银行卡验证器 + validate.RegisterValidation("validBankCard", validateBankCard) + // 价格验证器(非负数) validate.RegisterValidation("price", validatePrice) @@ -52,6 +74,9 @@ func RegisterCustomValidators(validate *validator.Validate) { // IP地址验证器 validate.RegisterValidation("ip", validateIP) + // 非空字符串验证器(不能为空字符串或只包含空格) + validate.RegisterValidation("notEmpty", validateNotEmpty) + // 授权日期验证器 validate.RegisterValidation("auth_date", validateAuthDate) } @@ -156,6 +181,20 @@ func validateIP(fl validator.FieldLevel) bool { pattern := `^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$` matched, _ := regexp.MatchString(pattern, ip) return matched +} + +// validateName 姓名验证器 +func validateName(fl validator.FieldLevel) bool { + name := fl.Field().String() + // 去除首尾空格后检查长度 + trimmedName := strings.TrimSpace(name) + if len(trimmedName) < 1 || len(trimmedName) > 50 { + return false + } + // 姓名不能只包含空格或特殊字符 + // 必须包含至少一个中文字符或英文字母 + hasValidChar := regexp.MustCompile(`[\p{Han}a-zA-Z]`).MatchString(trimmedName) + return hasValidChar } // validateAuthDate 授权日期验证器 @@ -236,4 +275,127 @@ func parseYYYYMMDD(dateStr string) (time.Time, error) { } return date, nil +} + +// validateUSCI 统一社会信用代码验证器 +func validateUSCI(fl validator.FieldLevel) bool { + usci := fl.Field().String() + // 统一社会信用代码格式:18位,由数字和大写字母组成 + // 格式:1位登记管理部门代码 + 1位机构类别代码 + 6位登记管理机关行政区划码 + 9位主体标识码 + 1位校验码 + matched, _ := regexp.MatchString(`^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$`, usci) + return matched +} + +// validateMobileNo 手机号验证器 +func validateMobileNo(fl validator.FieldLevel) bool { + mobile := fl.Field().String() + // 中国手机号格式:1开头,第二位是3-9,总共11位数字 + matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, mobile) + return matched +} + +// validateMobileType 手机类型验证器 +func validateMobileType(fl validator.FieldLevel) bool { + mobileType := fl.Field().String() + // 手机类型:移动、联通、电信等 + validTypes := []string{"移动", "联通", "电信", "广电", "虚拟运营商"} + for _, validType := range validTypes { + if mobileType == validType { + return true + } + } + return false +} + +// validateDate 日期验证器 +func validateDate(fl validator.FieldLevel) bool { + dateStr := fl.Field().String() + // 检查日期格式:YYYY-MM-DD + matched, _ := regexp.MatchString(`^\d{4}-\d{2}-\d{2}$`, dateStr) + if !matched { + return false + } + + // 尝试解析日期 + _, err := time.Parse("2006-01-02", dateStr) + return err == nil +} + +// validateTimeRange 时间范围验证器 +func validateTimeRange(fl validator.FieldLevel) bool { + timeRange := fl.Field().String() + if timeRange == "" { + return true // 空值由omitempty标签处理 + } + + // 时间范围格式:HH:MM-HH:MM + parts := strings.Split(timeRange, "-") + if len(parts) != 2 { + return false + } + + startTime := parts[0] + endTime := parts[1] + + // 检查时间格式:HH:MM + timePattern := `^([01]?[0-9]|2[0-3]):[0-5][0-9]$` + startMatched, _ := regexp.MatchString(timePattern, startTime) + endMatched, _ := regexp.MatchString(timePattern, endTime) + + if !startMatched || !endMatched { + return false + } + + // 检查开始时间不能晚于结束时间 + start, _ := time.Parse("15:04", startTime) + end, _ := time.Parse("15:04", endTime) + + return start.Before(end) || start.Equal(end) +} + +// validateNotEmpty 非空字符串验证器 +func validateNotEmpty(fl validator.FieldLevel) bool { + value := fl.Field().String() + // 去除首尾空格后检查是否为空 + trimmedValue := strings.TrimSpace(value) + return len(trimmedValue) > 0 +} + +// validateBankCard 银行卡验证器 +func validateBankCard(fl validator.FieldLevel) bool { + bankCard := fl.Field().String() + // 银行卡号格式:13-19位数字 + matched, _ := regexp.MatchString(`^\d{13,19}$`, bankCard) + if !matched { + return false + } + + // 使用Luhn算法验证银行卡号 + return validateLuhn(bankCard) +} + +// validateLuhn Luhn算法验证银行卡号 +func validateLuhn(cardNumber string) bool { + sum := 0 + alternate := false + + // 从右到左遍历 + for i := len(cardNumber) - 1; i >= 0; i-- { + digit, err := strconv.Atoi(string(cardNumber[i])) + if err != nil { + return false + } + + if alternate { + digit *= 2 + if digit > 9 { + digit = digit%10 + digit/10 + } + } + + sum += digit + alternate = !alternate + } + + return sum%10 == 0 } \ No newline at end of file