From b4134d7942ee3a3358a2e62a7f1d0f7800bbe6ad Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Mon, 3 Nov 2025 13:32:05 +0800 Subject: [PATCH] fix --- .../product_application_service_impl.go | 628 +++++++++++++++++- internal/container/container.go | 18 +- .../http/handlers/product_handler.go | 6 +- 3 files changed, 633 insertions(+), 19 deletions(-) diff --git a/internal/application/product/product_application_service_impl.go b/internal/application/product/product_application_service_impl.go index a841ecb..63c3b23 100644 --- a/internal/application/product/product_application_service_impl.go +++ b/internal/application/product/product_application_service_impl.go @@ -3,6 +3,8 @@ package product import ( "context" "fmt" + "reflect" + "strings" "github.com/shopspring/decimal" "go.uber.org/zap" @@ -10,6 +12,8 @@ import ( "tyapi-server/internal/application/product/dto/commands" appQueries "tyapi-server/internal/application/product/dto/queries" "tyapi-server/internal/application/product/dto/responses" + "tyapi-server/internal/domains/api/dto" + api_services "tyapi-server/internal/domains/api/services" "tyapi-server/internal/domains/product/entities" product_service "tyapi-server/internal/domains/product/services" "tyapi-server/internal/shared/interfaces" @@ -22,6 +26,7 @@ type ProductApplicationServiceImpl struct { productSubscriptionService *product_service.ProductSubscriptionService productApiConfigAppService ProductApiConfigApplicationService documentationAppService DocumentationApplicationServiceInterface + formConfigService api_services.FormConfigService logger *zap.Logger } @@ -31,6 +36,7 @@ func NewProductApplicationService( productSubscriptionService *product_service.ProductSubscriptionService, productApiConfigAppService ProductApiConfigApplicationService, documentationAppService DocumentationApplicationServiceInterface, + formConfigService api_services.FormConfigService, logger *zap.Logger, ) ProductApplicationService { return &ProductApplicationServiceImpl{ @@ -38,12 +44,13 @@ func NewProductApplicationService( productSubscriptionService: productSubscriptionService, productApiConfigAppService: productApiConfigAppService, documentationAppService: documentationAppService, + formConfigService: formConfigService, logger: logger, } } // CreateProduct 创建产品 -// 业务流程:1. 构建产品实体 2. 创建产品 +// 业务流程�?. 构建产品实体 2. 创建产品 func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) error { // 1. 构建产品实体 product := &entities.Product{ @@ -67,7 +74,7 @@ func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd * } // UpdateProduct 更新产品 -// 业务流程:1. 获取现有产品 2. 更新产品信息 3. 保存产品 +// 业务流程�?. 获取现有产品 2. 更新产品信息 3. 保存产品 func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error { // 1. 获取现有产品 existingProduct, err := s.productManagementService.GetProductByID(ctx, cmd.ID) @@ -94,13 +101,13 @@ func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd * } // DeleteProduct 删除产品 -// 业务流程:1. 删除产品 +// 业务流程�?. 删除产品 func (s *ProductApplicationServiceImpl) DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error { return s.productManagementService.DeleteProduct(ctx, cmd.ID) } // ListProducts 获取产品列表 -// 业务流程:1. 获取产品列表 2. 构建响应数据 +// 业务流程�?. 获取产品列表 2. 构建响应数据 func (s *ProductApplicationServiceImpl) ListProducts(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error) { // 检查是否有用户ID,如果有则使用带订阅状态的方法 if userID, ok := filters["user_id"].(string); ok && userID != "" { @@ -128,7 +135,7 @@ func (s *ProductApplicationServiceImpl) ListProducts(ctx context.Context, filter } // ListProductsWithSubscriptionStatus 获取产品列表(包含订阅状态) -// 业务流程:1. 获取产品列表和订阅状态 2. 构建响应数据 +// 业务流程�?. 获取产品列表和订阅状�?2. 构建响应数据 func (s *ProductApplicationServiceImpl) ListProductsWithSubscriptionStatus(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error) { // 调用领域服务获取产品列表(包含订阅状态) products, subscriptionStatusMap, total, err := s.productManagementService.ListProductsWithSubscriptionStatus(ctx, filters, options) @@ -158,7 +165,7 @@ func (s *ProductApplicationServiceImpl) ListProductsWithSubscriptionStatus(ctx c } // GetProductsByIDs 根据ID列表获取产品 -// 业务流程:1. 获取产品列表 2. 构建响应数据 +// 业务流程�?. 获取产品列表 2. 构建响应数据 func (s *ProductApplicationServiceImpl) GetProductsByIDs(ctx context.Context, query *appQueries.GetProductsByIDsQuery) ([]*responses.ProductInfoResponse, error) { // 这里需要扩展领域服务来支持批量获取 // 暂时返回空列表 @@ -191,7 +198,7 @@ func (s *ProductApplicationServiceImpl) GetSubscribableProducts(ctx context.Cont } // GetProductByID 根据ID获取产品 -// 业务流程:1. 获取产品信息 2. 构建响应数据 +// 业务流程�?. 获取产品信息 2. 构建响应数据 func (s *ProductApplicationServiceImpl) GetProductByID(ctx context.Context, query *appQueries.GetProductQuery) (*responses.ProductInfoResponse, error) { product, err := s.productManagementService.GetProductWithCategory(ctx, query.ID) if err != nil { @@ -202,7 +209,7 @@ func (s *ProductApplicationServiceImpl) GetProductByID(ctx context.Context, quer } // GetProductStats 获取产品统计信息 -// 业务流程:1. 获取产品统计 2. 构建响应数据 +// 业务流程�?. 获取产品统计 2. 构建响应数据 func (s *ProductApplicationServiceImpl) GetProductStats(ctx context.Context) (*responses.ProductStatsResponse, error) { stats, err := s.productSubscriptionService.GetProductStats(ctx) if err != nil { @@ -353,7 +360,6 @@ func (s *ProductApplicationServiceImpl) GetAvailableProducts(ctx context.Context filters := make(map[string]interface{}) filters["is_package"] = false // 只获取非组合包产品 filters["is_enabled"] = true // 只获取启用产品 - if query.Keyword != "" { filters["keyword"] = query.Keyword } @@ -434,7 +440,8 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForAdmin(ctx context.Conte } // GetProductByIDForUser 根据ID获取产品(用户端专用) -// 业务流程:1. 获取产品信息 2. 验证产品可见性 3. 构建用户响应数据 +// 业务流程:1. 获取产品信息 2. 构建用户响应数据 +// 注意:详情接口不受 is_visible 字段影响,可通过直接访问详情接口查看任何产品 func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Context, query *appQueries.GetProductDetailQuery) (*responses.ProductInfoWithDocumentResponse, error) { // 首先尝试通过新ID查找产品 product, err := s.productManagementService.GetProductWithCategory(ctx, query.ID) @@ -446,11 +453,6 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex } } - // 用户端只能查看可见的产品 - if !product.IsVisible { - return nil, fmt.Errorf("产品不存在或不可见") - } - response := &responses.ProductInfoWithDocumentResponse{ ProductInfoResponse: *s.convertToProductInfoResponse(product), } @@ -460,6 +462,12 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex doc, err := s.documentationAppService.GetDocumentationByProductID(ctx, product.ID) if err == nil && doc != nil { response.Documentation = doc + } else if product.IsPackage && len(response.PackageItems) > 0 { + // 如果是组合包且没有自己的文档,尝试合并子产品的文档 + mergedDoc := s.mergePackageItemsDocumentation(ctx, product.Code, response.PackageItems) + if mergedDoc != nil { + response.Documentation = mergedDoc + } } } @@ -585,4 +593,594 @@ func (s *ProductApplicationServiceImpl) DeleteProductApiConfig(ctx context.Conte return s.productApiConfigAppService.DeleteProductApiConfig(ctx, configID) } +// mergePackageItemsDocumentation 合并组合包子产品的文档 +// packageCode: 组合包的产品编号,用于生成请求地址 +func (s *ProductApplicationServiceImpl) mergePackageItemsDocumentation(ctx context.Context, packageCode string, packageItems []*responses.PackageItemResponse) *responses.DocumentationResponse { + if len(packageItems) == 0 { + return nil + } + // 收集所有子产品的ID + productIDs := make([]string, 0, len(packageItems)) + for _, item := range packageItems { + productIDs = append(productIDs, item.ProductID) + } + + // 批量获取子产品的文档 + docs, err := s.documentationAppService.GetDocumentationsByProductIDs(ctx, productIDs) + if err != nil || len(docs) == 0 { + s.logger.Debug("组合包子产品文档获取失败或为空", zap.Error(err)) + return nil + } + + // 创建文档映射,方便按产品ID查找 + docMap := make(map[string]*responses.DocumentationResponse) + for i := range docs { + docMap[docs[i].ProductID] = &docs[i] + } + + // 合并文档内容 + mergedDoc := &responses.DocumentationResponse{ + ProductID: packageItems[0].ProductID, // 使用第一个子产品的ID作为标识 + RequestMethod: "POST", // 默认方法 + Version: "1.0", + } + + // 收集子产品的文档数据,用于构建组合包响应结构 + subProductDocs := make([]subProductDocInfo, 0) + + // 按packageItems的顺序合并文档 + for _, item := range packageItems { + doc, exists := docMap[item.ProductID] + if !exists || doc == nil { + continue + } + + // 保存子产品文档信息,用于构建组合包响应结构 + subProductDocs = append(subProductDocs, subProductDocInfo{ + item: item, + doc: doc, + responseFields: doc.ResponseFields, + responseExample: doc.ResponseExample, + }) + } + + // 构建组合包的返回字段说明 + if len(subProductDocs) > 0 { + mergedDoc.ResponseFields = s.buildCombPackageResponseFields(subProductDocs) + } + + // 构建组合包的响应示例 + if len(subProductDocs) > 0 { + mergedDoc.ResponseExample = s.buildCombPackageResponseExample(subProductDocs) + } + + // 合并请求参数(从DTO结构体中提取所有子产品的参数,去重后生成统一文档) + if len(packageItems) > 0 { + mergedDoc.RequestParams = s.mergeRequestParamsFromDTOs(ctx, packageItems) + } + + // 设置请求地址和方法(使用组合包的产品编号) + mergedDoc.RequestURL = fmt.Sprintf("https://api.tianyuanapi.com/api/v1/%s?t=13位时间戳", packageCode) + mergedDoc.RequestMethod = "POST" + + // 错误代码和请求方式部分设置为空,前端会显示默认内容 + mergedDoc.ErrorCodes = "" + mergedDoc.BasicInfo = "" + + // 如果没有任何有意义的内容(请求参数、返回字段、响应示例都没有),返回nil + if mergedDoc.RequestParams == "" && mergedDoc.ResponseFields == "" && mergedDoc.ResponseExample == "" { + return nil + } + + return mergedDoc +} + +// joinParts 使用分隔符连接文档部分 +func joinParts(parts []string) string { + if len(parts) == 0 { + return "" + } + if len(parts) == 1 { + return parts[0] + } + + // 使用分隔线分隔不同产品的文档 + return strings.Join(parts, "\n\n---\n\n") +} + +// subProductDocInfo 子产品文档信息 +type subProductDocInfo struct { + item *responses.PackageItemResponse + doc *responses.DocumentationResponse + responseFields string + responseExample string +} + +// buildCombPackageResponseFields 构建组合包的返回字段说明 +func (s *ProductApplicationServiceImpl) buildCombPackageResponseFields(subProductDocs []subProductDocInfo) string { + var builder strings.Builder + + // 组合包响应结构说明 + builder.WriteString("## 组合包响应结构\n\n") + builder.WriteString("组合包API返回的是一个包含所有子产品响应结果的数组。\n\n") + builder.WriteString("| 字段名 | 类型 | 说明 |\n") + builder.WriteString("|--------|------|------|\n") + builder.WriteString("| responses | array | 子产品响应列表 |\n") + builder.WriteString("| responses[].api_code | string | 子产品代码 |\n") + builder.WriteString("| responses[].success | boolean | 该子产品调用是否成功 |\n") + builder.WriteString("| responses[].data | object/null | 子产品的响应数据(成功时返回数据,失败时为null) |\n") + builder.WriteString("| responses[].error | string | 错误信息(仅在失败时返回) |\n\n") + + // 各个子产品的data字段说明 + builder.WriteString("## 子产品响应数据说明\n\n") + for i, spDoc := range subProductDocs { + if i > 0 { + builder.WriteString("\n---\n\n") + } + productTitle := fmt.Sprintf("### %s (%s) 的 data 字段", spDoc.item.ProductName, spDoc.item.ProductCode) + builder.WriteString(productTitle + "\n\n") + if spDoc.responseFields != "" { + // 移除子产品文档中可能存在的标题,只保留字段说明 + fields := spDoc.responseFields + // 如果包含标题,尝试提取data字段相关的部分 + if strings.Contains(fields, "data") || strings.Contains(fields, "返回字段") { + builder.WriteString(fields) + } else { + builder.WriteString(fields) + } + } else { + builder.WriteString("该子产品的 data 字段结构请参考该产品的单独文档。") + } + builder.WriteString("\n") + } + + return builder.String() +} + +// buildCombPackageResponseExample 构建组合包的响应示例 +func (s *ProductApplicationServiceImpl) buildCombPackageResponseExample(subProductDocs []subProductDocInfo) string { + var builder strings.Builder + + builder.WriteString("## 组合包响应示例\n\n") + builder.WriteString("### 成功响应(部分子产品成功)\n\n") + builder.WriteString("```json\n") + builder.WriteString("{\n") + builder.WriteString(" \"responses\": [\n") + + // 构建示例JSON - 第一个成功,第二个可能失败 + for i, spDoc := range subProductDocs { + if i > 0 { + builder.WriteString(",\n") + } + builder.WriteString(" {\n") + builder.WriteString(fmt.Sprintf(" \"api_code\": \"%s\",\n", spDoc.item.ProductCode)) + + // 第一个示例显示成功,其他可能显示部分失败 + if i == 0 { + builder.WriteString(" \"success\": true,\n") + builder.WriteString(" \"data\": {\n") + builder.WriteString(fmt.Sprintf(" // %s 的实际响应数据\n", spDoc.item.ProductName)) + builder.WriteString(" // 具体字段结构请参考下方该子产品的详细响应示例\n") + builder.WriteString(" }\n") + } else { + // 第二个可能成功也可能失败,展示成功的情况 + builder.WriteString(" \"success\": true,\n") + builder.WriteString(" \"data\": {}\n") + } + builder.WriteString(" }") + } + + builder.WriteString("\n ]\n") + builder.WriteString("}\n") + builder.WriteString("```\n\n") + + // 添加失败示例 + builder.WriteString("### 部分失败响应示例\n\n") + builder.WriteString("```json\n") + builder.WriteString("{\n") + builder.WriteString(" \"responses\": [\n") + builder.WriteString(" {\n") + if len(subProductDocs) > 0 { + builder.WriteString(fmt.Sprintf(" \"api_code\": \"%s\",\n", subProductDocs[0].item.ProductCode)) + } + builder.WriteString(" \"success\": false,\n") + builder.WriteString(" \"data\": null,\n") + builder.WriteString(" \"error\": \"参数校验不正确\"\n") + builder.WriteString(" }") + if len(subProductDocs) > 1 { + builder.WriteString(",\n") + builder.WriteString(" {\n") + builder.WriteString(fmt.Sprintf(" \"api_code\": \"%s\",\n", subProductDocs[1].item.ProductCode)) + builder.WriteString(" \"success\": true,\n") + builder.WriteString(" \"data\": {}\n") + builder.WriteString(" }") + } + builder.WriteString("\n ]\n") + builder.WriteString("}\n") + builder.WriteString("```\n\n") + + // 添加各子产品的详细响应示例说明 + if len(subProductDocs) > 0 { + builder.WriteString("## 各子产品详细响应示例\n\n") + builder.WriteString("以下为各个子产品的详细响应示例,组合包响应中的 `data` 字段即为对应子产品的完整响应数据。\n\n") + + for i, spDoc := range subProductDocs { + if i > 0 { + builder.WriteString("\n---\n\n") + } + productTitle := fmt.Sprintf("### %s (%s)", spDoc.item.ProductName, spDoc.item.ProductCode) + builder.WriteString(productTitle + "\n\n") + if spDoc.responseExample != "" { + builder.WriteString(spDoc.responseExample) + } else { + builder.WriteString("该子产品的响应示例请参考该产品的单独文档。") + } + builder.WriteString("\n") + } + } + + return builder.String() +} + + + + + +// paramField 参数字段信息 +type paramField struct { + Name string + Type string + Required string + Description string +} + +// mergeRequestParamsFromDTOs 从DTO结构体中提取并合并组合包的请求参数 +func (s *ProductApplicationServiceImpl) mergeRequestParamsFromDTOs(ctx context.Context, packageItems []*responses.PackageItemResponse) string { + if len(packageItems) == 0 { + return "" + } + + // 用于存储所有参数字段,key为字段名(json tag) + paramMap := make(map[string]*paramField) + + // 获取DTO映射(复用form_config_service的逻辑) + dtoMap := s.getDTOMap() + + // 遍历每个子产品,从DTO中提取字段 + for _, item := range packageItems { + // 从DTO映射中获取子产品的DTO结构体 + if dtoStruct, exists := dtoMap[item.ProductCode]; exists { + // 通过反射解析DTO字段 + s.extractFieldsFromDTO(dtoStruct, paramMap) + } else { + // 如果没有找到DTO,尝试通过FormConfigService获取 + if s.formConfigService != nil { + formConfig, err := s.formConfigService.GetFormConfig(ctx, item.ProductCode) + if err == nil && formConfig != nil { + // 从表单配置中提取字段 + for _, field := range formConfig.Fields { + if _, exists := paramMap[field.Name]; !exists { + requiredStr := "否" + if field.Required { + requiredStr = "是" + } + paramMap[field.Name] = ¶mField{ + Name: field.Name, + Type: s.mapFieldTypeToDocType(field.Type), + Required: requiredStr, + Description: field.Description, + } + } else { + // 如果字段已存在,保留更详细的描述,如果新字段是必填则更新 + existing := paramMap[field.Name] + if field.Description != "" && existing.Description == "" { + existing.Description = field.Description + } + if field.Required && existing.Required != "是" { + existing.Required = "是" + } + } + } + } + } + } + } + + // 如果没有提取到任何参数,返回空字符串 + if len(paramMap) == 0 { + return "" + } + + // 构建合并后的请求参数文档 + var result strings.Builder + result.WriteString("## 请求参数\n\n") + + // 构建JSON示例 + result.WriteString("```json\n{\n") + first := true + for fieldName := range paramMap { + if !first { + result.WriteString(",\n") + } + result.WriteString(fmt.Sprintf(` "%s": "string"`, fieldName)) + first = false + } + result.WriteString("\n}\n```\n\n") + + // 构建表格 + result.WriteString("| 字段名 | 类型 | 必填 | 描述 |\n") + result.WriteString("|--------|------|------|------|\n") + + // 按字段名排序输出 + fieldNames := make([]string, 0, len(paramMap)) + for fieldName := range paramMap { + fieldNames = append(fieldNames, fieldName) + } + // 简单排序 + for i := 0; i < len(fieldNames)-1; i++ { + for j := i + 1; j < len(fieldNames); j++ { + if fieldNames[i] > fieldNames[j] { + fieldNames[i], fieldNames[j] = fieldNames[j], fieldNames[i] + } + } + } + + for _, fieldName := range fieldNames { + field := paramMap[fieldName] + result.WriteString(fmt.Sprintf("| %s | %s | %s | %s |\n", + field.Name, field.Type, field.Required, field.Description)) + } + + result.WriteString("\n通过加密后得到 Base64 字符串,将其放入到请求体中,字段名为 `data`。\n\n") + result.WriteString("```json\n{\n \"data\": \"xxxx(base64)\"\n}\n```") + + return result.String() +} + +// getDTOMap 获取API代码到DTO结构体的映射(复用form_config_service的逻辑) +func (s *ProductApplicationServiceImpl) getDTOMap() map[string]interface{} { + return map[string]interface{}{ + "IVYZ9363": &dto.IVYZ9363Req{}, + "IVYZ385E": &dto.IVYZ385EReq{}, + "IVYZ5733": &dto.IVYZ5733Req{}, + "FLXG3D56": &dto.FLXG3D56Req{}, + "FLXG75FE": &dto.FLXG75FEReq{}, + "FLXG0V3B": &dto.FLXG0V3BReq{}, + "FLXG0V4B": &dto.FLXG0V4BReq{}, + "FLXG54F5": &dto.FLXG54F5Req{}, + "FLXG162A": &dto.FLXG162AReq{}, + "FLXG0687": &dto.FLXG0687Req{}, + "FLXGBC21": &dto.FLXGBC21Req{}, + "FLXG970F": &dto.FLXG970FReq{}, + "FLXG5876": &dto.FLXG5876Req{}, + "FLXG9687": &dto.FLXG9687Req{}, + "FLXGC9D1": &dto.FLXGC9D1Req{}, + "FLXGCA3D": &dto.FLXGCA3DReq{}, + "FLXGDEC7": &dto.FLXGDEC7Req{}, + "JRZQ0A03": &dto.JRZQ0A03Req{}, + "JRZQ4AA8": &dto.JRZQ4AA8Req{}, + "JRZQ8203": &dto.JRZQ8203Req{}, + "JRZQDBCE": &dto.JRZQDBCEReq{}, + "QYGL2ACD": &dto.QYGL2ACDReq{}, + "QYGL6F2D": &dto.QYGL6F2DReq{}, + "QYGL45BD": &dto.QYGL45BDReq{}, + "QYGL8261": &dto.QYGL8261Req{}, + "QYGL8271": &dto.QYGL8271Req{}, + "QYGLB4C0": &dto.QYGLB4C0Req{}, + "QYGL23T7": &dto.QYGL23T7Req{}, + "QYGL5A3C": &dto.QYGL5A3CReq{}, + "QYGL8B4D": &dto.QYGL8B4DReq{}, + "QYGL9E2F": &dto.QYGL9E2FReq{}, + "QYGL7C1A": &dto.QYGL7C1AReq{}, + "QYGL3F8E": &dto.QYGL3F8EReq{}, + "YYSY4B37": &dto.YYSY4B37Req{}, + "YYSY4B21": &dto.YYSY4B21Req{}, + "YYSY6F2E": &dto.YYSY6F2EReq{}, + "YYSY09CD": &dto.YYSY09CDReq{}, + "IVYZ0b03": &dto.IVYZ0b03Req{}, + "YYSYBE08": &dto.YYSYBE08Req{}, + "YYSYD50F": &dto.YYSYD50FReq{}, + "YYSYF7DB": &dto.YYSYF7DBReq{}, + "IVYZ9A2B": &dto.IVYZ9A2BReq{}, + "IVYZ7F2A": &dto.IVYZ7F2AReq{}, + "IVYZ4E8B": &dto.IVYZ4E8BReq{}, + "IVYZ1C9D": &dto.IVYZ1C9DReq{}, + "IVYZGZ08": &dto.IVYZGZ08Req{}, + "FLXG8A3F": &dto.FLXG8A3FReq{}, + "FLXG5B2E": &dto.FLXG5B2EReq{}, + "COMB298Y": &dto.COMB298YReq{}, + "COMB86PM": &dto.COMB86PMReq{}, + "QCXG7A2B": &dto.QCXG7A2BReq{}, + "COMENT01": &dto.COMENT01Req{}, + "JRZQ09J8": &dto.JRZQ09J8Req{}, + "FLXGDEA8": &dto.FLXGDEA8Req{}, + "FLXGDEA9": &dto.FLXGDEA9Req{}, + "JRZQ1D09": &dto.JRZQ1D09Req{}, + "IVYZ2A8B": &dto.IVYZ2A8BReq{}, + "IVYZ7C9D": &dto.IVYZ7C9DReq{}, + "IVYZ5E3F": &dto.IVYZ5E3FReq{}, + "YYSY4F2E": &dto.YYSY4F2EReq{}, + "YYSY8B1C": &dto.YYSY8B1CReq{}, + "YYSY6D9A": &dto.YYSY6D9AReq{}, + "YYSY3E7F": &dto.YYSY3E7FReq{}, + "FLXG5A3B": &dto.FLXG5A3BReq{}, + "FLXG9C1D": &dto.FLXG9C1DReq{}, + "FLXG2E8F": &dto.FLXG2E8FReq{}, + "JRZQ3C7B": &dto.JRZQ3C7BReq{}, + "JRZQ8A2D": &dto.JRZQ8A2DReq{}, + "JRZQ5E9F": &dto.JRZQ5E9FReq{}, + "JRZQ4B6C": &dto.JRZQ4B6CReq{}, + "JRZQ7F1A": &dto.JRZQ7F1AReq{}, + "DWBG6A2C": &dto.DWBG6A2CReq{}, + "DWBG8B4D": &dto.DWBG8B4DReq{}, + "FLXG8B4D": &dto.FLXG8B4DReq{}, + "IVYZ81NC": &dto.IVYZ81NCReq{}, + "IVYZ7F3A": &dto.IVYZ7F3AReq{}, + "IVYZ3A7F": &dto.IVYZ3A7FReq{}, + "IVYZ9D2E": &dto.IVYZ9D2EReq{}, + "DWBG7F3A": &dto.DWBG7F3AReq{}, + "YYSY8F3A": &dto.YYSY8F3AReq{}, + "QCXG9P1C": &dto.QCXG9P1CReq{}, + "JRZQ9E2A": &dto.JRZQ9E2AReq{}, + "YYSY9A1B": &dto.YYSY9A1BReq{}, + "YYSY8C2D": &dto.YYSY8C2DReq{}, + "YYSY7D3E": &dto.YYSY7D3EReq{}, + "YYSY9E4A": &dto.YYSY9E4AReq{}, + "JRZQ6F2A": &dto.JRZQ6F2AReq{}, + "JRZQ8B3C": &dto.JRZQ8B3CReq{}, + "JRZQ9D4E": &dto.JRZQ9D4EReq{}, + "FLXG7E8F": &dto.FLXG7E8FReq{}, + "QYGL5F6A": &dto.QYGL5F6AReq{}, + "IVYZ6G7H": &dto.IVYZ6G7HReq{}, + "IVYZ8I9J": &dto.IVYZ8I9JReq{}, + "JRZQ0L85": &dto.JRZQ0L85Req{}, + } +} + +// extractFieldsFromDTO 从DTO结构体中提取字段信息 +func (s *ProductApplicationServiceImpl) extractFieldsFromDTO(dtoStruct interface{}, paramMap map[string]*paramField) { + if dtoStruct == nil { + return + } + + t := reflect.TypeOf(dtoStruct).Elem() + if t == nil { + return + } + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + // 获取JSON标签 + jsonTag := field.Tag.Get("json") + if jsonTag == "" || jsonTag == "-" { + continue + } + + // 去除omitempty等选项 + jsonTag = strings.Split(jsonTag, ",")[0] + + // 跳过加密相关的字段(不应该出现在业务参数文档中) + if jsonTag == "data" || jsonTag == "encrypt" { + continue + } + + // 获取验证标签 + validateTag := field.Tag.Get("validate") + required := strings.Contains(validateTag, "required") + requiredStr := "否" + if required { + requiredStr = "是" + } + + // 获取字段类型 + fieldType := s.getGoTypeName(field.Type) + + // 生成字段描述 + description := s.generateFieldDescription(jsonTag, validateTag) + + // 如果字段已存在,保留更详细的描述,如果新字段是必填则更新 + if existing, exists := paramMap[jsonTag]; exists { + if description != "" && existing.Description == "" { + existing.Description = description + } + if required && existing.Required != "是" { + existing.Required = "是" + } + } else { + paramMap[jsonTag] = ¶mField{ + Name: jsonTag, + Type: fieldType, + Required: requiredStr, + Description: description, + } + } + } +} + +// getGoTypeName 获取Go类型的名称(转换为文档中的类型描述) +func (s *ProductApplicationServiceImpl) getGoTypeName(fieldType reflect.Type) string { + switch fieldType.Kind() { + case reflect.String: + return "string" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return "int" + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return "int" + case reflect.Float32, reflect.Float64: + return "float" + case reflect.Bool: + return "boolean" + case reflect.Slice, reflect.Array: + return "array" + case reflect.Map: + return "object" + default: + return "string" + } +} + +// generateFieldDescription 生成字段描述 +func (s *ProductApplicationServiceImpl) generateFieldDescription(jsonTag string, validateTag string) string { + // 基础字段描述映射 + descMap := map[string]string{ + "mobile_no": "手机号码(11位)", + "id_card": "身份证号码(18位)", + "name": "姓名", + "man_name": "男方姓名", + "woman_name": "女方姓名", + "man_id_card": "男方身份证号码", + "woman_id_card": "女方身份证号码", + "ent_name": "企业名称", + "legal_person": "法人姓名", + "ent_code": "统一社会信用代码", + "auth_date": "授权日期范围(格式:YYYYMMDD-YYYYMMDD)", + "time_range": "时间范围(格式:HH:MM-HH:MM)", + "authorized": "是否授权(0-未授权,1-已授权)", + "authorization_url": "授权书URL地址(支持格式:pdf/jpg/jpeg/png/bmp)", + "unique_id": "唯一标识", + "return_url": "返回链接", + "mobile_type": "手机类型", + "start_date": "开始日期", + "years": "查询年数(0-100)", + "bank_card": "银行卡号", + "user_type": "关系类型(1-ETC开户人;2-车辆所有人;3-ETC经办人)", + "vehicle_type": "车辆类型(0-客车;1-货车;2-全部)", + "page_num": "页码(从1开始)", + "page_size": "每页数量(1-100)", + "use_scenario": "使用场景(1-信贷审核;2-保险评估;3-招聘背景调查;4-其他业务场景;99-其他)", + "wo": "我的", + } + + if desc, exists := descMap[jsonTag]; exists { + return desc + } + + // 如果没有预定义,根据验证规则生成描述 + if strings.Contains(validateTag, "required") { + return "必填字段" + } + + return "" +} + +// mapFieldTypeToDocType 将前端字段类型映射为文档类型 +func (s *ProductApplicationServiceImpl) mapFieldTypeToDocType(frontendType string) string { + switch frontendType { + case "tel", "text": + return "string" + case "number": + return "int" + case "checkbox": + return "boolean" + case "select": + return "string" + case "date": + return "string" + case "url": + return "string" + default: + return "string" + } +} \ No newline at end of file diff --git a/internal/container/container.go b/internal/container/container.go index 4734b6f..03e6cf2 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -875,7 +875,23 @@ func NewContainer() *Container { ), // 产品应用服务 - 绑定到接口 fx.Annotate( - product.NewProductApplicationService, + func( + productManagementService *product_services.ProductManagementService, + productSubscriptionService *product_services.ProductSubscriptionService, + productApiConfigAppService product.ProductApiConfigApplicationService, + documentationAppService product.DocumentationApplicationServiceInterface, + formConfigService api_services.FormConfigService, + logger *zap.Logger, + ) product.ProductApplicationService { + return product.NewProductApplicationService( + productManagementService, + productSubscriptionService, + productApiConfigAppService, + documentationAppService, + formConfigService, + logger, + ) + }, fx.As(new(product.ProductApplicationService)), ), // 产品API配置应用服务 - 绑定到接口 diff --git a/internal/infrastructure/http/handlers/product_handler.go b/internal/infrastructure/http/handlers/product_handler.go index 7ca63e1..690ebba 100644 --- a/internal/infrastructure/http/handlers/product_handler.go +++ b/internal/infrastructure/http/handlers/product_handler.go @@ -176,7 +176,7 @@ func (h *ProductHandler) getCurrentUserID(c *gin.Context) string { // GetProductDetail 获取产品详情 // @Summary 获取产品详情 -// @Description 获取产品详细信息,用户端只能查看可见的产品 +// @Description 获取产品详细信息,详情接口不受 is_visible 字段影响,可通过直接访问查看任何产品 // @Tags 数据大厅 // @Accept json // @Produce json @@ -184,7 +184,7 @@ func (h *ProductHandler) getCurrentUserID(c *gin.Context) string { // @Param with_document query bool false "是否包含文档信息" // @Success 200 {object} responses.ProductInfoWithDocumentResponse "获取产品详情成功" // @Failure 400 {object} map[string]interface{} "请求参数错误" -// @Failure 404 {object} map[string]interface{} "产品不存在或不可见" +// @Failure 404 {object} map[string]interface{} "产品不存在" // @Failure 500 {object} map[string]interface{} "服务器内部错误" // @Router /api/v1/products/{id} [get] func (h *ProductHandler) GetProductDetail(c *gin.Context) { @@ -205,7 +205,7 @@ func (h *ProductHandler) GetProductDetail(c *gin.Context) { result, err := h.appService.GetProductByIDForUser(c.Request.Context(), &query) if err != nil { h.logger.Error("获取产品详情失败", zap.Error(err)) - h.responseBuilder.NotFound(c, "产品不存在或不可见") + h.responseBuilder.NotFound(c, "产品不存在") return }