From 5cee8ff0352016732e322c9f861d1f1d509f686b Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Wed, 27 May 2026 13:00:51 +0800 Subject: [PATCH] f --- .../product/product_application_service.go | 3 + .../product_application_service_impl.go | 370 +++++++++++------- internal/container/container.go | 2 + internal/domains/api/dto/api_request_dto.go | 18 + .../api/services/api_request_service.go | 4 +- .../api/services/form_config_service.go | 3 + .../processors/qygl/qygl2ysb_processor.go | 47 +++ .../processors/qygl/qygl3ysb_processor.go | 48 +++ .../processors/qygl/qygl4yab_processor.go | 49 +++ .../services/product_management_service.go | 43 ++ .../product/gorm_product_repository.go | 32 +- .../http/handlers/product_admin_handler.go | 51 +++ .../http/routes/product_admin_routes.go | 3 + internal/shared/export/export.go | 33 +- internal/shared/services/export_service.go | 9 +- 15 files changed, 569 insertions(+), 146 deletions(-) create mode 100644 internal/domains/api/services/processors/qygl/qygl2ysb_processor.go create mode 100644 internal/domains/api/services/processors/qygl/qygl3ysb_processor.go create mode 100644 internal/domains/api/services/processors/qygl/qygl4yab_processor.go diff --git a/internal/application/product/product_application_service.go b/internal/application/product/product_application_service.go index 6b3c54d..0682407 100644 --- a/internal/application/product/product_application_service.go +++ b/internal/application/product/product_application_service.go @@ -47,4 +47,7 @@ type ProductApplicationService interface { CreateProductApiConfig(ctx context.Context, productID string, config *responses.ProductApiConfigResponse) error UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error DeleteProductApiConfig(ctx context.Context, configID string) error + + // 产品字典导出 + ExportProductDictionary(ctx context.Context, format string) ([]byte, error) } diff --git a/internal/application/product/product_application_service_impl.go b/internal/application/product/product_application_service_impl.go index 25ae9ec..3d0cb56 100644 --- a/internal/application/product/product_application_service_impl.go +++ b/internal/application/product/product_application_service_impl.go @@ -16,6 +16,7 @@ import ( 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/export" "tyapi-server/internal/shared/interfaces" ) @@ -28,6 +29,7 @@ type ProductApplicationServiceImpl struct { documentationAppService DocumentationApplicationServiceInterface formConfigService api_services.FormConfigService logger *zap.Logger + exportManager *export.ExportManager } // NewProductApplicationService 创建产品应用服务 @@ -38,6 +40,7 @@ func NewProductApplicationService( documentationAppService DocumentationApplicationServiceInterface, formConfigService api_services.FormConfigService, logger *zap.Logger, + exportManager *export.ExportManager, ) ProductApplicationService { return &ProductApplicationServiceImpl{ productManagementService: productManagementService, @@ -46,6 +49,7 @@ func NewProductApplicationService( documentationAppService: documentationAppService, formConfigService: formConfigService, logger: logger, + exportManager: exportManager, } } @@ -492,24 +496,24 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex // convertToProductInfoResponse 转换为产品信息响应 func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse { response := &responses.ProductInfoResponse{ - ID: product.ID, - OldID: product.OldID, - Name: product.Name, - Code: product.Code, - Description: product.Description, - Content: product.Content, - CategoryID: product.CategoryID, - SubCategoryID: product.SubCategoryID, - Price: product.Price.InexactFloat64(), - IsEnabled: product.IsEnabled, - IsPackage: product.IsPackage, - SellUIComponent: product.SellUIComponent, + ID: product.ID, + OldID: product.OldID, + Name: product.Name, + Code: product.Code, + Description: product.Description, + Content: product.Content, + CategoryID: product.CategoryID, + SubCategoryID: product.SubCategoryID, + Price: product.Price.InexactFloat64(), + IsEnabled: product.IsEnabled, + IsPackage: product.IsPackage, + SellUIComponent: product.SellUIComponent, UIComponentPrice: product.UIComponentPrice.InexactFloat64(), - SEOTitle: product.SEOTitle, - SEODescription: product.SEODescription, - SEOKeywords: product.SEOKeywords, - CreatedAt: product.CreatedAt, - UpdatedAt: product.UpdatedAt, + SEOTitle: product.SEOTitle, + SEODescription: product.SEODescription, + SEOKeywords: product.SEOKeywords, + CreatedAt: product.CreatedAt, + UpdatedAt: product.UpdatedAt, } // 添加一级分类信息 @@ -544,27 +548,27 @@ func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *en // convertToProductAdminInfoResponse 转换为管理员产品信息响应 func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse { response := &responses.ProductAdminInfoResponse{ - ID: product.ID, - OldID: product.OldID, - Name: product.Name, - Code: product.Code, - Description: product.Description, - Content: product.Content, - CategoryID: product.CategoryID, - SubCategoryID: product.SubCategoryID, - Price: product.Price.InexactFloat64(), - CostPrice: product.CostPrice.InexactFloat64(), - Remark: product.Remark, - IsEnabled: product.IsEnabled, - IsVisible: product.IsVisible, // 管理员可以看到可见状态 - IsPackage: product.IsPackage, - SellUIComponent: product.SellUIComponent, + ID: product.ID, + OldID: product.OldID, + Name: product.Name, + Code: product.Code, + Description: product.Description, + Content: product.Content, + CategoryID: product.CategoryID, + SubCategoryID: product.SubCategoryID, + Price: product.Price.InexactFloat64(), + CostPrice: product.CostPrice.InexactFloat64(), + Remark: product.Remark, + IsEnabled: product.IsEnabled, + IsVisible: product.IsVisible, // 管理员可以看到可见状态 + IsPackage: product.IsPackage, + SellUIComponent: product.SellUIComponent, UIComponentPrice: product.UIComponentPrice.InexactFloat64(), - SEOTitle: product.SEOTitle, - SEODescription: product.SEODescription, - SEOKeywords: product.SEOKeywords, - CreatedAt: product.CreatedAt, - UpdatedAt: product.UpdatedAt, + SEOTitle: product.SEOTitle, + SEODescription: product.SEODescription, + SEOKeywords: product.SEOKeywords, + CreatedAt: product.CreatedAt, + UpdatedAt: product.UpdatedAt, } // 添加一级分类信息 @@ -994,103 +998,103 @@ func (s *ProductApplicationServiceImpl) mergeRequestParamsFromDTOs(ctx context.C // 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{}, - "JRZQDCBE": &dto.JRZQDCBEReq{}, - "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{}, + "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{}, + "JRZQDCBE": &dto.JRZQDCBEReq{}, + "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{}, "YYSYBE08TEST": &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{}, - "IVYZ2MN6": &dto.IVYZ2MN6Req{}, - "IVYZ7F3A": &dto.IVYZ7F3AReq{}, - "IVYZ3P9M": &dto.IVYZ3P9MReq{}, - "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{}, + "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{}, + "IVYZ2MN6": &dto.IVYZ2MN6Req{}, + "IVYZ7F3A": &dto.IVYZ7F3AReq{}, + "IVYZ3P9M": &dto.IVYZ3P9MReq{}, + "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{}, } } @@ -1240,3 +1244,105 @@ func (s *ProductApplicationServiceImpl) mapFieldTypeToDocType(frontendType strin return "string" } } + +// ExportProductDictionary 导出产品字典 +func (s *ProductApplicationServiceImpl) ExportProductDictionary(ctx context.Context, format string) ([]byte, error) { + // 查询所有启用且可见的产品及其分类信息 + products, err := s.productManagementService.GetAllProductsForDictionary(ctx) + if err != nil { + s.logger.Error("获取产品字典数据失败", zap.Error(err)) + return nil, err + } + + if len(products) == 0 { + return nil, fmt.Errorf("没有找到符合条件的产品数据") + } + + // 按分类分组整理数据 + categoryGroups := make(map[string][]map[string]interface{}) + categoryOrder := []string{} // 保持分类顺序 + + for _, product := range products { + // 获取分类名称 + categoryName := "未分类" + if product.Category != nil { + categoryName = product.Category.Name + // 如果有二级分类,添加到分类名称中 + if product.SubCategory != nil && product.SubCategory.Name != "" { + categoryName = categoryName + " / " + product.SubCategory.Name + } + } + + // 如果分类不存在,初始化并添加到顺序列表 + if _, exists := categoryGroups[categoryName]; !exists { + categoryGroups[categoryName] = []map[string]interface{}{} + categoryOrder = append(categoryOrder, categoryName) + } + + // 添加产品到对应分类 + productInfo := map[string]interface{}{ + "category": categoryName, + "product_code": product.Code, + "product_name": product.Name, + "description": product.Description, + } + categoryGroups[categoryName] = append(categoryGroups[categoryName], productInfo) + } + + // 准备导出数据 + headers := []string{"分类", "产品编码", "产品名称", "产品简介"} + columnWidths := []float64{25, 15, 20, 40} + + // 构建数据行 + var data [][]interface{} + for _, categoryName := range categoryOrder { + productsInCategory := categoryGroups[categoryName] + for i, product := range productsInCategory { + // 只有每个分类的第一个产品才显示分类名称 + var categoryNameForRow interface{} + if i == 0 { + categoryNameForRow = product["category"] + } else { + categoryNameForRow = "" + } + + row := []interface{}{ + categoryNameForRow, + product["product_code"], + product["product_name"], + product["description"], + } + data = append(data, row) + } + } + + // 计算需要合并的行 + mergedRegions := [][]int{} + currentRow := 1 // 从第1行开始(第0行是表头) + for _, categoryName := range categoryOrder { + productsCount := len(categoryGroups[categoryName]) + if productsCount > 1 { + // 合并相同分类的单元格:从当前行开始,合并productsCount行,第0列 + // Excel格式:[startRow, startCol, endRow, endCol] + mergedRegions = append(mergedRegions, []int{ + currentRow, // startRow + 0, // startCol (分类列) + currentRow + productsCount - 1, // endRow + 0, // endCol + }) + } + currentRow += productsCount + } + + // 创建导出配置 + config := &export.ExportConfig{ + SheetName: "产品字典", + Headers: headers, + Data: data, + ColumnWidths: columnWidths, + MergedRegions: mergedRegions, + } + + // 使用导出管理器生成文件 + return s.exportManager.Export(ctx, config, format) +} diff --git a/internal/container/container.go b/internal/container/container.go index 0d9b43d..79e9f78 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -1026,6 +1026,7 @@ func NewContainer() *Container { documentationAppService product.DocumentationApplicationServiceInterface, formConfigService api_services.FormConfigService, logger *zap.Logger, + exportManager *export.ExportManager, ) product.ProductApplicationService { return product.NewProductApplicationService( productManagementService, @@ -1034,6 +1035,7 @@ func NewContainer() *Container { documentationAppService, formConfigService, logger, + exportManager, ) }, fx.As(new(product.ProductApplicationService)), diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 6bfa06c..c3fca07 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -488,6 +488,24 @@ type IVYZ2A8BReq struct { Authorized string `json:"authorized" validate:"required,oneof=0 1"` } +type QYGL4YABReq struct { + EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"` + EntCode string `json:"ent_code" validate:"required,validUSCI"` + IDCard string `json:"id_card" validate:"required,validIDCard"` + Name string `json:"name" validate:"required,min=1,validName"` +} + +type QYGL3YSBReq struct { + Name string `json:"name" validate:"required,min=1,validName"` + EntCode string `json:"ent_code" validate:"required,validUSCI"` + EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"` +} + +type QYGL2YSBReq struct { + EntCode string `json:"ent_code" validate:"required,validUSCI"` + Name string `json:"name" validate:"required,min=1,validName"` +} + type IVYZ7C9DReq struct { IDCard string `json:"id_card" validate:"required,validIDCard"` Name string `json:"name" validate:"required,min=1,validName"` diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index f47e630..a4f0f01 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -256,7 +256,9 @@ func registerAllProcessors(combService *comb.CombService) { "QYGL8848": qygl.ProcessQYGL8848Request, //企业税收违法核查 "QYGLDJ33": qygl.ProcessQYGLDJ33Request, //企业年报信息核验 "QYGLBH7Y": qygl.ProcessQYGLBH7YRequest, //企业涉诉案件查询汇博 - + "QYGL4YAB": qygl.ProcessQYGL4YABRequest, //企业四要素认证shumai + "QYGL3YSB": qygl.ProcessQYGL3YSBRequest, //企业三要素认证shumai + "QYGL2YSB": qygl.ProcessQYGL2YSBRequest, //企业二要素认证shumai // YYSY系列处理器 "YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖 "YYSYD50F": yysy.ProcessYYSYD50FRequest, diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 69cf966..5e447e3 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -283,6 +283,9 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "IVYZ2MN7": &dto.IVYZ2MN6Req{}, //学历Bzhicha "FLXGHB4F": &dto.FLXGHB4FReq{}, //个人涉诉案件查询汇博 "QYGLBH7Y": &dto.QYGLBH7YReq{}, //企业涉诉案件查询汇博 + "QYGL4YAB": &dto.QYGL4YABReq{}, //企业四要素认证shumai + "QYGL3YSB": &dto.QYGL3YSBReq{}, //企业三要素认证shumai + "QYGL2YSB": &dto.QYGL2YSBReq{}, //企业二要素认证shumai } // 优先返回已配置的DTO diff --git a/internal/domains/api/services/processors/qygl/qygl2ysb_processor.go b/internal/domains/api/services/processors/qygl/qygl2ysb_processor.go new file mode 100644 index 0000000..d293abd --- /dev/null +++ b/internal/domains/api/services/processors/qygl/qygl2ysb_processor.go @@ -0,0 +1,47 @@ +package qygl + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shumai" +) + +// ProcessQYGL2YSBRequest QYGL2YSB API处理方法 - 企业二要素认证 +func ProcessQYGL2YSBRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.QYGL2YSBReq + if err := json.Unmarshal(params, ¶msDto); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + if err := deps.Validator.ValidateStruct(paramsDto); err != nil { + return nil, errors.Join(processors.ErrInvalidParam, err) + } + reqFormData := map[string]interface{}{ + "legalPerson": paramsDto.Name, + "creditNo": paramsDto.EntCode, + } + apiPath := "/v4/company/two/check" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else if errors.Is(err, shumai.ErrNotFound) { + // 查无记录 + return nil, errors.Join(processors.ErrNotFound, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + + } + return respBytes, nil + +} diff --git a/internal/domains/api/services/processors/qygl/qygl3ysb_processor.go b/internal/domains/api/services/processors/qygl/qygl3ysb_processor.go new file mode 100644 index 0000000..b8bd31f --- /dev/null +++ b/internal/domains/api/services/processors/qygl/qygl3ysb_processor.go @@ -0,0 +1,48 @@ +package qygl + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shumai" +) + +// ProcessQYGL3YSBRequest QYGL3YSB API处理方法 - 企业三要素认证 +func ProcessQYGL3YSBRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.QYGL3YSBReq + if err := json.Unmarshal(params, ¶msDto); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + if err := deps.Validator.ValidateStruct(paramsDto); err != nil { + return nil, errors.Join(processors.ErrInvalidParam, err) + } + reqFormData := map[string]interface{}{ + "legalPerson": paramsDto.Name, + "companyName": paramsDto.EntName, + "creditNo": paramsDto.EntCode, + } + apiPath := "/v4/company-three/check" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else if errors.Is(err, shumai.ErrNotFound) { + // 查无记录 + return nil, errors.Join(processors.ErrNotFound, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + + } + return respBytes, nil + +} diff --git a/internal/domains/api/services/processors/qygl/qygl4yab_processor.go b/internal/domains/api/services/processors/qygl/qygl4yab_processor.go new file mode 100644 index 0000000..d5ecd5d --- /dev/null +++ b/internal/domains/api/services/processors/qygl/qygl4yab_processor.go @@ -0,0 +1,49 @@ +package qygl + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shumai" +) + +// ProcessQYGL4YABRequest QYGL4YAB API处理方法 - 企业四要素认证 +func ProcessQYGL4YABRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.QYGL4YABReq + if err := json.Unmarshal(params, ¶msDto); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + if err := deps.Validator.ValidateStruct(paramsDto); err != nil { + return nil, errors.Join(processors.ErrInvalidParam, err) + } + reqFormData := map[string]interface{}{ + "idCard": paramsDto.IDCard, + "legalPerson": paramsDto.Name, + "companyName": paramsDto.EntName, + "creditNo": paramsDto.EntCode, + } + apiPath := "/v4/company-four/check" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else if errors.Is(err, shumai.ErrNotFound) { + // 查无记录 + return nil, errors.Join(processors.ErrNotFound, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + + } + return respBytes, nil + +} diff --git a/internal/domains/product/services/product_management_service.go b/internal/domains/product/services/product_management_service.go index 9b784d7..37fccbc 100644 --- a/internal/domains/product/services/product_management_service.go +++ b/internal/domains/product/services/product_management_service.go @@ -435,3 +435,46 @@ func (s *ProductManagementService) ListProductsWithSubscriptionStatus(ctx contex return products, subscriptionStatusMap, total, nil } + +// GetAllProductsForDictionary 获取所有启用且可见的产品(用于导出产品字典) +func (s *ProductManagementService) GetAllProductsForDictionary(ctx context.Context) ([]*entities.Product, error) { + // 构建查询条件:启用且可见 + isEnabled := true + isVisible := true + + filters := map[string]interface{}{ + "is_enabled": isEnabled, + "is_visible": isVisible, + } + + options := interfaces.ListOptions{ + Page: 1, + PageSize: 1000, // 获取所有产品 + Sort: "sort", // 使用 products 表的 sort 字段,排序后再按分类分组 + Order: "asc", + } + + // 获取产品列表 + products, _, err := s.ListProducts(ctx, filters, options) + if err != nil { + s.logger.Error("获取产品字典数据失败", zap.Error(err)) + return nil, fmt.Errorf("获取产品字典数据失败: %w", err) + } + + // 预加载分类信息 + for _, product := range products { + if product.CategoryID != "" { + category, _ := s.categoryRepo.GetByID(ctx, product.CategoryID) + product.Category = &category + } + + if product.SubCategoryID != nil && *product.SubCategoryID != "" { + subCategory, err := s.subCategoryRepo.GetByID(ctx, *product.SubCategoryID) + if err == nil && subCategory != nil { + product.SubCategory = subCategory + } + } + } + + return products, nil +} diff --git a/internal/infrastructure/database/repositories/product/gorm_product_repository.go b/internal/infrastructure/database/repositories/product/gorm_product_repository.go index 685299d..c72a074 100644 --- a/internal/infrastructure/database/repositories/product/gorm_product_repository.go +++ b/internal/infrastructure/database/repositories/product/gorm_product_repository.go @@ -3,6 +3,8 @@ package repositories import ( "context" "errors" + "strings" + "tyapi-server/internal/domains/product/entities" "tyapi-server/internal/domains/product/repositories" "tyapi-server/internal/domains/product/repositories/queries" @@ -165,13 +167,33 @@ func (r *GormProductRepository) ListProducts(ctx context.Context, query *queries // 应用排序 if query.SortBy != "" { - order := query.SortBy - if query.SortOrder == "desc" { - order += " DESC" + // 检查是否是关联表字段排序 + if strings.Contains(query.SortBy, ".") { + parts := strings.Split(query.SortBy, ".") + if len(parts) == 2 { + // 关联表字段排序,需要JOIN表 + joinTable := parts[0] + sortField := parts[1] + dbQuery = dbQuery.Joins("JOIN "+joinTable+" ON products.category_id = "+joinTable+".id") + + order := joinTable + "." + sortField + if query.SortOrder == "desc" { + order += " DESC" + } else { + order += " ASC" + } + dbQuery = dbQuery.Order(order) + } } else { - order += " ASC" + // 本表字段排序 + order := query.SortBy + if query.SortOrder == "desc" { + order += " DESC" + } else { + order += " ASC" + } + dbQuery = dbQuery.Order(order) } - dbQuery = dbQuery.Order(order) } else { dbQuery = dbQuery.Order("created_at DESC") } diff --git a/internal/infrastructure/http/handlers/product_admin_handler.go b/internal/infrastructure/http/handlers/product_admin_handler.go index b49f6f5..b3d18d8 100644 --- a/internal/infrastructure/http/handlers/product_admin_handler.go +++ b/internal/infrastructure/http/handlers/product_admin_handler.go @@ -1659,3 +1659,54 @@ func (h *ProductAdminHandler) ExportAdminApiCalls(c *gin.Context) { c.Header("Content-Disposition", "attachment; filename="+filename) c.Data(200, contentType, fileData) } + +// ExportProductDictionary 导出产品字典 +// @Summary 导出产品字典 +// @Description 导出所有启用且可见的产品字典,按分类分组,左侧分类列合并单元格 +// @Tags 产品管理 +// @Accept json +// @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv +// @Security Bearer +// @Param format query string false "导出格式" Enums(excel, csv) default(excel) +// @Success 200 {file} file "导出文件" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/admin/products/export-dictionary [get] +func (h *ProductAdminHandler) ExportProductDictionary(c *gin.Context) { + // 获取导出格式,默认为excel + format := c.DefaultQuery("format", "excel") + if format != "excel" && format != "csv" { + h.responseBuilder.BadRequest(c, "不支持的导出格式") + return + } + + // 调用应用服务导出数据 + fileData, err := h.productAppService.ExportProductDictionary(c.Request.Context(), format) + if err != nil { + h.logger.Error("导出产品字典失败", zap.Error(err)) + + // 根据错误信息返回具体的提示 + errMsg := err.Error() + if strings.Contains(errMsg, "没有找到符合条件的产品数据") || strings.Contains(errMsg, "没有数据") { + h.responseBuilder.NotFound(c, "没有找到符合条件的产品数据,请确保有启用且可见的产品") + } else if strings.Contains(errMsg, "参数") || strings.Contains(errMsg, "参数错误") { + h.responseBuilder.BadRequest(c, errMsg) + } else { + h.responseBuilder.BadRequest(c, "导出产品字典失败:"+errMsg) + } + return + } + + // 设置响应头 + contentType := "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + filename := "产品字典.xlsx" + if format == "csv" { + contentType = "text/csv;charset=utf-8" + filename = "产品字典.csv" + } + + c.Header("Content-Type", contentType) + c.Header("Content-Disposition", "attachment; filename="+filename) + c.Data(200, contentType, fileData) +} diff --git a/internal/infrastructure/http/routes/product_admin_routes.go b/internal/infrastructure/http/routes/product_admin_routes.go index 6277daa..c9fa881 100644 --- a/internal/infrastructure/http/routes/product_admin_routes.go +++ b/internal/infrastructure/http/routes/product_admin_routes.go @@ -60,6 +60,9 @@ func (r *ProductAdminRoutes) Register(router *sharedhttp.GinRouter) { products.GET("/:id/documentation", r.handler.GetProductDocumentation) products.POST("/:id/documentation", r.handler.CreateOrUpdateProductDocumentation) products.DELETE("/:id/documentation", r.handler.DeleteProductDocumentation) + + // 产品字典导出 + products.GET("/export-dictionary", r.handler.ExportProductDictionary) } // 分类管理 diff --git a/internal/shared/export/export.go b/internal/shared/export/export.go index c363912..0554a0a 100644 --- a/internal/shared/export/export.go +++ b/internal/shared/export/export.go @@ -11,10 +11,11 @@ import ( // ExportConfig 定义了导出所需的配置 type ExportConfig struct { - SheetName string // 工作表名称 - Headers []string // 表头 - Data [][]interface{} // 导出数据 - ColumnWidths []float64 // 列宽 + SheetName string // 工作表名称 + Headers []string // 表头 + Data [][]interface{} // 导出数据 + ColumnWidths []float64 // 列宽 + MergedRegions [][]int // 合并单元格配置 [[startRow, startCol, endRow, endCol], ...] } // ExportManager 负责管理不同格式的导出 @@ -95,6 +96,30 @@ func (m *ExportManager) generateExcel(ctx context.Context, config *ExportConfig) } } + // 合并单元格 + if len(config.MergedRegions) > 0 { + for _, region := range config.MergedRegions { + startRow := region[0] + startCol := region[1] + endRow := region[2] + endCol := region[3] + + startCell, err := excelize.CoordinatesToCellName(startCol+1, startRow+1) + if err != nil { + return nil, fmt.Errorf("生成合并区域起始单元格坐标失败: %v", err) + } + endCell, err := excelize.CoordinatesToCellName(endCol+1, endRow+1) + if err != nil { + return nil, fmt.Errorf("生成合并区域结束单元格坐标失败: %v", err) + } + + err = f.MergeCell(sheetName, startCell, endCell) + if err != nil { + return nil, fmt.Errorf("合并单元格失败: %v", err) + } + } + } + // 设置列宽 for i, width := range config.ColumnWidths { col, err := excelize.ColumnNumberToName(i + 1) diff --git a/internal/shared/services/export_service.go b/internal/shared/services/export_service.go index abcc0c3..3d3229a 100644 --- a/internal/shared/services/export_service.go +++ b/internal/shared/services/export_service.go @@ -11,10 +11,11 @@ import ( // ExportConfig 定义了导出所需的配置 type ExportConfig struct { - SheetName string // 工作表名称 - Headers []string // 表头 - Data [][]interface{} // 导出数据 - ColumnWidths []float64 // 列宽 + SheetName string // 工作表名称 + Headers []string // 表头 + Data [][]interface{} // 导出数据 + ColumnWidths []float64 // 列宽 + MergedRegions [][]int // 合并单元格配置 [[startRow, startCol, endRow, endCol], ...] } // ExportManager 负责管理不同格式的导出