feat: 部署初版测试
This commit is contained in:
@@ -1,22 +1,35 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/zs/InsightReply/internal/service"
|
||||
)
|
||||
|
||||
type AIHandler struct {
|
||||
svc *service.AIService
|
||||
svc *service.AIService
|
||||
profileSvc *service.ProductProfileService
|
||||
strategySvc *service.CustomStrategyService
|
||||
}
|
||||
|
||||
func NewAIHandler(svc *service.AIService) *AIHandler {
|
||||
return &AIHandler{svc: svc}
|
||||
func NewAIHandler(svc *service.AIService, profileSvc *service.ProductProfileService, strategySvc *service.CustomStrategyService) *AIHandler {
|
||||
return &AIHandler{
|
||||
svc: svc,
|
||||
profileSvc: profileSvc,
|
||||
strategySvc: strategySvc,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AIHandler) Test(w http.ResponseWriter, r *http.Request) {
|
||||
// ...
|
||||
ctx := r.Context()
|
||||
msg, err := h.svc.TestConnection(ctx)
|
||||
if err != nil {
|
||||
SendError(w, http.StatusInternalServerError, 5000, err.Error())
|
||||
return
|
||||
}
|
||||
SendSuccess(w, map[string]string{"status": msg})
|
||||
}
|
||||
|
||||
func (h *AIHandler) Generate(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -24,6 +37,8 @@ func (h *AIHandler) Generate(w http.ResponseWriter, r *http.Request) {
|
||||
TweetContent string `json:"tweet_content"`
|
||||
Strategy string `json:"strategy"`
|
||||
Identity string `json:"identity"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
@@ -37,13 +52,56 @@ func (h *AIHandler) Generate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
reply, err := h.svc.GenerateReply(ctx, body.TweetContent, body.Strategy, body.Identity)
|
||||
userID := ctx.Value("userID").(string)
|
||||
|
||||
// Fetch Product Profile Context
|
||||
var productContext string
|
||||
if profile, err := h.profileSvc.GetProfile(userID); err == nil && profile.IsActive {
|
||||
productContext = "Product Context: " + profile.ProductName
|
||||
if profile.Tagline != "" {
|
||||
productContext += " - " + profile.Tagline
|
||||
}
|
||||
if profile.KeyFeatures != "" && profile.KeyFeatures != "[]" {
|
||||
productContext += ". Key Features: " + profile.KeyFeatures
|
||||
}
|
||||
if profile.CustomContext != "" {
|
||||
productContext += ". Context: " + profile.CustomContext
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Custom Strategies Context
|
||||
if strategies, err := h.strategySvc.ListStrategies(userID); err == nil && len(strategies) > 0 {
|
||||
productContext += "\n\nAvailable User Custom Strategies:\n"
|
||||
for _, s := range strategies {
|
||||
productContext += "- " + s.StrategyKey + " (" + s.Label + "): " + s.Description + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
replyString, err := h.svc.GenerateReply(ctx, body.TweetContent, productContext, body.Identity, body.Provider, body.Model)
|
||||
if err != nil {
|
||||
SendError(w, http.StatusBadGateway, 5002, "Failed to generate AI reply: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SendSuccess(w, map[string]string{
|
||||
"reply": reply,
|
||||
// Clean up potential markdown wrappers from LLM output
|
||||
cleanReply := strings.TrimSpace(replyString)
|
||||
cleanReply = strings.TrimPrefix(cleanReply, "```json")
|
||||
cleanReply = strings.TrimPrefix(cleanReply, "```")
|
||||
cleanReply = strings.TrimSuffix(cleanReply, "```")
|
||||
cleanReply = strings.TrimSpace(cleanReply)
|
||||
|
||||
var replies []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(cleanReply), &replies); err != nil {
|
||||
// Fallback: return as single string object if parsing totally fails
|
||||
replies = []map[string]interface{}{
|
||||
{
|
||||
"strategy": "Fallback",
|
||||
"content": replyString,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
SendSuccess(w, map[string]interface{}{
|
||||
"replies": replies,
|
||||
})
|
||||
}
|
||||
|
||||
67
server/internal/handler/competitor_monitor_handler.go
Normal file
67
server/internal/handler/competitor_monitor_handler.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/zs/InsightReply/internal/model"
|
||||
"github.com/zs/InsightReply/internal/service"
|
||||
)
|
||||
|
||||
type CompetitorMonitorHandler struct {
|
||||
svc *service.CompetitorMonitorService
|
||||
}
|
||||
|
||||
func NewCompetitorMonitorHandler(svc *service.CompetitorMonitorService) *CompetitorMonitorHandler {
|
||||
return &CompetitorMonitorHandler{svc: svc}
|
||||
}
|
||||
|
||||
func (h *CompetitorMonitorHandler) ListMonitors(w http.ResponseWriter, r *http.Request) {
|
||||
userID := r.Context().Value("userID").(string)
|
||||
|
||||
monitors, err := h.svc.ListMonitors(userID)
|
||||
if err != nil {
|
||||
SendError(w, http.StatusInternalServerError, 5001, "Failed to list monitors")
|
||||
return
|
||||
}
|
||||
|
||||
SendSuccess(w, monitors)
|
||||
}
|
||||
|
||||
func (h *CompetitorMonitorHandler) CreateMonitor(w http.ResponseWriter, r *http.Request) {
|
||||
userIDStr := r.Context().Value("userID").(string)
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
SendError(w, http.StatusUnauthorized, 4010, "Invalid user ID")
|
||||
return
|
||||
}
|
||||
|
||||
var monitor model.CompetitorMonitor
|
||||
if err := json.NewDecoder(r.Body).Decode(&monitor); err != nil {
|
||||
SendError(w, http.StatusBadRequest, 4001, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
monitor.UserID = userID
|
||||
|
||||
if err := h.svc.CreateMonitor(&monitor); err != nil {
|
||||
SendError(w, http.StatusInternalServerError, 5001, "Failed to create monitor")
|
||||
return
|
||||
}
|
||||
|
||||
SendSuccess(w, monitor)
|
||||
}
|
||||
|
||||
func (h *CompetitorMonitorHandler) DeleteMonitor(w http.ResponseWriter, r *http.Request) {
|
||||
userID := r.Context().Value("userID").(string)
|
||||
monitorID := chi.URLParam(r, "id")
|
||||
|
||||
if err := h.svc.DeleteMonitor(monitorID, userID); err != nil {
|
||||
SendError(w, http.StatusInternalServerError, 5001, "Failed to delete monitor")
|
||||
return
|
||||
}
|
||||
|
||||
SendSuccess(w, map[string]string{"status": "deleted"})
|
||||
}
|
||||
67
server/internal/handler/custom_strategy_handler.go
Normal file
67
server/internal/handler/custom_strategy_handler.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/zs/InsightReply/internal/model"
|
||||
"github.com/zs/InsightReply/internal/service"
|
||||
)
|
||||
|
||||
type CustomStrategyHandler struct {
|
||||
svc *service.CustomStrategyService
|
||||
}
|
||||
|
||||
func NewCustomStrategyHandler(svc *service.CustomStrategyService) *CustomStrategyHandler {
|
||||
return &CustomStrategyHandler{svc: svc}
|
||||
}
|
||||
|
||||
func (h *CustomStrategyHandler) ListStrategies(w http.ResponseWriter, r *http.Request) {
|
||||
userID := r.Context().Value("userID").(string)
|
||||
|
||||
strategies, err := h.svc.ListStrategies(userID)
|
||||
if err != nil {
|
||||
SendError(w, http.StatusInternalServerError, 5001, "Failed to list strategies")
|
||||
return
|
||||
}
|
||||
|
||||
SendSuccess(w, strategies)
|
||||
}
|
||||
|
||||
func (h *CustomStrategyHandler) CreateStrategy(w http.ResponseWriter, r *http.Request) {
|
||||
userIDStr := r.Context().Value("userID").(string)
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
SendError(w, http.StatusUnauthorized, 4010, "Invalid user ID")
|
||||
return
|
||||
}
|
||||
|
||||
var strategy model.UserCustomStrategy
|
||||
if err := json.NewDecoder(r.Body).Decode(&strategy); err != nil {
|
||||
SendError(w, http.StatusBadRequest, 4001, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
strategy.UserID = userID
|
||||
|
||||
if err := h.svc.CreateStrategy(&strategy); err != nil {
|
||||
SendError(w, http.StatusInternalServerError, 5001, "Failed to create strategy")
|
||||
return
|
||||
}
|
||||
|
||||
SendSuccess(w, strategy)
|
||||
}
|
||||
|
||||
func (h *CustomStrategyHandler) DeleteStrategy(w http.ResponseWriter, r *http.Request) {
|
||||
userID := r.Context().Value("userID").(string)
|
||||
strategyID := chi.URLParam(r, "id")
|
||||
|
||||
if err := h.svc.DeleteStrategy(strategyID, userID); err != nil {
|
||||
SendError(w, http.StatusInternalServerError, 5001, "Failed to delete strategy")
|
||||
return
|
||||
}
|
||||
|
||||
SendSuccess(w, map[string]string{"status": "deleted"})
|
||||
}
|
||||
54
server/internal/handler/product_profile_handler.go
Normal file
54
server/internal/handler/product_profile_handler.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/zs/InsightReply/internal/model"
|
||||
"github.com/zs/InsightReply/internal/service"
|
||||
)
|
||||
|
||||
type ProductProfileHandler struct {
|
||||
svc *service.ProductProfileService
|
||||
}
|
||||
|
||||
func NewProductProfileHandler(svc *service.ProductProfileService) *ProductProfileHandler {
|
||||
return &ProductProfileHandler{svc: svc}
|
||||
}
|
||||
|
||||
func (h *ProductProfileHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
userID := r.Context().Value("userID").(string)
|
||||
|
||||
profile, err := h.svc.GetProfile(userID)
|
||||
if err != nil {
|
||||
SendError(w, http.StatusNotFound, 4004, "Product profile not found")
|
||||
return
|
||||
}
|
||||
|
||||
SendSuccess(w, profile)
|
||||
}
|
||||
|
||||
func (h *ProductProfileHandler) SaveProfile(w http.ResponseWriter, r *http.Request) {
|
||||
userIDStr := r.Context().Value("userID").(string)
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
SendError(w, http.StatusUnauthorized, 4010, "Invalid user ID")
|
||||
return
|
||||
}
|
||||
|
||||
var profile model.UserProductProfile
|
||||
if err := json.NewDecoder(r.Body).Decode(&profile); err != nil {
|
||||
SendError(w, http.StatusBadRequest, 4001, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
profile.UserID = userID // Ensure user cannot overwrite other's profile
|
||||
|
||||
if err := h.svc.SaveProfile(&profile); err != nil {
|
||||
SendError(w, http.StatusInternalServerError, 5001, "Failed to save product profile")
|
||||
return
|
||||
}
|
||||
|
||||
SendSuccess(w, profile)
|
||||
}
|
||||
97
server/internal/handler/reply_handler.go
Normal file
97
server/internal/handler/reply_handler.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/zs/InsightReply/internal/model"
|
||||
"github.com/zs/InsightReply/internal/repository"
|
||||
)
|
||||
|
||||
type ReplyHandler struct {
|
||||
repo *repository.ReplyRepository
|
||||
}
|
||||
|
||||
func NewReplyHandler(repo *repository.ReplyRepository) *ReplyHandler {
|
||||
return &ReplyHandler{repo: repo}
|
||||
}
|
||||
|
||||
func (h *ReplyHandler) RecordReply(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
TweetID string `json:"tweet_id"`
|
||||
StrategyType string `json:"strategy_type"`
|
||||
Content string `json:"content"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
SendError(w, http.StatusBadRequest, 4001, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
userID, ok := ctx.Value("userID").(string)
|
||||
if !ok || userID == "" {
|
||||
SendError(w, http.StatusUnauthorized, 4002, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
SendError(w, http.StatusBadRequest, 4003, "Invalid user ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve the raw string X_Tweet_ID into our internal UUID
|
||||
// Create a dummy tweet entry via Upsert if it doesn't exist yet so foreign keys don't panic
|
||||
tweet := &model.Tweet{
|
||||
XTweetID: body.TweetID,
|
||||
Content: body.Content, // Temporarily store AI content as a placeholder if original is missing
|
||||
IsProcessed: false,
|
||||
}
|
||||
|
||||
err = h.repo.UpsertDummyTweet(tweet)
|
||||
if err != nil {
|
||||
SendError(w, http.StatusInternalServerError, 5001, "Failed to resolve tweet reference")
|
||||
return
|
||||
}
|
||||
|
||||
reply := &model.GeneratedReply{
|
||||
UserID: userUUID,
|
||||
TweetID: tweet.ID,
|
||||
StrategyType: body.StrategyType,
|
||||
Content: body.Content,
|
||||
Status: "copied",
|
||||
Language: body.Language,
|
||||
}
|
||||
|
||||
if err := h.repo.CreateGeneratedReply(reply); err != nil {
|
||||
SendError(w, http.StatusInternalServerError, 5002, "Failed to log generated reply")
|
||||
return
|
||||
}
|
||||
|
||||
SendSuccess(w, map[string]string{ "message": "Reply recorded successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetReplies handles GET /api/v1/replies
|
||||
func (h *ReplyHandler) GetReplies(w http.ResponseWriter, r *http.Request) {
|
||||
userIDStr := r.Header.Get("X-User-ID")
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
replies, err := h.repo.GetGeneratedRepliesByUser(userID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get replies: %v", err)
|
||||
http.Error(w, "Failed to get replies", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(replies)
|
||||
}
|
||||
48
server/internal/handler/tweet_handler.go
Normal file
48
server/internal/handler/tweet_handler.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/zs/InsightReply/internal/repository"
|
||||
)
|
||||
|
||||
type TweetHandler struct {
|
||||
repo *repository.TweetRepository
|
||||
}
|
||||
|
||||
func NewTweetHandler(repo *repository.TweetRepository) *TweetHandler {
|
||||
return &TweetHandler{repo: repo}
|
||||
}
|
||||
|
||||
// GetHotTweets returns the top heating tweets spanning across all tracking targets
|
||||
func (h *TweetHandler) GetHotTweets(w http.ResponseWriter, r *http.Request) {
|
||||
// Standardize to take the top 50 hottest tweets that haven't been manually marked as processed
|
||||
tweets, err := h.repo.GetTopHeatingTweets(50)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to retrieve hot tweets"})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(tweets)
|
||||
}
|
||||
|
||||
// SearchTweets provides the multi-rule filtering API for Epic 5
|
||||
func (h *TweetHandler) GetSearchTweets(w http.ResponseWriter, r *http.Request) {
|
||||
keyword := r.URL.Query().Get("keyword")
|
||||
handle := r.URL.Query().Get("handle")
|
||||
|
||||
tweets, err := h.repo.SearchTweets(keyword, handle, 50)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to search tweets"})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(tweets)
|
||||
}
|
||||
@@ -34,3 +34,38 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
SendSuccess(w, user)
|
||||
}
|
||||
|
||||
func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
// Assumes JWTAuth middleware has placed userID in context
|
||||
userID := r.Context().Value("userID").(string)
|
||||
|
||||
user, err := h.svc.GetUserByID(userID)
|
||||
if err != nil {
|
||||
SendError(w, http.StatusNotFound, 4004, "User not found")
|
||||
return
|
||||
}
|
||||
|
||||
SendSuccess(w, user)
|
||||
}
|
||||
|
||||
func (h *UserHandler) UpdatePreferences(w http.ResponseWriter, r *http.Request) {
|
||||
userID := r.Context().Value("userID").(string)
|
||||
|
||||
var body struct {
|
||||
IdentityLabel string `json:"identity_label"`
|
||||
LanguagePreference string `json:"language_preference"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
SendError(w, http.StatusBadRequest, 4001, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.svc.UpdatePreferences(userID, body.IdentityLabel, body.LanguagePreference)
|
||||
if err != nil {
|
||||
SendError(w, http.StatusInternalServerError, 5001, "Failed to update preferences")
|
||||
return
|
||||
}
|
||||
|
||||
SendSuccess(w, user)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user