Go-ZERO(Gin+GORM)实现聚合推特数据进行情感分析 技术设计实战

设计思路关联文章地址:Gin+GORM实现聚合推特数据进行情感分析 技术设计-将持续更新并且开源

开源地址:github开源地址

一、项目技术栈与核心设计思路

  • Web框架:Gin(高性能HTTP框架,快速开发API)
  • ORM:GORM v2(简化MySQL操作,支持自动迁移、关联查询)
  • 数据来源:Twitter(X)API v2(基于官方返回字段设计表结构,确保数据完整性)
  • 核心功能:大V订阅、推文拉取(增量去重)、情感分析、向量存储(Qdrant)、API查询
  • 设计原则:字段与Twitter API返回对齐,冗余必要字段,支持增量拉取和快速查询

二、MySQL表设计(与Twitter API字段对齐)

1. 大V订阅表(influencers

存储Twitter大V的核心信息,字段对应Twitter API GET /users/by/username/{username} 响应

CREATE TABLE `influencers` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `twitter_user_id` varchar(64) NOT NULL COMMENT 'Twitter官方用户ID(API返回的id_str)',
  `username` varchar(64) NOT NULL COMMENT 'Twitter用户名(如elonmusk,API返回的username)',
  `name` varchar(128) NOT NULL COMMENT '大V昵称(API返回的name)',
  `avatar_url` varchar(255) DEFAULT '' COMMENT '头像URL(API返回的profile_image_url)',
  `description` text COMMENT '个人简介(API返回的description)',
  `location` varchar(128) DEFAULT '' COMMENT '所在地(API返回的location)',
  `url` varchar(255) DEFAULT '' COMMENT '个人网站(API返回的url)',
  `public_metrics_followers` bigint NOT NULL DEFAULT '0' COMMENT '粉丝数(API返回的public_metrics.followers_count)',
  `public_metrics_following` int NOT NULL DEFAULT '0' COMMENT '关注数(API返回的public_metrics.following_count)',
  `public_metrics_tweet_count` int NOT NULL DEFAULT '0' COMMENT '推文总数(API返回的public_metrics.tweet_count)',
  `is_verified` tinyint NOT NULL DEFAULT '0' COMMENT '是否认证(API返回的verified)',
  `is_active` tinyint NOT NULL DEFAULT '1' COMMENT '是否活跃(1=拉取推文,0=暂停)',
  `pull_frequency` int NOT NULL DEFAULT '30' COMMENT '拉取频率(分钟)',
  `last_pull_tweet_id` varchar(64) DEFAULT '' COMMENT '上次拉取的最后一条推文ID(用于增量拉取)',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '订阅时间',
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '信息更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_twitter_user_id` (`twitter_user_id`),
  UNIQUE KEY `uk_username` (`username`),
  KEY `idx_is_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Twitter大V订阅表';
2. 推文表(tweets

存储拉取的推文原始数据,字段对应Twitter API GET /users/:id/tweets 响应

CREATE TABLE `tweets` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tweet_id` varchar(64) NOT NULL COMMENT 'Twitter推文ID(API返回的id_str)',
  `twitter_user_id` varchar(64) NOT NULL COMMENT '推文作者ID(关联influencers.twitter_user_id)',
  `content` text NOT NULL COMMENT '推文内容(API返回的text)',
  `created_at` datetime NOT NULL COMMENT '发布时间(API返回的created_at,转本地时间)',
  `lang` varchar(16) DEFAULT '' COMMENT '语言(API返回的lang)',
  `source` varchar(128) DEFAULT '' COMMENT '发布来源(API返回的source)',
  `is_original` tinyint NOT NULL DEFAULT '1' COMMENT '是否原创(1=原创,0=转发/引用)',
  `referenced_tweet_id` varchar(64) DEFAULT '' COMMENT '引用/转发的推文ID(API返回的referenced_tweets.id_str)',
  `referenced_tweet_type` varchar(32) DEFAULT '' COMMENT '引用类型(retweeted、quoted、replied_to)',
  `public_metrics_like_count` int NOT NULL DEFAULT '0' COMMENT '点赞数(API返回的public_metrics.like_count)',
  `public_metrics_retweet_count` int NOT NULL DEFAULT '0' COMMENT '转发数(API返回的public_metrics.retweet_count)',
  `public_metrics_reply_count` int NOT NULL DEFAULT '0' COMMENT '回复数(API返回的public_metrics.reply_count)',
  `public_metrics_quote_count` int NOT NULL DEFAULT '0' COMMENT '引用数(API返回的public_metrics.quote_count)',
  `public_metrics_impression_count` bigint NOT NULL DEFAULT '0' COMMENT '曝光量(API返回的public_metrics.impression_count)',
  `pulled_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '拉取时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tweet_id` (`tweet_id`),
  KEY `idx_twitter_user_id` (`twitter_user_id`),
  KEY `idx_created_at` (`created_at`),
  CONSTRAINT `fk_tweet_influencer` FOREIGN KEY (`twitter_user_id`) REFERENCES `influencers` (`twitter_user_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Twitter推文原始表';
3. 推文情感分析结果表(tweet_sentiments

存储情感分析结果,与推文表一对一关联

CREATE TABLE `tweet_sentiments` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tweet_id` varchar(64) NOT NULL COMMENT '关联推文ID(tweets.tweet_id)',
  `sentiment_score` decimal(5,4) NOT NULL COMMENT '情感得分(-1~1,越接近1越正面)',
  `sentiment_label` varchar(16) NOT NULL COMMENT '情感标签(positive/negative/neutral)',
  `confidence` decimal(5,4) NOT NULL DEFAULT '0.0000' COMMENT '分析置信度(0~1)',
  `keyword_score` decimal(5,4) NOT NULL DEFAULT '0.0000' COMMENT '领域关键词加权分(如比特币相关词汇)',
  `similar_tweet_count` int NOT NULL DEFAULT '0' COMMENT '相似历史推文数量(用于优化得分)',
  `analyzed_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '分析时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tweet_id` (`tweet_id`),
  CONSTRAINT `fk_sentiment_tweet` FOREIGN KEY (`tweet_id`) REFERENCES `tweets` (`tweet_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推文情感分析结果表';
4. 情感词典表(sentiment_keywords

存储领域情感关键词(优化情感分析精度)

CREATE TABLE `sentiment_keywords` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `keyword` varchar(64) NOT NULL COMMENT '情感关键词(如bullish、crash、FOMO)',
  `weight` decimal(3,2) NOT NULL COMMENT '情感权重(正数=正面,负数=负面,绝对值越大权重越高)',
  `category` varchar(32) NOT NULL DEFAULT 'crypto' COMMENT '关键词分类(crypto/stock/tech等)',
  `frequency` int NOT NULL DEFAULT '0' COMMENT '使用频率(优化关键词优先级)',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_keyword_category` (`keyword`,`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='情感分析关键词词典表';

三、GORM Model定义(Go代码)

internal/model/ 目录下创建模型文件,与数据库表一一对应:

1. 大V模型(internal/model/influencer.go
package model

import (
	"time"

	"gorm.io/gorm"
)

// Influencer 大V订阅模型(对应Twitter API /users/by/username响应)
type Influencer struct {
	ID                          uint64         `gorm:"column:id;type:bigint unsigned;primaryKey;autoIncrement" json:"id"`
	TwitterUserID               string         `gorm:"column:twitter_user_id;type:varchar(64);not null;uniqueIndex" json:"twitter_user_id"` // Twitter官方ID(id_str)
	Username                    string         `gorm:"column:username;type:varchar(64);not null;uniqueIndex" json:"username"`             // 用户名(如elonmusk)
	Name                        string         `gorm:"column:name;type:varchar(128);not null" json:"name"`                               // 昵称
	AvatarURL                   string         `gorm:"column:avatar_url;type:varchar(255);default:''" json:"avatar_url"`                   // 头像URL
	Description                 string         `gorm:"column:description;type:text" json:"description"`                                   // 个人简介
	Location                    string         `gorm:"column:location;type:varchar(128);default:''" json:"location"`                     // 所在地
	URL                         string         `gorm:"column:url;type:varchar(255);default:''" json:"url"`                                   // 个人网站
	PublicMetricsFollowers      int64          `gorm:"column:public_metrics_followers;type:bigint;not null;default:0" json:"public_metrics_followers"` // 粉丝数
	PublicMetricsFollowing      int            `gorm:"column:public_metrics_following;type:int;not null;default:0" json:"public_metrics_following"`   // 关注数
	PublicMetricsTweetCount     int            `gorm:"column:public_metrics_tweet_count;type:int;not null;default:0" json:"public_metrics_tweet_count"` // 推文总数
	IsVerified                  int8           `gorm:"column:is_verified;type:tinyint;not null;default:0" json:"is_verified"`               // 是否认证(1=是)
	IsActive                    int8           `gorm:"column:is_active;type:tinyint;not null;default:1" json:"is_active"`                   // 是否活跃拉取
	PullFrequency               int            `gorm:"column:pull_frequency;type:int;not null;default:30" json:"pull_frequency"`           // 拉取频率(分钟)
	LastPullTweetID             string         `gorm:"column:last_pull_tweet_id;type:varchar(64);default:''" json:"last_pull_tweet_id"`     // 上次拉取的最后一条推文ID
	CreatedAt                   time.Time      `gorm:"column:created_at;type:datetime;not null;default:current_timestamp" json:"created_at"`
	UpdatedAt                   time.Time      `gorm:"column:updated_at;type:datetime;not null;default:current_timestamp;autoUpdateTime" json:"updated_at"`
	DeletedAt                   gorm.DeletedAt `gorm:"column:deleted_at;type:datetime;index" json:"-"` // 软删除
}

// TableName 表名映射
func (Influencer) TableName() string {
	return "influencers"
}
2. 推文模型(internal/model/tweet.go
package model

import (
	"time"

	"gorm.io/gorm"
)

// Tweet 推文原始模型(对应Twitter API /users/:id/tweets响应)
type Tweet struct {
	ID                              uint64         `gorm:"column:id;type:bigint unsigned;primaryKey;autoIncrement" json:"id"`
	TweetID                         string         `gorm:"column:tweet_id;type:varchar(64);not null;uniqueIndex" json:"tweet_id"` // Twitter推文ID(id_str)
	TwitterUserID                   string         `gorm:"column:twitter_user_id;type:varchar(64);not null;index" json:"twitter_user_id"` // 作者ID
	Content                         string         `gorm:"column:content;type:text;not null" json:"content"`                               // 推文内容
	CreatedAt                       time.Time      `gorm:"column:created_at;type:datetime;not null;index" json:"created_at"`             // 发布时间(转本地时间)
	Lang                            string         `gorm:"column:lang;type:varchar(16);default:''" json:"lang"`                           // 语言
	Source                          string         `gorm:"column:source;type:varchar(128);default:''" json:"source"`                     // 发布来源
	IsOriginal                      int8           `gorm:"column:is_original;type:tinyint;not null;default:1" json:"is_original"`         // 是否原创
	ReferencedTweetID               string         `gorm:"column:referenced_tweet_id;type:varchar(64);default:''" json:"referenced_tweet_id"` // 引用/转发的推文ID
	ReferencedTweetType             string         `gorm:"column:referenced_tweet_type;type:varchar(32);default:''" json:"referenced_tweet_type"` // 引用类型
	PublicMetricsLikeCount          int            `gorm:"column:public_metrics_like_count;type:int;not null;default:0" json:"public_metrics_like_count"` // 点赞数
	PublicMetricsRetweetCount       int            `gorm:"column:public_metrics_retweet_count;type:int;not null;default:0" json:"public_metrics_retweet_count"` // 转发数
	PublicMetricsReplyCount         int            `gorm:"column:public_metrics_reply_count;type:int;not null;default:0" json:"public_metrics_reply_count"` // 回复数
	PublicMetricsQuoteCount         int            `gorm:"column:public_metrics_quote_count;type:int;not null;default:0" json:"public_metrics_quote_count"` // 引用数
	PublicMetricsImpressionCount    int64          `gorm:"column:public_metrics_impression_count;type:bigint;not null;default:0" json:"public_metrics_impression_count"` // 曝光量
	PulledAt                        time.Time      `gorm:"column:pulled_at;type:datetime;not null;default:current_timestamp" json:"pulled_at"` // 拉取时间
	DeletedAt                       gorm.DeletedAt `gorm:"column:deleted_at;type:datetime;index" json:"-"` // 软删除
}

// TableName 表名映射
func (Tweet) TableName() string {
	return "tweets"
}
3. 情感分析结果模型(internal/model/tweet_sentiment.go
package model

import (
	"time"

	"gorm.io/gorm"
)

// TweetSentiment 推文情感分析结果模型
type TweetSentiment struct {
	ID                  uint64         `gorm:"column:id;type:bigint unsigned;primaryKey;autoIncrement" json:"id"`
	TweetID             string         `gorm:"column:tweet_id;type:varchar(64);not null;uniqueIndex" json:"tweet_id"` // 关联推文ID
	SentimentScore      float64        `gorm:"column:sentiment_score;type:decimal(5,4);not null" json:"sentiment_score"` // 情感得分(-1~1)
	SentimentLabel      string         `gorm:"column:sentiment_label;type:varchar(16);not null" json:"sentiment_label"` // 情感标签
	Confidence          float64        `gorm:"column:confidence;type:decimal(5,4);not null;default:0.0000" json:"confidence"` // 置信度
	KeywordScore        float64        `gorm:"column:keyword_score;type:decimal(5,4);not null;default:0.0000" json:"keyword_score"` // 关键词加权分
	SimilarTweetCount   int            `gorm:"column:similar_tweet_count;type:int;not null;default:0" json:"similar_tweet_count"` // 相似推文数量
	AnalyzedAt          time.Time      `gorm:"column:analyzed_at;type:datetime;not null;default:current_timestamp" json:"analyzed_at"` // 分析时间
	DeletedAt           gorm.DeletedAt `gorm:"column:deleted_at;type:datetime;index" json:"-"` // 软删除
}

// TableName 表名映射
func (TweetSentiment) TableName() string {
	return "tweet_sentiments"
}
4. 情感关键词模型(internal/model/sentiment_keyword.go
package model

import (
	"time"

	"gorm.io/gorm"
)

// SentimentKeyword 情感关键词模型
type SentimentKeyword struct {
	ID        uint64         `gorm:"column:id;type:bigint unsigned;primaryKey;autoIncrement" json:"id"`
	Keyword   string         `gorm:"column:keyword;type:varchar(64);not null" json:"keyword"` // 关键词
	Weight    float64        `gorm:"column:weight;type:decimal(3,2);not null" json:"weight"`   // 情感权重
	Category  string         `gorm:"column:category;type:varchar(32);not null;default:'crypto'" json:"category"` // 分类
	Frequency int            `gorm:"column:frequency;type:int;not null;default:0" json:"frequency"` // 使用频率
	CreatedAt time.Time      `gorm:"column:created_at;type:datetime;not null;default:current_timestamp" json:"created_at"`
	UpdatedAt time.Time      `gorm:"column:updated_at;type:datetime;not null;default:current_timestamp;autoUpdateTime" json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:datetime;index" json:"-"` // 软删除
}

// TableName 表名映射
func (SentimentKeyword) TableName() string {
	return "sentiment_keywords"
}

四、数据库初始化与GORM配置

1. 配置文件(configs/config.yaml
# 数据库配置
mysql:
  dsn: "root:123456@tcp(127.0.0.1:3306)/twitter_sentiment?charset=utf8mb4&parseTime=True&loc=Local"
  max_open_conns: 20
  max_idle_conns: 10
  conn_max_lifetime: 3600 # 连接最大存活时间(秒)

# Twitter API配置(对应开发者平台的Token)
twitter:
  api_key: "your_api_key"
  api_secret: "your_api_secret"
  access_token: "your_access_token"
  access_token_secret: "your_access_token_secret"
  api_base_url: "https://api.x.com/2" # Twitter API v2基础地址

# Qdrant配置
qdrant:
  addr: "127.0.0.1:6333"
  collection_name: "tweet_embeddings"
  vector_dim: 384 # 与sentence-transformers模型维度一致

# 定时任务配置
cron:
  pull_tweet_cron: "0 */30 * * * ?" # 每30分钟拉取一次推文
  analyze_sentiment_cron: "0 */10 * * * ?" # 每10分钟分析一次未处理的推文

# 服务配置
server:
  port: 8080
  mode: "debug" # debug/release
2. GORM初始化(internal/config/db.go
package config

import (
	"log"
	"time"

	"github.com/spf13/viper"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"

	"twitter-sentiment/internal/model"
)

// DB 全局数据库连接
var DB *gorm.DB

// InitDB 初始化数据库连接并自动迁移表结构
func InitDB() {
	dsn := viper.GetString("mysql.dsn")
	maxOpenConns := viper.GetInt("mysql.max_open_conns")
	maxIdleConns := viper.GetInt("mysql.max_idle_conns")
	connMaxLifetime := viper.GetInt("mysql.conn_max_lifetime")

	// 初始化GORM
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info), // 打印SQL日志(debug模式)
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true, // 禁用表名复数
		},
	})
	if err != nil {
		log.Fatalf("初始化数据库失败:%v", err)
	}

	// 配置连接池
	sqlDB, err := db.DB()
	if err != nil {
		log.Fatalf("获取数据库连接池失败:%v", err)
	}
	sqlDB.SetMaxOpenConns(maxOpenConns)       // 最大打开连接数
	sqlDB.SetMaxIdleConns(maxIdleConns)       // 最大空闲连接数
	sqlDB.SetConnMaxLifetime(time.Duration(connMaxLifetime) * time.Second) // 连接最大存活时间

	// 自动迁移表结构(创建不存在的表,不删除已有字段)
	err = db.AutoMigrate(
		&model.Influencer{},
		&model.Tweet{},
		&model.TweetSentiment{},
		&model.SentimentKeyword{},
	)
	if err != nil {
		log.Fatalf("自动迁移表结构失败:%v", err)
	}

	DB = db
	log.Println("数据库初始化成功")
}

五、核心API实现(Gin路由)

internal/handler/ 目录下实现API接口,路由注册在 internal/server/router.go

1. 路由注册(internal/server/router.go
package server

import (
	"github.com/gin-gonic/gin"
	"twitter-sentiment/internal/handler"
)

// InitRouter 初始化Gin路由
func InitRouter() *gin.Engine {
	r := gin.Default()

	// 基础路由组
	api := r.Group("/api/v1")
	{
		// 大V相关接口
		influencer := api.Group("/influencers")
		{
			influencer.GET("", handler.GetInfluencers)       // 获取所有订阅大V
			influencer.POST("", handler.AddInfluencer)       // 添加订阅大V
			influencer.GET("/:username", handler.GetInfluencerByUsername) // 获取单个大V详情
			influencer.PUT("/:username/active", handler.UpdateInfluencerActive) // 暂停/恢复拉取

			// 推文相关接口
			influencer.GET("/:username/tweets", handler.GetInfluencerTweets) // 获取大V推文
			influencer.GET("/:username/sentiment", handler.GetInfluencerSentimentSummary) // 获取大V情感分析汇总
		}

		// 情感关键词接口
		keyword := api.Group("/keywords")
		{
			keyword.POST("", handler.AddSentimentKeyword) // 添加情感关键词
			keyword.GET("/:category", handler.GetKeywordsByCategory) // 获取指定分类的关键词
		}
	}

	return r
}
2. 核心接口实现示例(internal/handler/influencer.go
package handler

import (
	"net/http"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"gorm.io/gorm"

	"twitter-sentiment/internal/config"
	"twitter-sentiment/internal/model"
	"twitter-sentiment/internal/twitter"
)

// GetInfluencers 获取所有订阅大V
func GetInfluencers(c *gin.Context) {
	var influencers []model.Influencer
	if err := config.DB.Find(&influencers).Error; err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"code":    500,
			"message": "查询大V列表失败:" + err.Error(),
			"data":    nil,
		})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"code":    200,
		"message": "success",
		"data":    influencers,
	})
}

// AddInfluencer 添加订阅大V(通过Twitter用户名)
func AddInfluencer(c *gin.Context) {
	type Request struct {
		Username       string `json:"username" binding:"required,min=1,max=64"` // Twitter用户名
		PullFrequency  int    `json:"pull_frequency" binding:"min=5,max=1440"`  // 拉取频率(分钟,5~1440)
	}

	var req Request
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    400,
			"message": "参数校验失败:" + err.Error(),
			"data":    nil,
		})
		return
	}

	// 1. 检查是否已订阅
	var existing model.Influencer
	if err := config.DB.Where("username = ?", req.Username).First(&existing).Error; err != gorm.ErrRecordNotFound {
		c.JSON(http.StatusConflict, gin.H{
			"code":    409,
			"message": "该大V已订阅",
			"data":    nil,
		})
		return
	}

	// 2. 调用Twitter API获取大V详情
	twitterClient := twitter.NewClient()
	influencerInfo, err := twitterClient.GetUserByUsername(req.Username)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"code":    500,
			"message": "获取Twitter大V信息失败:" + err.Error(),
			"data":    nil,
		})
		return
	}

	// 3. 保存到数据库
	newInfluencer := model.Influencer{
		TwitterUserID:               influencerInfo.TwitterUserID,
		Username:                    influencerInfo.Username,
		Name:                        influencerInfo.Name,
		AvatarURL:                   influencerInfo.AvatarURL,
		Description:                 influencerInfo.Description,
		Location:                    influencerInfo.Location,
		URL:                         influencerInfo.URL,
		PublicMetricsFollowers:      influencerInfo.PublicMetricsFollowers,
		PublicMetricsFollowing:      influencerInfo.PublicMetricsFollowing,
		PublicMetricsTweetCount:     influencerInfo.PublicMetricsTweetCount,
		IsVerified:                  influencerInfo.IsVerified,
		IsActive:                    1,
		PullFrequency:               req.PullFrequency,
		LastPullTweetID:             "",
		CreatedAt:                   time.Now(),
		UpdatedAt:                   time.Now(),
	}

	if err := config.DB.Create(&newInfluencer).Error; err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"code":    500,
			"message": "保存大V信息失败:" + err.Error(),
			"data":    nil,
		})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"code":    200,
		"message": "订阅大V成功",
		"data":    newInfluencer,
	})
}

// GetInfluencerTweets 获取大V推文(支持分页、时间筛选)
func GetInfluencerTweets(c *gin.Context) {
	username := c.Param("username")
	pageStr := c.DefaultQuery("page", "1")
	pageSizeStr := c.DefaultQuery("pageSize", "20")
	startTimeStr := c.Query("startTime")
	endTimeStr := c.Query("endTime")

	// 解析分页参数
	page, _ := strconv.Atoi(pageStr)
	pageSize, _ := strconv.Atoi(pageSizeStr)
	offset := (page - 1) * pageSize

	// 1. 获取大V的TwitterUserID
	var influencer model.Influencer
	if err := config.DB.Where("username = ?", username).First(&influencer).Error; err != nil {
		c.JSON(http.StatusNotFound, gin.H{
			"code":    404,
			"message": "未找到该大V",
			"data":    nil,
		})
		return
	}

	// 2. 构建查询条件
	db := config.DB.Model(&model.Tweet{}).Where("twitter_user_id = ?", influencer.TwitterUserID)

	// 时间筛选
	if startTimeStr != "" {
		startTime, err := time.Parse("2006-01-02 15:04:05", startTimeStr)
		if err == nil {
			db = db.Where("created_at >= ?", startTime)
		}
	}
	if endTimeStr != "" {
		endTime, err := time.Parse("2006-01-02 15:04:05", endTimeStr)
		if err == nil {
			db = db.Where("created_at <= ?", endTime)
		}
	}

	// 3. 分页查询
	var tweets []model.Tweet
	var total int64
	if err := db.Count(&total).Error; err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"code":    500,
			"message": "查询推文总数失败:" + err.Error(),
			"data":    nil,
		})
		return
	}

	if err := db.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&tweets).Error; err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"code":    500,
			"message": "查询推文列表失败:" + err.Error(),
			"data":    nil,
		})
		return
	}

	// 4. 关联查询情感分析结果
	tweetIDs := make([]string, len(tweets))
	for i, tweet := range tweets {
		tweetIDs[i] = tweet.TweetID
	}

	var sentiments []model.TweetSentiment
	if err := config.DB.Where("tweet_id IN (?)", tweetIDs).Find(&sentiments).Error; err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"code":    500,
			"message": "查询情感分析结果失败:" + err.Error(),
			"data":    nil,
		})
		return
	}

	// 5. 组装结果(推文+情感分析)
	type TweetWithSentiment struct {
		model.Tweet
		Sentiment *model.TweetSentiment `json:"sentiment,omitempty"`
	}
	result := make([]TweetWithSentiment, len(tweets))
	sentimentMap := make(map[string]*model.TweetSentiment)
	for i, s := range sentiments {
		sentimentMap[s.TweetID] = &sentiments[i]
	}

	for i, tweet := range tweets {
		result[i] = TweetWithSentiment{
			Tweet:     tweet,
			Sentiment: sentimentMap[tweet.TweetID],
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"code":    200,
		"message": "success",
		"data": gin.H{
			"list":  result,
			"total": total,
			"page":  page,
			"size":  pageSize,
		},
	})
}

六、Twitter API客户端封装(internal/twitter/client.go

与Twitter API v2对接,拉取大V信息和推文(字段与Model对齐):

package twitter

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"github.com/dghubble/oauth1"
	"twitter-sentiment/internal/config"
	"github.com/spf13/viper"
)

// TwitterClient Twitter API客户端
type TwitterClient struct {
	client *http.Client
	baseURL string
}

// UserInfo Twitter大V信息(与API响应对齐)
type UserInfo struct {
	TwitterUserID               string `json:"twitter_user_id"`
	Username                    string `json:"username"`
	Name                        string `json:"name"`
	AvatarURL                   string `json:"avatar_url"`
	Description                 string `json:"description"`
	Location                    string `json:"location"`
	URL                         string `json:"url"`
	PublicMetricsFollowers      int64  `json:"public_metrics_followers"`
	PublicMetricsFollowing      int    `json:"public_metrics_following"`
	PublicMetricsTweetCount     int    `json:"public_metrics_tweet_count"`
	IsVerified                  int8   `json:"is_verified"`
}

// NewClient 创建Twitter API客户端
func NewClient() *TwitterClient {
	// 从配置文件读取Token
	apiKey := viper.GetString("twitter.api_key")
	apiSecret := viper.GetString("twitter.api_secret")
	accessToken := viper.GetString("twitter.access_token")
	accessTokenSecret := viper.GetString("twitter.access_token_secret")

	// 初始化OAuth1认证
	config := oauth1.NewConfig(apiKey, apiSecret)
	token := oauth1.NewToken(accessToken, accessTokenSecret)
	httpClient := config.Client(oauth1.NoContext, token)

	return &TwitterClient{
		client: httpClient,
		baseURL: viper.GetString("twitter.api_base_url"),
	}
}

// GetUserByUsername 通过用户名获取大V信息(调用API /users/by/username/:username)
func (c *TwitterClient) GetUserByUsername(username string) (*UserInfo, error) {
	url := fmt.Sprintf("%s/users/by/username/%s?user.fields=id,name,username,profile_image_url,description,location,url,public_metrics,verified", c.baseURL, username)

	resp, err := c.client.Get(url)
	if err != nil {
		return nil, fmt.Errorf("API请求失败:%v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("API返回错误状态码:%d", resp.StatusCode)
	}

	// 解析API响应(Twitter API v2响应格式)
	type APIResponse struct {
		Data struct {
			ID            string `json:"id"`
			Name          string `json:"name"`
			Username      string `json:"username"`
			ProfileImageURL string `json:"profile_image_url"`
			Description   string `json:"description"`
			Location      string `json:"location"`
			URL           string `json:"url"`
			Verified      bool   `json:"verified"`
			PublicMetrics struct {
				FollowersCount  int64 `json:"followers_count"`
				FollowingCount  int   `json:"following_count"`
				TweetCount      int   `json:"tweet_count"`
			} `json:"public_metrics"`
		} `json:"data"`
	}

	var apiResp APIResponse
	if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
		return nil, fmt.Errorf("解析API响应失败:%v", err)
	}

	// 转换为本地Model结构
	return &UserInfo{
		TwitterUserID:               apiResp.Data.ID,
		Username:                    apiResp.Data.Username,
		Name:                        apiResp.Data.Name,
		AvatarURL:                   apiResp.Data.ProfileImageURL,
		Description:                 apiResp.Data.Description,
		Location:                    apiResp.Data.Location,
		URL:                         apiResp.Data.URL,
		PublicMetricsFollowers:      apiResp.Data.PublicMetrics.FollowersCount,
		PublicMetricsFollowing:      apiResp.Data.PublicMetrics.FollowingCount,
		PublicMetricsTweetCount:     apiResp.Data.PublicMetrics.TweetCount,
		IsVerified:                  func() int8 { if apiResp.Data.Verified { return 1 } else { return 0 } }(),
	}, nil
}

// PullTweets 拉取大V推文(增量拉取,调用API /users/:id/tweets)
func (c *TwitterClient) PullTweets(twitterUserID, lastTweetID string, maxResults int) ([]model.Tweet, error) {
	// 构建API参数(增量拉取:since_id=lastTweetID,只拉取比上次更新的推文)
	params := fmt.Sprintf("tweet.fields=id,text,created_at,lang,source,public_metrics,referenced_tweets&max_results=%d", maxResults)
	if lastTweetID != "" {
		params += fmt.Sprintf("&since_id=%s", lastTweetID)
	}
	url := fmt.Sprintf("%s/users/%s/tweets?%s", c.baseURL, twitterUserID, params)

	resp, err := c.client.Get(url)
	if err != nil {
		return nil, fmt.Errorf("API请求失败:%v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("API返回错误状态码:%d", resp.StatusCode)
	}

	// 解析API响应
	type ReferencedTweet struct {
		ID   string `json:"id"`
		Type string `json:"type"`
	}
	type APITweet struct {
		ID            string `json:"id"`
		Text          string `json:"text"`
		CreatedAt     string `json:"created_at"` // ISO8601格式(如2025-11-16T12:00:00Z)
		Lang          string `json:"lang"`
		Source        string `json:"source"`
		ReferencedTweets []ReferencedTweet `json:"referenced_tweets"`
		PublicMetrics struct {
			LikeCount      int `json:"like_count"`
			RetweetCount   int `json:"retweet_count"`
			ReplyCount     int `json:"reply_count"`
			QuoteCount     int `json:"quote_count"`
			ImpressionCount int64 `json:"impression_count"`
		} `json:"public_metrics"`
	}
	type APIResponse struct {
		Data []APITweet `json:"data"`
	}

	var apiResp APIResponse
	if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
		return nil, fmt.Errorf("解析API响应失败:%v", err)
	}

	// 转换为本地Tweet模型
	tweets := make([]model.Tweet, 0, len(apiResp.Data))
	for _, apiTweet := range apiResp.Data {
		// 解析发布时间(ISO8601转本地时间)
		createdAt, err := time.Parse(time.RFC3339, apiTweet.CreatedAt)
		if err != nil {
			continue // 时间解析失败则跳过
		}

		// 处理引用/转发信息
		isOriginal := int8(1)
		referencedTweetID := ""
		referencedTweetType := ""
		if len(apiTweet.ReferencedTweets) > 0 {
			isOriginal = 0
			referencedTweetID = apiTweet.ReferencedTweets[0].ID
			referencedTweetType = apiTweet.ReferencedTweets[0].Type
		}

		tweets = append(tweets, model.Tweet{
			TweetID:                     apiTweet.ID,
			TwitterUserID:               twitterUserID,
			Content:                     apiTweet.Text,
			CreatedAt:                   createdAt.Local(),
			Lang:                        apiTweet.Lang,
			Source:                      apiTweet.Source,
			IsOriginal:                  isOriginal,
			ReferencedTweetID:           referencedTweetID,
			ReferencedTweetType:         referencedTweetType,
			PublicMetricsLikeCount:      apiTweet.PublicMetrics.LikeCount,
			PublicMetricsRetweetCount:   apiTweet.PublicMetrics.RetweetCount,
			PublicMetricsReplyCount:     apiTweet.PublicMetrics.ReplyCount,
			PublicMetricsQuoteCount:     apiTweet.PublicMetrics.QuoteCount,
			PublicMetricsImpressionCount: apiTweet.PublicMetrics.ImpressionCount,
			PulledAt:                    time.Now(),
		})
	}

	return tweets, nil
}

七、项目启动入口(main.go

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/spf13/viper"
	"twitter-sentiment/internal/config"
	"twitter-sentiment/internal/server"
	"twitter-sentiment/internal/cron"
)

func main() {
	// 1. 加载配置文件
	viper.SetConfigName("config")
	viper.SetConfigType("yaml")
	viper.AddConfigPath("./configs")
	if err := viper.ReadInConfig(); err != nil {
		log.Fatalf("加载配置文件失败:%v", err)
	}

	// 2. 初始化数据库
	config.InitDB()

	// 3. 启动定时任务(拉取推文、情感分析)
	cron.InitCron()

	// 4. 启动HTTP服务
	r := server.InitRouter()
	port := viper.GetString("server.port")
	srv := &http.Server{
		Addr:    ":" + port,
		Handler: r,
	}

	log.Printf("服务启动成功,监听端口:%s", port)
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatalf("服务启动失败:%v", err)
	}
}

八、关键依赖与安装命令

1. go.mod 核心依赖
module twitter-sentiment

go 1.21

require (
	github.com/dghubble/oauth1 v1.1.0          // Twitter API OAuth1认证
	github.com/gin-gonic/gin v1.9.1           // Web框架
	github.com/spf13/viper v1.18.2            // 配置文件解析
	gorm.io/driver/mysql v1.5.5               // MySQL驱动
	gorm.io/gorm v1.25.4                      // ORM框架
	github.com/robfig/cron/v3 v3.0.1          // 定时任务
	github.com/jdkato/prose v1.2.1            // 文本情感分析基础库
	github.com/qdrant/go-client/v2 v2.12.0    // Qdrant向量数据库客户端
)
2. 依赖安装命令
# 初始化项目
go mod init twitter-sentiment

# 安装核心依赖
go get github.com/gin-gonic/gin@v1.9.1
go get gorm.io/gorm@v1.25.4
go get gorm.io/driver/mysql@v1.5.5
go get github.com/spf13/viper@v1.18.2
go get github.com/dghubble/oauth1@v1.1.0
go get github.com/robfig/cron/v3@v3.0.1
go get github.com/jdkato/prose@v1.2.1
go get github.com/qdrant/go-client/v2@v2.12.0

# 下载依赖
go mod download

九、核心特性总结

  1. 字段对齐:数据库表和Model严格对应Twitter API v2返回字段,确保数据不丢失。
  2. 增量拉取:通过last_pull_tweet_id记录上次拉取位置,避免重复拉取推文。
  3. 情感分析:分离推文原始数据和情感分析结果,支持后续优化算法。
  4. 高性能:GORM连接池配置、批量插入、索引优化,支持高并发查询。
  5. 可扩展:模块化设计(Twitter客户端、情感分析、向量存储),便于后续迭代。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Han_coding1208

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值