Răsfoiți Sursa

第二个版本

light 6 luni în urmă
părinte
comite
20b166fad1

+ 10 - 2
config/config.go

@@ -13,8 +13,16 @@ type Config struct {
 	DBPassword string `env:"DB_PASSWORD" envDefault:"password"`
 	DBName     string `env:"DB_NAME" envDefault:"tasks"`
 	//服务器配置
-	Concurrency int    `env:"CONCURRENCY" envDefault:"20"`
-	Interval    string `env:"INTERVAL" envDefault:"10m"`
+	Concurrency  int    `env:"CONCURRENCY" envDefault:"20"`
+	Interval     string `env:"INTERVAL" envDefault:"10m"`
+	MaxTaskBatch int    `env:"MAX_TASK_BATCH" envDefault:"1000"`
+	MaxTaskAge   string `env:"MAX_TASK_AGE" envDefault:"24h"` // 支持time.ParseDuration格式
+	// Redis配置
+	RedisHost       string `env:"REDIS_HOST" envDefault:"localhost"`
+	RedisPort       int    `env:"REDIS_PORT" envDefault:"6379"`
+	RedisPassword   string `env:"REDIS_PASSWORD" envDefault:""`
+	RedisDB         int    `env:"REDIS_DB" envDefault:"0"`
+	IsSkipSSLVerify bool   `env:"IS_SKIP_SSL_VERIFY" envDefault:"false"`
 	//日志配置
 	LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
 	//业务配置

+ 5 - 28
main.go

@@ -2,14 +2,10 @@
 package main
 
 import (
-	"context"
 	"go-policy-service/config"
-	"go-policy-service/models"
 	"go-policy-service/services"
 	"go-policy-service/utils"
 	"time"
-
-	"github.com/robfig/cron/v3"
 )
 
 func main() {
@@ -18,39 +14,20 @@ func main() {
 	if err != nil {
 		panic("Failed to load config: " + err.Error())
 	}
-
 	// 初始化日志
 	utils.InitLogger(cfg.LogLevel)
 
-	// 初始化数据库连接
-	if err := models.InitDB(cfg); err != nil {
-		utils.Logger.Fatal("Failed to connect database: ", err)
-	}
+	//初始化其他服务
+	services.Initialize(cfg)
 
 	// 创建处理器
 	processor := services.NewTaskProcessor(
-		utils.NewHttpClient(30*time.Second),
-		cfg.Concurrency,
+		utils.NewHttpClient(30*time.Second, cfg.IsSkipSSLVerify),
+		cfg,
 	)
+	processor.Run(cfg)
 
-	// 设置定时任务
-	c := cron.New()
-	_, err = c.AddFunc("@every "+cfg.Interval, func() {
-		utils.Logger.Info("Starting scheduled task processing...")
-		ctx, cancel := context.WithTimeout(context.Background(), 9*time.Minute)
-		defer cancel()
-
-		if err := processor.ProcessTasks(ctx); err != nil {
-			utils.Logger.Error("Task processing failed: ", err)
-		}
-	})
-	if err != nil {
-		utils.Logger.Fatal("Failed to schedule task: ", err)
-	}
-
-	c.Start()
 	utils.Logger.Info("Service started successfully")
-
 	// 保持主进程运行
 	select {}
 }

+ 65 - 0
models/account.go

@@ -0,0 +1,65 @@
+package models
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"go-policy-service/utils"
+
+	"gorm.io/gorm"
+)
+
+type ThirdAccount struct {
+	ID          uint   `gorm:"primaryKey;column:id"`
+	CreatedAt   *int   `gorm:"column:created_at"` // 可选:你可以换成 time.Time
+	UpdatedAt   *int   `gorm:"column:updated_at"` // 同上
+	Name        string `gorm:"type:varchar(45);column:name"`
+	GroupName   string `gorm:"type:varchar(45);column:group"` // 注意 group 是 Go 的关键字,不建议用作字段名
+	Cookie      string `gorm:"type:text;column:cookie"`
+	Emails      string `gorm:"type:text;column:emails"`
+	Config      string `gorm:"type:text;column:config"`
+	LastUsedAt  *int   `gorm:"column:last_used_at"`
+	Username    string `gorm:"type:varchar(45);column:username"`
+	Password    string `gorm:"type:varchar(45);column:password"`
+	Status      int    `gorm:"column:status;type:tinyint(1)"` // 使用int更明确
+	MerchantID  *int   `gorm:"column:merchant_id"`
+	Tag         string `gorm:"type:varchar(45);column:tag"`
+	UseType     string `gorm:"type:varchar(45);column:use_type;default:下单"`
+	MaxQpsOrder *int   `gorm:"column:max_qps_order"` // 最大同时下单数
+}
+
+func (ThirdAccount) TableName() string {
+	return "third_account" // 需要添加这个方法明确表名
+}
+
+type HgConfig struct {
+	Account       string `json:"account"`
+	Password      string `json:"password"`
+	TicketChannel string `json:"ticket_channel"`
+	PayMethod     string `json:"pay_method"`
+}
+
+func GetHGConf(ctx context.Context) (*HgConfig, error) {
+	var account ThirdAccount
+	result := utils.Db.WithContext(ctx).Model(&ThirdAccount{}).
+		Where("`name` = ? AND `group` = ? AND `status` = ? AND `merchant_id` = ?",
+			"航管分销API",
+			"hgapi",
+			1,
+			3).
+		First(&account)
+
+	if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+		return nil, errors.New("account not found")
+	}
+	return ParseAccountConfig(account.Config)
+}
+
+func ParseAccountConfig(configStr string) (*HgConfig, error) {
+	var cfg HgConfig
+	if err := json.Unmarshal([]byte(configStr), &cfg); err != nil {
+		return nil, fmt.Errorf("failed to parse account config: %w", err)
+	}
+	return &cfg, nil
+}

+ 25 - 50
models/task.go

@@ -1,25 +1,29 @@
-// models/task.go
 package models
 
 import (
 	"context"
-	"fmt"
-	"go-policy-service/config"
-	"time"
+	"go-policy-service/utils"
 
-	"gorm.io/driver/mysql"
 	"gorm.io/gorm"
 )
 
-var db *gorm.DB
+type HgFlightSearchTask struct {
+	ID         uint   `gorm:"primaryKey;column:id"`
+	Dep        string `gorm:"type:varchar(12);column:dep"`
+	Arr        string `gorm:"type:varchar(12);column:arr"`
+	Date       uint   `gorm:"column:date"` // 通常是 YYYYMMDD 的整数表示
+	TripType   string `gorm:"type:char(2);column:trip_type"`
+	FlightNo   string `gorm:"type:varchar(12);column:flight_no"`
+	Flight     string `gorm:"type:char(2);column:flight"`
+	CreatedAt  uint   `gorm:"column:created_at"` // 如果你想用 time.Time,可以改成 time.Time 并加解析
+	UpdatedAt  uint   `gorm:"column:updated_at"`
+	MerchantID uint   `gorm:"column:merchant_id;default:3"`
+	ServiceTag string `gorm:"type:varchar(45);column:service_tag"`
+}
 
-type Task struct {
-	gorm.Model
-	Params    string `gorm:"type:text"`
-	Status    string `gorm:"type:varchar(20)"`
-	Result    string `gorm:"type:text"`
-	Attempts  int    `gorm:"default:0"`
-	NextTryAt time.Time
+// TableName 设置表名
+func (HgFlightSearchTask) TableName() string {
+	return "hg_flight_search_task"
 }
 
 type ProcessedData struct {
@@ -28,50 +32,21 @@ type ProcessedData struct {
 	Data   string `gorm:"type:text"`
 }
 
-func InitDB(cfg *config.Config) error {
-	dsn := getDSN(cfg)
-	conn, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
-	if err != nil {
-		return err
-	}
-
-	db = conn
-	// 自动迁移表结构
-	if err := db.AutoMigrate(&Task{}, &ProcessedData{}); err != nil {
-		return err
-	}
-	return nil
-}
-
-func getDSN(cfg *config.Config) string {
-	return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4",
-		cfg.DBUser,
-		cfg.DBPassword,
-		cfg.DBHost,
-		cfg.DBPort,
-		cfg.DBName)
-}
-
-func GetPendingTasks(ctx context.Context, limit int) ([]Task, error) {
-	var tasks []Task
-	result := db.WithContext(ctx).
-		Where("status = ? AND next_try_at <= ?", "pending", time.Now()).
-		Limit(limit).
-		Find(&tasks)
-	return tasks, result.Error
+func GetPendingTasks(ctx context.Context, limit int) ([]HgFlightSearchTask, error) {
+	var hgShFlNotasks []HgFlightSearchTask
+	result := utils.Db.WithContext(ctx).Model(&HgFlightSearchTask{}).Where("status = ?", 1).Limit(limit).Find(&hgShFlNotasks)
+	return hgShFlNotasks, result.Error
 }
 
 func SaveProcessedData(ctx context.Context, data *ProcessedData) error {
-	return db.WithContext(ctx).Create(data).Error
+	return utils.Db.WithContext(ctx).Model(&HgFlightSearchTask{}).Create(data).Error
 }
 
 func UpdateTaskStatus(ctx context.Context, taskID uint, status string, attempts int) error {
-	return db.WithContext(ctx).
-		Model(&Task{}).
+	return utils.Db.WithContext(ctx).Model(&HgFlightSearchTask{}).
 		Where("id = ?", taskID).
 		Updates(map[string]interface{}{
-			"status":      status,
-			"attempts":    attempts,
-			"next_try_at": time.Now().Add(5 * time.Minute),
+			"status":   status,
+			"attempts": attempts,
 		}).Error
 }

+ 91 - 0
services/hangguan.go

@@ -0,0 +1,91 @@
+// services/hangguan.go
+package services
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"go-policy-service/config"
+	"go-policy-service/models"
+	"go-policy-service/structs"
+	"go-policy-service/utils"
+)
+
+type HangguanService struct {
+	cfg        *config.Config
+	httpClient utils.HTTPClient
+	tokenMgr   *TokenManager
+}
+
+// 更新后的New方法
+func NewHangguanService(
+	cfg *config.Config,
+	httpClient utils.HTTPClient,
+	tokenMgr *TokenManager,
+) *HangguanService {
+	return &HangguanService{
+		cfg:        cfg,
+		httpClient: httpClient,
+		tokenMgr:   tokenMgr,
+	}
+}
+
+func (s *HangguanService) requestSFDataConvert(task models.HgFlightSearchTask) (string, error) {
+	// 将 uint 日期转换为 YYYY-MM-DD 格式字符串)
+	req := structs.ReqSearchFlightData{
+		DepCode: task.Dep,
+		ArrCode: task.Arr,
+		Date:    utils.TimestampToString(int64(task.Date), "2006-01-02"),
+	}
+
+	// 将结构体转换为 JSON
+	jsonData, err := json.Marshal(req)
+	if err != nil {
+		return "", fmt.Errorf("JSON 编码失败: %w", err)
+	}
+	return string(jsonData), nil
+}
+
+func (h *HangguanService) RequestSFlightData(ctx context.Context, task models.HgFlightSearchTask) {
+	token, err := h.tokenMgr.GetAccessToken()
+	if err != nil {
+		utils.Logger.WithField("task_id", task.ID).Error("Failed to get access token: ", err)
+	}
+	// 构建请求数据
+	reqData, _ := h.requestSFDataConvert(task)
+	// 调用第三方接口
+	resp, err := h.httpClient.PostJSON(ctx, h.cfg.HgApiUrl+"distribution/api/shopping/flight/list?token="+token, reqData)
+	if err != nil {
+		utils.Logger.WithField("task_id", task.ID).Error("API request failed: ", err)
+		return
+	}
+
+	// 处理响应数据
+	processedData, err := processResponse(resp)
+	if err != nil {
+		utils.Logger.WithField("task_id", task.ID).Error("Response processing failed: ", err)
+		//models.UpdateTaskStatus(ctx, task.ID, "failed", task.Attempts+1)
+		return
+	}
+
+	// 保存处理后的数据
+	if err := models.SaveProcessedData(ctx, &models.ProcessedData{
+		TaskID: task.ID,
+		Data:   processedData,
+	}); err != nil {
+		utils.Logger.WithField("task_id", task.ID).Error("Failed to save processed data: ", err)
+		return
+	}
+
+	// 更新任务状态
+	// if err := models.UpdateTaskStatus(ctx, task.ID, "completed", task.Attempts+1); err != nil {
+	// 	utils.Logger.WithField("task_id", task.ID).Error("Failed to update task status: ", err)
+	// }
+}
+
+func processResponse(response []byte) (string, error) {
+	// 实现具体的响应处理逻辑
+
+	// 示例:直接返回原始响应
+	return string(response), nil
+}

+ 93 - 59
services/processor.go

@@ -3,48 +3,116 @@ package services
 
 import (
 	"context"
+	"fmt"
+	"go-policy-service/config"
 	"go-policy-service/models"
 	"go-policy-service/utils"
+	"math"
 	"sync"
+	"time"
+
+	"github.com/robfig/cron"
 )
 
+var HgConfig *models.HgConfig
+
+func Initialize(cfg *config.Config) {
+	// 初始化数据库连接
+	if err := utils.InitDB(cfg); err != nil {
+		utils.Logger.Fatal("Failed to connect database: ", err)
+	}
+	// 初始化Redis
+	err := utils.InitRedis(cfg)
+	if err != nil {
+		utils.Logger.Fatal(err)
+	}
+
+	// 获取航管配置(添加上下文和错误处理)
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+
+	// 获取航管账号配置
+	HgConfig, err = models.GetHGConf(ctx)
+	if err != nil {
+		utils.Logger.Fatal("获取航管配置失败: ", err)
+	}
+
+	// 获取航管服务
+	// hgService := services.NewHangguanService(cfg, utils.NewHttpClient(30*time.Second), rdb, hgConfig)
+}
+
 type TaskProcessor struct {
-	httpClient  utils.HTTPClient
-	concurrency int
+	httpClient utils.HTTPClient
+	cfg        *config.Config // 添加配置字段
+	hgService  *HangguanService
 }
 
-func NewTaskProcessor(httpClient utils.HTTPClient, concurrency int) *TaskProcessor {
+func NewTaskProcessor(
+	httpClient utils.HTTPClient,
+	cfg *config.Config,
+) *TaskProcessor {
 	return &TaskProcessor{
-		httpClient:  httpClient,
-		concurrency: concurrency,
+		httpClient: httpClient,
+		cfg:        cfg,
 	}
 }
 
+func (p *TaskProcessor) Run(cfg *config.Config) {
+	// 启动Token刷新任务
+	tokenMgr := NewTokenManager(utils.Rdb, HgConfig, p.httpClient, cfg.HgApiUrl)
+	go func() {
+		tokenMgr.RefreshAccessToken()
+	}()
+	// 初始化HangguanService
+	p.hgService = NewHangguanService(cfg, p.httpClient, tokenMgr)
+
+	// 初始化cron调度器,无自定义logger
+	c := cron.New()
+	scheduleSpec := fmt.Sprintf("@every %s", cfg.Interval)
+	// AddFunc 只返回 EntryID
+	entryID := c.AddFunc(scheduleSpec, func() {
+		utils.Logger.Info("Starting scheduled task processing...")
+		ctx, cancel := context.WithTimeout(context.Background(), 9*time.Minute)
+		defer cancel()
+
+		if err := p.ProcessTasks(ctx); err != nil {
+			utils.Logger.Error("Task processing failed: ", err)
+		}
+	})
+	utils.Logger.Info(fmt.Sprintf("Scheduled task with entry ID: %v", entryID))
+
+	// 开启调度器
+	c.Start()
+	utils.Logger.Info("Service started successfully")
+}
+
+// ProcessTasks方法改进
 func (p *TaskProcessor) ProcessTasks(ctx context.Context) error {
-	// 获取待处理任务
-	tasks, err := models.GetPendingTasks(ctx, 1000) // 每次最多处理1000个任务
+	tasks, err := models.GetPendingTasks(ctx, 1000)
 	if err != nil {
 		return err
 	}
 
-	// 创建任务通道
-	taskChan := make(chan models.Task)
+	taskChan := make(chan models.HgFlightSearchTask, len(tasks)) // 带缓冲通道
 	var wg sync.WaitGroup
 
-	// 启动worker
-	for i := 0; i < p.concurrency; i++ {
+	// 动态调整worker数量
+	workerCount := int(math.Min(float64(p.cfg.Concurrency), float64(len(tasks))))
+	for i := 0; i < workerCount; i++ {
 		wg.Add(1)
-		go p.worker(ctx, &wg, taskChan)
+		go func() {
+			defer wg.Done()
+			p.worker(ctx, taskChan)
+		}()
 	}
 
-	// 分发任务
+	// 分发任务优化
+DISTRIBUTE:
 	for _, task := range tasks {
 		select {
 		case taskChan <- task:
 		case <-ctx.Done():
-			close(taskChan)
-			wg.Wait()
-			return ctx.Err()
+			break DISTRIBUTE
 		}
 	}
 
@@ -53,54 +121,20 @@ func (p *TaskProcessor) ProcessTasks(ctx context.Context) error {
 	return nil
 }
 
-func (p *TaskProcessor) worker(ctx context.Context, wg *sync.WaitGroup, tasks <-chan models.Task) {
-	defer wg.Done()
-
+func (p *TaskProcessor) worker(ctx context.Context, tasks <-chan models.HgFlightSearchTask) {
 	for task := range tasks {
 		select {
 		case <-ctx.Done():
 			return
 		default:
-			p.processTask(ctx, task)
+			// 可以访问p.cfg获取配置
+			if err := validateTask(task, p.cfg); !valid {
+				// 处理无效任务
+				utils.Logger.WithField("task_id", task.ID).Error("Invalid task parameters")
+				continue
+			}
+			// 请求数据
+			p.hgService.RequestSFlightData(ctx, task)
 		}
 	}
 }
-
-func (p *TaskProcessor) processTask(ctx context.Context, task models.Task) {
-	// 调用第三方接口
-	resp, err := p.httpClient.PostJSON(ctx, "https://partner.huoli.com/distribution/api/shopping/flight/list?token=", task.Params)
-	if err != nil {
-		utils.Logger.WithField("task_id", task.ID).Error("API request failed: ", err)
-		//models.UpdateTaskStatus(ctx, task.ID, "failed", task.Attempts+1)
-		return
-	}
-
-	// 处理响应数据
-	processedData, err := processResponse(resp)
-	if err != nil {
-		utils.Logger.WithField("task_id", task.ID).Error("Response processing failed: ", err)
-		//models.UpdateTaskStatus(ctx, task.ID, "failed", task.Attempts+1)
-		return
-	}
-
-	// 保存处理后的数据
-	if err := models.SaveProcessedData(ctx, &models.ProcessedData{
-		TaskID: task.ID,
-		Data:   processedData,
-	}); err != nil {
-		utils.Logger.WithField("task_id", task.ID).Error("Failed to save processed data: ", err)
-		return
-	}
-
-	// 更新任务状态
-	// if err := models.UpdateTaskStatus(ctx, task.ID, "completed", task.Attempts+1); err != nil {
-	// 	utils.Logger.WithField("task_id", task.ID).Error("Failed to update task status: ", err)
-	// }
-}
-
-func processResponse(response []byte) (string, error) {
-	// 实现具体的响应处理逻辑
-	
-	// 示例:直接返回原始响应
-	return string(response), nil
-}

+ 109 - 0
services/token.go

@@ -0,0 +1,109 @@
+package services
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"go-policy-service/models"
+	"go-policy-service/structs"
+	"go-policy-service/utils"
+	"log"
+	"time"
+
+	"github.com/go-redis/redis/v8"
+)
+
+type TokenManager struct {
+	rdb        *redis.Client
+	hgConfig   *models.HgConfig
+	httpClient utils.HTTPClient // 自己请求 token
+	apiUrl     string           // 自己请求 token 的接口
+}
+
+// 修复构造函数参数声明
+func NewTokenManager(
+	rdb *redis.Client,
+	hgConfig *models.HgConfig,
+	httpClient utils.HTTPClient, // 明确参数类型
+	apiUrl string, // 添加类型声明
+) *TokenManager {
+	return &TokenManager{
+		rdb:        rdb,
+		hgConfig:   hgConfig,
+		httpClient: httpClient,
+		apiUrl:     apiUrl,
+	}
+}
+
+func (t *TokenManager) GetAccessToken() (string, error) {
+	tokenKey := fmt.Sprintf("60s-hgapi-%s", t.hgConfig.Account)
+	ctx := context.Background()
+
+	// 重试机制
+	const maxRetries = 10
+	for i := 0; i < maxRetries; i++ {
+		token, err := t.rdb.Get(ctx, tokenKey).Result()
+		if err == nil {
+			return token, nil
+		}
+		log.Printf("获取hgApi token失败,剩余重试次数: %d, 错误: %v", maxRetries-i-1, err)
+		if i < maxRetries-1 {
+			time.Sleep(5 * time.Second)
+		}
+	}
+	log.Println("hgApi token获取彻底失败,请检查接口状态")
+	return "", fmt.Errorf("failed to get token after %d attempts", maxRetries)
+}
+
+func (t *TokenManager) RefreshAccessToken() error {
+	tokenKey := fmt.Sprintf("60s-hgapi-%s", t.hgConfig.Account)
+	ctx := context.Background()
+
+	// 尝试从 Redis 获取现有 token
+	_, err := t.rdb.Get(ctx, tokenKey).Result()
+	if err == nil {
+		return nil
+	}
+
+	// 重试机制
+	const maxRetries = 3
+	for i := 0; i < maxRetries; i++ {
+		token, err := t.requestToken()
+		if err == nil {
+			// 成功获取 token 后存入 Redis
+			if setErr := t.rdb.SetEX(ctx, tokenKey, token, 3600*time.Second).Err(); setErr != nil {
+				log.Printf("Failed to set token in Redis: %v", setErr)
+			}
+			return nil
+		}
+
+		log.Printf("获取hgApi token失败,剩余重试次数: %d, 错误: %v", maxRetries-i-1, err)
+
+		if i < maxRetries-1 {
+			time.Sleep(1 * time.Second)
+		}
+	}
+	log.Println("hgApi token获取彻底失败,请检查接口状态")
+	return fmt.Errorf("failed to get token after %d attempts", maxRetries)
+}
+
+func (t *TokenManager) requestToken() (string, error) {
+	url := fmt.Sprintf("%s/gateway/oauth/token?grant_type=client_credentials&client_id=%s&client_secret=%s",
+		t.apiUrl, t.hgConfig.Account, t.hgConfig.Password)
+
+	respBytes, err := t.httpClient.PostJSON(context.Background(), url, nil)
+	if err != nil {
+		return "", fmt.Errorf("HTTP请求失败: %w", err)
+	}
+
+	var tokenResp structs.TokenResponse
+	if err := json.Unmarshal(respBytes, &tokenResp); err != nil {
+		return "", fmt.Errorf("JSON解析失败: %w", err)
+	}
+
+	if !tokenResp.Success || tokenResp.Data.AccessToken == "" {
+		return "", fmt.Errorf("接口返回无效响应")
+	}
+
+	return tokenResp.Data.AccessToken, nil
+}

+ 24 - 0
services/validate.go

@@ -0,0 +1,24 @@
+package services
+
+import (
+	"fmt"
+	"go-policy-service/config"
+	"go-policy-service/models"
+	"go-policy-service/utils"
+)
+
+func validateTask(task models.HgFlightSearchTask, cfg *config.Config) error {
+	// 服务标签
+	serviceTag := []string{"3uSpecial", "lowPriceFlashSale", "carrieFlight", "muCarrieFlight", "discountPriceC1", "discountPriceB1"}
+
+	// 验证参数
+	if task.Dep == "" || task.Arr == "" || task.Date == 0 || task.ServiceTag == "" {
+		return fmt.Errorf("dep, arr, date, service_tag参数不能为空: %v", task)
+	}
+	// 验证服务标签
+	if !utils.Contains(serviceTag, task.ServiceTag) {
+		return fmt.Errorf("service_tag参数不在serviceTag中: %v", serviceTag)
+	}
+
+	return nil
+}

+ 120 - 0
structs/search_flight_api.go

@@ -0,0 +1,120 @@
+package structs
+
+type ReqSearchFlightData struct {
+	DepCode string `json:"depCode"`
+	ArrCode string `json:"arrCode"`
+	Date    string `json:"date"`
+}
+
+type ResSearchFlightData struct {
+	Code    int64                   `json:"code"`
+	Msg     string                  `json:"msg"`
+	Data    ResSearchFlightDataData `json:"data"`
+	TraceID string                  `json:"traceId"`
+	Success bool                    `json:"success"`
+}
+
+type ResSearchFlightDataData struct {
+	Total          int64           `json:"total"`
+	Status         int64           `json:"status"`
+	Datas          []DataElement   `json:"datas"`
+	ServicePackets []ServicePacket `json:"servicePackets"`
+}
+
+type DataElement struct {
+	FlightInfo FlightInfo  `json:"flightInfo"`
+	CabinInfos []CabinInfo `json:"cabinInfos"`
+}
+
+type CabinInfo struct {
+	PriceInfoID      string           `json:"priceInfoId"`
+	BaseCabin        string           `json:"baseCabin"`
+	Voucher          int64            `json:"voucher"`
+	ExpectTicketTime int64            `json:"expectTicketTime"`
+	Discount         float64          `json:"discount"`
+	Left             int64            `json:"left"`
+	ProductAttribute int64            `json:"productAttribute"`
+	SaleControl      []SaleControl    `json:"saleControl"`
+	ServicePackets   []int64          `json:"servicePackets"`
+	CabinProductDesc CabinProductDesc `json:"cabinProductDesc"`
+	ADTPrice         Price            `json:"adtPrice"`
+	ChdPrice         Price            `json:"chdPrice"`
+	BusinessField    string           `json:"businessField"`
+}
+
+type Price struct {
+	ProductType   string `json:"productType"`
+	Cabin         string `json:"cabin"`
+	Price         int64  `json:"price"`
+	TicketPrice   int64  `json:"ticketPrice"`
+	RulePrice     int64  `json:"rulePrice"`
+	OilFee        *int64 `json:"oilFee,omitempty"`
+	AirportFee    *int64 `json:"airportFee,omitempty"`
+	FareBasisCode string `json:"fareBasisCode"`
+	Rule          Rule   `json:"rule"`
+}
+
+type Rule struct {
+	RefundRule         string      `json:"refundRule"`
+	ChangeRule         string      `json:"changeRule"`
+	TransferRule       string      `json:"transferRule"`
+	RefundExp          []Exp       `json:"refundExp"`
+	ChangeExp          []Exp       `json:"changeExp"`
+	HandBaggageRule    BaggageRule `json:"handBaggageRule"`
+	ConsignBaggageRule BaggageRule `json:"consignBaggageRule"`
+}
+
+type Exp struct {
+	TimeTxt string `json:"timeTxt"`
+	TimeExp string `json:"timeExp"`
+	Price   int64  `json:"price"`
+	Percent int64  `json:"percent"`
+}
+
+type BaggageRule struct {
+	Txt    string `json:"txt"`
+	Pieces int64  `json:"pieces"`
+	Weight int64  `json:"weight"`
+	Volume string `json:"volume"`
+}
+
+type CabinProductDesc struct {
+	ID          int64  `json:"id"`
+	ProductDesc string `json:"productDesc"`
+}
+
+type SaleControl struct {
+	ID                int64   `json:"id"`
+	PSNum             *string `json:"psNum,omitempty"`
+	PSType            string  `json:"psType"`
+	PSIDType          *string `json:"psIdType,omitempty"`
+	PSIDNo            *string `json:"psIdNo,omitempty"`
+	PSAge             *string `json:"psAge,omitempty"`
+	PSGender          *string `json:"psGender,omitempty"`
+	NewMember         *int64  `json:"newMember,omitempty"`
+	HasAge            *string `json:"hasAge,omitempty"`
+	CheckThreeElement *int64  `json:"checkThreeElement,omitempty"`
+}
+
+type FlightInfo struct {
+	FlightInfoID string `json:"flightInfoId"`
+	FlyNo        string `json:"flyNo"`
+	ACCode       string `json:"acCode"`
+	DepCode      string `json:"depCode"`
+	ArrCode      string `json:"arrCode"`
+	DepDateTime  string `json:"depDateTime"`
+	ArrDateTime  string `json:"arrDateTime"`
+	DepTerminal  string `json:"depTerminal"`
+	ArrTerminal  string `json:"arrTerminal"`
+	Model        string `json:"model"`
+	IsStop       int64  `json:"isStop"`
+	IsShare      int64  `json:"isShare"`
+	Meal         int64  `json:"meal"`
+	IsCancel     int64  `json:"isCancel"`
+}
+
+type ServicePacket struct {
+	ID    int64  `json:"id"`
+	Title string `json:"title"`
+	Desc  string `json:"desc"`
+}

+ 13 - 0
structs/token.go

@@ -0,0 +1,13 @@
+package structs
+
+type TokenResponse struct {
+	Success bool   `json:"success"`
+	Code    int    `json:"code"`
+	Msg     string `json:"msg"`
+	Data    struct {
+		AccessToken string `json:"access_token"`
+		ExpiresIn   int    `json:"expires_in"`
+		ExpireTime  string `json:"expireTime"`
+	} `json:"data"`
+	TraceID string `json:"traceId"`
+}

+ 23 - 0
utils/function.go

@@ -0,0 +1,23 @@
+package utils
+
+import "time"
+
+// TimestampToString 将 Unix 时间戳转换为指定格式的时间字符串
+// 参数:
+// t - Unix 时间戳(秒级)
+// format - 时间格式字符串(例如 "2006-01-02 15:04:05")
+// 返回值:
+// 格式化后的时间字符串
+func TimestampToString(t int64, format string) string {
+	t_obj := time.Unix(t, 0)
+	return t_obj.Format(format)
+}
+
+func Contains[T comparable](slice []T, item T) bool {
+	for _, v := range slice {
+		if v == item {
+			return true
+		}
+	}
+	return false
+}

+ 52 - 9
utils/http_client.go

@@ -2,9 +2,14 @@
 package utils
 
 import (
+	"bytes"
 	"context"
-	"time"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
 	"net/http"
+	"time"
 )
 
 type HTTPClient interface {
@@ -15,13 +20,16 @@ type httpClient struct {
 	client *http.Client
 }
 
-func NewHttpClient(timeout time.Duration) HTTPClient {
+func NewHttpClient(timeout time.Duration, isSkipSSLVerify bool) HTTPClient {
 	return &httpClient{
 		client: &http.Client{
-			Timeout:   timeout,
+			Timeout: timeout,
 			Transport: &http.Transport{
-				MaxIdleConns:        100,
-				IdleConnTimeout:     90 * time.Second,
+				TLSClientConfig: &tls.Config{
+					InsecureSkipVerify: isSkipSSLVerify, // 跳过 SSL 验证
+				},
+				MaxIdleConns:       100,
+				IdleConnTimeout:    90 * time.Second,
 				DisableCompression: true,
 			},
 		},
@@ -29,7 +37,42 @@ func NewHttpClient(timeout time.Duration) HTTPClient {
 }
 
 func (c *httpClient) PostJSON(ctx context.Context, url string, body interface{}) ([]byte, error) {
-	// 实现具体的HTTP请求逻辑
-	// 示例:返回空响应
-	return []byte("{}"), nil
-}
+	// 创建请求体
+	var reqBody []byte
+	if body != nil {
+		var err error
+		reqBody, err = json.Marshal(body)
+		if err != nil {
+			return nil, fmt.Errorf("JSON序列化失败: %w", err)
+		}
+	}
+
+	// 创建HTTP请求
+	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(reqBody))
+	if err != nil {
+		return nil, fmt.Errorf("创建请求失败: %w", err)
+	}
+
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Accept", "application/json")
+
+	// 发送请求
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("网络请求失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	// 检查状态码
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		return nil, fmt.Errorf("非成功状态码: %d", resp.StatusCode)
+	}
+
+	// 读取响应内容
+	respBody, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("读取响应失败: %w", err)
+	}
+
+	return respBody, nil
+}

+ 51 - 0
utils/init_service.go

@@ -0,0 +1,51 @@
+package utils
+
+import (
+	"context"
+	"fmt"
+	"go-policy-service/config"
+	"time"
+
+	"github.com/go-redis/redis/v8"
+	"gorm.io/driver/mysql"
+	"gorm.io/gorm"
+)
+
+var Db *gorm.DB
+var Rdb *redis.Client
+
+func InitDB(cfg *config.Config) error {
+	dsn := getDSN(cfg)
+	conn, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
+	if err != nil {
+		return err
+	}
+	Db = conn
+	return nil
+}
+
+func getDSN(cfg *config.Config) string {
+	return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4",
+		cfg.DBUser,
+		cfg.DBPassword,
+		cfg.DBHost,
+		cfg.DBPort,
+		cfg.DBName)
+}
+
+// 初始化 Redis
+func InitRedis(cfg *config.Config) error {
+	Rdb = redis.NewClient(&redis.Options{
+		Addr:     fmt.Sprintf("%s:%d", cfg.RedisHost, cfg.RedisPort),
+		Password: cfg.RedisPassword,
+		DB:       cfg.RedisDB,
+	})
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	if _, err := Rdb.Ping(ctx).Result(); err != nil {
+		return fmt.Errorf("redis connection failed: %w", err)
+	}
+
+	return nil
+}