From 05b6623e75c783cf0dcc4d4256f4e44c4147205e Mon Sep 17 00:00:00 2001 From: 18278715334 <18278715334@163.com> Date: Fri, 5 Dec 2025 14:59:23 +0800 Subject: [PATCH] 18278715334@163.com --- go.mod | 1 + go.sum | 2 + .../subscription_application_service_impl.go | 21 +- internal/domains/api/dto/api_request_dto.go | 12 + .../api/repositories/api_call_repository.go | 3 + .../api/services/api_request_service.go | 2 + .../api/services/form_config_service.go | 7 + .../processors/ivyz/ivyz2b2t_processor.go | 58 ++++ .../processors/ivyz/ivyz5a9o_processor.go | 57 ++++ .../processors/jrzq/jrzq4b6c_processor.go | 1 - .../api/gorm_api_call_repository.go | 31 ++- .../gorm_recharge_record_repository.go | 148 +++++++++-- .../gorm_wallet_transaction_repository.go | 18 +- .../external/zhicha/zhicha_service.go | 13 +- .../http/handlers/product_admin_handler.go | 63 ++++- internal/shared/pdf/page_builder.go | 250 ++++++++++++++++++ resources/pdf/后勤服务.txt | 27 ++ 17 files changed, 677 insertions(+), 37 deletions(-) create mode 100644 internal/domains/api/services/processors/ivyz/ivyz2b2t_processor.go create mode 100644 internal/domains/api/services/processors/ivyz/ivyz5a9o_processor.go create mode 100644 resources/pdf/后勤服务.txt diff --git a/go.mod b/go.mod index 733a38b..40fb840 100644 --- a/go.mod +++ b/go.mod @@ -91,6 +91,7 @@ require ( github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.4 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/smartwalle/ncrypto v1.0.4 // indirect github.com/smartwalle/ngx v1.0.9 // indirect github.com/smartwalle/nsign v1.0.9 // indirect diff --git a/go.sum b/go.sum index 5e821d0..ff8fa28 100644 --- a/go.sum +++ b/go.sum @@ -208,6 +208,8 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/smartwalle/alipay/v3 v3.2.25 h1:cRDN+fpDWTVHnuHIF/vsJETskRXS/S+fDOdAkzXmV/Q= github.com/smartwalle/alipay/v3 v3.2.25/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE= github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8= diff --git a/internal/application/product/subscription_application_service_impl.go b/internal/application/product/subscription_application_service_impl.go index f687b37..6e8794c 100644 --- a/internal/application/product/subscription_application_service_impl.go +++ b/internal/application/product/subscription_application_service_impl.go @@ -10,6 +10,7 @@ import ( "tyapi-server/internal/application/product/dto/commands" appQueries "tyapi-server/internal/application/product/dto/queries" "tyapi-server/internal/application/product/dto/responses" + domain_api_repo "tyapi-server/internal/domains/api/repositories" "tyapi-server/internal/domains/product/entities" repoQueries "tyapi-server/internal/domains/product/repositories/queries" product_service "tyapi-server/internal/domains/product/services" @@ -21,6 +22,7 @@ import ( type SubscriptionApplicationServiceImpl struct { productSubscriptionService *product_service.ProductSubscriptionService userRepo user_repositories.UserRepository + apiCallRepository domain_api_repo.ApiCallRepository logger *zap.Logger } @@ -28,11 +30,13 @@ type SubscriptionApplicationServiceImpl struct { func NewSubscriptionApplicationService( productSubscriptionService *product_service.ProductSubscriptionService, userRepo user_repositories.UserRepository, + apiCallRepository domain_api_repo.ApiCallRepository, logger *zap.Logger, ) SubscriptionApplicationService { return &SubscriptionApplicationServiceImpl{ productSubscriptionService: productSubscriptionService, userRepo: userRepo, + apiCallRepository: apiCallRepository, logger: logger, } } @@ -262,17 +266,30 @@ func (s *SubscriptionApplicationServiceImpl) GetProductSubscriptions(ctx context } // GetSubscriptionUsage 获取订阅使用情况 -// 业务流程:1. 获取订阅使用情况 2. 构建响应数据 +// 业务流程:1. 获取订阅信息 2. 根据产品ID和用户ID统计API调用次数 3. 构建响应数据 func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error) { + // 获取订阅信息 subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID) if err != nil { return nil, err } + // 根据用户ID和产品ID统计API调用次数 + apiCallCount, err := s.apiCallRepository.CountByUserIdAndProductId(ctx, subscription.UserID, subscription.ProductID) + if err != nil { + s.logger.Warn("统计API调用次数失败,使用订阅记录中的值", + zap.String("subscription_id", subscriptionID), + zap.String("user_id", subscription.UserID), + zap.String("product_id", subscription.ProductID), + zap.Error(err)) + // 如果统计失败,使用订阅实体中的APIUsed字段作为备选 + apiCallCount = subscription.APIUsed + } + return &responses.SubscriptionUsageResponse{ ID: subscription.ID, ProductID: subscription.ProductID, - APIUsed: subscription.APIUsed, + APIUsed: apiCallCount, }, nil } diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 23d0ba5..a2266fe 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -195,6 +195,18 @@ type IVYZGZ08Req struct { Name string `json:"name" validate:"required,min=1,validName"` } +type IVYZ2B2TReq struct { + IDCard string `json:"id_card" validate:"required,validIDCard"` + Name string `json:"name" validate:"required,min=1,validName"` + QueryReasonId int64 `json:"query_reason_id" validate:"required"` +} + +type IVYZ5A9tReq struct { + IDCard string `json:"id_card" validate:"required,validIDCard"` + Name string `json:"name" validate:"required,min=1,validName"` + AuthAuthorizeFileCode string `json:"auth_authorize_file_code" validate:"required"` +} + type FLXG8A3FReq struct { IDCard string `json:"id_card" validate:"required,validIDCard"` Name string `json:"name" validate:"required,min=1,validName"` diff --git a/internal/domains/api/repositories/api_call_repository.go b/internal/domains/api/repositories/api_call_repository.go index 0d9dcf2..08c93e6 100644 --- a/internal/domains/api/repositories/api_call_repository.go +++ b/internal/domains/api/repositories/api_call_repository.go @@ -25,6 +25,9 @@ type ApiCallRepository interface { // 新增:统计用户API调用次数 CountByUserId(ctx context.Context, userId string) (int64, error) + // 新增:根据用户ID和产品ID统计API调用次数 + CountByUserIdAndProductId(ctx context.Context, userId string, productId string) (int64, error) + // 新增:根据TransactionID查询 FindByTransactionId(ctx context.Context, transactionId string) (*entities.ApiCall, error) diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 08e936b..e2355e5 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -202,6 +202,8 @@ func registerAllProcessors(combService *comb.CombService) { "IVYZ9K2L": ivyz.ProcessIVYZ9K2LRequest, "IVYZ2C1P": ivyz.ProcessIVYZ2C1PRequest, "IVYZP2Q6": ivyz.ProcessIVYZP2Q6Request, + "IVYZ2B2T": ivyz.ProcessIVYZ2B2TRequest, //能力资质核验(学历) + "IVYZ5A9O": ivyz.ProcessIVYZ5A9ORequest, //全国⾃然⼈⻛险评估评分模型 // COMB系列处理器 - 只注册有自定义逻辑的组合包 "COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑:重命名ApiCode diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 02007b7..41a8632 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -195,6 +195,8 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "QYGL5A9T": &dto.QYGL5A9TReq{}, //全国企业各类工商风险统计数量查询 "JRZQ3P01": &dto.JRZQ3P01Req{}, //天远风控决策 "JRZQ3AG6": &dto.JRZQ3AG6Req{}, //轻松查公积 + "IVYZ2B2T": &dto.IVYZ2B2TReq{}, //能力资质核验(学历) + "IVYZ5A9O": &dto.IVYZ5A9tReq{}, //全国⾃然⼈⻛险评估评分模型 } @@ -319,6 +321,7 @@ func (s *FormConfigServiceImpl) parseValidationRules(validateTag string) string values := strings.TrimPrefix(rule, "oneof=") frontendRules = append(frontendRules, "可选值: "+values) } + } return strings.Join(frontendRules, "、") @@ -394,6 +397,7 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string { "photo_data": "人脸图片", "owner_type": "企业主类型", "type": "查询类型", + "query_reason_id": "查询原因ID", } if label, exists := labelMap[jsonTag]; exists { @@ -438,6 +442,7 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso "photo_data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "ownerType": "1", "type": "per", + "query_reason_id": "1", } if example, exists := exampleMap[jsonTag]; exists { @@ -491,6 +496,7 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st "photo_data": "请输入base64编码的人脸图片(支持JPG、BMP、PNG格式)", "ownerType": "请选择企业主类型", "type": "请选择查询类型", + "query_reason_id": "请选择查询原因ID", } if placeholder, exists := placeholderMap[jsonTag]; exists { @@ -546,6 +552,7 @@ func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation s "photo_data": "人脸图片(必填):base64编码的图片数据,仅支持JPG、BMP、PNG三种格式", "owner_type": "企业主类型编码:1-法定代表人;2-主要人员;3-自然人股东;4-法定代表人及自然人股东;5-其他", "type": "查询类型:per-人员,ent-企业 1", + "query_reason_id": "查询原因ID:1-授信审批;2-贷中管理;3-贷后管理;4-异议处理;5-担保查询;6-租赁资质审查;7-融资租赁审批;8-借贷撮合查询;9-保险审批;10-资质审核;11-风控审核;12-企业背调", } if desc, exists := descMap[jsonTag]; exists { diff --git a/internal/domains/api/services/processors/ivyz/ivyz2b2t_processor.go b/internal/domains/api/services/processors/ivyz/ivyz2b2t_processor.go new file mode 100644 index 0000000..2fc3654 --- /dev/null +++ b/internal/domains/api/services/processors/ivyz/ivyz2b2t_processor.go @@ -0,0 +1,58 @@ +package ivyz + +import ( + "context" + "encoding/json" + "errors" + "strconv" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/westdex" +) + +// ProcessIVYZ2B2TRequest IVYZ2B2T API处理方法 能力资质核验(学历) +func ProcessIVYZ2B2TRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + + var paramsDto dto.IVYZ2B2TReq + 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.WestDexService.Encrypt(paramsDto.Name) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + encryptedIDCard, err := deps.WestDexService.Encrypt(paramsDto.IDCard) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + encryptedQueryReasonId, err := deps.WestDexService.Encrypt(strconv.FormatInt(paramsDto.QueryReasonId, 10)) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + reqData := map[string]interface{}{ + "data": map[string]interface{}{ + "idCard": encryptedIDCard, + "name": encryptedName, + "queryReasonId": encryptedQueryReasonId, + }, + } + + respBytes, err := deps.WestDexService.CallAPI(ctx, "G11JX01", reqData) + if err != nil { + if errors.Is(err, westdex.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } else { + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/domains/api/services/processors/ivyz/ivyz5a9o_processor.go b/internal/domains/api/services/processors/ivyz/ivyz5a9o_processor.go new file mode 100644 index 0000000..95132bf --- /dev/null +++ b/internal/domains/api/services/processors/ivyz/ivyz5a9o_processor.go @@ -0,0 +1,57 @@ +package ivyz + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/westdex" +) + +// ProcessIVYZ5A9ORequest IVYZ5A9O API处理方法 全国⾃然⼈⻛险评估评分模型 +func ProcessIVYZ5A9ORequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + + var paramsDto dto.IVYZ5A9tReq + 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.WestDexService.Encrypt(paramsDto.Name) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + encryptedIDCard, err := deps.WestDexService.Encrypt(paramsDto.IDCard) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + encryptedAuthAuthorizeFileCode, err := deps.WestDexService.Encrypt(paramsDto.AuthAuthorizeFileCode) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + reqData := map[string]interface{}{ + "data": map[string]interface{}{ + "idcard": encryptedIDCard, + "name": encryptedName, + "auth_authorizeFileCode": encryptedAuthAuthorizeFileCode, + }, + } + + respBytes, err := deps.WestDexService.CallAPI(ctx, "G01SC01", reqData) + if err != nil { + if errors.Is(err, westdex.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } else { + return nil, errors.Join(processors.ErrSystem, err) + } + } + + return respBytes, nil +} diff --git a/internal/domains/api/services/processors/jrzq/jrzq4b6c_processor.go b/internal/domains/api/services/processors/jrzq/jrzq4b6c_processor.go index b64ad48..6a27faa 100644 --- a/internal/domains/api/services/processors/jrzq/jrzq4b6c_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzq4b6c_processor.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "errors" - "tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/services/processors" "tyapi-server/internal/infrastructure/external/zhicha" diff --git a/internal/infrastructure/database/repositories/api/gorm_api_call_repository.go b/internal/infrastructure/database/repositories/api/gorm_api_call_repository.go index d6e135b..8d0a8fe 100644 --- a/internal/infrastructure/database/repositories/api/gorm_api_call_repository.go +++ b/internal/infrastructure/database/repositories/api/gorm_api_call_repository.go @@ -3,6 +3,7 @@ package api import ( "context" "fmt" + "strings" "time" "tyapi-server/internal/domains/api/entities" "tyapi-server/internal/domains/api/repositories" @@ -229,6 +230,11 @@ func (r *GormApiCallRepository) CountByUserId(ctx context.Context, userId string return r.CountWhere(ctx, &entities.ApiCall{}, "user_id = ?", userId) } +// CountByUserIdAndProductId 按用户ID和产品ID统计API调用次数 +func (r *GormApiCallRepository) CountByUserIdAndProductId(ctx context.Context, userId string, productId string) (int64, error) { + return r.CountWhere(ctx, &entities.ApiCall{}, "user_id = ? AND product_id = ?", userId, productId) +} + // CountByUserIdAndDateRange 按用户ID和日期范围统计API调用次数 func (r *GormApiCallRepository) CountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (int64, error) { return r.CountWhere(ctx, &entities.ApiCall{}, "user_id = ? AND created_at >= ? AND created_at < ?", userId, startDate, endDate) @@ -304,8 +310,29 @@ func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Contex // 应用筛选条件 if filters != nil { - // 用户ID筛选 - if userId, ok := filters["user_id"].(string); ok && userId != "" { + // 用户ID筛选(支持单个user_id和多个user_ids) + // 如果同时存在,优先使用user_ids(批量查询) + if userIds, ok := filters["user_ids"].(string); ok && userIds != "" { + // 解析逗号分隔的用户ID列表 + userIdsList := strings.Split(userIds, ",") + // 去除空白字符 + var cleanUserIds []string + for _, id := range userIdsList { + id = strings.TrimSpace(id) + if id != "" { + cleanUserIds = append(cleanUserIds, id) + } + } + if len(cleanUserIds) > 0 { + placeholders := strings.Repeat("?,", len(cleanUserIds)) + placeholders = placeholders[:len(placeholders)-1] // 移除最后一个逗号 + whereCondition += " AND ac.user_id IN (" + placeholders + ")" + for _, id := range cleanUserIds { + whereArgs = append(whereArgs, id) + } + } + } else if userId, ok := filters["user_id"].(string); ok && userId != "" { + // 单个用户ID筛选 whereCondition += " AND ac.user_id = ?" whereArgs = append(whereArgs, userId) } diff --git a/internal/infrastructure/database/repositories/finance/gorm_recharge_record_repository.go b/internal/infrastructure/database/repositories/finance/gorm_recharge_record_repository.go index ba00a68..2605d48 100644 --- a/internal/infrastructure/database/repositories/finance/gorm_recharge_record_repository.go +++ b/internal/infrastructure/database/repositories/finance/gorm_recharge_record_repository.go @@ -89,37 +89,86 @@ func (r *GormRechargeRecordRepository) UpdateStatus(ctx context.Context, id stri func (r *GormRechargeRecordRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) { var count int64 - query := r.GetDB(ctx).Model(&entities.RechargeRecord{}) + + // 检查是否有 company_name 筛选,如果有则需要 JOIN 表 + hasCompanyNameFilter := false + if options.Filters != nil { + if companyName, ok := options.Filters["company_name"].(string); ok && companyName != "" { + hasCompanyNameFilter = true + } + } + + var query *gorm.DB + if hasCompanyNameFilter { + // 使用 JOIN 查询以支持企业名称筛选 + query = r.GetDB(ctx).Table("recharge_records rr"). + Joins("LEFT JOIN users u ON rr.user_id = u.id"). + Joins("LEFT JOIN enterprise_infos ei ON u.id = ei.user_id") + } else { + // 普通查询 + query = r.GetDB(ctx).Model(&entities.RechargeRecord{}) + } + if options.Filters != nil { for key, value := range options.Filters { // 特殊处理时间范围过滤器 if key == "start_time" { if startTime, ok := value.(time.Time); ok { - query = query.Where("created_at >= ?", startTime) + if hasCompanyNameFilter { + query = query.Where("rr.created_at >= ?", startTime) + } else { + query = query.Where("created_at >= ?", startTime) + } } } else if key == "end_time" { if endTime, ok := value.(time.Time); ok { - query = query.Where("created_at <= ?", endTime) + if hasCompanyNameFilter { + query = query.Where("rr.created_at <= ?", endTime) + } else { + query = query.Where("created_at <= ?", endTime) + } + } + } else if key == "company_name" { + // 处理企业名称筛选 + if companyName, ok := value.(string); ok && companyName != "" { + query = query.Where("ei.company_name LIKE ?", "%"+companyName+"%") } } else if key == "min_amount" { // 处理最小金额,支持string、int、int64类型 if amount, err := r.parseAmount(value); err == nil { - query = query.Where("amount >= ?", amount) + if hasCompanyNameFilter { + query = query.Where("rr.amount >= ?", amount) + } else { + query = query.Where("amount >= ?", amount) + } } } else if key == "max_amount" { // 处理最大金额,支持string、int、int64类型 if amount, err := r.parseAmount(value); err == nil { - query = query.Where("amount <= ?", amount) + if hasCompanyNameFilter { + query = query.Where("rr.amount <= ?", amount) + } else { + query = query.Where("amount <= ?", amount) + } } } else { // 其他过滤器使用等值查询 - query = query.Where(key+" = ?", value) + if hasCompanyNameFilter { + query = query.Where("rr."+key+" = ?", value) + } else { + query = query.Where(key+" = ?", value) + } } } } if options.Search != "" { - query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?", - "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") + if hasCompanyNameFilter { + query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ?", + "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") + } else { + query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?", + "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") + } } return count, query.Count(&count).Error } @@ -132,45 +181,98 @@ func (r *GormRechargeRecordRepository) Exists(ctx context.Context, id string) (b func (r *GormRechargeRecordRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.RechargeRecord, error) { var records []entities.RechargeRecord - query := r.GetDB(ctx).Model(&entities.RechargeRecord{}) + + // 检查是否有 company_name 筛选,如果有则需要 JOIN 表 + hasCompanyNameFilter := false + if options.Filters != nil { + if companyName, ok := options.Filters["company_name"].(string); ok && companyName != "" { + hasCompanyNameFilter = true + } + } + + var query *gorm.DB + if hasCompanyNameFilter { + // 使用 JOIN 查询以支持企业名称筛选 + query = r.GetDB(ctx).Table("recharge_records rr"). + Select("rr.*"). + Joins("LEFT JOIN users u ON rr.user_id = u.id"). + Joins("LEFT JOIN enterprise_infos ei ON u.id = ei.user_id") + } else { + // 普通查询 + query = r.GetDB(ctx).Model(&entities.RechargeRecord{}) + } if options.Filters != nil { for key, value := range options.Filters { // 特殊处理 user_ids 过滤器 if key == "user_ids" { if userIds, ok := value.(string); ok && userIds != "" { - query = query.Where("user_id IN ?", strings.Split(userIds, ",")) + if hasCompanyNameFilter { + query = query.Where("rr.user_id IN ?", strings.Split(userIds, ",")) + } else { + query = query.Where("user_id IN ?", strings.Split(userIds, ",")) + } + } + } else if key == "company_name" { + // 处理企业名称筛选 + if companyName, ok := value.(string); ok && companyName != "" { + query = query.Where("ei.company_name LIKE ?", "%"+companyName+"%") } } else if key == "start_time" { // 处理开始时间范围 if startTime, ok := value.(time.Time); ok { - query = query.Where("created_at >= ?", startTime) + if hasCompanyNameFilter { + query = query.Where("rr.created_at >= ?", startTime) + } else { + query = query.Where("created_at >= ?", startTime) + } } } else if key == "end_time" { // 处理结束时间范围 if endTime, ok := value.(time.Time); ok { - query = query.Where("created_at <= ?", endTime) + if hasCompanyNameFilter { + query = query.Where("rr.created_at <= ?", endTime) + } else { + query = query.Where("created_at <= ?", endTime) + } } } else if key == "min_amount" { // 处理最小金额,支持string、int、int64类型 if amount, err := r.parseAmount(value); err == nil { - query = query.Where("amount >= ?", amount) + if hasCompanyNameFilter { + query = query.Where("rr.amount >= ?", amount) + } else { + query = query.Where("amount >= ?", amount) + } } } else if key == "max_amount" { // 处理最大金额,支持string、int、int64类型 if amount, err := r.parseAmount(value); err == nil { - query = query.Where("amount <= ?", amount) + if hasCompanyNameFilter { + query = query.Where("rr.amount <= ?", amount) + } else { + query = query.Where("amount <= ?", amount) + } } } else { // 其他过滤器使用等值查询 - query = query.Where(key+" = ?", value) + if hasCompanyNameFilter { + query = query.Where("rr."+key+" = ?", value) + } else { + query = query.Where(key+" = ?", value) + } } } } if options.Search != "" { - query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?", - "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") + if hasCompanyNameFilter { + query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ?", + "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") + } else { + query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?", + "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") + } } if options.Sort != "" { @@ -178,9 +280,17 @@ func (r *GormRechargeRecordRepository) List(ctx context.Context, options interfa if options.Order == "desc" || options.Order == "DESC" { order = "DESC" } - query = query.Order(options.Sort + " " + order) + if hasCompanyNameFilter { + query = query.Order("rr." + options.Sort + " " + order) + } else { + query = query.Order(options.Sort + " " + order) + } } else { - query = query.Order("created_at DESC") + if hasCompanyNameFilter { + query = query.Order("rr.created_at DESC") + } else { + query = query.Order("created_at DESC") + } } if options.Page > 0 && options.PageSize > 0 { diff --git a/internal/infrastructure/database/repositories/finance/gorm_wallet_transaction_repository.go b/internal/infrastructure/database/repositories/finance/gorm_wallet_transaction_repository.go index 8cb7b07..b9b14f5 100644 --- a/internal/infrastructure/database/repositories/finance/gorm_wallet_transaction_repository.go +++ b/internal/infrastructure/database/repositories/finance/gorm_wallet_transaction_repository.go @@ -382,12 +382,26 @@ func (r *GormWalletTransactionRepository) ListWithFiltersAndProductName(ctx cont // 应用筛选条件 if filters != nil { - // 用户ID筛选 - if userId, ok := filters["user_id"].(string); ok && userId != "" { + // 用户ID筛选(支持单个和多个) + if userIds, ok := filters["user_ids"].(string); ok && userIds != "" { + // 多个用户ID,逗号分隔 + userIdsList := strings.Split(userIds, ",") + whereCondition += " AND wt.user_id IN ?" + whereArgs = append(whereArgs, userIdsList) + } else if userId, ok := filters["user_id"].(string); ok && userId != "" { + // 单个用户ID whereCondition += " AND wt.user_id = ?" whereArgs = append(whereArgs, userId) } + // 产品ID筛选(支持多个) + if productIds, ok := filters["product_ids"].(string); ok && productIds != "" { + // 多个产品ID,逗号分隔 + productIdsList := strings.Split(productIds, ",") + whereCondition += " AND wt.product_id IN ?" + whereArgs = append(whereArgs, productIdsList) + } + // 时间范围筛选 if startTime, ok := filters["start_time"].(time.Time); ok { whereCondition += " AND wt.created_at >= ?" diff --git a/internal/infrastructure/external/zhicha/zhicha_service.go b/internal/infrastructure/external/zhicha/zhicha_service.go index 7c23fab..5282f80 100644 --- a/internal/infrastructure/external/zhicha/zhicha_service.go +++ b/internal/infrastructure/external/zhicha/zhicha_service.go @@ -34,7 +34,7 @@ type ZhichaResp struct { type ZhichaConfig struct { URL string AppID string -AppSecret string + AppSecret string EncryptKey string } @@ -133,14 +133,13 @@ func (z *ZhichaService) CallAPI(ctx context.Context, proID string, params map[st } else if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() { // 检查是否是网络超时错误 isTimeout = true - } else if errStr := err.Error(); - errStr == "context deadline exceeded" || - errStr == "timeout" || - errStr == "Client.Timeout exceeded" || - errStr == "net/http: request canceled" { + } else if errStr := err.Error(); errStr == "context deadline exceeded" || + errStr == "timeout" || + errStr == "Client.Timeout exceeded" || + errStr == "net/http: request canceled" { isTimeout = true } - + if isTimeout { // 超时错误应该返回数据源异常,而不是系统异常 err = errors.Join(ErrDatasource, fmt.Errorf("API请求超时: %v", err)) diff --git a/internal/infrastructure/http/handlers/product_admin_handler.go b/internal/infrastructure/http/handlers/product_admin_handler.go index 45fa14e..a1864fe 100644 --- a/internal/infrastructure/http/handlers/product_admin_handler.go +++ b/internal/infrastructure/http/handlers/product_admin_handler.go @@ -2,6 +2,7 @@ package handlers import ( "strconv" + "strings" "time" "tyapi-server/internal/application/api" "tyapi-server/internal/application/finance" @@ -1237,13 +1238,27 @@ func (h *ProductAdminHandler) ExportAdminWalletTransactions(c *gin.Context) { // 时间范围筛选 if startTime := c.Query("start_time"); startTime != "" { + // 处理URL编码的+号,转换为空格 + startTime = strings.ReplaceAll(startTime, "+", " ") if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil { filters["start_time"] = t + } else { + // 尝试其他格式 + if t, err := time.Parse("2006-01-02T15:04:05", startTime); err == nil { + filters["start_time"] = t + } } } if endTime := c.Query("end_time"); endTime != "" { + // 处理URL编码的+号,转换为空格 + endTime = strings.ReplaceAll(endTime, "+", " ") if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil { filters["end_time"] = t + } else { + // 尝试其他格式 + if t, err := time.Parse("2006-01-02T15:04:05", endTime); err == nil { + filters["end_time"] = t + } } } @@ -1262,6 +1277,11 @@ func (h *ProductAdminHandler) ExportAdminWalletTransactions(c *gin.Context) { filters["product_ids"] = productIds } + // 企业名称筛选 + if companyName := c.Query("company_name"); companyName != "" { + filters["company_name"] = companyName + } + // 金额范围筛选 if minAmount := c.Query("min_amount"); minAmount != "" { filters["min_amount"] = minAmount @@ -1281,7 +1301,16 @@ func (h *ProductAdminHandler) ExportAdminWalletTransactions(c *gin.Context) { fileData, err := h.financeAppService.ExportAdminWalletTransactions(c.Request.Context(), filters, format) if err != nil { h.logger.Error("导出消费记录失败", zap.Error(err)) - h.responseBuilder.BadRequest(c, "导出消费记录失败") + + // 根据错误信息返回具体的提示 + errMsg := err.Error() + if strings.Contains(errMsg, "没有找到符合条件的数据") || strings.Contains(errMsg, "没有数据") { + h.responseBuilder.NotFound(c, "没有找到符合筛选条件的数据,请调整筛选条件后重试") + } else if strings.Contains(errMsg, "参数") || strings.Contains(errMsg, "参数错误") { + h.responseBuilder.BadRequest(c, errMsg) + } else { + h.responseBuilder.BadRequest(c, "导出消费记录失败:"+errMsg) + } return } @@ -1364,6 +1393,11 @@ func (h *ProductAdminHandler) GetAdminRechargeRecords(c *gin.Context) { filters["max_amount"] = maxAmount } + // 企业名称筛选 + if companyName := c.Query("company_name"); companyName != "" { + filters["company_name"] = companyName + } + // 构建分页选项 options := interfaces.ListOptions{ Page: page, @@ -1442,7 +1476,16 @@ func (h *ProductAdminHandler) ExportAdminRechargeRecords(c *gin.Context) { fileData, err := h.financeAppService.ExportAdminRechargeRecords(c.Request.Context(), filters, format) if err != nil { h.logger.Error("导出充值记录失败", zap.Error(err)) - h.responseBuilder.BadRequest(c, "导出充值记录失败") + + // 根据错误信息返回具体的提示 + errMsg := err.Error() + if strings.Contains(errMsg, "没有找到符合条件的数据") || strings.Contains(errMsg, "没有数据") { + h.responseBuilder.NotFound(c, "没有找到符合筛选条件的充值记录,请调整筛选条件后重试") + } else if strings.Contains(errMsg, "参数") || strings.Contains(errMsg, "参数错误") { + h.responseBuilder.BadRequest(c, errMsg) + } else { + h.responseBuilder.BadRequest(c, "导出充值记录失败:"+errMsg) + } return } @@ -1468,7 +1511,10 @@ func (h *ProductAdminHandler) GetAdminApiCalls(c *gin.Context) { // 构建筛选条件 filters := make(map[string]interface{}) - // 用户ID筛选 + // 用户ID筛选(支持单个user_id和多个user_ids,根据需求使用) + if userId := c.Query("user_id"); userId != "" { + filters["user_id"] = userId + } if userIds := c.Query("user_ids"); userIds != "" { filters["user_ids"] = userIds } @@ -1566,7 +1612,16 @@ func (h *ProductAdminHandler) ExportAdminApiCalls(c *gin.Context) { fileData, err := h.apiAppService.ExportAdminApiCalls(c.Request.Context(), filters, format) if err != nil { h.logger.Error("导出API调用记录失败", zap.Error(err)) - h.responseBuilder.BadRequest(c, "导出API调用记录失败") + + // 根据错误信息返回具体的提示 + errMsg := err.Error() + if strings.Contains(errMsg, "没有找到符合条件的数据") || strings.Contains(errMsg, "没有数据") { + h.responseBuilder.NotFound(c, "没有找到符合筛选条件的API调用记录,请调整筛选条件后重试") + } else if strings.Contains(errMsg, "参数") || strings.Contains(errMsg, "参数错误") { + h.responseBuilder.BadRequest(c, errMsg) + } else { + h.responseBuilder.BadRequest(c, "导出API调用记录失败:"+errMsg) + } return } diff --git a/internal/shared/pdf/page_builder.go b/internal/shared/pdf/page_builder.go index 2cb305c..2cc5d19 100644 --- a/internal/shared/pdf/page_builder.go +++ b/internal/shared/pdf/page_builder.go @@ -5,11 +5,13 @@ import ( "fmt" "math" "os" + "path/filepath" "strings" "tyapi-server/internal/domains/product/entities" "github.com/jung-kurt/gofpdf/v2" + qrcode "github.com/skip2/go-qrcode" "go.uber.org/zap" ) @@ -298,6 +300,9 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro } } } + + // 添加说明文字和二维码 + pb.addAdditionalInfo(pdf, doc, chineseFontAvailable) } // addSection 添加章节 @@ -829,3 +834,248 @@ func (pb *PageBuilder) safeSplitText(pdf *gofpdf.Fpdf, text string, width float6 return lines } + +// addAdditionalInfo 添加说明文字和二维码 +func (pb *PageBuilder) addAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) { + // 检查是否需要换页 + pageWidth, pageHeight := pdf.GetPageSize() + _, _, _, bottomMargin := pdf.GetMargins() + currentY := pdf.GetY() + remainingHeight := pageHeight - currentY - bottomMargin + + // 如果剩余空间不足,添加新页 + if remainingHeight < 100 { + pdf.AddPage() + pb.addHeader(pdf, chineseFontAvailable) + pb.addWatermark(pdf, chineseFontAvailable) + pdf.SetY(45) + } + + // 添加分隔线 + pdf.Ln(10) + pdf.SetLineWidth(0.5) + pdf.SetDrawColor(200, 200, 200) + pdf.Line(15, pdf.GetY(), pageWidth-15, pdf.GetY()) + pdf.SetDrawColor(0, 0, 0) + + // 添加说明文字标题 + pdf.Ln(15) + pdf.SetTextColor(0, 0, 0) + pb.fontManager.SetFont(pdf, "B", 16) + _, lineHt := pdf.GetFontSize() + pdf.CellFormat(0, lineHt, "接入流程说明", "", 1, "L", false, 0, "") + + // 读取说明文本文件 + explanationText := pb.readExplanationText() + if explanationText != "" { + pb.logger.Debug("开始渲染说明文本", + zap.Int("text_length", len(explanationText)), + zap.Int("line_count", len(strings.Split(explanationText, "\n"))), + ) + + pdf.Ln(5) + pb.fontManager.SetFont(pdf, "", 11) + _, lineHt = pdf.GetFontSize() + + // 处理说明文本,按行分割并显示 + lines := strings.Split(explanationText, "\n") + renderedLines := 0 + + for i, line := range lines { + // 保留原始行用于日志 + originalLine := line + line = strings.TrimSpace(line) + + // 处理空行 + if line == "" { + pdf.Ln(3) + continue + } + + // 清理文本(保留中文字符和标点) + cleanLine := pb.textProcessor.CleanText(line) + + // 检查清理后的文本是否为空 + if strings.TrimSpace(cleanLine) == "" { + pb.logger.Warn("文本行清理后为空,跳过渲染", + zap.Int("line_number", i+1), + zap.String("original_line", originalLine), + ) + continue + } + + // 渲染文本行 + // 使用MultiCell自动换行,支持长文本 + pdf.MultiCell(0, lineHt*1.4, cleanLine, "", "L", false) + renderedLines++ + } + + pb.logger.Info("说明文本渲染完成", + zap.Int("total_lines", len(lines)), + zap.Int("rendered_lines", renderedLines), + ) + } else { + pb.logger.Warn("说明文本为空,跳过渲染") + } + + // 添加二维码生成方法和使用方法说明 + pb.addQRCodeSection(pdf, doc, chineseFontAvailable) +} + +// readExplanationText 读取说明文本文件 +func (pb *PageBuilder) readExplanationText() string { + resourcesPDFDir := GetResourcesPDFDir() + textFilePath := filepath.Join(resourcesPDFDir, "后勤服务.txt") + + // 检查文件是否存在 + if _, err := os.Stat(textFilePath); os.IsNotExist(err) { + pb.logger.Warn("说明文本文件不存在", zap.String("path", textFilePath)) + return "" + } + + // 尝试读取文件(使用os.ReadFile替代已废弃的ioutil.ReadFile) + content, err := os.ReadFile(textFilePath) + if err != nil { + pb.logger.Error("读取说明文本文件失败", + zap.String("path", textFilePath), + zap.Error(err), + ) + return "" + } + + // 转换为字符串 + text := string(content) + + // 记录读取成功的信息 + pb.logger.Info("成功读取说明文本文件", + zap.String("path", textFilePath), + zap.Int("file_size", len(content)), + zap.Int("text_length", len(text)), + zap.Int("line_count", len(strings.Split(text, "\n"))), + ) + + // 返回文本内容 + return text +} + +// addQRCodeSection 添加二维码生成方法和使用方法说明 +func (pb *PageBuilder) addQRCodeSection(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) { + _, pageHeight := pdf.GetPageSize() + _, _, _, bottomMargin := pdf.GetMargins() + currentY := pdf.GetY() + + // 检查是否需要换页(为二维码预留空间) + if pageHeight-currentY-bottomMargin < 120 { + pdf.AddPage() + pb.addHeader(pdf, chineseFontAvailable) + pb.addWatermark(pdf, chineseFontAvailable) + pdf.SetY(45) + } + + // 添加二维码标题 + pdf.Ln(15) + pdf.SetTextColor(0, 0, 0) + pb.fontManager.SetFont(pdf, "B", 16) + _, lineHt := pdf.GetFontSize() + pdf.CellFormat(0, lineHt, "天远api官网二维码", "", 1, "L", false, 0, "") + + // 先生成并添加二维码图片(确保二维码能够正常显示) + pb.addQRCodeImage(pdf, "https://tianyuanapi.com/", chineseFontAvailable) + + // 二维码说明文字(简化版,放在二维码之后) + pdf.Ln(10) + pb.fontManager.SetFont(pdf, "", 11) + _, lineHt = pdf.GetFontSize() + + qrCodeExplanation := "使用手机扫描上方二维码可直接跳转到天远API官网(https://tianyuanapi.com/),获取更多接口文档和资源。\n\n" + + "二维码使用方法:\n" + + "1. 使用手机相机或二维码扫描应用扫描二维码\n" + + "2. 扫描后会自动跳转到天远API官网首页\n" + + "3. 在官网可以查看完整的产品列表、接口文档和使用说明" + + // 处理说明文本,按行分割并显示 + lines := strings.Split(qrCodeExplanation, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + pdf.Ln(2) + continue + } + + // 普通文本行 + cleanLine := pb.textProcessor.CleanText(line) + if strings.TrimSpace(cleanLine) != "" { + pdf.MultiCell(0, lineHt*1.3, cleanLine, "", "L", false) + } + } +} + +// addQRCodeImage 生成并添加二维码图片到PDF +func (pb *PageBuilder) addQRCodeImage(pdf *gofpdf.Fpdf, content string, chineseFontAvailable bool) { + // 检查是否需要换页 + pageWidth, pageHeight := pdf.GetPageSize() + _, _, _, bottomMargin := pdf.GetMargins() + currentY := pdf.GetY() + + // 二维码大小(40mm) + qrSize := 40.0 + if pageHeight-currentY-bottomMargin < qrSize+20 { + pdf.AddPage() + pb.addHeader(pdf, chineseFontAvailable) + pb.addWatermark(pdf, chineseFontAvailable) + pdf.SetY(45) + } + + // 生成二维码 + qr, err := qrcode.New(content, qrcode.Medium) + if err != nil { + pb.logger.Warn("生成二维码失败", zap.Error(err)) + return + } + + // 将二维码转换为PNG字节 + qrBytes, err := qr.PNG(256) + if err != nil { + pb.logger.Warn("转换二维码为PNG失败", zap.Error(err)) + return + } + + // 创建临时文件保存二维码(使用os.CreateTemp替代已废弃的ioutil.TempFile) + tmpFile, err := os.CreateTemp("", "qrcode_*.png") + if err != nil { + pb.logger.Warn("创建临时文件失败", zap.Error(err)) + return + } + defer os.Remove(tmpFile.Name()) // 清理临时文件 + + // 写入二维码数据 + if _, err := tmpFile.Write(qrBytes); err != nil { + pb.logger.Warn("写入二维码数据失败", zap.Error(err)) + tmpFile.Close() + return + } + tmpFile.Close() + + // 添加二维码说明 + pdf.Ln(10) + pb.fontManager.SetFont(pdf, "", 10) + _, lineHt := pdf.GetFontSize() + pdf.CellFormat(0, lineHt, "官网二维码:", "", 1, "L", false, 0, "") + + // 计算二维码位置(居中) + qrX := (pageWidth - qrSize) / 2 + + // 添加二维码图片 + pdf.Ln(5) + pdf.ImageOptions(tmpFile.Name(), qrX, pdf.GetY(), qrSize, qrSize, false, gofpdf.ImageOptions{}, 0, "") + + // 添加二维码下方的说明文字 + pdf.SetY(pdf.GetY() + qrSize + 5) + pb.fontManager.SetFont(pdf, "", 9) + _, lineHt = pdf.GetFontSize() + qrNote := "使用手机扫描上方二维码可访问官网获取更多详情" + noteWidth := pdf.GetStringWidth(qrNote) + noteX := (pageWidth - noteWidth) / 2 + pdf.SetX(noteX) + pdf.CellFormat(noteWidth, lineHt, qrNote, "", 1, "C", false, 0, "") +} diff --git a/resources/pdf/后勤服务.txt b/resources/pdf/后勤服务.txt new file mode 100644 index 0000000..d887fa7 --- /dev/null +++ b/resources/pdf/后勤服务.txt @@ -0,0 +1,27 @@ +天远数据安全测试接入流程说明 +若您希望接入天远数据的安全测试服务,可按照以下详细流程进行操作: + +1. 联系商务了解接入流程 +请您首先与天远数据的商务团队取得联系,深入了解安全测试接入的具体流程、要求以及相关注意事项。您可以通过以下方式联系我们的商务人员: + +商务邮箱:jiaowuzhe@aitoolpath.com + +商务联系电话:13876051080 微信同号 + +获得更多详情请访问 [https://www.tianyuanapi.com/] + +2. 提供正式生产环境公网 IP +在与商务团队沟通并了解清楚接入流程后,请您将正式生产环境的公网 IP 提供给天远数据。我们将依据您提供的公网 IP 进行 IP 访问设置,以确保后续接口调用的顺利进行。 + +3. 构造并加密请求报文 +您需要构造 JSON 明文请求报文,然后使用 AES-128 算法(基于账户获得的16进制字符串密钥/Access Key)对该明文请求报文进行加密处理。加密时采用AES-CBC模式(密钥长度128位/16字节,填充方式PKCS7),每次加密随机生成16字节(128位)的IV(初始化向量),将IV与加密后的密文拼接在一起,最后通过Base64编码形成可传输的字符串,并将该Base64字符串放入请求体的data字段传参。此步骤中涉及的代码部分,您可参考我们提供的demo包,里面有详细的示例和说明,能帮助您顺利完成报文的构造、加密及Base64编码操作。 + +4. 调用接口获取返回结果 +完成请求报文的构造、加密及Base64编码后,您可以使用处理好的报文(即包含Base64编码数据的数据体)调用天远数据的接口。调用接口后,您将获得相应的返回结果(该返回结果为经过Base64编码且拼接了IV的密文数据)。 + +5. 解密获得明文结果 +当您获得接口返回的结果后,需要先对Base64解码后的数据提取前16字节作为IV,再使用该IV通过AES-CBC模式解密剩余密文,最后去除PKCS7填充得到原始明文。同样,关于Base64解码及AES解密(含IV提取、填充去除)的代码实现,您可参考test包中的相关内容,以顺利完成返回结果的解密操作。 + + +若您在接入过程中有任何疑问或需要进一步的帮助,请随时与我们联系。您可以通过上述的商务邮箱和商务联系电话与我们的团队沟通,我们将竭诚为您服务。 +