This commit is contained in:
2025-12-05 14:59:23 +08:00
parent bfedec249f
commit 05b6623e75
17 changed files with 677 additions and 37 deletions

View File

@@ -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
}

View File

@@ -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"`

View File

@@ -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)

View File

@@ -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

View File

@@ -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": "查询原因ID1-授信审批2-贷中管理3-贷后管理4-异议处理5-担保查询6-租赁资质审查7-融资租赁审批8-借贷撮合查询9-保险审批10-资质审核11-风控审核12-企业背调",
}
if desc, exists := descMap[jsonTag]; exists {

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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 >= ?"

View File

@@ -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))

View File

@@ -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
}

View File

@@ -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, "")
}