From 00a3f0f1e969ca8bbf2bfb8ec2b7664dd60adb02 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Thu, 13 Nov 2025 20:43:35 +0800 Subject: [PATCH] add new processor --- internal/app/app.go | 1 - .../product/dto/commands/product_commands.go | 4 + .../dto/responses/product_responses.go | 2 + .../product_application_service_impl.go | 6 + internal/domains/api/dto/api_request_dto.go | 31 + .../api/services/api_request_service.go | 5 + .../api/services/form_config_service.go | 19 + .../processors/jrzq/jrzq1e7b_processor.go | 63 ++ .../processors/jrzq/jrzq2f8a_processor.go | 63 ++ .../processors/qcxg/qcxg6b4e_processor.go | 46 ++ .../processors/qcxg/qcxg8a3d_processor.go | 50 ++ .../processors/qygl/qygl2b5c_processor.go | 63 ++ internal/domains/product/entities/product.go | 2 + .../services/product_management_service.go | 4 + scripts/analyze_processors.go | 603 ++++++++++++++++++ 15 files changed, 961 insertions(+), 1 deletion(-) create mode 100644 internal/domains/api/services/processors/jrzq/jrzq1e7b_processor.go create mode 100644 internal/domains/api/services/processors/jrzq/jrzq2f8a_processor.go create mode 100644 internal/domains/api/services/processors/qcxg/qcxg6b4e_processor.go create mode 100644 internal/domains/api/services/processors/qcxg/qcxg8a3d_processor.go create mode 100644 internal/domains/api/services/processors/qygl/qygl2b5c_processor.go create mode 100644 scripts/analyze_processors.go diff --git a/internal/app/app.go b/internal/app/app.go index a8c6631..bb7568e 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -102,7 +102,6 @@ func (a *Application) Run() error { // RunMigrations 运行数据库迁移 func (a *Application) RunMigrations() error { - return nil a.logger.Info("Running database migrations...") // 创建数据库连接 diff --git a/internal/application/product/dto/commands/product_commands.go b/internal/application/product/dto/commands/product_commands.go index fa1c57e..b85f082 100644 --- a/internal/application/product/dto/commands/product_commands.go +++ b/internal/application/product/dto/commands/product_commands.go @@ -8,6 +8,8 @@ type CreateProductCommand struct { Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"` CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"` Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"` + CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"` + Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"` IsEnabled bool `json:"is_enabled" comment:"是否启用"` IsVisible bool `json:"is_visible" comment:"是否展示"` IsPackage bool `json:"is_package" comment:"是否组合包"` @@ -27,6 +29,8 @@ type UpdateProductCommand struct { Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"` CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"` Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"` + CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"` + Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"` IsEnabled bool `json:"is_enabled" comment:"是否启用"` IsVisible bool `json:"is_visible" comment:"是否展示"` IsPackage bool `json:"is_package" comment:"是否组合包"` diff --git a/internal/application/product/dto/responses/product_responses.go b/internal/application/product/dto/responses/product_responses.go index 47b4702..bcc7a45 100644 --- a/internal/application/product/dto/responses/product_responses.go +++ b/internal/application/product/dto/responses/product_responses.go @@ -88,6 +88,8 @@ type ProductAdminInfoResponse struct { Content string `json:"content" comment:"产品内容"` CategoryID string `json:"category_id" comment:"产品分类ID"` Price float64 `json:"price" comment:"产品价格"` + CostPrice float64 `json:"cost_price" comment:"成本价"` + Remark string `json:"remark" comment:"备注"` IsEnabled bool `json:"is_enabled" comment:"是否启用"` IsVisible bool `json:"is_visible" comment:"是否可见"` IsPackage bool `json:"is_package" comment:"是否组合包"` diff --git a/internal/application/product/product_application_service_impl.go b/internal/application/product/product_application_service_impl.go index 0e0c422..bfc242b 100644 --- a/internal/application/product/product_application_service_impl.go +++ b/internal/application/product/product_application_service_impl.go @@ -60,6 +60,8 @@ func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd * Content: cmd.Content, CategoryID: cmd.CategoryID, Price: decimal.NewFromFloat(cmd.Price), + CostPrice: decimal.NewFromFloat(cmd.CostPrice), + Remark: cmd.Remark, IsEnabled: cmd.IsEnabled, IsVisible: cmd.IsVisible, IsPackage: cmd.IsPackage, @@ -89,6 +91,8 @@ func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd * existingProduct.Content = cmd.Content existingProduct.CategoryID = cmd.CategoryID existingProduct.Price = decimal.NewFromFloat(cmd.Price) + existingProduct.CostPrice = decimal.NewFromFloat(cmd.CostPrice) + existingProduct.Remark = cmd.Remark existingProduct.IsEnabled = cmd.IsEnabled existingProduct.IsVisible = cmd.IsVisible existingProduct.IsPackage = cmd.IsPackage @@ -528,6 +532,8 @@ func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(produc Content: product.Content, CategoryID: product.CategoryID, Price: product.Price.InexactFloat64(), + CostPrice: product.CostPrice.InexactFloat64(), + Remark: product.Remark, IsEnabled: product.IsEnabled, IsVisible: product.IsVisible, // 管理员可以看到可见状态 IsPackage: product.IsPackage, diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 444bce9..e0b7612 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -452,6 +452,37 @@ type QCXG9P1CReq struct { Authorized string `json:"authorized" validate:"required,oneof=0 1"` } +type QCXG8A3DReq struct { + PlateNo string `json:"plate_no" validate:"required"` + PlateType string `json:"plate_type" validate:"omitempty,oneof=01 02"` + Authorized string `json:"authorized" validate:"required,oneof=0 1"` +} + +type QCXG6B4EReq struct { + VINCode string `json:"vin_code" validate:"required"` + Authorized string `json:"authorized" validate:"required,oneof=0 1"` +} + +type QYGL2B5CReq struct { + EntName string `json:"ent_name" validate:"omitempty,min=1,validEnterpriseName"` + EntCode string `json:"ent_code" validate:"omitempty,validUSCI"` + Authorized string `json:"authorized" validate:"required,oneof=0 1"` +} + +type JRZQ2F8AReq struct { + Name string `json:"name" validate:"required,min=1,validName"` + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` + IDCard string `json:"id_card" validate:"required,validIDCard"` + Authorized string `json:"authorized" validate:"required,oneof=0 1"` +} + +type JRZQ1E7BReq struct { + Name string `json:"name" validate:"required,min=1,validName"` + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` + IDCard string `json:"id_card" validate:"required,validIDCard"` + Authorized string `json:"authorized" validate:"required,oneof=0 1"` +} + type JRZQ9E2AReq struct { MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` IDCard string `json:"id_card" validate:"required,validIDCard"` diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 0244ac2..4c58191 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -123,6 +123,8 @@ func registerAllProcessors(combService *comb.CombService) { "JRZQ8B3C": jrzq.ProcessJRZQ8B3CRequest, "JRZQ9D4E": jrzq.ProcessJRZQ9D4ERequest, "JRZQ0L85": jrzq.ProcessJRZQ0L85Request, + "JRZQ2F8A": jrzq.ProcessJRZQ2F8ARequest, + "JRZQ1E7B": jrzq.ProcessJRZQ1E7BRequest, // QYGL系列处理器 "QYGL8261": qygl.ProcessQYGL8261Request, @@ -141,6 +143,7 @@ func registerAllProcessors(combService *comb.CombService) { "QYGL4B2E": qygl.ProcessQYGL4B2ERequest, // 税收违法 "COMENT01": qygl.ProcessCOMENT01Request, // 企业风险报告 "QYGL5F6A": qygl.ProcessQYGL5F6ARequest, // 企业相关查询 + "QYGL2B5C": qygl.ProcessQYGL2B5CRequest, // 企业联系人实际经营地址 // YYSY系列处理器 "YYSYD50F": yysy.ProcessYYSYD50FRequest, @@ -190,6 +193,8 @@ func registerAllProcessors(combService *comb.CombService) { // QCXG系列处理器 "QCXG7A2B": qcxg.ProcessQCXG7A2BRequest, "QCXG9P1C": qcxg.ProcessQCXG9P1CRequest, + "QCXG8A3D": qcxg.ProcessQCXG8A3DRequest, + "QCXG6B4E": qcxg.ProcessQCXG6B4ERequest, // DWBG系列处理器 - 多维报告 "DWBG6A2C": dwbg.ProcessDWBG6A2CRequest, diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index ba57ae7..ec8710d 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -172,6 +172,11 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "IVYZ8I9J": &dto.IVYZ8I9JReq{}, "JRZQ0L85": &dto.JRZQ0L85Req{}, "COMBHZY2": &dto.COMBHZY2Req{}, + "QCXG8A3D": &dto.QCXG8A3DReq{}, + "QCXG6B4E": &dto.QCXG6B4EReq{}, + "QYGL2B5C": &dto.QYGL2B5CReq{}, + "JRZQ2F8A": &dto.JRZQ2F8AReq{}, + "JRZQ1E7B": &dto.JRZQ1E7BReq{}, } // 优先返回已配置的DTO @@ -271,6 +276,8 @@ func (s *FormConfigServiceImpl) parseValidationRules(validateTag string) string frontendRules = append(frontendRules, "姓名格式") case rule == "validUSCI": frontendRules = append(frontendRules, "统一社会信用代码格式") + case rule == "validEnterpriseName" || rule == "enterprise_name": + frontendRules = append(frontendRules, "企业名称格式") case rule == "validBankCard": frontendRules = append(frontendRules, "银行卡号格式") case rule == "validDate": @@ -357,6 +364,9 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string { "page_size": "每页数量", "use_scenario": "使用场景", "auth_authorize_file_code": "授权文件编码", + "plate_no": "车牌号", + "plate_type": "号牌类型", + "vin_code": "车辆识别代号VIN码", } if label, exists := labelMap[jsonTag]; exists { @@ -394,6 +404,9 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso "page_size": "10", "use_scenario": "1", "auth_authorize_file_code": "AUTH123456", + "plate_no": "京A12345", + "plate_type": "01", + "vin_code": "LSGBF53M8DS123456", } if example, exists := exampleMap[jsonTag]; exists { @@ -440,6 +453,9 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st "page_size": "请输入每页数量(1-100)", "use_scenario": "请选择使用场景", "auth_authorize_file_code": "请输入授权文件编码", + "plate_no": "请输入车牌号", + "plate_type": "请选择号牌类型(01或02)", + "vin_code": "请输入17位车辆识别代号VIN码", } if placeholder, exists := placeholderMap[jsonTag]; exists { @@ -488,6 +504,9 @@ func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation s "page_size": "请输入每页数量,范围1-100", "use_scenario": "使用场景:1-信贷审核;2-保险评估;3-招聘背景调查;4-其他业务场景;99-其他", "auth_authorize_file_code": "请输入授权文件编码", + "plate_no": "请输入车牌号", + "plate_type": "号牌类型:01-小型汽车;02-大型汽车(可选)", + "vin_code": "请输入17位车辆识别代号VIN码(Vehicle Identification Number)", } if desc, exists := descMap[jsonTag]; exists { diff --git a/internal/domains/api/services/processors/jrzq/jrzq1e7b_processor.go b/internal/domains/api/services/processors/jrzq/jrzq1e7b_processor.go new file mode 100644 index 0000000..c991b8d --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzq1e7b_processor.go @@ -0,0 +1,63 @@ +package jrzq + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/zhicha" +) + +// ProcessJRZQ1E7BRequest JRZQ1E7B API处理方法 - 消费交易特征 +func ProcessJRZQ1E7BRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.JRZQ1E7BReq + 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) + } + + encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + reqData := map[string]interface{}{ + "name": encryptedName, + "idCard": encryptedIDCard, + "phone": encryptedMobileNo, + "authorized": paramsDto.Authorized, + } + + respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI034", reqData) + if err != nil { + if errors.Is(err, zhicha.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } else { + return nil, errors.Join(processors.ErrSystem, err) + } + } + + // 将响应数据转换为 JSON 字节 + respBytes, err := json.Marshal(respData) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + return respBytes, nil +} + diff --git a/internal/domains/api/services/processors/jrzq/jrzq2f8a_processor.go b/internal/domains/api/services/processors/jrzq/jrzq2f8a_processor.go new file mode 100644 index 0000000..4aed478 --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzq2f8a_processor.go @@ -0,0 +1,63 @@ +package jrzq + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/zhicha" +) + +// ProcessJRZQ2F8ARequest JRZQ2F8A API处理方法 - 探针A +func ProcessJRZQ2F8ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.JRZQ2F8AReq + 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) + } + + encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + reqData := map[string]interface{}{ + "name": encryptedName, + "idCard": encryptedIDCard, + "phone": encryptedMobileNo, + "authorized": paramsDto.Authorized, + } + + respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI009", reqData) + if err != nil { + if errors.Is(err, zhicha.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } else { + return nil, errors.Join(processors.ErrSystem, err) + } + } + + // 将响应数据转换为 JSON 字节 + respBytes, err := json.Marshal(respData) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + return respBytes, nil +} + diff --git a/internal/domains/api/services/processors/qcxg/qcxg6b4e_processor.go b/internal/domains/api/services/processors/qcxg/qcxg6b4e_processor.go new file mode 100644 index 0000000..aa0923e --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxg6b4e_processor.go @@ -0,0 +1,46 @@ +package qcxg + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/zhicha" +) + +// ProcessQCXG6B4ERequest QCXG6B4E API处理方法 - 车辆出险记录查验 +func ProcessQCXG6B4ERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.QCXG6B4EReq + 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) + } + + reqData := map[string]interface{}{ + "vin": paramsDto.VINCode, + "authorized": paramsDto.Authorized, + } + + respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI049", reqData) + if err != nil { + if errors.Is(err, zhicha.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } else { + return nil, errors.Join(processors.ErrSystem, err) + } + } + + // 将响应数据转换为 JSON 字节 + respBytes, err := json.Marshal(respData) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + return respBytes, nil +} + diff --git a/internal/domains/api/services/processors/qcxg/qcxg8a3d_processor.go b/internal/domains/api/services/processors/qcxg/qcxg8a3d_processor.go new file mode 100644 index 0000000..0d4aa16 --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxg8a3d_processor.go @@ -0,0 +1,50 @@ +package qcxg + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/zhicha" +) + +// ProcessQCXG8A3DRequest QCXG8A3D API处理方法 - 车辆七项信息核验 +func ProcessQCXG8A3DRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.QCXG8A3DReq + 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) + } + + reqData := map[string]interface{}{ + "plate": paramsDto.PlateNo, + "authorized": paramsDto.Authorized, + } + // 如果传了车牌类型,则添加到请求数据中 + if paramsDto.PlateType != "" { + reqData["vehType"] = paramsDto.PlateType + } + + respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI048", reqData) + if err != nil { + if errors.Is(err, zhicha.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } else { + return nil, errors.Join(processors.ErrSystem, err) + } + } + + // 将响应数据转换为 JSON 字节 + respBytes, err := json.Marshal(respData) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + return respBytes, nil +} + diff --git a/internal/domains/api/services/processors/qygl/qygl2b5c_processor.go b/internal/domains/api/services/processors/qygl/qygl2b5c_processor.go new file mode 100644 index 0000000..1ee2876 --- /dev/null +++ b/internal/domains/api/services/processors/qygl/qygl2b5c_processor.go @@ -0,0 +1,63 @@ +package qygl + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/zhicha" +) + +// ProcessQYGL2B5CRequest QYGL2B5C API处理方法 - 企业联系人实际经营地址 +func ProcessQYGL2B5CRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.QYGL2B5CReq + 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) + } + + // 两选一校验:EntName 和 EntCode 至少传一个 + var keyword string + if paramsDto.EntName != "" && paramsDto.EntCode != "" { + return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("企业名称和企业统一信用代码只能传其中一个")) + } + if paramsDto.EntName == "" && paramsDto.EntCode == "" { + return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("必须提供企业名称或企业统一信用代码中的其中一个")) + } + + // 确定使用哪个值作为 keyword + if paramsDto.EntName != "" { + keyword = paramsDto.EntName + } else { + keyword = paramsDto.EntCode + } + + reqData := map[string]interface{}{ + "keyword": keyword, + "authorized": paramsDto.Authorized, + } + + respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI050", reqData) + if err != nil { + if errors.Is(err, zhicha.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } else { + return nil, errors.Join(processors.ErrSystem, err) + } + } + + // 将响应数据转换为 JSON 字节 + respBytes, err := json.Marshal(respData) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + return respBytes, nil +} + diff --git a/internal/domains/product/entities/product.go b/internal/domains/product/entities/product.go index b6b523b..d452e64 100644 --- a/internal/domains/product/entities/product.go +++ b/internal/domains/product/entities/product.go @@ -18,6 +18,8 @@ type Product struct { Content string `gorm:"type:text" comment:"产品内容"` CategoryID string `gorm:"type:varchar(36);not null" comment:"产品分类ID"` Price decimal.Decimal `gorm:"type:decimal(10,2);not null;default:0" comment:"产品价格"` + CostPrice decimal.Decimal `gorm:"type:decimal(10,2);default:0" comment:"成本价"` + Remark string `gorm:"type:text" comment:"备注"` IsEnabled bool `gorm:"default:true" comment:"是否启用"` IsVisible bool `gorm:"default:true" comment:"是否展示"` IsPackage bool `gorm:"default:false" comment:"是否组合包"` diff --git a/internal/domains/product/services/product_management_service.go b/internal/domains/product/services/product_management_service.go index 5254216..f198a56 100644 --- a/internal/domains/product/services/product_management_service.go +++ b/internal/domains/product/services/product_management_service.go @@ -290,6 +290,10 @@ func (s *ProductManagementService) ValidateProduct(product *entities.Product) er return errors.New("产品价格不能为负数") } + if product.CostPrice.IsNegative() { + return errors.New("成本价不能为负数") + } + // 验证分类是否存在 if product.CategoryID != "" { category, err := s.categoryRepo.GetByID(context.Background(), product.CategoryID) diff --git a/scripts/analyze_processors.go b/scripts/analyze_processors.go new file mode 100644 index 0000000..23ef56b --- /dev/null +++ b/scripts/analyze_processors.go @@ -0,0 +1,603 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + + "github.com/shopspring/decimal" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/gorm/schema" +) + +// ProcessorInfo 处理器信息 +type ProcessorInfo struct { + ProductCode string // 产品编号(从文件名提取) + ProductName string // 产品名称(从数据库查询) + Category string // 分类(从数据库查询) + Price string // 价格(从数据库查询) + DataSource string // 数据源(根据service确定) + DataSourceCode string // 数据源编号(CallAPI的第一个参数) + CostPrice string // 成本价(留空) +} + +// Product 产品实体(简化版) +type Product struct { + Code string `gorm:"column:code"` + Name string `gorm:"column:name"` + CategoryID string `gorm:"column:category_id"` + Category *ProductCategory `gorm:"foreignKey:CategoryID"` + Price decimal.Decimal `gorm:"column:price"` +} + +// ProductCategory 产品分类实体(简化版) +type ProductCategory struct { + ID string `gorm:"column:id"` + Name string `gorm:"column:name"` +} + +func main() { + // 获取处理器目录 + processorsDir := filepath.Join("internal", "domains", "api", "services", "processors") + if len(os.Args) > 1 { + processorsDir = os.Args[1] + } + + // 扫描所有处理器文件 + processors, err := scanProcessors(processorsDir) + if err != nil { + fmt.Fprintf(os.Stderr, "扫描处理器失败: %v\n", err) + os.Exit(1) + } + + // 连接数据库 + db, err := connectDB() + if err != nil { + fmt.Fprintf(os.Stderr, "连接数据库失败: %v\n", err) + os.Exit(1) + } + + // 检查数据库产品数量和处理器数量 + ctx := context.Background() + checkProductAndProcessorCounts(ctx, db, processors) + + // 查询产品信息并填充 + fmt.Println("正在查询数据库...") + successCount := 0 + for i := range processors { + product, err := queryProduct(ctx, db, processors[i].ProductCode) + if err == nil && product != nil && product.Name != "" { + processors[i].ProductName = product.Name + if product.Category != nil && product.Category.Name != "" { + processors[i].Category = product.Category.Name + } + // 格式化价格,保留两位小数 + if !product.Price.IsZero() { + processors[i].Price = product.Price.String() + } + successCount++ + } else { + // 如果查询失败,保留空值(不输出错误,避免日志过多) + processors[i].ProductName = "" + processors[i].Category = "" + processors[i].Price = "" + } + } + fmt.Printf("成功查询到 %d/%d 个产品的信息\n", successCount, len(processors)) + + // 按数据源排序 + sortProcessorsByDataSource(processors) + + // 输出表格 + printTable(processors) + + // 同时输出到文件 + writeToFiles(processors) +} + +// sortProcessorsByDataSource 按数据源排序 +func sortProcessorsByDataSource(processors []ProcessorInfo) { + // 定义数据源的排序优先级 + dataSourceOrder := map[string]int{ + "安徽智查": 1, + "羽山数据": 2, + "西部数据": 3, + "四川星维": 4, + "天眼查": 5, + "阿里云": 6, + "木子数据": 7, + "内部处理": 8, + "未知": 9, + } + + sort.Slice(processors, func(i, j int) bool { + orderI, existsI := dataSourceOrder[processors[i].DataSource] + orderJ, existsJ := dataSourceOrder[processors[j].DataSource] + + // 如果数据源不存在,放在最后 + if !existsI { + orderI = 999 + } + if !existsJ { + orderJ = 999 + } + + // 首先按数据源排序 + if orderI != orderJ { + return orderI < orderJ + } + + // 如果数据源相同,按产品编号排序 + return processors[i].ProductCode < processors[j].ProductCode + }) +} + +// writeToFiles 将表格写入文件 +func writeToFiles(processors []ProcessorInfo) { + // 写入 CSV 文件 + csvFile, err := os.Create("processors_table.csv") + if err == nil { + defer csvFile.Close() + csvFile.WriteString("\xEF\xBB\xBF") // UTF-8 BOM + csvFile.WriteString("产品编号,产品名称,分类,价格,数据源,数据源编号,成本价\n") + for _, p := range processors { + productName := strings.ReplaceAll(p.ProductName, ",", ",") + category := strings.ReplaceAll(p.Category, ",", ",") + csvFile.WriteString(fmt.Sprintf("%s,%s,%s,%s,%s,%s,%s\n", + p.ProductCode, + productName, + category, + p.Price, + p.DataSource, + p.DataSourceCode, + p.CostPrice)) + } + fmt.Println("\n✅ CSV 文件已保存到: processors_table.csv") + } + + // 写入 Markdown 文件 + mdFile, err := os.Create("processors_table.md") + if err == nil { + defer mdFile.Close() + mdFile.WriteString("# 处理器产品信息表\n\n") + mdFile.WriteString("| 产品编号 | 产品名称 | 分类 | 价格 | 数据源 | 数据源编号 | 成本价 |\n") + mdFile.WriteString("|---------|---------|------|------|--------|-----------|--------|\n") + for _, p := range processors { + productName := p.ProductName + if productName == "" { + productName = "-" + } + category := p.Category + if category == "" { + category = "-" + } + price := p.Price + if price == "" { + price = "-" + } + dataSource := p.DataSource + if dataSource == "" { + dataSource = "-" + } + dataSourceCode := p.DataSourceCode + if dataSourceCode == "" { + dataSourceCode = "-" + } + costPrice := p.CostPrice + if costPrice == "" { + costPrice = "-" + } + mdFile.WriteString(fmt.Sprintf("| %s | %s | %s | %s | %s | %s | %s |\n", + p.ProductCode, + productName, + category, + price, + dataSource, + dataSourceCode, + costPrice)) + } + fmt.Println("✅ Markdown 文件已保存到: processors_table.md") + } +} + +// scanProcessors 扫描处理器文件 +func scanProcessors(dir string) ([]ProcessorInfo, error) { + var processors []ProcessorInfo + + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // 跳过非 .go 文件 + if !strings.HasSuffix(path, "_processor.go") { + return nil + } + + // 跳过 comb 和 test 目录 + if strings.Contains(path, string(filepath.Separator)+"comb"+string(filepath.Separator)) { + return nil + } + if strings.Contains(path, string(filepath.Separator)+"test"+string(filepath.Separator)) { + return nil + } + + // 读取文件内容 + content, err := os.ReadFile(path) + if err != nil { + return err + } + + // 提取产品编号(从文件名) + filename := filepath.Base(path) + productCode := extractProductCode(filename) + + // 解析文件内容,提取数据源和数据源编号 + dataSource, dataSourceCode := extractDataSourceInfo(string(content)) + + processors = append(processors, ProcessorInfo{ + ProductCode: productCode, + ProductName: "", + Category: "", + Price: "", + DataSource: dataSource, + DataSourceCode: dataSourceCode, + CostPrice: "", + }) + + return nil + }) + + return processors, err +} + +// extractProductCode 从文件名提取产品编号 +func extractProductCode(filename string) string { + // 例如: dwbg6a2c_processor.go -> DWBG6A2C + name := strings.TrimSuffix(filename, "_processor.go") + return strings.ToUpper(name) +} + +// extractDataSourceInfo 从代码中提取数据源和数据源编号 +func extractDataSourceInfo(content string) (string, string) { + // 先尝试匹配特殊的 CallAPI 方法(如 G05HZ01CallAPI) + specialCallAPIPattern := regexp.MustCompile(`deps\.(\w+Service)\.(\w+CallAPI)\([^,]+,\s*"([^"]+)"`) + specialMatches := specialCallAPIPattern.FindAllStringSubmatch(content, -1) + if len(specialMatches) > 0 { + serviceName := specialMatches[0][1] + dataSourceCode := specialMatches[0][3] + return getDataSourceName(serviceName), dataSourceCode + } + + // 先尝试匹配 AlicloudService 的特殊情况(第一个参数是字符串路径,没有 ctx) + // 匹配: deps.AlicloudService.CallAPI("path", ...) + alicloudPattern := regexp.MustCompile(`deps\.AlicloudService\.CallAPI\(\s*"([^"]+)"`) + alicloudMatches := alicloudPattern.FindAllStringSubmatch(content, -1) + if len(alicloudMatches) > 0 { + path := alicloudMatches[0][1] + parts := strings.Split(path, "/") + dataSourceCode := path + if len(parts) > 0 { + dataSourceCode = parts[len(parts)-1] + } + return "阿里云", dataSourceCode + } + + // 匹配普通的 CallAPI 调用 + // 匹配模式: deps.ServiceName.CallAPI(ctx, "CODE", ...) 或 deps.ServiceName.CallAPI(ctx, projectID, ...) + callAPIPattern := regexp.MustCompile(`deps\.(\w+Service)\.CallAPI\([^,]*,\s*([^,)]+)`) + matches := callAPIPattern.FindAllStringSubmatch(content, -1) + + if len(matches) == 0 { + // 检查是否有直接的 HTTP 请求(如 COMENT01) + if strings.Contains(content, "http.NewRequest") || strings.Contains(content, "http.Client") { + return "内部处理", "" + } + // 检查是否调用了其他处理器(如 QYGL3F8E) + if strings.Contains(content, "Process") && strings.Contains(content, "Request") { + return "内部处理", "" + } + return "未知", "" + } + + // 取第一个匹配的服务 + serviceName := matches[0][1] + codeOrVar := strings.TrimSpace(matches[0][2]) + + var dataSourceCode string + + // 如果是字符串字面量(带引号),直接提取 + if strings.HasPrefix(codeOrVar, `"`) && strings.HasSuffix(codeOrVar, `"`) { + dataSourceCode = strings.Trim(codeOrVar, `"`) + // 对于 AlicloudService,提取路径的最后部分作为数据源编号 + if serviceName == "AlicloudService" { + parts := strings.Split(dataSourceCode, "/") + if len(parts) > 0 { + dataSourceCode = parts[len(parts)-1] + } + } + } else { + // 如果是变量(如 projectID),尝试查找变量定义 + // 对于 XingweiService,查找 projectID 变量 + if strings.Contains(codeOrVar, "projectID") || codeOrVar == "projectID" { + projectIDPattern := regexp.MustCompile(`projectID\s*:=\s*"([^"]+)"`) + projectIDMatches := projectIDPattern.FindAllStringSubmatch(content, -1) + if len(projectIDMatches) > 0 { + // 取最后一个匹配的 projectID(通常是最接近 CallAPI 的那个) + dataSourceCode = projectIDMatches[len(projectIDMatches)-1][1] + } else { + dataSourceCode = "变量未找到" + } + } else { + // 尝试查找变量定义(通用模式) + varPattern := regexp.MustCompile(regexp.QuoteMeta(codeOrVar) + `\s*:=\s*"([^"]+)"`) + varMatches := varPattern.FindAllStringSubmatch(content, -1) + if len(varMatches) > 0 { + dataSourceCode = varMatches[len(varMatches)-1][1] + } else { + dataSourceCode = codeOrVar + } + } + } + + // 根据服务名确定数据源 + dataSource := getDataSourceName(serviceName) + + return dataSource, dataSourceCode +} + +// getDataSourceName 根据服务名获取数据源名称 +func getDataSourceName(serviceName string) string { + switch serviceName { + case "ZhichaService": + return "安徽智查" + case "YushanService": + return "羽山数据" + case "WestDexService": + return "西部数据" + case "XingweiService": + return "四川星维" + case "TianYanChaService": + return "天眼查" + case "AlicloudService": + return "阿里云" + case "MuziService": + return "木子数据" + default: + return "未知" + } +} + +// connectDB 连接数据库 +func connectDB() (*gorm.DB, error) { + // 数据库连接配置 + dsn := "host=1.117.67.95 user=tyapi_user password=Pg9mX4kL8nW2rT5y dbname=tyapi port=25010 sslmode=disable TimeZone=Asia/Shanghai" + + // 配置GORM,使用单数表名(与项目配置一致) + gormConfig := &gorm.Config{ + NamingStrategy: schema.NamingStrategy{ + SingularTable: true, // 使用单数表名 + }, + Logger: logger.Default.LogMode(logger.Silent), // 禁用日志输出,减少噪音 + } + + db, err := gorm.Open(postgres.Open(dsn), gormConfig) + if err != nil { + return nil, fmt.Errorf("连接数据库失败: %w", err) + } + + // 测试连接 + sqlDB, err := db.DB() + if err != nil { + return nil, fmt.Errorf("获取数据库实例失败: %w", err) + } + + if err := sqlDB.Ping(); err != nil { + return nil, fmt.Errorf("数据库连接测试失败: %w", err) + } + + fmt.Println("数据库连接成功") + return db, nil +} + +// checkProductAndProcessorCounts 检查数据库产品数量和处理器数量 +func checkProductAndProcessorCounts(ctx context.Context, db *gorm.DB, processors []ProcessorInfo) { + // 统计处理器数量(排除 comb) + processorCount := len(processors) + fmt.Printf("\n=== 统计信息 ===\n") + fmt.Printf("处理器数量(排除 comb): %d\n", processorCount) + + // 统计数据库中的产品数量(排除组合包) + var productCount int64 + // 先尝试单数表名 + err := db.WithContext(ctx). + Table("product"). + Where("is_package = ? AND deleted_at IS NULL", false). + Count(&productCount).Error + + // 如果单数表名查询失败,尝试复数表名 + if err != nil { + if strings.Contains(err.Error(), "does not exist") { + err = db.WithContext(ctx). + Table("products"). + Where("is_package = ? AND deleted_at IS NULL", false). + Count(&productCount).Error + } + } + + if err == nil { + fmt.Printf("数据库产品数量(排除组合包): %d\n", productCount) + } else { + fmt.Printf("数据库产品数量查询失败: %v\n", err) + } + + // 统计有匹配产品的处理器数量 + matchedCount := 0 + for _, p := range processors { + var count int64 + err := db.WithContext(ctx). + Table("product"). + Where("code = ? AND deleted_at IS NULL", p.ProductCode). + Count(&count).Error + + if err != nil && strings.Contains(err.Error(), "does not exist") { + err = db.WithContext(ctx). + Table("products"). + Where("code = ? AND deleted_at IS NULL", p.ProductCode). + Count(&count).Error + } + + if err == nil && count > 0 { + matchedCount++ + } + } + + fmt.Printf("有匹配产品的处理器数量: %d/%d\n", matchedCount, processorCount) + fmt.Printf("无匹配产品的处理器数量: %d/%d\n", processorCount-matchedCount, processorCount) + fmt.Println() +} + +// queryProduct 查询产品信息 +func queryProduct(ctx context.Context, db *gorm.DB, code string) (*Product, error) { + var product Product + var err error + + // 尝试不同的表名查询(先尝试单数表名,因为用户确认表名是 product) + tableNames := []string{"product", "products"} + for _, tableName := range tableNames { + err = db.WithContext(ctx). + Table(tableName). + Select("code, name, category_id, price"). + Where("code = ? AND deleted_at IS NULL", code). + First(&product).Error + + if err == nil { + // 查询成功,查询分类信息 + if product.CategoryID != "" { + var category ProductCategory + // 尝试不同的分类表名(先尝试单数表名) + categoryTableNames := []string{"product_category", "product_categories"} + for _, catTableName := range categoryTableNames { + err = db.WithContext(ctx). + Table(catTableName). + Select("id, name"). + Where("id = ? AND deleted_at IS NULL", product.CategoryID). + First(&category).Error + if err == nil { + product.Category = &category + break + } + // 如果是表不存在的错误,继续尝试下一个表名 + if err != nil && strings.Contains(err.Error(), "does not exist") { + continue + } + // 如果是记录不存在的错误,也继续尝试(可能是表名不对) + if err == gorm.ErrRecordNotFound { + continue + } + } + // 即使分类查询失败,也返回产品信息(分类可以为空) + } + return &product, nil + } + + // 检查错误类型 + errStr := err.Error() + // 如果是表不存在的错误,继续尝试下一个表名 + if strings.Contains(errStr, "does not exist") { + continue + } + // 如果是记录不存在的错误,也继续尝试(可能是表名不对) + if err == gorm.ErrRecordNotFound { + continue + } + // 其他错误直接返回 + return nil, err + } + + // 所有表名都查询失败 + return nil, gorm.ErrRecordNotFound +} + +// printTable 打印表格 +func printTable(processors []ProcessorInfo) { + // 打印表头 + fmt.Printf("%-15s %-30s %-20s %-15s %-15s %-20s %-15s\n", + "产品编号", "产品名称", "分类", "价格", "数据源", "数据源编号", "成本价") + fmt.Println(strings.Repeat("-", 130)) + + // 打印数据 + for _, p := range processors { + fmt.Printf("%-15s %-30s %-20s %-15s %-15s %-20s %-15s\n", + p.ProductCode, + p.ProductName, + p.Category, + p.Price, + p.DataSource, + p.DataSourceCode, + p.CostPrice) + } + + // 打印 CSV 格式 + fmt.Println("\n=== CSV 格式 ===") + fmt.Println("产品编号,产品名称,分类,价格,数据源,数据源编号,成本价") + for _, p := range processors { + // 转义 CSV 中的特殊字符 + productName := strings.ReplaceAll(p.ProductName, ",", ",") + category := strings.ReplaceAll(p.Category, ",", ",") + fmt.Printf("%s,%s,%s,%s,%s,%s,%s\n", + p.ProductCode, + productName, + category, + p.Price, + p.DataSource, + p.DataSourceCode, + p.CostPrice) + } + + // 打印 Markdown 表格格式 + fmt.Println("\n=== Markdown 表格格式 ===") + fmt.Println("| 产品编号 | 产品名称 | 分类 | 价格 | 数据源 | 数据源编号 | 成本价 |") + fmt.Println("|---------|---------|------|------|--------|-----------|--------|") + for _, p := range processors { + productName := p.ProductName + if productName == "" { + productName = "-" + } + category := p.Category + if category == "" { + category = "-" + } + price := p.Price + if price == "" { + price = "-" + } + dataSource := p.DataSource + if dataSource == "" { + dataSource = "-" + } + dataSourceCode := p.DataSourceCode + if dataSourceCode == "" { + dataSourceCode = "-" + } + costPrice := p.CostPrice + if costPrice == "" { + costPrice = "-" + } + fmt.Printf("| %s | %s | %s | %s | %s | %s | %s |\n", + p.ProductCode, + productName, + category, + price, + dataSource, + dataSourceCode, + costPrice) + } +} +