diff --git a/Makefile b/Makefile index 983887e..dcd27bc 100644 --- a/Makefile +++ b/Makefile @@ -135,7 +135,7 @@ endif test: @echo "Running tests..." ifeq ($(OS),Windows_NT) - $(GOTEST) -v -coverprofile=coverage.out ./... + @where gcc >nul 2>&1 && $(GOTEST) -v -race -coverprofile=coverage.out ./... || $(GOTEST) -v -coverprofile=coverage.out ./... else $(GOTEST) -v -race -coverprofile=coverage.out ./... endif diff --git a/config.yaml b/config.yaml index b6ee98a..1e620cc 100644 --- a/config.yaml +++ b/config.yaml @@ -195,3 +195,11 @@ alipay: # =========================================== domain: api: "" # 开发环境不限制域名,生产环境为 "api.tianyuancha.com" + + +# =========================================== +# 🔍 天眼查配置 +# =========================================== +tianyancha: + base_url: http://open.api.tianyancha.com/services + api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2 \ No newline at end of file diff --git a/configs/env.development.yaml b/configs/env.development.yaml index 1a2dd39..255d019 100644 --- a/configs/env.development.yaml +++ b/configs/env.development.yaml @@ -96,4 +96,11 @@ alipay: # =========================================== recharge: min_amount: "0.01" # 开发环境最低充值金额 - max_amount: "100000.00" # 单次最高充值金额 \ No newline at end of file + max_amount: "100000.00" # 单次最高充值金额 + +# =========================================== +# 🔍 天眼查配置 +# =========================================== +tianyancha: + base_url: http://open.api.tianyancha.com/services + api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2 \ No newline at end of file diff --git a/configs/env.production.yaml b/configs/env.production.yaml index 6f3e102..9072b3d 100644 --- a/configs/env.production.yaml +++ b/configs/env.production.yaml @@ -116,10 +116,10 @@ ocr: # 📝 e签宝服务配置 # =========================================== esign: - app_id: "7439073713" - app_secret: "c7d8cb0d701f7890601d221e9b6edfef" - server_url: "https://smlopenapi.esign.cn" - template_id: "1fd7ed9c6d134d1db7b5af9582633d76" + app_id: "5112008003" + app_secret: "d487672273e7aa70c800804a1d9499b9" + server_url: "https://openapi.esign.cn" + template_id: "c82af4df2790430299c81321f309eef3" contract: name: "天远数据API合作协议" expire_days: 7 diff --git a/docs/产品列表功能修复总结.md b/docs/产品列表功能修复总结.md new file mode 100644 index 0000000..a243298 --- /dev/null +++ b/docs/产品列表功能修复总结.md @@ -0,0 +1,158 @@ +# 产品列表功能修复总结 + +## 问题描述 + +管理员专用的产品列表功能存在以下问题: +1. 不返回产品的展示状态(`is_visible` 字段) +2. 与用户端的产品列表功能没有明确区分 +3. 用户端可能看到隐藏的产品 +4. 缺乏清晰的链路分离,不利于后续维护 + +## 解决方案 + +### 1. 创建专门的响应结构 + +**新增响应结构:** +- `ProductAdminInfoResponse`:管理员专用,包含 `is_visible` 字段 +- `ProductAdminListResponse`:管理员专用列表响应 + +**保持原有结构:** +- `ProductInfoResponse`:用户端使用,不包含 `is_visible` 字段 +- `ProductListResponse`:用户端列表响应 + +### 2. 应用服务层方法分离 + +**新增管理员专用方法:** +```go +ListProductsForAdmin(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductAdminListResponse, error) +GetProductByIDForAdmin(ctx context.Context, query *queries.GetProductQuery) (*responses.ProductAdminInfoResponse, error) +``` + +**新增用户端专用方法:** +```go +GetProductByIDForAdmin(ctx context.Context, query *queries.GetProductQuery) (*responses.ProductInfoResponse, error) +``` + +### 3. 筛选逻辑优化 + +**用户端筛选逻辑:** +- 默认只显示可见产品(`is_visible = true`) +- 包含用户订阅状态信息 +- 无法查看隐藏产品 + +**管理员端筛选逻辑:** +- 可以看到所有产品(包括隐藏的) +- 支持按可见状态筛选 +- 不包含用户订阅状态 + +### 4. 产品详情获取分离 + +**用户端产品详情:** +- 验证产品可见性 +- 隐藏产品返回 404 错误 +- 不包含可见状态信息 + +**管理员端产品详情:** +- 可以获取任何产品的详情 +- 包含可见状态信息 +- 无可见性限制 + +## 修改的文件 + +### 1. 响应结构文件 +- `internal/application/product/dto/responses/product_responses.go` + - 新增 `ProductAdminInfoResponse` 结构 + - 新增 `ProductAdminListResponse` 结构 + +### 2. 应用服务接口 +- `internal/application/product/product_application_service.go` + - 新增管理员专用方法接口 + - 新增用户端专用方法接口 + +### 3. 应用服务实现 +- `internal/application/product/product_application_service_impl.go` + - 实现 `ListProductsForAdmin` 方法 + - 实现 `GetProductByIDForAdmin` 方法 + - 实现 `GetProductByIDForUser` 方法 + - 新增 `convertToProductAdminInfoResponse` 转换方法 + +### 4. HTTP 处理器 +- `internal/infrastructure/http/handlers/product_admin_handler.go` + - 修改 `ListProducts` 方法使用管理员专用服务 + - 修改 `GetProductDetail` 方法使用管理员专用服务 + - 更新 Swagger 文档注释 + +- `internal/infrastructure/http/handlers/product_handler.go` + - 修改 `ListProducts` 方法默认只显示可见产品 + - 修改 `GetProductDetail` 方法使用用户端专用服务 + - 更新 Swagger 文档注释 + +### 5. 测试文件 +- `test/admin_product_list_test.go` + - 新增管理员筛选功能测试 + - 新增用户筛选功能测试 + - 新增响应结构差异测试 + - 新增功能区分逻辑测试 + +### 6. 文档文件 +- `docs/产品列表功能区分说明.md` + - 详细说明功能区分 + - 响应结构对比 + - 实现细节说明 + - 维护建议 + +## 功能验证 + +### 1. 编译测试 +- ✅ 代码编译成功,无语法错误 + +### 2. 单元测试 +- ✅ 管理员筛选功能测试通过 +- ✅ 用户筛选功能测试通过 +- ✅ 响应结构差异测试通过 +- ✅ 功能区分逻辑测试通过 + +### 3. 功能验证 + +**管理员端功能:** +- ✅ 可以看到所有产品(包括隐藏的) +- ✅ 返回产品可见状态信息 +- ✅ 支持按可见状态筛选 +- ✅ 不包含用户订阅状态 + +**用户端功能:** +- ✅ 默认只显示可见产品 +- ✅ 包含用户订阅状态信息 +- ✅ 无法查看隐藏产品详情 +- ✅ 响应结构不包含可见状态 + +## 维护链路 + +### 1. 清晰的职责分离 +- 管理员端和用户端使用不同的响应结构 +- 应用服务层方法明确分离 +- HTTP 处理器职责清晰 + +### 2. 易于扩展 +- 新增功能时可以选择合适的响应结构 +- 筛选逻辑可以独立修改 +- 测试覆盖完整 + +### 3. 文档完善 +- 详细的实现说明文档 +- 清晰的 API 文档注释 +- 完整的测试用例 + +## 注意事项 + +1. **向后兼容**:保持了原有的用户端 API 接口不变 +2. **权限控制**:确保路由级别的权限控制正确实现 +3. **数据安全**:用户无法看到产品的可见状态信息 +4. **性能考虑**:筛选逻辑在应用层实现,避免数据库层面的复杂查询 + +## 后续建议 + +1. **监控**:添加产品列表访问的监控指标 +2. **缓存**:考虑对产品列表进行缓存优化 +3. **分页优化**:优化大数据量时的分页性能 +4. **搜索优化**:考虑添加全文搜索功能 \ No newline at end of file diff --git a/docs/产品列表功能区分说明.md b/docs/产品列表功能区分说明.md new file mode 100644 index 0000000..16d33a0 --- /dev/null +++ b/docs/产品列表功能区分说明.md @@ -0,0 +1,166 @@ +# 产品列表功能区分说明 + +## 概述 + +为了确保管理员和用户端的产品列表功能正确区分,我们对产品列表功能进行了重构,实现了以下目标: + +1. **管理员端**:可以看到所有产品(包括隐藏的),包含产品的可见状态信息 +2. **用户端**:默认只看到可见的产品,包含用户的订阅状态信息 + +## 功能区分 + +### 管理员端产品列表 (`/api/v1/admin/products`) + +**特点:** +- 可以看到所有产品,包括隐藏的产品 +- 返回 `ProductAdminInfoResponse` 结构,包含 `is_visible` 字段 +- 不包含用户的订阅状态信息 +- 支持按可见状态筛选 + +**响应结构:** +```json +{ + "total": 100, + "page": 1, + "size": 10, + "items": [ + { + "id": "product-id", + "name": "产品名称", + "code": "PRODUCT001", + "description": "产品描述", + "price": 99.99, + "is_enabled": true, + "is_visible": false, // 管理员可以看到可见状态 + "is_package": false, + "category": {...}, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +### 用户端产品列表 (`/api/v1/products`) + +**特点:** +- 默认只显示可见的产品 +- 返回 `ProductInfoResponse` 结构,不包含 `is_visible` 字段 +- 包含用户的订阅状态信息(如果用户已登录) +- 不支持查看隐藏的产品 + +**响应结构:** +```json +{ + "total": 50, + "page": 1, + "size": 10, + "items": [ + { + "id": "product-id", + "name": "产品名称", + "code": "PRODUCT001", + "description": "产品描述", + "price": 99.99, + "is_enabled": true, + "is_package": false, + "is_subscribed": true, // 用户端包含订阅状态 + "category": {...}, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +## 实现细节 + +### 1. 响应结构区分 + +创建了两个不同的响应结构: + +- `ProductInfoResponse`:用户端使用,不包含 `is_visible` 字段 +- `ProductAdminInfoResponse`:管理员端使用,包含 `is_visible` 字段 + +### 2. 应用服务方法区分 + +在 `ProductApplicationService` 接口中添加了专用方法: + +```go +// 管理员专用方法 +ListProductsForAdmin(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductAdminListResponse, error) +GetProductByIDForAdmin(ctx context.Context, query *queries.GetProductQuery) (*responses.ProductAdminInfoResponse, error) + +// 用户端专用方法 +GetProductByIDForUser(ctx context.Context, query *queries.GetProductQuery) (*responses.ProductInfoResponse, error) +``` + +### 3. 筛选逻辑区分 + +**用户端筛选逻辑:** +```go +// 可见状态筛选 - 用户端默认只显示可见的产品 +if isVisible := c.Query("is_visible"); isVisible != "" { + if visible, err := strconv.ParseBool(isVisible); err == nil { + filters["is_visible"] = visible + } +} else { + // 如果没有指定可见状态,默认只显示可见的产品 + filters["is_visible"] = true +} +``` + +**管理员端筛选逻辑:** +```go +// 可见状态筛选 - 管理员可以看到所有产品 +if isVisible := c.Query("is_visible"); isVisible != "" { + if visible, err := strconv.ParseBool(isVisible); err == nil { + filters["is_visible"] = visible + } +} +``` + +### 4. 产品详情获取区分 + +**用户端产品详情:** +- 使用 `GetProductByIDForUser` 方法 +- 验证产品可见性,隐藏产品返回 404 +- 不包含可见状态信息 + +**管理员端产品详情:** +- 使用 `GetProductByIDForAdmin` 方法 +- 可以获取任何产品的详情 +- 包含可见状态信息 + +## 路由区分 + +### 管理员路由 +- `GET /api/v1/admin/products` - 管理员产品列表 +- `GET /api/v1/admin/products/{id}` - 管理员产品详情 + +### 用户路由 +- `GET /api/v1/products` - 用户产品列表 +- `GET /api/v1/products/{id}` - 用户产品详情 + +## 测试 + +创建了专门的测试文件 `test/admin_product_list_test.go` 来验证: + +1. 管理员筛选功能 +2. 用户筛选功能 +3. 响应结构差异 +4. 功能区分逻辑 + +## 维护建议 + +1. **保持分离**:确保管理员端和用户端的功能保持分离,避免混淆 +2. **权限控制**:确保路由级别的权限控制正确实现 +3. **文档更新**:及时更新 API 文档,明确说明不同端的功能差异 +4. **测试覆盖**:确保测试覆盖所有场景,包括边界情况 + +## 注意事项 + +1. 用户端默认只显示可见产品,这是业务逻辑要求 +2. 管理员端可以看到所有产品,包括隐藏的,便于管理 +3. 响应结构的差异确保了数据安全,用户无法看到产品的可见状态 +4. 订阅状态只在用户端显示,管理员端不需要此信息 \ No newline at end of file diff --git a/internal/application/api/utils/error_translator.go b/internal/application/api/utils/error_translator.go index b6a35ae..de1aaca 100644 --- a/internal/application/api/utils/error_translator.go +++ b/internal/application/api/utils/error_translator.go @@ -15,7 +15,7 @@ func TranslateErrorMsg(errorType, errorMsg *string) *string { "not_subscribed": "未订阅该产品", "product_not_found": "产品不存在", "product_disabled": "产品已停用", - "system_error": "系统内部错误", + "system_error": "接口异常", "datasource_error": "数据源异常", "invalid_param": "参数校验失败", "decrypt_fail": "参数解密失败", @@ -37,4 +37,4 @@ func TranslateErrorMsg(errorType, errorMsg *string) *string { } return &translatedMsg -} \ No newline at end of file +} diff --git a/internal/application/certification/certification_application_service_impl.go b/internal/application/certification/certification_application_service_impl.go index 5282f41..dd50e2d 100644 --- a/internal/application/certification/certification_application_service_impl.go +++ b/internal/application/certification/certification_application_service_impl.go @@ -117,7 +117,6 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo( cmd.LegalPersonID, cmd.LegalPersonPhone, cmd.EnterpriseAddress, - cmd.EnterpriseEmail, ) enterpriseInfo := &certification_value_objects.EnterpriseInfo{ CompanyName: cmd.CompanyName, @@ -126,7 +125,6 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo( LegalPersonID: cmd.LegalPersonID, LegalPersonPhone: cmd.LegalPersonPhone, EnterpriseAddress: cmd.EnterpriseAddress, - EnterpriseEmail: cmd.EnterpriseEmail, } err = enterpriseInfo.Validate() if err != nil { @@ -432,7 +430,10 @@ func (s *CertificationApplicationServiceImpl) GetCertification( response := s.convertToResponse(cert) // 3. 添加状态相关的元数据 - meta := cert.GetDataByStatus() + meta, err := s.AddStatusMetadata(ctx, cert) + if err != nil { + return nil, err + } if meta != nil { response.Metadata = meta } @@ -519,7 +520,6 @@ func (s *CertificationApplicationServiceImpl) HandleEsignCallback( record.LegalPersonID, record.LegalPersonPhone, record.EnterpriseAddress, - record.EnterpriseEmail, ) if err != nil { s.logger.Error("同步企业信息到用户域失败", zap.Error(err)) @@ -527,7 +527,7 @@ func (s *CertificationApplicationServiceImpl) HandleEsignCallback( } // 生成合同 - err = s.generateAndAddContractFile(txCtx, cert, record.CompanyName, record.LegalPersonName, record.UnifiedSocialCode, record.EnterpriseAddress, record.LegalPersonPhone, record.LegalPersonID, record.EnterpriseEmail) + err = s.generateAndAddContractFile(txCtx, cert, record.CompanyName, record.LegalPersonName, record.UnifiedSocialCode, record.EnterpriseAddress, record.LegalPersonPhone, record.LegalPersonID) if err != nil { return err } @@ -679,7 +679,6 @@ func (s *CertificationApplicationServiceImpl) completeEnterpriseVerification( record.LegalPersonID, record.LegalPersonPhone, record.EnterpriseAddress, - record.EnterpriseEmail, ) if err != nil { s.logger.Error("保存企业信息到用户域失败", zap.Error(err)) @@ -689,7 +688,7 @@ func (s *CertificationApplicationServiceImpl) completeEnterpriseVerification( } // 生成合同 - err = s.generateAndAddContractFile(ctx, cert, record.CompanyName, record.LegalPersonName, record.UnifiedSocialCode, record.EnterpriseAddress, record.LegalPersonPhone, record.LegalPersonID, record.EnterpriseEmail) + err = s.generateAndAddContractFile(ctx, cert, record.CompanyName, record.LegalPersonName, record.UnifiedSocialCode, record.EnterpriseAddress, record.LegalPersonPhone, record.LegalPersonID) if err != nil { return err } @@ -714,7 +713,6 @@ func (s *CertificationApplicationServiceImpl) generateAndAddContractFile( enterpriseAddress string, legalPersonPhone string, legalPersonID string, - enterpriseEmail string, ) error { fileComponent := map[string]string{ "YFCompanyName": companyName, @@ -725,7 +723,6 @@ func (s *CertificationApplicationServiceImpl) generateAndAddContractFile( "YFEnterpriseAddress": enterpriseAddress, "YFContactPerson": legalPersonName, "YFMobile": legalPersonPhone, - "YFEmail": enterpriseEmail, "SignDate": time.Now().Format("2006年01月02日"), "SignDate2": time.Now().Format("2006年01月02日"), "SignDate3": time.Now().Format("2006年01月02日"), @@ -753,7 +750,7 @@ func (s *CertificationApplicationServiceImpl) updateContractFile(ctx context.Con } // 生成合同 - err = s.generateAndAddContractFile(ctx, cert, enterpriseInfo.EnterpriseInfo.CompanyName, enterpriseInfo.EnterpriseInfo.LegalPersonName, enterpriseInfo.EnterpriseInfo.UnifiedSocialCode, enterpriseInfo.EnterpriseInfo.EnterpriseAddress, enterpriseInfo.EnterpriseInfo.LegalPersonPhone, enterpriseInfo.EnterpriseInfo.LegalPersonID, enterpriseInfo.EnterpriseInfo.EnterpriseEmail) + err = s.generateAndAddContractFile(ctx, cert, enterpriseInfo.EnterpriseInfo.CompanyName, enterpriseInfo.EnterpriseInfo.LegalPersonName, enterpriseInfo.EnterpriseInfo.UnifiedSocialCode, enterpriseInfo.EnterpriseInfo.EnterpriseAddress, enterpriseInfo.EnterpriseInfo.LegalPersonPhone, enterpriseInfo.EnterpriseInfo.LegalPersonID) if err != nil { return err } @@ -849,7 +846,7 @@ func (s *CertificationApplicationServiceImpl) checkAndUpdateSignStatus(ctx conte // handleContractAfterSignComplete 处理签署完成后的合同 func (s *CertificationApplicationServiceImpl) handleContractAfterSignComplete(ctx context.Context, cert *entities.Certification) error { // 获取用户的企业信息 - user, err := s.userAggregateService.LoadUser(ctx, cert.UserID) + user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, cert.UserID) if err != nil { return fmt.Errorf("加载用户信息失败: %w", err) } @@ -941,3 +938,29 @@ func (s *CertificationApplicationServiceImpl) downloadFileContent(ctx context.Co } return io.ReadAll(resp.Body) } + +// 添加状态相关的元数据 +func (s *CertificationApplicationServiceImpl) AddStatusMetadata(ctx context.Context, cert *entities.Certification) (map[string]interface{}, error) { + metadata := make(map[string]interface{}) + metadata = cert.GetDataByStatus() + switch cert.Status { + case enums.StatusPending, enums.StatusInfoSubmitted, enums.StatusEnterpriseVerified: + record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cert.UserID) + if err == nil && record != nil { + metadata["company_name"] = record.CompanyName + metadata["legal_person_name"] = record.LegalPersonName + metadata["unified_social_code"] = record.UnifiedSocialCode + metadata["enterprise_address"] = record.EnterpriseAddress + metadata["legal_person_phone"] = record.LegalPersonPhone + metadata["legal_person_id"] = record.LegalPersonID + } + case enums.StatusCompleted: + // 获取最终合同信息 + contracts, err := s.contractAggregateService.FindByUserID(ctx, cert.UserID) + if err == nil && len(contracts) > 0 { + metadata["contract_url"] = contracts[0].ContractFileURL + } + } + + return metadata, nil +} diff --git a/internal/application/certification/dto/commands/certification_commands.go b/internal/application/certification/dto/commands/certification_commands.go index e1ff752..3c6918c 100644 --- a/internal/application/certification/dto/commands/certification_commands.go +++ b/internal/application/certification/dto/commands/certification_commands.go @@ -90,6 +90,5 @@ type SubmitEnterpriseInfoCommand struct { LegalPersonID string `json:"legal_person_id" binding:"required,id_card" comment:"法定代表人身份证号码,18位,如:110101199001011234"` LegalPersonPhone string `json:"legal_person_phone" binding:"required,phone" comment:"法定代表人手机号,11位,如:13800138000"` EnterpriseAddress string `json:"enterprise_address" binding:"required,enterprise_address" comment:"企业地址,如:北京市海淀区"` - EnterpriseEmail string `json:"enterprise_email" binding:"required,enterprise_email" comment:"企业邮箱,如:info@example.com"` VerificationCode string `json:"verification_code" binding:"required,len=6" comment:"验证码"` } diff --git a/internal/application/product/dto/responses/product_responses.go b/internal/application/product/dto/responses/product_responses.go index 11ff7a2..6ec0edf 100644 --- a/internal/application/product/dto/responses/product_responses.go +++ b/internal/application/product/dto/responses/product_responses.go @@ -75,3 +75,39 @@ type ProductStatsResponse struct { VisibleProducts int64 `json:"visible_products" comment:"可见产品数"` PackageProducts int64 `json:"package_products" comment:"组合包产品数"` } + +// ProductAdminInfoResponse 管理员产品详情响应 +type ProductAdminInfoResponse struct { + ID string `json:"id" comment:"产品ID"` + Name string `json:"name" comment:"产品名称"` + Code string `json:"code" comment:"产品编号"` + Description string `json:"description" comment:"产品简介"` + Content string `json:"content" comment:"产品内容"` + CategoryID string `json:"category_id" comment:"产品分类ID"` + Price float64 `json:"price" comment:"产品价格"` + IsEnabled bool `json:"is_enabled" comment:"是否启用"` + IsVisible bool `json:"is_visible" comment:"是否可见"` + IsPackage bool `json:"is_package" comment:"是否组合包"` + + // SEO信息 + SEOTitle string `json:"seo_title" comment:"SEO标题"` + SEODescription string `json:"seo_description" comment:"SEO描述"` + SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"` + + // 关联信息 + Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"` + + // 组合包信息 + PackageItems []*PackageItemResponse `json:"package_items,omitempty" comment:"组合包项目列表"` + + CreatedAt time.Time `json:"created_at" comment:"创建时间"` + UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` +} + +// ProductAdminListResponse 管理员产品列表响应 +type ProductAdminListResponse struct { + Total int64 `json:"total" comment:"总数"` + Page int `json:"page" comment:"页码"` + Size int `json:"size" comment:"每页数量"` + Items []ProductAdminInfoResponse `json:"items" comment:"产品列表"` +} diff --git a/internal/application/product/product_application_service.go b/internal/application/product/product_application_service.go index 2bdd920..0f5cac8 100644 --- a/internal/application/product/product_application_service.go +++ b/internal/application/product/product_application_service.go @@ -20,6 +20,13 @@ type ProductApplicationService interface { ListProductsWithSubscriptionStatus(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error) GetProductsByIDs(ctx context.Context, query *queries.GetProductsByIDsQuery) ([]*responses.ProductInfoResponse, error) + // 管理员专用方法 + ListProductsForAdmin(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductAdminListResponse, error) + GetProductByIDForAdmin(ctx context.Context, query *queries.GetProductQuery) (*responses.ProductAdminInfoResponse, error) + + // 用户端专用方法 + GetProductByIDForUser(ctx context.Context, query *queries.GetProductQuery) (*responses.ProductInfoResponse, error) + // 业务查询 GetSubscribableProducts(ctx context.Context, query *queries.GetSubscribableProductsQuery) ([]*responses.ProductInfoResponse, error) GetProductStats(ctx context.Context) (*responses.ProductStatsResponse, error) @@ -30,6 +37,8 @@ type ProductApplicationService interface { RemovePackageItem(ctx context.Context, packageID, itemID string) error ReorderPackageItems(ctx context.Context, packageID string, cmd *commands.ReorderPackageItemsCommand) error UpdatePackageItems(ctx context.Context, packageID string, cmd *commands.UpdatePackageItemsCommand) error + + // 可选子产品查询 GetAvailableProducts(ctx context.Context, query *queries.GetAvailableProductsQuery) (*responses.ProductListResponse, error) // API配置管理 diff --git a/internal/application/product/product_application_service_impl.go b/internal/application/product/product_application_service_impl.go index 2da2968..dc46bf2 100644 --- a/internal/application/product/product_application_service_impl.go +++ b/internal/application/product/product_application_service_impl.go @@ -344,6 +344,7 @@ func (s *ProductApplicationServiceImpl) UpdatePackageItems(ctx context.Context, } // GetAvailableProducts 获取可选子产品列表 +// 业务流程:1. 获取启用产品 2. 过滤可订阅产品 3. 构建响应数据 func (s *ProductApplicationServiceImpl) GetAvailableProducts(ctx context.Context, query *appQueries.GetAvailableProductsQuery) (*responses.ProductListResponse, error) { // 构建筛选条件 filters := make(map[string]interface{}) @@ -385,6 +386,56 @@ func (s *ProductApplicationServiceImpl) GetAvailableProducts(ctx context.Context }, nil } +// ListProductsForAdmin 获取产品列表(管理员专用) +// 业务流程:1. 获取所有产品列表(包括隐藏的) 2. 构建管理员响应数据 +func (s *ProductApplicationServiceImpl) ListProductsForAdmin(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductAdminListResponse, error) { + // 调用领域服务获取产品列表(管理员可以看到所有产品) + products, total, err := s.productManagementService.ListProducts(ctx, filters, options) + if err != nil { + return nil, err + } + + // 转换为管理员响应对象 + items := make([]responses.ProductAdminInfoResponse, len(products)) + for i := range products { + items[i] = *s.convertToProductAdminInfoResponse(products[i]) + } + + return &responses.ProductAdminListResponse{ + Total: total, + Page: options.Page, + Size: options.PageSize, + Items: items, + }, nil +} + +// GetProductByIDForAdmin 根据ID获取产品(管理员专用) +// 业务流程:1. 获取产品信息 2. 构建管理员响应数据 +func (s *ProductApplicationServiceImpl) GetProductByIDForAdmin(ctx context.Context, query *appQueries.GetProductQuery) (*responses.ProductAdminInfoResponse, error) { + product, err := s.productManagementService.GetProductWithCategory(ctx, query.ID) + if err != nil { + return nil, err + } + + return s.convertToProductAdminInfoResponse(product), nil +} + +// GetProductByIDForUser 根据ID获取产品(用户端专用) +// 业务流程:1. 获取产品信息 2. 验证产品可见性 3. 构建用户响应数据 +func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Context, query *appQueries.GetProductQuery) (*responses.ProductInfoResponse, error) { + product, err := s.productManagementService.GetProductWithCategory(ctx, query.ID) + if err != nil { + return nil, err + } + + // 用户端只能查看可见的产品 + if !product.IsVisible { + return nil, fmt.Errorf("产品不存在或不可见") + } + + return s.convertToProductInfoResponse(product), nil +} + // convertToProductInfoResponse 转换为产品信息响应 func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse { response := &responses.ProductInfoResponse{ @@ -427,6 +478,49 @@ func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *en return response } +// convertToProductAdminInfoResponse 转换为管理员产品信息响应 +func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse { + response := &responses.ProductAdminInfoResponse{ + ID: product.ID, + Name: product.Name, + Code: product.Code, + Description: product.Description, + Content: product.Content, + CategoryID: product.CategoryID, + Price: product.Price.InexactFloat64(), + IsEnabled: product.IsEnabled, + IsVisible: product.IsVisible, // 管理员可以看到可见状态 + IsPackage: product.IsPackage, + SEOTitle: product.SEOTitle, + SEODescription: product.SEODescription, + SEOKeywords: product.SEOKeywords, + CreatedAt: product.CreatedAt, + UpdatedAt: product.UpdatedAt, + } + + // 添加分类信息 + if product.Category != nil { + response.Category = s.convertToCategoryInfoResponse(product.Category) + } + + // 转换组合包项目信息 + if product.IsPackage && len(product.PackageItems) > 0 { + response.PackageItems = make([]*responses.PackageItemResponse, len(product.PackageItems)) + for i, item := range product.PackageItems { + response.PackageItems[i] = &responses.PackageItemResponse{ + ID: item.ID, + ProductID: item.ProductID, + ProductCode: item.Product.Code, + ProductName: item.Product.Name, + SortOrder: item.SortOrder, + Price: item.Product.Price.InexactFloat64(), + } + } + } + + return response +} + // convertToCategoryInfoResponse 转换为分类信息响应 func (s *ProductApplicationServiceImpl) convertToCategoryInfoResponse(category *entities.ProductCategory) *responses.CategoryInfoResponse { return &responses.CategoryInfoResponse{ diff --git a/internal/application/user/dto/responses/user_list_response.go b/internal/application/user/dto/responses/user_list_response.go index 2e3c42a..7e5deee 100644 --- a/internal/application/user/dto/responses/user_list_response.go +++ b/internal/application/user/dto/responses/user_list_response.go @@ -30,7 +30,6 @@ type EnterpriseInfoItem struct { LegalPersonName string `json:"legal_person_name"` LegalPersonPhone string `json:"legal_person_phone"` EnterpriseAddress string `json:"enterprise_address"` - EnterpriseEmail string `json:"enterprise_email"` CreatedAt time.Time `json:"created_at"` } diff --git a/internal/application/user/dto/responses/user_responses.go b/internal/application/user/dto/responses/user_responses.go index 43c4640..e6f62ee 100644 --- a/internal/application/user/dto/responses/user_responses.go +++ b/internal/application/user/dto/responses/user_responses.go @@ -21,7 +21,6 @@ type EnterpriseInfoResponse struct { LegalPersonID string `json:"legal_person_id" example:"110101199001011234"` LegalPersonPhone string `json:"legal_person_phone" example:"13800138000"` EnterpriseAddress string `json:"enterprise_address" example:"北京市朝阳区xxx街道xxx号"` - EnterpriseEmail string `json:"enterprise_email" example:"contact@example.com"` CertifiedAt *time.Time `json:"certified_at,omitempty" example:"2024-01-01T00:00:00Z"` CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"` UpdatedAt time.Time `json:"updated_at" example:"2024-01-01T00:00:00Z"` diff --git a/internal/application/user/user_application_service_impl.go b/internal/application/user/user_application_service_impl.go index f19b0ca..4654edd 100644 --- a/internal/application/user/user_application_service_impl.go +++ b/internal/application/user/user_application_service_impl.go @@ -277,7 +277,6 @@ func (s *UserApplicationServiceImpl) GetUserProfile(ctx context.Context, userID LegalPersonID: user.EnterpriseInfo.LegalPersonID, LegalPersonPhone: user.EnterpriseInfo.LegalPersonPhone, EnterpriseAddress: user.EnterpriseInfo.EnterpriseAddress, - EnterpriseEmail: user.EnterpriseInfo.EnterpriseEmail, CreatedAt: user.EnterpriseInfo.CreatedAt, UpdatedAt: user.EnterpriseInfo.UpdatedAt, } @@ -341,7 +340,6 @@ func (s *UserApplicationServiceImpl) ListUsers(ctx context.Context, query *queri LegalPersonName: user.EnterpriseInfo.LegalPersonName, LegalPersonPhone: user.EnterpriseInfo.LegalPersonPhone, EnterpriseAddress: user.EnterpriseInfo.EnterpriseAddress, - EnterpriseEmail: user.EnterpriseInfo.EnterpriseEmail, CreatedAt: user.EnterpriseInfo.CreatedAt, } } diff --git a/internal/config/config.go b/internal/config/config.go index b25244e..7fc04ab 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -29,6 +29,7 @@ type Config struct { AliPay AliPayConfig `mapstructure:"alipay"` Recharge RechargeConfig `mapstructure:"recharge"` Yushan YushanConfig `mapstructure:"yushan"` + TianYanCha TianYanChaConfig `mapstructure:"tianyancha"` Domain DomainConfig `mapstructure:"domain"` } @@ -91,8 +92,8 @@ type LoggerConfig struct { UseColor bool `mapstructure:"use_color"` // 是否使用彩色输出 UseDaily bool `mapstructure:"use_daily"` // 是否按日分包 // 按级别分文件配置 - EnableLevelSeparation bool `mapstructure:"enable_level_separation"` // 是否启用按级别分文件 - LevelConfigs map[string]LevelFileConfig `mapstructure:"level_configs"` // 各级别配置 + EnableLevelSeparation bool `mapstructure:"enable_level_separation"` // 是否启用按级别分文件 + LevelConfigs map[string]LevelFileConfig `mapstructure:"level_configs"` // 各级别配置 } // LevelFileConfig 单个级别文件配置 @@ -261,7 +262,7 @@ type AuthConfig struct { DefaultAuthMode string `mapstructure:"default_auth_mode"` // 默认认证模式 PsnAuthModes []string `mapstructure:"psn_auth_modes"` // 个人可用认证模式 WillingnessAuthModes []string `mapstructure:"willingness_auth_modes"` // 意愿认证模式 - RedirectURL string `mapstructure:"redirect_url"` // 重定向URL + RedirectURL string `mapstructure:"redirect_url"` // 重定向URL } // SignConfig 签署配置 @@ -287,12 +288,12 @@ type WestDexConfig struct { // AliPayConfig 支付宝配置 type AliPayConfig struct { - AppID string `mapstructure:"app_id"` - PrivateKey string `mapstructure:"private_key"` - AlipayPublicKey string `mapstructure:"alipay_public_key"` - IsProduction bool `mapstructure:"is_production"` - NotifyURL string `mapstructure:"notify_url"` - ReturnURL string `mapstructure:"return_url"` + AppID string `mapstructure:"app_id"` + PrivateKey string `mapstructure:"private_key"` + AlipayPublicKey string `mapstructure:"alipay_public_key"` + IsProduction bool `mapstructure:"is_production"` + NotifyURL string `mapstructure:"notify_url"` + ReturnURL string `mapstructure:"return_url"` } // RechargeConfig 充值配置 @@ -308,7 +309,13 @@ type YushanConfig struct { AcctID string `mapstructure:"acct_id"` } +// TianYanChaConfig 天眼查配置 +type TianYanChaConfig struct { + BaseURL string `mapstructure:"base_url"` + APIKey string `mapstructure:"api_key"` +} + // DomainConfig 域名配置 type DomainConfig struct { API string `mapstructure:"api"` // API域名 -} \ No newline at end of file +} diff --git a/internal/container/container.go b/internal/container/container.go index b2612b8..4e63f21 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -29,6 +29,7 @@ import ( "tyapi-server/internal/infrastructure/external/ocr" "tyapi-server/internal/infrastructure/external/sms" "tyapi-server/internal/infrastructure/external/storage" + "tyapi-server/internal/infrastructure/external/tianyancha" "tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/yushan" "tyapi-server/internal/infrastructure/http/handlers" @@ -305,6 +306,14 @@ func NewContainer() *Container { cfg.Yushan.AcctID, ) }, + // TianYanChaService - 天眼查服务 + func(cfg *config.Config) *tianyancha.TianYanChaService { + return tianyancha.NewTianYanChaService( + cfg.TianYanCha.BaseURL, // 天眼查API基础URL + cfg.TianYanCha.APIKey, + 30*time.Second, // 默认超时时间 + ) + }, sharedhttp.NewGinRouter, ), diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 8e25f8a..bde285d 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -119,6 +119,13 @@ type QYGLB4C0Req struct { IDCard string `json:"id_card" validate:"required,validIDCard"` } +type QYGL23T7Req struct { + EntName string `json:"ent_name" validate:"required,min=1,validName"` + LegalPerson string `json:"legal_person" validate:"required,min=1,validName"` + EntCode string `json:"ent_code" validate:"required,validUSCI"` + IDCard string `json:"id_card" validate:"required,validIDCard"` +} + type YYSY4B37Req struct { MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` } @@ -141,9 +148,9 @@ type IVYZ0b03Req struct { MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` Name string `json:"name" validate:"required,min=1,validName"` } -type YYSYBE08Req struct{ - Name string `json:"name" validate:"required,min=1,validName"` - IDCard string `json:"id_card" validate:"required,validIDCard"` +type YYSYBE08Req struct { + Name string `json:"name" validate:"required,min=1,validName"` + IDCard string `json:"id_card" validate:"required,validIDCard"` } type YYSYD50FReq struct { MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 7f8bb82..19ceafc 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -13,6 +13,7 @@ import ( "tyapi-server/internal/domains/api/services/processors/qygl" "tyapi-server/internal/domains/api/services/processors/yysy" "tyapi-server/internal/domains/product/services" + "tyapi-server/internal/infrastructure/external/tianyancha" "tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/yushan" "tyapi-server/internal/shared/interfaces" @@ -27,16 +28,18 @@ var ( type ApiRequestService struct { // 可注入依赖,如第三方服务、模型等 - westDexService *westdex.WestDexService - yushanService *yushan.YushanService - validator interfaces.RequestValidator - processorDeps *processors.ProcessorDependencies - combService *comb.CombService + westDexService *westdex.WestDexService + yushanService *yushan.YushanService + tianYanChaService *tianyancha.TianYanChaService + validator interfaces.RequestValidator + processorDeps *processors.ProcessorDependencies + combService *comb.CombService } func NewApiRequestService( westDexService *westdex.WestDexService, yushanService *yushan.YushanService, + tianYanChaService *tianyancha.TianYanChaService, validator interfaces.RequestValidator, productManagementService *services.ProductManagementService, ) *ApiRequestService { @@ -44,17 +47,18 @@ func NewApiRequestService( combService := comb.NewCombService(productManagementService) // 创建处理器依赖容器 - processorDeps := processors.NewProcessorDependencies(westDexService, yushanService, validator, combService) + processorDeps := processors.NewProcessorDependencies(westDexService, yushanService, tianYanChaService, validator, combService) // 统一注册所有处理器 registerAllProcessors(combService) return &ApiRequestService{ - westDexService: westDexService, - yushanService: yushanService, - validator: validator, - processorDeps: processorDeps, - combService: combService, + westDexService: westDexService, + yushanService: yushanService, + tianYanChaService: tianYanChaService, + validator: validator, + processorDeps: processorDeps, + combService: combService, } } @@ -89,6 +93,7 @@ func registerAllProcessors(combService *comb.CombService) { "QYGL6F2D": qygl.ProcessQYGL6F2DRequest, "QYGL8271": qygl.ProcessQYGL8271Request, "QYGLB4C0": qygl.ProcessQYGLB4C0Request, + "QYGL23T7": qygl.ProcessQYGL23T7Request, // 企业三要素验证 // YYSY系列处理器 "YYSYD50F": yysy.ProcessYYSYD50FRequest, @@ -128,8 +133,8 @@ var RequestProcessors map[string]processors.ProcessorFunc func (a *ApiRequestService) PreprocessRequestApi(ctx context.Context, apiCode string, params []byte, options *commands.ApiCallOptions) ([]byte, error) { if processor, exists := RequestProcessors[apiCode]; exists { // 设置Options到依赖容器 - depsWithOptions := a.processorDeps.WithOptions(options) - return processor(ctx, params, depsWithOptions) + deps := a.processorDeps.WithOptions(options) + return processor(ctx, params, deps) } - return nil, fmt.Errorf("%w: %s", ErrSystem, "api请求, 未找到相应的处理程序") + return nil, fmt.Errorf("%s: 未找到处理器: %s", ErrSystem, apiCode) } diff --git a/internal/domains/api/services/api_request_service_test.go b/internal/domains/api/services/api_request_service_test.go new file mode 100644 index 0000000..5ddc940 --- /dev/null +++ b/internal/domains/api/services/api_request_service_test.go @@ -0,0 +1,62 @@ +package services + +import ( + "context" + "encoding/json" + "testing" + "tyapi-server/internal/application/api/commands" +) + +// 基础测试结构体 +type apiRequestServiceTestSuite struct { + t *testing.T + ctx context.Context + service *ApiRequestService +} + +// 初始化测试套件 +func newApiRequestServiceTestSuite(t *testing.T) *apiRequestServiceTestSuite { + // 这里可以初始化依赖的mock或fake对象 + // 例如:mockProcessorDeps := &MockProcessorDeps{} + // service := &ApiRequestService{processorDeps: mockProcessorDeps} + // 这里只做基础架构,具体mock实现后续补充 + return &apiRequestServiceTestSuite{ + t: t, + ctx: context.Background(), + service: nil, // 这里后续可替换为实际service或mock + } +} + +// 示例:测试PreprocessRequestApi方法(仅结构,具体mock和断言后续补充) +func TestApiRequestService_PreprocessRequestApi(t *testing.T) { + suite := newApiRequestServiceTestSuite(t) + + // 假设有一个mock processor和注册 + // RequestProcessors = map[string]processors.ProcessorFunc{ + // "MOCKAPI": func(ctx context.Context, params []byte, deps interface{}) ([]byte, error) { + // return []byte("ok"), nil + // }, + // } + + // 这里仅做结构示例 + apiCode := "QYGL23T7" + params := map[string]string{ + "code": "91460000MAE471M58X", + "name": "海南天远大数据科技有限公司", + "legalPersonName": "刘福思", + } + paramsByte, err := json.Marshal(params) + if err != nil { + t.Fatalf("参数序列化失败: %v", err) + } + options := commands.ApiCallOptions{} // 实际应为*commands.ApiCallOptions + + // 由于service为nil,这里仅做断言结构示例 + if suite.service != nil { + resp, err := suite.service.PreprocessRequestApi(suite.ctx, apiCode, paramsByte, &options) + if err != nil { + t.Errorf("PreprocessRequestApi 调用出错: %v", err) + } + t.Logf("PreprocessRequestApi 返回结果: %s", string(resp)) + } +} diff --git a/internal/domains/api/services/processors/OPTIONS_USAGE.md b/internal/domains/api/services/processors/OPTIONS_USAGE.md deleted file mode 100644 index fa7bddf..0000000 --- a/internal/domains/api/services/processors/OPTIONS_USAGE.md +++ /dev/null @@ -1,195 +0,0 @@ -# Options 使用指南 - -## 概述 - -Options 机制允许在 API 调用时传递额外的配置选项,这些选项会传递给所有处理器(包括组合包处理器和子处理器),实现灵活的配置控制。 - -## 架构设计 - -``` -ApiCallCommand.Options → api_application_service → api_request_service → 所有处理器 - ↓ - 组合包处理器 → 子处理器 -``` - -## Options 字段说明 - -```go -type ApiCallOptions struct { - Json bool `json:"json,omitempty"` // 是否返回JSON格式 - Debug bool `json:"debug,omitempty"` // 调试模式 - Timeout int `json:"timeout,omitempty"` // 超时时间(秒) - RetryCount int `json:"retry_count,omitempty"` // 重试次数 - Async bool `json:"async,omitempty"` // 异步处理 - Priority int `json:"priority,omitempty"` // 优先级(1-10) - Cache bool `json:"cache,omitempty"` // 是否使用缓存 - Compress bool `json:"compress,omitempty"` // 是否压缩响应 - Encrypt bool `json:"encrypt,omitempty"` // 是否加密响应 - Validate bool `json:"validate,omitempty"` // 是否严格验证 - LogLevel string `json:"log_level,omitempty"` // 日志级别 -} -``` - -## 使用示例 - -### 1. 普通处理器中使用 Options - -```go -func ProcessYYSY4B37Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { - // ... 参数验证 ... - - reqData := map[string]interface{}{ - "mobile": paramsDto.Mobile, - } - - // 使用 Options 调整请求参数 - if deps.Options != nil { - if deps.Options.Timeout > 0 { - reqData["timeout"] = deps.Options.Timeout - } - - if deps.Options.RetryCount > 0 { - reqData["retry_count"] = deps.Options.RetryCount - } - - if deps.Options.Debug { - reqData["debug"] = true - } - } - - return deps.YushanService.CallAPI("YYSY4B37", reqData) -} -``` - -### 2. 组合包处理器中使用 Options - -```go -func ProcessCOMB298YRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { - // ... 参数验证 ... - - // 根据 Options 调整组合策略 - if deps.Options != nil { - if deps.Options.Priority > 0 { - // 高优先级时调整处理策略 - fmt.Printf("组合包处理优先级: %d\n", deps.Options.Priority) - } - - if deps.Options.Debug { - fmt.Printf("组合包调试模式开启\n") - } - } - - // Options 会自动传递给所有子处理器 - return deps.CombService.ProcessCombRequest(ctx, params, deps, "COMB298Y") -} -``` - -### 3. 客户端调用示例 - -```json -{ - "data": "加密的请求数据", - "options": { - "debug": true, - "timeout": 30, - "retry_count": 3, - "priority": 5, - "cache": true, - "log_level": "debug" - } -} -``` - -## 最佳实践 - -### 1. 空值检查 -始终检查 `deps.Options` 是否为 `nil`,避免空指针异常: - -```go -if deps.Options != nil { - // 使用 Options -} -``` - -### 2. 默认值处理 -为 Options 字段提供合理的默认值: - -```go -timeout := 30 // 默认30秒 -if deps.Options != nil && deps.Options.Timeout > 0 { - timeout = deps.Options.Timeout -} -``` - -### 3. 验证选项值 -对 Options 中的值进行验证: - -```go -if deps.Options != nil { - if deps.Options.Priority < 1 || deps.Options.Priority > 10 { - return nil, fmt.Errorf("优先级必须在1-10之间") - } -} -``` - -### 4. 日志记录 -在调试模式下记录 Options 信息: - -```go -if deps.Options != nil && deps.Options.Debug { - log.Printf("处理器选项: %+v", deps.Options) -} -``` - -## 扩展性 - -### 1. 添加新的 Options 字段 -在 `ApiCallOptions` 结构体中添加新字段: - -```go -type ApiCallOptions struct { - // ... 现有字段 ... - CustomField string `json:"custom_field,omitempty"` // 自定义字段 -} -``` - -### 2. 处理器特定选项 -可以为特定处理器创建专门的选项结构: - -```go -type YYSYOptions struct { - ApiCallOptions - YYSYSpecific bool `json:"yysy_specific,omitempty"` -} -``` - -## 注意事项 - -1. **性能影响**: Options 传递会增加少量性能开销,但影响微乎其微 -2. **向后兼容**: 新增的 Options 字段应该使用 `omitempty` 标签 -3. **安全性**: 敏感配置不应该通过 Options 传递 -4. **文档化**: 新增 Options 字段时应该更新文档 - -## 调试技巧 - -### 1. 启用调试模式 -```json -{ - "options": { - "debug": true, - "log_level": "debug" - } -} -``` - -### 2. 查看 Options 传递 -在处理器中添加日志: - -```go -if deps.Options != nil { - log.Printf("处理器 %s 收到选项: %+v", "YYSY4B37", deps.Options) -} -``` - -### 3. 组合包调试 -组合包处理器会自动将 Options 传递给所有子处理器,无需额外配置。 \ No newline at end of file diff --git a/internal/domains/api/services/processors/README.md b/internal/domains/api/services/processors/README.md deleted file mode 100644 index 33fb300..0000000 --- a/internal/domains/api/services/processors/README.md +++ /dev/null @@ -1,155 +0,0 @@ -# API处理器架构说明 - -## 概述 - -本目录实现了基于依赖注入容器的API处理器架构,支持灵活的依赖管理和组合调用模式。 - -## 架构模式 - -### 1. 依赖注入容器模式 - -#### ProcessorDependencies -```go -type ProcessorDependencies struct { - WestDexService *westdex.WestDexService - YushanService *yushan.YushanService - Validator interfaces.RequestValidator -} -``` - -**优势:** -- 统一的依赖管理 -- 类型安全的依赖注入 -- 易于测试和mock -- 支持未来扩展新的服务 - -#### 处理器函数签名 -```go -type ProcessorFunc func(ctx context.Context, params []byte, deps *ProcessorDependencies) ([]byte, error) -``` - -### 2. 组合处理器模式 - -#### CompositeProcessor -专门用于处理组合包(COMB系列)的处理器,支持: -- 动态注册其他处理器 -- 批量调用多个处理器 -- 结果组合和格式化 - -```go -type CompositeProcessor struct { - processors map[string]ProcessorFunc - deps *ProcessorDependencies -} -``` - -## 目录结构 - -``` -processors/ -├── dependencies.go # 依赖容器定义 -├── comb/ -│ ├── comb_processor.go # 组合处理器基类 -│ └── comb298y_processor.go # COMB298Y组合处理器 -├── flxg/ # FLXG系列处理器 -├── jrzq/ # JRZQ系列处理器 -├── qygl/ # QYGL系列处理器 -├── yysy/ # YYSY系列处理器 -└── ivyz/ # IVYZ系列处理器 -``` - -## 服务分配策略 - -### WestDexService -- FLXG系列:使用WestDexService -- JRZQ系列:使用WestDexService -- IVYZ系列:使用WestDexService - -### YushanService -- QYGL系列:使用YushanService -- YYSY系列:使用YushanService - -### 组合包(COMB) -- 调用多个其他处理器 -- 组合结果并返回统一格式 - -## 使用示例 - -### 普通处理器 -```go -func ProcessFLXG0V3Bequest(ctx context.Context, params []byte, deps *ProcessorDependencies) ([]byte, error) { - // 参数验证 - var paramsDto dto.FLXG0V3BequestReq - if err := json.Unmarshal(params, ¶msDto); err != nil { - return nil, fmt.Errorf("参数校验不正确: 解密后的数据格式错误") - } - - if err := deps.Validator.ValidateStruct(paramsDto); err != nil { - return nil, fmt.Errorf("参数校验不正确: %s", err.Error()) - } - - // 调用服务 - reqData := map[string]interface{}{ - "name": paramsDto.Name, - "idCard": paramsDto.IDCard, - "mobile": paramsDto.Mobile, - } - - respBytes, err := deps.WestDexService.CallAPI("FLXG0V3B", reqData) - if err != nil { - return nil, fmt.Errorf("调用外部服务失败: %s", err.Error()) - } - - return respBytes, nil -} -``` - -### 组合处理器 -```go -func ProcessCOMB298YRequest(ctx context.Context, params []byte, deps *ProcessorDependencies) ([]byte, error) { - // 创建组合处理器 - compositeProcessor := NewCompositeProcessor(deps) - - // 注册需要调用的处理器 - compositeProcessor.RegisterProcessor("FLXG0V3B", flxg.ProcessFLXG0V3Bequest) - compositeProcessor.RegisterProcessor("JRZQ8203", jrzq.ProcessJRZQ8203Request) - - // 调用并组合结果 - results := make(map[string]interface{}) - - flxgResult, err := compositeProcessor.CallProcessor(ctx, "FLXG0V3B", params) - if err != nil { - return nil, fmt.Errorf("调用FLXG0V3B处理器失败: %s", err.Error()) - } - results["flxg0v3b"] = string(flxgResult) - - // 返回组合结果 - return compositeProcessor.CombineResults(results) -} -``` - -## 扩展指南 - -### 添加新的服务依赖 -1. 在 `ProcessorDependencies` 中添加新字段 -2. 更新 `NewProcessorDependencies` 构造函数 -3. 在 `ApiRequestService` 中注入新服务 - -### 添加新的处理器 -1. 在对应目录下创建新的处理器文件 -2. 实现 `ProcessorFunc` 接口 -3. 在 `RequestProcessors` 映射中注册 - -### 添加新的组合包 -1. 在 `comb/` 目录下创建新的组合处理器 -2. 使用 `CompositeProcessor` 基类 -3. 注册需要调用的处理器并组合结果 - -## 优势 - -1. **解耦**:处理器与具体服务实现解耦 -2. **可测试**:易于进行单元测试和集成测试 -3. **可扩展**:支持添加新的服务和处理器 -4. **类型安全**:编译时检查依赖关系 -5. **组合支持**:灵活的组合调用模式 -6. **维护性**:清晰的代码结构和职责分离 \ No newline at end of file diff --git a/internal/domains/api/services/processors/UPDATE_SUMMARY.md b/internal/domains/api/services/processors/UPDATE_SUMMARY.md deleted file mode 100644 index 5f5e062..0000000 --- a/internal/domains/api/services/processors/UPDATE_SUMMARY.md +++ /dev/null @@ -1,117 +0,0 @@ -# 处理器文件更新总结 - -## 更新概述 - -已成功将所有36个API处理器文件更新为使用新的依赖注入容器模式。 - -## 更新统计 - -### 已更新的处理器文件总数:36个 - -#### FLXG系列 (12个) -- ✅ flxg0v3b_processor.go -- ✅ flxg0v4b_processor.go -- ✅ flxg162a_processor.go -- ✅ flxg3d56_processor.go -- ✅ flxg54f5_processor.go -- ✅ flxg5876_processor.go -- ✅ flxg75fe_processor.go -- ✅ flxg9687_processor.go -- ✅ flxg970f_processor.go -- ✅ flxgc9d1_processor.go -- ✅ flxgca3d_processor.go -- ✅ flxgdec7_processor.go - -#### JRZQ系列 (4个) -- ✅ jrzq8203_processor.go -- ✅ jrzq0a03_processor.go -- ✅ jrzq4aa8_processor.go -- ✅ jrzqdcbe_processor.go - -#### QYGL系列 (6个) -- ✅ qygl8261_processor.go -- ✅ qygl2acd_processor.go -- ✅ qygl45bd_processor.go -- ✅ qygl6f2d_processor.go -- ✅ qygl8271_processor.go -- ✅ qyglb4c0_processor.go - -#### YYSY系列 (7个) -- ✅ yysyd50f_processor.go -- ✅ yysy09cd_processor.go -- ✅ yysy4b21_processor.go -- ✅ yysy4b37_processor.go -- ✅ yysy6f2e_processor.go -- ✅ yysybe08_processor.go -- ✅ yysyf7db_processor.go - -#### IVYZ系列 (7个) -- ✅ ivyz0b03_processor.go -- ✅ ivyz2125_processor.go -- ✅ ivyz385e_processor.go -- ✅ ivyz5733_processor.go -- ✅ ivyz9363_processor.go -- ✅ ivyz9a2b_processor.go -- ✅ ivyzadee_processor.go - -#### COMB系列 (1个) -- ✅ comb298y_processor.go (组合处理器) - -## 更新内容 - -### 1. 函数签名更新 -所有处理器函数的签名已从: -```go -func ProcessXXXRequest(ctx context.Context, params []byte, validator interfaces.RequestValidator) ([]byte, error) -``` -更新为: -```go -func ProcessXXXRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) -``` - -### 2. 导入更新 -- 移除了 `"tyapi-server/internal/shared/interfaces"` 导入 -- 添加了 `"tyapi-server/internal/domains/api/services/processors"` 导入 - -### 3. 验证器调用更新 -- 从 `validator.ValidateStruct(paramsDto)` -- 更新为 `deps.Validator.ValidateStruct(paramsDto)` - -### 4. 服务调用实现 -根据API前缀分配不同的服务: - -#### WestDexService (FLXG, JRZQ, IVYZ系列) -```go -respBytes, err := deps.WestDexService.CallAPI("API_CODE", reqData) -``` - -#### YushanService (QYGL, YYSY系列) -```go -respBytes, err := deps.WestDexService.CallAPI("API_CODE", reqData) -``` - -### 5. 组合处理器 -COMB298Y处理器实现了组合调用模式: -- 使用 `CompositeProcessor` 基类 -- 动态注册其他处理器 -- 组合多个处理器的结果 - -## 架构优势 - -1. **统一依赖管理**:所有处理器通过 `ProcessorDependencies` 容器访问依赖 -2. **类型安全**:编译时检查依赖关系 -3. **易于测试**:可以轻松mock依赖进行单元测试 -4. **可扩展性**:新增服务只需在容器中添加 -5. **组合支持**:COMB系列支持灵活的组合调用 -6. **维护性**:清晰的代码结构和职责分离 - -## 编译验证 - -✅ 项目编译成功,无语法错误 - -## 下一步建议 - -1. **单元测试**:为各个处理器编写单元测试 -2. **集成测试**:测试实际的API调用流程 -3. **性能测试**:验证新架构的性能表现 -4. **文档完善**:补充API文档和使用说明 \ No newline at end of file diff --git a/internal/domains/api/services/processors/dependencies.go b/internal/domains/api/services/processors/dependencies.go index 9820ba2..308ecd5 100644 --- a/internal/domains/api/services/processors/dependencies.go +++ b/internal/domains/api/services/processors/dependencies.go @@ -3,6 +3,7 @@ package processors import ( "context" "tyapi-server/internal/application/api/commands" + "tyapi-server/internal/infrastructure/external/tianyancha" "tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/yushan" "tyapi-server/internal/shared/interfaces" @@ -15,26 +16,29 @@ type CombServiceInterface interface { // ProcessorDependencies 处理器依赖容器 type ProcessorDependencies struct { - WestDexService *westdex.WestDexService - YushanService *yushan.YushanService - Validator interfaces.RequestValidator - CombService CombServiceInterface // Changed to interface to break import cycle - Options *commands.ApiCallOptions // 添加Options支持 + WestDexService *westdex.WestDexService + YushanService *yushan.YushanService + TianYanChaService *tianyancha.TianYanChaService + Validator interfaces.RequestValidator + CombService CombServiceInterface // Changed to interface to break import cycle + Options *commands.ApiCallOptions // 添加Options支持 } // NewProcessorDependencies 创建处理器依赖容器 func NewProcessorDependencies( westDexService *westdex.WestDexService, yushanService *yushan.YushanService, + tianYanChaService *tianyancha.TianYanChaService, validator interfaces.RequestValidator, combService CombServiceInterface, // Changed to interface ) *ProcessorDependencies { return &ProcessorDependencies{ - WestDexService: westDexService, - YushanService: yushanService, - Validator: validator, - CombService: combService, - Options: nil, // 初始化为nil,在调用时设置 + WestDexService: westDexService, + YushanService: yushanService, + TianYanChaService: tianYanChaService, + Validator: validator, + CombService: combService, + Options: nil, // 初始化为nil,在调用时设置 } } diff --git a/internal/domains/api/services/processors/qygl/qygl23t7_processor.go b/internal/domains/api/services/processors/qygl/qygl23t7_processor.go new file mode 100644 index 0000000..3ffdfc6 --- /dev/null +++ b/internal/domains/api/services/processors/qygl/qygl23t7_processor.go @@ -0,0 +1,140 @@ +package qygl + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/westdex" + + "github.com/tidwall/gjson" +) + +// ProcessQYGL23T7Request QYGL23T7 API处理方法 - 企业三要素验证 +func ProcessQYGL23T7Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.QYGL23T7Req + if err := json.Unmarshal(params, ¶msDto); err != nil { + return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err) + } + + if err := deps.Validator.ValidateStruct(paramsDto); err != nil { + return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err) + } + + // 构建API调用参数 + apiParams := map[string]string{ + "code": paramsDto.EntCode, + "name": paramsDto.EntName, + "legalPersonName": paramsDto.LegalPerson, + } + + // 调用天眼查API - 使用通用的CallAPI方法 + response, err := deps.TianYanChaService.CallAPI(ctx, "VerifyThreeElements", apiParams) + if err != nil { + if err.Error() == "数据源异常" { // Specific error handling for data source issues + return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err) + } else { + return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err) + } + } + + // 检查天眼查API调用是否成功 + if !response.Success { + // 天眼查API调用失败,返回企业信息校验不通过 + return createStatusResponse(1), nil + } + + // 解析天眼查响应数据 + if response.Data == nil { + // 天眼查响应数据为空,返回企业信息校验不通过 + return createStatusResponse(1), nil + } + + // 将response.Data转换为JSON字符串,然后使用gjson解析 + dataBytes, err := json.Marshal(response.Data) + if err != nil { + // 数据序列化失败,返回企业信息校验不通过 + return createStatusResponse(1), nil + } + + // 使用gjson解析嵌套的data.result.data字段 + result := gjson.GetBytes(dataBytes, "result") + if !result.Exists() { + // 字段不存在,返回企业信息校验不通过 + return createStatusResponse(1), nil + } + + // 检查data.result.data是否等于1 + if result.Int() != 1 { + // 不等于1,返回企业信息校验不通过 + return createStatusResponse(1), nil + } + + // 天眼查三要素验证通过,继续调用WestDex身份证二要素验证 + // 加密姓名和身份证号 + encryptedName, err := deps.WestDexService.Encrypt(paramsDto.LegalPerson) + if err != nil { + return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err) + } + + encryptedIDCard, err := deps.WestDexService.Encrypt(paramsDto.EntCode) + if err != nil { + return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err) + } + + // 构建WestDex身份证二要素验证请求参数(参考yysybe08_processor.go) + reqData := map[string]interface{}{ + "data": map[string]interface{}{ + "xM": encryptedName, + "gMSFZHM": encryptedIDCard, + "customerNumber": deps.WestDexService.GetConfig().Key, + "timeStamp": fmt.Sprintf("%d", time.Now().UnixNano()/int64(time.Millisecond)), + }, + } + + // 调用WestDex身份证二要素验证API + respBytes, err := deps.WestDexService.CallAPI("layoutIdcard", reqData) + if err != nil { + if !errors.Is(err, westdex.ErrDatasource) { + return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err) + } + } + + // 使用gjson获取resultCode + resultCode := gjson.GetBytes(respBytes, "ctidRequest.ctidAuth.resultCode") + if !resultCode.Exists() { + return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err) + } + + // 获取resultCode的第一个字符 + resultCodeStr := resultCode.String() + if len(resultCodeStr) == 0 { + return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err) + } + + firstChar := string(resultCodeStr[0]) + if firstChar != "0" && firstChar != "5" { + return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err) + } + if firstChar == "0" { + return createStatusResponse(0), nil + } else if firstChar == "5" { + return createStatusResponse(2), nil + } else { + return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err) + } +} + +// createStatusResponse 创建状态响应 +func createStatusResponse(status int) []byte { + response := map[string]interface{}{ + "status": status, + } + + respBytes, _ := json.Marshal(response) + return respBytes +} diff --git a/internal/domains/api/services/processors/yysy/yysybe08_processor.go b/internal/domains/api/services/processors/yysy/yysybe08_processor.go index 00db58f..1f6b176 100644 --- a/internal/domains/api/services/processors/yysy/yysybe08_processor.go +++ b/internal/domains/api/services/processors/yysy/yysybe08_processor.go @@ -32,24 +32,22 @@ func ProcessYYSYBE08Request(ctx context.Context, params []byte, deps *processors if err != nil { return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err) } - + reqData := map[string]interface{}{ "data": map[string]interface{}{ - "xM": encryptedName, - "gMSFZHM": encryptedIDCard, - "customerNumber": deps.WestDexService.GetConfig().Key, - "timeStamp":fmt.Sprintf("%d", time.Now().UnixNano()/int64(time.Millisecond)), + "xM": encryptedName, + "gMSFZHM": encryptedIDCard, + "customerNumber": deps.WestDexService.GetConfig().Key, + "timeStamp": fmt.Sprintf("%d", time.Now().UnixNano()/int64(time.Millisecond)), }, } respBytes, err := deps.WestDexService.CallAPI("layoutIdcard", reqData) if err != nil { - if errors.Is(err, westdex.ErrDatasource) { - return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err) - } else { + if !errors.Is(err, westdex.ErrDatasource) { return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err) } } return respBytes, nil -} \ No newline at end of file +} diff --git a/internal/domains/certification/entities/certification.go b/internal/domains/certification/entities/certification.go index 34fd35b..21c119d 100644 --- a/internal/domains/certification/entities/certification.go +++ b/internal/domains/certification/entities/certification.go @@ -463,9 +463,7 @@ func (c *Certification) GetDataByStatus() map[string]interface{} { case enums.StatusContractApplied: data["contract_sign_url"] = c.ContractSignURL case enums.StatusContractSigned: - data["contract_url"] = c.ContractURL case enums.StatusCompleted: - data["contract_url"] = c.ContractURL data["completed_at"] = c.CompletedAt case enums.StatusContractRejected: data["failure_reason"] = c.FailureReason diff --git a/internal/domains/certification/entities/enterprise_info_submit_record.go b/internal/domains/certification/entities/enterprise_info_submit_record.go index c0c558e..0f0d249 100644 --- a/internal/domains/certification/entities/enterprise_info_submit_record.go +++ b/internal/domains/certification/entities/enterprise_info_submit_record.go @@ -19,7 +19,6 @@ type EnterpriseInfoSubmitRecord struct { LegalPersonID string `json:"legal_person_id" gorm:"type:varchar(50);not null"` LegalPersonPhone string `json:"legal_person_phone" gorm:"type:varchar(50);not null"` EnterpriseAddress string `json:"enterprise_address" gorm:"type:varchar(200);not null"` // 新增企业地址 - EnterpriseEmail string `json:"enterprise_email" gorm:"type:varchar(100);not null"` // 企业邮箱 // 提交状态 Status string `json:"status" gorm:"type:varchar(20);not null;default:'submitted'"` // submitted, verified, failed SubmitAt time.Time `json:"submit_at" gorm:"not null"` @@ -40,7 +39,7 @@ func (EnterpriseInfoSubmitRecord) TableName() string { // NewEnterpriseInfoSubmitRecord 创建新的企业信息提交记录 func NewEnterpriseInfoSubmitRecord( - userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail string, + userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string, ) *EnterpriseInfoSubmitRecord { return &EnterpriseInfoSubmitRecord{ ID: uuid.New().String(), @@ -51,7 +50,6 @@ func NewEnterpriseInfoSubmitRecord( LegalPersonID: legalPersonID, LegalPersonPhone: legalPersonPhone, EnterpriseAddress: enterpriseAddress, - EnterpriseEmail: enterpriseEmail, Status: "submitted", SubmitAt: time.Now(), CreatedAt: time.Now(), diff --git a/internal/domains/certification/entities/value_objects/enterprise_info.go b/internal/domains/certification/entities/value_objects/enterprise_info.go index 1046877..8dd8482 100644 --- a/internal/domains/certification/entities/value_objects/enterprise_info.go +++ b/internal/domains/certification/entities/value_objects/enterprise_info.go @@ -22,11 +22,10 @@ type EnterpriseInfo struct { // 企业详细信息 RegisteredAddress string `json:"registered_address"` // 注册地址 EnterpriseAddress string `json:"enterprise_address"` // 企业地址(新增) - EnterpriseEmail string `json:"enterprise_email"` // 企业邮箱 } // NewEnterpriseInfo 创建企业信息值对象 -func NewEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail string) (*EnterpriseInfo, error) { +func NewEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string) (*EnterpriseInfo, error) { info := &EnterpriseInfo{ CompanyName: strings.TrimSpace(companyName), UnifiedSocialCode: strings.TrimSpace(unifiedSocialCode), @@ -34,7 +33,6 @@ func NewEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPer LegalPersonID: strings.TrimSpace(legalPersonID), LegalPersonPhone: strings.TrimSpace(legalPersonPhone), EnterpriseAddress: strings.TrimSpace(enterpriseAddress), - EnterpriseEmail: strings.TrimSpace(enterpriseEmail), } if err := info.Validate(); err != nil { @@ -70,10 +68,6 @@ func (e *EnterpriseInfo) Validate() error { return err } - if err := e.validateEnterpriseEmail(); err != nil { - return err - } - return nil } @@ -237,27 +231,6 @@ func (e *EnterpriseInfo) validateEnterpriseAddress() error { return nil } -// validateEnterpriseEmail 验证企业邮箱 -func (e *EnterpriseInfo) validateEnterpriseEmail() error { - if strings.TrimSpace(e.EnterpriseEmail) == "" { - return errors.New("企业邮箱不能为空") - } - if len(e.EnterpriseEmail) < 5 { - return errors.New("企业邮箱长度不能少于5个字符") - } - if len(e.EnterpriseEmail) > 100 { - return errors.New("企业邮箱长度不能超过100个字符") - } - - // 邮箱格式验证 - emailPattern := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) - if !emailPattern.MatchString(e.EnterpriseEmail) { - return errors.New("企业邮箱格式不正确") - } - - return nil -} - // IsComplete 检查企业信息是否完整 func (e *EnterpriseInfo) IsComplete() bool { return e.CompanyName != "" && @@ -265,8 +238,7 @@ func (e *EnterpriseInfo) IsComplete() bool { e.LegalPersonName != "" && e.LegalPersonID != "" && e.LegalPersonPhone != "" && - e.EnterpriseAddress != "" && - e.EnterpriseEmail != "" + e.EnterpriseAddress != "" } // IsDetailComplete 检查企业详细信息是否完整 @@ -324,8 +296,7 @@ func (e *EnterpriseInfo) Equals(other *EnterpriseInfo) bool { e.UnifiedSocialCode == other.UnifiedSocialCode && e.LegalPersonName == other.LegalPersonName && e.LegalPersonID == other.LegalPersonID && - e.LegalPersonPhone == other.LegalPersonPhone && - e.EnterpriseEmail == other.EnterpriseEmail + e.LegalPersonPhone == other.LegalPersonPhone } // Clone 创建企业信息的副本 @@ -338,7 +309,6 @@ func (e *EnterpriseInfo) Clone() *EnterpriseInfo { LegalPersonPhone: e.LegalPersonPhone, RegisteredAddress: e.RegisteredAddress, EnterpriseAddress: e.EnterpriseAddress, - EnterpriseEmail: e.EnterpriseEmail, } } @@ -360,7 +330,6 @@ func (e *EnterpriseInfo) ToMap() map[string]interface{} { "legal_person_phone": e.LegalPersonPhone, "registered_address": e.RegisteredAddress, "enterprise_address": e.EnterpriseAddress, - "enterprise_email": e.EnterpriseEmail, } } @@ -383,7 +352,6 @@ func FromMap(data map[string]interface{}) (*EnterpriseInfo, error) { LegalPersonPhone: getString("legal_person_phone"), RegisteredAddress: getString("registered_address"), EnterpriseAddress: getString("enterprise_address"), - EnterpriseEmail: getString("enterprise_email"), } if err := info.Validate(); err != nil { diff --git a/internal/domains/certification/services/enterprise_info_submit_record_service.go b/internal/domains/certification/services/enterprise_info_submit_record_service.go index 2132b9c..00b5d88 100644 --- a/internal/domains/certification/services/enterprise_info_submit_record_service.go +++ b/internal/domains/certification/services/enterprise_info_submit_record_service.go @@ -58,12 +58,12 @@ func (s *EnterpriseInfoSubmitRecordService) ValidateWithWestdex(ctx context.Cont } // 开发环境下跳过外部验证 - // if s.appConfig.IsDevelopment() { - // s.logger.Info("开发环境:跳过企业信息外部验证", - // zap.String("company_name", info.CompanyName), - // zap.String("legal_person", info.LegalPersonName)) - // return nil - // } + if s.appConfig.IsDevelopment() { + s.logger.Info("开发环境:跳过企业信息外部验证", + zap.String("company_name", info.CompanyName), + zap.String("legal_person", info.LegalPersonName)) + return nil + } encryptedEntName, err := s.westdexService.Encrypt(info.CompanyName) if err != nil { return fmt.Errorf("%s: %w", "企业四要素验证", err) diff --git a/internal/domains/user/entities/enterprise_info.go b/internal/domains/user/entities/enterprise_info.go index fda991c..9c0e860 100644 --- a/internal/domains/user/entities/enterprise_info.go +++ b/internal/domains/user/entities/enterprise_info.go @@ -24,8 +24,6 @@ type EnterpriseInfo struct { LegalPersonID string `gorm:"type:varchar(50);not null" json:"legal_person_id" comment:"法定代表人身份证号"` LegalPersonPhone string `gorm:"type:varchar(50);not null" json:"legal_person_phone" comment:"法定代表人手机号"` EnterpriseAddress string `json:"enterprise_address" gorm:"type:varchar(200);not null" comment:"企业地址"` - EnterpriseEmail string `json:"enterprise_email" gorm:"type:varchar(100);not null" comment:"企业邮箱"` - // 时间戳字段 CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"` @@ -54,7 +52,7 @@ func (e *EnterpriseInfo) BeforeCreate(tx *gorm.DB) error { // ================ 工厂方法 ================ // NewEnterpriseInfo 创建新的企业信息 -func NewEnterpriseInfo(userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone,enterpriseAddress, enterpriseEmail string) (*EnterpriseInfo, error) { +func NewEnterpriseInfo(userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone,enterpriseAddress string) (*EnterpriseInfo, error) { if userID == "" { return nil, fmt.Errorf("用户ID不能为空") } @@ -76,9 +74,6 @@ func NewEnterpriseInfo(userID, companyName, unifiedSocialCode, legalPersonName, if enterpriseAddress == "" { return nil, fmt.Errorf("企业地址不能为空") } - if enterpriseEmail == "" { - return nil, fmt.Errorf("企业邮箱不能为空") - } enterpriseInfo := &EnterpriseInfo{ ID: uuid.New().String(), @@ -89,7 +84,6 @@ func NewEnterpriseInfo(userID, companyName, unifiedSocialCode, legalPersonName, LegalPersonID: legalPersonID, LegalPersonPhone: legalPersonPhone, EnterpriseAddress: enterpriseAddress, - EnterpriseEmail: enterpriseEmail, domainEvents: make([]interface{}, 0), } @@ -108,7 +102,7 @@ func NewEnterpriseInfo(userID, companyName, unifiedSocialCode, legalPersonName, // ================ 聚合根核心方法 ================ // UpdateEnterpriseInfo 更新企业信息 -func (e *EnterpriseInfo) UpdateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail string) error { +func (e *EnterpriseInfo) UpdateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string) error { // 验证输入参数 if companyName == "" { return fmt.Errorf("企业名称不能为空") @@ -128,9 +122,6 @@ func (e *EnterpriseInfo) UpdateEnterpriseInfo(companyName, unifiedSocialCode, le if enterpriseAddress == "" { return fmt.Errorf("企业地址不能为空") } - if enterpriseEmail == "" { - return fmt.Errorf("企业邮箱不能为空") - } // 记录原始值用于事件 oldCompanyName := e.CompanyName @@ -143,7 +134,6 @@ func (e *EnterpriseInfo) UpdateEnterpriseInfo(companyName, unifiedSocialCode, le e.LegalPersonID = legalPersonID e.LegalPersonPhone = legalPersonPhone e.EnterpriseAddress = enterpriseAddress - e.EnterpriseEmail = enterpriseEmail // 添加领域事件 e.addDomainEvent(&EnterpriseInfoUpdatedEvent{ @@ -198,10 +188,6 @@ func (e *EnterpriseInfo) validateBasicFields() error { if e.LegalPersonPhone == "" { return fmt.Errorf("法定代表人手机号不能为空") } - if e.EnterpriseEmail == "" { - return fmt.Errorf("企业邮箱不能为空") - } - // 统一社会信用代码格式验证 if !e.isValidUnifiedSocialCode(e.UnifiedSocialCode) { return fmt.Errorf("统一社会信用代码格式无效") @@ -217,11 +203,6 @@ func (e *EnterpriseInfo) validateBasicFields() error { return fmt.Errorf("法定代表人手机号格式无效") } - // 邮箱格式验证 (简单示例,实际应更严格) - if !e.isValidEmail(e.EnterpriseEmail) { - return fmt.Errorf("企业邮箱格式无效") - } - return nil } @@ -237,11 +218,6 @@ func (e *EnterpriseInfo) validateBusinessLogic() error { return fmt.Errorf("法定代表人姓名长度不能超过100个字符") } - // 企业邮箱格式验证 (简单示例,实际应更严格) - if !e.isValidEmail(e.EnterpriseEmail) { - return fmt.Errorf("企业邮箱格式无效") - } - return nil } @@ -255,8 +231,7 @@ func (e *EnterpriseInfo) IsComplete() bool { e.UnifiedSocialCode != "" && e.LegalPersonName != "" && e.LegalPersonID != "" && - e.LegalPersonPhone != "" && - e.EnterpriseEmail != "" + e.LegalPersonPhone != "" } // GetCertificationProgress 获取认证进度 diff --git a/internal/domains/user/entities/user.go b/internal/domains/user/entities/user.go index 8db9dac..c99bd45 100644 --- a/internal/domains/user/entities/user.go +++ b/internal/domains/user/entities/user.go @@ -102,14 +102,14 @@ func (u *User) CompleteCertification() error { // ================ 企业信息管理方法 ================ // CreateEnterpriseInfo 创建企业信息 -func (u *User) CreateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail string) error { +func (u *User) CreateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string) error { // 检查是否已有企业信息 if u.EnterpriseInfo != nil { return fmt.Errorf("用户已有企业信息") } // 创建企业信息实体 - enterpriseInfo, err := NewEnterpriseInfo(u.ID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail) + enterpriseInfo, err := NewEnterpriseInfo(u.ID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress) if err != nil { return fmt.Errorf("创建企业信息失败: %w", err) } @@ -130,7 +130,7 @@ func (u *User) CreateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonN } // UpdateEnterpriseInfo 更新企业信息 -func (u *User) UpdateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail string) error { +func (u *User) UpdateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string) error { // 检查是否有企业信息 if u.EnterpriseInfo == nil { return fmt.Errorf("用户暂无企业信息") @@ -141,7 +141,7 @@ func (u *User) UpdateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonN oldUnifiedSocialCode := u.EnterpriseInfo.UnifiedSocialCode // 更新企业信息 - err := u.EnterpriseInfo.UpdateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail) + err := u.EnterpriseInfo.UpdateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress) if err != nil { return err } diff --git a/internal/domains/user/services/user_aggregate_service.go b/internal/domains/user/services/user_aggregate_service.go index e893764..f4797ed 100644 --- a/internal/domains/user/services/user_aggregate_service.go +++ b/internal/domains/user/services/user_aggregate_service.go @@ -36,14 +36,14 @@ type UserAggregateService interface { GetUserStats(ctx context.Context) (*repositories.UserStats, error) // 企业信息管理 - CreateEnterpriseInfo(ctx context.Context, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail string) error - UpdateEnterpriseInfo(ctx context.Context, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail string) error + CreateEnterpriseInfo(ctx context.Context, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string) error + UpdateEnterpriseInfo(ctx context.Context, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string) error GetUserWithEnterpriseInfo(ctx context.Context, userID string) (*entities.User, error) ValidateEnterpriseInfo(ctx context.Context, userID string) error CheckUnifiedSocialCodeExists(ctx context.Context, unifiedSocialCode string, excludeUserID string) (bool, error) // 认证域专用:写入/覆盖企业信息 - CreateOrUpdateEnterpriseInfo(ctx context.Context, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail string) error + CreateOrUpdateEnterpriseInfo(ctx context.Context, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string) error CompleteCertification(ctx context.Context, userID string) error } @@ -122,7 +122,7 @@ func (s *UserAggregateServiceImpl) LoadUser(ctx context.Context, userID string) // 验证业务规则 if err := s.ValidateBusinessRules(ctx, &user); err != nil { - s.logger.Warn("用户业务规则验证失败", + s.logger.Warn("用户业务规则验证失败", zap.String("user_id", userID), zap.Error(err), ) @@ -186,7 +186,7 @@ func (s *UserAggregateServiceImpl) LoadUserByPhone(ctx context.Context, phone st // 验证业务规则 if err := s.ValidateBusinessRules(ctx, user); err != nil { - s.logger.Warn("用户业务规则验证失败", + s.logger.Warn("用户业务规则验证失败", zap.String("phone", phone), zap.Error(err), ) @@ -303,7 +303,7 @@ func (s *UserAggregateServiceImpl) UpdateLoginStats(ctx context.Context, userID // ================ 企业信息管理 ================ // CreateEnterpriseInfo 创建企业信息 -func (s *UserAggregateServiceImpl) CreateEnterpriseInfo(ctx context.Context, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail string) error { +func (s *UserAggregateServiceImpl) CreateEnterpriseInfo(ctx context.Context, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string) error { s.logger.Debug("创建企业信息", zap.String("user_id", userID)) // 1. 加载用户聚合根 @@ -327,7 +327,7 @@ func (s *UserAggregateServiceImpl) CreateEnterpriseInfo(ctx context.Context, use } // 4. 使用聚合根方法创建企业信息 - err = user.CreateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail) + err = user.CreateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress) if err != nil { return fmt.Errorf("创建企业信息失败: %w", err) } @@ -349,7 +349,7 @@ func (s *UserAggregateServiceImpl) CreateEnterpriseInfo(ctx context.Context, use } // UpdateEnterpriseInfo 更新企业信息 -func (s *UserAggregateServiceImpl) UpdateEnterpriseInfo(ctx context.Context, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail string) error { +func (s *UserAggregateServiceImpl) UpdateEnterpriseInfo(ctx context.Context, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string) error { s.logger.Debug("更新企业信息", zap.String("user_id", userID)) // 1. 加载用户聚合根 @@ -373,7 +373,7 @@ func (s *UserAggregateServiceImpl) UpdateEnterpriseInfo(ctx context.Context, use } // 4. 使用聚合根方法更新企业信息 - err = user.UpdateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail) + err = user.UpdateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress) if err != nil { return fmt.Errorf("更新企业信息失败: %w", err) } @@ -394,7 +394,6 @@ func (s *UserAggregateServiceImpl) UpdateEnterpriseInfo(ctx context.Context, use return nil } - // GetUserWithEnterpriseInfo 获取用户信息(包含企业信息) func (s *UserAggregateServiceImpl) GetUserWithEnterpriseInfo(ctx context.Context, userID string) (*entities.User, error) { s.logger.Debug("获取用户信息(包含企业信息)", zap.String("user_id", userID)) @@ -407,7 +406,7 @@ func (s *UserAggregateServiceImpl) GetUserWithEnterpriseInfo(ctx context.Context // 验证业务规则 if err := s.ValidateBusinessRules(ctx, &user); err != nil { - s.logger.Warn("用户业务规则验证失败", + s.logger.Warn("用户业务规则验证失败", zap.String("user_id", userID), zap.Error(err), ) @@ -437,7 +436,7 @@ func (s *UserAggregateServiceImpl) ValidateEnterpriseInfo(ctx context.Context, u // CheckUnifiedSocialCodeExists 检查统一社会信用代码是否存在 func (s *UserAggregateServiceImpl) CheckUnifiedSocialCodeExists(ctx context.Context, unifiedSocialCode string, excludeUserID string) (bool, error) { - s.logger.Debug("检查统一社会信用代码是否存在", + s.logger.Debug("检查统一社会信用代码是否存在", zap.String("unified_social_code", unifiedSocialCode), zap.String("exclude_user_id", excludeUserID), ) @@ -455,12 +454,12 @@ func (s *UserAggregateServiceImpl) CheckUnifiedSocialCodeExists(ctx context.Cont } if exists { - s.logger.Info("统一社会信用代码已存在", + s.logger.Info("统一社会信用代码已存在", zap.String("unified_social_code", unifiedSocialCode), zap.String("exclude_user_id", excludeUserID), ) } else { - s.logger.Debug("统一社会信用代码不存在", + s.logger.Debug("统一社会信用代码不存在", zap.String("unified_social_code", unifiedSocialCode), ) } @@ -471,20 +470,20 @@ func (s *UserAggregateServiceImpl) CheckUnifiedSocialCodeExists(ctx context.Cont // CreateOrUpdateEnterpriseInfo 认证域专用:写入/覆盖企业信息 func (s *UserAggregateServiceImpl) CreateOrUpdateEnterpriseInfo( ctx context.Context, - userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail string, + userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string, ) error { user, err := s.LoadUser(ctx, userID) if err != nil { return fmt.Errorf("用户不存在: %w", err) } if user.EnterpriseInfo == nil { - enterpriseInfo, err := entities.NewEnterpriseInfo(userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail) + enterpriseInfo, err := entities.NewEnterpriseInfo(userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress) if err != nil { return err } user.EnterpriseInfo = enterpriseInfo } else { - err := user.EnterpriseInfo.UpdateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress, enterpriseEmail) + err := user.EnterpriseInfo.UpdateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress) if err != nil { return err } @@ -504,7 +503,7 @@ func (s *UserAggregateServiceImpl) CompleteCertification(ctx context.Context, us // ListUsers 获取用户列表 func (s *UserAggregateServiceImpl) ListUsers(ctx context.Context, query *queries.ListUsersQuery) ([]*entities.User, int64, error) { - s.logger.Debug("获取用户列表", + s.logger.Debug("获取用户列表", zap.Int("page", query.Page), zap.Int("page_size", query.PageSize), ) @@ -516,7 +515,7 @@ func (s *UserAggregateServiceImpl) ListUsers(ctx context.Context, query *queries return nil, 0, fmt.Errorf("查询用户列表失败: %w", err) } - s.logger.Info("用户列表查询成功", + s.logger.Info("用户列表查询成功", zap.Int("count", len(users)), zap.Int64("total", total), ) @@ -535,7 +534,7 @@ func (s *UserAggregateServiceImpl) GetUserStats(ctx context.Context) (*repositor return nil, fmt.Errorf("查询用户统计信息失败: %w", err) } - s.logger.Info("用户统计信息查询成功", + s.logger.Info("用户统计信息查询成功", zap.Int64("total_users", stats.TotalUsers), zap.Int64("active_users", stats.ActiveUsers), zap.Int64("certified_users", stats.CertifiedUsers), @@ -556,7 +555,7 @@ func (s *UserAggregateServiceImpl) publishDomainEvents(ctx context.Context, user for _, event := range events { // 这里需要将领域事件转换为标准事件格式 // 暂时跳过,后续可以完善事件转换逻辑 - s.logger.Debug("发布领域事件", + s.logger.Debug("发布领域事件", zap.String("user_id", user.ID), zap.Any("event", event), ) @@ -566,4 +565,4 @@ func (s *UserAggregateServiceImpl) publishDomainEvents(ctx context.Context, user user.ClearDomainEvents() return nil -} \ No newline at end of file +} diff --git a/internal/infrastructure/external/tianyancha/tianyancha_service.go b/internal/infrastructure/external/tianyancha/tianyancha_service.go new file mode 100644 index 0000000..1b8ad64 --- /dev/null +++ b/internal/infrastructure/external/tianyancha/tianyancha_service.go @@ -0,0 +1,147 @@ +package tianyancha + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +var ( + ErrDatasource = errors.New("数据源异常") + ErrNotFound = errors.New("查询为空") + ErrSystem = errors.New("系统异常") + ErrInvalidParam = errors.New("参数错误") +) + +// APIEndpoints 天眼查 API 端点映射 +var APIEndpoints = map[string]string{ + "VerifyThreeElements": "/open/ic/verify/2.0", // 企业三要素验证 +} + +// TianYanChaConfig 天眼查配置 +type TianYanChaConfig struct { + BaseURL string + Token string + Timeout time.Duration +} + +// TianYanChaService 天眼查服务 +type TianYanChaService struct { + config TianYanChaConfig +} + +// APIResponse 标准API响应结构 +type APIResponse struct { + Success bool `json:"success"` + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +// TianYanChaResponse 天眼查原始响应结构 +type TianYanChaResponse struct { + ErrorCode int `json:"error_code"` + Reason string `json:"reason"` + Result interface{} `json:"result"` +} +// NewTianYanChaService 创建天眼查服务实例 +func NewTianYanChaService(baseURL, token string, timeout time.Duration) *TianYanChaService { + if timeout == 0 { + timeout = 30 * time.Second + } + + return &TianYanChaService{ + config: TianYanChaConfig{ + BaseURL: baseURL, + Token: token, + Timeout: timeout, + }, + } +} + +// CallAPI 调用天眼查API - 通用方法,由外部处理器传入具体参数 +func (t *TianYanChaService) CallAPI(ctx context.Context, apiCode string, params map[string]string) (*APIResponse, error) { + // 从映射中获取 API 端点 + endpoint, exists := APIEndpoints[apiCode] + if !exists { + return nil, fmt.Errorf("%w: 未找到 API 代码对应的端点: %s", ErrInvalidParam, apiCode) + } + + // 构建完整 URL + fullURL := strings.TrimRight(t.config.BaseURL, "/") + "/" + strings.TrimLeft(endpoint, "/") + + // 检查 Token 是否配置 + if t.config.Token == "" { + return nil, fmt.Errorf("%w: 天眼查 API Token 未配置", ErrSystem) + } + + // 构建查询参数 + queryParams := url.Values{} + for key, value := range params { + queryParams.Set(key, value) + } + + // 构建完整URL + requestURL := fullURL + if len(queryParams) > 0 { + requestURL += "?" + queryParams.Encode() + } + + // 创建请求 + req, err := http.NewRequestWithContext(ctx, "GET", requestURL, nil) + if err != nil { + return nil, fmt.Errorf("%w: 创建请求失败: %v", ErrSystem, err) + } + + // 设置请求头 + req.Header.Set("Authorization", t.config.Token) + + // 发送请求 + client := &http.Client{Timeout: t.config.Timeout} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("%w: API 请求异常: %v", ErrDatasource, err) + } + defer resp.Body.Close() + + // 检查 HTTP 状态码 + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%w: API 请求失败,状态码: %d", ErrDatasource, resp.StatusCode) + } + + // 读取响应体 + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("%w: 读取响应体失败: %v", ErrSystem, err) + } + + // 解析 JSON 响应 + var tianYanChaResp TianYanChaResponse + if err := json.Unmarshal(body, &tianYanChaResp); err != nil { + return nil, fmt.Errorf("%w: 解析响应 JSON 失败: %v", ErrSystem, err) + } + + // 检查天眼查业务状态码 + if tianYanChaResp.ErrorCode != 0 { + return &APIResponse{ + Success: false, + Code: tianYanChaResp.ErrorCode, + Message: tianYanChaResp.Reason, + Data: tianYanChaResp.Result, + }, nil + } + + // 成功情况 + return &APIResponse{ + Success: true, + Code: 0, + Message: tianYanChaResp.Reason, + Data: tianYanChaResp.Result, + }, nil +} diff --git a/internal/infrastructure/http/handlers/product_admin_handler.go b/internal/infrastructure/http/handlers/product_admin_handler.go index f7cbce3..a9e1cf9 100644 --- a/internal/infrastructure/http/handlers/product_admin_handler.go +++ b/internal/infrastructure/http/handlers/product_admin_handler.go @@ -258,7 +258,7 @@ func (h *ProductAdminHandler) UpdateSubscriptionPrice(c *gin.Context) { // ListProducts 获取产品列表(管理员) // @Summary 获取产品列表 -// @Description 管理员获取产品列表,支持筛选和分页 +// @Description 管理员获取产品列表,支持筛选和分页,包含所有产品(包括隐藏的) // @Tags 产品管理 // @Accept json // @Produce json @@ -272,7 +272,7 @@ func (h *ProductAdminHandler) UpdateSubscriptionPrice(c *gin.Context) { // @Param is_package query bool false "是否组合包" // @Param sort_by query string false "排序字段" // @Param sort_order query string false "排序方向" Enums(asc, desc) -// @Success 200 {object} responses.ProductListResponse "获取产品列表成功" +// @Success 200 {object} responses.ProductAdminListResponse "获取产品列表成功" // @Failure 400 {object} map[string]interface{} "请求参数错误" // @Failure 401 {object} map[string]interface{} "未认证" // @Failure 500 {object} map[string]interface{} "服务器内部错误" @@ -336,7 +336,8 @@ func (h *ProductAdminHandler) ListProducts(c *gin.Context) { Order: sortOrder, } - result, err := h.productAppService.ListProducts(c.Request.Context(), filters, options) + // 使用管理员专用的产品列表方法 + result, err := h.productAppService.ListProductsForAdmin(c.Request.Context(), filters, options) if err != nil { h.logger.Error("获取产品列表失败", zap.Error(err)) h.responseBuilder.InternalError(c, "获取产品列表失败") @@ -358,13 +359,13 @@ func (h *ProductAdminHandler) getIntQuery(c *gin.Context, key string, defaultVal // GetProductDetail 获取产品详情(管理员) // @Summary 获取产品详情 -// @Description 管理员获取产品详细信息 +// @Description 管理员获取产品详细信息,包含可见状态 // @Tags 产品管理 // @Accept json // @Produce json // @Security Bearer // @Param id path string true "产品ID" -// @Success 200 {object} responses.ProductInfoResponse "获取产品详情成功" +// @Success 200 {object} responses.ProductAdminInfoResponse "获取产品详情成功" // @Failure 400 {object} map[string]interface{} "请求参数错误" // @Failure 401 {object} map[string]interface{} "未认证" // @Failure 404 {object} map[string]interface{} "产品不存在" @@ -379,7 +380,8 @@ func (h *ProductAdminHandler) GetProductDetail(c *gin.Context) { return } - result, err := h.productAppService.GetProductByID(c.Request.Context(), &query) + // 使用管理员专用的产品详情获取方法 + result, err := h.productAppService.GetProductByIDForAdmin(c.Request.Context(), &query) if err != nil { h.logger.Error("获取产品详情失败", zap.Error(err), zap.String("product_id", query.ID)) h.responseBuilder.NotFound(c, "产品不存在") diff --git a/internal/infrastructure/http/handlers/product_handler.go b/internal/infrastructure/http/handlers/product_handler.go index cce1bcc..75eac34 100644 --- a/internal/infrastructure/http/handlers/product_handler.go +++ b/internal/infrastructure/http/handlers/product_handler.go @@ -45,7 +45,7 @@ func NewProductHandler( // ListProducts 获取产品列表(数据大厅) // @Summary 获取产品列表 -// @Description 分页获取可用的产品列表,支持筛选 +// @Description 分页获取可用的产品列表,支持筛选,默认只返回可见的产品 // @Tags 数据大厅 // @Accept json // @Produce json @@ -91,11 +91,14 @@ func (h *ProductHandler) ListProducts(c *gin.Context) { } } - // 可见状态筛选 + // 可见状态筛选 - 用户端默认只显示可见的产品 if isVisible := c.Query("is_visible"); isVisible != "" { if visible, err := strconv.ParseBool(isVisible); err == nil { filters["is_visible"] = visible } + } else { + // 如果没有指定可见状态,默认只显示可见的产品 + filters["is_visible"] = true } // 产品类型筛选 @@ -168,7 +171,7 @@ func (h *ProductHandler) getCurrentUserID(c *gin.Context) string { // GetProductDetail 获取产品详情 // @Summary 获取产品详情 -// @Description 根据产品ID获取产品详细信息 +// @Description 根据产品ID获取产品详细信息,只能获取可见的产品 // @Tags 数据大厅 // @Accept json // @Produce json @@ -187,7 +190,8 @@ func (h *ProductHandler) GetProductDetail(c *gin.Context) { return } - result, err := h.appService.GetProductByID(c.Request.Context(), &query) + // 使用用户端专用的产品详情获取方法 + result, err := h.appService.GetProductByIDForUser(c.Request.Context(), &query) if err != nil { h.logger.Error("获取产品详情失败", zap.Error(err), zap.String("product_id", query.ID)) h.responseBuilder.NotFound(c, "产品不存在") diff --git a/test/product_list_test.go b/test/product_list_test.go deleted file mode 100644 index 7d0093f..0000000 --- a/test/product_list_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package test - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -// TestProductListWithSubscriptionStatus 测试带订阅状态的产品列表功能 -func TestProductListWithSubscriptionStatus(t *testing.T) { - // 这个测试需要完整的应用上下文,包括数据库连接 - // 在实际项目中,这里应该使用测试容器或模拟数据 - - t.Run("测试未认证用户的产品列表", func(t *testing.T) { - // 模拟未认证用户的请求 - filters := map[string]interface{}{ - "keyword": "测试产品", - "is_enabled": true, - "is_visible": true, - } - - // 这里应该调用实际的应用服务 - // 由于没有完整的测试环境,我们只验证参数构建 - assert.NotNil(t, filters) - assert.Equal(t, 1, 1) // options.Page is removed - assert.Equal(t, 10, 10) // options.PageSize is removed - }) - - t.Run("测试已认证用户的产品列表", func(t *testing.T) { - // 模拟已认证用户的请求 - filters := map[string]interface{}{ - "keyword": "测试产品", - "is_enabled": true, - "is_visible": true, - "user_id": "test-user-id", - "is_subscribed": true, // 筛选已订阅的产品 - } - - // 验证筛选条件 - assert.NotNil(t, filters) - assert.Equal(t, "test-user-id", filters["user_id"]) - assert.Equal(t, true, filters["is_subscribed"]) - }) - - t.Run("测试订阅状态筛选", func(t *testing.T) { - // 测试筛选未订阅的产品 - filters := map[string]interface{}{ - "user_id": "test-user-id", - "is_subscribed": false, - } - - // 验证筛选条件 - assert.Equal(t, false, filters["is_subscribed"]) - }) -} - -// TestProductResponseWithSubscriptionStatus 测试产品响应中的订阅状态字段 -func TestProductResponseWithSubscriptionStatus(t *testing.T) { - t.Run("测试产品响应结构", func(t *testing.T) { - // 这里应该测试ProductInfoResponse结构是否包含IsSubscribed字段 - // 由于这是结构体定义,我们只需要确保字段存在 - assert.True(t, true, "ProductInfoResponse应该包含IsSubscribed字段") - }) -} diff --git a/test/product_subscription_filter_test.go b/test/product_subscription_filter_test.go deleted file mode 100644 index a3fa8b2..0000000 --- a/test/product_subscription_filter_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package test - -import ( - "testing" -) - -// TestProductSubscriptionFilter 测试产品订阅状态筛选功能 -func TestProductSubscriptionFilter(t *testing.T) { - // 测试筛选条件构建 - filters := map[string]interface{}{ - "category_id": "test-category", - "is_package": false, - "is_subscribed": true, - "keyword": "test", - "user_id": "test-user", - } - - // 验证筛选条件 - if filters["category_id"] != "test-category" { - t.Errorf("Expected category_id to be 'test-category', got %v", filters["category_id"]) - } - - if filters["is_subscribed"] != true { - t.Errorf("Expected is_subscribed to be true, got %v", filters["is_subscribed"]) - } - - if filters["user_id"] != "test-user" { - t.Errorf("Expected user_id to be 'test-user', got %v", filters["user_id"]) - } - - t.Log("Product subscription filter test passed") -} - -// TestProductResponseStructure 测试产品响应结构 -func TestProductResponseStructure(t *testing.T) { - // 模拟产品响应结构 - type ProductInfoResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Code string `json:"code"` - Description string `json:"description"` - Price float64 `json:"price"` - IsEnabled bool `json:"is_enabled"` - IsPackage bool `json:"is_package"` - IsSubscribed *bool `json:"is_subscribed,omitempty"` - // 注意:IsVisible 字段已被移除 - } - - // 验证结构体字段 - product := ProductInfoResponse{ - ID: "test-id", - Name: "Test Product", - Code: "TEST001", - Description: "Test Description", - Price: 99.99, - IsEnabled: true, - IsPackage: false, - } - - // 验证必要字段存在 - if product.ID == "" { - t.Error("Product ID should not be empty") - } - - if product.Name == "" { - t.Error("Product name should not be empty") - } - - if product.IsSubscribed != nil { - t.Log("IsSubscribed field is present and optional") - } - - t.Log("Product response structure test passed") -} \ No newline at end of file