FloorVisualizer/internal/handler/user_handler.go
dindang 94895cbc22 Initial commit: FloorVisualizer backend
- Go API server with PostgreSQL + Redis
- AI floor replacement (OpenRouter Gemini)
- Product database (10 brands, 3539 products)
- Recommendation engine, calculator, articles
- Redis async queue + worker pool
- Hot product caching, brand view tracking
- JWT auth, favorites, projects
- Docker deployment ready

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-16 09:47:11 +08:00

293 lines
8.6 KiB
Go

package handler
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"floorvisualizer/internal/middleware"
"floorvisualizer/internal/model"
"floorvisualizer/internal/repository"
)
func handleMe(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID := middleware.UserID(r.Context())
switch r.Method {
case http.MethodGet:
user, err := repository.GetUserByID(db, userID)
if err != nil {
writeJSON(w, 404, map[string]string{"error": "user not found"})
return
}
writeJSON(w, 200, user)
case http.MethodPut:
var input struct {
Name string `json:"name"`
AvatarURL string `json:"avatar_url"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeJSON(w, 400, map[string]string{"error": "invalid request body"})
return
}
user, err := repository.UpdateUser(db, userID, input.Name, input.AvatarURL)
if err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, user)
default:
writeErr(w, 405, "Method not allowed")
}
}
}
func handleFavorites(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID := middleware.UserID(r.Context())
switch r.Method {
case http.MethodGet:
favs, err := repository.ListFavorites(db, userID)
if err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
if favs == nil {
favs = []model.Favorite{}
}
writeJSON(w, 200, map[string]interface{}{"favorites": favs, "total": len(favs)})
case http.MethodPost:
sku := r.URL.Query().Get("sku")
if sku == "" || strings.Contains(sku, "/") {
writeJSON(w, 400, map[string]string{"error": "sku is required"})
return
}
product, err := repository.GetProductBySKU(db, sku)
if err != nil {
writeJSON(w, 404, map[string]string{"error": "product not found"})
return
}
fav := model.Favorite{
SKU: product.SKU, Brand: product.Brand, SeriesName: product.SeriesName,
StyleName: product.StyleName, MainImageURL: product.MainImageURL,
Category: product.Category, Material: product.Material,
ColorTone: product.ColorTone, PricePerSqft: product.PricePerSqft,
}
if err := repository.AddFavorite(db, userID, fav); err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 201, fav)
case http.MethodDelete:
sku := strings.TrimSpace(strings.TrimPrefix(r.URL.Path, "/user/favorites/"))
if sku == "" || strings.Contains(sku, "/") {
writeJSON(w, 400, map[string]string{"error": "missing sku"})
return
}
if err := repository.RemoveFavorite(db, userID, sku); err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, map[string]string{"ok": "removed"})
default:
writeErr(w, 405, "Method not allowed")
}
}
}
func handleProjects(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID := middleware.UserID(r.Context())
projectID := strings.TrimPrefix(r.URL.Path, "/user/projects/")
if projectID != "" && projectID != r.URL.Path {
handleSingleProject(w, r, db, userID, projectID)
return
}
switch r.Method {
case http.MethodGet:
projects, err := repository.ListProjects(db, userID)
if err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
if projects == nil {
projects = []model.Project{}
}
writeJSON(w, 200, map[string]interface{}{"projects": projects, "total": len(projects)})
case http.MethodPost:
var input struct {
Name string `json:"name"`
OriginalImageURL string `json:"original_image_url"`
GeneratedImageURL string `json:"generated_image_url"`
FloorStyle string `json:"floor_style"`
Pattern string `json:"pattern"`
RoomType string `json:"room_type"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeJSON(w, 400, map[string]string{"error": "invalid request body"})
return
}
if input.Name == "" {
input.Name = "Untitled"
}
project, err := repository.CreateProject(db, userID, input.Name,
input.OriginalImageURL, input.GeneratedImageURL, input.FloorStyle, input.Pattern, input.RoomType)
if err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 201, project)
default:
writeErr(w, 405, "Method not allowed")
}
}
}
func handleSingleProject(w http.ResponseWriter, r *http.Request, db *sql.DB, userID, projectID string) {
switch r.Method {
case http.MethodGet:
project, err := repository.GetProject(db, projectID, userID)
if err != nil {
writeJSON(w, 404, map[string]string{"error": "project not found"})
return
}
writeJSON(w, 200, project)
case http.MethodPut:
var input struct{ Name string `json:"name"` }
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeJSON(w, 400, map[string]string{"error": "invalid request body"})
return
}
project, err := repository.UpdateProject(db, projectID, userID, input.Name)
if err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, project)
case http.MethodDelete:
if err := repository.DeleteProject(db, projectID, userID); err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, map[string]string{"ok": "deleted"})
default:
writeErr(w, 405, "Method not allowed")
}
}
func handleUploadAvatar(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID := middleware.UserID(r.Context())
if err := r.ParseMultipartForm(5 << 20); err != nil {
writeJSON(w, 400, map[string]string{"error": "image too large (max 5MB)"})
return
}
file, _, err := r.FormFile("image")
if err != nil {
writeJSON(w, 400, map[string]string{"error": "missing image file"})
return
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil || len(data) == 0 {
writeJSON(w, 400, map[string]string{"error": "failed to read image"})
return
}
ct := http.DetectContentType(data)
if !strings.HasPrefix(ct, "image/") {
writeJSON(w, 400, map[string]string{"error": "file is not an image"})
return
}
hash := sha256.Sum256(data)
name := hex.EncodeToString(hash[:])[:16] + "_" + fmt.Sprintf("%d", time.Now().UnixNano())
dir := "uploads/avatars"
os.MkdirAll(dir, 0755)
filePath := filepath.Join(dir, name+".jpg")
if err := os.WriteFile(filePath, data, 0644); err != nil {
writeJSON(w, 500, map[string]string{"error": "failed to save image"})
return
}
current, err := repository.GetUserByID(db, userID)
if err != nil || current == nil {
writeJSON(w, 500, map[string]string{"error": "user not found"})
return
}
avatarURL := "/uploads/avatars/" + name + ".jpg"
user, err := repository.UpdateUser(db, userID, current.Name, avatarURL)
if err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, user)
}
}
// ── Brand view tracking ───────────────────────────────────
func handleBrandView(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeErr(w, 405, "Method not allowed")
return
}
var body struct {
Brand string `json:"brand"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, 400, map[string]string{"error": "invalid request body"})
return
}
if body.Brand == "" {
writeJSON(w, 400, map[string]string{"error": "brand is required"})
return
}
userID := middleware.UserID(r.Context())
if userID == "" {
writeJSON(w, 401, map[string]string{"error": "login required"})
return
}
if err := repository.RecordBrandView(db, userID, body.Brand); err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, map[string]string{"ok": "recorded"})
}
}
func handleFrequentBrands(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID := middleware.UserID(r.Context())
if userID == "" {
writeJSON(w, 401, map[string]string{"error": "login required"})
return
}
brands, err := repository.GetFrequentBrands(db, userID)
if err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
if brands == nil {
brands = []repository.FrequentBrand{}
}
writeJSON(w, 200, map[string]interface{}{
"brands": brands,
"total": len(brands),
})
}
}