feat: 部署初版测试
Some checks failed
Extension Build & Release / build (push) Failing after 1m5s
Backend Deploy (Go + Docker) / deploy (push) Failing after 1m40s
Web Console Deploy (Vue 3 + Vite) / deploy (push) Has been cancelled

This commit is contained in:
zs
2026-03-02 21:25:21 +08:00
parent db3abb3174
commit 8cf6cb944b
97 changed files with 10250 additions and 209 deletions

View File

@@ -0,0 +1,34 @@
package repository
import (
"github.com/zs/InsightReply/internal/model"
"gorm.io/gorm"
)
type CompetitorMonitorRepository struct {
db *gorm.DB
}
func NewCompetitorMonitorRepository(db *gorm.DB) *CompetitorMonitorRepository {
return &CompetitorMonitorRepository{db: db}
}
func (r *CompetitorMonitorRepository) ListByUserID(userID string) ([]model.CompetitorMonitor, error) {
var monitors []model.CompetitorMonitor
err := r.db.Where("user_id = ? AND is_active = ?", userID, true).Order("created_at desc").Find(&monitors).Error
return monitors, err
}
func (r *CompetitorMonitorRepository) ListAllActive() ([]model.CompetitorMonitor, error) {
var monitors []model.CompetitorMonitor
err := r.db.Where("is_active = ?", true).Find(&monitors).Error
return monitors, err
}
func (r *CompetitorMonitorRepository) Create(monitor *model.CompetitorMonitor) error {
return r.db.Create(monitor).Error
}
func (r *CompetitorMonitorRepository) Delete(id string, userID string) error {
return r.db.Where("id = ? AND user_id = ?", id, userID).Delete(&model.CompetitorMonitor{}).Error
}

View File

@@ -0,0 +1,38 @@
package repository
import (
"github.com/zs/InsightReply/internal/model"
"gorm.io/gorm"
)
type CustomStrategyRepository struct {
db *gorm.DB
}
func NewCustomStrategyRepository(db *gorm.DB) *CustomStrategyRepository {
return &CustomStrategyRepository{db: db}
}
func (r *CustomStrategyRepository) ListByUserID(userID string) ([]model.UserCustomStrategy, error) {
var strategies []model.UserCustomStrategy
err := r.db.Where("user_id = ? AND is_active = ?", userID, true).Order("sort_order asc, created_at desc").Find(&strategies).Error
return strategies, err
}
func (r *CustomStrategyRepository) Create(strategy *model.UserCustomStrategy) error {
return r.db.Create(strategy).Error
}
func (r *CustomStrategyRepository) Update(strategy *model.UserCustomStrategy) error {
return r.db.Save(strategy).Error
}
func (r *CustomStrategyRepository) Delete(id string, userID string) error {
return r.db.Where("id = ? AND user_id = ?", id, userID).Delete(&model.UserCustomStrategy{}).Error
}
func (r *CustomStrategyRepository) GetByIDAndUser(id string, userID string) (*model.UserCustomStrategy, error) {
var strategy model.UserCustomStrategy
err := r.db.Where("id = ? AND user_id = ?", id, userID).First(&strategy).Error
return &strategy, err
}

View File

@@ -0,0 +1,25 @@
package repository
import (
"github.com/zs/InsightReply/internal/model"
"gorm.io/gorm"
)
type ProductProfileRepository struct {
db *gorm.DB
}
func NewProductProfileRepository(db *gorm.DB) *ProductProfileRepository {
return &ProductProfileRepository{db: db}
}
func (r *ProductProfileRepository) GetByUserID(userID string) (*model.UserProductProfile, error) {
var profile model.UserProductProfile
err := r.db.Where("user_id = ?", userID).First(&profile).Error
return &profile, err
}
func (r *ProductProfileRepository) Save(profile *model.UserProductProfile) error {
// Use Save to either create or update based on primary key
return r.db.Save(profile).Error
}

View File

@@ -0,0 +1,74 @@
package repository
import (
"github.com/google/uuid"
"github.com/zs/InsightReply/internal/model"
"gorm.io/gorm"
)
type ReplyRepository struct {
db *gorm.DB
}
func NewReplyRepository(db *gorm.DB) *ReplyRepository {
return &ReplyRepository{db: db}
}
// CreateGeneratedReply logs an AI generated response when it is copied/used by the user
func (r *ReplyRepository) CreateGeneratedReply(reply *model.GeneratedReply) error {
return r.db.Create(reply).Error
}
// GetPendingPerformanceChecks returns copied replies that need their performance checked (e.g. older than 24h)
func (r *ReplyRepository) GetPendingPerformanceChecks() ([]model.GeneratedReply, error) {
var replies []model.GeneratedReply
// Complex: Fetch replies that are "copied", created more than 24 hours ago,
// and DO NOT already have a corresponding entry in reply_performance.
err := r.db.Table("generated_replies").
Select("generated_replies.*").
Joins("LEFT JOIN reply_performance rp ON rp.reply_id = generated_replies.id").
Where("generated_replies.status = ?", "copied").
Where("generated_replies.created_at < NOW() - INTERVAL '1 day'").
Where("rp.id IS NULL").
Find(&replies).Error
return replies, err
}
// SaveReplyPerformance persists the checked engagement scores of a generated reply
func (r *ReplyRepository) SaveReplyPerformance(perf *model.ReplyPerformance) error {
return r.db.Create(perf).Error
}
// UpsertDummyTweet acts as a safety hook to guarantee foreign key integrity exists before recording a reply onto an un-crawled Tweet.
func (r *ReplyRepository) UpsertDummyTweet(tweet *model.Tweet) error {
return r.db.Where("x_tweet_id = ?", tweet.XTweetID).FirstOrCreate(tweet).Error
}
// GetTweetXTweetID returns the string identifier string X uses, converting backwards from the postgres UUID
func (r *ReplyRepository) GetTweetXTweetID(tweetID uuid.UUID) (string, error) {
var tweet model.Tweet
err := r.db.Where("id = ?", tweetID).First(&tweet).Error
return tweet.XTweetID, err
}
// SaveStyleExtraction commits an AI-learned writing style profile against the user for future inference injection
func (r *ReplyRepository) SaveStyleExtraction(userID uuid.UUID, styleDesc string) error {
// user_style_profiles might not exist yet; use raw SQL or Gorm Upsert
return r.db.Exec(`
INSERT INTO user_style_profiles (user_id, tone_preference)
VALUES (?, ?)
ON CONFLICT (user_id)
DO UPDATE SET tone_preference = EXCLUDED.tone_preference, updated_at = NOW()
`, userID, styleDesc).Error
}
// GetGeneratedRepliesByUser retrieves all AI replies for a user to display in the History dashboard
func (r *ReplyRepository) GetGeneratedRepliesByUser(userID uuid.UUID) ([]model.GeneratedReply, error) {
var replies []model.GeneratedReply
// Preload the performance data if it exists. Preloading "Performance" requires GORM association.
// We'll just fetch replies and order by newest first.
err := r.db.Where("user_id = ?", userID).Order("created_at desc").Limit(100).Find(&replies).Error
return replies, err
}

View File

@@ -0,0 +1,82 @@
package repository
import (
"github.com/zs/InsightReply/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type TweetRepository struct {
db *gorm.DB
}
func NewTweetRepository(db *gorm.DB) *TweetRepository {
return &TweetRepository{db: db}
}
// Upsert intelligently inserts a new tweet or updates an existing one.
// Crucially, on conflict, it dynamically calculates the 'heat_score' by
// comparing the new metrics against the old metrics currently in the database.
func (r *TweetRepository) Upsert(tweet *model.Tweet) error {
// For new tweets being inserted, their base heat score evaluates to their current absolute metrics.
// For existing tweets, we calculate the delta and add it to their existing heat score.
tweet.HeatScore = float64(tweet.LikeCount*1 + tweet.RetweetCount*2 + tweet.ReplyCount*3)
err := r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "x_tweet_id"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"author_id": clause.Column{Table: "excluded", Name: "author_id"},
"author_handle": clause.Column{Table: "excluded", Name: "author_handle"},
"content": clause.Column{Table: "excluded", Name: "content"},
"posted_at": clause.Column{Table: "excluded", Name: "posted_at"},
"last_crawled_at": clause.Column{Table: "excluded", Name: "last_crawled_at"},
"like_count": clause.Column{Table: "excluded", Name: "like_count"},
"retweet_count": clause.Column{Table: "excluded", Name: "retweet_count"},
"reply_count": clause.Column{Table: "excluded", Name: "reply_count"},
// Calculate delta only if the old values exist and are lower than the new values (to prevent negative spikes from X UI glitches).
// heatTracker = old.heat_score + MAX(0, new.like - old.like)*1 + MAX(0, new.rt - old.rt)*2 + MAX(0, new.reply - old.reply)*3
"heat_score": gorm.Expr("tweets.heat_score + GREATEST(0, EXCLUDED.like_count - tweets.like_count) * 1.0 + GREATEST(0, EXCLUDED.retweet_count - tweets.retweet_count) * 2.0 + GREATEST(0, EXCLUDED.reply_count - tweets.reply_count) * 3.0"),
// Smart Crawling logic: If heat score breaches threshold (e.g. 50), promote to high. If old & cold, demote.
"crawl_queue": gorm.Expr(`
CASE
WHEN tweets.heat_score + GREATEST(0, EXCLUDED.like_count - tweets.like_count) * 1.0 + GREATEST(0, EXCLUDED.retweet_count - tweets.retweet_count) * 2.0 + GREATEST(0, EXCLUDED.reply_count - tweets.reply_count) * 3.0 > 50 THEN 'high'
WHEN EXCLUDED.last_crawled_at - tweets.posted_at > INTERVAL '7 days' THEN 'low'
ELSE 'normal'
END
`),
}),
}).Create(tweet).Error
return err
}
// GetTopHeatingTweets returns unprocessed tweets ordered by their generated heat score
func (r *TweetRepository) GetTopHeatingTweets(limit int) ([]model.Tweet, error) {
var tweets []model.Tweet
err := r.db.Where("is_processed = ?", false).Order("heat_score desc").Limit(limit).Find(&tweets).Error
return tweets, err
}
// MarkAsProcessed tags a tweet so we don't present it to the user repeatedly
func (r *TweetRepository) MarkAsProcessed(id string) error {
return r.db.Model(&model.Tweet{}).Where("id = ?", id).Update("is_processed", true).Error
}
// SearchTweets allows dynamic multi-rule filtering
func (r *TweetRepository) SearchTweets(keyword, handle string, limit int) ([]model.Tweet, error) {
var tweets []model.Tweet
query := r.db.Model(&model.Tweet{})
if keyword != "" {
// PostgreSQL ILIKE for case-insensitive keyword searching
query = query.Where("content ILIKE ?", "%"+keyword+"%")
}
if handle != "" {
query = query.Where("author_handle = ?", handle)
}
err := query.Order("heat_score desc, posted_at desc").Limit(limit).Find(&tweets).Error
return tweets, err
}

View File

@@ -22,3 +22,13 @@ func (r *UserRepository) GetByEmail(email string) (*model.User, error) {
err := r.db.Where("email = ?", email).First(&user).Error
return &user, err
}
func (r *UserRepository) GetByID(id string) (*model.User, error) {
var user model.User
err := r.db.Where("id = ?", id).First(&user).Error
return &user, err
}
func (r *UserRepository) Update(user *model.User) error {
return r.db.Save(user).Error
}